Merge branch 'dev' into 'main'

WIP

See merge request trustcafe/trustcafe-api-wrapper!5
This commit is contained in:
Simon Little 2026-04-08 23:42:20 +00:00
commit 81680d91b1
14 changed files with 289 additions and 39 deletions

View file

@ -1,18 +1,107 @@
# Basic usage (without an .env)
Here's how to make a post to the homepage (aka Maintrunk)
## Setup
Here's a very basic example of setting up:
```python
import trustcafeapiwrapper
from trustcafeapiwrapper.wrappers.post.create_post import create_post
# Setup the API Client
API = trustcafeapiwrapper.APIClient(
client_id="YOUR_CLIENT_ID"
client_secret="YOUR_CLIENT_SECRET"
client_id="YOUR_CLIENT_ID",
client_secret="YOUR_CLIENT_SECRET",
env="alpha" # alpha | production.
debug=False
)
```
or with .env
```python
import trustcafeapiwrapper, os
from dotenv import load_dotenv
load_dotenv()
API = APIClient(
client_id=os.getenv("client_id"),
client_secret=os.getenv("client_secret"),
env="alpha", # alpha | production.
debug=False,
)
```
## Use a wrapper
Wrappers make it as simple as possible to perform an action.
A wrapper will come with a job and a payload ready to be passed to the `wrapped` function.
eg. Use the `create_post` wrapper
```python
from trustcafeapiwrapper.wrappers.post.create_post import create_post
# Use the create_post wrapper
API.wrapped(create_post(
"This is a test post created via the create_post wrapper function.",
))
```
**_NOTE:_** There are not as many wrappers as jobs because fetching is simpler. More will be added though to make it consistent.
## Make a request via `job`
Jobs make the requests to the API. They have the endpoint and path setup already, we just pass the payload. If you want to avoid making your own payload, use a `wrapper`.
Use the job for getting public posts.
Now you don't need to worry about which API
```python
postlist = API.run_job('posts.getpublic')
print(postlist)
'''
{
"Items": [
{
...
'''
```
**_NOTE:_** There are aren't the complete set yet created here.
## Custom Request
There should hopefully be a `job` or a `wrapper` that exists for what you need but if not, then this is how you make a request using the core mechanism.
Use the `make_request` function to make a request from the content API to get posts in the music branch.
```python
postlist = API.make_request("GET", "content", f"post/ref-subwiki/music")
print(postlist)
'''
{
"Items": [
{
...
'''
```
# Jobs
## block
## branch
### `get`
### `listbyname`
## change
## comment
### `create`
### `listbypostid`
## feed
## mute
## notifcation
### `listnotifications`
## post
## reaction
## trust
## userprofile
## vote
# Wrappers
## post
### `create_post`
### `update_post`
## comment
### `create_comment`
# Utils

View file

@ -1,6 +1,6 @@
[project]
name = "trustcafeapiwrapper"
version = "0.1.0.6"
version = "0.1.0.7"
description = "Wraps the Trust Cafe API"
readme = "README.md"
requires-python = ">=3.13"

View file

