diff --git a/CHANGELOG.md b/CHANGELOG.md index aca8edb4..28ed9303 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to this project will be documented in this file. + +## Version 0.9.4 (development) + +### What's New + +- Add new parameter `for_handle` to get channel by handle. + + ## Version 0.9.3 (2023-11-22) ### What's New diff --git a/pyyoutube/api.py b/pyyoutube/api.py index 252af52c..37fada2d 100644 --- a/pyyoutube/api.py +++ b/pyyoutube/api.py @@ -725,6 +725,7 @@ def get_channel_info( self, *, channel_id: Optional[Union[str, list, tuple, set]] = None, + for_handle: Optional[str] = None, for_username: Optional[str] = None, mine: Optional[bool] = None, parts: Optional[Union[str, list, tuple, set]] = None, @@ -744,6 +745,11 @@ def get_channel_info( channel_id ((str,list,tuple,set), optional): The id or comma-separated id string for youtube channel which you want to get. You can also pass this with an id list, tuple, set. + for_handle (str, optional): + The parameter specifies a YouTube handle, thereby requesting the channel associated with that handle. + The parameter value can be prepended with an @ symbol. For example, to retrieve the resource for + the "Google for Developers" channel, set the forHandle parameter value to + either GoogleDevelopers or @GoogleDevelopers. for_username (str, optional): The name for YouTube username which you want to get. Note: This name may the old youtube version's channel's user's username, Not the the channel name. @@ -768,7 +774,9 @@ def get_channel_info( "part": enf_parts(resource="channels", value=parts), "hl": hl, } - if for_username is not None: + if for_handle is not None: + args["forHandle"] = for_handle + elif for_username is not None: args["forUsername"] = for_username elif channel_id is not None: args["id"] = enf_comma_separated("channel_id", channel_id) diff --git a/pyyoutube/client.py b/pyyoutube/client.py index aa7006c9..bdbdfa10 100644 --- a/pyyoutube/client.py +++ b/pyyoutube/client.py @@ -1,6 +1,7 @@ """ New Client for YouTube API """ + import inspect import json from typing import List, Optional, Tuple, Union diff --git a/pyyoutube/media.py b/pyyoutube/media.py index e8493b8e..02e04a11 100644 --- a/pyyoutube/media.py +++ b/pyyoutube/media.py @@ -1,6 +1,7 @@ """ Media object to upload. """ + import mimetypes import os from typing import IO, Optional, Tuple @@ -180,9 +181,9 @@ def next_chunk(self) -> Tuple[Optional[MediaUploadProgress], Optional[dict]]: # sending "bytes 0--1/0" results in an invalid request # Only add header "Content-Range" if chunk_end != -1 if chunk_end != -1: - headers[ - "Content-Range" - ] = f"bytes {self.resumable_progress}-{chunk_end}/{size}" + headers["Content-Range"] = ( + f"bytes {self.resumable_progress}-{chunk_end}/{size}" + ) resp = self.client.request( path=self.resumable_uri, diff --git a/pyyoutube/models/category.py b/pyyoutube/models/category.py index 0d8395f0..7260e3db 100644 --- a/pyyoutube/models/category.py +++ b/pyyoutube/models/category.py @@ -2,6 +2,7 @@ These are category related models. Include VideoCategory """ + from dataclasses import dataclass, field from typing import List, Optional diff --git a/pyyoutube/models/channel.py b/pyyoutube/models/channel.py index 2b4a7d6e..4986602e 100644 --- a/pyyoutube/models/channel.py +++ b/pyyoutube/models/channel.py @@ -3,6 +3,7 @@ References: https://developers.google.com/youtube/v3/docs/channels#properties """ + from dataclasses import dataclass, field from typing import List, Optional diff --git a/pyyoutube/models/channel_section.py b/pyyoutube/models/channel_section.py index cfbbbb41..73d4c9ed 100644 --- a/pyyoutube/models/channel_section.py +++ b/pyyoutube/models/channel_section.py @@ -61,5 +61,4 @@ class ChannelSectionResponse(BaseApiResponse): @dataclass -class ChannelSectionListResponse(ChannelSectionResponse): - ... +class ChannelSectionListResponse(ChannelSectionResponse): ... diff --git a/pyyoutube/models/comment_thread.py b/pyyoutube/models/comment_thread.py index baab3f28..eed6ffc8 100644 --- a/pyyoutube/models/comment_thread.py +++ b/pyyoutube/models/comment_thread.py @@ -1,6 +1,7 @@ """ These are comment threads related models. """ + from dataclasses import dataclass, field from typing import Optional, List diff --git a/pyyoutube/models/common.py b/pyyoutube/models/common.py index 7c81c362..6e554f14 100644 --- a/pyyoutube/models/common.py +++ b/pyyoutube/models/common.py @@ -1,6 +1,7 @@ """ These are common models for multi resource. """ + from dataclasses import dataclass, field from typing import Optional, List diff --git a/pyyoutube/models/mixins.py b/pyyoutube/models/mixins.py index fa92b067..24e463fe 100644 --- a/pyyoutube/models/mixins.py +++ b/pyyoutube/models/mixins.py @@ -1,6 +1,7 @@ """ These are some mixin for models """ + import datetime from typing import Optional diff --git a/pyyoutube/models/playlist.py b/pyyoutube/models/playlist.py index c8cb3c58..a1fa680b 100644 --- a/pyyoutube/models/playlist.py +++ b/pyyoutube/models/playlist.py @@ -1,6 +1,7 @@ """ These are playlist related models. """ + from dataclasses import dataclass, field from typing import Optional, List diff --git a/pyyoutube/models/search_result.py b/pyyoutube/models/search_result.py index d3d92308..c8b242d3 100644 --- a/pyyoutube/models/search_result.py +++ b/pyyoutube/models/search_result.py @@ -1,6 +1,7 @@ """ These are search result related models. """ + from dataclasses import dataclass, field from typing import Optional, List diff --git a/pyyoutube/models/video.py b/pyyoutube/models/video.py index ba39b7b6..19477162 100644 --- a/pyyoutube/models/video.py +++ b/pyyoutube/models/video.py @@ -1,6 +1,7 @@ """ These are video related models. """ + from dataclasses import dataclass, field from typing import Optional, List diff --git a/pyyoutube/resources/activities.py b/pyyoutube/resources/activities.py index 9885957b..57538f4d 100644 --- a/pyyoutube/resources/activities.py +++ b/pyyoutube/resources/activities.py @@ -1,6 +1,7 @@ """ Activities resource implementation """ + from typing import Optional, Union from pyyoutube.error import PyYouTubeException, ErrorMessage, ErrorCode diff --git a/pyyoutube/resources/base_resource.py b/pyyoutube/resources/base_resource.py index 8359688b..48230a18 100644 --- a/pyyoutube/resources/base_resource.py +++ b/pyyoutube/resources/base_resource.py @@ -1,6 +1,7 @@ """ Base resource class. """ + from typing import Optional, TYPE_CHECKING if TYPE_CHECKING: diff --git a/pyyoutube/resources/captions.py b/pyyoutube/resources/captions.py index c1bb2569..cebf11ea 100644 --- a/pyyoutube/resources/captions.py +++ b/pyyoutube/resources/captions.py @@ -1,6 +1,7 @@ """ Captions resource implementation """ + from typing import Optional, Union from requests import Response diff --git a/pyyoutube/resources/channel_banners.py b/pyyoutube/resources/channel_banners.py index eb5d6dc1..4c28f1f0 100644 --- a/pyyoutube/resources/channel_banners.py +++ b/pyyoutube/resources/channel_banners.py @@ -1,6 +1,7 @@ """ Channel banners resource implementation. """ + from typing import Optional from pyyoutube.resources.base_resource import Resource diff --git a/pyyoutube/resources/channels.py b/pyyoutube/resources/channels.py index 2fe11912..b8eee7fa 100644 --- a/pyyoutube/resources/channels.py +++ b/pyyoutube/resources/channels.py @@ -1,6 +1,7 @@ """ Channel resource implementation. """ + from typing import Optional, Union from pyyoutube.error import PyYouTubeException, ErrorMessage, ErrorCode @@ -18,6 +19,7 @@ class ChannelsResource(Resource): def list( self, parts: Optional[Union[str, list, tuple, set]] = None, + for_handle: Optional[str] = None, for_username: Optional[str] = None, channel_id: Optional[Union[str, list, tuple, set]] = None, managed_by_me: Optional[bool] = None, @@ -36,6 +38,11 @@ def list( Comma-separated list of one or more channel resource properties. Accepted values: id,auditDetails,brandingSettings,contentDetails,contentOwnerDetails, localizations,snippet,statistics,status,topicDetails + for_handle: + The parameter specifies a YouTube handle, thereby requesting the channel associated with that handle. + The parameter value can be prepended with an @ symbol. For example, to retrieve the resource for + the "Google for Developers" channel, set the forHandle parameter value to + either GoogleDevelopers or @GoogleDevelopers. for_username: The parameter specifies a YouTube username, thereby requesting the channel associated with that username. @@ -90,7 +97,9 @@ def list( "pageToken": page_token, **kwargs, } - if for_username is not None: + if for_handle is not None: + params["forHandle"] = for_handle + elif for_username is not None: params["forUsername"] = for_username elif channel_id is not None: params["id"] = enf_comma_separated(field="channel_id", value=channel_id) diff --git a/pyyoutube/resources/comment_threads.py b/pyyoutube/resources/comment_threads.py index 05803d97..d5ae48fc 100644 --- a/pyyoutube/resources/comment_threads.py +++ b/pyyoutube/resources/comment_threads.py @@ -1,6 +1,7 @@ """ Comment threads resource implementation. """ + from typing import Optional, Union from pyyoutube.error import PyYouTubeException, ErrorMessage, ErrorCode diff --git a/pyyoutube/resources/membership_levels.py b/pyyoutube/resources/membership_levels.py index 4bbc790d..3799b936 100644 --- a/pyyoutube/resources/membership_levels.py +++ b/pyyoutube/resources/membership_levels.py @@ -1,6 +1,7 @@ """ Membership levels resource implementation. """ + from typing import Optional, Union from pyyoutube.models import MembershipsLevelListResponse diff --git a/pyyoutube/resources/search.py b/pyyoutube/resources/search.py index 03dc77a0..45007d73 100644 --- a/pyyoutube/resources/search.py +++ b/pyyoutube/resources/search.py @@ -1,6 +1,7 @@ """ Search resource implementation. """ + from typing import Optional, Union from pyyoutube.resources.base_resource import Resource diff --git a/pyyoutube/resources/thumbnails.py b/pyyoutube/resources/thumbnails.py index 27641be5..67d92a4a 100644 --- a/pyyoutube/resources/thumbnails.py +++ b/pyyoutube/resources/thumbnails.py @@ -1,6 +1,7 @@ """ Thumbnails resources implementation. """ + from typing import Optional from pyyoutube.resources.base_resource import Resource diff --git a/pyyoutube/resources/video_abuse_report_reasons.py b/pyyoutube/resources/video_abuse_report_reasons.py index 1ab85014..4697fac1 100644 --- a/pyyoutube/resources/video_abuse_report_reasons.py +++ b/pyyoutube/resources/video_abuse_report_reasons.py @@ -1,6 +1,7 @@ """ Video abuse report reasons resource implementation. """ + from typing import Optional, Union from pyyoutube.resources.base_resource import Resource diff --git a/pyyoutube/resources/watermarks.py b/pyyoutube/resources/watermarks.py index 8ca6820c..c4ac8b4f 100644 --- a/pyyoutube/resources/watermarks.py +++ b/pyyoutube/resources/watermarks.py @@ -1,6 +1,7 @@ """ Watermarks resource implementation. """ + from typing import Optional, Union from pyyoutube.resources.base_resource import Resource diff --git a/pyyoutube/utils/params_checker.py b/pyyoutube/utils/params_checker.py index 43d0fe08..7f51fcb6 100644 --- a/pyyoutube/utils/params_checker.py +++ b/pyyoutube/utils/params_checker.py @@ -1,6 +1,7 @@ """ function's params checker. """ + import logging from typing import Optional, Union diff --git a/testdata/apidata/channel_info_single.json b/testdata/apidata/channel_info_single.json index 086a4065..2e4e3902 100644 --- a/testdata/apidata/channel_info_single.json +++ b/testdata/apidata/channel_info_single.json @@ -13,7 +13,7 @@ "snippet": { "title": "Google Developers", "description": "The Google Developers channel features talks from events, educational series, best practices, tips, and the latest updates across our products and platforms.", - "customUrl": "googledevelopers", + "customUrl": "@googledevelopers", "publishedAt": "2007-08-23T00:34:43.000Z", "thumbnails": { "default": { diff --git a/tests/apis/test_channels.py b/tests/apis/test_channels.py index 351a4a98..942789c3 100644 --- a/tests/apis/test_channels.py +++ b/tests/apis/test_channels.py @@ -62,6 +62,14 @@ def testGetChannelInfo(self) -> None: ) self.assertEqual(res_by_channel_id.items[0].id, "UC_x5XG1OV2P6uZZ5FSM9Ttw") + res_by_channel_handle = self.api.get_channel_info( + for_handle="googledevelopers", return_json=True + ) + self.assertEqual( + res_by_channel_handle["items"][0]["snippet"]["customUrl"], + "@googledevelopers", + ) + res_by_channel_name = self.api.get_channel_info( for_username="GoogleDevelopers", return_json=True ) diff --git a/tests/clients/test_captions.py b/tests/clients/test_captions.py index 62016ec8..bb5dab33 100644 --- a/tests/clients/test_captions.py +++ b/tests/clients/test_captions.py @@ -1,6 +1,7 @@ """ Tests for captions resources. """ + import io import pytest diff --git a/tests/clients/test_channel_banners.py b/tests/clients/test_channel_banners.py index 6d6fb71e..295674d7 100644 --- a/tests/clients/test_channel_banners.py +++ b/tests/clients/test_channel_banners.py @@ -1,6 +1,7 @@ """ Tests for channel banners """ + import io from .base import BaseTestCase diff --git a/tests/clients/test_channels.py b/tests/clients/test_channels.py index 7f4c68e1..ee9c5467 100644 --- a/tests/clients/test_channels.py +++ b/tests/clients/test_channels.py @@ -28,6 +28,12 @@ def test_list(self, helpers, authed_cli, key_cli): assert res.items[0].id == self.channel_id assert key_cli.channels.api_key == "api key" + res = key_cli.channels.list( + parts="id,snippet", + for_handle="@googledevelopers", + ) + assert res.items[0].snippet.customUrl == "@googledevelopers" + res = key_cli.channels.list( parts=["id", "snippet"], for_username="googledevelopers" ) diff --git a/tests/clients/test_client.py b/tests/clients/test_client.py index 9010454e..6b31ef2d 100644 --- a/tests/clients/test_client.py +++ b/tests/clients/test_client.py @@ -1,6 +1,7 @@ """ Tests for client. """ + import pytest import responses diff --git a/tests/clients/test_media.py b/tests/clients/test_media.py index 98545c34..283e820b 100644 --- a/tests/clients/test_media.py +++ b/tests/clients/test_media.py @@ -1,6 +1,7 @@ """ Tests for media upload. """ + import io import pytest diff --git a/tests/clients/test_thumbnails.py b/tests/clients/test_thumbnails.py index e5b5b1d3..4826c1eb 100644 --- a/tests/clients/test_thumbnails.py +++ b/tests/clients/test_thumbnails.py @@ -1,6 +1,7 @@ """ Tests for thumbnails. """ + import io from .base import BaseTestCase