diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..5cb0767 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,9 @@ +__pycache__/ +.boardwalk/ +.boardwalkd/ +.DS_store +.idea +.vscode/ +*.egg-info/ +build/ +dist/ diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..a34f319 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,7 @@ +## What and why? + +## How was this tested? + +## Checklist +[ ] Have you run `make test`? +[ ] Have you updated the VERSION file (if applicable)? diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5cb0767 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +__pycache__/ +.boardwalk/ +.boardwalkd/ +.DS_store +.idea +.vscode/ +*.egg-info/ +build/ +dist/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..11b27f8 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,48 @@ +# Contributing + +## Bug Reports & Feature Requests +Bug reports and feature requests are really helpful. Head over to +[Issues](https://github.com/Backblaze/boardwalk/issues), and provide +plenty of detail and context. + +## Development Guidelines + +### Development Dependencies +- `black` (`pip3 install black`) +- `build` (`pip3 install build`) +- `make` +- `pip3` +- `podman` +- `pyenv` +- `pyright` (`pip3 install pyright`) +- `usort` (`pip3 install usort`) + +#### Makefile +The [Makefile](./Makefile) has some useful targets for typical development +operations, like formatting, building, and locally installing the module. + +To install the module in editable mode run `make develop`. +To run the server in development mode, run `make develop-server`. + +See the content of the Makefile for other useful targets. + +### Code Style + +#### Automated Formatting +- Run `make format` to format to this codebase's standard. + +#### Not Automated Styling +- The last sentence in a code comments, logs, or error messages should not end + in a period (`.`). +- Comments should be used generously. + +### Testing +Automated tests are run with `make test`. + +Automated tests should be developed for cases that clearly improve Boardwalk's +reliability, user and developer experience. Otherwise there is no specific +enforcement of test coverage. + +### Versioning +The boardwalk pip module uses semantic versioning. Please make sure to update +the VERSION file along with any changes to the package. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..aba6abe --- /dev/null +++ b/Dockerfile @@ -0,0 +1,10 @@ +FROM python:3.10 AS build +WORKDIR /build +COPY . . +RUN make build + +FROM python:3.10-slim +COPY --from=build /build/dist ./dist +RUN python3 -m pip install ./dist/boardwalk-*.whl && mkdir /var/boardwalk && rm -rf ./dist +WORKDIR /var/boardwalk +ENTRYPOINT [ "python3", "-m"] diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..ceeea23 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +include VERSION diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..938189c --- /dev/null +++ b/Makefile @@ -0,0 +1,76 @@ +# Builds module artifacts +.PHONY: build +build: dist + +.PHONY: container +container: + podman build . --tag boardwalk:$(shell cat VERSION | tr -d '\n') + +# Cleans up temporary data that might get created during normal devlopment +.PHONY: clean +clean: + rm -r \ + build \ + dist \ + src/*.egg-info \ + src/boardwalk/__pycache__ \ + src/boardwalkd/__pycache__ \ + .boardwalk \ + .boardwalkd \ + || : + podman image rm localhost/boardwalk || : + +# Installs modules in editable mode +.PHONY: develop +develop: + python3 -m pip install --editable . + +.PHONY: develop-server +develop-server: +ifdef BOARDWALKD_SLACK_WEBHOOK_URL + boardwalkd serve \ + --develop \ + --host-header-pattern="(localhost|127\.0\.0\.1)" \ + --port=8888 \ + --slack-webhook-url="$(BOARDWALKD_SLACK_WEBHOOK_URL)" \ + --url='http://localhost:8888' +else + boardwalkd serve \ + --develop \ + --host-header-pattern="(localhost|127\.0\.0\.1)" \ + --port=8888 \ + --url='http://localhost:8888' +endif + +dist: clean + python3 -m pip install -U build pip + python3 -m build + +# Applys project's required code style +.PHONY: format +format: + black . + usort format . + + +# Installs modules to the local system +.PHONY: install +install: + python3 -m pip install --upgrade . + +# Runs all available tests +.PHONY: test +test: test-black test-pyright test-usort + +# Test that code is formatted with black +.PHONY: test-black +test-black: + black . --check + +.PHONY: test-pyright +test-pyright: + pyright + +.PHONY: test-usort +test-usort: + usort check . diff --git a/README.md b/README.md new file mode 100644 index 0000000..3fef61e --- /dev/null +++ b/README.md @@ -0,0 +1,461 @@ +# Boardwalk + +Boardwalk is a linear [Ansible](https://www.ansible.com/) workflow engine. It's +purpose-built to help systems engineers automate low-and-slow background jobs +against large numbers of production hosts. It's ideal for rolling-maintenance +jobs like kernel and operating system upgrades. + +Boardwalk makes it easy to define workflows composed of a series of jobs to +perform tasks on hosts using Ansible. It connects to hosts one-at-a-time running +jobs in a defined order and maintaining a local state as it goes; this makes +stopping and resuming long-running Ansible workflows easy and efficient. + +💥 __Boardwalk is alpha software. Interfaces and behaviors may change between +updates.__ 💥 + +## Motivation & Goals + +Ansible is already a very capable agentless, modular, remote execution engine. +It uses task playbooks that are easy to read and run against any number of +hosts, in parallel or in serial. However, Ansible is connection-oriented: most +operations are performed on remote hosts. Ansible connects to a host, copies +code over, and executes it. There's no practical interface to perform +calculations about a host before the host is contacted. This makes long-running +background jobs difficult to work with using Ansible alone. For example, if a +playbook is running for days or weeks and fails, Ansible doesn't retain any +knowledge of where it left off, and can't make any offline calculations about +which hosts it needs to finish up with. When the playbook is re-run, Ansible +will attempt to connect to all the hosts it had previously connected to, +potentially resulting in a long recovery time for a failed job. And this makes +sense: __Ansible is primarily a remote execution engine, and not a workflow +engine__. A workflow engine wrapping Ansible can do calculations and state +management beyond what Ansible is designed for. + +Boardwalk is implemented as a workflow engine to run jobs that wrap Ansible +tasks. It's specifically aimed towards long-running, low-and-slow, rolling +Ansible workflows. It interfaces with Ansible via +[ansible-runner](https://github.com/ansible/ansible-runner), which is the same +interface used by [AWX](https://github.com/ansible/awx). + +### V1 Goals + +- [x] Be able to walk through sets of hosts one-at-a-time using Ansible. +- [x] Collect Ansible facts from hosts to a local state. +- [x] Provide interfaces for defining and using isolated Workspaces. +- [x] Provide an interface for defining Jobs. +- [x] Provide an interface for defining and running series' of Jobs as + Workflows. +- [x] Provide an interface to pre-process facts in state to determine Job + applicability. +- [x] Provide an interface to control Job failure behavior. +- [x] Provide interfaces to catch and release Workflows. +- [x] Lock hosts when operations are being performed to prevent conflicting + operations. +- [x] Warn logged-in users when Boardwalk performs operations on hosts. +- [x] Provide a central network daemon for Boardwalk executors to report status + into and a way to view Workspace status via a web UI. +- [x] Have a way to "lock" Workspaces network-wide, using the network daemon, to + prevent multiple operations on the same Workspace anywhere in the network. +- [x] Provide a way to remotely catch a Workspace using a web UI. +- [x] Provide a way to push Slack updates from Workflows. +- [x] Have a way to capture and report on execution history. (Can be done by + using the Slack webhook on the server.) +- [x] Provide a way for multiple Workspaces to be run in parallel in the same + directory. +- [x] Have a release process and version tagging based on semantic versioning. +- [x] Add a command-line option to print the tool version +- [x] Have CSRF protection on the server UI. +- [x] Support server UI authentication. +- [x] Have a way to explore saved state. +- [ ] Support server API authentication. +- [ ] Have a way for Boardwalkfile.py's to specify Boardwalk version + constraints. +- [ ] Support TLS on the server. + +### Non-Goals + +- ⛔️ Be a scheduler. Boardwalk doesn't need to solve the problems that cron, + systemd timers or other schedulers do. Boardwalk does have primitives for + locking/mutexes to prevent overlapping operations, however. +- ⛔️ Reproduce features of Ansible. As much as possible, Boardwalk should not + reproduce core features of Ansible. Boardwalk doesn't need its own modular + task execution system because it can interface with Ansible for that. + Boardwalk shouldn't create its own inventory system, nor should it create its + own secret management system, because Ansible already has solutions to those + areas. + +## Contributing + +See [here](./CONTRIBUTING.md). + +## Concepts + +Conceptual overview diagram: + +```txt ++----------------------------------------+ +| Workspace | +| +------------------------------------+ | +| | Workflow | | +| | +------+ +------+ +------+ | | +| | | Job1 | --> | Job2 | --> | JobN | | | +| | +------+ +------+ +------+ | | +| +------------------------------------+ | ++----------------------------------------+ +``` + +### Workspace + +Workspaces define isolated configurations and state for working on projects with +Boardwalk. They define the Ansible host pattern Boardwalk should target, the +Workflow Boardwalk will use, and some essential configuration options. + +Workspaces are defined in the [Boardwalkfile.py](#the-boardwalkfile-py). The +active Workspace is selected with `boardwalk workspace use `. + +### Workflow + +Every Workspace specifies a Workflow. Workflows are the set of Jobs that will be +run against hosts in the Workspace. They define the Jobs, Jobs options, and the +order in which Jobs are run. Jobs are run in series, one after another. The +purpose of a Workflow is to mutate a host from one state to another. Typically +Workflows depend upon some Ansible fact(s) having changed after the Workflow has +completed all of its Jobs. + +Workflows are defined in the [Boardwalkfile.py](#the-boardwalkfile-py). +Workflows can be dry-run with `boardwalk check`, which runs Ansible in `--check` +mode. Workflows are run with `boardwalk run`. + +### Jobs + +Jobs define what is actually executed in a Workflow. They define Ansible tasks +that are run against hosts. They accept options that can be passed into them and +used in tasks. They define preconditions that a host must meet before a Workflow +will run against it. + +Jobs are defined in the [Boardwalkfile.py](#the-boardwalkfile-py). + +#### Job Preconditions + +Preconditions are an important feature of Boardwalk Jobs; they are the mechanism +indicating to Boardwalk whether a Workflow needs to be run on a host. When used +effectively, __Preconditions are the feature that allows Boardwalk to skip hosts +that it does not need to act upon or has already acted upon and completed.__ + +Preconditions are simply a Python boolean expression that return True or False. +Generally the preconditions are based upon Ansible facts and vars, but they may +consider anything that the execution environment has access to. Workflows +consider the preconditions of _all_ Jobs in the Workflow. If _any_ Job +preconditions are not True, the host the Workflow is acting upon will be skipped +unless the workflow had already started but never completed. + +Both facts and variables from the inventory may be used in preconditions. Facts +must be gathered from hosts using `boardwalk init`, but inventory vars are +processed at runtime when `boardwalk check` or `boardwalk run` is used. +Inventory vars may contain [Jinja](https://jinja.palletsprojects.com/) +expressions, and Boardwalk does not process them. + +Boardwalk will ignore preconditions for hosts where a Workflow was started but +never finished. Boardwalk automatically assumes the host did originally meet +preconditions but still needs the Workflow to complete. This behavior merits +some important considerations. If a host has started a Workflow, but never +finished, preconditions are ignored even if the preconditions or Workflow Jobs +have changed. Thus, it's very important not to rely on preconditions alone for +safety. Additional safety checks should be included in the Ansible tasks run by +Jobs. + +### Local State + +Boardwalk maintains a local "statefile" for each Workspace. The primary data +contained in the local state are Ansible facts associated with hosts. Inventory +vars are not stored in the statefile because they are processed at runtime. + +The state is initially built using `boardwalk init`, and Workflows update the +state with fresh Ansible facts as hosts are completed. The local state can be +reset with `boardwalk workspace reset`. + +### Remote State + +Boardwalk maintains a remote statefile on each host in +`/etc/ansible/facts.d/boardwalk_state`. This file is used internally by +Boardwalk. + +## Usage + +### Installation + +```sh +python3 -m pip install git+ssh://git@github.com/Backblaze/boardwalk.git +# Optionally appending @ allows for installing specific commits, +# branches, or tags. +``` + +__Note__: +Boardwalk should be designed to be compatible with the current stable Python3 +release, minus 1 minor version. So, if the current stable version is `3.22.x`, +Boardwalk should work with `3.21.x`. Consider using +[`pyenv`](https://github.com/pyenv/pyenv) to maintain fresh python environment. + +#### Container Install + +Boardwalk may be built as a container image by running `make container`. + +The entrypoint is simply `python -m` so either `boardwalk` or `boardwalkd` +must be specified as the command when running. + +### The `Boardwalkfile.py` + +Boardwalk is both a python library and command-line tool. The `boardwalk` +command-line tool expects a file called `Boardwalkfile.py` to exist in the +current working directory where it's run. The `Boardwalkfile.py` defines what +Workspaces, Workflows, and Jobs are available for use and how they are used. + +#### Example `Boardwalkfile.py` + +```python +""" +In this example, we define a Workspace, Workflow, and Jobs for performing +OS upgrades for storage pods in the staging environment +""" + +# Imports the boardwalk module and classes used in the Boardwalkfile.py +from boardwalk import Job, Workflow, Workspace, WorkspaceConfig, path + +# Optionally sets the URL to a boardwalkd server +boardwalkd_url = "http://localhost:8888" + +# Defines a Workspace with the name "StagingPodDistUpgrades" +class StagingPodDistUpgrades(Workspace): + """ + Workspaces have a required "config" method that returns a WorkspaceConfig + object. The WorkspaceConfig defines essential options. Some options have + defaults. Currently, the best way to see what options are available, which + are required, and what details are set to is to use a Python IDE + """ + def config(self): + return WorkspaceConfig( + # Required. Defines the Ansible host pattern + host_pattern="staging:&pod", + # Required. Defines the Workflow to use + workflow=PodDistUpgradeWorkflow(), + # Optional. Defines the default ordering Boardwalk will use to walk + # through hosts, based upon hostname. See `boardwalk run --help` for + # available options + default_sort_order="shuffle" + ) + + +# Defines a Workflow with the name "PodDistUpgradeWorkflow", referenced by the +# StagingPodDistUpgrades Workspace +class PodDistUpgradeWorkflow(Workflow): + """ + Workflows have a required "jobs" method that returns a tuple of Jobs. Jobs + in the Workflow are executed in the order defined here. This is where + options are provided to Jobs (if any) + + There is also an optional exit_jobs method, which is the same as this method + except that the Jobs returned by exit_jobs are run after regular Jobs, and + they are always attempted, even upon failure + """ + def jobs(self): + return ( + PodPreTasksJob(), + DistUpgradeJob(options={"target_version": 10}), + PodPostTasksJob(), + ) + + +# Defines a Job with the name "PodPreTasksJob", which is called in the +# PodDistUpgradeWorkflow Workflow as the first Job +class PodPreTasksJob(Job): + """ + Jobs have an optional "tasks" method that returns a list of Ansible tasks. + The format for tasks is structurally the same as Ansible tasks defined using + YAML, but they must be formatted as a list of Python dictionaries as shown + + The path() function used here as an example is an important helper for cases + where local paths are reference on the controller running Boardwalk. The + underlying interface Boardwalk uses for Ansible dynamically renders a + playbook to a temporary file outside of the working directory. The path() + function translates relative paths to absolute paths so the generated + playbook is able to locate referenced file paths + """ + def tasks(self): + return [{"import_tasks": path("dist_upgrade/pod_pre_tasks.yml")}] + +# Defines a Job with the name "DistUpgradeJob", which is called in the +# PodDistUpgradeWorkflow Workflow as the second Job +class DistUpgradeJob(Job): + """ + Jobs have an optional "required_options" method that returns either a string + or tuple of strings. The return value specifies options that can be passed + into the Job class, via the "options" argument, as a dict. Options being + passed into this Job can be seen in the PodDistUpgradeWorkflow class above. + Options specified here will be required, and an exception will be raised + if they are missing when called by a Workflow + """ + def required_options(self): + return "target_version" # This is the major Debian version to upgrade to + + """ + Jobs also have an optional "preconditions" method that returns either True + or False. The purpose of this method is to determine whether or not a Job + should be run on a given host. Ansible facts and vars for the hosts a Job + will work on are passed into this method so they can be considered. Notice + the usage of facts, inventory_vars, and options here + + In this example the expression says "If the linux distribution is Debian and + the Debian version is less than the target_version, and there is no + do_not_upgrade variable set by the inventory, then return True, else + return False" + + Note that preconditions can exist for a Job without any tasks. This may be + useful for cases where a Workflow should have additional preconditions + + Preconditions are ignored if a workflow has started on a host but never + completed + """ + def preconditions(self, facts: dict, inventory_vars: dict): + return (facts["ansible_distribution"] == "Debian") + and (int(facts["ansible_distribution_major_version"]) < int(self.options["target_version"])) + and not "do_not_upgrade" in inventory_vars + + """ + This example of the tasks method shows a way of passing an option from the + Job class into the underlying Ansible tasks by using set_fact + """ + def tasks(self): + return [ + {"set_fact": {"target_debian_version": self.options["target_version"]}}, + {"import_tasks": path("dist_upgrade/main.yml")}, + ] + +# Defines a Job with the name "PodPostTasksJob", which is called in the +# PodDistUpgradeWorkflow Workflow as the last Job +class PodPostTasksJob(Job): + def tasks(self): + return [ + {"import_tasks": path("dist_upgrade/pod_post_tasks.yml")} + ] +``` + +### Command-line Interface + +Once you have a `Boardwalkfile.py`, `boardwalk` can be used to run workflows. The +example command-line usage below is using the `Boardwalkfile.py` example above. +Not all subcommands and options are listed here. Explore `boardwalk --help` for +more. + +```sh +# In the same directory as the Boardwalkfile.py... +# List workspaces to see what's available: +boardwalk workspace list + +# Select the available workspace: +boardwalk workspace use StagingPodDistUpgrades + +# Initialize the local state. This will run Ansible fact gathering against +# all of the hosts matching the host_pattern configured for the workspace: +boardwalk init + +# Check that the workflow is working as expected. This checks Job preconditions +# and runs the Ansible tasks defined in Jobs in --check mode: +boardwalk check + +# If everything looks good, run the workflow: +boardwalk run + +# In another terminal (in the same directory) you can catch execution _locally_ +# if needed. This will pause the execution of the workflow before starting the +# next host: +boardwalk catch + +# ... and this will resume the workflow if it has been _locally_ caught: +boardwalk release + +# boardwalk also has command-line help: +boardwalk --help + +# boardwalk's subcommands also have help. For example: +boardwalk workspace --help +``` + +### Environment Variables + +- `BOARDWALK_WORKSPACE`: When set, Boardwalk will use the value as the Workspace + in use. This allows the tool to run against multiple Workspaces at the same + time from the same working directory. + +### Ansible Configuration & Options + +Ansible, when run inside Boardwalk, will use almost the exact same configuration +as any Ansible commands will use outside of Boardwalk. The only differences are: + +- Boardwalk will force using its own fact cache +- `boardwalk init` will timeout hosts after 5 minutes + +Boardwalk doesn't support all the command-line flags for `ansible-playbook`, +such as the `-i` option and others. Ansible options must be configured via [an +ansible.cfg](https://docs.ansible.com/ansible/latest/reference_appendices/config.html) +and/or [environment +variables](https://docs.ansible.com/ansible/latest/reference_appendices/config.html). + +#### Ansible Fact Cache + +Boardwalk maintains a fact cache per-workspace. The fact cache is populated for +all hosts in a workspace during `boardwalk init`, and for each individual host +as a Workflow runs. If `boardwalk init` hasn't been run in a long time, it may +have stale data about hosts in the workspace. + +#### Ansible Performance + +Some optimal configuration settings are recommended. Ansible settings below are +shown as environment variables here, but they may also be provided in an +`ansible.cfg` or via other usual means: + +Boardwalk frequently connects and disconnects from hosts during normal +operation. In order to reduce the overhead of this behavior, it's a good idea to +enable persistent connections, which temporarily keeps a control socket open to +remote hosts: + +```sh +export ANSIBLE_USE_PERSISTENT_CONNECTIONS=True +export ANSIBLE_PERSISTENT_CONNECT_TIMEOUT=60 # Or higher... +``` + +`boardwalk init`, connects to all hosts matching the active Workspace's host +pattern to gather facts. Speeding up `init` requires the same kind of +optimizations that would normally be expected for running Ansible playbooks +against large numbers of hosts: `ANSIBLE_FORKS` should be set to as high a value +as the controller can handle. Maximizing `ANSIBLE_FORKS` depends upon the +controller's CPU, memory, and open files limits. + +### Automatic Vars + +Boardwalk injects a boolean variable into Ansible operations, +`boardwalk_operation`. This variable is always `True` when Ansible is being run +by Boardwalk. + +### `boardwalkd` Server + +Boardwalk is bundled along with `boardwalkd`, which is a central network server +that Boardwalk can coordinate with. By specifying `boardwalkd_url` in the +`Boardwalkfile.py`, `boardwalk` instances become referred to as "workers". +Boardwalk will use the server for several purposes: It enables Boardwalk to: +- Ensure Workspaces are only running in one place, by "locking" Workspaces on + the server. +- Allows operators to catch/release Workflows from a UI. +- Displays Workspaces in a UI with status updates and worker details. +- Allows posting status updates to a Slack webhook. + +See `boardwalkd serve --help` for options to run the server. + +#### Catch & Release Behavior With Boardwalkd + +When Boardwalk encounters an error on a host, it will automatically catch the +workspace. When a `boardwalkd` server is configured, automatic catching of +Workspaces will occur on the remote server rather than locally. It's still +possible to perform a local catch/release even when connected to `boardwalkd`, +and local catches cannot be released from the server UI. If Boardwalk encounters +an error and for any reason it cannot catch the Workspace on the server, it will +fall back to catching locally. diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..be14282 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.5.3 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..8a5e5ca --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,13 @@ +[build-system] +build-backend = "setuptools.build_meta" +requires = ["setuptools>=62"] + +[tool.pyright] +exclude = [ + "build/", + "typings/", +] +pythonPlatform = "All" +reportImportCycles = false +reportMissingTypeStubs = false +typeCheckingMode = "strict" diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..c013078 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,35 @@ +[metadata] +name = boardwalk +version = file: VERSION +author = Mat Hornbeek (Backblaze, Inc.) +author_email = 84995001+m4wh6k@users.noreply.github.com +description = Boardwalk is a linear Ansible workflow engine. +long_description = file: README.md +long_description_content_type = text/markdown +url = https://github.com/Backblaze/boardwalk +project_urls = + Bug Tracker = https://github.com/Backblaze/boardwalk/issues +classifiers = + Programming Language :: Python :: 3 + Operating System :: OS Independent + +[options] +python_requires = >=3.9 +install_requires = + ansible + ansible-runner + click + pydantic + tornado + +[options.packages.find] +where = src + +[options.entry_points] +console_scripts = + boardwalk = boardwalk.cli:cli + boardwalkd = boardwalkd.cli:cli + +[options.package_data] +boardwalk = py.typed +boardwalkd = static/*.css, static/*.js, templates/*.html, py.typed diff --git a/src/boardwalk/README.md b/src/boardwalk/README.md new file mode 100644 index 0000000..bc7e862 --- /dev/null +++ b/src/boardwalk/README.md @@ -0,0 +1,3 @@ +# boardwalk Python Module +This module contains the boardwalk worker code, along with its CLI interface and +the python API used in Boardwalkfile.py manifests. diff --git a/src/boardwalk/__init__.py b/src/boardwalk/__init__.py new file mode 100644 index 0000000..636410d --- /dev/null +++ b/src/boardwalk/__init__.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from .manifest import ( + Job as Job, + path as path, + Workflow as Workflow, + Workspace as Workspace, + WorkspaceConfig as WorkspaceConfig, +) +from .state import RemoteStateModel as RemoteStateModel + + +if TYPE_CHECKING: + from boardwalk.ansible import ( + AnsibleTasksType as AnsibleTasksType, + InventoryHostVars as InventoryHostVars, + ) diff --git a/src/boardwalk/__main__.py b/src/boardwalk/__main__.py new file mode 100644 index 0000000..b4c849b --- /dev/null +++ b/src/boardwalk/__main__.py @@ -0,0 +1,4 @@ +from boardwalk.cli import cli + +if __name__ == "__main__": + cli() diff --git a/src/boardwalk/ansible.py b/src/boardwalk/ansible.py new file mode 100644 index 0000000..95ba3fb --- /dev/null +++ b/src/boardwalk/ansible.py @@ -0,0 +1,231 @@ +""" +This file has utilities for working with Ansible +""" +from __future__ import annotations + +import json +import sys +from functools import partial +from pathlib import Path +from typing import TYPE_CHECKING + +import ansible_runner +import click +from click import ClickException + +import boardwalk + +if TYPE_CHECKING: + from typing import Any, Optional, TypedDict + + from ansible_runner import Runner + + from boardwalk.manifest import Workspace + + class InventoryData(TypedDict): + _meta: InventoryMetaData + + class InventoryMetaData(TypedDict): + hostvars: HostVarsType + + AnsibleFacts = dict[str, Any] + InventoryHostVars = dict[str, Any] + HostVarsType = dict[str, InventoryHostVars] + AnsibleTaskType = dict[str, str | int | bool | list[str] | "AnsibleTaskType"] + AnsibleTasksType = list[AnsibleTaskType] + + class RunnerKwargs(TypedDict, total=False): + cancel_callback: partial[bool] + cmdline: Optional[str] + envvars: RunnerKwargsEnvvars + fact_cache_type: str + limit: Optional[str] + passwords: dict[str, str | None] + project_dir: str + quiet: bool + suppress_env_files: bool + playbook: RunnerPlaybook + + class RunnerKwargsEnvvars(TypedDict, total=False): + ANSIBLE_BECOME_ASK_PASS: str + ANSIBLE_CACHE_PLUGIN_CONNECTION: str + ANSIBLE_CACHE_PLUGIN: str + ANSIBLE_NOCOLOR: str + ANSIBLE_TASK_TIMEOUT: str + + class RunnerPlaybook(TypedDict, total=False): + become: bool + gather_facts: bool + hosts: str + tasks: AnsibleTasksType + vars: dict[str, str | bool] + + +def ansible_runner_cancel_callback(ws: Workspace): + """ + ansible_runner needs a callback to tell it to stop execution. It returns + True to stop execution. We check for the workspace.mutex because that file + is always supposed to be torn down on exit + """ + if ws.path.joinpath("workspace.mutex").exists(): + return False + else: + return True + + +def ansible_runner_errors_to_output(runner: Runner, include_msg: bool = True) -> str: + """Collects error messages from a Runner into a multiline string""" + output: list[str] = [] + for event in runner.events: + if ( + event["event"] == "runner_on_failed" + or event["event"] == "runner_item_on_failed" + or event["event"] == "runner_on_async_failed" + or event["event"] == "runner_on_unreachable" + ) and not ( + "ignore_errors" in event["event_data"] + and event["event_data"]["ignore_errors"] + ): + msg: list[str] = [ + event["event"], + event["event_data"]["task"], + event["event_data"]["task_action"], + event["event_data"]["host"], + ] + if include_msg and not ( + "_ansible_no_log" in event["event_data"]["res"] + and event["event_data"]["res"]["_ansible_no_log"] + ): + try: + msg.append(event["event_data"]["res"]["msg"]) + except KeyError: + click.echo("Warning: Event error did not contain msg") + try: + msg.append(event["stdout"]) + except KeyError: + click.echo("Warning: Event error did not contain stdout") + output.append(": ".join(msg)) + return "\n".join(output) + + +def ansible_runner_run_tasks( + hosts: str, + invocation_msg: str, + tasks: AnsibleTasksType, + become: bool = False, + become_password: str | None = None, + check: bool = False, + gather_facts: bool = True, + limit: str | None = None, + quiet: bool = True, + timeout: int | None = None, +) -> ansible_runner.Runner: + """ + Wraps ansible_runner.run to run Ansible tasks with some defaults for + Boardwalk + """ + workspace = boardwalk.manifest.get_ws() + + runner_kwargs: RunnerKwargs = { + "cancel_callback": partial(ansible_runner_cancel_callback, workspace), + "envvars": { + "ANSIBLE_BECOME_ASK_PASS": "False" if become_password is None else "True", + "ANSIBLE_CACHE_PLUGIN_CONNECTION": str( + workspace.path.joinpath("fact_cache") + ), + "ANSIBLE_CACHE_PLUGIN": "community.general.pickle", + "ANSIBLE_NOCOLOR": "True", + }, + "fact_cache_type": "community.general.pickle", + "passwords": {r"^BECOME password:\s*$": become_password}, + "project_dir": str(Path.cwd()), + "quiet": quiet, + "suppress_env_files": True, + } + if check: + runner_kwargs["cmdline"] = "--check" + if limit: + runner_kwargs["limit"] = limit + if timeout: + runner_kwargs["envvars"]["ANSIBLE_TASK_TIMEOUT"] = str(timeout) + + runner_kwargs["playbook"] = { + "hosts": hosts, + "gather_facts": gather_facts, + "become": become, + "tasks": tasks, + "vars": {"boardwalk_operation": True}, + } + + output_msg_prefix = f"{hosts}: ansible_runner invocation" + if limit: + output_msg_prefix = f"{hosts}(limit: {limit}): ansible_runner invocation" + output_msg = f"{output_msg_prefix}: {invocation_msg}" + click.echo(output_msg) + runner: Runner = ansible_runner.run(**runner_kwargs) # type: ignore + runner_errors = ansible_runner_errors_to_output(runner) + fail_msg = f"Error:\n{output_msg}\n{runner_errors}" + if runner.rc != 0: + if runner.rc == 1: + raise AnsibleRunError(fail_msg, output_msg, runner) + elif runner.rc == 2: + raise AnsibleRunnerFailedHost(fail_msg, output_msg, runner) + elif runner.rc == 4: + raise AnsibleRunnerUnreachableHost(fail_msg, output_msg, runner) + # Catch any other errors here + else: + raise AnsibleRunnerGeneralError(fail_msg, output_msg, runner) + else: + return runner + + +def ansible_inventory() -> InventoryData: + """Uses ansible-inventory to fetch the inventory and returns it as a dict""" + click.echo("Processing ansible-inventory") + """ + Note that for the moment we have --export set here. --export won't expand Jinja + expressions and this is done for performance reasons. It's entirely possible + that there will be cases where a user does want Jinja expressions to be fully + processed + """ + out, err, rc = ansible_runner.run_command( + envvars={"ANSIBLE_VERBOSITY": 0}, + executable_cmd="ansible-inventory", + cmdline_args=["--list", "--export"], + input_fd=sys.stdin, + project_dir=str(Path.cwd()), + quiet=True, + suppress_env_files=True, + ) + if rc != 0: + ClickException(f"Failed to render inventory. {err}") + + return json.loads(out) + + +class AnsibleRunnerBaseException(Exception): + """ + Base class for throwing ansible_runner exceptions. Allows the Runner to be + passed in + """ + + def __init__(self, message: str, runner_msg: str, runner: Runner): + super().__init__(message) + self.runner_msg = runner_msg + self.runner = runner + + +class AnsibleRunnerGeneralError(AnsibleRunnerBaseException): + """There was a non-specific ansible_runner failure""" + + +class AnsibleRunError(AnsibleRunnerBaseException): + """There was an error running a playbook""" + + +class AnsibleRunnerUnreachableHost(AnsibleRunnerBaseException): + """A host was unreachable to ansible_runner""" + + +class AnsibleRunnerFailedHost(AnsibleRunnerBaseException): + """A host failed while running ansible_runner""" diff --git a/src/boardwalk/cli.py b/src/boardwalk/cli.py new file mode 100755 index 0000000..fa14700 --- /dev/null +++ b/src/boardwalk/cli.py @@ -0,0 +1,87 @@ +""" +This is the main file for handling the CLI +""" +from __future__ import annotations + +import signal +from importlib.metadata import version as lib_version +from typing import TYPE_CHECKING + +import click +from click import ClickException + +from boardwalk.cli_catch import catch, release +from boardwalk.cli_init import init +from boardwalk.cli_run import check, run +from boardwalk.cli_workspace import workspace +from boardwalk.manifest import ( + get_ws, + ManifestNotFound, + NoActiveWorkspace, + WorkspaceNotFound, +) + +if TYPE_CHECKING: + from typing import Any + +terminating = False + + +def handle_signal(sig: int, frame: Any): + """ + Handles process exit signals so the CLI has a better chance of cleaning up + after itself. Boardwalk needs to perform certain cleanup actions in most + cases, and this function helps resist unclean exits. It cannot handle + signals sent to child processes (ansible-playbook) + """ + global terminating + click.echo(f"Received signal {sig}") + if not terminating: + terminating = True + raise KeyboardInterrupt + else: + click.echo("Boardwalk is already terminating") + + +@click.group() +@click.pass_context +def cli(ctx: click.Context): + """ + Boardwalk is a linear remote execution workflow engine built on top of Ansible. + See the README.md @ https://github.com/Backblaze/boardwalk for more info + + To see more info about any subcommand, do `boardwalk --help` + """ + # There's not much we can do without a Boardwalkfile.py. Print help and + # exit if it's missing + signal.signal(signal.SIGINT, handle_signal) + signal.signal(signal.SIGHUP, handle_signal) + signal.signal(signal.SIGTERM, handle_signal) + try: + get_ws() + except ManifestNotFound: + # The version subcommand is the only one that doesn't need a Boardwalkfile.py + if ctx.invoked_subcommand == "version": + return + click.echo(cli.get_help(ctx)) + raise ClickException("No Boardwalkfile.py found") + except NoActiveWorkspace: + return + except WorkspaceNotFound: + return + + +@cli.command( + "version", +) +def version(): + """Prints the boardwalk module version number and exits""" + click.echo(lib_version("boardwalk")) + + +cli.add_command(catch) +cli.add_command(check) +cli.add_command(init) +cli.add_command(release) +cli.add_command(run) +cli.add_command(workspace) diff --git a/src/boardwalk/cli_catch.py b/src/boardwalk/cli_catch.py new file mode 100644 index 0000000..59ca57f --- /dev/null +++ b/src/boardwalk/cli_catch.py @@ -0,0 +1,35 @@ +""" +catch and release CLI subcommands +""" +import click +from click import ClickException + +from boardwalk.manifest import get_ws, NoActiveWorkspace + + +@click.command( + "catch", + short_help="Catch workflow in active workspace", +) +def catch(): + """Creates 'catch' in the active workspace. Workflows will stop at the next host. Catch remains in place until released""" + try: + ws = get_ws() + except NoActiveWorkspace as e: + raise ClickException(e.message) + click.echo(f"Using workspace: {ws.name}") + ws.catch() + + +@click.command( + "release", + short_help="Removes catch from active workspace", +) +def release(): + """Removes catch from active workspace. Any running workflow will resume if it was caught""" + try: + ws = get_ws() + except NoActiveWorkspace as e: + raise ClickException(e.message) + click.echo(f"Using workspace: {ws.name}") + ws.release() diff --git a/src/boardwalk/cli_init.py b/src/boardwalk/cli_init.py new file mode 100644 index 0000000..9af3ae3 --- /dev/null +++ b/src/boardwalk/cli_init.py @@ -0,0 +1,151 @@ +""" +init CLI subcommand +""" +from __future__ import annotations + +from pathlib import Path +from typing import TYPE_CHECKING, TypedDict + +import click +from click import ClickException + +from boardwalk.ansible import ( + ansible_runner_run_tasks, + AnsibleRunnerFailedHost, + AnsibleRunnerGeneralError, + AnsibleRunnerUnreachableHost, +) +from boardwalk.host import Host +from boardwalk.manifest import get_ws, NoActiveWorkspace, Workspace + +if TYPE_CHECKING: + from ansible_runner import RunnerEvent + + from boardwalk.ansible import AnsibleTasksType + + class runnerKwargs(TypedDict, total=False): + gather_facts: bool + hosts: str + hosts: str + invocation_msg: str + limit: str + tasks: AnsibleTasksType + timeout: int + + +@click.command(short_help="Inits local workspace state by getting host facts") +@click.option( + "--retry/--no-retry", + "-r/-nr", + default=False, + help="Retry getting state for hosts that were unreachable/failed on the last attempt", + show_default=True, +) +@click.pass_context +def init(ctx: click.Context, retry: bool): + """ + Inits the workspace state with host data. Gathers Ansible facts for hosts + matching the workspaces host pattern. OK to run multiple times; hosts are + only added or updated, never removed by this operation. Use + `boardwalk workspace reset` to clear existing state if needed + """ + try: + ws = get_ws() + except NoActiveWorkspace as e: + raise ClickException(e.message) + click.echo(f"Using workspace: {ws.name}") + + ws.assert_host_pattern_unchanged() + + ws.mutex() + ctx.call_on_close(ws.unmutex) + + retry_file_path = ws.path.joinpath("init.retry") + + # Set-up Ansible args + runner_kwargs: runnerKwargs = { + "gather_facts": False, + "hosts": ws.cfg.host_pattern, + "invocation_msg": "Gathering facts", + "tasks": [{"name": "setup", "ansible.builtin.setup": {"gather_timeout": 30}}], + "timeout": 300, + } + if retry: + if not retry_file_path.exists(): + raise ClickException("No retry file exists") + runner_kwargs["limit"] = "@" + str(retry_file_path) + + # Save the host pattern we are initializing with. If the pattern changes after + # this point, other operations will need the state reset and init to be re-done + ws.state.host_pattern = ws.cfg.host_pattern + + # Run Ansible. + hosts_were_unreachable = False + try: + runner = ansible_runner_run_tasks(**runner_kwargs) + except ( + AnsibleRunnerFailedHost, + AnsibleRunnerGeneralError, + AnsibleRunnerUnreachableHost, + ) as e: + # Unreachable and failed hosts are not a hard failure + # We note to the user later on if hosts were unreachable + hosts_were_unreachable = True + runner = e.runner + + # Clear the retry file after we use it to start fresh before we build a new one + retry_file_path.unlink(missing_ok=True) + + # Process Ansible output + # Collects facts into state + for event in runner.events: + add_gathered_facts_to_state(event, ws) + handle_failed_init_hosts(event, retry_file_path) + ws.flush() + + # Write out stats + for event in runner.events: + if event["event"] == "playbook_on_stats": + click.echo(event["stdout"]) + + # Note if any hosts were unreachable + if hosts_were_unreachable: + click.echo("Some hosts were unreachable. Consider running again with --retry") + + # If we didn't find any hosts, raise an exception + if len(ws.state.hosts) == 0: + raise ClickException("No hosts gathered") + + +def add_gathered_facts_to_state(event: RunnerEvent, ws: Workspace): + """ + Adds or updates gathered host facts in the workspace state + """ + if event["event"] == "runner_on_ok" and event["event_data"]["task"] == "setup": + # If the host is already in the statefile, just update record + if event["event_data"]["host"] in ws.state.hosts: + ws.state.hosts[event["event_data"]["host"]].ansible_facts = event[ + "event_data" + ]["res"]["ansible_facts"] + # Otherwise, add the host to the state as a new host + else: + ws.state.hosts[event["event_data"]["host"]] = Host( + ansible_facts=event["event_data"]["res"]["ansible_facts"], + name=event["event_data"]["host"], + ) + + +def handle_failed_init_hosts(event: RunnerEvent, retry_file_path: Path): + """Processes runner events to find failed hosts during init. Saves any failed + or unreachable hosts to a retry file and writes warnings and errors to stdout""" + # Save any unreachable/failed hosts to the retry file + if ( + event["event"] == "runner_on_unreachable" + or event["event"] == "runner_on_failed" + ): + click.echo(event["stdout"]) + with open(retry_file_path, "a") as file: + file.write(f"{event['event_data']['host']}\n") + # If no hosts matched or there are warnings, write them out + if event["event"] == "warning" or event["event"] == "playbook_on_no_hosts_matched": + click.echo(event["stdout"]) diff --git a/src/boardwalk/cli_run.py b/src/boardwalk/cli_run.py new file mode 100644 index 0000000..ff0c542 --- /dev/null +++ b/src/boardwalk/cli_run.py @@ -0,0 +1,792 @@ +""" +run and check CLI subcommands +""" +from __future__ import annotations + +import concurrent.futures +import getpass +import os +import random +import socket +import sys +import time +from distutils.util import strtobool +from itertools import chain +from pathlib import Path +from typing import TYPE_CHECKING + +import ansible_runner +import click +from boardwalkd.protocol import ( + WorkspaceClient, + WorkspaceDetails, + WorkspaceEvent, + WorkspaceHasMutex, +) +from click import ClickException +from tornado.simple_httpclient import HTTPTimeoutError + +from boardwalk.ansible import ( + ansible_inventory, + ansible_runner_errors_to_output, + AnsibleRunError, + AnsibleRunnerBaseException, + AnsibleRunnerFailedHost, + AnsibleRunnerGeneralError, + AnsibleRunnerUnreachableHost, +) +from boardwalk.host import Host, RemoteHostLocked +from boardwalk.manifest import get_boardwalkd_url, get_ws, NoActiveWorkspace, Workspace +from boardwalk.state import RemoteStateModel, RemoteStateWorkflow, RemoteStateWorkspace + +if TYPE_CHECKING: + from typing import ItemsView + + from boardwalk.ansible import HostVarsType, InventoryHostVars + + +become_password: str | None = None +boardwalkd_client: WorkspaceClient | None = None +boardwalkd_send_broadcasts: bool = False +_check_mode: bool = True +_stomp_locks: bool = False + + +@click.command("run", short_help="Runs workflow jobs") +@click.option( + "--ask-become-pass/--no-ask-become-pass", + "-K/-nK", + help="Whether or not ask for a become password. The ANSIBLE_BECOME_ASK_PASS env var can also set this", + default=False, + show_default=True, +) +@click.option( + "--check/--no-check", + "-C/-nC", + help="Whether or not to run Ansible in --check/-C mode", + default=False, + show_default=True, +) +@click.option( + "--limit", + "-l", + help="An Ansible pattern to limit hosts by. Defaults to no limit", + default="all", +) +@click.option( + "--server-connect/--no-server-connect", + "-sc/-nsc", + help=( + "Whether or not connect to the configured boardwalkd server, if any." + " It may be dangerous to run workflows without connecting to the server" + ), + default=True, + show_default=True, +) +@click.option( + "--sort-hosts", + "-s", + help="Overrides the workspace's default sort ordering. May be specified with first letter", + type=click.Choice( + ["shuffle", "s", "ascending", "a", "descending", "d", ""], case_sensitive=False + ), + default="", +) +@click.option( + "--stomp-locks/--no-stomp-locks", + help="Whether or not to ignore and override existing host locks. Probably dangerous", + default=False, + show_default=True, +) +@click.pass_context +def run( + ctx: click.Context, + ask_become_pass: bool, + check: bool, + limit: str, + server_connect: bool, + sort_hosts: str, + stomp_locks: bool, +): + """ + Runs workflow jobs defined in the Boardwalkfile.py + """ + # Set globals from CLI options + global _check_mode + _check_mode = check + global _stomp_locks + _stomp_locks = stomp_locks + + try: + ws = get_ws() + except NoActiveWorkspace as e: + raise ClickException(e.message) + click.echo(f"Using workspace: {ws.name}") + + # See if we have any hosts + if len(ws.state.hosts) == 0: + raise ClickException("No hosts found in state. Have you run `boardwalk init`?") + + ws.assert_host_pattern_unchanged() + + # Setup boardwalkd client if configured + boardwalkd_url = get_boardwalkd_url() + if boardwalkd_url and server_connect: + global boardwalkd_client + boardwalkd_client = WorkspaceClient(boardwalkd_url, ws.name) + + if boardwalkd_client and not _check_mode: + global boardwalkd_send_broadcasts + boardwalkd_send_broadcasts = True + + if boardwalkd_client: + bootstrap_with_server(ws, ctx) + + # Lock the local Workspace + ws.mutex() + ctx.call_on_close(ws.unmutex) + + # Multiplex slow inventory operations + with concurrent.futures.ThreadPoolExecutor() as executor: + # Process --limit + filter_hosts_by_limit_future = executor.submit( + filter_hosts_by_limit, ws, ws.state.hosts.items(), limit + ) + # Get data for inventory_vars used in Jobs + inventory_data_future = executor.submit(ansible_inventory) + try: + hosts_working_list = filter_hosts_by_limit_future.result() + except NoHostsMatched: + raise ClickException( + ( + "No host matched the given limit pattern. Ensure the host exists" + " in the Ansible inventory and was reachable during `boardwalk init`" + ) + ) + inventory_vars = inventory_data_future.result()["_meta"]["hostvars"] + + # Sort hosts + # If no --sort-hosts override was passed, then use the workspace default + if not sort_hosts: + sort_hosts = ws.cfg.default_sort_order + hosts_working_list = sort_host_list(hosts_working_list, sort_hosts) + + # Check preconditions locally + hosts_working_list = check_host_preconditions_locally( + hosts_working_list, inventory_vars, ws + ) + if len(hosts_working_list) < 1: + click.echo("No hosts meet preconditions") + return + + # Get the become password if necessary + try: + if ask_become_pass or strtobool(os.environ["ANSIBLE_BECOME_ASK_PASS"]): + global become_password + become_password = getpass.getpass("BECOME password: ") + except ValueError: + raise ClickException( + "ANSIBLE_BECOME_ASK_PASS env variable has an invalid boolean value" + ) + except KeyError: + pass + + run_workflow( + hosts=hosts_working_list, + inventory_vars=inventory_vars, + workspace=ws, + ) + + +@click.option( + "--ask-become-pass/--no-ask-become-pass", + "-K/-nK", + help="Whether or not ask for a become password. The ANSIBLE_BECOME_ASK_PASS env var can also set this", + default=False, +) +@click.option( + "--limit", + "-l", + help="An Ansible pattern to limit hosts by. Defaults to no limit", + default="all", +) +@click.option( + "--server-connect/--no-server-connect", + "-sc/-nsc", + help=( + "Whether or not connect to the configured boardwalkd server, if any." + " It may be dangerous to run workflows without connecting to the server" + ), + default=True, + show_default=True, +) +@click.option( + "--sort-hosts", + "-s", + help="Overrides the workspace's default sort ordering. May be specified with first letter", + type=click.Choice( + ["shuffle", "s", "ascending", "a", "descending", "d", ""], case_sensitive=False + ), + default="", +) +@click.command( + "check", short_help="Runs workflow in check mode. Equivalent to run --check" +) +@click.pass_context +def check( + ctx: click.Context, + ask_become_pass: bool, + limit: str, + server_connect: bool, + sort_hosts: str, +): + """Runs workflow in check mode. Equivalent to run --check""" + ctx.invoke( + run, + ask_become_pass=ask_become_pass, + limit=limit, + server_connect=server_connect, + sort_hosts=sort_hosts, + check=True, + ) + + +def run_workflow( + hosts: list[Host], + inventory_vars: HostVarsType, + workspace: Workspace, +): + """Runs the workspace's workflow against a list of hosts""" + i = 0 + while i < len(hosts): + host = hosts[i] + + click.echo(f"{host.name}: Workflow iteration on host {i+1} of {len(hosts)}") + if boardwalkd_client: + boardwalkd_client.queue_event( + WorkspaceEvent( + severity="info", + message=f"{host.name}: Workflow iteration on host {i+1} of {len(hosts)}", + ), + ) + + handle_workflow_catch(workspace=workspace, hostname=host.name) + + # Connect to the remote host + # Wrap everything in try/except so we can handle failures + try: + lock_remote_host(host) + # Wrap everything in a try/finally so we always try to unlock the + # remote host + unreachable_exception = None + try: + directly_confirm_host_preconditions( + host, inventory_vars[host.name], workspace + ) + execute_host_workflow(host, workspace) + except AnsibleRunnerUnreachableHost as e: + unreachable_exception = e + finally: + # If the host was unreachable there's no point in trying to recover here + if unreachable_exception: + raise unreachable_exception + # Finish by releasing the remote lock + click.echo(f"{host.name}: Release remote host lock") + if boardwalkd_client: + boardwalkd_client.queue_event( + WorkspaceEvent( + severity="info", + message=f"{host.name}: Release remote host lock", + ), + ) + host.release(become_password=become_password, check=_check_mode) + except (AnsibleRunnerGeneralError, AnsibleRunError) as e: + # These errors probably indicate a local issue with Ansible that should + # caught early, such as syntax errors, so we always bail when encountered + if boardwalkd_client: + boardwalkd_client.queue_event( + WorkspaceEvent( + severity="error", + message=f"{host.name}: {e.__class__.__qualname__}", + ) + ) + raise ClickException(e.runner_msg) + except ( + AnsibleRunnerFailedHost, + AnsibleRunnerUnreachableHost, + RemoteHostLocked, + ) as e: + run_failure_mode_handler( + exception=e, + hostname=host.name, + workspace=workspace, + ) + continue + except HostPreConditionsUnmet: + continue + + i += 1 + + +def run_failure_mode_handler( + exception: Exception, + hostname: str, + workspace: Workspace, +): + """ + Common failure handler that used in catching exceptions during workflow runs + """ + if boardwalkd_client: + if isinstance(exception, AnsibleRunnerBaseException): + runner_errors = ansible_runner_errors_to_output( + runner=exception.runner, include_msg=False + ) + msg = f"{exception.runner_msg}: {runner_errors}" + else: + try: + msg: str = exception.message # type: ignore + except AttributeError: + msg = exception.__class__.__qualname__ + boardwalkd_client.queue_event( + WorkspaceEvent( + severity="error", + message=msg, + ), + broadcast=boardwalkd_send_broadcasts, + ) + + click.echo(f"{exception}\n{hostname}: Job encountered error; Workspace will catch") + if boardwalkd_client: + try: + boardwalkd_client.post_catch() + except ConnectionRefusedError: + click.echo( + ( + f"{hostname}: Could not catch Workspace at server because" + " connection was refused. Falling back to local catch" + ) + ) + workspace.catch() + else: + workspace.catch() + + +def filter_hosts_by_limit( + workspace: Workspace, hosts: ItemsView[str, Host], pattern: str +) -> list[Host]: + """Accepts a list of host objects and returns a list of object matching a host + pattern string""" + click.echo("Reading inventory to process any --limit") + out, err, rc = ansible_runner.run_command( + cmdline_args=[ + "--list-hosts", + workspace.cfg.host_pattern, + "--limit", + pattern, + ], + envvars={"ANSIBLE_BECOME_ASK_PASS": False}, + executable_cmd="ansible", + input_fd=sys.stdin, + project_dir=str(Path.cwd()), + quiet=True, + suppress_env_files=True, + ) + if rc != 0: + ClickException(f"Failed to process --limit pattern. {err}") + + # Format the output into a clean list + inventory_host_list = [line.strip() for line in out.splitlines()[1:]] + + # Get the intersection of the list of hosts from the inventory with the + # hosts present in the state + hosts_filtered: list[Host] = [] + for hostname, host in hosts: + if hostname in inventory_host_list: + hosts_filtered.append(host) + if len(hosts_filtered) == 0: + raise NoHostsMatched + return hosts_filtered + + +def sort_host_list(hosts: list[Host], sort_method: str): + """Accepts a list of host objects and return a list of host objects sorted + by the requested sort method. The acceptable sorting methods are/must be + defined and validated by the calling click function""" + if sort_method == "shuffle" or sort_method == "s": + random.shuffle(hosts) + elif sort_method == "ascending" or sort_method == "a": + hosts.sort(key=lambda h: h.name) + elif sort_method == "descending" or sort_method == "d": + hosts.sort(key=lambda h: h.name, reverse=True) + return hosts + + +def check_host_preconditions_locally( + hosts: list[Host], inventory_vars: HostVarsType, workspace: Workspace +) -> list[Host]: + """Checks preconditions of jobs defined in workspace's workflow. Returns + a list of the hosts that meet preconditions. Prints warnings for hosts that + don't pass. Preconditions are ignored for hosts that have started a workflow + but haven't finished""" + hosts_meeting_preconditions: list[Host] = [] + workflow = workspace.cfg.workflow + for host in hosts: + # See if preconditions are met based upon local state + any_job_preconditions_unmet = False + + # If the workflow was started but never finished, ignore preconditions + try: + boardwalk_state = RemoteStateModel.parse_obj( + host.ansible_facts["ansible_local"]["boardwalk_state"] + ) + if ( + boardwalk_state.workspaces[workspace.name].workflow.started + and not boardwalk_state.workspaces[workspace.name].workflow.succeeded + ): + hosts_meeting_preconditions.append(host) + click.echo( + ( + f"{host.name}: Warning: Host started workflow but never completed." + " Job preconditions are ignored for this host" + ) + ) + continue + except KeyError: + pass + + for job in chain(workflow.i_jobs, workflow.i_exit_jobs): + if not job.preconditions( + facts=host.ansible_facts, inventory_vars=inventory_vars[host.name] + ): + any_job_preconditions_unmet = True + click.echo( + ( + f"{host.name}: Job {job.name} preconditions unmet in local state" + " and will be skipped. If this is in error, re-run `boardwalk init`" + ) + ) + # If any Job preconditions were unmet, then skip this host + if any_job_preconditions_unmet: + continue + + hosts_meeting_preconditions.append(host) + return hosts_meeting_preconditions + + +def handle_workflow_catch(workspace: Workspace, hostname: str): + """Handles local and remote workspace catches. Blocks under caught conditions""" + if workspace.caught(): + click.echo( + f"{hostname}: The {workspace.name} workspace is locally caught. Waiting for release before continuing..." + ) + if boardwalkd_client: + boardwalkd_client.queue_event( + WorkspaceEvent( + severity="info", + message=f"{hostname}: Waiting for local worker catch to release", + ), + ) + while workspace.caught(): + time.sleep(5) + + # Now check if there is a remote catch + def check_boardwalkd_catch(client: WorkspaceClient) -> bool: + """ + Wraps catch checking method so that the client can consider the + remote workspace locked if it can't be reached + """ + # First check if there is a local catch. This takes precedence over remote catches + try: + return client.caught() + except (ConnectionRefusedError, HTTPTimeoutError): + click.echo( + ( + f"Could not connect to {client.url.geturl()} while checking for remote catch." + " Boardwalk considers the remote workspace caught if it can't be reached" + ) + ) + return True + + if boardwalkd_client and check_boardwalkd_catch(boardwalkd_client): + click.echo( + ( + f"{hostname}: The {workspace.name} workspace is remotely caught on {boardwalkd_client.url.geturl()}" + " Waiting for release before continuing" + ) + ) + boardwalkd_client.queue_event( + WorkspaceEvent( + severity="info", + message=f"{hostname}: Waiting for remote catch to release", + ) + ) + while check_boardwalkd_catch(boardwalkd_client): + time.sleep(5) + + +def lock_remote_host(host: Host): + if boardwalkd_client: + boardwalkd_client.queue_event( + WorkspaceEvent( + severity="info", + message=f"{host.name}: Locking remote host", + ), + ) + host.lock( + become_password=become_password, + check=_check_mode, + stomp_existing_locks=_stomp_locks, + ) + + +def bootstrap_with_server(workspace: Workspace, ctx: click.Context): + """Performs all of the initial set-up actions needed when boardwalk is + configured to connect to a central boardwalkd""" + if not boardwalkd_client: + raise ClickException( + "bootstrap_with_server called but no boardwalkd_client exists" + ) + boardwalkd_url = boardwalkd_client.url.geturl() + # Check if the if the Workspace is locked. We don't want to conflict with another worker + try: + if boardwalkd_client.has_mutex(): + raise ClickException( + f"A workspace with the name {workspace.name} has already locked on {boardwalkd_url}" + ) + except ConnectionRefusedError: + raise ClickException(f"Could not connect to server {boardwalkd_url}") + except socket.gaierror: + raise ClickException(f"Could not resolve server {boardwalkd_url}") + + # Post the worker's details, which also creates the workspace + try: + boardwalkd_client.post_details( + WorkspaceDetails( + host_pattern=workspace.cfg.host_pattern, + workflow=workspace.cfg.workflow.__class__.__qualname__, + worker_command="check" if _check_mode else "run", + worker_hostname=socket.gethostname(), + worker_username=getpass.getuser(), + ) + ) + except ConnectionRefusedError: + raise ClickException(f"Could not connect to server {boardwalkd_url}") + + # Lock the Workspace at the server + try: + boardwalkd_client.mutex() + except WorkspaceHasMutex: + raise ClickException( + f"A workspace with the name {workspace.name} has already locked on {boardwalkd_url}" + ) + except ConnectionRefusedError: + raise ClickException(f"Could not connect to server {boardwalkd_url}") + + # Create unmutex callback + def unmutex_boardwalkd_workspace(): + """Wraps unmutex to prevent crashing if we can't connect""" + if not boardwalkd_client: + raise ClickException( + "unmutex_boardwalkd_workspace called but no boardwalkd_client exists" + ) + try: + boardwalkd_client.unmutex() + except ConnectionRefusedError: + click.echo( + f"Could not connect to {boardwalkd_url}. Cannot unmutex Workspace" + ) + + ctx.call_on_close(unmutex_boardwalkd_workspace) + + # Send heartbeats in background + heartbeat_coroutine = boardwalkd_client.heartbeat_keepalive_connect() + ctx.call_on_close(heartbeat_coroutine.cancel) + + +def update_host_facts_in_local_state(host: Host, workspace: Workspace): + """Updates fetches latest host facts for a host and saves to the workspace state""" + click.echo(f"{host.name}: Updating Ansible facts in local state") + if boardwalkd_client: + boardwalkd_client.queue_event( + WorkspaceEvent( + severity="info", + message=f"{host.name}: Updating Ansible facts in local state", + ), + ) + workspace.state.hosts[host.name].ansible_facts = host.gather_facts() + workspace.flush() + + +def directly_confirm_host_preconditions( + host: Host, inventory_vars: InventoryHostVars, workspace: Workspace +) -> bool: + """Connects directly to the host and confirms that the workflow job preconditions + are actually met. Raises an exception if any are unmet. If a workflow was run but + never completed, preconditions are ignored. Also posts events to the central server""" + if boardwalkd_client: + boardwalkd_client.queue_event( + WorkspaceEvent( + severity="info", + message=f"{host.name}: Checking Job preconditions on host", + ), + ) + update_host_facts_in_local_state(host, workspace) + + # If the workflow was started but never finished, ignore preconditions + try: + boardwalk_state = RemoteStateModel.parse_obj( + host.ansible_facts["ansible_local"]["boardwalk_state"] + ) + if ( + boardwalk_state.workspaces[workspace.name].workflow.started + and not boardwalk_state.workspaces[workspace.name].workflow.succeeded + ): + click.echo( + ( + f"{host.name}: Warning: Host started workflow but never completed." + " Job preconditions are ignored for this host" + ) + ) + return True + except KeyError: + pass + + all_job_preconditions_met = True + workflow = workspace.cfg.workflow + for job in chain(workflow.i_jobs, workflow.i_exit_jobs): + if not job.preconditions( + facts=host.ansible_facts, inventory_vars=inventory_vars + ): + all_job_preconditions_met = False + click.echo(f"Job {job.name} preconditions unmet on host") + if boardwalkd_client and not all_job_preconditions_met: + boardwalkd_client.queue_event( + WorkspaceEvent( + severity="info", + message=f"{host.name}: Host didn't meet job preconditions", + ), + ) + + if not all_job_preconditions_met: + raise HostPreConditionsUnmet + + return True + + +def execute_workflow_jobs(host: Host, workspace: Workspace, job_kind: str): + """ + Executes workflow jobs. Different kinds of job types are specified + """ + workflow = workspace.cfg.workflow + if job_kind == "main": + jobs = workflow.i_jobs + elif job_kind == "exit": + jobs = workflow.i_exit_jobs + else: + raise Exception + if len(jobs) == 0: + return + click.echo(f"{host.name}: Running workflow {job_kind} jobs") + if boardwalkd_client: + boardwalkd_client.queue_event( + WorkspaceEvent( + severity="info", + message=f"{host.name}: Running workflow {job_kind} jobs", + ), + ) + workflow = workspace.cfg.workflow + for job in jobs: + if boardwalkd_client: + boardwalkd_client.queue_event( + WorkspaceEvent( + severity="info", + message=f"{host.name}: Running {job_kind} job {job.name}", + ), + ) + # Get tasks (which may also run user-supplied python code locally) + tasks = job.tasks() + if len(tasks) > 0: + host.ansible_run( + become_password=become_password, + become=True, + check=_check_mode, + gather_facts=False, + invocation_msg=f"{job_kind}_Job_{job.name}", + quiet=False, + tasks=tasks, + ) + + +def execute_host_workflow(host: Host, workspace: Workspace): + """Handles executing all jobs defined in a workflow against a host""" + unreachable_exception = None + + click.echo(f"{host.name}: Updating remote state") + if boardwalkd_client: + boardwalkd_client.queue_event( + WorkspaceEvent( + severity="info", + message=f"{host.name}: Updating remote state", + ), + ) + remote_state = host.get_remote_state() + try: + remote_state.workspaces[workspace.name].workflow.started = True + except KeyError: + remote_state.workspaces[workspace.name] = RemoteStateWorkspace( + workflow=RemoteStateWorkflow(started=True) + ) + host.set_remote_state(remote_state, become_password, _check_mode) + + if boardwalkd_client: + boardwalkd_client.queue_event( + WorkspaceEvent( + severity="info", + message=f"{host.name}: Starting workflow", + ), + broadcast=boardwalkd_send_broadcasts, + ) + try: + execute_workflow_jobs(host, workspace, job_kind="main") + except AnsibleRunnerUnreachableHost as e: + unreachable_exception = e + finally: + # If the host was unreachable there's no point in trying to recover here + if unreachable_exception: + raise unreachable_exception + execute_workflow_jobs(host, workspace, job_kind="exit") + + click.echo(f"{host.name}: Updating remote state") + if boardwalkd_client: + boardwalkd_client.queue_event( + WorkspaceEvent( + severity="info", + message=f"{host.name}: Updating remote state", + ), + ) + remote_state = host.get_remote_state() + try: + remote_state.workspaces[workspace.name].workflow.succeeded = True + except KeyError: + remote_state.workspaces[workspace.name] = RemoteStateWorkspace( + workflow=RemoteStateWorkflow(started=True, succeeded=True) + ) + host.set_remote_state(remote_state, become_password, _check_mode) + + if boardwalkd_client: + boardwalkd_client.queue_event( + WorkspaceEvent( + severity="success", + message=f"{host.name}: Host completed successfully; wrapping up", + ), + broadcast=boardwalkd_send_broadcasts, + ) + update_host_facts_in_local_state(host, workspace) + + +class NoHostsMatched(Exception): + """No hosts matched regex""" + + +class HostPreConditionsUnmet(Exception): + """The host doesn't meet job preconditions""" diff --git a/src/boardwalk/cli_workspace.py b/src/boardwalk/cli_workspace.py new file mode 100644 index 0000000..8252cf0 --- /dev/null +++ b/src/boardwalk/cli_workspace.py @@ -0,0 +1,70 @@ +""" +workspace CLI subcommand group +""" +import click +from click import ClickException + +from boardwalk.manifest import get_ws, NoActiveWorkspace, Workspace, WorkspaceNotFound + + +@click.group(short_help="Subcommand group for working with workspaces") +def workspace(): + pass + + +@workspace.command("show", help="Displays the active workspace") +def workspace_show(): + """Gets and prints the active workspace""" + try: + ws = get_ws() + except NoActiveWorkspace as e: + raise ClickException(e.message) + click.echo(ws.name) + + +@workspace.command( + "use", + short_help="Sets the active workspace", +) +@click.argument("workspace_name") +def workspace_use(workspace_name: str): + Workspace.use(workspace_name) + ws = get_ws() + click.echo(f"Using workspace: {ws.name}") + + +@workspace.command("list", help="Lists available workspaces from the Boardwalkfile.py") +def workspace_list(): + try: + get_ws() + except NoActiveWorkspace: + pass + except WorkspaceNotFound: + pass + for item in Workspace.__subclasses__(): + click.echo(item.__qualname__) + + +@workspace.command("reset", short_help="Resets active workspace") +@click.confirmation_option( + prompt="Are you sure you want to reset the active workspace?" +) +def workspace_reset(): + """Resets/clears the active workspace state""" + try: + ws = get_ws() + except NoActiveWorkspace as e: + raise ClickException(e.message) + click.echo(f"Using workspace: {ws.name}") + ws.reset() + + +@workspace.command("dump") +def workspace_dump(): + """Prints the active workspace's state to stdout as JSON""" + try: + ws = get_ws() + except NoActiveWorkspace as e: + raise ClickException(e.message) + click.echo(f"Using workspace: {ws.name}", err=True) + click.echo(ws.state.json()) diff --git a/src/boardwalk/host.py b/src/boardwalk/host.py new file mode 100644 index 0000000..d16d8fc --- /dev/null +++ b/src/boardwalk/host.py @@ -0,0 +1,293 @@ +""" +This file contains everything about what host data is stored and what can be +done to hosts +""" +from __future__ import annotations + +import getpass +import socket +from base64 import b64decode +from datetime import datetime +from typing import Any, TYPE_CHECKING + +from click import ClickException +from pydantic import BaseModel, Extra + +import boardwalk +from boardwalk.ansible import ansible_runner_run_tasks + +if TYPE_CHECKING: + from ansible_runner import Runner + + from boardwalk.ansible import AnsibleTasksType + + +class Host(BaseModel, extra=Extra.forbid): + """Data and methods for managing an individual host""" + + ansible_facts: dict[str, Any] + name: str + meta: dict[str, str | int | bool] = {} + remote_mutex_path = "/opt/boardwalk.mutex" + remote_alert_msg = "ALERT: Boardwalk is running a workflow against this host. Services may be interrupted" + remote_alert_string_formatted = f"$(tput -T xterm bold)$(tput -T xterm setaf 1)'{remote_alert_msg}'$(tput -T xterm sgr0)" + remote_alert_motd = f"#!/bin/sh\necho {remote_alert_string_formatted}" + remote_alert_motd_path = "/etc/update-motd.d/99-boardwalk-alert" + remote_alert_wall_cmd = f"wall {remote_alert_string_formatted}" + + def ansible_run( + self, + invocation_msg: str, + tasks: AnsibleTasksType, + become: bool = False, + become_password: str | None = None, + check: bool = False, + gather_facts: bool = True, + quiet: bool = True, + ) -> Runner: + """ + Wraps ansible_runner_run_tasks for performing Ansible tasks against this host + """ + return ansible_runner_run_tasks( + hosts=self.name, + invocation_msg=invocation_msg, + tasks=tasks, + become=become, + become_password=become_password, + check=check, + gather_facts=gather_facts, + quiet=quiet, + ) + + def is_locked(self) -> str | bool: + """ + Checks if a remote host is locked + Returns string with lockfile content if so, False otherwise + """ + tasks: AnsibleTasksType = [ + { + "name": "remote_mutex_check", + "ansible.builtin.stat": {"path": self.remote_mutex_path}, + "register": "lockfile", + }, + { + "name": "slurp_mutex_content", + "ansible.builtin.slurp": {"src": self.remote_mutex_path}, + "when": "lockfile.stat.exists", + }, + ] + + runner = self.ansible_run( + invocation_msg="check_remote_host_lock", + gather_facts=False, + tasks=tasks, + ) + + lockfile_stat_exists = False + slurp_mutex_content = "" + for event in runner.events: + if event["event"] == "runner_on_ok": + if event["event_data"]["task"] == "remote_mutex_check": + lockfile_stat_exists = event["event_data"]["res"]["stat"]["exists"] + if event["event_data"]["task"] == "slurp_mutex_content": + slurp_mutex_content = event["event_data"]["res"]["content"] + if lockfile_stat_exists: + return b64decode(slurp_mutex_content).decode("utf-8").rstrip() + return False + + def lock( + self, + become_password: str | None = None, + check: bool = False, + stomp_existing_locks: bool = False, + ): + """ + Sets a remote lock and writes an alert for any local users + If a lock is already set, an error is raised + """ + + if not stomp_existing_locks and (res := self.is_locked()): + # Check if there is already a lock on the host + raise RemoteHostLocked(f"{self.name}: Host is locked by {res}") + tasks: AnsibleTasksType = [ + {"name": "get_ansible_system", "setup": {"filter": ["ansible_system"]}}, + { + "name": "set_linux_facts", + "ansible.builtin.set_fact": {"admin_group": "root"}, + "when": "ansible_system == 'Linux'", + }, + { + "name": "set_darwin_facts", + "ansible.builtin.set_fact": {"admin_group": "wheel"}, + "when": "ansible_system == 'Darwin'", + }, + { + "name": "create_remote_lock", + "ansible.builtin.copy": { + "content": f"{getpass.getuser()}@{socket.gethostname()} at {datetime.utcnow()}", + "dest": str(self.remote_mutex_path), + "mode": "0644", + "owner": "root", + "group": "{{ admin_group }}", + }, + }, + { + "name": "create_motd_banner", + "ansible.builtin.copy": { + "content": self.remote_alert_motd, + "dest": self.remote_alert_motd_path, + "owner": "root", + "group": "{{ admin_group }}", + "mode": "0755", + }, + "when": "ansible_system == 'Linux'", + }, + { + "name": "write_wall_msg", + "ansible.builtin.shell": {"cmd": self.remote_alert_wall_cmd}, + "when": "ansible_system == 'Linux'", + }, + ] + self.ansible_run( + become=True, + become_password=become_password, + check=check, + gather_facts=False, + invocation_msg="lock_remote_host", + tasks=tasks, + ) + + def release(self, become_password: str | None = None, check: bool = False) -> None: + """Undoes the lock method""" + tasks: AnsibleTasksType = [ + { + "name": "release_remote_lock", + "ansible.builtin.file": { + "path": self.remote_mutex_path, + "state": "absent", + }, + }, + { + "name": "delete_motd_banner", + "ansible.builtin.file": { + "path": self.remote_alert_motd_path, + "state": "absent", + }, + }, + ] + self.ansible_run( + become=True, + become_password=become_password, + check=check, + gather_facts=False, + invocation_msg="release_remote_host", + tasks=tasks, + ) + + def gather_facts(self) -> dict[str, Any]: + """Returns the output of Ansible's setup module""" + tasks: AnsibleTasksType = [ + {"name": "setup", "ansible.builtin.setup": {"gather_timeout": 30}} + ] + runner = self.ansible_run( + invocation_msg="gather_facts", + gather_facts=False, + tasks=tasks, + ) + facts: dict[str, Any] = {"ansible_local": {}} + for event in runner.events: + if ( + event["event"] == "runner_on_ok" + and event["event_data"]["task"] == "setup" + ): + facts = event["event_data"]["res"]["ansible_facts"] + if len(facts) > 0: + return facts + else: + raise ClickException("gather_facts returned nothing") + + def get_remote_state(self) -> boardwalk.state.RemoteStateModel: + """Gets boardwalk's remote state fact as an object""" + + # Get existing ansible_local facts, if any + tasks: AnsibleTasksType = [ + {"name": "setup", "ansible.builtin.setup": {"filter": ["ansible_local"]}} + ] + runner = self.ansible_run( + invocation_msg="get_remote_state", + gather_facts=False, + tasks=tasks, + ) + + # Get existing boardwalk_state fact, if any + for event in runner.events: + if ( + event["event"] == "runner_on_ok" + and event["event_data"]["task"] == "setup" + ): + try: + return boardwalk.RemoteStateModel.parse_obj( + event["event_data"]["res"]["ansible_facts"]["ansible_local"][ + "boardwalk_state" + ] + ) + except KeyError: + pass + + return boardwalk.RemoteStateModel() + + def set_remote_state( + self, + remote_state_obj: boardwalk.state.RemoteStateModel, + become_password: str | None = None, + check: bool = False, + ): + """Sets the remote state fact from an object in the remote and local state""" + workspace = boardwalk.manifest.get_ws() + tasks: AnsibleTasksType = [ + {"name": "get_ansible_system", "setup": {"filter": ["ansible_system"]}}, + { + "name": "set_linux_facts", + "ansible.builtin.set_fact": {"admin_group": "root"}, + "when": "ansible_system == 'Linux'", + }, + { + "name": "set_darwin_facts", + "ansible.builtin.set_fact": {"admin_group": "wheel"}, + "when": "ansible_system == 'Darwin'", + }, + { + "name": "ensure_ansible_local_facts_dir", + "ansible.builtin.file": { + "state": "directory", + "path": "/etc/ansible/facts.d", + }, + }, + { + "name": "update_remote_state", + "ansible.builtin.copy": { + "content": remote_state_obj.json(), + "dest": "/etc/ansible/facts.d/boardwalk_state.fact", + "mode": "0644", + "owner": "root", + "group": "{{ admin_group }}", + }, + }, + ] + self.ansible_run( + become=True, + become_password=become_password, + check=check, + gather_facts=False, + invocation_msg="set_remote_state", + tasks=tasks, + ) + if not check: + self.ansible_facts["ansible_local"][ + "boardwalk_state" + ] = remote_state_obj.dict() + workspace.flush() + + +class RemoteHostLocked(ClickException): + """The remote host is locked by another job""" diff --git a/src/boardwalk/manifest.py b/src/boardwalk/manifest.py new file mode 100644 index 0000000..3a08dea --- /dev/null +++ b/src/boardwalk/manifest.py @@ -0,0 +1,380 @@ +""" +This file has all of the classes and functions that are used in a +Boardwalkfile.py +""" +from __future__ import annotations + +import os +import sys +from abc import ABC, abstractmethod +from pathlib import Path +from tempfile import NamedTemporaryFile +from typing import TYPE_CHECKING + +from click import ClickException + +from boardwalk.state import LocalState + +if TYPE_CHECKING: + from typing import Any, Callable + + from boardwalk.ansible import AnsibleFacts, AnsibleTasksType, InventoryHostVars + +workspaces_dir = Path.cwd().joinpath(".boardwalk/workspaces") +active_workspace_file = workspaces_dir.joinpath("active_workspace.txt") + + +class DuplicateManifestClass(Exception): + """More than one class with same name is defined in the manifest""" + + +class ManifestNotFound(Exception): + """No Boardwalkfile.py was found""" + + +class NoActiveWorkspace(Exception): + """There is no workspace active""" + + message = ( + "No workspace selected." + " Use `boardwalk workspace list` to list workspaces" + " and `boardwalk workspace use` to select one" + ) + + +class WorkspaceNotFound(Exception): + """The workspace does not exist""" + + +def get_ws() -> Workspace: + """Imports the Boardwalkfile.py and returns the active Workspace""" + + # Try to import the Boardwalkfile.py + try: + sys.path.append(str(Path.cwd())) + import Boardwalkfile # pyright: reportMissingImports=false,reportUnknownVariableType=false,reportUnusedImport=false + + sys.path.pop() + except ModuleNotFoundError: + raise ManifestNotFound + + # Check if there are duplicate class names + class_list = [Job, Workspace, Workflow, WorkspaceConfig] + for item in class_list: + subclasses: list[str] = [] + for subclass in item.__subclasses__(): + subclasses.append(subclass.__qualname__) + if len(subclasses) != len(set(subclasses)): + raise DuplicateManifestClass( + "Duplicate class names defined in Boardwalkfile.py" + ) + + # Get the active Workspace name, if there is one + # If the BOARDWALK_WORKSPACE var is set, try to use that Workspace name + if "BOARDWALK_WORKSPACE" in os.environ: + active_workspace_name = os.getenv("BOARDWALK_WORKSPACE") + else: + # If a workspace is already active, use that one, if it's in the Boardwalkfile + try: + active_workspace_name = active_workspace_file.read_text().rstrip() + except FileNotFoundError: + raise NoActiveWorkspace + + # If somehow the active workspace name did not get set, we panic + if not active_workspace_name: + raise Exception("active_workspace_name is not set but it should have been") + + # Ensure the active Workspace name actually exists. + if Workspace.exists(active_workspace_name): + return Workspace.fetch_subclass(active_workspace_name)() + else: + raise WorkspaceNotFound(f'Workspace "{active_workspace_name}" does not exist.') + + +def get_boardwalkd_url() -> str: + try: + sys.path.append(str(Path.cwd())) + from Boardwalkfile import boardwalkd_url + + sys.path.pop() + except ModuleNotFoundError: + raise ManifestNotFound + except ImportError: + return None + return boardwalkd_url + + +class Job: + """Defines a single Job as methods""" + + def __init__(self, options: dict[str, Any] = dict()): + self.name = self.__class__.__name__ + self._check_options(options) + self.options = options + """Optional dict of options that can be leveraged inside the class""" + + def required_options(self) -> tuple[str]: + """Optional user method. Defines any required Job input options""" + return tuple() + + def preconditions( + self, facts: AnsibleFacts, inventory_vars: InventoryHostVars + ) -> bool: + """Optional user method. Return True if preconditions are met, else return False""" + return True + + def tasks(self) -> AnsibleTasksType: + """Optional user method. Return list of Ansible tasks to run. If an + empty list is returned, then the workflow doesn't connect to a host, + however, any code in this method still runs""" + return [] + + def _required_options(self) -> tuple[str]: + """ + Internal helper method. Always returns self.required_options() as a + tuple, even if the user returns as single string + """ + req_options = self.required_options() + if isinstance(self.required_options(), str): + req_options: tuple[str] = ( + req_options, # pyright: reportGeneralTypeIssues=false + ) + return req_options + + def _check_options(self, options: dict[str, Any]): + """Internal method. Checks required options have been set. Raises a ValueError if not""" + missing_options: list[str] = [] + for opt in self._required_options(): + if opt not in options: + missing_options.append(opt) + if len(missing_options) > 0: + raise ValueError(f"Required options missing: {', '.join(missing_options)}") + + +class Workflow(ABC): + """Defines a workflow of Jobs""" + + def __init__(self): + # If user-provided Jobs as a single Job, convert to tuple + workflow_jobs = self.jobs() + if isinstance(workflow_jobs, Job): + workflow_jobs = (workflow_jobs,) + workflow_exit_jobs = self.exit_jobs() + if isinstance(workflow_exit_jobs, Job): + workflow_exit_jobs = (workflow_exit_jobs,) + # self._jobs is the list of initialized Jobs. + self.i_jobs = workflow_jobs + # self._exit_jobs is the list of initialized Jobs. + self.i_exit_jobs = workflow_exit_jobs + + @abstractmethod + def jobs(self) -> Job | tuple[Job, ...]: + """Required user method. Defines the Jobs in a workflow. Order matters""" + raise NotImplementedError + + def exit_jobs(self) -> Job | tuple[Job, ...]: + """ + Optional user method. Defines Workflow Jobs that we will always try + to run, even on failure. Order matters. exit_jobs run after regular Jobs + """ + return () + + +class WorkspaceConfig: + """Configuration block for workspaces with defaults validation""" + + valid_sort_orders = ["ascending", "descending", "shuffle"] + + def __init__( + self, + host_pattern: str, + workflow: Workflow, + default_sort_order: str = "shuffle", + ): + self.default_sort_order = default_sort_order + """The default order hosts will be walked through (by hostname)""" + self.host_pattern = host_pattern + """The Ansible host pattern the workspace targets. If this changes after + initialization, the workspace needs to be re-initialized""" + self.workflow = workflow + """The workflow the workspace uses""" + + @property + def default_sort_order(self) -> str: + return self._default_sort_order + + @default_sort_order.setter + def default_sort_order(self, value: str): + self._is_valid_sort_order(value) + self._default_sort_order = value + + def _is_valid_sort_order(self, value: str): + """Checks if a given sort order is valid. Raises a ValueError if not""" + if value not in self.valid_sort_orders: + raise ValueError( + f"Valid default_sort_order values are: {', '.join(self.valid_sort_orders)}" + ) + + +class Workspace(ABC): + """ + Handles everything to do with the active workspace directory. "Workspaces" + are used to hold configuration and temporary information. This is done so + different upgrade projects are possible to do in parallel. This class + implements the singleton pattern, because the same Workspace may be + instantiated in many areas + """ + + _initialized: bool = False + _instance: Workspace | None = None + + def __new__(cls): + # Singleton. Only create a new instance if one doesn't already exist + if cls._instance is None: + cls._instance = super(Workspace, cls).__new__(cls) + return cls._instance + + def __init__(self): + if not self._initialized: + # Set the workspace name and set it as active by writing it down + self.name = self.__class__.__name__ + + # Set and create the workspace dir. + self.path = workspaces_dir.joinpath(self.name) + self.path.mkdir(parents=True, exist_ok=True) + + self.cfg = self.config() + + # Get and set the state if there is one, else create one + try: + self.state = LocalState.parse_file(self.path.joinpath("statefile.json")) + except FileNotFoundError: + self.state = LocalState(host_pattern=self.cfg.host_pattern) + self.flush() + + self._initialized = True + + def assert_host_pattern_unchanged(self) -> None: + """Asserts the host pattern has not changed improperly. If it has, it could + cause unexpected issues such as hosts being in the state that shouldn't""" + if self.cfg.host_pattern != self.state.host_pattern: + raise ClickException( + ( + "The workspace's host_pattern has changed since `boardwalk init` was first ran." + f' It was "{self.state.host_pattern}" but is now "{self.cfg.host_pattern}".' + " Run `boardwalk workspace reset` and then `boardwalk init`. Or, change it back" + ) + ) + + @abstractmethod + def config(self) -> WorkspaceConfig: + """Required user method. Sets the WorkspaceConfig""" + raise NotImplementedError + + def flush(self): + """Flush workspace state to disk""" + # The statefile is first written to a temp file so that failures in flushing + # will not corrupt an existing statefile + with Path( + NamedTemporaryFile( + mode="wb", delete=False, dir=self.path, prefix="statefile.json." + ).name + ) as tf: + tf.write_text(self.state.json()) + tf.rename(self.path.joinpath("statefile.json")) + + def reset(self): + """Resets active workspace. Configuration is retained but other state is lost""" + # We try to get a mutex on the workspace so we don't reset a workspace that has something running + self.mutex() + self.state = LocalState(host_pattern=self.cfg.host_pattern) + self.flush() + self.unmutex() + + def mutex(self): + """ + Method to try and prevent multiple parallel operations on the same + workspace. Should be called any time we are making changes to the workspace + """ + try: + self.path.joinpath("workspace.mutex").touch(exist_ok=False) + except FileExistsError: + raise ClickException("Workspace is locked by another operation") + + def has_mutex(self): + """Checks if the workspace has a mutex. Returns True if so""" + return self.path.joinpath("workspace.mutex").exists() + + def unmutex(self): + """Removes the mutex created by mutex method, if any""" + try: + self.path.joinpath("workspace.mutex").unlink(missing_ok=True) + except WorkspaceNotFound: + pass + + def catch(self): + """Catche workspace workflow at next host""" + self.path.joinpath("catch.lock").touch() + + def caught(self): + """Checks if workspace is caught. Returns true if it is""" + return self.path.joinpath("catch.lock").exists() + + def release(self): + """Removes workspace workflow catch if set""" + self.path.joinpath("catch.lock").unlink(missing_ok=True) + + @staticmethod + def use(name: str): + """Sets the active workspace""" + try: + ws = get_ws() + # Check if the active Workspace is mutexed + if ws.has_mutex(): + raise ClickException( + "Workspace is locked by another operation. `boardwalk workspace use` cannot be called while the workspace is locked" + ) + except NoActiveWorkspace: + pass + except WorkspaceNotFound: + pass + + # Check if the workspace exists + if not Workspace.exists(name): + raise WorkspaceNotFound( + f'Workspace "{name}" doesn\'t exist in Boardwalkfile.py. List workspaces with `boardwalk workspace list`' + ) + + # Create the workspace directory. + workspaces_dir.mkdir(parents=True, exist_ok=True) + + # Set the selected workspace as the active workspace + active_workspace_file.write_text(name) + + # Try to initialize the newly active workspace + ws = get_ws() + + @staticmethod + def exists(name: str) -> bool: + """Checks if a Workspace subclass exists by name""" + workspace_names: list[str] = [] + for workspace in Workspace.__subclasses__(): + workspace_names.append(workspace.__qualname__) + if name in workspace_names: + return True + return False + + @staticmethod + def fetch_subclass(name: str) -> Callable[..., Workspace]: + """Returns a subclass by name""" + for workspace in Workspace.__subclasses__(): + if str(workspace.__qualname__) == name: + return workspace + raise WorkspaceNotFound(f"No Workspace exists matching {name}") + + +def path(file_path: str) -> str: + """Helper to get absolute path from a string with a relative path and return as a string""" + if not Path(file_path).exists(): + raise FileNotFoundError(f"{file_path} does not exist") + return str(Path(file_path).absolute()) diff --git a/src/boardwalk/py.typed b/src/boardwalk/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/src/boardwalk/state.py b/src/boardwalk/state.py new file mode 100644 index 0000000..986bf61 --- /dev/null +++ b/src/boardwalk/state.py @@ -0,0 +1,36 @@ +""" +This file holds the state model +""" +from pydantic import BaseModel, Extra + +from boardwalk.host import Host + + +class StateBaseModel(BaseModel, extra=Extra.forbid): + """Base model for local and remote state""" + + +class LocalState(StateBaseModel): + """Model for local workspace state""" + + host_pattern: str + hosts: dict[str, Host] = {} + + +class RemoteStateWorkflow(StateBaseModel): + """Workflow data model used for remote state""" + + started: bool = False + succeeded: bool = False + + +class RemoteStateWorkspace(StateBaseModel): + """Workspace data model used for remote state""" + + workflow: RemoteStateWorkflow + + +class RemoteStateModel(StateBaseModel): + """Model for remote workspace state save as a fact on hosts""" + + workspaces: dict[str, RemoteStateWorkspace] = {} diff --git a/src/boardwalkd/README.md b/src/boardwalkd/README.md new file mode 100644 index 0000000..6093b7e --- /dev/null +++ b/src/boardwalkd/README.md @@ -0,0 +1,3 @@ +# boardwalkd Python module +This module contains the boardwalkd server code along with API protocol +definitions that are used by boardwalk workers as API clients. diff --git a/src/boardwalkd/__init__.py b/src/boardwalkd/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/boardwalkd/__main__.py b/src/boardwalkd/__main__.py new file mode 100644 index 0000000..acce0fb --- /dev/null +++ b/src/boardwalkd/__main__.py @@ -0,0 +1,4 @@ +from boardwalkd.cli import cli + +if __name__ == "__main__": + cli() diff --git a/src/boardwalkd/broadcast.py b/src/boardwalkd/broadcast.py new file mode 100644 index 0000000..713a573 --- /dev/null +++ b/src/boardwalkd/broadcast.py @@ -0,0 +1,74 @@ +""" +Code for handling server broadcasts +""" +import json +import logging + +from tornado.httpclient import AsyncHTTPClient, HTTPError, HTTPRequest + +from boardwalkd.protocol import WorkspaceEvent + + +async def handle_slack_broadcast( + event: WorkspaceEvent, + workspace: str, + webhook_url: str | None, + error_webhook_url: str | None, + server_url: str, +): + """Handles posting events to slack. If an error_webhook_url is not None, + then all error events will be posted there""" + if not (webhook_url or error_webhook_url): + raise ValueError("No slack webhook urls defined") + + if event.severity == "info": + slack_message_severity = ":large_blue_circle: INFO" + elif event.severity == "success": + slack_message_severity = ":large_green_circle: SUCCESS" + elif event.severity == "error": + slack_message_severity = ":red_circle: ERROR" + else: + raise ValueError(f"Event severity is invalid: {event.severity}") + slack_message_blocks = { + "blocks": [ + { + "type": "section", + "fields": [ + { + "type": "mrkdwn", + "text": f"*{slack_message_severity}*", + }, + { + "type": "mrkdwn", + "text": f"*<{server_url}#{workspace}|{workspace}>*", + }, + ], + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": f"```\n{event.message}\n```", + }, + }, + ] + } + payload = json.dumps(slack_message_blocks) + + async def post_msg(url: str): + request = HTTPRequest( + method="POST", + headers={"Content-Type": "application/json"}, + body=payload, + url=url, + ) + client = AsyncHTTPClient() + try: + await client.fetch(request) + except HTTPError as e: + logging.error(f"slack_webhook:{e}") + + if error_webhook_url and event.severity == "error": + await post_msg(error_webhook_url) + elif webhook_url: + await post_msg(webhook_url) diff --git a/src/boardwalkd/cli.py b/src/boardwalkd/cli.py new file mode 100644 index 0000000..168a7b7 --- /dev/null +++ b/src/boardwalkd/cli.py @@ -0,0 +1,105 @@ +""" +This file contains the boardwalkd CLI code +""" +import asyncio +import re +from importlib.metadata import version as lib_version + +import click +from click import ClickException + +from boardwalkd.server import run + + +@click.group() +def cli(): + """ + Boardwalkd is the server component of Boardwalk. + See the README.md @ https://github.com/Backblaze/boardwalk for more info + + To see more info about any subcommand, do `boardwalkd --help` + """ + pass + + +@cli.command() +@click.option( + "--develop/--no-develop", + default=False, + help="Runs the server in development mode with auto-reloading and tracebacks", + show_default=True, +) +@click.option( + "--enable-google-oauth/--no-enable-google-oauth", + help=( + "Enables Google Oauth2 for the web UI." + " BOARDWALK_GOOGLE_OAUTH_CLIENT_ID and BOARDWALK_GOOGLE_OAUTH_SECRET" + " environment variables must be set along with BOARDWALK_SECRET" + ), + default=False, + show_default=True, +) +@click.option( + "--host-header-pattern", + help="A valid python regex pattern to match accepted Host header values", + type=str, + required=True, +) +@click.option( + "--port", help="The port number the server binds to", type=int, required=True +) +@click.option( + "--slack-webhook-url", + help="A Slack webhook URL to broadcast all key events to", + type=str, + default=None, +) +@click.option( + "--slack-error-webhook-url", + help=( + "A Slack webhook URL to broadcast error events to." + " If defined, errors will not be sent to the URL defined by --slack-webhook-url" + ), + type=str, + default=None, +) +@click.option( + "--url", + help="The base URL where the server can be reached", + type=str, + required=True, +) +def serve( + develop: bool, + enable_google_oauth: bool, + host_header_pattern: str, + port: int, + slack_webhook_url: str, + slack_error_webhook_url: str, + url: str, +): + """Runs the server""" + try: + host_header_regex = re.compile(host_header_pattern) + except re.error: + raise ClickException("Host pattern regex invalid") + + asyncio.run( + run( + develop=develop, + enable_google_oauth=enable_google_oauth, + host_header_pattern=host_header_regex, + port_number=port, + slack_webhook_url=slack_webhook_url, + slack_error_webhook_url=slack_error_webhook_url, + url=url, + ) + ) + + +@cli.command( + "version", +) +def version(): + """Prints the boardwalk module version number and exits""" + click.echo(lib_version("boardwalk")) diff --git a/src/boardwalkd/protocol.py b/src/boardwalkd/protocol.py new file mode 100644 index 0000000..8fd2caf --- /dev/null +++ b/src/boardwalkd/protocol.py @@ -0,0 +1,332 @@ +""" +This file defines objects that are passed between worker clients and the server, +and contains functions to support clients using the server +""" + +import concurrent.futures +import socket +import time +from collections import deque +from datetime import datetime +from urllib.parse import urljoin, urlparse + +from pydantic import BaseModel, Extra, validator +from tornado.httpclient import ( + AsyncHTTPClient, + HTTPClient, + HTTPClientError, + HTTPError, + HTTPRequest, +) +from tornado.simple_httpclient import HTTPTimeoutError + + +class ProtocolBaseModel(BaseModel, extra=Extra.forbid): + """BaseModel for protocol usage""" + + +class WorkspaceDetails(ProtocolBaseModel): + """Model for basic workspace details from workers""" + + host_pattern: str = "" + workflow: str = "" + worker_command: str = "" + worker_hostname: str = "" + worker_username: str = "" + + +class WorkspaceEvent(ProtocolBaseModel): + """Model for workspace events sent from boardwalk workers""" + + message: str + severity: str + create_time: datetime | None = None + received_time: datetime | None = None + + def __init__(self, **kwargs: str): + super().__init__(**kwargs) + if not self.create_time: + self.create_time: datetime | None = datetime.utcnow() + + @validator("severity") + def severity_level(cls, v: str): + if v not in ["info", "success", "error"]: + raise ValueError(f"invalid severity level") + return v + + +class WorkspaceSemaphores(ProtocolBaseModel): + """Model for server-side workspace semaphores""" + + caught: bool = False + has_mutex: bool = False + + +class WorkspaceNotFound(Exception): + """The API doesn't have this workspace""" + + +class WorkspaceHasMutex(Exception): + """The workspace is locked""" + + +class Client: + """Boardwalkd protocol client""" + + def __init__(self, url: str): + self.async_client = AsyncHTTPClient() + self.event_queue = deque([]) + self.url = urlparse(url) + + def workspace_delete_mutex(self, workspace_name: str): + """Deletes a workspace mutex""" + url = urljoin( + self.url.geturl(), f"/api/workspace/{workspace_name}/semaphores/has_mutex" + ) + request = HTTPRequest( + method="DELETE", + body=None, + url=url, + ) + client = HTTPClient() + + try: + request = client.fetch(request) + except HTTPError as e: + if e.code == 404: + raise WorkspaceNotFound + else: + raise e + + def workspace_get_details(self, workspace_name: str) -> WorkspaceDetails: + """Queries the server for workspace details""" + url = urljoin(self.url.geturl(), f"/api/workspace/{workspace_name}/details") + client = HTTPClient() + + try: + request = client.fetch(url) + except HTTPError as e: + if e.code == 404: + raise WorkspaceNotFound + else: + raise e + + details = WorkspaceDetails() + return details.parse_raw(request.body) + + def workspace_post_catch(self, workspace_name: str): + """Posts a catch to the server""" + url = urljoin( + self.url.geturl(), f"/api/workspace/{workspace_name}/semaphores/caught" + ) + request = HTTPRequest( + method="POST", + body="catch", + url=url, + ) + client = HTTPClient() + + try: + client.fetch(request) + except HTTPError as e: + if e.code == 404: + raise WorkspaceNotFound + else: + raise e + + def workspace_post_details( + self, workspace_name: str, workspace_details: WorkspaceDetails + ): + """Updates the workspace details at the server""" + url = urljoin(self.url.geturl(), f"/api/workspace/{workspace_name}/details") + request = HTTPRequest( + method="POST", + headers={"Content-Type": "application/json"}, + body=workspace_details.json(), + url=url, + ) + client = HTTPClient() + + try: + client.fetch(request) + except HTTPError as e: + if e.code == 404: + raise WorkspaceNotFound + else: + raise e + + def workspace_post_heartbeat(self, workspace_name: str): + """Posts a heartbeat to the server""" + url = urljoin(self.url.geturl(), f"/api/workspace/{workspace_name}/heartbeat") + request = HTTPRequest( + method="POST", + body="ping", + url=url, + ) + client = HTTPClient() + + try: + client.fetch(request) + except HTTPError as e: + if e.code == 404: + raise WorkspaceNotFound + else: + raise e + + def workspace_heartbeat_keepalive(self, workspace_name: str) -> None: + """Tries to post a heartbeat to the server every 5 seconds""" + while True: + try: + self.workspace_post_heartbeat(workspace_name) + except ( + ConnectionRefusedError, + HTTPClientError, + HTTPTimeoutError, + socket.gaierror, + ): + pass + time.sleep(5) + + def workspace_heartbeat_keepalive_connect( + self, workspace_name: str + ) -> concurrent.futures.Future[None]: + """Starts a background thread to post heartbeats to the server so it + knows when a client is alive""" + executor = concurrent.futures.ThreadPoolExecutor() + future = executor.submit(self.workspace_heartbeat_keepalive, workspace_name) + return future + + def workspace_post_event( + self, + workspace_name: str, + workspace_event: WorkspaceEvent, + broadcast: bool = False, + ): + """Sends a event to the server to be logged or broadcast""" + url = urljoin(self.url.geturl(), f"/api/workspace/{workspace_name}/event") + if broadcast: + url = url + "?broadcast=1" + request = HTTPRequest( + method="POST", + headers={"Content-Type": "application/json"}, + body=workspace_event.json(), + url=url, + ) + client = HTTPClient() + + try: + client.fetch(request) + except HTTPError as e: + if e.code == 404: + raise WorkspaceNotFound + else: + raise e + + def workspace_queue_event( + self, + workspace_name: str, + workspace_event: WorkspaceEvent, + broadcast: bool = False, + ): + """ + Implements self.workspace_post_event(), maintaining a local queue of + events to send. If an event cannot be sent, it will be sent along with + the next successful attempt + """ + self.event_queue.append( + { + "workspace_name": workspace_name, + "workspace_event": workspace_event, + "broadcast": broadcast, + } + ) + + try: + for event in self.event_queue.copy(): + self.workspace_post_event(**event) + self.event_queue.popleft() + except ConnectionRefusedError: + pass + + def workspace_post_mutex(self, workspace_name: str): + """Posts a mutex to the server""" + url = urljoin( + self.url.geturl(), f"/api/workspace/{workspace_name}/semaphores/has_mutex" + ) + request = HTTPRequest( + method="POST", + body="mutex", + url=url, + ) + client = HTTPClient() + + try: + client.fetch(request) + except HTTPError as e: + if e.code == 404: + raise WorkspaceNotFound + if e.code == 409: + raise WorkspaceHasMutex + else: + raise e + + def workspace_get_semaphores(self, workspace_name: str) -> WorkspaceSemaphores: + """Queries the server for workspace semaphores""" + url = urljoin(self.url.geturl(), f"/api/workspace/{workspace_name}/semaphores") + client = HTTPClient() + + try: + request = client.fetch(url) + except HTTPError as e: + if e.code == 404: + raise WorkspaceNotFound + else: + raise e + + semaphores = WorkspaceSemaphores() + return semaphores.parse_raw(request.body) + + +class WorkspaceClient(Client): + """Subclass of the Boardwalkd protocol client with the Workspace name + pre-initialized""" + + def __init__(self, url: str, workspace_name: str): + super().__init__(url) + self.workspace_name = workspace_name + + def get_semaphores(self) -> WorkspaceSemaphores: + return self.workspace_get_semaphores(self.workspace_name) + + def has_mutex(self) -> bool: + try: + return self.get_semaphores().has_mutex + except WorkspaceNotFound: + return False + + def post_catch(self): + self.workspace_post_catch(self.workspace_name) + + def caught(self) -> bool: + return self.workspace_get_semaphores(self.workspace_name).caught + + def heartbeat_keepalive_connect(self): + return self.workspace_heartbeat_keepalive_connect(self.workspace_name) + + def mutex(self): + self.workspace_post_mutex(self.workspace_name) + + def post_details(self, workspace_details: WorkspaceDetails): + self.workspace_post_details(self.workspace_name, workspace_details) + + def post_heartbeat(self): + self.workspace_post_heartbeat(self.workspace_name) + + def post_event(self, workspace_event: WorkspaceEvent, broadcast: bool = False): + self.workspace_post_event(self.workspace_name, workspace_event, broadcast) + + def queue_event(self, workspace_event: WorkspaceEvent, broadcast: bool = False): + self.workspace_queue_event(self.workspace_name, workspace_event, broadcast) + + def unmutex(self): + self.workspace_delete_mutex(self.workspace_name) diff --git a/src/boardwalkd/py.typed b/src/boardwalkd/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/src/boardwalkd/server.py b/src/boardwalkd/server.py new file mode 100644 index 0000000..2f77b7a --- /dev/null +++ b/src/boardwalkd/server.py @@ -0,0 +1,504 @@ +""" +This file contains the main HTTP server code +""" +from __future__ import annotations + +import asyncio +import json +import logging +import os +import re +from collections import deque +from datetime import datetime, timedelta +from distutils.util import strtobool +from importlib.metadata import version as lib_version +from pathlib import Path +from typing import TYPE_CHECKING + +import tornado.auth +import tornado.web +import tornado.websocket +from click import ClickException +from pydantic import ValidationError +from tornado.routing import HostMatches + +from boardwalkd.broadcast import handle_slack_broadcast +from boardwalkd.protocol import WorkspaceDetails, WorkspaceEvent +from boardwalkd.state import load_state, WorkspaceState + +logging.basicConfig(level=logging.INFO) + +module_dir = Path(__file__).resolve().parent +state = load_state() + +if TYPE_CHECKING: + from typing import Any, Callable + + +class APIBaseHandler(tornado.web.RequestHandler): + """Base request handler for API paths""" + + def check_xsrf_cookie(self): + """We ignore this method on API requests""" + pass + + +class BaseHandler(tornado.web.RequestHandler): + """Base request handler for all paths""" + + +class UIBaseHandler(tornado.web.RequestHandler): + """Base request handler for UI paths""" + + def get_current_user(self): + """Required method for @tornado.web.authenticated to work""" + return self.get_secure_cookie("boardwalk_user") + + +""" +UI handlers +""" + + +def ui_method_secondsdelta(handler: BaseHandler, time: datetime) -> float: + """Custom UI templating method. Accepts a datetime and returns the delta + between time given and now in number of seconds""" + delta = datetime.utcnow() - time + return delta.total_seconds() + + +def ui_method_server_version(handler: BaseHandler) -> str: + """Returns the version number of the server""" + return lib_version("boardwalk") + + +def ui_method_sort_events_by_date( + handler: BaseHandler, events: deque[WorkspaceEvent] +) -> list[WorkspaceEvent]: + """Custom UI templating method. Accepts a deque of Workspace events and + sorts them by datetime in ascending order""" + key: Callable[[WorkspaceEvent], datetime] = lambda x: x.create_time + return sorted(events, key=key, reverse=True) + + +class AnonymousLoginHandler(UIBaseHandler): + """Handles "logging in" the UI when no auth is actually configured""" + + async def get(self): # pyright: reportIncompatibleMethodOverride=false + self.set_secure_cookie("boardwalk_user", "anonymous@example.com") + return self.redirect( + self.get_query_argument("next", "/") + ) # pyright: reportGeneralTypeIssues=false + + +class GoogleOAuth2LoginHandler(UIBaseHandler, tornado.auth.GoogleOAuth2Mixin): + """Handles logging into the UI with Google Oauth2""" + + async def get(self, *args: Any, **kwargs: Any): + try: + self.get_argument("code") + access = await self.get_authenticated_user( + redirect_uri=self.settings["login_url"], + code=self.get_argument("code"), + ) + user = await self.oauth2_request( + "https://www.googleapis.com/oauth2/v1/userinfo", + access_token=access["access_token"], + ) + self.set_secure_cookie("boardwalk_user", user["email"]) + return self.redirect("/") + except tornado.web.MissingArgumentError: + return self.authorize_redirect( + redirect_uri=self.settings["login_url"], + client_id=self.settings["google_oauth"]["key"], + scope=["email"], + response_type="code", + extra_params={"approval_prompt": "auto"}, + ) + + +class IndexHandler(UIBaseHandler): + """Handles serving the index UI""" + + @tornado.web.authenticated + def get(self): + try: + edit: str | int | bool = self.get_argument("edit", default=0) + edit = strtobool(edit) + except (AttributeError, ValueError): + edit = 0 + return self.render( + "index.html", title="Index", workspaces=state.workspaces, edit=edit + ) + + +class WorkspaceCatchHandler(UIBaseHandler): + """Handles receiving catch requests for workspaces from the UI""" + + @tornado.web.authenticated + def post(self, workspace: str): + try: + state.workspaces[workspace].semaphores.caught = True + state.flush() + return self.render("index_workspace_release.html", workspace_name=workspace) + except KeyError: + return self.send_error(404) + + @tornado.web.authenticated + def delete(self, workspace: str): + try: + state.workspaces[workspace].semaphores.caught = False + state.flush() + return self.render("index_workspace_catch.html", workspace_name=workspace) + except KeyError: + return self.send_error(404) + + +class WorkspaceEventsHandler(UIBaseHandler): + """Handles serving workspace events in the UI""" + + @tornado.web.authenticated + def get(self, workspace: str): + try: + state.workspaces[workspace] + except KeyError: + return self.send_error(404) + return self.render( + "workspace_events.html", workspace_name=workspace, title="Events" + ) + + +class WorkspaceEventsTableHandler(UIBaseHandler): + """Handles serving workspace events tables in the UI""" + + @tornado.web.authenticated + def get(self, workspace: str): + try: + workspace = state.workspaces[workspace] + except KeyError: + return self.send_error(404) + return self.render("workspace_events_table.html", workspace=workspace) + + +class WorkspaceMutexHandler(UIBaseHandler): + """Handles mutex requests for workspaces from the UI""" + + @tornado.web.authenticated + def delete(self, workspace: str): + try: + # If the host is possibly still connected we will not delete the + # mutex. Workspaces should send a heartbeat every 5 seconds + delta: timedelta = datetime.utcnow() - state.workspaces[workspace].last_seen + if delta.total_seconds() < 10: + return self.send_error(412) + state.workspaces[workspace].semaphores.has_mutex = False + state.flush() + self.set_header(name="HX-Refresh", value="true") + return + except KeyError: + return self.send_error(404) + + +class WorkspaceDeleteHandler(UIBaseHandler): + """Handles delete requests for workspaces from the UI""" + + @tornado.web.authenticated + def post(self, workspace: str): + try: + # If there is a mutex on the workspace we will not delete it + if state.workspaces[workspace].semaphores.has_mutex: + return self.send_error(412) + del state.workspaces[workspace] + state.flush() + self.set_header(name="HX-Refresh", value="true") + return + except KeyError: + return self.send_error(404) + + +class WorkspacesHandler(UIBaseHandler): + """Handles serving the list of workspaces in the UI""" + + @tornado.web.authenticated + def get(self): + try: + edit: str | int | bool = self.get_argument("edit", default=0) + edit = strtobool(edit) + except (AttributeError, ValueError): + edit = 0 + return self.render( + "index_workspace.html", workspaces=state.workspaces, edit=edit + ) + + +""" +API handlers +""" + + +class WorkspaceCatchApiHandler(APIBaseHandler): + """Handles setting a catch on a workspace""" + + def post(self, workspace: str): + try: + state.workspaces[workspace].semaphores.caught = True + state.flush() + except KeyError: + return self.send_error(404) + + +class WorkspaceDetailsApiHandler(APIBaseHandler): + """Handles getting and updating WorkspaceDetails for workspaces""" + + def get(self, workspace: str): + try: + return self.write( + state.workspaces[workspace].details.dict() + ) # pyright: reportUnknownMemberType=false + except KeyError: + return self.send_error(404) + + def post(self, workspace: str): + try: + payload = json.loads(self.request.body) + except json.decoder.JSONDecodeError: + return self.send_error(415) + + try: + new_details = WorkspaceDetails().parse_obj(payload) + except ValidationError as e: + logging.error(e) + return self.send_error(422) + + try: + state.workspaces[workspace].details = new_details + except KeyError: + state.workspaces[workspace] = WorkspaceState() + state.workspaces[workspace].details = new_details + state.workspaces[workspace].last_seen = datetime.utcnow() + state.flush() + + +class WorkspaceHeartbeatApiHandler(APIBaseHandler): + """Handles receiving heartbeats from workers""" + + def post(self, workspace: str): + try: + state.workspaces[workspace].last_seen = datetime.utcnow() + except KeyError: + return self.send_error(404) + + +class WorkspaceEventApiHandler(APIBaseHandler): + """ + Handles events sent from clients to the server. Events are always logged to + the server's stdout and a limited number of events are visible in the UI. + Optionally the client can request the server "broadcast" an event message, + and the server will post the broadcasted message to slack as well, if a + slack webhook is configured + """ + + async def post(self, workspace: str): + try: + broadcast: str | int | bool = self.get_argument("broadcast", default=0) + broadcast = strtobool(broadcast) + except (AttributeError, ValueError): + broadcast = 0 + + try: + payload = json.loads(self.request.body) + except json.decoder.JSONDecodeError: + return self.send_error(415) + + try: + event = WorkspaceEvent.parse_obj(payload) + except ValidationError as e: + logging.error(e) + return self.send_error(422) + + event.received_time = datetime.utcnow() + + try: + state.workspaces[workspace].events.append(event) + except KeyError: + return self.send_error(404) + + logging.info( + f"worker_event: {self.request.remote_ip} {workspace} {event.severity} {event.message}" + ) + + if broadcast: + if ( + self.settings["slack_webhook_url"] + or self.settings["slack_error_webhook_url"] + ): + await handle_slack_broadcast( + event, + workspace, + self.settings["slack_webhook_url"], + self.settings["slack_error_webhook_url"], + self.settings["url"], + ) + + state.flush() + + +class WorkspaceMutexApiHandler(APIBaseHandler): + """Handles workspace mutex api requests""" + + def post(self, workspace: str): + try: + if state.workspaces[workspace].semaphores.has_mutex: + return self.send_error(409) + state.workspaces[workspace].semaphores.has_mutex = True + state.flush() + except KeyError: + return self.send_error(404) + + def delete(self, workspace: str): + try: + state.workspaces[workspace].semaphores.has_mutex = False + state.flush() + return + except KeyError: + return self.send_error(404) + + +class WorkspaceSemaphoresApiHandler(APIBaseHandler): + """Handles getting server-side WorkspaceSemaphores""" + + def get(self, workspace: str): + try: + return self.write(state.workspaces[workspace].semaphores.dict()) + except KeyError: + return self.send_error(404) + + +""" +Server functions +""" + + +def make_server( + develop: bool, + enable_google_oauth: bool, + host_header_pattern: re.Pattern[str], + slack_error_webhook_url: str, + slack_webhook_url: str, + url: str, +): + """Builds the tornado application server object""" + handlers: list[tornado.web.OutputTransform] = [] + settings = { + "login_url": url + "/auth/login", + "slack_webhook_url": slack_webhook_url, + "slack_error_webhook_url": slack_error_webhook_url, + "enable_google_oauth": enable_google_oauth, + "static_path": module_dir.joinpath("static"), + "template_path": module_dir.joinpath("templates"), + "ui_methods": { + "secondsdelta": ui_method_secondsdelta, + "server_version": ui_method_server_version, + "sort_events_by_date": ui_method_sort_events_by_date, + }, + "url": url, + "xsrf_cookies": True, + } + if develop: + settings["debug"] = True + + # Set-up authentication + if enable_google_oauth: + any_auth_enabled = True + else: + any_auth_enabled = False + + if not any_auth_enabled: + handlers.append((r"/auth/login", AnonymousLoginHandler)) + settings["cookie_secret"] = "ANONYMOUS" + elif any_auth_enabled: + try: + settings["cookie_secret"] = os.environ["BOARDWALK_SECRET"] + except KeyError: + raise ClickException( + ( + "The BOARDWALK_SECRET environment variable is required when any" + " authentication method is enabled in order to generate secure cookies" + ) + ) + if enable_google_oauth: + try: + settings["google_oauth"] = { + "key": os.environ["BOARDWALK_GOOGLE_OAUTH_CLIENT_ID"], + "secret": os.environ["BOARDWALK_GOOGLE_OAUTH_SECRET"], + } + except KeyError: + raise ClickException( + ( + "BOARDWALK_GOOGLE_OAUTH_CLIENT_ID and BOARDWALK_GOOGLE_OAUTH_SECRET env vars" + " are required when --enable-google-oauth is set" + ) + ) + handlers.append((r"/auth/login", GoogleOAuth2LoginHandler)) + handlers.extend( + [ + # UI handlers + (r"/", IndexHandler), + (r"/workspaces", WorkspacesHandler), + (r"/workspace/(\w+)/events", WorkspaceEventsHandler), + (r"/workspace/(\w+)/events/table", WorkspaceEventsTableHandler), + (r"/workspace/(\w+)/semaphores/caught", WorkspaceCatchHandler), + (r"/workspace/(\w+)/semaphores/has_mutex", WorkspaceMutexHandler), + (r"/workspace/(\w+)/delete", WorkspaceDeleteHandler), + # API handlers + ( + r"/api/workspace/(\w+)/details", + WorkspaceDetailsApiHandler, + ), + ( + r"/api/workspace/(\w+)/heartbeat", + WorkspaceHeartbeatApiHandler, + ), + ( + r"/api/workspace/(\w+)/event", + WorkspaceEventApiHandler, + ), + ( + r"/api/workspace/(\w+)/semaphores", + WorkspaceSemaphoresApiHandler, + ), + ( + r"/api/workspace/(\w+)/semaphores/caught", + WorkspaceCatchApiHandler, + ), + ( + r"/api/workspace/(\w+)/semaphores/has_mutex", + WorkspaceMutexApiHandler, + ), + ] + ) + rules = [(HostMatches(host_header_pattern), handlers)] + return tornado.web.Application(rules, **settings) + + +async def run( + develop: bool, + enable_google_oauth: bool, + host_header_pattern: re.Pattern[str], + port_number: int, + slack_error_webhook_url: str, + slack_webhook_url: str, + url: str, +): + """Starts the tornado server and IO loop""" + server = make_server( + develop=develop, + enable_google_oauth=enable_google_oauth, + host_header_pattern=host_header_pattern, + slack_error_webhook_url=slack_error_webhook_url, + slack_webhook_url=slack_webhook_url, + url=url, + ) + server.listen(port_number) + logging.info(f"Server listening on port: {port_number}") + await asyncio.Event().wait() diff --git a/src/boardwalkd/state.py b/src/boardwalkd/state.py new file mode 100644 index 0000000..95519d1 --- /dev/null +++ b/src/boardwalkd/state.py @@ -0,0 +1,53 @@ +""" +This file defines models and helpers to support data that's persisted to a local +state and survives service restarts +""" +from collections import deque +from datetime import datetime +from pathlib import Path + +from pydantic import BaseModel, Extra + +from boardwalkd.protocol import WorkspaceDetails, WorkspaceEvent, WorkspaceSemaphores + +statefile_dir_path = Path.cwd().joinpath(".boardwalkd") +statefile_path = statefile_dir_path.joinpath("statefile.json") + + +class StateBaseModel(BaseModel, extra=Extra.forbid): + """BaseModel for state usage""" + + +class WorkspaceState(StateBaseModel): + """Model for persistent server workspace data""" + + details: WorkspaceDetails = WorkspaceDetails() + last_seen: datetime | None = None # When the worker last updated anything + events: deque[WorkspaceEvent] = deque([], maxlen=64) + semaphores: WorkspaceSemaphores = WorkspaceSemaphores() + + +class State(StateBaseModel): + """Model for persistent server state""" + + workspaces: dict[str, WorkspaceState] = {} + + def flush(self): + """ + Writes state to disk for persistence + Some items are excluded because they should only be set during runtime + """ + # Write state to disk. + statefile_path.write_text(self.json()) + + +def load_state() -> State: + """If the statefile exists, then returns the State object, else creates + a statefile and returns an empty State object""" + try: + return State().parse_file(statefile_path) + except FileNotFoundError: + statefile_dir_path.mkdir(parents=True, exist_ok=True) + state = State() + state.flush() + return state diff --git a/src/boardwalkd/static/boardwalkd.css b/src/boardwalkd/static/boardwalkd.css new file mode 100644 index 0000000..e9769af --- /dev/null +++ b/src/boardwalkd/static/boardwalkd.css @@ -0,0 +1,32 @@ +.workspace-details-table td { + font-family: monospace; + text-align: end; + max-width: 290px; + word-wrap: break-word; +} + +.workspace-events-preview-col { + position: relative; +} + +.workspace-events-preview-col table { + font-family: monospace; +} + +.workspace-events-container { + height: 200px; + overflow-y: hidden; + overflow-x: hidden; +} + +.workspace-events-table { + font-family: monospace; + overflow-y: hidden; + overflow-x: none; +} + +.more-events { + position: absolute; + bottom: 2px; + right: 2px; +} diff --git a/src/boardwalkd/static/bootstrap.bundle.min.js b/src/boardwalkd/static/bootstrap.bundle.min.js new file mode 100644 index 0000000..d395851 --- /dev/null +++ b/src/boardwalkd/static/bootstrap.bundle.min.js @@ -0,0 +1,7 @@ +/*! + * Bootstrap v5.2.0-beta1 (https://getbootstrap.com/) + * Copyright 2011-2022 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).bootstrap=e()}(this,(function(){"use strict";const t="transitionend",e=t=>{let e=t.getAttribute("data-bs-target");if(!e||"#"===e){let i=t.getAttribute("href");if(!i||!i.includes("#")&&!i.startsWith("."))return null;i.includes("#")&&!i.startsWith("#")&&(i=`#${i.split("#")[1]}`),e=i&&"#"!==i?i.trim():null}return e},i=t=>{const i=e(t);return i&&document.querySelector(i)?i:null},n=t=>{const i=e(t);return i?document.querySelector(i):null},s=e=>{e.dispatchEvent(new Event(t))},o=t=>!(!t||"object"!=typeof t)&&(void 0!==t.jquery&&(t=t[0]),void 0!==t.nodeType),r=t=>o(t)?t.jquery?t[0]:t:"string"==typeof t&&t.length>0?document.querySelector(t):null,a=t=>{if(!o(t)||0===t.getClientRects().length)return!1;const e="visible"===getComputedStyle(t).getPropertyValue("visibility"),i=t.closest("details:not([open])");if(!i)return e;if(i!==t){const e=t.closest("summary");if(e&&e.parentNode!==i)return!1;if(null===e)return!1}return e},l=t=>!t||t.nodeType!==Node.ELEMENT_NODE||!!t.classList.contains("disabled")||(void 0!==t.disabled?t.disabled:t.hasAttribute("disabled")&&"false"!==t.getAttribute("disabled")),c=t=>{if(!document.documentElement.attachShadow)return null;if("function"==typeof t.getRootNode){const e=t.getRootNode();return e instanceof ShadowRoot?e:null}return t instanceof ShadowRoot?t:t.parentNode?c(t.parentNode):null},h=()=>{},d=t=>{t.offsetHeight},u=()=>window.jQuery&&!document.body.hasAttribute("data-bs-no-jquery")?window.jQuery:null,f=[],p=()=>"rtl"===document.documentElement.dir,g=t=>{var e;e=()=>{const e=u();if(e){const i=t.NAME,n=e.fn[i];e.fn[i]=t.jQueryInterface,e.fn[i].Constructor=t,e.fn[i].noConflict=()=>(e.fn[i]=n,t.jQueryInterface)}},"loading"===document.readyState?(f.length||document.addEventListener("DOMContentLoaded",(()=>{for(const t of f)t()})),f.push(e)):e()},m=t=>{"function"==typeof t&&t()},_=(e,i,n=!0)=>{if(!n)return void m(e);const o=(t=>{if(!t)return 0;let{transitionDuration:e,transitionDelay:i}=window.getComputedStyle(t);const n=Number.parseFloat(e),s=Number.parseFloat(i);return n||s?(e=e.split(",")[0],i=i.split(",")[0],1e3*(Number.parseFloat(e)+Number.parseFloat(i))):0})(i)+5;let r=!1;const a=({target:n})=>{n===i&&(r=!0,i.removeEventListener(t,a),m(e))};i.addEventListener(t,a),setTimeout((()=>{r||s(i)}),o)},b=(t,e,i,n)=>{const s=t.length;let o=t.indexOf(e);return-1===o?!i&&n?t[s-1]:t[0]:(o+=i?1:-1,n&&(o=(o+s)%s),t[Math.max(0,Math.min(o,s-1))])},v=/[^.]*(?=\..*)\.|.*/,y=/\..*/,w=/::\d+$/,A={};let E=1;const T={mouseenter:"mouseover",mouseleave:"mouseout"},C=new Set(["click","dblclick","mouseup","mousedown","contextmenu","mousewheel","DOMMouseScroll","mouseover","mouseout","mousemove","selectstart","selectend","keydown","keypress","keyup","orientationchange","touchstart","touchmove","touchend","touchcancel","pointerdown","pointermove","pointerup","pointerleave","pointercancel","gesturestart","gesturechange","gestureend","focus","blur","change","reset","select","submit","focusin","focusout","load","unload","beforeunload","resize","move","DOMContentLoaded","readystatechange","error","abort","scroll"]);function O(t,e){return e&&`${e}::${E++}`||t.uidEvent||E++}function x(t){const e=O(t);return t.uidEvent=e,A[e]=A[e]||{},A[e]}function k(t,e,i=null){return Object.values(t).find((t=>t.originalHandler===e&&t.delegationSelector===i))}function L(t,e,i){const n="string"==typeof e,s=n?i:e;let o=N(t);return C.has(o)||(o=t),[n,s,o]}function D(t,e,i,n,s){if("string"!=typeof e||!t)return;if(i||(i=n,n=null),e in T){const t=t=>function(e){if(!e.relatedTarget||e.relatedTarget!==e.delegateTarget&&!e.delegateTarget.contains(e.relatedTarget))return t.call(this,e)};n?n=t(n):i=t(i)}const[o,r,a]=L(e,i,n),l=x(t),c=l[a]||(l[a]={}),h=k(c,r,o?i:null);if(h)return void(h.oneOff=h.oneOff&&s);const d=O(r,e.replace(v,"")),u=o?function(t,e,i){return function n(s){const o=t.querySelectorAll(e);for(let{target:r}=s;r&&r!==this;r=r.parentNode)for(const a of o)if(a===r)return s.delegateTarget=r,n.oneOff&&P.off(t,s.type,e,i),i.apply(r,[s])}}(t,i,n):function(t,e){return function i(n){return n.delegateTarget=t,i.oneOff&&P.off(t,n.type,e),e.apply(t,[n])}}(t,i);u.delegationSelector=o?i:null,u.originalHandler=r,u.oneOff=s,u.uidEvent=d,c[d]=u,t.addEventListener(a,u,o)}function S(t,e,i,n,s){const o=k(e[i],n,s);o&&(t.removeEventListener(i,o,Boolean(s)),delete e[i][o.uidEvent])}function I(t,e,i,n){const s=e[i]||{};for(const o of Object.keys(s))if(o.includes(n)){const n=s[o];S(t,e,i,n.originalHandler,n.delegationSelector)}}function N(t){return t=t.replace(y,""),T[t]||t}const P={on(t,e,i,n){D(t,e,i,n,!1)},one(t,e,i,n){D(t,e,i,n,!0)},off(t,e,i,n){if("string"!=typeof e||!t)return;const[s,o,r]=L(e,i,n),a=r!==e,l=x(t),c=e.startsWith(".");if(void 0!==o){if(!l||!l[r])return;return void S(t,l,r,o,s?i:null)}if(c)for(const i of Object.keys(l))I(t,l,i,e.slice(1));const h=l[r]||{};for(const i of Object.keys(h)){const n=i.replace(w,"");if(!a||e.includes(n)){const e=h[i];S(t,l,r,e.originalHandler,e.delegationSelector)}}},trigger(t,e,i){if("string"!=typeof e||!t)return null;const n=u();let s=null,o=!0,r=!0,a=!1;e!==N(e)&&n&&(s=n.Event(e,i),n(t).trigger(s),o=!s.isPropagationStopped(),r=!s.isImmediatePropagationStopped(),a=s.isDefaultPrevented());const l=new Event(e,{bubbles:o,cancelable:!0});if(void 0!==i)for(const t of Object.keys(i))Object.defineProperty(l,t,{get:()=>i[t]});return a&&l.preventDefault(),r&&t.dispatchEvent(l),l.defaultPrevented&&s&&s.preventDefault(),l}},M=new Map,j={set(t,e,i){M.has(t)||M.set(t,new Map);const n=M.get(t);n.has(e)||0===n.size?n.set(e,i):console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(n.keys())[0]}.`)},get:(t,e)=>M.has(t)&&M.get(t).get(e)||null,remove(t,e){if(!M.has(t))return;const i=M.get(t);i.delete(e),0===i.size&&M.delete(t)}};function H(t){if("true"===t)return!0;if("false"===t)return!1;if(t===Number(t).toString())return Number(t);if(""===t||"null"===t)return null;if("string"!=typeof t)return t;try{return JSON.parse(decodeURIComponent(t))}catch(e){return t}}function $(t){return t.replace(/[A-Z]/g,(t=>`-${t.toLowerCase()}`))}const W={setDataAttribute(t,e,i){t.setAttribute(`data-bs-${$(e)}`,i)},removeDataAttribute(t,e){t.removeAttribute(`data-bs-${$(e)}`)},getDataAttributes(t){if(!t)return{};const e={},i=Object.keys(t.dataset).filter((t=>t.startsWith("bs")&&!t.startsWith("bsConfig")));for(const n of i){let i=n.replace(/^bs/,"");i=i.charAt(0).toLowerCase()+i.slice(1,i.length),e[i]=H(t.dataset[n])}return e},getDataAttribute:(t,e)=>H(t.getAttribute(`data-bs-${$(e)}`))};class B{static get Default(){return{}}static get DefaultType(){return{}}static get NAME(){throw new Error('You have to implement the static method "NAME", for each component!')}_getConfig(t){return t=this._mergeConfigObj(t),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}_configAfterMerge(t){return t}_mergeConfigObj(t,e){const i=o(e)?W.getDataAttribute(e,"config"):{};return{...this.constructor.Default,..."object"==typeof i?i:{},...o(e)?W.getDataAttributes(e):{},..."object"==typeof t?t:{}}}_typeCheckConfig(t,e=this.constructor.DefaultType){for(const n of Object.keys(e)){const s=e[n],r=t[n],a=o(r)?"element":null==(i=r)?`${i}`:Object.prototype.toString.call(i).match(/\s([a-z]+)/i)[1].toLowerCase();if(!new RegExp(s).test(a))throw new TypeError(`${this.constructor.NAME.toUpperCase()}: Option "${n}" provided type "${a}" but expected type "${s}".`)}var i}}class F extends B{constructor(t,e){super(),(t=r(t))&&(this._element=t,this._config=this._getConfig(e),j.set(this._element,this.constructor.DATA_KEY,this))}dispose(){j.remove(this._element,this.constructor.DATA_KEY),P.off(this._element,this.constructor.EVENT_KEY);for(const t of Object.getOwnPropertyNames(this))this[t]=null}_queueCallback(t,e,i=!0){_(t,e,i)}_getConfig(t){return t=this._mergeConfigObj(t,this._element),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}static getInstance(t){return j.get(r(t),this.DATA_KEY)}static getOrCreateInstance(t,e={}){return this.getInstance(t)||new this(t,"object"==typeof e?e:null)}static get VERSION(){return"5.2.0-beta1"}static get DATA_KEY(){return`bs.${this.NAME}`}static get EVENT_KEY(){return`.${this.DATA_KEY}`}static eventName(t){return`${t}${this.EVENT_KEY}`}}const z=(t,e="hide")=>{const i=`click.dismiss${t.EVENT_KEY}`,s=t.NAME;P.on(document,i,`[data-bs-dismiss="${s}"]`,(function(i){if(["A","AREA"].includes(this.tagName)&&i.preventDefault(),l(this))return;const o=n(this)||this.closest(`.${s}`);t.getOrCreateInstance(o)[e]()}))};class R extends F{static get NAME(){return"alert"}close(){if(P.trigger(this._element,"close.bs.alert").defaultPrevented)return;this._element.classList.remove("show");const t=this._element.classList.contains("fade");this._queueCallback((()=>this._destroyElement()),this._element,t)}_destroyElement(){this._element.remove(),P.trigger(this._element,"closed.bs.alert"),this.dispose()}static jQueryInterface(t){return this.each((function(){const e=R.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}z(R,"close"),g(R);const q='[data-bs-toggle="button"]';class V extends F{static get NAME(){return"button"}toggle(){this._element.setAttribute("aria-pressed",this._element.classList.toggle("active"))}static jQueryInterface(t){return this.each((function(){const e=V.getOrCreateInstance(this);"toggle"===t&&e[t]()}))}}P.on(document,"click.bs.button.data-api",q,(t=>{t.preventDefault();const e=t.target.closest(q);V.getOrCreateInstance(e).toggle()})),g(V);const K={find:(t,e=document.documentElement)=>[].concat(...Element.prototype.querySelectorAll.call(e,t)),findOne:(t,e=document.documentElement)=>Element.prototype.querySelector.call(e,t),children:(t,e)=>[].concat(...t.children).filter((t=>t.matches(e))),parents(t,e){const i=[];let n=t.parentNode.closest(e);for(;n;)i.push(n),n=n.parentNode.closest(e);return i},prev(t,e){let i=t.previousElementSibling;for(;i;){if(i.matches(e))return[i];i=i.previousElementSibling}return[]},next(t,e){let i=t.nextElementSibling;for(;i;){if(i.matches(e))return[i];i=i.nextElementSibling}return[]},focusableChildren(t){const e=["a","button","input","textarea","select","details","[tabindex]",'[contenteditable="true"]'].map((t=>`${t}:not([tabindex^="-"])`)).join(",");return this.find(e,t).filter((t=>!l(t)&&a(t)))}},Q={leftCallback:null,rightCallback:null,endCallback:null},X={leftCallback:"(function|null)",rightCallback:"(function|null)",endCallback:"(function|null)"};class Y extends B{constructor(t,e){super(),this._element=t,t&&Y.isSupported()&&(this._config=this._getConfig(e),this._deltaX=0,this._supportPointerEvents=Boolean(window.PointerEvent),this._initEvents())}static get Default(){return Q}static get DefaultType(){return X}static get NAME(){return"swipe"}dispose(){P.off(this._element,".bs.swipe")}_start(t){this._supportPointerEvents?this._eventIsPointerPenTouch(t)&&(this._deltaX=t.clientX):this._deltaX=t.touches[0].clientX}_end(t){this._eventIsPointerPenTouch(t)&&(this._deltaX=t.clientX-this._deltaX),this._handleSwipe(),m(this._config.endCallback)}_move(t){this._deltaX=t.touches&&t.touches.length>1?0:t.touches[0].clientX-this._deltaX}_handleSwipe(){const t=Math.abs(this._deltaX);if(t<=40)return;const e=t/this._deltaX;this._deltaX=0,e&&m(e>0?this._config.rightCallback:this._config.leftCallback)}_initEvents(){this._supportPointerEvents?(P.on(this._element,"pointerdown.bs.swipe",(t=>this._start(t))),P.on(this._element,"pointerup.bs.swipe",(t=>this._end(t))),this._element.classList.add("pointer-event")):(P.on(this._element,"touchstart.bs.swipe",(t=>this._start(t))),P.on(this._element,"touchmove.bs.swipe",(t=>this._move(t))),P.on(this._element,"touchend.bs.swipe",(t=>this._end(t))))}_eventIsPointerPenTouch(t){return this._supportPointerEvents&&("pen"===t.pointerType||"touch"===t.pointerType)}static isSupported(){return"ontouchstart"in document.documentElement||navigator.maxTouchPoints>0}}const U="next",G="prev",J="left",Z="right",tt="slid.bs.carousel",et="carousel",it="active",nt={ArrowLeft:Z,ArrowRight:J},st={interval:5e3,keyboard:!0,pause:"hover",ride:!1,touch:!0,wrap:!0},ot={interval:"(number|boolean)",keyboard:"boolean",ride:"(boolean|string)",pause:"(string|boolean)",touch:"boolean",wrap:"boolean"};class rt extends F{constructor(t,e){super(t,e),this._interval=null,this._activeElement=null,this._isSliding=!1,this.touchTimeout=null,this._swipeHelper=null,this._indicatorsElement=K.findOne(".carousel-indicators",this._element),this._addEventListeners(),this._config.ride===et&&this.cycle()}static get Default(){return st}static get DefaultType(){return ot}static get NAME(){return"carousel"}next(){this._slide(U)}nextWhenVisible(){!document.hidden&&a(this._element)&&this.next()}prev(){this._slide(G)}pause(){this._isSliding&&s(this._element),this._clearInterval()}cycle(){this._clearInterval(),this._updateInterval(),this._interval=setInterval((()=>this.nextWhenVisible()),this._config.interval)}_maybeEnableCycle(){this._config.ride&&(this._isSliding?P.one(this._element,tt,(()=>this.cycle())):this.cycle())}to(t){const e=this._getItems();if(t>e.length-1||t<0)return;if(this._isSliding)return void P.one(this._element,tt,(()=>this.to(t)));const i=this._getItemIndex(this._getActive());if(i===t)return;const n=t>i?U:G;this._slide(n,e[t])}dispose(){this._swipeHelper&&this._swipeHelper.dispose(),super.dispose()}_configAfterMerge(t){return t.defaultInterval=t.interval,t}_addEventListeners(){this._config.keyboard&&P.on(this._element,"keydown.bs.carousel",(t=>this._keydown(t))),"hover"===this._config.pause&&(P.on(this._element,"mouseenter.bs.carousel",(()=>this.pause())),P.on(this._element,"mouseleave.bs.carousel",(()=>this._maybeEnableCycle()))),this._config.touch&&Y.isSupported()&&this._addTouchEventListeners()}_addTouchEventListeners(){for(const t of K.find(".carousel-item img",this._element))P.on(t,"dragstart.bs.carousel",(t=>t.preventDefault()));const t={leftCallback:()=>this._slide(this._directionToOrder(J)),rightCallback:()=>this._slide(this._directionToOrder(Z)),endCallback:()=>{"hover"===this._config.pause&&(this.pause(),this.touchTimeout&&clearTimeout(this.touchTimeout),this.touchTimeout=setTimeout((()=>this._maybeEnableCycle()),500+this._config.interval))}};this._swipeHelper=new Y(this._element,t)}_keydown(t){if(/input|textarea/i.test(t.target.tagName))return;const e=nt[t.key];e&&(t.preventDefault(),this._slide(this._directionToOrder(e)))}_getItemIndex(t){return this._getItems().indexOf(t)}_setActiveIndicatorElement(t){if(!this._indicatorsElement)return;const e=K.findOne(".active",this._indicatorsElement);e.classList.remove(it),e.removeAttribute("aria-current");const i=K.findOne(`[data-bs-slide-to="${t}"]`,this._indicatorsElement);i&&(i.classList.add(it),i.setAttribute("aria-current","true"))}_updateInterval(){const t=this._activeElement||this._getActive();if(!t)return;const e=Number.parseInt(t.getAttribute("data-bs-interval"),10);this._config.interval=e||this._config.defaultInterval}_slide(t,e=null){if(this._isSliding)return;const i=this._getActive(),n=t===U,s=e||b(this._getItems(),i,n,this._config.wrap);if(s===i)return;const o=this._getItemIndex(s),r=e=>P.trigger(this._element,e,{relatedTarget:s,direction:this._orderToDirection(t),from:this._getItemIndex(i),to:o});if(r("slide.bs.carousel").defaultPrevented)return;if(!i||!s)return;const a=Boolean(this._interval);this.pause(),this._isSliding=!0,this._setActiveIndicatorElement(o),this._activeElement=s;const l=n?"carousel-item-start":"carousel-item-end",c=n?"carousel-item-next":"carousel-item-prev";s.classList.add(c),d(s),i.classList.add(l),s.classList.add(l),this._queueCallback((()=>{s.classList.remove(l,c),s.classList.add(it),i.classList.remove(it,c,l),this._isSliding=!1,r(tt)}),i,this._isAnimated()),a&&this.cycle()}_isAnimated(){return this._element.classList.contains("slide")}_getActive(){return K.findOne(".active.carousel-item",this._element)}_getItems(){return K.find(".carousel-item",this._element)}_clearInterval(){this._interval&&(clearInterval(this._interval),this._interval=null)}_directionToOrder(t){return p()?t===J?G:U:t===J?U:G}_orderToDirection(t){return p()?t===G?J:Z:t===G?Z:J}static jQueryInterface(t){return this.each((function(){const e=rt.getOrCreateInstance(this,t);if("number"!=typeof t){if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}else e.to(t)}))}}P.on(document,"click.bs.carousel.data-api","[data-bs-slide], [data-bs-slide-to]",(function(t){const e=n(this);if(!e||!e.classList.contains(et))return;t.preventDefault();const i=rt.getOrCreateInstance(e),s=this.getAttribute("data-bs-slide-to");return s?(i.to(s),void i._maybeEnableCycle()):"next"===W.getDataAttribute(this,"slide")?(i.next(),void i._maybeEnableCycle()):(i.prev(),void i._maybeEnableCycle())})),P.on(window,"load.bs.carousel.data-api",(()=>{const t=K.find('[data-bs-ride="carousel"]');for(const e of t)rt.getOrCreateInstance(e)})),g(rt);const at="show",lt="collapse",ct="collapsing",ht='[data-bs-toggle="collapse"]',dt={toggle:!0,parent:null},ut={toggle:"boolean",parent:"(null|element)"};class ft extends F{constructor(t,e){super(t,e),this._isTransitioning=!1,this._triggerArray=[];const n=K.find(ht);for(const t of n){const e=i(t),n=K.find(e).filter((t=>t===this._element));null!==e&&n.length&&this._triggerArray.push(t)}this._initializeChildren(),this._config.parent||this._addAriaAndCollapsedClass(this._triggerArray,this._isShown()),this._config.toggle&&this.toggle()}static get Default(){return dt}static get DefaultType(){return ut}static get NAME(){return"collapse"}toggle(){this._isShown()?this.hide():this.show()}show(){if(this._isTransitioning||this._isShown())return;let t=[];if(this._config.parent&&(t=this._getFirstLevelChildren(".collapse.show, .collapse.collapsing").filter((t=>t!==this._element)).map((t=>ft.getOrCreateInstance(t,{toggle:!1})))),t.length&&t[0]._isTransitioning)return;if(P.trigger(this._element,"show.bs.collapse").defaultPrevented)return;for(const e of t)e.hide();const e=this._getDimension();this._element.classList.remove(lt),this._element.classList.add(ct),this._element.style[e]=0,this._addAriaAndCollapsedClass(this._triggerArray,!0),this._isTransitioning=!0;const i=`scroll${e[0].toUpperCase()+e.slice(1)}`;this._queueCallback((()=>{this._isTransitioning=!1,this._element.classList.remove(ct),this._element.classList.add(lt,at),this._element.style[e]="",P.trigger(this._element,"shown.bs.collapse")}),this._element,!0),this._element.style[e]=`${this._element[i]}px`}hide(){if(this._isTransitioning||!this._isShown())return;if(P.trigger(this._element,"hide.bs.collapse").defaultPrevented)return;const t=this._getDimension();this._element.style[t]=`${this._element.getBoundingClientRect()[t]}px`,d(this._element),this._element.classList.add(ct),this._element.classList.remove(lt,at);for(const t of this._triggerArray){const e=n(t);e&&!this._isShown(e)&&this._addAriaAndCollapsedClass([t],!1)}this._isTransitioning=!0,this._element.style[t]="",this._queueCallback((()=>{this._isTransitioning=!1,this._element.classList.remove(ct),this._element.classList.add(lt),P.trigger(this._element,"hidden.bs.collapse")}),this._element,!0)}_isShown(t=this._element){return t.classList.contains(at)}_configAfterMerge(t){return t.toggle=Boolean(t.toggle),t.parent=r(t.parent),t}_getDimension(){return this._element.classList.contains("collapse-horizontal")?"width":"height"}_initializeChildren(){if(!this._config.parent)return;const t=this._getFirstLevelChildren(ht);for(const e of t){const t=n(e);t&&this._addAriaAndCollapsedClass([e],this._isShown(t))}}_getFirstLevelChildren(t){const e=K.find(":scope .collapse .collapse",this._config.parent);return K.find(t,this._config.parent).filter((t=>!e.includes(t)))}_addAriaAndCollapsedClass(t,e){if(t.length)for(const i of t)i.classList.toggle("collapsed",!e),i.setAttribute("aria-expanded",e)}static jQueryInterface(t){const e={};return"string"==typeof t&&/show|hide/.test(t)&&(e.toggle=!1),this.each((function(){const i=ft.getOrCreateInstance(this,e);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t]()}}))}}P.on(document,"click.bs.collapse.data-api",ht,(function(t){("A"===t.target.tagName||t.delegateTarget&&"A"===t.delegateTarget.tagName)&&t.preventDefault();const e=i(this),n=K.find(e);for(const t of n)ft.getOrCreateInstance(t,{toggle:!1}).toggle()})),g(ft);var pt="top",gt="bottom",mt="right",_t="left",bt="auto",vt=[pt,gt,mt,_t],yt="start",wt="end",At="clippingParents",Et="viewport",Tt="popper",Ct="reference",Ot=vt.reduce((function(t,e){return t.concat([e+"-"+yt,e+"-"+wt])}),[]),xt=[].concat(vt,[bt]).reduce((function(t,e){return t.concat([e,e+"-"+yt,e+"-"+wt])}),[]),kt="beforeRead",Lt="read",Dt="afterRead",St="beforeMain",It="main",Nt="afterMain",Pt="beforeWrite",Mt="write",jt="afterWrite",Ht=[kt,Lt,Dt,St,It,Nt,Pt,Mt,jt];function $t(t){return t?(t.nodeName||"").toLowerCase():null}function Wt(t){if(null==t)return window;if("[object Window]"!==t.toString()){var e=t.ownerDocument;return e&&e.defaultView||window}return t}function Bt(t){return t instanceof Wt(t).Element||t instanceof Element}function Ft(t){return t instanceof Wt(t).HTMLElement||t instanceof HTMLElement}function zt(t){return"undefined"!=typeof ShadowRoot&&(t instanceof Wt(t).ShadowRoot||t instanceof ShadowRoot)}const Rt={name:"applyStyles",enabled:!0,phase:"write",fn:function(t){var e=t.state;Object.keys(e.elements).forEach((function(t){var i=e.styles[t]||{},n=e.attributes[t]||{},s=e.elements[t];Ft(s)&&$t(s)&&(Object.assign(s.style,i),Object.keys(n).forEach((function(t){var e=n[t];!1===e?s.removeAttribute(t):s.setAttribute(t,!0===e?"":e)})))}))},effect:function(t){var e=t.state,i={popper:{position:e.options.strategy,left:"0",top:"0",margin:"0"},arrow:{position:"absolute"},reference:{}};return Object.assign(e.elements.popper.style,i.popper),e.styles=i,e.elements.arrow&&Object.assign(e.elements.arrow.style,i.arrow),function(){Object.keys(e.elements).forEach((function(t){var n=e.elements[t],s=e.attributes[t]||{},o=Object.keys(e.styles.hasOwnProperty(t)?e.styles[t]:i[t]).reduce((function(t,e){return t[e]="",t}),{});Ft(n)&&$t(n)&&(Object.assign(n.style,o),Object.keys(s).forEach((function(t){n.removeAttribute(t)})))}))}},requires:["computeStyles"]};function qt(t){return t.split("-")[0]}var Vt=Math.max,Kt=Math.min,Qt=Math.round;function Xt(t,e){void 0===e&&(e=!1);var i=t.getBoundingClientRect(),n=1,s=1;if(Ft(t)&&e){var o=t.offsetHeight,r=t.offsetWidth;r>0&&(n=Qt(i.width)/r||1),o>0&&(s=Qt(i.height)/o||1)}return{width:i.width/n,height:i.height/s,top:i.top/s,right:i.right/n,bottom:i.bottom/s,left:i.left/n,x:i.left/n,y:i.top/s}}function Yt(t){var e=Xt(t),i=t.offsetWidth,n=t.offsetHeight;return Math.abs(e.width-i)<=1&&(i=e.width),Math.abs(e.height-n)<=1&&(n=e.height),{x:t.offsetLeft,y:t.offsetTop,width:i,height:n}}function Ut(t,e){var i=e.getRootNode&&e.getRootNode();if(t.contains(e))return!0;if(i&&zt(i)){var n=e;do{if(n&&t.isSameNode(n))return!0;n=n.parentNode||n.host}while(n)}return!1}function Gt(t){return Wt(t).getComputedStyle(t)}function Jt(t){return["table","td","th"].indexOf($t(t))>=0}function Zt(t){return((Bt(t)?t.ownerDocument:t.document)||window.document).documentElement}function te(t){return"html"===$t(t)?t:t.assignedSlot||t.parentNode||(zt(t)?t.host:null)||Zt(t)}function ee(t){return Ft(t)&&"fixed"!==Gt(t).position?t.offsetParent:null}function ie(t){for(var e=Wt(t),i=ee(t);i&&Jt(i)&&"static"===Gt(i).position;)i=ee(i);return i&&("html"===$t(i)||"body"===$t(i)&&"static"===Gt(i).position)?e:i||function(t){var e=-1!==navigator.userAgent.toLowerCase().indexOf("firefox");if(-1!==navigator.userAgent.indexOf("Trident")&&Ft(t)&&"fixed"===Gt(t).position)return null;var i=te(t);for(zt(i)&&(i=i.host);Ft(i)&&["html","body"].indexOf($t(i))<0;){var n=Gt(i);if("none"!==n.transform||"none"!==n.perspective||"paint"===n.contain||-1!==["transform","perspective"].indexOf(n.willChange)||e&&"filter"===n.willChange||e&&n.filter&&"none"!==n.filter)return i;i=i.parentNode}return null}(t)||e}function ne(t){return["top","bottom"].indexOf(t)>=0?"x":"y"}function se(t,e,i){return Vt(t,Kt(e,i))}function oe(t){return Object.assign({},{top:0,right:0,bottom:0,left:0},t)}function re(t,e){return e.reduce((function(e,i){return e[i]=t,e}),{})}const ae={name:"arrow",enabled:!0,phase:"main",fn:function(t){var e,i=t.state,n=t.name,s=t.options,o=i.elements.arrow,r=i.modifiersData.popperOffsets,a=qt(i.placement),l=ne(a),c=[_t,mt].indexOf(a)>=0?"height":"width";if(o&&r){var h=function(t,e){return oe("number"!=typeof(t="function"==typeof t?t(Object.assign({},e.rects,{placement:e.placement})):t)?t:re(t,vt))}(s.padding,i),d=Yt(o),u="y"===l?pt:_t,f="y"===l?gt:mt,p=i.rects.reference[c]+i.rects.reference[l]-r[l]-i.rects.popper[c],g=r[l]-i.rects.reference[l],m=ie(o),_=m?"y"===l?m.clientHeight||0:m.clientWidth||0:0,b=p/2-g/2,v=h[u],y=_-d[c]-h[f],w=_/2-d[c]/2+b,A=se(v,w,y),E=l;i.modifiersData[n]=((e={})[E]=A,e.centerOffset=A-w,e)}},effect:function(t){var e=t.state,i=t.options.element,n=void 0===i?"[data-popper-arrow]":i;null!=n&&("string"!=typeof n||(n=e.elements.popper.querySelector(n)))&&Ut(e.elements.popper,n)&&(e.elements.arrow=n)},requires:["popperOffsets"],requiresIfExists:["preventOverflow"]};function le(t){return t.split("-")[1]}var ce={top:"auto",right:"auto",bottom:"auto",left:"auto"};function he(t){var e,i=t.popper,n=t.popperRect,s=t.placement,o=t.variation,r=t.offsets,a=t.position,l=t.gpuAcceleration,c=t.adaptive,h=t.roundOffsets,d=t.isFixed,u=r.x,f=void 0===u?0:u,p=r.y,g=void 0===p?0:p,m="function"==typeof h?h({x:f,y:g}):{x:f,y:g};f=m.x,g=m.y;var _=r.hasOwnProperty("x"),b=r.hasOwnProperty("y"),v=_t,y=pt,w=window;if(c){var A=ie(i),E="clientHeight",T="clientWidth";A===Wt(i)&&"static"!==Gt(A=Zt(i)).position&&"absolute"===a&&(E="scrollHeight",T="scrollWidth"),(s===pt||(s===_t||s===mt)&&o===wt)&&(y=gt,g-=(d&&A===w&&w.visualViewport?w.visualViewport.height:A[E])-n.height,g*=l?1:-1),s!==_t&&(s!==pt&&s!==gt||o!==wt)||(v=mt,f-=(d&&A===w&&w.visualViewport?w.visualViewport.width:A[T])-n.width,f*=l?1:-1)}var C,O=Object.assign({position:a},c&&ce),x=!0===h?function(t){var e=t.x,i=t.y,n=window.devicePixelRatio||1;return{x:Qt(e*n)/n||0,y:Qt(i*n)/n||0}}({x:f,y:g}):{x:f,y:g};return f=x.x,g=x.y,l?Object.assign({},O,((C={})[y]=b?"0":"",C[v]=_?"0":"",C.transform=(w.devicePixelRatio||1)<=1?"translate("+f+"px, "+g+"px)":"translate3d("+f+"px, "+g+"px, 0)",C)):Object.assign({},O,((e={})[y]=b?g+"px":"",e[v]=_?f+"px":"",e.transform="",e))}const de={name:"computeStyles",enabled:!0,phase:"beforeWrite",fn:function(t){var e=t.state,i=t.options,n=i.gpuAcceleration,s=void 0===n||n,o=i.adaptive,r=void 0===o||o,a=i.roundOffsets,l=void 0===a||a,c={placement:qt(e.placement),variation:le(e.placement),popper:e.elements.popper,popperRect:e.rects.popper,gpuAcceleration:s,isFixed:"fixed"===e.options.strategy};null!=e.modifiersData.popperOffsets&&(e.styles.popper=Object.assign({},e.styles.popper,he(Object.assign({},c,{offsets:e.modifiersData.popperOffsets,position:e.options.strategy,adaptive:r,roundOffsets:l})))),null!=e.modifiersData.arrow&&(e.styles.arrow=Object.assign({},e.styles.arrow,he(Object.assign({},c,{offsets:e.modifiersData.arrow,position:"absolute",adaptive:!1,roundOffsets:l})))),e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-placement":e.placement})},data:{}};var ue={passive:!0};const fe={name:"eventListeners",enabled:!0,phase:"write",fn:function(){},effect:function(t){var e=t.state,i=t.instance,n=t.options,s=n.scroll,o=void 0===s||s,r=n.resize,a=void 0===r||r,l=Wt(e.elements.popper),c=[].concat(e.scrollParents.reference,e.scrollParents.popper);return o&&c.forEach((function(t){t.addEventListener("scroll",i.update,ue)})),a&&l.addEventListener("resize",i.update,ue),function(){o&&c.forEach((function(t){t.removeEventListener("scroll",i.update,ue)})),a&&l.removeEventListener("resize",i.update,ue)}},data:{}};var pe={left:"right",right:"left",bottom:"top",top:"bottom"};function ge(t){return t.replace(/left|right|bottom|top/g,(function(t){return pe[t]}))}var me={start:"end",end:"start"};function _e(t){return t.replace(/start|end/g,(function(t){return me[t]}))}function be(t){var e=Wt(t);return{scrollLeft:e.pageXOffset,scrollTop:e.pageYOffset}}function ve(t){return Xt(Zt(t)).left+be(t).scrollLeft}function ye(t){var e=Gt(t),i=e.overflow,n=e.overflowX,s=e.overflowY;return/auto|scroll|overlay|hidden/.test(i+s+n)}function we(t){return["html","body","#document"].indexOf($t(t))>=0?t.ownerDocument.body:Ft(t)&&ye(t)?t:we(te(t))}function Ae(t,e){var i;void 0===e&&(e=[]);var n=we(t),s=n===(null==(i=t.ownerDocument)?void 0:i.body),o=Wt(n),r=s?[o].concat(o.visualViewport||[],ye(n)?n:[]):n,a=e.concat(r);return s?a:a.concat(Ae(te(r)))}function Ee(t){return Object.assign({},t,{left:t.x,top:t.y,right:t.x+t.width,bottom:t.y+t.height})}function Te(t,e){return e===Et?Ee(function(t){var e=Wt(t),i=Zt(t),n=e.visualViewport,s=i.clientWidth,o=i.clientHeight,r=0,a=0;return n&&(s=n.width,o=n.height,/^((?!chrome|android).)*safari/i.test(navigator.userAgent)||(r=n.offsetLeft,a=n.offsetTop)),{width:s,height:o,x:r+ve(t),y:a}}(t)):Bt(e)?function(t){var e=Xt(t);return e.top=e.top+t.clientTop,e.left=e.left+t.clientLeft,e.bottom=e.top+t.clientHeight,e.right=e.left+t.clientWidth,e.width=t.clientWidth,e.height=t.clientHeight,e.x=e.left,e.y=e.top,e}(e):Ee(function(t){var e,i=Zt(t),n=be(t),s=null==(e=t.ownerDocument)?void 0:e.body,o=Vt(i.scrollWidth,i.clientWidth,s?s.scrollWidth:0,s?s.clientWidth:0),r=Vt(i.scrollHeight,i.clientHeight,s?s.scrollHeight:0,s?s.clientHeight:0),a=-n.scrollLeft+ve(t),l=-n.scrollTop;return"rtl"===Gt(s||i).direction&&(a+=Vt(i.clientWidth,s?s.clientWidth:0)-o),{width:o,height:r,x:a,y:l}}(Zt(t)))}function Ce(t){var e,i=t.reference,n=t.element,s=t.placement,o=s?qt(s):null,r=s?le(s):null,a=i.x+i.width/2-n.width/2,l=i.y+i.height/2-n.height/2;switch(o){case pt:e={x:a,y:i.y-n.height};break;case gt:e={x:a,y:i.y+i.height};break;case mt:e={x:i.x+i.width,y:l};break;case _t:e={x:i.x-n.width,y:l};break;default:e={x:i.x,y:i.y}}var c=o?ne(o):null;if(null!=c){var h="y"===c?"height":"width";switch(r){case yt:e[c]=e[c]-(i[h]/2-n[h]/2);break;case wt:e[c]=e[c]+(i[h]/2-n[h]/2)}}return e}function Oe(t,e){void 0===e&&(e={});var i=e,n=i.placement,s=void 0===n?t.placement:n,o=i.boundary,r=void 0===o?At:o,a=i.rootBoundary,l=void 0===a?Et:a,c=i.elementContext,h=void 0===c?Tt:c,d=i.altBoundary,u=void 0!==d&&d,f=i.padding,p=void 0===f?0:f,g=oe("number"!=typeof p?p:re(p,vt)),m=h===Tt?Ct:Tt,_=t.rects.popper,b=t.elements[u?m:h],v=function(t,e,i){var n="clippingParents"===e?function(t){var e=Ae(te(t)),i=["absolute","fixed"].indexOf(Gt(t).position)>=0&&Ft(t)?ie(t):t;return Bt(i)?e.filter((function(t){return Bt(t)&&Ut(t,i)&&"body"!==$t(t)})):[]}(t):[].concat(e),s=[].concat(n,[i]),o=s[0],r=s.reduce((function(e,i){var n=Te(t,i);return e.top=Vt(n.top,e.top),e.right=Kt(n.right,e.right),e.bottom=Kt(n.bottom,e.bottom),e.left=Vt(n.left,e.left),e}),Te(t,o));return r.width=r.right-r.left,r.height=r.bottom-r.top,r.x=r.left,r.y=r.top,r}(Bt(b)?b:b.contextElement||Zt(t.elements.popper),r,l),y=Xt(t.elements.reference),w=Ce({reference:y,element:_,strategy:"absolute",placement:s}),A=Ee(Object.assign({},_,w)),E=h===Tt?A:y,T={top:v.top-E.top+g.top,bottom:E.bottom-v.bottom+g.bottom,left:v.left-E.left+g.left,right:E.right-v.right+g.right},C=t.modifiersData.offset;if(h===Tt&&C){var O=C[s];Object.keys(T).forEach((function(t){var e=[mt,gt].indexOf(t)>=0?1:-1,i=[pt,gt].indexOf(t)>=0?"y":"x";T[t]+=O[i]*e}))}return T}function xe(t,e){void 0===e&&(e={});var i=e,n=i.placement,s=i.boundary,o=i.rootBoundary,r=i.padding,a=i.flipVariations,l=i.allowedAutoPlacements,c=void 0===l?xt:l,h=le(n),d=h?a?Ot:Ot.filter((function(t){return le(t)===h})):vt,u=d.filter((function(t){return c.indexOf(t)>=0}));0===u.length&&(u=d);var f=u.reduce((function(e,i){return e[i]=Oe(t,{placement:i,boundary:s,rootBoundary:o,padding:r})[qt(i)],e}),{});return Object.keys(f).sort((function(t,e){return f[t]-f[e]}))}const ke={name:"flip",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,n=t.name;if(!e.modifiersData[n]._skip){for(var s=i.mainAxis,o=void 0===s||s,r=i.altAxis,a=void 0===r||r,l=i.fallbackPlacements,c=i.padding,h=i.boundary,d=i.rootBoundary,u=i.altBoundary,f=i.flipVariations,p=void 0===f||f,g=i.allowedAutoPlacements,m=e.options.placement,_=qt(m),b=l||(_!==m&&p?function(t){if(qt(t)===bt)return[];var e=ge(t);return[_e(t),e,_e(e)]}(m):[ge(m)]),v=[m].concat(b).reduce((function(t,i){return t.concat(qt(i)===bt?xe(e,{placement:i,boundary:h,rootBoundary:d,padding:c,flipVariations:p,allowedAutoPlacements:g}):i)}),[]),y=e.rects.reference,w=e.rects.popper,A=new Map,E=!0,T=v[0],C=0;C=0,D=L?"width":"height",S=Oe(e,{placement:O,boundary:h,rootBoundary:d,altBoundary:u,padding:c}),I=L?k?mt:_t:k?gt:pt;y[D]>w[D]&&(I=ge(I));var N=ge(I),P=[];if(o&&P.push(S[x]<=0),a&&P.push(S[I]<=0,S[N]<=0),P.every((function(t){return t}))){T=O,E=!1;break}A.set(O,P)}if(E)for(var M=function(t){var e=v.find((function(e){var i=A.get(e);if(i)return i.slice(0,t).every((function(t){return t}))}));if(e)return T=e,"break"},j=p?3:1;j>0&&"break"!==M(j);j--);e.placement!==T&&(e.modifiersData[n]._skip=!0,e.placement=T,e.reset=!0)}},requiresIfExists:["offset"],data:{_skip:!1}};function Le(t,e,i){return void 0===i&&(i={x:0,y:0}),{top:t.top-e.height-i.y,right:t.right-e.width+i.x,bottom:t.bottom-e.height+i.y,left:t.left-e.width-i.x}}function De(t){return[pt,mt,gt,_t].some((function(e){return t[e]>=0}))}const Se={name:"hide",enabled:!0,phase:"main",requiresIfExists:["preventOverflow"],fn:function(t){var e=t.state,i=t.name,n=e.rects.reference,s=e.rects.popper,o=e.modifiersData.preventOverflow,r=Oe(e,{elementContext:"reference"}),a=Oe(e,{altBoundary:!0}),l=Le(r,n),c=Le(a,s,o),h=De(l),d=De(c);e.modifiersData[i]={referenceClippingOffsets:l,popperEscapeOffsets:c,isReferenceHidden:h,hasPopperEscaped:d},e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-reference-hidden":h,"data-popper-escaped":d})}},Ie={name:"offset",enabled:!0,phase:"main",requires:["popperOffsets"],fn:function(t){var e=t.state,i=t.options,n=t.name,s=i.offset,o=void 0===s?[0,0]:s,r=xt.reduce((function(t,i){return t[i]=function(t,e,i){var n=qt(t),s=[_t,pt].indexOf(n)>=0?-1:1,o="function"==typeof i?i(Object.assign({},e,{placement:t})):i,r=o[0],a=o[1];return r=r||0,a=(a||0)*s,[_t,mt].indexOf(n)>=0?{x:a,y:r}:{x:r,y:a}}(i,e.rects,o),t}),{}),a=r[e.placement],l=a.x,c=a.y;null!=e.modifiersData.popperOffsets&&(e.modifiersData.popperOffsets.x+=l,e.modifiersData.popperOffsets.y+=c),e.modifiersData[n]=r}},Ne={name:"popperOffsets",enabled:!0,phase:"read",fn:function(t){var e=t.state,i=t.name;e.modifiersData[i]=Ce({reference:e.rects.reference,element:e.rects.popper,strategy:"absolute",placement:e.placement})},data:{}},Pe={name:"preventOverflow",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,n=t.name,s=i.mainAxis,o=void 0===s||s,r=i.altAxis,a=void 0!==r&&r,l=i.boundary,c=i.rootBoundary,h=i.altBoundary,d=i.padding,u=i.tether,f=void 0===u||u,p=i.tetherOffset,g=void 0===p?0:p,m=Oe(e,{boundary:l,rootBoundary:c,padding:d,altBoundary:h}),_=qt(e.placement),b=le(e.placement),v=!b,y=ne(_),w="x"===y?"y":"x",A=e.modifiersData.popperOffsets,E=e.rects.reference,T=e.rects.popper,C="function"==typeof g?g(Object.assign({},e.rects,{placement:e.placement})):g,O="number"==typeof C?{mainAxis:C,altAxis:C}:Object.assign({mainAxis:0,altAxis:0},C),x=e.modifiersData.offset?e.modifiersData.offset[e.placement]:null,k={x:0,y:0};if(A){if(o){var L,D="y"===y?pt:_t,S="y"===y?gt:mt,I="y"===y?"height":"width",N=A[y],P=N+m[D],M=N-m[S],j=f?-T[I]/2:0,H=b===yt?E[I]:T[I],$=b===yt?-T[I]:-E[I],W=e.elements.arrow,B=f&&W?Yt(W):{width:0,height:0},F=e.modifiersData["arrow#persistent"]?e.modifiersData["arrow#persistent"].padding:{top:0,right:0,bottom:0,left:0},z=F[D],R=F[S],q=se(0,E[I],B[I]),V=v?E[I]/2-j-q-z-O.mainAxis:H-q-z-O.mainAxis,K=v?-E[I]/2+j+q+R+O.mainAxis:$+q+R+O.mainAxis,Q=e.elements.arrow&&ie(e.elements.arrow),X=Q?"y"===y?Q.clientTop||0:Q.clientLeft||0:0,Y=null!=(L=null==x?void 0:x[y])?L:0,U=N+K-Y,G=se(f?Kt(P,N+V-Y-X):P,N,f?Vt(M,U):M);A[y]=G,k[y]=G-N}if(a){var J,Z="x"===y?pt:_t,tt="x"===y?gt:mt,et=A[w],it="y"===w?"height":"width",nt=et+m[Z],st=et-m[tt],ot=-1!==[pt,_t].indexOf(_),rt=null!=(J=null==x?void 0:x[w])?J:0,at=ot?nt:et-E[it]-T[it]-rt+O.altAxis,lt=ot?et+E[it]+T[it]-rt-O.altAxis:st,ct=f&&ot?function(t,e,i){var n=se(t,e,i);return n>i?i:n}(at,et,lt):se(f?at:nt,et,f?lt:st);A[w]=ct,k[w]=ct-et}e.modifiersData[n]=k}},requiresIfExists:["offset"]};function Me(t,e,i){void 0===i&&(i=!1);var n,s,o=Ft(e),r=Ft(e)&&function(t){var e=t.getBoundingClientRect(),i=Qt(e.width)/t.offsetWidth||1,n=Qt(e.height)/t.offsetHeight||1;return 1!==i||1!==n}(e),a=Zt(e),l=Xt(t,r),c={scrollLeft:0,scrollTop:0},h={x:0,y:0};return(o||!o&&!i)&&(("body"!==$t(e)||ye(a))&&(c=(n=e)!==Wt(n)&&Ft(n)?{scrollLeft:(s=n).scrollLeft,scrollTop:s.scrollTop}:be(n)),Ft(e)?((h=Xt(e,!0)).x+=e.clientLeft,h.y+=e.clientTop):a&&(h.x=ve(a))),{x:l.left+c.scrollLeft-h.x,y:l.top+c.scrollTop-h.y,width:l.width,height:l.height}}function je(t){var e=new Map,i=new Set,n=[];function s(t){i.add(t.name),[].concat(t.requires||[],t.requiresIfExists||[]).forEach((function(t){if(!i.has(t)){var n=e.get(t);n&&s(n)}})),n.push(t)}return t.forEach((function(t){e.set(t.name,t)})),t.forEach((function(t){i.has(t.name)||s(t)})),n}var He={placement:"bottom",modifiers:[],strategy:"absolute"};function $e(){for(var t=arguments.length,e=new Array(t),i=0;iNumber.parseInt(t,10))):"function"==typeof t?e=>t(e,this._element):t}_getPopperConfig(){const t={placement:this._getPlacement(),modifiers:[{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"offset",options:{offset:this._getOffset()}}]};return(this._inNavbar||"static"===this._config.display)&&(W.setDataAttribute(this._menu,"popper","static"),t.modifiers=[{name:"applyStyles",enabled:!1}]),{...t,..."function"==typeof this._config.popperConfig?this._config.popperConfig(t):this._config.popperConfig}}_selectMenuItem({key:t,target:e}){const i=K.find(".dropdown-menu .dropdown-item:not(.disabled):not(:disabled)",this._menu).filter((t=>a(t)));i.length&&b(i,e,t===Ke,!i.includes(e)).focus()}static jQueryInterface(t){return this.each((function(){const e=ai.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}static clearMenus(t){if(2===t.button||"keyup"===t.type&&"Tab"!==t.key)return;const e=K.find(Ge);for(const i of e){const e=ai.getInstance(i);if(!e||!1===e._config.autoClose)continue;const n=t.composedPath(),s=n.includes(e._menu);if(n.includes(e._element)||"inside"===e._config.autoClose&&!s||"outside"===e._config.autoClose&&s)continue;if(e._menu.contains(t.target)&&("keyup"===t.type&&"Tab"===t.key||/input|select|option|textarea|form/i.test(t.target.tagName)))continue;const o={relatedTarget:e._element};"click"===t.type&&(o.clickEvent=t),e._completeHide(o)}}static dataApiKeydownHandler(t){const e=/input|textarea/i.test(t.target.tagName),i="Escape"===t.key,n=[Ve,Ke].includes(t.key);if(!n&&!i)return;if(e&&!i)return;t.preventDefault();const s=K.findOne(Ue,t.delegateTarget.parentNode),o=ai.getOrCreateInstance(s);if(n)return t.stopPropagation(),o.show(),void o._selectMenuItem(t);o._isShown()&&(t.stopPropagation(),o.hide(),s.focus())}}P.on(document,Xe,Ue,ai.dataApiKeydownHandler),P.on(document,Xe,Je,ai.dataApiKeydownHandler),P.on(document,Qe,ai.clearMenus),P.on(document,"keyup.bs.dropdown.data-api",ai.clearMenus),P.on(document,Qe,Ue,(function(t){t.preventDefault(),ai.getOrCreateInstance(this).toggle()})),g(ai);const li=".fixed-top, .fixed-bottom, .is-fixed, .sticky-top",ci=".sticky-top",hi="padding-right",di="margin-right";class ui{constructor(){this._element=document.body}getWidth(){const t=document.documentElement.clientWidth;return Math.abs(window.innerWidth-t)}hide(){const t=this.getWidth();this._disableOverFlow(),this._setElementAttributes(this._element,hi,(e=>e+t)),this._setElementAttributes(li,hi,(e=>e+t)),this._setElementAttributes(ci,di,(e=>e-t))}reset(){this._resetElementAttributes(this._element,"overflow"),this._resetElementAttributes(this._element,hi),this._resetElementAttributes(li,hi),this._resetElementAttributes(ci,di)}isOverflowing(){return this.getWidth()>0}_disableOverFlow(){this._saveInitialAttribute(this._element,"overflow"),this._element.style.overflow="hidden"}_setElementAttributes(t,e,i){const n=this.getWidth();this._applyManipulationCallback(t,(t=>{if(t!==this._element&&window.innerWidth>t.clientWidth+n)return;this._saveInitialAttribute(t,e);const s=window.getComputedStyle(t).getPropertyValue(e);t.style.setProperty(e,`${i(Number.parseFloat(s))}px`)}))}_saveInitialAttribute(t,e){const i=t.style.getPropertyValue(e);i&&W.setDataAttribute(t,e,i)}_resetElementAttributes(t,e){this._applyManipulationCallback(t,(t=>{const i=W.getDataAttribute(t,e);null!==i?(W.removeDataAttribute(t,e),t.style.setProperty(e,i)):t.style.removeProperty(e)}))}_applyManipulationCallback(t,e){if(o(t))e(t);else for(const i of K.find(t,this._element))e(i)}}const fi="show",pi="mousedown.bs.backdrop",gi={className:"modal-backdrop",isVisible:!0,isAnimated:!1,rootElement:"body",clickCallback:null},mi={className:"string",isVisible:"boolean",isAnimated:"boolean",rootElement:"(element|string)",clickCallback:"(function|null)"};class _i extends B{constructor(t){super(),this._config=this._getConfig(t),this._isAppended=!1,this._element=null}static get Default(){return gi}static get DefaultType(){return mi}static get NAME(){return"backdrop"}show(t){if(!this._config.isVisible)return void m(t);this._append();const e=this._getElement();this._config.isAnimated&&d(e),e.classList.add(fi),this._emulateAnimation((()=>{m(t)}))}hide(t){this._config.isVisible?(this._getElement().classList.remove(fi),this._emulateAnimation((()=>{this.dispose(),m(t)}))):m(t)}dispose(){this._isAppended&&(P.off(this._element,pi),this._element.remove(),this._isAppended=!1)}_getElement(){if(!this._element){const t=document.createElement("div");t.className=this._config.className,this._config.isAnimated&&t.classList.add("fade"),this._element=t}return this._element}_configAfterMerge(t){return t.rootElement=r(t.rootElement),t}_append(){if(this._isAppended)return;const t=this._getElement();this._config.rootElement.append(t),P.on(t,pi,(()=>{m(this._config.clickCallback)})),this._isAppended=!0}_emulateAnimation(t){_(t,this._getElement(),this._config.isAnimated)}}const bi=".bs.focustrap",vi="backward",yi={trapElement:null,autofocus:!0},wi={trapElement:"element",autofocus:"boolean"};class Ai extends B{constructor(t){super(),this._config=this._getConfig(t),this._isActive=!1,this._lastTabNavDirection=null}static get Default(){return yi}static get DefaultType(){return wi}static get NAME(){return"focustrap"}activate(){this._isActive||(this._config.autofocus&&this._config.trapElement.focus(),P.off(document,bi),P.on(document,"focusin.bs.focustrap",(t=>this._handleFocusin(t))),P.on(document,"keydown.tab.bs.focustrap",(t=>this._handleKeydown(t))),this._isActive=!0)}deactivate(){this._isActive&&(this._isActive=!1,P.off(document,bi))}_handleFocusin(t){const{trapElement:e}=this._config;if(t.target===document||t.target===e||e.contains(t.target))return;const i=K.focusableChildren(e);0===i.length?e.focus():this._lastTabNavDirection===vi?i[i.length-1].focus():i[0].focus()}_handleKeydown(t){"Tab"===t.key&&(this._lastTabNavDirection=t.shiftKey?vi:"forward")}}const Ei="hidden.bs.modal",Ti="show.bs.modal",Ci="modal-open",Oi="show",xi="modal-static",ki={backdrop:!0,keyboard:!0,focus:!0},Li={backdrop:"(boolean|string)",keyboard:"boolean",focus:"boolean"};class Di extends F{constructor(t,e){super(t,e),this._dialog=K.findOne(".modal-dialog",this._element),this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._isShown=!1,this._isTransitioning=!1,this._scrollBar=new ui,this._addEventListeners()}static get Default(){return ki}static get DefaultType(){return Li}static get NAME(){return"modal"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||this._isTransitioning||P.trigger(this._element,Ti,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._isTransitioning=!0,this._scrollBar.hide(),document.body.classList.add(Ci),this._adjustDialog(),this._backdrop.show((()=>this._showElement(t))))}hide(){this._isShown&&!this._isTransitioning&&(P.trigger(this._element,"hide.bs.modal").defaultPrevented||(this._isShown=!1,this._isTransitioning=!0,this._focustrap.deactivate(),this._element.classList.remove(Oi),this._queueCallback((()=>this._hideModal()),this._element,this._isAnimated())))}dispose(){for(const t of[window,this._dialog])P.off(t,".bs.modal");this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}handleUpdate(){this._adjustDialog()}_initializeBackDrop(){return new _i({isVisible:Boolean(this._config.backdrop),isAnimated:this._isAnimated()})}_initializeFocusTrap(){return new Ai({trapElement:this._element})}_showElement(t){document.body.contains(this._element)||document.body.append(this._element),this._element.style.display="block",this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.scrollTop=0;const e=K.findOne(".modal-body",this._dialog);e&&(e.scrollTop=0),d(this._element),this._element.classList.add(Oi),this._queueCallback((()=>{this._config.focus&&this._focustrap.activate(),this._isTransitioning=!1,P.trigger(this._element,"shown.bs.modal",{relatedTarget:t})}),this._dialog,this._isAnimated())}_addEventListeners(){P.on(this._element,"keydown.dismiss.bs.modal",(t=>{if("Escape"===t.key)return this._config.keyboard?(t.preventDefault(),void this.hide()):void this._triggerBackdropTransition()})),P.on(window,"resize.bs.modal",(()=>{this._isShown&&!this._isTransitioning&&this._adjustDialog()})),P.on(this._element,"click.dismiss.bs.modal",(t=>{t.target===t.currentTarget&&("static"!==this._config.backdrop?this._config.backdrop&&this.hide():this._triggerBackdropTransition())}))}_hideModal(){this._element.style.display="none",this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._isTransitioning=!1,this._backdrop.hide((()=>{document.body.classList.remove(Ci),this._resetAdjustments(),this._scrollBar.reset(),P.trigger(this._element,Ei)}))}_isAnimated(){return this._element.classList.contains("fade")}_triggerBackdropTransition(){if(P.trigger(this._element,"hidePrevented.bs.modal").defaultPrevented)return;const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._element.style.overflowY;"hidden"===e||this._element.classList.contains(xi)||(t||(this._element.style.overflowY="hidden"),this._element.classList.add(xi),this._queueCallback((()=>{this._element.classList.remove(xi),this._queueCallback((()=>{this._element.style.overflowY=e}),this._dialog)}),this._dialog),this._element.focus())}_adjustDialog(){const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._scrollBar.getWidth(),i=e>0;if(i&&!t){const t=p()?"paddingLeft":"paddingRight";this._element.style[t]=`${e}px`}if(!i&&t){const t=p()?"paddingRight":"paddingLeft";this._element.style[t]=`${e}px`}}_resetAdjustments(){this._element.style.paddingLeft="",this._element.style.paddingRight=""}static jQueryInterface(t,e){return this.each((function(){const i=Di.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t](e)}}))}}P.on(document,"click.bs.modal.data-api",'[data-bs-toggle="modal"]',(function(t){const e=n(this);["A","AREA"].includes(this.tagName)&&t.preventDefault(),P.one(e,Ti,(t=>{t.defaultPrevented||P.one(e,Ei,(()=>{a(this)&&this.focus()}))}));const i=K.findOne(".modal.show");i&&Di.getInstance(i).hide(),Di.getOrCreateInstance(e).toggle(this)})),z(Di),g(Di);const Si="show",Ii="showing",Ni="hiding",Pi=".offcanvas.show",Mi="hidePrevented.bs.offcanvas",ji="hidden.bs.offcanvas",Hi={backdrop:!0,keyboard:!0,scroll:!1},$i={backdrop:"(boolean|string)",keyboard:"boolean",scroll:"boolean"};class Wi extends F{constructor(t,e){super(t,e),this._isShown=!1,this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._addEventListeners()}static get Default(){return Hi}static get DefaultType(){return $i}static get NAME(){return"offcanvas"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||P.trigger(this._element,"show.bs.offcanvas",{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._backdrop.show(),this._config.scroll||(new ui).hide(),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.classList.add(Ii),this._queueCallback((()=>{this._config.scroll||this._focustrap.activate(),this._element.classList.add(Si),this._element.classList.remove(Ii),P.trigger(this._element,"shown.bs.offcanvas",{relatedTarget:t})}),this._element,!0))}hide(){this._isShown&&(P.trigger(this._element,"hide.bs.offcanvas").defaultPrevented||(this._focustrap.deactivate(),this._element.blur(),this._isShown=!1,this._element.classList.add(Ni),this._backdrop.hide(),this._queueCallback((()=>{this._element.classList.remove(Si,Ni),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._config.scroll||(new ui).reset(),P.trigger(this._element,ji)}),this._element,!0)))}dispose(){this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}_initializeBackDrop(){const t=Boolean(this._config.backdrop);return new _i({className:"offcanvas-backdrop",isVisible:t,isAnimated:!0,rootElement:this._element.parentNode,clickCallback:t?()=>{"static"!==this._config.backdrop?this.hide():P.trigger(this._element,Mi)}:null})}_initializeFocusTrap(){return new Ai({trapElement:this._element})}_addEventListeners(){P.on(this._element,"keydown.dismiss.bs.offcanvas",(t=>{"Escape"===t.key&&(this._config.keyboard?this.hide():P.trigger(this._element,Mi))}))}static jQueryInterface(t){return this.each((function(){const e=Wi.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}P.on(document,"click.bs.offcanvas.data-api",'[data-bs-toggle="offcanvas"]',(function(t){const e=n(this);if(["A","AREA"].includes(this.tagName)&&t.preventDefault(),l(this))return;P.one(e,ji,(()=>{a(this)&&this.focus()}));const i=K.findOne(Pi);i&&i!==e&&Wi.getInstance(i).hide(),Wi.getOrCreateInstance(e).toggle(this)})),P.on(window,"load.bs.offcanvas.data-api",(()=>{for(const t of K.find(Pi))Wi.getOrCreateInstance(t).show()})),P.on(window,"resize.bs.offcanvas",(()=>{for(const t of K.find("[aria-modal][class*=show][class*=offcanvas-]"))"fixed"!==getComputedStyle(t).position&&Wi.getOrCreateInstance(t).hide()})),z(Wi),g(Wi);const Bi=new Set(["background","cite","href","itemtype","longdesc","poster","src","xlink:href"]),Fi=/^(?:(?:https?|mailto|ftp|tel|file|sms):|[^#&/:?]*(?:[#/?]|$))/i,zi=/^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm)|audio\/(?:mp3|oga|ogg|opus));base64,[\d+/a-z]+=*$/i,Ri=(t,e)=>{const i=t.nodeName.toLowerCase();return e.includes(i)?!Bi.has(i)||Boolean(Fi.test(t.nodeValue)||zi.test(t.nodeValue)):e.filter((t=>t instanceof RegExp)).some((t=>t.test(i)))},qi={"*":["class","dir","id","lang","role",/^aria-[\w-]*$/i],a:["target","href","title","rel"],area:[],b:[],br:[],col:[],code:[],div:[],em:[],hr:[],h1:[],h2:[],h3:[],h4:[],h5:[],h6:[],i:[],img:["src","srcset","alt","title","width","height"],li:[],ol:[],p:[],pre:[],s:[],small:[],span:[],sub:[],sup:[],strong:[],u:[],ul:[]},Vi={extraClass:"",template:"
",content:{},html:!1,sanitize:!0,sanitizeFn:null,allowList:qi},Ki={extraClass:"(string|function)",template:"string",content:"object",html:"boolean",sanitize:"boolean",sanitizeFn:"(null|function)",allowList:"object"},Qi={selector:"(string|element)",entry:"(string|element|function|null)"};class Xi extends B{constructor(t){super(),this._config=this._getConfig(t)}static get Default(){return Vi}static get DefaultType(){return Ki}static get NAME(){return"TemplateFactory"}getContent(){return Object.values(this._config.content).map((t=>this._resolvePossibleFunction(t))).filter(Boolean)}hasContent(){return this.getContent().length>0}changeContent(t){return this._checkContent(t),this._config.content={...this._config.content,...t},this}toHtml(){const t=document.createElement("div");t.innerHTML=this._maybeSanitize(this._config.template);for(const[e,i]of Object.entries(this._config.content))this._setContent(t,i,e);const e=t.children[0],i=this._resolvePossibleFunction(this._config.extraClass);return i&&e.classList.add(...i.split(" ")),e}_typeCheckConfig(t){super._typeCheckConfig(t),this._checkContent(t.content)}_checkContent(t){for(const[e,i]of Object.entries(t))super._typeCheckConfig({selector:e,entry:i},Qi)}_setContent(t,e,i){const n=K.findOne(i,t);n&&((e=this._resolvePossibleFunction(e))?o(e)?this._putElementInTemplate(r(e),n):this._config.html?n.innerHTML=this._maybeSanitize(e):n.textContent=e:n.remove())}_maybeSanitize(t){return this._config.sanitize?function(t,e,i){if(!t.length)return t;if(i&&"function"==typeof i)return i(t);const n=(new window.DOMParser).parseFromString(t,"text/html"),s=[].concat(...n.body.querySelectorAll("*"));for(const t of s){const i=t.nodeName.toLowerCase();if(!Object.keys(e).includes(i)){t.remove();continue}const n=[].concat(...t.attributes),s=[].concat(e["*"]||[],e[i]||[]);for(const e of n)Ri(e,s)||t.removeAttribute(e.nodeName)}return n.body.innerHTML}(t,this._config.allowList,this._config.sanitizeFn):t}_resolvePossibleFunction(t){return"function"==typeof t?t(this):t}_putElementInTemplate(t,e){if(this._config.html)return e.innerHTML="",void e.append(t);e.textContent=t.textContent}}const Yi=new Set(["sanitize","allowList","sanitizeFn"]),Ui="fade",Gi="show",Ji=".modal",Zi="hide.bs.modal",tn="hover",en="focus",nn={AUTO:"auto",TOP:"top",RIGHT:p()?"left":"right",BOTTOM:"bottom",LEFT:p()?"right":"left"},sn={animation:!0,template:'',trigger:"hover focus",title:"",delay:0,html:!1,selector:!1,placement:"top",offset:[0,0],container:!1,fallbackPlacements:["top","right","bottom","left"],boundary:"clippingParents",customClass:"",sanitize:!0,sanitizeFn:null,allowList:qi,popperConfig:null},on={animation:"boolean",template:"string",title:"(string|element|function)",trigger:"string",delay:"(number|object)",html:"boolean",selector:"(string|boolean)",placement:"(string|function)",offset:"(array|string|function)",container:"(string|element|boolean)",fallbackPlacements:"array",boundary:"(string|element)",customClass:"(string|function)",sanitize:"boolean",sanitizeFn:"(null|function)",allowList:"object",popperConfig:"(null|object|function)"};class rn extends F{constructor(t,e){if(void 0===Re)throw new TypeError("Bootstrap's tooltips require Popper (https://popper.js.org)");super(t,e),this._isEnabled=!0,this._timeout=0,this._isHovered=!1,this._activeTrigger={},this._popper=null,this._templateFactory=null,this.tip=null,this._setListeners()}static get Default(){return sn}static get DefaultType(){return on}static get NAME(){return"tooltip"}enable(){this._isEnabled=!0}disable(){this._isEnabled=!1}toggleEnabled(){this._isEnabled=!this._isEnabled}toggle(t){if(this._isEnabled){if(t){const e=this._initializeOnDelegatedTarget(t);return e._activeTrigger.click=!e._activeTrigger.click,void(e._isWithActiveTrigger()?e._enter():e._leave())}this._isShown()?this._leave():this._enter()}}dispose(){clearTimeout(this._timeout),P.off(this._element.closest(Ji),Zi,this._hideModalHandler),this.tip&&this.tip.remove(),this._disposePopper(),super.dispose()}show(){if("none"===this._element.style.display)throw new Error("Please use show on visible elements");if(!this._isWithContent()||!this._isEnabled)return;const t=P.trigger(this._element,this.constructor.eventName("show")),e=(c(this._element)||this._element.ownerDocument.documentElement).contains(this._element);if(t.defaultPrevented||!e)return;const i=this._getTipElement();this._element.setAttribute("aria-describedby",i.getAttribute("id"));const{container:n}=this._config;if(this._element.ownerDocument.documentElement.contains(this.tip)||(n.append(i),P.trigger(this._element,this.constructor.eventName("inserted"))),this._popper?this._popper.update():this._createPopper(i),i.classList.add(Gi),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))P.on(t,"mouseover",h);this._queueCallback((()=>{const t=this._isHovered;this._isHovered=!1,P.trigger(this._element,this.constructor.eventName("shown")),t&&this._leave()}),this.tip,this._isAnimated())}hide(){if(!this._isShown())return;if(P.trigger(this._element,this.constructor.eventName("hide")).defaultPrevented)return;const t=this._getTipElement();if(t.classList.remove(Gi),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))P.off(t,"mouseover",h);this._activeTrigger.click=!1,this._activeTrigger.focus=!1,this._activeTrigger.hover=!1,this._isHovered=!1,this._queueCallback((()=>{this._isWithActiveTrigger()||(this._isHovered||t.remove(),this._element.removeAttribute("aria-describedby"),P.trigger(this._element,this.constructor.eventName("hidden")),this._disposePopper())}),this.tip,this._isAnimated())}update(){this._popper&&this._popper.update()}_isWithContent(){return Boolean(this._getTitle())}_getTipElement(){return this.tip||(this.tip=this._createTipElement(this._getContentForTemplate())),this.tip}_createTipElement(t){const e=this._getTemplateFactory(t).toHtml();if(!e)return null;e.classList.remove(Ui,Gi),e.classList.add(`bs-${this.constructor.NAME}-auto`);const i=(t=>{do{t+=Math.floor(1e6*Math.random())}while(document.getElementById(t));return t})(this.constructor.NAME).toString();return e.setAttribute("id",i),this._isAnimated()&&e.classList.add(Ui),e}setContent(t){let e=!1;this.tip&&(e=this._isShown(),this.tip.remove(),this.tip=null),this._disposePopper(),this.tip=this._createTipElement(t),e&&this.show()}_getTemplateFactory(t){return this._templateFactory?this._templateFactory.changeContent(t):this._templateFactory=new Xi({...this._config,content:t,extraClass:this._resolvePossibleFunction(this._config.customClass)}),this._templateFactory}_getContentForTemplate(){return{".tooltip-inner":this._getTitle()}}_getTitle(){return this._config.title}_initializeOnDelegatedTarget(t){return this.constructor.getOrCreateInstance(t.delegateTarget,this._getDelegateConfig())}_isAnimated(){return this._config.animation||this.tip&&this.tip.classList.contains(Ui)}_isShown(){return this.tip&&this.tip.classList.contains(Gi)}_createPopper(t){const e="function"==typeof this._config.placement?this._config.placement.call(this,t,this._element):this._config.placement,i=nn[e.toUpperCase()];this._popper=ze(this._element,t,this._getPopperConfig(i))}_getOffset(){const{offset:t}=this._config;return"string"==typeof t?t.split(",").map((t=>Number.parseInt(t,10))):"function"==typeof t?e=>t(e,this._element):t}_resolvePossibleFunction(t){return"function"==typeof t?t.call(this._element):t}_getPopperConfig(t){const e={placement:t,modifiers:[{name:"flip",options:{fallbackPlacements:this._config.fallbackPlacements}},{name:"offset",options:{offset:this._getOffset()}},{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"arrow",options:{element:`.${this.constructor.NAME}-arrow`}},{name:"preSetPlacement",enabled:!0,phase:"beforeMain",fn:t=>{this._getTipElement().setAttribute("data-popper-placement",t.state.placement)}}]};return{...e,..."function"==typeof this._config.popperConfig?this._config.popperConfig(e):this._config.popperConfig}}_setListeners(){const t=this._config.trigger.split(" ");for(const e of t)if("click"===e)P.on(this._element,this.constructor.eventName("click"),this._config.selector,(t=>this.toggle(t)));else if("manual"!==e){const t=e===tn?this.constructor.eventName("mouseenter"):this.constructor.eventName("focusin"),i=e===tn?this.constructor.eventName("mouseleave"):this.constructor.eventName("focusout");P.on(this._element,t,this._config.selector,(t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger["focusin"===t.type?en:tn]=!0,e._enter()})),P.on(this._element,i,this._config.selector,(t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger["focusout"===t.type?en:tn]=e._element.contains(t.relatedTarget),e._leave()}))}this._hideModalHandler=()=>{this._element&&this.hide()},P.on(this._element.closest(Ji),Zi,this._hideModalHandler),this._config.selector?this._config={...this._config,trigger:"manual",selector:""}:this._fixTitle()}_fixTitle(){const t=this._config.originalTitle;t&&(this._element.getAttribute("aria-label")||this._element.textContent||this._element.setAttribute("aria-label",t),this._element.removeAttribute("title"))}_enter(){this._isShown()||this._isHovered?this._isHovered=!0:(this._isHovered=!0,this._setTimeout((()=>{this._isHovered&&this.show()}),this._config.delay.show))}_leave(){this._isWithActiveTrigger()||(this._isHovered=!1,this._setTimeout((()=>{this._isHovered||this.hide()}),this._config.delay.hide))}_setTimeout(t,e){clearTimeout(this._timeout),this._timeout=setTimeout(t,e)}_isWithActiveTrigger(){return Object.values(this._activeTrigger).includes(!0)}_getConfig(t){const e=W.getDataAttributes(this._element);for(const t of Object.keys(e))Yi.has(t)&&delete e[t];return t={...e,..."object"==typeof t&&t?t:{}},t=this._mergeConfigObj(t),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}_configAfterMerge(t){return t.container=!1===t.container?document.body:r(t.container),"number"==typeof t.delay&&(t.delay={show:t.delay,hide:t.delay}),t.originalTitle=this._element.getAttribute("title")||"",t.title=this._resolvePossibleFunction(t.title)||t.originalTitle,"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),t}_getDelegateConfig(){const t={};for(const e in this._config)this.constructor.Default[e]!==this._config[e]&&(t[e]=this._config[e]);return t}_disposePopper(){this._popper&&(this._popper.destroy(),this._popper=null)}static jQueryInterface(t){return this.each((function(){const e=rn.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}g(rn);const an={...rn.Default,placement:"right",offset:[0,8],trigger:"click",content:"",template:''},ln={...rn.DefaultType,content:"(null|string|element|function)"};class cn extends rn{static get Default(){return an}static get DefaultType(){return ln}static get NAME(){return"popover"}_isWithContent(){return this._getTitle()||this._getContent()}_getContentForTemplate(){return{".popover-header":this._getTitle(),".popover-body":this._getContent()}}_getContent(){return this._resolvePossibleFunction(this._config.content)}static jQueryInterface(t){return this.each((function(){const e=cn.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}g(cn);const hn="click.bs.scrollspy",dn="active",un="[href]",fn={offset:null,rootMargin:"0px 0px -25%",smoothScroll:!1,target:null},pn={offset:"(number|null)",rootMargin:"string",smoothScroll:"boolean",target:"element"};class gn extends F{constructor(t,e){super(t,e),this._targetLinks=new Map,this._observableSections=new Map,this._rootElement="visible"===getComputedStyle(this._element).overflowY?null:this._element,this._activeTarget=null,this._observer=null,this._previousScrollData={visibleEntryTop:0,parentScrollTop:0},this.refresh()}static get Default(){return fn}static get DefaultType(){return pn}static get NAME(){return"scrollspy"}refresh(){this._initializeTargetsAndObservables(),this._maybeEnableSmoothScroll(),this._observer?this._observer.disconnect():this._observer=this._getNewObserver();for(const t of this._observableSections.values())this._observer.observe(t)}dispose(){this._observer.disconnect(),super.dispose()}_configAfterMerge(t){return t.target=r(t.target)||document.body,t}_maybeEnableSmoothScroll(){this._config.smoothScroll&&(P.off(this._config.target,hn),P.on(this._config.target,hn,un,(t=>{const e=this._observableSections.get(t.target.hash);if(e){t.preventDefault();const i=this._rootElement||window,n=e.offsetTop-this._element.offsetTop;if(i.scrollTo)return void i.scrollTo({top:n});i.scrollTop=n}})))}_getNewObserver(){const t={root:this._rootElement,threshold:[.1,.5,1],rootMargin:this._getRootMargin()};return new IntersectionObserver((t=>this._observerCallback(t)),t)}_observerCallback(t){const e=t=>this._targetLinks.get(`#${t.target.id}`),i=t=>{this._previousScrollData.visibleEntryTop=t.target.offsetTop,this._process(e(t))},n=(this._rootElement||document.documentElement).scrollTop,s=n>=this._previousScrollData.parentScrollTop;this._previousScrollData.parentScrollTop=n;for(const o of t){if(!o.isIntersecting){this._activeTarget=null,this._clearActiveClass(e(o));continue}const t=o.target.offsetTop>=this._previousScrollData.visibleEntryTop;if(s&&t){if(i(o),!n)return}else s||t||i(o)}}_getRootMargin(){return this._config.offset?`${this._config.offset}px 0px -30%`:this._config.rootMargin}_initializeTargetsAndObservables(){this._targetLinks=new Map,this._observableSections=new Map;const t=K.find(un,this._config.target);for(const e of t){if(!e.hash||l(e))continue;const t=K.findOne(e.hash,this._element);a(t)&&(this._targetLinks.set(e.hash,e),this._observableSections.set(e.hash,t))}}_process(t){this._activeTarget!==t&&(this._clearActiveClass(this._config.target),this._activeTarget=t,t.classList.add(dn),this._activateParents(t),P.trigger(this._element,"activate.bs.scrollspy",{relatedTarget:t}))}_activateParents(t){if(t.classList.contains("dropdown-item"))K.findOne(".dropdown-toggle",t.closest(".dropdown")).classList.add(dn);else for(const e of K.parents(t,".nav, .list-group"))for(const t of K.prev(e,".nav-link, .nav-item > .nav-link, .list-group-item"))t.classList.add(dn)}_clearActiveClass(t){t.classList.remove(dn);const e=K.find("[href].active",t);for(const t of e)t.classList.remove(dn)}static jQueryInterface(t){return this.each((function(){const e=gn.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}))}}P.on(window,"load.bs.scrollspy.data-api",(()=>{for(const t of K.find('[data-bs-spy="scroll"]'))gn.getOrCreateInstance(t)})),g(gn);const mn="ArrowLeft",_n="ArrowRight",bn="ArrowUp",vn="ArrowDown",yn="active",wn="fade",An="show",En='[data-bs-toggle="tab"], [data-bs-toggle="pill"], [data-bs-toggle="list"]',Tn=`.nav-link:not(.dropdown-toggle), .list-group-item:not(.dropdown-toggle), [role="tab"]:not(.dropdown-toggle), ${En}`;class Cn extends F{constructor(t){super(t),this._parent=this._element.closest('.list-group, .nav, [role="tablist"]'),this._parent&&(this._setInitialAttributes(this._parent,this._getChildren()),P.on(this._element,"keydown.bs.tab",(t=>this._keydown(t))))}static get NAME(){return"tab"}show(){const t=this._element;if(this._elemIsActive(t))return;const e=this._getActiveElem(),i=e?P.trigger(e,"hide.bs.tab",{relatedTarget:t}):null;P.trigger(t,"show.bs.tab",{relatedTarget:e}).defaultPrevented||i&&i.defaultPrevented||(this._deactivate(e,t),this._activate(t,e))}_activate(t,e){if(!t)return;t.classList.add(yn),this._activate(n(t));const i=t.classList.contains(wn);this._queueCallback((()=>{i&&t.classList.add(An),"tab"===t.getAttribute("role")&&(t.focus(),t.removeAttribute("tabindex"),t.setAttribute("aria-selected",!0),this._toggleDropDown(t,!0),P.trigger(t,"shown.bs.tab",{relatedTarget:e}))}),t,i)}_deactivate(t,e){if(!t)return;t.classList.remove(yn),t.blur(),this._deactivate(n(t));const i=t.classList.contains(wn);this._queueCallback((()=>{i&&t.classList.remove(An),"tab"===t.getAttribute("role")&&(t.setAttribute("aria-selected",!1),t.setAttribute("tabindex","-1"),this._toggleDropDown(t,!1),P.trigger(t,"hidden.bs.tab",{relatedTarget:e}))}),t,i)}_keydown(t){if(![mn,_n,bn,vn].includes(t.key))return;t.stopPropagation(),t.preventDefault();const e=[_n,vn].includes(t.key),i=b(this._getChildren().filter((t=>!l(t))),t.target,e,!0);i&&Cn.getOrCreateInstance(i).show()}_getChildren(){return K.find(Tn,this._parent)}_getActiveElem(){return this._getChildren().find((t=>this._elemIsActive(t)))||null}_setInitialAttributes(t,e){this._setAttributeIfNotExists(t,"role","tablist");for(const t of e)this._setInitialAttributesOnChild(t)}_setInitialAttributesOnChild(t){t=this._getInnerElement(t);const e=this._elemIsActive(t),i=this._getOuterElement(t);t.setAttribute("aria-selected",e),i!==t&&this._setAttributeIfNotExists(i,"role","presentation"),e||t.setAttribute("tabindex","-1"),this._setAttributeIfNotExists(t,"role","tab"),this._setInitialAttributesOnTargetPanel(t)}_setInitialAttributesOnTargetPanel(t){const e=n(t);e&&(this._setAttributeIfNotExists(e,"role","tabpanel"),t.id&&this._setAttributeIfNotExists(e,"aria-labelledby",`#${t.id}`))}_toggleDropDown(t,e){const i=this._getOuterElement(t);if(!i.classList.contains("dropdown"))return;const n=(t,n)=>{const s=K.findOne(t,i);s&&s.classList.toggle(n,e)};n(".dropdown-toggle",yn),n(".dropdown-menu",An),n(".dropdown-item",yn),i.setAttribute("aria-expanded",e)}_setAttributeIfNotExists(t,e,i){t.hasAttribute(e)||t.setAttribute(e,i)}_elemIsActive(t){return t.classList.contains(yn)}_getInnerElement(t){return t.matches(Tn)?t:K.findOne(Tn,t)}_getOuterElement(t){return t.closest(".nav-item, .list-group-item")||t}static jQueryInterface(t){return this.each((function(){const e=Cn.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}))}}P.on(document,"click.bs.tab",En,(function(t){["A","AREA"].includes(this.tagName)&&t.preventDefault(),l(this)||Cn.getOrCreateInstance(this).show()})),P.on(window,"load.bs.tab",(()=>{for(const t of K.find('.active[data-bs-toggle="tab"], .active[data-bs-toggle="pill"], .active[data-bs-toggle="list"]'))Cn.getOrCreateInstance(t)})),g(Cn);const On="hide",xn="show",kn="showing",Ln={animation:"boolean",autohide:"boolean",delay:"number"},Dn={animation:!0,autohide:!0,delay:5e3};class Sn extends F{constructor(t,e){super(t,e),this._timeout=null,this._hasMouseInteraction=!1,this._hasKeyboardInteraction=!1,this._setListeners()}static get Default(){return Dn}static get DefaultType(){return Ln}static get NAME(){return"toast"}show(){P.trigger(this._element,"show.bs.toast").defaultPrevented||(this._clearTimeout(),this._config.animation&&this._element.classList.add("fade"),this._element.classList.remove(On),d(this._element),this._element.classList.add(xn,kn),this._queueCallback((()=>{this._element.classList.remove(kn),P.trigger(this._element,"shown.bs.toast"),this._maybeScheduleHide()}),this._element,this._config.animation))}hide(){this.isShown()&&(P.trigger(this._element,"hide.bs.toast").defaultPrevented||(this._element.classList.add(kn),this._queueCallback((()=>{this._element.classList.add(On),this._element.classList.remove(kn,xn),P.trigger(this._element,"hidden.bs.toast")}),this._element,this._config.animation)))}dispose(){this._clearTimeout(),this.isShown()&&this._element.classList.remove(xn),super.dispose()}isShown(){return this._element.classList.contains(xn)}_maybeScheduleHide(){this._config.autohide&&(this._hasMouseInteraction||this._hasKeyboardInteraction||(this._timeout=setTimeout((()=>{this.hide()}),this._config.delay)))}_onInteraction(t,e){switch(t.type){case"mouseover":case"mouseout":this._hasMouseInteraction=e;break;case"focusin":case"focusout":this._hasKeyboardInteraction=e}if(e)return void this._clearTimeout();const i=t.relatedTarget;this._element===i||this._element.contains(i)||this._maybeScheduleHide()}_setListeners(){P.on(this._element,"mouseover.bs.toast",(t=>this._onInteraction(t,!0))),P.on(this._element,"mouseout.bs.toast",(t=>this._onInteraction(t,!1))),P.on(this._element,"focusin.bs.toast",(t=>this._onInteraction(t,!0))),P.on(this._element,"focusout.bs.toast",(t=>this._onInteraction(t,!1)))}_clearTimeout(){clearTimeout(this._timeout),this._timeout=null}static jQueryInterface(t){return this.each((function(){const e=Sn.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}return z(Sn),g(Sn),{Alert:R,Button:V,Carousel:rt,Collapse:ft,Dropdown:ai,Modal:Di,Offcanvas:Wi,Popover:cn,ScrollSpy:gn,Tab:Cn,Toast:Sn,Tooltip:rn}})); +//# sourceMappingURL=bootstrap.bundle.min.js.map \ No newline at end of file diff --git a/src/boardwalkd/static/bootstrap.min.css b/src/boardwalkd/static/bootstrap.min.css new file mode 100644 index 0000000..6f93a18 --- /dev/null +++ b/src/boardwalkd/static/bootstrap.min.css @@ -0,0 +1,7 @@ +@charset "UTF-8";/*! + * Bootstrap v5.2.0-beta1 (https://getbootstrap.com/) + * Copyright 2011-2022 The Bootstrap Authors + * Copyright 2011-2022 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */:root{--bs-blue:#0d6efd;--bs-indigo:#6610f2;--bs-purple:#6f42c1;--bs-pink:#d63384;--bs-red:#dc3545;--bs-orange:#fd7e14;--bs-yellow:#ffc107;--bs-green:#198754;--bs-teal:#20c997;--bs-cyan:#0dcaf0;--bs-black:#000;--bs-white:#fff;--bs-gray:#6c757d;--bs-gray-dark:#343a40;--bs-gray-100:#f8f9fa;--bs-gray-200:#e9ecef;--bs-gray-300:#dee2e6;--bs-gray-400:#ced4da;--bs-gray-500:#adb5bd;--bs-gray-600:#6c757d;--bs-gray-700:#495057;--bs-gray-800:#343a40;--bs-gray-900:#212529;--bs-primary:#0d6efd;--bs-secondary:#6c757d;--bs-success:#198754;--bs-info:#0dcaf0;--bs-warning:#ffc107;--bs-danger:#dc3545;--bs-light:#f8f9fa;--bs-dark:#212529;--bs-primary-rgb:13,110,253;--bs-secondary-rgb:108,117,125;--bs-success-rgb:25,135,84;--bs-info-rgb:13,202,240;--bs-warning-rgb:255,193,7;--bs-danger-rgb:220,53,69;--bs-light-rgb:248,249,250;--bs-dark-rgb:33,37,41;--bs-white-rgb:255,255,255;--bs-black-rgb:0,0,0;--bs-body-color-rgb:33,37,41;--bs-body-bg-rgb:255,255,255;--bs-font-sans-serif:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue","Noto Sans","Liberation Sans",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--bs-font-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--bs-gradient:linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));--bs-body-font-family:var(--bs-font-sans-serif);--bs-body-font-size:1rem;--bs-body-font-weight:400;--bs-body-line-height:1.5;--bs-body-color:#212529;--bs-body-bg:#fff;--bs-border-width:1px;--bs-border-style:solid;--bs-border-color:#dee2e6;--bs-border-color-translucent:rgba(0, 0, 0, 0.175);--bs-border-radius:0.375rem;--bs-border-radius-sm:0.25rem;--bs-border-radius-lg:0.5rem;--bs-border-radius-xl:1rem;--bs-border-radius-2xl:2rem;--bs-border-radius-pill:50rem;--bs-heading-color: ;--bs-link-color:#0d6efd;--bs-link-hover-color:#0a58ca;--bs-code-color:#d63384;--bs-highlight-bg:#fff3cd}*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;border:0;border-top:1px solid;opacity:.25}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2;color:var(--bs-heading-color)}.h1,h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){.h1,h1{font-size:2.5rem}}.h2,h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){.h2,h2{font-size:2rem}}.h3,h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){.h3,h3{font-size:1.75rem}}.h4,h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){.h4,h4{font-size:1.5rem}}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}.small,small{font-size:.875em}.mark,mark{padding:.1875em;background-color:var(--bs-highlight-bg)}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:var(--bs-link-color);text-decoration:underline}a:hover{color:var(--bs-link-hover-color)}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:var(--bs-font-monospace);font-size:1em}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:var(--bs-code-color);word-wrap:break-word}a>code{color:inherit}kbd{padding:.1875rem .375rem;font-size:.875em;color:var(--bs-body-bg);background-color:var(--bs-body-color);border-radius:.25rem}kbd kbd{padding:0;font-size:1em}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:rgba(var(--bs-body-color-rgb),.75);text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator{display:none!important}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}::file-selector-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:calc(1.625rem + 4.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-1{font-size:5rem}}.display-2{font-size:calc(1.575rem + 3.9vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-2{font-size:4.5rem}}.display-3{font-size:calc(1.525rem + 3.3vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-3{font-size:4rem}}.display-4{font-size:calc(1.475rem + 2.7vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-4{font-size:3.5rem}}.display-5{font-size:calc(1.425rem + 2.1vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-5{font-size:3rem}}.display-6{font-size:calc(1.375rem + 1.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-6{font-size:2.5rem}}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:.875em;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote>:last-child{margin-bottom:0}.blockquote-footer{margin-top:-1rem;margin-bottom:1rem;font-size:.875em;color:#6c757d}.blockquote-footer::before{content:"— "}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#fff;border:1px solid var(--bs-border-color);border-radius:.375rem;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:.875em;color:#6c757d}.container,.container-fluid,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{--bs-gutter-x:1.5rem;--bs-gutter-y:0;width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-right:auto;margin-left:auto}@media (min-width:576px){.container,.container-sm{max-width:540px}}@media (min-width:768px){.container,.container-md,.container-sm{max-width:720px}}@media (min-width:992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media (min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}@media (min-width:1400px){.container,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{max-width:1320px}}.row{--bs-gutter-x:1.5rem;--bs-gutter-y:0;display:flex;flex-wrap:wrap;margin-top:calc(-1 * var(--bs-gutter-y));margin-right:calc(-.5 * var(--bs-gutter-x));margin-left:calc(-.5 * var(--bs-gutter-x))}.row>*{flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-top:var(--bs-gutter-y)}.col{flex:1 0 0%}.row-cols-auto>*{flex:0 0 auto;width:auto}.row-cols-1>*{flex:0 0 auto;width:100%}.row-cols-2>*{flex:0 0 auto;width:50%}.row-cols-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-4>*{flex:0 0 auto;width:25%}.row-cols-5>*{flex:0 0 auto;width:20%}.row-cols-6>*{flex:0 0 auto;width:16.6666666667%}.col-auto{flex:0 0 auto;width:auto}.col-1{flex:0 0 auto;width:8.33333333%}.col-2{flex:0 0 auto;width:16.66666667%}.col-3{flex:0 0 auto;width:25%}.col-4{flex:0 0 auto;width:33.33333333%}.col-5{flex:0 0 auto;width:41.66666667%}.col-6{flex:0 0 auto;width:50%}.col-7{flex:0 0 auto;width:58.33333333%}.col-8{flex:0 0 auto;width:66.66666667%}.col-9{flex:0 0 auto;width:75%}.col-10{flex:0 0 auto;width:83.33333333%}.col-11{flex:0 0 auto;width:91.66666667%}.col-12{flex:0 0 auto;width:100%}.offset-1{margin-left:8.33333333%}.offset-2{margin-left:16.66666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.33333333%}.offset-5{margin-left:41.66666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.33333333%}.offset-8{margin-left:66.66666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.33333333%}.offset-11{margin-left:91.66666667%}.g-0,.gx-0{--bs-gutter-x:0}.g-0,.gy-0{--bs-gutter-y:0}.g-1,.gx-1{--bs-gutter-x:0.25rem}.g-1,.gy-1{--bs-gutter-y:0.25rem}.g-2,.gx-2{--bs-gutter-x:0.5rem}.g-2,.gy-2{--bs-gutter-y:0.5rem}.g-3,.gx-3{--bs-gutter-x:1rem}.g-3,.gy-3{--bs-gutter-y:1rem}.g-4,.gx-4{--bs-gutter-x:1.5rem}.g-4,.gy-4{--bs-gutter-y:1.5rem}.g-5,.gx-5{--bs-gutter-x:3rem}.g-5,.gy-5{--bs-gutter-y:3rem}@media (min-width:576px){.col-sm{flex:1 0 0%}.row-cols-sm-auto>*{flex:0 0 auto;width:auto}.row-cols-sm-1>*{flex:0 0 auto;width:100%}.row-cols-sm-2>*{flex:0 0 auto;width:50%}.row-cols-sm-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-sm-4>*{flex:0 0 auto;width:25%}.row-cols-sm-5>*{flex:0 0 auto;width:20%}.row-cols-sm-6>*{flex:0 0 auto;width:16.6666666667%}.col-sm-auto{flex:0 0 auto;width:auto}.col-sm-1{flex:0 0 auto;width:8.33333333%}.col-sm-2{flex:0 0 auto;width:16.66666667%}.col-sm-3{flex:0 0 auto;width:25%}.col-sm-4{flex:0 0 auto;width:33.33333333%}.col-sm-5{flex:0 0 auto;width:41.66666667%}.col-sm-6{flex:0 0 auto;width:50%}.col-sm-7{flex:0 0 auto;width:58.33333333%}.col-sm-8{flex:0 0 auto;width:66.66666667%}.col-sm-9{flex:0 0 auto;width:75%}.col-sm-10{flex:0 0 auto;width:83.33333333%}.col-sm-11{flex:0 0 auto;width:91.66666667%}.col-sm-12{flex:0 0 auto;width:100%}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.33333333%}.offset-sm-2{margin-left:16.66666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.33333333%}.offset-sm-5{margin-left:41.66666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.33333333%}.offset-sm-8{margin-left:66.66666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.33333333%}.offset-sm-11{margin-left:91.66666667%}.g-sm-0,.gx-sm-0{--bs-gutter-x:0}.g-sm-0,.gy-sm-0{--bs-gutter-y:0}.g-sm-1,.gx-sm-1{--bs-gutter-x:0.25rem}.g-sm-1,.gy-sm-1{--bs-gutter-y:0.25rem}.g-sm-2,.gx-sm-2{--bs-gutter-x:0.5rem}.g-sm-2,.gy-sm-2{--bs-gutter-y:0.5rem}.g-sm-3,.gx-sm-3{--bs-gutter-x:1rem}.g-sm-3,.gy-sm-3{--bs-gutter-y:1rem}.g-sm-4,.gx-sm-4{--bs-gutter-x:1.5rem}.g-sm-4,.gy-sm-4{--bs-gutter-y:1.5rem}.g-sm-5,.gx-sm-5{--bs-gutter-x:3rem}.g-sm-5,.gy-sm-5{--bs-gutter-y:3rem}}@media (min-width:768px){.col-md{flex:1 0 0%}.row-cols-md-auto>*{flex:0 0 auto;width:auto}.row-cols-md-1>*{flex:0 0 auto;width:100%}.row-cols-md-2>*{flex:0 0 auto;width:50%}.row-cols-md-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-md-4>*{flex:0 0 auto;width:25%}.row-cols-md-5>*{flex:0 0 auto;width:20%}.row-cols-md-6>*{flex:0 0 auto;width:16.6666666667%}.col-md-auto{flex:0 0 auto;width:auto}.col-md-1{flex:0 0 auto;width:8.33333333%}.col-md-2{flex:0 0 auto;width:16.66666667%}.col-md-3{flex:0 0 auto;width:25%}.col-md-4{flex:0 0 auto;width:33.33333333%}.col-md-5{flex:0 0 auto;width:41.66666667%}.col-md-6{flex:0 0 auto;width:50%}.col-md-7{flex:0 0 auto;width:58.33333333%}.col-md-8{flex:0 0 auto;width:66.66666667%}.col-md-9{flex:0 0 auto;width:75%}.col-md-10{flex:0 0 auto;width:83.33333333%}.col-md-11{flex:0 0 auto;width:91.66666667%}.col-md-12{flex:0 0 auto;width:100%}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.33333333%}.offset-md-2{margin-left:16.66666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.33333333%}.offset-md-5{margin-left:41.66666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.33333333%}.offset-md-8{margin-left:66.66666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.33333333%}.offset-md-11{margin-left:91.66666667%}.g-md-0,.gx-md-0{--bs-gutter-x:0}.g-md-0,.gy-md-0{--bs-gutter-y:0}.g-md-1,.gx-md-1{--bs-gutter-x:0.25rem}.g-md-1,.gy-md-1{--bs-gutter-y:0.25rem}.g-md-2,.gx-md-2{--bs-gutter-x:0.5rem}.g-md-2,.gy-md-2{--bs-gutter-y:0.5rem}.g-md-3,.gx-md-3{--bs-gutter-x:1rem}.g-md-3,.gy-md-3{--bs-gutter-y:1rem}.g-md-4,.gx-md-4{--bs-gutter-x:1.5rem}.g-md-4,.gy-md-4{--bs-gutter-y:1.5rem}.g-md-5,.gx-md-5{--bs-gutter-x:3rem}.g-md-5,.gy-md-5{--bs-gutter-y:3rem}}@media (min-width:992px){.col-lg{flex:1 0 0%}.row-cols-lg-auto>*{flex:0 0 auto;width:auto}.row-cols-lg-1>*{flex:0 0 auto;width:100%}.row-cols-lg-2>*{flex:0 0 auto;width:50%}.row-cols-lg-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-lg-4>*{flex:0 0 auto;width:25%}.row-cols-lg-5>*{flex:0 0 auto;width:20%}.row-cols-lg-6>*{flex:0 0 auto;width:16.6666666667%}.col-lg-auto{flex:0 0 auto;width:auto}.col-lg-1{flex:0 0 auto;width:8.33333333%}.col-lg-2{flex:0 0 auto;width:16.66666667%}.col-lg-3{flex:0 0 auto;width:25%}.col-lg-4{flex:0 0 auto;width:33.33333333%}.col-lg-5{flex:0 0 auto;width:41.66666667%}.col-lg-6{flex:0 0 auto;width:50%}.col-lg-7{flex:0 0 auto;width:58.33333333%}.col-lg-8{flex:0 0 auto;width:66.66666667%}.col-lg-9{flex:0 0 auto;width:75%}.col-lg-10{flex:0 0 auto;width:83.33333333%}.col-lg-11{flex:0 0 auto;width:91.66666667%}.col-lg-12{flex:0 0 auto;width:100%}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.33333333%}.offset-lg-2{margin-left:16.66666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.33333333%}.offset-lg-5{margin-left:41.66666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.33333333%}.offset-lg-8{margin-left:66.66666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.33333333%}.offset-lg-11{margin-left:91.66666667%}.g-lg-0,.gx-lg-0{--bs-gutter-x:0}.g-lg-0,.gy-lg-0{--bs-gutter-y:0}.g-lg-1,.gx-lg-1{--bs-gutter-x:0.25rem}.g-lg-1,.gy-lg-1{--bs-gutter-y:0.25rem}.g-lg-2,.gx-lg-2{--bs-gutter-x:0.5rem}.g-lg-2,.gy-lg-2{--bs-gutter-y:0.5rem}.g-lg-3,.gx-lg-3{--bs-gutter-x:1rem}.g-lg-3,.gy-lg-3{--bs-gutter-y:1rem}.g-lg-4,.gx-lg-4{--bs-gutter-x:1.5rem}.g-lg-4,.gy-lg-4{--bs-gutter-y:1.5rem}.g-lg-5,.gx-lg-5{--bs-gutter-x:3rem}.g-lg-5,.gy-lg-5{--bs-gutter-y:3rem}}@media (min-width:1200px){.col-xl{flex:1 0 0%}.row-cols-xl-auto>*{flex:0 0 auto;width:auto}.row-cols-xl-1>*{flex:0 0 auto;width:100%}.row-cols-xl-2>*{flex:0 0 auto;width:50%}.row-cols-xl-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-xl-4>*{flex:0 0 auto;width:25%}.row-cols-xl-5>*{flex:0 0 auto;width:20%}.row-cols-xl-6>*{flex:0 0 auto;width:16.6666666667%}.col-xl-auto{flex:0 0 auto;width:auto}.col-xl-1{flex:0 0 auto;width:8.33333333%}.col-xl-2{flex:0 0 auto;width:16.66666667%}.col-xl-3{flex:0 0 auto;width:25%}.col-xl-4{flex:0 0 auto;width:33.33333333%}.col-xl-5{flex:0 0 auto;width:41.66666667%}.col-xl-6{flex:0 0 auto;width:50%}.col-xl-7{flex:0 0 auto;width:58.33333333%}.col-xl-8{flex:0 0 auto;width:66.66666667%}.col-xl-9{flex:0 0 auto;width:75%}.col-xl-10{flex:0 0 auto;width:83.33333333%}.col-xl-11{flex:0 0 auto;width:91.66666667%}.col-xl-12{flex:0 0 auto;width:100%}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.33333333%}.offset-xl-2{margin-left:16.66666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.33333333%}.offset-xl-5{margin-left:41.66666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.33333333%}.offset-xl-8{margin-left:66.66666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.33333333%}.offset-xl-11{margin-left:91.66666667%}.g-xl-0,.gx-xl-0{--bs-gutter-x:0}.g-xl-0,.gy-xl-0{--bs-gutter-y:0}.g-xl-1,.gx-xl-1{--bs-gutter-x:0.25rem}.g-xl-1,.gy-xl-1{--bs-gutter-y:0.25rem}.g-xl-2,.gx-xl-2{--bs-gutter-x:0.5rem}.g-xl-2,.gy-xl-2{--bs-gutter-y:0.5rem}.g-xl-3,.gx-xl-3{--bs-gutter-x:1rem}.g-xl-3,.gy-xl-3{--bs-gutter-y:1rem}.g-xl-4,.gx-xl-4{--bs-gutter-x:1.5rem}.g-xl-4,.gy-xl-4{--bs-gutter-y:1.5rem}.g-xl-5,.gx-xl-5{--bs-gutter-x:3rem}.g-xl-5,.gy-xl-5{--bs-gutter-y:3rem}}@media (min-width:1400px){.col-xxl{flex:1 0 0%}.row-cols-xxl-auto>*{flex:0 0 auto;width:auto}.row-cols-xxl-1>*{flex:0 0 auto;width:100%}.row-cols-xxl-2>*{flex:0 0 auto;width:50%}.row-cols-xxl-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-xxl-4>*{flex:0 0 auto;width:25%}.row-cols-xxl-5>*{flex:0 0 auto;width:20%}.row-cols-xxl-6>*{flex:0 0 auto;width:16.6666666667%}.col-xxl-auto{flex:0 0 auto;width:auto}.col-xxl-1{flex:0 0 auto;width:8.33333333%}.col-xxl-2{flex:0 0 auto;width:16.66666667%}.col-xxl-3{flex:0 0 auto;width:25%}.col-xxl-4{flex:0 0 auto;width:33.33333333%}.col-xxl-5{flex:0 0 auto;width:41.66666667%}.col-xxl-6{flex:0 0 auto;width:50%}.col-xxl-7{flex:0 0 auto;width:58.33333333%}.col-xxl-8{flex:0 0 auto;width:66.66666667%}.col-xxl-9{flex:0 0 auto;width:75%}.col-xxl-10{flex:0 0 auto;width:83.33333333%}.col-xxl-11{flex:0 0 auto;width:91.66666667%}.col-xxl-12{flex:0 0 auto;width:100%}.offset-xxl-0{margin-left:0}.offset-xxl-1{margin-left:8.33333333%}.offset-xxl-2{margin-left:16.66666667%}.offset-xxl-3{margin-left:25%}.offset-xxl-4{margin-left:33.33333333%}.offset-xxl-5{margin-left:41.66666667%}.offset-xxl-6{margin-left:50%}.offset-xxl-7{margin-left:58.33333333%}.offset-xxl-8{margin-left:66.66666667%}.offset-xxl-9{margin-left:75%}.offset-xxl-10{margin-left:83.33333333%}.offset-xxl-11{margin-left:91.66666667%}.g-xxl-0,.gx-xxl-0{--bs-gutter-x:0}.g-xxl-0,.gy-xxl-0{--bs-gutter-y:0}.g-xxl-1,.gx-xxl-1{--bs-gutter-x:0.25rem}.g-xxl-1,.gy-xxl-1{--bs-gutter-y:0.25rem}.g-xxl-2,.gx-xxl-2{--bs-gutter-x:0.5rem}.g-xxl-2,.gy-xxl-2{--bs-gutter-y:0.5rem}.g-xxl-3,.gx-xxl-3{--bs-gutter-x:1rem}.g-xxl-3,.gy-xxl-3{--bs-gutter-y:1rem}.g-xxl-4,.gx-xxl-4{--bs-gutter-x:1.5rem}.g-xxl-4,.gy-xxl-4{--bs-gutter-y:1.5rem}.g-xxl-5,.gx-xxl-5{--bs-gutter-x:3rem}.g-xxl-5,.gy-xxl-5{--bs-gutter-y:3rem}}.table{--bs-table-color:var(--bs-body-color);--bs-table-bg:transparent;--bs-table-border-color:var(--bs-border-color);--bs-table-accent-bg:transparent;--bs-table-striped-color:var(--bs-body-color);--bs-table-striped-bg:rgba(0, 0, 0, 0.05);--bs-table-active-color:var(--bs-body-color);--bs-table-active-bg:rgba(0, 0, 0, 0.1);--bs-table-hover-color:var(--bs-body-color);--bs-table-hover-bg:rgba(0, 0, 0, 0.075);width:100%;margin-bottom:1rem;color:var(--bs-table-color);vertical-align:top;border-color:var(--bs-table-border-color)}.table>:not(caption)>*>*{padding:.5rem .5rem;background-color:var(--bs-table-bg);border-bottom-width:1px;box-shadow:inset 0 0 0 9999px var(--bs-table-accent-bg)}.table>tbody{vertical-align:inherit}.table>thead{vertical-align:bottom}.table-group-divider{border-top:2px solid currentcolor}.caption-top{caption-side:top}.table-sm>:not(caption)>*>*{padding:.25rem .25rem}.table-bordered>:not(caption)>*{border-width:1px 0}.table-bordered>:not(caption)>*>*{border-width:0 1px}.table-borderless>:not(caption)>*>*{border-bottom-width:0}.table-borderless>:not(:first-child){border-top-width:0}.table-striped>tbody>tr:nth-of-type(odd)>*{--bs-table-accent-bg:var(--bs-table-striped-bg);color:var(--bs-table-striped-color)}.table-striped-columns>:not(caption)>tr>:nth-child(2n){--bs-table-accent-bg:var(--bs-table-striped-bg);color:var(--bs-table-striped-color)}.table-active{--bs-table-accent-bg:var(--bs-table-active-bg);color:var(--bs-table-active-color)}.table-hover>tbody>tr:hover>*{--bs-table-accent-bg:var(--bs-table-hover-bg);color:var(--bs-table-hover-color)}.table-primary{--bs-table-color:#000;--bs-table-bg:#cfe2ff;--bs-table-border-color:#bacbe6;--bs-table-striped-bg:#c5d7f2;--bs-table-striped-color:#000;--bs-table-active-bg:#bacbe6;--bs-table-active-color:#000;--bs-table-hover-bg:#bfd1ec;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-secondary{--bs-table-color:#000;--bs-table-bg:#e2e3e5;--bs-table-border-color:#cbccce;--bs-table-striped-bg:#d7d8da;--bs-table-striped-color:#000;--bs-table-active-bg:#cbccce;--bs-table-active-color:#000;--bs-table-hover-bg:#d1d2d4;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-success{--bs-table-color:#000;--bs-table-bg:#d1e7dd;--bs-table-border-color:#bcd0c7;--bs-table-striped-bg:#c7dbd2;--bs-table-striped-color:#000;--bs-table-active-bg:#bcd0c7;--bs-table-active-color:#000;--bs-table-hover-bg:#c1d6cc;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-info{--bs-table-color:#000;--bs-table-bg:#cff4fc;--bs-table-border-color:#badce3;--bs-table-striped-bg:#c5e8ef;--bs-table-striped-color:#000;--bs-table-active-bg:#badce3;--bs-table-active-color:#000;--bs-table-hover-bg:#bfe2e9;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-warning{--bs-table-color:#000;--bs-table-bg:#fff3cd;--bs-table-border-color:#e6dbb9;--bs-table-striped-bg:#f2e7c3;--bs-table-striped-color:#000;--bs-table-active-bg:#e6dbb9;--bs-table-active-color:#000;--bs-table-hover-bg:#ece1be;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-danger{--bs-table-color:#000;--bs-table-bg:#f8d7da;--bs-table-border-color:#dfc2c4;--bs-table-striped-bg:#eccccf;--bs-table-striped-color:#000;--bs-table-active-bg:#dfc2c4;--bs-table-active-color:#000;--bs-table-hover-bg:#e5c7ca;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-light{--bs-table-color:#000;--bs-table-bg:#f8f9fa;--bs-table-border-color:#dfe0e1;--bs-table-striped-bg:#ecedee;--bs-table-striped-color:#000;--bs-table-active-bg:#dfe0e1;--bs-table-active-color:#000;--bs-table-hover-bg:#e5e6e7;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-dark{--bs-table-color:#fff;--bs-table-bg:#212529;--bs-table-border-color:#373b3e;--bs-table-striped-bg:#2c3034;--bs-table-striped-color:#fff;--bs-table-active-bg:#373b3e;--bs-table-active-color:#fff;--bs-table-hover-bg:#323539;--bs-table-hover-color:#fff;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-responsive{overflow-x:auto;-webkit-overflow-scrolling:touch}@media (max-width:575.98px){.table-responsive-sm{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:767.98px){.table-responsive-md{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:991.98px){.table-responsive-lg{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1199.98px){.table-responsive-xl{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1399.98px){.table-responsive-xxl{overflow-x:auto;-webkit-overflow-scrolling:touch}}.form-label{margin-bottom:.5rem}.col-form-label{padding-top:calc(.375rem + 1px);padding-bottom:calc(.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + 1px);padding-bottom:calc(.5rem + 1px);font-size:1.25rem}.col-form-label-sm{padding-top:calc(.25rem + 1px);padding-bottom:calc(.25rem + 1px);font-size:.875rem}.form-text{margin-top:.25rem;font-size:.875em;color:rgba(var(--bs-body-color-rgb),.75)}.form-control{display:block;width:100%;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;-webkit-appearance:none;-moz-appearance:none;appearance:none;border-radius:.375rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control[type=file]{overflow:hidden}.form-control[type=file]:not(:disabled):not([readonly]){cursor:pointer}.form-control:focus{color:#212529;background-color:#fff;border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-control::-webkit-date-and-time-value{height:1.5em}.form-control::-moz-placeholder{color:#6c757d;opacity:1}.form-control::placeholder{color:#6c757d;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#e9ecef;opacity:1}.form-control::-webkit-file-upload-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:#212529;background-color:#e9ecef;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;-webkit-transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}.form-control::file-selector-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:#212529;background-color:#e9ecef;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control::-webkit-file-upload-button{-webkit-transition:none;transition:none}.form-control::file-selector-button{transition:none}}.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button{background-color:#dde0e3}.form-control:hover:not(:disabled):not([readonly])::file-selector-button{background-color:#dde0e3}.form-control-plaintext{display:block;width:100%;padding:.375rem 0;margin-bottom:0;line-height:1.5;color:#212529;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{min-height:calc(1.5em + .5rem + 2px);padding:.25rem .5rem;font-size:.875rem;border-radius:.25rem}.form-control-sm::-webkit-file-upload-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-sm::file-selector-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-lg{min-height:calc(1.5em + 1rem + 2px);padding:.5rem 1rem;font-size:1.25rem;border-radius:.5rem}.form-control-lg::-webkit-file-upload-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}.form-control-lg::file-selector-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}textarea.form-control{min-height:calc(1.5em + .75rem + 2px)}textarea.form-control-sm{min-height:calc(1.5em + .5rem + 2px)}textarea.form-control-lg{min-height:calc(1.5em + 1rem + 2px)}.form-control-color{width:3rem;height:auto;padding:.375rem}.form-control-color:not(:disabled):not([readonly]){cursor:pointer}.form-control-color::-moz-color-swatch{height:1.5em;border-radius:.375rem}.form-control-color::-webkit-color-swatch{height:1.5em;border-radius:.375rem}.form-select{display:block;width:100%;padding:.375rem 2.25rem .375rem .75rem;-moz-padding-start:calc(0.75rem - 3px);font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right .75rem center;background-size:16px 12px;border:1px solid #ced4da;border-radius:.375rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-select{transition:none}}.form-select:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-select[multiple],.form-select[size]:not([size="1"]){padding-right:.75rem;background-image:none}.form-select:disabled{background-color:#e9ecef}.form-select:-moz-focusring{color:transparent;text-shadow:0 0 0 #212529}.form-select-sm{padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem;border-radius:.25rem}.form-select-lg{padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem;border-radius:.5rem}.form-check{display:block;min-height:1.5rem;padding-left:1.5em;margin-bottom:.125rem}.form-check .form-check-input{float:left;margin-left:-1.5em}.form-check-reverse{padding-right:1.5em;padding-left:0;text-align:right}.form-check-reverse .form-check-input{float:right;margin-right:-1.5em;margin-left:0}.form-check-input{width:1em;height:1em;margin-top:.25em;vertical-align:top;background-color:#fff;background-repeat:no-repeat;background-position:center;background-size:contain;border:1px solid rgba(0,0,0,.25);-webkit-appearance:none;-moz-appearance:none;appearance:none;-webkit-print-color-adjust:exact;color-adjust:exact;print-color-adjust:exact}.form-check-input[type=checkbox]{border-radius:.25em}.form-check-input[type=radio]{border-radius:50%}.form-check-input:active{filter:brightness(90%)}.form-check-input:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-check-input:checked{background-color:#0d6efd;border-color:#0d6efd}.form-check-input:checked[type=checkbox]{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='m6 10 3 3 6-6'/%3e%3c/svg%3e")}.form-check-input:checked[type=radio]{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23fff'/%3e%3c/svg%3e")}.form-check-input[type=checkbox]:indeterminate{background-color:#0d6efd;border-color:#0d6efd;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e")}.form-check-input:disabled{pointer-events:none;filter:none;opacity:.5}.form-check-input:disabled~.form-check-label,.form-check-input[disabled]~.form-check-label{cursor:default;opacity:.5}.form-switch{padding-left:2.5em}.form-switch .form-check-input{width:2em;margin-left:-2.5em;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%280, 0, 0, 0.25%29'/%3e%3c/svg%3e");background-position:left center;border-radius:2em;transition:background-position .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-switch .form-check-input{transition:none}}.form-switch .form-check-input:focus{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%2386b7fe'/%3e%3c/svg%3e")}.form-switch .form-check-input:checked{background-position:right center;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.form-switch.form-check-reverse{padding-right:2.5em;padding-left:0}.form-switch.form-check-reverse .form-check-input{margin-right:-2.5em;margin-left:0}.form-check-inline{display:inline-block;margin-right:1rem}.btn-check{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.btn-check:disabled+.btn,.btn-check[disabled]+.btn{pointer-events:none;filter:none;opacity:.65}.form-range{width:100%;height:1.5rem;padding:0;background-color:transparent;-webkit-appearance:none;-moz-appearance:none;appearance:none}.form-range:focus{outline:0}.form-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(13,110,253,.25)}.form-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(13,110,253,.25)}.form-range::-moz-focus-outer{border:0}.form-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;background-color:#0d6efd;border:0;border-radius:1rem;-webkit-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-range::-webkit-slider-thumb{-webkit-transition:none;transition:none}}.form-range::-webkit-slider-thumb:active{background-color:#b6d4fe}.form-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.form-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#0d6efd;border:0;border-radius:1rem;-moz-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-range::-moz-range-thumb{-moz-transition:none;transition:none}}.form-range::-moz-range-thumb:active{background-color:#b6d4fe}.form-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.form-range:disabled{pointer-events:none}.form-range:disabled::-webkit-slider-thumb{background-color:#adb5bd}.form-range:disabled::-moz-range-thumb{background-color:#adb5bd}.form-floating{position:relative}.form-floating>.form-control,.form-floating>.form-control-plaintext,.form-floating>.form-select{height:calc(3.5rem + 2px);line-height:1.25}.form-floating>label{position:absolute;top:0;left:0;height:100%;padding:1rem .75rem;pointer-events:none;border:1px solid transparent;transform-origin:0 0;transition:opacity .1s ease-in-out,transform .1s ease-in-out}@media (prefers-reduced-motion:reduce){.form-floating>label{transition:none}}.form-floating>.form-control,.form-floating>.form-control-plaintext{padding:1rem .75rem}.form-floating>.form-control-plaintext::-moz-placeholder,.form-floating>.form-control::-moz-placeholder{color:transparent}.form-floating>.form-control-plaintext::placeholder,.form-floating>.form-control::placeholder{color:transparent}.form-floating>.form-control-plaintext:not(:-moz-placeholder-shown),.form-floating>.form-control:not(:-moz-placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control-plaintext:focus,.form-floating>.form-control-plaintext:not(:placeholder-shown),.form-floating>.form-control:focus,.form-floating>.form-control:not(:placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control-plaintext:-webkit-autofill,.form-floating>.form-control:-webkit-autofill{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-select{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:not(:-moz-placeholder-shown)~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control-plaintext~label,.form-floating>.form-control:focus~label,.form-floating>.form-control:not(:placeholder-shown)~label,.form-floating>.form-select~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control:-webkit-autofill~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control-plaintext~label{border-width:1px 0}.input-group{position:relative;display:flex;flex-wrap:wrap;align-items:stretch;width:100%}.input-group>.form-control,.input-group>.form-select{position:relative;flex:1 1 auto;width:1%;min-width:0}.input-group>.form-control:focus,.input-group>.form-select:focus{z-index:3}.input-group .btn{position:relative;z-index:2}.input-group .btn:focus{z-index:3}.input-group-text{display:flex;align-items:center;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:center;white-space:nowrap;background-color:#e9ecef;border:1px solid #ced4da;border-radius:.375rem}.input-group-lg>.btn,.input-group-lg>.form-control,.input-group-lg>.form-select,.input-group-lg>.input-group-text{padding:.5rem 1rem;font-size:1.25rem;border-radius:.5rem}.input-group-sm>.btn,.input-group-sm>.form-control,.input-group-sm>.form-select,.input-group-sm>.input-group-text{padding:.25rem .5rem;font-size:.875rem;border-radius:.25rem}.input-group-lg>.form-select,.input-group-sm>.form-select{padding-right:3rem}.input-group:not(.has-validation)>.dropdown-toggle:nth-last-child(n+3),.input-group:not(.has-validation)>:not(:last-child):not(.dropdown-toggle):not(.dropdown-menu){border-top-right-radius:0;border-bottom-right-radius:0}.input-group.has-validation>.dropdown-toggle:nth-last-child(n+4),.input-group.has-validation>:nth-last-child(n+3):not(.dropdown-toggle):not(.dropdown-menu){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>:not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback){margin-left:-1px;border-top-left-radius:0;border-bottom-left-radius:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:#198754}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:rgba(25,135,84,.9);border-radius:.375rem}.is-valid~.valid-feedback,.is-valid~.valid-tooltip,.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip{display:block}.form-control.is-valid,.was-validated .form-control:valid{border-color:#198754;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:#198754;box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-valid,.was-validated .form-select:valid{border-color:#198754}.form-select.is-valid:not([multiple]):not([size]),.form-select.is-valid:not([multiple])[size="1"],.was-validated .form-select:valid:not([multiple]):not([size]),.was-validated .form-select:valid:not([multiple])[size="1"]{padding-right:4.125rem;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e"),url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-valid:focus,.was-validated .form-select:valid:focus{border-color:#198754;box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.form-control-color.is-valid,.was-validated .form-control-color:valid{width:calc(3rem + calc(1.5em + .75rem))}.form-check-input.is-valid,.was-validated .form-check-input:valid{border-color:#198754}.form-check-input.is-valid:checked,.was-validated .form-check-input:valid:checked{background-color:#198754}.form-check-input.is-valid:focus,.was-validated .form-check-input:valid:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:#198754}.form-check-inline .form-check-input~.valid-feedback{margin-left:.5em}.input-group .form-control.is-valid,.input-group .form-select.is-valid,.was-validated .input-group .form-control:valid,.was-validated .input-group .form-select:valid{z-index:1}.input-group .form-control.is-valid:focus,.input-group .form-select.is-valid:focus,.was-validated .input-group .form-control:valid:focus,.was-validated .input-group .form-select:valid:focus{z-index:3}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:#dc3545}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:rgba(220,53,69,.9);border-radius:.375rem}.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip,.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip{display:block}.form-control.is-invalid,.was-validated .form-control:invalid{border-color:#dc3545;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-invalid,.was-validated .form-select:invalid{border-color:#dc3545}.form-select.is-invalid:not([multiple]):not([size]),.form-select.is-invalid:not([multiple])[size="1"],.was-validated .form-select:invalid:not([multiple]):not([size]),.was-validated .form-select:invalid:not([multiple])[size="1"]{padding-right:4.125rem;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e"),url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-invalid:focus,.was-validated .form-select:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.form-control-color.is-invalid,.was-validated .form-control-color:invalid{width:calc(3rem + calc(1.5em + .75rem))}.form-check-input.is-invalid,.was-validated .form-check-input:invalid{border-color:#dc3545}.form-check-input.is-invalid:checked,.was-validated .form-check-input:invalid:checked{background-color:#dc3545}.form-check-input.is-invalid:focus,.was-validated .form-check-input:invalid:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:#dc3545}.form-check-inline .form-check-input~.invalid-feedback{margin-left:.5em}.input-group .form-control.is-invalid,.input-group .form-select.is-invalid,.was-validated .input-group .form-control:invalid,.was-validated .input-group .form-select:invalid{z-index:2}.input-group .form-control.is-invalid:focus,.input-group .form-select.is-invalid:focus,.was-validated .input-group .form-control:invalid:focus,.was-validated .input-group .form-select:invalid:focus{z-index:3}.btn{--bs-btn-padding-x:0.75rem;--bs-btn-padding-y:0.375rem;--bs-btn-font-family: ;--bs-btn-font-size:1rem;--bs-btn-font-weight:400;--bs-btn-line-height:1.5;--bs-btn-color:#212529;--bs-btn-bg:transparent;--bs-btn-border-width:1px;--bs-btn-border-color:transparent;--bs-btn-border-radius:0.375rem;--bs-btn-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.15),0 1px 1px rgba(0, 0, 0, 0.075);--bs-btn-disabled-opacity:0.65;--bs-btn-focus-box-shadow:0 0 0 0.25rem rgba(var(--bs-btn-focus-shadow-rgb), .5);display:inline-block;padding:var(--bs-btn-padding-y) var(--bs-btn-padding-x);font-family:var(--bs-btn-font-family);font-size:var(--bs-btn-font-size);font-weight:var(--bs-btn-font-weight);line-height:var(--bs-btn-line-height);color:var(--bs-btn-color);text-align:center;text-decoration:none;vertical-align:middle;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;border:var(--bs-btn-border-width) solid var(--bs-btn-border-color);border-radius:var(--bs-btn-border-radius);background-color:var(--bs-btn-bg);transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:var(--bs-btn-hover-color);background-color:var(--bs-btn-hover-bg);border-color:var(--bs-btn-hover-border-color)}.btn-check:focus+.btn,.btn:focus{color:var(--bs-btn-hover-color);background-color:var(--bs-btn-hover-bg);border-color:var(--bs-btn-hover-border-color);outline:0;box-shadow:var(--bs-btn-focus-box-shadow)}.btn-check:active+.btn,.btn-check:checked+.btn,.btn.active,.btn.show,.btn:active{color:var(--bs-btn-active-color);background-color:var(--bs-btn-active-bg);border-color:var(--bs-btn-active-border-color)}.btn-check:active+.btn:focus,.btn-check:checked+.btn:focus,.btn.active:focus,.btn.show:focus,.btn:active:focus{box-shadow:var(--bs-btn-focus-box-shadow)}.btn.disabled,.btn:disabled,fieldset:disabled .btn{color:var(--bs-btn-disabled-color);pointer-events:none;background-color:var(--bs-btn-disabled-bg);border-color:var(--bs-btn-disabled-border-color);opacity:var(--bs-btn-disabled-opacity)}.btn-primary{--bs-btn-color:#fff;--bs-btn-bg:#0d6efd;--bs-btn-border-color:#0d6efd;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#0b5ed7;--bs-btn-hover-border-color:#0a58ca;--bs-btn-focus-shadow-rgb:49,132,253;--bs-btn-active-color:#fff;--bs-btn-active-bg:#0a58ca;--bs-btn-active-border-color:#0a53be;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#0d6efd;--bs-btn-disabled-border-color:#0d6efd}.btn-secondary{--bs-btn-color:#fff;--bs-btn-bg:#6c757d;--bs-btn-border-color:#6c757d;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#5c636a;--bs-btn-hover-border-color:#565e64;--bs-btn-focus-shadow-rgb:130,138,145;--bs-btn-active-color:#fff;--bs-btn-active-bg:#565e64;--bs-btn-active-border-color:#51585e;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#6c757d;--bs-btn-disabled-border-color:#6c757d}.btn-success{--bs-btn-color:#fff;--bs-btn-bg:#198754;--bs-btn-border-color:#198754;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#157347;--bs-btn-hover-border-color:#146c43;--bs-btn-focus-shadow-rgb:60,153,110;--bs-btn-active-color:#fff;--bs-btn-active-bg:#146c43;--bs-btn-active-border-color:#13653f;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#198754;--bs-btn-disabled-border-color:#198754}.btn-info{--bs-btn-color:#000;--bs-btn-bg:#0dcaf0;--bs-btn-border-color:#0dcaf0;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#31d2f2;--bs-btn-hover-border-color:#25cff2;--bs-btn-focus-shadow-rgb:11,172,204;--bs-btn-active-color:#000;--bs-btn-active-bg:#3dd5f3;--bs-btn-active-border-color:#25cff2;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#000;--bs-btn-disabled-bg:#0dcaf0;--bs-btn-disabled-border-color:#0dcaf0}.btn-warning{--bs-btn-color:#000;--bs-btn-bg:#ffc107;--bs-btn-border-color:#ffc107;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#ffca2c;--bs-btn-hover-border-color:#ffc720;--bs-btn-focus-shadow-rgb:217,164,6;--bs-btn-active-color:#000;--bs-btn-active-bg:#ffcd39;--bs-btn-active-border-color:#ffc720;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#000;--bs-btn-disabled-bg:#ffc107;--bs-btn-disabled-border-color:#ffc107}.btn-danger{--bs-btn-color:#fff;--bs-btn-bg:#dc3545;--bs-btn-border-color:#dc3545;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#bb2d3b;--bs-btn-hover-border-color:#b02a37;--bs-btn-focus-shadow-rgb:225,83,97;--bs-btn-active-color:#fff;--bs-btn-active-bg:#b02a37;--bs-btn-active-border-color:#a52834;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#dc3545;--bs-btn-disabled-border-color:#dc3545}.btn-light{--bs-btn-color:#000;--bs-btn-bg:#f8f9fa;--bs-btn-border-color:#f8f9fa;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#f9fafb;--bs-btn-hover-border-color:#f9fafb;--bs-btn-focus-shadow-rgb:211,212,213;--bs-btn-active-color:#000;--bs-btn-active-bg:#f9fafb;--bs-btn-active-border-color:#f9fafb;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#000;--bs-btn-disabled-bg:#f8f9fa;--bs-btn-disabled-border-color:#f8f9fa}.btn-dark{--bs-btn-color:#fff;--bs-btn-bg:#212529;--bs-btn-border-color:#212529;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#1c1f23;--bs-btn-hover-border-color:#1a1e21;--bs-btn-focus-shadow-rgb:66,70,73;--bs-btn-active-color:#fff;--bs-btn-active-bg:#1a1e21;--bs-btn-active-border-color:#191c1f;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#212529;--bs-btn-disabled-border-color:#212529}.btn-outline-primary{--bs-btn-color:#0d6efd;--bs-btn-border-color:#0d6efd;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#0d6efd;--bs-btn-hover-border-color:#0d6efd;--bs-btn-focus-shadow-rgb:13,110,253;--bs-btn-active-color:#fff;--bs-btn-active-bg:#0d6efd;--bs-btn-active-border-color:#0d6efd;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#0d6efd;--bs-btn-disabled-bg:transparent;--bs-gradient:none}.btn-outline-secondary{--bs-btn-color:#6c757d;--bs-btn-border-color:#6c757d;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#6c757d;--bs-btn-hover-border-color:#6c757d;--bs-btn-focus-shadow-rgb:108,117,125;--bs-btn-active-color:#fff;--bs-btn-active-bg:#6c757d;--bs-btn-active-border-color:#6c757d;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#6c757d;--bs-btn-disabled-bg:transparent;--bs-gradient:none}.btn-outline-success{--bs-btn-color:#198754;--bs-btn-border-color:#198754;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#198754;--bs-btn-hover-border-color:#198754;--bs-btn-focus-shadow-rgb:25,135,84;--bs-btn-active-color:#fff;--bs-btn-active-bg:#198754;--bs-btn-active-border-color:#198754;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#198754;--bs-btn-disabled-bg:transparent;--bs-gradient:none}.btn-outline-info{--bs-btn-color:#0dcaf0;--bs-btn-border-color:#0dcaf0;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#0dcaf0;--bs-btn-hover-border-color:#0dcaf0;--bs-btn-focus-shadow-rgb:13,202,240;--bs-btn-active-color:#000;--bs-btn-active-bg:#0dcaf0;--bs-btn-active-border-color:#0dcaf0;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#0dcaf0;--bs-btn-disabled-bg:transparent;--bs-gradient:none}.btn-outline-warning{--bs-btn-color:#ffc107;--bs-btn-border-color:#ffc107;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#ffc107;--bs-btn-hover-border-color:#ffc107;--bs-btn-focus-shadow-rgb:255,193,7;--bs-btn-active-color:#000;--bs-btn-active-bg:#ffc107;--bs-btn-active-border-color:#ffc107;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#ffc107;--bs-btn-disabled-bg:transparent;--bs-gradient:none}.btn-outline-danger{--bs-btn-color:#dc3545;--bs-btn-border-color:#dc3545;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#dc3545;--bs-btn-hover-border-color:#dc3545;--bs-btn-focus-shadow-rgb:220,53,69;--bs-btn-active-color:#fff;--bs-btn-active-bg:#dc3545;--bs-btn-active-border-color:#dc3545;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#dc3545;--bs-btn-disabled-bg:transparent;--bs-gradient:none}.btn-outline-light{--bs-btn-color:#f8f9fa;--bs-btn-border-color:#f8f9fa;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#f8f9fa;--bs-btn-hover-border-color:#f8f9fa;--bs-btn-focus-shadow-rgb:248,249,250;--bs-btn-active-color:#000;--bs-btn-active-bg:#f8f9fa;--bs-btn-active-border-color:#f8f9fa;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#f8f9fa;--bs-btn-disabled-bg:transparent;--bs-gradient:none}.btn-outline-dark{--bs-btn-color:#212529;--bs-btn-border-color:#212529;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#212529;--bs-btn-hover-border-color:#212529;--bs-btn-focus-shadow-rgb:33,37,41;--bs-btn-active-color:#fff;--bs-btn-active-bg:#212529;--bs-btn-active-border-color:#212529;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#212529;--bs-btn-disabled-bg:transparent;--bs-gradient:none}.btn-link{--bs-btn-font-weight:400;--bs-btn-color:var(--bs-link-color);--bs-btn-bg:transparent;--bs-btn-border-color:transparent;--bs-btn-hover-color:var(--bs-link-hover-color);--bs-btn-hover-border-color:transparent;--bs-btn-active-border-color:transparent;--bs-btn-disabled-color:#6c757d;--bs-btn-disabled-border-color:transparent;--bs-btn-box-shadow:none;text-decoration:underline}.btn-group-lg>.btn,.btn-lg{--bs-btn-padding-y:0.5rem;--bs-btn-padding-x:1rem;--bs-btn-font-size:1.25rem;--bs-btn-border-radius:0.5rem}.btn-group-sm>.btn,.btn-sm{--bs-btn-padding-y:0.25rem;--bs-btn-padding-x:0.5rem;--bs-btn-font-size:0.875rem;--bs-btn-border-radius:0.25rem}.fade{transition:opacity .15s linear}@media (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{height:0;overflow:hidden;transition:height .35s ease}@media (prefers-reduced-motion:reduce){.collapsing{transition:none}}.collapsing.collapse-horizontal{width:0;height:auto;transition:width .35s ease}@media (prefers-reduced-motion:reduce){.collapsing.collapse-horizontal{transition:none}}.dropdown,.dropdown-center,.dropend,.dropstart,.dropup,.dropup-center{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{--bs-dropdown-min-width:10rem;--bs-dropdown-padding-x:0;--bs-dropdown-padding-y:0.5rem;--bs-dropdown-spacer:0.125rem;--bs-dropdown-font-size:1rem;--bs-dropdown-color:#212529;--bs-dropdown-bg:#fff;--bs-dropdown-border-color:var(--bs-border-color-translucent);--bs-dropdown-border-radius:0.375rem;--bs-dropdown-border-width:1px;--bs-dropdown-inner-border-radius:calc(0.375rem - 1px);--bs-dropdown-divider-bg:var(--bs-border-color-translucent);--bs-dropdown-divider-margin-y:0.5rem;--bs-dropdown-box-shadow:0 0.5rem 1rem rgba(0, 0, 0, 0.15);--bs-dropdown-link-color:#212529;--bs-dropdown-link-hover-color:#1e2125;--bs-dropdown-link-hover-bg:#e9ecef;--bs-dropdown-link-active-color:#fff;--bs-dropdown-link-active-bg:#0d6efd;--bs-dropdown-link-disabled-color:#adb5bd;--bs-dropdown-item-padding-x:1rem;--bs-dropdown-item-padding-y:0.25rem;--bs-dropdown-header-color:#6c757d;--bs-dropdown-header-padding-x:1rem;--bs-dropdown-header-padding-y:0.5rem;position:absolute;z-index:1000;display:none;min-width:var(--bs-dropdown-min-width);padding:var(--bs-dropdown-padding-y) var(--bs-dropdown-padding-x);margin:0;font-size:var(--bs-dropdown-font-size);color:var(--bs-dropdown-color);text-align:left;list-style:none;background-color:var(--bs-dropdown-bg);background-clip:padding-box;border:var(--bs-dropdown-border-width) solid var(--bs-dropdown-border-color);border-radius:var(--bs-dropdown-border-radius)}.dropdown-menu[data-bs-popper]{top:100%;left:0;margin-top:var(--bs-dropdown-spacer)}.dropdown-menu-start{--bs-position:start}.dropdown-menu-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-end{--bs-position:end}.dropdown-menu-end[data-bs-popper]{right:0;left:auto}@media (min-width:576px){.dropdown-menu-sm-start{--bs-position:start}.dropdown-menu-sm-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-sm-end{--bs-position:end}.dropdown-menu-sm-end[data-bs-popper]{right:0;left:auto}}@media (min-width:768px){.dropdown-menu-md-start{--bs-position:start}.dropdown-menu-md-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-md-end{--bs-position:end}.dropdown-menu-md-end[data-bs-popper]{right:0;left:auto}}@media (min-width:992px){.dropdown-menu-lg-start{--bs-position:start}.dropdown-menu-lg-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-lg-end{--bs-position:end}.dropdown-menu-lg-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1200px){.dropdown-menu-xl-start{--bs-position:start}.dropdown-menu-xl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xl-end{--bs-position:end}.dropdown-menu-xl-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1400px){.dropdown-menu-xxl-start{--bs-position:start}.dropdown-menu-xxl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xxl-end{--bs-position:end}.dropdown-menu-xxl-end[data-bs-popper]{right:0;left:auto}}.dropup .dropdown-menu[data-bs-popper]{top:auto;bottom:100%;margin-top:0;margin-bottom:var(--bs-dropdown-spacer)}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-menu[data-bs-popper]{top:0;right:auto;left:100%;margin-top:0;margin-left:var(--bs-dropdown-spacer)}.dropend .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropend .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-toggle::after{vertical-align:0}.dropstart .dropdown-menu[data-bs-popper]{top:0;right:100%;left:auto;margin-top:0;margin-right:var(--bs-dropdown-spacer)}.dropstart .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropstart .dropdown-toggle::after{display:none}.dropstart .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropstart .dropdown-toggle:empty::after{margin-left:0}.dropstart .dropdown-toggle::before{vertical-align:0}.dropdown-divider{height:0;margin:var(--bs-dropdown-divider-margin-y) 0;overflow:hidden;border-top:1px solid var(--bs-dropdown-divider-bg);opacity:1}.dropdown-item{display:block;width:100%;padding:var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x);clear:both;font-weight:400;color:var(--bs-dropdown-link-color);text-align:inherit;text-decoration:none;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:focus,.dropdown-item:hover{color:var(--bs-dropdown-link-hover-color);background-color:var(--bs-dropdown-link-hover-bg)}.dropdown-item.active,.dropdown-item:active{color:var(--bs-dropdown-link-active-color);text-decoration:none;background-color:var(--bs-dropdown-link-active-bg)}.dropdown-item.disabled,.dropdown-item:disabled{color:var(--bs-dropdown-link-disabled-color);pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:var(--bs-dropdown-header-padding-y) var(--bs-dropdown-header-padding-x);margin-bottom:0;font-size:.875rem;color:var(--bs-dropdown-header-color);white-space:nowrap}.dropdown-item-text{display:block;padding:var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x);color:var(--bs-dropdown-link-color)}.dropdown-menu-dark{--bs-dropdown-color:#dee2e6;--bs-dropdown-bg:#343a40;--bs-dropdown-border-color:var(--bs-border-color-translucent);--bs-dropdown-box-shadow: ;--bs-dropdown-link-color:#dee2e6;--bs-dropdown-link-hover-color:#fff;--bs-dropdown-divider-bg:var(--bs-border-color-translucent);--bs-dropdown-link-hover-bg:rgba(255, 255, 255, 0.15);--bs-dropdown-link-active-color:#fff;--bs-dropdown-link-active-bg:#0d6efd;--bs-dropdown-link-disabled-color:#adb5bd;--bs-dropdown-header-color:#adb5bd}.btn-group,.btn-group-vertical{position:relative;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;flex:1 1 auto}.btn-group-vertical>.btn-check:checked+.btn,.btn-group-vertical>.btn-check:focus+.btn,.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn-check:checked+.btn,.btn-group>.btn-check:focus+.btn,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:1}.btn-toolbar{display:flex;flex-wrap:wrap;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group{border-radius:.375rem}.btn-group>.btn-group:not(:first-child),.btn-group>.btn:not(:first-child){margin-left:-1px}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn.dropdown-toggle-split:first-child,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:nth-child(n+3),.btn-group>:not(.btn-check)+.btn{border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropend .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropstart .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{flex-direction:column;align-items:flex-start;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:-1px}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn~.btn{border-top-left-radius:0;border-top-right-radius:0}.nav{--bs-nav-link-padding-x:1rem;--bs-nav-link-padding-y:0.5rem;--bs-nav-link-font-weight: ;--bs-nav-link-color:var(--bs-link-color);--bs-nav-link-hover-color:var(--bs-link-hover-color);--bs-nav-link-disabled-color:#6c757d;display:flex;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:var(--bs-nav-link-padding-y) var(--bs-nav-link-padding-x);font-size:var(--bs-nav-link-font-size);font-weight:var(--bs-nav-link-font-weight);color:var(--bs-nav-link-color);text-decoration:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out}@media (prefers-reduced-motion:reduce){.nav-link{transition:none}}.nav-link:focus,.nav-link:hover{color:var(--bs-nav-link-hover-color)}.nav-link.disabled{color:var(--bs-nav-link-disabled-color);pointer-events:none;cursor:default}.nav-tabs{--bs-nav-tabs-border-width:1px;--bs-nav-tabs-border-color:#dee2e6;--bs-nav-tabs-border-radius:0.375rem;--bs-nav-tabs-link-hover-border-color:#e9ecef #e9ecef #dee2e6;--bs-nav-tabs-link-active-color:#495057;--bs-nav-tabs-link-active-bg:#fff;--bs-nav-tabs-link-active-border-color:#dee2e6 #dee2e6 #fff;border-bottom:var(--bs-nav-tabs-border-width) solid var(--bs-nav-tabs-border-color)}.nav-tabs .nav-link{margin-bottom:calc(var(--bs-nav-tabs-border-width) * -1);background:0 0;border:var(--bs-nav-tabs-border-width) solid transparent;border-top-left-radius:var(--bs-nav-tabs-border-radius);border-top-right-radius:var(--bs-nav-tabs-border-radius)}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{isolation:isolate;border-color:var(--bs-nav-tabs-link-hover-border-color)}.nav-tabs .nav-link.disabled,.nav-tabs .nav-link:disabled{color:var(--bs-nav-link-disabled-color);background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:var(--bs-nav-tabs-link-active-color);background-color:var(--bs-nav-tabs-link-active-bg);border-color:var(--bs-nav-tabs-link-active-border-color)}.nav-tabs .dropdown-menu{margin-top:calc(var(--bs-nav-tabs-border-width) * -1);border-top-left-radius:0;border-top-right-radius:0}.nav-pills{--bs-nav-pills-border-radius:0.375rem;--bs-nav-pills-link-active-color:#fff;--bs-nav-pills-link-active-bg:#0d6efd}.nav-pills .nav-link{background:0 0;border:0;border-radius:var(--bs-nav-pills-border-radius)}.nav-pills .nav-link:disabled{color:var(--bs-nav-link-disabled-color);background-color:transparent;border-color:transparent}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:var(--bs-nav-pills-link-active-color);background-color:var(--bs-nav-pills-link-active-bg)}.nav-fill .nav-item,.nav-fill>.nav-link{flex:1 1 auto;text-align:center}.nav-justified .nav-item,.nav-justified>.nav-link{flex-basis:0;flex-grow:1;text-align:center}.nav-fill .nav-item .nav-link,.nav-justified .nav-item .nav-link{width:100%}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{--bs-navbar-padding-x:0;--bs-navbar-padding-y:0.5rem;--bs-navbar-color:rgba(0, 0, 0, 0.55);--bs-navbar-hover-color:rgba(0, 0, 0, 0.7);--bs-navbar-disabled-color:rgba(0, 0, 0, 0.3);--bs-navbar-active-color:rgba(0, 0, 0, 0.9);--bs-navbar-brand-padding-y:0.3125rem;--bs-navbar-brand-margin-end:1rem;--bs-navbar-brand-font-size:1.25rem;--bs-navbar-brand-color:rgba(0, 0, 0, 0.9);--bs-navbar-brand-hover-color:rgba(0, 0, 0, 0.9);--bs-navbar-nav-link-padding-x:0.5rem;--bs-navbar-toggler-padding-y:0.25rem;--bs-navbar-toggler-padding-x:0.75rem;--bs-navbar-toggler-font-size:1.25rem;--bs-navbar-toggler-icon-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%280, 0, 0, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");--bs-navbar-toggler-border-color:rgba(0, 0, 0, 0.1);--bs-navbar-toggler-border-radius:0.375rem;--bs-navbar-toggler-focus-width:0.25rem;--bs-navbar-toggler-transition:box-shadow 0.15s ease-in-out;position:relative;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;padding:var(--bs-navbar-padding-y) var(--bs-navbar-padding-x)}.navbar>.container,.navbar>.container-fluid,.navbar>.container-lg,.navbar>.container-md,.navbar>.container-sm,.navbar>.container-xl,.navbar>.container-xxl{display:flex;flex-wrap:inherit;align-items:center;justify-content:space-between}.navbar-brand{padding-top:var(--bs-navbar-brand-padding-y);padding-bottom:var(--bs-navbar-brand-padding-y);margin-right:var(--bs-navbar-brand-margin-end);font-size:var(--bs-navbar-brand-font-size);color:var(--bs-navbar-brand-color);text-decoration:none;white-space:nowrap}.navbar-brand:focus,.navbar-brand:hover{color:var(--bs-navbar-brand-hover-color)}.navbar-nav{--bs-nav-link-padding-x:0;--bs-nav-link-padding-y:0.5rem;--bs-nav-link-color:var(--bs-navbar-color);--bs-nav-link-hover-color:var(--bs-navbar-hover-color);--bs-nav-link-disabled-color:var(--bs-navbar-disabled-color);display:flex;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link.active,.navbar-nav .show>.nav-link{color:var(--bs-navbar-active-color)}.navbar-nav .dropdown-menu{position:static}.navbar-text{padding-top:.5rem;padding-bottom:.5rem;color:var(--bs-navbar-color)}.navbar-text a,.navbar-text a:focus,.navbar-text a:hover{color:var(--bs-navbar-active-color)}.navbar-collapse{flex-basis:100%;flex-grow:1;align-items:center}.navbar-toggler{padding:var(--bs-navbar-toggler-padding-y) var(--bs-navbar-toggler-padding-x);font-size:var(--bs-navbar-toggler-font-size);line-height:1;color:var(--bs-navbar-color);background-color:transparent;border:var(--bs-border-width) solid var(--bs-navbar-toggler-border-color);border-radius:var(--bs-navbar-toggler-border-radius);transition:var(--bs-navbar-toggler-transition)}@media (prefers-reduced-motion:reduce){.navbar-toggler{transition:none}}.navbar-toggler:hover{text-decoration:none}.navbar-toggler:focus{text-decoration:none;outline:0;box-shadow:0 0 0 var(--bs-navbar-toggler-focus-width)}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;background-image:var(--bs-navbar-toggler-icon-bg);background-repeat:no-repeat;background-position:center;background-size:100%}.navbar-nav-scroll{max-height:var(--bs-scroll-height,75vh);overflow-y:auto}@media (min-width:576px){.navbar-expand-sm{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-sm .navbar-nav{flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-sm .navbar-nav-scroll{overflow:visible}.navbar-expand-sm .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}.navbar-expand-sm .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-sm .offcanvas .offcanvas-header{display:none}.navbar-expand-sm .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:768px){.navbar-expand-md{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-md .navbar-nav{flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-md .navbar-nav-scroll{overflow:visible}.navbar-expand-md .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}.navbar-expand-md .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-md .offcanvas .offcanvas-header{display:none}.navbar-expand-md .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:992px){.navbar-expand-lg{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-lg .navbar-nav{flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-lg .navbar-nav-scroll{overflow:visible}.navbar-expand-lg .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}.navbar-expand-lg .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-lg .offcanvas .offcanvas-header{display:none}.navbar-expand-lg .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:1200px){.navbar-expand-xl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xl .navbar-nav{flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-xl .navbar-nav-scroll{overflow:visible}.navbar-expand-xl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}.navbar-expand-xl .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-xl .offcanvas .offcanvas-header{display:none}.navbar-expand-xl .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:1400px){.navbar-expand-xxl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xxl .navbar-nav{flex-direction:row}.navbar-expand-xxl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xxl .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-xxl .navbar-nav-scroll{overflow:visible}.navbar-expand-xxl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xxl .navbar-toggler{display:none}.navbar-expand-xxl .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-xxl .offcanvas .offcanvas-header{display:none}.navbar-expand-xxl .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}.navbar-expand{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand .navbar-nav{flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand .navbar-nav-scroll{overflow:visible}.navbar-expand .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-expand .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand .offcanvas .offcanvas-header{display:none}.navbar-expand .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}.navbar-dark{--bs-navbar-color:rgba(255, 255, 255, 0.55);--bs-navbar-hover-color:rgba(255, 255, 255, 0.75);--bs-navbar-disabled-color:rgba(255, 255, 255, 0.25);--bs-navbar-active-color:#fff;--bs-navbar-brand-color:#fff;--bs-navbar-brand-hover-color:#fff;--bs-navbar-toggler-border-color:rgba(255, 255, 255, 0.1);--bs-navbar-toggler-icon-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.card{--bs-card-spacer-y:1rem;--bs-card-spacer-x:1rem;--bs-card-title-spacer-y:0.5rem;--bs-card-border-width:1px;--bs-card-border-color:var(--bs-border-color-translucent);--bs-card-border-radius:0.375rem;--bs-card-box-shadow: ;--bs-card-inner-border-radius:calc(0.375rem - 1px);--bs-card-cap-padding-y:0.5rem;--bs-card-cap-padding-x:1rem;--bs-card-cap-bg:rgba(0, 0, 0, 0.03);--bs-card-cap-color: ;--bs-card-height: ;--bs-card-color: ;--bs-card-bg:#fff;--bs-card-img-overlay-padding:1rem;--bs-card-group-margin:0.75rem;position:relative;display:flex;flex-direction:column;min-width:0;height:var(--bs-card-height);word-wrap:break-word;background-color:var(--bs-card-bg);background-clip:border-box;border:var(--bs-card-border-width) solid var(--bs-card-border-color);border-radius:var(--bs-card-border-radius)}.card>hr{margin-right:0;margin-left:0}.card>.list-group{border-top:inherit;border-bottom:inherit}.card>.list-group:first-child{border-top-width:0;border-top-left-radius:var(--bs-card-inner-border-radius);border-top-right-radius:var(--bs-card-inner-border-radius)}.card>.list-group:last-child{border-bottom-width:0;border-bottom-right-radius:var(--bs-card-inner-border-radius);border-bottom-left-radius:var(--bs-card-inner-border-radius)}.card>.card-header+.list-group,.card>.list-group+.card-footer{border-top:0}.card-body{flex:1 1 auto;padding:var(--bs-card-spacer-y) var(--bs-card-spacer-x);color:var(--bs-card-color)}.card-title{margin-bottom:var(--bs-card-title-spacer-y)}.card-subtitle{margin-top:calc(-.5 * var(--bs-card-title-spacer-y));margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link+.card-link{margin-left:var(--bs-card-spacer-x)}.card-header{padding:var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x);margin-bottom:0;color:var(--bs-card-cap-color);background-color:var(--bs-card-cap-bg);border-bottom:var(--bs-card-border-width) solid var(--bs-card-border-color)}.card-header:first-child{border-radius:var(--bs-card-inner-border-radius) var(--bs-card-inner-border-radius) 0 0}.card-footer{padding:var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x);color:var(--bs-card-cap-color);background-color:var(--bs-card-cap-bg);border-top:var(--bs-card-border-width) solid var(--bs-card-border-color)}.card-footer:last-child{border-radius:0 0 var(--bs-card-inner-border-radius) var(--bs-card-inner-border-radius)}.card-header-tabs{margin-right:calc(-.5 * var(--bs-card-cap-padding-x));margin-bottom:calc(-1 * var(--bs-card-cap-padding-y));margin-left:calc(-.5 * var(--bs-card-cap-padding-x));border-bottom:0}.card-header-tabs .nav-link.active{background-color:var(--bs-card-bg);border-bottom-color:var(--bs-card-bg)}.card-header-pills{margin-right:calc(-.5 * var(--bs-card-cap-padding-x));margin-left:calc(-.5 * var(--bs-card-cap-padding-x))}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:var(--bs-card-img-overlay-padding);border-radius:var(--bs-card-inner-border-radius)}.card-img,.card-img-bottom,.card-img-top{width:100%}.card-img,.card-img-top{border-top-left-radius:var(--bs-card-inner-border-radius);border-top-right-radius:var(--bs-card-inner-border-radius)}.card-img,.card-img-bottom{border-bottom-right-radius:var(--bs-card-inner-border-radius);border-bottom-left-radius:var(--bs-card-inner-border-radius)}.card-group>.card{margin-bottom:var(--bs-card-group-margin)}@media (min-width:576px){.card-group{display:flex;flex-flow:row wrap}.card-group>.card{flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-header,.card-group>.card:not(:last-child) .card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-footer,.card-group>.card:not(:last-child) .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-header,.card-group>.card:not(:first-child) .card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-footer,.card-group>.card:not(:first-child) .card-img-bottom{border-bottom-left-radius:0}}.accordion{--bs-accordion-color:#000;--bs-accordion-bg:#fff;--bs-accordion-transition:color 0.15s ease-in-out,background-color 0.15s ease-in-out,border-color 0.15s ease-in-out,box-shadow 0.15s ease-in-out,border-radius 0.15s ease;--bs-accordion-border-color:var(--bs-border-color);--bs-accordion-border-width:1px;--bs-accordion-border-radius:0.375rem;--bs-accordion-inner-border-radius:calc(0.375rem - 1px);--bs-accordion-btn-padding-x:1.25rem;--bs-accordion-btn-padding-y:1rem;--bs-accordion-btn-color:var(--bs-body-color);--bs-accordion-btn-bg:var(--bs-accordion-bg);--bs-accordion-btn-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='var%28--bs-body-color%29'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");--bs-accordion-btn-icon-width:1.25rem;--bs-accordion-btn-icon-transform:rotate(-180deg);--bs-accordion-btn-icon-transition:transform 0.2s ease-in-out;--bs-accordion-btn-active-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%230c63e4'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");--bs-accordion-btn-focus-border-color:#86b7fe;--bs-accordion-btn-focus-box-shadow:0 0 0 0.25rem rgba(13, 110, 253, 0.25);--bs-accordion-body-padding-x:1.25rem;--bs-accordion-body-padding-y:1rem;--bs-accordion-active-color:#0c63e4;--bs-accordion-active-bg:#e7f1ff}.accordion-button{position:relative;display:flex;align-items:center;width:100%;padding:var(--bs-accordion-btn-padding-y) var(--bs-accordion-btn-padding-x);font-size:1rem;color:var(--bs-accordion-btn-color);text-align:left;background-color:var(--bs-accordion-btn-bg);border:0;border-radius:0;overflow-anchor:none;transition:var(--bs-accordion-transition)}@media (prefers-reduced-motion:reduce){.accordion-button{transition:none}}.accordion-button:not(.collapsed){color:var(--bs-accordion-active-color);background-color:var(--bs-accordion-active-bg);box-shadow:inset 0 calc(var(--bs-accordion-border-width) * -1) 0 var(--bs-accordion-border-color)}.accordion-button:not(.collapsed)::after{background-image:var(--bs-accordion-btn-active-icon);transform:var(--bs-accordion-btn-icon-transform)}.accordion-button::after{flex-shrink:0;width:var(--bs-accordion-btn-icon-width);height:var(--bs-accordion-btn-icon-width);margin-left:auto;content:"";background-image:var(--bs-accordion-btn-icon);background-repeat:no-repeat;background-size:var(--bs-accordion-btn-icon-width);transition:var(--bs-accordion-btn-icon-transition)}@media (prefers-reduced-motion:reduce){.accordion-button::after{transition:none}}.accordion-button:hover{z-index:2}.accordion-button:focus{z-index:3;border-color:var(--bs-accordion-btn-focus-border-color);outline:0;box-shadow:var(--bs-accordion-btn-focus-box-shadow)}.accordion-header{margin-bottom:0}.accordion-item{color:var(--bs-accordion-color);background-color:var(--bs-accordion-bg);border:var(--bs-accordion-border-width) solid var(--bs-accordion-border-color)}.accordion-item:first-of-type{border-top-left-radius:var(--bs-accordion-border-radius);border-top-right-radius:var(--bs-accordion-border-radius)}.accordion-item:first-of-type .accordion-button{border-top-left-radius:var(--bs-accordion-inner-border-radius);border-top-right-radius:var(--bs-accordion-inner-border-radius)}.accordion-item:not(:first-of-type){border-top:0}.accordion-item:last-of-type{border-bottom-right-radius:var(--bs-accordion-border-radius);border-bottom-left-radius:var(--bs-accordion-border-radius)}.accordion-item:last-of-type .accordion-button.collapsed{border-bottom-right-radius:var(--bs-accordion-inner-border-radius);border-bottom-left-radius:var(--bs-accordion-inner-border-radius)}.accordion-item:last-of-type .accordion-collapse{border-bottom-right-radius:var(--bs-accordion-border-radius);border-bottom-left-radius:var(--bs-accordion-border-radius)}.accordion-body{padding:var(--bs-accordion-body-padding-y) var(--bs-accordion-body-padding-x)}.accordion-flush .accordion-collapse{border-width:0}.accordion-flush .accordion-item{border-right:0;border-left:0;border-radius:0}.accordion-flush .accordion-item:first-child{border-top:0}.accordion-flush .accordion-item:last-child{border-bottom:0}.accordion-flush .accordion-item .accordion-button{border-radius:0}.breadcrumb{--bs-breadcrumb-padding-x:0;--bs-breadcrumb-padding-y:0;--bs-breadcrumb-margin-bottom:1rem;--bs-breadcrumb-bg: ;--bs-breadcrumb-border-radius: ;--bs-breadcrumb-divider-color:#6c757d;--bs-breadcrumb-item-padding-x:0.5rem;--bs-breadcrumb-item-active-color:#6c757d;display:flex;flex-wrap:wrap;padding:var(--bs-breadcrumb-padding-y) var(--bs-breadcrumb-padding-x);margin-bottom:var(--bs-breadcrumb-margin-bottom);font-size:var(--bs-breadcrumb-font-size);list-style:none;background-color:var(--bs-breadcrumb-bg);border-radius:var(--bs-breadcrumb-border-radius)}.breadcrumb-item+.breadcrumb-item{padding-left:var(--bs-breadcrumb-item-padding-x)}.breadcrumb-item+.breadcrumb-item::before{float:left;padding-right:var(--bs-breadcrumb-item-padding-x);color:var(--bs-breadcrumb-divider-color);content:var(--bs-breadcrumb-divider, "/")}.breadcrumb-item.active{color:var(--bs-breadcrumb-item-active-color)}.pagination{--bs-pagination-padding-x:0.75rem;--bs-pagination-padding-y:0.375rem;--bs-pagination-font-size:1rem;--bs-pagination-color:var(--bs-link-color);--bs-pagination-bg:#fff;--bs-pagination-border-width:1px;--bs-pagination-border-color:#dee2e6;--bs-pagination-border-radius:0.375rem;--bs-pagination-hover-color:var(--bs-link-hover-color);--bs-pagination-hover-bg:#e9ecef;--bs-pagination-hover-border-color:#dee2e6;--bs-pagination-focus-color:var(--bs-link-hover-color);--bs-pagination-focus-bg:#e9ecef;--bs-pagination-focus-box-shadow:0 0 0 0.25rem rgba(13, 110, 253, 0.25);--bs-pagination-active-color:#fff;--bs-pagination-active-bg:#0d6efd;--bs-pagination-active-border-color:#0d6efd;--bs-pagination-disabled-color:#6c757d;--bs-pagination-disabled-bg:#fff;--bs-pagination-disabled-border-color:#dee2e6;display:flex;padding-left:0;list-style:none}.page-link{position:relative;display:block;padding:var(--bs-pagination-padding-y) var(--bs-pagination-padding-x);font-size:var(--bs-pagination-font-size);color:var(--bs-pagination-color);text-decoration:none;background-color:var(--bs-pagination-bg);border:var(--bs-pagination-border-width) solid var(--bs-pagination-border-color);transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.page-link{transition:none}}.page-link:hover{z-index:2;color:var(--bs-pagination-hover-color);background-color:var(--bs-pagination-hover-bg);border-color:var(--bs-pagination-hover-border-color)}.page-link:focus{z-index:3;color:var(--bs-pagination-focus-color);background-color:var(--bs-pagination-focus-bg);outline:0;box-shadow:var(--bs-pagination-focus-box-shadow)}.active>.page-link,.page-link.active{z-index:3;color:var(--bs-pagination-active-color);background-color:var(--bs-pagination-active-bg);border-color:var(--bs-pagination-active-border-color)}.disabled>.page-link,.page-link.disabled{color:var(--bs-pagination-disabled-color);pointer-events:none;background-color:var(--bs-pagination-disabled-bg);border-color:var(--bs-pagination-disabled-border-color)}.page-item:not(:first-child) .page-link{margin-left:-1px}.page-item:first-child .page-link{border-top-left-radius:var(--bs-pagination-border-radius);border-bottom-left-radius:var(--bs-pagination-border-radius)}.page-item:last-child .page-link{border-top-right-radius:var(--bs-pagination-border-radius);border-bottom-right-radius:var(--bs-pagination-border-radius)}.pagination-lg{--bs-pagination-padding-x:1.5rem;--bs-pagination-padding-y:0.75rem;--bs-pagination-font-size:1.25rem;--bs-pagination-border-radius:0.5rem}.pagination-sm{--bs-pagination-padding-x:0.5rem;--bs-pagination-padding-y:0.25rem;--bs-pagination-font-size:0.875rem;--bs-pagination-border-radius:0.25rem}.badge{--bs-badge-padding-x:0.65em;--bs-badge-padding-y:0.35em;--bs-badge-font-size:0.75em;--bs-badge-font-weight:700;--bs-badge-color:#fff;--bs-badge-border-radius:0.375rem;display:inline-block;padding:var(--bs-badge-padding-y) var(--bs-badge-padding-x);font-size:var(--bs-badge-font-size);font-weight:var(--bs-badge-font-weight);line-height:1;color:var(--bs-badge-color);text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:var(--bs-badge-border-radius,0)}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.alert{--bs-alert-bg:transparent;--bs-alert-padding-x:1rem;--bs-alert-padding-y:1rem;--bs-alert-margin-bottom:1rem;--bs-alert-color:inherit;--bs-alert-border-color:transparent;--bs-alert-border:1px solid var(--bs-alert-border-color);--bs-alert-border-radius:0.375rem;position:relative;padding:var(--bs-alert-padding-y) var(--bs-alert-padding-x);margin-bottom:var(--bs-alert-margin-bottom);color:var(--bs-alert-color);background-color:var(--bs-alert-bg);border:var(--bs-alert-border);border-radius:var(--bs-alert-border-radius,0)}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:3rem}.alert-dismissible .btn-close{position:absolute;top:0;right:0;z-index:2;padding:1.25rem 1rem}.alert-primary{--bs-alert-color:#084298;--bs-alert-bg:#cfe2ff;--bs-alert-border-color:#b6d4fe}.alert-primary .alert-link{color:#06357a}.alert-secondary{--bs-alert-color:#41464b;--bs-alert-bg:#e2e3e5;--bs-alert-border-color:#d3d6d8}.alert-secondary .alert-link{color:#34383c}.alert-success{--bs-alert-color:#0f5132;--bs-alert-bg:#d1e7dd;--bs-alert-border-color:#badbcc}.alert-success .alert-link{color:#0c4128}.alert-info{--bs-alert-color:#055160;--bs-alert-bg:#cff4fc;--bs-alert-border-color:#b6effb}.alert-info .alert-link{color:#04414d}.alert-warning{--bs-alert-color:#664d03;--bs-alert-bg:#fff3cd;--bs-alert-border-color:#ffecb5}.alert-warning .alert-link{color:#523e02}.alert-danger{--bs-alert-color:#842029;--bs-alert-bg:#f8d7da;--bs-alert-border-color:#f5c2c7}.alert-danger .alert-link{color:#6a1a21}.alert-light{--bs-alert-color:#636464;--bs-alert-bg:#fefefe;--bs-alert-border-color:#fdfdfe}.alert-light .alert-link{color:#4f5050}.alert-dark{--bs-alert-color:#141619;--bs-alert-bg:#d3d3d4;--bs-alert-border-color:#bcbebf}.alert-dark .alert-link{color:#101214}@-webkit-keyframes progress-bar-stripes{0%{background-position-x:1rem}}@keyframes progress-bar-stripes{0%{background-position-x:1rem}}.progress{--bs-progress-height:1rem;--bs-progress-font-size:0.75rem;--bs-progress-bg:#e9ecef;--bs-progress-border-radius:0.375rem;--bs-progress-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.075);--bs-progress-bar-color:#fff;--bs-progress-bar-bg:#0d6efd;--bs-progress-bar-transition:width 0.6s ease;display:flex;height:var(--bs-progress-height);overflow:hidden;font-size:var(--bs-progress-font-size);background-color:var(--bs-progress-bg);border-radius:var(--bs-progress-border-radius)}.progress-bar{display:flex;flex-direction:column;justify-content:center;overflow:hidden;color:var(--bs-progress-bar-color);text-align:center;white-space:nowrap;background-color:var(--bs-progress-bar-bg);transition:var(--bs-progress-bar-transition)}@media (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:var(--bs-progress-height) var(--bs-progress-height)}.progress-bar-animated{-webkit-animation:1s linear infinite progress-bar-stripes;animation:1s linear infinite progress-bar-stripes}@media (prefers-reduced-motion:reduce){.progress-bar-animated{-webkit-animation:none;animation:none}}.list-group{--bs-list-group-color:#212529;--bs-list-group-bg:#fff;--bs-list-group-border-color:rgba(0, 0, 0, 0.125);--bs-list-group-border-width:1px;--bs-list-group-border-radius:0.375rem;--bs-list-group-item-padding-x:1rem;--bs-list-group-item-padding-y:0.5rem;--bs-list-group-action-color:#495057;--bs-list-group-action-hover-color:#495057;--bs-list-group-action-hover-bg:#f8f9fa;--bs-list-group-action-active-color:#212529;--bs-list-group-action-active-bg:#e9ecef;--bs-list-group-disabled-color:#6c757d;--bs-list-group-disabled-bg:#fff;--bs-list-group-active-color:#fff;--bs-list-group-active-bg:#0d6efd;--bs-list-group-active-border-color:#0d6efd;display:flex;flex-direction:column;padding-left:0;margin-bottom:0;border-radius:var(--bs-list-group-border-radius)}.list-group-numbered{list-style-type:none;counter-reset:section}.list-group-numbered>.list-group-item::before{content:counters(section, ".") ". ";counter-increment:section}.list-group-item-action{width:100%;color:var(--bs-list-group-action-color);text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{z-index:1;color:var(--bs-list-group-action-hover-color);text-decoration:none;background-color:var(--bs-list-group-action-hover-bg)}.list-group-item-action:active{color:var(--bs-list-group-action-active-color);background-color:var(--bs-list-group-action-active-bg)}.list-group-item{position:relative;display:block;padding:var(--bs-list-group-item-padding-y) var(--bs-list-group-item-padding-x);color:var(--bs-list-group-color);text-decoration:none;background-color:var(--bs-list-group-bg);border:var(--bs-list-group-border-width) solid var(--bs-list-group-border-color)}.list-group-item:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.list-group-item:last-child{border-bottom-right-radius:inherit;border-bottom-left-radius:inherit}.list-group-item.disabled,.list-group-item:disabled{color:var(--bs-list-group-disabled-color);pointer-events:none;background-color:var(--bs-list-group-disabled-bg)}.list-group-item.active{z-index:2;color:var(--bs-list-group-active-color);background-color:var(--bs-list-group-active-bg);border-color:var(--bs-list-group-active-border-color)}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:calc(var(--bs-list-group-border-width) * -1);border-top-width:var(--bs-list-group-border-width)}.list-group-horizontal{flex-direction:row}.list-group-horizontal>.list-group-item:first-child{border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal>.list-group-item:last-child{border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:calc(var(--bs-list-group-border-width) * -1);border-left-width:var(--bs-list-group-border-width)}@media (min-width:576px){.list-group-horizontal-sm{flex-direction:row}.list-group-horizontal-sm>.list-group-item:first-child{border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-sm>.list-group-item:last-child{border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:calc(var(--bs-list-group-border-width) * -1);border-left-width:var(--bs-list-group-border-width)}}@media (min-width:768px){.list-group-horizontal-md{flex-direction:row}.list-group-horizontal-md>.list-group-item:first-child{border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-md>.list-group-item:last-child{border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:calc(var(--bs-list-group-border-width) * -1);border-left-width:var(--bs-list-group-border-width)}}@media (min-width:992px){.list-group-horizontal-lg{flex-direction:row}.list-group-horizontal-lg>.list-group-item:first-child{border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-lg>.list-group-item:last-child{border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:calc(var(--bs-list-group-border-width) * -1);border-left-width:var(--bs-list-group-border-width)}}@media (min-width:1200px){.list-group-horizontal-xl{flex-direction:row}.list-group-horizontal-xl>.list-group-item:first-child{border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-xl>.list-group-item:last-child{border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:calc(var(--bs-list-group-border-width) * -1);border-left-width:var(--bs-list-group-border-width)}}@media (min-width:1400px){.list-group-horizontal-xxl{flex-direction:row}.list-group-horizontal-xxl>.list-group-item:first-child{border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-xxl>.list-group-item:last-child{border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-xxl>.list-group-item.active{margin-top:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item.active{margin-left:calc(var(--bs-list-group-border-width) * -1);border-left-width:var(--bs-list-group-border-width)}}.list-group-flush{border-radius:0}.list-group-flush>.list-group-item{border-width:0 0 var(--bs-list-group-border-width)}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{color:#084298;background-color:#cfe2ff}.list-group-item-primary.list-group-item-action:focus,.list-group-item-primary.list-group-item-action:hover{color:#084298;background-color:#bacbe6}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#084298;border-color:#084298}.list-group-item-secondary{color:#41464b;background-color:#e2e3e5}.list-group-item-secondary.list-group-item-action:focus,.list-group-item-secondary.list-group-item-action:hover{color:#41464b;background-color:#cbccce}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#41464b;border-color:#41464b}.list-group-item-success{color:#0f5132;background-color:#d1e7dd}.list-group-item-success.list-group-item-action:focus,.list-group-item-success.list-group-item-action:hover{color:#0f5132;background-color:#bcd0c7}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#0f5132;border-color:#0f5132}.list-group-item-info{color:#055160;background-color:#cff4fc}.list-group-item-info.list-group-item-action:focus,.list-group-item-info.list-group-item-action:hover{color:#055160;background-color:#badce3}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#055160;border-color:#055160}.list-group-item-warning{color:#664d03;background-color:#fff3cd}.list-group-item-warning.list-group-item-action:focus,.list-group-item-warning.list-group-item-action:hover{color:#664d03;background-color:#e6dbb9}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#664d03;border-color:#664d03}.list-group-item-danger{color:#842029;background-color:#f8d7da}.list-group-item-danger.list-group-item-action:focus,.list-group-item-danger.list-group-item-action:hover{color:#842029;background-color:#dfc2c4}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#842029;border-color:#842029}.list-group-item-light{color:#636464;background-color:#fefefe}.list-group-item-light.list-group-item-action:focus,.list-group-item-light.list-group-item-action:hover{color:#636464;background-color:#e5e5e5}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#636464;border-color:#636464}.list-group-item-dark{color:#141619;background-color:#d3d3d4}.list-group-item-dark.list-group-item-action:focus,.list-group-item-dark.list-group-item-action:hover{color:#141619;background-color:#bebebf}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#141619;border-color:#141619}.btn-close{box-sizing:content-box;width:1em;height:1em;padding:.25em .25em;color:#000;background:transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 0 1 1.414 0L8 6.586 14.293.293a1 1 0 1 1 1.414 1.414L9.414 8l6.293 6.293a1 1 0 0 1-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L6.586 8 .293 1.707a1 1 0 0 1 0-1.414z'/%3e%3c/svg%3e") center/1em auto no-repeat;border:0;border-radius:.375rem;opacity:.5}.btn-close:hover{color:#000;text-decoration:none;opacity:.75}.btn-close:focus{outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25);opacity:1}.btn-close.disabled,.btn-close:disabled{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;opacity:.25}.btn-close-white{filter:invert(1) grayscale(100%) brightness(200%)}.toast{--bs-toast-padding-x:0.75rem;--bs-toast-padding-y:0.5rem;--bs-toast-spacing:1.5rem;--bs-toast-max-width:350px;--bs-toast-font-size:0.875rem;--bs-toast-color: ;--bs-toast-bg:rgba(255, 255, 255, 0.85);--bs-toast-border-width:1px;--bs-toast-border-color:var(--bs-border-color-translucent);--bs-toast-border-radius:0.375rem;--bs-toast-box-shadow:0 0.5rem 1rem rgba(0, 0, 0, 0.15);--bs-toast-header-color:#6c757d;--bs-toast-header-bg:rgba(255, 255, 255, 0.85);--bs-toast-header-border-color:rgba(0, 0, 0, 0.05);width:var(--bs-toast-max-width);max-width:100%;font-size:var(--bs-toast-font-size);color:var(--bs-toast-color);pointer-events:auto;background-color:var(--bs-toast-bg);background-clip:padding-box;border:var(--bs-toast-border-width) solid var(--bs-toast-border-color);box-shadow:var(--bs-toast-box-shadow);border-radius:var(--bs-toast-border-radius)}.toast.showing{opacity:0}.toast:not(.show){display:none}.toast-container{position:absolute;z-index:1090;width:-webkit-max-content;width:-moz-max-content;width:max-content;max-width:100%;pointer-events:none}.toast-container>:not(:last-child){margin-bottom:var(--bs-toast-spacing)}.toast-header{display:flex;align-items:center;padding:var(--bs-toast-padding-y) var(--bs-toast-padding-x);color:var(--bs-toast-header-color);background-color:var(--bs-toast-header-bg);background-clip:padding-box;border-bottom:var(--bs-toast-border-width) solid var(--bs-toast-header-border-color);border-top-left-radius:calc(var(--bs-toast-border-radius) - var(--bs-toast-border-width));border-top-right-radius:calc(var(--bs-toast-border-radius) - var(--bs-toast-border-width))}.toast-header .btn-close{margin-right:calc(var(--bs-toast-padding-x) * -.5);margin-left:var(--bs-toast-padding-x)}.toast-body{padding:var(--bs-toast-padding-x);word-wrap:break-word}.modal{--bs-modal-zindex:1055;--bs-modal-width:500px;--bs-modal-padding:1rem;--bs-modal-margin:0.5rem;--bs-modal-color: ;--bs-modal-bg:#fff;--bs-modal-border-color:var(--bs-border-color-translucent);--bs-modal-border-width:1px;--bs-modal-border-radius:0.5rem;--bs-modal-box-shadow:0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);--bs-modal-inner-border-radius:calc(0.5rem - 1px);--bs-modal-header-padding-x:1rem;--bs-modal-header-padding-y:1rem;--bs-modal-header-padding:1rem 1rem;--bs-modal-header-border-color:var(--bs-border-color);--bs-modal-header-border-width:1px;--bs-modal-title-line-height:1.5;--bs-modal-footer-gap:0.5rem;--bs-modal-footer-bg: ;--bs-modal-footer-border-color:var(--bs-border-color);--bs-modal-footer-border-width:1px;position:fixed;top:0;left:0;z-index:var(--bs-modal-zindex);display:none;width:100%;height:100%;overflow-x:hidden;overflow-y:auto;outline:0}.modal-dialog{position:relative;width:auto;margin:var(--bs-modal-margin);pointer-events:none}.modal.fade .modal-dialog{transition:transform .3s ease-out;transform:translate(0,-50px)}@media (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{transform:none}.modal.modal-static .modal-dialog{transform:scale(1.02)}.modal-dialog-scrollable{height:calc(100% - var(--bs-modal-margin) * 2)}.modal-dialog-scrollable .modal-content{max-height:100%;overflow:hidden}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:flex;align-items:center;min-height:calc(100% - var(--bs-modal-margin) * 2)}.modal-content{position:relative;display:flex;flex-direction:column;width:100%;color:var(--bs-modal-color);pointer-events:auto;background-color:var(--bs-modal-bg);background-clip:padding-box;border:var(--bs-modal-border-width) solid var(--bs-modal-border-color);border-radius:var(--bs-modal-border-radius);outline:0}.modal-backdrop{--bs-backdrop-zindex:1050;--bs-backdrop-bg:#000;--bs-backdrop-opacity:0.5;position:fixed;top:0;left:0;z-index:var(--bs-backdrop-zindex);width:100vw;height:100vh;background-color:var(--bs-backdrop-bg)}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:var(--bs-backdrop-opacity)}.modal-header{display:flex;flex-shrink:0;align-items:center;justify-content:space-between;padding:var(--bs-modal-header-padding);border-bottom:var(--bs-modal-header-border-width) solid var(--bs-modal-header-border-color);border-top-left-radius:var(--bs-modal-inner-border-radius);border-top-right-radius:var(--bs-modal-inner-border-radius)}.modal-header .btn-close{padding:calc(var(--bs-modal-header-padding-y) * .5) calc(var(--bs-modal-header-padding-x) * .5);margin:calc(var(--bs-modal-header-padding-y) * -.5) calc(var(--bs-modal-header-padding-x) * -.5) calc(var(--bs-modal-header-padding-y) * -.5) auto}.modal-title{margin-bottom:0;line-height:var(--bs-modal-title-line-height)}.modal-body{position:relative;flex:1 1 auto;padding:var(--bs-modal-padding)}.modal-footer{display:flex;flex-shrink:0;flex-wrap:wrap;align-items:center;justify-content:flex-end;padding:calc(var(--bs-modal-padding) - var(--bs-modal-footer-gap) * .5);background-color:var(--bs-modal-footer-bg);border-top:var(--bs-modal-footer-border-width) solid var(--bs-modal-footer-border-color);border-bottom-right-radius:var(--bs-modal-inner-border-radius);border-bottom-left-radius:var(--bs-modal-inner-border-radius)}.modal-footer>*{margin:calc(var(--bs-modal-footer-gap) * .5)}@media (min-width:576px){.modal{--bs-modal-margin:1.75rem;--bs-modal-box-shadow:0 0.5rem 1rem rgba(0, 0, 0, 0.15)}.modal-dialog{max-width:var(--bs-modal-width);margin-right:auto;margin-left:auto}.modal-sm{--bs-modal-width:300px}}@media (min-width:992px){.modal-lg,.modal-xl{--bs-modal-width:800px}}@media (min-width:1200px){.modal-xl{--bs-modal-width:1140px}}.modal-fullscreen{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen .modal-footer,.modal-fullscreen .modal-header{border-radius:0}.modal-fullscreen .modal-body{overflow-y:auto}@media (max-width:575.98px){.modal-fullscreen-sm-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-sm-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-sm-down .modal-footer,.modal-fullscreen-sm-down .modal-header{border-radius:0}.modal-fullscreen-sm-down .modal-body{overflow-y:auto}}@media (max-width:767.98px){.modal-fullscreen-md-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-md-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-md-down .modal-footer,.modal-fullscreen-md-down .modal-header{border-radius:0}.modal-fullscreen-md-down .modal-body{overflow-y:auto}}@media (max-width:991.98px){.modal-fullscreen-lg-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-lg-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-lg-down .modal-footer,.modal-fullscreen-lg-down .modal-header{border-radius:0}.modal-fullscreen-lg-down .modal-body{overflow-y:auto}}@media (max-width:1199.98px){.modal-fullscreen-xl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xl-down .modal-footer,.modal-fullscreen-xl-down .modal-header{border-radius:0}.modal-fullscreen-xl-down .modal-body{overflow-y:auto}}@media (max-width:1399.98px){.modal-fullscreen-xxl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xxl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xxl-down .modal-footer,.modal-fullscreen-xxl-down .modal-header{border-radius:0}.modal-fullscreen-xxl-down .modal-body{overflow-y:auto}}.tooltip{--bs-tooltip-zindex:1080;--bs-tooltip-max-width:200px;--bs-tooltip-padding-x:0.5rem;--bs-tooltip-padding-y:0.25rem;--bs-tooltip-margin: ;--bs-tooltip-font-size:0.875rem;--bs-tooltip-color:#fff;--bs-tooltip-bg:#000;--bs-tooltip-border-radius:0.375rem;--bs-tooltip-opacity:0.9;--bs-tooltip-arrow-width:0.8rem;--bs-tooltip-arrow-height:0.4rem;z-index:var(--bs-tooltip-zindex);display:block;padding:var(--bs-tooltip-arrow-height);margin:var(--bs-tooltip-margin);font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;white-space:normal;word-spacing:normal;line-break:auto;font-size:var(--bs-tooltip-font-size);word-wrap:break-word;opacity:0}.tooltip.show{opacity:var(--bs-tooltip-opacity)}.tooltip .tooltip-arrow{display:block;width:var(--bs-tooltip-arrow-width);height:var(--bs-tooltip-arrow-height)}.tooltip .tooltip-arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow,.bs-tooltip-top .tooltip-arrow{bottom:0}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow::before,.bs-tooltip-top .tooltip-arrow::before{top:-1px;border-width:var(--bs-tooltip-arrow-height) calc(var(--bs-tooltip-arrow-width) * .5) 0;border-top-color:var(--bs-tooltip-bg)}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow,.bs-tooltip-end .tooltip-arrow{left:0;width:var(--bs-tooltip-arrow-height);height:var(--bs-tooltip-arrow-width)}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow::before,.bs-tooltip-end .tooltip-arrow::before{right:-1px;border-width:calc(var(--bs-tooltip-arrow-width) * .5) var(--bs-tooltip-arrow-height) calc(var(--bs-tooltip-arrow-width) * .5) 0;border-right-color:var(--bs-tooltip-bg)}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow,.bs-tooltip-bottom .tooltip-arrow{top:0}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow::before,.bs-tooltip-bottom .tooltip-arrow::before{bottom:-1px;border-width:0 calc(var(--bs-tooltip-arrow-width) * .5) var(--bs-tooltip-arrow-height);border-bottom-color:var(--bs-tooltip-bg)}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow,.bs-tooltip-start .tooltip-arrow{right:0;width:var(--bs-tooltip-arrow-height);height:var(--bs-tooltip-arrow-width)}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow::before,.bs-tooltip-start .tooltip-arrow::before{left:-1px;border-width:calc(var(--bs-tooltip-arrow-width) * .5) 0 calc(var(--bs-tooltip-arrow-width) * .5) var(--bs-tooltip-arrow-height);border-left-color:var(--bs-tooltip-bg)}.tooltip-inner{max-width:var(--bs-tooltip-max-width);padding:var(--bs-tooltip-padding-y) var(--bs-tooltip-padding-x);color:var(--bs-tooltip-color);text-align:center;background-color:var(--bs-tooltip-bg);border-radius:var(--bs-tooltip-border-radius,0)}.popover{--bs-popover-zindex:1070;--bs-popover-max-width:276px;--bs-popover-font-size:0.875rem;--bs-popover-bg:#fff;--bs-popover-border-width:1px;--bs-popover-border-color:var(--bs-border-color-translucent);--bs-popover-border-radius:0.5rem;--bs-popover-inner-border-radius:calc(0.5rem - 1px);--bs-popover-box-shadow:0 0.5rem 1rem rgba(0, 0, 0, 0.15);--bs-popover-header-padding-x:1rem;--bs-popover-header-padding-y:0.5rem;--bs-popover-header-font-size:1rem;--bs-popover-header-color:var(--bs-heading-color);--bs-popover-header-bg:#f0f0f0;--bs-popover-body-padding-x:1rem;--bs-popover-body-padding-y:1rem;--bs-popover-body-color:#212529;--bs-popover-arrow-width:1rem;--bs-popover-arrow-height:0.5rem;--bs-popover-arrow-border:var(--bs-popover-border-color);z-index:var(--bs-popover-zindex);display:block;max-width:var(--bs-popover-max-width);font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;white-space:normal;word-spacing:normal;line-break:auto;font-size:var(--bs-popover-font-size);word-wrap:break-word;background-color:var(--bs-popover-bg);background-clip:padding-box;border:var(--bs-popover-border-width) solid var(--bs-popover-border-color);border-radius:var(--bs-popover-border-radius)}.popover .popover-arrow{display:block;width:var(--bs-popover-arrow-width);height:var(--bs-popover-arrow-height)}.popover .popover-arrow::after,.popover .popover-arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid;border-width:0}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow,.bs-popover-top>.popover-arrow{bottom:calc(var(--bs-popover-arrow-height) * -1 - var(--bs-popover-border-width))}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::after,.bs-popover-top>.popover-arrow::before{border-width:var(--bs-popover-arrow-height) calc(var(--bs-popover-arrow-width) * .5) 0}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::before{bottom:0;border-top-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after,.bs-popover-top>.popover-arrow::after{bottom:var(--bs-popover-border-width);border-top-color:var(--bs-popover-bg)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow,.bs-popover-end>.popover-arrow{left:calc(var(--bs-popover-arrow-height) * -1 - var(--bs-popover-border-width));width:var(--bs-popover-arrow-height);height:var(--bs-popover-arrow-width)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::after,.bs-popover-end>.popover-arrow::before{border-width:calc(var(--bs-popover-arrow-width) * .5) var(--bs-popover-arrow-height) calc(var(--bs-popover-arrow-width) * .5) 0}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::before{left:0;border-right-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after,.bs-popover-end>.popover-arrow::after{left:var(--bs-popover-border-width);border-right-color:var(--bs-popover-bg)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow,.bs-popover-bottom>.popover-arrow{top:calc(var(--bs-popover-arrow-height) * -1 - var(--bs-popover-border-width))}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::after,.bs-popover-bottom>.popover-arrow::before{border-width:0 calc(var(--bs-popover-arrow-width) * .5) var(--bs-popover-arrow-height)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::before{top:0;border-bottom-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after,.bs-popover-bottom>.popover-arrow::after{top:var(--bs-popover-border-width);border-bottom-color:var(--bs-popover-bg)}.bs-popover-auto[data-popper-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:var(--bs-popover-arrow-width);margin-left:calc(var(--bs-popover-arrow-width) * -.5);content:"";border-bottom:var(--bs-popover-border-width) solid var(--bs-popover-header-bg)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow,.bs-popover-start>.popover-arrow{right:calc(var(--bs-popover-arrow-height) * -1 - var(--bs-popover-border-width));width:var(--bs-popover-arrow-height);height:var(--bs-popover-arrow-width)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::after,.bs-popover-start>.popover-arrow::before{border-width:calc(var(--bs-popover-arrow-width) * .5) 0 calc(var(--bs-popover-arrow-width) * .5) var(--bs-popover-arrow-height)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::before{right:0;border-left-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after,.bs-popover-start>.popover-arrow::after{right:var(--bs-popover-border-width);border-left-color:var(--bs-popover-bg)}.popover-header{padding:var(--bs-popover-header-padding-y) var(--bs-popover-header-padding-x);margin-bottom:0;font-size:var(--bs-popover-header-font-size);color:var(--bs-popover-header-color);background-color:var(--bs-popover-header-bg);border-bottom:var(--bs-popover-border-width) solid var(--bs-popover-border-color);border-top-left-radius:var(--bs-popover-inner-border-radius);border-top-right-radius:var(--bs-popover-inner-border-radius)}.popover-header:empty{display:none}.popover-body{padding:var(--bs-popover-body-padding-y) var(--bs-popover-body-padding-x);color:var(--bs-popover-body-color)}.carousel{position:relative}.carousel.pointer-event{touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;transition:transform .6s ease-in-out}@media (prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-end,.carousel-item-next:not(.carousel-item-start){transform:translateX(100%)}.active.carousel-item-start,.carousel-item-prev:not(.carousel-item-end){transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;transform:none}.carousel-fade .carousel-item-next.carousel-item-start,.carousel-fade .carousel-item-prev.carousel-item-end,.carousel-fade .carousel-item.active{z-index:1;opacity:1}.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{z-index:0;opacity:0;transition:opacity 0s .6s}@media (prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{transition:none}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;z-index:1;display:flex;align-items:center;justify-content:center;width:15%;padding:0;color:#fff;text-align:center;background:0 0;border:0;opacity:.5;transition:opacity .15s ease}@media (prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:2rem;height:2rem;background-repeat:no-repeat;background-position:50%;background-size:100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:2;display:flex;justify-content:center;padding:0;margin-right:15%;margin-bottom:1rem;margin-left:15%;list-style:none}.carousel-indicators [data-bs-target]{box-sizing:content-box;flex:0 1 auto;width:30px;height:3px;padding:0;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border:0;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media (prefers-reduced-motion:reduce){.carousel-indicators [data-bs-target]{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:1.25rem;left:15%;padding-top:1.25rem;padding-bottom:1.25rem;color:#fff;text-align:center}.carousel-dark .carousel-control-next-icon,.carousel-dark .carousel-control-prev-icon{filter:invert(1) grayscale(100)}.carousel-dark .carousel-indicators [data-bs-target]{background-color:#000}.carousel-dark .carousel-caption{color:#000}.spinner-border,.spinner-grow{display:inline-block;width:var(--bs-spinner-width);height:var(--bs-spinner-height);vertical-align:var(--bs-spinner-vertical-align);border-radius:50%;-webkit-animation:var(--bs-spinner-animation-speed) linear infinite var(--bs-spinner-animation-name);animation:var(--bs-spinner-animation-speed) linear infinite var(--bs-spinner-animation-name)}@-webkit-keyframes spinner-border{to{transform:rotate(360deg)}}@keyframes spinner-border{to{transform:rotate(360deg)}}.spinner-border{--bs-spinner-width:2rem;--bs-spinner-height:2rem;--bs-spinner-vertical-align:-0.125em;--bs-spinner-border-width:0.25em;--bs-spinner-animation-speed:0.75s;--bs-spinner-animation-name:spinner-border;border:var(--bs-spinner-border-width) solid currentcolor;border-right-color:transparent}.spinner-border-sm{--bs-spinner-width:1rem;--bs-spinner-height:1rem;--bs-spinner-border-width:0.2em}@-webkit-keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}@keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}.spinner-grow{--bs-spinner-width:2rem;--bs-spinner-height:2rem;--bs-spinner-vertical-align:-0.125em;--bs-spinner-animation-speed:0.75s;--bs-spinner-animation-name:spinner-grow;background-color:currentcolor;opacity:0}.spinner-grow-sm{--bs-spinner-width:1rem;--bs-spinner-height:1rem}@media (prefers-reduced-motion:reduce){.spinner-border,.spinner-grow{--bs-spinner-animation-speed:1.5s}}.offcanvas,.offcanvas-lg,.offcanvas-md,.offcanvas-sm,.offcanvas-xl,.offcanvas-xxl{--bs-offcanvas-width:400px;--bs-offcanvas-height:30vh;--bs-offcanvas-padding-x:1rem;--bs-offcanvas-padding-y:1rem;--bs-offcanvas-color: ;--bs-offcanvas-bg:#fff;--bs-offcanvas-border-width:1px;--bs-offcanvas-border-color:var(--bs-border-color-translucent);--bs-offcanvas-box-shadow:0 0.125rem 0.25rem rgba(0, 0, 0, 0.075)}@media (max-width:575.98px){.offcanvas-sm{position:fixed;bottom:0;z-index:1045;display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:transform .3s ease-in-out}}@media (max-width:575.98px) and (prefers-reduced-motion:reduce){.offcanvas-sm{transition:none}}@media (max-width:575.98px){.offcanvas-sm.show:not(.hiding),.offcanvas-sm.showing{transform:none}}@media (max-width:575.98px){.offcanvas-sm.hiding,.offcanvas-sm.show,.offcanvas-sm.showing{visibility:visible}}@media (max-width:575.98px){.offcanvas-sm.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}}@media (max-width:575.98px){.offcanvas-sm.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}}@media (max-width:575.98px){.offcanvas-sm.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}}@media (max-width:575.98px){.offcanvas-sm.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}}@media (min-width:576px){.offcanvas-sm{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-sm .offcanvas-header{display:none}.offcanvas-sm .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:767.98px){.offcanvas-md{position:fixed;bottom:0;z-index:1045;display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:transform .3s ease-in-out}}@media (max-width:767.98px) and (prefers-reduced-motion:reduce){.offcanvas-md{transition:none}}@media (max-width:767.98px){.offcanvas-md.show:not(.hiding),.offcanvas-md.showing{transform:none}}@media (max-width:767.98px){.offcanvas-md.hiding,.offcanvas-md.show,.offcanvas-md.showing{visibility:visible}}@media (max-width:767.98px){.offcanvas-md.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}}@media (max-width:767.98px){.offcanvas-md.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}}@media (max-width:767.98px){.offcanvas-md.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}}@media (max-width:767.98px){.offcanvas-md.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}}@media (min-width:768px){.offcanvas-md{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-md .offcanvas-header{display:none}.offcanvas-md .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:991.98px){.offcanvas-lg{position:fixed;bottom:0;z-index:1045;display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:transform .3s ease-in-out}}@media (max-width:991.98px) and (prefers-reduced-motion:reduce){.offcanvas-lg{transition:none}}@media (max-width:991.98px){.offcanvas-lg.show:not(.hiding),.offcanvas-lg.showing{transform:none}}@media (max-width:991.98px){.offcanvas-lg.hiding,.offcanvas-lg.show,.offcanvas-lg.showing{visibility:visible}}@media (max-width:991.98px){.offcanvas-lg.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}}@media (max-width:991.98px){.offcanvas-lg.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}}@media (max-width:991.98px){.offcanvas-lg.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}}@media (max-width:991.98px){.offcanvas-lg.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}}@media (min-width:992px){.offcanvas-lg{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-lg .offcanvas-header{display:none}.offcanvas-lg .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:1199.98px){.offcanvas-xl{position:fixed;bottom:0;z-index:1045;display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:transform .3s ease-in-out}}@media (max-width:1199.98px) and (prefers-reduced-motion:reduce){.offcanvas-xl{transition:none}}@media (max-width:1199.98px){.offcanvas-xl.show:not(.hiding),.offcanvas-xl.showing{transform:none}}@media (max-width:1199.98px){.offcanvas-xl.hiding,.offcanvas-xl.show,.offcanvas-xl.showing{visibility:visible}}@media (max-width:1199.98px){.offcanvas-xl.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}}@media (max-width:1199.98px){.offcanvas-xl.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}}@media (max-width:1199.98px){.offcanvas-xl.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}}@media (max-width:1199.98px){.offcanvas-xl.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}}@media (min-width:1200px){.offcanvas-xl{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-xl .offcanvas-header{display:none}.offcanvas-xl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:1399.98px){.offcanvas-xxl{position:fixed;bottom:0;z-index:1045;display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:transform .3s ease-in-out}}@media (max-width:1399.98px) and (prefers-reduced-motion:reduce){.offcanvas-xxl{transition:none}}@media (max-width:1399.98px){.offcanvas-xxl.show:not(.hiding),.offcanvas-xxl.showing{transform:none}}@media (max-width:1399.98px){.offcanvas-xxl.hiding,.offcanvas-xxl.show,.offcanvas-xxl.showing{visibility:visible}}@media (max-width:1399.98px){.offcanvas-xxl.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}}@media (max-width:1399.98px){.offcanvas-xxl.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}}@media (max-width:1399.98px){.offcanvas-xxl.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}}@media (max-width:1399.98px){.offcanvas-xxl.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}}@media (min-width:1400px){.offcanvas-xxl{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-xxl .offcanvas-header{display:none}.offcanvas-xxl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}.offcanvas{position:fixed;bottom:0;z-index:1045;display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:transform .3s ease-in-out}@media (prefers-reduced-motion:reduce){.offcanvas{transition:none}}.offcanvas.show:not(.hiding),.offcanvas.showing{transform:none}.offcanvas.hiding,.offcanvas.show,.offcanvas.showing{visibility:visible}.offcanvas.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.offcanvas-backdrop.fade{opacity:0}.offcanvas-backdrop.show{opacity:.5}.offcanvas-header{display:flex;align-items:center;justify-content:space-between;padding:var(--bs-offcanvas-padding-y) var(--bs-offcanvas-padding-x)}.offcanvas-header .btn-close{padding:calc(var(--bs-offcanvas-padding-y) * .5) calc(var(--bs-offcanvas-padding-x) * .5);margin-top:calc(var(--bs-offcanvas-padding-y) * -.5);margin-right:calc(var(--bs-offcanvas-padding-x) * -.5);margin-bottom:calc(var(--bs-offcanvas-padding-y) * -.5)}.offcanvas-title{margin-bottom:0;line-height:1.5}.offcanvas-body{flex-grow:1;padding:var(--bs-offcanvas-padding-y) var(--bs-offcanvas-padding-x);overflow-y:auto}.placeholder{display:inline-block;min-height:1em;vertical-align:middle;cursor:wait;background-color:currentcolor;opacity:.5}.placeholder.btn::before{display:inline-block;content:""}.placeholder-xs{min-height:.6em}.placeholder-sm{min-height:.8em}.placeholder-lg{min-height:1.2em}.placeholder-glow .placeholder{-webkit-animation:placeholder-glow 2s ease-in-out infinite;animation:placeholder-glow 2s ease-in-out infinite}@-webkit-keyframes placeholder-glow{50%{opacity:.2}}@keyframes placeholder-glow{50%{opacity:.2}}.placeholder-wave{-webkit-mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,0.8) 75%,#000 95%);mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,0.8) 75%,#000 95%);-webkit-mask-size:200% 100%;mask-size:200% 100%;-webkit-animation:placeholder-wave 2s linear infinite;animation:placeholder-wave 2s linear infinite}@-webkit-keyframes placeholder-wave{100%{-webkit-mask-position:-200% 0%;mask-position:-200% 0%}}@keyframes placeholder-wave{100%{-webkit-mask-position:-200% 0%;mask-position:-200% 0%}}.clearfix::after{display:block;clear:both;content:""}.text-bg-primary{color:#fff!important;background-color:RGBA(13,110,253,var(--bs-bg-opacity,1))!important}.text-bg-secondary{color:#fff!important;background-color:RGBA(108,117,125,var(--bs-bg-opacity,1))!important}.text-bg-success{color:#fff!important;background-color:RGBA(25,135,84,var(--bs-bg-opacity,1))!important}.text-bg-info{color:#000!important;background-color:RGBA(13,202,240,var(--bs-bg-opacity,1))!important}.text-bg-warning{color:#000!important;background-color:RGBA(255,193,7,var(--bs-bg-opacity,1))!important}.text-bg-danger{color:#fff!important;background-color:RGBA(220,53,69,var(--bs-bg-opacity,1))!important}.text-bg-light{color:#000!important;background-color:RGBA(248,249,250,var(--bs-bg-opacity,1))!important}.text-bg-dark{color:#fff!important;background-color:RGBA(33,37,41,var(--bs-bg-opacity,1))!important}.link-primary{color:#0d6efd!important}.link-primary:focus,.link-primary:hover{color:#0a58ca!important}.link-secondary{color:#6c757d!important}.link-secondary:focus,.link-secondary:hover{color:#565e64!important}.link-success{color:#198754!important}.link-success:focus,.link-success:hover{color:#146c43!important}.link-info{color:#0dcaf0!important}.link-info:focus,.link-info:hover{color:#3dd5f3!important}.link-warning{color:#ffc107!important}.link-warning:focus,.link-warning:hover{color:#ffcd39!important}.link-danger{color:#dc3545!important}.link-danger:focus,.link-danger:hover{color:#b02a37!important}.link-light{color:#f8f9fa!important}.link-light:focus,.link-light:hover{color:#f9fafb!important}.link-dark{color:#212529!important}.link-dark:focus,.link-dark:hover{color:#1a1e21!important}.ratio{position:relative;width:100%}.ratio::before{display:block;padding-top:var(--bs-aspect-ratio);content:""}.ratio>*{position:absolute;top:0;left:0;width:100%;height:100%}.ratio-1x1{--bs-aspect-ratio:100%}.ratio-4x3{--bs-aspect-ratio:75%}.ratio-16x9{--bs-aspect-ratio:56.25%}.ratio-21x9{--bs-aspect-ratio:42.8571428571%}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}@media (min-width:576px){.sticky-sm-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-sm-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media (min-width:768px){.sticky-md-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-md-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media (min-width:992px){.sticky-lg-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-lg-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media (min-width:1200px){.sticky-xl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-xl-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media (min-width:1400px){.sticky-xxl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-xxl-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}.hstack{display:flex;flex-direction:row;align-items:center;align-self:stretch}.vstack{display:flex;flex:1 1 auto;flex-direction:column;align-self:stretch}.visually-hidden,.visually-hidden-focusable:not(:focus):not(:focus-within){position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:""}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.vr{display:inline-block;align-self:stretch;width:1px;min-height:1em;background-color:currentcolor;opacity:.25}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.float-start{float:left!important}.float-end{float:right!important}.float-none{float:none!important}.opacity-0{opacity:0!important}.opacity-25{opacity:.25!important}.opacity-50{opacity:.5!important}.opacity-75{opacity:.75!important}.opacity-100{opacity:1!important}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.overflow-visible{overflow:visible!important}.overflow-scroll{overflow:scroll!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-grid{display:grid!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:flex!important}.d-inline-flex{display:inline-flex!important}.d-none{display:none!important}.shadow{box-shadow:0 .5rem 1rem rgba(0,0,0,.15)!important}.shadow-sm{box-shadow:0 .125rem .25rem rgba(0,0,0,.075)!important}.shadow-lg{box-shadow:0 1rem 3rem rgba(0,0,0,.175)!important}.shadow-none{box-shadow:none!important}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:-webkit-sticky!important;position:sticky!important}.top-0{top:0!important}.top-50{top:50%!important}.top-100{top:100%!important}.bottom-0{bottom:0!important}.bottom-50{bottom:50%!important}.bottom-100{bottom:100%!important}.start-0{left:0!important}.start-50{left:50%!important}.start-100{left:100%!important}.end-0{right:0!important}.end-50{right:50%!important}.end-100{right:100%!important}.translate-middle{transform:translate(-50%,-50%)!important}.translate-middle-x{transform:translateX(-50%)!important}.translate-middle-y{transform:translateY(-50%)!important}.border{border:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-0{border:0!important}.border-top{border-top:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-top-0{border-top:0!important}.border-end{border-right:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-end-0{border-right:0!important}.border-bottom{border-bottom:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-bottom-0{border-bottom:0!important}.border-start{border-left:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-start-0{border-left:0!important}.border-primary{--bs-border-opacity:1;border-color:rgba(var(--bs-primary-rgb),var(--bs-border-opacity))!important}.border-secondary{--bs-border-opacity:1;border-color:rgba(var(--bs-secondary-rgb),var(--bs-border-opacity))!important}.border-success{--bs-border-opacity:1;border-color:rgba(var(--bs-success-rgb),var(--bs-border-opacity))!important}.border-info{--bs-border-opacity:1;border-color:rgba(var(--bs-info-rgb),var(--bs-border-opacity))!important}.border-warning{--bs-border-opacity:1;border-color:rgba(var(--bs-warning-rgb),var(--bs-border-opacity))!important}.border-danger{--bs-border-opacity:1;border-color:rgba(var(--bs-danger-rgb),var(--bs-border-opacity))!important}.border-light{--bs-border-opacity:1;border-color:rgba(var(--bs-light-rgb),var(--bs-border-opacity))!important}.border-dark{--bs-border-opacity:1;border-color:rgba(var(--bs-dark-rgb),var(--bs-border-opacity))!important}.border-white{--bs-border-opacity:1;border-color:rgba(var(--bs-white-rgb),var(--bs-border-opacity))!important}.border-1{--bs-border-width:1px}.border-2{--bs-border-width:2px}.border-3{--bs-border-width:3px}.border-4{--bs-border-width:4px}.border-5{--bs-border-width:5px}.border-opacity-10{--bs-border-opacity:0.1}.border-opacity-25{--bs-border-opacity:0.25}.border-opacity-50{--bs-border-opacity:0.5}.border-opacity-75{--bs-border-opacity:0.75}.border-opacity-100{--bs-border-opacity:1}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.mw-100{max-width:100%!important}.vw-100{width:100vw!important}.min-vw-100{min-width:100vw!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mh-100{max-height:100%!important}.vh-100{height:100vh!important}.min-vh-100{min-height:100vh!important}.flex-fill{flex:1 1 auto!important}.flex-row{flex-direction:row!important}.flex-column{flex-direction:column!important}.flex-row-reverse{flex-direction:row-reverse!important}.flex-column-reverse{flex-direction:column-reverse!important}.flex-grow-0{flex-grow:0!important}.flex-grow-1{flex-grow:1!important}.flex-shrink-0{flex-shrink:0!important}.flex-shrink-1{flex-shrink:1!important}.flex-wrap{flex-wrap:wrap!important}.flex-nowrap{flex-wrap:nowrap!important}.flex-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-start{justify-content:flex-start!important}.justify-content-end{justify-content:flex-end!important}.justify-content-center{justify-content:center!important}.justify-content-between{justify-content:space-between!important}.justify-content-around{justify-content:space-around!important}.justify-content-evenly{justify-content:space-evenly!important}.align-items-start{align-items:flex-start!important}.align-items-end{align-items:flex-end!important}.align-items-center{align-items:center!important}.align-items-baseline{align-items:baseline!important}.align-items-stretch{align-items:stretch!important}.align-content-start{align-content:flex-start!important}.align-content-end{align-content:flex-end!important}.align-content-center{align-content:center!important}.align-content-between{align-content:space-between!important}.align-content-around{align-content:space-around!important}.align-content-stretch{align-content:stretch!important}.align-self-auto{align-self:auto!important}.align-self-start{align-self:flex-start!important}.align-self-end{align-self:flex-end!important}.align-self-center{align-self:center!important}.align-self-baseline{align-self:baseline!important}.align-self-stretch{align-self:stretch!important}.order-first{order:-1!important}.order-0{order:0!important}.order-1{order:1!important}.order-2{order:2!important}.order-3{order:3!important}.order-4{order:4!important}.order-5{order:5!important}.order-last{order:6!important}.m-0{margin:0!important}.m-1{margin:.25rem!important}.m-2{margin:.5rem!important}.m-3{margin:1rem!important}.m-4{margin:1.5rem!important}.m-5{margin:3rem!important}.m-auto{margin:auto!important}.mx-0{margin-right:0!important;margin-left:0!important}.mx-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-3{margin-right:1rem!important;margin-left:1rem!important}.mx-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-5{margin-right:3rem!important;margin-left:3rem!important}.mx-auto{margin-right:auto!important;margin-left:auto!important}.my-0{margin-top:0!important;margin-bottom:0!important}.my-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-0{margin-top:0!important}.mt-1{margin-top:.25rem!important}.mt-2{margin-top:.5rem!important}.mt-3{margin-top:1rem!important}.mt-4{margin-top:1.5rem!important}.mt-5{margin-top:3rem!important}.mt-auto{margin-top:auto!important}.me-0{margin-right:0!important}.me-1{margin-right:.25rem!important}.me-2{margin-right:.5rem!important}.me-3{margin-right:1rem!important}.me-4{margin-right:1.5rem!important}.me-5{margin-right:3rem!important}.me-auto{margin-right:auto!important}.mb-0{margin-bottom:0!important}.mb-1{margin-bottom:.25rem!important}.mb-2{margin-bottom:.5rem!important}.mb-3{margin-bottom:1rem!important}.mb-4{margin-bottom:1.5rem!important}.mb-5{margin-bottom:3rem!important}.mb-auto{margin-bottom:auto!important}.ms-0{margin-left:0!important}.ms-1{margin-left:.25rem!important}.ms-2{margin-left:.5rem!important}.ms-3{margin-left:1rem!important}.ms-4{margin-left:1.5rem!important}.ms-5{margin-left:3rem!important}.ms-auto{margin-left:auto!important}.p-0{padding:0!important}.p-1{padding:.25rem!important}.p-2{padding:.5rem!important}.p-3{padding:1rem!important}.p-4{padding:1.5rem!important}.p-5{padding:3rem!important}.px-0{padding-right:0!important;padding-left:0!important}.px-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-3{padding-right:1rem!important;padding-left:1rem!important}.px-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-5{padding-right:3rem!important;padding-left:3rem!important}.py-0{padding-top:0!important;padding-bottom:0!important}.py-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-0{padding-top:0!important}.pt-1{padding-top:.25rem!important}.pt-2{padding-top:.5rem!important}.pt-3{padding-top:1rem!important}.pt-4{padding-top:1.5rem!important}.pt-5{padding-top:3rem!important}.pe-0{padding-right:0!important}.pe-1{padding-right:.25rem!important}.pe-2{padding-right:.5rem!important}.pe-3{padding-right:1rem!important}.pe-4{padding-right:1.5rem!important}.pe-5{padding-right:3rem!important}.pb-0{padding-bottom:0!important}.pb-1{padding-bottom:.25rem!important}.pb-2{padding-bottom:.5rem!important}.pb-3{padding-bottom:1rem!important}.pb-4{padding-bottom:1.5rem!important}.pb-5{padding-bottom:3rem!important}.ps-0{padding-left:0!important}.ps-1{padding-left:.25rem!important}.ps-2{padding-left:.5rem!important}.ps-3{padding-left:1rem!important}.ps-4{padding-left:1.5rem!important}.ps-5{padding-left:3rem!important}.gap-0{gap:0!important}.gap-1{gap:.25rem!important}.gap-2{gap:.5rem!important}.gap-3{gap:1rem!important}.gap-4{gap:1.5rem!important}.gap-5{gap:3rem!important}.font-monospace{font-family:var(--bs-font-monospace)!important}.fs-1{font-size:calc(1.375rem + 1.5vw)!important}.fs-2{font-size:calc(1.325rem + .9vw)!important}.fs-3{font-size:calc(1.3rem + .6vw)!important}.fs-4{font-size:calc(1.275rem + .3vw)!important}.fs-5{font-size:1.25rem!important}.fs-6{font-size:1rem!important}.fst-italic{font-style:italic!important}.fst-normal{font-style:normal!important}.fw-light{font-weight:300!important}.fw-lighter{font-weight:lighter!important}.fw-normal{font-weight:400!important}.fw-bold{font-weight:700!important}.fw-semibold{font-weight:600!important}.fw-bolder{font-weight:bolder!important}.lh-1{line-height:1!important}.lh-sm{line-height:1.25!important}.lh-base{line-height:1.5!important}.lh-lg{line-height:2!important}.text-start{text-align:left!important}.text-end{text-align:right!important}.text-center{text-align:center!important}.text-decoration-none{text-decoration:none!important}.text-decoration-underline{text-decoration:underline!important}.text-decoration-line-through{text-decoration:line-through!important}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.text-wrap{white-space:normal!important}.text-nowrap{white-space:nowrap!important}.text-break{word-wrap:break-word!important;word-break:break-word!important}.text-primary{--bs-text-opacity:1;color:rgba(var(--bs-primary-rgb),var(--bs-text-opacity))!important}.text-secondary{--bs-text-opacity:1;color:rgba(var(--bs-secondary-rgb),var(--bs-text-opacity))!important}.text-success{--bs-text-opacity:1;color:rgba(var(--bs-success-rgb),var(--bs-text-opacity))!important}.text-info{--bs-text-opacity:1;color:rgba(var(--bs-info-rgb),var(--bs-text-opacity))!important}.text-warning{--bs-text-opacity:1;color:rgba(var(--bs-warning-rgb),var(--bs-text-opacity))!important}.text-danger{--bs-text-opacity:1;color:rgba(var(--bs-danger-rgb),var(--bs-text-opacity))!important}.text-light{--bs-text-opacity:1;color:rgba(var(--bs-light-rgb),var(--bs-text-opacity))!important}.text-dark{--bs-text-opacity:1;color:rgba(var(--bs-dark-rgb),var(--bs-text-opacity))!important}.text-black{--bs-text-opacity:1;color:rgba(var(--bs-black-rgb),var(--bs-text-opacity))!important}.text-white{--bs-text-opacity:1;color:rgba(var(--bs-white-rgb),var(--bs-text-opacity))!important}.text-body{--bs-text-opacity:1;color:rgba(var(--bs-body-color-rgb),var(--bs-text-opacity))!important}.text-muted{--bs-text-opacity:1;color:rgba(var(--bs-body-color-rgb),.75)!important}.text-black-50{--bs-text-opacity:1;color:rgba(0,0,0,.5)!important}.text-white-50{--bs-text-opacity:1;color:rgba(255,255,255,.5)!important}.text-reset{--bs-text-opacity:1;color:inherit!important}.text-opacity-25{--bs-text-opacity:0.25}.text-opacity-50{--bs-text-opacity:0.5}.text-opacity-75{--bs-text-opacity:0.75}.text-opacity-100{--bs-text-opacity:1}.bg-primary{--bs-bg-opacity:1;background-color:rgba(var(--bs-primary-rgb),var(--bs-bg-opacity))!important}.bg-secondary{--bs-bg-opacity:1;background-color:rgba(var(--bs-secondary-rgb),var(--bs-bg-opacity))!important}.bg-success{--bs-bg-opacity:1;background-color:rgba(var(--bs-success-rgb),var(--bs-bg-opacity))!important}.bg-info{--bs-bg-opacity:1;background-color:rgba(var(--bs-info-rgb),var(--bs-bg-opacity))!important}.bg-warning{--bs-bg-opacity:1;background-color:rgba(var(--bs-warning-rgb),var(--bs-bg-opacity))!important}.bg-danger{--bs-bg-opacity:1;background-color:rgba(var(--bs-danger-rgb),var(--bs-bg-opacity))!important}.bg-light{--bs-bg-opacity:1;background-color:rgba(var(--bs-light-rgb),var(--bs-bg-opacity))!important}.bg-dark{--bs-bg-opacity:1;background-color:rgba(var(--bs-dark-rgb),var(--bs-bg-opacity))!important}.bg-black{--bs-bg-opacity:1;background-color:rgba(var(--bs-black-rgb),var(--bs-bg-opacity))!important}.bg-white{--bs-bg-opacity:1;background-color:rgba(var(--bs-white-rgb),var(--bs-bg-opacity))!important}.bg-body{--bs-bg-opacity:1;background-color:rgba(var(--bs-body-bg-rgb),var(--bs-bg-opacity))!important}.bg-transparent{--bs-bg-opacity:1;background-color:transparent!important}.bg-opacity-10{--bs-bg-opacity:0.1}.bg-opacity-25{--bs-bg-opacity:0.25}.bg-opacity-50{--bs-bg-opacity:0.5}.bg-opacity-75{--bs-bg-opacity:0.75}.bg-opacity-100{--bs-bg-opacity:1}.bg-gradient{background-image:var(--bs-gradient)!important}.user-select-all{-webkit-user-select:all!important;-moz-user-select:all!important;user-select:all!important}.user-select-auto{-webkit-user-select:auto!important;-moz-user-select:auto!important;user-select:auto!important}.user-select-none{-webkit-user-select:none!important;-moz-user-select:none!important;user-select:none!important}.pe-none{pointer-events:none!important}.pe-auto{pointer-events:auto!important}.rounded{border-radius:var(--bs-border-radius)!important}.rounded-0{border-radius:0!important}.rounded-1{border-radius:var(--bs-border-radius-sm)!important}.rounded-2{border-radius:var(--bs-border-radius)!important}.rounded-3{border-radius:var(--bs-border-radius-lg)!important}.rounded-4{border-radius:var(--bs-border-radius-xl)!important}.rounded-5{border-radius:var(--bs-border-radius-2xl)!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:var(--bs-border-radius-pill)!important}.rounded-top{border-top-left-radius:var(--bs-border-radius)!important;border-top-right-radius:var(--bs-border-radius)!important}.rounded-end{border-top-right-radius:var(--bs-border-radius)!important;border-bottom-right-radius:var(--bs-border-radius)!important}.rounded-bottom{border-bottom-right-radius:var(--bs-border-radius)!important;border-bottom-left-radius:var(--bs-border-radius)!important}.rounded-start{border-bottom-left-radius:var(--bs-border-radius)!important;border-top-left-radius:var(--bs-border-radius)!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}@media (min-width:576px){.float-sm-start{float:left!important}.float-sm-end{float:right!important}.float-sm-none{float:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-grid{display:grid!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:flex!important}.d-sm-inline-flex{display:inline-flex!important}.d-sm-none{display:none!important}.flex-sm-fill{flex:1 1 auto!important}.flex-sm-row{flex-direction:row!important}.flex-sm-column{flex-direction:column!important}.flex-sm-row-reverse{flex-direction:row-reverse!important}.flex-sm-column-reverse{flex-direction:column-reverse!important}.flex-sm-grow-0{flex-grow:0!important}.flex-sm-grow-1{flex-grow:1!important}.flex-sm-shrink-0{flex-shrink:0!important}.flex-sm-shrink-1{flex-shrink:1!important}.flex-sm-wrap{flex-wrap:wrap!important}.flex-sm-nowrap{flex-wrap:nowrap!important}.flex-sm-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-sm-start{justify-content:flex-start!important}.justify-content-sm-end{justify-content:flex-end!important}.justify-content-sm-center{justify-content:center!important}.justify-content-sm-between{justify-content:space-between!important}.justify-content-sm-around{justify-content:space-around!important}.justify-content-sm-evenly{justify-content:space-evenly!important}.align-items-sm-start{align-items:flex-start!important}.align-items-sm-end{align-items:flex-end!important}.align-items-sm-center{align-items:center!important}.align-items-sm-baseline{align-items:baseline!important}.align-items-sm-stretch{align-items:stretch!important}.align-content-sm-start{align-content:flex-start!important}.align-content-sm-end{align-content:flex-end!important}.align-content-sm-center{align-content:center!important}.align-content-sm-between{align-content:space-between!important}.align-content-sm-around{align-content:space-around!important}.align-content-sm-stretch{align-content:stretch!important}.align-self-sm-auto{align-self:auto!important}.align-self-sm-start{align-self:flex-start!important}.align-self-sm-end{align-self:flex-end!important}.align-self-sm-center{align-self:center!important}.align-self-sm-baseline{align-self:baseline!important}.align-self-sm-stretch{align-self:stretch!important}.order-sm-first{order:-1!important}.order-sm-0{order:0!important}.order-sm-1{order:1!important}.order-sm-2{order:2!important}.order-sm-3{order:3!important}.order-sm-4{order:4!important}.order-sm-5{order:5!important}.order-sm-last{order:6!important}.m-sm-0{margin:0!important}.m-sm-1{margin:.25rem!important}.m-sm-2{margin:.5rem!important}.m-sm-3{margin:1rem!important}.m-sm-4{margin:1.5rem!important}.m-sm-5{margin:3rem!important}.m-sm-auto{margin:auto!important}.mx-sm-0{margin-right:0!important;margin-left:0!important}.mx-sm-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-sm-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-sm-3{margin-right:1rem!important;margin-left:1rem!important}.mx-sm-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-sm-5{margin-right:3rem!important;margin-left:3rem!important}.mx-sm-auto{margin-right:auto!important;margin-left:auto!important}.my-sm-0{margin-top:0!important;margin-bottom:0!important}.my-sm-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-sm-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-sm-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-sm-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-sm-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-sm-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-sm-0{margin-top:0!important}.mt-sm-1{margin-top:.25rem!important}.mt-sm-2{margin-top:.5rem!important}.mt-sm-3{margin-top:1rem!important}.mt-sm-4{margin-top:1.5rem!important}.mt-sm-5{margin-top:3rem!important}.mt-sm-auto{margin-top:auto!important}.me-sm-0{margin-right:0!important}.me-sm-1{margin-right:.25rem!important}.me-sm-2{margin-right:.5rem!important}.me-sm-3{margin-right:1rem!important}.me-sm-4{margin-right:1.5rem!important}.me-sm-5{margin-right:3rem!important}.me-sm-auto{margin-right:auto!important}.mb-sm-0{margin-bottom:0!important}.mb-sm-1{margin-bottom:.25rem!important}.mb-sm-2{margin-bottom:.5rem!important}.mb-sm-3{margin-bottom:1rem!important}.mb-sm-4{margin-bottom:1.5rem!important}.mb-sm-5{margin-bottom:3rem!important}.mb-sm-auto{margin-bottom:auto!important}.ms-sm-0{margin-left:0!important}.ms-sm-1{margin-left:.25rem!important}.ms-sm-2{margin-left:.5rem!important}.ms-sm-3{margin-left:1rem!important}.ms-sm-4{margin-left:1.5rem!important}.ms-sm-5{margin-left:3rem!important}.ms-sm-auto{margin-left:auto!important}.p-sm-0{padding:0!important}.p-sm-1{padding:.25rem!important}.p-sm-2{padding:.5rem!important}.p-sm-3{padding:1rem!important}.p-sm-4{padding:1.5rem!important}.p-sm-5{padding:3rem!important}.px-sm-0{padding-right:0!important;padding-left:0!important}.px-sm-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-sm-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-sm-3{padding-right:1rem!important;padding-left:1rem!important}.px-sm-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-sm-5{padding-right:3rem!important;padding-left:3rem!important}.py-sm-0{padding-top:0!important;padding-bottom:0!important}.py-sm-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-sm-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-sm-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-sm-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-sm-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-sm-0{padding-top:0!important}.pt-sm-1{padding-top:.25rem!important}.pt-sm-2{padding-top:.5rem!important}.pt-sm-3{padding-top:1rem!important}.pt-sm-4{padding-top:1.5rem!important}.pt-sm-5{padding-top:3rem!important}.pe-sm-0{padding-right:0!important}.pe-sm-1{padding-right:.25rem!important}.pe-sm-2{padding-right:.5rem!important}.pe-sm-3{padding-right:1rem!important}.pe-sm-4{padding-right:1.5rem!important}.pe-sm-5{padding-right:3rem!important}.pb-sm-0{padding-bottom:0!important}.pb-sm-1{padding-bottom:.25rem!important}.pb-sm-2{padding-bottom:.5rem!important}.pb-sm-3{padding-bottom:1rem!important}.pb-sm-4{padding-bottom:1.5rem!important}.pb-sm-5{padding-bottom:3rem!important}.ps-sm-0{padding-left:0!important}.ps-sm-1{padding-left:.25rem!important}.ps-sm-2{padding-left:.5rem!important}.ps-sm-3{padding-left:1rem!important}.ps-sm-4{padding-left:1.5rem!important}.ps-sm-5{padding-left:3rem!important}.gap-sm-0{gap:0!important}.gap-sm-1{gap:.25rem!important}.gap-sm-2{gap:.5rem!important}.gap-sm-3{gap:1rem!important}.gap-sm-4{gap:1.5rem!important}.gap-sm-5{gap:3rem!important}.text-sm-start{text-align:left!important}.text-sm-end{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.float-md-start{float:left!important}.float-md-end{float:right!important}.float-md-none{float:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-grid{display:grid!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:flex!important}.d-md-inline-flex{display:inline-flex!important}.d-md-none{display:none!important}.flex-md-fill{flex:1 1 auto!important}.flex-md-row{flex-direction:row!important}.flex-md-column{flex-direction:column!important}.flex-md-row-reverse{flex-direction:row-reverse!important}.flex-md-column-reverse{flex-direction:column-reverse!important}.flex-md-grow-0{flex-grow:0!important}.flex-md-grow-1{flex-grow:1!important}.flex-md-shrink-0{flex-shrink:0!important}.flex-md-shrink-1{flex-shrink:1!important}.flex-md-wrap{flex-wrap:wrap!important}.flex-md-nowrap{flex-wrap:nowrap!important}.flex-md-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-md-start{justify-content:flex-start!important}.justify-content-md-end{justify-content:flex-end!important}.justify-content-md-center{justify-content:center!important}.justify-content-md-between{justify-content:space-between!important}.justify-content-md-around{justify-content:space-around!important}.justify-content-md-evenly{justify-content:space-evenly!important}.align-items-md-start{align-items:flex-start!important}.align-items-md-end{align-items:flex-end!important}.align-items-md-center{align-items:center!important}.align-items-md-baseline{align-items:baseline!important}.align-items-md-stretch{align-items:stretch!important}.align-content-md-start{align-content:flex-start!important}.align-content-md-end{align-content:flex-end!important}.align-content-md-center{align-content:center!important}.align-content-md-between{align-content:space-between!important}.align-content-md-around{align-content:space-around!important}.align-content-md-stretch{align-content:stretch!important}.align-self-md-auto{align-self:auto!important}.align-self-md-start{align-self:flex-start!important}.align-self-md-end{align-self:flex-end!important}.align-self-md-center{align-self:center!important}.align-self-md-baseline{align-self:baseline!important}.align-self-md-stretch{align-self:stretch!important}.order-md-first{order:-1!important}.order-md-0{order:0!important}.order-md-1{order:1!important}.order-md-2{order:2!important}.order-md-3{order:3!important}.order-md-4{order:4!important}.order-md-5{order:5!important}.order-md-last{order:6!important}.m-md-0{margin:0!important}.m-md-1{margin:.25rem!important}.m-md-2{margin:.5rem!important}.m-md-3{margin:1rem!important}.m-md-4{margin:1.5rem!important}.m-md-5{margin:3rem!important}.m-md-auto{margin:auto!important}.mx-md-0{margin-right:0!important;margin-left:0!important}.mx-md-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-md-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-md-3{margin-right:1rem!important;margin-left:1rem!important}.mx-md-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-md-5{margin-right:3rem!important;margin-left:3rem!important}.mx-md-auto{margin-right:auto!important;margin-left:auto!important}.my-md-0{margin-top:0!important;margin-bottom:0!important}.my-md-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-md-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-md-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-md-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-md-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-md-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-md-0{margin-top:0!important}.mt-md-1{margin-top:.25rem!important}.mt-md-2{margin-top:.5rem!important}.mt-md-3{margin-top:1rem!important}.mt-md-4{margin-top:1.5rem!important}.mt-md-5{margin-top:3rem!important}.mt-md-auto{margin-top:auto!important}.me-md-0{margin-right:0!important}.me-md-1{margin-right:.25rem!important}.me-md-2{margin-right:.5rem!important}.me-md-3{margin-right:1rem!important}.me-md-4{margin-right:1.5rem!important}.me-md-5{margin-right:3rem!important}.me-md-auto{margin-right:auto!important}.mb-md-0{margin-bottom:0!important}.mb-md-1{margin-bottom:.25rem!important}.mb-md-2{margin-bottom:.5rem!important}.mb-md-3{margin-bottom:1rem!important}.mb-md-4{margin-bottom:1.5rem!important}.mb-md-5{margin-bottom:3rem!important}.mb-md-auto{margin-bottom:auto!important}.ms-md-0{margin-left:0!important}.ms-md-1{margin-left:.25rem!important}.ms-md-2{margin-left:.5rem!important}.ms-md-3{margin-left:1rem!important}.ms-md-4{margin-left:1.5rem!important}.ms-md-5{margin-left:3rem!important}.ms-md-auto{margin-left:auto!important}.p-md-0{padding:0!important}.p-md-1{padding:.25rem!important}.p-md-2{padding:.5rem!important}.p-md-3{padding:1rem!important}.p-md-4{padding:1.5rem!important}.p-md-5{padding:3rem!important}.px-md-0{padding-right:0!important;padding-left:0!important}.px-md-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-md-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-md-3{padding-right:1rem!important;padding-left:1rem!important}.px-md-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-md-5{padding-right:3rem!important;padding-left:3rem!important}.py-md-0{padding-top:0!important;padding-bottom:0!important}.py-md-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-md-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-md-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-md-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-md-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-md-0{padding-top:0!important}.pt-md-1{padding-top:.25rem!important}.pt-md-2{padding-top:.5rem!important}.pt-md-3{padding-top:1rem!important}.pt-md-4{padding-top:1.5rem!important}.pt-md-5{padding-top:3rem!important}.pe-md-0{padding-right:0!important}.pe-md-1{padding-right:.25rem!important}.pe-md-2{padding-right:.5rem!important}.pe-md-3{padding-right:1rem!important}.pe-md-4{padding-right:1.5rem!important}.pe-md-5{padding-right:3rem!important}.pb-md-0{padding-bottom:0!important}.pb-md-1{padding-bottom:.25rem!important}.pb-md-2{padding-bottom:.5rem!important}.pb-md-3{padding-bottom:1rem!important}.pb-md-4{padding-bottom:1.5rem!important}.pb-md-5{padding-bottom:3rem!important}.ps-md-0{padding-left:0!important}.ps-md-1{padding-left:.25rem!important}.ps-md-2{padding-left:.5rem!important}.ps-md-3{padding-left:1rem!important}.ps-md-4{padding-left:1.5rem!important}.ps-md-5{padding-left:3rem!important}.gap-md-0{gap:0!important}.gap-md-1{gap:.25rem!important}.gap-md-2{gap:.5rem!important}.gap-md-3{gap:1rem!important}.gap-md-4{gap:1.5rem!important}.gap-md-5{gap:3rem!important}.text-md-start{text-align:left!important}.text-md-end{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.float-lg-start{float:left!important}.float-lg-end{float:right!important}.float-lg-none{float:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-grid{display:grid!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:flex!important}.d-lg-inline-flex{display:inline-flex!important}.d-lg-none{display:none!important}.flex-lg-fill{flex:1 1 auto!important}.flex-lg-row{flex-direction:row!important}.flex-lg-column{flex-direction:column!important}.flex-lg-row-reverse{flex-direction:row-reverse!important}.flex-lg-column-reverse{flex-direction:column-reverse!important}.flex-lg-grow-0{flex-grow:0!important}.flex-lg-grow-1{flex-grow:1!important}.flex-lg-shrink-0{flex-shrink:0!important}.flex-lg-shrink-1{flex-shrink:1!important}.flex-lg-wrap{flex-wrap:wrap!important}.flex-lg-nowrap{flex-wrap:nowrap!important}.flex-lg-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-lg-start{justify-content:flex-start!important}.justify-content-lg-end{justify-content:flex-end!important}.justify-content-lg-center{justify-content:center!important}.justify-content-lg-between{justify-content:space-between!important}.justify-content-lg-around{justify-content:space-around!important}.justify-content-lg-evenly{justify-content:space-evenly!important}.align-items-lg-start{align-items:flex-start!important}.align-items-lg-end{align-items:flex-end!important}.align-items-lg-center{align-items:center!important}.align-items-lg-baseline{align-items:baseline!important}.align-items-lg-stretch{align-items:stretch!important}.align-content-lg-start{align-content:flex-start!important}.align-content-lg-end{align-content:flex-end!important}.align-content-lg-center{align-content:center!important}.align-content-lg-between{align-content:space-between!important}.align-content-lg-around{align-content:space-around!important}.align-content-lg-stretch{align-content:stretch!important}.align-self-lg-auto{align-self:auto!important}.align-self-lg-start{align-self:flex-start!important}.align-self-lg-end{align-self:flex-end!important}.align-self-lg-center{align-self:center!important}.align-self-lg-baseline{align-self:baseline!important}.align-self-lg-stretch{align-self:stretch!important}.order-lg-first{order:-1!important}.order-lg-0{order:0!important}.order-lg-1{order:1!important}.order-lg-2{order:2!important}.order-lg-3{order:3!important}.order-lg-4{order:4!important}.order-lg-5{order:5!important}.order-lg-last{order:6!important}.m-lg-0{margin:0!important}.m-lg-1{margin:.25rem!important}.m-lg-2{margin:.5rem!important}.m-lg-3{margin:1rem!important}.m-lg-4{margin:1.5rem!important}.m-lg-5{margin:3rem!important}.m-lg-auto{margin:auto!important}.mx-lg-0{margin-right:0!important;margin-left:0!important}.mx-lg-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-lg-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-lg-3{margin-right:1rem!important;margin-left:1rem!important}.mx-lg-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-lg-5{margin-right:3rem!important;margin-left:3rem!important}.mx-lg-auto{margin-right:auto!important;margin-left:auto!important}.my-lg-0{margin-top:0!important;margin-bottom:0!important}.my-lg-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-lg-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-lg-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-lg-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-lg-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-lg-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-lg-0{margin-top:0!important}.mt-lg-1{margin-top:.25rem!important}.mt-lg-2{margin-top:.5rem!important}.mt-lg-3{margin-top:1rem!important}.mt-lg-4{margin-top:1.5rem!important}.mt-lg-5{margin-top:3rem!important}.mt-lg-auto{margin-top:auto!important}.me-lg-0{margin-right:0!important}.me-lg-1{margin-right:.25rem!important}.me-lg-2{margin-right:.5rem!important}.me-lg-3{margin-right:1rem!important}.me-lg-4{margin-right:1.5rem!important}.me-lg-5{margin-right:3rem!important}.me-lg-auto{margin-right:auto!important}.mb-lg-0{margin-bottom:0!important}.mb-lg-1{margin-bottom:.25rem!important}.mb-lg-2{margin-bottom:.5rem!important}.mb-lg-3{margin-bottom:1rem!important}.mb-lg-4{margin-bottom:1.5rem!important}.mb-lg-5{margin-bottom:3rem!important}.mb-lg-auto{margin-bottom:auto!important}.ms-lg-0{margin-left:0!important}.ms-lg-1{margin-left:.25rem!important}.ms-lg-2{margin-left:.5rem!important}.ms-lg-3{margin-left:1rem!important}.ms-lg-4{margin-left:1.5rem!important}.ms-lg-5{margin-left:3rem!important}.ms-lg-auto{margin-left:auto!important}.p-lg-0{padding:0!important}.p-lg-1{padding:.25rem!important}.p-lg-2{padding:.5rem!important}.p-lg-3{padding:1rem!important}.p-lg-4{padding:1.5rem!important}.p-lg-5{padding:3rem!important}.px-lg-0{padding-right:0!important;padding-left:0!important}.px-lg-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-lg-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-lg-3{padding-right:1rem!important;padding-left:1rem!important}.px-lg-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-lg-5{padding-right:3rem!important;padding-left:3rem!important}.py-lg-0{padding-top:0!important;padding-bottom:0!important}.py-lg-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-lg-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-lg-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-lg-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-lg-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-lg-0{padding-top:0!important}.pt-lg-1{padding-top:.25rem!important}.pt-lg-2{padding-top:.5rem!important}.pt-lg-3{padding-top:1rem!important}.pt-lg-4{padding-top:1.5rem!important}.pt-lg-5{padding-top:3rem!important}.pe-lg-0{padding-right:0!important}.pe-lg-1{padding-right:.25rem!important}.pe-lg-2{padding-right:.5rem!important}.pe-lg-3{padding-right:1rem!important}.pe-lg-4{padding-right:1.5rem!important}.pe-lg-5{padding-right:3rem!important}.pb-lg-0{padding-bottom:0!important}.pb-lg-1{padding-bottom:.25rem!important}.pb-lg-2{padding-bottom:.5rem!important}.pb-lg-3{padding-bottom:1rem!important}.pb-lg-4{padding-bottom:1.5rem!important}.pb-lg-5{padding-bottom:3rem!important}.ps-lg-0{padding-left:0!important}.ps-lg-1{padding-left:.25rem!important}.ps-lg-2{padding-left:.5rem!important}.ps-lg-3{padding-left:1rem!important}.ps-lg-4{padding-left:1.5rem!important}.ps-lg-5{padding-left:3rem!important}.gap-lg-0{gap:0!important}.gap-lg-1{gap:.25rem!important}.gap-lg-2{gap:.5rem!important}.gap-lg-3{gap:1rem!important}.gap-lg-4{gap:1.5rem!important}.gap-lg-5{gap:3rem!important}.text-lg-start{text-align:left!important}.text-lg-end{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.float-xl-start{float:left!important}.float-xl-end{float:right!important}.float-xl-none{float:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-grid{display:grid!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:flex!important}.d-xl-inline-flex{display:inline-flex!important}.d-xl-none{display:none!important}.flex-xl-fill{flex:1 1 auto!important}.flex-xl-row{flex-direction:row!important}.flex-xl-column{flex-direction:column!important}.flex-xl-row-reverse{flex-direction:row-reverse!important}.flex-xl-column-reverse{flex-direction:column-reverse!important}.flex-xl-grow-0{flex-grow:0!important}.flex-xl-grow-1{flex-grow:1!important}.flex-xl-shrink-0{flex-shrink:0!important}.flex-xl-shrink-1{flex-shrink:1!important}.flex-xl-wrap{flex-wrap:wrap!important}.flex-xl-nowrap{flex-wrap:nowrap!important}.flex-xl-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-xl-start{justify-content:flex-start!important}.justify-content-xl-end{justify-content:flex-end!important}.justify-content-xl-center{justify-content:center!important}.justify-content-xl-between{justify-content:space-between!important}.justify-content-xl-around{justify-content:space-around!important}.justify-content-xl-evenly{justify-content:space-evenly!important}.align-items-xl-start{align-items:flex-start!important}.align-items-xl-end{align-items:flex-end!important}.align-items-xl-center{align-items:center!important}.align-items-xl-baseline{align-items:baseline!important}.align-items-xl-stretch{align-items:stretch!important}.align-content-xl-start{align-content:flex-start!important}.align-content-xl-end{align-content:flex-end!important}.align-content-xl-center{align-content:center!important}.align-content-xl-between{align-content:space-between!important}.align-content-xl-around{align-content:space-around!important}.align-content-xl-stretch{align-content:stretch!important}.align-self-xl-auto{align-self:auto!important}.align-self-xl-start{align-self:flex-start!important}.align-self-xl-end{align-self:flex-end!important}.align-self-xl-center{align-self:center!important}.align-self-xl-baseline{align-self:baseline!important}.align-self-xl-stretch{align-self:stretch!important}.order-xl-first{order:-1!important}.order-xl-0{order:0!important}.order-xl-1{order:1!important}.order-xl-2{order:2!important}.order-xl-3{order:3!important}.order-xl-4{order:4!important}.order-xl-5{order:5!important}.order-xl-last{order:6!important}.m-xl-0{margin:0!important}.m-xl-1{margin:.25rem!important}.m-xl-2{margin:.5rem!important}.m-xl-3{margin:1rem!important}.m-xl-4{margin:1.5rem!important}.m-xl-5{margin:3rem!important}.m-xl-auto{margin:auto!important}.mx-xl-0{margin-right:0!important;margin-left:0!important}.mx-xl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xl-auto{margin-right:auto!important;margin-left:auto!important}.my-xl-0{margin-top:0!important;margin-bottom:0!important}.my-xl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xl-0{margin-top:0!important}.mt-xl-1{margin-top:.25rem!important}.mt-xl-2{margin-top:.5rem!important}.mt-xl-3{margin-top:1rem!important}.mt-xl-4{margin-top:1.5rem!important}.mt-xl-5{margin-top:3rem!important}.mt-xl-auto{margin-top:auto!important}.me-xl-0{margin-right:0!important}.me-xl-1{margin-right:.25rem!important}.me-xl-2{margin-right:.5rem!important}.me-xl-3{margin-right:1rem!important}.me-xl-4{margin-right:1.5rem!important}.me-xl-5{margin-right:3rem!important}.me-xl-auto{margin-right:auto!important}.mb-xl-0{margin-bottom:0!important}.mb-xl-1{margin-bottom:.25rem!important}.mb-xl-2{margin-bottom:.5rem!important}.mb-xl-3{margin-bottom:1rem!important}.mb-xl-4{margin-bottom:1.5rem!important}.mb-xl-5{margin-bottom:3rem!important}.mb-xl-auto{margin-bottom:auto!important}.ms-xl-0{margin-left:0!important}.ms-xl-1{margin-left:.25rem!important}.ms-xl-2{margin-left:.5rem!important}.ms-xl-3{margin-left:1rem!important}.ms-xl-4{margin-left:1.5rem!important}.ms-xl-5{margin-left:3rem!important}.ms-xl-auto{margin-left:auto!important}.p-xl-0{padding:0!important}.p-xl-1{padding:.25rem!important}.p-xl-2{padding:.5rem!important}.p-xl-3{padding:1rem!important}.p-xl-4{padding:1.5rem!important}.p-xl-5{padding:3rem!important}.px-xl-0{padding-right:0!important;padding-left:0!important}.px-xl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xl-0{padding-top:0!important;padding-bottom:0!important}.py-xl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xl-0{padding-top:0!important}.pt-xl-1{padding-top:.25rem!important}.pt-xl-2{padding-top:.5rem!important}.pt-xl-3{padding-top:1rem!important}.pt-xl-4{padding-top:1.5rem!important}.pt-xl-5{padding-top:3rem!important}.pe-xl-0{padding-right:0!important}.pe-xl-1{padding-right:.25rem!important}.pe-xl-2{padding-right:.5rem!important}.pe-xl-3{padding-right:1rem!important}.pe-xl-4{padding-right:1.5rem!important}.pe-xl-5{padding-right:3rem!important}.pb-xl-0{padding-bottom:0!important}.pb-xl-1{padding-bottom:.25rem!important}.pb-xl-2{padding-bottom:.5rem!important}.pb-xl-3{padding-bottom:1rem!important}.pb-xl-4{padding-bottom:1.5rem!important}.pb-xl-5{padding-bottom:3rem!important}.ps-xl-0{padding-left:0!important}.ps-xl-1{padding-left:.25rem!important}.ps-xl-2{padding-left:.5rem!important}.ps-xl-3{padding-left:1rem!important}.ps-xl-4{padding-left:1.5rem!important}.ps-xl-5{padding-left:3rem!important}.gap-xl-0{gap:0!important}.gap-xl-1{gap:.25rem!important}.gap-xl-2{gap:.5rem!important}.gap-xl-3{gap:1rem!important}.gap-xl-4{gap:1.5rem!important}.gap-xl-5{gap:3rem!important}.text-xl-start{text-align:left!important}.text-xl-end{text-align:right!important}.text-xl-center{text-align:center!important}}@media (min-width:1400px){.float-xxl-start{float:left!important}.float-xxl-end{float:right!important}.float-xxl-none{float:none!important}.d-xxl-inline{display:inline!important}.d-xxl-inline-block{display:inline-block!important}.d-xxl-block{display:block!important}.d-xxl-grid{display:grid!important}.d-xxl-table{display:table!important}.d-xxl-table-row{display:table-row!important}.d-xxl-table-cell{display:table-cell!important}.d-xxl-flex{display:flex!important}.d-xxl-inline-flex{display:inline-flex!important}.d-xxl-none{display:none!important}.flex-xxl-fill{flex:1 1 auto!important}.flex-xxl-row{flex-direction:row!important}.flex-xxl-column{flex-direction:column!important}.flex-xxl-row-reverse{flex-direction:row-reverse!important}.flex-xxl-column-reverse{flex-direction:column-reverse!important}.flex-xxl-grow-0{flex-grow:0!important}.flex-xxl-grow-1{flex-grow:1!important}.flex-xxl-shrink-0{flex-shrink:0!important}.flex-xxl-shrink-1{flex-shrink:1!important}.flex-xxl-wrap{flex-wrap:wrap!important}.flex-xxl-nowrap{flex-wrap:nowrap!important}.flex-xxl-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-xxl-start{justify-content:flex-start!important}.justify-content-xxl-end{justify-content:flex-end!important}.justify-content-xxl-center{justify-content:center!important}.justify-content-xxl-between{justify-content:space-between!important}.justify-content-xxl-around{justify-content:space-around!important}.justify-content-xxl-evenly{justify-content:space-evenly!important}.align-items-xxl-start{align-items:flex-start!important}.align-items-xxl-end{align-items:flex-end!important}.align-items-xxl-center{align-items:center!important}.align-items-xxl-baseline{align-items:baseline!important}.align-items-xxl-stretch{align-items:stretch!important}.align-content-xxl-start{align-content:flex-start!important}.align-content-xxl-end{align-content:flex-end!important}.align-content-xxl-center{align-content:center!important}.align-content-xxl-between{align-content:space-between!important}.align-content-xxl-around{align-content:space-around!important}.align-content-xxl-stretch{align-content:stretch!important}.align-self-xxl-auto{align-self:auto!important}.align-self-xxl-start{align-self:flex-start!important}.align-self-xxl-end{align-self:flex-end!important}.align-self-xxl-center{align-self:center!important}.align-self-xxl-baseline{align-self:baseline!important}.align-self-xxl-stretch{align-self:stretch!important}.order-xxl-first{order:-1!important}.order-xxl-0{order:0!important}.order-xxl-1{order:1!important}.order-xxl-2{order:2!important}.order-xxl-3{order:3!important}.order-xxl-4{order:4!important}.order-xxl-5{order:5!important}.order-xxl-last{order:6!important}.m-xxl-0{margin:0!important}.m-xxl-1{margin:.25rem!important}.m-xxl-2{margin:.5rem!important}.m-xxl-3{margin:1rem!important}.m-xxl-4{margin:1.5rem!important}.m-xxl-5{margin:3rem!important}.m-xxl-auto{margin:auto!important}.mx-xxl-0{margin-right:0!important;margin-left:0!important}.mx-xxl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xxl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xxl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xxl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xxl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xxl-auto{margin-right:auto!important;margin-left:auto!important}.my-xxl-0{margin-top:0!important;margin-bottom:0!important}.my-xxl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xxl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xxl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xxl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xxl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xxl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xxl-0{margin-top:0!important}.mt-xxl-1{margin-top:.25rem!important}.mt-xxl-2{margin-top:.5rem!important}.mt-xxl-3{margin-top:1rem!important}.mt-xxl-4{margin-top:1.5rem!important}.mt-xxl-5{margin-top:3rem!important}.mt-xxl-auto{margin-top:auto!important}.me-xxl-0{margin-right:0!important}.me-xxl-1{margin-right:.25rem!important}.me-xxl-2{margin-right:.5rem!important}.me-xxl-3{margin-right:1rem!important}.me-xxl-4{margin-right:1.5rem!important}.me-xxl-5{margin-right:3rem!important}.me-xxl-auto{margin-right:auto!important}.mb-xxl-0{margin-bottom:0!important}.mb-xxl-1{margin-bottom:.25rem!important}.mb-xxl-2{margin-bottom:.5rem!important}.mb-xxl-3{margin-bottom:1rem!important}.mb-xxl-4{margin-bottom:1.5rem!important}.mb-xxl-5{margin-bottom:3rem!important}.mb-xxl-auto{margin-bottom:auto!important}.ms-xxl-0{margin-left:0!important}.ms-xxl-1{margin-left:.25rem!important}.ms-xxl-2{margin-left:.5rem!important}.ms-xxl-3{margin-left:1rem!important}.ms-xxl-4{margin-left:1.5rem!important}.ms-xxl-5{margin-left:3rem!important}.ms-xxl-auto{margin-left:auto!important}.p-xxl-0{padding:0!important}.p-xxl-1{padding:.25rem!important}.p-xxl-2{padding:.5rem!important}.p-xxl-3{padding:1rem!important}.p-xxl-4{padding:1.5rem!important}.p-xxl-5{padding:3rem!important}.px-xxl-0{padding-right:0!important;padding-left:0!important}.px-xxl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xxl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xxl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xxl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xxl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xxl-0{padding-top:0!important;padding-bottom:0!important}.py-xxl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xxl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xxl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xxl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xxl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xxl-0{padding-top:0!important}.pt-xxl-1{padding-top:.25rem!important}.pt-xxl-2{padding-top:.5rem!important}.pt-xxl-3{padding-top:1rem!important}.pt-xxl-4{padding-top:1.5rem!important}.pt-xxl-5{padding-top:3rem!important}.pe-xxl-0{padding-right:0!important}.pe-xxl-1{padding-right:.25rem!important}.pe-xxl-2{padding-right:.5rem!important}.pe-xxl-3{padding-right:1rem!important}.pe-xxl-4{padding-right:1.5rem!important}.pe-xxl-5{padding-right:3rem!important}.pb-xxl-0{padding-bottom:0!important}.pb-xxl-1{padding-bottom:.25rem!important}.pb-xxl-2{padding-bottom:.5rem!important}.pb-xxl-3{padding-bottom:1rem!important}.pb-xxl-4{padding-bottom:1.5rem!important}.pb-xxl-5{padding-bottom:3rem!important}.ps-xxl-0{padding-left:0!important}.ps-xxl-1{padding-left:.25rem!important}.ps-xxl-2{padding-left:.5rem!important}.ps-xxl-3{padding-left:1rem!important}.ps-xxl-4{padding-left:1.5rem!important}.ps-xxl-5{padding-left:3rem!important}.gap-xxl-0{gap:0!important}.gap-xxl-1{gap:.25rem!important}.gap-xxl-2{gap:.5rem!important}.gap-xxl-3{gap:1rem!important}.gap-xxl-4{gap:1.5rem!important}.gap-xxl-5{gap:3rem!important}.text-xxl-start{text-align:left!important}.text-xxl-end{text-align:right!important}.text-xxl-center{text-align:center!important}}@media (min-width:1200px){.fs-1{font-size:2.5rem!important}.fs-2{font-size:2rem!important}.fs-3{font-size:1.75rem!important}.fs-4{font-size:1.5rem!important}}@media print{.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-grid{display:grid!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:flex!important}.d-print-inline-flex{display:inline-flex!important}.d-print-none{display:none!important}} +/*# sourceMappingURL=bootstrap.min.css.map */ \ No newline at end of file diff --git a/src/boardwalkd/static/htmx.min.js b/src/boardwalkd/static/htmx.min.js new file mode 100644 index 0000000..998414c --- /dev/null +++ b/src/boardwalkd/static/htmx.min.js @@ -0,0 +1 @@ +(function(e,t){if(typeof define==="function"&&define.amd){define([],t)}else{e.htmx=t()}})(typeof self!=="undefined"?self:this,function(){return function(){"use strict";var U={onLoad:t,process:ct,on:M,off:D,trigger:$,ajax:er,find:C,findAll:R,closest:H,values:function(e,t){var r=Mt(e,t||"post");return r.values},remove:O,addClass:L,removeClass:q,toggleClass:A,takeClass:T,defineExtension:or,removeExtension:ar,logAll:E,logger:null,config:{historyEnabled:true,historyCacheSize:10,refreshOnHistoryMiss:false,defaultSwapStyle:"innerHTML",defaultSwapDelay:0,defaultSettleDelay:20,includeIndicatorStyles:true,indicatorClass:"htmx-indicator",requestClass:"htmx-request",addedClass:"htmx-added",settlingClass:"htmx-settling",swappingClass:"htmx-swapping",allowEval:true,inlineScriptNonce:"",attributesToSettle:["class","style","width","height"],withCredentials:false,timeout:0,wsReconnectDelay:"full-jitter",disableSelector:"[hx-disable], [data-hx-disable]",useTemplateFragments:false,scrollBehavior:"smooth",defaultFocusScroll:false},parseInterval:v,_:e,createEventSource:function(e){return new EventSource(e,{withCredentials:true})},createWebSocket:function(e){return new WebSocket(e,[])},version:"1.7.0"};var r={bodyContains:Y,filterValues:jt,hasAttribute:s,getAttributeValue:V,getClosestMatch:h,getExpressionVars:Gt,getHeaders:Xt,getInputValues:Mt,getInternalData:_,getSwapSpecification:Ut,getTriggerSpecs:ke,getTarget:ne,makeFragment:g,mergeObjects:Q,makeSettleInfo:zt,oobSwap:B,selectAndSwap:we,settleImmediately:Ct,shouldCancel:Pe,triggerEvent:$,triggerErrorEvent:J,withExtensions:gt};var n=["get","post","put","delete","patch"];var i=n.map(function(e){return"[hx-"+e+"], [data-hx-"+e+"]"}).join(", ");function v(e){if(e==undefined){return undefined}if(e.slice(-2)=="ms"){return parseFloat(e.slice(0,-2))||undefined}if(e.slice(-1)=="s"){return parseFloat(e.slice(0,-1))*1e3||undefined}return parseFloat(e)||undefined}function f(e,t){return e.getAttribute&&e.getAttribute(t)}function s(e,t){return e.hasAttribute&&(e.hasAttribute(t)||e.hasAttribute("data-"+t))}function V(e,t){return f(e,t)||f(e,"data-"+t)}function u(e){return e.parentElement}function z(){return document}function h(e,t){if(t(e)){return e}else if(u(e)){return h(u(e),t)}else{return null}}function o(e,t,r){var n=V(t,r);var i=V(t,"hx-disinherit");if(e!==t&&i&&(i==="*"||i.split(" ").indexOf(r)>=0)){return"unset"}else{return n}}function G(t,r){var n=null;h(t,function(e){return n=o(t,e,r)});if(n!=="unset"){return n}}function d(e,t){var r=e.matches||e.matchesSelector||e.msMatchesSelector||e.mozMatchesSelector||e.webkitMatchesSelector||e.oMatchesSelector;return r&&r.call(e,t)}function a(e){var t=/<([a-z][^\/\0>\x20\t\r\n\f]*)/i;var r=t.exec(e);if(r){return r[1].toLowerCase()}else{return""}}function l(e,t){var r=new DOMParser;var n=r.parseFromString(e,"text/html");var i=n.body;while(t>0){t--;i=i.firstChild}if(i==null){i=z().createDocumentFragment()}return i}function g(e){if(U.config.useTemplateFragments){var t=l("",0);return t.querySelector("template").content}else{var r=a(e);switch(r){case"thead":case"tbody":case"tfoot":case"colgroup":case"caption":return l(""+e+"
",1);case"col":return l(""+e+"
",2);case"tr":return l(""+e+"
",2);case"td":case"th":return l(""+e+"
",3);case"script":return l("
"+e+"
",1);default:return l(e,0)}}}function K(e){if(e){e()}}function p(e,t){return Object.prototype.toString.call(e)==="[object "+t+"]"}function m(e){return p(e,"Function")}function x(e){return p(e,"Object")}function _(e){var t="htmx-internal-data";var r=e[t];if(!r){r=e[t]={}}return r}function y(e){var t=[];if(e){for(var r=0;r=0}function Y(e){if(e.getRootNode()instanceof ShadowRoot){return z().body.contains(e.getRootNode().host)}else{return z().body.contains(e)}}function w(e){return e.trim().split(/\s+/)}function Q(e,t){for(var r in t){if(t.hasOwnProperty(r)){e[r]=t[r]}}return e}function S(e){try{return JSON.parse(e)}catch(e){pt(e);return null}}function e(e){return Jt(z().body,function(){return eval(e)})}function t(t){var e=U.on("htmx:load",function(e){t(e.detail.elt)});return e}function E(){U.logger=function(e,t,r){if(console){console.log(t,e,r)}}}function C(e,t){if(t){return e.querySelector(t)}else{return C(z(),e)}}function R(e,t){if(t){return e.querySelectorAll(t)}else{return R(z(),e)}}function O(e,t){e=k(e);if(t){setTimeout(function(){O(e)},t)}else{e.parentElement.removeChild(e)}}function L(e,t,r){e=k(e);if(r){setTimeout(function(){L(e,t)},r)}else{e.classList&&e.classList.add(t)}}function q(e,t,r){e=k(e);if(r){setTimeout(function(){q(e,t)},r)}else{if(e.classList){e.classList.remove(t);if(e.classList.length===0){e.removeAttribute("class")}}}}function A(e,t){e=k(e);e.classList.toggle(t)}function T(e,t){e=k(e);W(e.parentElement.children,function(e){q(e,t)});L(e,t)}function H(e,t){e=k(e);if(e.closest){return e.closest(t)}else{do{if(e==null||d(e,t)){return e}}while(e=e&&u(e))}}function N(e,t){if(t.indexOf("closest ")===0){return[H(e,t.substr(8))]}else if(t.indexOf("find ")===0){return[C(e,t.substr(5))]}else if(t==="document"){return[document]}else if(t==="window"){return[window]}else{return z().querySelectorAll(t)}}function ee(e,t){if(t){return N(e,t)[0]}else{return N(z().body,e)[0]}}function k(e){if(p(e,"String")){return C(e)}else{return e}}function I(e,t,r){if(m(t)){return{target:z().body,event:e,listener:t}}else{return{target:k(e),event:t,listener:r}}}function M(t,r,n){lr(function(){var e=I(t,r,n);e.target.addEventListener(e.event,e.listener)});var e=m(r);return e?r:n}function D(t,r,n){lr(function(){var e=I(t,r,n);e.target.removeEventListener(e.event,e.listener)});return m(r)?r:n}var te=z().createElement("output");function F(e,t){var r=G(e,t);if(r){if(r==="this"){return[re(e,t)]}else{var n=N(e,r);if(n.length===0){pt('The selector "'+r+'" on '+t+" returned no matches!");return[te]}else{return n}}}}function re(e,t){return h(e,function(e){return V(e,t)!=null})}function ne(e){var t=G(e,"hx-target");if(t){if(t==="this"){return re(e,"hx-target")}else{return ee(e,t)}}else{var r=_(e);if(r.boosted){return z().body}else{return e}}}function P(e){var t=U.config.attributesToSettle;for(var r=0;r0){a=e.substr(0,e.indexOf(":"));t=e.substr(e.indexOf(":")+1,e.length)}else{a=e}var r=z().querySelectorAll(t);if(r){W(r,function(e){var t;var r=i.cloneNode(true);t=z().createDocumentFragment();t.appendChild(r);if(!j(a,e)){t=r}var n={shouldSwap:true,target:e,fragment:t};if(!$(e,"htmx:oobBeforeSwap",n))return;e=n.target;if(n["shouldSwap"]){ye(a,e,e,t,o)}W(o.elts,function(e){$(e,"htmx:oobAfterSwap",n)})});i.parentNode.removeChild(i)}else{i.parentNode.removeChild(i);J(z().body,"htmx:oobErrorNoTarget",{content:i})}return e}function ie(e,r){W(R(e,"[hx-swap-oob], [data-hx-swap-oob]"),function(e){var t=V(e,"hx-swap-oob");if(t!=null){B(t,e,r)}})}function oe(e){W(R(e,"[hx-preserve], [data-hx-preserve]"),function(e){var t=V(e,"id");var r=z().getElementById(t);if(r!=null){e.parentNode.replaceChild(r,e)}})}function ae(n,e,i){W(e.querySelectorAll("[id]"),function(e){if(e.id&&e.id.length>0){var t=n.querySelector(e.tagName+"[id='"+e.id+"']");if(t&&t!==n){var r=e.cloneNode();X(e,t);i.tasks.push(function(){X(e,r)})}}})}function se(e){return function(){q(e,U.config.addedClass);ct(e);at(e);le(e);$(e,"htmx:load")}}function le(e){var t="[autofocus]";var r=d(e,t)?e:e.querySelector(t);if(r!=null){r.focus()}}function ue(e,t,r,n){ae(e,r,n);while(r.childNodes.length>0){var i=r.firstChild;L(i,U.config.addedClass);e.insertBefore(i,t);if(i.nodeType!==Node.TEXT_NODE&&i.nodeType!==Node.COMMENT_NODE){n.tasks.push(se(i))}}}function fe(t){var e=_(t);if(e.webSocket){e.webSocket.close()}if(e.sseEventSource){e.sseEventSource.close()}$(t,"htmx:beforeCleanupElement");if(e.listenerInfos){W(e.listenerInfos,function(e){if(t!==e.on){e.on.removeEventListener(e.trigger,e.listener)}})}if(t.children){W(t.children,function(e){fe(e)})}}function ce(e,t,r){if(e.tagName==="BODY"){return me(e,t,r)}else{var n;var i=e.previousSibling;ue(u(e),e,t,r);if(i==null){n=u(e).firstChild}else{n=i.nextSibling}_(e).replacedWith=n;r.elts=[];while(n&&n!==e){if(n.nodeType===Node.ELEMENT_NODE){r.elts.push(n)}n=n.nextElementSibling}fe(e);u(e).removeChild(e)}}function he(e,t,r){return ue(e,e.firstChild,t,r)}function de(e,t,r){return ue(u(e),e,t,r)}function ve(e,t,r){return ue(e,null,t,r)}function ge(e,t,r){return ue(u(e),e.nextSibling,t,r)}function pe(e,t,r){fe(e);return u(e).removeChild(e)}function me(e,t,r){var n=e.firstChild;ue(e,n,t,r);if(n){while(n.nextSibling){fe(n.nextSibling);e.removeChild(n.nextSibling)}fe(n);e.removeChild(n)}}function xe(e,t){var r=G(e,"hx-select");if(r){var n=z().createDocumentFragment();W(t.querySelectorAll(r),function(e){n.appendChild(e)});t=n}return t}function ye(e,t,r,n,i){switch(e){case"none":return;case"outerHTML":ce(r,n,i);return;case"afterbegin":he(r,n,i);return;case"beforebegin":de(r,n,i);return;case"beforeend":ve(r,n,i);return;case"afterend":ge(r,n,i);return;case"delete":pe(r,n,i);return;default:var o=sr(t);for(var a=0;a-1){var t=e.replace(/]*>|>)([\s\S]*?)<\/svg>/gim,"");var r=t.match(/]*>|>)([\s\S]*?)<\/title>/im);if(r){return r[2]}}}function we(e,t,r,n,i){i.title=be(n);var o=g(n);if(o){ie(o,i);o=xe(r,o);oe(o);return ye(e,r,t,o,i)}}function Se(e,t,r){var n=e.getResponseHeader(t);if(n.indexOf("{")===0){var i=S(n);for(var o in i){if(i.hasOwnProperty(o)){var a=i[o];if(!x(a)){a={value:a}}$(r,o,a)}}}else{$(r,n,[])}}var Ee=/\s/;var Ce=/[\s,]/;var Re=/[_$a-zA-Z]/;var Oe=/[_$a-zA-Z0-9]/;var Le=['"',"'","/"];var qe=/[^\s]/;function Ae(e){var t=[];var r=0;while(r0){var a=t[0];if(a==="]"){n--;if(n===0){if(o===null){i=i+"true"}t.shift();i+=")})";try{var s=Jt(e,function(){return Function(i)()},function(){return true});s.source=i;return s}catch(e){J(z().body,"htmx:syntax:error",{error:e,source:i});return null}}}else if(a==="["){n++}if(Te(a,o,r)){i+="(("+r+"."+a+") ? ("+r+"."+a+") : (window."+a+"))"}else{i=i+a}o=t.shift()}}}function c(e,t){var r="";while(e.length>0&&!e[0].match(t)){r+=e.shift()}return r}var Ne="input, textarea, select";function ke(e){var t=V(e,"hx-trigger");var r=[];if(t){var n=Ae(t);do{c(n,qe);var f=n.length;var i=c(n,/[,\[\s]/);if(i!==""){if(i==="every"){var o={trigger:"every"};c(n,qe);o.pollInterval=v(c(n,/[,\[\s]/));c(n,qe);var a=He(e,n,"event");if(a){o.eventFilter=a}r.push(o)}else if(i.indexOf("sse:")===0){r.push({trigger:"sse",sseEvent:i.substr(4)})}else{var s={trigger:i};var a=He(e,n,"event");if(a){s.eventFilter=a}while(n.length>0&&n[0]!==","){c(n,qe);var l=n.shift();if(l==="changed"){s.changed=true}else if(l==="once"){s.once=true}else if(l==="consume"){s.consume=true}else if(l==="delay"&&n[0]===":"){n.shift();s.delay=v(c(n,Ce))}else if(l==="from"&&n[0]===":"){n.shift();var u=c(n,Ce);if(u==="closest"||u==="find"){n.shift();u+=" "+c(n,Ce)}s.from=u}else if(l==="target"&&n[0]===":"){n.shift();s.target=c(n,Ce)}else if(l==="throttle"&&n[0]===":"){n.shift();s.throttle=v(c(n,Ce))}else if(l==="queue"&&n[0]===":"){n.shift();s.queue=c(n,Ce)}else if((l==="root"||l==="threshold")&&n[0]===":"){n.shift();s[l]=c(n,Ce)}else{J(e,"htmx:syntax:error",{token:n.shift()})}}r.push(s)}}if(n.length===f){J(e,"htmx:syntax:error",{token:n.shift()})}c(n,qe)}while(n[0]===","&&n.shift())}if(r.length>0){return r}else if(d(e,"form")){return[{trigger:"submit"}]}else if(d(e,Ne)){return[{trigger:"change"}]}else{return[{trigger:"click"}]}}function Ie(e){_(e).cancelled=true}function Me(e,t,r,n){var i=_(e);i.timeout=setTimeout(function(){if(Y(e)&&i.cancelled!==true){if(!je(n,dt("hx:poll:trigger",{triggerSpec:n,target:e}))){Z(t,r,e)}Me(e,t,V(e,"hx-"+t),n)}},n.pollInterval)}function De(e){return location.hostname===e.hostname&&f(e,"href")&&f(e,"href").indexOf("#")!==0}function Fe(t,r,e){if(t.tagName==="A"&&De(t)&&t.target===""||t.tagName==="FORM"){r.boosted=true;var n,i;if(t.tagName==="A"){n="get";i=f(t,"href");r.pushURL=true}else{var o=f(t,"method");n=o?o.toLowerCase():"get";if(n==="get"){r.pushURL=true}i=f(t,"action")}e.forEach(function(e){Be(t,n,i,r,e,true)})}}function Pe(e,t){if(e.type==="submit"||e.type==="click"){if(t.tagName==="FORM"){return true}if(d(t,'input[type="submit"], button')&&H(t,"form")!==null){return true}if(t.tagName==="A"&&t.href&&(t.getAttribute("href")==="#"||t.getAttribute("href").indexOf("#")!==0)){return true}}return false}function Xe(e,t){return _(e).boosted&&e.tagName==="A"&&t.type==="click"&&(t.ctrlKey||t.metaKey)}function je(e,t){var r=e.eventFilter;if(r){try{return r(t)!==true}catch(e){J(z().body,"htmx:eventFilter:error",{error:e,source:r.source});return true}}return false}function Be(o,a,s,e,l,u){var t;if(l.from){t=N(o,l.from)}else{t=[o]}W(t,function(n){var i=function(e){if(!Y(o)){n.removeEventListener(l.trigger,i);return}if(Xe(o,e)){return}if(u||Pe(e,o)){e.preventDefault()}if(je(l,e)){return}var t=_(e);t.triggerSpec=l;if(t.handledFor==null){t.handledFor=[]}var r=_(o);if(t.handledFor.indexOf(o)<0){t.handledFor.push(o);if(l.consume){e.stopPropagation()}if(l.target&&e.target){if(!d(e.target,l.target)){return}}if(l.once){if(r.triggeredOnce){return}else{r.triggeredOnce=true}}if(l.changed){if(r.lastValue===o.value){return}else{r.lastValue=o.value}}if(r.delayed){clearTimeout(r.delayed)}if(r.throttle){return}if(l.throttle){if(!r.throttle){Z(a,s,o,e);r.throttle=setTimeout(function(){r.throttle=null},l.throttle)}}else if(l.delay){r.delayed=setTimeout(function(){Z(a,s,o,e)},l.delay)}else{Z(a,s,o,e)}}};if(e.listenerInfos==null){e.listenerInfos=[]}e.listenerInfos.push({trigger:l.trigger,listener:i,on:n});n.addEventListener(l.trigger,i)})}var Ue=false;var Ve=null;function ze(){if(!Ve){Ve=function(){Ue=true};window.addEventListener("scroll",Ve);setInterval(function(){if(Ue){Ue=false;W(z().querySelectorAll("[hx-trigger='revealed'],[data-hx-trigger='revealed']"),function(e){_e(e)})}},200)}}function _e(e){if(!s(e,"data-hx-revealed")&&b(e)){e.setAttribute("data-hx-revealed","true");var t=_(e);if(t.initialized){Z(t.verb,t.path,e)}else{e.addEventListener("htmx:afterProcessNode",function(){Z(t.verb,t.path,e)},{once:true})}}}function We(e,t,r){var n=w(r);for(var i=0;i=0){var t=Ge(n);setTimeout(function(){Je(s,r,n+1)},t)}};t.onopen=function(e){n=0};_(s).webSocket=t;t.addEventListener("message",function(e){if($e(s)){return}var t=e.data;gt(s,function(e){t=e.transformResponse(t,null,s)});var r=zt(s);var n=g(t);var i=y(n.children);for(var o=0;o0){$(u,"htmx:validation:halted",i);return}t.send(JSON.stringify(l));if(Pe(e,u)){e.preventDefault()}})}else{J(u,"htmx:noWebSocketSourceError")}}function Ge(e){var t=U.config.wsReconnectDelay;if(typeof t==="function"){return t(e)}if(t==="full-jitter"){var r=Math.min(e,6);var n=1e3*Math.pow(2,r);return n*Math.random()}pt('htmx.config.wsReconnectDelay must either be a function or the string "full-jitter"')}function Ke(e,t,r){var n=w(r);for(var i=0;iU.config.historyCacheSize){i.shift()}while(i.length>0){try{localStorage.setItem("htmx-history-cache",JSON.stringify(i));break}catch(e){J(z().body,"htmx:historyCacheError",{cause:e,cache:i});i.shift()}}}function bt(e){var t=S(localStorage.getItem("htmx-history-cache"))||[];for(var r=0;r=200&&this.status<400){$(z().body,"htmx:historyCacheMissLoad",i);var e=g(this.response);e=e.querySelector("[hx-history-elt],[data-hx-history-elt]")||e;var t=xt();var r=zt(t);me(t,e,r);Ct(r.tasks);mt=n;$(z().body,"htmx:historyRestore",{path:n})}else{J(z().body,"htmx:historyCacheMissLoadError",i)}};e.send()}function Ot(e){St();e=e||location.pathname+location.search;var t=bt(e);if(t){var r=g(t.content);var n=xt();var i=zt(n);me(n,r,i);Ct(i.tasks);document.title=t.title;window.scrollTo(0,t.scroll);mt=e;$(z().body,"htmx:historyRestore",{path:e})}else{if(U.config.refreshOnHistoryMiss){window.location.reload(true)}else{Rt(e)}}}function Lt(e){var t=G(e,"hx-push-url");return t&&t!=="false"||_(e).boosted&&_(e).pushURL}function qt(e){var t=G(e,"hx-push-url");return t==="true"||t==="false"?null:t}function At(e){var t=F(e,"hx-indicator");if(t==null){t=[e]}W(t,function(e){e.classList["add"].call(e.classList,U.config.requestClass)});return t}function Tt(e){W(e,function(e){e.classList["remove"].call(e.classList,U.config.requestClass)})}function Ht(e,t){for(var r=0;r=0}function Ut(e,t){var r=t?t:G(e,"hx-swap");var n={swapStyle:_(e).boosted?"innerHTML":U.config.defaultSwapStyle,swapDelay:U.config.defaultSwapDelay,settleDelay:U.config.defaultSettleDelay};if(_(e).boosted&&!Bt(e)){n["show"]="top"}if(r){var i=w(r);if(i.length>0){n["swapStyle"]=i[0];for(var o=1;o0?l.join(":"):null;n["scroll"]=f;n["scrollTarget"]=u}if(a.indexOf("show:")===0){var c=a.substr(5);var l=c.split(":");var h=l.pop();var u=l.length>0?l.join(":"):null;n["show"]=h;n["showTarget"]=u}if(a.indexOf("focus-scroll:")===0){var d=a.substr("focus-scroll:".length);n["focusScroll"]=d=="true"}}}}return n}function Vt(t,r,n){var i=null;gt(r,function(e){if(i==null){i=e.encodeParameters(t,n,r)}});if(i!=null){return i}else{if(G(r,"hx-encoding")==="multipart/form-data"||d(r,"form")&&f(r,"enctype")==="multipart/form-data"){return Pt(n)}else{return Ft(n)}}}function zt(e){return{tasks:[],elts:[e]}}function _t(e,t){var r=e[0];var n=e[e.length-1];if(t.scroll){var i=null;if(t.scrollTarget){i=ee(r,t.scrollTarget)}if(t.scroll==="top"&&(r||i)){i=i||r;i.scrollTop=0}if(t.scroll==="bottom"&&(n||i)){i=i||n;i.scrollTop=i.scrollHeight}}if(t.show){var i=null;if(t.showTarget){var o=t.showTarget;if(t.showTarget==="window"){o="body"}i=ee(r,o)}if(t.show==="top"&&(r||i)){i=i||r;i.scrollIntoView({block:"start",behavior:U.config.scrollBehavior})}if(t.show==="bottom"&&(n||i)){i=i||n;i.scrollIntoView({block:"end",behavior:U.config.scrollBehavior})}}}function Wt(e,t,r,n){if(n==null){n={}}if(e==null){return n}var i=V(e,t);if(i){var o=i.trim();var a=r;if(o.indexOf("javascript:")===0){o=o.substr(11);a=true}else if(o.indexOf("js:")===0){o=o.substr(3);a=true}if(o.indexOf("{")!==0){o="{"+o+"}"}var s;if(a){s=Jt(e,function(){return Function("return ("+o+")")()},{})}else{s=S(o)}for(var l in s){if(s.hasOwnProperty(l)){if(n[l]==null){n[l]=s[l]}}}}return Wt(u(e),t,r,n)}function Jt(e,t,r){if(U.config.allowEval){return t()}else{J(e,"htmx:evalDisallowedError");return r}}function $t(e,t){return Wt(e,"hx-vars",true,t)}function Zt(e,t){return Wt(e,"hx-vals",false,t)}function Gt(e){return Q($t(e),Zt(e))}function Kt(t,r,n){if(n!==null){try{t.setRequestHeader(r,n)}catch(e){t.setRequestHeader(r,encodeURIComponent(n));t.setRequestHeader(r+"-URI-AutoEncoded","true")}}}function Yt(t){if(t.responseURL&&typeof URL!=="undefined"){try{var e=new URL(t.responseURL);return e.pathname+e.search}catch(e){J(z().body,"htmx:badResponseUrl",{url:t.responseURL})}}}function Qt(e,t){return e.getAllResponseHeaders().match(t)}function er(e,t,r){e=e.toLowerCase();if(r){if(r instanceof Element||p(r,"String")){return Z(e,t,null,null,{targetOverride:k(r),returnPromise:true})}else{return Z(e,t,k(r.source),r.event,{handler:r.handler,headers:r.headers,values:r.values,targetOverride:k(r.target),swapOverride:r.swap,returnPromise:true})}}else{return Z(e,t,null,null,{returnPromise:true})}}function tr(e){var t=[];while(e){t.push(e);e=e.parentElement}return t}function Z(e,t,n,f,r){var c=null;var h=null;r=r!=null?r:{};if(r.returnPromise&&typeof Promise!=="undefined"){var d=new Promise(function(e,t){c=e;h=t})}if(n==null){n=z().body}var v=r.handler||rr;if(!Y(n)){return}var g=r.targetOverride||ne(n);if(g==null||g==te){J(n,"htmx:targetError",{target:V(n,"hx-target")});return}var p=n;var i=_(n);var o=G(n,"hx-sync");var m=null;var x=false;if(o){var y=o.split(":");var b=y[0].trim();if(b==="this"){p=re(n,"hx-sync")}else{p=ee(n,b)}o=(y[1]||"drop").trim();i=_(p);if(o==="drop"&&i.xhr&&i.abortable!==true){return}else if(o==="abort"){if(i.xhr){return}else{x=true}}else if(o==="replace"){$(p,"htmx:abort")}else if(o.indexOf("queue")===0){var w=o.split(" ");m=(w[1]||"last").trim()}}if(i.xhr){if(i.abortable){$(p,"htmx:abort")}else{if(m==null){if(f){var S=_(f);if(S&&S.triggerSpec&&S.triggerSpec.queue){m=S.triggerSpec.queue}}if(m==null){m="last"}}if(i.queuedRequests==null){i.queuedRequests=[]}if(m==="first"&&i.queuedRequests.length===0){i.queuedRequests.push(function(){Z(e,t,n,f,r)})}else if(m==="all"){i.queuedRequests.push(function(){Z(e,t,n,f,r)})}else if(m==="last"){i.queuedRequests=[];i.queuedRequests.push(function(){Z(e,t,n,f,r)})}return}}var a=new XMLHttpRequest;i.xhr=a;i.abortable=x;var s=function(){i.xhr=null;i.abortable=false;if(i.queuedRequests!=null&&i.queuedRequests.length>0){var e=i.queuedRequests.shift();e()}};var E=G(n,"hx-prompt");if(E){var C=prompt(E);if(C===null||!$(n,"htmx:prompt",{prompt:C,target:g})){K(c);s();return d}}var R=G(n,"hx-confirm");if(R){if(!confirm(R)){K(c);s();return d}}var O=Xt(n,g,C);if(r.headers){O=Q(O,r.headers)}var L=Mt(n,e);var q=L.errors;var A=L.values;if(r.values){A=Q(A,r.values)}var T=Gt(n);var H=Q(A,T);var N=jt(H,n);if(e!=="get"&&G(n,"hx-encoding")==null){O["Content-Type"]="application/x-www-form-urlencoded"}if(t==null||t===""){t=z().location.href}var k=Wt(n,"hx-request");var l={parameters:N,unfilteredParameters:H,headers:O,target:g,verb:e,errors:q,withCredentials:r.credentials||k.credentials||U.config.withCredentials,timeout:r.timeout||k.timeout||U.config.timeout,path:t,triggeringEvent:f};if(!$(n,"htmx:configRequest",l)){K(c);s();return d}t=l.path;e=l.verb;O=l.headers;N=l.parameters;q=l.errors;if(q&&q.length>0){$(n,"htmx:validation:halted",l);K(c);s();return d}var I=t.split("#");var M=I[0];var D=I[1];if(e==="get"){var F=M;var P=Object.keys(N).length!==0;if(P){if(F.indexOf("?")<0){F+="?"}else{F+="&"}F+=Ft(N);if(D){F+="#"+D}}a.open("GET",F,true)}else{a.open(e.toUpperCase(),t,true)}a.overrideMimeType("text/html");a.withCredentials=l.withCredentials;a.timeout=l.timeout;if(k.noHeaders){}else{for(var X in O){if(O.hasOwnProperty(X)){var j=O[X];Kt(a,X,j)}}}var u={xhr:a,target:g,requestConfig:l,etc:r,pathInfo:{path:t,finalPath:F,anchor:D}};a.onload=function(){try{var e=tr(n);v(n,u);Tt(B);$(n,"htmx:afterRequest",u);$(n,"htmx:afterOnLoad",u);if(!Y(n)){var t=null;while(e.length>0&&t==null){var r=e.shift();if(Y(r)){t=r}}if(t){$(t,"htmx:afterRequest",u);$(t,"htmx:afterOnLoad",u)}}K(c);s()}catch(e){J(n,"htmx:onLoadError",Q({error:e},u));throw e}};a.onerror=function(){Tt(B);J(n,"htmx:afterRequest",u);J(n,"htmx:sendError",u);K(h);s()};a.onabort=function(){Tt(B);J(n,"htmx:afterRequest",u);J(n,"htmx:sendAbort",u);K(h);s()};a.ontimeout=function(){Tt(B);J(n,"htmx:afterRequest",u);J(n,"htmx:timeout",u);K(h);s()};if(!$(n,"htmx:beforeRequest",u)){K(c);s();return d}var B=At(n);W(["loadstart","loadend","progress","abort"],function(t){W([a,a.upload],function(e){e.addEventListener(t,function(e){$(n,"htmx:xhr:"+t,{lengthComputable:e.lengthComputable,loaded:e.loaded,total:e.total})})})});$(n,"htmx:beforeSend",u);a.send(e==="get"?null:Vt(a,n,N));return d}function rr(s,l){var u=l.xhr;var f=l.target;var r=l.etc;if(!$(s,"htmx:beforeOnLoad",l))return;if(Qt(u,/HX-Trigger:/i)){Se(u,"HX-Trigger",s)}if(Qt(u,/HX-Push:/i)){var c=u.getResponseHeader("HX-Push")}if(Qt(u,/HX-Redirect:/i)){window.location.href=u.getResponseHeader("HX-Redirect");return}if(Qt(u,/HX-Refresh:/i)){if("true"===u.getResponseHeader("HX-Refresh")){location.reload();return}}if(Qt(u,/HX-Retarget:/i)){l.target=z().querySelector(u.getResponseHeader("HX-Retarget"))}var h;if(c=="false"){h=false}else{h=Lt(s)||c}var n=u.status>=200&&u.status<400&&u.status!==204;var d=u.response;var e=u.status>=400;var t=Q({shouldSwap:n,serverResponse:d,isError:e},l);if(!$(f,"htmx:beforeSwap",t))return;f=t.target;d=t.serverResponse;e=t.isError;l.failed=e;l.successful=!e;if(t.shouldSwap){if(u.status===286){Ie(s)}gt(s,function(e){d=e.transformResponse(d,u,s)});if(h){St()}var i=r.swapOverride;var v=Ut(s,i);f.classList.add(U.config.swappingClass);var o=function(){try{var e=document.activeElement;var t={};try{t={elt:e,start:e?e.selectionStart:null,end:e?e.selectionEnd:null}}catch(e){}var n=zt(f);we(v.swapStyle,f,s,d,n);if(t.elt&&!Y(t.elt)&&t.elt.id){var r=document.getElementById(t.elt.id);var i={preventScroll:v.focusScroll!==undefined?!v.focusScroll:!U.config.defaultFocusScroll};if(r){if(t.start&&r.setSelectionRange){r.setSelectionRange(t.start,t.end)}r.focus(i)}}f.classList.remove(U.config.swappingClass);W(n.elts,function(e){if(e.classList){e.classList.add(U.config.settlingClass)}$(e,"htmx:afterSwap",l)});if(l.pathInfo.anchor){location.hash=l.pathInfo.anchor}if(Qt(u,/HX-Trigger-After-Swap:/i)){var o=s;if(!Y(s)){o=z().body}Se(u,"HX-Trigger-After-Swap",o)}var a=function(){W(n.tasks,function(e){e.call()});W(n.elts,function(e){if(e.classList){e.classList.remove(U.config.settlingClass)}$(e,"htmx:afterSettle",l)});if(h){var e=c||qt(s)||Yt(u)||l.pathInfo.finalPath||l.pathInfo.path;Et(e);$(z().body,"htmx:pushedIntoHistory",{path:e})}if(n.title){var t=C("title");if(t){t.innerHTML=n.title}else{window.document.title=n.title}}_t(n.elts,v);if(Qt(u,/HX-Trigger-After-Settle:/i)){var r=s;if(!Y(s)){r=z().body}Se(u,"HX-Trigger-After-Settle",r)}};if(v.settleDelay>0){setTimeout(a,v.settleDelay)}else{a()}}catch(e){J(s,"htmx:swapError",l);throw e}};if(v.swapDelay>0){setTimeout(o,v.swapDelay)}else{o()}}if(e){J(s,"htmx:responseError",Q({error:"Response Status Error Code "+u.status+" from "+l.pathInfo.path},l))}}var nr={};function ir(){return{init:function(e){return null},onEvent:function(e,t){return true},transformResponse:function(e,t,r){return e},isInlineSwap:function(e){return false},handleSwap:function(e,t,r,n){return false},encodeParameters:function(e,t,r){return null}}}function or(e,t){if(t.init){t.init(r)}nr[e]=Q(ir(),t)}function ar(e){delete nr[e]}function sr(e,r,n){if(e==undefined){return r}if(r==undefined){r=[]}if(n==undefined){n=[]}var t=V(e,"hx-ext");if(t){W(t.split(","),function(e){e=e.replace(/ /g,"");if(e.slice(0,7)=="ignore:"){n.push(e.slice(7));return}if(n.indexOf(e)<0){var t=nr[e];if(t&&r.indexOf(t)<0){r.push(t)}}})}return sr(u(e),r,n)}function lr(e){if(z().readyState!=="loading"){e()}else{z().addEventListener("DOMContentLoaded",e)}}function ur(){if(U.config.includeIndicatorStyles!==false){z().head.insertAdjacentHTML("beforeend","")}}function fr(){var e=z().querySelector('meta[name="htmx-config"]');if(e){return S(e.content)}else{return null}}function cr(){var e=fr();if(e){U.config=Q(U.config,e)}}lr(function(){cr();ur();var e=z().body;ct(e);var t=z().querySelectorAll("[hx-trigger='restored'],[data-hx-trigger='restored']");e.addEventListener("htmx:abort",function(e){var t=e.target;var r=_(t);if(r&&r.xhr){r.xhr.abort()}});window.onpopstate=function(e){if(e.state&&e.state.htmx){Ot();W(t,function(e){$(e,"htmx:restored",{document:z(),triggerEvent:$})})}};setTimeout(function(){$(e,"htmx:load",{})},0)});return U}()}); \ No newline at end of file diff --git a/src/boardwalkd/templates/base.html b/src/boardwalkd/templates/base.html new file mode 100644 index 0000000..a791a09 --- /dev/null +++ b/src/boardwalkd/templates/base.html @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + Boardwalk · {{ title }} + + {% block head %}{% end %} + + +{# The line below causes the _xsrf cookie to always be set #} + + + + + + +
+ {% block main %}{% end %} +
+ + + + + + {% block bottom %}{% end %} + + + diff --git a/src/boardwalkd/templates/index.html b/src/boardwalkd/templates/index.html new file mode 100644 index 0000000..56fd253 --- /dev/null +++ b/src/boardwalkd/templates/index.html @@ -0,0 +1,13 @@ +{% extends "base.html" %} + +{% block main %} +
+ {% if edit %} +
+
+ {% else %} +
+
+ {% end %} +
+{% end %} diff --git a/src/boardwalkd/templates/index_workspace.html b/src/boardwalkd/templates/index_workspace.html new file mode 100644 index 0000000..bab70b5 --- /dev/null +++ b/src/boardwalkd/templates/index_workspace.html @@ -0,0 +1,130 @@ +
+
+

Workspaces

+
+
+ {% if edit %} + Edit + {% else %} + Edit + {% end %} +
+
+{% for workspace_name, workspace in sorted(workspaces.items()) %} +
+
+
+
+ {{ workspace_name }} +
+
+ + {% if secondsdelta(workspace.last_seen) > 10 %} + ⚪️ + {% else %} + 🟢 + {% end %} + {% if workspace.semaphores.has_mutex %} + 🔒 + {% else %} + 🔓 + {% end %} + +
+
+ {% if workspace.semaphores.caught %} + {% include "index_workspace_release.html" %} + {% else %} + {% include "index_workspace_catch.html" %} + {% end %} +
+
+
+
+ + + + + + + + + + + + + + + + + + + +
Workflow{{ workspace.details.workflow }}
Worker{{ workspace.details.worker_username }}@{{ workspace.details.worker_hostname }}
Host Pattern{{ workspace.details.host_pattern }}
Command{{ workspace.details.worker_command }}
+
+
+
+ + + {% for event in sort_events_by_date(workspace.events)[:6] %} + {% if event.severity == "info" %} + + + + + {% end %} + {% if event.severity == "success" %} + + + + + {% end %} + {% if event.severity == "error" %} + + + + + {% end %} + {% end %} + +
{{ event.severity }}{{ squeeze(event.message) }}
{{ event.severity }}{{ squeeze(event.message) }}
{{ event.severity }}{{ squeeze(event.message) }}
+
+ More +
+
+ {% if edit %} +
+
+ {% if secondsdelta(workspace.last_seen) < 10 %} + {% elif workspace.semaphores.has_mutex %} + + {% else %} + + {% end %} + {% if workspace.semaphores.has_mutex or secondsdelta(workspace.last_seen) < 10 %} + {% else %} + + {% end %} +
+
+ {% end %} +
+
+{% end %} diff --git a/src/boardwalkd/templates/index_workspace_catch.html b/src/boardwalkd/templates/index_workspace_catch.html new file mode 100644 index 0000000..a94f865 --- /dev/null +++ b/src/boardwalkd/templates/index_workspace_catch.html @@ -0,0 +1,4 @@ + diff --git a/src/boardwalkd/templates/index_workspace_release.html b/src/boardwalkd/templates/index_workspace_release.html new file mode 100644 index 0000000..c70decd --- /dev/null +++ b/src/boardwalkd/templates/index_workspace_release.html @@ -0,0 +1,4 @@ + diff --git a/src/boardwalkd/templates/workspace_events.html b/src/boardwalkd/templates/workspace_events.html new file mode 100644 index 0000000..3179020 --- /dev/null +++ b/src/boardwalkd/templates/workspace_events.html @@ -0,0 +1,24 @@ +{% extends "base.html" %} + +{% block main %} +
+
+
+
+

{{ workspace_name}} Events

+
+
+
+
+
+ + + +
+
+
+
+
+
+{% end %} diff --git a/src/boardwalkd/templates/workspace_events_table.html b/src/boardwalkd/templates/workspace_events_table.html new file mode 100644 index 0000000..78258a0 --- /dev/null +++ b/src/boardwalkd/templates/workspace_events_table.html @@ -0,0 +1,20 @@ +{% for event in sort_events_by_date(workspace.events) %} +{% if event.severity == "info" %} + + {{ event.severity }} + {{ squeeze(event.message) }} + +{% end %} +{% if event.severity == "success" %} + + {{ event.severity }} + {{ squeeze(event.message) }} + +{% end %} +{% if event.severity == "error" %} + + {{ event.severity }} + {{ squeeze(event.message) }} + +{% end %} +{% end %} diff --git a/test/server-client/Boardwalkfile.py b/test/server-client/Boardwalkfile.py new file mode 100644 index 0000000..8577222 --- /dev/null +++ b/test/server-client/Boardwalkfile.py @@ -0,0 +1,52 @@ +from boardwalk import AnsibleTasksType, Job, Workflow, Workspace, WorkspaceConfig + +boardwalkd_url = "http://localhost:8888/" + + +class ShouldSucceedTestWorkspace(Workspace): + def config(self): + return WorkspaceConfig( + host_pattern="localhost", + workflow=TestWorkflow(), + ) + + +class ShouldFailTestWorkspace(Workspace): + def config(self): + return WorkspaceConfig( + host_pattern="localhost", + workflow=FailTestWorkflow(), + ) + + +class UIAbuseTestWorkspace(Workspace): + def config(self): + return WorkspaceConfig( + host_pattern="localhost:!foo:!bar:!baz:!fizz:!buzz:!my-very-long-host-name", + workflow=UITestVeryLongWorkflowNameWorkflow(), + ) + + +class UITestVeryLongWorkflowNameWorkflow(Workflow): + def jobs(self): + return TestJob() + + +class TestWorkflow(Workflow): + def jobs(self): + return TestJob() + + +class FailTestWorkflow(Workflow): + def jobs(self): + return FailTestJob() + + +class TestJob(Job): + def tasks(self) -> AnsibleTasksType: + return [{"debug": {"msg": "hello test"}}] + + +class FailTestJob(Job): + def tasks(self) -> AnsibleTasksType: + return [{"fail": {"msg": "failed successfully"}}] diff --git a/typings/ansible_runner/__init__.pyi b/typings/ansible_runner/__init__.pyi new file mode 100644 index 0000000..96f9b59 --- /dev/null +++ b/typings/ansible_runner/__init__.pyi @@ -0,0 +1,8 @@ +""" +This type stub file was generated by pyright and manually modified +""" + +from .exceptions import AnsibleRunnerException as AnsibleRunnerException +from .interface import run as run, run_command as run_command +from .runner import Runner as Runner, RunnerEvent as RunnerEvent +from .runner_config import RunnerConfig as RunnerConfig diff --git a/typings/ansible_runner/__main__.pyi b/typings/ansible_runner/__main__.pyi new file mode 100644 index 0000000..f47d5e7 --- /dev/null +++ b/typings/ansible_runner/__main__.pyi @@ -0,0 +1,47 @@ +""" +This type stub file was generated by pyright. +""" + +import argparse +from contextlib import contextmanager + +VERSION = ... +DEFAULT_ROLES_PATH = ... +DEFAULT_RUNNER_BINARY = ... +DEFAULT_RUNNER_PLAYBOOK = ... +DEFAULT_RUNNER_ROLE = ... +DEFAULT_RUNNER_MODULE = ... +DEFAULT_UUID = ... +DEFAULT_CLI_ARGS = ... +logger = ... + +class AnsibleRunnerArgumentParser(argparse.ArgumentParser): + def error(self, message): ... + +@contextmanager +def role_manager(vargs): ... +def print_common_usage(): ... +def add_args_to_parser(parser, args): # -> None: + """ + Traverse a tuple of argments to add to a parser + + :param argparse.ArgumentParser parser: Instance of a parser, subparser, or argument group + + :param tuple args: Tuple of tuples, format ((arg1, arg2), {'kwarg1':'val1'},) + + :returns: None + """ + ... + +def main(sys_args=...): # -> int | None: + """Main entry point for ansible-runner executable + + When the ```ansible-runner``` command is executed, this function + is the main entry point that is called and executed. + + :param list sys_args: List of arguments to be parsed by the parser + + :returns: an instance of SystemExit + :rtype: SystemExit + """ + ... diff --git a/typings/ansible_runner/cleanup.pyi b/typings/ansible_runner/cleanup.pyi new file mode 100644 index 0000000..7714fcc --- /dev/null +++ b/typings/ansible_runner/cleanup.pyi @@ -0,0 +1,31 @@ +""" +This type stub file was generated by pyright. +""" + +__all__ = ["add_cleanup_args", "run_cleanup"] + +def add_cleanup_args(command): ... +def run_command(cmd): # -> str: + """Given list cmd, runs command and returns standard out, expecting success""" + ... + +def is_alive(dir): ... +def project_idents(dir): # -> list[str]: + """Given dir, give list of idents that we have artifacts for""" + ... + +def delete_associated_folders(dir): # -> None: + """Where dir is the private_data_dir for a completed job, this deletes related tmp folders it used""" + ... + +def validate_pattern(pattern): ... +def cleanup_dirs(pattern, exclude_strings=..., grace_period=...): ... +def cleanup_images(images, runtime=...): # -> Literal[0]: + """Note: docker will just untag while podman will remove layers with same command""" + ... + +def prune_images(runtime=...): # -> bool: + """Run the prune images command and return changed status""" + ... + +def run_cleanup(vargs): ... diff --git a/typings/ansible_runner/config/__init__.pyi b/typings/ansible_runner/config/__init__.pyi new file mode 100644 index 0000000..cea7ef9 --- /dev/null +++ b/typings/ansible_runner/config/__init__.pyi @@ -0,0 +1,3 @@ +""" +This type stub file was generated by pyright. +""" diff --git a/typings/ansible_runner/config/_base.pyi b/typings/ansible_runner/config/_base.pyi new file mode 100644 index 0000000..0220bc9 --- /dev/null +++ b/typings/ansible_runner/config/_base.pyi @@ -0,0 +1,52 @@ +""" +This type stub file was generated by pyright. +""" + +logger = ... + +class BaseExecutionMode: + NONE = ... + ANSIBLE_COMMANDS = ... + GENERIC_COMMANDS = ... + +class BaseConfig: + def __init__( + self, + private_data_dir=..., + host_cwd=..., + envvars=..., + passwords=..., + settings=..., + project_dir=..., + artifact_dir=..., + fact_cache_type=..., + fact_cache=..., + process_isolation=..., + process_isolation_executable=..., + container_image=..., + container_volume_mounts=..., + container_options=..., + container_workdir=..., + container_auth_data=..., + ident=..., + rotate_artifacts=..., + timeout=..., + ssh_key=..., + quiet=..., + json_mode=..., + check_job_event_data=..., + suppress_env_files=..., + ) -> None: ... + + _CONTAINER_ENGINES = ... + @property + def containerized(self): ... + def wrap_args_for_containerization(self, args, execution_mode, cmdline_args): ... + def wrap_args_with_ssh_agent( + self, args, ssh_key_path, ssh_auth_sock=..., silence_ssh_add=... + ): # -> list[str]: + """ + Given an existing command line and parameterization this will return the same command line wrapped with the + necessary calls to ``ssh-agent`` + """ + ... diff --git a/typings/ansible_runner/config/ansible_cfg.pyi b/typings/ansible_runner/config/ansible_cfg.pyi new file mode 100644 index 0000000..3d1059c --- /dev/null +++ b/typings/ansible_runner/config/ansible_cfg.pyi @@ -0,0 +1,32 @@ +""" +This type stub file was generated by pyright. +""" + +from ansible_runner.config._base import BaseConfig + +logger = ... + +class AnsibleCfgConfig(BaseConfig): + """ + A ``Runner`` configuration object that's meant to encapsulate the configuration used by the + :py:mod:`ansible_runner.runner.AnsibleCfgConfig` object to launch and manage the invocation of + command execution. + + Typically this object is initialized for you when using the standard ``get_ansible_config`` interfaces in :py:mod:`ansible_runner.interface` + but can be used to construct the ``AnsibleCfgConfig`` configuration to be invoked elsewhere. It can also be overridden to provide different + functionality to the AnsibleCfgConfig object. + + :Example: + + >>> ac = AnsibleCfgConfig(...) + >>> r = Runner(config=ac) + >>> r.run() + + """ + + def __init__(self, runner_mode=..., **kwargs) -> None: ... + + _supported_actions = ... + def prepare_ansible_config_command( + self, action, config_file=..., only_changed=... + ): ... diff --git a/typings/ansible_runner/config/command.pyi b/typings/ansible_runner/config/command.pyi new file mode 100644 index 0000000..07b9008 --- /dev/null +++ b/typings/ansible_runner/config/command.pyi @@ -0,0 +1,30 @@ +""" +This type stub file was generated by pyright. +""" + +from ansible_runner.config._base import BaseConfig + +logger = ... + +class CommandConfig(BaseConfig): + """ + A ``Runner`` configuration object that's meant to encapsulate the configuration used by the + :py:mod:`ansible_runner.runner.CommandConfig` object to launch and manage the invocation of + command execution. + + Typically this object is initialized for you when using the standard ``run`_command` interfaces in :py:mod:`ansible_runner.interface` + but can be used to construct the ``CommandConfig`` configuration to be invoked elsewhere. It can also be overridden to provide different + functionality to the CommandConfig object. + :Example: + + >>> cc = CommandConfig(...) + >>> r = Runner(config=cc) + >>> r.run() + """ + + def __init__( + self, input_fd=..., output_fd=..., error_fd=..., runner_mode=..., **kwargs + ) -> None: ... + + _ANSIBLE_NON_INERACTIVE_CMDS = ... + def prepare_run_command(self, executable_cmd, cmdline_args=...): ... diff --git a/typings/ansible_runner/config/doc.pyi b/typings/ansible_runner/config/doc.pyi new file mode 100644 index 0000000..846eaab --- /dev/null +++ b/typings/ansible_runner/config/doc.pyi @@ -0,0 +1,58 @@ +""" +This type stub file was generated by pyright. +""" + +from ansible_runner.config._base import BaseConfig + +logger = ... + +class DocConfig(BaseConfig): + """ + A ``Runner`` configuration object that's meant to encapsulate the configuration used by the + :py:mod:`ansible_runner.runner.DocConfig` object to launch and manage the invocation of + command execution. + + Typically this object is initialized for you when using the standard ``get_plugin_docs`` or ``get_plugin_list`` interfaces + in :py:mod:`ansible_runner.interface` but can be used to construct the ``DocConfig`` configuration to be invoked elsewhere. + It can also be overridden to provide different functionality to the DocConfig object. + + :Example: + + >>> dc = DocConfig(...) + >>> r = Runner(config=dc) + >>> r.run() + + """ + + def __init__(self, runner_mode=..., **kwargs) -> None: ... + + _supported_response_formats = ... + def prepare_plugin_docs_command( + self, + plugin_names, + plugin_type=..., + response_format=..., + snippet=..., + playbook_dir=..., + module_path=..., + ): ... + def prepare_plugin_list_command( + self, + list_files=..., + response_format=..., + plugin_type=..., + playbook_dir=..., + module_path=..., + ): ... + def prepare_role_list_command(self, collection_name, playbook_dir): # -> None: + """ + ansible-doc -t role -l -j + """ + ... + def prepare_role_argspec_command( + self, role_name, collection_name, playbook_dir + ): # -> None: + """ + ansible-doc -t role -j . + """ + ... diff --git a/typings/ansible_runner/config/inventory.pyi b/typings/ansible_runner/config/inventory.pyi new file mode 100644 index 0000000..8ebba6c --- /dev/null +++ b/typings/ansible_runner/config/inventory.pyi @@ -0,0 +1,42 @@ +""" +This type stub file was generated by pyright. +""" + +from ansible_runner.config._base import BaseConfig + +logger = ... + +class InventoryConfig(BaseConfig): + """ + A ``Runner`` configuration object that's meant to encapsulate the configuration used by the + :py:mod:`ansible_runner.runner.InventoryConfig` object to launch and manage the invocation of + command execution. + + Typically this object is initialized for you when using the standard ``get_inventory`` interfaces in :py:mod:`ansible_runner.interface` + but can be used to construct the ``InventoryConfig`` configuration to be invoked elsewhere. It can also be overridden to provide different + functionality to the InventoryConfig object. + + :Example: + + >>> ic = InventoryConfig(...) + >>> r = Runner(config=ic) + >>> r.run() + + """ + + def __init__(self, runner_mode=..., **kwargs) -> None: ... + + _supported_response_formats = ... + _supported_actions = ... + def prepare_inventory_command( + self, + action, + inventories, + response_format=..., + host=..., + playbook_dir=..., + vault_ids=..., + vault_password_file=..., + output_file=..., + export=..., + ): ... diff --git a/typings/ansible_runner/config/runner.pyi b/typings/ansible_runner/config/runner.pyi new file mode 100644 index 0000000..6fb9559 --- /dev/null +++ b/typings/ansible_runner/config/runner.pyi @@ -0,0 +1,104 @@ +""" +This type stub file was generated by pyright. +""" + +from ansible_runner.config._base import BaseConfig + +logger = ... + +class ExecutionMode: + NONE = ... + ANSIBLE = ... + ANSIBLE_PLAYBOOK = ... + RAW = ... + +class RunnerConfig(BaseConfig): + """ + A ``Runner`` configuration object that's meant to encapsulate the configuration used by the + :py:mod:`ansible_runner.runner.Runner` object to launch and manage the invocation of ``ansible`` + and ``ansible-playbook`` + + Typically this object is initialized for you when using the standard ``run`` interfaces in :py:mod:`ansible_runner.interface` + but can be used to construct the ``Runner`` configuration to be invoked elsewhere. It can also be overridden to provide different + functionality to the Runner object. + + :Example: + + >>> rc = RunnerConfig(...) + >>> r = Runner(config=rc) + >>> r.run() + + """ + + def __init__( + self, + private_data_dir, + playbook=..., + inventory=..., + roles_path=..., + limit=..., + module=..., + module_args=..., + verbosity=..., + host_pattern=..., + binary=..., + extravars=..., + suppress_output_file=..., + suppress_ansible_output=..., + process_isolation_path=..., + process_isolation_hide_paths=..., + process_isolation_show_paths=..., + process_isolation_ro_paths=..., + tags=..., + skip_tags=..., + directory_isolation_base_path=..., + forks=..., + cmdline=..., + omit_event_data=..., + only_failed_event_data=..., + **kwargs + ) -> None: ... + @property + def sandboxed(self): ... + def prepare(self): # -> None: + """ + Performs basic checks and then properly invokes + + - prepare_inventory + - prepare_env + - prepare_command + + It's also responsible for wrapping the command with the proper ssh agent invocation + and setting early ANSIBLE_ environment variables. + """ + ... + def prepare_inventory(self): # -> None: + """ + Prepares the inventory default under ``private_data_dir`` if it's not overridden by the constructor. + """ + ... + def prepare_env(self): # -> None: + """ + Manages reading environment metadata files under ``private_data_dir`` and merging/updating + with existing values so the :py:class:`ansible_runner.runner.Runner` object can read and use them easily + """ + ... + def prepare_command(self): ... + def generate_ansible_command(self): # -> list[Unknown | str]: + """ + Given that the ``RunnerConfig`` preparation methods have been run to gather the inputs this method + will generate the ``ansible`` or ``ansible-playbook`` command that will be used by the + :py:class:`ansible_runner.runner.Runner` object to start the process + """ + ... + def build_process_isolation_temp_dir(self): # -> str: + """ + Create a temporary directory for process isolation to use. + """ + ... + def wrap_args_for_sandbox(self, args): # -> list[Unknown | str | Any]: + """ + Wrap existing command line with bwrap to restrict access to: + - self.process_isolation_path (generally, /tmp) (except for own /tmp files) + """ + ... diff --git a/typings/ansible_runner/defaults.pyi b/typings/ansible_runner/defaults.pyi new file mode 100644 index 0000000..337439b --- /dev/null +++ b/typings/ansible_runner/defaults.pyi @@ -0,0 +1,10 @@ +""" +This type stub file was generated by pyright. +""" + +default_process_isolation_executable = ... +default_container_image = ... +registry_auth_prefix = ... +GRACE_PERIOD_DEFAULT = ... +AUTO_CREATE_NAMING = ... +AUTO_CREATE_DIR = ... diff --git a/typings/ansible_runner/display_callback/__init__.pyi b/typings/ansible_runner/display_callback/__init__.pyi new file mode 100644 index 0000000..cea7ef9 --- /dev/null +++ b/typings/ansible_runner/display_callback/__init__.pyi @@ -0,0 +1,3 @@ +""" +This type stub file was generated by pyright. +""" diff --git a/typings/ansible_runner/exceptions.pyi b/typings/ansible_runner/exceptions.pyi new file mode 100644 index 0000000..02fec2d --- /dev/null +++ b/typings/ansible_runner/exceptions.pyi @@ -0,0 +1,18 @@ +""" +This type stub file was generated by pyright. +""" + +class AnsibleRunnerException(Exception): + """Generic Runner Error""" + + ... + +class ConfigurationError(AnsibleRunnerException): + """Misconfiguration of Runner""" + + ... + +class CallbackError(AnsibleRunnerException): + """Exception occurred in Callback""" + + ... diff --git a/typings/ansible_runner/interface.pyi b/typings/ansible_runner/interface.pyi new file mode 100644 index 0000000..d560f85 --- /dev/null +++ b/typings/ansible_runner/interface.pyi @@ -0,0 +1,750 @@ +""" +This type stub file was generated by pyright and manually modified +""" +import typing + +from runner import Runner + +def init_runner(**kwargs): # -> Transmitter | Worker | Processor | Runner: + """ + Initialize the Runner() instance + + This function will properly initialize both run() and run_async() + functions in the same way and return a value instance of Runner. + + See parameters given to :py:func:`ansible_runner.interface.run` + """ + ... + +def run(**kwargs: runnerKwargs) -> Runner: + """ + Run an Ansible Runner task in the foreground and return a Runner object when complete. + + :param str private_data_dir: The directory containing all runner metadata needed to invoke the runner + module. Output artifacts will also be stored here for later consumption. + :param str ident: The run identifier for this invocation of Runner. Will be used to create and name + the artifact directory holding the results of the invocation. + :param bool json_mode: Store event data in place of stdout on the console and in the stdout file + :param str or list playbook: The playbook (either a list or dictionary of plays, or as a path relative to + ``private_data_dir/project``) that will be invoked by runner when executing Ansible. + :param str module: The module that will be invoked in ad-hoc mode by runner when executing Ansible. + :param str module_args: The module arguments that will be supplied to ad-hoc mode. + :param str host_pattern: The host pattern to match when running in ad-hoc mode. + :param str or dict or list inventory: Overrides the inventory directory/file (supplied at ``private_data_dir/inventory``) with + a specific host or list of hosts. This can take the form of: + + - Path to the inventory file in the ``private_data_dir`` + - Native python dict supporting the YAML/json inventory structure + - A text INI formatted string + - A list of inventory sources, or an empty list to disable passing inventory + + :param str role: Name of the role to execute. + :param dict or list roles_path: Directory or list of directories to assign to ANSIBLE_ROLES_PATH + :param dict envvars: Environment variables to be used when running Ansible. Environment variables will also be + read from ``env/envvars`` in ``private_data_dir`` + :param dict extravars: Extra variables to be passed to Ansible at runtime using ``-e``. Extra vars will also be + read from ``env/extravars`` in ``private_data_dir``. + :param dict passwords: A dictionary containing password prompt patterns and response values used when processing output from + Ansible. Passwords will also be read from ``env/passwords`` in ``private_data_dir``. + :param dict settings: A dictionary containing settings values for the ``ansible-runner`` runtime environment. These will also + be read from ``env/settings`` in ``private_data_dir``. + :param str ssh_key: The ssh private key passed to ``ssh-agent`` as part of the ansible-playbook run. + :param str cmdline: Command line options passed to Ansible read from ``env/cmdline`` in ``private_data_dir`` + :param bool suppress_env_files: Disable the writing of files into the ``env`` which may store sensitive information + :param str limit: Matches ansible's ``--limit`` parameter to further constrain the inventory to be used + :param int forks: Control Ansible parallel concurrency + :param int verbosity: Control how verbose the output of ansible-playbook is + :param bool quiet: Disable all output + :param str artifact_dir: The path to the directory where artifacts should live, this defaults to 'artifacts' under the private data dir + :param str project_dir: The path to the playbook content, this defaults to 'project' within the private data dir + :param int rotate_artifacts: Keep at most n artifact directories, disable with a value of 0 which is the default + :param int timeout: The timeout value in seconds that will be passed to either ``pexpect`` of ``subprocess`` invocation + (based on ``runner_mode`` selected) while executing command. It the timeout is triggered it will force cancel the + execution. + :param str streamer: Optionally invoke ansible-runner as one of the steps in the streaming pipeline + :param io.FileIO _input: An optional file or file-like object for use as input in a streaming pipeline + :param io.FileIO _output: An optional file or file-like object for use as output in a streaming pipeline + :param Callable event_handler: An optional callback that will be invoked any time an event is received by Runner itself, return True to keep the event + :param Callable cancel_callback: An optional callback that can inform runner to cancel (returning True) or not (returning False) + :param Callable finished_callback: An optional callback that will be invoked at shutdown after process cleanup. + :param Callable status_handler: An optional callback that will be invoked any time the status changes (e.g...started, running, failed, successful, timeout) + :param Callable artifacts_handler: An optional callback that will be invoked at the end of the run to deal with the artifacts from the run. + :param bool process_isolation: Enable process isolation, using either a container engine (e.g. podman) or a sandbox (e.g. bwrap). + :param str process_isolation_executable: Process isolation executable or container engine used to isolate execution. (default: podman) + :param str process_isolation_path: Path that an isolated playbook run will use for staging. (default: /tmp) + :param str or list process_isolation_hide_paths: A path or list of paths on the system that should be hidden from the playbook run. + :param str or list process_isolation_show_paths: A path or list of paths on the system that should be exposed to the playbook run. + :param str or list process_isolation_ro_paths: A path or list of paths on the system that should be exposed to the playbook run as read-only. + :param str container_image: Container image to use when running an ansible task (default: quay.io/ansible/ansible-runner:devel) + :param list container_volume_mounts: List of bind mounts in the form 'host_dir:/container_dir. (default: None) + :param list container_options: List of container options to pass to execution engine. + :param str directory_isolation_base_path: An optional path will be used as the base path to create a temp directory, the project contents will be + copied to this location which will then be used as the working directory during playbook execution. + :param str fact_cache: A string that will be used as the name for the subdirectory of the fact cache in artifacts directory. + This is only used for 'jsonfile' type fact caches. + :param str fact_cache_type: A string of the type of fact cache to use. Defaults to 'jsonfile'. + :param bool omit_event_data: Omits extra ansible event data from event payload (stdout and event still included) + :param bool only_failed_event_data: Omits extra ansible event data unless it's a failed event (stdout and event still included) + :param bool check_job_event_data: Check if job events data is completely generated. If event data is not completely generated and if + value is set to 'True' it will raise 'AnsibleRunnerException' exception, + if set to 'False' it log a debug message and continue execution. Default value is 'False' + + :returns: A :py:class:`ansible_runner.runner.Runner` object, or a simple object containing ``rc`` if run remotely + """ + ... + +def run_async(**kwargs): # -> tuple[Thread, Transmitter | Worker | Processor | Runner]: + """ + Runs an Ansible Runner task in the background which will start immediately. Returns the thread object and a Runner object. + + This uses the same parameters as :py:func:`ansible_runner.interface.run` + + :returns: A tuple containing a :py:class:`threading.Thread` object and a :py:class:`ansible_runner.runner.Runner` object + """ + ... + +def init_command_config(executable_cmd, cmdline_args=..., **kwargs): # -> Runner: + """ + Initialize the Runner() instance + + This function will properly initialize both run_command() and run_command_async() + functions in the same way and return a value instance of Runner. + + See parameters given to :py:func:`ansible_runner.interface.run_command` + """ + ... + +def run_command( + executable_cmd: str, + quiet: bool, + project_dir: str, + suppress_env_files: bool, + input_fd: typing.TextIO, + envvars: dict[str, str | int], + cmdline_args: list[str] = ..., +) -> tuple[str, str, int]: + """ + Run an (Ansible) commands in the foreground and return a Runner object when complete. + + :param str executable_cmd: The command to be executed. + :param list cmdline_args: A list of arguments to be passed to the executable command. + :param int input_fd: This parameter is applicable when ``runner_mode`` is set to ``subprocess``, it provides the + input file descrption to interact with the sub-process running the command. + :param int output_fd: The output file descriptor to stream the output of command execution. + :param int error_fd: This parameter is applicable when ``runner_mode`` is set to ``subprocess``, it provides the + error file descrption to read the error received while executing the command. + :param str runner_mode: The applicable values are ``pexpect`` and ``subprocess``. If the value of ``input_fd`` parameter + is set or the executable command is one of ``ansible-config``, ``ansible-doc`` or ``ansible-galaxy`` + the default value is set to ``subprocess`` else in other cases it is set to ``pexpect``. + :param str host_cwd: The host current working directory to be mounted within the container (if enabled) and will be + the work directory within container. + :param dict envvars: Environment variables to be used when running Ansible. Environment variables will also be + read from ``env/envvars`` in ``private_data_dir`` + :param dict passwords: A dictionary containing password prompt patterns and response values used when processing output from + Ansible. Passwords will also be read from ``env/passwords`` in ``private_data_dir``. + :param dict settings: A dictionary containing settings values for the ``ansible-runner`` runtime environment. These will also + be read from ``env/settings`` in ``private_data_dir``. + :param str ssh_key: The ssh private key passed to ``ssh-agent`` as part of the ansible-playbook run. + :param bool quiet: Disable all output + :param bool json_mode: Store event data in place of stdout on the console and in the stdout file + :param str artifact_dir: The path to the directory where artifacts should live, this defaults to 'artifacts' under the private data dir + :param str project_dir: The path to the playbook content, this defaults to 'project' within the private data dir + :param int rotate_artifacts: Keep at most n artifact directories, disable with a value of 0 which is the default + :param int timeout: The timeout value in seconds that will be passed to either ``pexpect`` of ``subprocess`` invocation + (based on ``runner_mode`` selected) while executing command. It the timeout is triggered it will force cancel the + execution. + :param bool process_isolation: Enable process isolation, using a container engine (e.g. podman). + :param str process_isolation_executable: Process isolation executable or container engine used to isolate execution. (default: podman) + :param str container_image: Container image to use when running an ansible task (default: quay.io/ansible/ansible-runner:devel) + :param list container_volume_mounts: List of bind mounts in the form 'host_dir:/container_dir:labels. (default: None) + :param list container_options: List of container options to pass to execution engine. + :param str container_workdir: The working directory within the container. + :param str fact_cache: A string that will be used as the name for the subdirectory of the fact cache in artifacts directory. + This is only used for 'jsonfile' type fact caches. + :param str fact_cache_type: A string of the type of fact cache to use. Defaults to 'jsonfile'. + :param str private_data_dir: The directory containing all runner metadata needed to invoke the runner + module. Output artifacts will also be stored here for later consumption. + :param str ident: The run identifier for this invocation of Runner. Will be used to create and name + the artifact directory holding the results of the invocation. + :param Callable event_handler: An optional callback that will be invoked any time an event is received by Runner itself, return True to keep the event + :param Callable cancel_callback: An optional callback that can inform runner to cancel (returning True) or not (returning False) + :param Callable finished_callback: An optional callback that will be invoked at shutdown after process cleanup. + :param Callable status_handler: An optional callback that will be invoked any time the status changes (e.g...started, running, failed, successful, timeout) + :param Callable artifacts_handler: An optional callback that will be invoked at the end of the run to deal with the artifacts from the run. + :param bool check_job_event_data: Check if job events data is completely generated. If event data is not completely generated and if + value is set to 'True' it will raise 'AnsibleRunnerException' exception, + if set to 'False' it log a debug message and continue execution. Default value is 'False' + + :returns: Returns a tuple of response, error string and return code. + In case if ``runner_mode`` is set to ``pexpect`` the error value is empty as + ``pexpect`` uses same output descriptor for stdout and stderr. + """ + ... + +def run_command_async( + executable_cmd, cmdline_args=..., **kwargs +): # -> tuple[Thread, Runner]: + """ + Run an (Ansible) commands in the background which will start immediately. Returns the thread object and a Runner object. + + This uses the same parameters as :py:func:`ansible_runner.interface.run_command` + + :returns: A tuple containing a :py:class:`threading.Thread` object and a :py:class:`ansible_runner.runner.Runner` object + """ + ... + +def init_plugin_docs_config( + plugin_names, + plugin_type=..., + response_format=..., + snippet=..., + playbook_dir=..., + module_path=..., + **kwargs +): # -> Runner: + """ + Initialize the Runner() instance + + This function will properly initialize both get_plugin_docs() and get_plugin_docs_async() + functions in the same way and return a value instance of Runner. + + See parameters given to :py:func:`ansible_runner.interface.get_plugin_docs` + """ + ... + +def get_plugin_docs( + plugin_names, + plugin_type=..., + response_format=..., + snippet=..., + playbook_dir=..., + module_path=..., + **kwargs +): # -> tuple[Any | str, str]: + """ + Run an ansible-doc command to get plugin docs in the foreground and return a Runner object when complete. + + :param plugin_names: The name of the plugins to get docs. + :param plugin_type: The type of the plugin mentioned in plugins_names. Valid values are ``become``, ``cache``, ``callback``, + ``cliconf``, ``connection``, ``httpapi``, ``inventory``, ``lookup``, ``netconf``, ``shell``, ``vars``, + ``module``, ``strategy``. If the value is not provided it defaults to ``module``. + :param response_format: The output format for response. Valid values can be one of ``json`` or ``human`` and the response + is either json string or plain text in human readable foramt. Default value is ``json``. + :param snippet: Show playbook snippet for specified plugin(s). + :param playbook_dir: This parameter is used to sets the relative path to handle playbook adjacent installed plugins. + :param module_path: This parameter is prepend colon-separated path(s) to module library + (default=~/.ansible/plugins/modules:/usr/share/ansible/plugins/modules). + :param runner_mode: The applicable values are ``pexpect`` and ``subprocess``. Default is set to ``subprocess``. + :param host_cwd: The host current working directory to be mounted within the container (if enabled) and will be + the work directory within container. + :param envvars: Environment variables to be used when running Ansible. Environment variables will also be + read from ``env/envvars`` in ``private_data_dir`` + :param passwords: A dictionary containing password prompt patterns and response values used when processing output from + Ansible. Passwords will also be read from ``env/passwords`` in ``private_data_dir``. + :param settings: A dictionary containing settings values for the ``ansible-runner`` runtime environment. These will also + be read from ``env/settings`` in ``private_data_dir``. + :param ssh_key: The ssh private key passed to ``ssh-agent`` as part of the ansible-playbook run. + :param quiet: Disable all output + :param json_mode: Store event data in place of stdout on the console and in the stdout file + :param artifact_dir: The path to the directory where artifacts should live, this defaults to 'artifacts' under the private data dir + :param project_dir: The path to the playbook content, this defaults to 'project' within the private data dir + :param rotate_artifacts: Keep at most n artifact directories, disable with a value of 0 which is the default + :param timeout: The timeout value in seconds that will be passed to either ``pexpect`` of ``subprocess`` invocation + (based on ``runner_mode`` selected) while executing command. It the timeout is triggered it will force cancel the + execution. + :param process_isolation: Enable process isolation, using a container engine (e.g. podman). + :param process_isolation_executable: Process isolation executable or container engine used to isolate execution. (default: podman) + :param container_image: Container image to use when running an ansible task (default: quay.io/ansible/ansible-runner:devel) + :param container_volume_mounts: List of bind mounts in the form 'host_dir:/container_dir:labels. (default: None) + :param container_options: List of container options to pass to execution engine. + :param container_workdir: The working directory within the container. + :param fact_cache: A string that will be used as the name for the subdirectory of the fact cache in artifacts directory. + This is only used for 'jsonfile' type fact caches. + :param fact_cache_type: A string of the type of fact cache to use. Defaults to 'jsonfile'. + :param private_data_dir: The directory containing all runner metadata needed to invoke the runner + module. Output artifacts will also be stored here for later consumption. + :param ident: The run identifier for this invocation of Runner. Will be used to create and name + the artifact directory holding the results of the invocation. + :param event_handler: An optional callback that will be invoked any time an event is received by Runner itself, return True to keep the event + :param cancel_callback: An optional callback that can inform runner to cancel (returning True) or not (returning False) + :param finished_callback: An optional callback that will be invoked at shutdown after process cleanup. + :param status_handler: An optional callback that will be invoked any time the status changes (e.g...started, running, failed, successful, timeout) + :param artifacts_handler: An optional callback that will be invoked at the end of the run to deal with the artifacts from the run. + :param check_job_event_data: Check if job events data is completely generated. If event data is not completely generated and if + value is set to 'True' it will raise 'AnsibleRunnerException' exception, + if set to 'False' it log a debug message and continue execution. Default value is 'False' + + :type plugin_names: list + :type plugin_type: str + :type response_format: str + :type snippet: bool + :type playbook_dir: str + :type module_path: str + :type runner_mode: str + :type host_cwd: str + :type envvars: dict + :type passwords: dict + :type settings: dict + :type private_data_dir: str + :type project_dir: str + :type artifact_dir: str + :type fact_cache_type: str + :type fact_cache: str + :type process_isolation: bool + :type process_isolation_executable: str + :type container_image: str + :type container_volume_mounts: list + :type container_options: list + :type container_workdir: str + :type ident: str + :type rotate_artifacts: int + :type timeout: int + :type ssh_key: str + :type quiet: bool + :type json_mode: bool + :type event_handler: Callable + :type cancel_callback: Callable + :type finished_callback: Callable + :type status_handler: Callable + :type artifacts_handler: Callable + :type check_job_event_data: bool + + :returns: Returns a tuple of response and error string. In case if ``runner_mode`` is set to ``pexpect`` the error value is empty as + ``pexpect`` uses same output descriptor for stdout and stderr. If the value of ``response_format`` is ``json`` + it returns a python dictionary object. + """ + ... + +def get_plugin_docs_async( + plugin_names, + plugin_type=..., + response_format=..., + snippet=..., + playbook_dir=..., + module_path=..., + **kwargs +): # -> tuple[Thread, Runner]: + """ + Run an ansible-doc command in the background which will start immediately. Returns the thread object and a Runner object. + + This uses the same parameters as :py:func:`ansible_runner.interface.get_plugin_docs` + + :returns: A tuple containing a :py:class:`threading.Thread` object and a :py:class:`ansible_runner.runner.Runner` object + """ + ... + +def get_plugin_list( + list_files=..., + response_format=..., + plugin_type=..., + playbook_dir=..., + module_path=..., + **kwargs +): # -> tuple[Any | str, str]: + """ + Run an ansible-doc command to get list of installed Ansible plugins. + + :param list_files: The boolean parameter is set to ``True`` returns file path of the plugin along with the plugin name. + :param response_format: The output format for response. Valid values can be one of ``json`` or ``human`` and the response + is either json string or plain text in human readable foramt. Default value is ``json``. + :param plugin_type: The type of the plugin mentioned in plugins_names. Valid values are ``become``, ``cache``, ``callback``, + ``cliconf``, ``connection``, ``httpapi``, ``inventory``, ``lookup``, ``netconf``, ``shell``, ``vars``, + ``module``, ``strategy``. If the value is not provided it defaults to ``module``. + :param playbook_dir: This parameter is used to sets the relative path to handle playbook adjacent installed plugins. + :param module_path: This parameter is prepend colon-separated path(s) to module library + (default=~/.ansible/plugins/modules:/usr/share/ansible/plugins/modules). + :param runner_mode: The applicable values are ``pexpect`` and ``subprocess``. Default is set to ``subprocess``. + :param host_cwd: The host current working directory to be mounted within the container (if enabled) and will be + the work directory within container. + :param envvars: Environment variables to be used when running Ansible. Environment variables will also be + read from ``env/envvars`` in ``private_data_dir`` + :param passwords: A dictionary containing password prompt patterns and response values used when processing output from + Ansible. Passwords will also be read from ``env/passwords`` in ``private_data_dir``. + :param settings: A dictionary containing settings values for the ``ansible-runner`` runtime environment. These will also + be read from ``env/settings`` in ``private_data_dir``. + :param ssh_key: The ssh private key passed to ``ssh-agent`` as part of the ansible-playbook run. + :param quiet: Disable all output + :param json_mode: Store event data in place of stdout on the console and in the stdout file + :param artifact_dir: The path to the directory where artifacts should live, this defaults to 'artifacts' under the private data dir + :param project_dir: The path to the playbook content, this defaults to 'project' within the private data dir + :param rotate_artifacts: Keep at most n artifact directories, disable with a value of 0 which is the default + :param timeout: The timeout value in seconds that will be passed to either ``pexpect`` of ``subprocess`` invocation + (based on ``runner_mode`` selected) while executing command. It the timeout is triggered it will force cancel the + execution. + :param process_isolation: Enable process isolation, using a container engine (e.g. podman). + :param process_isolation_executable: Process isolation executable or container engine used to isolate execution. (default: podman) + :param container_image: Container image to use when running an ansible task (default: quay.io/ansible/ansible-runner:devel) + :param container_volume_mounts: List of bind mounts in the form 'host_dir:/container_dir:labels. (default: None) + :param container_options: List of container options to pass to execution engine. + :param container_workdir: The working directory within the container. + :param fact_cache: A string that will be used as the name for the subdirectory of the fact cache in artifacts directory. + This is only used for 'jsonfile' type fact caches. + :param fact_cache_type: A string of the type of fact cache to use. Defaults to 'jsonfile'. + :param private_data_dir: The directory containing all runner metadata needed to invoke the runner + module. Output artifacts will also be stored here for later consumption. + :param ident: The run identifier for this invocation of Runner. Will be used to create and name + the artifact directory holding the results of the invocation. + :param event_handler: An optional callback that will be invoked any time an event is received by Runner itself, return True to keep the event + :param cancel_callback: An optional callback that can inform runner to cancel (returning True) or not (returning False) + :param finished_callback: An optional callback that will be invoked at shutdown after process cleanup. + :param status_handler: An optional callback that will be invoked any time the status changes (e.g...started, running, failed, successful, timeout) + :param artifacts_handler: An optional callback that will be invoked at the end of the run to deal with the artifacts from the run. + :param check_job_event_data: Check if job events data is completely generated. If event data is not completely generated and if + value is set to 'True' it will raise 'AnsibleRunnerException' exception, + if set to 'False' it log a debug message and continue execution. Default value is 'False' + + :type list_files: bool + :type plugin_type: str + :type response_format: str + :type playbook_dir: str + :type module_path: str + :type runner_mode: str + :type host_cwd: str + :type envvars: dict + :type passwords: dict + :type settings: dict + :type private_data_dir: str + :type project_dir: str + :type artifact_dir: str + :type fact_cache_type: str + :type fact_cache: str + :type process_isolation: bool + :type process_isolation_executable: str + :type container_image: str + :type container_volume_mounts: list + :type container_options: list + :type container_workdir: str + :type ident: str + :type rotate_artifacts: int + :type timeout: int + :type ssh_key: str + :type quiet: bool + :type json_mode: bool + :type event_handler: Callable + :type cancel_callback: Callable + :type finished_callback: Callable + :type status_handler: Callable + :type artifacts_handler: Callable + :type check_job_event_data: bool + + :returns: Returns a tuple of response and error string. In case if ``runner_mode`` is set to ``pexpect`` the error value is empty as + ``pexpect`` uses same output descriptor for stdout and stderr. If the value of ``response_format`` is ``json`` + it returns a python dictionary object. + """ + ... + +def get_inventory( + action, + inventories, + response_format=..., + host=..., + playbook_dir=..., + vault_ids=..., + vault_password_file=..., + output_file=..., + export=..., + **kwargs +): # -> tuple[Any | str, str]: + """ + Run an ansible-inventory command to get inventory related details. + + :param action: Valid values are one of ``graph``, ``host``, ``list`` + ``graph`` create inventory graph, ``host`` returns specific host info and works as inventory script and + ``list`` output all hosts info and also works as inventory script. + :param inventories: List of inventory host path. + :param response_format: The output format for response. Valid values can be one of ``json``, ``yaml``, ``toml``. + Default is ``json``. If ``action`` is ``graph`` only allowed value is ``json``. + :param host: When ``action`` is set to ``host`` this parameter is used to get the host specific information. + :param playbook_dir: This parameter is used to sets the relative path for the inventory. + :param vault_ids: The vault identity to use. + :param vault_password_file: The vault password files to use. + :param output_file: The file path in which inventory details should be sent to. + :param export: The boolean value if set represent in a way that is optimized for export,not as an accurate + representation of how Ansible has processed it. + :param runner_mode: The applicable values are ``pexpect`` and ``subprocess``. Default is set to ``subprocess``. + :param host_cwd: The host current working directory to be mounted within the container (if enabled) and will be + the work directory within container. + :param envvars: Environment variables to be used when running Ansible. Environment variables will also be + read from ``env/envvars`` in ``private_data_dir`` + :param passwords: A dictionary containing password prompt patterns and response values used when processing output from + Ansible. Passwords will also be read from ``env/passwords`` in ``private_data_dir``. + :param settings: A dictionary containing settings values for the ``ansible-runner`` runtime environment. These will also + be read from ``env/settings`` in ``private_data_dir``. + :param ssh_key: The ssh private key passed to ``ssh-agent`` as part of the ansible-playbook run. + :param quiet: Disable all output + :param json_mode: Store event data in place of stdout on the console and in the stdout file + :param artifact_dir: The path to the directory where artifacts should live, this defaults to 'artifacts' under the private data dir + :param project_dir: The path to the playbook content, this defaults to 'project' within the private data dir + :param rotate_artifacts: Keep at most n artifact directories, disable with a value of 0 which is the default + :param timeout: The timeout value in seconds that will be passed to either ``pexpect`` of ``subprocess`` invocation + (based on ``runner_mode`` selected) while executing command. It the timeout is triggered it will force cancel the + execution. + :param process_isolation: Enable process isolation, using a container engine (e.g. podman). + :param process_isolation_executable: Process isolation executable or container engine used to isolate execution. (default: podman) + :param container_image: Container image to use when running an ansible task (default: quay.io/ansible/ansible-runner:devel) + :param container_volume_mounts: List of bind mounts in the form 'host_dir:/container_dir:labels. (default: None) + :param container_options: List of container options to pass to execution engine. + :param container_workdir: The working directory within the container. + :param fact_cache: A string that will be used as the name for the subdirectory of the fact cache in artifacts directory. + This is only used for 'jsonfile' type fact caches. + :param fact_cache_type: A string of the type of fact cache to use. Defaults to 'jsonfile'. + :param private_data_dir: The directory containing all runner metadata needed to invoke the runner + module. Output artifacts will also be stored here for later consumption. + :param ident: The run identifier for this invocation of Runner. Will be used to create and name + the artifact directory holding the results of the invocation. + :param event_handler: An optional callback that will be invoked any time an event is received by Runner itself, return True to keep the event + :param cancel_callback: An optional callback that can inform runner to cancel (returning True) or not (returning False) + :param finished_callback: An optional callback that will be invoked at shutdown after process cleanup. + :param status_handler: An optional callback that will be invoked any time the status changes (e.g...started, running, failed, successful, timeout) + :param artifacts_handler: An optional callback that will be invoked at the end of the run to deal with the artifacts from the run. + :param check_job_event_data: Check if job events data is completely generated. If event data is not completely generated and if + value is set to 'True' it will raise 'AnsibleRunnerException' exception, + if set to 'False' it log a debug message and continue execution. Default value is 'False' + :type action: str + :type inventories: list + :type response_format: str + :type host: str + :type playbook_dir: str + :type vault_ids: str + :type vault_password_file: str + :type output_file: str + :type export: bool + :type runner_mode: str + :type host_cwd: str + :type envvars: dict + :type passwords: dict + :type settings: dict + :type private_data_dir: str + :type project_dir: str + :type artifact_dir: str + :type fact_cache_type: str + :type fact_cache: str + :type process_isolation: bool + :type process_isolation_executable: str + :type container_image: str + :type container_volume_mounts: list + :type container_options: list + :type container_workdir: str + :type ident: str + :type rotate_artifacts: int + :type timeout: int + :type ssh_key: str + :type quiet: bool + :type json_mode: bool + :type event_handler: Callable + :type cancel_callback: Callable + :type finished_callback: Callable + :type status_handler: Callable + :type artifacts_handler: Callable + :type check_job_event_data: bool + + :returns: Returns a tuple of response and error string. In case if ``runner_mode`` is set to ``pexpect`` the error value is + empty as ``pexpect`` uses same output descriptor for stdout and stderr. If the vaue of ``response_format`` is ``json`` + it returns a python dictionary object. + """ + ... + +def get_ansible_config( + action, config_file=..., only_changed=..., **kwargs +): # -> tuple[str, str]: + """ + Run an ansible-config command to get ansible configuration releated details. + + :param action: Valid values are one of ``list``, ``dump``, ``view`` + ``list`` returns all config options, ``dump`` returns the active configuration and + ``view`` returns the view of configuration file. + :param config_file: Path to configuration file, defaults to first file found in precedence. . + :param only_changed: The boolean value when set to ``True`` returns only the configurations that have changed + from the default. This parameter is applicable only when ``action`` is set to ``dump``. + :param runner_mode: The applicable values are ``pexpect`` and ``subprocess``. Default is set to ``subprocess``. + :param host_cwd: The current working directory from which the command in executable_cmd should be be executed. + :param envvars: Environment variables to be used when running Ansible. Environment variables will also be + read from ``env/envvars`` in ``private_data_dir`` + :param passwords: A dictionary containing password prompt patterns and response values used when processing output from Ansible. + Passwords will also be read from ``env/passwords`` in ``private_data_dir``. + :param settings: A dictionary containing settings values for the ``ansible-runner`` runtime environment. These will also + be read from ``env/settings`` in ``private_data_dir``. + :param ssh_key: The ssh private key passed to ``ssh-agent`` as part of the ansible-playbook run. + :param quiet: Disable all output + :param json_mode: Store event data in place of stdout on the console and in the stdout file + :param artifact_dir: The path to the directory where artifacts should live, this defaults to 'artifacts' under the private data dir + :param project_dir: The path to the playbook content, this defaults to 'project' within the private data dir + :param rotate_artifacts: Keep at most n artifact directories, disable with a value of 0 which is the default + :param timeout: The timeout value in seconds that will be passed to either ``pexpect`` of ``subprocess`` invocation + (based on ``runner_mode`` selected) while executing command. It the timeout is triggered it will force cancel the + execution. + :param process_isolation: Enable process isolation, using a container engine (e.g. podman). + :param process_isolation_executable: Process isolation executable or container engine used to isolate execution. (default: podman) + :param container_image: Container image to use when running an ansible task (default: quay.io/ansible/ansible-runner:devel) + :param container_volume_mounts: List of bind mounts in the form 'host_dir:/container_dir:labels. (default: None) + :param container_options: List of container options to pass to execution engine. + :param container_workdir: The working directory within the container. + :param fact_cache: A string that will be used as the name for the subdirectory of the fact cache in artifacts directory. + This is only used for 'jsonfile' type fact caches. + :param fact_cache_type: A string of the type of fact cache to use. Defaults to 'jsonfile'. + :param private_data_dir: The directory containing all runner metadata needed to invoke the runner + module. Output artifacts will also be stored here for later consumption. + :param ident: The run identifier for this invocation of Runner. Will be used to create and name + the artifact directory holding the results of the invocation. + :param event_handler: An optional callback that will be invoked any time an event is received by Runner itself, return True to keep the event + :param cancel_callback: An optional callback that can inform runner to cancel (returning True) or not (returning False) + :param finished_callback: An optional callback that will be invoked at shutdown after process cleanup. + :param status_handler: An optional callback that will be invoked any time the status changes (e.g...started, running, failed, successful, timeout) + :param artifacts_handler: An optional callback that will be invoked at the end of the run to deal with the artifacts from the run. + :param check_job_event_data: Check if job events data is completely generated. If event data is not completely generated and if + value is set to 'True' it will raise 'AnsibleRunnerException' exception, + if set to 'False' it log a debug message and continue execution. Default value is 'False' + :type action: str + :type config_file: str + :type only_changed: bool + :type runner_mode: str + :type host_cwd: str + :type envvars: dict + :type passwords: dict + :type settings: dict + :type private_data_dir: str + :type project_dir: str + :type artifact_dir: str + :type fact_cache_type: str + :type fact_cache: str + :type process_isolation: bool + :type process_isolation_executable: str + :type container_image: str + :type container_volume_mounts: list + :type container_options: list + :type container_workdir: str + :type ident: str + :type rotate_artifacts: int + :type timeout: int + :type ssh_key: str + :type quiet: bool + :type json_mode: bool + :type event_handler: Callable + :type cancel_callback: Callable + :type finished_callback: Callable + :type status_handler: Callable + :type artifacts_handler: Callable + :type check_job_event_data: bool + + :returns: Returns a tuple of response and error string. In case if ``runner_mode`` is set to ``pexpect`` the error value is + empty as ``pexpect`` uses same output descriptor for stdout and stderr. + """ + ... + +def get_role_list( + collection=..., playbook_dir=..., **kwargs +): # -> tuple[Any | str, str]: + """ + Run an ``ansible-doc`` command to get list of installed collection roles. + + Only roles that have an argument specification defined are returned. + + .. note:: Version added: 2.2 + + :param str collection: A fully qualified collection name used to filter the results. + :param str playbook_dir: This parameter is used to set the relative path to handle playbook adjacent installed roles. + + :param str runner_mode: The applicable values are ``pexpect`` and ``subprocess``. Default is set to ``subprocess``. + :param str host_cwd: The host current working directory to be mounted within the container (if enabled) and will be + the work directory within container. + :param dict envvars: Environment variables to be used when running Ansible. Environment variables will also be + read from ``env/envvars`` in ``private_data_dir`` + :param dict passwords: A dictionary containing password prompt patterns and response values used when processing output from + Ansible. Passwords will also be read from ``env/passwords`` in ``private_data_dir``. + :param dict settings: A dictionary containing settings values for the ``ansible-runner`` runtime environment. These will also + be read from ``env/settings`` in ``private_data_dir``. + :param str ssh_key: The ssh private key passed to ``ssh-agent`` as part of the ansible-playbook run. + :param bool quiet: Disable all output + :param bool json_mode: Store event data in place of stdout on the console and in the stdout file + :param str artifact_dir: The path to the directory where artifacts should live, this defaults to 'artifacts' under the private data dir + :param str project_dir: The path to the playbook content, this defaults to 'project' within the private data dir + :param int rotate_artifacts: Keep at most n artifact directories, disable with a value of 0 which is the default + :param int timeout: The timeout value in seconds that will be passed to either ``pexpect`` of ``subprocess`` invocation + (based on ``runner_mode`` selected) while executing command. If the timeout is triggered, it will force cancel the execution. + :param bool process_isolation: Enable process isolation using a container engine, such as podman. + :param str process_isolation_executable: Process isolation executable or container engine used to isolate execution. (default: podman) + :param str container_image: Container image to use when running an Ansible task (default: quay.io/ansible/ansible-runner:devel) + :param list container_volume_mounts: List of bind mounts in the form ``host_dir:/container_dir:labels``. (default: None) + :param list container_options: List of container options to pass to execution engine. + :param str container_workdir: The working directory within the container. + :param str fact_cache: A string that will be used as the name for the subdirectory of the fact cache in artifacts directory. + This is only used for 'jsonfile' type fact caches. + :param str fact_cache_type: A string of the type of fact cache to use. Defaults to 'jsonfile'. + :param str private_data_dir: The directory containing all runner metadata needed to invoke the runner + module. Output artifacts will also be stored here for later consumption. + :param str ident: The run identifier for this invocation of Runner. Will be used to create and name + the artifact directory holding the results of the invocation. + :param Callable event_handler: An optional callback that will be invoked any time an event is received by Runner itself, return True to keep the event + :param Callable cancel_callback: An optional callback that can inform runner to cancel (returning True) or not (returning False) + :param Callable finished_callback: An optional callback that will be invoked at shutdown after process cleanup. + :param Callable status_handler: An optional callback that will be invoked any time the status changes + (for example: started, running, failed, successful, timeout) + :param Callable artifacts_handler: An optional callback that will be invoked at the end of the run to deal with the artifacts from the run. + :param bool check_job_event_data: Check if job events data is completely generated. If event data is not completely generated and if + value is set to 'True' it will raise 'AnsibleRunnerException' exception. If set to 'False', log a debug message and continue execution. + Default value is 'False' + + :returns: A tuple of response and error string. The response is a dictionary object + (as returned by ansible-doc JSON output) containing each role found, or an empty dict + if none are found. + """ + ... + +def get_role_argspec( + role, collection=..., playbook_dir=..., **kwargs +): # -> tuple[Any | str, str]: + """ + Run an ``ansible-doc`` command to get a role argument specification. + + .. note:: Version added: 2.2 + + :param str role: Simple role name, or fully qualified collection role name, to query. + :param str collection: If specified, will be combined with the role name to form a fully qualified collection role name. + If this is supplied, the ``role`` param should not be fully qualified. + :param str playbook_dir: This parameter is used to set the relative path to handle playbook adjacent installed roles. + + :param str runner_mode: The applicable values are ``pexpect`` and ``subprocess``. Default is set to ``subprocess``. + :param str host_cwd: The host current working directory to be mounted within the container (if enabled) and will be + the work directory within container. + :param dict envvars: Environment variables to be used when running Ansible. Environment variables will also be + read from ``env/envvars`` in ``private_data_dir`` + :param dict passwords: A dictionary containing password prompt patterns and response values used when processing output from + Ansible. Passwords will also be read from ``env/passwords`` in ``private_data_dir``. + :param dict settings: A dictionary containing settings values for the ``ansible-runner`` runtime environment. These will also + be read from ``env/settings`` in ``private_data_dir``. + :param str ssh_key: The ssh private key passed to ``ssh-agent`` as part of the ansible-playbook run. + :param bool quiet: Disable all output + :param bool json_mode: Store event data in place of stdout on the console and in the stdout file + :param str artifact_dir: The path to the directory where artifacts should live, this defaults to 'artifacts' under the private data dir + :param str project_dir: The path to the playbook content, this defaults to 'project' within the private data dir + :param int rotate_artifacts: Keep at most n artifact directories, disable with a value of 0 which is the default + :param int timeout: The timeout value in seconds that will be passed to either ``pexpect`` of ``subprocess`` invocation + (based on ``runner_mode`` selected) while executing command. If the timeout is triggered, it will force cancel the execution. + :param bool process_isolation: Enable process isolation using a container engine, such as podman. + :param str process_isolation_executable: Process isolation executable or container engine used to isolate execution. (default: podman) + :param str container_image: Container image to use when running an Ansible task (default: quay.io/ansible/ansible-runner:devel) + :param list container_volume_mounts: List of bind mounts in the form ``host_dir:/container_dir:labels``. (default: None) + :param list container_options: List of container options to pass to execution engine. + :param str container_workdir: The working directory within the container. + :param str fact_cache: A string that will be used as the name for the subdirectory of the fact cache in artifacts directory. + This is only used for 'jsonfile' type fact caches. + :param str fact_cache_type: A string of the type of fact cache to use. Defaults to 'jsonfile'. + :param str private_data_dir: The directory containing all runner metadata needed to invoke the runner + module. Output artifacts will also be stored here for later consumption. + :param str ident: The run identifier for this invocation of Runner. Will be used to create and name + the artifact directory holding the results of the invocation. + :param Callable event_handler: An optional callback that will be invoked any time an event is received by Runner itself, return True to keep the event + :param Callable cancel_callback: An optional callback that can inform runner to cancel (returning True) or not (returning False) + :param Callable finished_callback: An optional callback that will be invoked at shutdown after process cleanup. + :param Callable status_handler: An optional callback that will be invoked any time the status changes + (for example: started, running, failed, successful, timeout) + :param Callable artifacts_handler: An optional callback that will be invoked at the end of the run to deal with the artifacts from the run. + :param bool check_job_event_data: Check if job events data is completely generated. If event data is not completely generated and if + value is set to 'True' it will raise 'AnsibleRunnerException' exception. If set to 'False', log a debug message and continue execution. + Default value is 'False' + + :returns: A tuple of response and error string. The response is a dictionary object + (as returned by ansible-doc JSON output) containing each role found, or an empty dict + if none are found. + """ + ... diff --git a/typings/ansible_runner/loader.pyi b/typings/ansible_runner/loader.pyi new file mode 100644 index 0000000..67d068a --- /dev/null +++ b/typings/ansible_runner/loader.pyi @@ -0,0 +1,82 @@ +""" +This type stub file was generated by pyright. +""" + +class ArtifactLoader: + """ + Handles loading and caching file contents from disk + + This class will load the file contents and attempt to deserialize the + contents as either JSON or YAML. If the file contents cannot be + deserialized, the contents will be returned to the caller as a string. + + The deserialized file contents are stored as a cached object in the + instance to avoid any additional reads from disk for subsequent calls + to load the same file. + """ + + def __init__(self, base_path) -> None: ... + def get_contents(self, path): # -> str: + """ + Loads the contents of the file specified by path + + Args: + path (string): The relative or absolute path to the file to + be loaded. If the path is relative, then it is combined + with the base_path to generate a full path string + + Returns: + string: The contents of the file as a string + + Raises: + ConfigurationError: If the file cannot be loaded + """ + ... + def abspath(self, path): # -> str: + """ + Transform the path to an absolute path + + Args: + path (string): The path to transform to an absolute path + + Returns: + string: The absolute path to the file + """ + ... + def isfile(self, path): # -> bool: + """ + Check if the path is a file + + :params path: The path to the file to check. If the path is relative + it will be exanded to an absolute path + + :returns: boolean + """ + ... + def load_file( + self, path, objtype=..., encoding=... + ): # -> Any | bytes | str | None: + """ + Load the file specified by path + + This method will first try to load the file contents from cache and + if there is a cache miss, it will load the contents from disk + + Args: + path (string): The full or relative path to the file to be loaded + + encoding (string): The file contents text encoding + + objtype (object): The object type of the file contents. This + is used to type check the deserialized content against the + contents loaded from disk. + Ignore serializing if objtype is string_types + + Returns: + object: The deserialized file contents which could be either a + string object or a dict object + + Raises: + ConfigurationError: + """ + ... diff --git a/typings/ansible_runner/output.pyi b/typings/ansible_runner/output.pyi new file mode 100644 index 0000000..d1b1297 --- /dev/null +++ b/typings/ansible_runner/output.pyi @@ -0,0 +1,25 @@ +""" +This type stub file was generated by pyright. +""" + +DEBUG_ENABLED = ... +TRACEBACK_ENABLED = ... +_display_logger = ... +_debug_logger = ... + +def display(msg, log_only=...): ... +def debug(msg): ... +def set_logfile(filename): ... +def set_debug(value): ... +def set_traceback(value): ... +def configure(): # -> None: + """ + Configures the logging facility + + This function will setup an initial logging facility for handling display + and debug outputs. The default facility will send display messages to + stdout and the default debug facility will do nothing. + + :returns: None + """ + ... diff --git a/typings/ansible_runner/plugins/__init__.pyi b/typings/ansible_runner/plugins/__init__.pyi new file mode 100644 index 0000000..cea7ef9 --- /dev/null +++ b/typings/ansible_runner/plugins/__init__.pyi @@ -0,0 +1,3 @@ +""" +This type stub file was generated by pyright. +""" diff --git a/typings/ansible_runner/runner.pyi b/typings/ansible_runner/runner.pyi new file mode 100644 index 0000000..2e0210c --- /dev/null +++ b/typings/ansible_runner/runner.pyi @@ -0,0 +1,149 @@ +""" +This type stub file was generated by pyright and then manually improved +""" +from typing import Any, Iterable, TypedDict + +logger = ... + +class RunnerEvent(TypedDict): + event_data: EventData + event: str + stdout: str + +class EventData(TypedDict): + host: str + ignore_errors: bool + res: EventDataResult + task_action: str + task: str + +class EventDataResult(TypedDict): + _ansible_no_log: bool + ansible_facts: dict[str, Any] + content: str + msg: str + stat: EventDataStat + +class EventDataStat(TypedDict): + exists: bool + +class Runner: + def __init__( + self, + config, + cancel_callback=..., + remove_partials=..., + event_handler=..., + artifacts_handler=..., + finished_callback=..., + status_handler=..., + ) -> None: + self.rc: int | None = None + def event_callback(self, event_data): + """ + Invoked for every Ansible event to collect stdout with the event data and store it for + later use + """ + ... + def status_callback(self, status): ... + def run(self): # -> tuple[str | Unknown, int | Unknown]: + """ + Launch the Ansible task configured in self.config (A RunnerConfig object), returns once the + invocation is complete + """ + ... + @property + def stdout(self): # -> TextIOWrapper: + """ + Returns an open file handle to the stdout representing the Ansible run + """ + ... + @property + def stderr(self): # -> TextIOWrapper: + """ + Returns an open file handle to the stderr representing the Ansible run + """ + ... + @property + def events(self) -> Iterable[RunnerEvent]: + """ + A generator that will return all ansible job events in the order that they were emitted from Ansible + + :Example: + + .. code-block:: + + { + "event": "runner_on_ok", + "uuid": "00a50d9c-161a-4b74-b978-9f60becaf209", + "stdout": "ok: [localhost] => {\\r\\n \\" msg\\":\\"Test!\\"\\r\\n}", + "counter": 6, + "pid": 740, + "created": "2018-04-05T18:24:36.096725", + "end_line": 10, + "start_line": 7, + "event_data": { + "play_pattern": "all", + "play": "all", + "task": "debug", + "task_args": "msg=Test!", + "remote_addr": "localhost", + "res": { + "msg": "Test!", + "changed": false, + "_ansible_verbose_always": true, + "_ansible_no_log": false + }, + "pid": 740, + "play_uuid": "0242ac11-0002-443b-cdb1-000000000006", + "task_uuid": "0242ac11-0002-443b-cdb1-000000000008", + "event_loop": null, + "playbook_uuid": "634edeee-3228-4c17-a1b4-f010fdd42eb2", + "playbook": "test.yml", + "task_action": "debug", + "host": "localhost", + "task_path": "/tmp/demo/project/test.yml:3" + } + } + """ + ... + @property + def stats(self): # -> dict[str, Any] | None: + """ + Returns the final high level stats from the Ansible run + + Example: + {'dark': {}, 'failures': {}, 'skipped': {}, 'ok': {u'localhost': 2}, 'processed': {u'localhost': 1}} + """ + ... + def host_events(self, host): # -> filter[Any]: + """ + Given a host name, this will return all task events executed on that host + """ + ... + def kill_container(self): # -> None: + """ + Internal method to terminate a container being used for job isolation + """ + ... + @classmethod + def handle_termination(cls, pid, pidfile=..., is_cancel=...): # -> None: + """ + Internal method to terminate a subprocess spawned by ``pexpect`` representing an invocation of runner. + + :param pid: the process id of the running the job. + :param pidfile: the daemon's PID file + :param is_cancel: flag showing whether this termination is caused by + instance's cancel_flag. + """ + ... + def get_fact_cache(self, host): # -> Any | dict[Unknown, Unknown]: + """ + Get the entire fact cache only if the fact_cache_type is 'jsonfile' + """ + ... + def set_fact_cache(self, host, data): # -> int: + """ + Set the entire fact cache data only if the fact_cache_type is 'jsonfile' + """ + ... diff --git a/typings/ansible_runner/runner_config.pyi b/typings/ansible_runner/runner_config.pyi new file mode 100644 index 0000000..3bf9894 --- /dev/null +++ b/typings/ansible_runner/runner_config.pyi @@ -0,0 +1,5 @@ +""" +This type stub file was generated by pyright. +""" + +from ansible_runner.config.runner import * diff --git a/typings/ansible_runner/streaming.pyi b/typings/ansible_runner/streaming.pyi new file mode 100644 index 0000000..8e94687 --- /dev/null +++ b/typings/ansible_runner/streaming.pyi @@ -0,0 +1,40 @@ +""" +This type stub file was generated by pyright. +""" + +import json + +class UUIDEncoder(json.JSONEncoder): + def default(self, obj): ... + +class MockConfig: + def __init__(self, settings) -> None: ... + +class Transmitter: + def __init__(self, _output=..., **kwargs) -> None: ... + def run(self): ... + +class Worker: + def __init__(self, _input=..., _output=..., **kwargs) -> None: ... + def update_paths(self, kwargs): ... + def run(self): ... + def status_handler(self, status_data, runner_config): ... + def event_handler(self, event_data): ... + def artifacts_handler(self, artifact_dir): ... + def finished_callback(self, runner_obj): ... + +class Processor: + def __init__( + self, + _input=..., + status_handler=..., + event_handler=..., + artifacts_handler=..., + cancel_callback=..., + finished_callback=..., + **kwargs + ) -> None: ... + def status_callback(self, status_data): ... + def event_callback(self, event_data): ... + def artifacts_callback(self, artifacts_data): ... + def run(self): ... diff --git a/typings/ansible_runner/utils/__init__.pyi b/typings/ansible_runner/utils/__init__.pyi new file mode 100644 index 0000000..30fd53c --- /dev/null +++ b/typings/ansible_runner/utils/__init__.pyi @@ -0,0 +1,179 @@ +""" +This type stub file was generated by pyright. +""" + +import atexit +import base64 +import codecs +import fcntl +import hashlib +import json +import os +import pipes +import pwd +import re +import shutil +import signal +import stat +import subprocess +import sys +import tempfile +import threading +import uuid +from io import StringIO +from pathlib import Path + +from ansible_runner.exceptions import ConfigurationError +from six import binary_type, PY2, PY3, string_types, text_type + +def cleanup_folder(folder): # -> bool: + """Deletes folder, returns True or False based on whether a change happened.""" + ... + +def register_for_cleanup(folder): # -> None: + """ + Provide the path to a folder to make sure it is deleted when execution finishes. + The folder need not exist at the time when this is called. + """ + ... + +def get_plugin_dir(): ... +def get_callback_dir(): ... +def is_dir_owner(directory): # -> bool: + """Returns True if current user is the owner of directory""" + ... + +class Bunch: + """ + Collect a bunch of variables together in an object. + This is a slight modification of Alex Martelli's and Doug Hudgeon's Bunch pattern. + """ + + def __init__(self, **kwargs) -> None: ... + def update(self, **kwargs): ... + def get(self, key): ... + +def isplaybook(obj): # -> bool: + """ + Inspects the object and returns if it is a playbook + + Args: + obj (object): The object to be inspected by this function + + Returns: + boolean: True if the object is a list and False if it is not + """ + ... + +def isinventory(obj): # -> bool: + """ + Inspects the object and returns if it is an inventory + + Args: + obj (object): The object to be inspected by this function + + Returns: + boolean: True if the object is an inventory dict and False if it is not + """ + ... + +def check_isolation_executable_installed(isolation_executable): # -> bool: + """ + Check that process isolation executable (e.g. podman, docker, bwrap) is installed. + """ + ... + +def dump_artifact(obj, path, filename=...): # -> str | LiteralString: + """ + Write the artifact to disk at the specified path + + Args: + obj (string): The string object to be dumped to disk in the specified + path. The artifact filename will be automatically created + + path (string): The full path to the artifacts data directory. + + filename (string, optional): The name of file to write the artifact to. + If the filename is not provided, then one will be generated. + + Returns: + string: The full path filename for the artifact that was generated + """ + ... + +def cleanup_artifact_dir(path, num_keep=...): ... +def dump_artifacts(kwargs): # -> None: + """ + Introspect the kwargs and dump objects to disk + """ + ... + +def collect_new_events( + event_path, old_events +): # -> Generator[tuple[Any, Unknown], None, None]: + """ + Collect new events for the 'events' generator property + """ + ... + +class OutputEventFilter: + """ + File-like object that looks for encoded job events in stdout data. + """ + + EVENT_DATA_RE = ... + def __init__( + self, handle, event_callback, suppress_ansible_output=..., output_json=... + ) -> None: ... + def flush(self): ... + def write(self, data): ... + def close(self): ... + +def open_fifo_write(path, data): # -> None: + """open_fifo_write opens the fifo named pipe in a new thread. + This blocks the thread until an external process (such as ssh-agent) + reads data from the pipe. + """ + ... + +def args2cmdline(*args): ... +def ensure_str(s, encoding=..., errors=...): # -> bytes | str: + """ + Copied from six==1.12 + + Coerce *s* to ``str``. + + For Python 2: + + - ``unicode`` -> encoded to ``str`` + - ``str`` -> ``str`` + + For Python 3: + + - ``str`` -> ``str`` + - ``bytes`` -> decoded to ``str`` + """ + ... + +def sanitize_container_name(original_name): # -> str: + """ + Docker and podman will only accept certain characters in container names + This takes a given name from user-specified values and replaces the + invalid characters so it can be used in docker/podman CLI commands + + :param str original_name: Container name containing potentially invalid characters + """ + ... + +def cli_mounts(): ... +def sanitize_json_response(data): # -> str: + """ + Removes warning message from response message emitted by Ansible + command line utilities. + + :param str data: The string data to be sanitized + """ + ... + +def get_executable_path(name): ... +def signal_handler(): ... diff --git a/typings/ansible_runner/utils/base64io.pyi b/typings/ansible_runner/utils/base64io.pyi new file mode 100644 index 0000000..f910e88 --- /dev/null +++ b/typings/ansible_runner/utils/base64io.pyi @@ -0,0 +1,147 @@ +""" +This type stub file was generated by pyright. +""" + +import io + +"""Base64 stream with context manager support.""" +LOGGER_NAME = ... +__all__ = ("Base64IO",) +__version__ = ... +_LOGGER = ... + +class Base64IO(io.IOBase): + """Base64 stream with context manager support. + + Wraps a stream, base64-decoding read results before returning them and base64-encoding + written bytes before writing them to the stream. Instances + of this class are not reusable in order maintain consistency with the :class:`io.IOBase` + behavior on ``close()``. + + .. note:: + + Provides iterator and context manager interfaces. + + .. warning:: + + Because up to two bytes of data must be buffered to ensure correct base64 encoding + of all data written, this object **must** be closed after you are done writing to + avoid data loss. If used as a context manager, we take care of that for you. + + :param wrapped: Stream to wrap + """ + + closed = ... + def __init__(self: Base64IO, wrapped: IO) -> None: + """Check for required methods on wrapped stream and set up read buffer. + + :raises TypeError: if ``wrapped`` does not have attributes needed to determine the stream's state + """ + ... + def __enter__(self) -> Base64IO: + """Return self on enter.""" + ... + def __exit__( + self, + exc_type: Optional[Type[BaseException]], + exc_value: Optional[BaseException], + traceback: Optional[TracebackType], + ) -> Literal[False]: + """Properly close self on exit.""" + ... + def close(self) -> None: + """Close this stream, encoding and writing any buffered bytes is present. + + .. note:: + + This does **not** close the wrapped stream. + """ + ... + def writable(self) -> bool: + """Determine if the stream can be written to. + + Delegates to wrapped stream when possible. + Otherwise returns False. + + :rtype: bool + """ + ... + def readable(self) -> bool: + """Determine if the stream can be read from. + + Delegates to wrapped stream when possible. + Otherwise returns False. + + :rtype: bool + """ + ... + def flush(self) -> None: + """Flush the write buffer of the wrapped stream.""" + ... + def write(self, b: bytes) -> int: + """Base64-encode the bytes and write them to the wrapped stream. + + Any bytes that would require padding for the next write call are buffered until the + next write or close. + + .. warning:: + + Because up to two bytes of data must be buffered to ensure correct base64 encoding + of all data written, this object **must** be closed after you are done writing to + avoid data loss. If used as a context manager, we take care of that for you. + + :param bytes b: Bytes to write to wrapped stream + :raises ValueError: if called on closed Base64IO object + :raises IOError: if underlying stream is not writable + """ + ... + def writelines(self, lines: Iterable[bytes]) -> None: + """Write a list of lines. + + :param list lines: Lines to write + """ + ... + def read(self, b: int = ...) -> bytes: + """Read bytes from wrapped stream, base64-decoding before return. + + .. note:: + + The number of bytes requested from the wrapped stream is adjusted to return the + requested number of bytes after decoding returned bytes. + + :param int b: Number of bytes to read + :returns: Decoded bytes from wrapped stream + :rtype: bytes + """ + ... + def __iter__(self): # -> Self@Base64IO: + """Let this class act as an iterator.""" + ... + def readline(self, limit: int = ...) -> bytes: + """Read and return one line from the stream. + + If limit is specified, at most limit bytes will be read. + + .. note:: + + Because the source that this reads from may not contain any OEL characters, we + read "lines" in chunks of length ``io.DEFAULT_BUFFER_SIZE``. + + :param int limit: Maximum number of bytes to read + :rtype: bytes + """ + ... + def readlines(self, hint: int = ...) -> List[bytes]: + """Read and return a list of lines from the stream. + + ``hint`` can be specified to control the number of lines read: no more lines will + be read if the total size (in bytes/characters) of all lines so far exceeds hint. + + :param int hint: Number of lines to read + :returns: Lines of data + :rtype: list of bytes + """ + ... + def __next__(self) -> bytes: + """Python 3 iterator hook.""" + ... diff --git a/typings/ansible_runner/utils/capacity.pyi b/typings/ansible_runner/utils/capacity.pyi new file mode 100644 index 0000000..8bbfe70 --- /dev/null +++ b/typings/ansible_runner/utils/capacity.pyi @@ -0,0 +1,7 @@ +""" +This type stub file was generated by pyright. +""" + +def get_cpu_count(): ... +def get_mem_in_bytes(): ... +def ensure_uuid(uuid_file_path=..., mode=...): ... diff --git a/typings/ansible_runner/utils/streaming.pyi b/typings/ansible_runner/utils/streaming.pyi new file mode 100644 index 0000000..af0c777 --- /dev/null +++ b/typings/ansible_runner/utils/streaming.pyi @@ -0,0 +1,6 @@ +""" +This type stub file was generated by pyright. +""" + +def stream_dir(source_directory, stream): ... +def unstream_dir(stream, length, target_directory): ...