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
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
name: Run e2e tests for garf-youtube-data-api
name: Run tests for garf-youtube

on:
workflow_dispatch:
pull_request:
branches: [main]
paths:
- 'libs/community/google/youtube/youtube-data-api/**'
- 'libs/community/google/youtube/**'

env:
UV_SYSTEM_PYTHON: 1
Expand All @@ -26,14 +26,24 @@ jobs:
enable-cache: true
- name: Install dependencies
run: |
uv pip install pytest python-dotenv
- name: Test ${{ matrix.library }}
- name: Run unit tests
run: |
uv pip install -e libs/core/.[all]
uv pip install -e libs/io/.[test,all]
uv pip install -e libs/executors/.[all]
cd libs/community/google/youtube/youtube-data-api/
uv pip install -e .
cd libs/community/google/youtube/
uv pip install -e .[test]
pytest tests/unit
env:
GARF_YOUTUBE_DATA_API_KEY: ${{ secrets.GARF_YOUTUBE_DATA_API_KEY }}
YOUTUBE_ID: ${{ secrets.YOUTUBE_ID }}
- name: Run end-to-end tests
run: |
uv pip install -e libs/core/.[all]
uv pip install -e libs/io/.[test,all]
uv pip install -e libs/executors/.[all]
cd libs/community/google/youtube/
uv pip install -e .[test]
pytest tests/e2e
env:
GARF_YOUTUBE_DATA_API_KEY: ${{ secrets.GARF_YOUTUBE_DATA_API_KEY }}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,39 @@

## Prerequisites

### YouTube Data API

