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.
- arcane_tiktok-0.1.0/PKG-INFO +27 -0
- arcane_tiktok-0.1.0/README.md +5 -0
- arcane_tiktok-0.1.0/arcane/tiktok/__init__.py +4 -0
- arcane_tiktok-0.1.0/arcane/tiktok/const.py +2 -0
- arcane_tiktok-0.1.0/arcane/tiktok/exceptions.py +7 -0
- arcane_tiktok-0.1.0/arcane/tiktok/lib.py +82 -0
- arcane_tiktok-0.1.0/arcane/tiktok/tiktok.py +89 -0
- arcane_tiktok-0.1.0/pyproject.toml +24 -0
|
@@ -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,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"
|