@ -11,7 +11,17 @@ endpoints = {
"auth": "https://oo0wks9pbi.execute-api.us-east-1.amazonaws.com/alpha/",
"content": "https://w1yygdhayc.execute-api.us-east-1.amazonaws.com/alpha/",
"megaphone": "https://opdhjaktnl.execute-api.us-east-1.amazonaws.com/alpha/",
}
"moderation": "https://sbb1xrqsf1.execute-api.us-east-1.amazonaws.com/alpha/",
"spider": "https://xuvz1oj1sk.execute-api.us-east-1.amazonaws.com/dev/"
},
"production": {
"audrey": "https://iiouau5d2k.execute-api.us-east-1.amazonaws.com/production/",
"auth": "https://e2we3nktl4.execute-api.us-east-1.amazonaws.com/production/",
"content": "https://32hho6rvg1.execute-api.us-east-1.amazonaws.com/production/",
"megaphone": "https://yfnamdpnc8.execute-api.us-east-1.amazonaws.com/production/",
"moderation": "https://egfpoo3mw3.execute-api.us-east-1.amazonaws.com/production/",
"spider": "https://coi5kvgypc.execute-api.us-east-1.amazonaws.com/production/"
},
}
class APIClient(BaseModel):
# Internal State (Not passed in __init__)
@ -22,10 +32,16 @@ class APIClient(BaseModel):
client_id: SecretStr
client_secret: SecretStr
_access_token: str = PrivateAttr(default="")
_access_token_timeout: int = PrivateAttr(default=0)
def __init__(self, **data):
super().__init__(**data)
self.validate_environment()
def validate_environment(self):
if self.environment not in endpoints:
raise ValueError(f"Environment '{self.environment}' is not valid. Must be one of: {list(endpoints.keys())}")
def make_request(self, method: str, endpoint: str, path: str, data: dict = None, authenticate: bool = True, query_params: dict = None) -> dict:
@ -45,13 +61,16 @@ class APIClient(BaseModel):
url += f"?{urlencode(query_params)}"
# Set up headers for the request
headers = {
"Content-Type": "application/json",
}
# Add the Authorization header with the access token if authentication is required
if authenticate:
token = self._access_token
headers["Authorization"] = f"Bearer {token}"
else:
token = 'guest'
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {token}"
}
# Debugging output to show the request details before making the API call
if self.debug:
@ -112,6 +131,17 @@ class APIClient(BaseModel):
self._access_token = token_data.get("access_token", "")
self._access_token_timeout = token_data.get("access_token_timeout", 0)
def set_environment(self, environment: str) -> None:
"""
Set the environment for the API client.
Args:
environment (str): The environment to set (e.g., "alpha", "production").
"""
self.validate_environment()
self.environment = environment
def is_token_valid(self) -> bool:
"""
Checks if the current access token is still valid based on the current time and the token's expiration time.

View file

@ -1,7 +1,8 @@
from .update import update
from .create import create
from .get import get
from .listbybranch import listbybranch
from .listbyuserprofile import listbyuserprofile
from .create import create
from .listall import listall
from .listpublic import listpublic
from .listremoved import listremoved

View file

@ -7,5 +7,5 @@ def listpublic(API, lastEvaluatedKey=None) -> dict:
Returns:
dict: The list of public posts.
"""
post_list = API.make_request("GET", "content", f"post/public", authenticate=True, query_params=lastEvaluatedKey)
post_list = API.make_request("GET", "content", f"public/posts", authenticate=False, query_params=lastEvaluatedKey)
return post_list

View file

@ -0,0 +1,12 @@
def update(API, payload: dict) -> dict:
"""
Updates an existing post in the API.
Args:
payload (dict): The data for the post update.
Returns:
dict: The post data.
"""
post_data = API.make_request("PUT", "content", "post/update", data=payload, authenticate=True)
return post_data

View file

@ -1 +1,2 @@
from .create_post import create_post
from .update_post import update_post

View file

@ -0,0 +1,37 @@
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):
"""
Updates an existing post.
Args:
post_slug (str): The slug of the post to update.
post_text (str): The new text for the post.
parent_path (str, optional): The parent path for the post. Defaults to '/'.
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.
Returns:
dict: The updated post data.
"""
parent_pksk = get_parent_pksk_from_path(parent_path)
post_pksk = get_post_pksk(parent_pksk, post_path)
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,
}
return {
"job_function": "post.update",
"payload": payload
}

View file

@ -61,9 +61,9 @@ def save_response(response):
# print("-----------------------------")
# feed = API.run_job('post.listbybranch', "music")
# print(feed)
print("-----------------------------")
feed = save_response(API.run_job('post.listremoved'))
print(feed)
# print("-----------------------------")
# feed = save_response(API.run_job('post.listremoved'))
# print(feed)
# print("----------------------------z
# branchlist = API.run_job('branch.listbyname')
# print(branchlist)
@ -80,6 +80,16 @@ print(feed)
# print(feed)
# print("-----------------------------")
# save_response(API.run_job('post.create', {
# "blurLabel": None,
# "cardUrl": None,
# "postText": "This is a test post created via the API wrapper.",
# "collaborative": False,
# "parent": {
# "pk": "maintrunk#maintrunk",
# "sk": "maintrunk#maintrunk"
# }
# }))
# save_response(API.run_job('post.create', {
# "blurLabel": None,
# "cardUrl": None,
@ -139,7 +149,7 @@ print(feed)
# }))
# save_response(API.run_job('post.listall'))
# save_response(API.run_job('post.listpublic'))
save_response(API.run_job('post.listpublic'))
# from trustcafeapiwrapper.wrappers.post.create_post import create_post
@ -183,8 +193,15 @@ print(feed)
# users = API.run_job('trust.listbyuserhas', "simon-little ")
# print(users)
from trustcafeapiwrapper.wrappers.trust import trust
save_response(API.wrapped(trust(
100,
"/user/bossman"
)))
# from trustcafeapiwrapper.wrappers.trust import trust
# save_response(API.wrapped(trust(
# 100,
# "/user/bossman"
# )))
# from trustcafeapiwrapper.wrappers.post.update_post import update_post
# save_response(API.wrapped(update_post(
# post_text="This is an updated version of the test post created via the create_post wrapper function.",
# post_path="/post/1775143460-ef45186a",
# parent_path="/",
# )))