* [YouTube Data API](https://console.cloud.google.com/apis/library/youtube.googleapis.com) enabled.
* [API key](https://support.google.com/googleapi/answer/6158862?hl=en) to access to access YouTube Data API.
> Once generated expose API key as `export GARF_YOUTUBE_DATA_API_KEY=<YOUR_API_KEY>`

### YouTube Reporting API
* [YouTube Reporting API](https://console.cloud.google.com/apis/library/youtubereporting.googleapis.com) enabled.
* [Client ID, client secret](https://support.google.com/cloud/answer/6158849?hl=en) and refresh token generated. \
> Please note you'll need to use another OAuth2 credentials type - *Web application*, and set "https://developers.google.com/oauthplayground" as redirect url in it.
* Refresh token. You can use [OAuth Playground](https://developers.google.com/oauthplayground/) to generate refresh token.
* Select `https://www.googleapis.com/auth/yt-analytics.readonly` scope
* Enter OAuth Client ID and OAuth Client secret under *Use your own OAuth credentials*;
* Click on *Authorize APIs*

* Expose client id, client secret and refresh token as environmental variables:

```
export GARF_YOUTUBE_REPORTING_API_CLIENT_ID=
export GARF_YOUTUBE_REPORTING_API_CLIENT_SECRET=
export GARF_YOUTUBE_REPORTING_API_REFRESH_TOKEN=
```

## Installation

`pip install garf-youtube-data-api`
`pip install garf-youtube`

## Usage

### Run as a library
```
from garf_youtube_data_api import report_fetcher
from garf_io import writer
from garf.community.google.youtube import report_fetcher
from garf.io import writer


# Specify query
Expand All @@ -37,7 +56,7 @@ console_writer = writer.create_writer('console')
console_writer.write(fetched_report, 'output')
```

Learn [more](https://google.github.io/garf/fetchers/youtube-data-api/#python) on library usage.
Learn [more](https://google.github.io/garf/fetchers/youtube/#python) on library usage.

### Run via CLI

Expand All @@ -53,13 +72,13 @@ where:

* `<PATH_TO_QUERIES>` - local or remove files containing queries
* `<OUTPUT_TYPE>` - output supported by [`garf-io` library](https://google.github.io/garf/usage/writers/).
* `<SOURCE_PARAMETER=VALUE` - key-value pairs to refine fetching, check [available source parameters](https://google.github.io/garf/fetchers/youtube-data-api/#available-source-parameters).
* `<SOURCE_PARAMETER=VALUE` - key-value pairs to refine fetching, check [available source parameters](https://google.github.io/garf/fetchers/youtube/#available-source-parameters).

Learn [more](https://google.github.io/garf/fetchers/youtube-data-api/#cli) on CLI usage.
Learn [more](https://google.github.io/garf/fetchers/youtube/#cli) on CLI usage.

## Documentation

You can find a documentation on `garf-youtube-data-api` [here](https://google.github.io/garf/fetchers/youtube-data-api/).
You can find a documentation on `garf-youtube` [here](https://google.github.io/garf/fetchers/youtube/).

## Samples

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Example queries for `garf-youtube-data-api`
# Example queries for `garf-youtube`

Contains sample queries for everyone's reference.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@ SELECT
privacy_status,
definition,
caption
FROM builtin.channelVideos
FROM builtin.channelVideos
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright 2024 Google LLC
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand All @@ -12,16 +12,24 @@
# See the License for the specific language governing permissions and
# limitations under the License.

"""Interacts with YouTube Data API via garf."""
"""Interacts with YouTube APIs via garf."""

from __future__ import annotations

from garf_youtube_data_api.api_clients import YouTubeDataApiClient
from garf_youtube_data_api.report_fetcher import YouTubeDataApiReportFetcher
from garf.community.google.youtube.api_clients import (
YouTubeAnalyticsApiClient,
YouTubeDataApiClient,
)
from garf.community.google.youtube.report_fetcher import (
YouTubeAnalyticsApiReportFetcher,
YouTubeDataApiReportFetcher,
)

__all__ = [
'YouTubeDataApiClient',
'YouTubeDataApiReportFetcher',
'YouTubeAnalyticsApiClient',
'YouTubeAnalyticsApiReportFetcher',
]

__version__ = '0.0.13'
__version__ = '1.0.0'
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright 2024 Google LLC
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand All @@ -19,18 +19,19 @@
import operator
import os
import warnings
from collections import defaultdict
from typing import Any

import dateutil
import pydantic
from garf_core import api_clients, query_editor
from garf.community.google.youtube import exceptions, telemetry
from garf.core import api_clients, query_editor
from google.oauth2.credentials import Credentials
from googleapiclient.discovery import build
from googleapiclient.errors import HttpError
from opentelemetry import trace
from typing_extensions import override

from garf_youtube_data_api import exceptions, telemetry

logging.getLogger('googleapiclient.discovery_cache').setLevel(logging.ERROR)


Expand Down Expand Up @@ -200,6 +201,99 @@ def _list(
return {'items': None}


class YouTubeAnalyticsApiClientError(exceptions.GarfYouTubeAnalyticsApiError):
"""API client specific exception."""


class YouTubeAnalyticsApiClient(api_clients.BaseClient):
"""Responsible for for getting data from YouTube Analytics API."""

def __init__(self, api_version: str = 'v2') -> None:
"""Initializes YouTubeAnalyticsApiClient."""
if (
not os.getenv('GARF_YOUTUBE_REPORTING_API_REFRESH_TOKEN')
or not os.getenv('GARF_YOUTUBE_REPORTING_API_CLIENT_ID')
or not os.getenv('GARF_YOUTUBE_REPORTING_API_CLIENT_SECRET')
):
raise YouTubeAnalyticsApiClientError(
'YouTubeAnalyticsApiClient requests all ENV variables to be set up: '
'GARF_YOUTUBE_REPORTING_API_REFRESH_TOKEN, '
'GARF_YOUTUBE_REPORTING_API_CLIENT_ID, '
'GARF_YOUTUBE_REPORTING_API_CLIENT_SECRET'
)
self.api_version = api_version
self._credentials = None
self._service = None

@property
def credentials(self) -> Credentials:
"""OAuth2.0 credentials to access API."""
if self._credentials:
return self._credentials
return Credentials(
None,
refresh_token=os.getenv('GARF_YOUTUBE_REPORTING_API_REFRESH_TOKEN'),
token_uri='https://oauth2.googleapis.com/token',
client_id=os.getenv('GARF_YOUTUBE_REPORTING_API_CLIENT_ID'),
client_secret=os.getenv('GARF_YOUTUBE_REPORTING_API_CLIENT_SECRET'),
)

@property
def service(self):
"""Services for accessing YouTube Analytics API."""
if self._service:
return self._service
return build(
'youtubeAnalytics', self.api_version, credentials=self.credentials
)

@override
def get_response(
self, request: query_editor.BaseQueryElements, **kwargs: str
) -> api_clients.GarfApiResponse:
metrics = []
dimensions = []
filters = []
for field in request.fields:
if field.startswith('metrics'):
metrics.append(field.replace('metrics.', ''))
elif field.startswith('dimensions'):
dimensions.append(field.replace('dimensions.', ''))
for filter_statement in request.filters:
if filter_statement.startswith('channel'):
ids = filter_statement
elif filter_statement.startswith('startDate'):
start_date = filter_statement.split('=')
elif filter_statement.startswith('endDate'):
end_date = filter_statement.split('=')
else:
filters.append(filter_statement)
result = (
self.service.reports()
.query(
dimensions=','.join(dimensions),
metrics=','.join(metrics),
filters=';'.join(filters),
ids=ids,
startDate=start_date[1].strip(),
endDate=end_date[1].strip(),
alt='json',
)
.execute()
)
results = []
for row in result.get('rows'):
response_row: dict[str, dict[str, str]] = defaultdict(dict)
for position, header in enumerate(result.get('columnHeaders')):
header_name = header.get('name')
if header.get('columnType') == 'DIMENSION':
response_row['dimensions'].update({header_name: row[position]})
elif header.get('columnType') == 'METRIC':
response_row['metrics'].update({header_name: row[position]})
results.append(response_row)
return api_clients.GarfApiResponse(results=results)


class Comparator(pydantic.BaseModel):
field: str
operator: str
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright 2025 Google LLC
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand All @@ -12,8 +12,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.

"""Defines common library exceptions."""
from garf.community.google.youtube.builtins import channel_videos


class GarfYouTubeReportingApiError(Exception):
"""Base class for all library exceptions."""
BUILTIN_QUERIES = {
'channelVideos': channel_videos.get_youtube_channel_videos,
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright 2025 Google LLC
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand All @@ -15,7 +15,7 @@


def get_youtube_channel_videos(
report_fetcher: 'garf_youtube_data_api.YouTubeDataApiReportFetcher',
report_fetcher: 'garf.community.google.youtube.report_fetcher.YouTubeDataApiReportFetcher',
**kwargs: str,
):
channel_uploads_playlist_query = """
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright 2025 Google LLC
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand All @@ -11,9 +11,18 @@
# 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 common library exceptions."""

from garf.core import exceptions


class GarfYouTubeDataApiError(Exception):
class GarfYouTubeApiError(exceptions.GarfError):
"""Base class for all library exceptions."""


class GarfYouTubeDataApiError(GarfYouTubeApiError):
"""YouTube Data API error."""


class GarfYouTubeAnalyticsApiError(GarfYouTubeApiError):
"""YouTube Analytics API error."""
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright 2024 Google LLC
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand All @@ -11,10 +11,15 @@
# 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 YouTubeDataApiQuery."""

from garf_core import query_editor
"""Defines YouTube specific queries."""

from garf.core import query_editor


class YouTubeDataApiQuery(query_editor.QuerySpecification):
"""Query to youtube data api."""
"""Query to YouTube Data API."""


class YouTubeAnalyticsApiQuery(query_editor.QuerySpecification):
"""Query to YouTube Analytics Api."""
Loading