From 105baef99ad46fd677df7d64a09c69421492b429 Mon Sep 17 00:00:00 2001 From: Joe Stanley Date: Thu, 15 Jun 2023 20:26:27 -0700 Subject: [PATCH 01/10] enhance upload method to support new Lychee API --- pychee/pychee.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/pychee/pychee.py b/pychee/pychee.py index 246fc16..1b928e2 100644 --- a/pychee/pychee.py +++ b/pychee/pychee.py @@ -6,12 +6,13 @@ For additional information, visit: https://github.com/LycheeOrg/Lychee. """ from posixpath import join -from typing import List +from typing import List, Dict, Optional from urllib.parse import unquote +from datetime import datetime from requests import Session -__version__ = '0.2.2' +__version__ = '0.2.3' class LycheeForbidden(Exception): """Raised when the Lychee request is unauthorized.""" @@ -308,7 +309,13 @@ def set_photos_tags(self, photo_ids: List[str], tags: List[str]): data = {'photoIDs': ','.join(photo_ids), 'tags': ','.join(tags)} self._session.post('Photo::setTags', json=data) - def add_photo(self, photo: bytes, photo_name: str, album_id: str) -> str: + def add_photo( + self, + photo: bytes, + photo_name: str, + album_id: str, + file_last_modified_time: Optional[int] = None + ) -> Dict: """ Upload a photo into an album. @@ -316,7 +323,12 @@ def add_photo(self, photo: bytes, photo_name: str, album_id: str) -> str: Return the ID of the uploaded image. """ - data = {'albumID': album_id} + if file_last_modified_time is None: + file_last_modified_time = datetime.now().microsecond + data = { + 'albumID': album_id, + 'fileLastModifiedTime': file_last_modified_time + } # Lychee expects a multipart/form-data with a field called name and being `file`, # which contradicts with API doc for now # See syntax there : https://stackoverflow.com/a/12385661 From 587fa6559bf28f5411acfa7e3bb8b737f48d55db Mon Sep 17 00:00:00 2001 From: Joe Stanley Date: Thu, 15 Jun 2023 22:26:22 -0700 Subject: [PATCH 02/10] fix get public album info --- pychee/pychee.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pychee/pychee.py b/pychee/pychee.py index 1b928e2..f33905c 100644 --- a/pychee/pychee.py +++ b/pychee/pychee.py @@ -159,7 +159,7 @@ def get_public_album(self, album_id: str, password: str = 'rand'): password. """ data = {'albumID': album_id, 'password': password} - self._session.post('Album::getPublic', json=data) + return self._session.post('Album::getPublic', json=data).json() def add_album(self, title: str, parent_id: str = None) -> str: """ From 5f281d370e5350a9c74a9c1e7736260f861e6500 Mon Sep 17 00:00:00 2001 From: Joe Stanley Date: Thu, 15 Jun 2023 22:46:46 -0700 Subject: [PATCH 03/10] update set_album_public to match present interface --- pychee/pychee.py | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/pychee/pychee.py b/pychee/pychee.py index f33905c..298fe8f 100644 --- a/pychee/pychee.py +++ b/pychee/pychee.py @@ -185,13 +185,12 @@ def set_album_description(self, album_id: str, description: str): def set_album_public( self, album_id: str, - public: int, - visible: int, - nsfw: int, - downloadable: int, - share_button_visible: int, - full_photo: int, - password: str = "" + public: bool, + link_required: bool, + nsfw: bool, + downloadable: bool, + full_photo_access: int, + password: Optional[str] = None ): """ Change the sharing properties of the album. @@ -200,15 +199,14 @@ def set_album_public( """ data = { 'albumID': album_id, - 'public': public, - 'visible': visible, - 'nsfw': nsfw, - 'downloadable': downloadable, - 'share_button_visible': share_button_visible, - 'full_photo': full_photo, + 'grants_download': downloadable, + 'grants_full_photo_access': full_photo_access, + 'is_link_required': link_required, + 'is_nsfw': nsfw, + 'is_public': public, 'password': password } - self._session.post('Album::setPublic', json=data) + self._session.post('Album::setProtectionPolicy', json=data) def delete_album(self, album_id: List[str]): """Delete the albums and all pictures in the album.""" From 513cc08f5fb95c97740a4e5622709f7f1158a73e Mon Sep 17 00:00:00 2001 From: Joe Stanley Date: Sat, 30 Dec 2023 10:43:20 -0800 Subject: [PATCH 04/10] add not authenticated error --- pychee/pychee.py | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/pychee/pychee.py b/pychee/pychee.py index 298fe8f..363204f 100644 --- a/pychee/pychee.py +++ b/pychee/pychee.py @@ -12,17 +12,20 @@ from requests import Session -__version__ = '0.2.3' +__version__ = '0.2.4' -class LycheeForbidden(Exception): +class LycheeError(Exception): + """Raised for general Lychee errors.""" + +class LycheeNotAuthenticated(LycheeError): + """Raised when a call results in an unauthenticated error.""" + +class LycheeForbidden(LycheeError): """Raised when the Lychee request is unauthorized.""" -class LycheeNotFound(Exception): +class LycheeNotFound(LycheeError): """Raised when the requested resource was not found.""" -class LycheeError(Exception): - """Raised for general Lychee errors.""" - #FIXME add error code handling #FIXME adjust to API sending JSON because we changed Accept #FIXME fix type hints... @@ -39,6 +42,10 @@ class LycheeAPISession(Session): '"Error: validation failed"' ] + NOT_AUTHENTICATED_MESSAGES = [ + 'User is not authenticated', + ] + NOT_FOUND_MESSAGES = [ '"Error: no pictures found!"' ] @@ -68,8 +75,10 @@ def request(self, method, url, *args, **kwargs): # Update CSRF header if changed if response.text in self.FORBID_MESSAGES: raise LycheeForbidden(response.text) - if response.text in self.NOT_FOUND_MESSAGES: + elif response.text in self.NOT_FOUND_MESSAGES: raise LycheeNotFound(response.text) + elif response.text in self.NOT_AUTHENTICATED_MESSAGES: + raise LycheeNotAuthenticated(response.text) if response.text == 'false' or response.text is None: raise LycheeError('Could be unauthorized, wrong args, who knows?') return response From b9ec1ff6feae0865c6a23ed38c150930d1a4216c Mon Sep 17 00:00:00 2001 From: Joe Stanley Date: Sat, 30 Dec 2023 10:46:39 -0800 Subject: [PATCH 05/10] add not authenticated error --- pychee/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pychee/__init__.py b/pychee/__init__.py index 4a365fa..c589a50 100644 --- a/pychee/__init__.py +++ b/pychee/__init__.py @@ -5,3 +5,5 @@ For additonal information, visit: [Lychee](https://github.com/LycheeOrg/Lychee). """ + +from pychee.pychee import __version__ From 58d07eb6ad3b8a7080ab035e524166574a1a5a19 Mon Sep 17 00:00:00 2001 From: Joe Stanley Date: Sat, 30 Dec 2023 10:51:24 -0800 Subject: [PATCH 06/10] add not authenticated error --- pychee/__init__.py | 3 ++- pychee/pychee.py | 7 ++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/pychee/__init__.py b/pychee/__init__.py index c589a50..d1947dd 100644 --- a/pychee/__init__.py +++ b/pychee/__init__.py @@ -3,7 +3,8 @@ """ # pychee: Client for Lychee, written in Python. -For additonal information, visit: [Lychee](https://github.com/LycheeOrg/Lychee). +For additional information, visit: +[Lychee](https://github.com/LycheeOrg/Lychee). """ from pychee.pychee import __version__ diff --git a/pychee/pychee.py b/pychee/pychee.py index 363204f..39842dd 100644 --- a/pychee/pychee.py +++ b/pychee/pychee.py @@ -77,10 +77,11 @@ def request(self, method, url, *args, **kwargs): raise LycheeForbidden(response.text) elif response.text in self.NOT_FOUND_MESSAGES: raise LycheeNotFound(response.text) - elif response.text in self.NOT_AUTHENTICATED_MESSAGES: - raise LycheeNotAuthenticated(response.text) - if response.text == 'false' or response.text is None: + elif response.text == 'false' or response.text is None: raise LycheeError('Could be unauthorized, wrong args, who knows?') + elif hasattr(json := response.json(), 'message'): + if json.get('message') in self.NOT_AUTHENTICATED_MESSAGES: + raise LycheeNotAuthenticated(response.text) return response def _set_csrf_header(self) -> None: From 4cc9c6215a70a29b9bfdfd7f9241e7ea34f43034 Mon Sep 17 00:00:00 2001 From: Joe Stanley Date: Sat, 30 Dec 2023 10:58:32 -0800 Subject: [PATCH 07/10] add not authenticated error --- pychee/pychee.py | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/pychee/pychee.py b/pychee/pychee.py index 39842dd..ba5ecd8 100644 --- a/pychee/pychee.py +++ b/pychee/pychee.py @@ -113,13 +113,17 @@ class LycheeClient: def __init__(self, url: str): """Initialize a new Lychee session for given URL.""" self._session = LycheeAPISession(url) - self._session.post('Session::init', json={}) + self._session.request('post', 'Session::init', json={}) def login(self, username: str, password: str) -> None: """Log in to Lychee server.""" auth = {'username': username, 'password': password} # Session takes care of setting cookies - login_response = self._session.post('Session::login', json=auth) + login_response = self._session.request( + 'post', + 'Session::login', + json=auth + ) def logout(self): """Log out from Lychee server.""" @@ -132,7 +136,7 @@ def get_albums(self) -> dict: Returns an array of albums or false on failure. """ - return self._session.post('Albums::get', json={}).json() + return self._session.request('post', 'Albums::get', json={}).json() def get_albums_tree(self): """ @@ -141,7 +145,7 @@ def get_albums_tree(self): Returns a list of albums dictionaries or an informative message on failure. """ - return self._session.post('Albums::tree', json={}).json() + return self._session.request('post', 'Albums::tree', json={}).json() def get_albums_position_data(self) -> dict: """ @@ -180,17 +184,17 @@ def add_album(self, title: str, parent_id: str = None) -> str: Return the ID of the new image. """ data = {'title': title, 'parent_id': parent_id} - return self._session.post('Album::add', json=data).json() + return self._session.request('post', 'Album::add', json=data).json() def set_albums_title(self, album_ids: List[str], title: str): """Change the title of the albums.""" data = {'albumIDs': ','.join(album_ids), 'title': title} - self._session.post('Album::setTitle', json=data) + self._session.request('post', 'Album::setTitle', json=data) def set_album_description(self, album_id: str, description: str): """Change the description of the album.""" data = {'albumID': album_id, 'description': description} - self._session.post('Album::setDescription', json=data) + self._session.request('post', 'Album::setDescription', json=data) def set_album_public( self, @@ -216,12 +220,12 @@ def set_album_public( 'is_public': public, 'password': password } - self._session.post('Album::setProtectionPolicy', json=data) + self._session.request('post', 'Album::setProtectionPolicy', json=data) def delete_album(self, album_id: List[str]): """Delete the albums and all pictures in the album.""" data = {'albumIDs': album_id} - self._session.post('Album::delete', json=data) + self._session.request('post', 'Album::delete', json=data) def merge_albums(self, dest_id: str, source_ids: List[str]): """ @@ -231,12 +235,12 @@ def merge_albums(self, dest_id: str, source_ids: List[str]): it will be deleted. Don't do this. """ data = {'albumIDs': dest_id + ',' + ','.join(source_ids)} - self._session.post('Album::merge', json=data) + self._session.request('post', 'Album::merge', json=data) def move_albums(self, dest_id: str, source_ids: List[str]): """Move albums into another one, which becomes their parent.""" data = {'albumIDs': dest_id + ',' + ','.join(source_ids)} - self._session.post('Album::move', json=data) + self._session.request('post', 'Album::move', json=data) def set_album_license(self, album_id: str, license: str): """ @@ -248,7 +252,7 @@ def set_album_license(self, album_id: str, license: str): Returns false if license name is unrecognized. """ data = {'albumID': album_id, 'license': license} - self._session.post('Album::setLicense', json=data) + self._session.request('post', 'Album::setLicense', json=data) def get_albums_archive(self, album_ids: List[str]) -> bytes: """ @@ -260,7 +264,11 @@ def get_albums_archive(self, album_ids: List[str]) -> bytes: data = {'albumIDs': ','.join(album_ids)} # For large archives, maybe we would use # stream=True and iterate over chunks of answer. - return self._session.get('Album::getArchive', params=data).content + return self._session.request( + 'post', + 'Album::getArchive', + params=data + ).content def get_frame_settings(self) -> dict: """ From ae5af7a7fe53b80bd640fe54e710227c41a0d697 Mon Sep 17 00:00:00 2001 From: Joe Stanley Date: Sat, 30 Dec 2023 11:22:48 -0800 Subject: [PATCH 08/10] add not authenticated error --- pychee/pychee.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pychee/pychee.py b/pychee/pychee.py index ba5ecd8..c3e33d1 100644 --- a/pychee/pychee.py +++ b/pychee/pychee.py @@ -72,6 +72,7 @@ def request(self, method, url, *args, **kwargs): url = join(self._prefix_url, self.BASE_API_FRAGMENT, url) response = super().request(method, url, *args, **kwargs) self._set_csrf_header() + json = response.json() # Update CSRF header if changed if response.text in self.FORBID_MESSAGES: raise LycheeForbidden(response.text) @@ -79,9 +80,8 @@ def request(self, method, url, *args, **kwargs): raise LycheeNotFound(response.text) elif response.text == 'false' or response.text is None: raise LycheeError('Could be unauthorized, wrong args, who knows?') - elif hasattr(json := response.json(), 'message'): - if json.get('message') in self.NOT_AUTHENTICATED_MESSAGES: - raise LycheeNotAuthenticated(response.text) + elif json.get('message') in self.NOT_AUTHENTICATED_MESSAGES: + raise LycheeNotAuthenticated(response.text) return response def _set_csrf_header(self) -> None: From c4e731b6229a524e70dcf1310d2abdf40df5d6f8 Mon Sep 17 00:00:00 2001 From: Joe Stanley Date: Sat, 30 Dec 2023 11:35:42 -0800 Subject: [PATCH 09/10] add not authenticated error --- pychee/pychee.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/pychee/pychee.py b/pychee/pychee.py index c3e33d1..286eec1 100644 --- a/pychee/pychee.py +++ b/pychee/pychee.py @@ -11,6 +11,7 @@ from datetime import datetime from requests import Session +from requests.exceptions import JSONDecodeError __version__ = '0.2.4' @@ -72,7 +73,6 @@ def request(self, method, url, *args, **kwargs): url = join(self._prefix_url, self.BASE_API_FRAGMENT, url) response = super().request(method, url, *args, **kwargs) self._set_csrf_header() - json = response.json() # Update CSRF header if changed if response.text in self.FORBID_MESSAGES: raise LycheeForbidden(response.text) @@ -80,8 +80,14 @@ def request(self, method, url, *args, **kwargs): raise LycheeNotFound(response.text) elif response.text == 'false' or response.text is None: raise LycheeError('Could be unauthorized, wrong args, who knows?') - elif json.get('message') in self.NOT_AUTHENTICATED_MESSAGES: - raise LycheeNotAuthenticated(response.text) + else: + try: + json = response.json() + if json.get('message') in self.NOT_AUTHENTICATED_MESSAGES: + raise LycheeNotAuthenticated(response.text) + except JSONDecodeError: + pass # Do Nothing + response.raise_for_status() return response def _set_csrf_header(self) -> None: From 4cd7f9226325b0c28635eca68c91f9f6d21aadd3 Mon Sep 17 00:00:00 2001 From: Joe Stanley Date: Sat, 30 Dec 2023 14:11:03 -0800 Subject: [PATCH 10/10] removed deprecated vscode settings --- .vscode/settings.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index ff136c3..7a73a41 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,2 @@ { - "python.linting.pylintEnabled": true, - "python.linting.pydocstyleEnabled": true, - "python.linting.enabled": true } \ No newline at end of file