View file

@ -47,10 +47,42 @@ class TestAPIClient(unittest.TestCase):
result = self.api_client.run_job(mock_job, "value1")
self.assertEqual(result, "Job executed with value1")
def test_set_production_env(self):
Prod_API = APIClient(
client_id="prod_client_id",
client_secret="prod_client_secret",
debug=False,
environment="production"
)
self.assertEqual(Prod_API.environment, "production")
def test_set_bad_env(self):
with self.assertRaises(ValueError):
APIClient(
client_id="test_client_id",
client_secret="test_client_secret",
debug=False,
environment="invalid_env"
)
def test_set_environment_method(self):
self.api_client.set_environment("production")
self.assertEqual(self.api_client.environment, "production")
def test_make_non_supported_request_type(self):
with self.assertRaises(ValueError):
self.api_client.make_request("PATCH", "content", "test")
def test_make_request_with_bad_endpoint(self):
with self.assertRaises(ValueError):
self.api_client.make_request("GET", "invalid_endpoint", "test")
@patch('trustcafeapiwrapper.apiclient.requests.request')
def test_make_request(self, mock_request):
# This is a placeholder test. In a real test, you'd mock the HTTP request and response.
response = self.api_client.make_request("GET", "content", "https://api.example.com/test")
# This test should be expanded
# Or the the functions should be broken up more to be more easily testable
response = self.api_client.make_request("GET", "content", "test")
#
mock_request.assert_called_once()

View file

@ -13,10 +13,10 @@ class TestGetChildSpkskFromPaths(unittest.TestCase):
self.assertEqual(get_child_spksk_from_paths(parent_path, item_path), expected_output)
def test_comment_reaction(self):
parent_path = '/post/12345'
parent_path = '/post/12345-abcv'
item_path = '/comment/67890'
expected_output = {
"pk": "post#12345",
"pk": "post#12345-abcv",
"sk": "comment#67890",
"entity": "comment",
"slug": "67890"

View file

@ -5,7 +5,7 @@ class TestReact(unittest.TestCase):
def test_react_to_post(self):
reaction_type = 'like'
parent_path = '/'
item_path = '/post/12345'
item_path = '/post/12345-abcv'
result = react(reaction_type, parent_path, item_path)
self.assertIsInstance(result, dict)
@ -14,13 +14,13 @@ class TestReact(unittest.TestCase):
self.assertEqual(result["job_function"], "reaction.reacttosomething")
self.assertEqual(result["payload"]["reaction"], reaction_type)
self.assertEqual(result["payload"]["parent"]["pk"], "maintrunk#maintrunk")
self.assertEqual(result["payload"]["parent"]["sk"], "post#12345")
self.assertEqual(result["payload"]["parent"]["sk"], "post#12345-abcv")
self.assertEqual(result["payload"]["parent"]["entity"], "post")
self.assertEqual(result["payload"]["parent"]["slug"], "12345")
self.assertEqual(result["payload"]["parent"]["slug"], "12345-abcv")
def test_react_to_comment(self):
reaction_type = 'like'
parent_path = '/post/12345'
parent_path = '/post/12345-abcv'
item_path = '/comment/67890'
result = react(reaction_type, parent_path, item_path)
@ -29,7 +29,7 @@ class TestReact(unittest.TestCase):
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")
self.assertEqual(result["payload"]["parent"]["pk"], "post#12345-abcv")
self.assertEqual(result["payload"]["parent"]["sk"], "comment#67890")
self.assertEqual(result["payload"]["parent"]["entity"], "comment")
self.assertEqual(result["payload"]["parent"]["slug"], "67890")

View file

@ -0,0 +1,30 @@
import unittest
from trustcafeapiwrapper.wrappers.post.update_post import update_post
class TestUpdatePost(unittest.TestCase):
def setUp(self):
self.post_text = "This is an updated test post created via the update_post wrapper function."
self.blur_label = None
self.card_url = None
self.collaborative = False
def test_update_post(self):
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=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"]["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"])

View file

@ -11,6 +11,7 @@ from tests.utils.get_user_slug_from_path import TestGetUserSlugFromPath
from tests.utils.get_userprofile_pksk_from_slug import TestGetUserprofilePkskFromSlug
from tests.wrappers.create_post import TestCreatePost
from tests.wrappers.update_post import TestUpdatePost
from tests.wrappers.create_comment import TestCreateComment
from tests.wrappers.react import TestReact
from tests.wrappers.vote import TestVoteCast