diff --git a/README.md b/README.md index 4c38e44..25b6be4 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ import pystream if __name__ == '__main__': kwargs = dict( - authorization=[{"Alan Turing": "Pr0gRamM1ng"}, {"Linus Torvalds": "LinuxOS"}], + authorization={"Alan Turing": "Pr0gRamM1ng", "Linus Torvalds": "LinuxOS"}, video_source=os.path.join(os.path.expanduser('~'), 'Downloads') ) # Add the following to host on local IP address, skip for localhost (127.0.0.1) @@ -37,7 +37,7 @@ if __name__ == '__main__': > To use custom filenames, set the env var `env_file` as `key` and the _filename_ as its `value` **Mandatory** -- **AUTHORIZATION**: List of dictionaries with `username` as key and `password` as value. +- **AUTHORIZATION**: Dictionary of key-value pairs with `username` as key and `password` as value. - **VIDEO_SOURCE**: Source path for video files. > :bulb:   Files starting with `_` _(underscore)_ and `.` _(dot)_ will be ignored diff --git a/docs/README.html b/docs/README.html index 1c5f0eb..57f3bdf 100644 --- a/docs/README.html +++ b/docs/README.html @@ -70,7 +70,7 @@

Sample Usageif __name__ == '__main__': kwargs = dict( - authorization=[{"Alan Turing": "Pr0gRamM1ng"}, {"Linus Torvalds": "LinuxOS"}], + authorization={"Alan Turing": "Pr0gRamM1ng", "Linus Torvalds": "LinuxOS"}, video_source=os.path.join(os.path.expanduser('~'), 'Downloads') ) # Add the following to host on local IP address, skip for localhost (127.0.0.1) @@ -86,7 +86,7 @@

Env Variables -
  • AUTHORIZATION: List of dictionaries with username as key and password as value.

  • +
  • AUTHORIZATION: Dictionary of key-value pairs with username as key and password as value.

  • VIDEO_SOURCE: Source path for video files.

  • diff --git a/docs/README.md b/docs/README.md index 4c38e44..25b6be4 100644 --- a/docs/README.md +++ b/docs/README.md @@ -24,7 +24,7 @@ import pystream if __name__ == '__main__': kwargs = dict( - authorization=[{"Alan Turing": "Pr0gRamM1ng"}, {"Linus Torvalds": "LinuxOS"}], + authorization={"Alan Turing": "Pr0gRamM1ng", "Linus Torvalds": "LinuxOS"}, video_source=os.path.join(os.path.expanduser('~'), 'Downloads') ) # Add the following to host on local IP address, skip for localhost (127.0.0.1) @@ -37,7 +37,7 @@ if __name__ == '__main__': > To use custom filenames, set the env var `env_file` as `key` and the _filename_ as its `value` **Mandatory** -- **AUTHORIZATION**: List of dictionaries with `username` as key and `password` as value. +- **AUTHORIZATION**: Dictionary of key-value pairs with `username` as key and `password` as value. - **VIDEO_SOURCE**: Source path for video files. > :bulb:   Files starting with `_` _(underscore)_ and `.` _(dot)_ will be ignored diff --git a/docs/_sources/README.md.txt b/docs/_sources/README.md.txt index 4c38e44..25b6be4 100644 --- a/docs/_sources/README.md.txt +++ b/docs/_sources/README.md.txt @@ -24,7 +24,7 @@ import pystream if __name__ == '__main__': kwargs = dict( - authorization=[{"Alan Turing": "Pr0gRamM1ng"}, {"Linus Torvalds": "LinuxOS"}], + authorization={"Alan Turing": "Pr0gRamM1ng", "Linus Torvalds": "LinuxOS"}, video_source=os.path.join(os.path.expanduser('~'), 'Downloads') ) # Add the following to host on local IP address, skip for localhost (127.0.0.1) @@ -37,7 +37,7 @@ if __name__ == '__main__': > To use custom filenames, set the env var `env_file` as `key` and the _filename_ as its `value` **Mandatory** -- **AUTHORIZATION**: List of dictionaries with `username` as key and `password` as value. +- **AUTHORIZATION**: Dictionary of key-value pairs with `username` as key and `password` as value. - **VIDEO_SOURCE**: Source path for video files. > :bulb:   Files starting with `_` _(underscore)_ and `.` _(dot)_ will be ignored diff --git a/docs/genindex.html b/docs/genindex.html index 978afc1..fa1d2d4 100644 --- a/docs/genindex.html +++ b/docs/genindex.html @@ -68,6 +68,8 @@

    A

      @@ -296,6 +298,8 @@

      N

      P

      + -
      +
      • pystream.models.secure @@ -346,8 +352,6 @@

        P

      • module
      -
      • pystream.models.squire @@ -482,10 +486,6 @@

        U

        -
        diff --git a/docs/index.html b/docs/index.html index 0c0c7bd..2e2ab52 100644 --- a/docs/index.html +++ b/docs/index.html @@ -169,9 +169,26 @@

        Models

        Config

        +
        +
        +pystream.models.config.as_dict(pairs: List[Tuple[str, str]]) Dict[str, SecretStr]
        +

        Custom decoder for json.loads passed via object_pairs_hook raising error on duplicate keys.

        +
        +
        Parameters:
        +

        pairs – Takes the ordered list of pairs as an argument.

        +
        +
        Returns:
        +

        A dictionary of key as string and value as a secret.

        +
        +
        Return type:
        +

        Dict[str, SecretStr]

        +
        +
        +
        +
        -class pystream.models.config.EnvConfig(_case_sensitive: bool | None = None, _env_prefix: str | None = None, _env_file: DotenvType | None = PosixPath('.'), _env_file_encoding: str | None = None, _env_nested_delimiter: str | None = None, _secrets_dir: str | Path | None = None, *, authorization: List[Dict[str, SecretStr]], video_source: Path, users_allowed: List[str] = [], video_host: IPv4Address = '127.0.0.1', video_port: int = 8000, session_duration: int = 3600, file_formats: Sequence[str] = ('.mov', '.mp4'), workers: int = 1, website: Optional[List[str]] = [], auto_thumbnail: bool = True)
        +class pystream.models.config.EnvConfig(_case_sensitive: bool | None = None, _env_prefix: str | None = None, _env_file: DotenvType | None = PosixPath('.'), _env_file_encoding: str | None = None, _env_nested_delimiter: str | None = None, _secrets_dir: str | Path | None = None, *, authorization: Any, video_source: Path, video_host: IPv4Address = '127.0.0.1', video_port: int = 8000, session_duration: int = 3600, file_formats: Sequence[str] = ('.mov', '.mp4'), workers: int = 1, website: Optional[List[str]] = [], auto_thumbnail: bool = True)

        Configure all env vars and validate using pydantic to share across modules.

        >>> EnvConfig
         
        @@ -183,7 +200,7 @@

        Modelsself as a field name.

        -authorization: List[Dict[str, SecretStr]]
        +authorization: Any
        @@ -191,11 +208,6 @@

        Modelsvideo_source: Path

        -
        -
        -users_allowed: List[str]
        -
        -
        video_host: IPv4Address
        @@ -257,6 +269,12 @@

        Models

        +
        +
        +classmethod parse_authorization(value: Any) Dict[str, SecretStr]
        +

        Validates the authorization parameter.

        +
        +
        classmethod parse_video_host(value: IPv4Address) str
        diff --git a/docs/objects.inv b/docs/objects.inv index a768593..7775b9d 100644 Binary files a/docs/objects.inv and b/docs/objects.inv differ diff --git a/docs/searchindex.js b/docs/searchindex.js index 5bd9947..a315ad6 100644 --- a/docs/searchindex.js +++ b/docs/searchindex.js @@ -1 +1 @@ -Search.setIndex({"docnames": ["README", "authentication", "index"], "filenames": ["README.md", "authentication.md", "index.rst"], "titles": ["PyStream", "Authentication", "Stream-Localhost - A secured interface to stream videos"], "terms": {"deploy": 0, "python": 0, "modul": 0, "stream": [0, 1], "video": 0, "via": 0, "authent": 0, "session": [0, 2], "us": [0, 1, 2], "fastapi": [0, 1, 2], "m": 0, "pip": 0, "localhost": 0, "import": 0, "asyncio": 0, "o": 0, "__name__": 0, "__main__": 0, "kwarg": [0, 2], "dict": [0, 2], "author": [0, 1, 2], "alan": 0, "ture": 0, "pr0gramm1ng": 0, "linu": 0, "torvald": 0, "linuxo": 0, "video_sourc": [0, 2], "path": [0, 2], "join": 0, "expandus": 0, "download": 0, "add": [0, 2], "follow": [0, 1], "host": [0, 2], "local": [0, 2], "ip": [0, 2], "address": [0, 2], "skip": 0, "127": [0, 2], "0": [0, 2], "1": [0, 2], "video_host": [0, 2], "util": 0, "get_local_ip": [0, 2], "run": [0, 2], "start": [0, 2], "bulb": 0, "environ": [0, 2], "can": [0, 1], "load": [0, 2], "from": [0, 1, 2], "ani": [0, 2], "file": [0, 2], "filenam": [0, 2], "default": 0, "To": 0, "custom": [0, 2], "set": [0, 2], "var": [0, 2], "env_fil": [0, 2], "kei": [0, 1, 2], "its": [0, 2], "valu": [0, 1, 2], "mandatori": 0, "list": [0, 2], "dictionari": [0, 2], "usernam": [0, 2], "password": [0, 2], "_": 0, "sourc": [0, 2], "underscor": 0, "dot": 0, "ignor": [0, 2], "option": [0, 2], "port": 0, "number": [0, 2], "applic": 0, "8000": [0, 2], "format": 0, "sequenc": [0, 2], "support": 0, "mp4": [0, 2], "mov": [0, 2], "worker": [0, 2], "spin": 0, "up": 0, "uvicorn": 0, "server": [0, 1, 2], "websit": [0, 2], "regex": 0, "cor": 0, "configur": [0, 2], "requir": [0, 2], "onli": [0, 1, 2], "tunnel": 0, "cdn": 0, "auto": 0, "thumbnail": [0, 2], "boolean": [0, 2], "flag": [0, 2], "gener": [0, 1, 2], "imag": 0, "preview": [0, 2], "true": [0, 2], "docstr": 0, "googl": 0, "style": 0, "convent": 0, "pep": 0, "8": 0, "isort": 0, "gitvers": 0, "revers": 0, "f": 0, "release_not": 0, "rst": 0, "t": [0, 2], "pre": 0, "commit": 0, "ensur": 0, "pytest": 0, "valid": [0, 2], "hyperlink": 0, "all": [0, 1, 2], "markdown": 0, "includ": [0, 1, 2], "wiki": 0, "page": [0, 1, 2], "sphinx": 0, "5": 0, "recommonmark": 0, "http": [0, 2], "org": 0, "project": 0, "thevickypedia": 0, "github": 0, "io": 0, "vignesh": 0, "rao": 0, "under": 0, "mit": 0, "pystream": [1, 2], "two": 1, "wai": [1, 2], "gain": 1, "access": [1, 2], "session_token": [1, 2], "directori": [1, 2], "content": [1, 2], "signatur": 1, "ui": [1, 2], "creat": [1, 2], "hex": [1, 2], "nativ": 1, "j": [1, 2], "These": 1, "ar": [1, 2], "calcul": [1, 2], "hash": [1, 2], "i": [1, 2], "base64": [1, 2], "encod": [1, 2], "befor": [1, 2], "send": [1, 2], "api": [1, 2], "an": [1, 2], "header": [1, 2], "decod": [1, 2], "ascii": 1, "string": [1, 2], "receiv": [1, 2], "Then": 1, "broken": [1, 2], "down": 1, "timestamp": [1, 2], "The": [1, 2], "get": [1, 2], "store": [1, 2], "env": [1, 2], "variabl": [1, 2], "which": [1, 2], "compar": 1, "purpos": 1, "symmetr": [1, 2], "encrypt": [1, 2], "onc": 1, "login": [1, 2], "ha": [1, 2], "been": 1, "success": [1, 2], "randomli": 1, "64": [1, 2], "bit": [1, 2], "url": [1, 2], "safe": [1, 2], "thi": [1, 2], "uniqu": 1, "each": [1, 2], "user": [1, 2], "form": [1, 2], "payload": [1, 2], "cryptographi": [1, 2], "": [1, 2], "fernet": [1, 2], "retriev": [1, 2], "cooki": [1, 2], "jsonrespons": [1, 2], "redirect_url": 1, "sinc": [1, 2], "ajax": 1, "post": 1, "call": 1, "redirectrespons": [1, 2], "work": [1, 2], "simpli": 1, "redirect": [1, 2], "instead": [1, 2], "json": 1, "respons": [1, 2], "fetch": 1, "alter": 1, "locat": [1, 2], "href": 1, "transfer": 1, "new": [1, 2], "so": [1, 2], "lost": 1, "point": 1, "navig": 1, "carri": 1, "onward": 1, "instal": 2, "sampl": 2, "usag": 2, "code": 2, "standard": 2, "releas": 2, "note": 2, "lint": 2, "pypi": 2, "packag": 2, "runbook": 2, "licens": 2, "copyright": 2, "token": 2, "refer": 2, "async": 2, "redirect_exception_handl": 2, "request": 2, "except": 2, "redirectexcept": 2, "handler": 2, "handl": 2, "paramet": 2, "take": 2, "object": 2, "argument": 2, "inherit": 2, "return": 2, "statu": 2, "type": 2, "startup_task": 2, "none": 2, "task": 2, "need": 2, "dure": 2, "startup": 2, "shutdown_task": 2, "shutdown": 2, "starter": 2, "function": 2, "keyword": 2, "failed_auth_count": 2, "keep": 2, "track": 2, "fail": 2, "attempt": 2, "3": 2, "more": 2, "time": 2, "extract_credenti": 2, "str": 2, "extract": 2, "credenti": 2, "raise_error": 2, "noreturn": 2, "rais": 2, "401": 2, "unauthor": 2, "error": 2, "case": 2, "bad": 2, "verify_login": 2, "union": 2, "int": 2, "verifi": 2, "verify_token": 2, "decrypt": 2, "expir": 2, "class": 2, "envconfig": 2, "_case_sensit": 2, "bool": 2, "_env_prefix": 2, "_env_fil": 2, "dotenvtyp": 2, "posixpath": 2, "_env_file_encod": 2, "_env_nested_delimit": 2, "_secrets_dir": 2, "secretstr": 2, "users_allow": 2, "ipv4address": 2, "video_port": 2, "session_dur": 2, "3600": 2, "file_format": 2, "auto_thumbnail": 2, "pydant": 2, "share": 2, "across": 2, "pars": 2, "input": 2, "data": 2, "validationerror": 2, "pydantic_cor": 2, "cannot": 2, "__init__": 2, "__pydantic_self__": 2, "common": 2, "self": 2, "first": 2, "arg": 2, "allow": 2, "field": 2, "name": 2, "env_prefix": 2, "extra": 2, "hide_input_in_error": 2, "classmethod": 2, "parse_video_host": 2, "notion": 2, "parse_websit": 2, "evalu": 2, "fileio": 2, "index": 2, "html": 2, "land": 2, "static": 2, "query_param": 2, "home_endpoint": 2, "home": 2, "login_endpoint": 2, "logout_endpoint": 2, "logout": 2, "streaming_endpoint": 2, "chunk_siz": 2, "1048576": 2, "delet": 2, "pathlib": 2, "cipher_suit": 2, "arbitrary_types_allow": 2, "info": 2, "invalid": 2, "map": 2, "inform": 2, "webtoken": 2, "ecrypt": 2, "detail": 2, "within": 2, "httpexcept": 2, "doesn": 2, "demand": 2, "where": 2, "solut": 2, "There": 2, "altern": 2, "our": 2, "javascript": 2, "come": 2, "handi": 2, "mani": 2, "unexpect": 2, "scenario": 2, "tiangolo": 2, "com": 2, "tutori": 2, "instanti": 2, "reason": 2, "alia": 2, "filepath": 2, "initi": 2, "captur": 2, "frame": 2, "particular": 2, "opencv": 2, "videocaptur": 2, "generate_thumbnail": 2, "interv": 2, "output_dir": 2, "second": 2, "output": 2, "failur": 2, "get_video_length": 2, "tupl": 2, "timedelta": 2, "length": 2, "datetim": 2, "generate_preview": 2, "at_second": 2, "should": 2, "calculate_hash": 2, "given": 2, "base64_encod": 2, "base64_decod": 2, "hex_decod": 2, "convert": 2, "hex_encod": 2, "log_connect": 2, "log": 2, "connect": 2, "devic": 2, "avoid": 2, "multipl": 2, "when": 2, "same": 2, "differ": 2, "natural_sort_kei": 2, "sort": 2, "natur": 2, "element": 2, "deriv": 2, "split": 2, "part": 2, "regular": 2, "express": 2, "get_dir_stream_cont": 2, "parent": 2, "subdir": 2, "insid": 2, "displai": 2, "subdirectori": 2, "exist": 2, "pair": 2, "get_all_stream_cont": 2, "folder": 2, "contain": 2, "section": 2, "get_it": 2, "purepath": 2, "current": 2, "serv": 2, "previou": 2, "next": 2, "render": 2, "remove_thumbnail": 2, "img_path": 2, "trigger": 2, "timer": 2, "remov": 2, "keygen": 2, "secret": 2, "forc": 2, "restart": 2, "send_bytes_range_request": 2, "file_obj": 2, "binaryio": 2, "start_rang": 2, "end_rang": 2, "asynciter": 2, "bytestr": 2, "chunk": 2, "rang": 2, "specif": 2, "rfc7233": 2, "byte": 2, "end": 2, "yield": 2, "iter": 2, "get_range_head": 2, "range_head": 2, "file_s": 2, "proce": 2, "size": 2, "range_requests_respons": 2, "file_path": 2, "streamingrespons": 2, "srt_to_vtt": 2, "srt": 2, "vtt": 2, "compat": 2, "vtt_to_srt": 2, "auth": 2, "get_expiri": 2, "lease_start": 2, "lease_dur": 2, "expiri": 2, "max": 2, "ag": 2, "wa": 2, "made": 2, "until": 2, "date": 2, "gmt": 2, "home_pag": 2, "templaterespons": 2, "after": 2, "htmlrespons": 2, "termin": 2, "back": 2, "upon": 2, "refresh": 2, "base": 2, "get_favicon": 2, "filerespons": 2, "favicon": 2, "ico": 2, "endpoint": 2, "robinhood": 2, "script": 2, "root": 2, "relat": 2, "preview_load": 2, "setup": 2, "track_load": 2, "track_path": 2, "stream_video": 2, "video_path": 2, "templat": 2, "video_endpoint": 2, "rootfilt": 2, "filter": 2, "while": 2, "preserv": 2, "other": 2, "200": 2, "ok": 2, "307": 2, "temporari": 2, "vid_nam": 2, "redund": 2, "pass": 2, "overrid": 2, "implement": 2, "subclass": 2, "method": 2, "record": 2, "examin": 2, "fals": 2, "discard": 2, "togeth": 2, "children": 2, "have": 2, "event": 2, "through": 2, "If": 2, "specifi": 2, "everi": 2, "logrecord": 2, "out": 2, "repres": 2, "someth": 2, "simpl": 2, "check": 2, "network": 2, "id": 2, "privat": 2, "machin": 2, "get_public_ip": 2, "public": 2, "make": 2, "extern": 2, "search": 2}, "objects": {"pystream": [[2, 0, 0, "-", "logger"], [2, 0, 0, "-", "main"], [2, 0, 0, "-", "utils"]], "pystream.logger": [[2, 1, 1, "", "RootFilter"]], "pystream.logger.RootFilter": [[2, 2, 1, "", "filter"]], "pystream.main": [[2, 3, 1, "", "redirect_exception_handler"], [2, 3, 1, "", "shutdown_tasks"], [2, 3, 1, "", "start"], [2, 3, 1, "", "startup_tasks"]], "pystream.models": [[2, 0, 0, "-", "authenticator"], [2, 0, 0, "-", "config"], [2, 0, 0, "-", "images"], [2, 0, 0, "-", "secure"], [2, 0, 0, "-", "squire"], [2, 0, 0, "-", "stream"], [2, 0, 0, "-", "subtitles"]], "pystream.models.authenticator": [[2, 3, 1, "", "extract_credentials"], [2, 3, 1, "", "failed_auth_counter"], [2, 3, 1, "", "raise_error"], [2, 3, 1, "", "verify_login"], [2, 3, 1, "", "verify_token"]], "pystream.models.config": [[2, 1, 1, "", "EnvConfig"], [2, 1, 1, "", "FileIO"], [2, 5, 1, "", "RedirectException"], [2, 1, 1, "", "Session"], [2, 1, 1, "", "Static"], [2, 1, 1, "", "WebToken"], [2, 4, 1, "", "env"]], "pystream.models.config.EnvConfig": [[2, 1, 1, "", "Config"], [2, 4, 1, "", "authorization"], [2, 4, 1, "", "auto_thumbnail"], [2, 4, 1, "", "file_formats"], [2, 2, 1, "", "parse_video_host"], [2, 2, 1, "", "parse_website"], [2, 4, 1, "", "session_duration"], [2, 4, 1, "", "users_allowed"], [2, 4, 1, "", "video_host"], [2, 4, 1, "", "video_port"], [2, 4, 1, "", "video_source"], [2, 4, 1, "", "website"], [2, 4, 1, "", "workers"]], "pystream.models.config.EnvConfig.Config": [[2, 4, 1, "", "env_file"], [2, 4, 1, "", "env_prefix"], [2, 4, 1, "", "extra"], [2, 4, 1, "", "hide_input_in_errors"]], "pystream.models.config.FileIO": [[2, 4, 1, "", "index"], [2, 4, 1, "", "landing"], [2, 4, 1, "", "listing"]], "pystream.models.config.Session": [[2, 4, 1, "", "info"], [2, 4, 1, "", "invalid"], [2, 4, 1, "", "mapping"]], "pystream.models.config.Static": [[2, 1, 1, "", "Config"], [2, 4, 1, "", "chunk_size"], [2, 4, 1, "", "cipher_suite"], [2, 4, 1, "", "deletions"], [2, 4, 1, "", "home_endpoint"], [2, 4, 1, "", "login_endpoint"], [2, 4, 1, "", "logout_endpoint"], [2, 4, 1, "", "preview"], [2, 4, 1, "", "query_param"], [2, 4, 1, "", "stream"], [2, 4, 1, "", "streaming_endpoint"], [2, 4, 1, "", "track"]], "pystream.models.config.Static.Config": [[2, 4, 1, "", "arbitrary_types_allowed"]], "pystream.models.config.WebToken": [[2, 4, 1, "", "timestamp"], [2, 4, 1, "", "token"], [2, 4, 1, "", "username"]], "pystream.models.images": [[2, 1, 1, "", "Images"]], "pystream.models.images.Images": [[2, 2, 1, "", "generate_preview"], [2, 2, 1, "", "generate_thumbnails"], [2, 2, 1, "", "get_video_length"]], "pystream.models.secure": [[2, 3, 1, "", "base64_decode"], [2, 3, 1, "", "base64_encode"], [2, 3, 1, "", "calculate_hash"], [2, 3, 1, "", "hex_decode"], [2, 3, 1, "", "hex_encode"]], "pystream.models.squire": [[2, 3, 1, "", "get_all_stream_content"], [2, 3, 1, "", "get_dir_stream_content"], [2, 3, 1, "", "get_iter"], [2, 3, 1, "", "keygen"], [2, 3, 1, "", "log_connection"], [2, 3, 1, "", "natural_sort_key"], [2, 3, 1, "", "remove_thumbnail"]], "pystream.models.stream": [[2, 3, 1, "", "get_range_header"], [2, 3, 1, "", "range_requests_response"], [2, 3, 1, "", "send_bytes_range_requests"]], "pystream.models.subtitles": [[2, 3, 1, "", "srt_to_vtt"], [2, 3, 1, "", "vtt_to_srt"]], "pystream.routers": [[2, 0, 0, "-", "auth"], [2, 0, 0, "-", "basics"], [2, 0, 0, "-", "video"]], "pystream.routers.auth": [[2, 3, 1, "", "get_expiry"], [2, 3, 1, "", "home_page"], [2, 3, 1, "", "login"], [2, 3, 1, "", "logout"]], "pystream.routers.basics": [[2, 3, 1, "", "error"], [2, 3, 1, "", "get_favicon"], [2, 3, 1, "", "root"]], "pystream.routers.video": [[2, 3, 1, "", "preview_loader"], [2, 3, 1, "", "stream_video"], [2, 3, 1, "", "track_loader"], [2, 3, 1, "", "video_endpoint"]], "pystream.utils": [[2, 3, 1, "", "get_local_ip"], [2, 3, 1, "", "get_public_ip"]]}, "objtypes": {"0": "py:module", "1": "py:class", "2": "py:method", "3": "py:function", "4": "py:attribute", "5": "py:exception"}, "objnames": {"0": ["py", "module", "Python module"], "1": ["py", "class", "Python class"], "2": ["py", "method", "Python method"], "3": ["py", "function", "Python function"], "4": ["py", "attribute", "Python attribute"], "5": ["py", "exception", "Python exception"]}, "titleterms": {"pystream": 0, "instal": 0, "sampl": 0, "usag": 0, "env": 0, "variabl": 0, "code": 0, "standard": 0, "releas": 0, "note": 0, "lint": 0, "pypi": 0, "packag": 0, "runbook": 0, "licens": 0, "copyright": 0, "authent": [1, 2], "usernam": 1, "password": 1, "frontend": 1, "backend": 1, "session": 1, "token": 1, "refer": 1, "stream": 2, "localhost": 2, "A": 2, "secur": 2, "interfac": 2, "video": 2, "read": 2, "me": 2, "main": 2, "modul": 2, "model": 2, "config": 2, "imag": 2, "squir": 2, "subtitl": 2, "router": 2, "basic": 2, "support": 2, "logger": 2, "util": 2, "indic": 2, "tabl": 2}, "envversion": {"sphinx.domains.c": 2, "sphinx.domains.changeset": 1, "sphinx.domains.citation": 1, "sphinx.domains.cpp": 6, "sphinx.domains.index": 1, "sphinx.domains.javascript": 2, "sphinx.domains.math": 2, "sphinx.domains.python": 3, "sphinx.domains.rst": 2, "sphinx.domains.std": 2, "sphinx": 56}}) \ No newline at end of file +Search.setIndex({"docnames": ["README", "authentication", "index"], "filenames": ["README.md", "authentication.md", "index.rst"], "titles": ["PyStream", "Authentication", "Stream-Localhost - A secured interface to stream videos"], "terms": {"deploy": 0, "python": 0, "modul": 0, "stream": [0, 1], "video": 0, "via": [0, 2], "authent": 0, "session": [0, 2], "us": [0, 1, 2], "fastapi": [0, 1, 2], "m": 0, "pip": 0, "localhost": 0, "import": 0, "asyncio": 0, "o": 0, "__name__": 0, "__main__": 0, "kwarg": [0, 2], "dict": [0, 2], "author": [0, 1, 2], "alan": 0, "ture": 0, "pr0gramm1ng": 0, "linu": 0, "torvald": 0, "linuxo": 0, "video_sourc": [0, 2], "path": [0, 2], "join": 0, "expandus": 0, "download": 0, "add": [0, 2], "follow": [0, 1], "host": [0, 2], "local": [0, 2], "ip": [0, 2], "address": [0, 2], "skip": 0, "127": [0, 2], "0": [0, 2], "1": [0, 2], "video_host": [0, 2], "util": 0, "get_local_ip": [0, 2], "run": [0, 2], "start": [0, 2], "bulb": 0, "environ": [0, 2], "can": [0, 1], "load": [0, 2], "from": [0, 1, 2], "ani": [0, 2], "file": [0, 2], "filenam": [0, 2], "default": 0, "To": 0, "custom": [0, 2], "set": [0, 2], "var": [0, 2], "env_fil": [0, 2], "kei": [0, 1, 2], "its": [0, 2], "valu": [0, 1, 2], "mandatori": 0, "dictionari": [0, 2], "pair": [0, 2], "usernam": [0, 2], "password": [0, 2], "_": 0, "sourc": [0, 2], "underscor": 0, "dot": 0, "ignor": [0, 2], "option": [0, 2], "port": 0, "number": [0, 2], "applic": 0, "8000": [0, 2], "format": 0, "sequenc": [0, 2], "support": 0, "mp4": [0, 2], "mov": [0, 2], "worker": [0, 2], "spin": 0, "up": 0, "uvicorn": 0, "server": [0, 1, 2], "websit": [0, 2], "list": [0, 2], "regex": 0, "cor": 0, "configur": [0, 2], "requir": [0, 2], "onli": [0, 1, 2], "tunnel": 0, "cdn": 0, "auto": 0, "thumbnail": [0, 2], "boolean": [0, 2], "flag": [0, 2], "gener": [0, 1, 2], "imag": 0, "preview": [0, 2], "true": [0, 2], "docstr": 0, "googl": 0, "style": 0, "convent": 0, "pep": 0, "8": 0, "isort": 0, "gitvers": 0, "revers": 0, "f": 0, "release_not": 0, "rst": 0, "t": [0, 2], "pre": 0, "commit": 0, "ensur": 0, "pytest": 0, "valid": [0, 2], "hyperlink": 0, "all": [0, 1, 2], "markdown": 0, "includ": [0, 1, 2], "wiki": 0, "page": [0, 1, 2], "sphinx": 0, "5": 0, "recommonmark": 0, "http": [0, 2], "org": 0, "project": 0, "thevickypedia": 0, "github": 0, "io": 0, "vignesh": 0, "rao": 0, "under": 0, "mit": 0, "pystream": [1, 2], "two": 1, "wai": [1, 2], "gain": 1, "access": [1, 2], "session_token": [1, 2], "directori": [1, 2], "content": [1, 2], "signatur": 1, "ui": [1, 2], "creat": [1, 2], "hex": [1, 2], "nativ": 1, "j": [1, 2], "These": 1, "ar": [1, 2], "calcul": [1, 2], "hash": [1, 2], "i": [1, 2], "base64": [1, 2], "encod": [1, 2], "befor": [1, 2], "send": [1, 2], "api": [1, 2], "an": [1, 2], "header": [1, 2], "decod": [1, 2], "ascii": 1, "string": [1, 2], "receiv": [1, 2], "Then": 1, "broken": [1, 2], "down": 1, "timestamp": [1, 2], "The": [1, 2], "get": [1, 2], "store": [1, 2], "env": [1, 2], "variabl": [1, 2], "which": [1, 2], "compar": 1, "purpos": 1, "symmetr": [1, 2], "encrypt": [1, 2], "onc": 1, "login": [1, 2], "ha": [1, 2], "been": 1, "success": [1, 2], "randomli": 1, "64": [1, 2], "bit": [1, 2], "url": [1, 2], "safe": [1, 2], "thi": [1, 2], "uniqu": 1, "each": [1, 2], "user": [1, 2], "form": [1, 2], "payload": [1, 2], "cryptographi": [1, 2], "": [1, 2], "fernet": [1, 2], "retriev": [1, 2], "cooki": [1, 2], "jsonrespons": [1, 2], "redirect_url": 1, "sinc": [1, 2], "ajax": 1, "post": 1, "call": 1, "redirectrespons": [1, 2], "work": [1, 2], "simpli": 1, "redirect": [1, 2], "instead": [1, 2], "json": [1, 2], "respons": [1, 2], "fetch": 1, "alter": 1, "locat": [1, 2], "href": 1, "transfer": 1, "new": [1, 2], "so": [1, 2], "lost": 1, "point": 1, "navig": 1, "carri": 1, "onward": 1, "instal": 2, "sampl": 2, "usag": 2, "code": 2, "standard": 2, "releas": 2, "note": 2, "lint": 2, "pypi": 2, "packag": 2, "runbook": 2, "licens": 2, "copyright": 2, "token": 2, "refer": 2, "async": 2, "redirect_exception_handl": 2, "request": 2, "except": 2, "redirectexcept": 2, "handler": 2, "handl": 2, "paramet": 2, "take": 2, "object": 2, "argument": 2, "inherit": 2, "return": 2, "statu": 2, "type": 2, "startup_task": 2, "none": 2, "task": 2, "need": 2, "dure": 2, "startup": 2, "shutdown_task": 2, "shutdown": 2, "starter": 2, "function": 2, "keyword": 2, "failed_auth_count": 2, "keep": 2, "track": 2, "fail": 2, "attempt": 2, "3": 2, "more": 2, "time": 2, "extract_credenti": 2, "str": 2, "extract": 2, "credenti": 2, "raise_error": 2, "noreturn": 2, "rais": 2, "401": 2, "unauthor": 2, "error": 2, "case": 2, "bad": 2, "verify_login": 2, "union": 2, "int": 2, "verifi": 2, "verify_token": 2, "decrypt": 2, "expir": 2, "as_dict": 2, "tupl": 2, "secretstr": 2, "pass": 2, "object_pairs_hook": 2, "duplic": 2, "order": 2, "secret": 2, "class": 2, "envconfig": 2, "_case_sensit": 2, "bool": 2, "_env_prefix": 2, "_env_fil": 2, "dotenvtyp": 2, "posixpath": 2, "_env_file_encod": 2, "_env_nested_delimit": 2, "_secrets_dir": 2, "ipv4address": 2, "video_port": 2, "session_dur": 2, "3600": 2, "file_format": 2, "auto_thumbnail": 2, "pydant": 2, "share": 2, "across": 2, "pars": 2, "input": 2, "data": 2, "validationerror": 2, "pydantic_cor": 2, "cannot": 2, "__init__": 2, "__pydantic_self__": 2, "common": 2, "self": 2, "first": 2, "arg": 2, "allow": 2, "field": 2, "name": 2, "env_prefix": 2, "extra": 2, "hide_input_in_error": 2, "classmethod": 2, "parse_author": 2, "parse_video_host": 2, "notion": 2, "parse_websit": 2, "evalu": 2, "fileio": 2, "index": 2, "html": 2, "land": 2, "static": 2, "query_param": 2, "home_endpoint": 2, "home": 2, "login_endpoint": 2, "logout_endpoint": 2, "logout": 2, "streaming_endpoint": 2, "chunk_siz": 2, "1048576": 2, "delet": 2, "pathlib": 2, "cipher_suit": 2, "arbitrary_types_allow": 2, "info": 2, "invalid": 2, "map": 2, "inform": 2, "webtoken": 2, "ecrypt": 2, "detail": 2, "within": 2, "httpexcept": 2, "doesn": 2, "demand": 2, "where": 2, "solut": 2, "There": 2, "altern": 2, "our": 2, "javascript": 2, "come": 2, "handi": 2, "mani": 2, "unexpect": 2, "scenario": 2, "tiangolo": 2, "com": 2, "tutori": 2, "instanti": 2, "reason": 2, "alia": 2, "filepath": 2, "initi": 2, "captur": 2, "frame": 2, "particular": 2, "opencv": 2, "videocaptur": 2, "generate_thumbnail": 2, "interv": 2, "output_dir": 2, "second": 2, "output": 2, "failur": 2, "get_video_length": 2, "timedelta": 2, "length": 2, "datetim": 2, "generate_preview": 2, "at_second": 2, "should": 2, "calculate_hash": 2, "given": 2, "base64_encod": 2, "base64_decod": 2, "hex_decod": 2, "convert": 2, "hex_encod": 2, "log_connect": 2, "log": 2, "connect": 2, "devic": 2, "avoid": 2, "multipl": 2, "when": 2, "same": 2, "differ": 2, "natural_sort_kei": 2, "sort": 2, "natur": 2, "element": 2, "deriv": 2, "split": 2, "part": 2, "regular": 2, "express": 2, "get_dir_stream_cont": 2, "parent": 2, "subdir": 2, "insid": 2, "displai": 2, "subdirectori": 2, "exist": 2, "get_all_stream_cont": 2, "folder": 2, "contain": 2, "section": 2, "get_it": 2, "purepath": 2, "current": 2, "serv": 2, "previou": 2, "next": 2, "render": 2, "remove_thumbnail": 2, "img_path": 2, "trigger": 2, "timer": 2, "remov": 2, "keygen": 2, "forc": 2, "restart": 2, "send_bytes_range_request": 2, "file_obj": 2, "binaryio": 2, "start_rang": 2, "end_rang": 2, "asynciter": 2, "bytestr": 2, "chunk": 2, "rang": 2, "specif": 2, "rfc7233": 2, "byte": 2, "end": 2, "yield": 2, "iter": 2, "get_range_head": 2, "range_head": 2, "file_s": 2, "proce": 2, "size": 2, "range_requests_respons": 2, "file_path": 2, "streamingrespons": 2, "srt_to_vtt": 2, "srt": 2, "vtt": 2, "compat": 2, "vtt_to_srt": 2, "auth": 2, "get_expiri": 2, "lease_start": 2, "lease_dur": 2, "expiri": 2, "max": 2, "ag": 2, "wa": 2, "made": 2, "until": 2, "date": 2, "gmt": 2, "home_pag": 2, "templaterespons": 2, "after": 2, "htmlrespons": 2, "termin": 2, "back": 2, "upon": 2, "refresh": 2, "base": 2, "get_favicon": 2, "filerespons": 2, "favicon": 2, "ico": 2, "endpoint": 2, "robinhood": 2, "script": 2, "root": 2, "relat": 2, "preview_load": 2, "setup": 2, "track_load": 2, "track_path": 2, "stream_video": 2, "video_path": 2, "templat": 2, "video_endpoint": 2, "rootfilt": 2, "filter": 2, "while": 2, "preserv": 2, "other": 2, "200": 2, "ok": 2, "307": 2, "temporari": 2, "vid_nam": 2, "redund": 2, "overrid": 2, "implement": 2, "subclass": 2, "method": 2, "record": 2, "examin": 2, "fals": 2, "discard": 2, "togeth": 2, "children": 2, "have": 2, "event": 2, "through": 2, "If": 2, "specifi": 2, "everi": 2, "logrecord": 2, "out": 2, "repres": 2, "someth": 2, "simpl": 2, "check": 2, "network": 2, "id": 2, "privat": 2, "machin": 2, "get_public_ip": 2, "public": 2, "make": 2, "extern": 2, "search": 2}, "objects": {"pystream": [[2, 0, 0, "-", "logger"], [2, 0, 0, "-", "main"], [2, 0, 0, "-", "utils"]], "pystream.logger": [[2, 1, 1, "", "RootFilter"]], "pystream.logger.RootFilter": [[2, 2, 1, "", "filter"]], "pystream.main": [[2, 3, 1, "", "redirect_exception_handler"], [2, 3, 1, "", "shutdown_tasks"], [2, 3, 1, "", "start"], [2, 3, 1, "", "startup_tasks"]], "pystream.models": [[2, 0, 0, "-", "authenticator"], [2, 0, 0, "-", "config"], [2, 0, 0, "-", "images"], [2, 0, 0, "-", "secure"], [2, 0, 0, "-", "squire"], [2, 0, 0, "-", "stream"], [2, 0, 0, "-", "subtitles"]], "pystream.models.authenticator": [[2, 3, 1, "", "extract_credentials"], [2, 3, 1, "", "failed_auth_counter"], [2, 3, 1, "", "raise_error"], [2, 3, 1, "", "verify_login"], [2, 3, 1, "", "verify_token"]], "pystream.models.config": [[2, 1, 1, "", "EnvConfig"], [2, 1, 1, "", "FileIO"], [2, 5, 1, "", "RedirectException"], [2, 1, 1, "", "Session"], [2, 1, 1, "", "Static"], [2, 1, 1, "", "WebToken"], [2, 3, 1, "", "as_dict"], [2, 4, 1, "", "env"]], "pystream.models.config.EnvConfig": [[2, 1, 1, "", "Config"], [2, 4, 1, "", "authorization"], [2, 4, 1, "", "auto_thumbnail"], [2, 4, 1, "", "file_formats"], [2, 2, 1, "", "parse_authorization"], [2, 2, 1, "", "parse_video_host"], [2, 2, 1, "", "parse_website"], [2, 4, 1, "", "session_duration"], [2, 4, 1, "", "video_host"], [2, 4, 1, "", "video_port"], [2, 4, 1, "", "video_source"], [2, 4, 1, "", "website"], [2, 4, 1, "", "workers"]], "pystream.models.config.EnvConfig.Config": [[2, 4, 1, "", "env_file"], [2, 4, 1, "", "env_prefix"], [2, 4, 1, "", "extra"], [2, 4, 1, "", "hide_input_in_errors"]], "pystream.models.config.FileIO": [[2, 4, 1, "", "index"], [2, 4, 1, "", "landing"], [2, 4, 1, "", "listing"]], "pystream.models.config.Session": [[2, 4, 1, "", "info"], [2, 4, 1, "", "invalid"], [2, 4, 1, "", "mapping"]], "pystream.models.config.Static": [[2, 1, 1, "", "Config"], [2, 4, 1, "", "chunk_size"], [2, 4, 1, "", "cipher_suite"], [2, 4, 1, "", "deletions"], [2, 4, 1, "", "home_endpoint"], [2, 4, 1, "", "login_endpoint"], [2, 4, 1, "", "logout_endpoint"], [2, 4, 1, "", "preview"], [2, 4, 1, "", "query_param"], [2, 4, 1, "", "stream"], [2, 4, 1, "", "streaming_endpoint"], [2, 4, 1, "", "track"]], "pystream.models.config.Static.Config": [[2, 4, 1, "", "arbitrary_types_allowed"]], "pystream.models.config.WebToken": [[2, 4, 1, "", "timestamp"], [2, 4, 1, "", "token"], [2, 4, 1, "", "username"]], "pystream.models.images": [[2, 1, 1, "", "Images"]], "pystream.models.images.Images": [[2, 2, 1, "", "generate_preview"], [2, 2, 1, "", "generate_thumbnails"], [2, 2, 1, "", "get_video_length"]], "pystream.models.secure": [[2, 3, 1, "", "base64_decode"], [2, 3, 1, "", "base64_encode"], [2, 3, 1, "", "calculate_hash"], [2, 3, 1, "", "hex_decode"], [2, 3, 1, "", "hex_encode"]], "pystream.models.squire": [[2, 3, 1, "", "get_all_stream_content"], [2, 3, 1, "", "get_dir_stream_content"], [2, 3, 1, "", "get_iter"], [2, 3, 1, "", "keygen"], [2, 3, 1, "", "log_connection"], [2, 3, 1, "", "natural_sort_key"], [2, 3, 1, "", "remove_thumbnail"]], "pystream.models.stream": [[2, 3, 1, "", "get_range_header"], [2, 3, 1, "", "range_requests_response"], [2, 3, 1, "", "send_bytes_range_requests"]], "pystream.models.subtitles": [[2, 3, 1, "", "srt_to_vtt"], [2, 3, 1, "", "vtt_to_srt"]], "pystream.routers": [[2, 0, 0, "-", "auth"], [2, 0, 0, "-", "basics"], [2, 0, 0, "-", "video"]], "pystream.routers.auth": [[2, 3, 1, "", "get_expiry"], [2, 3, 1, "", "home_page"], [2, 3, 1, "", "login"], [2, 3, 1, "", "logout"]], "pystream.routers.basics": [[2, 3, 1, "", "error"], [2, 3, 1, "", "get_favicon"], [2, 3, 1, "", "root"]], "pystream.routers.video": [[2, 3, 1, "", "preview_loader"], [2, 3, 1, "", "stream_video"], [2, 3, 1, "", "track_loader"], [2, 3, 1, "", "video_endpoint"]], "pystream.utils": [[2, 3, 1, "", "get_local_ip"], [2, 3, 1, "", "get_public_ip"]]}, "objtypes": {"0": "py:module", "1": "py:class", "2": "py:method", "3": "py:function", "4": "py:attribute", "5": "py:exception"}, "objnames": {"0": ["py", "module", "Python module"], "1": ["py", "class", "Python class"], "2": ["py", "method", "Python method"], "3": ["py", "function", "Python function"], "4": ["py", "attribute", "Python attribute"], "5": ["py", "exception", "Python exception"]}, "titleterms": {"pystream": 0, "instal": 0, "sampl": 0, "usag": 0, "env": 0, "variabl": 0, "code": 0, "standard": 0, "releas": 0, "note": 0, "lint": 0, "pypi": 0, "packag": 0, "runbook": 0, "licens": 0, "copyright": 0, "authent": [1, 2], "usernam": 1, "password": 1, "frontend": 1, "backend": 1, "session": 1, "token": 1, "refer": 1, "stream": 2, "localhost": 2, "A": 2, "secur": 2, "interfac": 2, "video": 2, "read": 2, "me": 2, "main": 2, "modul": 2, "model": 2, "config": 2, "imag": 2, "squir": 2, "subtitl": 2, "router": 2, "basic": 2, "support": 2, "logger": 2, "util": 2, "indic": 2, "tabl": 2}, "envversion": {"sphinx.domains.c": 2, "sphinx.domains.changeset": 1, "sphinx.domains.citation": 1, "sphinx.domains.cpp": 6, "sphinx.domains.index": 1, "sphinx.domains.javascript": 2, "sphinx.domains.math": 2, "sphinx.domains.python": 3, "sphinx.domains.rst": 2, "sphinx.domains.std": 2, "sphinx": 56}}) \ No newline at end of file diff --git a/pystream/main.py b/pystream/main.py index 2ae141e..fbed136 100644 --- a/pystream/main.py +++ b/pystream/main.py @@ -42,12 +42,7 @@ async def redirect_exception_handler(request: Request, async def startup_tasks() -> None: """Tasks that need to run during the API startup.""" - logger.info("Validating authorization keys") - config.env.users_allowed = sum([list(user.keys()) for user in config.env.authorization], []) - if dupe := set(x for x in config.env.users_allowed if config.env.users_allowed.count(x) > 1): - raise ValueError( - f"authorization\n\tInput list should have dictionaries with unique keys\n\tduplicate(s): {dupe}" - ) + logger.info("Users allowed [%d]: %s", len(config.env.authorization), list(config.env.authorization.keys())) logger.info('Setting up CORS policy.') origins = ["http://localhost.com", "https://localhost.com"] origins.extend(config.env.website) diff --git a/pystream/models/authenticator.py b/pystream/models/authenticator.py index d73ac52..2056c02 100644 --- a/pystream/models/authenticator.py +++ b/pystream/models/authenticator.py @@ -55,15 +55,12 @@ async def verify_login(request: Request) -> Dict[str, Union[str, int]]: Returns a dictionary with the payload required to create the session token. """ username, signature, timestamp = await extract_credentials(request) - if username not in config.env.users_allowed: - await raise_error(request) - for item in config.env.authorization: - if password := item.get(username): - break + if password := config.env.authorization.get(username): + hex_user = await secure.hex_encode(username) + hex_pass = await secure.hex_encode(password.get_secret_value()) else: + logger.warning("User '%s' not allowed", username) await raise_error(request) - hex_user = await secure.hex_encode(username) - hex_pass = await secure.hex_encode(password.get_secret_value()) message = f"{hex_user}{hex_pass}{timestamp}" expected_signature = await secure.calculate_hash(message) if secrets.compare_digest(signature, expected_signature): diff --git a/pystream/models/config.py b/pystream/models/config.py index f9d74e7..e610d38 100644 --- a/pystream/models/config.py +++ b/pystream/models/config.py @@ -1,8 +1,9 @@ +import json import os import pathlib import socket from ipaddress import IPv4Address -from typing import Dict, List, Optional, Sequence, Set, Union +from typing import Any, Dict, List, Optional, Sequence, Set, Tuple, Union from cryptography.fernet import Fernet from pydantic import (BaseModel, DirectoryPath, Field, PositiveInt, SecretStr, @@ -12,6 +13,25 @@ template_storage = os.path.join(pathlib.Path(__file__).parent.parent, "templates") +def as_dict(pairs: List[Tuple[str, str]]) -> Dict[str, SecretStr]: + """Custom decoder for ``json.loads`` passed via ``object_pairs_hook`` raising error on duplicate keys. + + Args: + pairs: Takes the ordered list of pairs as an argument. + + Returns: + Dict[str, SecretStr]: + A dictionary of key as string and value as a secret. + """ + dictionary = {} + for key, value in pairs: + if key in dictionary: + raise ValueError(f"Duplicate key: {key!r}") + else: + dictionary[key.strip()] = SecretStr(value.strip()) + return dictionary + + class EnvConfig(BaseSettings): """Configure all env vars and validate using ``pydantic`` to share across modules. @@ -19,9 +39,8 @@ class EnvConfig(BaseSettings): """ - authorization: List[Dict[str, SecretStr]] + authorization: Any video_source: DirectoryPath - users_allowed: List[str] = [] video_host: IPv4Address = socket.gethostbyname("localhost") video_port: PositiveInt = 8000 @@ -40,6 +59,22 @@ class Config: extra = "ignore" # Ignores additional environment variables present in env files hide_input_in_errors = True # Avoids revealing sensitive information in validation error messages + # noinspection PyMethodParameters + @field_validator("authorization", mode='before', check_fields=False) + def parse_authorization(cls, value: Any) -> Dict[str, SecretStr]: + """Validates the authorization parameter.""" + val = json.loads(value, object_pairs_hook=as_dict) + if isinstance(val, dict): + r = {} + for k, v in val.items(): + if len(k) < 3: + raise ValueError(f"[{k}: {v}] username should be at least 4 or more characters") + if len(v) < 8: + raise ValueError(f"[{k}: {v}] password should be at least 8 or more characters") + r[k] = v + return r + raise ValueError("input should be a valid dictionary with username as key and password as value") + # noinspection PyMethodParameters @field_validator("video_host", mode='after', check_fields=True) def parse_video_host(cls, value: IPv4Address) -> str: