Repository navigation
Expand file tree
/
Copy pathexceptions.py
More file actions
218 lines (166 loc) · 16.1 KB
/
Copy pathexceptions.py
File metadata and controls
218 lines (166 loc) · 16.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
"""
GoAdServer Public API
Public REST API for advertisers and publishers on the GoAdServer platform. **Authentication.** All endpoints (except `/api/public/*`) require an API key passed as `Authorization: Bearer gas_live_…`. Create and manage keys in your account settings — each key has its own scopes, optional resource pinning, optional IP allowlist, and per-key rate limit. **Apps (OAuth 2.0).** A tool other people connect uses an app token instead: authorization code + PKCE at `/oauth/authorize`, tokens from `/api/public/oauth/token`, passed as `Authorization: Bearer oat_…` on the same endpoints with the same scopes. See the [Build an integration](/api/docs/integrations) guide. **System rules apply over API rules.** The public API never lets a key bypass restrictions the same user faces in the panel: - **Source names** (`stats_source_names` right). When the user lacks this right, advertiser stats responses return `{id, name}` with `name` equal to the id — no human label leaks through. - **SubID encoding** (`modules.global.subid_encoding`). When enabled for the system, the user only ever sees encoded SubIDs in the panel. The API mirrors this both ways: `subid_excl`/`subid_incl` filter endpoints accept the encoded form on write (decoded server-side before storage) and emit the encoded form on read. A plain SubID sent on write is stored as sent — a value is decoded only when it is a genuine encoded token. - **Module gating.** Endpoints whose module is disabled for the tenant don't appear in the spec or routes.txt at all. **Rate limits.** Every response to an API key or app token carries the caller's budget for the current one-minute window: | Header | Meaning | |---|---| | `X-RateLimit-Limit` | Requests per minute for this key (its `rate_limit_rpm`, default 60; an app's per-installation limit for app tokens). | | `X-RateLimit-Remaining` | Calls left in the current window. | | `X-RateLimit-Reset` | Unix seconds at which the window resets. | | `Retry-After` | On `429 rate_limited` only: seconds until a retry can succeed. | Windows are fixed and start with the first call, so `Reset` is exact. The MCP endpoint (`POST /api/v1/mcp`) is charged per `tools/call` against its own bucket (default 240/min), and its response headers report that bucket; an exhausted MCP bucket answers the tool call with an error result rather than an HTTP 429, and the HTTP response then carries `Retry-After`. **Idempotent writes.** Send `Idempotency-Key: <1–255 printable ASCII characters>` (a UUID is ideal) on a `POST` or `PATCH` to make a retry safe. For 24 hours per key (per app installation for app tokens): - the first request runs and its response — status, body, `Content-Type`, `Location`, `ETag` — is stored; - the same key with the same method, path, query and body replays that response without running anything, adding `Idempotent-Replayed: true`; - the same key with a different request is `422 idempotency_key_reused`; - the same key while the first request is still running is `409 idempotency_key_in_progress` (with `Retry-After: 1`). `5xx` results are not stored — the key is released and a retry runs again. Bodies over 32 MB cannot carry the header (`413`). An invalid key is `400 invalid_idempotency_key`. **Concurrency control (ETag / If-Match).** Every JSON `GET` response (up to 1 MB) carries a weak `ETag`, a hash of the body. Send it back: - `If-None-Match: <etag>` on a `GET` — `304 Not Modified` when unchanged; - `If-Match: <etag>` on a `PATCH`, `PUT` or `DELETE` — the server reads the resource's current representation (the `GET` of the same path, or of the nearest parent path that has a `GET`: `PATCH …/campaigns/ron/7/active` is checked against `GET …/campaigns/ron/7`) and answers `412 precondition_failed` when it no longer matches, with the current tag in `ETag` and `current_etag`. `If-Match: *` only requires the resource to exist. A resource with no readable representation is also `412`. The check is optimistic, not a lock: it closes the read-modify-write window two agents (or an agent and a person in the panel) would otherwise race in. Fields that change on their own (live spend counters) change the tag too — re-read and retry. A write response carries no new ETag; `GET` again. **Strict fields.** Unknown body fields are a client error, not something to ignore: a request that sends a field the endpoint does not accept gets `422 unknown_fields` naming it. This is the default for keys created from 2026-10-12 (the key's `strict_fields` setting, shown on the key screen); older keys keep ignoring unknown fields (except on the few endpoints that have always refused them — the budget and on/off toggles). Override per request with `?strict=1` (reject) or `?strict=0` (ignore, everywhere). App tokens default to ignoring unknown fields; `?strict=1` opts in. **Errors.** Every non-2xx response has the same shape: ```json { \"error\": \"validation_error\", \"message\": \"Name is required.\", \"errors\": { \"name\": [\"Name is required.\"] } } ``` `error` is a stable machine-readable code — branch on it, never on `message`. `errors` (field → messages) appears on field-level validation failures; some codes add their own members (`scope`, `accepted_scopes`, `unknown_fields`, `current_etag`). Codes: | Status | Codes | |---|---| | 400 | `bad_request`, `invalid_idempotency_key`, `invalid_body` | | 401 | `unauthenticated`, `invalid_api_key`, `revoked_api_key`, `expired_api_key`, `invalid_token`, `revoked_token`, `expired_token`, `user_not_found`, `account_deleted`, `account_disabled`, `admin_key_on_user_api`, `user_key_on_admin_api` | | 403 | `forbidden`, `missing_scope`, `forbidden_resource`, `ip_not_allowed`, `api_disabled`, `systemid_mismatch`, `endpoint_not_api_accessible`, `oauth_token_wrong_surface`, `mcp_not_enabled_for_app` | | 404 | `not_found` (also a disabled module or missing right — see ModuleDisabled) | | 405 | `method_not_allowed` — the `Allow` header (and the message) lists the methods the path does accept | | 409 | `conflict`, `idempotency_key_in_progress`, and resource-specific conflicts (e.g. `email_exists`) | | 412 | `precondition_failed` | | 413 | `payload_too_large` | | 422 | `validation_error`, `unknown_fields`, `idempotency_key_reused` | | 429 | `rate_limited` | | 5xx | `server_error`, `service_unavailable`, `bad_gateway`, `gateway_timeout` | Endpoint-specific codes are listed on the endpoint. A response that would otherwise lack a code gets the generic one for its status from this table. **Client SDKs.** Each SDK is generated from this spec and published as a public GitHub repository under the `goadserver` organization. The same tarball is also embedded in the API server and downloadable from `/api/public/sdk/<lang>.tgz` — useful when GitHub is unreachable. > **Per-network host.** The default `https://up.go-adserver.com` > baked into the published SDKs is a reference host. Every white-label > tenant runs on its own panel domain — override the host on first use > (`setHost()` / `Configuration.host` / `basePath` etc.). Or, fetch the > spec from your own tenant (`/api/public/openapi.yaml` returns it with > *your* hostname as the default) and regenerate the SDK locally — the > regenerated client has your URL baked in. - **PHP** — `github.com/goadserver/goadserver-php` (namespace `GoAdServer\\Api`, composer name `goadserver/sdk`). ```bash composer config repositories.goadserver vcs https://github.com/goadserver/goadserver-php composer require goadserver/sdk:dev-main ``` ```php $cfg = GoAdServer\\Api\\Configuration::getDefaultConfiguration() ->setHost('https://up.go-adserver.com') ->setAccessToken('gas_live_…'); $api = new GoAdServer\\Api\\Api\\AccountAPIApi(null, $cfg); $me = $api->apiV1AccountGet(); ``` - **Go** — `github.com/goadserver/goadserver-go`. ```bash go get github.com/goadserver/goadserver-go@latest ``` ```go import gas \"github.com/goadserver/goadserver-go\" cfg := gas.NewConfiguration() cfg.Servers = gas.ServerConfigurations{{URL: \"https://up.go-adserver.com\"}} cli := gas.NewAPIClient(cfg) ctx := context.WithValue(context.Background(), gas.ContextAccessToken, \"gas_live_…\") me, _, err := cli.AccountAPI.ApiV1AccountGet(ctx).Execute() ``` - **Python** — `github.com/goadserver/goadserver-python` (package `goadserver`). Pulls `pydantic` + `urllib3` automatically. ```bash pip install git+https://github.com/goadserver/goadserver-python.git ``` ```python from goadserver import ApiClient, Configuration from goadserver.api import AccountApi, CampaignsApi cfg = Configuration(host=\"https://up.go-adserver.com\") cfg.access_token = \"gas_live_…\" client = ApiClient(cfg) me = AccountApi(client).api_v1_account_get() camps = CampaignsApi(client).api_v1_campaigns_get(per_page=10) ``` - **JavaScript / TypeScript** — `github.com/goadserver/goadserver-js` (npm name `goadserver-sdk`, generator `typescript-fetch`). ```bash npm install github:goadserver/goadserver-js ``` ```ts import { Configuration, AccountApi, CampaignsApi } from \"goadserver-sdk\"; const cfg = new Configuration({ basePath: \"https://up.go-adserver.com\", accessToken: \"gas_live_…\", }); const me = await new AccountApi(cfg).apiV1AccountGet(); const list = await new CampaignsApi(cfg).apiV1CampaignsGet({ perPage: 10 }); ``` The CJS build is broadly compatible (Node + bundlers); the ESM build is missing trailing `.js` extensions in its imports, a known `typescript-fetch` quirk — most consumers use CJS. **Offline / firewalled install.** If the customer's network can't reach GitHub, every SDK's source is also served as a tarball from the API host: ```bash curl -O https://up.go-adserver.com/api/public/sdk/python.tgz tar xzf python.tgz && pip install ./python ``` Round-trip smoke tests for all four SDKs live at `clients/_smoke/` in the platform repo.
The version of the OpenAPI document: 1.0.0
Generated by OpenAPI Generator (https://openapi-generator.tech)
Do not edit the class manually.
""" # noqa: E501
from typing import Any, Optional
from typing_extensions import Self
class OpenApiException(Exception):
"""The base exception class for all OpenAPIExceptions"""
class ApiTypeError(OpenApiException, TypeError):
def __init__(self, msg, path_to_item=None, valid_classes=None,
key_type=None) -> None:
""" Raises an exception for TypeErrors
Args:
msg (str): the exception message
Keyword Args:
path_to_item (list): a list of keys an indices to get to the
current_item
None if unset
valid_classes (tuple): the primitive classes that current item
should be an instance of
None if unset
key_type (bool): False if our value is a value in a dict
True if it is a key in a dict
False if our item is an item in a list
None if unset
"""
self.path_to_item = path_to_item
self.valid_classes = valid_classes
self.key_type = key_type
full_msg = msg
if path_to_item:
full_msg = "{0} at {1}".format(msg, render_path(path_to_item))
super(ApiTypeError, self).__init__(full_msg)
class ApiValueError(OpenApiException, ValueError):
def __init__(self, msg, path_to_item=None) -> None:
"""
Args:
msg (str): the exception message
Keyword Args:
path_to_item (list) the path to the exception in the
received_data dict. None if unset
"""
self.path_to_item = path_to_item
full_msg = msg
if path_to_item:
full_msg = "{0} at {1}".format(msg, render_path(path_to_item))
super(ApiValueError, self).__init__(full_msg)
class ApiAttributeError(OpenApiException, AttributeError):
def __init__(self, msg, path_to_item=None) -> None:
"""
Raised when an attribute reference or assignment fails.
Args:
msg (str): the exception message
Keyword Args:
path_to_item (None/list) the path to the exception in the
received_data dict
"""
self.path_to_item = path_to_item
full_msg = msg
if path_to_item:
full_msg = "{0} at {1}".format(msg, render_path(path_to_item))
super(ApiAttributeError, self).__init__(full_msg)
class ApiKeyError(OpenApiException, KeyError):
def __init__(self, msg, path_to_item=None) -> None:
"""
Args:
msg (str): the exception message
Keyword Args:
path_to_item (None/list) the path to the exception in the
received_data dict
"""
self.path_to_item = path_to_item
full_msg = msg
if path_to_item:
full_msg = "{0} at {1}".format(msg, render_path(path_to_item))
super(ApiKeyError, self).__init__(full_msg)
class ApiException(OpenApiException):
def __init__(
self,
status=None,
reason=None,
http_resp=None,
*,
body: Optional[str] = None,
data: Optional[Any] = None,
) -> None:
self.status = status
self.reason = reason
self.body = body
self.data = data
self.headers = None
if http_resp:
if self.status is None:
self.status = http_resp.status
if self.reason is None:
self.reason = http_resp.reason
if self.body is None:
try:
self.body = http_resp.data.decode('utf-8')
except Exception:
pass
self.headers = http_resp.headers
@classmethod
def from_response(
cls,
*,
http_resp,
body: Optional[str],
data: Optional[Any],
) -> Self:
if http_resp.status == 400:
raise BadRequestException(http_resp=http_resp, body=body, data=data)
if http_resp.status == 401:
raise UnauthorizedException(http_resp=http_resp, body=body, data=data)
if http_resp.status == 403:
raise ForbiddenException(http_resp=http_resp, body=body, data=data)
if http_resp.status == 404:
raise NotFoundException(http_resp=http_resp, body=body, data=data)
# Added new conditions for 409 and 422
if http_resp.status == 409:
raise ConflictException(http_resp=http_resp, body=body, data=data)
if http_resp.status == 422:
raise UnprocessableEntityException(http_resp=http_resp, body=body, data=data)
if 500 <= http_resp.status <= 599:
raise ServiceException(http_resp=http_resp, body=body, data=data)
raise ApiException(http_resp=http_resp, body=body, data=data)
def __str__(self):
"""Custom error messages for exception"""
error_message = "({0})\n"\
"Reason: {1}\n".format(self.status, self.reason)
if self.headers:
error_message += "HTTP response headers: {0}\n".format(
self.headers)
if self.body:
error_message += "HTTP response body: {0}\n".format(self.body)
if self.data:
error_message += "HTTP response data: {0}\n".format(self.data)
return error_message
class BadRequestException(ApiException):
pass
class NotFoundException(ApiException):
pass
class UnauthorizedException(ApiException):
pass
class ForbiddenException(ApiException):
pass
class ServiceException(ApiException):
pass
class ConflictException(ApiException):
"""Exception for HTTP 409 Conflict."""
pass
class UnprocessableEntityException(ApiException):
"""Exception for HTTP 422 Unprocessable Entity."""
pass
def render_path(path_to_item):
"""Returns a string representation of a path"""
result = ""
for pth in path_to_item:
if isinstance(pth, int):
result += "[{0}]".format(pth)
else:
result += "['{0}']".format(pth)
return result