Merge branch 'dev' into 'main'
0.1.0.13: Following See merge request trustcafe/trustcafe-api-wrapper!11
This commit is contained in:
commit
14346ed02e
24 changed files with 323 additions and 34 deletions
|
|
@ -19,21 +19,19 @@ backend, relfollow for follow, but don't have relblock, instead it's
|
|||
userblock. It's a bit inconsistent and not very obvious at all.
|
||||
Feels helpful to make it more obvious inside the wrapper but is also
|
||||
potentially misleading.
|
||||
7. It makes more sense to organise like: `user.follow` rather than
|
||||
`follow.follow`, and `post.vote` instead of `vote.vote`, but what about
|
||||
`post.comment.vote` vs `comment.vote`, what about
|
||||
`post.comment.comment`, they seem weird.
|
||||
|
||||
## ToDo:
|
||||
|
||||
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
|
||||
Mute
|
||||
Block
|
||||
|
||||
Remvoved posts
|
||||
Branch Mute
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[project]
|
||||
name = "trustcafeapiwrapper"
|
||||
version = "0.1.0.11"
|
||||
version = "0.1.0.13"
|
||||
description = "Wraps the Trust Cafe API"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
|
|
|
|||
|
|
@ -82,7 +82,10 @@ class APIClient(BaseModel):
|
|||
|
||||
# Make the API request and handle potential exceptions
|
||||
try:
|
||||
response = requests.request(method.upper(), url, json=data, headers=headers, timeout=20)
|
||||
with requests.Session() as session:
|
||||
session.headers.update(headers)
|
||||
response = session.request(method.upper(), url, json=data, headers=headers, timeout=20)
|
||||
response.raise_for_status() # Raise an exception for HTTP errors
|
||||
return_json = response.json()
|
||||
except requests.exceptions.RequestException as e:
|
||||
raise ConnectionError(f"An error occurred while making the request: {e}")
|
||||
|
|
@ -170,9 +173,11 @@ class APIClient(BaseModel):
|
|||
|
||||
# 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()
|
||||
token_data = self.sign_in()
|
||||
with open(token_data_path, "w") as f:
|
||||
json.dump(tokendata, f, indent=2)
|
||||
json.dump(token_data, f, indent=2)
|
||||
|
||||
return token_data
|
||||
|
||||
def run_job(self, job_function, *args, **kwargs):
|
||||
"""
|
||||
|
|
|
|||
1
src/trustcafeapiwrapper/jobs/follow/__init__.py
Normal file
1
src/trustcafeapiwrapper/jobs/follow/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from .follow import follow
|
||||
12
src/trustcafeapiwrapper/jobs/follow/follow.py
Normal file
12
src/trustcafeapiwrapper/jobs/follow/follow.py
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
def follow(API, payload: dict) -> dict:
|
||||
"""
|
||||
Creates or updates a follow relationship in the API.
|
||||
|
||||
Args:
|
||||
payload (dict): The data for the follow relationship.
|
||||
|
||||
Returns:
|
||||
dict: The follow relationship data.
|
||||
"""
|
||||
follow_data = API.make_request("POST", "content", "relfollow", data=payload, authenticate=True)
|
||||
return follow_data
|
||||
|
|
@ -5,3 +5,4 @@ from .make_post_sk import make_post_sk
|
|||
from .get_child_spksk_from_paths import get_child_spksk_from_paths
|
||||
from .get_user_slug_from_path import get_user_slug_from_path
|
||||
from .get_userprofile_pksk_from_slug import get_userprofile_pksk_from_slug
|
||||
from .get_entity_from_str import get_entity_from_str
|
||||
34
src/trustcafeapiwrapper/utils/get_entity_from_str.py
Normal file
34
src/trustcafeapiwrapper/utils/get_entity_from_str.py
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
def get_entity_from_str(entity):
|
||||
|
||||
# Translate the more obvious names to our
|
||||
# obscure internal ones - sorry about that!
|
||||
if entity == 'branch':
|
||||
entity = 'subwiki'
|
||||
elif entity == 'user':
|
||||
entity = 'userprofile'
|
||||
elif entity == 'follow':
|
||||
entity = 'relfollow'
|
||||
elif entity == 'trust':
|
||||
entity = 'reltrust'
|
||||
elif entity == 'block':
|
||||
entity = 'userblock'
|
||||
elif entity == 'mute':
|
||||
entity = 'usermute'
|
||||
|
||||
|
||||
valid = [
|
||||
'comment',
|
||||
'post',
|
||||
'reaction',
|
||||
'relfollow',
|
||||
'reltrust',
|
||||
'subwiki',
|
||||
'userblock',
|
||||
'usermute',
|
||||
'userprofile',
|
||||
'vote',
|
||||
]
|
||||
if entity not in valid:
|
||||
raise ValueError(f"Invalid entity: {entity}. Must be one of {', '.join(valid)}.")
|
||||
|
||||
return entity
|
||||
|
|
@ -1,13 +1,11 @@
|
|||
from .get_entity_from_str import get_entity_from_str
|
||||
def get_parent_pksk_from_path(parent_path):
|
||||
if parent_path == '/':
|
||||
return 'maintrunk#maintrunk'
|
||||
|
||||
entity, slug = parent_path.strip('/').split('/')
|
||||
|
||||
if entity == 'branch':
|
||||
entity = 'subwiki'
|
||||
elif entity == 'user':
|
||||
entity = 'userprofile'
|
||||
entity = get_entity_from_str(entity)
|
||||
|
||||
if entity not in ['userprofile', 'subwiki']:
|
||||
raise ValueError(f"Invalid parent entity: {entity}. Must be 'userprofile' or 'subwiki'.")
|
||||
|
|
|
|||
|
|
@ -1,7 +1,14 @@
|
|||
|
||||
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=None, parent_path=None, post_key=None, blur_label=None, version=3):
|
||||
def create_comment(
|
||||
comment_text:str,
|
||||
post_slug:str|None=None,
|
||||
parent_path:str|None=None,
|
||||
post_key:dict|None=None,
|
||||
blur_label:str|None=None,
|
||||
version:int=3
|
||||
):
|
||||
"""
|
||||
Creates a new comment.
|
||||
|
||||
|
|
|
|||
40
src/trustcafeapiwrapper/wrappers/follow/follow.py
Normal file
40
src/trustcafeapiwrapper/wrappers/follow/follow.py
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
from trustcafeapiwrapper.utils.get_entity_from_str import get_entity_from_str
|
||||
|
||||
def follow(
|
||||
entity: str,
|
||||
is_following: bool,
|
||||
parent_slug: str,
|
||||
):
|
||||
"""
|
||||
Creates new or update existing follow entry in the API.
|
||||
|
||||
Args:
|
||||
entity (str): The type of entity to follow (userprofile | subwiki).
|
||||
is_following (bool): Indicates whether the entity is being followed.
|
||||
parent_slug (str): The slug of the parent entity.
|
||||
|
||||
Returns:
|
||||
dict: A dictionary containing the job name and payload for creating the post
|
||||
that will be processed by the API client wrapper function.
|
||||
"""
|
||||
entity = get_entity_from_str(entity)
|
||||
|
||||
itemPKSK = f"{entity}#{parent_slug}"
|
||||
|
||||
return {
|
||||
"job_function": "follow.follow",
|
||||
"payload": {
|
||||
"isFollowing": is_following,
|
||||
"parent": {
|
||||
"pk": itemPKSK,
|
||||
"sk": itemPKSK
|
||||
},
|
||||
"followType": entity,
|
||||
"parentSlug": parent_slug,
|
||||
"preferences": {
|
||||
"notification": True,
|
||||
"emailNew": False,
|
||||
"emailDigest": True
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,12 @@
|
|||
from trustcafeapiwrapper.utils.get_parent_pksk_from_path import get_parent_pksk_from_path
|
||||
|
||||
def create_post(post_text, parent_path='/', blur_label=None, card_url=None, collaborative=False):
|
||||
def create_post(
|
||||
post_text:str,
|
||||
parent_path:str|None='/',
|
||||
blur_label:str|None=None,
|
||||
card_url:str|None=None,
|
||||
collaborative:bool=False
|
||||
):
|
||||
"""
|
||||
Creates a new post.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,14 @@
|
|||
from trustcafeapiwrapper.utils import get_post_pksk, get_parent_pksk_from_path
|
||||
|
||||
def update_post( post_text, post_path=None, parent_path='/', post_key=None, blur_label=None, card_url=None, collaborative=False):
|
||||
def update_post(
|
||||
post_text:str,
|
||||
post_path:str|None=None,
|
||||
parent_path:str|None='/',
|
||||
post_key:dict|None=None,
|
||||
blur_label:str|None=None,
|
||||
card_url:str|None=None,
|
||||
collaborative:bool=False
|
||||
):
|
||||
"""
|
||||
Updates an existing post.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
|
||||
from trustcafeapiwrapper.utils import get_child_spksk_from_paths
|
||||
def react(reaction_type: str, parent_path: str, item_path: str):
|
||||
def react(
|
||||
reaction_type: str,
|
||||
parent_path: str|None=None,
|
||||
item_path: str|None=None,
|
||||
item_key: dict|None=None
|
||||
):
|
||||
"""
|
||||
React to something. ie a post or a comment.
|
||||
This is one endpoint for creating, updating and deleting reactions.
|
||||
|
|
@ -12,6 +17,12 @@ def react(reaction_type: str, parent_path: str, item_path: str):
|
|||
dict: A dictionary containing the job name and payload for creating the post
|
||||
that will be processed by the API client wrapper function.
|
||||
"""
|
||||
if item_key is not None:
|
||||
parent = {
|
||||
'pk': item_key.get('pk', None),
|
||||
'sk': item_key.get('sk', None)
|
||||
}
|
||||
else:
|
||||
parent = get_child_spksk_from_paths(parent_path, item_path)
|
||||
return {
|
||||
"job_function": "reaction.reacttosomething",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
from trustcafeapiwrapper.utils import get_user_slug_from_path, get_userprofile_pksk_from_slug
|
||||
|
||||
def trust(trustLevel: str, userprofile_path: str):
|
||||
def trust(
|
||||
trustLevel: str,
|
||||
userprofile_path: str
|
||||
):
|
||||
"""
|
||||
Creates new or update existing trust entry in the API.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
from trustcafeapiwrapper.utils import get_child_spksk_from_paths
|
||||
|
||||
def votecast(vote: str, parent_path: str, item_path: str):
|
||||
def votecast(
|
||||
vote: str,
|
||||
parent_path: str|None=None,
|
||||
item_path: str|None=None,
|
||||
item_key: dict|None=None
|
||||
):
|
||||
"""
|
||||
Creates a new vote in the API.
|
||||
|
||||
|
|
@ -10,6 +15,14 @@ def votecast(vote: str, parent_path: str, item_path: str):
|
|||
dict: A dictionary containing the job name and payload for creating the post
|
||||
that will be processed by the API client wrapper function.
|
||||
"""
|
||||
if item_key is not None:
|
||||
parent = {
|
||||
'pk': item_key.get('pk', None),
|
||||
'sk': item_key.get('sk', None),
|
||||
'slug': item_key.get('sk', '').split('#')[-1],
|
||||
'entity': item_key.get('sk', '').split('#')[0]
|
||||
}
|
||||
else:
|
||||
parent = get_child_spksk_from_paths(parent_path, item_path)
|
||||
|
||||
return {
|
||||
|
|
|
|||
16
testing.py
16
testing.py
|
|
@ -26,7 +26,9 @@ API = APIClient(
|
|||
# Keep a token cache to avoid unnecessary sign-ins during development.
|
||||
# (In production, you'd handle this more robustly and securely)
|
||||
|
||||
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.
|
||||
token_data = 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.
|
||||
if token_data is None or 'access_token' not in token_data:
|
||||
raise Exception("Failed to obtain a valid access token. Please check your credentials and token handling.")
|
||||
|
||||
'''
|
||||
END IMPORTANT BIT
|
||||
|
|
@ -41,8 +43,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)
|
||||
|
|
@ -197,3 +199,11 @@ print(profile)
|
|||
# post_path="/post/1775143460-ef45186a",
|
||||
# parent_path="/",
|
||||
# )))
|
||||
|
||||
|
||||
from trustcafeapiwrapper.wrappers.follow.follow import follow
|
||||
save_response(API.wrapped(follow(
|
||||
entity='userprofile',
|
||||
is_following=True,
|
||||
parent_slug='alphaemail-test2'
|
||||
)))
|
||||
|
|
@ -79,7 +79,7 @@ class TestAPIClient(unittest.TestCase):
|
|||
self.api_client.make_request("GET", "invalid_endpoint", "test")
|
||||
|
||||
|
||||
@patch('trustcafeapiwrapper.apiclient.requests.request')
|
||||
@patch('trustcafeapiwrapper.apiclient.requests.Session.request')
|
||||
def test_make_request(self, mock_request):
|
||||
# This test should be expanded
|
||||
# Or the the functions should be broken up more to be more easily testable
|
||||
|
|
|
|||
19
tests/utils/get_entity_from_str.py
Normal file
19
tests/utils/get_entity_from_str.py
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import unittest
|
||||
from trustcafeapiwrapper.utils.get_entity_from_str import get_entity_from_str
|
||||
|
||||
class TestGetEntityFromStr(unittest.TestCase):
|
||||
def test_branch(self):
|
||||
self.assertEqual(get_entity_from_str('branch'), 'subwiki')
|
||||
|
||||
def test_user(self):
|
||||
self.assertEqual(get_entity_from_str('user'), 'userprofile')
|
||||
|
||||
def test_subwiki(self):
|
||||
self.assertEqual(get_entity_from_str('subwiki'), 'subwiki')
|
||||
|
||||
def test_userprofile(self):
|
||||
self.assertEqual(get_entity_from_str('userprofile'), 'userprofile')
|
||||
|
||||
def test_invalid_entity(self):
|
||||
with self.assertRaises(ValueError):
|
||||
get_entity_from_str('invalidentity')
|
||||
43
tests/wrappers/follow.py
Normal file
43
tests/wrappers/follow.py
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import unittest
|
||||
from trustcafeapiwrapper.wrappers.follow.follow import follow
|
||||
|
||||
class TestFollow(unittest.TestCase):
|
||||
def test_follow(self):
|
||||
|
||||
result = follow(
|
||||
entity='user',
|
||||
is_following=True,
|
||||
parent_slug='janedoe'
|
||||
)
|
||||
|
||||
self.assertIsInstance(result, dict)
|
||||
self.assertIn("job_function", result)
|
||||
self.assertIn("payload", result)
|
||||
self.assertEqual(result["job_function"], "follow.follow")
|
||||
self.assertIsInstance(result["payload"], dict)
|
||||
self.assertIn("isFollowing", result["payload"])
|
||||
self.assertIn("parent", result["payload"])
|
||||
self.assertIn("followType", result["payload"])
|
||||
self.assertIn("preferences", result["payload"])
|
||||
self.assertEqual(result["payload"]["isFollowing"], True)
|
||||
self.assertEqual(result["payload"]["parentSlug"], "janedoe")
|
||||
self.assertEqual(result["payload"]["parent"]["pk"], "userprofile#janedoe")
|
||||
|
||||
def test_unfollow(self):
|
||||
|
||||
result = follow(
|
||||
entity='user',
|
||||
is_following=False,
|
||||
parent_slug='janedoe'
|
||||
)
|
||||
|
||||
self.assertIsInstance(result, dict)
|
||||
self.assertIn("job_function", result)
|
||||
self.assertIn("payload", result)
|
||||
self.assertEqual(result["job_function"], "follow.follow")
|
||||
self.assertIsInstance(result["payload"], dict)
|
||||
self.assertIn("isFollowing", result["payload"])
|
||||
self.assertIn("parent", result["payload"])
|
||||
self.assertIn("followType", result["payload"])
|
||||
self.assertIn("preferences", result["payload"])
|
||||
self.assertEqual(result["payload"]["isFollowing"], False)
|
||||
|
|
@ -33,3 +33,19 @@ class TestReact(unittest.TestCase):
|
|||
self.assertEqual(result["payload"]["parent"]["sk"], "comment#67890")
|
||||
self.assertEqual(result["payload"]["parent"]["entity"], "comment")
|
||||
self.assertEqual(result["payload"]["parent"]["slug"], "67890")
|
||||
|
||||
def react_with_parent_key(self):
|
||||
reaction_type = 'like'
|
||||
parent_key = {
|
||||
'pk': 'post#12345-abcv',
|
||||
'sk': 'comment#67890'
|
||||
}
|
||||
result = react(reaction_type, parent_key=parent_key)
|
||||
|
||||
self.assertIsInstance(result, dict)
|
||||
self.assertIn("job_function", result)
|
||||
self.assertIn("payload", result)
|
||||
self.assertEqual(result["job_function"], "reaction.reacttosomething")
|
||||
self.assertEqual(result["payload"]["reaction"], reaction_type)
|
||||
self.assertEqual(result["payload"]["parent"]["pk"], "post#12345-abcv")
|
||||
self.assertEqual(result["payload"]["parent"]["sk"], "comment#67890")
|
||||
|
|
@ -91,3 +91,47 @@ class TestUpdatePost(unittest.TestCase):
|
|||
self.assertNotIn("slug", result["payload"]["key"])
|
||||
self.assertEqual(result["payload"]["blurLabel"], blur_label)
|
||||
self.assertEqual(result["payload"]["cardUrl"], card_url)
|
||||
|
||||
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_post_missing_post_text(self):
|
||||
with self.assertRaises(ValueError) as context:
|
||||
update_post(
|
||||
parent_path='/',
|
||||
post_path='/post/1235-abcv',
|
||||
post_text=None,
|
||||
blur_label=self.blur_label,
|
||||
card_url=self.card_url,
|
||||
collaborative=self.collaborative
|
||||
)
|
||||
self.assertEqual(str(context.exception), "post_text is required.")
|
||||
|
||||
def test_update_post_missing_identification(self):
|
||||
with self.assertRaises(ValueError) as context:
|
||||
update_post(
|
||||
post_text=self.post_text,
|
||||
blur_label=self.blur_label,
|
||||
card_url=self.card_url,
|
||||
collaborative=self.collaborative
|
||||
)
|
||||
self.assertEqual(str(context.exception), "Either post_path and parent_path or post_key must be provided.")
|
||||
|
|
@ -32,3 +32,22 @@ class TestVoteCast(unittest.TestCase):
|
|||
self.assertEqual(result["payload"]["parent"]["sk"], "comment#67890")
|
||||
self.assertEqual(result["payload"]["parent"]["entity"], "comment")
|
||||
self.assertEqual(result["payload"]["parent"]["slug"], "67890")
|
||||
|
||||
def test_vote_cast_with_parent_key(self):
|
||||
vote = 'up'
|
||||
parent_key = {
|
||||
'pk': 'post#12345',
|
||||
'sk': 'comment#67890'
|
||||
}
|
||||
|
||||
result = votecast(vote, item_key=parent_key)
|
||||
|
||||
self.assertIsInstance(result, dict)
|
||||
self.assertIn("job_function", result)
|
||||
self.assertIn("payload", result)
|
||||
self.assertEqual(result["job_function"], "vote.votecast")
|
||||
self.assertEqual(result["payload"]["vote"], vote)
|
||||
self.assertEqual(result["payload"]["parent"]["pk"], "post#12345")
|
||||
self.assertEqual(result["payload"]["parent"]["sk"], "comment#67890")
|
||||
self.assertEqual(result["payload"]["parent"]["entity"], "comment")
|
||||
self.assertEqual(result["payload"]["parent"]["slug"], "67890")
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import sys
|
|||
sys.path.insert(0, './src/')
|
||||
import unittest
|
||||
|
||||
from tests.utils.get_entity_from_str import TestGetEntityFromStr
|
||||
from tests.utils.get_post_pksk import TestGetPostPksk
|
||||
from tests.utils.get_parent_pksk_from_path import TestGetParentPkskFromPath
|
||||
from tests.utils.make_comment_sk import TestMakeCommentSk
|
||||
|
|
@ -16,7 +17,7 @@ from tests.wrappers.create_comment import TestCreateComment
|
|||
from tests.wrappers.react import TestReact
|
||||
from tests.wrappers.vote import TestVoteCast
|
||||
from tests.wrappers.trust import TestTrustCreateOrUpdate
|
||||
|
||||
from tests.wrappers.follow import TestFollow
|
||||
|
||||
from tests.apiclient import TestAPIClient
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue