mirror of
https://github.com/nwithan8/plex-prerolls
synced 2024-08-30 16:52:17 +00:00
adding Cerberus YAML validation checks,
additional logging
This commit is contained in:
parent
eadb86933b
commit
bd891ccf88
@ -131,9 +131,9 @@ date_range:
|
||||
path: /path/to/video.mp4
|
||||
weekly:
|
||||
enabled: (yes/no)
|
||||
1: /path/to/file(s)
|
||||
"1": /path/to/file(s)
|
||||
...
|
||||
52: /path/to/file(s)
|
||||
"52": /path/to/file(s)
|
||||
misc:
|
||||
enabled: (yes/no)
|
||||
always_use: /path/to/file(s)
|
||||
|
@ -1,22 +1,22 @@
|
||||
---
|
||||
# all Key items must be in lowercase
|
||||
# all PATH entries will be case sensitive based on your Environment (Linux, Windows)
|
||||
monthly:
|
||||
monthly:
|
||||
# If enabled, List out each month (lowercase;3-char abreviation) (optional)
|
||||
# dont have to have all the months here, script will skip but log a Warning
|
||||
enabled: Yes
|
||||
jan: /path/to/video.mp4
|
||||
feb:
|
||||
mar:
|
||||
apr:
|
||||
May: /path/to/video.mp4
|
||||
jun:
|
||||
jul:
|
||||
aug:
|
||||
feb:
|
||||
mar:
|
||||
apr:
|
||||
may: /path/to/video.mp4
|
||||
jun:
|
||||
jul:
|
||||
aug:
|
||||
sep: /path/to/video.mp4;/path/to/video.mp4
|
||||
oct:
|
||||
nov:
|
||||
dec:
|
||||
oct:
|
||||
nov:
|
||||
dec:
|
||||
date_range:
|
||||
# If enabled, use a list of various date ranges/timeframes for different lists of pre-rolls
|
||||
# list must be under ranges:
|
||||
@ -38,11 +38,12 @@ date_range:
|
||||
path: /path/to/holiday_video.mp4
|
||||
weekly:
|
||||
# If enabled, list any weeks of the year to have specific prerolls 1-52 (optional)
|
||||
# Done need to have all the week numbers; the script skips over missing entries
|
||||
# Dont need to have all the week numbers; the script skips over missing entries
|
||||
# each week number must be surrounded by quotes
|
||||
enabled: No
|
||||
1:
|
||||
2: /path/to/video.mkv
|
||||
52:
|
||||
"1":
|
||||
"2": /path/to/video.mkv
|
||||
"52":
|
||||
misc:
|
||||
# If enabled, additional items for preroll selecton processing
|
||||
# always_use: If enabled, always include these video files in pre-role listing
|
||||
|
@ -1,4 +1,5 @@
|
||||
plexapi==4.13.*
|
||||
configparser==5.0.*
|
||||
requests==2.25.*
|
||||
pyyaml==5.3.*
|
||||
pyyaml==5.3.*
|
||||
cerberus==1.3.*
|
@ -33,6 +33,8 @@ Raises:
|
||||
FileNotFoundError: [description]
|
||||
"""
|
||||
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
@ -43,10 +45,12 @@ from typing import Dict, List, NamedTuple, Optional, Tuple, Union
|
||||
import requests
|
||||
import urllib3
|
||||
import yaml
|
||||
from cerberus import Validator # type: ignore
|
||||
from cerberus.schema import SchemaError
|
||||
from plexapi.server import PlexServer
|
||||
|
||||
# import local util modules
|
||||
import plexutil
|
||||
from util import plexutil
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -73,7 +77,7 @@ def arguments() -> Namespace:
|
||||
argparse.Namespace: Namespace object
|
||||
"""
|
||||
description = "Automate scheduling of pre-roll intros for Plex"
|
||||
version = "0.10.1"
|
||||
version = "0.12.0"
|
||||
|
||||
config_default = "./config.ini"
|
||||
log_config_default = "./logging.conf"
|
||||
@ -274,6 +278,82 @@ def make_datetime(value: Union[str, date, datetime], lowtime: bool = True) -> da
|
||||
return dt_val
|
||||
|
||||
|
||||
def schedulefile_contents(schedule_filename: Optional[str]) -> dict[str, any]: # type: ignore
|
||||
"""Returns a contents of the provided YAML file and validates
|
||||
|
||||
Args:
|
||||
schedule_filename (string, optional]): schedule file to load, will use defaults if not specified
|
||||
|
||||
Raises:
|
||||
Validation Errors
|
||||
|
||||
Returns:
|
||||
YAML Contents: YAML structure of Dict[str, Any]
|
||||
"""
|
||||
default_files = ["preroll_schedules.yaml", "preroll_schedules.yml"]
|
||||
|
||||
filename = None
|
||||
if schedule_filename not in ("", None):
|
||||
if os.path.exists(str(schedule_filename)):
|
||||
filename = schedule_filename
|
||||
else:
|
||||
msg = f'Pre-roll Schedule file "{schedule_filename}" not found'
|
||||
logger.error(msg)
|
||||
raise FileNotFoundError(msg)
|
||||
else:
|
||||
for f in default_files:
|
||||
if os.path.exists(f):
|
||||
filename = f
|
||||
break
|
||||
|
||||
# if we still cant find a schedule file, we abort
|
||||
if not filename:
|
||||
filestr = '" / "'.join(default_files)
|
||||
logger.error('Missing schedule file: "%s"', filestr)
|
||||
raise FileNotFoundError(filestr)
|
||||
|
||||
schema_filename = os.path.abspath("util/schedulefile_schema.json")
|
||||
|
||||
# make sure the Schema validation file is available
|
||||
if not os.path.exists(str(schema_filename)):
|
||||
msg = f'Pre-roll Schema Validation file "{schema_filename}" not found'
|
||||
logger.error(msg)
|
||||
raise FileNotFoundError(msg)
|
||||
|
||||
# Open Schedule file
|
||||
try:
|
||||
with open(filename, "r", encoding="utf8") as file:
|
||||
contents = yaml.load(file, Loader=yaml.SafeLoader) # type: ignore
|
||||
except yaml.YAMLError as ye:
|
||||
logger.error("YAML Error: %s", filename, exc_info=ye)
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(e, exc_info=e)
|
||||
raise
|
||||
|
||||
# Validate the loaded YAML data against the required schema
|
||||
try:
|
||||
with open(schema_filename, "r", encoding="utf8") as schema_file:
|
||||
schema = json.loads(schema_file.read())
|
||||
|
||||
v = Validator(schema) # type: ignore
|
||||
except json.JSONDecodeError as je:
|
||||
logger.error("JSON Error: %s", os.path.abspath("schedule_schema.json"), exc_info=je)
|
||||
raise
|
||||
except SchemaError as se:
|
||||
logger.error("Schema Error %s", os.path.abspath("schedule_schema.json"), exc_info=se)
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(e, exc_info=e)
|
||||
raise
|
||||
|
||||
if not v.validate(contents): # type: ignore
|
||||
logger.error("Preroll-Schedule YAML Validation Error: %s", v.errors) # type: ignore
|
||||
raise yaml.YAMLError(f"Preroll-Schedule YAML Validation Error: {v.errors}") # type: ignore
|
||||
|
||||
return contents
|
||||
|
||||
|
||||
def preroll_schedule(schedule_file: Optional[str] = None) -> List[ScheduleEntry]:
|
||||
"""Return a listing of defined preroll schedules for searching/use
|
||||
|
||||
@ -286,41 +366,26 @@ def preroll_schedule(schedule_file: Optional[str] = None) -> List[ScheduleEntry]
|
||||
Returns:
|
||||
list: list of ScheduleEntries
|
||||
"""
|
||||
default_files = ["preroll_schedules.yaml", "preroll_schedules.yml"]
|
||||
|
||||
filename = None
|
||||
if schedule_file not in ("", None):
|
||||
if os.path.exists(str(schedule_file)):
|
||||
filename = schedule_file
|
||||
else:
|
||||
msg = f'Pre-roll Schedule file "{schedule_file}" not found'
|
||||
raise FileNotFoundError(msg)
|
||||
else:
|
||||
for f in default_files:
|
||||
if os.path.exists(f):
|
||||
filename = f
|
||||
break
|
||||
|
||||
# if we still cant find a schedule file, we abort
|
||||
if not filename:
|
||||
filestr = '" / "'.join(default_files)
|
||||
logger.critical('Missing schedule file: "%s"', filestr)
|
||||
raise FileNotFoundError(filestr)
|
||||
|
||||
with open(filename, "r") as file:
|
||||
contents = yaml.load(file, Loader=yaml.SafeLoader) # type: ignore
|
||||
contents = schedulefile_contents(schedule_file) # type: ignore
|
||||
|
||||
today = date.today()
|
||||
schedule: List[ScheduleEntry] = []
|
||||
for schedule_section in schedule_types():
|
||||
# test if section exists
|
||||
try:
|
||||
section_contents = contents[schedule_section] # type: ignore
|
||||
except KeyError:
|
||||
logger.info('"%s" section not included in schedule file', schedule_section)
|
||||
# continue to other sections
|
||||
continue
|
||||
|
||||
if schedule_section == "weekly":
|
||||
try:
|
||||
use = contents[schedule_section]["enabled"]
|
||||
|
||||
if use:
|
||||
if section_contents["enabled"]:
|
||||
for i in range(1, 53):
|
||||
try:
|
||||
path = contents[schedule_section][i]
|
||||
path = str(section_contents[i]) # type: ignore
|
||||
|
||||
if path:
|
||||
start, end = week_range(today.year, i)
|
||||
@ -341,19 +406,16 @@ def preroll_schedule(schedule_file: Optional[str] = None) -> List[ScheduleEntry]
|
||||
schedule_section,
|
||||
i,
|
||||
)
|
||||
pass
|
||||
except KeyError as ke:
|
||||
logger.error('Key Value not found in "%s" section', schedule_section, exc_info=ke)
|
||||
raise
|
||||
elif schedule_section == "monthly":
|
||||
try:
|
||||
use = contents[schedule_section]["enabled"]
|
||||
|
||||
if use:
|
||||
if section_contents["enabled"]:
|
||||
for i in range(1, 13):
|
||||
month_abrev = date(today.year, i, 1).strftime("%b").lower()
|
||||
try:
|
||||
path = contents[schedule_section][month_abrev]
|
||||
path = str(section_contents[month_abrev]) # type: ignore
|
||||
|
||||
if path:
|
||||
start, end = month_range(today.year, i)
|
||||
@ -374,32 +436,29 @@ def preroll_schedule(schedule_file: Optional[str] = None) -> List[ScheduleEntry]
|
||||
schedule_section,
|
||||
month_abrev,
|
||||
)
|
||||
pass
|
||||
except KeyError as ke:
|
||||
logger.error('Key Value not found in "%s" section', schedule_section, exc_info=ke)
|
||||
raise
|
||||
elif schedule_section == "date_range":
|
||||
try:
|
||||
use = contents[schedule_section]["enabled"]
|
||||
if use:
|
||||
for r in contents[schedule_section]["ranges"]:
|
||||
if section_contents["enabled"]:
|
||||
for r in section_contents["ranges"]: # type: ignore
|
||||
try:
|
||||
path = r["path"]
|
||||
path = str(r["path"]) # type: ignore
|
||||
|
||||
if path:
|
||||
try:
|
||||
force = r["force"]
|
||||
force = r["force"] # type: ignore
|
||||
except KeyError as ke:
|
||||
# special case Optional, ignore
|
||||
force = False
|
||||
pass
|
||||
|
||||
start = make_datetime(r["start_date"], lowtime=True)
|
||||
end = make_datetime(r["end_date"], lowtime=False)
|
||||
start = make_datetime(r["start_date"], lowtime=True) # type: ignore
|
||||
end = make_datetime(r["end_date"], lowtime=False) # type: ignore
|
||||
|
||||
entry = ScheduleEntry(
|
||||
type=schedule_section,
|
||||
force=force,
|
||||
force=force, # type: ignore
|
||||
startdate=start,
|
||||
enddate=end,
|
||||
path=path,
|
||||
@ -409,15 +468,20 @@ def preroll_schedule(schedule_file: Optional[str] = None) -> List[ScheduleEntry]
|
||||
except KeyError as ke:
|
||||
logger.error('Key Value not found for entry: "%s"', entry, exc_info=ke) # type: ignore
|
||||
raise
|
||||
except TypeError as te:
|
||||
logger.error('Type Error "%s" Entry: "%s"', te, entry, exc_info=te) # type: ignore
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error('Exception: %s %s Entry: "%s"', type(e), e, entry, exc_info=e) # type: ignore
|
||||
raise
|
||||
except KeyError as ke:
|
||||
logger.error('Key Value not found in "%s" section', schedule_section, exc_info=ke)
|
||||
raise
|
||||
elif schedule_section == "misc":
|
||||
try:
|
||||
use = contents[schedule_section]["enabled"]
|
||||
if use:
|
||||
if section_contents["enabled"]:
|
||||
try:
|
||||
path = contents[schedule_section]["always_use"]
|
||||
path = str(section_contents["always_use"]) # type: ignore
|
||||
|
||||
if path:
|
||||
entry = ScheduleEntry(
|
||||
@ -438,10 +502,9 @@ def preroll_schedule(schedule_file: Optional[str] = None) -> List[ScheduleEntry]
|
||||
raise
|
||||
elif schedule_section == "default":
|
||||
try:
|
||||
use = contents[schedule_section]["enabled"]
|
||||
if use:
|
||||
if section_contents["enabled"]:
|
||||
try:
|
||||
path = contents[schedule_section]["path"]
|
||||
path = str(section_contents["path"]) # type: ignore
|
||||
|
||||
if path:
|
||||
entry = ScheduleEntry(
|
||||
@ -480,6 +543,10 @@ def build_listing_string(items: List[str], play_all: bool = False) -> str:
|
||||
Returns:
|
||||
string: CSV Listing (, or ;) based on play_all param of preroll video paths
|
||||
"""
|
||||
|
||||
if len(items) == 0:
|
||||
return ";"
|
||||
|
||||
if play_all:
|
||||
# use , to play all entries
|
||||
listing = ",".join(items)
|
||||
|
414
util/schedulefile_schema.json
Normal file
414
util/schedulefile_schema.json
Normal file
@ -0,0 +1,414 @@
|
||||
{
|
||||
"monthly": {
|
||||
"required": false,
|
||||
"type": "dict",
|
||||
"schema": {
|
||||
"enabled": {
|
||||
"required": true,
|
||||
"type": "boolean"
|
||||
},
|
||||
"jan": {
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"feb": {
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"mar": {
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"apr": {
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"may": {
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"jun": {
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"jul": {
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"aug": {
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"sep": {
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"oct": {
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"nov": {
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"dec": {
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"weekly": {
|
||||
"required": false,
|
||||
"type": "dict",
|
||||
"schema": {
|
||||
"enabled": {
|
||||
"required": true,
|
||||
"type": "boolean"
|
||||
},
|
||||
"1": {
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"2": {
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"3": {
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"4": {
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"5": {
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"6": {
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"7": {
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"8": {
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"9": {
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"10": {
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"11": {
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"12": {
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"13": {
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"14": {
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"15": {
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"16": {
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"17": {
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"18": {
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"19": {
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"20": {
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"21": {
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"22": {
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"23": {
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"24": {
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"25": {
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"26": {
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"27": {
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"28": {
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"29": {
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"30": {
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"31": {
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"32": {
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"33": {
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"34": {
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"35": {
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"36": {
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"37": {
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"38": {
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"39": {
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"40": {
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"41": {
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"42": {
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"43": {
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"44": {
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"45": {
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"46": {
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"47": {
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"48": {
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"49": {
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"50": {
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"51": {
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"52": {
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"date_range": {
|
||||
"required": false,
|
||||
"type": "dict",
|
||||
"schema": {
|
||||
"enabled": {
|
||||
"required": true,
|
||||
"type": "boolean"
|
||||
},
|
||||
"ranges": {
|
||||
"required": true,
|
||||
"type": "list",
|
||||
"schema": {
|
||||
"type": "dict",
|
||||
"schema": {
|
||||
"start_date": {
|
||||
"required": true,
|
||||
"type": [
|
||||
"string",
|
||||
"date"
|
||||
]
|
||||
},
|
||||
"end_date": {
|
||||
"required": true,
|
||||
"type": [
|
||||
"string",
|
||||
"date"
|
||||
]
|
||||
},
|
||||
"path": {
|
||||
"required": true,
|
||||
"type": "string"
|
||||
},
|
||||
"force": {
|
||||
"required": false,
|
||||
"type": "boolean",
|
||||
"nullable": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"misc": {
|
||||
"required": false,
|
||||
"type": "dict",
|
||||
"schema": {
|
||||
"enabled": {
|
||||
"required": true,
|
||||
"type": "boolean"
|
||||
},
|
||||
"always_use": {
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"required": false,
|
||||
"type": "dict",
|
||||
"schema": {
|
||||
"enabled": {
|
||||
"required": true,
|
||||
"type": "boolean"
|
||||
},
|
||||
"path": {
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user