playlist_max maybe working? need to eval

This commit is contained in:
Jesse Bannon 2023-12-11 17:47:18 -08:00
parent 229af2351a
commit 6bb9e96384
13 changed files with 322 additions and 230 deletions

View file

@ -0,0 +1,132 @@
import os
import posixpath
from yt_dlp.utils import sanitize_filename
from ytdl_sub.script.functions import Functions
from ytdl_sub.script.types.map import Map
from ytdl_sub.script.types.resolvable import AnyArgument
from ytdl_sub.script.types.resolvable import Integer
from ytdl_sub.script.types.resolvable import ReturnableArgument
from ytdl_sub.script.types.resolvable import String
from ytdl_sub.script.utils.exceptions import RuntimeException
from ytdl_sub.utils.file_path import FilePathTruncater
def _pad(num: int, width: int):
return str(num).zfill(width)
_days_in_month = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
class CustomFunctions:
@staticmethod
def legacy_bracket_safety(value: ReturnableArgument) -> ReturnableArgument:
if isinstance(value, String):
value = String(value.value.replace("{", "").replace("}", ""))
return value
@staticmethod
def to_native_filepath(filepath: String) -> String:
return String(filepath.value.replace(posixpath.sep, os.sep))
@staticmethod
def truncate_filepath_if_too_long(filepath: String) -> String:
return String(FilePathTruncater.maybe_truncate_file_path(filepath.value))
@staticmethod
def sanitize(value: AnyArgument) -> String:
return String(sanitize_filename(str(value)))
@staticmethod
def sanitize_plex_episode(string: String) -> String:
sanitized_string = CustomFunctions.sanitize(string).value
out = ""
for char in sanitized_string:
match char:
case "0":
out += ""
case "1":
out += ""
case "2":
out += ""
case "3":
out += ""
case "4":
out += ""
case "5":
out += ""
case "6":
out += ""
case "7":
out += ""
case "8":
out += ""
case "9":
out += ""
case _:
out += char
return String(out)
@staticmethod
def to_date_metadata(yyyymmdd: String) -> Map:
date_str = yyyymmdd.value
if not (date_str.isnumeric() and len(date_str) == 8):
raise RuntimeException(
f"Expected input of date_metadata to be YYYYMMDD, but received {date_str}"
)
year: int = int(date_str[:4])
month_padded: str = date_str[4:6]
day_padded: str = date_str[6:8]
month: int = int(month_padded)
day: int = int(day_padded)
year_truncated: int = int(str(year)[-2:])
day_of_year: int = sum(_days_in_month[:month]) + day
total_days_in_month: int = _days_in_month[month]
total_days_in_year: int = 365
if year % 4 == 0:
total_days_in_year += 1
if month == 2:
total_days_in_month += 1
if month > 2:
day_of_year += 1
day_of_year_reversed: int = total_days_in_year + 1 - day_of_year
month_reversed: int = 13 - month
day_reversed: int = total_days_in_month + 1 - day
return Map(
{
String("date"): yyyymmdd,
String("date_standardized"): String(f"{year}-{month_padded}-{day_padded}"),
String("year"): Integer(year),
String("month"): Integer(month),
String("day"): Integer(day),
String("year_truncated"): String(year_truncated),
String("month_padded"): String(month_padded),
String("day_padded"): String(day_padded),
String("year_truncated_reversed"): Integer(100 - year_truncated),
String("month_reversed"): Integer(month_reversed),
String("month_reversed_padded"): String(_pad(month_reversed, width=2)),
String("day_reversed"): Integer(day_reversed),
String("day_reversed_padded"): String(_pad(day_reversed, width=2)),
String("day_of_year"): Integer(day_of_year),
String("day_of_year_padded"): String(_pad(day_of_year, width=3)),
String("day_of_year_reversed"): Integer(day_of_year_reversed),
String("day_of_year_reversed_padded"): String(_pad(day_of_year_reversed, width=3)),
}
)
@staticmethod
def register():
if not Functions.is_built_in("sanitize"):
Functions.register_function(CustomFunctions.legacy_bracket_safety)
Functions.register_function(CustomFunctions.truncate_filepath_if_too_long)
Functions.register_function(CustomFunctions.to_native_filepath)
Functions.register_function(CustomFunctions.sanitize)
Functions.register_function(CustomFunctions.sanitize_plex_episode)
Functions.register_function(CustomFunctions.to_date_metadata)

View file

@ -1,132 +1,32 @@
import os from typing import Dict
import posixpath
from yt_dlp.utils import sanitize_filename from ytdl_sub.entries.script.variable_definitions import VARIABLES as v
from ytdl_sub.script.functions import Functions CUSTOM_FUNCTION_SCRIPTS: Dict[str, str] = {
from ytdl_sub.script.types.map import Map "%extract_field_from_metadata_array_getter": "{ %map_get( %map(%array_at($0, 0)), %array_at($0, 1) ) }",
from ytdl_sub.script.types.resolvable import AnyArgument "%extract_field_from_metadata_array": f"""{{
from ytdl_sub.script.types.resolvable import Integer %if(
from ytdl_sub.script.types.resolvable import ReturnableArgument %bool($0),
from ytdl_sub.script.types.resolvable import String %array_extend(
from ytdl_sub.script.utils.exceptions import RuntimeException %array_apply(
from ytdl_sub.utils.file_path import FilePathTruncater %array_product(
%array($0),
[ %string($1) ]
def _pad(num: int, width: int): ),
return str(num).zfill(width) %extract_field_from_metadata_array_getter
_days_in_month = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
class CustomFunctions:
@staticmethod
def legacy_bracket_safety(value: ReturnableArgument) -> ReturnableArgument:
if isinstance(value, String):
value = String(value.value.replace("{", "").replace("}", ""))
return value
@staticmethod
def to_native_filepath(filepath: String) -> String:
return String(filepath.value.replace(posixpath.sep, os.sep))
@staticmethod
def truncate_filepath_if_too_long(filepath: String) -> String:
return String(FilePathTruncater.maybe_truncate_file_path(filepath.value))
@staticmethod
def sanitize(value: AnyArgument) -> String:
return String(sanitize_filename(str(value)))
@staticmethod
def sanitize_plex_episode(string: String) -> String:
sanitized_string = CustomFunctions.sanitize(string).value
out = ""
for char in sanitized_string:
match char:
case "0":
out += ""
case "1":
out += ""
case "2":
out += ""
case "3":
out += ""
case "4":
out += ""
case "5":
out += ""
case "6":
out += ""
case "7":
out += ""
case "8":
out += ""
case "9":
out += ""
case _:
out += char
return String(out)
@staticmethod
def to_date_metadata(yyyymmdd: String) -> Map:
date_str = yyyymmdd.value
if not (date_str.isnumeric() and len(date_str) == 8):
raise RuntimeException(
f"Expected input of date_metadata to be YYYYMMDD, but received {date_str}"
) )
),
year: int = int(date_str[:4]) []
month_padded: str = date_str[4:6]
day_padded: str = date_str[6:8]
month: int = int(month_padded)
day: int = int(day_padded)
year_truncated: int = int(str(year)[-2:])
day_of_year: int = sum(_days_in_month[:month]) + day
total_days_in_month: int = _days_in_month[month]
total_days_in_year: int = 365
if year % 4 == 0:
total_days_in_year += 1
if month == 2:
total_days_in_month += 1
if month > 2:
day_of_year += 1
day_of_year_reversed: int = total_days_in_year + 1 - day_of_year
month_reversed: int = 13 - month
day_reversed: int = total_days_in_month + 1 - day
return Map(
{
String("date"): yyyymmdd,
String("date_standardized"): String(f"{year}-{month_padded}-{day_padded}"),
String("year"): Integer(year),
String("month"): Integer(month),
String("day"): Integer(day),
String("year_truncated"): String(year_truncated),
String("month_padded"): String(month_padded),
String("day_padded"): String(day_padded),
String("year_truncated_reversed"): Integer(100 - year_truncated),
String("month_reversed"): Integer(month_reversed),
String("month_reversed_padded"): String(_pad(month_reversed, width=2)),
String("day_reversed"): Integer(day_reversed),
String("day_reversed_padded"): String(_pad(day_reversed, width=2)),
String("day_of_year"): Integer(day_of_year),
String("day_of_year_padded"): String(_pad(day_of_year, width=3)),
String("day_of_year_reversed"): Integer(day_of_year_reversed),
String("day_of_year_reversed_padded"): String(_pad(day_of_year_reversed, width=3)),
}
) )
}}""",
@staticmethod "%extract_field_from_siblings": f"""{{
def register(): %if(
if not Functions.is_built_in("sanitize"): %bool({v.sibling_metadata.variable_name}),
Functions.register_function(CustomFunctions.legacy_bracket_safety) %extract_field_from_metadata_array(
Functions.register_function(CustomFunctions.truncate_filepath_if_too_long) {v.sibling_metadata.variable_name},
Functions.register_function(CustomFunctions.to_native_filepath) $0
Functions.register_function(CustomFunctions.sanitize) ),
Functions.register_function(CustomFunctions.sanitize_plex_episode) []
Functions.register_function(CustomFunctions.to_date_metadata) )
}}""",
}

View file

