diff --git a/Makefile b/Makefile index fe1de81..8ab00d2 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ PACKAGE_NAME:=armasec ROOT_DIR:=$(shell dirname $(shell pwd)) install: - poetry install + poetry install --extras=cli test: install poetry run pytest @@ -12,16 +12,13 @@ mypy: install poetry run mypy ${PACKAGE_NAME} --pretty lint: install - poetry run black --check ${PACKAGE_NAME} tests - poetry run isort --check ${PACKAGE_NAME} tests - poetry run pflake8 ${PACKAGE_NAME} tests + poetry run ruff check ${PACKAGE_NAME} tests cli qa: test mypy lint echo "All quality checks pass!" format: install - poetry run black ${PACKAGE_NAME} tests - poetry run isort ${PACKAGE_NAME} tests + poetry run ruff check --fix ${PACKAGE_NAME} tests cli example: install poetry run uvicorn --host 0.0.0.0 --app-dir=examples basic:app --reload diff --git a/armasec/token_security.py b/armasec/token_security.py index 7b247e7..d64781c 100644 --- a/armasec/token_security.py +++ b/armasec/token_security.py @@ -89,7 +89,7 @@ def __init__( # Settings needed for FastAPI's APIKeyBase self.model: APIKey = APIKey( - **{"in": APIKeyIn.header}, + **{"in": APIKeyIn.header}, # type: ignore[arg-type] name=TokenManager.header_key, description=self.__class__.__doc__, ) diff --git a/cli/auth.py b/cli/auth.py new file mode 100644 index 0000000..aabef9a --- /dev/null +++ b/cli/auth.py @@ -0,0 +1,272 @@ +from __future__ import annotations + +from time import sleep +from typing import cast + +import snick +from jose.exceptions import ExpiredSignatureError +from loguru import logger +from jose import jwt +from pydantic import ValidationError + +from cli.exceptions import Abort, ArmasecCliError +from cli.schemas import DeviceCodeData, TokenSet, IdentityData +from cli.cache import load_tokens_from_cache, save_tokens_to_cache +from cli.client import make_request +from cli.config import OidcProvider +from cli.schemas import Persona, CliContext +from cli.format import terminal_message +from cli.time_loop import TimeLoop + + +def extract_persona(token_set: TokenSet | None = None): + + if token_set is None: + token_set = load_tokens_from_cache() + + try: + identity_data = validate_token_and_extract_identity(token_set) + except ExpiredSignatureError: + Abort.require_condition( + token_set.refresh_token is not None, + "The auth token is expired. Please retrieve a new and log in again.", + raise_kwargs=dict( + subject="Expired access token", + ), + ) + + logger.debug("The access token is expired. Attempting to refresh token") + refresh_access_token(token_set) + identity_data = validate_token_and_extract_identity(token_set) + + logger.debug(f"Persona created with identity_data: {identity_data}") + + save_tokens_to_cache(token_set) + + return Persona( + token_set=token_set, + identity_data=identity_data, + ) + + +def validate_token_and_extract_identity(token_set: TokenSet) -> IdentityData: + logger.debug("Validating access token") + + token_file_is_empty = not token_set.access_token + if token_file_is_empty: + logger.debug("Access token file exists but it is empty") + raise Abort( + """ + Access token file exists but it is empty. + + Please try logging in again. + """, + subject="Empty access token file", + log_message="Empty access token file", + ) + + with Abort.handle_errors( + """ + There was an unknown error while validating the access token. + + Please try logging in again. + """, + ignore_exc_class=ExpiredSignatureError, # Will be handled in calling context + raise_kwargs=dict( + subject="Invalid access token", + log_message="Unknown error while validating access access token", + ), + ): + token_data = jwt.decode( + token_set.access_token, + None, + options=dict( + verify_signature=False, + verify_aud=False, + verify_exp=True, + ), + ) + + logger.debug("Extracting identity data from the access token") + with Abort.handle_errors( + """ + There was an error extracting the user's identity from the access token. + + Please try logging in again. + """, + handle_exc_class=ValidationError, + raise_kwargs=dict( + subject="Missing user data", + log_message="Token data could not be extracted to identity", + ), + ): + identity = IdentityData( + email=token_data.get("email"), + client_id=token_data.get("azp"), + ) + + return identity + + +def init_persona(ctx: CliContext, token_set: TokenSet | None = None): + if token_set is None: + token_set = load_tokens_from_cache() + + try: + identity_data = validate_token_and_extract_identity(token_set) + except ExpiredSignatureError: + Abort.require_condition( + token_set.refresh_token is not None, + "The auth token is expired. Please retrieve a new and log in again.", + raise_kwargs=dict( + subject="Expired access token", + ), + ) + + logger.debug("The access token is expired. Attempting to refresh token") + refresh_access_token(ctx, token_set) + identity_data = validate_token_and_extract_identity(token_set) + + logger.debug(f"Persona created with identity_data: {identity_data}") + + save_tokens_to_cache(token_set) + + return Persona( + token_set=token_set, + identity_data=identity_data, + ) + + +def refresh_access_token(ctx: CliContext, token_set: TokenSet): + """ + Attempt to fetch a new access token given a refresh token in a token_set. + + Sets the access token in-place. + + If refresh fails, notify the user that they need to log in again. + """ + print("MAKE THIS FUCKING THING USE THE BASE URL") + url = "/protocol/openid-connect/token" + logger.debug(f"Requesting refreshed access token from {url}") + + refreshed_token_set: TokenSet = cast( + TokenSet, + make_request( + ctx.client, + "/protocol/openid-connect/token", + "POST", + abort_message="The auth token could not be refreshed. Please try logging in again.", + abort_subject="EXPIRED ACCESS TOKEN", + response_model_cls=TokenSet, + data=dict( + client_id=ctx.settings.oidc_client_id, + audience=ctx.settings.oidc_audience, + grant_type="refresh_token", + refresh_token=token_set.refresh_token, + ), + ), + ) + + token_set.access_token = refreshed_token_set.access_token + + +def fetch_auth_tokens(ctx: CliContext) -> TokenSet: + """ + Fetch an access token (and possibly a refresh token) from Auth0. + + Prints out a URL for the user to use to authenticate and polls the token endpoint to fetch it + when the browser-based process finishes. + """ + if ctx.settings.oidc_provider == OidcProvider.KEYCLOAK: + device_path = "/protocol/openid-connect/auth/device" + token_path = "/protocol/openid-connect/token" + elif ctx.settings.oidc_provider == OidcProvider.AUTH0: + device_path = "/oauth/device/code" + token_path = "oauth/token" + else: + raise ArmasecCliError("Unsupported OIDC Provider.") + + device_code_data: DeviceCodeData = cast( + DeviceCodeData, + make_request( + ctx.client, + device_path, + "POST", + expected_status=200, + abort_message=( + """ + There was a problem retrieving a device verification code from + the auth provider + """ + ), + abort_subject="COULD NOT RETRIEVE TOKEN", + response_model_cls=DeviceCodeData, + data=dict( + client_id=ctx.settings.oidc_client_id, + grant_type="client_credentials", + audience=ctx.settings.oidc_audience, + ), + ), + ) + + max_poll_time = 6 * 60 # 5 minutes + terminal_message( + f""" + To complete login, please open the following link in a browser: + + {device_code_data.verification_uri_complete} + + Waiting up to {max_poll_time / 60} minutes for you to complete the process... + """, + subject="Waiting for login", + ) + + for tick in TimeLoop( + ctx.settings.oidc_max_poll_time, + message="Waiting for web login", + ): + response_data: dict = cast( + dict, + make_request( + ctx.client, + token_path, + "POST", + abort_message=snick.unwrap( + """ + There was a problem retrieving a device verification code + from the auth provider + """ + ), + abort_subject="COULD NOT FETCH ACCESS TOKEN", + data=dict( + grant_type="urn:ietf:params:oauth:grant-type:device_code", + device_code=device_code_data.device_code, + client_id=ctx.settings.oidc_client_id, + ), + ), + ) + if "error" in response_data: + if response_data["error"] == "authorization_pending": + logger.debug(f"Token fetch attempt #{tick.counter} failed") + sleep(device_code_data.interval) + else: + # TODO: Test this failure condition + raise Abort( + snick.unwrap( + """ + There was a problem retrieving a device verification code + from the auth provider: + Unexpected failure retrieving access token. + """ + ), + subject="Unexpected error", + log_message=f"Unexpected error response: {response_data}", + ) + else: + return TokenSet(**response_data) + + raise Abort( + "Login process was not completed in time. Please try again.", + subject="Timed out", + log_message="Timed out while waiting for user to complete login", + ) diff --git a/cli/cache.py b/cli/cache.py new file mode 100644 index 0000000..d26e90b --- /dev/null +++ b/cli/cache.py @@ -0,0 +1,95 @@ +from __future__ import annotations + +from pathlib import Path +from functools import wraps + +from loguru import logger + +from cli.schemas import TokenSet +from cli.exceptions import Abort +from cli.config import cache_dir + + +def init_cache(func): + + @wraps(func) + def wrapper(*args, **kwargs): + try: + cache_dir.mkdir(exist_ok=True, parents=True) + token_dir = cache_dir / "token" + token_dir.mkdir(exist_ok=True) + info_file = cache_dir / "info.txt" + info_file.write_text("This directory is used by Armasec CLI for its cache.") + except Exception: + raise Abort( + """ + Cache directory {cache_dir} doesn't exist, is not writable, or could not be created. + + Please check your home directory permissions and try again. + """, + subject="Non-writable cache dir", + log_message="Non-writable cache dir", + ) + return func(*args, **kwargs) + return wrapper + + +def _get_token_paths() -> tuple[Path, Path]: + token_dir = cache_dir / "token" + access_token_path: Path = token_dir / "access.token" + refresh_token_path: Path = token_dir / "refresh.token" + return (access_token_path, refresh_token_path) + + +def load_tokens_from_cache() -> TokenSet: + """ + Loads an access token (and a refresh token if one exists) from the cache. + """ + (access_token_path, refresh_token_path) = _get_token_paths() + + Abort.require_condition( + access_token_path.exists(), + "Please login with your auth token first using the `armasec login` command", + raise_kwargs=dict(subject="You need to login"), + ) + + logger.debug("Retrieving access token from cache") + token_set: TokenSet = TokenSet(access_token=access_token_path.read_text()) + + if refresh_token_path.exists(): + logger.debug("Retrieving refresh token from cache") + token_set.refresh_token = refresh_token_path.read_text() + + return token_set + + +def save_tokens_to_cache(token_set: TokenSet): + """ + Saves tokens from a token_set to the cache. + """ + (access_token_path, refresh_token_path) = _get_token_paths() + + logger.debug(f"Caching access token at {access_token_path}") + access_token_path.write_text(token_set.access_token) + access_token_path.chmod(0o600) + + if token_set.refresh_token is not None: + logger.debug(f"Caching refresh token at {refresh_token_path}") + refresh_token_path.write_text(token_set.refresh_token) + refresh_token_path.chmod(0o600) + + +def clear_token_cache(): + """ + Clears the token cache. + """ + logger.debug("Clearing cached tokens") + (access_token_path, refresh_token_path) = _get_token_paths() + + logger.debug(f"Removing access token at {access_token_path}") + if access_token_path.exists(): + access_token_path.unlink() + + logger.debug(f"Removing refresh token at {refresh_token_path}") + if refresh_token_path.exists(): + refresh_token_path.unlink() diff --git a/cli/client.py b/cli/client.py new file mode 100644 index 0000000..5ec26b1 --- /dev/null +++ b/cli/client.py @@ -0,0 +1,265 @@ +from __future__ import annotations + +from functools import wraps +from pathlib import Path +from typing import Any, TypeVar + +import httpx +import pydantic +import snick +import typer +from loguru import logger + +from cli.exceptions import Abort +from cli.config import Settings + + +def build_client(settings: Settings): + protocol = "https" if settings.oidc_use_https else "http" + base_url = f"{protocol}://{settings.oidc_domain}" + logger.debug(f"Creating client with base URL {base_url}") + return httpx.Client( + base_url=base_url, + headers={"content-type": "application/x-www-form-urlencoded"}, + ) + +def attach_client(func): + + @wraps(func) + def wrapper(ctx: typer.Context, *args, **kwargs): + + if ctx.obj.settings is None: + raise Abort( + """ + Cannot attach client before settings! + + Check the order of the decorators and make sure settings are attached + first. + """, + subject="Attaching out of order", + log_message="Attaching out of order", + ) + + logger.debug("Binding client to CLI context") + ctx.obj.client = build_client(ctx.obj.settings) + return func(ctx, *args, **kwargs) + return wrapper + + +def _deserialize_request_model( + request_model: pydantic.BaseModel, + request_kwargs: dict[str, Any], + abort_message: str, + abort_subject: str, +): + """ + Deserialize a pydantic model instance into request_kwargs for a request in place. + """ + Abort.require_condition( + all( + [ + "data" not in request_kwargs, + "json" not in request_kwargs, + "content" not in request_kwargs, + ] + ), + snick.unwrap( + f""" + {abort_message}: + Request was incorrectly structured. + """ + ), + raise_kwargs=dict( + subject=abort_subject, + log_message=snick.unwrap( + """ + When using `request_model`, you may not pass + `data`, `json`, or `content` in the `request_kwargs` + """ + ), + ), + ) + with Abort.handle_errors( + snick.unwrap( + f""" + {abort_message}: + Request data could not be deserialized for http request. + """ + ), + raise_kwargs=dict( + subject=abort_subject, + log_message=snick.unwrap( + f""" + Could not deserialize instance of {request_model.__class__}: + {request_model} + """ + ), + ), + ): + request_kwargs["content"] = request_model.json() + request_kwargs["headers"] = {"Content-Type": "application/json"} + + +ResponseModel = TypeVar("ResponseModel", bound=pydantic.BaseModel) + + +def make_request( + client: httpx.Client, + url_path: str, + method: str, + *, + expected_status: int | None = None, + expect_response: bool = True, + abort_message: str = "There was an error communicating with the API", + abort_subject: str = "REQUEST FAILED", + response_model_cls: type[ResponseModel] | None = None, + request_model: pydantic.BaseModel | None = None, + save_to_file: Path | None = None, + **request_kwargs, +) -> ResponseModel | dict | int: + """ + Make a request against the Jobbergate API. + + Args: + url_path: The path to add to the base url of the client where the + request should be sent + method: The REST method to use for the request + (GET, PUT, UPDATE, POST, DELETE, etc) + expected_status: The status code to expect on the response. If it is not + received, raise an Abort + expect_response: Indicates if response data (JSON) is expected from the API + endpoint + abort_message: The message to show the user if there is a problem and the + app must be aborted + abort_subject: The subject to use in Abort output to the user + response_model_cls: If supplied, serialize the response data into this Pydantic + model class + request_model: Use a pydantic model instance as the data body for the + request + request_kwargs: Any additional keyword arguments that need to be passed on + to the client + """ + + if request_model is not None: + _deserialize_request_model( + request_model, + request_kwargs, + abort_message, + abort_subject, + ) + + logger.debug(f"Making request to url_path={url_path}") + request = client.build_request(method, url_path, **request_kwargs) + + # Look for the request body in the request_kwargs + debug_request_body = request_kwargs.get( + "data", + request_kwargs.get("json", request_kwargs.get("content")), + ) + logger.debug( + snick.dedent( + f""" + Request built with: + url: {request.url} + method: {method} + headers: {request.headers} + body: {debug_request_body} + """ + ) + ) + + with Abort.handle_errors( + snick.unwrap( + f""" + {abort_message}: + Communication with the API failed. + """ + ), + raise_kwargs=dict( + subject=abort_subject, + log_message="There was an error making the request to the API", + ), + ): + response = client.send(request) + + if expected_status is not None and response.status_code != expected_status: + if ( + method in ("PATCH", "PUT", "DELETE") + and response.status_code == 403 + and "does not own" in response.text + ): + raise Abort( + snick.dedent( + f""" + {abort_message}: + [red]You do not own this resource.[/red] + Please contact the owner if you need it to be modified. + """, + ), + subject=abort_subject, + log_message=snick.unwrap( + f""" + Resource could not be modified by non-owner: + {response.text} + """ + ), + ) + else: + raise Abort( + snick.unwrap( + f""" + {abort_message}: + Received an error response. + """ + ), + subject=abort_subject, + log_message=snick.unwrap( + f""" + Got an error code for request: + {response.status_code}: + {response.text} + """ + ), + ) + + if save_to_file is not None: + save_to_file.parent.mkdir(parents=True, exist_ok=True) + save_to_file.write_bytes(response.content) + return response.status_code + + # TODO: constrain methods with a named enum + if expect_response is False or method == "DELETE": + return response.status_code + + with Abort.handle_errors( + snick.unwrap( + f""" + {abort_message}: + Response carried no data. + """ + ), + raise_kwargs=dict( + subject=abort_subject, + log_message=f"Failed unpacking json: {response.text}", + ), + ): + data = response.json() + logger.debug(f"Extracted data from response: {data}") + + if response_model_cls is None: + return data + + logger.debug("Validating response data with ResponseModel") + with Abort.handle_errors( + snick.unwrap( + f""" + {abort_message}: + Unexpected data in response. + """ + ), + raise_kwargs=dict( + subject=abort_subject, + log_message=f"Unexpected format in response data: {data}", + ), + ): + return response_model_cls(**data) diff --git a/cli/config.py b/cli/config.py new file mode 100644 index 0000000..b2a9e9a --- /dev/null +++ b/cli/config.py @@ -0,0 +1,82 @@ +import json +from functools import wraps +from pathlib import Path + +import snick +import typer +from auto_name_enum import AutoNameEnum, auto +from loguru import logger +from pydantic import BaseModel, ValidationError + +from cli.exceptions import Abort + +cache_dir: Path = Path.home() / ".local/share/armasec-cli" +settings_path = cache_dir / "armasec.json" + + +class OidcProvider(AutoNameEnum): + AUTH0 = auto() + KEYCLOAK = auto() + + + +class Settings(BaseModel): + oidc_domain: str + oidc_audience: str + oidc_client_id: str + oidc_use_https: bool = True + oidc_max_poll_time: int = 5 * 60 # 5 minutes + oidc_provider: OidcProvider = OidcProvider.KEYCLOAK + + +def init_settings(**settings_values): + try: + logger.debug("Validating settings") + return Settings(**settings_values) + except ValidationError as err: + raise Abort( + snick.conjoin( + "A configuration error was detected.", + "", + "Details:", + "", + f"[red]{err}[/red]", + ), + subject="Configuration Error", + log_message="Configuration error", + ) + + +def attach_settings(func): + + @wraps(func) + def wrapper(ctx: typer.Context, *args, **kwargs): + + try: + logger.debug(f"Loading settings from {settings_path}") + settings_values = json.loads(settings_path.read_text()) + except FileNotFoundError: + raise Abort( + f""" + No settings file found at {settings_path}! + + Run the set-config sub-command first to establish your OIDC settings. + """, + subject="Settings file missing!", + log_message="Settings file missing!", + ) + logger.debug("Binding settings to CLI context") + ctx.obj.settings = init_settings(**settings_values) + return func(ctx, *args, **kwargs) + return wrapper + + +def dump_settings(settings: Settings): + logger.debug(f"Saving settings to {settings_path}") + settings_values = json.dumps(settings.dict()) + settings_path.write_text(settings_values) + + +def clear_settings(): + logger.debug(f"Removing saved settings at {settings_path}") + settings_path.unlink(missing_ok=True) diff --git a/cli/exceptions.py b/cli/exceptions.py new file mode 100644 index 0000000..eb58e0d --- /dev/null +++ b/cli/exceptions.py @@ -0,0 +1,64 @@ +from sys import exc_info +from functools import wraps + +import buzz +import snick +import typer +from loguru import logger +from rich import traceback +from rich.console import Console +from rich.panel import Panel + + + +# Enables prettified traceback printing via rich +traceback.install() + + +class ArmasecCliError(buzz.Buzz): + pass + + +class Abort(buzz.Buzz): + def __init__( + self, + message, + *args, + subject=None, + log_message=None, + warn_only=False, + **kwargs, + ): + self.subject = subject + self.log_message = log_message + self.warn_only = warn_only + (_, self.original_error, __) = exc_info() + super().__init__(message, *args, **kwargs) + + +def handle_abort(func): + + @wraps(func) + def wrapper(*args, **kwargs): + try: + func(*args, **kwargs) + except Abort as err: + if not err.warn_only: + if err.log_message is not None: + logger.error(err.log_message) + + if err.original_error is not None: + logger.error(f"Original exception: {err.original_error}") + + panel_kwargs = dict() + if err.subject is not None: + panel_kwargs["title"] = f"[red]{err.subject}" + message = snick.dedent(err.message) + + console = Console() + console.print() + console.print(Panel(message, **panel_kwargs)) + console.print() + raise typer.Exit(code=1) + + return wrapper diff --git a/cli/format.py b/cli/format.py new file mode 100644 index 0000000..4a89a0b --- /dev/null +++ b/cli/format.py @@ -0,0 +1,28 @@ +import json +from typing import Any + +import snick +from rich.console import Console +from rich.panel import Panel + + +def terminal_message(message, subject=None, color="green", footer=None, indent=True): + panel_kwargs = dict(padding=1) + if subject is not None: + panel_kwargs["title"] = f"[{color}]{subject}" + if footer is not None: + panel_kwargs["subtitle"] = f"[dim italic]{footer}[/dim italic]" + text = snick.dedent(message) + if indent: + text = snick.indent(text, prefix=" ") + console = Console() + console.print() + console.print(Panel(text, **panel_kwargs)) + console.print() + + +def render_json(data: Any): + console = Console() + console.print() + console.print_json(json.dumps(data)) + console.print() diff --git a/cli/logging.py b/cli/logging.py new file mode 100644 index 0000000..99b8112 --- /dev/null +++ b/cli/logging.py @@ -0,0 +1,12 @@ +import sys + +from loguru import logger + + +def init_logs(verbose=False): + logger.remove() + + if verbose: + logger.add(sys.stdout, level="DEBUG") + + logger.debug("Logging initialized") diff --git a/cli/main.py b/cli/main.py new file mode 100644 index 0000000..71016b5 --- /dev/null +++ b/cli/main.py @@ -0,0 +1,232 @@ +from __future__ import annotations + + +import jose +import pyperclip +import snick +import typer + +from cli.exceptions import Abort, handle_abort +from cli.schemas import TokenSet, Persona, CliContext +from cli.cache import init_cache, load_tokens_from_cache, clear_token_cache +from cli.format import terminal_message, render_json +from cli.auth import fetch_auth_tokens, extract_persona +from cli.config import OidcProvider, attach_settings, init_settings, dump_settings, clear_settings +from cli.client import attach_client +from cli.logging import init_logs + + +app = typer.Typer() + + +@app.callback(invoke_without_command=True) +@handle_abort +def main( + ctx: typer.Context, + verbose: bool = typer.Option(False, help="Enable verbose logging to the terminal"), +): + """ + Welcome to the Armasec CLI! + + More information can be shown for each command listed below by running it with the + --help option. + """ + + if ctx.invoked_subcommand is None: + terminal_message( + snick.conjoin( + "No command provided. Please check [bold magenta]usage[/bold magenta]", + "", + f"[yellow]{ctx.get_help()}[/yellow]", + ), + subject="Need an Armasec command", + ) + raise typer.Exit() + + init_logs(verbose=verbose) + ctx.obj = CliContext() + + + +@app.command() +@handle_abort +@init_cache +@attach_settings +@attach_client +def login(ctx: typer.Context): + token_set: TokenSet = fetch_auth_tokens(ctx.obj) + persona: Persona = extract_persona(token_set) + terminal_message( + f"User was logged in with email '{persona.identity_data.email}'", + subject="Logged in!", + ) + +@app.command() +@handle_abort +@init_cache +def logout(): + """ + Logs out of the jobbergate-cli. Clears the saved user credentials. + """ + clear_token_cache() + terminal_message( + "User was logged out.", + subject="Logged out", + ) + + +@app.command() +@init_cache +def set_config( + domain: str = typer.Option(..., help="The domain used by your OIDC provider"), + audience: str = typer.Option( + ..., + help="The audience required by your OIDC provider", + ), + client_id: str = typer.Option( + ..., + help="The unique client ID to use for logging in via CLI", + ), + use_https: bool = typer.Option(True, help="Use https for requests"), + max_poll_time: int = typer.Option( + 5 * 60, + help="The max time to wait for login to complete", + ), + provider: OidcProvider = typer.Option( + OidcProvider.KEYCLOAK, + help="The OIDC provider you use", + ), +): + settings = init_settings( + oidc_domain=domain, + oidc_audience=audience, + oidc_client_id=client_id, + oidc_use_https=use_https, + oidc_max_poll_time=max_poll_time, + oidc_provider=provider, + ) + dump_settings(settings) + + +@app.command() +@handle_abort +@init_cache +@attach_settings +def show_config(ctx: typer.Context): + """ + Show the current config. + """ + render_json(ctx.obj.settings.dict()) + + +@app.command() +@handle_abort +@init_cache +def clear_config(): + """ + Show the current config. + """ + clear_settings() + + +@app.command() +@handle_abort +@init_cache +def show_token( + plain: bool = typer.Option( + False, + help="Show the token in plain text.", + ), + refresh: bool = typer.Option( + False, + help="Show the refresh token instead of the access token.", + ), + show_prefix: bool = typer.Option( + False, + "--prefix", + help="Include the 'Bearer' prefix in the output.", + ), + show_header: bool = typer.Option( + False, + "--header", + help="Show the token as it would appear in a request header.", + ), + decode: bool = typer.Option( + False, + "--decode", + help="Show the content of the decoded access token.", + ), +): + """ + Show the token for the logged in user. + + Token output is automatically copied to your clipboard. + """ + token_set: TokenSet = load_tokens_from_cache() + token: str | None = None + if not refresh: + token = token_set.access_token + subject = "Access Token" + Abort.require_condition( + token is not None, + "User is not logged in. Please log in first.", + raise_kwargs=dict( + subject="Not logged in", + ), + ) + else: + token = token_set.refresh_token + subject = "Refresh Token" + Abort.require_condition( + token is not None, + snick.unwrap( + """ + User is not logged in or does not have a refresh token. + Please try loggin in again. + """ + ), + raise_kwargs=dict( + subject="No refresh token", + ), + ) + + if decode: + # Decode the token with ALL verification turned off (we just want to unpack it) + content = jose.jwt.decode( + token, + "secret-will-be-ignored", + options=dict( + verify_signature=False, + verify_aud=False, + verify_iat=False, + verify_exp=False, + verify_nbf=False, + verify_iss=False, + verify_sub=False, + verify_jti=False, + verify_at_hash=False, + ), + ) + render_json(content) + return + + if show_header: + token_text = f"""{{ "Authorization": "Bearer {token}" }}""" + else: + prefix = "Bearer " if show_prefix else "" + token_text = f"{prefix}{token}" + + try: + pyperclip.copy(token_text) + on_clipboard = True + except Exception: + on_clipboard = False + + if plain: + print(token_text) + else: + kwargs = dict(subject=subject, indent=False) + if on_clipboard: + kwargs["footer"] = "The output was copied to your clipboard" + + terminal_message(token_text, **kwargs) diff --git a/cli/schemas.py b/cli/schemas.py new file mode 100644 index 0000000..40b2d46 --- /dev/null +++ b/cli/schemas.py @@ -0,0 +1,33 @@ +from typing import Optional + +import httpx +from pydantic import BaseModel + +from cli.config import Settings + + +class TokenSet(BaseModel): + access_token: str + refresh_token: Optional[str] = None + + +class IdentityData(BaseModel): + client_id: str + email: Optional[str] = None + + +class Persona(BaseModel): + token_set: TokenSet + identity_data: IdentityData + + +class DeviceCodeData(BaseModel): + device_code: str + verification_uri_complete: str + interval: int + + +class CliContext(BaseModel, arbitrary_types_allowed=True): + persona: Optional[Persona] + client: Optional[httpx.Client] + settings: Optional[Settings] diff --git a/cli/time_loop.py b/cli/time_loop.py new file mode 100644 index 0000000..34e1ef3 --- /dev/null +++ b/cli/time_loop.py @@ -0,0 +1,112 @@ +from __future__ import annotations + +from dataclasses import dataclass + +import pendulum +from rich.progress import Progress + +from cli.exceptions import ArmasecCliError + + +@dataclass +class Tick: + counter: int + elapsed: pendulum.Duration + total_elapsed: pendulum.Duration + + +class TimeLoop: + advent: pendulum.DateTime | None + moment: pendulum.DateTime | None + last_moment: pendulum.DateTime | None + counter: int + progress: Progress | None + duration: pendulum.Duration + message: str + color: str + + def __init__( + self, + duration: pendulum.Duration | int, + message: str = "Processing", + color: str = "green", + ): + self.moment = None + self.last_moment = None + self.counter = 0 + self.progress = None + if isinstance(duration, int): + ArmasecCliError.require_condition( + duration > 0, + "The duration must be a positive integer", + ) + self.duration = pendulum.duration(seconds=duration) + else: + self.duration = duration + self.message = message + self.color = color + + def __del__(self): + """ + Explicitly clear the progress meter if the time-loop is destroyed. + """ + self.clear() + + def __iter__(self) -> "TimeLoop": + """ + Start the iterator. + + Creates and starts the progress meter + """ + self.advent = self.last_moment = self.moment = pendulum.now() + self.counter = 0 + self.progress = Progress() + self.progress.add_task( + f"[{self.color}]{self.message}...", + total=self.duration.total_seconds(), + ) + self.progress.start() + return self + + def __next__(self) -> Tick: + """ + Iterates the time loop and returns a tick. + + If the duration is complete, clear the progress meter and stop iteration. + """ + progress: Progress = ArmasecCliError.enforce_defined( + self.progress, + "Progress bar has not been initialized...this should not happen", + ) + + self.counter += 1 + self.last_moment = self.moment + self.moment: pendulum.DateTime = pendulum.now() + elapsed: pendulum.Duration = self.moment - self.last_moment + total_elapsed: pendulum.Duration = self.moment - self.advent + + for task_id in progress.task_ids: + progress.advance(task_id, elapsed.total_seconds()) + + if progress.finished: + self.clear() + raise StopIteration + + return Tick( + counter=self.counter, + elapsed=elapsed, + total_elapsed=total_elapsed, + ) + + def clear(self): + """ + Clear the time-loop. + + Stops the progress meter (if set) and reset moments, counter, progress meter. + """ + if self.progress is not None: + self.progress.stop() + self.counter = 0 + self.progress = None + self.moment = None + self.last_moment = None diff --git a/poetry.lock b/poetry.lock index dcf085f..a056834 100644 --- a/poetry.lock +++ b/poetry.lock @@ -119,40 +119,23 @@ files = [ ] [[package]] -name = "black" -version = "22.12.0" -description = "The uncompromising code formatter." +name = "beautifulsoup4" +version = "4.12.2" +description = "Screen-scraping library" category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.6.0" files = [ - {file = "black-22.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9eedd20838bd5d75b80c9f5487dbcb06836a43833a37846cf1d8c1cc01cef59d"}, - {file = "black-22.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:159a46a4947f73387b4d83e87ea006dbb2337eab6c879620a3ba52699b1f4351"}, - {file = "black-22.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d30b212bffeb1e252b31dd269dfae69dd17e06d92b87ad26e23890f3efea366f"}, - {file = "black-22.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:7412e75863aa5c5411886804678b7d083c7c28421210180d67dfd8cf1221e1f4"}, - {file = "black-22.12.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c116eed0efb9ff870ded8b62fe9f28dd61ef6e9ddd28d83d7d264a38417dcee2"}, - {file = "black-22.12.0-cp37-cp37m-win_amd64.whl", hash = "sha256:1f58cbe16dfe8c12b7434e50ff889fa479072096d79f0a7f25e4ab8e94cd8350"}, - {file = "black-22.12.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77d86c9f3db9b1bf6761244bc0b3572a546f5fe37917a044e02f3166d5aafa7d"}, - {file = "black-22.12.0-cp38-cp38-win_amd64.whl", hash = "sha256:82d9fe8fee3401e02e79767016b4907820a7dc28d70d137eb397b92ef3cc5bfc"}, - {file = "black-22.12.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:101c69b23df9b44247bd88e1d7e90154336ac4992502d4197bdac35dd7ee3320"}, - {file = "black-22.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:559c7a1ba9a006226f09e4916060982fd27334ae1998e7a38b3f33a37f7a2148"}, - {file = "black-22.12.0-py3-none-any.whl", hash = "sha256:436cc9167dd28040ad90d3b404aec22cedf24a6e4d7de221bec2730ec0c97bcf"}, - {file = "black-22.12.0.tar.gz", hash = "sha256:229351e5a18ca30f447bf724d007f890f97e13af070bb6ad4c0a441cd7596a2f"}, + {file = "beautifulsoup4-4.12.2-py3-none-any.whl", hash = "sha256:bd2520ca0d9d7d12694a53d44ac482d181b4ec1888909b035a3dbf40d0f57d4a"}, + {file = "beautifulsoup4-4.12.2.tar.gz", hash = "sha256:492bbc69dca35d12daac71c4db1bfff0c876c00ef4a2ffacce226d4638eb72da"}, ] [package.dependencies] -click = ">=8.0.0" -mypy-extensions = ">=0.4.3" -pathspec = ">=0.9.0" -platformdirs = ">=2" -tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""} -typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} +soupsieve = ">1.2" [package.extras] -colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.7.4)"] -jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] -uvloop = ["uvloop (>=0.15.2)"] +html5lib = ["html5lib"] +lxml = ["lxml"] [[package]] name = "blinker" @@ -342,14 +325,14 @@ files = [ [[package]] name = "click" -version = "8.1.6" +version = "8.1.7" description = "Composable command line interface toolkit" -category = "dev" +category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "click-8.1.6-py3-none-any.whl", hash = "sha256:fa244bb30b3b5ee2cae3da8f55c9e5e0c0e86093306301fb418eb9dc40fbded5"}, - {file = "click-8.1.6.tar.gz", hash = "sha256:48ee849951919527a045bfe3bf7baa8a959c423134e1a5b98c05c20ba75a1cbd"}, + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, ] [package.dependencies] @@ -369,72 +352,64 @@ files = [ [[package]] name = "coverage" -version = "7.2.7" +version = "7.3.0" description = "Code coverage measurement for Python" category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "coverage-7.2.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d39b5b4f2a66ccae8b7263ac3c8170994b65266797fb96cbbfd3fb5b23921db8"}, - {file = "coverage-7.2.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d040ef7c9859bb11dfeb056ff5b3872436e3b5e401817d87a31e1750b9ae2fb"}, - {file = "coverage-7.2.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba90a9563ba44a72fda2e85302c3abc71c5589cea608ca16c22b9804262aaeb6"}, - {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7d9405291c6928619403db1d10bd07888888ec1abcbd9748fdaa971d7d661b2"}, - {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31563e97dae5598556600466ad9beea39fb04e0229e61c12eaa206e0aa202063"}, - {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ebba1cd308ef115925421d3e6a586e655ca5a77b5bf41e02eb0e4562a111f2d1"}, - {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cb017fd1b2603ef59e374ba2063f593abe0fc45f2ad9abdde5b4d83bd922a353"}, - {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62a5c7dad11015c66fbb9d881bc4caa5b12f16292f857842d9d1871595f4495"}, - {file = "coverage-7.2.7-cp310-cp310-win32.whl", hash = "sha256:ee57190f24fba796e36bb6d3aa8a8783c643d8fa9760c89f7a98ab5455fbf818"}, - {file = "coverage-7.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:f75f7168ab25dd93110c8a8117a22450c19976afbc44234cbf71481094c1b850"}, - {file = "coverage-7.2.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06a9a2be0b5b576c3f18f1a241f0473575c4a26021b52b2a85263a00f034d51f"}, - {file = "coverage-7.2.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5baa06420f837184130752b7c5ea0808762083bf3487b5038d68b012e5937dbe"}, - {file = "coverage-7.2.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdec9e8cbf13a5bf63290fc6013d216a4c7232efb51548594ca3631a7f13c3a3"}, - {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52edc1a60c0d34afa421c9c37078817b2e67a392cab17d97283b64c5833f427f"}, - {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63426706118b7f5cf6bb6c895dc215d8a418d5952544042c8a2d9fe87fcf09cb"}, - {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:afb17f84d56068a7c29f5fa37bfd38d5aba69e3304af08ee94da8ed5b0865833"}, - {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:48c19d2159d433ccc99e729ceae7d5293fbffa0bdb94952d3579983d1c8c9d97"}, - {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0e1f928eaf5469c11e886fe0885ad2bf1ec606434e79842a879277895a50942a"}, - {file = "coverage-7.2.7-cp311-cp311-win32.whl", hash = "sha256:33d6d3ea29d5b3a1a632b3c4e4f4ecae24ef170b0b9ee493883f2df10039959a"}, - {file = "coverage-7.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:5b7540161790b2f28143191f5f8ec02fb132660ff175b7747b95dcb77ac26562"}, - {file = "coverage-7.2.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f2f67fe12b22cd130d34d0ef79206061bfb5eda52feb6ce0dba0644e20a03cf4"}, - {file = "coverage-7.2.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a342242fe22407f3c17f4b499276a02b01e80f861f1682ad1d95b04018e0c0d4"}, - {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:171717c7cb6b453aebac9a2ef603699da237f341b38eebfee9be75d27dc38e01"}, - {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49969a9f7ffa086d973d91cec8d2e31080436ef0fb4a359cae927e742abfaaa6"}, - {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b46517c02ccd08092f4fa99f24c3b83d8f92f739b4657b0f146246a0ca6a831d"}, - {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:a3d33a6b3eae87ceaefa91ffdc130b5e8536182cd6dfdbfc1aa56b46ff8c86de"}, - {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:976b9c42fb2a43ebf304fa7d4a310e5f16cc99992f33eced91ef6f908bd8f33d"}, - {file = "coverage-7.2.7-cp312-cp312-win32.whl", hash = "sha256:8de8bb0e5ad103888d65abef8bca41ab93721647590a3f740100cd65c3b00511"}, - {file = "coverage-7.2.7-cp312-cp312-win_amd64.whl", hash = "sha256:9e31cb64d7de6b6f09702bb27c02d1904b3aebfca610c12772452c4e6c21a0d3"}, - {file = "coverage-7.2.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58c2ccc2f00ecb51253cbe5d8d7122a34590fac9646a960d1430d5b15321d95f"}, - {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d22656368f0e6189e24722214ed8d66b8022db19d182927b9a248a2a8a2f67eb"}, - {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a895fcc7b15c3fc72beb43cdcbdf0ddb7d2ebc959edac9cef390b0d14f39f8a9"}, - {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e84606b74eb7de6ff581a7915e2dab7a28a0517fbe1c9239eb227e1354064dcd"}, - {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0a5f9e1dbd7fbe30196578ca36f3fba75376fb99888c395c5880b355e2875f8a"}, - {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:419bfd2caae268623dd469eff96d510a920c90928b60f2073d79f8fe2bbc5959"}, - {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2aee274c46590717f38ae5e4650988d1af340fe06167546cc32fe2f58ed05b02"}, - {file = "coverage-7.2.7-cp37-cp37m-win32.whl", hash = "sha256:61b9a528fb348373c433e8966535074b802c7a5d7f23c4f421e6c6e2f1697a6f"}, - {file = "coverage-7.2.7-cp37-cp37m-win_amd64.whl", hash = "sha256:b1c546aca0ca4d028901d825015dc8e4d56aac4b541877690eb76490f1dc8ed0"}, - {file = "coverage-7.2.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:54b896376ab563bd38453cecb813c295cf347cf5906e8b41d340b0321a5433e5"}, - {file = "coverage-7.2.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3d376df58cc111dc8e21e3b6e24606b5bb5dee6024f46a5abca99124b2229ef5"}, - {file = "coverage-7.2.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e330fc79bd7207e46c7d7fd2bb4af2963f5f635703925543a70b99574b0fea9"}, - {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e9d683426464e4a252bf70c3498756055016f99ddaec3774bf368e76bbe02b6"}, - {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d13c64ee2d33eccf7437961b6ea7ad8673e2be040b4f7fd4fd4d4d28d9ccb1e"}, - {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7aa5f8a41217360e600da646004f878250a0d6738bcdc11a0a39928d7dc2050"}, - {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8fa03bce9bfbeeef9f3b160a8bed39a221d82308b4152b27d82d8daa7041fee5"}, - {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:245167dd26180ab4c91d5e1496a30be4cd721a5cf2abf52974f965f10f11419f"}, - {file = "coverage-7.2.7-cp38-cp38-win32.whl", hash = "sha256:d2c2db7fd82e9b72937969bceac4d6ca89660db0a0967614ce2481e81a0b771e"}, - {file = "coverage-7.2.7-cp38-cp38-win_amd64.whl", hash = "sha256:2e07b54284e381531c87f785f613b833569c14ecacdcb85d56b25c4622c16c3c"}, - {file = "coverage-7.2.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:537891ae8ce59ef63d0123f7ac9e2ae0fc8b72c7ccbe5296fec45fd68967b6c9"}, - {file = "coverage-7.2.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06fb182e69f33f6cd1d39a6c597294cff3143554b64b9825d1dc69d18cc2fff2"}, - {file = "coverage-7.2.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:201e7389591af40950a6480bd9edfa8ed04346ff80002cec1a66cac4549c1ad7"}, - {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f6951407391b639504e3b3be51b7ba5f3528adbf1a8ac3302b687ecababf929e"}, - {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f48351d66575f535669306aa7d6d6f71bc43372473b54a832222803eb956fd1"}, - {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b29019c76039dc3c0fd815c41392a044ce555d9bcdd38b0fb60fb4cd8e475ba9"}, - {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:81c13a1fc7468c40f13420732805a4c38a105d89848b7c10af65a90beff25250"}, - {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:975d70ab7e3c80a3fe86001d8751f6778905ec723f5b110aed1e450da9d4b7f2"}, - {file = "coverage-7.2.7-cp39-cp39-win32.whl", hash = "sha256:7ee7d9d4822c8acc74a5e26c50604dff824710bc8de424904c0982e25c39c6cb"}, - {file = "coverage-7.2.7-cp39-cp39-win_amd64.whl", hash = "sha256:eb393e5ebc85245347950143969b241d08b52b88a3dc39479822e073a1a8eb27"}, - {file = "coverage-7.2.7-pp37.pp38.pp39-none-any.whl", hash = "sha256:b7b4c971f05e6ae490fef852c218b0e79d4e52f79ef0c8475566584a8fb3e01d"}, - {file = "coverage-7.2.7.tar.gz", hash = "sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59"}, + {file = "coverage-7.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:db76a1bcb51f02b2007adacbed4c88b6dee75342c37b05d1822815eed19edee5"}, + {file = "coverage-7.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c02cfa6c36144ab334d556989406837336c1d05215a9bdf44c0bc1d1ac1cb637"}, + {file = "coverage-7.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:477c9430ad5d1b80b07f3c12f7120eef40bfbf849e9e7859e53b9c93b922d2af"}, + {file = "coverage-7.3.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce2ee86ca75f9f96072295c5ebb4ef2a43cecf2870b0ca5e7a1cbdd929cf67e1"}, + {file = "coverage-7.3.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68d8a0426b49c053013e631c0cdc09b952d857efa8f68121746b339912d27a12"}, + {file = "coverage-7.3.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b3eb0c93e2ea6445b2173da48cb548364f8f65bf68f3d090404080d338e3a689"}, + {file = "coverage-7.3.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:90b6e2f0f66750c5a1178ffa9370dec6c508a8ca5265c42fbad3ccac210a7977"}, + {file = "coverage-7.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:96d7d761aea65b291a98c84e1250cd57b5b51726821a6f2f8df65db89363be51"}, + {file = "coverage-7.3.0-cp310-cp310-win32.whl", hash = "sha256:63c5b8ecbc3b3d5eb3a9d873dec60afc0cd5ff9d9f1c75981d8c31cfe4df8527"}, + {file = "coverage-7.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:97c44f4ee13bce914272589b6b41165bbb650e48fdb7bd5493a38bde8de730a1"}, + {file = "coverage-7.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:74c160285f2dfe0acf0f72d425f3e970b21b6de04157fc65adc9fd07ee44177f"}, + {file = "coverage-7.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b543302a3707245d454fc49b8ecd2c2d5982b50eb63f3535244fd79a4be0c99d"}, + {file = "coverage-7.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad0f87826c4ebd3ef484502e79b39614e9c03a5d1510cfb623f4a4a051edc6fd"}, + {file = "coverage-7.3.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:13c6cbbd5f31211d8fdb477f0f7b03438591bdd077054076eec362cf2207b4a7"}, + {file = "coverage-7.3.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fac440c43e9b479d1241fe9d768645e7ccec3fb65dc3a5f6e90675e75c3f3e3a"}, + {file = "coverage-7.3.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:3c9834d5e3df9d2aba0275c9f67989c590e05732439b3318fa37a725dff51e74"}, + {file = "coverage-7.3.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4c8e31cf29b60859876474034a83f59a14381af50cbe8a9dbaadbf70adc4b214"}, + {file = "coverage-7.3.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7a9baf8e230f9621f8e1d00c580394a0aa328fdac0df2b3f8384387c44083c0f"}, + {file = "coverage-7.3.0-cp311-cp311-win32.whl", hash = "sha256:ccc51713b5581e12f93ccb9c5e39e8b5d4b16776d584c0f5e9e4e63381356482"}, + {file = "coverage-7.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:887665f00ea4e488501ba755a0e3c2cfd6278e846ada3185f42d391ef95e7e70"}, + {file = "coverage-7.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d000a739f9feed900381605a12a61f7aaced6beae832719ae0d15058a1e81c1b"}, + {file = "coverage-7.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:59777652e245bb1e300e620ce2bef0d341945842e4eb888c23a7f1d9e143c446"}, + {file = "coverage-7.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9737bc49a9255d78da085fa04f628a310c2332b187cd49b958b0e494c125071"}, + {file = "coverage-7.3.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5247bab12f84a1d608213b96b8af0cbb30d090d705b6663ad794c2f2a5e5b9fe"}, + {file = "coverage-7.3.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2ac9a1de294773b9fa77447ab7e529cf4fe3910f6a0832816e5f3d538cfea9a"}, + {file = "coverage-7.3.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:85b7335c22455ec12444cec0d600533a238d6439d8d709d545158c1208483873"}, + {file = "coverage-7.3.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:36ce5d43a072a036f287029a55b5c6a0e9bd73db58961a273b6dc11a2c6eb9c2"}, + {file = "coverage-7.3.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:211a4576e984f96d9fce61766ffaed0115d5dab1419e4f63d6992b480c2bd60b"}, + {file = "coverage-7.3.0-cp312-cp312-win32.whl", hash = "sha256:56afbf41fa4a7b27f6635bc4289050ac3ab7951b8a821bca46f5b024500e6321"}, + {file = "coverage-7.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f297e0c1ae55300ff688568b04ff26b01c13dfbf4c9d2b7d0cb688ac60df479"}, + {file = "coverage-7.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ac0dec90e7de0087d3d95fa0533e1d2d722dcc008bc7b60e1143402a04c117c1"}, + {file = "coverage-7.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:438856d3f8f1e27f8e79b5410ae56650732a0dcfa94e756df88c7e2d24851fcd"}, + {file = "coverage-7.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1084393c6bda8875c05e04fce5cfe1301a425f758eb012f010eab586f1f3905e"}, + {file = "coverage-7.3.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49ab200acf891e3dde19e5aa4b0f35d12d8b4bd805dc0be8792270c71bd56c54"}, + {file = "coverage-7.3.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a67e6bbe756ed458646e1ef2b0778591ed4d1fcd4b146fc3ba2feb1a7afd4254"}, + {file = "coverage-7.3.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8f39c49faf5344af36042b293ce05c0d9004270d811c7080610b3e713251c9b0"}, + {file = "coverage-7.3.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:7df91fb24c2edaabec4e0eee512ff3bc6ec20eb8dccac2e77001c1fe516c0c84"}, + {file = "coverage-7.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:34f9f0763d5fa3035a315b69b428fe9c34d4fc2f615262d6be3d3bf3882fb985"}, + {file = "coverage-7.3.0-cp38-cp38-win32.whl", hash = "sha256:bac329371d4c0d456e8d5f38a9b0816b446581b5f278474e416ea0c68c47dcd9"}, + {file = "coverage-7.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:b859128a093f135b556b4765658d5d2e758e1fae3e7cc2f8c10f26fe7005e543"}, + {file = "coverage-7.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fc0ed8d310afe013db1eedd37176d0839dc66c96bcfcce8f6607a73ffea2d6ba"}, + {file = "coverage-7.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61260ec93f99f2c2d93d264b564ba912bec502f679793c56f678ba5251f0393"}, + {file = "coverage-7.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97af9554a799bd7c58c0179cc8dbf14aa7ab50e1fd5fa73f90b9b7215874ba28"}, + {file = "coverage-7.3.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3558e5b574d62f9c46b76120a5c7c16c4612dc2644c3d48a9f4064a705eaee95"}, + {file = "coverage-7.3.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37d5576d35fcb765fca05654f66aa71e2808d4237d026e64ac8b397ffa66a56a"}, + {file = "coverage-7.3.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:07ea61bcb179f8f05ffd804d2732b09d23a1238642bf7e51dad62082b5019b34"}, + {file = "coverage-7.3.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:80501d1b2270d7e8daf1b64b895745c3e234289e00d5f0e30923e706f110334e"}, + {file = "coverage-7.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4eddd3153d02204f22aef0825409091a91bf2a20bce06fe0f638f5c19a85de54"}, + {file = "coverage-7.3.0-cp39-cp39-win32.whl", hash = "sha256:2d22172f938455c156e9af2612650f26cceea47dc86ca048fa4e0b2d21646ad3"}, + {file = "coverage-7.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:60f64e2007c9144375dd0f480a54d6070f00bb1a28f65c408370544091c9bc9e"}, + {file = "coverage-7.3.0-pp38.pp39.pp310-none-any.whl", hash = "sha256:5492a6ce3bdb15c6ad66cb68a0244854d9917478877a25671d70378bdc8562d0"}, + {file = "coverage-7.3.0.tar.gz", hash = "sha256:49dbb19cdcafc130f597d9e04a29d0a032ceedf729e41b181f51cd170e6ee865"}, ] [package.dependencies] @@ -445,35 +420,35 @@ toml = ["tomli"] [[package]] name = "cryptography" -version = "41.0.2" +version = "41.0.3" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "cryptography-41.0.2-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:01f1d9e537f9a15b037d5d9ee442b8c22e3ae11ce65ea1f3316a41c78756b711"}, - {file = "cryptography-41.0.2-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:079347de771f9282fbfe0e0236c716686950c19dee1b76240ab09ce1624d76d7"}, - {file = "cryptography-41.0.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:439c3cc4c0d42fa999b83ded80a9a1fb54d53c58d6e59234cfe97f241e6c781d"}, - {file = "cryptography-41.0.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f14ad275364c8b4e525d018f6716537ae7b6d369c094805cae45300847e0894f"}, - {file = "cryptography-41.0.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:84609ade00a6ec59a89729e87a503c6e36af98ddcd566d5f3be52e29ba993182"}, - {file = "cryptography-41.0.2-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:49c3222bb8f8e800aead2e376cbef687bc9e3cb9b58b29a261210456a7783d83"}, - {file = "cryptography-41.0.2-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:d73f419a56d74fef257955f51b18d046f3506270a5fd2ac5febbfa259d6c0fa5"}, - {file = "cryptography-41.0.2-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:2a034bf7d9ca894720f2ec1d8b7b5832d7e363571828037f9e0c4f18c1b58a58"}, - {file = "cryptography-41.0.2-cp37-abi3-win32.whl", hash = "sha256:d124682c7a23c9764e54ca9ab5b308b14b18eba02722b8659fb238546de83a76"}, - {file = "cryptography-41.0.2-cp37-abi3-win_amd64.whl", hash = "sha256:9c3fe6534d59d071ee82081ca3d71eed3210f76ebd0361798c74abc2bcf347d4"}, - {file = "cryptography-41.0.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a719399b99377b218dac6cf547b6ec54e6ef20207b6165126a280b0ce97e0d2a"}, - {file = "cryptography-41.0.2-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:182be4171f9332b6741ee818ec27daff9fb00349f706629f5cbf417bd50e66fd"}, - {file = "cryptography-41.0.2-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:7a9a3bced53b7f09da251685224d6a260c3cb291768f54954e28f03ef14e3766"}, - {file = "cryptography-41.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f0dc40e6f7aa37af01aba07277d3d64d5a03dc66d682097541ec4da03cc140ee"}, - {file = "cryptography-41.0.2-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:674b669d5daa64206c38e507808aae49904c988fa0a71c935e7006a3e1e83831"}, - {file = "cryptography-41.0.2-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7af244b012711a26196450d34f483357e42aeddb04128885d95a69bd8b14b69b"}, - {file = "cryptography-41.0.2-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:9b6d717393dbae53d4e52684ef4f022444fc1cce3c48c38cb74fca29e1f08eaa"}, - {file = "cryptography-41.0.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:192255f539d7a89f2102d07d7375b1e0a81f7478925b3bc2e0549ebf739dae0e"}, - {file = "cryptography-41.0.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f772610fe364372de33d76edcd313636a25684edb94cee53fd790195f5989d14"}, - {file = "cryptography-41.0.2-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:b332cba64d99a70c1e0836902720887fb4529ea49ea7f5462cf6640e095e11d2"}, - {file = "cryptography-41.0.2-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:9a6673c1828db6270b76b22cc696f40cde9043eb90373da5c2f8f2158957f42f"}, - {file = "cryptography-41.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:342f3767e25876751e14f8459ad85e77e660537ca0a066e10e75df9c9e9099f0"}, - {file = "cryptography-41.0.2.tar.gz", hash = "sha256:7d230bf856164de164ecb615ccc14c7fc6de6906ddd5b491f3af90d3514c925c"}, + {file = "cryptography-41.0.3-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:652627a055cb52a84f8c448185922241dd5217443ca194d5739b44612c5e6507"}, + {file = "cryptography-41.0.3-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:8f09daa483aedea50d249ef98ed500569841d6498aa9c9f4b0531b9964658922"}, + {file = "cryptography-41.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fd871184321100fb400d759ad0cddddf284c4b696568204d281c902fc7b0d81"}, + {file = "cryptography-41.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84537453d57f55a50a5b6835622ee405816999a7113267739a1b4581f83535bd"}, + {file = "cryptography-41.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3fb248989b6363906827284cd20cca63bb1a757e0a2864d4c1682a985e3dca47"}, + {file = "cryptography-41.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:42cb413e01a5d36da9929baa9d70ca90d90b969269e5a12d39c1e0d475010116"}, + {file = "cryptography-41.0.3-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:aeb57c421b34af8f9fe830e1955bf493a86a7996cc1338fe41b30047d16e962c"}, + {file = "cryptography-41.0.3-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:6af1c6387c531cd364b72c28daa29232162010d952ceb7e5ca8e2827526aceae"}, + {file = "cryptography-41.0.3-cp37-abi3-win32.whl", hash = "sha256:0d09fb5356f975974dbcb595ad2d178305e5050656affb7890a1583f5e02a306"}, + {file = "cryptography-41.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:a983e441a00a9d57a4d7c91b3116a37ae602907a7618b882c8013b5762e80574"}, + {file = "cryptography-41.0.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5259cb659aa43005eb55a0e4ff2c825ca111a0da1814202c64d28a985d33b087"}, + {file = "cryptography-41.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:67e120e9a577c64fe1f611e53b30b3e69744e5910ff3b6e97e935aeb96005858"}, + {file = "cryptography-41.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:7efe8041897fe7a50863e51b77789b657a133c75c3b094e51b5e4b5cec7bf906"}, + {file = "cryptography-41.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ce785cf81a7bdade534297ef9e490ddff800d956625020ab2ec2780a556c313e"}, + {file = "cryptography-41.0.3-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:57a51b89f954f216a81c9d057bf1a24e2f36e764a1ca9a501a6964eb4a6800dd"}, + {file = "cryptography-41.0.3-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:4c2f0d35703d61002a2bbdcf15548ebb701cfdd83cdc12471d2bae80878a4207"}, + {file = "cryptography-41.0.3-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:23c2d778cf829f7d0ae180600b17e9fceea3c2ef8b31a99e3c694cbbf3a24b84"}, + {file = "cryptography-41.0.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:95dd7f261bb76948b52a5330ba5202b91a26fbac13ad0e9fc8a3ac04752058c7"}, + {file = "cryptography-41.0.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:41d7aa7cdfded09b3d73a47f429c298e80796c8e825ddfadc84c8a7f12df212d"}, + {file = "cryptography-41.0.3-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d0d651aa754ef58d75cec6edfbd21259d93810b73f6ec246436a21b7841908de"}, + {file = "cryptography-41.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:ab8de0d091acbf778f74286f4989cf3d1528336af1b59f3e5d2ebca8b5fe49e1"}, + {file = "cryptography-41.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a74fbcdb2a0d46fe00504f571a2a540532f4c188e6ccf26f1f178480117b33c4"}, + {file = "cryptography-41.0.3.tar.gz", hash = "sha256:6d192741113ef5e30d89dcb5b956ef4e1578f304708701b8b73d38e3e1461f34"}, ] [package.dependencies] @@ -489,6 +464,18 @@ ssh = ["bcrypt (>=3.1.5)"] test = ["pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] test-randomorder = ["pytest-randomly"] +[[package]] +name = "cssselect" +version = "1.2.0" +description = "cssselect parses CSS3 Selectors and translates them to XPath 1.0" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "cssselect-1.2.0-py2.py3-none-any.whl", hash = "sha256:da1885f0c10b60c03ed5eccbb6b68d6eff248d91976fcde348f395d54c9fd35e"}, + {file = "cssselect-1.2.0.tar.gz", hash = "sha256:666b19839cfaddb9ce9d36bfe4c969132c647b92fc9088c4e23f786b30f1b3dc"}, +] + [[package]] name = "decorator" version = "5.1.1" @@ -545,14 +532,14 @@ gmpy2 = ["gmpy2"] [[package]] name = "exceptiongroup" -version = "1.1.2" +version = "1.1.3" description = "Backport of PEP 654 (exception groups)" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "exceptiongroup-1.1.2-py3-none-any.whl", hash = "sha256:e346e69d186172ca7cf029c8c1d16235aa0e04035e5750b4b95039e65204328f"}, - {file = "exceptiongroup-1.1.2.tar.gz", hash = "sha256:12c3e887d6485d16943a309616de20ae5582633e0a2eda17f4e10fd61c1e8af5"}, + {file = "exceptiongroup-1.1.3-py3-none-any.whl", hash = "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3"}, + {file = "exceptiongroup-1.1.3.tar.gz", hash = "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9"}, ] [package.extras] @@ -560,51 +547,34 @@ test = ["pytest (>=6)"] [[package]] name = "fastapi" -version = "0.100.0" +version = "0.103.0" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "fastapi-0.100.0-py3-none-any.whl", hash = "sha256:271662daf986da8fa98dc2b7c7f61c4abdfdccfb4786d79ed8b2878f172c6d5f"}, - {file = "fastapi-0.100.0.tar.gz", hash = "sha256:acb5f941ea8215663283c10018323ba7ea737c571b67fc7e88e9469c7eb1d12e"}, + {file = "fastapi-0.103.0-py3-none-any.whl", hash = "sha256:61ab72c6c281205dd0cbaccf503e829a37e0be108d965ac223779a8479243665"}, + {file = "fastapi-0.103.0.tar.gz", hash = "sha256:4166732f5ddf61c33e9fa4664f73780872511e0598d4d5434b1816dc1e6d9421"}, ] [package.dependencies] -pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<3.0.0" +pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" starlette = ">=0.27.0,<0.28.0" typing-extensions = ">=4.5.0" [package.extras] all = ["email-validator (>=2.0.0)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.5)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] -[[package]] -name = "flake8" -version = "5.0.4" -description = "the modular source code checker: pep8 pyflakes and co" -category = "dev" -optional = false -python-versions = ">=3.6.1" -files = [ - {file = "flake8-5.0.4-py2.py3-none-any.whl", hash = "sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248"}, - {file = "flake8-5.0.4.tar.gz", hash = "sha256:6fbe320aad8d6b95cec8b8e47bc933004678dc63095be98528b7bdd2a9f510db"}, -] - -[package.dependencies] -mccabe = ">=0.7.0,<0.8.0" -pycodestyle = ">=2.9.0,<2.10.0" -pyflakes = ">=2.5.0,<2.6.0" - [[package]] name = "flask" -version = "2.3.2" +version = "2.3.3" description = "A simple framework for building complex web applications." category = "dev" optional = false python-versions = ">=3.8" files = [ - {file = "Flask-2.3.2-py3-none-any.whl", hash = "sha256:77fd4e1249d8c9923de34907236b747ced06e5467ecac1a7bb7115ae0e9670b0"}, - {file = "Flask-2.3.2.tar.gz", hash = "sha256:8c2f9abd47a9e8df7f0c3f091ce9497d011dc3b31effcf4c85a6e2b50f4114ef"}, + {file = "flask-2.3.3-py3-none-any.whl", hash = "sha256:f69fcd559dc907ed196ab9df0e48471709175e696d6e698dd4dbe940f96ce66b"}, + {file = "flask-2.3.3.tar.gz", hash = "sha256:09c347a92aa7ff4a8e7f3206795f30d826654baf38b873d0744cd571ca609efc"}, ] [package.dependencies] @@ -613,7 +583,7 @@ click = ">=8.1.3" importlib-metadata = {version = ">=3.6.0", markers = "python_version < \"3.10\""} itsdangerous = ">=2.1.2" Jinja2 = ">=3.1.2" -Werkzeug = ">=2.3.3" +Werkzeug = ">=2.3.7" [package.extras] async = ["asgiref (>=3.2)"] @@ -639,14 +609,14 @@ dev = ["flake8", "markdown", "twine", "wheel"] [[package]] name = "griffe" -version = "0.32.3" +version = "0.35.2" description = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API." category = "dev" optional = false python-versions = ">=3.8" files = [ - {file = "griffe-0.32.3-py3-none-any.whl", hash = "sha256:d9471934225818bf8f309822f70451cc6abb4b24e59e0bb27402a45f9412510f"}, - {file = "griffe-0.32.3.tar.gz", hash = "sha256:14983896ad581f59d5ad7b6c9261ff12bdaa905acccc1129341d13e545da8521"}, + {file = "griffe-0.35.2-py3-none-any.whl", hash = "sha256:9650d6d0369c22f29f2c1bec9548ddc7f448f8ca38698a5799f92f736824e749"}, + {file = "griffe-0.35.2.tar.gz", hash = "sha256:84ecfe3df17454993b8dd485201566609ac6706a2eb22e3f402da2a39f9f6b5f"}, ] [package.dependencies] @@ -827,24 +797,6 @@ parallel = ["ipyparallel"] qtconsole = ["qtconsole"] test = ["ipykernel", "nbformat", "nose (>=0.10.1)", "numpy (>=1.17)", "pygments", "requests", "testpath"] -[[package]] -name = "isort" -version = "5.12.0" -description = "A Python utility / library to sort Python imports." -category = "dev" -optional = false -python-versions = ">=3.8.0" -files = [ - {file = "isort-5.12.0-py3-none-any.whl", hash = "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6"}, - {file = "isort-5.12.0.tar.gz", hash = "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504"}, -] - -[package.extras] -colors = ["colorama (>=0.4.3)"] -pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib"] -plugins = ["setuptools"] -requirements-deprecated-finder = ["pip-api", "pipreqs"] - [[package]] name = "itsdangerous" version = "2.1.2" @@ -859,22 +811,22 @@ files = [ [[package]] name = "jedi" -version = "0.18.2" +version = "0.19.0" description = "An autocompletion tool for Python that can be used for text editors." category = "dev" optional = false python-versions = ">=3.6" files = [ - {file = "jedi-0.18.2-py2.py3-none-any.whl", hash = "sha256:203c1fd9d969ab8f2119ec0a3342e0b49910045abe6af0a3ae83a5764d54639e"}, - {file = "jedi-0.18.2.tar.gz", hash = "sha256:bae794c30d07f6d910d32a7048af09b5a39ed740918da923c6b780790ebac612"}, + {file = "jedi-0.19.0-py2.py3-none-any.whl", hash = "sha256:cb8ce23fbccff0025e9386b5cf85e892f94c9b822378f8da49970471335ac64e"}, + {file = "jedi-0.19.0.tar.gz", hash = "sha256:bcf9894f1753969cbac8022a8c2eaee06bfa3724e4192470aaffe7eb6272b0c4"}, ] [package.dependencies] -parso = ">=0.8.0,<0.9.0" +parso = ">=0.8.3,<0.9.0" [package.extras] docs = ["Jinja2 (==2.11.3)", "MarkupSafe (==1.1.1)", "Pygments (==2.8.1)", "alabaster (==0.7.12)", "babel (==2.9.1)", "chardet (==4.0.0)", "commonmark (==0.8.1)", "docutils (==0.17.1)", "future (==0.18.2)", "idna (==2.10)", "imagesize (==1.2.0)", "mock (==1.0.1)", "packaging (==20.9)", "pyparsing (==2.4.7)", "pytz (==2021.1)", "readthedocs-sphinx-ext (==2.1.4)", "recommonmark (==0.5.0)", "requests (==2.25.1)", "six (==1.15.0)", "snowballstemmer (==2.1.0)", "sphinx (==1.8.5)", "sphinx-rtd-theme (==0.4.3)", "sphinxcontrib-serializinghtml (==1.1.4)", "sphinxcontrib-websupport (==1.2.4)", "urllib3 (==1.26.4)"] -qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] +qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] testing = ["Django (<3.1)", "attrs", "colorama", "docopt", "pytest (<7.0.0)"] [[package]] @@ -899,7 +851,7 @@ i18n = ["Babel (>=2.7)"] name = "loguru" version = "0.5.3" description = "Python logging made (stupidly) simple" -category = "dev" +category = "main" optional = false python-versions = ">=3.5" files = [ @@ -914,6 +866,114 @@ win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""} [package.extras] dev = ["Sphinx (>=2.2.1)", "black (>=19.10b0)", "codecov (>=2.0.15)", "colorama (>=0.3.4)", "flake8 (>=3.7.7)", "isort (>=5.1.1)", "pytest (>=4.6.2)", "pytest-cov (>=2.7.1)", "sphinx-autobuild (>=0.7.1)", "sphinx-rtd-theme (>=0.4.3)", "tox (>=3.9.0)", "tox-travis (>=0.12)"] +[[package]] +name = "lxml" +version = "4.9.3" +description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, != 3.4.*" +files = [ + {file = "lxml-4.9.3-cp27-cp27m-macosx_11_0_x86_64.whl", hash = "sha256:b0a545b46b526d418eb91754565ba5b63b1c0b12f9bd2f808c852d9b4b2f9b5c"}, + {file = "lxml-4.9.3-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:075b731ddd9e7f68ad24c635374211376aa05a281673ede86cbe1d1b3455279d"}, + {file = "lxml-4.9.3-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:1e224d5755dba2f4a9498e150c43792392ac9b5380aa1b845f98a1618c94eeef"}, + {file = "lxml-4.9.3-cp27-cp27m-win32.whl", hash = "sha256:2c74524e179f2ad6d2a4f7caf70e2d96639c0954c943ad601a9e146c76408ed7"}, + {file = "lxml-4.9.3-cp27-cp27m-win_amd64.whl", hash = "sha256:4f1026bc732b6a7f96369f7bfe1a4f2290fb34dce00d8644bc3036fb351a4ca1"}, + {file = "lxml-4.9.3-cp27-cp27mu-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c0781a98ff5e6586926293e59480b64ddd46282953203c76ae15dbbbf302e8bb"}, + {file = "lxml-4.9.3-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:cef2502e7e8a96fe5ad686d60b49e1ab03e438bd9123987994528febd569868e"}, + {file = "lxml-4.9.3-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:b86164d2cff4d3aaa1f04a14685cbc072efd0b4f99ca5708b2ad1b9b5988a991"}, + {file = "lxml-4.9.3-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:42871176e7896d5d45138f6d28751053c711ed4d48d8e30b498da155af39aebd"}, + {file = "lxml-4.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:ae8b9c6deb1e634ba4f1930eb67ef6e6bf6a44b6eb5ad605642b2d6d5ed9ce3c"}, + {file = "lxml-4.9.3-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:411007c0d88188d9f621b11d252cce90c4a2d1a49db6c068e3c16422f306eab8"}, + {file = "lxml-4.9.3-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:cd47b4a0d41d2afa3e58e5bf1f62069255aa2fd6ff5ee41604418ca925911d76"}, + {file = "lxml-4.9.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0e2cb47860da1f7e9a5256254b74ae331687b9672dfa780eed355c4c9c3dbd23"}, + {file = "lxml-4.9.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1247694b26342a7bf47c02e513d32225ededd18045264d40758abeb3c838a51f"}, + {file = "lxml-4.9.3-cp310-cp310-win32.whl", hash = "sha256:cdb650fc86227eba20de1a29d4b2c1bfe139dc75a0669270033cb2ea3d391b85"}, + {file = "lxml-4.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:97047f0d25cd4bcae81f9ec9dc290ca3e15927c192df17331b53bebe0e3ff96d"}, + {file = "lxml-4.9.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:1f447ea5429b54f9582d4b955f5f1985f278ce5cf169f72eea8afd9502973dd5"}, + {file = "lxml-4.9.3-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:57d6ba0ca2b0c462f339640d22882acc711de224d769edf29962b09f77129cbf"}, + {file = "lxml-4.9.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:9767e79108424fb6c3edf8f81e6730666a50feb01a328f4a016464a5893f835a"}, + {file = "lxml-4.9.3-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:71c52db65e4b56b8ddc5bb89fb2e66c558ed9d1a74a45ceb7dcb20c191c3df2f"}, + {file = "lxml-4.9.3-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:d73d8ecf8ecf10a3bd007f2192725a34bd62898e8da27eb9d32a58084f93962b"}, + {file = "lxml-4.9.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0a3d3487f07c1d7f150894c238299934a2a074ef590b583103a45002035be120"}, + {file = "lxml-4.9.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9e28c51fa0ce5674be9f560c6761c1b441631901993f76700b1b30ca6c8378d6"}, + {file = "lxml-4.9.3-cp311-cp311-win32.whl", hash = "sha256:0bfd0767c5c1de2551a120673b72e5d4b628737cb05414f03c3277bf9bed3305"}, + {file = "lxml-4.9.3-cp311-cp311-win_amd64.whl", hash = "sha256:25f32acefac14ef7bd53e4218fe93b804ef6f6b92ffdb4322bb6d49d94cad2bc"}, + {file = "lxml-4.9.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:d3ff32724f98fbbbfa9f49d82852b159e9784d6094983d9a8b7f2ddaebb063d4"}, + {file = "lxml-4.9.3-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:48d6ed886b343d11493129e019da91d4039826794a3e3027321c56d9e71505be"}, + {file = "lxml-4.9.3-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:9a92d3faef50658dd2c5470af249985782bf754c4e18e15afb67d3ab06233f13"}, + {file = "lxml-4.9.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b4e4bc18382088514ebde9328da057775055940a1f2e18f6ad2d78aa0f3ec5b9"}, + {file = "lxml-4.9.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fc9b106a1bf918db68619fdcd6d5ad4f972fdd19c01d19bdb6bf63f3589a9ec5"}, + {file = "lxml-4.9.3-cp312-cp312-win_amd64.whl", hash = "sha256:d37017287a7adb6ab77e1c5bee9bcf9660f90ff445042b790402a654d2ad81d8"}, + {file = "lxml-4.9.3-cp35-cp35m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:56dc1f1ebccc656d1b3ed288f11e27172a01503fc016bcabdcbc0978b19352b7"}, + {file = "lxml-4.9.3-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:578695735c5a3f51569810dfebd05dd6f888147a34f0f98d4bb27e92b76e05c2"}, + {file = "lxml-4.9.3-cp35-cp35m-win32.whl", hash = "sha256:704f61ba8c1283c71b16135caf697557f5ecf3e74d9e453233e4771d68a1f42d"}, + {file = "lxml-4.9.3-cp35-cp35m-win_amd64.whl", hash = "sha256:c41bfca0bd3532d53d16fd34d20806d5c2b1ace22a2f2e4c0008570bf2c58833"}, + {file = "lxml-4.9.3-cp36-cp36m-macosx_11_0_x86_64.whl", hash = "sha256:64f479d719dc9f4c813ad9bb6b28f8390360660b73b2e4beb4cb0ae7104f1c12"}, + {file = "lxml-4.9.3-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:dd708cf4ee4408cf46a48b108fb9427bfa00b9b85812a9262b5c668af2533ea5"}, + {file = "lxml-4.9.3-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c31c7462abdf8f2ac0577d9f05279727e698f97ecbb02f17939ea99ae8daa98"}, + {file = "lxml-4.9.3-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:e3cd95e10c2610c360154afdc2f1480aea394f4a4f1ea0a5eacce49640c9b190"}, + {file = "lxml-4.9.3-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:4930be26af26ac545c3dffb662521d4e6268352866956672231887d18f0eaab2"}, + {file = "lxml-4.9.3-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4aec80cde9197340bc353d2768e2a75f5f60bacda2bab72ab1dc499589b3878c"}, + {file = "lxml-4.9.3-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:14e019fd83b831b2e61baed40cab76222139926b1fb5ed0e79225bc0cae14584"}, + {file = "lxml-4.9.3-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:0c0850c8b02c298d3c7006b23e98249515ac57430e16a166873fc47a5d549287"}, + {file = "lxml-4.9.3-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:aca086dc5f9ef98c512bac8efea4483eb84abbf926eaeedf7b91479feb092458"}, + {file = "lxml-4.9.3-cp36-cp36m-win32.whl", hash = "sha256:50baa9c1c47efcaef189f31e3d00d697c6d4afda5c3cde0302d063492ff9b477"}, + {file = "lxml-4.9.3-cp36-cp36m-win_amd64.whl", hash = "sha256:bef4e656f7d98aaa3486d2627e7d2df1157d7e88e7efd43a65aa5dd4714916cf"}, + {file = "lxml-4.9.3-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:46f409a2d60f634fe550f7133ed30ad5321ae2e6630f13657fb9479506b00601"}, + {file = "lxml-4.9.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:4c28a9144688aef80d6ea666c809b4b0e50010a2aca784c97f5e6bf143d9f129"}, + {file = "lxml-4.9.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:141f1d1a9b663c679dc524af3ea1773e618907e96075262726c7612c02b149a4"}, + {file = "lxml-4.9.3-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:53ace1c1fd5a74ef662f844a0413446c0629d151055340e9893da958a374f70d"}, + {file = "lxml-4.9.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:17a753023436a18e27dd7769e798ce302963c236bc4114ceee5b25c18c52c693"}, + {file = "lxml-4.9.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:7d298a1bd60c067ea75d9f684f5f3992c9d6766fadbc0bcedd39750bf344c2f4"}, + {file = "lxml-4.9.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:081d32421db5df44c41b7f08a334a090a545c54ba977e47fd7cc2deece78809a"}, + {file = "lxml-4.9.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:23eed6d7b1a3336ad92d8e39d4bfe09073c31bfe502f20ca5116b2a334f8ec02"}, + {file = "lxml-4.9.3-cp37-cp37m-win32.whl", hash = "sha256:1509dd12b773c02acd154582088820893109f6ca27ef7291b003d0e81666109f"}, + {file = "lxml-4.9.3-cp37-cp37m-win_amd64.whl", hash = "sha256:120fa9349a24c7043854c53cae8cec227e1f79195a7493e09e0c12e29f918e52"}, + {file = "lxml-4.9.3-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:4d2d1edbca80b510443f51afd8496be95529db04a509bc8faee49c7b0fb6d2cc"}, + {file = "lxml-4.9.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:8d7e43bd40f65f7d97ad8ef5c9b1778943d02f04febef12def25f7583d19baac"}, + {file = "lxml-4.9.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:71d66ee82e7417828af6ecd7db817913cb0cf9d4e61aa0ac1fde0583d84358db"}, + {file = "lxml-4.9.3-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:6fc3c450eaa0b56f815c7b62f2b7fba7266c4779adcf1cece9e6deb1de7305ce"}, + {file = "lxml-4.9.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:65299ea57d82fb91c7f019300d24050c4ddeb7c5a190e076b5f48a2b43d19c42"}, + {file = "lxml-4.9.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:eadfbbbfb41b44034a4c757fd5d70baccd43296fb894dba0295606a7cf3124aa"}, + {file = "lxml-4.9.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3e9bdd30efde2b9ccfa9cb5768ba04fe71b018a25ea093379c857c9dad262c40"}, + {file = "lxml-4.9.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fcdd00edfd0a3001e0181eab3e63bd5c74ad3e67152c84f93f13769a40e073a7"}, + {file = "lxml-4.9.3-cp38-cp38-win32.whl", hash = "sha256:57aba1bbdf450b726d58b2aea5fe47c7875f5afb2c4a23784ed78f19a0462574"}, + {file = "lxml-4.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:92af161ecbdb2883c4593d5ed4815ea71b31fafd7fd05789b23100d081ecac96"}, + {file = "lxml-4.9.3-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:9bb6ad405121241e99a86efff22d3ef469024ce22875a7ae045896ad23ba2340"}, + {file = "lxml-4.9.3-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:8ed74706b26ad100433da4b9d807eae371efaa266ffc3e9191ea436087a9d6a7"}, + {file = "lxml-4.9.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:fbf521479bcac1e25a663df882c46a641a9bff6b56dc8b0fafaebd2f66fb231b"}, + {file = "lxml-4.9.3-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:303bf1edce6ced16bf67a18a1cf8339d0db79577eec5d9a6d4a80f0fb10aa2da"}, + {file = "lxml-4.9.3-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:5515edd2a6d1a5a70bfcdee23b42ec33425e405c5b351478ab7dc9347228f96e"}, + {file = "lxml-4.9.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:690dafd0b187ed38583a648076865d8c229661ed20e48f2335d68e2cf7dc829d"}, + {file = "lxml-4.9.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:b6420a005548ad52154c8ceab4a1290ff78d757f9e5cbc68f8c77089acd3c432"}, + {file = "lxml-4.9.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bb3bb49c7a6ad9d981d734ef7c7193bc349ac338776a0360cc671eaee89bcf69"}, + {file = "lxml-4.9.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d27be7405547d1f958b60837dc4c1007da90b8b23f54ba1f8b728c78fdb19d50"}, + {file = "lxml-4.9.3-cp39-cp39-win32.whl", hash = "sha256:8df133a2ea5e74eef5e8fc6f19b9e085f758768a16e9877a60aec455ed2609b2"}, + {file = "lxml-4.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:4dd9a263e845a72eacb60d12401e37c616438ea2e5442885f65082c276dfb2b2"}, + {file = "lxml-4.9.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:6689a3d7fd13dc687e9102a27e98ef33730ac4fe37795d5036d18b4d527abd35"}, + {file = "lxml-4.9.3-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:f6bdac493b949141b733c5345b6ba8f87a226029cbabc7e9e121a413e49441e0"}, + {file = "lxml-4.9.3-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:05186a0f1346ae12553d66df1cfce6f251589fea3ad3da4f3ef4e34b2d58c6a3"}, + {file = "lxml-4.9.3-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c2006f5c8d28dee289f7020f721354362fa304acbaaf9745751ac4006650254b"}, + {file = "lxml-4.9.3-pp38-pypy38_pp73-macosx_11_0_x86_64.whl", hash = "sha256:5c245b783db29c4e4fbbbfc9c5a78be496c9fea25517f90606aa1f6b2b3d5f7b"}, + {file = "lxml-4.9.3-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:4fb960a632a49f2f089d522f70496640fdf1218f1243889da3822e0a9f5f3ba7"}, + {file = "lxml-4.9.3-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:50670615eaf97227d5dc60de2dc99fb134a7130d310d783314e7724bf163f75d"}, + {file = "lxml-4.9.3-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:9719fe17307a9e814580af1f5c6e05ca593b12fb7e44fe62450a5384dbf61b4b"}, + {file = "lxml-4.9.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:3331bece23c9ee066e0fb3f96c61322b9e0f54d775fccefff4c38ca488de283a"}, + {file = "lxml-4.9.3-pp39-pypy39_pp73-macosx_11_0_x86_64.whl", hash = "sha256:ed667f49b11360951e201453fc3967344d0d0263aa415e1619e85ae7fd17b4e0"}, + {file = "lxml-4.9.3-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:8b77946fd508cbf0fccd8e400a7f71d4ac0e1595812e66025bac475a8e811694"}, + {file = "lxml-4.9.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:e4da8ca0c0c0aea88fd46be8e44bd49716772358d648cce45fe387f7b92374a7"}, + {file = "lxml-4.9.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fe4bda6bd4340caa6e5cf95e73f8fea5c4bfc55763dd42f1b50a94c1b4a2fbd4"}, + {file = "lxml-4.9.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:f3df3db1d336b9356dd3112eae5f5c2b8b377f3bc826848567f10bfddfee77e9"}, + {file = "lxml-4.9.3.tar.gz", hash = "sha256:48628bd53a426c9eb9bc066a923acaa0878d1e86129fd5359aee99285f4eed9c"}, +] + +[package.extras] +cssselect = ["cssselect (>=0.7)"] +html5 = ["html5lib"] +htmlsoup = ["BeautifulSoup4"] +source = ["Cython (>=0.29.35)"] + [[package]] name = "markdown" version = "3.4.4" @@ -933,6 +993,48 @@ importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} docs = ["mdx-gh-links (>=0.2)", "mkdocs (>=1.0)", "mkdocs-nature (>=0.4)"] testing = ["coverage", "pyyaml"] +[[package]] +name = "markdown-it-py" +version = "3.0.0" +description = "Python port of markdown-it. Markdown parsing, done right!" +category = "main" +optional = true +python-versions = ">=3.8" +files = [ + {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, + {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, +] + +[package.dependencies] +mdurl = ">=0.1,<1.0" + +[package.extras] +benchmarking = ["psutil", "pytest", "pytest-benchmark"] +code-style = ["pre-commit (>=3.0,<4.0)"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] +linkify = ["linkify-it-py (>=1,<3)"] +plugins = ["mdit-py-plugins"] +profiling = ["gprof2dot"] +rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] + +[[package]] +name = "markdown2" +version = "2.4.10" +description = "A fast and complete Python implementation of Markdown" +category = "dev" +optional = false +python-versions = ">=3.5, <4" +files = [ + {file = "markdown2-2.4.10-py2.py3-none-any.whl", hash = "sha256:e6105800483783831f5dc54f827aa5b44eb137ecef5a70293d8ecfbb4109ecc6"}, + {file = "markdown2-2.4.10.tar.gz", hash = "sha256:cdba126d90dc3aef6f4070ac342f974d63f415678959329cc7909f96cc235d72"}, +] + +[package.extras] +all = ["pygments (>=2.7.3)", "wavedrom"] +code-syntax-highlighting = ["pygments (>=2.7.3)"] +wavedrom = ["wavedrom"] + [[package]] name = "markupsafe" version = "2.1.3" @@ -1009,15 +1111,15 @@ files = [ traitlets = "*" [[package]] -name = "mccabe" -version = "0.7.0" -description = "McCabe checker, plugin for flake8" -category = "dev" -optional = false -python-versions = ">=3.6" +name = "mdurl" +version = "0.1.2" +description = "Markdown URL utilities" +category = "main" +optional = true +python-versions = ">=3.7" files = [ - {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, - {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, ] [[package]] @@ -1082,24 +1184,28 @@ mkdocs = ">=1.1" [[package]] name = "mkdocs-material" -version = "9.1.21" +version = "9.2.5" description = "Documentation that simply works" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "mkdocs_material-9.1.21-py3-none-any.whl", hash = "sha256:58bb2f11ef240632e176d6f0f7d1cff06be1d11c696a5a1b553b808b4280ed47"}, - {file = "mkdocs_material-9.1.21.tar.gz", hash = "sha256:71940cdfca84ab296b6362889c25395b1621273fb16c93deda257adb7ff44ec8"}, + {file = "mkdocs_material-9.2.5-py3-none-any.whl", hash = "sha256:315a59725f0565bccfec7f9d1313beae7658bf874a176264b98f804a0cbc1298"}, + {file = "mkdocs_material-9.2.5.tar.gz", hash = "sha256:02b4d1f662bc022e9497411e679323c30185e031a08a7004c763aa8d47ae9a29"}, ] [package.dependencies] +babel = ">=2.10.3" colorama = ">=0.4" jinja2 = ">=3.0" +lxml = ">=4.6" markdown = ">=3.2" -mkdocs = ">=1.5.0" +mkdocs = ">=1.5.2" mkdocs-material-extensions = ">=1.1" +paginate = ">=0.5.6" pygments = ">=2.14" pymdown-extensions = ">=9.9.1" +readtime = ">=2.0" regex = ">=2022.4.24" requests = ">=2.26" @@ -1145,69 +1251,65 @@ python-legacy = ["mkdocstrings-python-legacy (>=0.2.1)"] [[package]] name = "mkdocstrings-python" -version = "1.3.0" +version = "1.6.0" description = "A Python handler for mkdocstrings." category = "dev" optional = false python-versions = ">=3.8" files = [ - {file = "mkdocstrings_python-1.3.0-py3-none-any.whl", hash = "sha256:36c224c86ab77e90e0edfc9fea3307f7d0d245dd7c28f48bbb2203cf6e125530"}, - {file = "mkdocstrings_python-1.3.0.tar.gz", hash = "sha256:f967f84bab530fcc13cc9c02eccf0c18bdb2c3bab5c55fa2045938681eec4fc4"}, + {file = "mkdocstrings_python-1.6.0-py3-none-any.whl", hash = "sha256:06f116112b335114372f2554b1bf61b709c74ab72605010e1605c1086932dffe"}, + {file = "mkdocstrings_python-1.6.0.tar.gz", hash = "sha256:6164ccaa6e488abc2a8fbccdfd1f21948c2c344d3f347847783a5d1c6fa2bfbf"}, ] [package.dependencies] -griffe = ">=0.30,<0.33" +griffe = ">=0.35" mkdocstrings = ">=0.20" [[package]] name = "mypy" -version = "0.991" +version = "1.5.1" description = "Optional static typing for Python" category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "mypy-0.991-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7d17e0a9707d0772f4a7b878f04b4fd11f6f5bcb9b3813975a9b13c9332153ab"}, - {file = "mypy-0.991-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0714258640194d75677e86c786e80ccf294972cc76885d3ebbb560f11db0003d"}, - {file = "mypy-0.991-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0c8f3be99e8a8bd403caa8c03be619544bc2c77a7093685dcf308c6b109426c6"}, - {file = "mypy-0.991-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc9ec663ed6c8f15f4ae9d3c04c989b744436c16d26580eaa760ae9dd5d662eb"}, - {file = "mypy-0.991-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4307270436fd7694b41f913eb09210faff27ea4979ecbcd849e57d2da2f65305"}, - {file = "mypy-0.991-cp310-cp310-win_amd64.whl", hash = "sha256:901c2c269c616e6cb0998b33d4adbb4a6af0ac4ce5cd078afd7bc95830e62c1c"}, - {file = "mypy-0.991-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d13674f3fb73805ba0c45eb6c0c3053d218aa1f7abead6e446d474529aafc372"}, - {file = "mypy-0.991-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1c8cd4fb70e8584ca1ed5805cbc7c017a3d1a29fb450621089ffed3e99d1857f"}, - {file = "mypy-0.991-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:209ee89fbb0deed518605edddd234af80506aec932ad28d73c08f1400ef80a33"}, - {file = "mypy-0.991-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37bd02ebf9d10e05b00d71302d2c2e6ca333e6c2a8584a98c00e038db8121f05"}, - {file = "mypy-0.991-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:26efb2fcc6b67e4d5a55561f39176821d2adf88f2745ddc72751b7890f3194ad"}, - {file = "mypy-0.991-cp311-cp311-win_amd64.whl", hash = "sha256:3a700330b567114b673cf8ee7388e949f843b356a73b5ab22dd7cff4742a5297"}, - {file = "mypy-0.991-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:1f7d1a520373e2272b10796c3ff721ea1a0712288cafaa95931e66aa15798813"}, - {file = "mypy-0.991-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:641411733b127c3e0dab94c45af15fea99e4468f99ac88b39efb1ad677da5711"}, - {file = "mypy-0.991-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:3d80e36b7d7a9259b740be6d8d906221789b0d836201af4234093cae89ced0cd"}, - {file = "mypy-0.991-cp37-cp37m-win_amd64.whl", hash = "sha256:e62ebaad93be3ad1a828a11e90f0e76f15449371ffeecca4a0a0b9adc99abcef"}, - {file = "mypy-0.991-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b86ce2c1866a748c0f6faca5232059f881cda6dda2a893b9a8373353cfe3715a"}, - {file = "mypy-0.991-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ac6e503823143464538efda0e8e356d871557ef60ccd38f8824a4257acc18d93"}, - {file = "mypy-0.991-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0cca5adf694af539aeaa6ac633a7afe9bbd760df9d31be55ab780b77ab5ae8bf"}, - {file = "mypy-0.991-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a12c56bf73cdab116df96e4ff39610b92a348cc99a1307e1da3c3768bbb5b135"}, - {file = "mypy-0.991-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:652b651d42f155033a1967739788c436491b577b6a44e4c39fb340d0ee7f0d70"}, - {file = "mypy-0.991-cp38-cp38-win_amd64.whl", hash = "sha256:4175593dc25d9da12f7de8de873a33f9b2b8bdb4e827a7cae952e5b1a342e243"}, - {file = "mypy-0.991-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:98e781cd35c0acf33eb0295e8b9c55cdbef64fcb35f6d3aa2186f289bed6e80d"}, - {file = "mypy-0.991-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6d7464bac72a85cb3491c7e92b5b62f3dcccb8af26826257760a552a5e244aa5"}, - {file = "mypy-0.991-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c9166b3f81a10cdf9b49f2d594b21b31adadb3d5e9db9b834866c3258b695be3"}, - {file = "mypy-0.991-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8472f736a5bfb159a5e36740847808f6f5b659960115ff29c7cecec1741c648"}, - {file = "mypy-0.991-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5e80e758243b97b618cdf22004beb09e8a2de1af481382e4d84bc52152d1c476"}, - {file = "mypy-0.991-cp39-cp39-win_amd64.whl", hash = "sha256:74e259b5c19f70d35fcc1ad3d56499065c601dfe94ff67ae48b85596b9ec1461"}, - {file = "mypy-0.991-py3-none-any.whl", hash = "sha256:de32edc9b0a7e67c2775e574cb061a537660e51210fbf6006b0b36ea695ae9bb"}, - {file = "mypy-0.991.tar.gz", hash = "sha256:3c0165ba8f354a6d9881809ef29f1a9318a236a6d81c690094c5df32107bde06"}, + {file = "mypy-1.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f33592ddf9655a4894aef22d134de7393e95fcbdc2d15c1ab65828eee5c66c70"}, + {file = "mypy-1.5.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:258b22210a4a258ccd077426c7a181d789d1121aca6db73a83f79372f5569ae0"}, + {file = "mypy-1.5.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9ec1f695f0c25986e6f7f8778e5ce61659063268836a38c951200c57479cc12"}, + {file = "mypy-1.5.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:abed92d9c8f08643c7d831300b739562b0a6c9fcb028d211134fc9ab20ccad5d"}, + {file = "mypy-1.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:a156e6390944c265eb56afa67c74c0636f10283429171018446b732f1a05af25"}, + {file = "mypy-1.5.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6ac9c21bfe7bc9f7f1b6fae441746e6a106e48fc9de530dea29e8cd37a2c0cc4"}, + {file = "mypy-1.5.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:51cb1323064b1099e177098cb939eab2da42fea5d818d40113957ec954fc85f4"}, + {file = "mypy-1.5.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:596fae69f2bfcb7305808c75c00f81fe2829b6236eadda536f00610ac5ec2243"}, + {file = "mypy-1.5.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:32cb59609b0534f0bd67faebb6e022fe534bdb0e2ecab4290d683d248be1b275"}, + {file = "mypy-1.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:159aa9acb16086b79bbb0016145034a1a05360626046a929f84579ce1666b315"}, + {file = "mypy-1.5.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f6b0e77db9ff4fda74de7df13f30016a0a663928d669c9f2c057048ba44f09bb"}, + {file = "mypy-1.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:26f71b535dfc158a71264e6dc805a9f8d2e60b67215ca0bfa26e2e1aa4d4d373"}, + {file = "mypy-1.5.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fc3a600f749b1008cc75e02b6fb3d4db8dbcca2d733030fe7a3b3502902f161"}, + {file = "mypy-1.5.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:26fb32e4d4afa205b24bf645eddfbb36a1e17e995c5c99d6d00edb24b693406a"}, + {file = "mypy-1.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:82cb6193de9bbb3844bab4c7cf80e6227d5225cc7625b068a06d005d861ad5f1"}, + {file = "mypy-1.5.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4a465ea2ca12804d5b34bb056be3a29dc47aea5973b892d0417c6a10a40b2d65"}, + {file = "mypy-1.5.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9fece120dbb041771a63eb95e4896791386fe287fefb2837258925b8326d6160"}, + {file = "mypy-1.5.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d28ddc3e3dfeab553e743e532fb95b4e6afad51d4706dd22f28e1e5e664828d2"}, + {file = "mypy-1.5.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:57b10c56016adce71fba6bc6e9fd45d8083f74361f629390c556738565af8eeb"}, + {file = "mypy-1.5.1-cp38-cp38-win_amd64.whl", hash = "sha256:ff0cedc84184115202475bbb46dd99f8dcb87fe24d5d0ddfc0fe6b8575c88d2f"}, + {file = "mypy-1.5.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8f772942d372c8cbac575be99f9cc9d9fb3bd95c8bc2de6c01411e2c84ebca8a"}, + {file = "mypy-1.5.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5d627124700b92b6bbaa99f27cbe615c8ea7b3402960f6372ea7d65faf376c14"}, + {file = "mypy-1.5.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:361da43c4f5a96173220eb53340ace68cda81845cd88218f8862dfb0adc8cddb"}, + {file = "mypy-1.5.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:330857f9507c24de5c5724235e66858f8364a0693894342485e543f5b07c8693"}, + {file = "mypy-1.5.1-cp39-cp39-win_amd64.whl", hash = "sha256:c543214ffdd422623e9fedd0869166c2f16affe4ba37463975043ef7d2ea8770"}, + {file = "mypy-1.5.1-py3-none-any.whl", hash = "sha256:f757063a83970d67c444f6e01d9550a7402322af3557ce7630d3c957386fa8f5"}, + {file = "mypy-1.5.1.tar.gz", hash = "sha256:b031b9601f1060bf1281feab89697324726ba0c0bae9d7cd7ab4b690940f0b92"}, ] [package.dependencies] -mypy-extensions = ">=0.4.3" +mypy-extensions = ">=1.0.0" tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing-extensions = ">=3.10" +typing-extensions = ">=4.1.0" [package.extras] dmypy = ["psutil (>=4.0)"] install-types = ["pip"] -python2 = ["typed-ast (>=1.4.0,<2)"] reports = ["lxml"] [[package]] @@ -1234,6 +1336,17 @@ files = [ {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"}, ] +[[package]] +name = "paginate" +version = "0.5.6" +description = "Divides large result sets into pages for easier browsing" +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "paginate-0.5.6.tar.gz", hash = "sha256:5e6007b6a9398177a7e1648d04fdd9f8c9766a1a945bceac82f1929e8c78af2d"}, +] + [[package]] name = "parso" version = "0.8.3" @@ -1263,21 +1376,21 @@ files = [ [[package]] name = "pathspec" -version = "0.11.1" +version = "0.11.2" description = "Utility library for gitignore style pattern matching of file paths." category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "pathspec-0.11.1-py3-none-any.whl", hash = "sha256:d8af70af76652554bd134c22b3e8a1cc46ed7d91edcdd721ef1a0c51a84a5293"}, - {file = "pathspec-0.11.1.tar.gz", hash = "sha256:2798de800fa92780e33acca925945e9a19a133b715067cf165b8866c15a31687"}, + {file = "pathspec-0.11.2-py3-none-any.whl", hash = "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20"}, + {file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"}, ] [[package]] name = "pendulum" version = "2.1.2" description = "Python datetimes made easy" -category = "dev" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -1337,30 +1450,30 @@ files = [ [[package]] name = "platformdirs" -version = "3.9.1" +version = "3.10.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "platformdirs-3.9.1-py3-none-any.whl", hash = "sha256:ad8291ae0ae5072f66c16945166cb11c63394c7a3ad1b1bc9828ca3162da8c2f"}, - {file = "platformdirs-3.9.1.tar.gz", hash = "sha256:1b42b450ad933e981d56e59f1b97495428c9bd60698baab9f3eb3d00d5822421"}, + {file = "platformdirs-3.10.0-py3-none-any.whl", hash = "sha256:d7c24979f292f916dc9cbf8648319032f551ea8c49a4c9bf2fb556a02070ec1d"}, + {file = "platformdirs-3.10.0.tar.gz", hash = "sha256:b45696dab2d7cc691a3226759c0d3b00c47c8b6e293d96f6436f733303f77f6d"}, ] [package.extras] -docs = ["furo (>=2023.5.20)", "proselint (>=0.13)", "sphinx (>=7.0.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.3.1)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)"] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] [[package]] name = "pluggy" -version = "1.2.0" +version = "1.3.0" description = "plugin and hook calling mechanisms for python" category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pluggy-1.2.0-py3-none-any.whl", hash = "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849"}, - {file = "pluggy-1.2.0.tar.gz", hash = "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3"}, + {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"}, + {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, ] [package.extras] @@ -1369,23 +1482,23 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "plummet" -version = "1.1.0" +version = "1.2.1" description = "Utilities for testing with pendulum timestamps" category = "dev" optional = false -python-versions = ">=3.6.2,<4.0.0" +python-versions = ">=3.8,<4.0" files = [ - {file = "plummet-1.1.0-py3-none-any.whl", hash = "sha256:ebd1f772e1fdad23df78ae10e9755879a877ae77301b5a7f6d7cb18cc4abb2ff"}, - {file = "plummet-1.1.0.tar.gz", hash = "sha256:c3fecbbecbb7131b4b9c579561f592419cf72ad3f04e0d34739e8a92c0eab2c1"}, + {file = "plummet-1.2.1-py3-none-any.whl", hash = "sha256:543f54a291f581e31b542ca6017d2fdfd456631e4d98997c8e385efcc4c0be9c"}, + {file = "plummet-1.2.1.tar.gz", hash = "sha256:91fe3cb11e11b7acc56fbb1ef7c9af2489421e98c53c89c00a944be8ac6b008d"}, ] [package.dependencies] -pendulum = ">=2.1.2,<3.0.0" -py-buzz = ">=3.1.0,<4.0.0" -time-machine = {version = ">=2.4.0,<3.0.0", optional = true, markers = "extra == \"time-machine\""} +pendulum = ">=2" +py-buzz = ">=3.1" +time-machine = {version = ">=2", optional = true, markers = "extra == \"time-machine\""} [package.extras] -time-machine = ["time-machine (>=2.4.0,<3.0.0)"] +time-machine = ["time-machine (>=2)"] [[package]] name = "pprintpp" @@ -1428,14 +1541,14 @@ files = [ [[package]] name = "py-buzz" -version = "3.2.1" +version = "4.1.0" description = "\"That's not flying, it's falling with style\": Exceptions with extras" category = "main" optional = false -python-versions = ">=3.6.2,<4.0.0" +python-versions = ">=3.8,<4.0" files = [ - {file = "py-buzz-3.2.1.tar.gz", hash = "sha256:f4fc6a119d9d74bc60822cbdac65876423cd56d9911833968db1c9ee8e2ed069"}, - {file = "py_buzz-3.2.1-py3-none-any.whl", hash = "sha256:2dfc7dc9c7e19d49e0e75def2efad84510fcbf06dad538c8960faa50d39d34bf"}, + {file = "py_buzz-4.1.0-py3-none-any.whl", hash = "sha256:77dc0dc9c9923b6f8079dc2e2c3b4fbebd2308acaca1500f8eda2711cd308f97"}, + {file = "py_buzz-4.1.0.tar.gz", hash = "sha256:ac11dba4922b2af114126044597d2fcd15e8c6a74368bed91f3c6732c8f09812"}, ] [[package]] @@ -1450,18 +1563,6 @@ files = [ {file = "pyasn1-0.5.0.tar.gz", hash = "sha256:97b7290ca68e62a832558ec3976f15cbf911bf5d7c7039d8b861c2a0ece69fde"}, ] -[[package]] -name = "pycodestyle" -version = "2.9.1" -description = "Python style guide checker" -category = "dev" -optional = false -python-versions = ">=3.6" -files = [ - {file = "pycodestyle-2.9.1-py2.py3-none-any.whl", hash = "sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b"}, - {file = "pycodestyle-2.9.1.tar.gz", hash = "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785"}, -] - [[package]] name = "pycparser" version = "2.21" @@ -1527,23 +1628,11 @@ typing-extensions = ">=4.2.0" dotenv = ["python-dotenv (>=0.10.4)"] email = ["email-validator (>=1.0.3)"] -[[package]] -name = "pyflakes" -version = "2.5.0" -description = "passive checker of Python programs" -category = "dev" -optional = false -python-versions = ">=3.6" -files = [ - {file = "pyflakes-2.5.0-py2.py3-none-any.whl", hash = "sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2"}, - {file = "pyflakes-2.5.0.tar.gz", hash = "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3"}, -] - [[package]] name = "pygments" version = "2.16.1" description = "Pygments is a syntax highlighting package written in Python." -category = "dev" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1556,35 +1645,52 @@ plugins = ["importlib-metadata"] [[package]] name = "pymdown-extensions" -version = "10.1" +version = "10.2" description = "Extension pack for Python Markdown." category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "pymdown_extensions-10.1-py3-none-any.whl", hash = "sha256:ef25dbbae530e8f67575d222b75ff0649b1e841e22c2ae9a20bad9472c2207dc"}, - {file = "pymdown_extensions-10.1.tar.gz", hash = "sha256:508009b211373058debb8247e168de4cbcb91b1bff7b5e961b2c3e864e00b195"}, + {file = "pymdown_extensions-10.2-py3-none-any.whl", hash = "sha256:fbb86243db9a681602e3b869deef000211c55d0261015a5cc41d6f34d2afc57f"}, + {file = "pymdown_extensions-10.2.tar.gz", hash = "sha256:06042274876eb4267f12a389daf505eabaebc38bdca26725560c9afda5867549"}, ] [package.dependencies] markdown = ">=3.2" pyyaml = "*" +[package.extras] +extra = ["pygments (>=2.12)"] + [[package]] -name = "pyproject-flake8" -version = "5.0.4.post1" -description = "pyproject-flake8 (`pflake8`), a monkey patching wrapper to connect flake8 with pyproject.toml configuration" +name = "pyperclip" +version = "1.8.2" +description = "A cross-platform clipboard module for Python. (Only handles plain text for now.)" +category = "main" +optional = true +python-versions = "*" +files = [ + {file = "pyperclip-1.8.2.tar.gz", hash = "sha256:105254a8b04934f0bc84e9c24eb360a591aaf6535c9def5f29d92af107a9bf57"}, +] + +[[package]] +name = "pyquery" +version = "2.0.0" +description = "A jquery-like library for python" category = "dev" optional = false python-versions = "*" files = [ - {file = "pyproject-flake8-5.0.4.post1.tar.gz", hash = "sha256:c2dfdf1064f47efbb2e4faf1a32b0b6a6ea67dc4d1debb98d862b0cdee377941"}, - {file = "pyproject_flake8-5.0.4.post1-py2.py3-none-any.whl", hash = "sha256:457e52dde1b7a1f84b5230c70d61afa58ced64a44b81a609f19e972319fa68ed"}, + {file = "pyquery-2.0.0-py3-none-any.whl", hash = "sha256:8dfc9b4b7c5f877d619bbae74b1898d5743f6ca248cfd5d72b504dd614da312f"}, + {file = "pyquery-2.0.0.tar.gz", hash = "sha256:963e8d4e90262ff6d8dec072ea97285dc374a2f69cad7776f4082abcf6a1d8ae"}, ] [package.dependencies] -flake8 = "5.0.4" -tomli = {version = "*", markers = "python_version < \"3.11\""} +cssselect = ">=1.2.0" +lxml = ">=2.1" + +[package.extras] +test = ["pytest", "pytest-cov", "requests", "webob", "webtest"] [[package]] name = "pytest" @@ -1647,6 +1753,24 @@ pytest = ">=4.6" [package.extras] testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] +[[package]] +name = "pytest-mock" +version = "3.11.1" +description = "Thin-wrapper around the mock package for easier use with pytest" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-mock-3.11.1.tar.gz", hash = "sha256:7f6b125602ac6d743e523ae0bfa71e1a697a2f5534064528c6ff84c2f7c2fc7f"}, + {file = "pytest_mock-3.11.1-py3-none-any.whl", hash = "sha256:21c279fff83d70763b05f8874cc9cfb3fcacd6d354247a976f9529d19f9acf39"}, +] + +[package.dependencies] +pytest = ">=5.0" + +[package.extras] +dev = ["pre-commit", "pytest-asyncio", "tox"] + [[package]] name = "pytest-random-order" version = "1.1.0" @@ -1686,7 +1810,7 @@ dev = ["black", "flake8", "pre-commit"] name = "python-dateutil" version = "2.8.2" description = "Extensions to the standard Python datetime module" -category = "dev" +category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" files = [ @@ -1751,7 +1875,7 @@ files = [ name = "pytzdata" version = "2020.1" description = "The Olson timezone database for Python." -category = "dev" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -1824,6 +1948,22 @@ files = [ [package.dependencies] pyyaml = "*" +[[package]] +name = "readtime" +version = "3.0.0" +description = "Calculates the time some text takes the average human to read, based on Medium's read time forumula" +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "readtime-3.0.0.tar.gz", hash = "sha256:76c5a0d773ad49858c53b42ba3a942f62fbe20cc8c6f07875797ac7dc30963a9"}, +] + +[package.dependencies] +beautifulsoup4 = ">=4.0.1" +markdown2 = ">=2.4.3" +pyquery = ">=1.2" + [[package]] name = "regex" version = "2023.8.8" @@ -1959,6 +2099,26 @@ files = [ [package.dependencies] httpx = ">=0.21.0" +[[package]] +name = "rich" +version = "13.5.2" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +category = "main" +optional = true +python-versions = ">=3.7.0" +files = [ + {file = "rich-13.5.2-py3-none-any.whl", hash = "sha256:146a90b3b6b47cac4a73c12866a499e9817426423f57c5a66949c086191a8808"}, + {file = "rich-13.5.2.tar.gz", hash = "sha256:fb9d6c0a0f643c99eed3875b5377a184132ba9be4d61516a55273d3554d75a39"}, +] + +[package.dependencies] +markdown-it-py = ">=2.2.0" +pygments = ">=2.13.0,<3.0.0" +typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.9\""} + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<9)"] + [[package]] name = "rsa" version = "4.9" @@ -1974,21 +2134,48 @@ files = [ [package.dependencies] pyasn1 = ">=0.1.3" +[[package]] +name = "ruff" +version = "0.0.286" +description = "An extremely fast Python linter, written in Rust." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "ruff-0.0.286-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:8e22cb557e7395893490e7f9cfea1073d19a5b1dd337f44fd81359b2767da4e9"}, + {file = "ruff-0.0.286-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:68ed8c99c883ae79a9133cb1a86d7130feee0397fdf5ba385abf2d53e178d3fa"}, + {file = "ruff-0.0.286-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8301f0bb4ec1a5b29cfaf15b83565136c47abefb771603241af9d6038f8981e8"}, + {file = "ruff-0.0.286-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:acc4598f810bbc465ce0ed84417ac687e392c993a84c7eaf3abf97638701c1ec"}, + {file = "ruff-0.0.286-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88c8e358b445eb66d47164fa38541cfcc267847d1e7a92dd186dddb1a0a9a17f"}, + {file = "ruff-0.0.286-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:0433683d0c5dbcf6162a4beb2356e820a593243f1fa714072fec15e2e4f4c939"}, + {file = "ruff-0.0.286-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ddb61a0c4454cbe4623f4a07fef03c5ae921fe04fede8d15c6e36703c0a73b07"}, + {file = "ruff-0.0.286-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:47549c7c0be24c8ae9f2bce6f1c49fbafea83bca80142d118306f08ec7414041"}, + {file = "ruff-0.0.286-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:559aa793149ac23dc4310f94f2c83209eedb16908a0343663be19bec42233d25"}, + {file = "ruff-0.0.286-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d73cfb1c3352e7aa0ce6fb2321f36fa1d4a2c48d2ceac694cb03611ddf0e4db6"}, + {file = "ruff-0.0.286-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:3dad93b1f973c6d1db4b6a5da8690c5625a3fa32bdf38e543a6936e634b83dc3"}, + {file = "ruff-0.0.286-py3-none-musllinux_1_2_i686.whl", hash = "sha256:26afc0851f4fc3738afcf30f5f8b8612a31ac3455cb76e611deea80f5c0bf3ce"}, + {file = "ruff-0.0.286-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:9b6b116d1c4000de1b9bf027131dbc3b8a70507788f794c6b09509d28952c512"}, + {file = "ruff-0.0.286-py3-none-win32.whl", hash = "sha256:556e965ac07c1e8c1c2d759ac512e526ecff62c00fde1a046acb088d3cbc1a6c"}, + {file = "ruff-0.0.286-py3-none-win_amd64.whl", hash = "sha256:5d295c758961376c84aaa92d16e643d110be32add7465e197bfdaec5a431a107"}, + {file = "ruff-0.0.286-py3-none-win_arm64.whl", hash = "sha256:1d6142d53ab7f164204b3133d053c4958d4d11ec3a39abf23a40b13b0784e3f0"}, + {file = "ruff-0.0.286.tar.gz", hash = "sha256:f1e9d169cce81a384a26ee5bb8c919fe9ae88255f39a1a69fd1ebab233a85ed2"}, +] + [[package]] name = "setuptools" -version = "68.0.0" +version = "68.1.2" description = "Easily download, build, install, upgrade, and uninstall Python packages" category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "setuptools-68.0.0-py3-none-any.whl", hash = "sha256:11e52c67415a381d10d6b462ced9cfb97066179f0e871399e006c4ab101fc85f"}, - {file = "setuptools-68.0.0.tar.gz", hash = "sha256:baf1fdb41c6da4cd2eae722e135500da913332ab3f2f5c7d33af9b492acb5235"}, + {file = "setuptools-68.1.2-py3-none-any.whl", hash = "sha256:3d8083eed2d13afc9426f227b24fd1659489ec107c0e86cec2ffdde5c92e790b"}, + {file = "setuptools-68.1.2.tar.gz", hash = "sha256:3d4dfa6d95f1b101d695a6160a7626e15583af71a5f52176efa5d39a054d475d"}, ] [package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5,<=7.1.2)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] [[package]] @@ -2042,6 +2229,18 @@ files = [ {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, ] +[[package]] +name = "soupsieve" +version = "2.4.1" +description = "A modern CSS selector implementation for Beautiful Soup." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "soupsieve-2.4.1-py3-none-any.whl", hash = "sha256:1c1bfee6819544a3447586c889157365a27e10d88cde3ad3da0cf0ddf646feb8"}, + {file = "soupsieve-2.4.1.tar.gz", hash = "sha256:89d12b2d5dfcd2c9e8c22326da9d9aa9cb3dfab0a83a024f05704076ee8d35ea"}, +] + [[package]] name = "sphinx" version = "4.5.0" @@ -2209,57 +2408,68 @@ tests = ["pytest", "pytest-cov"] [[package]] name = "time-machine" -version = "2.11.0" +version = "2.12.0" description = "Travel through time in your tests." category = "dev" optional = false python-versions = ">=3.8" files = [ - {file = "time_machine-2.11.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a6c25f03b634ff9389d2ff78f2f0168421a1be692cb8ab135202a8fda3e7b30a"}, - {file = "time_machine-2.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4cbeac9003319a4e84540c794e9be9cc2fa69abe5d0005091419a16b8e199ded"}, - {file = "time_machine-2.11.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8cb21faf0192e9c7776921cec70dc36c54e69e3544effe2a3360700cc21b4ab"}, - {file = "time_machine-2.11.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1a41e6be04a8290e353218827d288dd5070b02758eb69bb58b8c4a823115b4e9"}, - {file = "time_machine-2.11.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcccab530ff39935f62c325004f55958e3349d8dfc693bc6a96dffe692b6ef1b"}, - {file = "time_machine-2.11.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:13502936af3e58396327fcff6228f113c06d94dd9965070d8c8d1b6a8ef20167"}, - {file = "time_machine-2.11.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:bdbf92b1a479356b97a277ed229d6b592f1fb14e71d3534b32346208eb7eff50"}, - {file = "time_machine-2.11.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:863c7e74539741737fedc5fff8b0db4de53db9c9a9e5428c2b977f34daa97bad"}, - {file = "time_machine-2.11.0-cp310-cp310-win32.whl", hash = "sha256:575342fbebceddf42a0d1cb3855db8d1bde26ac6fe969c6dc457ff227e63df2d"}, - {file = "time_machine-2.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:e26d26a318a609d3471ffb307c218ddb5c1514de65cb896bea0b230833340018"}, - {file = "time_machine-2.11.0-cp310-cp310-win_arm64.whl", hash = "sha256:c48c7705880608086dd9919173dd63985837e1b1db56ee048ff4797aed99018b"}, - {file = "time_machine-2.11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f1b1c0cd878b432d724cca59f9fe57acf29c3d4a9cb1e6c70278b77c55b23523"}, - {file = "time_machine-2.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:628c2eb2037eff591ac8abefce20488930d6227158c080b3d3256a0855436431"}, - {file = "time_machine-2.11.0-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:945bb190b1f1a1d1b642cbe77c4bc3598c8085840a539904aed2b28790cca5d2"}, - {file = "time_machine-2.11.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:98dff4645fddc1c7cddd2d22d56c10f87d23fd5091fbf03d65aa523de4f5de08"}, - {file = "time_machine-2.11.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b773c16427714b9f70a96e75834d47e4105450a9dcd75025b11f22a4e6b16c65"}, - {file = "time_machine-2.11.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ddd6a83a4a86de62e125ed4431934602a1074a946b9243eae8cd76ebab78b73"}, - {file = "time_machine-2.11.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c29cb7f7358b34722c3f005aa3ca34cee22c057a642e648af82aada30a2281a9"}, - {file = "time_machine-2.11.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:86207d54f1b4f4eab78b55d120a686d8cb55da77131ba5c1fc5d593e74e58e62"}, - {file = "time_machine-2.11.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ee1d6ac1d292953dc90d1a39aef3f727ea02cce4426f025bc55d4c1553006044"}, - {file = "time_machine-2.11.0-cp311-cp311-win32.whl", hash = "sha256:c6395911dad8c5ed138f143941ae4d40fd5da383649ac0ffe4c2f2a1ee88911a"}, - {file = "time_machine-2.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:e883660a606e696085fcc25833ed31a5227e201314c4e238c8abdaf937cd472f"}, - {file = "time_machine-2.11.0-cp311-cp311-win_arm64.whl", hash = "sha256:6a76908586b8d79cf4d38393f7b8164cedd9fad5e2100014351aff885d1ef5af"}, - {file = "time_machine-2.11.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:995de1e3d631be23c1ac340db75680cb04419205f652155fe5c40367a8810e6e"}, - {file = "time_machine-2.11.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1406a9487224d68e7e756093b8009a1946c8899d414676740d934b09f7e12f40"}, - {file = "time_machine-2.11.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:72fa4edd05fbadd77ccb77e184414cff3137f7ff80c610146481c88080a2848c"}, - {file = "time_machine-2.11.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9fd4ab2f4a056c17e75e22cafd0461fcf94ae824d3140f7f0e9d513548ca6b6c"}, - {file = "time_machine-2.11.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8843a705cbd44971b39d25e24140c4922a836afb7a66311aa4fc437c0113a89"}, - {file = "time_machine-2.11.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d876570c26ffce923032d1d9544a14fb48d764581fa96baa44cd3a40a9eab8a7"}, - {file = "time_machine-2.11.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:60ad90d8a4437c961be3b3de616fe12852eab802b87b086ac621b6a96eca125e"}, - {file = "time_machine-2.11.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:01c9732d7f396210fa384ec43c86395a94cb22f4687e9fdb972ecc05bb1b1570"}, - {file = "time_machine-2.11.0-cp38-cp38-win32.whl", hash = "sha256:855b6911ca20f6d4f118593ab79c5b45ee2311739944f8d1ddfdcc755f131c8e"}, - {file = "time_machine-2.11.0-cp38-cp38-win_amd64.whl", hash = "sha256:2c99bdd61d83fdd65ee61ad0b7d1e5cd8c85143ed6a4d0a4acb445a99e3646f9"}, - {file = "time_machine-2.11.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:cc801788d230a55927f8dededff534014c8023ccd4baf89558d234a9e8bed3c7"}, - {file = "time_machine-2.11.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e0cd973d829a09c1eb9292270141b319606bca16e427ff8fcf0df69982c987a6"}, - {file = "time_machine-2.11.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2714a00be7fcd2fd2c0cd430470693a7914f936f5218c5d1f1c7632ad6b003cf"}, - {file = "time_machine-2.11.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:64940b7f70d808c766f893c8f62d5fc1b2ec9217eebfbd50ad9cc4000325867d"}, - {file = "time_machine-2.11.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2b53b29895addca43a065d66af6e632b165299e8b4b514b4f62870b71af9aff"}, - {file = "time_machine-2.11.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b140c434c81331df16c3ca1654c7892aecf2fdfd4dc7b7136ca757a9847c1bf3"}, - {file = "time_machine-2.11.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:be037615c9f0b81a9d101a44c8d88a34c6a202b5eb60a4dcacfba183d48ae0d2"}, - {file = "time_machine-2.11.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:49da9ad3eda905534de6188e4040c857b9393d225ef1ce93d8ad477ba15b4714"}, - {file = "time_machine-2.11.0-cp39-cp39-win32.whl", hash = "sha256:defaed86af4fc06cee02a5e4ef7ae26ec17131f5e43d7820c559c95f28173710"}, - {file = "time_machine-2.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:c518ea79f443f93f7e164b1ed8763c49685759b38b94bd436386cc216bf52103"}, - {file = "time_machine-2.11.0-cp39-cp39-win_arm64.whl", hash = "sha256:a80ae2c92735b01bd1cac7fa433a13a499c23748adb2399acab2b3c6f50d3742"}, - {file = "time_machine-2.11.0.tar.gz", hash = "sha256:6c08a0f9ef8b53ca8b69c0be3f9ddb85a587a784fc239b74c35e6c47bf359515"}, + {file = "time_machine-2.12.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:786efcc336edc196e5a854a73ff714be198bc57da6856064083677a188c8e018"}, + {file = "time_machine-2.12.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:35ec4170e5045ac0d5dfb1255320e301d5b6fc359f9cf36010007bf572888e73"}, + {file = "time_machine-2.12.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47dc877f3a475d0e818b31a6ad6fc1fbe40f334dcd73d2cb076057aff4d73beb"}, + {file = "time_machine-2.12.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fcb41d70da706e14a805fcbf42bdb17435d4a91420bd5b6a88f8f61beb95b862"}, + {file = "time_machine-2.12.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8676471983482522f3e124ad2c8fe38d6d6ed957379504910d2ea0c646d96cb4"}, + {file = "time_machine-2.12.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:1e3b07d7aa993a2f24e3a2ef5a216869f0a1fcaaba6227ad73b265c4f15feca5"}, + {file = "time_machine-2.12.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:328266ea13f0c517cdf758c16a2d83f0118516b8ac7910bba4eba6d4d3b3b2f1"}, + {file = "time_machine-2.12.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:496a47e2eef78893eb6915d0a5215c59346ebe29d5c88a56301ed74deebe45cc"}, + {file = "time_machine-2.12.0-cp310-cp310-win32.whl", hash = "sha256:d7442e9cffccd76115521f8d64c270e923e566e9487ba9da9824149653cf0641"}, + {file = "time_machine-2.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:796968ca8e770ee1121fe209a18cee9bd462bc0cacf57e2b1d528df08c6f18d6"}, + {file = "time_machine-2.12.0-cp310-cp310-win_arm64.whl", hash = "sha256:a525dd4fd6f7a2ecf2b54fce3c8b9982650dc570992ca6e38987c3922684099a"}, + {file = "time_machine-2.12.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ead6c3a1858c551b4edbba781d48892a487fda6ef6416c87f8ed559bfb29c904"}, + {file = "time_machine-2.12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:93c84850c9e529433613af2b2097634d27b30e9853271b6ea1384ee00be5424a"}, + {file = "time_machine-2.12.0-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:8ec623cff18e328781ab7a6251f1ee77e225f14e1f5a26633028a14b7d90ed82"}, + {file = "time_machine-2.12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b3fe070414ef05359c55bbbf94b7a895d532af726705e6f33e6f2eeb26326042"}, + {file = "time_machine-2.12.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43a4a5d722f7a6b6ee8f1d3cdeffe6d7c3421452219dce0d22778e6810fb645c"}, + {file = "time_machine-2.12.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc80ba01ff5663c74ce74c9ee2267dbf900ee8e8d18d55937b5e83eb1e179998"}, + {file = "time_machine-2.12.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d4f546b262e0d955376bf0af9a4de13a910f5f27c5e44e4db46ceea61b4c4a7d"}, + {file = "time_machine-2.12.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:05f7320851edb3e887d79a5c797966c8c1b64458fb8b8ee74982c6593606a387"}, + {file = "time_machine-2.12.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:669437542e9027db55e06ff05e848a9cb0e88d1fc6e659b412e9721be227b9be"}, + {file = "time_machine-2.12.0-cp311-cp311-win32.whl", hash = "sha256:82062eef6096c42ce14c7c07a7898caa3d696ac189fdb1586f59562893f6abf4"}, + {file = "time_machine-2.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:8176eba6b182f88fa8afd9a964c9391b73f3456f6c2f59bb2514957ec6269724"}, + {file = "time_machine-2.12.0-cp311-cp311-win_arm64.whl", hash = "sha256:d09aaa1d323c4a4b5b4569f44a02bb24ba5030b55adc9710a895843796363c0c"}, + {file = "time_machine-2.12.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:2b84449a2b170ed51c26a725a2ca983bc98490c5f23d28e9473402adc7e694ba"}, + {file = "time_machine-2.12.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b9dec9619ff5e89798e9cfb5e2a53e1eed18afa1b20460d7158fa2db94dd2d3b"}, + {file = "time_machine-2.12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1b7b1b758de72de73fcf063be8ae9e2e98dd4bab0e6cd8b32c8e7d0462d78b0"}, + {file = "time_machine-2.12.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9f13f8c8dc72541654830d16efcc6249969bac1cbe591bee4a0ac19490592e2"}, + {file = "time_machine-2.12.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c764bd6690b83a72b4294934774044c8cea4356cb9b103b7dbb8232242b3047"}, + {file = "time_machine-2.12.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:fafb423453e711ea95a669373bdaf628e9e8a0c606c1366499835f3e446554dc"}, + {file = "time_machine-2.12.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:26d2be6009151de7aa210e8569c49eece6563b8beb7e290ebd4a10b2b8d2fc5c"}, + {file = "time_machine-2.12.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:81b88ac04e61c772854fa85b8e04428e3068fe690487a50b69bb07dda2168c36"}, + {file = "time_machine-2.12.0-cp312-cp312-win32.whl", hash = "sha256:58ec76d58dcc3ab6a3d7951ae08ae99c7b413a40c7e8255c106b5df4f768f8dd"}, + {file = "time_machine-2.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:156fdd17fde2a3ea9c41a8108b8ac877e4f90a7ac5e6db533ab6ecb86f723891"}, + {file = "time_machine-2.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:5240e1cb013826449a5065062b47a46ce3d431fc47cbddc938e3c05e3fe4a951"}, + {file = "time_machine-2.12.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:bb60d061978487db5cba8a20fb84b3ae29af5ca004a0e991cd5eaa31b0851b59"}, + {file = "time_machine-2.12.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e8d5ae0f0a25b3aa7207688edf23de514f918a91ea05edbeffdbdd56d8497c13"}, + {file = "time_machine-2.12.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3992b9285c75c6f74fabfdb0ca7f17f12e22d0fc631ff43d0e110ccd53382569"}, + {file = "time_machine-2.12.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:33ee51c6f9f02d7b1d792d379b42321a3d13b819ecd8d136fb287be4adc7b9da"}, + {file = "time_machine-2.12.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:715956ef123645ef22d1c7a13963bb9bc50c02b8578797704715a410bfa49575"}, + {file = "time_machine-2.12.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e45505414fba93a15957a43ff52bbf737c3ef7905464eb16ef45e1395e95206d"}, + {file = "time_machine-2.12.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4bd89a8bf7756b50de180258517004f30857deea82c1841f291a2c8e25cfaa83"}, + {file = "time_machine-2.12.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:9c389ef8508f787ff1ec963b473838616773db8e00bc043cab9374f36d9e8201"}, + {file = "time_machine-2.12.0-cp38-cp38-win32.whl", hash = "sha256:5e62e45a71674b5df9f9275ffbb342c78ba026c9b556478d0b4bc4470e9f2b4b"}, + {file = "time_machine-2.12.0-cp38-cp38-win_amd64.whl", hash = "sha256:9ac560499086184142b0a0b28eca0ea1d245e9df1c008ef3356b0e3ea6cb1536"}, + {file = "time_machine-2.12.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b535a2524e1adbac3c8028c49cdceb764f800ca95c2f7421aad11c5d4c274ed7"}, + {file = "time_machine-2.12.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d365d6e0faaf6bcddacfb71d8c033011b7a65f1a94142350a1bc9da3c85bfb8e"}, + {file = "time_machine-2.12.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f9c48f19a6af887ac769740e914f8eb8e406a3d33a651e107f28bba1adc3796"}, + {file = "time_machine-2.12.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a0a3fb0c316c23b0d79810cf7a158c7d4671acc02a5dfa5cda7aa673478a0dc"}, + {file = "time_machine-2.12.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:353b6b43e43aee22df79194584c587225ec1a06a2f444099ada2096d806d602e"}, + {file = "time_machine-2.12.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0613aef850db722f2ceee1923d67fc050ae8d6a09fa2cd1ca1dae0748864e6d7"}, + {file = "time_machine-2.12.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:a5304de3e41c33cd6e4be7b85f09409b1059b9ff6a8289482352c42fb50b4e42"}, + {file = "time_machine-2.12.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4cf10267610244d4398312c4eea5cfc2f68f9a0286260d2157d45d1a54dcc6b2"}, + {file = "time_machine-2.12.0-cp39-cp39-win32.whl", hash = "sha256:9b255feaa4f3c46c7ebd1319a630ee1e3aa87078c9b428f9428980597c3ce830"}, + {file = "time_machine-2.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:81095391ccef01c56b6061248216da4e2d749b543952fce199b628b8a8ce5ca2"}, + {file = "time_machine-2.12.0-cp39-cp39-win_arm64.whl", hash = "sha256:dfe8b2478b4c3556a913b187ce598ad2afd07e6acfcf652be8e5a56dee2bf200"}, + {file = "time_machine-2.12.0.tar.gz", hash = "sha256:e0c98003096624cc70caa5743fe6a1fd0e97ffeaf9b44560e4158b0e1a38168e"}, ] [package.dependencies] @@ -2293,6 +2503,28 @@ files = [ docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] test = ["argcomplete (>=2.0)", "pre-commit", "pytest", "pytest-mock"] +[[package]] +name = "typer" +version = "0.9.0" +description = "Typer, build great CLIs. Easy to code. Based on Python type hints." +category = "main" +optional = true +python-versions = ">=3.6" +files = [ + {file = "typer-0.9.0-py3-none-any.whl", hash = "sha256:5d96d986a21493606a358cae4461bd8cdf83cbf33a5aa950ae629ca3b51467ee"}, + {file = "typer-0.9.0.tar.gz", hash = "sha256:50922fd79aea2f4751a8e0408ff10d2662bd0c8bbfa84755a699f3bada2978b2"}, +] + +[package.dependencies] +click = ">=7.1.1,<9.0.0" +typing-extensions = ">=3.7.4.3" + +[package.extras] +all = ["colorama (>=0.4.3,<0.5.0)", "rich (>=10.11.0,<14.0.0)", "shellingham (>=1.3.0,<2.0.0)"] +dev = ["autoflake (>=1.3.1,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "pre-commit (>=2.17.0,<3.0.0)"] +doc = ["cairosvg (>=2.5.2,<3.0.0)", "mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "pillow (>=9.3.0,<10.0.0)"] +test = ["black (>=22.3.0,<23.0.0)", "coverage (>=6.2,<7.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.910)", "pytest (>=4.4.0,<8.0.0)", "pytest-cov (>=2.10.0,<5.0.0)", "pytest-sugar (>=0.9.4,<0.10.0)", "pytest-xdist (>=1.32.0,<4.0.0)", "rich (>=10.11.0,<14.0.0)", "shellingham (>=1.3.0,<2.0.0)"] + [[package]] name = "typing-extensions" version = "4.7.1" @@ -2397,14 +2629,14 @@ files = [ [[package]] name = "werkzeug" -version = "2.3.6" +version = "2.3.7" description = "The comprehensive WSGI web application library." category = "dev" optional = false python-versions = ">=3.8" files = [ - {file = "Werkzeug-2.3.6-py3-none-any.whl", hash = "sha256:935539fa1413afbb9195b24880778422ed620c0fc09670945185cce4d91a8890"}, - {file = "Werkzeug-2.3.6.tar.gz", hash = "sha256:98c774df2f91b05550078891dee5f0eb0cb797a522c757a2452b9cee5b202330"}, + {file = "werkzeug-2.3.7-py3-none-any.whl", hash = "sha256:effc12dba7f3bd72e605ce49807bbe692bd729c3bb122a3b91747a6ae77df528"}, + {file = "werkzeug-2.3.7.tar.gz", hash = "sha256:2b8c0e447b4b9dbcc85dd97b6eeb4dcbaf6c8b6c3be0bd654e25553e0a2157d8"}, ] [package.dependencies] @@ -2417,7 +2649,7 @@ watchdog = ["watchdog (>=2.3)"] name = "win32-setctime" version = "1.1.0" description = "A small Python utility to set file creation time on Windows" -category = "dev" +category = "main" optional = false python-versions = ">=3.5" files = [ @@ -2444,7 +2676,10 @@ files = [ docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy (>=0.9.1)", "pytest-ruff"] +[extras] +cli = ["loguru", "pendulum", "pyperclip", "rich", "typer"] + [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "ef2217f46cbd0df1395a22e6f473e3eda76dbc526adaa016b732ddae362e50f7" +content-hash = "e82f1f500894cef20e4c46f954e4c0ef6ff7eb311b2516d0a66d99da710292b0" diff --git a/pyproject.toml b/pyproject.toml index 747c578..3105c43 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,33 +21,47 @@ fastapi = ">=0.68" pydantic = "<2.0" httpx = "^0" snick = "^1.3" -py-buzz = "^3.1" +py-buzz = "^4.1" # These must be included as a main dependency for the pytest extension to work out of the box respx = "^0" pytest = ">=6, <8" auto-name-enum = "^2" +# These are needed for the "cli" extra +typer = {version = "^0.9.0", optional = true} +loguru = {version = "^0.5.3", optional = true} +rich = {version = "^13.5.2", optional = true} +pendulum = {version = "^2.1.2", optional = true} +pyperclip = {version = "^1.8.2", optional = true} + +[tool.poetry.extras] +cli = ["typer", "loguru", "rich", "pendulum", "pyperclip"] + + [tool.poetry.group.dev.dependencies] ipython = "^7" asgi-lifespan = "^1.0.1" pytest-asyncio = "^0" pytest-random-order = "^1.0.4" -mypy = "^0" -isort = "^5" +mypy = "^1.5" pytest-sugar = "^0.9.4" -black = "^22" python-dotenv = "^0.19" pytest-cov = "^4" uvicorn = "^0.15" loguru = "^0.5.3" Sphinx = "^4" -pyproject-flake8 = "^5" -plummet = {extras = ["time-machine"], version = "^1.1.0"} grip = "^4.6.1" mkdocs-material = "^9.1.21" mkdocstrings = {extras = ["python"], version = "^0.22.0"} pygments = "^2.16.1" +plummet = {extras = ["time-machine"], version = "^1.2.1"} +pytest-mock = "^3.11.1" +ruff = "^0.0.286" + + +[tool.poetry.scripts] +armasec = {callable = "cli.main:app", extras = ["cli"]} [tool.poetry.plugins.pytest11] pytest_armasec = "armasec.pytest_extension" @@ -69,18 +83,9 @@ module = [ ] ignore_missing_imports = true -[tool.black] +[tool.ruff] line-length = 100 -[tool.isort] -line_length = 100 -multi_line_output = 3 -include_trailing_comma = true - -[tool.flake8] -max-line-length = 100 -max-complexity = 40 - [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" diff --git a/tests/cli/__init__.py b/tests/cli/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/cli/check.py b/tests/cli/check.py new file mode 100644 index 0000000..1f80504 --- /dev/null +++ b/tests/cli/check.py @@ -0,0 +1,6 @@ +import pytest + +try: + import typer # noqa +except ImportError: + pytest.skip("CLI extra is not installed", allow_module_level=True) diff --git a/tests/cli/conftest.py b/tests/cli/conftest.py new file mode 100644 index 0000000..1f9a162 --- /dev/null +++ b/tests/cli/conftest.py @@ -0,0 +1,53 @@ +import pendulum +import plummet +import pytest +from jose import jwt +from typer import Context + +from cli.schemas import CliContext + + +@pytest.fixture +def make_token(): + """ + Provide a fixture that returns a helper function for creating an access_token for testing. + """ + + def _helper(azp: str = None, email: str = None, expires=plummet.AGGREGATE_TYPE) -> str: + """ + Create an access_token with a given user email, org name, and expiration moment. + """ + expires_moment: pendulum.DateTime = plummet.momentize(expires) + + extra_claims = dict() + if azp is not None: + extra_claims["azp"] = azp + if email is not None: + extra_claims["email"] = email + + return jwt.encode( + { + "exp": expires_moment.int_timestamp, + **extra_claims, + }, + "fake-secret", + algorithm="HS256", + ) + + return _helper + + + +@pytest.fixture +def override_cache_dir(tmp_path, mocker): + with mocker.patch("cli.cache.cache_dir", new=tmp_path): + token_path = tmp_path / "token" + token_path.mkdir() + yield + + +@pytest.fixture +def mock_context(mocker): + typer_context = Context(mocker.MagicMock()) + typer_context.obj = CliContext() + return typer_context diff --git a/tests/cli/test_auth.py b/tests/cli/test_auth.py new file mode 100644 index 0000000..b28db50 --- /dev/null +++ b/tests/cli/test_auth.py @@ -0,0 +1,310 @@ +import httpx +import pendulum +import plummet +import pytest +from jose import ExpiredSignatureError + +from cli.auth import ( + fetch_auth_tokens, + init_persona, + refresh_access_token, + validate_token_and_extract_identity, +) +from cli.cache import _get_token_paths +from cli.schemas import TokenSet, CliContext +from cli.exceptions import Abort +from cli.time_loop import Tick +from cli.config import init_settings +from cli.client import build_client + + +def test_validate_token_and_extract_identity__success(make_token): + access_token = make_token( + azp="dummy-client", + email="good@email.com", + expires="2022-02-16 22:30:00", + ) + with plummet.frozen_time("2022-02-16 21:30:00"): + identity_data = validate_token_and_extract_identity( + TokenSet(access_token=access_token) + ) + assert identity_data.client_id == "dummy-client" + assert identity_data.email == "good@email.com" + + +def test_validate_token_and_extract_identity__re_raises_ExpiredSignatureError(make_token): + access_token = make_token( + azp="dummy-client", + email="good@email.com", + expires="2022-02-16 20:30:00", + ) + with plummet.frozen_time("2022-02-16 21:30:00"): + with pytest.raises(ExpiredSignatureError): + validate_token_and_extract_identity(TokenSet(access_token=access_token)) + + +def test_validate_token_and_extract_identity__raises_abort_on_empty_token(): + test_token_set = TokenSet(access_token="") + with pytest.raises(Abort, match="Access token file exists but it is empty"): + validate_token_and_extract_identity(test_token_set) + + +def test_validate_token_and_extract_identity__raises_abort_on_unknown_error(mocker): + test_token_set = TokenSet(access_token="BOGUS-TOKEN") + mocker.patch("jose.jwt.decode", side_effect=Exception("BOOM!")) + with pytest.raises(Abort, match="There was an unknown error while validating"): + validate_token_and_extract_identity(test_token_set) + + +def test_validate_token_and_extract_identity__raises_abort_if_token_is_missing_identity_data( + make_token +): + access_token = make_token(expires="2022-02-16 22:30:00") + with plummet.frozen_time("2022-02-16 21:30:00"): + with pytest.raises(Abort, match="error extracting the user's identity"): + validate_token_and_extract_identity(TokenSet(access_token=access_token)) + + +@pytest.mark.usefixtures("override_cache_dir") +def test_init_persona__success(make_token, mock_context): + access_token = make_token( + azp="dummy-client", + email="good@email.com", + expires="2022-02-16 22:30:00", + ) + refresh_token = "dummy-refresh-token" + (access_token_path, refresh_token_path) = _get_token_paths() + access_token_path.write_text(access_token) + refresh_token_path.write_text(refresh_token) + + with plummet.frozen_time("2022-02-16 21:30:00"): + persona = init_persona(mock_context) + + assert persona.token_set.access_token == access_token + assert persona.token_set.refresh_token == refresh_token + assert persona.identity_data.client_id == "dummy-client" + assert persona.identity_data.email == "good@email.com" + + +@pytest.mark.usefixtures("override_cache_dir") +def test_init_persona__uses_passed_token_set(make_token, mock_context): + access_token = make_token( + azp="dummy-client", + email="good@email.com", + expires="2022-02-16 22:30:00", + ) + refresh_token = "dummy-refresh-token" + (access_token_path, refresh_token_path) = _get_token_paths() + + token_set = TokenSet( + access_token=access_token, + refresh_token=refresh_token, + ) + + assert not access_token_path.exists() + assert not refresh_token_path.exists() + + with plummet.frozen_time("2022-02-16 21:30:00"): + persona = init_persona(mock_context, token_set) + + assert persona.token_set.access_token == access_token + assert persona.token_set.refresh_token == refresh_token + assert persona.identity_data.client_id == "dummy-client" + assert persona.identity_data.email == "good@email.com" + + assert access_token_path.exists() + assert access_token_path.read_text() == access_token + assert refresh_token_path.exists() + + +@pytest.mark.usefixtures("override_cache_dir") +def test_init_persona__refreshes_access_token_if_it_is_expired(make_token, respx_mock): + access_token = make_token( + azp="dummy-client", + email="good@email.com", + expires="2022-02-16 22:30:00", + ) + refresh_token = "dummy-refresh-token" + (access_token_path, refresh_token_path) = _get_token_paths() + access_token_path.write_text(access_token) + refresh_token_path.write_text(refresh_token) + + refreshed_access_token = make_token( + azp="dummy-client", + email="good@email.com", + expires="2022-02-17 22:30:00", + ) + + respx_mock.post("https://test.domain/protocol/openid-connect/token").mock( + return_value=httpx.Response( + httpx.codes.OK, + json=dict(access_token=refreshed_access_token), + ), + ) + + + settings = init_settings( + oidc_domain="test.domain", + oidc_audience="https://test.domain/api", + oidc_client_id="test-client-id" + ) + client = build_client(settings) + cli_context = CliContext( + settings=settings, + client=client, + ) + + with plummet.frozen_time("2022-02-16 23:30:00"): + persona = init_persona(cli_context) + + assert persona.token_set.access_token == refreshed_access_token + assert persona.token_set.refresh_token == refresh_token + assert persona.identity_data.client_id == "dummy-client" + assert persona.identity_data.email == "good@email.com" + + assert access_token_path.exists() + assert access_token_path.read_text() == refreshed_access_token + assert refresh_token_path.exists() + assert refresh_token_path.read_text() == refresh_token + + +def test_refresh_access_token__success(make_token, respx_mock, mock_context): + access_token = "expired-access-token" + refresh_token = "dummy-refresh-token" + token_set = TokenSet(access_token=access_token, refresh_token=refresh_token) + + refreshed_access_token = make_token( + azp="dummy-client", + email="good@email.com", + expires="2022-02-17 22:30:00", + ) + + respx_mock.post("https://test.domain/protocol/openid-connect/token").mock( + return_value=httpx.Response( + httpx.codes.OK, + json=dict(access_token=refreshed_access_token), + ), + ) + + settings = init_settings( + oidc_domain="test.domain", + oidc_audience="https://test.domain/api", + oidc_client_id="test-client-id" + ) + client = build_client(settings) + cli_context = CliContext( + settings=settings, + client=client, + ) + + refresh_access_token(cli_context, token_set) + + assert token_set.access_token == refreshed_access_token + + +def test_refresh_access_token__raises_abort_on_non_200_response(respx_mock): + access_token = "expired-access-token" + refresh_token = "dummy-refresh-token" + token_set = TokenSet(access_token=access_token, refresh_token=refresh_token) + + respx_mock.post("https://test.domain/protocol/openid-connect/token").mock( + return_value=httpx.Response( + httpx.codes.BAD_REQUEST, + json=dict(error_description="BOOM!"), + ), + ) + + settings = init_settings( + oidc_domain="test.domain", + oidc_audience="https://test.domain/api", + oidc_client_id="test-client-id" + ) + client = build_client(settings) + cli_context = CliContext( + settings=settings, + client=client, + ) + + with pytest.raises(Abort, match="auth token could not be refreshed"): + refresh_access_token(cli_context, token_set) + + +def test_fetch_auth_tokens__success(respx_mock): + access_token = "dummy-access-token" + refresh_token = "dummy-refresh-token" + respx_mock.post("https://test.domain/protocol/openid-connect/auth/device").mock( + return_value=httpx.Response( + httpx.codes.OK, + json=dict( + device_code="dummy-code", + verification_uri_complete="https://dummy-uri.com", + interval=1, + ), + ), + ) + respx_mock.post("https://test.domain/protocol/openid-connect/token").mock( + return_value=httpx.Response( + httpx.codes.OK, + json=dict( + access_token=access_token, + refresh_token=refresh_token, + ), + ), + ) + + settings = init_settings( + oidc_domain="test.domain", + oidc_audience="https://test.domain/api", + oidc_client_id="test-client-id" + ) + client = build_client(settings) + cli_context = CliContext( + settings=settings, + client=client, + ) + + token_set = fetch_auth_tokens(cli_context) + assert token_set.access_token == access_token + assert token_set.refresh_token == refresh_token + + +def test_fetch_auth_tokens__raises_Abort_when_it_times_out_waiting_for_the_user( + respx_mock, + mocker, +): + respx_mock.post("https://test.domain/protocol/openid-connect/auth/device").mock( + return_value=httpx.Response( + httpx.codes.OK, + json=dict( + device_code="dummy-code", + verification_uri_complete="https://dummy-uri.com", + interval=0, + ), + ), + ) + respx_mock.post("https://test.domain/protocol/openid-connect/token").mock( + return_value=httpx.Response( + httpx.codes.BAD_REQUEST, + json=dict(error="authorization_pending"), + ), + ) + + settings = init_settings( + oidc_domain="test.domain", + oidc_audience="https://test.domain/api", + oidc_client_id="test-client-id" + ) + client = build_client(settings) + cli_context = CliContext( + settings=settings, + client=client, + ) + + one_tick = Tick( + counter=1, + elapsed=pendulum.Duration(seconds=1), + total_elapsed=pendulum.Duration(seconds=1), + ) + mocker.patch("cli.auth.TimeLoop", return_value=[one_tick]) + with pytest.raises(Abort, match="not completed in time"): + fetch_auth_tokens(cli_context) diff --git a/tests/cli/test_cache.py b/tests/cli/test_cache.py new file mode 100644 index 0000000..998f092 --- /dev/null +++ b/tests/cli/test_cache.py @@ -0,0 +1,152 @@ +import pytest + +from cli.cache import ( + _get_token_paths, + load_tokens_from_cache, + save_tokens_to_cache, + clear_token_cache, + init_cache, +) +from cli.exceptions import Abort +from cli.schemas import TokenSet + + +@pytest.mark.usefixtures("override_cache_dir") +def test_load_tokens_from_cache__success(make_token): + access_token = make_token( + azp="dummy-client", + email="good@email.com", + expires="2022-02-16 22:30:00", + ) + refresh_token = "dummy-refresh-token" + (access_token_path, refresh_token_path) = _get_token_paths() + access_token_path.write_text(access_token) + refresh_token_path.write_text(refresh_token) + + token_set = load_tokens_from_cache() + + assert token_set.access_token == access_token + assert token_set.refresh_token == refresh_token + + +@pytest.mark.usefixtures("override_cache_dir") +def test_load_tokens_from_cache__omits_refresh_token_if_it_is_not_found(make_token): + access_token = make_token( + azp="dummy-client", + email="good@email.com", + expires="2022-02-16 22:30:00", + ) + (access_token_path, _) = _get_token_paths() + access_token_path.write_text(access_token) + + token_set = load_tokens_from_cache() + + assert token_set.access_token == access_token + assert token_set.refresh_token is None + + +@pytest.mark.usefixtures("override_cache_dir") +def test_save_tokens_to_cache__success(make_token): + access_token = make_token( + azp="dummy-client", + email="good@email.com", + expires="2022-02-16 22:30:00", + ) + refresh_token = "dummy-refresh-token" + (access_token_path, refresh_token_path) = _get_token_paths() + token_set = TokenSet( + access_token=access_token, + refresh_token=refresh_token, + ) + + save_tokens_to_cache(token_set) + + assert access_token_path.exists() + assert access_token_path.read_text() == access_token + assert access_token_path.stat().st_mode & 0o777 == 0o600 + + assert refresh_token_path.exists() + assert refresh_token_path.read_text() == refresh_token + assert access_token_path.stat().st_mode & 0o777 == 0o600 + + +@pytest.mark.usefixtures("override_cache_dir") +def test_save_tokens_to_cache__only_saves_access_token_if_refresh_token_is_not_defined( + make_token, +): + access_token = make_token( + azp="dummy-client", + email="good@email.com", + expires="2022-02-16 22:30:00", + ) + (access_token_path, refresh_token_path) = _get_token_paths() + token_set = TokenSet( + access_token=access_token, + ) + + save_tokens_to_cache(token_set) + + assert access_token_path.exists() + assert access_token_path.read_text() == access_token + + assert not refresh_token_path.exists() + + +@pytest.mark.usefixtures("override_cache_dir") +def test_clear_token_cache__success(make_token): + access_token = make_token( + azp="dummy-client", + email="good@email.com", + expires="2022-02-16 22:30:00", + ) + refresh_token = "dummy-refresh-token" + (access_token_path, refresh_token_path) = _get_token_paths() + access_token_path.write_text(access_token) + refresh_token_path.write_text(refresh_token) + + assert access_token_path.exists() + assert refresh_token_path.exists() + + clear_token_cache() + + assert not access_token_path.exists() + + +@pytest.mark.usefixtures("override_cache_dir") +def test_clear_token_cache__does_not_fail_if_no_tokens_are_in_cache(): + (access_token_path, refresh_token_path) = _get_token_paths() + + assert not access_token_path.exists() + assert not refresh_token_path.exists() + + clear_token_cache() + + +@pytest.mark.usefixtures("override_cache_dir") +def test_init_cache__success(tmp_path, mocker): + new_cache_dir = tmp_path / "cache" + with mocker.patch("cli.cache.cache_dir", new=new_cache_dir): + @init_cache + def _helper(): + assert new_cache_dir.exists() + token_path = new_cache_dir / "token" + assert token_path.exists() + info_file_path = new_cache_dir / "info.txt" + assert info_file_path.exists() + assert "cache" in info_file_path.read_text() + + _helper() + + +@pytest.mark.usefixtures("override_cache_dir") +def test_init_cache__raises_Abort_if_cache_dir_is_not_usable(tmp_path): + info_file_path = tmp_path / "info.txt" + info_file_path.write_text("blah") + info_file_path.chmod(0o000) + + @init_cache + def _helper(): + pass + + with pytest.raises(Abort, match="check your home directory permissions"): + _helper() diff --git a/tests/cli/test_client.py b/tests/cli/test_client.py new file mode 100644 index 0000000..910b25d --- /dev/null +++ b/tests/cli/test_client.py @@ -0,0 +1,359 @@ +import json +from typing import Dict, Optional + +import httpx +import pydantic +import pytest + +from cli.exceptions import Abort +from cli.client import _deserialize_request_model, make_request + + +DEFAULT_DOMAIN = "https://dummy-domain.com" + + +@pytest.fixture +def dummy_client(): + + def _helper( + base_url: str = DEFAULT_DOMAIN, + headers: Optional[Dict] = None, + ) -> httpx.Client: + if headers is None: + headers = dict() + + return httpx.Client(base_url=base_url, headers=headers) + + return _helper + + +class DummyResponseModel(pydantic.BaseModel): + foo: int + bar: str + + +class ErrorResponseModel(pydantic.BaseModel): + error: str + + +def test__deserialize_request_model__success(): + request_kwargs = dict() + _deserialize_request_model( + DummyResponseModel(foo=1, bar="one"), + request_kwargs, + "Abort message does not matter here", + "Whatever Subject", + ) + assert json.loads(request_kwargs["content"]) == dict(foo=1, bar="one") + assert request_kwargs["headers"] == {"Content-Type": "application/json"} + + +def test__deserialize_request_model__raises_Abort_if_request_kwargs_already_has_other_body_parts(): + with pytest.raises(Abort, match="Request was incorrectly structured"): + _deserialize_request_model( + DummyResponseModel(foo=1, bar="one"), + dict(data=dict(foo=11)), + "Abort message does not matter here", + "Whatever Subject", + ) + + with pytest.raises(Abort, match="Request was incorrectly structured"): + _deserialize_request_model( + DummyResponseModel(foo=1, bar="one"), + dict(json=dict(foo=11)), + "Abort message does not matter here", + "Whatever Subject", + ) + + with pytest.raises(Abort, match="Request was incorrectly structured"): + _deserialize_request_model( + DummyResponseModel(foo=1, bar="one"), + dict(content=json.dumps(dict(foo=11))), + "Abort message does not matter here", + "Whatever Subject", + ) + + +def test_make_request__success(respx_mock, dummy_client): + client = dummy_client(headers={"content-type": "garbage"}) + req_path = "/fake-path" + + respx_mock.get(f"{DEFAULT_DOMAIN}{req_path}").mock( + return_value=httpx.Response( + httpx.codes.OK, + json=dict( + foo=1, + bar="one", + ), + ), + ) + dummy_response_instance = make_request( + client, + req_path, + "GET", + expected_status=200, + response_model_cls=DummyResponseModel, + ) + assert isinstance(dummy_response_instance, DummyResponseModel) + assert dummy_response_instance.foo == 1 + assert dummy_response_instance.bar == "one" + + +def test_make_request__raises_Abort_if_client_request_raises_exception( + respx_mock, + dummy_client, +): + client = dummy_client(headers={"content-type": "garbage"}) + req_path = "/fake-path" + original_error = httpx.RequestError("BOOM!") + + respx_mock.get(f"{DEFAULT_DOMAIN}{req_path}").mock(side_effect=original_error) + + with pytest.raises( + Abort, + match="There was a big problem: Communication with the API failed" + ) as err_info: + make_request( + client, + req_path, + "GET", + abort_message="There was a big problem", + abort_subject="BIG PROBLEM", + ) + assert err_info.value.subject == "BIG PROBLEM" + assert err_info.value.log_message == ( + "There was an error making the request to the API" + ) + assert err_info.value.original_error == original_error + + +def test_make_request__raises_Abort_with_ownership_message_for_403_for_non_owners( + respx_mock, + dummy_client, +): + client = dummy_client() + req_path = "/fake-path" + + respx_mock.delete(f"{DEFAULT_DOMAIN}{req_path}").mock( + return_value=httpx.Response( + httpx.codes.FORBIDDEN, + json=dict(detail="This jabroni does not own this whingding"), + ), + ) + + with pytest.raises(Abort, match="You do not own this resource") as err_info: + make_request( + client, + req_path, + "DELETE", + expected_status=204, + abort_message="There was a big problem", + abort_subject="BIG PROBLEM", + ) + assert err_info.value.subject == "BIG PROBLEM" + assert "Resource could not be modified by non-owner" in err_info.value.log_message + assert "This jabroni does not own this whingding" in err_info.value.log_message + assert err_info.value.original_error is None + + +def test_make_request__raises_Abort_when_expected_status_is_not_None_and_response_does_not_match( + respx_mock, dummy_client +): + client = dummy_client(headers={"content-type": "garbage"}) + req_path = "/fake-path" + + respx_mock.get(f"{DEFAULT_DOMAIN}{req_path}").mock( + return_value=httpx.Response( + httpx.codes.BAD_REQUEST, + text="It blowed up", + ), + ) + + with pytest.raises( + Abort, + match="There was a big problem: Received an error response", + ) as err_info: + make_request( + client, + req_path, + "GET", + expected_status=200, + abort_message="There was a big problem", + abort_subject="BIG PROBLEM", + ) + assert err_info.value.subject == "BIG PROBLEM" + assert err_info.value.log_message == ( + "Got an error code for request: 400: It blowed up" + ) + assert err_info.value.original_error is None + + +def test_make_request__does_not_raise_Abort_when_expected_status_is_None_and_response_has_fail_code( + respx_mock, dummy_client +): + client = dummy_client(headers={"content-type": "garbage"}) + req_path = "/fake-path" + + respx_mock.get(f"{DEFAULT_DOMAIN}{req_path}").mock( + return_value=httpx.Response( + httpx.codes.BAD_REQUEST, + json=dict(error="It blowed up"), + ), + ) + + err = make_request( + client, + req_path, + "GET", + response_model_cls=ErrorResponseModel, + ) + assert err.error == "It blowed up" + + +def test_make_request__returns_the_response_status_code_if_the_method_is_DELETE( + respx_mock, + dummy_client, +): + client = dummy_client(headers={"content-type": "garbage"}) + req_path = "/fake-path" + + respx_mock.delete(f"{DEFAULT_DOMAIN}{req_path}").mock( + return_value=httpx.Response( + httpx.codes.OK, + json=dict(error="It blowed up"), + ), + ) + + assert make_request(client, req_path, "DELETE") == httpx.codes.OK + + +def test_make_request__returns_the_response_status_code_if_expect_response_is_False( + respx_mock, + dummy_client, +): + client = dummy_client(headers={"content-type": "garbage"}) + req_path = "/fake-path" + + respx_mock.post(f"{DEFAULT_DOMAIN}{req_path}").mock( + return_value=httpx.Response(httpx.codes.BAD_REQUEST), + ) + + assert make_request( + client, req_path, + "POST", + expect_response=False, + ) == httpx.codes.BAD_REQUEST + + +def test_make_request__raises_an_Abort_if_the_response_cannot_be_deserialized_with_JSON( + respx_mock, + dummy_client, +): + client = dummy_client(headers={"content-type": "garbage"}) + req_path = "/fake-path" + + respx_mock.get(f"{DEFAULT_DOMAIN}{req_path}").mock( + return_value=httpx.Response( + httpx.codes.OK, + text="Not JSON, my dude", + ), + ) + + with pytest.raises( + Abort, + match="There was a big problem: Response carried no data", + ) as err_info: + make_request( + client, + req_path, + "GET", + abort_message="There was a big problem", + abort_subject="BIG PROBLEM", + ) + assert err_info.value.subject == "BIG PROBLEM" + assert err_info.value.log_message == "Failed unpacking json: Not JSON, my dude" + assert isinstance(err_info.value.original_error, json.decoder.JSONDecodeError) + + +def test_make_request__returns_a_plain_dict_if_response_model_cls_is_None( + respx_mock, + dummy_client, +): + client = dummy_client(headers={"content-type": "garbage"}) + req_path = "/fake-path" + + respx_mock.get(f"{DEFAULT_DOMAIN}{req_path}").mock( + return_value=httpx.Response( + httpx.codes.OK, + json=dict(a=1, b=2, c=3), + ), + ) + + assert make_request(client, req_path, "GET") == dict(a=1, b=2, c=3) + + +def test_make_request__raises_an_Abort_if_the_response_data_cannot_be_serialized( + respx_mock, dummy_client +): + client = dummy_client(headers={"content-type": "garbage"}) + req_path = "/fake-path" + + respx_mock.get(f"{DEFAULT_DOMAIN}{req_path}").mock( + return_value=httpx.Response( + httpx.codes.OK, + json=dict(a=1, b=2, c=3), + ), + ) + + with pytest.raises( + Abort, + match="There was a big problem: Unexpected data in response", + ) as err_info: + make_request( + client, + req_path, + "GET", + abort_message="There was a big problem", + abort_subject="BIG PROBLEM", + response_model_cls=DummyResponseModel, + ) + assert err_info.value.subject == "BIG PROBLEM" + assert err_info.value.log_message == ( + f"Unexpected format in response data: {dict(a=1, b=2, c=3)}" + ) + assert isinstance(err_info.value.original_error, pydantic.ValidationError) + + +def test_make_request__uses_request_model_instance_for_request_body_if_passed( + respx_mock, + dummy_client, +): + client = dummy_client(headers={"content-type": "garbage"}) + req_path = "/fake-path" + + dummy_route = respx_mock.post(f"{DEFAULT_DOMAIN}{req_path}") + dummy_route.mock( + return_value=httpx.Response( + httpx.codes.CREATED, + json=dict( + foo=1, + bar="one", + ), + ), + ) + dummy_response_instance = make_request( + client, + req_path, + "POST", + expected_status=201, + response_model_cls=DummyResponseModel, + request_model=DummyResponseModel(foo=1, bar="one"), + ) + assert isinstance(dummy_response_instance, DummyResponseModel) + assert dummy_response_instance.foo == 1 + assert dummy_response_instance.bar == "one" + + assert dummy_route.calls.last.request.content == json.dumps( + dict(foo=1, bar="one") + ).encode("utf-8") + assert dummy_route.calls.last.request.headers["Content-Type"] == "application/json" diff --git a/tests/cli/test_config.py b/tests/cli/test_config.py new file mode 100644 index 0000000..a1375d8 --- /dev/null +++ b/tests/cli/test_config.py @@ -0,0 +1,76 @@ + +import json +from pathlib import Path + +import pytest +from typer import Context + +from cli.exceptions import Abort +from cli.schemas import CliContext +from cli.config import init_settings, attach_settings, dump_settings + + +def test_init_settings__success(): + settings = init_settings( + oidc_domain="test.domain", + oidc_audience="https://test.domain/api", + oidc_client_id="test-client-id" + ) + assert settings.oidc_domain == "test.domain" + assert settings.oidc_audience == "https://test.domain/api" + assert settings.oidc_client_id == "test-client-id" + + +def test_init_settings__raises_Abort_on_invalid_config(): + with pytest.raises(Abort): + init_settings() + + +def test_dump_settings__success(tmp_path, mocker): + dummy_settings_path = tmp_path / "settings.json" + with mocker.patch("cli.config.settings_path", new=dummy_settings_path): + dump_settings( + init_settings( + oidc_domain="test.domain", + oidc_audience="https://test.domain/api", + oidc_client_id="test-client-id" + ) + ) + settings_dict = json.loads(dummy_settings_path.read_text()) + + assert settings_dict["oidc_domain"] == "test.domain" + assert settings_dict["oidc_audience"] == "https://test.domain/api" + assert settings_dict["oidc_client_id"] == "test-client-id" + + +def test_attach_settings__success(tmp_path, mocker, mock_context): + dummy_settings_path = tmp_path / "settings.json" + with mocker.patch("cli.config.settings_path", new=dummy_settings_path): + dump_settings( + init_settings( + oidc_domain="test.domain", + oidc_audience="https://test.domain/api", + oidc_client_id="test-client-id" + ) + ) + + @attach_settings + def _helper(ctx): + assert ctx.obj.settings.oidc_domain == "test.domain" + assert ctx.obj.settings.oidc_audience == "https://test.domain/api" + assert ctx.obj.settings.oidc_client_id == "test-client-id" + + _helper(mock_context) + + +def test_attach_settings__raises_Abort_if_settings_file_is_not_found(mocker): + with mocker.patch("cli.config.settings_path", new=Path("fake-path")): + + @attach_settings + def _helper(*_): + pass + + typer_context = Context(mocker.MagicMock()) + typer_context.obj = CliContext() + with pytest.raises(Abort, match="No settings file found"): + _helper(typer_context) diff --git a/tests/cli/test_exceptions.py b/tests/cli/test_exceptions.py new file mode 100644 index 0000000..558f04e --- /dev/null +++ b/tests/cli/test_exceptions.py @@ -0,0 +1,14 @@ +import pytest + +from cli.exceptions import Abort + + +def test_Abort_handle_errors(): + original_error = RuntimeError("Boom!") + with pytest.raises(Abort) as err_info: + with Abort.handle_errors("Something went wrong"): + raise original_error + + assert err_info.value.original_error is original_error + assert "Boom!" in str(err_info.value) + assert "Something went wrong" in str(err_info.value)