trackofferz 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,100 @@
1
+ Metadata-Version: 2.4
2
+ Name: trackofferz
3
+ Version: 0.1.0
4
+ Summary: Official Python SDK for the TrackOfferz reporting API.
5
+ Project-URL: Homepage, https://trackofferz.com
6
+ Project-URL: Documentation, https://trackofferz.com/docs
7
+ Project-URL: Source, https://github.com/trackofferz/trackofferz-python
8
+ Author-email: SPTSPL <support@trackofferz.com>
9
+ License: MIT
10
+ Keywords: affiliate,reporting,sdk,tracking,trackofferz
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Typing :: Typed
15
+ Requires-Python: >=3.9
16
+ Description-Content-Type: text/markdown
17
+
18
+ # TrackOfferz Python SDK
19
+
20
+ Official, zero-dependency Python client for the [TrackOfferz](https://trackofferz.com) reporting API.
21
+
22
+ - **Typed** — dataclass results, ships with `py.typed`.
23
+ - **Zero dependencies** — standard library only.
24
+ - **Resilient** — automatic retries on 429 (honours `Retry-After`) and transient 5xx.
25
+ - **Network-scoped** — every call is limited to your network by the API key.
26
+
27
+ ## Install
28
+
29
+ ```bash
30
+ pip install trackofferz
31
+ ```
32
+
33
+ ## Authenticate
34
+
35
+ Create a key in the panel under **Settings → API keys** (format `tofz_sk_…`).
36
+
37
+ ```python
38
+ from trackofferz import TrackOfferz
39
+
40
+ client = TrackOfferz(api_key="tofz_sk_...")
41
+ ```
42
+
43
+ ## Reporting
44
+
45
+ ```python
46
+ # Summary (defaults to the last 30 days)
47
+ s = client.reports.summary(from_="2026-06-01", to="2026-06-30")
48
+ print(s.clicks, s.conversions, s.revenue_cents)
49
+
50
+ # Breakdown by publisher
51
+ for row in client.reports.breakdown(by="publisher"):
52
+ print(row.label, row.conversions, row.payout_cents)
53
+
54
+ # One page of conversions for a campaign
55
+ page = client.reports.conversions(campaign_id=5, page=1, page_size=100)
56
+ print(page.total, page.has_next)
57
+ for c in page.rows:
58
+ print(c.transaction_id, c.payout_cents, c.status)
59
+
60
+ # Stream EVERY click across all pages (auto-pagination)
61
+ for click in client.reports.iter_clicks(from_="2026-06-01", to="2026-06-30"):
62
+ print(click.transaction_id, click.country_code, click.is_unique)
63
+ ```
64
+
65
+ > Money fields are integers in **cents** (`payout_cents=1250` → `$12.50`).
66
+ > Date params are `YYYY-MM-DD`. `from_` has a trailing underscore (`from` is a Python keyword) and maps to the `from` query parameter.
67
+
68
+ ## Errors
69
+
70
+ ```python
71
+ from trackofferz import (
72
+ AuthenticationError, ValidationError, RateLimitError, APIError, TrackOfferzError,
73
+ )
74
+
75
+ try:
76
+ client.reports.summary(from_="not-a-date")
77
+ except ValidationError as e:
78
+ print("bad input:", e.message)
79
+ except RateLimitError as e:
80
+ print("slow down; retry after", e.retry_after, "s")
81
+ except TrackOfferzError as e: # base class — catches all of the above
82
+ print(e.status, e.code, e.message)
83
+ ```
84
+
85
+ ## Configuration
86
+
87
+ ```python
88
+ TrackOfferz(
89
+ api_key="tofz_sk_...",
90
+ base_url="https://trackofferz.com/api/v1", # override for self-hosted
91
+ timeout=30.0,
92
+ max_retries=2,
93
+ )
94
+ ```
95
+
96
+ Full API reference: <https://trackofferz.com/docs>
97
+
98
+ ---
99
+
100
+ TrackOfferz is a product by [SPTSPL (S. P. Techno Solution Private Limited)](https://www.sptspl.com).
@@ -0,0 +1,83 @@
1
+ # TrackOfferz Python SDK
2
+
3
+ Official, zero-dependency Python client for the [TrackOfferz](https://trackofferz.com) reporting API.
4
+
5
+ - **Typed** — dataclass results, ships with `py.typed`.
6
+ - **Zero dependencies** — standard library only.
7
+ - **Resilient** — automatic retries on 429 (honours `Retry-After`) and transient 5xx.
8
+ - **Network-scoped** — every call is limited to your network by the API key.
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ pip install trackofferz
14
+ ```
15
+
16
+ ## Authenticate
17
+
18
+ Create a key in the panel under **Settings → API keys** (format `tofz_sk_…`).
19
+
20
+ ```python
21
+ from trackofferz import TrackOfferz
22
+
23
+ client = TrackOfferz(api_key="tofz_sk_...")
24
+ ```
25
+
26
+ ## Reporting
27
+
28
+ ```python
29
+ # Summary (defaults to the last 30 days)
30
+ s = client.reports.summary(from_="2026-06-01", to="2026-06-30")
31
+ print(s.clicks, s.conversions, s.revenue_cents)
32
+
33
+ # Breakdown by publisher
34
+ for row in client.reports.breakdown(by="publisher"):
35
+ print(row.label, row.conversions, row.payout_cents)
36
+
37
+ # One page of conversions for a campaign
38
+ page = client.reports.conversions(campaign_id=5, page=1, page_size=100)
39
+ print(page.total, page.has_next)
40
+ for c in page.rows:
41
+ print(c.transaction_id, c.payout_cents, c.status)
42
+
43
+ # Stream EVERY click across all pages (auto-pagination)
44
+ for click in client.reports.iter_clicks(from_="2026-06-01", to="2026-06-30"):
45
+ print(click.transaction_id, click.country_code, click.is_unique)
46
+ ```
47
+
48
+ > Money fields are integers in **cents** (`payout_cents=1250` → `$12.50`).
49
+ > Date params are `YYYY-MM-DD`. `from_` has a trailing underscore (`from` is a Python keyword) and maps to the `from` query parameter.
50
+
51
+ ## Errors
52
+
53
+ ```python
54
+ from trackofferz import (
55
+ AuthenticationError, ValidationError, RateLimitError, APIError, TrackOfferzError,
56
+ )
57
+
58
+ try:
59
+ client.reports.summary(from_="not-a-date")
60
+ except ValidationError as e:
61
+ print("bad input:", e.message)
62
+ except RateLimitError as e:
63
+ print("slow down; retry after", e.retry_after, "s")
64
+ except TrackOfferzError as e: # base class — catches all of the above
65
+ print(e.status, e.code, e.message)
66
+ ```
67
+
68
+ ## Configuration
69
+
70
+ ```python
71
+ TrackOfferz(
72
+ api_key="tofz_sk_...",
73
+ base_url="https://trackofferz.com/api/v1", # override for self-hosted
74
+ timeout=30.0,
75
+ max_retries=2,
76
+ )
77
+ ```
78
+
79
+ Full API reference: <https://trackofferz.com/docs>
80
+
81
+ ---
82
+
83
+ TrackOfferz is a product by [SPTSPL (S. P. Techno Solution Private Limited)](https://www.sptspl.com).
@@ -0,0 +1,35 @@
1
+ """Quickstart for the TrackOfferz Python SDK.
2
+
3
+ export TOFZ_API_KEY=tofz_sk_...
4
+ python examples/quickstart.py
5
+ """
6
+ import os
7
+
8
+ from trackofferz import TrackOfferz, TrackOfferzError
9
+
10
+ API_KEY = os.environ.get("TOFZ_API_KEY", "")
11
+ # Point at a non-production host if needed:
12
+ BASE_URL = os.environ.get("TOFZ_BASE_URL", "https://trackofferz.com/api/v1")
13
+
14
+
15
+ def main() -> None:
16
+ if not API_KEY:
17
+ raise SystemExit("Set TOFZ_API_KEY (Settings → API keys in the panel).")
18
+
19
+ client = TrackOfferz(api_key=API_KEY, base_url=BASE_URL)
20
+
21
+ try:
22
+ s = client.reports.summary()
23
+ print(f"Last 30 days: {s.clicks} clicks, {s.unique_clicks} unique, "
24
+ f"{s.conversions} conversions (CR {s.conversion_rate:.2%})")
25
+ print(f"Revenue: {s.revenue_cents/100:.2f} Payout: {s.payout_cents/100:.2f}")
26
+
27
+ print("\nTop campaigns:")
28
+ for row in client.reports.breakdown(by="campaign")[:10]:
29
+ print(f" {row.label or row.key:<24} {row.clicks:>6} clicks {row.conversions:>4} conv")
30
+ except TrackOfferzError as e:
31
+ raise SystemExit(f"API error [{e.code}]: {e.message}")
32
+
33
+
34
+ if __name__ == "__main__":
35
+ main()
@@ -0,0 +1,29 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "trackofferz"
7
+ version = "0.1.0"
8
+ description = "Official Python SDK for the TrackOfferz reporting API."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "SPTSPL", email = "support@trackofferz.com" }]
13
+ keywords = ["trackofferz", "affiliate", "tracking", "reporting", "sdk"]
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Operating System :: OS Independent",
18
+ "Typing :: Typed",
19
+ ]
20
+ # Zero runtime dependencies — built on the Python standard library only.
21
+ dependencies = []
22
+
23
+ [project.urls]
24
+ Homepage = "https://trackofferz.com"
25
+ Documentation = "https://trackofferz.com/docs"
26
+ Source = "https://github.com/trackofferz/trackofferz-python"
27
+
28
+ [tool.hatch.build.targets.wheel]
29
+ packages = ["trackofferz"]
@@ -0,0 +1,71 @@
1
+ """TrackOfferz — official Python SDK for the reporting API.
2
+
3
+ from trackofferz import TrackOfferz
4
+
5
+ client = TrackOfferz(api_key="tofz_sk_…")
6
+ print(client.reports.summary(from_="2026-06-01", to="2026-06-30"))
7
+
8
+ Create a key in the panel under Settings → API keys. Every call is scoped to
9
+ your network. Docs: https://trackofferz.com/docs
10
+ """
11
+ from __future__ import annotations
12
+
13
+ from ._client import DEFAULT_BASE_URL, HTTPClient, __version__
14
+ from .errors import (
15
+ APIError,
16
+ AuthenticationError,
17
+ RateLimitError,
18
+ TrackOfferzError,
19
+ ValidationError,
20
+ )
21
+ from .reports import Reports
22
+ from .types import (
23
+ BreakdownRow,
24
+ ClickRow,
25
+ ConversionRow,
26
+ DateRange,
27
+ Page,
28
+ Summary,
29
+ )
30
+
31
+ __all__ = [
32
+ "TrackOfferz",
33
+ "Reports",
34
+ "TrackOfferzError",
35
+ "AuthenticationError",
36
+ "ValidationError",
37
+ "RateLimitError",
38
+ "APIError",
39
+ "Summary",
40
+ "ClickRow",
41
+ "ConversionRow",
42
+ "BreakdownRow",
43
+ "DateRange",
44
+ "Page",
45
+ "__version__",
46
+ ]
47
+
48
+
49
+ class TrackOfferz:
50
+ """Top-level client. Holds the API key + transport; exposes namespaces.
51
+
52
+ Args:
53
+ api_key: Network API key (``tofz_sk_…``) from Settings → API keys.
54
+ base_url: Override the API base (default production).
55
+ timeout: Per-request timeout in seconds.
56
+ max_retries: Retries for 429 / transient 5xx (exp backoff, honours
57
+ Retry-After).
58
+ """
59
+
60
+ def __init__(
61
+ self,
62
+ api_key: str,
63
+ *,
64
+ base_url: str = DEFAULT_BASE_URL,
65
+ timeout: float = 30.0,
66
+ max_retries: int = 2,
67
+ ):
68
+ self._http = HTTPClient(
69
+ api_key, base_url=base_url, timeout=timeout, max_retries=max_retries
70
+ )
71
+ self.reports = Reports(self._http)
@@ -0,0 +1,120 @@
1
+ """Low-level HTTP transport. Standard library only (urllib) — no third-party
2
+ deps so the SDK adds nothing to your dependency tree.
3
+ """
4
+ from __future__ import annotations
5
+
6
+ import json
7
+ import time
8
+ import urllib.error
9
+ import urllib.parse
10
+ import urllib.request
11
+ from typing import Any, Dict, Optional
12
+
13
+ from .errors import (
14
+ APIError,
15
+ AuthenticationError,
16
+ RateLimitError,
17
+ TrackOfferzError,
18
+ ValidationError,
19
+ )
20
+
21
+ __version__ = "0.1.0"
22
+ DEFAULT_BASE_URL = "https://trackofferz.com/api/v1"
23
+
24
+
25
+ class HTTPClient:
26
+ def __init__(
27
+ self,
28
+ api_key: str,
29
+ *,
30
+ base_url: str = DEFAULT_BASE_URL,
31
+ timeout: float = 30.0,
32
+ max_retries: int = 2,
33
+ ):
34
+ if not api_key or not api_key.startswith("tofz_"):
35
+ raise ValueError("api_key must be a TrackOfferz key (starts with 'tofz_').")
36
+ self._api_key = api_key
37
+ self._base_url = base_url.rstrip("/")
38
+ self._timeout = timeout
39
+ self._max_retries = max_retries
40
+
41
+ def get(self, path: str, params: Optional[Dict[str, Any]] = None) -> Any:
42
+ """GET a path, returning the full parsed envelope ({data, meta}).
43
+ Retries 429 (honouring Retry-After) and transient 5xx with backoff."""
44
+ query = _encode_params(params or {})
45
+ url = f"{self._base_url}{path}"
46
+ if query:
47
+ url = f"{url}?{query}"
48
+
49
+ attempt = 0
50
+ while True:
51
+ try:
52
+ return self._request(url)
53
+ except RateLimitError as e:
54
+ if attempt >= self._max_retries:
55
+ raise
56
+ _sleep(e.retry_after or _backoff(attempt))
57
+ except APIError as e:
58
+ # Retry transient 5xx only.
59
+ if (e.status or 0) < 500 or attempt >= self._max_retries:
60
+ raise
61
+ _sleep(_backoff(attempt))
62
+ attempt += 1
63
+
64
+ # ── internals ──────────────────────────────────────────────────────────
65
+
66
+ def _request(self, url: str) -> Any:
67
+ req = urllib.request.Request(url, method="GET")
68
+ req.add_header("Authorization", f"Bearer {self._api_key}")
69
+ req.add_header("Accept", "application/json")
70
+ req.add_header("User-Agent", f"trackofferz-python/{__version__}")
71
+ try:
72
+ with urllib.request.urlopen(req, timeout=self._timeout) as resp:
73
+ body = resp.read().decode("utf-8")
74
+ return json.loads(body) if body else {}
75
+ except urllib.error.HTTPError as e:
76
+ self._raise_for_status(e)
77
+ except urllib.error.URLError as e:
78
+ raise TrackOfferzError(f"Network error: {e.reason}") from e
79
+
80
+ def _raise_for_status(self, e: "urllib.error.HTTPError") -> None:
81
+ status = e.code
82
+ code = "http_error"
83
+ message = e.reason or "request failed"
84
+ try:
85
+ err = json.loads(e.read().decode("utf-8")).get("error", {})
86
+ code = err.get("code", code)
87
+ message = err.get("message", message)
88
+ except Exception:
89
+ pass
90
+
91
+ if status == 401:
92
+ raise AuthenticationError(message, status=status, code=code)
93
+ if status == 422:
94
+ raise ValidationError(message, status=status, code=code)
95
+ if status == 429:
96
+ ra = e.headers.get("Retry-After") if e.headers else None
97
+ raise RateLimitError(
98
+ message,
99
+ status=status,
100
+ code=code,
101
+ retry_after=int(ra) if ra and ra.isdigit() else None,
102
+ )
103
+ raise APIError(message, status=status, code=code)
104
+
105
+
106
+ def _encode_params(params: Dict[str, Any]) -> str:
107
+ clean = {k: v for k, v in params.items() if v is not None}
108
+ # bools → lowercase strings the API expects ("true"/"false")
109
+ for k, v in list(clean.items()):
110
+ if isinstance(v, bool):
111
+ clean[k] = "true" if v else "false"
112
+ return urllib.parse.urlencode(clean)
113
+
114
+
115
+ def _backoff(attempt: int) -> float:
116
+ return min(8.0, 0.5 * (2 ** attempt))
117
+
118
+
119
+ def _sleep(seconds: float) -> None:
120
+ time.sleep(max(0.0, seconds))
@@ -0,0 +1,38 @@
1
+ """Typed exceptions raised by the TrackOfferz client.
2
+
3
+ Catch :class:`TrackOfferzError` to handle any SDK error, or the specific
4
+ subclasses for finer control.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ from typing import Optional
9
+
10
+
11
+ class TrackOfferzError(Exception):
12
+ """Base class for every error the SDK raises."""
13
+
14
+ def __init__(self, message: str, *, status: Optional[int] = None, code: Optional[str] = None):
15
+ super().__init__(message)
16
+ self.message = message
17
+ self.status = status
18
+ self.code = code
19
+
20
+
21
+ class AuthenticationError(TrackOfferzError):
22
+ """401 — missing, invalid, or revoked API key."""
23
+
24
+
25
+ class ValidationError(TrackOfferzError):
26
+ """422 — invalid request parameters (bad dates, ids, etc.)."""
27
+
28
+
29
+ class RateLimitError(TrackOfferzError):
30
+ """429 — too many requests. ``retry_after`` is seconds to wait."""
31
+
32
+ def __init__(self, message: str, *, retry_after: Optional[int] = None, **kw):
33
+ super().__init__(message, **kw)
34
+ self.retry_after = retry_after
35
+
36
+
37
+ class APIError(TrackOfferzError):
38
+ """Any other non-2xx response (incl. 5xx)."""
File without changes
@@ -0,0 +1,118 @@
1
+ """The ``reports`` namespace: client.reports.summary(), .clicks(), etc."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Iterator, Optional
5
+
6
+ from ._client import HTTPClient
7
+ from .types import BreakdownRow, ClickRow, ConversionRow, Page, Summary
8
+
9
+
10
+ class Reports:
11
+ def __init__(self, http: HTTPClient):
12
+ self._http = http
13
+
14
+ def summary(
15
+ self,
16
+ *,
17
+ from_: Optional[str] = None,
18
+ to: Optional[str] = None,
19
+ campaign_id: Optional[int] = None,
20
+ publisher_id: Optional[int] = None,
21
+ ) -> Summary:
22
+ """Aggregate totals over a date range (default: last 30 days)."""
23
+ payload = self._http.get(
24
+ "/reports/summary",
25
+ {"from": from_, "to": to, "campaign_id": campaign_id, "publisher_id": publisher_id},
26
+ )
27
+ return Summary._parse(payload.get("data", {}))
28
+
29
+ def clicks(
30
+ self,
31
+ *,
32
+ from_: Optional[str] = None,
33
+ to: Optional[str] = None,
34
+ campaign_id: Optional[int] = None,
35
+ publisher_id: Optional[int] = None,
36
+ page: int = 1,
37
+ page_size: int = 100,
38
+ include_rejected: bool = False,
39
+ ) -> Page:
40
+ """One page of click detail (newest first)."""
41
+ payload = self._http.get(
42
+ "/reports/clicks",
43
+ {
44
+ "from": from_, "to": to,
45
+ "campaign_id": campaign_id, "publisher_id": publisher_id,
46
+ "page": page, "page_size": page_size,
47
+ "include_rejected": include_rejected,
48
+ },
49
+ )
50
+ return _to_page(payload, ClickRow)
51
+
52
+ def conversions(
53
+ self,
54
+ *,
55
+ from_: Optional[str] = None,
56
+ to: Optional[str] = None,
57
+ campaign_id: Optional[int] = None,
58
+ publisher_id: Optional[int] = None,
59
+ page: int = 1,
60
+ page_size: int = 100,
61
+ ) -> Page:
62
+ """One page of conversion detail (newest first, refunds excluded)."""
63
+ payload = self._http.get(
64
+ "/reports/conversions",
65
+ {
66
+ "from": from_, "to": to,
67
+ "campaign_id": campaign_id, "publisher_id": publisher_id,
68
+ "page": page, "page_size": page_size,
69
+ },
70
+ )
71
+ return _to_page(payload, ConversionRow)
72
+
73
+ def breakdown(
74
+ self,
75
+ *,
76
+ by: str = "campaign",
77
+ from_: Optional[str] = None,
78
+ to: Optional[str] = None,
79
+ ) -> list:
80
+ """Performance grouped by 'campaign' | 'publisher' | 'country' | 'sub_id'."""
81
+ payload = self._http.get("/reports/breakdown", {"by": by, "from": from_, "to": to})
82
+ return [BreakdownRow._parse(r) for r in payload.get("data", [])]
83
+
84
+ # ── auto-paginating iterators ────────────────────────────────────────────
85
+
86
+ def iter_clicks(self, *, page_size: int = 1000, **kw) -> Iterator[ClickRow]:
87
+ """Yield every click across all pages (handles pagination for you)."""
88
+ page = 1
89
+ while True:
90
+ p = self.clicks(page=page, page_size=page_size, **kw)
91
+ for row in p.rows:
92
+ yield row
93
+ if not p.has_next:
94
+ break
95
+ page += 1
96
+
97
+ def iter_conversions(self, *, page_size: int = 1000, **kw) -> Iterator[ConversionRow]:
98
+ """Yield every conversion across all pages."""
99
+ page = 1
100
+ while True:
101
+ p = self.conversions(page=page, page_size=page_size, **kw)
102
+ for row in p.rows:
103
+ yield row
104
+ if not p.has_next:
105
+ break
106
+ page += 1
107
+
108
+
109
+ def _to_page(payload: dict, row_cls) -> Page:
110
+ rows = [row_cls._parse(r) for r in payload.get("data", [])]
111
+ meta = payload.get("meta", {}) or {}
112
+ return Page(
113
+ rows=rows,
114
+ page=int(meta.get("page", 1)),
115
+ page_size=int(meta.get("page_size", len(rows))),
116
+ total=int(meta.get("total", len(rows))),
117
+ total_pages=int(meta.get("total_pages", 1)),
118
+ )
@@ -0,0 +1,160 @@
1
+ """Typed result objects. Each parses straight from the API JSON.
2
+
3
+ Money fields are integers in CENTS (e.g. payout_cents=1250 → $12.50), matching
4
+ the API. Convert to your display currency at the edge.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ from dataclasses import dataclass, field
9
+ from typing import Any, Dict, List, Optional
10
+
11
+
12
+ @dataclass
13
+ class DateRange:
14
+ from_: str
15
+ to: str
16
+
17
+ @classmethod
18
+ def _parse(cls, d: Dict[str, Any]) -> "DateRange":
19
+ return cls(from_=d.get("from", ""), to=d.get("to", ""))
20
+
21
+
22
+ @dataclass
23
+ class Summary:
24
+ range: DateRange
25
+ clicks: int
26
+ unique_clicks: int
27
+ conversions: int
28
+ conversion_rate: float
29
+ payout_cents: int
30
+ revenue_cents: int
31
+ sale_amount_cents: int
32
+
33
+ @classmethod
34
+ def _parse(cls, d: Dict[str, Any]) -> "Summary":
35
+ return cls(
36
+ range=DateRange._parse(d.get("range", {})),
37
+ clicks=int(d.get("clicks", 0)),
38
+ unique_clicks=int(d.get("unique_clicks", 0)),
39
+ conversions=int(d.get("conversions", 0)),
40
+ conversion_rate=float(d.get("conversion_rate", 0.0)),
41
+ payout_cents=int(d.get("payout_cents", 0)),
42
+ revenue_cents=int(d.get("revenue_cents", 0)),
43
+ sale_amount_cents=int(d.get("sale_amount_cents", 0)),
44
+ )
45
+
46
+
47
+ @dataclass
48
+ class ClickRow:
49
+ event_time: str
50
+ transaction_id: str
51
+ campaign_id: int
52
+ publisher_id: int
53
+ country_code: str
54
+ region: str
55
+ city: str
56
+ os: str
57
+ browser: str
58
+ device_type: str
59
+ ip_address: str
60
+ asn: int
61
+ isp: str
62
+ sub_ids: List[str] = field(default_factory=list)
63
+ is_unique: bool = True
64
+ fraud_score: int = 0
65
+ rejected: bool = False
66
+ rejection_code: str = ""
67
+
68
+ @classmethod
69
+ def _parse(cls, d: Dict[str, Any]) -> "ClickRow":
70
+ return cls(
71
+ event_time=str(d.get("event_time", "")),
72
+ transaction_id=str(d.get("transaction_id", "")),
73
+ campaign_id=int(d.get("campaign_id", 0)),
74
+ publisher_id=int(d.get("publisher_id", 0)),
75
+ country_code=str(d.get("country_code", "")),
76
+ region=str(d.get("region", "")),
77
+ city=str(d.get("city", "")),
78
+ os=str(d.get("os", "")),
79
+ browser=str(d.get("browser", "")),
80
+ device_type=str(d.get("device_type", "")),
81
+ ip_address=str(d.get("ip_address", "")),
82
+ asn=int(d.get("asn", 0)),
83
+ isp=str(d.get("isp", "")),
84
+ sub_ids=list(d.get("sub_ids", []) or []),
85
+ is_unique=bool(d.get("is_unique", True)),
86
+ fraud_score=int(d.get("fraud_score", 0)),
87
+ rejected=bool(d.get("rejected", False)),
88
+ rejection_code=str(d.get("rejection_code", "")),
89
+ )
90
+
91
+
92
+ @dataclass
93
+ class ConversionRow:
94
+ event_time: str
95
+ transaction_id: str
96
+ campaign_id: int
97
+ publisher_id: int
98
+ event_type: str
99
+ status: str
100
+ payout_cents: int
101
+ sale_amount_cents: int
102
+ advertiser_cost_cents: int
103
+ currency: str
104
+ country_code: str
105
+ ctit_seconds: int
106
+
107
+ @classmethod
108
+ def _parse(cls, d: Dict[str, Any]) -> "ConversionRow":
109
+ return cls(
110
+ event_time=str(d.get("event_time", "")),
111
+ transaction_id=str(d.get("transaction_id", "")),
112
+ campaign_id=int(d.get("campaign_id", 0)),
113
+ publisher_id=int(d.get("publisher_id", 0)),
114
+ event_type=str(d.get("event_type", "")),
115
+ status=str(d.get("status", "")),
116
+ payout_cents=int(d.get("payout_cents", 0)),
117
+ sale_amount_cents=int(d.get("sale_amount_cents", 0)),
118
+ advertiser_cost_cents=int(d.get("advertiser_cost_cents", 0)),
119
+ currency=str(d.get("currency", "")),
120
+ country_code=str(d.get("country_code", "")),
121
+ ctit_seconds=int(d.get("ctit_seconds", 0)),
122
+ )
123
+
124
+
125
+ @dataclass
126
+ class BreakdownRow:
127
+ key: str
128
+ label: Optional[str]
129
+ clicks: int
130
+ unique_clicks: int
131
+ conversions: int
132
+ payout_cents: int
133
+ revenue_cents: int
134
+
135
+ @classmethod
136
+ def _parse(cls, d: Dict[str, Any]) -> "BreakdownRow":
137
+ return cls(
138
+ key=str(d.get("key", "")),
139
+ label=d.get("label"),
140
+ clicks=int(d.get("clicks", 0)),
141
+ unique_clicks=int(d.get("unique_clicks", 0)),
142
+ conversions=int(d.get("conversions", 0)),
143
+ payout_cents=int(d.get("payout_cents", 0)),
144
+ revenue_cents=int(d.get("revenue_cents", 0)),
145
+ )
146
+
147
+
148
+ @dataclass
149
+ class Page:
150
+ """One page of detail rows + pagination cursor."""
151
+
152
+ rows: List[Any]
153
+ page: int
154
+ page_size: int
155
+ total: int
156
+ total_pages: int
157
+
158
+ @property
159
+ def has_next(self) -> bool:
160
+ return self.page < self.total_pages