@ -5,7 +5,7 @@ from typing import Set
import mergedeep import mergedeep
from ytdl_sub.entries.script.function_scripts import CustomFunctions from ytdl_sub.entries.script.custom_functions import CustomFunctions
from ytdl_sub.entries.script.variable_definitions import VARIABLES as v from ytdl_sub.entries.script.variable_definitions import VARIABLES as v
from ytdl_sub.entries.script.variable_definitions import Metadata from ytdl_sub.entries.script.variable_definitions import Metadata
from ytdl_sub.entries.script.variable_definitions import MetadataVariable from ytdl_sub.entries.script.variable_definitions import MetadataVariable
@ -35,8 +35,11 @@ def date_metadata(date_key: Variable, metadata_key: str) -> str:
# Metadata Getters # Metadata Getters
def _get_internal( def _get(
metadata: Metadata, key: MetadataVariable, default: Optional[Variable | str | int | Dict | List] cast: str,
metadata: Metadata,
key: MetadataVariable,
default: Optional[Variable | str | int | Dict | List],
) -> str: ) -> str:
if default is None: if default is None:
# TODO: assert with good error message if key DNE # TODO: assert with good error message if key DNE
@ -52,57 +55,51 @@ def _get_internal(
else: else:
out = f"%map_get_non_empty({metadata.variable_name}, '{key.metadata_key}', {default})" out = f"%map_get_non_empty({metadata.variable_name}, '{key.metadata_key}', {default})"
return f"%legacy_bracket_safety({out})" return f"{{ %legacy_bracket_safety(%{cast}({out})) }}"
def _get_int(
metadata: Metadata, key: MetadataVariable, default: Optional[Variable | int] = None
) -> str:
return f"{{%int({_get_internal(metadata=metadata, key=key, default=default)})}}"
def _get(
metadata: Metadata, key: MetadataVariable, default: Optional[Variable | str | int | Dict | List]
) -> str:
return f"{{{_get_internal(metadata=metadata, key=key, default=default)}}}"
############################################################################################### ###############################################################################################
# Entry Getters # Entry Getters
def entry_get( def entry_get_str(key: MetadataVariable, default: Optional[Variable | str] = None) -> str:
key: MetadataVariable, default: Optional[Variable | str | int | Dict | List] = None return _get("string", metadata=v.entry_metadata, key=key, default=default)
) -> str:
return _get(metadata=v.entry_metadata, key=key, default=default)
def entry_get_int(key: MetadataVariable, default: Optional[Variable | int] = None) -> str: def entry_get_int(key: MetadataVariable, default: Optional[Variable | int] = None) -> str:
return _get_int(metadata=v.entry_metadata, key=key, default=default) return _get("int", metadata=v.entry_metadata, key=key, default=default)
def entry_get_map(key: MetadataVariable, default: Optional[Variable | Dict] = None):
return _get("map", metadata=v.entry_metadata, key=key, default=default)
def entry_get_array(key: MetadataVariable, default: Optional[Variable | List] = None):
return _get("array", metadata=v.entry_metadata, key=key, default=default)
############################################################################################### ###############################################################################################
# Playlist Getters # Playlist Getters
def playlist_get(key: MetadataVariable, default: Optional[Variable | str | int] = None) -> str: def playlist_get_str(key: MetadataVariable, default: Optional[Variable | str] = None) -> str:
return _get(metadata=v.playlist_metadata, key=key, default=default) return _get("string", metadata=v.playlist_metadata, key=key, default=default)
def playlist_get_int(key: MetadataVariable, default: Optional[Variable | int] = None) -> str: def playlist_get_int(key: MetadataVariable, default: Optional[Variable | int] = None) -> str:
return _get_int(metadata=v.playlist_metadata, key=key, default=default) return _get("int", metadata=v.playlist_metadata, key=key, default=default)
############################################################################################### ###############################################################################################
# Source Getters # Source Getters
def source_get(key: MetadataVariable, default: Optional[Variable | str | int] = None) -> str: def source_get_str(key: MetadataVariable, default: Optional[Variable | str] = None) -> str:
return _get(metadata=v.source_metadata, key=key, default=default) return _get("string", metadata=v.source_metadata, key=key, default=default)
def source_get_int(key: MetadataVariable, default: Optional[Variable | int] = None) -> str: def source_get_int(key: MetadataVariable, default: Optional[Variable | int] = None) -> str:
return _get_int(metadata=v.source_metadata, key=key, default=default) return _get("int", metadata=v.source_metadata, key=key, default=default)
############################################################################################### ###############################################################################################
@ -116,32 +113,32 @@ ENTRY_HARDCODED_VARIABLES: Dict[Variable, str] = {
} }
ENTRY_RELATIVE_VARIABLES: Dict[MetadataVariable, str] = { ENTRY_RELATIVE_VARIABLES: Dict[MetadataVariable, str] = {
v.playlist_metadata: entry_get(v.playlist_metadata, {}), v.playlist_metadata: entry_get_map(v.playlist_metadata, {}),
v.source_metadata: entry_get(v.source_metadata, {}), v.source_metadata: entry_get_map(v.source_metadata, {}),
v.sibling_metadata: entry_get(v.sibling_metadata, []), v.sibling_metadata: entry_get_array(v.sibling_metadata, []),
} }
ENTRY_REQUIRED_VARIABLES: Dict[MetadataVariable, str] = { ENTRY_REQUIRED_VARIABLES: Dict[MetadataVariable, str] = {
v.uid: entry_get(v.uid), v.uid: entry_get_str(v.uid),
v.extractor_key: entry_get(v.extractor_key), v.extractor_key: entry_get_str(v.extractor_key),
v.epoch: entry_get_int(v.epoch), v.epoch: entry_get_int(v.epoch),
v.webpage_url: entry_get(v.webpage_url), v.webpage_url: entry_get_str(v.webpage_url),
v.ext: entry_get(v.ext), v.ext: entry_get_str(v.ext),
} }
ENTRY_DEFAULT_VARIABLES: Dict[MetadataVariable, str] = { ENTRY_DEFAULT_VARIABLES: Dict[MetadataVariable, str] = {
v.playlist_index: playlist_get_int(v.playlist_index, 1), v.playlist_index: playlist_get_int(v.playlist_index, 1),
v.title: entry_get(v.title, v.uid), v.title: entry_get_str(v.title, v.uid),
v.extractor: entry_get(v.extractor, v.extractor_key), v.extractor: entry_get_str(v.extractor, v.extractor_key),
v.description: entry_get(v.description, ""), v.description: entry_get_str(v.description, ""),
v.uploader_id: entry_get(v.uploader_id, v.uid), v.uploader_id: entry_get_str(v.uploader_id, v.uid),
v.uploader: entry_get(v.uploader, v.uploader_id), v.uploader: entry_get_str(v.uploader, v.uploader_id),
v.uploader_url: entry_get(v.uploader_url, v.webpage_url), v.uploader_url: entry_get_str(v.uploader_url, v.webpage_url),
v.upload_date: entry_get(v.upload_date, v.epoch_date), v.upload_date: entry_get_str(v.upload_date, v.epoch_date),
v.release_date: entry_get(v.release_date, v.upload_date), v.release_date: entry_get_str(v.release_date, v.upload_date),
v.channel: entry_get(v.channel, v.uploader), v.channel: entry_get_str(v.channel, v.uploader),
v.creator: entry_get(v.creator, v.channel), v.creator: entry_get_str(v.creator, v.channel),
v.channel_id: entry_get(v.channel_id, v.uploader_id), v.channel_id: entry_get_str(v.channel_id, v.uploader_id),
} }
# MARK AS UNRESOLVABLE UNTIL THEY ARE ADDED # MARK AS UNRESOLVABLE UNTIL THEY ARE ADDED
@ -212,14 +209,14 @@ ENTRY_RELEASE_DATE_VARIABLES: Dict[Variable, str] = {
} }
PLAYLIST_VARIABLES: Dict[Variable, str] = { PLAYLIST_VARIABLES: Dict[Variable, str] = {
v.playlist_uid: playlist_get(v.playlist_uid, v.uid), v.playlist_uid: playlist_get_str(v.playlist_uid, v.uid),
v.playlist_title: playlist_get(v.playlist_title, v.title), v.playlist_title: playlist_get_str(v.playlist_title, v.title),
v.playlist_webpage_url: playlist_get(v.playlist_webpage_url, v.webpage_url), v.playlist_webpage_url: playlist_get_str(v.playlist_webpage_url, v.webpage_url),
v.playlist_count: playlist_get_int(v.playlist_count, 1), v.playlist_count: playlist_get_int(v.playlist_count, 1),
v.playlist_description: playlist_get(v.playlist_description, v.description), v.playlist_description: playlist_get_str(v.playlist_description, v.description),
v.playlist_uploader_id: playlist_get(v.playlist_uploader_id, v.uploader_id), v.playlist_uploader_id: playlist_get_str(v.playlist_uploader_id, v.uploader_id),
v.playlist_uploader: playlist_get(v.playlist_uploader, v.uploader), v.playlist_uploader: playlist_get_str(v.playlist_uploader, v.uploader),
v.playlist_uploader_url: playlist_get(v.playlist_uploader_url, v.playlist_webpage_url), v.playlist_uploader_url: playlist_get_str(v.playlist_uploader_url, v.playlist_webpage_url),
} }
PLAYLIST_DERIVED_VARIABLES: Dict[Variable, str] = { PLAYLIST_DERIVED_VARIABLES: Dict[Variable, str] = {
@ -234,15 +231,15 @@ PLAYLIST_DERIVED_VARIABLES: Dict[Variable, str] = {
SOURCE_VARIABLES: Dict[Variable, str] = { SOURCE_VARIABLES: Dict[Variable, str] = {
v.source_uid: source_get(v.source_uid, v.playlist_uid), v.source_uid: source_get_str(v.source_uid, v.playlist_uid),
v.source_title: source_get(v.source_title, v.playlist_title), v.source_title: source_get_str(v.source_title, v.playlist_title),
v.source_webpage_url: source_get(v.source_webpage_url, v.playlist_webpage_url), v.source_webpage_url: source_get_str(v.source_webpage_url, v.playlist_webpage_url),
v.source_index: source_get_int(v.source_index, 1), v.source_index: source_get_int(v.source_index, 1),
v.source_count: source_get_int(v.source_count, 1), v.source_count: source_get_int(v.source_count, 1),
v.source_description: source_get(v.source_description, v.playlist_description), v.source_description: source_get_str(v.source_description, v.playlist_description),
v.source_uploader_id: source_get(v.source_uploader_id, v.playlist_uploader_id), v.source_uploader_id: source_get_str(v.source_uploader_id, v.playlist_uploader_id),
v.source_uploader: source_get(v.source_uploader, v.playlist_uploader), v.source_uploader: source_get_str(v.source_uploader, v.playlist_uploader),
v.source_uploader_url: source_get(v.source_uploader_url, v.source_webpage_url), v.source_uploader_url: source_get_str(v.source_uploader_url, v.source_webpage_url),
} }
SOURCE_DERIVED_VARIABLES: Dict[Variable, str] = { SOURCE_DERIVED_VARIABLES: Dict[Variable, str] = {
@ -251,7 +248,15 @@ SOURCE_DERIVED_VARIABLES: Dict[Variable, str] = {
} }
SIBLING_VARIABLES: Dict[Variable, str] = { SIBLING_VARIABLES: Dict[Variable, str] = {
v.playlist_max_upload_year: "TODO!!!", v.playlist_max_upload_year: f"""{{
%array_reduce(
%if_passthrough(
%extract_field_from_siblings('{v.upload_year.variable_name}'),
[{v.upload_year.variable_name}]
),
%max
)
}}"""
} }
SIBLING_DERIVED_VARIABLES: Dict[Variable, str] = { SIBLING_DERIVED_VARIABLES: Dict[Variable, str] = {
@ -270,6 +275,8 @@ mergedeep.merge(
ENTRY_DERIVED_VARIABLES, ENTRY_DERIVED_VARIABLES,
ENTRY_UPLOAD_DATE_VARIABLES, ENTRY_UPLOAD_DATE_VARIABLES,
ENTRY_RELEASE_DATE_VARIABLES, ENTRY_RELEASE_DATE_VARIABLES,
SIBLING_VARIABLES,
SIBLING_DERIVED_VARIABLES,
PLAYLIST_VARIABLES, PLAYLIST_VARIABLES,
PLAYLIST_DERIVED_VARIABLES, PLAYLIST_DERIVED_VARIABLES,
SOURCE_VARIABLES, SOURCE_VARIABLES,

View file

@ -12,9 +12,22 @@ from ytdl_sub.script.types.resolvable import LambdaTwo
from ytdl_sub.script.types.resolvable import Resolvable from ytdl_sub.script.types.resolvable import Resolvable
from ytdl_sub.script.utils.exceptions import UNREACHABLE from ytdl_sub.script.utils.exceptions import UNREACHABLE
from ytdl_sub.script.utils.exceptions import ArrayValueDoesNotExist from ytdl_sub.script.utils.exceptions import ArrayValueDoesNotExist
from ytdl_sub.script.utils.exceptions import FunctionRuntimeException
class ArrayFunctions: class ArrayFunctions:
@staticmethod
def array(maybe_array: AnyArgument) -> Array:
if not isinstance(maybe_array, Array):
raise FunctionRuntimeException(
f"Tried and failed to cast {maybe_array.type_name()} as an Array"
)
return maybe_array
@staticmethod
def array_size(array: Array) -> Integer:
return Integer(len(array.value))
@staticmethod @staticmethod
def array_extend(*arrays: Array) -> Array: def array_extend(*arrays: Array) -> Array:
""" """
@ -27,7 +40,7 @@ class ArrayFunctions:
return Array(output) return Array(output)
@staticmethod @staticmethod
def array_at(array: Array, idx: Integer) -> Resolvable: def array_at(array: Array, idx: Integer) -> AnyArgument:
""" """
Return the element in the Array at index ``idx``. Return the element in the Array at index ``idx``.
""" """

View file

@ -17,3 +17,15 @@ class ConditionalFunctions:
if condition.value: if condition.value:
return true return true
return false return false
@staticmethod
def if_passthrough(
maybe_true_arg: ReturnableArgumentA, else_arg: ReturnableArgumentB
) -> Union[ReturnableArgumentA, ReturnableArgumentB]:
"""
Conditional ``if`` statement that returns the ``maybe_true_arg`` if it evaluates to True,
otherwise returns ``else_arg``.
"""
if bool(maybe_true_arg.value):
return maybe_true_arg
return else_arg

View file

@ -9,17 +9,45 @@ from ytdl_sub.script.types.resolvable import Integer
from ytdl_sub.script.types.resolvable import LambdaThree from ytdl_sub.script.types.resolvable import LambdaThree
from ytdl_sub.script.types.resolvable import LambdaTwo from ytdl_sub.script.types.resolvable import LambdaTwo
from ytdl_sub.script.types.resolvable import String from ytdl_sub.script.types.resolvable import String
from ytdl_sub.script.utils.exceptions import FunctionRuntimeException
from ytdl_sub.script.utils.exceptions import KeyDoesNotExistRuntimeException from ytdl_sub.script.utils.exceptions import KeyDoesNotExistRuntimeException
from ytdl_sub.script.utils.exceptions import KeyNotHashableRuntimeException
class MapFunctions: class MapFunctions:
@staticmethod @staticmethod
def map_get(mapping: Map, key: Hashable, default: Optional[AnyArgument] = None) -> AnyArgument: def map(maybe_mapping: AnyArgument) -> Map:
if not isinstance(maybe_mapping, Map):
raise FunctionRuntimeException(
f"Tried and failed to cast {maybe_mapping.type_name()} as a Map"
)
return maybe_mapping
@staticmethod
def map_size(mapping: Map) -> Integer:
return Integer(len(mapping.value))
@staticmethod
def map_contains(mapping: Map, key: AnyArgument) -> Boolean:
"""
Returns True if the key is in the Map. False otherwise.
"""
if not isinstance(key, Hashable):
raise KeyNotHashableRuntimeException(
f"Tried to use {key.type_name()} as a Map key, but it is not hashable."
)
return Boolean(key in mapping.value)
@staticmethod
def map_get(
mapping: Map, key: AnyArgument, default: Optional[AnyArgument] = None
) -> AnyArgument:
""" """
Return ``key``'s value within the Map. If ``key`` does not exist, and ``default`` is Return ``key``'s value within the Map. If ``key`` does not exist, and ``default`` is
provided, it will return ``default``. Otherwise, will error. provided, it will return ``default``. Otherwise, will error.
""" """
if key not in mapping.value: if not MapFunctions.map_contains(mapping=mapping, key=key).value:
if default is not None: if default is not None:
return default return default
@ -29,7 +57,7 @@ class MapFunctions:
return mapping.value[key] return mapping.value[key]
@staticmethod @staticmethod
def map_get_non_empty(mapping: Map, key: Hashable, default: AnyArgument) -> AnyArgument: def map_get_non_empty(mapping: Map, key: AnyArgument, default: AnyArgument) -> AnyArgument:
""" """
Return ``key``'s value within the Map. If ``key`` does not exist or is an empty string, Return ``key``'s value within the Map. If ``key`` does not exist or is an empty string,
return ``default``. Otherwise, will error. return ``default``. Otherwise, will error.
@ -39,13 +67,6 @@ class MapFunctions:
return default return default
return output return output
@staticmethod
def map_contains(mapping: Map, key: Hashable) -> Boolean:
"""
Returns True if the key is in the Map. False otherwise.
"""
return Boolean(key in mapping.value)
# pylint: disable=unused-argument # pylint: disable=unused-argument
@staticmethod @staticmethod

View file

@ -14,6 +14,7 @@ from ytdl_sub.script.types.array import UnresolvedArray
from ytdl_sub.script.types.resolvable import Argument from ytdl_sub.script.types.resolvable import Argument
from ytdl_sub.script.types.resolvable import BuiltInFunctionType from ytdl_sub.script.types.resolvable import BuiltInFunctionType
from ytdl_sub.script.types.resolvable import FunctionType from ytdl_sub.script.types.resolvable import FunctionType
from ytdl_sub.script.types.resolvable import FutureResolvable
from ytdl_sub.script.types.resolvable import Lambda from ytdl_sub.script.types.resolvable import Lambda
from ytdl_sub.script.types.resolvable import NamedCustomFunction from ytdl_sub.script.types.resolvable import NamedCustomFunction
from ytdl_sub.script.types.resolvable import Resolvable from ytdl_sub.script.types.resolvable import Resolvable
@ -105,6 +106,8 @@ class BuiltInFunction(Function, BuiltInFunctionType):
def _arg_output_type(cls, arg: Argument) -> Type[Argument]: def _arg_output_type(cls, arg: Argument) -> Type[Argument]:
if isinstance(arg, BuiltInFunction): if isinstance(arg, BuiltInFunction):
return arg.output_type() return arg.output_type()
if isinstance(arg, FutureResolvable):
return arg.future_resolvable_type()
return type(arg) return type(arg)
@classmethod @classmethod

View file

@ -67,7 +67,8 @@ def is_type_compatible(
arg_type: Type[NamedType] = arg.__class__ arg_type: Type[NamedType] = arg.__class__
if isinstance(arg, BuiltInFunctionType): if isinstance(arg, BuiltInFunctionType):
arg_type = arg.output_type() # built-in function arg_type = arg.output_type() # built-in function
elif isinstance(arg, FutureResolvable):
if isinstance(arg, FutureResolvable):
arg_type = arg.future_resolvable_type() arg_type = arg.future_resolvable_type()
elif isinstance(arg, FunctionType): elif isinstance(arg, FunctionType):
return True # custom-function, can be anything, so pass for now return True # custom-function, can be anything, so pass for now

View file

@ -4,6 +4,7 @@ from typing import Any
from typing import Dict from typing import Dict
from typing import Set from typing import Set
from ytdl_sub.entries.script.function_scripts import CUSTOM_FUNCTION_SCRIPTS
from ytdl_sub.entries.script.variable_scripts import UNRESOLVED_VARIABLES from ytdl_sub.entries.script.variable_scripts import UNRESOLVED_VARIABLES
from ytdl_sub.entries.script.variable_scripts import VARIABLE_SCRIPTS from ytdl_sub.entries.script.variable_scripts import VARIABLE_SCRIPTS
from ytdl_sub.script.script import Script from ytdl_sub.script.script import Script
@ -12,7 +13,9 @@ from ytdl_sub.utils.script import ScriptUtils
class Scriptable(ABC): class Scriptable(ABC):
def __init__(self): def __init__(self):
self.script = Script(copy.deepcopy(VARIABLE_SCRIPTS)) self.script = Script(
dict(copy.deepcopy(VARIABLE_SCRIPTS), **copy.deepcopy(CUSTOM_FUNCTION_SCRIPTS))
)
self.unresolvable: Set[str] = copy.deepcopy(UNRESOLVED_VARIABLES) self.unresolvable: Set[str] = copy.deepcopy(UNRESOLVED_VARIABLES)
def update_script(self) -> None: def update_script(self) -> None:

View file

@ -19,7 +19,7 @@ from resources import file_fixture_path
from yt_dlp.utils import sanitize_filename from yt_dlp.utils import sanitize_filename
from ytdl_sub.config.config_file import ConfigFile from ytdl_sub.config.config_file import ConfigFile
from ytdl_sub.entries.script.function_scripts import CustomFunctions from ytdl_sub.entries.script.custom_functions import CustomFunctions
from ytdl_sub.subscriptions.subscription_download import SubscriptionDownload from ytdl_sub.subscriptions.subscription_download import SubscriptionDownload
from ytdl_sub.utils.file_handler import FileHandler from ytdl_sub.utils.file_handler import FileHandler
from ytdl_sub.utils.logger import Logger from ytdl_sub.utils.logger import Logger

View file

@ -15,21 +15,21 @@ from ytdl_sub.script.utils.exceptions import InvalidSyntaxException
class TestArray: class TestArray:
def test_return(self): def test_return(self):
assert Script({"array": "{['a', 3.14]}"}).resolve() == ScriptOutput( assert Script({"arr": "{['a', 3.14]}"}).resolve() == ScriptOutput(
{"array": Array([String("a"), Float(3.14)])} {"arr": Array([String("a"), Float(3.14)])}
) )
def test_return_as_str(self): def test_return_as_str(self):
assert Script({"array": "str: {['a', 3.14]}"}).resolve() == ScriptOutput( assert Script({"arr": "str: {['a', 3.14]}"}).resolve() == ScriptOutput(
{"array": String('str: ["a", 3.14]')} {"arr": String('str: ["a", 3.14]')}
) )
def test_nested_array(self): def test_nested_array(self):
assert Script( assert Script(
{"array": "{['level1', ['level2', ['level3', 'level3'], 'level2'], 'level1']}"} {"arr": "{['level1', ['level2', ['level3', 'level3'], 'level2'], 'level1']}"}
).resolve() == ScriptOutput( ).resolve() == ScriptOutput(
{ {
"array": Array( "arr": Array(
[ [
String("level1"), String("level1"),
Array( Array(
@ -55,7 +55,7 @@ class TestArray:
], ],
) )
def test_empty(self, array: str): def test_empty(self, array: str):
assert Script({"array": array}).resolve() == ScriptOutput({"array": Array([])}) assert Script({"arr": array}).resolve() == ScriptOutput({"arr": Array([])})
@pytest.mark.parametrize( @pytest.mark.parametrize(
"array", "array",
@ -73,7 +73,7 @@ class TestArray:
InvalidSyntaxException, InvalidSyntaxException,
match=re.escape(str(_UNEXPECTED_COMMA_ARGUMENT(ParsedArgType.ARRAY))), match=re.escape(str(_UNEXPECTED_COMMA_ARGUMENT(ParsedArgType.ARRAY))),
): ):
Script({"array": array}).resolve() Script({"arr": array}).resolve()
@pytest.mark.parametrize( @pytest.mark.parametrize(
"array", "array",
@ -90,7 +90,7 @@ class TestArray:
InvalidSyntaxException, InvalidSyntaxException,
match=re.escape(str(_UNEXPECTED_CHAR_ARGUMENT(ParsedArgType.ARRAY))), match=re.escape(str(_UNEXPECTED_CHAR_ARGUMENT(ParsedArgType.ARRAY))),
): ):
assert Script({"array": array}).resolve() assert Script({"arr": array}).resolve()
@pytest.mark.parametrize( @pytest.mark.parametrize(
"array", "array",
@ -104,7 +104,7 @@ class TestArray:
InvalidSyntaxException, InvalidSyntaxException,
match=re.escape(str(_UNEXPECTED_CHAR_ARGUMENT(ParsedArgType.SCRIPT))), match=re.escape(str(_UNEXPECTED_CHAR_ARGUMENT(ParsedArgType.SCRIPT))),
): ):
assert Script({"array": array}).resolve() assert Script({"arr": array}).resolve()
def test_custom_function(self): def test_custom_function(self):
assert Script( assert Script(

View file

@ -40,7 +40,7 @@ class TestFunction:
with pytest.raises( with pytest.raises(
IncompatibleFunctionArguments, IncompatibleFunctionArguments,
match=_incompatible_arguments_match( match=_incompatible_arguments_match(
expected="Map, Hashable, Optional[AnyArgument]", expected="Map, AnyArgument, Optional[AnyArgument]",
recieved="%if(...)->Union[Array, Map], String", recieved="%if(...)->Union[Array, Map], String",
), ),
): ):

View file

@ -20,13 +20,13 @@ from ytdl_sub.script.utils.exceptions import KeyNotHashableRuntimeException
class TestMap: class TestMap:
def test_return(self): def test_return(self):
assert Script({"map": "{{'a': 3.14}}"}).resolve() == ScriptOutput( assert Script({"dict": "{{'a': 3.14}}"}).resolve() == ScriptOutput(
{"map": Map({String("a"): Float(3.14)})} {"dict": Map({String("a"): Float(3.14)})}
) )
def test_return_as_str(self): def test_return_as_str(self):
assert Script({"map": "json: {{'a': 3.14}}"}).resolve() == ScriptOutput( assert Script({"dict": "json: {{'a': 3.14}}"}).resolve() == ScriptOutput(
{"map": String('json: {"a": 3.14}')} {"dict": String('json: {"a": 3.14}')}
) )
def test_nested_map(self): def test_nested_map(self):
@ -45,9 +45,9 @@ class TestMap:
} }
}""" }"""
assert Script({"map": map_str}).resolve() == ScriptOutput( assert Script({"dict": map_str}).resolve() == ScriptOutput(
{ {
"map": Map( "dict": Map(
{ {
String("level1"): Map( String("level1"): Map(
{ {
@ -78,7 +78,7 @@ class TestMap:
], ],
) )
def test_empty_map(self, empty_map: str): def test_empty_map(self, empty_map: str):
assert Script({"map": empty_map}).resolve() == ScriptOutput({"map": Map({})}) assert Script({"dict": empty_map}).resolve() == ScriptOutput({"dict": Map({})})
@pytest.mark.parametrize( @pytest.mark.parametrize(
"map", "map",
@ -93,7 +93,7 @@ class TestMap:
) )
def test_map_not_closed(self, map: str): def test_map_not_closed(self, map: str):
with pytest.raises(InvalidSyntaxException, match=re.escape(str(BRACKET_NOT_CLOSED))): with pytest.raises(InvalidSyntaxException, match=re.escape(str(BRACKET_NOT_CLOSED))):
Script({"map": map}).resolve() Script({"dict": map}).resolve()
@pytest.mark.parametrize( @pytest.mark.parametrize(
"value", "value",
@ -108,7 +108,7 @@ class TestMap:
) )
def test_key_has_no_value(self, value: str): def test_key_has_no_value(self, value: str):
with pytest.raises(InvalidSyntaxException, match=re.escape(str(MAP_KEY_WITH_NO_VALUE))): with pytest.raises(InvalidSyntaxException, match=re.escape(str(MAP_KEY_WITH_NO_VALUE))):
Script({"map": value}).resolve() Script({"dict": value}).resolve()
@pytest.mark.parametrize( @pytest.mark.parametrize(
"value", "value",
@ -124,7 +124,7 @@ class TestMap:
InvalidSyntaxException, InvalidSyntaxException,
match=re.escape(str(_UNEXPECTED_COMMA_ARGUMENT(ParsedArgType.MAP_KEY))), match=re.escape(str(_UNEXPECTED_COMMA_ARGUMENT(ParsedArgType.MAP_KEY))),
): ):
Script({"map": value}).resolve() Script({"dict": value}).resolve()
@pytest.mark.parametrize( @pytest.mark.parametrize(
"value", "value",
@ -136,7 +136,7 @@ class TestMap:
) )
def test_map_multiple_keys(self, value: str): def test_map_multiple_keys(self, value: str):
with pytest.raises(InvalidSyntaxException, match=re.escape(str(MAP_KEY_MULTIPLE_VALUES))): with pytest.raises(InvalidSyntaxException, match=re.escape(str(MAP_KEY_MULTIPLE_VALUES))):
Script({"map": value}).resolve() Script({"dict": value}).resolve()
@pytest.mark.parametrize( @pytest.mark.parametrize(
"value", "value",
@ -148,7 +148,7 @@ class TestMap:
) )
def test_map_missing_key(self, value: str): def test_map_missing_key(self, value: str):
with pytest.raises(InvalidSyntaxException, match=re.escape(str(MAP_MISSING_KEY))): with pytest.raises(InvalidSyntaxException, match=re.escape(str(MAP_MISSING_KEY))):
Script({"map": value}).resolve() Script({"dict": value}).resolve()
@pytest.mark.parametrize( @pytest.mark.parametrize(
"value", "value",
@ -161,18 +161,18 @@ class TestMap:
) )
def test_map_key_not_hashable(self, value: str): def test_map_key_not_hashable(self, value: str):
with pytest.raises(InvalidSyntaxException, match=re.escape(str(MAP_KEY_NOT_HASHABLE))): with pytest.raises(InvalidSyntaxException, match=re.escape(str(MAP_KEY_NOT_HASHABLE))):
Script({"map": value}).resolve() Script({"dict": value}).resolve()
def test_map_key_is_hashable_variable(self): def test_map_key_is_hashable_variable(self):
assert Script( assert Script(
{ {
"map": "{{key_variable : 'value' }}", "dict": "{{key_variable : 'value' }}",
"key_variable": "hashable", "key_variable": "hashable",
} }
).resolve() == ScriptOutput( ).resolve() == ScriptOutput(
{ {
"key_variable": String("hashable"), "key_variable": String("hashable"),
"map": Map({String("hashable"): String("value")}), "dict": Map({String("hashable"): String("value")}),
} }
) )
@ -183,7 +183,7 @@ class TestMap:
): ):
Script( Script(
{ {
"map": "{{key_variable : 'value' }}", "dict": "{{key_variable : 'value' }}",
"key_variable": "{['non-hashable']}", "key_variable": "{['non-hashable']}",
} }
).resolve() ).resolve()