Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .speakeasy/gen.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ management:
docVersion: v0.1
speakeasyVersion: 1.794.0
generationVersion: 2.930.0
releaseVersion: 0.3.1
configChecksum: 412022fe57a148beb9a811f85df27ac1
releaseVersion: 0.3.2
configChecksum: a2f185276005c068df58d66545924402
repoURL: https://github.com/thetradedesk/ttd-data-python.git
installationURL: https://github.com/thetradedesk/ttd-data-python.git
published: true
Expand Down
2 changes: 1 addition & 1 deletion .speakeasy/gen.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ generation:
generateNewTests: false
skipResponseBodyAssertions: false
python:
version: 0.3.1
version: 0.3.2
additionalDependencies:
dev: {}
main:
Expand Down
50 changes: 50 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,56 @@ Each segment is a `SegmentInput`. `providerId` and `providerElementId` are alway

The supergraph reports authorization and policy failures as HTTP 200 with a top-level `errors` array, so those raise `GraphQLError` — which, like `APIError`, derives from `DataError`, so one `except DataError` covers the REST and GraphQL suites. Retries use the same configuration as the REST operations.

### 10. Third-Party Data Rates (GraphQL)

Rate operations run as GraphQL requests against the Platform API supergraph. They live under `client.third_party_data_rate`.

