rbx 3.20.0.dev175__tar.gz → 3.21.0.dev177__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.
- {rbx-3.20.0.dev175 → rbx-3.21.0.dev177}/PKG-INFO +1 -1
- {rbx-3.20.0.dev175 → rbx-3.21.0.dev177}/pyproject.toml +2 -2
- rbx-3.21.0.dev177/rbx/__init__.py +1 -0
- rbx-3.21.0.dev177/rbx/clients/place.py +277 -0
- {rbx-3.20.0.dev175 → rbx-3.21.0.dev177}/rbx.egg-info/PKG-INFO +1 -1
- {rbx-3.20.0.dev175 → rbx-3.21.0.dev177}/rbx.egg-info/SOURCES.txt +1 -0
- rbx-3.20.0.dev175/rbx/__init__.py +0 -1
- {rbx-3.20.0.dev175 → rbx-3.21.0.dev177}/LICENSE +0 -0
- {rbx-3.20.0.dev175 → rbx-3.21.0.dev177}/README.md +0 -0
- {rbx-3.20.0.dev175 → rbx-3.21.0.dev177}/rbx/auth/__init__.py +0 -0
- {rbx-3.20.0.dev175 → rbx-3.21.0.dev177}/rbx/auth/decorators.py +0 -0
- {rbx-3.20.0.dev175 → rbx-3.21.0.dev177}/rbx/auth/id_token.py +0 -0
- {rbx-3.20.0.dev175 → rbx-3.21.0.dev177}/rbx/auth/keystore.py +0 -0
- {rbx-3.20.0.dev175 → rbx-3.21.0.dev177}/rbx/auth/mock.py +0 -0
- {rbx-3.20.0.dev175 → rbx-3.21.0.dev177}/rbx/aws/__init__.py +0 -0
- {rbx-3.20.0.dev175 → rbx-3.21.0.dev177}/rbx/aws/s3.py +0 -0
- {rbx-3.20.0.dev175 → rbx-3.21.0.dev177}/rbx/buildtools/__init__.py +0 -0
- {rbx-3.20.0.dev175 → rbx-3.21.0.dev177}/rbx/buildtools/cli.py +0 -0
- {rbx-3.20.0.dev175 → rbx-3.21.0.dev177}/rbx/buildtools/tasks/__init__.py +0 -0
- {rbx-3.20.0.dev175 → rbx-3.21.0.dev177}/rbx/buildtools/tasks/ec2.py +0 -0
- {rbx-3.20.0.dev175 → rbx-3.21.0.dev177}/rbx/buildtools/tasks/image.py +0 -0
- {rbx-3.20.0.dev175 → rbx-3.21.0.dev177}/rbx/buildtools/tasks/misc.py +0 -0
- {rbx-3.20.0.dev175 → rbx-3.21.0.dev177}/rbx/clients/__init__.py +0 -0
- {rbx-3.20.0.dev175 → rbx-3.21.0.dev177}/rbx/clients/adsquare.py +0 -0
- {rbx-3.20.0.dev175 → rbx-3.21.0.dev177}/rbx/clients/broadsign.py +0 -0
- {rbx-3.20.0.dev175 → rbx-3.21.0.dev177}/rbx/clients/oxr.py +0 -0
- {rbx-3.20.0.dev175 → rbx-3.21.0.dev177}/rbx/clients/panels.py +0 -0
- {rbx-3.20.0.dev175 → rbx-3.21.0.dev177}/rbx/clients/reporting.py +0 -0
- {rbx-3.20.0.dev175 → rbx-3.21.0.dev177}/rbx/clients/retry.py +0 -0
- {rbx-3.20.0.dev175 → rbx-3.21.0.dev177}/rbx/exceptions.py +0 -0
- {rbx-3.20.0.dev175 → rbx-3.21.0.dev177}/rbx/gcp/__init__.py +0 -0
- {rbx-3.20.0.dev175 → rbx-3.21.0.dev177}/rbx/gcp/cloud_tasks.py +0 -0
- {rbx-3.20.0.dev175 → rbx-3.21.0.dev177}/rbx/gcp/pubsub.py +0 -0
- {rbx-3.20.0.dev175 → rbx-3.21.0.dev177}/rbx/gcp/storage.py +0 -0
- {rbx-3.20.0.dev175 → rbx-3.21.0.dev177}/rbx/logging.py +0 -0
- {rbx-3.20.0.dev175 → rbx-3.21.0.dev177}/rbx/settings.py +0 -0
- {rbx-3.20.0.dev175 → rbx-3.21.0.dev177}/rbx/utils/__init__.py +0 -0
- {rbx-3.20.0.dev175 → rbx-3.21.0.dev177}/rbx/utils/mdm.py +0 -0
- {rbx-3.20.0.dev175 → rbx-3.21.0.dev177}/rbx/utils/vast.py +0 -0
- {rbx-3.20.0.dev175 → rbx-3.21.0.dev177}/rbx/web/__init__.py +0 -0
- {rbx-3.20.0.dev175 → rbx-3.21.0.dev177}/rbx/web/handlers.py +0 -0
- {rbx-3.20.0.dev175 → rbx-3.21.0.dev177}/rbx.egg-info/dependency_links.txt +0 -0
- {rbx-3.20.0.dev175 → rbx-3.21.0.dev177}/rbx.egg-info/entry_points.txt +0 -0
- {rbx-3.20.0.dev175 → rbx-3.21.0.dev177}/rbx.egg-info/requires.txt +0 -0
- {rbx-3.20.0.dev175 → rbx-3.21.0.dev177}/rbx.egg-info/top_level.txt +0 -0
- {rbx-3.20.0.dev175 → rbx-3.21.0.dev177}/setup.cfg +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "rbx"
|
|
7
|
-
version = "3.
|
|
7
|
+
version = "3.21.0.dev177"
|
|
8
8
|
description = "A collection of common tools for Scoota services."
|
|
9
9
|
authors = [
|
|
10
10
|
{ name = "The Scoota Engineering Team", email = "engineering@scoota.com" }
|
|
@@ -80,7 +80,7 @@ homepage = "https://github.com/rockabox/rbx"
|
|
|
80
80
|
repository = "https://github.com/rockabox/rbx.git"
|
|
81
81
|
|
|
82
82
|
[tool.bumpversion]
|
|
83
|
-
current_version = "3.
|
|
83
|
+
current_version = "3.21.0.dev177"
|
|
84
84
|
commit = true
|
|
85
85
|
parse = """
|
|
86
86
|
(?P<major>\\d+)\\.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "3.21.0.dev177"
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Dict, Any, Optional
|
|
3
|
+
|
|
4
|
+
import requests
|
|
5
|
+
|
|
6
|
+
from requests.auth import AuthBase
|
|
7
|
+
from . import Client
|
|
8
|
+
from ..exceptions import ClientError, ServerError
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class BearerAuth(AuthBase):
|
|
15
|
+
"""Authentication class for Bearer token authorization.
|
|
16
|
+
|
|
17
|
+
Adds the Bearer token to the Authorization header of requests.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, token):
|
|
21
|
+
"""Initialize with the token.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
token (str): The authentication token
|
|
25
|
+
"""
|
|
26
|
+
self.token = token
|
|
27
|
+
|
|
28
|
+
def __call__(self, r):
|
|
29
|
+
"""Add Authorization header to the request.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
r (Request): The request object
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
Request: The modified request with Authorization header
|
|
36
|
+
"""
|
|
37
|
+
if self.token:
|
|
38
|
+
r.headers["Authorization"] = "Bearer " + self.token
|
|
39
|
+
return r
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class PlaceExchangeClient(Client):
|
|
43
|
+
"""Client for Place Exchange Creative API.
|
|
44
|
+
|
|
45
|
+
Handles authentication and API communication with Place Exchange.
|
|
46
|
+
This client only handles the HTTP requests and authentication,
|
|
47
|
+
not the formatting of data or interpretation of responses.
|
|
48
|
+
|
|
49
|
+
API documentation: https://api.placeexchange.com/v3/orgs/{id}/ads
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
ENDPOINT = "https://api.placeexchange.com/"
|
|
53
|
+
AUTH_PATH = "v3/token"
|
|
54
|
+
DEFAULT_TIMEOUT = 30
|
|
55
|
+
TOKEN = "access_token"
|
|
56
|
+
MAX_PAGE_SIZE = 4000
|
|
57
|
+
|
|
58
|
+
def __init__(self, org_id: str):
|
|
59
|
+
"""Initialize the Place Exchange client with authentication credentials.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
org_id: Organization ID for Place Exchange API
|
|
63
|
+
"""
|
|
64
|
+
super().__init__()
|
|
65
|
+
self.org_id = org_id
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def auth(self):
|
|
69
|
+
"""Get the authentication object for requests.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
BearerAuth: Authentication object with the token
|
|
73
|
+
"""
|
|
74
|
+
return BearerAuth(self.token)
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def is_authenticated(self):
|
|
78
|
+
"""Check if the client is authenticated."""
|
|
79
|
+
return self.token is not None
|
|
80
|
+
|
|
81
|
+
def refresh_access_token(self):
|
|
82
|
+
"""Refresh the access token.
|
|
83
|
+
|
|
84
|
+
Place Exchange doesn't support refresh tokens, so we just get a new token.
|
|
85
|
+
"""
|
|
86
|
+
self.logout()
|
|
87
|
+
self.login(self.credentials["username"], self.credentials["password"])
|
|
88
|
+
|
|
89
|
+
def get_creative(self, creative_id: str):
|
|
90
|
+
"""Get a creative by name.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
creative_id: The name of the ad to get
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
The raw API response data
|
|
97
|
+
|
|
98
|
+
Raises:
|
|
99
|
+
ServerError: On server-side errors or invalid responses
|
|
100
|
+
ClientError: On client-side errors
|
|
101
|
+
"""
|
|
102
|
+
path = f"v3/orgs/{self.org_id}/ads/{creative_id}"
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
return self.request(
|
|
106
|
+
method="get",
|
|
107
|
+
path=path,
|
|
108
|
+
)
|
|
109
|
+
except ClientError as e:
|
|
110
|
+
if e.status_code == 404:
|
|
111
|
+
return None
|
|
112
|
+
raise
|
|
113
|
+
|
|
114
|
+
def submit_creative(self, payload: Dict[str, Any]):
|
|
115
|
+
"""Create a new creative.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
payload: The creative data to submit
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
The raw API response data
|
|
122
|
+
|
|
123
|
+
Raises:
|
|
124
|
+
ServerError: On server-side errors or invalid responses
|
|
125
|
+
ClientError: On client-side errors
|
|
126
|
+
"""
|
|
127
|
+
path = f"v3/orgs/{self.org_id}/ads"
|
|
128
|
+
headers = {"Content-Type": "application/json"}
|
|
129
|
+
|
|
130
|
+
return self.request(
|
|
131
|
+
method="post",
|
|
132
|
+
path=path,
|
|
133
|
+
headers=headers,
|
|
134
|
+
data=payload,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
def update_creative(self, creative_id: str, payload: Dict[str, Any]):
|
|
138
|
+
"""Update an existing creative.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
creative_id: The name of the creative to update
|
|
142
|
+
payload: The creative data to update
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
The raw API response data
|
|
146
|
+
|
|
147
|
+
Raises:
|
|
148
|
+
ServerError: On server-side errors or invalid responses
|
|
149
|
+
ClientError: On client-side errors
|
|
150
|
+
"""
|
|
151
|
+
path = f"v3/orgs/{self.org_id}/ads/{creative_id}"
|
|
152
|
+
headers = {"Content-Type": "application/json"}
|
|
153
|
+
|
|
154
|
+
return self.request(
|
|
155
|
+
method="patch",
|
|
156
|
+
path=path,
|
|
157
|
+
headers=headers,
|
|
158
|
+
data=payload,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
def get_creative_adapprovals(self, creative_id: str):
|
|
162
|
+
"""Get approval information for a creative from individual publishers.
|
|
163
|
+
|
|
164
|
+
Calls the adapprovals endpoint to retrieve approval status for a creative
|
|
165
|
+
from each publisher. The response contains a list of objects, one per publisher,
|
|
166
|
+
with information about the approval status.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
creative_id: The ID of the creative to get approvals for
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
The raw API response data containing a list of approval objects.
|
|
173
|
+
Each object contains:
|
|
174
|
+
- owned_by: A string containing the publisher's ID
|
|
175
|
+
- audit: An object with status, feedback, and lastmod fields
|
|
176
|
+
- status: Integer representing approval status (1-5, 500+ for Exchange-specific)
|
|
177
|
+
1 - Pending Audit
|
|
178
|
+
2 - Pre-Approved
|
|
179
|
+
3 - Approved
|
|
180
|
+
4 - Denied
|
|
181
|
+
5 - Changed
|
|
182
|
+
- feedback: Array of strings with explanations for rejection or changes
|
|
183
|
+
- lastmod: Date/time of last modification in ISO 8601 format
|
|
184
|
+
|
|
185
|
+
Raises:
|
|
186
|
+
ServerError: On server-side errors or invalid responses
|
|
187
|
+
ClientError: On client-side errors
|
|
188
|
+
"""
|
|
189
|
+
path = f"v3/orgs/{self.org_id}/ads/{creative_id}/adapprovals"
|
|
190
|
+
params = {"page": self.MAX_PAGE_SIZE}
|
|
191
|
+
|
|
192
|
+
return self.request(
|
|
193
|
+
method="get",
|
|
194
|
+
path=path,
|
|
195
|
+
params=params,
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
def get_publishers(self):
|
|
199
|
+
"""Get publishers data from Place Exchange sellers.json file.
|
|
200
|
+
|
|
201
|
+
Fetches the sellers.json file from Place Exchange website and returns
|
|
202
|
+
the raw JSON response.
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
Dict containing the raw JSON response from the sellers.json endpoint
|
|
206
|
+
|
|
207
|
+
Raises:
|
|
208
|
+
ServerError: On server-side errors or invalid responses
|
|
209
|
+
ClientError: On client-side errors
|
|
210
|
+
"""
|
|
211
|
+
|
|
212
|
+
url = "https://www.placeexchange.com/sellers.json"
|
|
213
|
+
headers = {
|
|
214
|
+
# Pretend to be a modern browser to avoid bot/WAF blocks
|
|
215
|
+
"User-Agent": (
|
|
216
|
+
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 "
|
|
217
|
+
"(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
|
|
218
|
+
),
|
|
219
|
+
"Accept": "application/json, text/plain, */*",
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
try:
|
|
223
|
+
response = requests.get(url, timeout=self.DEFAULT_TIMEOUT, headers=headers)
|
|
224
|
+
response.raise_for_status() # Raise exception for 4XX/5XX responses
|
|
225
|
+
|
|
226
|
+
return response.json()
|
|
227
|
+
|
|
228
|
+
except requests.HTTPError as e:
|
|
229
|
+
logger.error(f"HTTP error fetching publishers from {url}: {str(e)}")
|
|
230
|
+
raise ServerError(
|
|
231
|
+
message=f"Failed to fetch publishers: {response.text if response is not None else str(e)}",
|
|
232
|
+
url=url,
|
|
233
|
+
)
|
|
234
|
+
except requests.RequestException as e:
|
|
235
|
+
logger.error(f"Error fetching publishers from {url}: {str(e)}")
|
|
236
|
+
raise ServerError(message=f"Failed to fetch publishers: {str(e)}", url=url)
|
|
237
|
+
except ValueError as e:
|
|
238
|
+
logger.error(f"Error parsing JSON from {url}: {str(e)}")
|
|
239
|
+
raise ServerError(
|
|
240
|
+
message=f"Failed to parse publishers data: {str(e)}", url=url
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
def get_creatives(
|
|
244
|
+
self,
|
|
245
|
+
lastmod: Optional[str] = None,
|
|
246
|
+
page: Optional[int] = None,
|
|
247
|
+
page_size: Optional[int] = None,
|
|
248
|
+
):
|
|
249
|
+
"""Get a collection of all ads/creatives for the organization.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
lastmod: Date/time filter in ISO format (YYYY-MM-DDTHH:MM:SSZ)
|
|
253
|
+
page: Page number to retrieve
|
|
254
|
+
page_size: Number of entities per page (max 4000)
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
API response data with timestamp, count, and ads array
|
|
258
|
+
|
|
259
|
+
Raises:
|
|
260
|
+
ServerError: On server-side errors or invalid responses
|
|
261
|
+
ClientError: On client-side errors
|
|
262
|
+
"""
|
|
263
|
+
path = f"v3/orgs/{self.org_id}/ads"
|
|
264
|
+
params = {}
|
|
265
|
+
|
|
266
|
+
if lastmod is not None:
|
|
267
|
+
params["lastmod"] = lastmod
|
|
268
|
+
if page is not None:
|
|
269
|
+
params["page"] = page
|
|
270
|
+
if page_size is not None:
|
|
271
|
+
params["page_size"] = page_size
|
|
272
|
+
|
|
273
|
+
return self.request(
|
|
274
|
+
method="get",
|
|
275
|
+
path=path,
|
|
276
|
+
params=params if params else None,
|
|
277
|
+
)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "3.20.0.dev175"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|