From e6d09b6fe70c5fba3f3160e8e6ed57004cd19c29 Mon Sep 17 00:00:00 2001 From: Andrei Markin Date: Thu, 24 Jul 2025 11:45:31 +0400 Subject: [PATCH] [dv360] initial commit --- libs/garf_community/google/dv360/README.md | 65 +++++++++++++++++++ .../google/dv360/garf_dv360_api/__init__.py | 29 +++++++++ .../dv360/garf_dv360_api/api_clients.py | 54 +++++++++++++++ .../google/dv360/garf_dv360_api/exceptions.py | 19 ++++++ .../dv360/garf_dv360_api/query_editor.py | 30 +++++++++ .../dv360/garf_dv360_api/report_fetcher.py | 32 +++++++++ .../google/dv360/pyproject.toml | 37 +++++++++++ 7 files changed, 266 insertions(+) create mode 100644 libs/garf_community/google/dv360/README.md create mode 100644 libs/garf_community/google/dv360/garf_dv360_api/__init__.py create mode 100644 libs/garf_community/google/dv360/garf_dv360_api/api_clients.py create mode 100644 libs/garf_community/google/dv360/garf_dv360_api/exceptions.py create mode 100644 libs/garf_community/google/dv360/garf_dv360_api/query_editor.py create mode 100644 libs/garf_community/google/dv360/garf_dv360_api/report_fetcher.py create mode 100644 libs/garf_community/google/dv360/pyproject.toml diff --git a/libs/garf_community/google/dv360/README.md b/libs/garf_community/google/dv360/README.md new file mode 100644 index 0000000..87048b0 --- /dev/null +++ b/libs/garf_community/google/dv360/README.md @@ -0,0 +1,65 @@ +# `garf` for Display & Video 360 API + +[![PyPI](https://img.shields.io/pypi/v/garf-dv360-api?logo=pypi&logoColor=white&style=flat-square)](https://pypi.org/project/garf-dv360-api) +[![Downloads PyPI](https://img.shields.io/pypi/dw/garf-dv360-api?logo=pypi)](https://pypi.org/project/garf-dv360-api/) + +`garf-dv360-api` simplifies fetching data from Merchant API using SQL-like queries. + +## Prerequisites + +* [Merchant API](https://console.cloud.google.com/apis/library/dv360api.googleapis.com) enabled. + +## Installation + +`pip install garf-dv360-api` + +## Usage + +### Run as a library +``` +from garf_dv360_api import report_fetcher +from garf_io import writer + + +# Specify query +query = """ + SELECT + date, + clicks + FROM product_performance_view + WHERE date BETWEEN '2023-12-01' AND '2023-12-03' + ORDER BY clicks DESC +"" + +# Fetch report +fetched_report = ( + report_fetcher.DV360ApiReportFetcher() + .fetch(query, query=", account=ACCOUNT_ID) +) + +# Write report to console +console_writer = writer.create_writer('console') +console_writer.write(fetched_report, 'output') +``` + +### Run via CLI + +> Install `garf-executors` package to run queries via CLI (`pip install garf-executors`). + +``` +garf --source dv360-api \ + --output \ + --source. +``` + +where: + +* `` - local or remove files containing queries +* `` - output supported by [`garf-io` library](../garf_io/README.md). +* ` None: + """Initializes DV360Client.""" + self.api_version = api_version + self.query_args = kwargs + + @override + def get_response( + self, request: query_editor.BaseQueryElements, **kwargs: str + ) -> api_clients.GarfApiResponse: + if not (account := kwargs.get('account')): + raise DV360ApiError('Missing account parameter') + client = dv360_reports_v1beta.ReportServiceClient() + dv360_request = dv360_reports_v1beta.SearchRequest( + parent=f'accounts/{account}', + query=request.text, + ) + response = client.search(request=dv360_request) + results = [] + for page in response: + for rows in page.get('results'): + for _, row in rows.items(): + results.append(row) + return api_clients.GarfApiResponse(results=results) diff --git a/libs/garf_community/google/dv360/garf_dv360_api/exceptions.py b/libs/garf_community/google/dv360/garf_dv360_api/exceptions.py new file mode 100644 index 0000000..5e1190f --- /dev/null +++ b/libs/garf_community/google/dv360/garf_dv360_api/exceptions.py @@ -0,0 +1,19 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from garf_core import exceptions + + +class GarfDV360ApiError(exceptions.GarfError): + """Base class for all library exceptions.""" diff --git a/libs/garf_community/google/dv360/garf_dv360_api/query_editor.py b/libs/garf_community/google/dv360/garf_dv360_api/query_editor.py new file mode 100644 index 0000000..8372135 --- /dev/null +++ b/libs/garf_community/google/dv360/garf_dv360_api/query_editor.py @@ -0,0 +1,30 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Defines DV360Query.""" + +from garf_core import query_editor + + +class DV360ApiQuery(query_editor.QuerySpecification): + """Query to Display & Video 360 API.""" + + def __init__( + self, + text: str, + title: str | None = None, + args: dict[str, str] | None = None, + **kwargs, + ) -> None: + """Initializes DV360ApiQuery.""" + super().__init__(text, title, args, **kwargs) diff --git a/libs/garf_community/google/dv360/garf_dv360_api/report_fetcher.py b/libs/garf_community/google/dv360/garf_dv360_api/report_fetcher.py new file mode 100644 index 0000000..6a84131 --- /dev/null +++ b/libs/garf_community/google/dv360/garf_dv360_api/report_fetcher.py @@ -0,0 +1,32 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Defines report fetcher.""" + +from garf_core import parsers, report_fetcher +from garf_dv360_api import DV360ApiClient, query_editor + + +class DV360ApiReportFetcher(report_fetcher.ApiReportFetcher): + """Defines report fetcher.""" + + def __init__( + self, + api_client: DV360ApiClient = DV360ApiClient(), + parser: parsers.BaseParser = parsers.NumericConverterDictParser, + query_spec: query_editor.DV360ApiQuery = query_editor.DV360ApiQuery, + **kwargs: str, + ) -> None: + """Initializes DV360ApiReportFetcher.""" + super().__init__(api_client, parser, query_spec, **kwargs) diff --git a/libs/garf_community/google/dv360/pyproject.toml b/libs/garf_community/google/dv360/pyproject.toml new file mode 100644 index 0000000..e2f3563 --- /dev/null +++ b/libs/garf_community/google/dv360/pyproject.toml @@ -0,0 +1,37 @@ +[build-system] +requires = ["setuptools >= 61.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "garf-dv360-api" +dependencies = [ + "garf-core", + "garf-io", + "google-shopping-dv360-reports", +] +authors = [ + {name = "Google Inc. (gTech gPS CSE team)", email = "no-reply@google.com"}, +] +license = {text = "Apache 2.0"} +requires-python = ">=3.8" +description = "Garf implementation for Display & Video 360 API" +readme = "README.md" +classifiers = [ + "Development Status :: 3 - Alpha", + "Programming Language :: Python" +] + +dynamic=["version"] + +[tool.setuptools.dynamic] +version = {attr = "garf_dv360_api.__version__"} + +[project.entry-points.garf] +dv360-api = "garf_dv360_api.report_fetcher" + +[options.extras_require] +test = [ + "pytest", + "pytest-cov", + "python-dotenv", +]