From 1c96234848a2572aae04e3b9f2490533a8b817d2 Mon Sep 17 00:00:00 2001 From: DinhHuy2010 Date: Tue, 1 Oct 2024 19:05:59 +0700 Subject: [PATCH] add stubs for m3u8 (#12683) --- pyrightconfig.stricter.json | 1 + stubs/m3u8/@tests/stubtest_allowlist.txt | 17 + stubs/m3u8/METADATA.toml | 2 + stubs/m3u8/m3u8/__init__.pyi | 74 ++++ stubs/m3u8/m3u8/httpclient.pyi | 19 + stubs/m3u8/m3u8/mixins.pyi | 25 ++ stubs/m3u8/m3u8/model.pyi | 406 +++++++++++++++++++++ stubs/m3u8/m3u8/parser.pyi | 14 + stubs/m3u8/m3u8/protocol.pyi | 41 +++ stubs/m3u8/m3u8/version_matching.pyi | 5 + stubs/m3u8/m3u8/version_matching_rules.pyi | 24 ++ 11 files changed, 628 insertions(+) create mode 100644 stubs/m3u8/@tests/stubtest_allowlist.txt create mode 100644 stubs/m3u8/METADATA.toml create mode 100644 stubs/m3u8/m3u8/__init__.pyi create mode 100644 stubs/m3u8/m3u8/httpclient.pyi create mode 100644 stubs/m3u8/m3u8/mixins.pyi create mode 100644 stubs/m3u8/m3u8/model.pyi create mode 100644 stubs/m3u8/m3u8/parser.pyi create mode 100644 stubs/m3u8/m3u8/protocol.pyi create mode 100644 stubs/m3u8/m3u8/version_matching.pyi create mode 100644 stubs/m3u8/m3u8/version_matching_rules.pyi diff --git a/pyrightconfig.stricter.json b/pyrightconfig.stricter.json index 6c00f5ed3..a46ec4f0d 100644 --- a/pyrightconfig.stricter.json +++ b/pyrightconfig.stricter.json @@ -59,6 +59,7 @@ "stubs/jsonschema", "stubs/jwcrypto", "stubs/ldap3", + "stubs/m3u8", "stubs/Markdown", "stubs/mysqlclient", "stubs/netaddr/netaddr/core.pyi", diff --git a/stubs/m3u8/@tests/stubtest_allowlist.txt b/stubs/m3u8/@tests/stubtest_allowlist.txt new file mode 100644 index 000000000..28f49d10c --- /dev/null +++ b/stubs/m3u8/@tests/stubtest_allowlist.txt @@ -0,0 +1,17 @@ +# type check only +m3u8.httpclient.HTTPSHandler.__new__ +# internal functions and attributes +m3u8.M3U8.simple_attributes +m3u8.model.M3U8.simple_attributes +m3u8.model.denormalize_attribute +m3u8.model.find_key +m3u8.model.number_to_string +m3u8.model.quoted +m3u8.parser.cast_date_time +m3u8.parser.format_date_time +m3u8.parser.get_segment_custom_value +m3u8.parser.normalize_attribute +m3u8.parser.remove_quotes +m3u8.parser.remove_quotes_parser +m3u8.parser.save_segment_custom_value +m3u8.parser.string_to_lines diff --git a/stubs/m3u8/METADATA.toml b/stubs/m3u8/METADATA.toml new file mode 100644 index 000000000..7dbe6dbef --- /dev/null +++ b/stubs/m3u8/METADATA.toml @@ -0,0 +1,2 @@ +version = "6.0.*" +upstream_repository = "https://github.com/globocom/m3u8" diff --git a/stubs/m3u8/m3u8/__init__.pyi b/stubs/m3u8/m3u8/__init__.pyi new file mode 100644 index 000000000..c2d7c4548 --- /dev/null +++ b/stubs/m3u8/m3u8/__init__.pyi @@ -0,0 +1,74 @@ +from _typeshed import Incomplete +from collections.abc import Callable, Mapping +from typing import Any +from typing_extensions import TypeAlias + +from m3u8.httpclient import _HTTPClientProtocol +from m3u8.model import ( + M3U8, + ContentSteering, + DateRange, + DateRangeList, + IFramePlaylist, + ImagePlaylist, + Key, + Media, + MediaList, + PartialSegment, + PartialSegmentList, + PartInformation, + Playlist, + PlaylistList, + PreloadHint, + RenditionReport, + RenditionReportList, + Segment, + SegmentList, + ServerControl, + Skip, + Start, + Tiles, +) +from m3u8.parser import ParseError, parse + +__all__ = ( + "M3U8", + "Segment", + "SegmentList", + "PartialSegment", + "PartialSegmentList", + "Key", + "Playlist", + "IFramePlaylist", + "Media", + "MediaList", + "PlaylistList", + "Start", + "RenditionReport", + "RenditionReportList", + "ServerControl", + "Skip", + "PartInformation", + "PreloadHint", + "DateRange", + "DateRangeList", + "ContentSteering", + "ImagePlaylist", + "Tiles", + "loads", + "load", + "parse", + "ParseError", +) + +_CustomTagsParser: TypeAlias = Callable[[str, int, dict[str, Any], dict[str, Any]], object] + +def loads(content: str, uri: str | None = None, custom_tags_parser: _CustomTagsParser | None = None) -> M3U8: ... +def load( + uri: str, + timeout: Incomplete | None = None, + headers: Mapping[str, Any] = {}, + custom_tags_parser: _CustomTagsParser | None = None, + http_client: _HTTPClientProtocol = ..., + verify_ssl: bool = True, +) -> M3U8: ... diff --git a/stubs/m3u8/m3u8/httpclient.pyi b/stubs/m3u8/m3u8/httpclient.pyi new file mode 100644 index 000000000..de76c1b84 --- /dev/null +++ b/stubs/m3u8/m3u8/httpclient.pyi @@ -0,0 +1,19 @@ +import urllib.request +from typing import Any, Protocol, type_check_only + +@type_check_only +class _HTTPClientProtocol(Protocol): # noqa: Y046 + def download( + self, uri: str, timeout: float | None = None, headers: dict[str, Any] = {}, verify_ssl: bool = True + ) -> tuple[str, str]: ... + +class DefaultHTTPClient: + proxies: dict[str, str] | None + + def __init__(self, proxies: dict[str, str] | None = None) -> None: ... + def download( + self, uri: str, timeout: float | None = None, headers: dict[str, Any] = {}, verify_ssl: bool = True + ) -> tuple[str, str]: ... + +class HTTPSHandler: + def __new__(cls, verify_ssl: bool = True) -> urllib.request.HTTPSHandler: ... # type: ignore diff --git a/stubs/m3u8/m3u8/mixins.pyi b/stubs/m3u8/m3u8/mixins.pyi new file mode 100644 index 000000000..884e8b51a --- /dev/null +++ b/stubs/m3u8/m3u8/mixins.pyi @@ -0,0 +1,25 @@ +from abc import ABCMeta +from collections.abc import Iterable +from typing import TypeVar + +_T = TypeVar("_T") + +class BasePathMixin: + uri: str | None + @property + def absolute_uri(self) -> str: ... + @property + def base_path(self) -> str: ... + @base_path.setter + def base_path(self, newbase_path: str) -> None: ... + def get_path_from_uri(self) -> str: ... + +class GroupedBasePathMixin(Iterable[_T], metaclass=ABCMeta): + @property + def base_uri(self) -> str: ... + @base_uri.setter + def base_uri(self, __new_url: str, /) -> None: ... + @property + def base_path(self) -> str: ... + @base_path.setter + def base_path(self, __new_url: str, /) -> None: ... diff --git a/stubs/m3u8/m3u8/model.pyi b/stubs/m3u8/m3u8/model.pyi new file mode 100644 index 000000000..17e7ef494 --- /dev/null +++ b/stubs/m3u8/m3u8/model.pyi @@ -0,0 +1,406 @@ +import datetime as dt +from _typeshed import Incomplete, StrOrBytesPath +from collections.abc import Callable, Mapping +from typing import ClassVar, Literal, Protocol, TypeVar, type_check_only +from typing_extensions import TypeAlias + +from m3u8.mixins import BasePathMixin, GroupedBasePathMixin +from m3u8.protocol import ext_x_map, ext_x_session_key + +_T = TypeVar("_T") +_CustomTagsParser: TypeAlias = Callable[[str, int, dict[str, Incomplete], dict[str, Incomplete]], object] + +@type_check_only +class _PlaylistProtocol(Protocol): + base_uri: str | None + uri: str | None + @property + def absolute_uri(self) -> str: ... + @property + def base_path(self) -> str: ... + @base_path.setter + def base_path(self, newbase_path: str) -> None: ... + def get_path_from_uri(self) -> str: ... + +_PlaylistAnyT = TypeVar("_PlaylistAnyT", bound=_PlaylistProtocol) + +class MalformedPlaylistError(Exception): ... + +class M3U8: + simple_attributes: list[tuple[str, str]] + data: dict[str, Incomplete] + keys: list[Key] + segment_map: list[InitializationSection] + segments: SegmentList + files: list[str | None] + media: MediaList + playlists: PlaylistList[Playlist] + iframe_playlists: PlaylistList[IFramePlaylist] + image_playlists: PlaylistList[ImagePlaylist] + start: Start + server_control: ServerControl + part_inf: PartInformation + skip: Skip + rendition_reports: RenditionReportList + session_data: SessionDataList + session_keys: list[SessionKey | None] + preload_hint: PreloadHint + content_steering: ContentSteering + + # inserted via setattr() + + is_variant: bool | None + is_endlist: bool | None + is_i_frames_only: bool | None + target_duration: float | None + media_sequence: int | None + program_date_time: str | None + is_independent_segments: bool | None + version: str | None + allow_cache: str | None + playlist_type: str | None + discontinuity_sequence: Incomplete | None # undocmented + is_images_only: bool | None + + def __init__( + self, + content: str | None = None, + base_path: str | None = None, + base_uri: str | None = None, + strict: bool = False, + custom_tags_parser: _CustomTagsParser | None = None, + ) -> None: ... + @property + def base_uri(self) -> str | None: ... + @base_uri.setter + def base_uri(self, new_base_uri: str) -> None: ... + @property + def base_path(self) -> str | None: ... + @base_path.setter + def base_path(self, newbase_path: str) -> None: ... + def add_playlist(self, playlist: Playlist) -> None: ... + def add_iframe_playlist(self, iframe_playlist: IFramePlaylist) -> None: ... + def add_image_playlist(self, image_playlist: ImagePlaylist) -> None: ... + def add_media(self, media: Media) -> None: ... + def add_segment(self, segment: Segment) -> None: ... + def add_rendition_report(self, report: RenditionReport) -> None: ... + def dumps(self, timespec: str = "milliseconds", infspec: str = "auto") -> str: ... + def dump(self, filename: StrOrBytesPath) -> None: ... + def __unicode__(self) -> str: ... + +class Segment(BasePathMixin): + media_sequence: int | None + uri: str | None + duration: float | None + title: str + bitrate: int | None + byterange: str | None + program_date_time: dt.datetime | None + current_program_date_time: dt.datetime | None + discontinuity: bool + cue_out_start: bool + cue_out_explicitly_duration: bool + cue_out: bool + cue_in: bool + scte35: str | None + oatcls_scte35: str | None + scte35_duration: float | None + scte35_elapsedtime: Incomplete | None + asset_metadata: dict[str, Incomplete] | None + key: Key | None + parts: PartialSegmentList + init_section: InitializationSection | None + dateranges: DateRangeList + gap_tag: Incomplete | None + custom_parser_values: dict[str, Incomplete] + def __init__( + self, + uri: str | None = None, + base_uri: str | None = None, + program_date_time: dt.datetime | None = None, + current_program_date_time=None, + duration: float | None = None, + title: str | None = None, + bitrate=None, + byterange=None, + cue_out: bool = False, + cue_out_start: bool = False, + cue_out_explicitly_duration: bool = False, + cue_in: bool = False, + discontinuity: bool = False, + key=None, + scte35=None, + oatcls_scte35: str | None = None, + scte35_duration=None, + scte35_elapsedtime=None, + asset_metadata: Mapping[str, str] | None = None, + keyobject: Key | None = None, + parts: list[Mapping[str, Incomplete]] | None = None, + init_section: Mapping[str, Incomplete] | None = None, + dateranges=None, + gap_tag: list[Mapping[str, Incomplete]] | None = None, + media_sequence: int | None = None, + custom_parser_values=None, + ) -> None: ... + def add_part(self, part: PartialSegment) -> None: ... + def dumps(self, last_segment: PartialSegment | None, timespec: str = "milliseconds", infspec: str = "auto") -> str: ... + @property + def base_path(self) -> str: ... + @base_path.setter + def base_path(self, newbase_path: str) -> None: ... + @property + def base_uri(self) -> str: ... + @base_uri.setter + def base_uri(self, newbase_uri: str) -> None: ... + +class SegmentList(list[Segment], GroupedBasePathMixin[Segment]): + def dumps(self, timespec: str = "milliseconds", infspec: str = "auto") -> str: ... + @property + def uri(self) -> list[str | None]: ... + def by_key(self, key: Key) -> list[Segment]: ... + +class PartialSegment(BasePathMixin): + base_uri: str + uri: str | None + duration: float | None + program_date_time: dt.datetime | None + current_program_date_time: dt.datetime | None + byterange: str | None + independent: bool + gap: str | None + dateranges: DateRangeList + gap_tag: str | None + + def __init__( + self, + base_uri: str, + uri: str | None, + duration: float | None, + program_date_time: dt.datetime | None = None, + current_program_date_time: Incomplete | None = None, + byterange: Incomplete | None = None, + independent: Incomplete | None = None, + gap: Incomplete | None = None, + dateranges: list[Mapping[str, Incomplete]] | None = None, + gap_tag: Incomplete | None = None, + ) -> None: ... + def dumps(self, last_segment) -> str: ... + +class PartialSegmentList(list[PartialSegment], GroupedBasePathMixin[PartialSegment]): ... + +class Key(BasePathMixin): + tag: ClassVar[str] = ... + method: str + base_uri: str + uri: str | None + iv: str | None + keyformat: str | None + keyformatversions: str | None + + def __init__( + self, + method: str, + base_uri: str, + uri: str | None = None, + iv: str | None = None, + keyformat: str | None = None, + keyformatversions: str | None = None, + **kwargs, + ) -> None: ... + def __eq__(self, other: object) -> bool: ... + def __ne__(self, other: object) -> bool: ... + +class InitializationSection(BasePathMixin): + tag = ext_x_map + base_uri: str + uri: str | None + byterange: str | None + def __init__(self, base_uri: str, uri: str | None, byterange: str | None = None) -> None: ... + def __eq__(self, other: object) -> bool: ... + def __ne__(self, other: object) -> bool: ... + +class SessionKey(Key): + tag = ext_x_session_key + +class Playlist(_PlaylistProtocol): + base_uri: str | None + uri: str | None + stream_info: StreamInfo + media: MediaList + def __init__(self, uri: str | None, stream_info: Mapping[str, Incomplete], media: MediaList, base_uri: str) -> None: ... + +class IFramePlaylist(_PlaylistProtocol): + uri: str | None + base_uri: str | None + iframe_stream_info: StreamInfo + def __init__(self, base_uri: str, uri: str | None, iframe_stream_info: Mapping[str, Incomplete]) -> None: ... + +class StreamInfo: + bandwidth: int | None + closed_captions: Incomplete | None + average_bandwidth: int | None + program_id: int | None + resolution: tuple[int, int] | None + codecs: str | None + audio: str | None + video: str | None + subtitles: str | None + frame_rate: float | None + video_range: str | None + hdcp_level: str | None + pathway_id: str | None + stable_variant_id: str | None + req_video_layout: str | None + def __init__(self, **kwargs) -> None: ... + +class Media(BasePathMixin): + base_uri: str | None + uri: str | None + type: str | None + group_id: str | None + language: str | None + name: str | None + default: str | None + autoselect: str | None + forced: str | None + assoc_language: str | None + instream_id: str | None + characteristics: str | None + channels: str | None + stable_rendition_id: str | None + extras: dict[str, Incomplete] + + def __init__( + self, + uri: str | None = None, + type: str | None = None, + group_id: str | None = None, + language: str | None = None, + name: str | None = None, + default: str | None = None, + autoselect: str | None = None, + forced: str | None = None, + characteristics: str | None = None, + channels: str | None = None, + stable_rendition_id: str | None = None, + assoc_language: str | None = None, + instream_id: str | None = None, + base_uri: str | None = None, + **extras, + ) -> None: ... + def dumps(self) -> str: ... + +class TagList(list[_T]): ... + +class MediaList(TagList[Media], list[Media], GroupedBasePathMixin[Media]): + @property + def uri(self) -> list[str | None]: ... + +class PlaylistList(TagList[_PlaylistAnyT], list[_PlaylistAnyT], GroupedBasePathMixin[_PlaylistAnyT]): ... +class SessionDataList(TagList[SessionData], list[SessionData]): ... + +class Start: + time_offset: float + precise: Literal["YES", "NO"] + def __init__(self, time_offset: float, precise: Literal["YES", "NO"] | None = None) -> None: ... + +class RenditionReport(BasePathMixin): + base_uri: str | None + uri: str | None + last_msn: int + last_part: int | None + def __init__(self, base_uri: str | None, uri: str | None, last_msn: int, last_part: int | None = None) -> None: ... + def dumps(self) -> str: ... + +class RenditionReportList(list[RenditionReport], GroupedBasePathMixin[RenditionReport]): ... + +class ServerControl: + can_skip_until: float | None + can_block_reload: str | None + hold_back: float | None + part_hold_back: float | None + can_skip_dateranges: str | None + def __init__( + self, + can_skip_until: float | None = None, + can_block_reload: str | None = None, + hold_back: float | None = None, + part_hold_back: float | None = None, + can_skip_dateranges: str | None = None, + ) -> None: ... + def __getitem__(self, item: str): ... + def dumps(self) -> str: ... + +class Skip: + skipped_segments: int | None + recently_removed_dateranges: str | None + def __init__(self, skipped_segments: int, recently_removed_dateranges: str | None = None) -> None: ... + def dumps(self) -> str: ... + +class PartInformation: + part_target: float | None + def __init__(self, part_target: float | None = None) -> None: ... + def dumps(self) -> str: ... + +class PreloadHint(BasePathMixin): + hint_type: str | None + base_uri: str | None + uri: str | None + byterange_start: int | None + byterange_length: int | None + def __init__( + self, + type: str | None, + base_uri: str | None, + uri: str | None, + byterange_start: int | None = None, + byterange_length: int | None = None, + ) -> None: ... + def __getitem__(self, item: str) -> str: ... + def dumps(self) -> str: ... + +class SessionData: + data_id: str + value: str | None + uri: str | None + language: str | None + def __init__(self, data_id: str, value: str | None = None, uri: str | None = None, language: str | None = None) -> None: ... + def dumps(self) -> str: ... + +class DateRangeList(TagList[DateRange]): ... + +class DateRange: + id: str + start_date: str | None + class_: str | None + end_date: str | None + duration: float | None + planned_duration: float | None + scte35_cmd: str | None + scte35_out: str | None + scte35_in: str | None + end_on_next: Incomplete + x_client_attrs: list[tuple[str, str]] + def __init__(self, **kwargs) -> None: ... + def dumps(self) -> str: ... + +class ContentSteering(BasePathMixin): + base_uri: str | None + uri: str | None + pathway_id: str | None + def __init__(self, base_uri: str | None, server_uri: str | None, pathway_id: str | None = None) -> None: ... + def dumps(self) -> str: ... + +class ImagePlaylist(_PlaylistProtocol): + uri: str | None + base_uri: str | None + image_stream_info: StreamInfo + def __init__(self, base_uri: str | None, uri: str | None, image_stream_info: Mapping[str, Incomplete]) -> None: ... + +class Tiles(BasePathMixin): # this is unused in runtime, so this is (temporary) has incomplete + uri: str | None + resolution: Incomplete + layout: Incomplete + duration: Incomplete + def __init__(self, resolution, layout, duration) -> None: ... + def dumps(self) -> str: ... diff --git a/stubs/m3u8/m3u8/parser.pyi b/stubs/m3u8/m3u8/parser.pyi new file mode 100644 index 000000000..f33e0126e --- /dev/null +++ b/stubs/m3u8/m3u8/parser.pyi @@ -0,0 +1,14 @@ +from collections.abc import Callable +from re import Pattern +from typing import Any +from typing_extensions import TypeAlias + +ATTRIBUTELISTPATTERN: Pattern[str] +_CustomTagsParser: TypeAlias = Callable[[str, int, dict[str, Any], dict[str, Any]], object] + +class ParseError(Exception): + lineno: int + line: str + def __init__(self, lineno: int, line: str) -> None: ... + +def parse(content: str, strict: bool = False, custom_tags_parser: _CustomTagsParser | None = None) -> dict[str, Any]: ... diff --git a/stubs/m3u8/m3u8/protocol.pyi b/stubs/m3u8/m3u8/protocol.pyi new file mode 100644 index 000000000..06bb58b5a --- /dev/null +++ b/stubs/m3u8/m3u8/protocol.pyi @@ -0,0 +1,41 @@ +ext_m3u: str +ext_x_targetduration: str +ext_x_media_sequence: str +ext_x_discontinuity_sequence: str +ext_x_program_date_time: str +ext_x_media: str +ext_x_playlist_type: str +ext_x_key: str +ext_x_stream_inf: str +ext_x_version: str +ext_x_allow_cache: str +ext_x_endlist: str +extinf: str +ext_i_frames_only: str +ext_x_asset: str +ext_x_bitrate: str +ext_x_byterange: str +ext_x_i_frame_stream_inf: str +ext_x_discontinuity: str +ext_x_cue_out: str +ext_x_cue_out_cont: str +ext_x_cue_in: str +ext_x_cue_span: str +ext_oatcls_scte35: str +ext_is_independent_segments: str +ext_x_map: str +ext_x_start: str +ext_x_server_control: str +ext_x_part_inf: str +ext_x_part: str +ext_x_rendition_report: str +ext_x_skip: str +ext_x_session_data: str +ext_x_session_key: str +ext_x_preload_hint: str +ext_x_daterange: str +ext_x_gap: str +ext_x_content_steering: str +ext_x_image_stream_inf: str +ext_x_images_only: str +ext_x_tiles: str diff --git a/stubs/m3u8/m3u8/version_matching.pyi b/stubs/m3u8/m3u8/version_matching.pyi new file mode 100644 index 000000000..6f4265b2e --- /dev/null +++ b/stubs/m3u8/m3u8/version_matching.pyi @@ -0,0 +1,5 @@ +from m3u8.version_matching_rules import VersionMatchingError + +def get_version(file_lines: list[str]) -> float | None: ... +def valid_in_all_rules(line_number: int, line: str, version: float) -> list[VersionMatchingError]: ... +def validate(file_lines: list[str]) -> list[VersionMatchingError]: ... diff --git a/stubs/m3u8/m3u8/version_matching_rules.pyi b/stubs/m3u8/m3u8/version_matching_rules.pyi new file mode 100644 index 000000000..2fb0bde72 --- /dev/null +++ b/stubs/m3u8/m3u8/version_matching_rules.pyi @@ -0,0 +1,24 @@ +from dataclasses import dataclass + +@dataclass +class VersionMatchingError(Exception): + line_number: int + line: str + how_to_fix: str = ... + description: str = ... + +class VersionMatchRuleBase: + description: str + how_to_fix: str + version: float + line_number: int + line: str + def __init__(self, version: float, line_number: int, line: str) -> None: ... + def validate(self) -> bool: ... + def get_error(self) -> VersionMatchingError: ... + +class ValidIVInEXTXKEY(VersionMatchRuleBase): ... +class ValidFloatingPointEXTINF(VersionMatchRuleBase): ... +class ValidEXTXBYTERANGEOrEXTXIFRAMESONLY(VersionMatchRuleBase): ... + +available_rules: list[type[VersionMatchRuleBase]]