arcane-tiktok 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,27 @@
1
+ Metadata-Version: 2.4
2
+ Name: arcane-tiktok
3
+ Version: 0.1.0
4
+ Summary: Helpers to request Tiktok API
5
+ Author: Arcane
6
+ Author-email: product@wearcane.com
7
+ Requires-Python: >=3.9,<4.0
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.9
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Programming Language :: Python :: 3.14
15
+ Requires-Dist: arcane-core (==1.7.0)
16
+ Requires-Dist: arcane-datastore (>=1,<2)
17
+ Requires-Dist: arcane-requests (>=0,<1)
18
+ Requires-Dist: backoff (>=1.10.0)
19
+ Requires-Dist: requests
20
+ Description-Content-Type: text/markdown
21
+
22
+ # Arcane tiktok README
23
+
24
+
25
+ ## Release history
26
+ To see changes, please see CHANGELOG.md
27
+
@@ -0,0 +1,5 @@
1
+ # Arcane tiktok README
2
+
3
+
4
+ ## Release history
5
+ To see changes, please see CHANGELOG.md
@@ -0,0 +1,4 @@
1
+ from .tiktok import *
2
+ from .const import *
3
+ from .exceptions import *
4
+ from .lib import *
@@ -0,0 +1,2 @@
1
+ TIKTOK_SERVER_URL = "https://business-api.tiktok.com/open_api/v1.3"
2
+ TIKTOK_OAUTH_CREDENTIALS_KIND = "tiktok-oauth-credentials"
@@ -0,0 +1,7 @@
1
+ class TikTokAuthError(Exception):
2
+ """Raised when there is an authentication error with TikTok API"""
3
+ pass
4
+
5
+ class TikTokApiError(Exception):
6
+ """Raised when there is a general API error with TikTok API"""
7
+ pass
@@ -0,0 +1,82 @@
1
+ from typing import Optional
2
+
3
+ from arcane.datastore import Client as DatastoreClient
4
+ from arcane.core import BadRequestError, BaseAccount, ALL_CLIENTS_RIGHTS, UserRightsEnum, RightsLevelEnum
5
+ from arcane.requests import call_get_route
6
+
7
+ from .const import TIKTOK_OAUTH_CREDENTIALS_KIND
8
+
9
+ def get_tiktok_account(
10
+ base_account: BaseAccount,
11
+ clients_service_url: Optional[str] = None,
12
+ firebase_api_key: Optional[str] = None,
13
+ gcp_service_account: Optional[str] = None,
14
+ auth_enabled: bool = True
15
+ ) -> dict:
16
+ """Fetch TikTok account details using the base account information.
17
+
18
+ Args:
19
+ base_account (BaseAccount): The base account object.
20
+ clients_service_url (Optional[str]): URL of the clients service.
21
+ firebase_api_key (Optional[str]): Firebase API key for authentication.
22
+ gcp_service_account (Optional[str]): Path to the GCP service account file.
23
+ auth_enabled (bool): Flag to enable or disable authentication.
24
+
25
+ Raises:
26
+ BadRequestError: Raised when required parameters are missing or invalid.
27
+
28
+ Returns:
29
+ dict: TikTok account.
30
+ """
31
+
32
+ if not (clients_service_url and firebase_api_key and gcp_service_account):
33
+ raise BadRequestError('clients_service_url or firebase_api_key or gcp_service_account should not be None if tiktok account is not provided')
34
+
35
+ url = f"{clients_service_url}/api/tiktok-account?advertiser_id={base_account['id']}&client_id={base_account['client_id']}"
36
+ accounts = call_get_route(
37
+ url,
38
+ firebase_api_key,
39
+ claims={'features_rights': { UserRightsEnum.AMS_GTP: RightsLevelEnum.VIEWER }, 'authorized_clients': [ALL_CLIENTS_RIGHTS]},
40
+ auth_enabled=auth_enabled,
41
+ credentials_path=gcp_service_account
42
+ )
43
+ if len(accounts) == 0:
44
+ raise BadRequestError(f'Error while getting tiktok account with: {base_account}. No account corresponding.')
45
+ elif len(accounts) > 1:
46
+ raise BadRequestError(f'Error while getting tiktok account with: {base_account}. Several account corresponding: {accounts}')
47
+
48
+ return accounts[0]
49
+
50
+
51
+ def get_tikok_user_credentials(
52
+ user_email: str,
53
+ gcp_credentials_path: Optional[str],
54
+ gcp_project: Optional[str],
55
+ datastore_client: Optional[DatastoreClient]
56
+ ):
57
+ """Retrieve and decrypt TikTok user credentials.
58
+
59
+ Args:
60
+ user_email (str): Email of the user whose credentials are to be fetched.
61
+ secret_key_file (str): Path to the secret key file for decryption.
62
+ gcp_credentials_path (Optional[str]): Path to GCP credentials.
63
+ gcp_project (Optional[str]): GCP project ID.
64
+ datastore_client (Optional[DatastoreClient]): Datastore client instance.
65
+
66
+ """
67
+
68
+ if not datastore_client:
69
+ if not gcp_credentials_path and not gcp_project:
70
+ raise BadRequestError('gcp_credentials_path or gcp_project should not be None if datastore_client is not provided')
71
+ datastore_client = DatastoreClient.from_service_account_json(gcp_credentials_path, project=gcp_project)
72
+
73
+ query = datastore_client.query(kind=TIKTOK_OAUTH_CREDENTIALS_KIND).add_filter('email', '=', user_email)
74
+ users_credential = list(query.fetch())
75
+ if len(users_credential) == 0:
76
+ raise BadRequestError(f'Error while getting tiktok user credentials with mail: {user_email}. No entity corresponding.')
77
+ elif len(users_credential) > 1:
78
+ raise BadRequestError(f'Error while getting tiktok user credentials with mail: {user_email}. Several entities corresponding: {users_credential}')
79
+
80
+ return users_credential[0]
81
+
82
+
@@ -0,0 +1,89 @@
1
+ import json
2
+ from typing import Optional, cast
3
+ import backoff
4
+ import requests
5
+
6
+ from arcane.core import BaseAccount, BadRequestError
7
+ from arcane.datastore import Client as DatastoreClient
8
+
9
+ from .const import TIKTOK_SERVER_URL
10
+ from .exceptions import TikTokAuthError, TikTokApiError
11
+ from .lib import get_tiktok_account, get_tikok_user_credentials
12
+
13
+ class TiktokClient:
14
+ def __init__(
15
+ self,
16
+ gcp_service_account: str,
17
+ base_account: Optional[BaseAccount] = None,
18
+ user_email: Optional[str] = None,
19
+ clients_service_url: Optional[str] = None,
20
+ firebase_api_key: Optional[str] = None,
21
+ gcp_credentials_path: Optional[str] = None,
22
+ datastore_client: Optional[DatastoreClient] = None,
23
+ gcp_project: Optional[str] = None,
24
+ auth_enabled: bool = True
25
+ ) -> None:
26
+
27
+ creator_email = None
28
+
29
+ if gcp_service_account and (base_account or user_email):
30
+ if user_email:
31
+ creator_email = user_email
32
+ else:
33
+ base_account = cast(BaseAccount, base_account)
34
+ tiktok_account = get_tiktok_account(
35
+ base_account=base_account,
36
+ clients_service_url=clients_service_url,
37
+ firebase_api_key=firebase_api_key,
38
+ gcp_service_account=gcp_service_account,
39
+ auth_enabled=auth_enabled
40
+ )
41
+
42
+ creator_email = cast(str, tiktok_account['creator_email'])
43
+
44
+ if creator_email is None:
45
+ raise BadRequestError('creator_email should not be None while using user access protocol')
46
+
47
+ credentials = get_tikok_user_credentials(
48
+ user_email=creator_email,
49
+ gcp_credentials_path=gcp_credentials_path,
50
+ gcp_project=gcp_project,
51
+ datastore_client=datastore_client
52
+ )
53
+
54
+ self._access_token = credentials['access_token']
55
+ else:
56
+ raise BadRequestError('gcp_service_account and (base_account or user_email) should be provided to initialize TiktokClient')
57
+
58
+
59
+ @backoff.on_exception(backoff.expo, requests.exceptions.HTTPError, max_tries=5)
60
+ def _make_request(self, endpoint: str, method: str, params: Optional[dict] = None, headers: Optional[dict] = None, **kwargs) -> dict:
61
+ """Send a request to TikTok API"""
62
+
63
+ default_headers = {"Access-Token": self._access_token}
64
+ if headers:
65
+ default_headers.update(headers)
66
+
67
+ response = requests.request(method=method, url=f"{TIKTOK_SERVER_URL}{endpoint}", headers=default_headers, params=params, **kwargs)
68
+ response.raise_for_status()
69
+
70
+ response = response.json()
71
+ # tiktok return error codes in 200 HTTP responses
72
+ api_code = response.get('code')
73
+ if api_code != 0:
74
+ if api_code in [40104, 40105, 40106]:
75
+ raise TikTokAuthError(f"{response.get('message')}")
76
+ raise TikTokApiError(f"{response.get('message')}")
77
+
78
+ return response.get('data', {})
79
+
80
+ def get_advertiser_info(self, advertiser_ids: list[str]) -> dict:
81
+ """Get advertiser info"""
82
+ params = {"advertiser_ids": json.dumps(advertiser_ids)}
83
+
84
+ response = self._make_request(
85
+ endpoint="/advertiser/info/",
86
+ method="GET",
87
+ params=params
88
+ )
89
+ return response.get('list', {})
@@ -0,0 +1,24 @@
1
+ [tool.poetry]
2
+ name = "arcane-tiktok"
3
+ version = "0.1.0"
4
+ description = "Helpers to request Tiktok API"
5
+ readme = "README.md"
6
+ authors = ["Arcane <product@wearcane.com>"]
7
+ packages = [
8
+ { include = "arcane" }
9
+ ]
10
+
11
+ [tool.poetry.dependencies]
12
+ python = "^3.9"
13
+ requests = '*'
14
+ backoff = ">=1.10.0"
15
+ arcane-core = "1.7.0"
16
+ arcane-datastore = "^1"
17
+ arcane-requests = "^0"
18
+
19
+
20
+ [tool.poetry.group.dev.dependencies]
21
+
22
+ [build-system]
23
+ requires = ["poetry>=0.12"]
24
+ build-backend = "poetry.masonry.api"