| SDK function | REST equivalent | What it does |
| --- | --- | --- |
| `query_brands(...)` | [`GET /v3/datarate/brands/{providerId}`](https://open.thetradedesk.com/rest/openttd/provider/content/docs/GuidesProvider/audience/ref/get-datarate-brands-providerid) | Lists a provider's third-party data brands. |
| `query_segment_data_rates(...)` | [`POST /v3/datarate/query`](https://open.thetradedesk.com/rest/openttd/provider/content/docs/GuidesProvider/audience/ref/post-datarate-query) | Lists data rates for a provider's segments, optionally filtered by segment or brand. |
| `query_data_rate_batches(...)` | [`GET /v3/datarate/batch/{batchId}`](https://open.thetradedesk.com/rest/openttd/provider/content/docs/GuidesProvider/audience/ref/get-datarate-batch-batchid) | Lists data rate batches for a provider, optionally filtered to one batch. |
| `create_data_rate_batch(...)` | [`POST /v3/datarate/batch`](https://open.thetradedesk.com/rest/openttd/provider/content/docs/GuidesProvider/audience/ref/post-datarate-batch) | Submits a batch of data rate creates for a provider. |
| `client.graphql.execute(...)` | — | Sends any GraphQL document and returns the parsed response body. |

```python
from ttd_data import DataClient

client = DataClient(ttd_auth=TTD_AUTH_TOKEN)
rates = client.third_party_data_rate

# List brands. Paginate with page.end_cursor while page.has_next_page.
page = rates.query_brands(provider_id=PROVIDER_ID, first=10)
for node in page.nodes:
print(node["id"], node["name"])

# List data rates for a provider's segments, optionally filtered by brand.
page = rates.query_segment_data_rates(provider_id=PROVIDER_ID, brand_id="brand-1")

# List/filter data rate batches.
page = rates.query_data_rate_batches(provider_id=PROVIDER_ID, batch_id="0006A7D")

# Submit a batch of data rate creates. Queued, not applied immediately.
result = rates.create_data_rate_batch(
provider_id=PROVIDER_ID,
data_rates=[
{
"providerElementId": "auto/in-market/ev",
"thirdPartyDataBrandId": "brand-1",
"cost": {"cpm": {"cpmCost": {"amount": 2.5, "currencyCode": "USD"}}},
}
],
)
if result.errors:
print(result.errors) # the batch was rejected outright
else:
print(result.data["id"], result.data["processingStatus"])
```

Each rate is a `DataRateInput`. `subject` sets exactly one of `partner`/`advertiser` (omit for a system rate); `cost` sets exactly one of `cpm`/`revShare`/`hybrid`.

`errors` being non-empty means the batch was rejected outright; poll `query_data_rate_batches` with the returned batch ID to follow processing and approval.

<!-- No SDK Example Usage [usage] -->

<!-- Start Available Resources and Operations [operations] -->
Expand Down
107 changes: 107 additions & 0 deletions examples/graphql_rates_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
"""Example: third-party data rate GraphQL operations via ttd-data-python's
DataClient.

TTD_AUTH_TOKEN=... required. Platform token, sent as `TTD-Auth`.
GRAPHQL_EXAMPLE_PROVIDER_ID=... required. Provider to operate on.
GRAPHQL_EXAMPLE_BRAND_ID=... optional. Brand to filter rates by.
GRAPHQL_EXAMPLE_ELEMENT_ID=... optional. Segment to price.

Reads run unconditionally. The batch submission at the end is a write, and is
skipped unless GRAPHQL_EXAMPLE_ELEMENT_ID and GRAPHQL_EXAMPLE_BRAND_ID are
both set. The submitter is taken from the token's email.

TTD_AUTH_TOKEN=... GRAPHQL_EXAMPLE_PROVIDER_ID=... \
python examples/graphql_rates_example.py
"""

import json
import os

from ttd_data import DataClient
from ttd_data.graphql import Page


def required(name: str, description: str) -> str:
value = os.getenv(name, "").strip()
if not value:
raise SystemExit(f"Set {name} to {description}.")
return value


token = required("TTD_AUTH_TOKEN", "a platform token")
PROVIDER_ID = required("GRAPHQL_EXAMPLE_PROVIDER_ID", "the provider to operate on")

BRAND_ID = os.getenv("GRAPHQL_EXAMPLE_BRAND_ID", "").strip()
ELEMENT_ID = os.getenv("GRAPHQL_EXAMPLE_ELEMENT_ID", "").strip()

client = DataClient(ttd_auth=token)
rates = client.third_party_data_rate


def show(label: str, page: Page) -> None:
print(f"\n{'=' * 60}\n {label} ({page.total_count} total)\n{'=' * 60}")
print(json.dumps(page.nodes, indent=2))


# ---------------------------------------------------------------------------
# Reads
# ---------------------------------------------------------------------------

show(
"Brands for provider",
rates.query_brands(provider_id=PROVIDER_ID, first=10),
)

show(
"Data rates for provider segments",
rates.query_segment_data_rates(provider_id=PROVIDER_ID, first=5),
)

if not BRAND_ID:
print("\nSkipping brand filter (set GRAPHQL_EXAMPLE_BRAND_ID to run it).")
else:
show(
f"Data rates filtered to brand {BRAND_ID}",
rates.query_segment_data_rates(
provider_id=PROVIDER_ID, brand_id=BRAND_ID, first=5
),
)

show(
"Data rate batches",
rates.query_data_rate_batches(provider_id=PROVIDER_ID, first=10),
)

# ---------------------------------------------------------------------------
# Mutation — this submits a rate batch against the provider
# ---------------------------------------------------------------------------

if not (ELEMENT_ID and BRAND_ID):
print(
"\nSkipping batch submission (needs GRAPHQL_EXAMPLE_ELEMENT_ID "
"and GRAPHQL_EXAMPLE_BRAND_ID)."
)
else:
# Omitting `subject` makes this a system (syndicated) rate.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we include how to set subject in the example too?

result = rates.create_data_rate_batch(
provider_id=PROVIDER_ID,
data_rates=[
{
"providerElementId": ELEMENT_ID,
"thirdPartyDataBrandId": BRAND_ID,
"cost": {"cpm": {"cpmCost": {"amount": 2.5, "currencyCode": "USD"}}},
}
],
)
print(f"\n{'=' * 60}\n Submit data rate batch\n{'=' * 60}")
if result.data is None:
print(f"rejected: {json.dumps(result.errors, indent=2)}")
else:
print(json.dumps(result.data, indent=2))
# Queued, not applied: read the batch back to follow its processing.
show(
"Submitted batch",
rates.query_data_rate_batches(
provider_id=PROVIDER_ID, batch_id=result.data["id"]
),
)
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
it, and upserts it — which creates or updates it in the provider's taxonomy.

TTD_AUTH_TOKEN=... GRAPHQL_EXAMPLE_PROVIDER_ID=... \
python examples/graphql_example.py
python examples/graphql_taxonomy_example.py
"""

import json
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "ttd-data"
version = "0.3.1"
version = "0.3.2"
description = "Python Client SDK for TTD Data API."
authors = [{ name = "Speakeasy" },]
readme = "README-PYPI.md"
Expand Down
4 changes: 2 additions & 2 deletions src/ttd_data/_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
import importlib.metadata

__title__: str = "ttd-data"
__version__: str = "0.3.1"
__version__: str = "0.3.2"
__openapi_doc_version__: str = "v0.1"
__gen_version__: str = "2.930.0"
__user_agent__: str = "speakeasy-sdk/python 0.3.1 2.930.0 v0.1 ttd-data"
__user_agent__: str = "speakeasy-sdk/python 0.3.2 2.930.0 v0.1 ttd-data"

try:
if __package__ is not None:
Expand Down
8 changes: 7 additions & 1 deletion src/ttd_data/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

from uid2_client import IdentityMapV3Client, IdentityMapV3Input # type: ignore[import-not-found,import-untyped]

from ttd_data.graphql import GraphQLTransport, TaxonomyOperations
from ttd_data.graphql import DataRateOperations, GraphQLTransport, TaxonomyOperations
from ttd_data.sdk import BaseDataClient
from ttd_data.types import BaseModel, OptionalNullable
from ttd_data.utils import RetryConfig
Expand Down Expand Up @@ -347,6 +347,12 @@ def third_party_taxonomy(self) -> TaxonomyOperations:
never on user identifiers."""
return TaxonomyOperations(self._graphql_transport)

@cached_property
def third_party_data_rate(self) -> DataRateOperations:
"""Third-party data rate operations over GraphQL: brand and rate
queries, plus data rate batch submission and lookup."""
return DataRateOperations(self._graphql_transport)

@cached_property
def graphql(self) -> GraphQLTransport:
"""Escape hatch for GraphQL operations the typed namespaces above do
Expand Down
14 changes: 12 additions & 2 deletions src/ttd_data/graphql/__init__.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
from ttd_data.graphql._response import GraphQLError, Page, UpsertResult
from ttd_data.graphql._response import (
GraphQLError,
MutationResult,
Page,
UpsertResult,
)
from ttd_data.graphql._transport import GraphQLTransport
from ttd_data.graphql.rates import QUERY_DOCUMENTS as _RATES_DOCUMENTS
from ttd_data.graphql.rates import DataRateInput, DataRateOperations
from ttd_data.graphql.taxonomy import QUERY_DOCUMENTS as _TAXONOMY_DOCUMENTS
from ttd_data.graphql.taxonomy import SegmentInput, TaxonomyOperations

# Every document the typed methods send, keyed by method name. The schema
# validator reads this so it can never drift from what callers actually send.
QUERY_DOCUMENTS = dict(_TAXONOMY_DOCUMENTS)
QUERY_DOCUMENTS = {**_TAXONOMY_DOCUMENTS, **_RATES_DOCUMENTS}

__all__ = [
"DataRateInput",
"DataRateOperations",
"GraphQLError",
"GraphQLTransport",
"MutationResult",
"Page",
"SegmentInput",
"TaxonomyOperations",
Expand Down
24 changes: 24 additions & 0 deletions src/ttd_data/graphql/_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,20 @@ class UpsertResult:
raw: Dict[str, Any] = field(default_factory=dict)


@dataclass(frozen=True)
class MutationResult:
"""Outcome of a mutation that returns one entity alongside field errors.

`data` is None when the mutation was rejected outright; `errors` is
non-empty in that case. Both can be set when the server accepted the
operation but flagged something about it.
"""

data: Optional[Dict[str, Any]] = None
errors: List[Dict[str, Any]] = field(default_factory=list)
raw: Dict[str, Any] = field(default_factory=dict)


def _resolve(raw: Dict[str, Any], *path: str) -> Dict[str, Any]:
"""Walk `path` under `data`, yielding `{}` at the first missing or null link."""
node: Any = raw.get("data") or {}
Expand Down Expand Up @@ -98,3 +112,13 @@ def build_upsert_result(raw: Dict[str, Any], *path: str) -> UpsertResult:
failed=payload.get("errors") or [],
raw=raw,
)


def build_mutation_result(raw: Dict[str, Any], *path: str) -> MutationResult:
"""Unwrap a single-entity mutation payload at `path` under `data`."""
payload = _resolve(raw, *path)
return MutationResult(
data=payload.get("data") or None,
errors=payload.get("errors") or [],
raw=raw,
)
Loading
Loading