From 0a76b6e7f82cc60d044f124f1252cd965e61555c Mon Sep 17 00:00:00 2001 From: simonwt Date: Mon, 13 Apr 2026 21:56:42 +0100 Subject: [PATCH 1/4] Acccept pk sk as well as path when updating post --- development.md | 5 ++ .../wrappers/post/update_post.py | 35 ++++++--- tests/wrappers/update_post.py | 71 +++++++++++++++++-- 3 files changed, 98 insertions(+), 13 deletions(-) diff --git a/development.md b/development.md index d46e7d3..e50b761 100644 --- a/development.md +++ b/development.md @@ -25,6 +25,11 @@ potentially misleading. 1. Make more jobs 2. Make more wrappers 3. Write more documentation +4. Wrappers should also accept actual keys for when you know them +5. Add utility to manage the token easily to encourage reuse over +new tokens every time + + Trust Follow diff --git a/src/trustcafeapiwrapper/wrappers/post/update_post.py b/src/trustcafeapiwrapper/wrappers/post/update_post.py index d302aab..f289124 100644 --- a/src/trustcafeapiwrapper/wrappers/post/update_post.py +++ b/src/trustcafeapiwrapper/wrappers/post/update_post.py @@ -1,6 +1,6 @@ from trustcafeapiwrapper.utils import get_post_pksk, get_parent_pksk_from_path -def update_post(parent_path, post_path, post_text, blur_label=None, card_url=None, collaborative=False): +def update_post( post_text, post_path=None, parent_path='/', post_key=None, blur_label=None, card_url=None, collaborative=False): """ Updates an existing post. @@ -15,21 +15,38 @@ def update_post(parent_path, post_path, post_text, blur_label=None, card_url=Non Returns: dict: The updated post data. """ - parent_pksk = get_parent_pksk_from_path(parent_path) - post_pksk = get_post_pksk(parent_pksk, post_path) + if post_key is not None: + post_pksk = { + 'pk': post_key.get('pk', None), + 'sk': post_key.get('sk', None) + } + elif post_path is not None and parent_path is not None: + parent_pksk = get_parent_pksk_from_path(parent_path) + post_pksk = get_post_pksk(parent_pksk, post_path) + else: + raise ValueError("Either post_path and parent_path or post_key must be provided.") + + + if post_text is None: + raise ValueError("post_text is required.") + payload = { "key": { "pk": post_pksk.get('pk', None), "sk": post_pksk.get('sk', None) }, - "postSlug": post_path.strip('/post/'), - "blurLabel": blur_label, - "cardUrl": card_url, - "postText": post_text, - "collaborative": collaborative, - + "postSlug": post_pksk.get('sk', '').replace('post#', ''), + "postText": post_text } + if collaborative is not None: + payload["collaborative"] = collaborative + + if blur_label is not None: + payload["blurLabel"] = blur_label + + if card_url is not None: + payload["cardUrl"] = card_url return { "job_function": "post.update", diff --git a/tests/wrappers/update_post.py b/tests/wrappers/update_post.py index 03c0d36..f8cd43d 100644 --- a/tests/wrappers/update_post.py +++ b/tests/wrappers/update_post.py @@ -21,10 +21,73 @@ class TestUpdatePost(unittest.TestCase): self.assertIn("payload", result) self.assertEqual(result["job_function"], "post.update") self.assertEqual(result["payload"]["postText"], self.post_text) - self.assertEqual(result["payload"]["blurLabel"], self.blur_label) - self.assertEqual(result["payload"]["cardUrl"], self.card_url) - self.assertEqual(result["payload"]["collaborative"], self.collaborative) self.assertEqual(result["payload"]["postSlug"], "1235-abcv") self.assertEqual(result["payload"]["key"]["pk"], "maintrunk#maintrunk") self.assertEqual(result["payload"]["key"]["sk"], "post#1235-abcv") - self.assertNotIn("slug", result["payload"]["key"]) \ No newline at end of file + self.assertNotIn("slug", result["payload"]["key"]) + + def test_update_post_with_post_key(self): + post_key = { + "pk": "maintrunk#maintrunk", + "sk": "post#1235-abcv" + } + result = update_post( + post_key=post_key, + post_text=self.post_text, + blur_label=self.blur_label, + card_url=self.card_url, + collaborative=self.collaborative, + ) + self.assertIsInstance(result, dict) + self.assertIn("job_function", result) + self.assertIn("payload", result) + self.assertEqual(result["job_function"], "post.update") + self.assertEqual(result["payload"]["postText"], self.post_text) + self.assertEqual(result["payload"]["postSlug"], "1235-abcv") + self.assertEqual(result["payload"]["key"]["pk"], post_key["pk"]) + self.assertEqual(result["payload"]["key"]["sk"], post_key["sk"]) + self.assertNotIn("slug", result["payload"]["key"]) + + def test_update_collaborative_post(self): + collaborative = True + result = update_post( + parent_path='/', + post_path='/post/1235-abcv', + post_text=self.post_text, + blur_label=self.blur_label, + card_url=self.card_url, + collaborative=collaborative + ) + self.assertIsInstance(result, dict) + self.assertIn("job_function", result) + self.assertIn("payload", result) + self.assertEqual(result["job_function"], "post.update") + self.assertEqual(result["payload"]["postText"], self.post_text) + self.assertEqual(result["payload"]["postSlug"], "1235-abcv") + self.assertEqual(result["payload"]["key"]["pk"], "maintrunk#maintrunk") + self.assertEqual(result["payload"]["key"]["sk"], "post#1235-abcv") + self.assertNotIn("slug", result["payload"]["key"]) + self.assertTrue(result["payload"]["collaborative"]) + + def test_update_blur_label_and_card_url(self): + blur_label = "Sensitive Content" + card_url = "https://example.com/card" + result = update_post( + parent_path='/', + post_path='/post/1235-abcv', + post_text=self.post_text, + blur_label=blur_label, + card_url=card_url, + collaborative=self.collaborative + ) + self.assertIsInstance(result, dict) + self.assertIn("job_function", result) + self.assertIn("payload", result) + self.assertEqual(result["job_function"], "post.update") + self.assertEqual(result["payload"]["postText"], self.post_text) + self.assertEqual(result["payload"]["postSlug"], "1235-abcv") + self.assertEqual(result["payload"]["key"]["pk"], "maintrunk#maintrunk") + self.assertEqual(result["payload"]["key"]["sk"], "post#1235-abcv") + self.assertNotIn("slug", result["payload"]["key"]) + self.assertEqual(result["payload"]["blurLabel"], blur_label) + self.assertEqual(result["payload"]["cardUrl"], card_url) \ No newline at end of file From deb19d78ae415ea32c941d2551c7c488fb9b941f Mon Sep 17 00:00:00 2001 From: simonwt Date: Mon, 13 Apr 2026 22:26:42 +0100 Subject: [PATCH 2/4] Handle token method for saving token token to a file --- .gitignore | 2 ++ src/trustcafeapiwrapper/apiclient.py | 25 +++++++++++++++++++++++-- testing.py | 17 ++++------------- 3 files changed, 29 insertions(+), 15 deletions(-) diff --git a/.gitignore b/.gitignore index d5bbe91..b66b49d 100644 --- a/.gitignore +++ b/.gitignore @@ -26,4 +26,6 @@ dist/* # Test Artifacts token_data.json +token_data_alpha.json +token_dataproduction.json test_output.json \ No newline at end of file diff --git a/src/trustcafeapiwrapper/apiclient.py b/src/trustcafeapiwrapper/apiclient.py index ab25a19..658c862 100644 --- a/src/trustcafeapiwrapper/apiclient.py +++ b/src/trustcafeapiwrapper/apiclient.py @@ -1,7 +1,6 @@ -import requests +import os,requests import simplejson as json from urllib.parse import urlencode - from pydantic import ( PrivateAttr, BaseModel, SecretStr, HttpUrl ) @@ -153,6 +152,28 @@ class APIClient(BaseModel): current_time = int(time.time()) return self._access_token and current_time < self._access_token_timeout + def handle_token(self, token_data_path=None): + """ + Handle access token retrieval and caching. + """ + if token_data_path is None: + token_data_path = f"token_data_{self.environment}.json" + + if os.path.exists(token_data_path): + with open(token_data_path, "r") as f: + token_data = json.load(f) + self.set_token(token_data) + else: + if self.debug: + print(f"No token data file found at '{token_data_path}'. A new token will be obtained.") + raise FileNotFoundError(f"Token data file '{token_data_path}' not found. Please ensure the file exists or handle token retrieval appropriately.") + + # Get a new one if we don't have one or if the existing one is expired + if not self.is_token_valid(): + tokendata = self.sign_in() + with open(token_data_path, "w") as f: + json.dump(tokendata, f, indent=2) + def run_job(self, job_function, *args, **kwargs): """ Utility method to run a job function with the API client as the first argument. diff --git a/testing.py b/testing.py index ef2be5f..72c8cc2 100644 --- a/testing.py +++ b/testing.py @@ -26,16 +26,7 @@ API = APIClient( # Keep a token cache to avoid unnecessary sign-ins during development. # (In production, you'd handle this more robustly and securely) -if os.path.exists("token_data.json"): - with open("token_data.json", "r") as f: - token_data = json.load(f) - API.set_token(token_data) - -# Get a new one if we don't have one or if the existing one is expired -if not API.is_token_valid(): - tokendata = API.sign_in() - with open("token_data.json", "w") as f: - json.dump(tokendata, f, indent=2) +API.handle_token() # This will load the token from file if it exists and is valid, or sign in to get a new one if not. ''' END IMPORTANT BIT @@ -50,8 +41,8 @@ def save_response(response): # print("-----------Get a user profile----------------") -# profile = API.run_job('userprofile.get', "simon-little") -# print(profile) +profile = API.run_job('userprofile.get', "simon-little") +print(profile) # print("-------------- Get a branch -----------------") # branch = API.run_job('branch.get', "music") # print(branch) @@ -138,7 +129,7 @@ def save_response(response): # x += 1 # save_response(API.run_job('notification.listnotifications')) -save_response(API.run_job('notification.markallasread')) +# save_response(API.run_job('notification.markallasread')) # save_response(API.run_job('feed.cafefeed')) From 8453c10efeaac5fedd1ddc158914a4a34098c4eb Mon Sep 17 00:00:00 2001 From: simonwt Date: Mon, 13 Apr 2026 22:31:11 +0100 Subject: [PATCH 3/4] Allow comments to be passed a key instead of path --- .../wrappers/comment/create_comment.py | 21 ++++++++++++++----- tests/wrappers/create_comment.py | 21 ++++++++++++++++++- 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/src/trustcafeapiwrapper/wrappers/comment/create_comment.py b/src/trustcafeapiwrapper/wrappers/comment/create_comment.py index 9664053..5c86c1e 100644 --- a/src/trustcafeapiwrapper/wrappers/comment/create_comment.py +++ b/src/trustcafeapiwrapper/wrappers/comment/create_comment.py @@ -1,7 +1,7 @@ from trustcafeapiwrapper.utils.get_parent_pksk_from_path import get_parent_pksk_from_path from trustcafeapiwrapper.utils.get_post_pksk import get_post_pksk -def create_comment(comment_text, post_slug, parent_path, blur_label=None, version=3): +def create_comment(comment_text, post_slug=None, parent_path=None, post_key=None, blur_label=None, version=3): """ Creates a new comment. @@ -15,9 +15,20 @@ def create_comment(comment_text, post_slug, parent_path, blur_label=None, versio dict: A dictionary containing the job name and payload for creating the comment that will be processed by the API client wrapper function. """ - - parent_pksk = get_parent_pksk_from_path(parent_path) - post_pksk = get_post_pksk(parent_pksk, post_slug) + if post_key is not None: + post_pksk = { + "pk": post_key.get('pk'), + "sk": post_key.get('sk') + } + elif parent_path is not None and post_slug is not None: + parent_pksk = get_parent_pksk_from_path(parent_path) + post_pksk = get_post_pksk(parent_pksk, post_slug) + else: + raise ValueError("Either post_key or both parent_path and post_slug must be provided.") + + if comment_text is None or comment_text.strip() == "": + raise ValueError("Comment text cannot be empty.") + return { "job_function": "comment.create", "payload": { @@ -26,7 +37,7 @@ def create_comment(comment_text, post_slug, parent_path, blur_label=None, versio "parent": { "pk": post_pksk['pk'], "sk": post_pksk['sk'], - "slug": parent_path.split('/')[-1] + "slug": post_pksk.get('sk', '').replace('post#', ''), }, "topLevel": { "pk": post_pksk['pk'], diff --git a/tests/wrappers/create_comment.py b/tests/wrappers/create_comment.py index 3156937..8f2df2a 100644 --- a/tests/wrappers/create_comment.py +++ b/tests/wrappers/create_comment.py @@ -19,4 +19,23 @@ class TestCreateComment(unittest.TestCase): self.assertEqual(result["payload"]["commentText"], self.comment_text) self.assertEqual(result["payload"]["blurLabel"], self.blur_label) self.assertEqual(result["payload"]["parent"]["pk"], "maintrunk#maintrunk") - self.assertEqual(result["payload"]["parent"]["sk"], "post#1774951384-98fe38df") \ No newline at end of file + self.assertEqual(result["payload"]["parent"]["sk"], "post#1774951384-98fe38df") + + def test_create_comment_with_post_key(self): + post_key = { + "pk": "maintrunk#maintrunk", + "sk": "post#1774951384-98fe38df" + } + result = create_comment( + comment_text=self.comment_text, + post_key=post_key, + blur_label=self.blur_label + ) + self.assertIsInstance(result, dict) + self.assertIn("job_function", result) + self.assertIn("payload", result) + self.assertEqual(result["job_function"], "comment.create") + self.assertEqual(result["payload"]["commentText"], self.comment_text) + self.assertEqual(result["payload"]["blurLabel"], self.blur_label) + self.assertEqual(result["payload"]["parent"]["pk"], post_key["pk"]) + self.assertEqual(result["payload"]["parent"]["sk"], post_key["sk"]) \ No newline at end of file From 5705c05189224e8227a7ff003902ce2274c60b89 Mon Sep 17 00:00:00 2001 From: simonwt Date: Mon, 13 Apr 2026 22:47:20 +0100 Subject: [PATCH 4/4] Documentation and version --- documentation.md | 24 +++++++++++++++++++ pyproject.toml | 2 +- .../wrappers/comment/create_comment.py | 5 +++- .../wrappers/post/update_post.py | 2 ++ 4 files changed, 31 insertions(+), 2 deletions(-) diff --git a/documentation.md b/documentation.md index 6363b30..5f07d0f 100644 --- a/documentation.md +++ b/documentation.md @@ -24,6 +24,30 @@ API = APIClient( debug=False, ) ``` +## Handle token (easy way) + +```python +API.handle_token() +``` +## Handling the token yourself + +```python + +def getMyToken(): + # get the token from your personal store + # initially this will be None + return token_data + +def saveMyToken(): + # save your token to your personal store + return True + +# Use the old token or None if we haven't logged in ever from here +API.set_token(getMyToken()) +# Get a new one if we don't have one or if the existing one is expired +if not API.is_token_valid(): + saveMyToken(API.sign_in()) +``` ## Use a wrapper Wrappers make it as simple as possible to perform an action. diff --git a/pyproject.toml b/pyproject.toml index 66daa47..c2eafcc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "trustcafeapiwrapper" -version = "0.1.0.10" +version = "0.1.0.11" description = "Wraps the Trust Cafe API" readme = "README.md" requires-python = ">=3.11" diff --git a/src/trustcafeapiwrapper/wrappers/comment/create_comment.py b/src/trustcafeapiwrapper/wrappers/comment/create_comment.py index 5c86c1e..32f4f1d 100644 --- a/src/trustcafeapiwrapper/wrappers/comment/create_comment.py +++ b/src/trustcafeapiwrapper/wrappers/comment/create_comment.py @@ -6,8 +6,11 @@ def create_comment(comment_text, post_slug=None, parent_path=None, post_key=None Creates a new comment. Args: + comment_text (str): The text content of the comment. - parent_path (str): The parent path for the comment, in the format 'userprofile/slug' or 'subwiki/slug'. + post_slug (str, optional): The slug of the post to which the comment belongs. + parent_path (str, optional): The parent path for the comment, in the format 'userprofile/slug' or 'subwiki/slug'. + post_key (dict, optional): A dictionary containing the primary key (pk) and sort key (sk) of the post. blur_label (str, optional): An optional label for blurring the comment content. version (int, optional): The version of the comment structure to use, default is 3. diff --git a/src/trustcafeapiwrapper/wrappers/post/update_post.py b/src/trustcafeapiwrapper/wrappers/post/update_post.py index f289124..c4b3cfe 100644 --- a/src/trustcafeapiwrapper/wrappers/post/update_post.py +++ b/src/trustcafeapiwrapper/wrappers/post/update_post.py @@ -7,7 +7,9 @@ def update_post( post_text, post_path=None, parent_path='/', post_key=None, blur Args: post_slug (str): The slug of the post to update. post_text (str): The new text for the post. + post_path (str, optional): The path of the post to update. Defaults to None. parent_path (str, optional): The parent path for the post. Defaults to '/'. + post_key (dict, optional): A dictionary containing the primary key (pk) and sort key (sk) of the post. Defaults to None. blur_label (str, optional): The blur label for the post. Defaults to None. card_url (str, optional): The card URL for the post. Defaults to None. collaborative (bool, optional): Whether the post is collaborative. Defaults to False.