Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-24.04]
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
python-version: ["3.10", "3.11", "3.12", "3.13"]
env:
PYTHONDONTWRITEBYTECODE: 1

Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# v15.0.0
- [breaking change] dropped support for python 3.9 (which is end-of-life), please use python 3.10+

# v14.0.0
- [breaking change] dropped support for python 3.8 (which is end-of-life), please use python 3.9+
- added support for python 3.12 and 3.13
Expand Down
397 changes: 170 additions & 227 deletions poetry.lock

Large diffs are not rendered by default.

17 changes: 8 additions & 9 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "rtbhouse-sdk"
version = "14.2.1"
version = "15.0.0"
description = "RTB House SDK"
authors = ["RTB House Apps Team <apps@rtbhouse.com>"]
license = "BSD License"
Expand All @@ -12,7 +12,6 @@ classifiers = [
"License :: OSI Approved :: BSD License",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
Expand All @@ -25,7 +24,7 @@ name = "PyPI"
priority = "primary"

[tool.poetry.dependencies]
python = ">=3.9, <4.0"
python = ">=3.10, <4.0"

httpx = "^0.28.0"
pydantic = ">=1.9, <3.0"
Expand All @@ -35,17 +34,17 @@ pydantic = "^2.0.0" # required for tests

black = "^25.0.0"
flake8 = "^7.0.0"
isort = "^6.0.0"
isort = "^7.0.0"
mypy = "^1.0"
pylint = "^3.0.0"
pytest = "^8.0.0"
pylint = "^4.0.0"
pytest = "^9.0.0"
pytest-asyncio = "^1.0.0"
pytest-cov = "^7.0.0"
respx = "^0.22.0"

[tool.black]
line-length = 120
target-version = ["py39", "py310", "py311", "py312", "py313"]
target-version = ["py310", "py311", "py312", "py313"]

[tool.coverage.run]
branch = true
Expand All @@ -59,7 +58,7 @@ line_length = 120
profile = "black"

[tool.mypy]
python_version = "3.9"
python_version = "3.10"
strict = true
plugins = ["pydantic.mypy"]

Expand All @@ -70,7 +69,7 @@ warn_required_dynamic_aliases = false
warn_untyped_fields = true

[tool.pylint.main]
py-version = "3.9"
py-version = "3.10"
load-plugins = """
pylint.extensions.check_elif,
pylint.extensions.confusing_elif,
Expand Down
2 changes: 1 addition & 1 deletion rtbhouse_sdk/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""RTB House Python SDK."""

__version__ = "14.0.0"
__version__ = "15.0.0"
68 changes: 34 additions & 34 deletions rtbhouse_sdk/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from datetime import date, timedelta
from json import JSONDecodeError
from types import TracebackType
from typing import Any, Optional, Union
from typing import Any

import httpx

Expand Down Expand Up @@ -61,7 +61,7 @@ class Client:

def __init__(
self,
auth: Union[BasicAuth, BasicTokenAuth],
auth: BasicAuth | BasicTokenAuth,
timeout: timedelta = DEFAULT_TIMEOUT,
):
self._httpx_client = httpx.Client(
Expand All @@ -86,7 +86,7 @@ def __exit__(
) -> None:
self._httpx_client.__exit__(exc_type, exc_value, traceback)

def _get(self, path: str, params: Optional[dict[str, Any]] = None) -> Any:
def _get(self, path: str, params: dict[str, Any] | None = None) -> Any:
response = self._httpx_client.get(path, params=params)
_validate_response(response)
try:
Expand All @@ -95,13 +95,13 @@ def _get(self, path: str, params: Optional[dict[str, Any]] = None) -> Any:
except (ValueError, KeyError) as exc:
raise ApiException("Invalid response format") from exc

def _get_dict(self, path: str, params: Optional[dict[str, Any]] = None) -> dict[str, Any]:
def _get_dict(self, path: str, params: dict[str, Any] | None = None) -> dict[str, Any]:
data = self._get(path, params)
if not isinstance(data, dict):
raise ValueError("Result is not a dict")
return data

def _get_list_of_dicts(self, path: str, params: Optional[dict[str, Any]] = None) -> list[dict[str, Any]]:
def _get_list_of_dicts(self, path: str, params: dict[str, Any] | None = None) -> list[dict[str, Any]]:
data = self._get(path, params)
if not isinstance(data, list) or not all(isinstance(item, dict) for item in data):
raise ValueError("Result is not a list of dicts")
Expand Down Expand Up @@ -161,8 +161,8 @@ def get_billing(
def get_rtb_creatives(
self,
adv_hash: str,
subcampaigns: Union[None, list[str], schema.SubcampaignsFilter] = None,
active_only: Optional[bool] = None,
subcampaigns: list[str] | schema.SubcampaignsFilter | None = None,
active_only: bool | None = None,
) -> list[schema.Creative]:
params = _build_rtb_creatives_params(subcampaigns, active_only)
data = self._get_list_of_dicts(f"/advertisers/{adv_hash}/rtb-creatives", params=params)
Expand Down Expand Up @@ -193,11 +193,11 @@ def get_rtb_stats(
day_to: date,
group_by: list[schema.StatsGroupBy],
metrics: list[schema.StatsMetric],
count_convention: Optional[schema.CountConvention] = None,
count_convention: schema.CountConvention | None = None,
utc_offset_hours: int = 0,
subcampaigns: Optional[list[str]] = None,
user_segments: Optional[list[schema.UserSegment]] = None,
device_types: Optional[list[schema.DeviceType]] = None,
subcampaigns: list[str] | None = None,
user_segments: list[schema.UserSegment] | None = None,
device_types: list[schema.DeviceType] | None = None,
) -> list[schema.Stats]:
params = _build_rtb_stats_params(
day_from,
Expand All @@ -221,9 +221,9 @@ def get_summary_stats(
day_to: date,
group_by: list[schema.StatsGroupBy],
metrics: list[schema.StatsMetric],
count_convention: Optional[schema.CountConvention] = None,
count_convention: schema.CountConvention | None = None,
utc_offset_hours: int = 0,
subcampaigns: Optional[list[str]] = None,
subcampaigns: list[str] | None = None,
) -> list[schema.Stats]:
params = _build_summary_stats_params(
day_from, day_to, group_by, metrics, count_convention, utc_offset_hours, subcampaigns
Expand All @@ -247,7 +247,7 @@ class AsyncClient:

def __init__(
self,
auth: Union[BasicAuth, BasicTokenAuth],
auth: BasicAuth | BasicTokenAuth,
timeout: timedelta = DEFAULT_TIMEOUT,
) -> None:
self._httpx_client = httpx.AsyncClient(
Expand All @@ -272,7 +272,7 @@ async def __aexit__(
) -> None:
await self._httpx_client.__aexit__(exc_type, exc_value, traceback)

async def _get(self, path: str, params: Optional[dict[str, Any]] = None) -> Any:
async def _get(self, path: str, params: dict[str, Any] | None = None) -> Any:
response = await self._httpx_client.get(path, params=params)
_validate_response(response)
try:
Expand All @@ -281,13 +281,13 @@ async def _get(self, path: str, params: Optional[dict[str, Any]] = None) -> Any:
except (ValueError, KeyError) as exc:
raise ApiException("Invalid response format") from exc

async def _get_dict(self, path: str, params: Optional[dict[str, Any]] = None) -> dict[str, Any]:
async def _get_dict(self, path: str, params: dict[str, Any] | None = None) -> dict[str, Any]:
data = await self._get(path, params)
if not isinstance(data, dict):
raise ValueError("Result is not a dict")
return data

async def _get_list_of_dicts(self, path: str, params: Optional[dict[str, Any]] = None) -> list[dict[str, Any]]:
async def _get_list_of_dicts(self, path: str, params: dict[str, Any] | None = None) -> list[dict[str, Any]]:
data = await self._get(path, params)
if not isinstance(data, list) or not all(isinstance(item, dict) for item in data):
raise ValueError("Result is not of a list of dicts")
Expand Down Expand Up @@ -350,8 +350,8 @@ async def get_billing(
async def get_rtb_creatives(
self,
adv_hash: str,
subcampaigns: Union[None, list[str], schema.SubcampaignsFilter] = None,
active_only: Optional[bool] = None,
subcampaigns: list[str] | schema.SubcampaignsFilter | None = None,
active_only: bool | None = None,
) -> list[schema.Creative]:
params = _build_rtb_creatives_params(subcampaigns, active_only)
data = await self._get_list_of_dicts(f"/advertisers/{adv_hash}/rtb-creatives", params=params)
Expand Down Expand Up @@ -382,11 +382,11 @@ async def get_rtb_stats(
day_to: date,
group_by: list[schema.StatsGroupBy],
metrics: list[schema.StatsMetric],
count_convention: Optional[schema.CountConvention] = None,
count_convention: schema.CountConvention | None = None,
utc_offset_hours: int = 0,
subcampaigns: Optional[list[str]] = None,
user_segments: Optional[list[schema.UserSegment]] = None,
device_types: Optional[list[schema.DeviceType]] = None,
subcampaigns: list[str] | None = None,
user_segments: list[schema.UserSegment] | None = None,
device_types: list[schema.DeviceType] | None = None,
) -> list[schema.Stats]:
params = _build_rtb_stats_params(
day_from,
Expand All @@ -410,9 +410,9 @@ async def get_summary_stats(
day_to: date,
group_by: list[schema.StatsGroupBy],
metrics: list[schema.StatsMetric],
count_convention: Optional[schema.CountConvention] = None,
count_convention: schema.CountConvention | None = None,
utc_offset_hours: int = 0,
subcampaigns: Optional[list[str]] = None,
subcampaigns: list[str] | None = None,
) -> list[schema.Stats]:
params = _build_summary_stats_params(
day_from, day_to, group_by, metrics, count_convention, utc_offset_hours, subcampaigns
Expand Down Expand Up @@ -443,7 +443,7 @@ def _build_headers() -> dict[str, str]:
}


def _choose_auth_backend(auth: Union[BasicAuth, BasicTokenAuth]) -> httpx.Auth:
def _choose_auth_backend(auth: BasicAuth | BasicTokenAuth) -> httpx.Auth:
if isinstance(auth, BasicAuth):
return httpx.BasicAuth(auth.username, auth.password)
if isinstance(auth, BasicTokenAuth):
Expand Down Expand Up @@ -492,8 +492,8 @@ def _validate_response(response: httpx.Response) -> None:


def _build_rtb_creatives_params(
subcampaigns: Union[None, list[str], schema.SubcampaignsFilter] = None,
active_only: Optional[bool] = None,
subcampaigns: list[str] | schema.SubcampaignsFilter | None = None,
active_only: bool | None = None,
) -> dict[str, Any]:
params: dict[str, Any] = {}
if subcampaigns:
Expand All @@ -512,11 +512,11 @@ def _build_rtb_stats_params(
day_to: date,
group_by: list[schema.StatsGroupBy],
metrics: list[schema.StatsMetric],
count_convention: Optional[schema.CountConvention] = None,
count_convention: schema.CountConvention | None = None,
utc_offset_hours: int = 0,
subcampaigns: Optional[list[str]] = None,
user_segments: Optional[list[schema.UserSegment]] = None,
device_types: Optional[list[schema.DeviceType]] = None,
subcampaigns: list[str] | None = None,
user_segments: list[schema.UserSegment] | None = None,
device_types: list[schema.DeviceType] | None = None,
) -> dict[str, Any]:
params: dict[str, Any] = {
"dayFrom": day_from,
Expand All @@ -543,9 +543,9 @@ def _build_summary_stats_params(
day_to: date,
group_by: list[schema.StatsGroupBy],
metrics: list[schema.StatsMetric],
count_convention: Optional[schema.CountConvention] = None,
count_convention: schema.CountConvention | None = None,
utc_offset_hours: int = 0,
subcampaigns: Optional[list[str]] = None,
subcampaigns: list[str] | None = None,
) -> dict[str, Any]:
params: dict[str, Any] = {
"dayFrom": day_from,
Expand Down
14 changes: 7 additions & 7 deletions rtbhouse_sdk/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
"""Definitions of exceptions used in SDK."""

import dataclasses
from typing import Any, Optional
from typing import Any


@dataclasses.dataclass
class ErrorDetails:
app_code: str
message: str
errors: Optional[dict[str, Any]]
errors: dict[str, Any] | None


class ApiException(Exception):
"""Base API Exception."""

message: str
error_details: Optional[ErrorDetails]
error_details: ErrorDetails | None

def __init__(self, message: str, details: Optional[ErrorDetails] = None) -> None:
def __init__(self, message: str, details: ErrorDetails | None = None) -> None:
super().__init__(message)
self.message = message
self.error_details = details
Expand All @@ -42,14 +42,14 @@ class ApiRateLimitException(ApiRequestException):
def __init__(
self,
message: str,
details: Optional[ErrorDetails],
usage_header: Optional[str],
details: ErrorDetails | None,
usage_header: str | None,
) -> None:
super().__init__(message, details)
self.limits = _parse_resource_usage_header(usage_header)


def _parse_resource_usage_header(header: Optional[str]) -> dict[str, dict[str, dict[str, float]]]:
def _parse_resource_usage_header(header: str | None) -> dict[str, dict[str, dict[str, float]]]:
"""parse string like WORKER_TIME-3600=11.7/10000000;BQ_TB_BILLED-21600=4.62/2000 into dict"""
if not header:
return {}
Expand Down
Loading