diff --git a/.gitignore b/.gitignore index 6b25e09..2582f0a 100644 --- a/.gitignore +++ b/.gitignore @@ -83,4 +83,4 @@ tags .tags # sphinx -documentation/_build/ +docs-test/ diff --git a/Makefile b/Makefile index df9ef2f..5e53823 100644 --- a/Makefile +++ b/Makefile @@ -37,17 +37,14 @@ $(generatedcode): VERSION ls $(generatedcode)* # generating jsonschema depends on mmif-python and pydantic -docs: mmif := $(shell grep mmif-python requirements.txt) -docs: pydantic := $(shell grep pydantic requirements.txt) -docs: VERSION $(generatedcode) - pip install --upgrade --no-input "$(mmif)" "$(pydantic)" - rm -rf docs - mkdir -p docs - python3 -m clams.appmetadata.__init__ > documentation/appmetadata.jsonschema - sphinx-build -a -b html documentation/ docs - mv documentation/appmetadata.jsonschema docs/ - touch docs/.nojekyll - echo 'sdk.clams.ai' > docs/CNAME +docs: + @echo "WARNING: The 'docs' target is deprecated and will be removed." + @echo "The 'docs' directory is no longer used. Documentation is now hosted in the central CLAMS documentation hub." + @echo "Use 'make doc' for local builds." + @echo "Nothing is done." + +doc: VERSION + python3 build-tools/docs.py package: VERSION pip install --upgrade -r requirements.dev diff --git a/build-tools/docs.py b/build-tools/docs.py new file mode 100644 index 0000000..c96fb9e --- /dev/null +++ b/build-tools/docs.py @@ -0,0 +1,84 @@ +import argparse +import subprocess +import sys +import os +import shutil +from pathlib import Path + +def run_command(command, cwd=None, check=True, env=None): + """Helper to run a shell command.""" + print(f"Running: {' '.join(str(c) for c in command)}") + result = subprocess.run(command, cwd=cwd, env=env) + if check and result.returncode != 0: + print(f"Error: Command failed with exit code {result.returncode}") + sys.exit(result.returncode) + return result + +def build_docs_local(source_dir: Path): + """ + Builds documentation for the provided source directory. + Assumes it's running in an environment with necessary tools. + """ + print("--- Running in Local Build Mode ---") + + # 1. Generate source code and install in editable mode. + print("\n--- Step 1: Installing in editable mode ---") + try: + run_command([sys.executable, "-m", "pip", "install", "-e", "."], cwd=source_dir) + # Explicitly run schema generation to be sure + run_command([sys.executable, "setup.py", "generate_schema"], cwd=source_dir) + except SystemExit: + print("Warning: 'pip install -e .' failed. This might be due to an externally managed environment.") + print("Attempting to proceed with documentation build assuming dependencies are met...") + + # 2. Install documentation-specific dependencies. + print("\n--- Step 2: Installing documentation dependencies ---") + doc_reqs = source_dir / "build-tools" / "requirements.docs.txt" + if not doc_reqs.exists(): + print(f"Error: Documentation requirements not found at {doc_reqs}") + sys.exit(1) + try: + run_command([sys.executable, "-m", "pip", "install", "-r", str(doc_reqs)]) + except SystemExit: + print("Warning: Failed to install documentation dependencies.") + # Check if sphinx-build is available + if shutil.which("sphinx-build") is None: + print("Error: 'sphinx-build' not found and installation failed.") + print("Please install dependencies manually or run this script inside a virtual environment.") + sys.exit(1) + print("Assuming dependencies are already installed...") + + # 3. Build the documentation using Sphinx. + print("\n--- Step 3: Building Sphinx documentation ---") + docs_source_dir = source_dir / "documentation" + docs_build_dir = source_dir / "docs-test" + + # Schema generation is now handled in conf.py + # schema_src = source_dir / "clams" / "appmetadata.jsonschema" + # schema_dst = docs_source_dir / "appmetadata.jsonschema" + # if schema_src.exists(): + # shutil.copy(schema_src, schema_dst) + + sphinx_command = [ + sys.executable, "-m", "sphinx.cmd.build", + str(docs_source_dir), + str(docs_build_dir), + "-b", "html", # build html + "-a", # write all files (rebuild everything) + "-E", # don't use a saved environment, reread all files + ] + run_command(sphinx_command) + + print(f"\nDocumentation build complete. Output in: {docs_build_dir}") + return docs_build_dir + +def main(): + parser = argparse.ArgumentParser( + description="Build documentation for the clams-python project." + ) + args = parser.parse_args() + + build_docs_local(Path.cwd()) + +if __name__ == "__main__": + main() diff --git a/build-tools/requirements.docs.txt b/build-tools/requirements.docs.txt new file mode 100644 index 0000000..43e71f2 --- /dev/null +++ b/build-tools/requirements.docs.txt @@ -0,0 +1,4 @@ +sphinx>=7.0,<8.0 +furo +m2r2 +sphinx-jsonschema diff --git a/clams/app/__init__.py b/clams/app/__init__.py index d6c6dcf..9f8b27b 100644 --- a/clams/app/__init__.py +++ b/clams/app/__init__.py @@ -13,10 +13,11 @@ from typing import Union, Any, Optional, Dict, List, Tuple from mmif import Mmif, Document, DocumentTypes, View +from mmif.utils.cli.describe import generate_param_hash # pytype: disable=import-error from clams.appmetadata import AppMetadata, real_valued_primitives, python_type, map_param_kv_delimiter logging.basicConfig( - level=logging.WARNING, + level=getattr(logging, os.environ.get('CLAMS_LOGLEVEL', 'WARNING').upper(), logging.WARNING), format="%(asctime)s %(name)s %(levelname)-8s %(thread)d %(message)s", datefmt="%Y-%m-%d %H:%M:%S") @@ -47,7 +48,7 @@ class ClamsApp(ABC): 'description': 'The JSON body of the HTTP response will be re-formatted with 2-space indentation', }, { - 'name': 'runningTime', 'type': 'boolean', 'choices': None, 'default': False, 'multivalued': False, + 'name': 'runningTime', 'type': 'boolean', 'choices': None, 'default': True, 'multivalued': False, 'description': 'The running time of the app will be recorded in the view metadata', }, { @@ -76,7 +77,7 @@ def __init__(self): self.metadata_param_caster = ParameterCaster(self.metadata_param_spec) self.annotate_param_caster = ParameterCaster(self.annotate_param_spec) - self.logger = logging.getLogger(self.metadata.identifier) + self.logger = logging.getLogger(str(self.metadata.identifier)) def appmetadata(self, **kwargs: List[str]) -> str: """ @@ -137,6 +138,7 @@ def annotate(self, mmif: Union[str, dict, Mmif], **runtime_params: List[str]) -> """ if not isinstance(mmif, Mmif): mmif = Mmif(mmif) + existing_view_ids = {view.id for view in mmif.views} issued_warnings = [] for key in runtime_params: if key not in self.annotate_param_spec: @@ -148,33 +150,47 @@ def annotate(self, mmif: Union[str, dict, Mmif], **runtime_params: List[str]) -> pretty = refined.get('pretty', False) t = datetime.now() with warnings.catch_warnings(record=True) as ws: - annotated = self._annotate(mmif, **refined) + annotated, cuda_profiler = self._profile_cuda_memory(self._annotate)(mmif, **refined) if ws: issued_warnings.extend(ws) if issued_warnings: warnings_view = annotated.new_view() self.sign_view(warnings_view, refined) warnings_view.metadata.warnings = issued_warnings - td = datetime.now() - t + run_id = datetime.now() + td = run_id - t runningTime = refined.get('runningTime', False) hwFetch = refined.get('hwFetch', False) runtime_recs = {} if hwFetch: + import multiprocessing import platform, shutil, subprocess - runtime_recs['architecture'] = platform.machine() - # runtime_recs['processor'] = platform.processor() # this only works on Windows + runtime_recs['cpu'] = f"{platform.machine()}, {multiprocessing.cpu_count()} cores" runtime_recs['cuda'] = [] - if shutil.which('nvidia-smi'): + # Use cuda_profiler data if available, otherwise fallback to nvidia-smi + if cuda_profiler: + for gpu_name, mem_info in cuda_profiler.items(): + total_str = self._cuda_memory_to_str(mem_info['total']) + available_str = self._cuda_memory_to_str(mem_info['available_before']) + peak_str = self._cuda_memory_to_str(mem_info['peak']) + runtime_recs['cuda'].append( + f"{gpu_name}, {total_str} total, {available_str} available, {peak_str} peak used" + ) + elif shutil.which('nvidia-smi'): for gpu in subprocess.run(['nvidia-smi', '--query-gpu=name,memory.total', '--format=csv,noheader'], stdout=subprocess.PIPE).stdout.decode('utf-8').strip().split('\n'): name, mem = gpu.split(', ') - runtime_recs['cuda'].append(f'{name} ({mem})') + runtime_recs['cuda'].append(self._cuda_device_name_concat(name, mem)) for annotated_view in annotated.views: - if annotated_view.metadata.app == self.metadata.identifier: + if annotated_view.id not in existing_view_ids and annotated_view.metadata.app == str(self.metadata.identifier): + annotated_view.metadata.timestamp = run_id + profiling_data = {} if runningTime: - annotated_view.metadata.set_additional_property('appRunningTime', str(td)) + profiling_data['runningTime'] = str(td) if len(runtime_recs) > 0: - annotated_view.metadata.set_additional_property('appRunningHardware', runtime_recs) + profiling_data['hardware'] = runtime_recs + if profiling_data: + annotated_view.metadata.set_additional_property('appProfiling', profiling_data) return annotated.serialize(pretty=pretty, sanitize=True) @@ -252,7 +268,7 @@ def sign_view(self, view: View, runtime_conf: dict) -> None: :param view: a view to sign :param runtime_conf: runtime configuration of the app as k-v pairs """ - view.metadata.app = self.metadata.identifier + view.metadata.app = str(self.metadata.identifier) params_map = {p.name: p for p in self.metadata.parameters} if self._RAW_PARAMS_KEY in runtime_conf: for k, v in runtime_conf.items(): @@ -287,7 +303,7 @@ def set_error_view(self, mmif: Union[str, dict, Mmif], **runtime_conf: List[str] mmif = Mmif(mmif) error_view: Optional[View] = None for view in reversed(mmif.views): - if view.metadata.app == self.metadata.identifier: + if view.metadata.app == str(self.metadata.identifier): error_view = view break if error_view is None: @@ -321,6 +337,237 @@ def validate_document_locations(mmif: Union[str, Mmif]) -> None: # (https://github.com/clamsproject/mmif/issues/150) , here is a good place for additional check for # file integrity + @staticmethod + def _cuda_memory_to_str(mem) -> str: + mib = mem / (1024 * 1024) + return f"{mib:.0f} MiB" # No decimal places + + @staticmethod + def _cuda_device_name_concat(name, mem): + if type(mem) in (bytes, int): + mem = ClamsApp._cuda_memory_to_str(mem) + return f"{name}, With {mem}" + + def _get_profile_path(self, param_hash: str) -> pathlib.Path: + """ + Get filesystem path for memory profile file. + + Profile files are stored in a per-app directory under user's cache. + + :param param_hash: Hash of parameters from :func:`mmif.utils.cli.describe.generate_param_hash` + :return: Path to the profile file + """ + # Sanitize app identifier for filesystem use + app_id = str(self.metadata.identifier).replace('/', '-').replace(':', '-') + cache_base = pathlib.Path(os.environ.get('XDG_CACHE_HOME', pathlib.Path.home() / '.cache')) + cache_dir = cache_base / 'clams' / 'memory_profiles' / app_id + return cache_dir / f"memory_{param_hash}.json" + + @staticmethod + def _get_available_vram() -> int: + """ + Get currently available VRAM in bytes (GPU-wide, across all processes). + + Uses nvidia-smi to get actual available memory, not just current process. + + :return: Available VRAM in bytes, or 0 if unavailable + """ + try: + import subprocess + import shutil + if shutil.which('nvidia-smi'): + # Get free memory from nvidia-smi (reports GPU-wide, not per-process) + result = subprocess.run( + ['nvidia-smi', '--query-gpu=memory.free', '--format=csv,noheader,nounits', '-i', '0'], + capture_output=True, text=True, timeout=5 + ) + if result.returncode == 0 and result.stdout.strip(): + free_mb = float(result.stdout.strip()) + return int(free_mb * 1024 * 1024) # Convert MB to bytes + except Exception: + pass + + # Fallback to torch (only sees current process memory) + try: + import torch # pytype: disable=import-error + if not torch.cuda.is_available(): + return 0 + + device = torch.cuda.current_device() + total = torch.cuda.get_device_properties(device).total_memory + used = max(torch.cuda.memory_allocated(device), + torch.cuda.memory_reserved(device)) + return total - used + except Exception: + return 0 + + def _record_vram_usage(self, parameters: dict, peak_bytes: int) -> None: + """ + Record peak memory usage to profile file. + + Uses atomic write (temp + rename) to avoid corruption from + concurrent writes. Only updates if new value is higher. + + Profile files are JSON containing: + - peak_bytes: Peak VRAM usage by the torch process + - parameters: Original parameters for human readability + + :param parameters: Request parameters (for hash and recording) + :param peak_bytes: Measured peak VRAM usage + """ + import json + + if peak_bytes <= 0: + return + + param_hash = generate_param_hash(parameters) + profile_path = self._get_profile_path(param_hash) + + try: + profile_path.parent.mkdir(parents=True, exist_ok=True) + + # Check if we should update + should_write = True + if profile_path.exists(): + try: + existing_data = json.loads(profile_path.read_text()) + existing = existing_data.get('peak_bytes', 0) + if peak_bytes <= existing: + should_write = False # Existing value is sufficient + else: + self.logger.debug( + f"Updating peak memory for {param_hash}: " + f"{self._cuda_memory_to_str(existing)} -> {self._cuda_memory_to_str(peak_bytes)}" + ) + except (ValueError, IOError, json.JSONDecodeError): + pass # Corrupted file, overwrite + + if should_write: + # Prepare profile data with original parameters for readability + # Filter out internal keys and non-serializable values + clean_params = { + k: v for k, v in parameters.items() + if k != self._RAW_PARAMS_KEY and not k.startswith('#') + } + profile_data = { + 'peak_bytes': peak_bytes, + 'parameters': clean_params + } + + # Atomic write: write to temp, then rename + temp_path = profile_path.with_suffix('.tmp') + temp_path.write_text(json.dumps(profile_data, indent=2)) + temp_path.rename(profile_path) # Atomic on POSIX + + self.logger.info( + f"Recorded peak memory for {param_hash}: " + f"{self._cuda_memory_to_str(peak_bytes)}" + ) + except Exception as e: + self.logger.warning(f"Failed to record memory profile: {e}") + + @staticmethod + def _profile_cuda_memory(func): + """ + Decorator for profiling CUDA memory usage and managing VRAM availability. + + This decorator: + 1. Checks VRAM requirements before execution (if conditions met) + 2. Rejects requests if insufficient VRAM + 3. Records peak memory usage after execution + 4. Calls empty_cache() for cleanup + + :param func: The function to wrap (typically _annotate) + :return: Decorated function that returns (result, cuda_profiler) + where cuda_profiler is dict with ", " keys + and dict values containing 'available_before' and 'peak' memory in bytes + """ + def wrapper(*args, **kwargs): + # Get the ClamsApp instance from the bound method + app_instance = getattr(func, '__self__', None) + + cuda_profiler = {} + torch_available = False + cuda_available = False + device_count = 0 + available_before = {} + + try: + import torch # pytype: disable=import-error + torch_available = True + cuda_available = torch.cuda.is_available() + device_count = torch.cuda.device_count() + except ImportError: + pass + + # Capture available VRAM before execution and reset stats + if torch_available and cuda_available: + for device_id in range(device_count): + device_id_str = f'cuda:{device_id}' + # Get GPU-wide available memory via nvidia-smi + try: + import subprocess + import shutil + if shutil.which('nvidia-smi'): + result = subprocess.run( + ['nvidia-smi', '--query-gpu=memory.free', + '--format=csv,noheader,nounits', '-i', str(device_id)], + capture_output=True, text=True, timeout=5 + ) + if result.returncode == 0 and result.stdout.strip(): + free_mb = float(result.stdout.strip()) + available_before[device_id] = int(free_mb * 1024 * 1024) + else: + # Fallback to torch (process-specific) + total = torch.cuda.get_device_properties(device_id_str).total_memory + allocated = torch.cuda.memory_allocated(device_id_str) + available_before[device_id] = total - allocated + else: + # Fallback to torch (process-specific) + total = torch.cuda.get_device_properties(device_id_str).total_memory + allocated = torch.cuda.memory_allocated(device_id_str) + available_before[device_id] = total - allocated + except Exception: + # Fallback to torch (process-specific) + total = torch.cuda.get_device_properties(device_id_str).total_memory + allocated = torch.cuda.memory_allocated(device_id_str) + available_before[device_id] = total - allocated + # Reset peak memory stats for all devices + torch.cuda.reset_peak_memory_stats('cuda') + + try: + result = func(*args, **kwargs) + + # Record peak memory usage + total_peak = 0 + if torch_available and cuda_available and device_count > 0: + for device_id in range(device_count): + device_id_str = f'cuda:{device_id}' + peak_memory = torch.cuda.max_memory_allocated(device_id_str) + total_peak = max(total_peak, peak_memory) + gpu_name = torch.cuda.get_device_name(device_id_str) + gpu_total_memory = torch.cuda.get_device_properties(device_id_str).total_memory + cuda_profiler[gpu_name] = { + 'total': gpu_total_memory, + 'available_before': available_before.get(device_id, 0), + 'peak': peak_memory + } + + # Record peak memory for future requests (if GPU app) + gpu_app = ( + hasattr(app_instance, 'metadata') and + getattr(app_instance.metadata, 'est_gpu_mem_min', 0) > 0 + ) + if gpu_app and total_peak > 0: + app_instance._record_vram_usage(kwargs, total_peak) + + return result, cuda_profiler + finally: + if torch_available and cuda_available: + torch.cuda.empty_cache() + + return wrapper + @staticmethod @contextmanager def open_document_location(document: Union[str, Document], opener: Any = open, **openerargs): diff --git a/clams/appmetadata/__init__.py b/clams/appmetadata/__init__.py index 4a7bd3c..8d0fec5 100644 --- a/clams/appmetadata/__init__.py +++ b/clams/appmetadata/__init__.py @@ -131,11 +131,6 @@ def validate_properties(cls, value): def __init__(self, **kwargs): super().__init__(**kwargs) - @pydantic.field_validator('at_type', mode='after') # because pydantic v2 doesn't auto-convert url to string - @classmethod - def stringify(cls, val): - return str(val) - @pydantic.field_validator('at_type', mode='before') @classmethod def at_type_must_be_str(cls, v): @@ -295,6 +290,10 @@ class AppMetadata(pydantic.BaseModel): None, description="(optional) Version of an analyzer software, if the app is working as a wrapper for one. " ) + analyzer_versions: Optional[Dict[str, str]] = pydantic.Field( + None, + description="(optional) Map of analyzer IDs to their versions, for apps that wrap multiple models or families." + ) app_license: str = pydantic.Field( ..., description="License information of the app." @@ -352,9 +351,20 @@ class AppMetadata(pydantic.BaseModel): "a package name and its version in the string value at the minimum (e.g., ``clams-python==1.2.3``)." ) more: Optional[Dict[str, str]] = pydantic.Field( - None, + None, description="(optional) A string-to-string map that can be used to store any additional metadata of the app." ) + est_gpu_mem_min: int = pydantic.Field( + 0, + description="(optional) Minimum GPU memory required to run the app, in megabytes (MB). " + "Set to 0 (default) if the app does not use GPU." + ) + est_gpu_mem_typ: int = pydantic.Field( + 0, + description="(optional) Typical GPU memory usage for default parameters, in megabytes (MB). " + "Must be equal or larger than est_gpu_mem_min. " + "Set to 0 (default) if the app does not use GPU." + ) model_config = { 'title': 'CLAMS AppMetadata', @@ -364,13 +374,26 @@ class AppMetadata(pydantic.BaseModel): } @pydantic.model_validator(mode='after') - @classmethod - def assign_versions(cls, data): - if data.app_version == '': - data.app_version = generate_app_version() - if data.mmif_version == '': - data.mmif_version = get_mmif_specver() - return data + def assign_versions(self): + if self.app_version == '': + self.app_version = generate_app_version() + if self.mmif_version == '': + self.mmif_version = get_mmif_specver() + return self + + @pydantic.model_validator(mode='after') + def validate_gpu_memory(self): + import warnings + if self.est_gpu_mem_typ > 0 and self.est_gpu_mem_min > 0: + if self.est_gpu_mem_typ < self.est_gpu_mem_min: + warnings.warn( + f"est_gpu_mem_typ ({self.est_gpu_mem_typ} MB) is less than " + f"est_gpu_mem_min ({self.est_gpu_mem_min} MB). " + f"Setting est_gpu_mem_typ to {self.est_gpu_mem_min} MB.", + UserWarning + ) + self.est_gpu_mem_typ = self.est_gpu_mem_min + return self @pydantic.field_validator('identifier', mode='before') @classmethod @@ -379,11 +402,7 @@ def append_version(cls, val): suffix = generate_app_version() return '/'.join(map(lambda x: x.strip('/'), filter(None, (prefix, val, suffix)))) - @pydantic.field_validator('url', 'identifier', mode='after') # because pydantic v2 doesn't auto-convert url to string - @classmethod - def stringify(cls, val): - return str(val) - + def _check_input_duplicate(self, a_input): for elem in self.input: if isinstance(elem, list): diff --git a/clams/develop/templates/app/metadata.py.template b/clams/develop/templates/app/metadata.py.template index 8b1f8c7..93aec79 100644 --- a/clams/develop/templates/app/metadata.py.template +++ b/clams/develop/templates/app/metadata.py.template @@ -39,6 +39,9 @@ def appmetadata() -> AppMetadata: # this trick can also be useful (replace ANALYZER_NAME with the pypi dist name) analyzer_version=[l.strip().rsplit('==')[-1] for l in open(pathlib.Path(__file__).parent / 'requirements.txt').readlines() if re.match(r'^ANALYZER_NAME==', l)][0], analyzer_license="", # short name for a software license + # GPU memory estimates (in MB). Set to 0 if the app does not use GPU. + est_gpu_mem_min=0, # estimated memory usage with minimal computation parameters + est_gpu_mem_typ=0, # estimated memory usage with default parameters, must be >= est_gpu_mem_min ) # and then add I/O specifications: an app must have at least one input and one output metadata.add_input(DocumentTypes.Document) diff --git a/clams/restify/__init__.py b/clams/restify/__init__.py index ad522b8..811ee4a 100644 --- a/clams/restify/__init__.py +++ b/clams/restify/__init__.py @@ -42,14 +42,55 @@ def run(self, **options): def serve_production(self, **options): """ Runs the CLAMS app as a flask webapp, using a production-ready web server (gunicorn, https://docs.gunicorn.org/en/stable/#). - + :param options: any additional options to pass to the web server. """ import gunicorn.app.base import multiprocessing + import os def number_of_workers(): - return (multiprocessing.cpu_count() * 2) + 1 # +1 to make sure at least two workers are running + # Allow override via environment variable + if 'CLAMS_GUNICORN_WORKERS' in os.environ: + return int(os.environ['CLAMS_GUNICORN_WORKERS']) + + cpu_workers = (multiprocessing.cpu_count() * 2) + 1 + + # Get GPU memory requirement from app metadata + # Use est_gpu_mem_typ (typical usage) for worker calculation + try: + metadata = self.cla.metadata + gpu_mem_mb = metadata.est_gpu_mem_typ # typical usage determines how many workers fit + except Exception: + gpu_mem_mb = 0 + + if gpu_mem_mb <= 0: + return cpu_workers + + # Calculate workers based on total VRAM of the first CUDA device (no other GPUs are considered for now) + # Use nvidia-smi instead of torch to avoid initializing CUDA in parent process before fork + try: + import subprocess + import shutil + if shutil.which('nvidia-smi'): + result = subprocess.run( + ['nvidia-smi', '--query-gpu=memory.total', '--format=csv,noheader,nounits', '-i', '0'], + capture_output=True, text=True, timeout=5 + ) + if result.returncode == 0 and result.stdout.strip(): + total_vram_mb = float(result.stdout.strip()) + vram_workers = max(1, int(total_vram_mb // gpu_mem_mb)) + workers = min(vram_workers, cpu_workers) + self.cla.logger.info( + f"GPU detected: {total_vram_mb:.0f} MB VRAM, " + f"app requires {gpu_mem_mb} MB, " + f"using {workers} workers (max {vram_workers} by VRAM, {cpu_workers} by CPU)" + ) + return workers + except Exception: + pass + + return cpu_workers class ProductionApplication(gunicorn.app.base.BaseApplication): @@ -58,9 +99,16 @@ def __init__(self, app, host, port, **options): 'bind': f'{host}:{port}', 'workers': number_of_workers(), 'threads': 2, + # disable timeout for long-running GPU workloads (default 30s is too short) + 'timeout': 0, # because the default is 'None' 'accesslog': '-', # errorlog, however, is redirected to stderr by default since 19.2, so no need to set + # log level is warning by default + 'loglevel': os.environ.get('CLAMS_LOGLEVEL', 'warning').lower(), + # default to 1 to free GPU memory after each request + # developers can override via serve_production(max_requests=N) for single-model apps + 'max_requests': 1, } self.options.update(options) self.application = app @@ -75,6 +123,13 @@ def load_config(self): def load(self): return self.application + # Log max_requests setting + max_req = options.get('max_requests', 1) # default is 1, meaning workers are killed after each request + if max_req == 0: + self.cla.logger.info("Worker recycling: disabled (workers persist)") + else: + self.cla.logger.info(f"Worker recycling: after {max_req} request(s)") + ProductionApplication(self.flask_app, self.host, self.port, **options).run() def serve_development(self, **options): diff --git a/docs/.buildinfo b/docs/.buildinfo deleted file mode 100644 index eeb916f..0000000 --- a/docs/.buildinfo +++ /dev/null @@ -1,4 +0,0 @@ -# Sphinx build info version 1 -# This file records the configuration used when building these files. When it is not found, a full rebuild will be done. -config: 6d67da2a1f4af48ce820f12e90493e11 -tags: 645f666f9bcd5a90fca523b33c5a78b7 diff --git a/docs/.doctrees/appdirectory.doctree b/docs/.doctrees/appdirectory.doctree deleted file mode 100644 index ad00ee1..0000000 Binary files a/docs/.doctrees/appdirectory.doctree and /dev/null differ diff --git a/docs/.doctrees/appmetadata.doctree b/docs/.doctrees/appmetadata.doctree deleted file mode 100644 index 3cc4052..0000000 Binary files a/docs/.doctrees/appmetadata.doctree and /dev/null differ diff --git a/docs/.doctrees/autodoc/clams.app.doctree b/docs/.doctrees/autodoc/clams.app.doctree deleted file mode 100644 index 74042e3..0000000 Binary files a/docs/.doctrees/autodoc/clams.app.doctree and /dev/null differ diff --git a/docs/.doctrees/autodoc/clams.appmetadata.doctree b/docs/.doctrees/autodoc/clams.appmetadata.doctree deleted file mode 100644 index 1d3fd89..0000000 Binary files a/docs/.doctrees/autodoc/clams.appmetadata.doctree and /dev/null differ diff --git a/docs/.doctrees/autodoc/clams.mmif_utils.doctree b/docs/.doctrees/autodoc/clams.mmif_utils.doctree deleted file mode 100644 index 042e83b..0000000 Binary files a/docs/.doctrees/autodoc/clams.mmif_utils.doctree and /dev/null differ diff --git a/docs/.doctrees/autodoc/clams.restify.doctree b/docs/.doctrees/autodoc/clams.restify.doctree deleted file mode 100644 index 1d15192..0000000 Binary files a/docs/.doctrees/autodoc/clams.restify.doctree and /dev/null differ diff --git a/docs/.doctrees/clamsapp.doctree b/docs/.doctrees/clamsapp.doctree deleted file mode 100644 index 0bba5d0..0000000 Binary files a/docs/.doctrees/clamsapp.doctree and /dev/null differ diff --git a/docs/.doctrees/cli.doctree b/docs/.doctrees/cli.doctree deleted file mode 100644 index bb18913..0000000 Binary files a/docs/.doctrees/cli.doctree and /dev/null differ diff --git a/docs/.doctrees/environment.pickle b/docs/.doctrees/environment.pickle deleted file mode 100644 index 5341720..0000000 Binary files a/docs/.doctrees/environment.pickle and /dev/null differ diff --git a/docs/.doctrees/index.doctree b/docs/.doctrees/index.doctree deleted file mode 100644 index a84f316..0000000 Binary files a/docs/.doctrees/index.doctree and /dev/null differ diff --git a/docs/.doctrees/input-output.doctree b/docs/.doctrees/input-output.doctree deleted file mode 100644 index 8467a84..0000000 Binary files a/docs/.doctrees/input-output.doctree and /dev/null differ diff --git a/docs/.doctrees/introduction.doctree b/docs/.doctrees/introduction.doctree deleted file mode 100644 index 6c4411d..0000000 Binary files a/docs/.doctrees/introduction.doctree and /dev/null differ diff --git a/docs/.doctrees/modules.doctree b/docs/.doctrees/modules.doctree deleted file mode 100644 index c517389..0000000 Binary files a/docs/.doctrees/modules.doctree and /dev/null differ diff --git a/docs/.doctrees/runtime-params.doctree b/docs/.doctrees/runtime-params.doctree deleted file mode 100644 index cd4c70b..0000000 Binary files a/docs/.doctrees/runtime-params.doctree and /dev/null differ diff --git a/docs/.doctrees/tutorial.doctree b/docs/.doctrees/tutorial.doctree deleted file mode 100644 index 587847b..0000000 Binary files a/docs/.doctrees/tutorial.doctree and /dev/null differ diff --git a/docs/.nojekyll b/docs/.nojekyll deleted file mode 100644 index e69de29..0000000 diff --git a/docs/CNAME b/docs/CNAME deleted file mode 100644 index f204280..0000000 --- a/docs/CNAME +++ /dev/null @@ -1 +0,0 @@ -sdk.clams.ai diff --git a/docs/_sources/appdirectory.rst.txt b/docs/_sources/appdirectory.rst.txt deleted file mode 100644 index 68c951b..0000000 --- a/docs/_sources/appdirectory.rst.txt +++ /dev/null @@ -1,8 +0,0 @@ -.. _appdirectory: - -CLAMS App Directory -=================== - -CLAMS App Directory is a public registry for free and open CLAMS apps, available at http://apps.clams.ai . - -To submit your app for registration on the directory, please find a submission guideline on the directory website. diff --git a/docs/_sources/appmetadata.rst.txt b/docs/_sources/appmetadata.rst.txt deleted file mode 100644 index 537c90c..0000000 --- a/docs/_sources/appmetadata.rst.txt +++ /dev/null @@ -1,236 +0,0 @@ -.. _appmetadata: - -CLAMS App Metadata -################## - -Overview -******** - -Every CLAMS app must provide information about the app itself. We call this set of information **App Metadata**. - -Format -====== - -A CLAMS App Metadata should be able to be serialized into a JSON string. - -Input/Output type specification -=============================== - -Essentially, all CLAMS apps are designed to take one MMIF file as input and produce another MMIF file as output. In this -section, we will discuss how to specify, in the App Metadata, the *semantics* of the input and output MMIF files, and -how that information should be formatted in terms of the App Metadata syntax, concretely by using ``input`` and -``output`` lists and type vocabularies where ``@type`` are defined. - - -.. note:: - CLAMS App Metadata is encoded in JSON format, but is not part of MMIF specification. - Full json schema for app metadata is available in the below section. - When an app is published to the CLAMS app directory, the app metadata will be rendered as a HTML page, with some - additional information about submission. Visit the `CLAMS app directory `_ to see how the app - metadata is rendered. - -Annotation types in MMIF ------------------------- - -As described in the `MMIF documentation `_, MMIF files can contain annotations of various types. -Currently, CLAMS team is using the following vocabularies with pre-defined annotation types: - -- `CLAMS vocabulary `_ -- `LAPPS types `_ - -Each annotation object type in the vocabularies has a unique URI that is used as the value of the ``@type`` field. -However, more important part of the type definition in the context of CLAMS app development is the ``metadata`` and -``properties`` fields. These fields provide additional information about the annotation type. Semantically, there is -no differences between a metadata field and a property field. The difference is in the intended use of the field. -a ``metadata`` field is used to provide common information about a group of annotation objects of the same type, while -a ``properties`` field is used to provide information about the individual annotation instance. In practice, metadata -fields are placed in the view metadata (``view[].metatadata.contains``) and properties fields are placed in the -annotation object itself. Because of this lack of distinction in the semantics, we will use the term "type property" to -refer to both metadata and properties in the context of annotation type (I/O) specifications in the app metadata. - -Type definitions in the vocabularies are intentionally kept minimal and underspecified. This is because the definitions -are meant to be extended by an app developers. For example, the LAPPS vocabulary defines a type called ``Token``, -primarily to represent a token in a natural language text. However, the usage of the type can be extended to represent -a sub-word token used in a machine learning model, or a minimal unit of a sign language video. If the app developer -needs to add additional information to the type definition, they can do so by adding arbitrary properties to the type -definition in action. In such a case, the app developer is expected to provide the explanation of the extended type in -the app metadata. See below for the syntax of I/O specification in the app metadata. - -Syntax for I/O specification in App Metadata --------------------------------------------- - -In the App Metadata, the input and output types are specified as lists of objects. Each object in the list should have -the following fields: - -- ``@type``: The URI of the annotation type. This field is required. -- ``description``: A human-readable description of the annotation type. This field is optional. -- ``properties``: A key-value pairs of type properties. This field is optional. -- ``required``: A boolean value indicating whether the annotation type is required as input. This field is optional and - defaults to ``true``. Not applicable for output types. - - -Simple case - using types as defined in the vocabularies -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -In the simplest case, where a developer merely re-uses an annotation type definition and pre-defined properties, an -input specification can be as simple as the following: - -.. code-block:: python - - { - # other app metadata fields, - "input": - [ - { - "@type": "http://vocab.lappsgrid.org/Token", - "properties": { - "posTagSet": "https://www.ling.upenn.edu/courses/Fall_2003/ling001/penn_treebank_pos.html" - } - } - ], - # and more app metadata fields, - } - -In the above example, the developer is declaring the app is expecting ``Token`` annotation objects, with a ``posTagSet`` -property of which value is the URL of the Penn Treebank POS tag set, *verbatim*, in the input MMIF, and all other -existing annotation types in the input MMIF will be ignored during processing. There are some *grammar* of how this -``input`` list can be written. - -- The value of a property specification can be a verbatim string, or ``"*"`` to indicate that the property can have - any value. -- If the app expects multiple types of annotations, the ``input`` field should contain multiple objects in the list. -- And if the app expects "one-of" specified types, one can specify the set of those types in a nested list. One nested - list in the input specification means one *required* type. -- And finally, if an input type is optional (i.e., ``required=false``), it indicates that the app can use extra - information from the optional annotations. In such a case, it is recommended to provide a description of differences - in the output MMIF when the extra information is available. - -For example, here is a more complex example of the simple case: - -.. code-block:: python - - { - # other app metadata fields, - "input": - [ - [ - { "@type": "https://mmif.clams.ai/vocabulary/AudioDocument/v1/" }, - { "@type": "https://mmif.clams.ai/vocabulary/VideoDocument/v1/" } - ], - { - "@type": "https://mmif.clams.ai/vocabulary/TimeFrame/v5", - "properties": { - "label": "speech", - } - "required": false - }, - ], - # and more app metadata fields, - } - -This app is a speech-to-text (automatic speech recognition) app that can take either an audio document or a video -document and transcribe the speech in the document. The app can also take a ``TimeFrame`` annotation objects with -``label="speech"`` property. When speech time frames are available, app can perform transcription only on the speech -segments, to save time and compute power. - -Another example with even more complex input specification: - -.. code-block:: python - - { - # other app metadata fields, - "input": - [ - { "@type": "https://mmif.clams.ai/vocabulary/VideoDocument/v1/" }, - [ - { - "@type": "https://mmif.clams.ai/vocabulary/TimeFrame/v5", - "properties": { - "timeUnit": "*" - "label": "slate", - } - }, - { - "@type": "https://mmif.clams.ai/vocabulary/TimeFrame/v5", - "properties": { - "timeUnit": "*" - "label": "chyron", - } - } - ] - ], - # and more app metadata fields, - } - -This is a text recognition app that can take a video document **and** ``TimeFrame`` annotations that are labels as -either ``slate`` or ``chyron``, and have ``timeUnit`` properties. The value of the ``timeUnit`` property doesn't matter, -but the input time frames must have it. - -.. note:: - Unfortunately, currently there is no way to specify optional properties within the type definition. - -Finally, let's take a look at the ``output`` specification of a scene recognition CLAMS app: - -.. code-block:: python - - { - # other app metadata fields, - "output": - [ - { - "@type": "https://mmif.clams.ai/vocabulary/TimePoint/v4/", - "description": "An individual \"still frame\"-level image classification results.", - "properties": { - "timeUnit": "milliseconds", - "labelset": ["slate", "chyron", "talking-heads-no-text"], - "classification": "*", - "label": "*" - } - } - ], - # and more app metadata fields, - } - -Note that in the actual output MMIF, more properties can be stored in the ``TimePoint`` objects. The output -specification in the app metadata is a subset of the properties to be produced that are useful for type checking -in the downstream apps, as well as for human readers to understand the output. - -Extended case - adding custom properties to the type definition -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -When the type definition is extended on the fly, developers are expected to provide the extended specification in the -form of key-value pairs in the ``properties`` field. The grammar of the JSON object does not change, but developers are -expected to provide a verbose description of the type extension in the ``description`` field. - -Runtime parameter specification -=============================== - -CLAMS apps designed to be run as HTTP servers, preferably as `stateless `_. -When accepting HTTP requests, the app should take the request data payload (body) as the input MMIF, and any exposed -configurations should be read from query strings in the URL. - -That said, the only allowed data type for users to pass as parameter values at the request time is a string. Hence, the -app developer is responsible for parsing the string values into the appropriate data types. (``clams-python`` SDK -provides some basic parsing functions, automatically called by the web framework wrapper.) At the app metadata level, -developers can specify the expected parameter data types, among ``integer``, ``number``, ``string``, ``boolean``, -``map``, and also can specify the default value of the parameter (when specified, default values should be properly -*typed*, not as strings). Noticeably, there's NO ``list`` in the available data types, and that is because a parameter -can be specified as ``multivalued=True`` to accept multiple values as a list. For details of how SDK's built-in -parameter value parsing works, please refer to the App Metadata json scheme (in the `below <#clams-app-runtime-parameter>`_ -section). - -Syntax for parameter specification in App Metadata --------------------------------------------------- - -Metadata Schema -=============== - -The schema for app metadata is as follows. -(You can also download the schema in `JSON Schema `_ format from `here `_.) - -.. jsonschema:: appmetadata.jsonschema - :lift_description: True - :lift_title: True - :lift_definitions: True - - diff --git a/docs/_sources/autodoc/clams.app.rst.txt b/docs/_sources/autodoc/clams.app.rst.txt deleted file mode 100644 index 1f18e8f..0000000 --- a/docs/_sources/autodoc/clams.app.rst.txt +++ /dev/null @@ -1,10 +0,0 @@ -clams.app package -================== - -Core package providing classes for representing CLAMS apps. - -.. automodule:: clams.app - :members: - :private-members: - :undoc-members: - :show-inheritance: diff --git a/docs/_sources/autodoc/clams.appmetadata.rst.txt b/docs/_sources/autodoc/clams.appmetadata.rst.txt deleted file mode 100644 index 1849208..0000000 --- a/docs/_sources/autodoc/clams.appmetadata.rst.txt +++ /dev/null @@ -1,19 +0,0 @@ -clams.appmetadata package -========================= - -Package providing classes for representing metadata of CLAMS apps. - -.. autoclass:: clams.appmetadata.AppMetadata - :members: - -.. autoclass:: clams.appmetadata.Input - :members: - :inherited-members: - -.. autoclass:: clams.appmetadata.Output - :members: - :inherited-members: - -.. autoclass:: clams.appmetadata.RuntimeParameter - :members: - :inherited-members: diff --git a/docs/_sources/autodoc/clams.mmif_utils.rst.txt b/docs/_sources/autodoc/clams.mmif_utils.rst.txt deleted file mode 100644 index d4f4f66..0000000 --- a/docs/_sources/autodoc/clams.mmif_utils.rst.txt +++ /dev/null @@ -1,16 +0,0 @@ -clams.mmif_utils package -======================== - -Package providing utility functions for working with MMIF data. - -``rewind`` module ------------------ - -.. automodule:: clams.mmif_utils.rewind - :members: - -``source`` module ------------------ - -.. automodule:: clams.mmif_utils.source - :members: diff --git a/docs/_sources/autodoc/clams.restify.rst.txt b/docs/_sources/autodoc/clams.restify.rst.txt deleted file mode 100644 index 6b4f47d..0000000 --- a/docs/_sources/autodoc/clams.restify.rst.txt +++ /dev/null @@ -1,9 +0,0 @@ -clams.restify package -===================== - -Package providing wrapper class for running a CLAMS app as a HTTP app. - -.. automodule:: clams.restify - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/_sources/clamsapp.md.txt b/docs/_sources/clamsapp.md.txt deleted file mode 100644 index 27d1a6e..0000000 --- a/docs/_sources/clamsapp.md.txt +++ /dev/null @@ -1,264 +0,0 @@ -## Using CLAMS App - -This document provides general instructions for installing and using CLAMS Apps. -App developers may provide additional information specific to their app, -hence it's advised also to look up the app website (or code repository) to get the additional information. - -### Requirements - -Generally, a CLAMS App requires - -- To run the app in a container (as an HTTP server), container management software such as `docker` or `podman`. This is the recommended way to use CLAMS Apps. - - (the CLAMS team is using `docker` for development and testing, hence the instructions are based on `docker` commands.) -- To run the app locally, Python3 with the `clams-python` module installed. Python 3.8 or higher is required. -- To invoke and execute analysis, HTTP client utility (such as `curl`). - -For Python dependencies, usually CLAMS Apps come with `requirements.txt` files that list up the Python library. -However, there could be other non-Python software/library that are required by the app. - -### Installation - -CLAMS Apps available on the CLAMS App Directory. Currently, all CLAMS Apps are open-source projects and are distributed as - -1. source code downloadable from code repository -1. pre-built container image - -Please visit [the app-directory](https://apps.clams.ai) to see which apps are available and where you can download them. - -In most cases, you can "install" a CLAMS App by either - -1. downloading pre-built container image directly (quick-and-easy way) -1. downloading source code from the app code repository and manually building a container image (more flexible way if you want to modify the app, or have to build for a specific HW) - -#### Download prebuilt image - -This is the quickest (and recommended) way to get started with a CLAMS App. -CLAMS apps in the App Directory come with public prebuilt container images, available in a container registry. - -``` bash -docker pull -``` - -The image name can be found on the App Directory entry of the app. - -#### Build an image from source code - -Alternatively, you can build a container image from the source code. -This is useful when you want to modify the app itself, or when you want to change the image building process to adjust to your hardware environment (e.g., specific compute engine), or additional software dependencies (e.g. [MMIF plugins](https://clams.ai/mmif-python/latest/plugins.html)). -To download the source code, you can either use `git clone` command or download a zip file from the source code repository. -The source code repository address can be found on the App Directory entry of the app. - -From the locally downloaded project directory, run the following in your terminal to build an image from the included container specification file. - -(Assuming you are using `docker` as your container manager) - -```bash -$ docker build . -f Containerfile -t -``` - -### Running CLAMS App - -CLAMS Apps are primarily designed to run as an HTTP server, but some apps written based on `clams-python` SDK additionally provide CLI equivalent to the HTTP requests. -In this session, we will first cover the usage of CLAMS apps as an HTTP server, and then cover the (optional) CLI. - -#### Starting the HTTP server as a container - -Once the image is built (by `docker build`) or downloaded (by `docker pull`), to create and start a container, run: - -```bash -$ docker run -v /path/to/data/directory:/data -p :5000 -``` - -where `/path/to/data/directory` is the local location of your media files or MMIF objects and `PORT` is the *host* port number you want your container to be listening to. -The HTTP inside the container will be listening to 5000 by default, so the second part of the `-p` argument is always `5000`. -Usually any number above 1024 is fine for the host port number, and you can use the same 5000 number for the host port number. - -The mount point for the data directory inside the container can be any path, and we used `/data` just as an example. -However, it is very important to understand that the file location in the input MMIF file must be a valid and available path inside the container (see below for more details). - -> **Note** -> If you are using a Mac, on recent versions of macOS, port 5000 is used by Airplay Receiver by default. So you may need to use a different port number, or turn off the Airplay Receiver in the System Preferences to release 5000. -> For more information on *safe* port numbers, see [IANA Port Number Registry](https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml) or [Wikipedia](https://en.wikipedia.org/wiki/List_of_TCP_and_UDP_port_numbers). - -> **Note** -> Another note for users of recent Macs with Apple Silicon (M1, M2, etc) CPU: you might see the following error message when you run the container image. -> ``` -> The requested image's platform (linux/amd64) does not match the detected host platform (linux/arm64/v8) and no specific platform was requested -> ``` -> This is because the image you are trying to run is built for Intel/AMD x64 CPUs. To force the container to run on an emulation layer, you can add `--platform linux/amd64` option to the `docker run` command. - -Additionally, you can mount a directory to `/cache/` inside the container to persist the cache data between container runs. -This is particularly handy when the app you are using downloads a fairly large pretrained model file on the first run, and you want to keep it for the next run. - -Unlike the data directory, the cache directory is not required to be mounted, but if you want to persist the cache data, you can mount a local directory to `/cache/` inside the container (fixed path). - -```bash -docker run -v /path/to/data/directory:/data -v /path/to/cache/directory:/cache -p :5000 -``` - -> **Note** -> One might be tempted bind-mount their entire local cache directory (usually `~/.cache` in Linux systems) to re-use locally downloaded model files, across different apps. -> However, doing so will expose all the cached data, not just model files, to the container. -> This can include sensitive information such as browser cache, authentication tokens, etc, hence will pose a great security risk. -> It is recommended to create a separate directory to use as a cache directory for CLAMS containers. - - -### Invoking the app server - -#### To get app metadata - -Once the app is running as an HTTP server, visit the server address ([localhost:5000](http://localhost:5000), or the remote host name if running on a remote computer) to get the app metadata. -App metadata is also available at the App Directory entry of the app if the app is published on the App Directory. -App metadata contains important information about the app that we will use in the following sections. - -#### To process input media - -To actually run the app and process input media through computational analysis, simply send a POST request to the app with a MMIF input as the request body. - -MMIF input files can be obtained from outputs of other CLAMS apps, or you can create an *empty* MMIF only with source media locations using `clams source` command. See the help message for a more detailed instructions. -(Make sure you have installed [`clams-python` package](https://pypi.org/project/clams-python/) version from PyPI.) - -```bash -$ pip install clams-python -$ clams source --help -``` - -For example; by running - -```bash -$ clams source audio:/data/audio/some-audio-file.mp3 -``` - -You will get - -```json -{ - "metadata": { - "mmif": "http://mmif.clams.ai/X.Y.Z" - }, - "documents": [ - { - "@type": "http://mmif.clams.ai/vocabulary/AudioDocument/v1", - "properties": { - "mime": "audio", - "id": "d1", - "location": "file:///data/audio/some-audio-file.mp3" - } - } - ], - "views": [] -} -``` - -If an app requires just `Document` inputs (see `input` section of the app metadata), an empty MMIF with required media file locations will suffice. -The location has to be a URL or an absolute path, and it is important to ensure that it exists. -Especially when running the app in a container, and the document location is specified as a file system path, the file must be available inside the container. -In the above, we bind-mounted `/path/to/data/directory` (host) to `/data` (container). -That is why we used `/data/audio/some-audio-file.mp3` as the location when generating this MMIF input. -So in this example, the file `/path/to/data/directory/audio/some-audio-file.mp3` must exist on the host side, so that inside the container, it can be accessed as `/data/audio/some-audio-file.mp3`. - -Some apps only works with input MMIF that already contains some annotations of specific types. To run such apps, you need to run different apps in a sequence. - -(TODO: added CLAMS workflow documentation link here.) - -When an input MMIF is ready, you can send it to the app server. -Here's an example of how to use the `curl` command, and store the response in a file `output.mmif`. - -```bash -$ clams source audio:/data/audio/some-audio-file.mp3 > input.mmif -$ curl -H "Accept: application/json" -X POST -d@input.mmif -s http://localhost:5000 > output.mmif - -# or using a bash pipeline -$ clams source audio:/data/audio/some-audio-file.mp3 | curl -X POST -d@- -s http://localhost:5000 > output.mmif -``` - -Windows PowerShell users may encounter an `Invoke-WebRequest` exception when attempting to send an input file with `curl`. -This can be resolved for the duration of the current session by using the command `remove-item alias:curl` before proceeding to use `curl`. - -#### Configuring the app - -Running as an HTTP server, CLAMS Apps are stateless, but can be configured for each HTTP request by providing configuration parameters as [query string](https://en.wikipedia.org/wiki/Query_string). - -For example, appending `?pretty=True` to the URL will result in a JSON output with indentation for better readability. - -> **Note** -> When you're using `curl` from a shell session, you need to escape the `?` or `&` characters with `\` to prevent the shell from interpreting it as a special character. - -Different apps have different configurability. For configuration parameters of an app, please refer to `parameter` section of the app metadata. - -### Using CLAMS App as a CLI program - -First and foremost, not all CLAMS Apps support command line interface (CLI). -At the minimum, a CLAMS app is required to support HTTP interfaces described in the previous section. -If any of the following instructions do not work for an app, it is likely that the app does not support CLI. - -#### Python entry points - -Apps written on `clams-python` SDK have three python entry points by default: `app.py`, `metadata.py`, and `cli.py`. - -#### `app.py`: Running app as a local HTTP server - -`app.py` is the main entry point for running the app as an HTTP server. -To run the app as a local HTTP server without containerization, you can run the following command from the source code directory. - -```bash -$ python app.py -``` - -* By default, the app will be listening to port 5000, but you can change the port number by passing `--port ` option. -* Be default, the app will be running in *debugging* mode, but you can change it to *production* mode by passing `--production` option to support larger traffic volume. -* As you might have noticed, the default `CMD` in the prebuilt containers is `python app.py --production --port 5000`. - -#### `metadata.py`: Getting app metadata - -Running `metadata.py` will print out the app metadata in JSON format. - -#### `cli.py`: Running as a CLI program - -`cli.py` is completely optional for app developers, and unlike the other two above that are guaranteed to be available, `cli.py` may not be available for some apps. -When running an app as a HTTP app, the input MMIF must be passed as POST request's body, and the output MMIF will be returned as the response body. -To mimic this behavior in a CLI, `cli.py` has two positional arguments; - -``` bash -$ python cli.py # will read INPUT_MMIF file, process it, and write the result to OUTPUT_MMIF file -``` - -`` and `` are file paths to the input and output MMIF files, respectively. -Following the common unix CLI practice, you can use `-` to represent STDIN and/or STDOUT - -``` bash -# will read from STDIN, process it, and write the result to STDOUT -$ python cli.py - - - -# or equivalently -$ python cli.py - -# read from a file, write to STDOUT -$ python cli.py input.mmif - - -# or equivalently -$ python cli.py input.mmif - -# read from STDIN, write to a file -$ cat input.mmif | python cli.py - output.mmif -``` - -As with the HTTP server, you can pass configuration parameters to the CLI program. -All parameter names are the same as the HTTP query parameters, but you need to use `--` prefix to indicate that it is a parameter. - -``` bash -$ python cli.py --pretty True input.mmif output.mmif -``` - -Finally, when running the app as a container, you can override the default `CMD` (`app.py`) by passing a `cli.py` command to the `docker run` command. - -``` bash -$ cat input.mmif | docker run -i -v /path/to/data/directory:/data python cli.py -``` - -Note that `input.mmif` is in the host machine, and the container is reading it from the STDIN. -You can also pass the input MMIF file as a volume to the container. -However, when you do so, you need to make sure that the file path in the MMIF is correctly set to the file path in the container. - -> **Note** -> Here, make sure to pass [`-i` option to the `docker run`](https://docs.docker.com/reference/cli/docker/container/run/#interactive) command to make host's STDIN work properly with the container. diff --git a/docs/_sources/cli.rst.txt b/docs/_sources/cli.rst.txt deleted file mode 100644 index 6e189bd..0000000 --- a/docs/_sources/cli.rst.txt +++ /dev/null @@ -1,15 +0,0 @@ -.. _cli: - -``clams`` shell command -======================= - -``clams-python`` comes with a command line interface (CLI) that allows you to - -#. create a new CLAMS app from a template -#. create a new MMIF file with selected source documents and an empty view - -The CLI is installed as ``clams`` shell command. To see the available commands, run - -.. code-block:: bash - - clams --help diff --git a/docs/_sources/index.rst.txt b/docs/_sources/index.rst.txt deleted file mode 100644 index 135da15..0000000 --- a/docs/_sources/index.rst.txt +++ /dev/null @@ -1,29 +0,0 @@ -Welcome to CLAMS Python SDK documentation! -========================================== - -.. mdinclude:: ../README.md - -.. toctree:: - :maxdepth: 2 - :caption: Contents - - introduction - input-output - runtime-params - appmetadata - appdirectory - cli - tutorial - -.. toctree:: - :maxdepth: 2 - :caption: API documentation: - - modules - -Indices and tables -================== - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` diff --git a/docs/_sources/input-output.rst.txt b/docs/_sources/input-output.rst.txt deleted file mode 100644 index 346b1f7..0000000 --- a/docs/_sources/input-output.rst.txt +++ /dev/null @@ -1,50 +0,0 @@ -.. _input-output: - -I/O Specification -================= - -A CLAMS app must be able to take a MMIF JSON as input as well as to return a MMIF JSON as output. MMIF is a JSON-based open source data format (with some JSON-LD components). For more details and discussions, please visit the `MMIF website `_ and the `issue tracker `_. - -To learn more about MMIF, please visit `MMIF specification documentation `_. - -``mmif`` package -^^^^^^^^^^^^^^^^^ -``mmif-python`` PyPI package comes together with the installation of ``clams-python``, and with it, you can use ``mmif`` python package. - -.. code-block:: python - - import mmif - from mmif.serialize import Mmif - new_mmif = Mmif() - # this will fail because an empty MMIF will fail to validate under the MMIF JSON schema - -Because API's of the ``mmif`` package is well documented in the `mmif-python website `_, we won't go into more details here. Please refer to the website. - -MMIF version and CLAMS apps -^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -As some parts of ``clams-python`` implementation is relying on structure of MMIF, it is possible that a MMIF file of a specific version does not work with a CLAMS app that is based on a incompatible version of ``mmif-python``. - -As we saw in the above, when using ``Mmif`` class, the MMIF JSON string is automatically validated under the MMIF JSON schema (shipped with the ``mmif-python``). -So in most cases you don't have to worry about the compatibility issue, but it is still important to understand the versioning scheme of MMIF + ``mmif-python`` and ``clams-python``, because all ``clams-python`` distributions are depending on specific versions of ``mmif-python`` as a Python library. - -.. code-block:: python - - import mmif - mmif.__specver__ - -And when an app targets a specific version, it means: - -#. The app can only process input MMIF files that are valid under the jsonschema of ``mmif-python`` version. -#. The app will output MMIF files exactly versioned as the target version. - -However, also take these into consideration - -#. Not all MMIF updates are about the jsonschema. That means, some MMIF versions share the same schema, hence the syntactic validation process can pass for differently versioned MMIF files. -#. Changing jsonschema is actually a *big* change in MMIF, and we will try to avoid it as much as possible. When it's unavoidable, we will release the new MMIF and ``mmif-python`` with the major version number incremented. - -For more information on the relation between ``mmif-python`` versions and MMIF specification versions, or MMIF version compatibility, please take time to read our decision on the subject `here `_. You can also find a table with all public ``clams-python`` packages and their target MMIF versions in the below. As we mentioned, developers don't usually need to worry about MMIF versions. - -.. csv-table:: Target Specification Versions - :file: target-versions.csv - :header-rows: 1 diff --git a/docs/_sources/introduction.rst.txt b/docs/_sources/introduction.rst.txt deleted file mode 100644 index fe3c9f6..0000000 --- a/docs/_sources/introduction.rst.txt +++ /dev/null @@ -1,159 +0,0 @@ -.. _introduction: - -Getting started -=============== - -Overview --------- - -The CLAMS project has many moving parts to make various computational analysis tools talk to each other to create customized workflows. However the most important part of the project must be the apps published for the CLAMS platform. The CLAMS Python SDK will help app developers handling MMIF data format with high-level classes and methods in Python, and publishing their code as a CLAMS app that can be easily deployed to the site via CLAMS workflow engines, such as the `CLAMS appliance `_. - -A CLAMS app can be any software that performs automated contents analysis on text, audio, and/or image/video data stream, while using `MMIF `_ as I/O format. When deployed into a CLAMS workflow engine, an app needs be running as a webapp wrapped in a container. In this documentation, we will explain what Python API's and HTTP API's an app must implement. - - -Prerequisites -------------- - -* `Python `_: the latest ``clams-python`` requires Python 3.8 or newer. We have no plan to support `Python 2.7 `_. -* Containerization software: when deployed to a CLAMS workflow engine, an app must be containerized. Developers can choose any containerization software for doing so, but ``clams-python`` SDK is developed with `Docker `_ in mind. - -Installation ------------- - -``clams-python`` distribution package is `available at PyPI `_. You can use ``pip`` to install the latest version. - -.. code-block:: bash - - pip install clams-python - -Note that installing ``clams-python`` will also install ``mmif-python`` PyPI package, which is a companion python library related to the MMIF data format. More information regarding MMIF specifications can be found `here `_. Documentation on ``mmif-python`` is available at the `Python API documentation website `_. - -Quick Start with the App Template ---------------------------------- - -``clams-python`` comes with a cookiecutter template for creating a CLAMS app. You can use it to create a new app project. - -.. code-block:: bash - - clams develop --help - -The newly created project will have a ``README.md`` file that explains how to develop and deploy the app. Here we will explain the basic structure of the project. Developing a CLAMS app has three (or four depending on the underlying analyzer) major components. - -#. (Writing computational analysis code, or use existing code in Python) -#. Make the analyzer to speak MMIF by wrapping with :class:`clams.app.ClamsApp`. -#. Make the app into a web app by wrapping with :class:`clams.restify.Restifier`. -#. Containerize the app by writing a ``Containerfile`` or ``Dockerfile``. - -CLAMS App API -^^^^^^^^^^^^^ -A CLAMS Python app is a python class that implements and exposes two core methods: ``annotate()``, ``appmetadata()``. In essence, these methods (discussed further below) are wrappers around ``_annotate()`` and ``_appmetadata()``, and they provide some common functionality such as making sure the output is serialized into a string. - -* :meth:`~clams.app.ClamsApp.annotate`: Takes a MMIF as input and processes the MMIF input, then returns serialized MMIF :class:`str`. -* :meth:`~clams.app.ClamsApp.appmetadata`: Returns JSON-formatted :class:`str` that contains metadata about the app. - -A good place to start writing a CLAMS app is to start with inheriting :class:`clams.app.ClamsApp`. Let's talk about the two methods in detail when inheriting the class. - -annotate() -"""""""""" - -The ``annotate()`` method is the core method of a CLAMS app. It takes a MMIF JSON string as the main input, along with other `kwargs `_ for runtime configurations, and analyzes the MMIF input, then returns analysis results in a serialized MMIF :class:`str`. -When you inherit :class:`~clams.app.ClamsApp`, you need to implement - -* :meth:`~clams.app.ClamsApp._annotate` instead of :meth:`~clams.app.ClamsApp.annotate` (read the docstrings as they contain important information about the app implementation). -* at a high level, ``_annotate()`` is mostly concerned with - - * finding processable documents and relevant annotations from previous views, - * creating new views, - * and calling the code that runs over the documents and inserts the results to the new views. - -As a developer you can expose different behaviors of the ``annotate()`` method by providing configurable parameters as keyword arguments of the method. For example, you can have the user specify a re-sample rate of an audio file to be analyzed by providing a ``resample_rate`` parameter. - -.. note:: - These runtime configurations are not part of the MMIF input, but for reproducible analysis, you should record these configurations in the output MMIF. - -.. note:: - There are *universal* parameters defined at the SDK-level that all CLAMS apps commonly use. See :const:`clams.app.ClamsApp.universal_parameters`. - -.. warning:: - All the runtime configurations should be pre-announced in the app metadata. - -Also see <:doc:`tutorial`> for a step-by-step tutorial on how to write the ``_annotate()`` method with a simple example NLP tool. - -appmetadata() -""""""""""""" - -App metadata is a map where important information about the app itself is stored as key-value pairs. That said, the ``appmetadata()`` method should not perform any analysis on the input MMIF. In fact, it shouldn't take any input at all. - -When using :class:`clams.app.ClamsApp`, you have different options to implement information source for the metadata. See :meth:`~clams.app.ClamsApp._load_appmetadata` for the options, and <:ref:`appmetadata`> for the metadata specification. - -.. note:: - - In the future, the app metadata will be used for automatic generation of :ref:`appdirectory`. - -HTTP webapp -^^^^^^^^^^^ -To be integrated into CLAMS workflow engines, a CLAMS app needs to serve as a webapp. Once your application class is ready, you can use :class:`clams.restify.Restifier` to wrap your app as a `Flask `_-based web application. - -.. code-block:: python - - from clams.app import ClamsApp - from clams.restify import Restifier - - class AnApp(ClamsApp): - # Implements an app that does this and that. - - if __name__ == "__main__": - app = AnApp() - webapp = Restifier(app) - webapp.run() - -When running the above code, Python will start a web server and host your CLAMS app. By default the serve will listen to ``0.0.0.0:5000``, but you can adjust hostname and port number. In this webapp, ``appmetadata`` and ``annotate`` will be respectively mapped to ``GET``, and ``POST`` to the root route. Hence, for example, you can ``POST`` a MMIF file to the web app and get a response with the annotated MMIF string in the body. - -.. note:: - Now with HTTP interface, users can pass runtime configuration as `URL query strings `_. As the values of query string parameters are always strings, ``Restifier`` will try to convert the values to the types specified in the app metadata, using :class:`clams.restify.ParameterCaster`. - -In the above example, :meth:`clams.restify.Restifier.run` will start the webapp in debug mode on a `Werkzeug `_ server, which is not always suitable for a production server. For a more robust server that can handle multiple requests asynchronously, you might want to use a production-ready HTTP server. In such a case you can use :meth:`~clams.restify.Restifier.serve_production`, which will spin up a multi-worker `Gunicorn `_ server. If you don't like it (because, for example, gunicorn does not support Windows OS), you can write your own HTTP wrapper. At the end of the day, all you need is a webapp that maps ``appmetadata`` and ``annotate`` on ``GET`` and ``POST`` requests. - -To test the behavior of the application in a Flask server, you should run the app as a webapp in a terminal (shell) session: - -.. code-block:: bash - - $ python app.py --develop --port 5000 - # default port number is 5000 - -And poke at it from a new shell session: - -.. code-block:: bash - - # in a new terminal session - $ curl http://localhost:5000/ - $ curl -H "Accept: application/json" -X POST -d@input/example-1.mmif "http://0.0.0.0:5000?pretty=True" - -The first command prints the metadata, and the second prints the output MMIF file. Appending ``?pretty=True`` to the URL will result in a pretty printed output. Note that with the ``--develop`` option we started a Flask development server. Without this option, a production server will be started. To get more information about the input file format (the contents of ``input/example-1.mmif``), please refer to `the user manual `_. - - -Containerization -^^^^^^^^^^^^^^^^ -In addition to the HTTP service, a CLAMS app is expected to be containerized for seamless deployment to CLAMS workflow engines. Also, independently from being compatible with the CLAMS platform, containerization of your app is recommended especially when your app processes video streams and is dependent on complicated system-level video processing libraries (e.g. `OpenCV `_, `FFmpeg `_). - -When you start developing an app with ``clams develop`` command, the command will create a ``Containerfile`` with some instructions as inline comments for you (you can always start from scratch with any containerization tool you like). - -.. note:: - If you are part of CLAMS team and you want to publish your app to the ``https://github.com/clamsproject`` organization, ``clams develop`` command will also create a GitHub Actions files to automatically build and push an app image to the organization's container registry. For the actions to work, you must use the name ``Containerfile`` instead of ``Dockerfile``. - -If you are not familiar with ``Containerfile`` or ``Dockerfile``, refer to the `official documentation `_ to learn how to write one. To integrate to the CLAMS workflow engines, a containerized CLAMS app must automatically start itself as a webapp when instantiated as a container, and listen to ``5000`` port. - -We have a `public GitHub Container Repository `_, and publishing Debian-based base images to help developers write ``Containerfile`` and save build time to install common libraries. At the moment we have various basic images with Python 3.8, ``clams-python``, and commonly used video and audio processing libraries installed. - -Once you finished writing your ``Containerfile``, you can build and test the containerized app locally. If you are not familiar with building and running container images To build a Docker image, these documentation will be helpful. - -* https://docs.docker.com/engine/reference/commandline/build/ -* https://docs.docker.com/engine/reference/run/ -* https://apps.clams.ai/clamsapp - - - - - - - diff --git a/docs/_sources/modules.rst.txt b/docs/_sources/modules.rst.txt deleted file mode 100644 index 7897b4a..0000000 --- a/docs/_sources/modules.rst.txt +++ /dev/null @@ -1,11 +0,0 @@ -clams package -============= -API documentation - -.. toctree:: - :maxdepth: 4 - - autodoc/clams.app - autodoc/clams.appmetadata - autodoc/clams.restify - autodoc/clams.mmif_utils diff --git a/docs/_sources/runtime-params.rst.txt b/docs/_sources/runtime-params.rst.txt deleted file mode 100644 index 1c29032..0000000 --- a/docs/_sources/runtime-params.rst.txt +++ /dev/null @@ -1,57 +0,0 @@ -.. _runtime-params: - -Runtime Configuration -===================== - -As Python API -------------- - -Using keyword arguments in the :meth:`~clams.app.ClamsApp.annotate` method, you -can make your app configurable at the runtime. - -For example, an app can be configured to use a different combination of optional -input annotation types, or to use a different unit for the output time-based -annotations. See an example below. - -.. code-block:: python - - class NamedEntityRecognizerApp(ClamsApp): - def __init__(self) - super().__init__() - self.ner_model = self._load_model() - - def annotate(self, input_mmif, **parameters) - ... # preamble to "sign" the view and prepare a new view to use - labels_to_use = parameters.get('labels', ['PERSON', 'ORG', 'GPE']) - text = self.get_text(input_mmif) - ne = self.ner_model(text) - for ent in ne: - if ent.label_ in labels_to_use: - self.add_annotation(input_mmif, ent.start_char, ent.end_char, ent.label_) - return input_mmif - - ... - -When you use a configuration parameter in your app, you should also expose it -to the user via the app metadata. See :ref:`appmetadata` section for more details. - -As HTTP Server --------------- - -When running as a HTTP server, a CLAMS app should be stateless (or always set to -default states), and all the state should be "configured" by the client for each -request, via the runtime configuration parameters we described above if necessary. -For HTTP interface, users can enter configuration values via -`query strings `_ as part of the -request URL. For example, if the user wants to use the above app as a server -with the `labels` parameter only set to ``PERSON`` and ``ORG``, then the user -can send a ``POST`` request to the server with the following URL: - -.. code-block:: bash - - http://app-server:5000?labels=PERSON&labels=ORG - -Note that for this example to work, the parameter must be specified as -``multivalued=True`` in the app metadata, so that the SDK can collect multiple -values for the same parameter name in a single python list and pass to the -``annotate()`` method. Otherwise, only the *first* value will be passed. diff --git a/docs/_sources/tutorial.md.txt b/docs/_sources/tutorial.md.txt deleted file mode 100644 index 1001fcf..0000000 --- a/docs/_sources/tutorial.md.txt +++ /dev/null @@ -1,375 +0,0 @@ -.. _tutorial: - -# Tutorial: Wrapping an NLP Application - -The following is a tutorial on how to wrap a simple NLP tool as a CLAMS application, using app template generated by `clams develop` command. -Particularly, this article is focused on writing `_annotate()` method in a CLAMS app (`app.py`). -This may not make a lot of sense without glancing over recent [MMIF specifications](https://mmif.clams.ai/) and [CLAMS SDK overview](introduction.html). - -## The NLP tool - -We use an ultra simple tokenizer as `tokenizer.py` as the example NLP tool. All it does is define a tokenize function that uses a simple regular expression and returns a list of offset pairs. - -```bash -$ cat tokenizer.py - - -import re - -__VERSION__ = 'v1' - -def tokenize(text): - return [tok.span() for tok in re.finditer("\w+", text)] - -``` - -``` python -$ python ->>> import tokenizer ->>> tokenizer.tokenize('Fido barks.') -[(0, 4), (5, 10)] -``` - -## Wrapping the tokenizer - -First, it is recommended to use `clams develop` in the command line and follow the instructions there to generate the necessary skeleton templates for developing the app. -In the rest of this tutorial, we will use `TokenizerWrapper` as the class name -(To generate a starter code with that class name, you need to use `-n tokenizer-wrapper` flag). - -By convention, all the "app" code is in a script named `app.py` (this is not a strict requirement and you can give it another name). The `app.py` script in general does several things: -1. importing the necessary code (preamble) -2. creating a subclass of `ClamsApp` that defines the metadata (`_appmetadata()`) and provides a method to run the wrapped tool (`_annotate()`) -3. providing a way to run the code as a HTTP Flask server (`__main__` block). - -The `clams develop` cookie cutter will generate the third part. Thus, the first and second parts of the code are explained here. - - -### Imports - -Aside from a few standard modules we need the following imports: - -```python -from clams import ClamsApp, Restifier -from mmif import Mmif, View, Annotation, Document, AnnotationTypes, DocumentTypes - -# For an NLP tool we need to import the LAPPS vocabulary items -from lapps.discriminators import Uri -# --- came from the starter code - -import tokenizer # THIS MEANS you put the `tokenizer.py` in the same directory with `app.py` -``` - -For non-NLP CLAMS applications we would also do `from mmif.vocabulary import AnnotationTypes`, but this is not needed for NLP applications because they do not need the CLAMS vocabulary. What we do need to import are the URIs of all LAPPS annotation types and the NLP tool itself. - -> *Note* -> MMIF uses LAPPS vocabulary for linguistic annotation types. - -Importing `lapps.discriminators.Uri` is for convenience since it gives us easy access to the URIs of annotation types and some of their attributes. The following code prints a list of available variables that point to URIs: - -``` python ->>> from lapps.discriminators import Uri ->>> attrs = [x for x in dir(Uri) if not x.startswith('__')] ->>> attrs = [a for a in attrs if not getattr(Uri, a).find('org/ns') > -1] ->>> print(' '.join(attrs)) -ANNOTATION CHUNK CONSTITUENT COREF DATE DEPENDENCY DEPENDENCY_STRUCTURE DOCUMENT GENERIC_RELATION LEMMA LOCATION LOOKUP MARKABLE MATCHES NCHUNK NE ORGANIZATION PARAGRAPH PERSON PHRASE_STRUCTURE POS RELATION SEMANTIC_ROLE SENTENCE TOKEN VCHUNK ->>> print(Uri.TOKEN) -http://vocab.lappsgrid.org/Token -``` - -### The application class - -With the imports in place we define a subclass of `ClamsApp` which needs two methods: - -```python -class TokenizerWrapper(ClamsApp): - def _appmetadata(self): pass - - def _annotate(self, mmif): pass -``` - -Here it is useful to introduce some background. The CLAMS HTTP API connects the GET and POST requests to the `appmetdata()` and `annotate()` methods on the app respectively, and those methods are both defined in `ClamsApp`. In essence, they are wrappers around `_appmetadata()` and `_annotate()` and provide some common functionality like making sure the output is serialized into a string. - - -#### App Metadata - -The `_appmetadata()` method should return an `AppMetadata` object that defines the relevant metadata for the app: - -(If you are using the app template, use `metadata.py` instead of `app.py` to define the metadata.) - -```python -APP_LICENSE = 'Apache 2.0' -TOKENIZER_LICENSE = 'Apache 2.0' -TOKENIZER_VERSION = tokenizer.__VERSION__ - -def _appmetadata(self): - metadata = AppMetadata( - identifier='tokenizer-wrapper', - name="Tokenizer Wrapper", - url='https://github.com/clamsproject/app-nlp-example', - description="Apply simple tokenization to all text documents in a MMIF file.", - app_license=APP_LICENSE, - analyzer_version=TOKENIZER_VERSION, - analyzer_license=TOKENIZER_LICENSE, - ) - metadata.add_input(DocumentTypes.TextDocument) - metadata.add_output(Uri.TOKEN) - metadata.add_parameter('error', 'Throw error if set to True', 'boolean') - metadata.add_parameter('eol', 'Insert sentence boundaries', 'boolean') - return metadata -``` - -> **Warning** -> When using the separately generated `metadata.py` created via `clams develop`, this method within `app.py` should be left empty with a `pass` statement as shown below: - -> ```python -> def _appmetadata(self): -> # When using the ``metadata.py`` leave this do-nothing "pass" method here. -> pass -> ``` - -> And the `appmetadata()` within `metadata.py` should be implemented instead. Follow the instructions in the template. - -As we see in the code above, the `AppMetadata` object is created with the following fields: -identifier, name, url, description, app_license, analyzer_version, and analyzer_license. If you used `clams develop` to generate the app template, you'll also notice that some of these fields are already filled in for you based on the `-n` argument you provided. - -More interesting are the `add_input()`, `add_output()`, and `add_parameter()` parts. -The `add_input()` method is used to specify the input annotation type(s) that the app expects. Here, we specify that the app just expects text documents. -The `add_output()` method is used to specify the output annotation type(s) that the app produces. -So our first CLAMS app will take text documents as input and produce token annotations as output. -Note that the I/O types must be specified using the URIs. We are using URIs defined in CLAMS and LAPPS vocabularies, but as long as the type is defined somewhere, any URI can be used. - -Finally, the `add_parameter()` method is used to specify the parameters that the app accepts. -For usage of these parameters, you'll find [Runtime Configuration](runtime-params) page helpful. -Now, as a developer, you can specify the parameters that the app accepts, so the users can set these parameters when running the app, using `add_parameter()` method. -Here, we are defining two parameters: `error` and `eol`, and in addition to the name of parameter, we also specify the description and the type of the parameter. With the desciption and the type, the parameters should be pretty self-explanatory to the user. -One think to note in this code snippet is that both parameters don't have default values. -This means that if the user doesn't specify the value for these parameters at the runtime, the app will not run and throw an error. -If you want to make a parameter "optional" by providing a default value, you can do so by adding a `default` argument to the `add_parameter()` method. - -> **Note** -> Also refer to [CLAMS App Metadata](https://sdk.clams.ai/appmetadata.html) for more details regarding what fields need to be specified. - -#### `_annotate()` -The `_annotate()` method should accept a MMIF file/string/object as its first parameter and always returns a `MMIF` object with an additional `view` containing annotation results. This is where the bulk of your logic will go. For a text processing app, it is mostly concerned with finding text documents, calling the code that runs over the text, creating new views and inserting the results. - -In addition to the input MMIF, this method can accept any number of keyword arguments, which are the parameters set by the user/caller. Note that when this method is called inside the `annotate()` public method in the `ClamsApp` class (which is the usual case when running as a CLAMS app), the keyword arguments are automatically "refined" before being passed here. The refinement includes - -1. inserting "default" values for parameters that are not set by the user -2. checking that the values are of the correct type and value, based on the parameter specification in the app metadata - -```python -def _annotate(self, mmif, **parameters): - # then, access the parameters: here to just print - # them and to willy-nilly throw an error if the caller wants that - for arg, val in parameters.items(): - print(f"Parameter {arg}={val}") - # as we defined this `error` parameter in the app metadata - if arg == 'error' and val is True: - raise Exception(f"Exception - {parameters['error']}") - # Initialize the MMIF object from the string if needed - self.mmif = mmif if isinstance(mmif, Mmif) else Mmif(mmif) - # process the text documents in the documents list - for doc in self.mmif.get_documents_by_type(DocumentTypes.TextDocument): - # prepare a new _View_ object to store output annotations - new_view = self._new_view(doc.long_id, parameters) # continue reading to see what `long_id` does - # _run_nlp_tool() is the method that does the actual work - self._run_nlp_tool(doc, new_view) - # return the MMIF object - return self.mmif -``` - -For language processing applications, one task is to retrieve all text documents from both the documents list and the views. Annotations generated by the NLP tool need to be anchored to the text documents, which in the case of text documents in the documents list is done by using the text document identifier, but for text documents in views we also need the view identifier. A view may have many text documents and typically all annotations created will be put in one view. - -For each text document from the document list, there is one invocation of `_new_view()` which gets handed a document identifier, so it can be put in the view metadata. And for each view with text documents there is also one invocation of `_new_view()`, but no document identifier is handed in so the identifier will not be put into the view metadata. - -The method `_run_nlp_tool()` is responsible for running the NLP tool and adding annotations to the new view. The third argument allows us to anchor annotations created by the tool by handing over the document identifier, possibly prefixed by the view the document lives in. - -One thing about `_annotate()` as it is defined above is that it will most likely be the same for each NLP application, all the application specific details are in the code that creates new views and the code that adds annotations. - -##### Creating a new view: - -```python -def _new_view(self, docid, runtime_config): - view = self.mmif.new_view() - view.metadata.app = self.metadata.identifier - # first thing you need to do after creating a new view is "sign" the view - # the sign_view() method will record the app's identifier and the timestamp - # as well as the user parameter inputs. This is important for reproducibility. - self.sign_view(view, runtime_config) - # then record what annotations you want to create in this view - view.new_contain(Uri.TOKEN, document=docid) - return view -``` - -This is the simplest NLP view possible since there is only one annotation type, and it has no metadata properties beyond the `document` property. Other applications may have more annotation types, which results in repeated invocations of `new_contain()`, and may define other metadata properties for those types. - -##### Adding annotations: - -```python -def _run_nlp_tool(self, doc, new_view): - """ - Run the NLP tool over the document and add annotations to the view, using the - full document identifier (which may include a view identifier) for the document - property. - """ - text = doc.text_value - tokens = tokenizer.tokenize(text) - for p1, p2 in tokens: - a = new_view.new_annotation(Uri.TOKEN) - # `long_id` will give you the annotation object's ID, prefixed by its parents view's ID (if it has one) - # so that when the targeting document is in a different view, we can still have back references - a.add_property('document', doc.long_id) - a.add_property('start', p1) - a.add_property('end', p2) - a.add_property('word', text[p1:p2]) - # see what properties are required / available in the LAPPS vocabulary https://vocab.lappsgrid.org/Token -``` - -First, with `text_value` we get the text from the text document, either from its `location` property or from its `text` property. Second, we apply the tokenizer to the text. And third, we loop over the token offsets in the tokenizer result and create annotations of type `Uri.TOKEN` with an identifier that is automatically generated by the SDK. All that is needed for adding an annotation is the `new_annotation()` method on the view object and the `add_property()` method on the annotation object. - -## Containerization with Docker - -Apps within CLAMS typically run as Flask servers in Docker containers, and after an app is tested as a local Flask application, it should be containerized. In fact, in some cases we don't even bother running a local Flask server and move straight to the container set up. - -Three configuration files for building a container image should be automatically generated through the `clams develop` command: - -| file | description | -| ---------------- | :----------------------------------------------------------- | -| Containerfile | Describes how to create a container image for this application. | -| .dockerignore | Specifies which files are not needed for running this application. | -| requirements.txt | File with all Python modules that need to be installed. | - -Here is the minimal Containerfile included with this example: - -```dockerfile -# make sure to use a specific version number here -FROM ghcr.io/clamsproject/clams-python:x.y.z -COPY ./ /app -WORKDIR ./app - -CMD ["python3", "app.py", "--production"] -``` - -This starts from the base CLAMS image which is created from an official Python image (Debian-based) with the clams-python package and the code it depends on added. The Containerfile only needs to be edited if additional installations are required to run the NLP tool. In that case the Containerfile will have a few more lines: - -```dockerfile -FROM ghcr.io/clamsproject/clams-python:x.y.z -RUN apt install -y -WORKDIR ./app -COPY ./requirements.txt . -RUN pip3 install --no-cache-dir -r requirements.txt # no-cache-dir will save some space and reduce final image size -COPY ./ ./ -CMD ["python3", "app.py"] -``` - -> **Note** -> `Containerfile` in the app starter template has more pre-configuration lines. - -With this Containerfile you typically only need to make changes to the requirements file for additional python installs. - -This repository also includes a `.dockerignore` file. Editing it is optional, but with large repositories with lots of documentation and images you may want to add some file paths just to keep the image as small as possible. - -Use one of the following commands to build the image, the first one builds an image with a production server using Gunicorn, the second one builds a development server using Flask. - -```bash -$ docker build -t clams-nlp-example:0.0.1 -f Containerfile . -``` - -The `-t` option lets you pick a name and a tag for the image. You can use another name if you like. You do not have to add a tag and you could just use `-t nlp-clams-example`, but it is usually a good idea to use the version name as the tag. -The `-f` option lets you specify a different Containerfile. If you do not specify a file then `docker` will look for a file called `Dockerfile` in the current directory (Note that in this tutorial we're using `Containerfile` as the name, not `Dockerfile`). - -> **Note** -> For full details on the `docker build` command see the [docker-build documentation](https://docs.docker.com/reference/cli/docker/image/build/). - -To test the Flask app in the container from your local machine do - -```bash -$ docker run --name clams-nlp-example --rm -d -p 4000:5000 clams-nlp-example:0.0.1 -``` - -There are a lot going on in this command, so let's break it down: -`--name clams-nlp-example`: This is the name of the container. You can use any name you like, but it has to be unique. You will need this name to stop the container later if you run it with `-d` (see below). -`--rm`: This option tells Docker to remove the container when it is stopped. -`-d`: This option tells Docker to run the container in the background. If you leave this option out you will see the output of the container in your terminal. -`-p 5000:5000`: This option tells Docker to map port 5000 on the container to port 4000 on your local machine. This is the port that the Flask server is running on in the container. -`clams-nlp-example:0.0.1`: This is the name and tag of the image that you want to run in a container. - -> **Note** -> For full details on the `docker run` command see the [docker-run documentation](https://docs.docker.com/reference/cli/docker/run/). - -Now you can call the server with curl command from your terminal, but first you need an input MMIF file. - -``` json -{ - "metadata": { - "mmif": "http://mmif.clams.ai/0.4.0" - }, - "documents": [ - { - "@type": "http://mmif.clams.ai/0.4.0/vocabulary/TextDocument", - "properties": { - "id": "m2", - "text": { - "@value": "Hello, this is Jim Lehrer with the NewsHour on PBS. In the nineteen eighties, barking dogs have increasingly become a problem in urban areas." - } - } - } - ], - "views": [] -} -``` - -Save this as `example.mmif` and run the following curl command: - -```bash -$ curl http://localhost:5000/ -$ curl -X POST -d@example.mmif http://localhost:5000/ -``` - -### Using the `document.location` property - -Typically `TextDocument` in a MMIF use the location property to point to a text file. This will not work with the setup laid out above because that's dependent on having a local path on your machine and the container has no access to that path. What you need to do is to make sure that the container can see the data on your local machine and you can use the `-v` option for that: - -```bash -$ docker run --name clams-nlp-example --rm -d -p 5000:5000 -v $PWD/input/data:/data clams-nlp-example:0.0.1 -``` - -We now have specified that the `/data ` directory on the container is a mount of the `./input/data` directory in the "host" machine. Given that `./input/data` contains a `example.txt` text file, now you need to make sure that the input MMIF file uses the path on the container: - -```json -{ - "@type": "http://mmif.clams.ai/vocabulary/TextDocument/v1", - "properties": { - "id": "m1", - "mime": "text/plain", - "location": "file:///data/text/example.txt" - } -} -``` -To generate a MMIF file like this, you can use `clams source` command from your shell. - -```bash -$ clams source --prefix /data/input text:example.txt -{ - "metadata": { - "mmif": "http://mmif.clams.ai/1.0.0" - }, - "documents": [ - { - "@type": "http://mmif.clams.ai/vocabulary/TextDocument/v1", - "properties": { - "mime": "text", - "id": "d1", - "location": "file:///data/input/example.txt" - } - } - ], - "views": [] -} -``` - -And now you can use curl again - -```bash -$ curl -X POST -d@input/example-3.mmif http://0.0.0.0:5000/ -``` diff --git a/docs/_static/_sphinx_javascript_frameworks_compat.js b/docs/_static/_sphinx_javascript_frameworks_compat.js deleted file mode 100644 index 8141580..0000000 --- a/docs/_static/_sphinx_javascript_frameworks_compat.js +++ /dev/null @@ -1,123 +0,0 @@ -/* Compatability shim for jQuery and underscores.js. - * - * Copyright Sphinx contributors - * Released under the two clause BSD licence - */ - -/** - * small helper function to urldecode strings - * - * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURIComponent#Decoding_query_parameters_from_a_URL - */ -jQuery.urldecode = function(x) { - if (!x) { - return x - } - return decodeURIComponent(x.replace(/\+/g, ' ')); -}; - -/** - * small helper function to urlencode strings - */ -jQuery.urlencode = encodeURIComponent; - -/** - * This function returns the parsed url parameters of the - * current request. Multiple values per key are supported, - * it will always return arrays of strings for the value parts. - */ -jQuery.getQueryParameters = function(s) { - if (typeof s === 'undefined') - s = document.location.search; - var parts = s.substr(s.indexOf('?') + 1).split('&'); - var result = {}; - for (var i = 0; i < parts.length; i++) { - var tmp = parts[i].split('=', 2); - var key = jQuery.urldecode(tmp[0]); - var value = jQuery.urldecode(tmp[1]); - if (key in result) - result[key].push(value); - else - result[key] = [value]; - } - return result; -}; - -/** - * highlight a given string on a jquery object by wrapping it in - * span elements with the given class name. - */ -jQuery.fn.highlightText = function(text, className) { - function highlight(node, addItems) { - if (node.nodeType === 3) { - var val = node.nodeValue; - var pos = val.toLowerCase().indexOf(text); - if (pos >= 0 && - !jQuery(node.parentNode).hasClass(className) && - !jQuery(node.parentNode).hasClass("nohighlight")) { - var span; - var isInSVG = jQuery(node).closest("body, svg, foreignObject").is("svg"); - if (isInSVG) { - span = document.createElementNS("http://www.w3.org/2000/svg", "tspan"); - } else { - span = document.createElement("span"); - span.className = className; - } - span.appendChild(document.createTextNode(val.substr(pos, text.length))); - node.parentNode.insertBefore(span, node.parentNode.insertBefore( - document.createTextNode(val.substr(pos + text.length)), - node.nextSibling)); - node.nodeValue = val.substr(0, pos); - if (isInSVG) { - var rect = document.createElementNS("http://www.w3.org/2000/svg", "rect"); - var bbox = node.parentElement.getBBox(); - rect.x.baseVal.value = bbox.x; - rect.y.baseVal.value = bbox.y; - rect.width.baseVal.value = bbox.width; - rect.height.baseVal.value = bbox.height; - rect.setAttribute('class', className); - addItems.push({ - "parent": node.parentNode, - "target": rect}); - } - } - } - else if (!jQuery(node).is("button, select, textarea")) { - jQuery.each(node.childNodes, function() { - highlight(this, addItems); - }); - } - } - var addItems = []; - var result = this.each(function() { - highlight(this, addItems); - }); - for (var i = 0; i < addItems.length; ++i) { - jQuery(addItems[i].parent).before(addItems[i].target); - } - return result; -}; - -/* - * backward compatibility for jQuery.browser - * This will be supported until firefox bug is fixed. - */ -if (!jQuery.browser) { - jQuery.uaMatch = function(ua) { - ua = ua.toLowerCase(); - - var match = /(chrome)[ \/]([\w.]+)/.exec(ua) || - /(webkit)[ \/]([\w.]+)/.exec(ua) || - /(opera)(?:.*version|)[ \/]([\w.]+)/.exec(ua) || - /(msie) ([\w.]+)/.exec(ua) || - ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec(ua) || - []; - - return { - browser: match[ 1 ] || "", - version: match[ 2 ] || "0" - }; - }; - jQuery.browser = {}; - jQuery.browser[jQuery.uaMatch(navigator.userAgent).browser] = true; -} diff --git a/docs/_static/basic.css b/docs/_static/basic.css deleted file mode 100644 index 7ebbd6d..0000000 --- a/docs/_static/basic.css +++ /dev/null @@ -1,914 +0,0 @@ -/* - * Sphinx stylesheet -- basic theme. - */ - -/* -- main layout ----------------------------------------------------------- */ - -div.clearer { - clear: both; -} - -div.section::after { - display: block; - content: ''; - clear: left; -} - -/* -- relbar ---------------------------------------------------------------- */ - -div.related { - width: 100%; - font-size: 90%; -} - -div.related h3 { - display: none; -} - -div.related ul { - margin: 0; - padding: 0 0 0 10px; - list-style: none; -} - -div.related li { - display: inline; -} - -div.related li.right { - float: right; - margin-right: 5px; -} - -/* -- sidebar --------------------------------------------------------------- */ - -div.sphinxsidebarwrapper { - padding: 10px 5px 0 10px; -} - -div.sphinxsidebar { - float: left; - width: 230px; - margin-left: -100%; - font-size: 90%; - word-wrap: break-word; - overflow-wrap : break-word; -} - -div.sphinxsidebar ul { - list-style: none; -} - -div.sphinxsidebar ul ul, -div.sphinxsidebar ul.want-points { - margin-left: 20px; - list-style: square; -} - -div.sphinxsidebar ul ul { - margin-top: 0; - margin-bottom: 0; -} - -div.sphinxsidebar form { - margin-top: 10px; -} - -div.sphinxsidebar input { - border: 1px solid #98dbcc; - font-family: sans-serif; - font-size: 1em; -} - -div.sphinxsidebar #searchbox form.search { - overflow: hidden; -} - -div.sphinxsidebar #searchbox input[type="text"] { - float: left; - width: 80%; - padding: 0.25em; - box-sizing: border-box; -} - -div.sphinxsidebar #searchbox input[type="submit"] { - float: left; - width: 20%; - border-left: none; - padding: 0.25em; - box-sizing: border-box; -} - - -img { - border: 0; - max-width: 100%; -} - -/* -- search page ----------------------------------------------------------- */ - -ul.search { - margin-top: 10px; -} - -ul.search li { - padding: 5px 0; -} - -ul.search li a { - font-weight: bold; -} - -ul.search li p.context { - color: #888; - margin: 2px 0 0 30px; - text-align: left; -} - -ul.keywordmatches li.goodmatch a { - font-weight: bold; -} - -/* -- index page ------------------------------------------------------------ */ - -table.contentstable { - width: 90%; - margin-left: auto; - margin-right: auto; -} - -table.contentstable p.biglink { - line-height: 150%; -} - -a.biglink { - font-size: 1.3em; -} - -span.linkdescr { - font-style: italic; - padding-top: 5px; - font-size: 90%; -} - -/* -- general index --------------------------------------------------------- */ - -table.indextable { - width: 100%; -} - -table.indextable td { - text-align: left; - vertical-align: top; -} - -table.indextable ul { - margin-top: 0; - margin-bottom: 0; - list-style-type: none; -} - -table.indextable > tbody > tr > td > ul { - padding-left: 0em; -} - -table.indextable tr.pcap { - height: 10px; -} - -table.indextable tr.cap { - margin-top: 10px; - background-color: #f2f2f2; -} - -img.toggler { - margin-right: 3px; - margin-top: 3px; - cursor: pointer; -} - -div.modindex-jumpbox { - border-top: 1px solid #ddd; - border-bottom: 1px solid #ddd; - margin: 1em 0 1em 0; - padding: 0.4em; -} - -div.genindex-jumpbox { - border-top: 1px solid #ddd; - border-bottom: 1px solid #ddd; - margin: 1em 0 1em 0; - padding: 0.4em; -} - -/* -- domain module index --------------------------------------------------- */ - -table.modindextable td { - padding: 2px; - border-collapse: collapse; -} - -/* -- general body styles --------------------------------------------------- */ - -div.body { - min-width: 360px; - max-width: 800px; -} - -div.body p, div.body dd, div.body li, div.body blockquote { - -moz-hyphens: auto; - -ms-hyphens: auto; - -webkit-hyphens: auto; - hyphens: auto; -} - -a.headerlink { - visibility: hidden; -} - -a:visited { - color: #551A8B; -} - -h1:hover > a.headerlink, -h2:hover > a.headerlink, -h3:hover > a.headerlink, -h4:hover > a.headerlink, -h5:hover > a.headerlink, -h6:hover > a.headerlink, -dt:hover > a.headerlink, -caption:hover > a.headerlink, -p.caption:hover > a.headerlink, -div.code-block-caption:hover > a.headerlink { - visibility: visible; -} - -div.body p.caption { - text-align: inherit; -} - -div.body td { - text-align: left; -} - -.first { - margin-top: 0 !important; -} - -p.rubric { - margin-top: 30px; - font-weight: bold; -} - -img.align-left, figure.align-left, .figure.align-left, object.align-left { - clear: left; - float: left; - margin-right: 1em; -} - -img.align-right, figure.align-right, .figure.align-right, object.align-right { - clear: right; - float: right; - margin-left: 1em; -} - -img.align-center, figure.align-center, .figure.align-center, object.align-center { - display: block; - margin-left: auto; - margin-right: auto; -} - -img.align-default, figure.align-default, .figure.align-default { - display: block; - margin-left: auto; - margin-right: auto; -} - -.align-left { - text-align: left; -} - -.align-center { - text-align: center; -} - -.align-default { - text-align: center; -} - -.align-right { - text-align: right; -} - -/* -- sidebars -------------------------------------------------------------- */ - -div.sidebar, -aside.sidebar { - margin: 0 0 0.5em 1em; - border: 1px solid #ddb; - padding: 7px; - background-color: #ffe; - width: 40%; - float: right; - clear: right; - overflow-x: auto; -} - -p.sidebar-title { - font-weight: bold; -} - -nav.contents, -aside.topic, -div.admonition, div.topic, blockquote { - clear: left; -} - -/* -- topics ---------------------------------------------------------------- */ - -nav.contents, -aside.topic, -div.topic { - border: 1px solid #ccc; - padding: 7px; - margin: 10px 0 10px 0; -} - -p.topic-title { - font-size: 1.1em; - font-weight: bold; - margin-top: 10px; -} - -/* -- admonitions ----------------------------------------------------------- */ - -div.admonition { - margin-top: 10px; - margin-bottom: 10px; - padding: 7px; -} - -div.admonition dt { - font-weight: bold; -} - -p.admonition-title { - margin: 0px 10px 5px 0px; - font-weight: bold; -} - -div.body p.centered { - text-align: center; - margin-top: 25px; -} - -/* -- content of sidebars/topics/admonitions -------------------------------- */ - -div.sidebar > :last-child, -aside.sidebar > :last-child, -nav.contents > :last-child, -aside.topic > :last-child, -div.topic > :last-child, -div.admonition > :last-child { - margin-bottom: 0; -} - -div.sidebar::after, -aside.sidebar::after, -nav.contents::after, -aside.topic::after, -div.topic::after, -div.admonition::after, -blockquote::after { - display: block; - content: ''; - clear: both; -} - -/* -- tables ---------------------------------------------------------------- */ - -table.docutils { - margin-top: 10px; - margin-bottom: 10px; - border: 0; - border-collapse: collapse; -} - -table.align-center { - margin-left: auto; - margin-right: auto; -} - -table.align-default { - margin-left: auto; - margin-right: auto; -} - -table caption span.caption-number { - font-style: italic; -} - -table caption span.caption-text { -} - -table.docutils td, table.docutils th { - padding: 1px 8px 1px 5px; - border-top: 0; - border-left: 0; - border-right: 0; - border-bottom: 1px solid #aaa; -} - -th { - text-align: left; - padding-right: 5px; -} - -table.citation { - border-left: solid 1px gray; - margin-left: 1px; -} - -table.citation td { - border-bottom: none; -} - -th > :first-child, -td > :first-child { - margin-top: 0px; -} - -th > :last-child, -td > :last-child { - margin-bottom: 0px; -} - -/* -- figures --------------------------------------------------------------- */ - -div.figure, figure { - margin: 0.5em; - padding: 0.5em; -} - -div.figure p.caption, figcaption { - padding: 0.3em; -} - -div.figure p.caption span.caption-number, -figcaption span.caption-number { - font-style: italic; -} - -div.figure p.caption span.caption-text, -figcaption span.caption-text { -} - -/* -- field list styles ----------------------------------------------------- */ - -table.field-list td, table.field-list th { - border: 0 !important; -} - -.field-list ul { - margin: 0; - padding-left: 1em; -} - -.field-list p { - margin: 0; -} - -.field-name { - -moz-hyphens: manual; - -ms-hyphens: manual; - -webkit-hyphens: manual; - hyphens: manual; -} - -/* -- hlist styles ---------------------------------------------------------- */ - -table.hlist { - margin: 1em 0; -} - -table.hlist td { - vertical-align: top; -} - -/* -- object description styles --------------------------------------------- */ - -.sig { - font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace; -} - -.sig-name, code.descname { - background-color: transparent; - font-weight: bold; -} - -.sig-name { - font-size: 1.1em; -} - -code.descname { - font-size: 1.2em; -} - -.sig-prename, code.descclassname { - background-color: transparent; -} - -.optional { - font-size: 1.3em; -} - -.sig-paren { - font-size: larger; -} - -.sig-param.n { - font-style: italic; -} - -/* C++ specific styling */ - -.sig-inline.c-texpr, -.sig-inline.cpp-texpr { - font-family: unset; -} - -.sig.c .k, .sig.c .kt, -.sig.cpp .k, .sig.cpp .kt { - color: #0033B3; -} - -.sig.c .m, -.sig.cpp .m { - color: #1750EB; -} - -.sig.c .s, .sig.c .sc, -.sig.cpp .s, .sig.cpp .sc { - color: #067D17; -} - - -/* -- other body styles ----------------------------------------------------- */ - -ol.arabic { - list-style: decimal; -} - -ol.loweralpha { - list-style: lower-alpha; -} - -ol.upperalpha { - list-style: upper-alpha; -} - -ol.lowerroman { - list-style: lower-roman; -} - -ol.upperroman { - list-style: upper-roman; -} - -:not(li) > ol > li:first-child > :first-child, -:not(li) > ul > li:first-child > :first-child { - margin-top: 0px; -} - -:not(li) > ol > li:last-child > :last-child, -:not(li) > ul > li:last-child > :last-child { - margin-bottom: 0px; -} - -ol.simple ol p, -ol.simple ul p, -ul.simple ol p, -ul.simple ul p { - margin-top: 0; -} - -ol.simple > li:not(:first-child) > p, -ul.simple > li:not(:first-child) > p { - margin-top: 0; -} - -ol.simple p, -ul.simple p { - margin-bottom: 0; -} - -aside.footnote > span, -div.citation > span { - float: left; -} -aside.footnote > span:last-of-type, -div.citation > span:last-of-type { - padding-right: 0.5em; -} -aside.footnote > p { - margin-left: 2em; -} -div.citation > p { - margin-left: 4em; -} -aside.footnote > p:last-of-type, -div.citation > p:last-of-type { - margin-bottom: 0em; -} -aside.footnote > p:last-of-type:after, -div.citation > p:last-of-type:after { - content: ""; - clear: both; -} - -dl.field-list { - display: grid; - grid-template-columns: fit-content(30%) auto; -} - -dl.field-list > dt { - font-weight: bold; - word-break: break-word; - padding-left: 0.5em; - padding-right: 5px; -} - -dl.field-list > dd { - padding-left: 0.5em; - margin-top: 0em; - margin-left: 0em; - margin-bottom: 0em; -} - -dl { - margin-bottom: 15px; -} - -dd > :first-child { - margin-top: 0px; -} - -dd ul, dd table { - margin-bottom: 10px; -} - -dd { - margin-top: 3px; - margin-bottom: 10px; - margin-left: 30px; -} - -.sig dd { - margin-top: 0px; - margin-bottom: 0px; -} - -.sig dl { - margin-top: 0px; - margin-bottom: 0px; -} - -dl > dd:last-child, -dl > dd:last-child > :last-child { - margin-bottom: 0; -} - -dt:target, span.highlighted { - background-color: #fbe54e; -} - -rect.highlighted { - fill: #fbe54e; -} - -dl.glossary dt { - font-weight: bold; - font-size: 1.1em; -} - -.versionmodified { - font-style: italic; -} - -.system-message { - background-color: #fda; - padding: 5px; - border: 3px solid red; -} - -.footnote:target { - background-color: #ffa; -} - -.line-block { - display: block; - margin-top: 1em; - margin-bottom: 1em; -} - -.line-block .line-block { - margin-top: 0; - margin-bottom: 0; - margin-left: 1.5em; -} - -.guilabel, .menuselection { - font-family: sans-serif; -} - -.accelerator { - text-decoration: underline; -} - -.classifier { - font-style: oblique; -} - -.classifier:before { - font-style: normal; - margin: 0 0.5em; - content: ":"; - display: inline-block; -} - -abbr, acronym { - border-bottom: dotted 1px; - cursor: help; -} - -.translated { - background-color: rgba(207, 255, 207, 0.2) -} - -.untranslated { - background-color: rgba(255, 207, 207, 0.2) -} - -/* -- code displays --------------------------------------------------------- */ - -pre { - overflow: auto; - overflow-y: hidden; /* fixes display issues on Chrome browsers */ -} - -pre, div[class*="highlight-"] { - clear: both; -} - -span.pre { - -moz-hyphens: none; - -ms-hyphens: none; - -webkit-hyphens: none; - hyphens: none; - white-space: nowrap; -} - -div[class*="highlight-"] { - margin: 1em 0; -} - -td.linenos pre { - border: 0; - background-color: transparent; - color: #aaa; -} - -table.highlighttable { - display: block; -} - -table.highlighttable tbody { - display: block; -} - -table.highlighttable tr { - display: flex; -} - -table.highlighttable td { - margin: 0; - padding: 0; -} - -table.highlighttable td.linenos { - padding-right: 0.5em; -} - -table.highlighttable td.code { - flex: 1; - overflow: hidden; -} - -.highlight .hll { - display: block; -} - -div.highlight pre, -table.highlighttable pre { - margin: 0; -} - -div.code-block-caption + div { - margin-top: 0; -} - -div.code-block-caption { - margin-top: 1em; - padding: 2px 5px; - font-size: small; -} - -div.code-block-caption code { - background-color: transparent; -} - -table.highlighttable td.linenos, -span.linenos, -div.highlight span.gp { /* gp: Generic.Prompt */ - user-select: none; - -webkit-user-select: text; /* Safari fallback only */ - -webkit-user-select: none; /* Chrome/Safari */ - -moz-user-select: none; /* Firefox */ - -ms-user-select: none; /* IE10+ */ -} - -div.code-block-caption span.caption-number { - padding: 0.1em 0.3em; - font-style: italic; -} - -div.code-block-caption span.caption-text { -} - -div.literal-block-wrapper { - margin: 1em 0; -} - -code.xref, a code { - background-color: transparent; - font-weight: bold; -} - -h1 code, h2 code, h3 code, h4 code, h5 code, h6 code { - background-color: transparent; -} - -.viewcode-link { - float: right; -} - -.viewcode-back { - float: right; - font-family: sans-serif; -} - -div.viewcode-block:target { - margin: -1px -10px; - padding: 0 10px; -} - -/* -- math display ---------------------------------------------------------- */ - -img.math { - vertical-align: middle; -} - -div.body div.math p { - text-align: center; -} - -span.eqno { - float: right; -} - -span.eqno a.headerlink { - position: absolute; - z-index: 1; -} - -div.math:hover a.headerlink { - visibility: visible; -} - -/* -- printout stylesheet --------------------------------------------------- */ - -@media print { - div.document, - div.documentwrapper, - div.bodywrapper { - margin: 0 !important; - width: 100%; - } - - div.sphinxsidebar, - div.related, - div.footer, - #top-link { - display: none; - } -} \ No newline at end of file diff --git a/docs/_static/css/badge_only.css b/docs/_static/css/badge_only.css deleted file mode 100644 index 88ba55b..0000000 --- a/docs/_static/css/badge_only.css +++ /dev/null @@ -1 +0,0 @@ -.clearfix{*zoom:1}.clearfix:after,.clearfix:before{display:table;content:""}.clearfix:after{clear:both}@font-face{font-family:FontAwesome;font-style:normal;font-weight:400;src:url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713?#iefix) format("embedded-opentype"),url(fonts/fontawesome-webfont.woff2?af7ae505a9eed503f8b8e6982036873e) format("woff2"),url(fonts/fontawesome-webfont.woff?fee66e712a8a08eef5805a46892932ad) format("woff"),url(fonts/fontawesome-webfont.ttf?b06871f281fee6b241d60582ae9369b9) format("truetype"),url(fonts/fontawesome-webfont.svg?912ec66d7572ff821749319396470bde#FontAwesome) format("svg")}.fa:before{font-family:FontAwesome;font-style:normal;font-weight:400;line-height:1}.fa:before,a .fa{text-decoration:inherit}.fa:before,a .fa,li .fa{display:inline-block}li .fa-large:before{width:1.875em}ul.fas{list-style-type:none;margin-left:2em;text-indent:-.8em}ul.fas li .fa{width:.8em}ul.fas li .fa-large:before{vertical-align:baseline}.fa-book:before,.icon-book:before{content:"\f02d"}.fa-caret-down:before,.icon-caret-down:before{content:"\f0d7"}.fa-caret-up:before,.icon-caret-up:before{content:"\f0d8"}.fa-caret-left:before,.icon-caret-left:before{content:"\f0d9"}.fa-caret-right:before,.icon-caret-right:before{content:"\f0da"}.rst-versions{position:fixed;bottom:0;left:0;width:300px;color:#fcfcfc;background:#1f1d1d;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;z-index:400}.rst-versions a{color:#2980b9;text-decoration:none}.rst-versions .rst-badge-small{display:none}.rst-versions .rst-current-version{padding:12px;background-color:#272525;display:block;text-align:right;font-size:90%;cursor:pointer;color:#27ae60}.rst-versions .rst-current-version:after{clear:both;content:"";display:block}.rst-versions .rst-current-version .fa{color:#fcfcfc}.rst-versions .rst-current-version .fa-book,.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version.rst-out-of-date{background-color:#e74c3c;color:#fff}.rst-versions .rst-current-version.rst-active-old-version{background-color:#f1c40f;color:#000}.rst-versions.shift-up{height:auto;max-height:100%;overflow-y:scroll}.rst-versions.shift-up .rst-other-versions{display:block}.rst-versions .rst-other-versions{font-size:90%;padding:12px;color:grey;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:1px solid #413d3d}.rst-versions .rst-other-versions dd{display:inline-block;margin:0}.rst-versions .rst-other-versions dd a{display:inline-block;padding:6px;color:#fcfcfc}.rst-versions .rst-other-versions .rtd-current-item{font-weight:700}.rst-versions.rst-badge{width:auto;bottom:20px;right:20px;left:auto;border:none;max-width:300px;max-height:90%}.rst-versions.rst-badge .fa-book,.rst-versions.rst-badge .icon-book{float:none;line-height:30px}.rst-versions.rst-badge.shift-up .rst-current-version{text-align:right}.rst-versions.rst-badge.shift-up .rst-current-version .fa-book,.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge>.rst-current-version{width:auto;height:30px;line-height:30px;padding:0 6px;display:block;text-align:center}@media screen and (max-width:768px){.rst-versions{width:85%;display:none}.rst-versions.shift{display:block}}#flyout-search-form{padding:6px} \ No newline at end of file diff --git a/docs/_static/css/fonts/Roboto-Slab-Bold.woff b/docs/_static/css/fonts/Roboto-Slab-Bold.woff deleted file mode 100644 index 6cb6000..0000000 Binary files a/docs/_static/css/fonts/Roboto-Slab-Bold.woff and /dev/null differ diff --git a/docs/_static/css/fonts/Roboto-Slab-Bold.woff2 b/docs/_static/css/fonts/Roboto-Slab-Bold.woff2 deleted file mode 100644 index 7059e23..0000000 Binary files a/docs/_static/css/fonts/Roboto-Slab-Bold.woff2 and /dev/null differ diff --git a/docs/_static/css/fonts/Roboto-Slab-Regular.woff b/docs/_static/css/fonts/Roboto-Slab-Regular.woff deleted file mode 100644 index f815f63..0000000 Binary files a/docs/_static/css/fonts/Roboto-Slab-Regular.woff and /dev/null differ diff --git a/docs/_static/css/fonts/Roboto-Slab-Regular.woff2 b/docs/_static/css/fonts/Roboto-Slab-Regular.woff2 deleted file mode 100644 index f2c76e5..0000000 Binary files a/docs/_static/css/fonts/Roboto-Slab-Regular.woff2 and /dev/null differ diff --git a/docs/_static/css/fonts/fontawesome-webfont.eot b/docs/_static/css/fonts/fontawesome-webfont.eot deleted file mode 100644 index e9f60ca..0000000 Binary files a/docs/_static/css/fonts/fontawesome-webfont.eot and /dev/null differ diff --git a/docs/_static/css/fonts/fontawesome-webfont.svg b/docs/_static/css/fonts/fontawesome-webfont.svg deleted file mode 100644 index 855c845..0000000 --- a/docs/_static/css/fonts/fontawesome-webfont.svg +++ /dev/null @@ -1,2671 +0,0 @@ - - - - -Created by FontForge 20120731 at Mon Oct 24 17:37:40 2016 - By ,,, -Copyright Dave Gandy 2016. All rights reserved. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/_static/css/fonts/fontawesome-webfont.ttf b/docs/_static/css/fonts/fontawesome-webfont.ttf deleted file mode 100644 index 35acda2..0000000 Binary files a/docs/_static/css/fonts/fontawesome-webfont.ttf and /dev/null differ diff --git a/docs/_static/css/fonts/fontawesome-webfont.woff b/docs/_static/css/fonts/fontawesome-webfont.woff deleted file mode 100644 index 400014a..0000000 Binary files a/docs/_static/css/fonts/fontawesome-webfont.woff and /dev/null differ diff --git a/docs/_static/css/fonts/fontawesome-webfont.woff2 b/docs/_static/css/fonts/fontawesome-webfont.woff2 deleted file mode 100644 index 4d13fc6..0000000 Binary files a/docs/_static/css/fonts/fontawesome-webfont.woff2 and /dev/null differ diff --git a/docs/_static/css/fonts/lato-bold-italic.woff b/docs/_static/css/fonts/lato-bold-italic.woff deleted file mode 100644 index 88ad05b..0000000 Binary files a/docs/_static/css/fonts/lato-bold-italic.woff and /dev/null differ diff --git a/docs/_static/css/fonts/lato-bold-italic.woff2 b/docs/_static/css/fonts/lato-bold-italic.woff2 deleted file mode 100644 index c4e3d80..0000000 Binary files a/docs/_static/css/fonts/lato-bold-italic.woff2 and /dev/null differ diff --git a/docs/_static/css/fonts/lato-bold.woff b/docs/_static/css/fonts/lato-bold.woff deleted file mode 100644 index c6dff51..0000000 Binary files a/docs/_static/css/fonts/lato-bold.woff and /dev/null differ diff --git a/docs/_static/css/fonts/lato-bold.woff2 b/docs/_static/css/fonts/lato-bold.woff2 deleted file mode 100644 index bb19504..0000000 Binary files a/docs/_static/css/fonts/lato-bold.woff2 and /dev/null differ diff --git a/docs/_static/css/fonts/lato-normal-italic.woff b/docs/_static/css/fonts/lato-normal-italic.woff deleted file mode 100644 index 76114bc..0000000 Binary files a/docs/_static/css/fonts/lato-normal-italic.woff and /dev/null differ diff --git a/docs/_static/css/fonts/lato-normal-italic.woff2 b/docs/_static/css/fonts/lato-normal-italic.woff2 deleted file mode 100644 index 3404f37..0000000 Binary files a/docs/_static/css/fonts/lato-normal-italic.woff2 and /dev/null differ diff --git a/docs/_static/css/fonts/lato-normal.woff b/docs/_static/css/fonts/lato-normal.woff deleted file mode 100644 index ae1307f..0000000 Binary files a/docs/_static/css/fonts/lato-normal.woff and /dev/null differ diff --git a/docs/_static/css/fonts/lato-normal.woff2 b/docs/_static/css/fonts/lato-normal.woff2 deleted file mode 100644 index 3bf9843..0000000 Binary files a/docs/_static/css/fonts/lato-normal.woff2 and /dev/null differ diff --git a/docs/_static/css/theme.css b/docs/_static/css/theme.css deleted file mode 100644 index 0f14f10..0000000 --- a/docs/_static/css/theme.css +++ /dev/null @@ -1,4 +0,0 @@ -html{box-sizing:border-box}*,:after,:before{box-sizing:inherit}article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block}audio,canvas,video{display:inline-block;*display:inline;*zoom:1}[hidden],audio:not([controls]){display:none}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}blockquote{margin:0}dfn{font-style:italic}ins{background:#ff9;text-decoration:none}ins,mark{color:#000}mark{background:#ff0;font-style:italic;font-weight:700}.rst-content code,.rst-content tt,code,kbd,pre,samp{font-family:monospace,serif;_font-family:courier new,monospace;font-size:1em}pre{white-space:pre}q{quotes:none}q:after,q:before{content:"";content:none}small{font-size:85%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}dl,ol,ul{margin:0;padding:0;list-style:none;list-style-image:none}li{list-style:none}dd{margin:0}img{border:0;-ms-interpolation-mode:bicubic;vertical-align:middle;max-width:100%}svg:not(:root){overflow:hidden}figure,form{margin:0}label{cursor:pointer}button,input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}button,input{line-height:normal}button,input[type=button],input[type=reset],input[type=submit]{cursor:pointer;-webkit-appearance:button;*overflow:visible}button[disabled],input[disabled]{cursor:default}input[type=search]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}textarea{resize:vertical}table{border-collapse:collapse;border-spacing:0}td{vertical-align:top}.chromeframe{margin:.2em 0;background:#ccc;color:#000;padding:.2em 0}.ir{display:block;border:0;text-indent:-999em;overflow:hidden;background-color:transparent;background-repeat:no-repeat;text-align:left;direction:ltr;*line-height:0}.ir br{display:none}.hidden{display:none!important;visibility:hidden}.visuallyhidden{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.visuallyhidden.focusable:active,.visuallyhidden.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.invisible{visibility:hidden}.relative{position:relative}big,small{font-size:100%}@media print{body,html,section{background:none!important}*{box-shadow:none!important;text-shadow:none!important;filter:none!important;-ms-filter:none!important}a,a:visited{text-decoration:underline}.ir a:after,a[href^="#"]:after,a[href^="javascript:"]:after{content:""}blockquote,pre{page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}img{max-width:100%!important}@page{margin:.5cm}.rst-content .toctree-wrapper>p.caption,h2,h3,p{orphans:3;widows:3}.rst-content .toctree-wrapper>p.caption,h2,h3{page-break-after:avoid}}.btn,.fa:before,.icon:before,.rst-content .admonition,.rst-content .admonition-title:before,.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .code-block-caption .headerlink:before,.rst-content .danger,.rst-content .eqno .headerlink:before,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .note,.rst-content .seealso,.rst-content .tip,.rst-content .warning,.rst-content code.download span:first-child:before,.rst-content dl dt .headerlink:before,.rst-content h1 .headerlink:before,.rst-content h2 .headerlink:before,.rst-content h3 .headerlink:before,.rst-content h4 .headerlink:before,.rst-content h5 .headerlink:before,.rst-content h6 .headerlink:before,.rst-content p.caption .headerlink:before,.rst-content p .headerlink:before,.rst-content table>caption .headerlink:before,.rst-content tt.download span:first-child:before,.wy-alert,.wy-dropdown .caret:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-menu-vertical li.current>a button.toctree-expand:before,.wy-menu-vertical li.on a button.toctree-expand:before,.wy-menu-vertical li button.toctree-expand:before,input[type=color],input[type=date],input[type=datetime-local],input[type=datetime],input[type=email],input[type=month],input[type=number],input[type=password],input[type=search],input[type=tel],input[type=text],input[type=time],input[type=url],input[type=week],select,textarea{-webkit-font-smoothing:antialiased}.clearfix{*zoom:1}.clearfix:after,.clearfix:before{display:table;content:""}.clearfix:after{clear:both}/*! - * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome - * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) - */@font-face{font-family:FontAwesome;src:url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713);src:url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713?#iefix&v=4.7.0) format("embedded-opentype"),url(fonts/fontawesome-webfont.woff2?af7ae505a9eed503f8b8e6982036873e) format("woff2"),url(fonts/fontawesome-webfont.woff?fee66e712a8a08eef5805a46892932ad) format("woff"),url(fonts/fontawesome-webfont.ttf?b06871f281fee6b241d60582ae9369b9) format("truetype"),url(fonts/fontawesome-webfont.svg?912ec66d7572ff821749319396470bde#fontawesomeregular) format("svg");font-weight:400;font-style:normal}.fa,.icon,.rst-content .admonition-title,.rst-content .code-block-caption .headerlink,.rst-content .eqno .headerlink,.rst-content code.download span:first-child,.rst-content dl dt .headerlink,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content p.caption .headerlink,.rst-content p .headerlink,.rst-content table>caption .headerlink,.rst-content tt.download span:first-child,.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand,.wy-menu-vertical li button.toctree-expand{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14286em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14286em;width:2.14286em;top:.14286em;text-align:center}.fa-li.fa-lg{left:-1.85714em}.fa-border{padding:.2em .25em .15em;border:.08em solid #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa-pull-left.icon,.fa.fa-pull-left,.rst-content .code-block-caption .fa-pull-left.headerlink,.rst-content .eqno .fa-pull-left.headerlink,.rst-content .fa-pull-left.admonition-title,.rst-content code.download span.fa-pull-left:first-child,.rst-content dl dt .fa-pull-left.headerlink,.rst-content h1 .fa-pull-left.headerlink,.rst-content h2 .fa-pull-left.headerlink,.rst-content h3 .fa-pull-left.headerlink,.rst-content h4 .fa-pull-left.headerlink,.rst-content h5 .fa-pull-left.headerlink,.rst-content h6 .fa-pull-left.headerlink,.rst-content p .fa-pull-left.headerlink,.rst-content table>caption .fa-pull-left.headerlink,.rst-content tt.download span.fa-pull-left:first-child,.wy-menu-vertical li.current>a button.fa-pull-left.toctree-expand,.wy-menu-vertical li.on a button.fa-pull-left.toctree-expand,.wy-menu-vertical li button.fa-pull-left.toctree-expand{margin-right:.3em}.fa-pull-right.icon,.fa.fa-pull-right,.rst-content .code-block-caption .fa-pull-right.headerlink,.rst-content .eqno .fa-pull-right.headerlink,.rst-content .fa-pull-right.admonition-title,.rst-content code.download span.fa-pull-right:first-child,.rst-content dl dt .fa-pull-right.headerlink,.rst-content h1 .fa-pull-right.headerlink,.rst-content h2 .fa-pull-right.headerlink,.rst-content h3 .fa-pull-right.headerlink,.rst-content h4 .fa-pull-right.headerlink,.rst-content h5 .fa-pull-right.headerlink,.rst-content h6 .fa-pull-right.headerlink,.rst-content p .fa-pull-right.headerlink,.rst-content table>caption .fa-pull-right.headerlink,.rst-content tt.download span.fa-pull-right:first-child,.wy-menu-vertical li.current>a button.fa-pull-right.toctree-expand,.wy-menu-vertical li.on a button.fa-pull-right.toctree-expand,.wy-menu-vertical li button.fa-pull-right.toctree-expand{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left,.pull-left.icon,.rst-content .code-block-caption .pull-left.headerlink,.rst-content .eqno .pull-left.headerlink,.rst-content .pull-left.admonition-title,.rst-content code.download span.pull-left:first-child,.rst-content dl dt .pull-left.headerlink,.rst-content h1 .pull-left.headerlink,.rst-content h2 .pull-left.headerlink,.rst-content h3 .pull-left.headerlink,.rst-content h4 .pull-left.headerlink,.rst-content h5 .pull-left.headerlink,.rst-content h6 .pull-left.headerlink,.rst-content p .pull-left.headerlink,.rst-content table>caption .pull-left.headerlink,.rst-content tt.download span.pull-left:first-child,.wy-menu-vertical li.current>a button.pull-left.toctree-expand,.wy-menu-vertical li.on a button.pull-left.toctree-expand,.wy-menu-vertical li button.pull-left.toctree-expand{margin-right:.3em}.fa.pull-right,.pull-right.icon,.rst-content .code-block-caption .pull-right.headerlink,.rst-content .eqno .pull-right.headerlink,.rst-content .pull-right.admonition-title,.rst-content code.download span.pull-right:first-child,.rst-content dl dt .pull-right.headerlink,.rst-content h1 .pull-right.headerlink,.rst-content h2 .pull-right.headerlink,.rst-content h3 .pull-right.headerlink,.rst-content h4 .pull-right.headerlink,.rst-content h5 .pull-right.headerlink,.rst-content h6 .pull-right.headerlink,.rst-content p .pull-right.headerlink,.rst-content table>caption .pull-right.headerlink,.rst-content tt.download span.pull-right:first-child,.wy-menu-vertical li.current>a button.pull-right.toctree-expand,.wy-menu-vertical li.on a button.pull-right.toctree-expand,.wy-menu-vertical li button.pull-right.toctree-expand{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s linear infinite;animation:fa-spin 2s linear infinite}.fa-pulse{-webkit-animation:fa-spin 1s steps(8) infinite;animation:fa-spin 1s steps(8) infinite}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scaleX(-1);-ms-transform:scaleX(-1);transform:scaleX(-1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scaleY(-1);-ms-transform:scaleY(-1);transform:scaleY(-1)}:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:""}.fa-music:before{content:""}.fa-search:before,.icon-search:before{content:""}.fa-envelope-o:before{content:""}.fa-heart:before{content:""}.fa-star:before{content:""}.fa-star-o:before{content:""}.fa-user:before{content:""}.fa-film:before{content:""}.fa-th-large:before{content:""}.fa-th:before{content:""}.fa-th-list:before{content:""}.fa-check:before{content:""}.fa-close:before,.fa-remove:before,.fa-times:before{content:""}.fa-search-plus:before{content:""}.fa-search-minus:before{content:""}.fa-power-off:before{content:""}.fa-signal:before{content:""}.fa-cog:before,.fa-gear:before{content:""}.fa-trash-o:before{content:""}.fa-home:before,.icon-home:before{content:""}.fa-file-o:before{content:""}.fa-clock-o:before{content:""}.fa-road:before{content:""}.fa-download:before,.rst-content code.download span:first-child:before,.rst-content tt.download span:first-child:before{content:""}.fa-arrow-circle-o-down:before{content:""}.fa-arrow-circle-o-up:before{content:""}.fa-inbox:before{content:""}.fa-play-circle-o:before{content:""}.fa-repeat:before,.fa-rotate-right:before{content:""}.fa-refresh:before{content:""}.fa-list-alt:before{content:""}.fa-lock:before{content:""}.fa-flag:before{content:""}.fa-headphones:before{content:""}.fa-volume-off:before{content:""}.fa-volume-down:before{content:""}.fa-volume-up:before{content:""}.fa-qrcode:before{content:""}.fa-barcode:before{content:""}.fa-tag:before{content:""}.fa-tags:before{content:""}.fa-book:before,.icon-book:before{content:""}.fa-bookmark:before{content:""}.fa-print:before{content:""}.fa-camera:before{content:""}.fa-font:before{content:""}.fa-bold:before{content:""}.fa-italic:before{content:""}.fa-text-height:before{content:""}.fa-text-width:before{content:""}.fa-align-left:before{content:""}.fa-align-center:before{content:""}.fa-align-right:before{content:""}.fa-align-justify:before{content:""}.fa-list:before{content:""}.fa-dedent:before,.fa-outdent:before{content:""}.fa-indent:before{content:""}.fa-video-camera:before{content:""}.fa-image:before,.fa-photo:before,.fa-picture-o:before{content:""}.fa-pencil:before{content:""}.fa-map-marker:before{content:""}.fa-adjust:before{content:""}.fa-tint:before{content:""}.fa-edit:before,.fa-pencil-square-o:before{content:""}.fa-share-square-o:before{content:""}.fa-check-square-o:before{content:""}.fa-arrows:before{content:""}.fa-step-backward:before{content:""}.fa-fast-backward:before{content:""}.fa-backward:before{content:""}.fa-play:before{content:""}.fa-pause:before{content:""}.fa-stop:before{content:""}.fa-forward:before{content:""}.fa-fast-forward:before{content:""}.fa-step-forward:before{content:""}.fa-eject:before{content:""}.fa-chevron-left:before{content:""}.fa-chevron-right:before{content:""}.fa-plus-circle:before{content:""}.fa-minus-circle:before{content:""}.fa-times-circle:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before{content:""}.fa-check-circle:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before{content:""}.fa-question-circle:before{content:""}.fa-info-circle:before{content:""}.fa-crosshairs:before{content:""}.fa-times-circle-o:before{content:""}.fa-check-circle-o:before{content:""}.fa-ban:before{content:""}.fa-arrow-left:before{content:""}.fa-arrow-right:before{content:""}.fa-arrow-up:before{content:""}.fa-arrow-down:before{content:""}.fa-mail-forward:before,.fa-share:before{content:""}.fa-expand:before{content:""}.fa-compress:before{content:""}.fa-plus:before{content:""}.fa-minus:before{content:""}.fa-asterisk:before{content:""}.fa-exclamation-circle:before,.rst-content .admonition-title:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before{content:""}.fa-gift:before{content:""}.fa-leaf:before{content:""}.fa-fire:before,.icon-fire:before{content:""}.fa-eye:before{content:""}.fa-eye-slash:before{content:""}.fa-exclamation-triangle:before,.fa-warning:before{content:""}.fa-plane:before{content:""}.fa-calendar:before{content:""}.fa-random:before{content:""}.fa-comment:before{content:""}.fa-magnet:before{content:""}.fa-chevron-up:before{content:""}.fa-chevron-down:before{content:""}.fa-retweet:before{content:""}.fa-shopping-cart:before{content:""}.fa-folder:before{content:""}.fa-folder-open:before{content:""}.fa-arrows-v:before{content:""}.fa-arrows-h:before{content:""}.fa-bar-chart-o:before,.fa-bar-chart:before{content:""}.fa-twitter-square:before{content:""}.fa-facebook-square:before{content:""}.fa-camera-retro:before{content:""}.fa-key:before{content:""}.fa-cogs:before,.fa-gears:before{content:""}.fa-comments:before{content:""}.fa-thumbs-o-up:before{content:""}.fa-thumbs-o-down:before{content:""}.fa-star-half:before{content:""}.fa-heart-o:before{content:""}.fa-sign-out:before{content:""}.fa-linkedin-square:before{content:""}.fa-thumb-tack:before{content:""}.fa-external-link:before{content:""}.fa-sign-in:before{content:""}.fa-trophy:before{content:""}.fa-github-square:before{content:""}.fa-upload:before{content:""}.fa-lemon-o:before{content:""}.fa-phone:before{content:""}.fa-square-o:before{content:""}.fa-bookmark-o:before{content:""}.fa-phone-square:before{content:""}.fa-twitter:before{content:""}.fa-facebook-f:before,.fa-facebook:before{content:""}.fa-github:before,.icon-github:before{content:""}.fa-unlock:before{content:""}.fa-credit-card:before{content:""}.fa-feed:before,.fa-rss:before{content:""}.fa-hdd-o:before{content:""}.fa-bullhorn:before{content:""}.fa-bell:before{content:""}.fa-certificate:before{content:""}.fa-hand-o-right:before{content:""}.fa-hand-o-left:before{content:""}.fa-hand-o-up:before{content:""}.fa-hand-o-down:before{content:""}.fa-arrow-circle-left:before,.icon-circle-arrow-left:before{content:""}.fa-arrow-circle-right:before,.icon-circle-arrow-right:before{content:""}.fa-arrow-circle-up:before{content:""}.fa-arrow-circle-down:before{content:""}.fa-globe:before{content:""}.fa-wrench:before{content:""}.fa-tasks:before{content:""}.fa-filter:before{content:""}.fa-briefcase:before{content:""}.fa-arrows-alt:before{content:""}.fa-group:before,.fa-users:before{content:""}.fa-chain:before,.fa-link:before,.icon-link:before{content:""}.fa-cloud:before{content:""}.fa-flask:before{content:""}.fa-cut:before,.fa-scissors:before{content:""}.fa-copy:before,.fa-files-o:before{content:""}.fa-paperclip:before{content:""}.fa-floppy-o:before,.fa-save:before{content:""}.fa-square:before{content:""}.fa-bars:before,.fa-navicon:before,.fa-reorder:before{content:""}.fa-list-ul:before{content:""}.fa-list-ol:before{content:""}.fa-strikethrough:before{content:""}.fa-underline:before{content:""}.fa-table:before{content:""}.fa-magic:before{content:""}.fa-truck:before{content:""}.fa-pinterest:before{content:""}.fa-pinterest-square:before{content:""}.fa-google-plus-square:before{content:""}.fa-google-plus:before{content:""}.fa-money:before{content:""}.fa-caret-down:before,.icon-caret-down:before,.wy-dropdown .caret:before{content:""}.fa-caret-up:before{content:""}.fa-caret-left:before{content:""}.fa-caret-right:before{content:""}.fa-columns:before{content:""}.fa-sort:before,.fa-unsorted:before{content:""}.fa-sort-desc:before,.fa-sort-down:before{content:""}.fa-sort-asc:before,.fa-sort-up:before{content:""}.fa-envelope:before{content:""}.fa-linkedin:before{content:""}.fa-rotate-left:before,.fa-undo:before{content:""}.fa-gavel:before,.fa-legal:before{content:""}.fa-dashboard:before,.fa-tachometer:before{content:""}.fa-comment-o:before{content:""}.fa-comments-o:before{content:""}.fa-bolt:before,.fa-flash:before{content:""}.fa-sitemap:before{content:""}.fa-umbrella:before{content:""}.fa-clipboard:before,.fa-paste:before{content:""}.fa-lightbulb-o:before{content:""}.fa-exchange:before{content:""}.fa-cloud-download:before{content:""}.fa-cloud-upload:before{content:""}.fa-user-md:before{content:""}.fa-stethoscope:before{content:""}.fa-suitcase:before{content:""}.fa-bell-o:before{content:""}.fa-coffee:before{content:""}.fa-cutlery:before{content:""}.fa-file-text-o:before{content:""}.fa-building-o:before{content:""}.fa-hospital-o:before{content:""}.fa-ambulance:before{content:""}.fa-medkit:before{content:""}.fa-fighter-jet:before{content:""}.fa-beer:before{content:""}.fa-h-square:before{content:""}.fa-plus-square:before{content:""}.fa-angle-double-left:before{content:""}.fa-angle-double-right:before{content:""}.fa-angle-double-up:before{content:""}.fa-angle-double-down:before{content:""}.fa-angle-left:before{content:""}.fa-angle-right:before{content:""}.fa-angle-up:before{content:""}.fa-angle-down:before{content:""}.fa-desktop:before{content:""}.fa-laptop:before{content:""}.fa-tablet:before{content:""}.fa-mobile-phone:before,.fa-mobile:before{content:""}.fa-circle-o:before{content:""}.fa-quote-left:before{content:""}.fa-quote-right:before{content:""}.fa-spinner:before{content:""}.fa-circle:before{content:""}.fa-mail-reply:before,.fa-reply:before{content:""}.fa-github-alt:before{content:""}.fa-folder-o:before{content:""}.fa-folder-open-o:before{content:""}.fa-smile-o:before{content:""}.fa-frown-o:before{content:""}.fa-meh-o:before{content:""}.fa-gamepad:before{content:""}.fa-keyboard-o:before{content:""}.fa-flag-o:before{content:""}.fa-flag-checkered:before{content:""}.fa-terminal:before{content:""}.fa-code:before{content:""}.fa-mail-reply-all:before,.fa-reply-all:before{content:""}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:""}.fa-location-arrow:before{content:""}.fa-crop:before{content:""}.fa-code-fork:before{content:""}.fa-chain-broken:before,.fa-unlink:before{content:""}.fa-question:before{content:""}.fa-info:before{content:""}.fa-exclamation:before{content:""}.fa-superscript:before{content:""}.fa-subscript:before{content:""}.fa-eraser:before{content:""}.fa-puzzle-piece:before{content:""}.fa-microphone:before{content:""}.fa-microphone-slash:before{content:""}.fa-shield:before{content:""}.fa-calendar-o:before{content:""}.fa-fire-extinguisher:before{content:""}.fa-rocket:before{content:""}.fa-maxcdn:before{content:""}.fa-chevron-circle-left:before{content:""}.fa-chevron-circle-right:before{content:""}.fa-chevron-circle-up:before{content:""}.fa-chevron-circle-down:before{content:""}.fa-html5:before{content:""}.fa-css3:before{content:""}.fa-anchor:before{content:""}.fa-unlock-alt:before{content:""}.fa-bullseye:before{content:""}.fa-ellipsis-h:before{content:""}.fa-ellipsis-v:before{content:""}.fa-rss-square:before{content:""}.fa-play-circle:before{content:""}.fa-ticket:before{content:""}.fa-minus-square:before{content:""}.fa-minus-square-o:before,.wy-menu-vertical li.current>a button.toctree-expand:before,.wy-menu-vertical li.on a button.toctree-expand:before{content:""}.fa-level-up:before{content:""}.fa-level-down:before{content:""}.fa-check-square:before{content:""}.fa-pencil-square:before{content:""}.fa-external-link-square:before{content:""}.fa-share-square:before{content:""}.fa-compass:before{content:""}.fa-caret-square-o-down:before,.fa-toggle-down:before{content:""}.fa-caret-square-o-up:before,.fa-toggle-up:before{content:""}.fa-caret-square-o-right:before,.fa-toggle-right:before{content:""}.fa-eur:before,.fa-euro:before{content:""}.fa-gbp:before{content:""}.fa-dollar:before,.fa-usd:before{content:""}.fa-inr:before,.fa-rupee:before{content:""}.fa-cny:before,.fa-jpy:before,.fa-rmb:before,.fa-yen:before{content:""}.fa-rouble:before,.fa-rub:before,.fa-ruble:before{content:""}.fa-krw:before,.fa-won:before{content:""}.fa-bitcoin:before,.fa-btc:before{content:""}.fa-file:before{content:""}.fa-file-text:before{content:""}.fa-sort-alpha-asc:before{content:""}.fa-sort-alpha-desc:before{content:""}.fa-sort-amount-asc:before{content:""}.fa-sort-amount-desc:before{content:""}.fa-sort-numeric-asc:before{content:""}.fa-sort-numeric-desc:before{content:""}.fa-thumbs-up:before{content:""}.fa-thumbs-down:before{content:""}.fa-youtube-square:before{content:""}.fa-youtube:before{content:""}.fa-xing:before{content:""}.fa-xing-square:before{content:""}.fa-youtube-play:before{content:""}.fa-dropbox:before{content:""}.fa-stack-overflow:before{content:""}.fa-instagram:before{content:""}.fa-flickr:before{content:""}.fa-adn:before{content:""}.fa-bitbucket:before,.icon-bitbucket:before{content:""}.fa-bitbucket-square:before{content:""}.fa-tumblr:before{content:""}.fa-tumblr-square:before{content:""}.fa-long-arrow-down:before{content:""}.fa-long-arrow-up:before{content:""}.fa-long-arrow-left:before{content:""}.fa-long-arrow-right:before{content:""}.fa-apple:before{content:""}.fa-windows:before{content:""}.fa-android:before{content:""}.fa-linux:before{content:""}.fa-dribbble:before{content:""}.fa-skype:before{content:""}.fa-foursquare:before{content:""}.fa-trello:before{content:""}.fa-female:before{content:""}.fa-male:before{content:""}.fa-gittip:before,.fa-gratipay:before{content:""}.fa-sun-o:before{content:""}.fa-moon-o:before{content:""}.fa-archive:before{content:""}.fa-bug:before{content:""}.fa-vk:before{content:""}.fa-weibo:before{content:""}.fa-renren:before{content:""}.fa-pagelines:before{content:""}.fa-stack-exchange:before{content:""}.fa-arrow-circle-o-right:before{content:""}.fa-arrow-circle-o-left:before{content:""}.fa-caret-square-o-left:before,.fa-toggle-left:before{content:""}.fa-dot-circle-o:before{content:""}.fa-wheelchair:before{content:""}.fa-vimeo-square:before{content:""}.fa-try:before,.fa-turkish-lira:before{content:""}.fa-plus-square-o:before,.wy-menu-vertical li button.toctree-expand:before{content:""}.fa-space-shuttle:before{content:""}.fa-slack:before{content:""}.fa-envelope-square:before{content:""}.fa-wordpress:before{content:""}.fa-openid:before{content:""}.fa-bank:before,.fa-institution:before,.fa-university:before{content:""}.fa-graduation-cap:before,.fa-mortar-board:before{content:""}.fa-yahoo:before{content:""}.fa-google:before{content:""}.fa-reddit:before{content:""}.fa-reddit-square:before{content:""}.fa-stumbleupon-circle:before{content:""}.fa-stumbleupon:before{content:""}.fa-delicious:before{content:""}.fa-digg:before{content:""}.fa-pied-piper-pp:before{content:""}.fa-pied-piper-alt:before{content:""}.fa-drupal:before{content:""}.fa-joomla:before{content:""}.fa-language:before{content:""}.fa-fax:before{content:""}.fa-building:before{content:""}.fa-child:before{content:""}.fa-paw:before{content:""}.fa-spoon:before{content:""}.fa-cube:before{content:""}.fa-cubes:before{content:""}.fa-behance:before{content:""}.fa-behance-square:before{content:""}.fa-steam:before{content:""}.fa-steam-square:before{content:""}.fa-recycle:before{content:""}.fa-automobile:before,.fa-car:before{content:""}.fa-cab:before,.fa-taxi:before{content:""}.fa-tree:before{content:""}.fa-spotify:before{content:""}.fa-deviantart:before{content:""}.fa-soundcloud:before{content:""}.fa-database:before{content:""}.fa-file-pdf-o:before{content:""}.fa-file-word-o:before{content:""}.fa-file-excel-o:before{content:""}.fa-file-powerpoint-o:before{content:""}.fa-file-image-o:before,.fa-file-photo-o:before,.fa-file-picture-o:before{content:""}.fa-file-archive-o:before,.fa-file-zip-o:before{content:""}.fa-file-audio-o:before,.fa-file-sound-o:before{content:""}.fa-file-movie-o:before,.fa-file-video-o:before{content:""}.fa-file-code-o:before{content:""}.fa-vine:before{content:""}.fa-codepen:before{content:""}.fa-jsfiddle:before{content:""}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-ring:before,.fa-life-saver:before,.fa-support:before{content:""}.fa-circle-o-notch:before{content:""}.fa-ra:before,.fa-rebel:before,.fa-resistance:before{content:""}.fa-empire:before,.fa-ge:before{content:""}.fa-git-square:before{content:""}.fa-git:before{content:""}.fa-hacker-news:before,.fa-y-combinator-square:before,.fa-yc-square:before{content:""}.fa-tencent-weibo:before{content:""}.fa-qq:before{content:""}.fa-wechat:before,.fa-weixin:before{content:""}.fa-paper-plane:before,.fa-send:before{content:""}.fa-paper-plane-o:before,.fa-send-o:before{content:""}.fa-history:before{content:""}.fa-circle-thin:before{content:""}.fa-header:before{content:""}.fa-paragraph:before{content:""}.fa-sliders:before{content:""}.fa-share-alt:before{content:""}.fa-share-alt-square:before{content:""}.fa-bomb:before{content:""}.fa-futbol-o:before,.fa-soccer-ball-o:before{content:""}.fa-tty:before{content:""}.fa-binoculars:before{content:""}.fa-plug:before{content:""}.fa-slideshare:before{content:""}.fa-twitch:before{content:""}.fa-yelp:before{content:""}.fa-newspaper-o:before{content:""}.fa-wifi:before{content:""}.fa-calculator:before{content:""}.fa-paypal:before{content:""}.fa-google-wallet:before{content:""}.fa-cc-visa:before{content:""}.fa-cc-mastercard:before{content:""}.fa-cc-discover:before{content:""}.fa-cc-amex:before{content:""}.fa-cc-paypal:before{content:""}.fa-cc-stripe:before{content:""}.fa-bell-slash:before{content:""}.fa-bell-slash-o:before{content:""}.fa-trash:before{content:""}.fa-copyright:before{content:""}.fa-at:before{content:""}.fa-eyedropper:before{content:""}.fa-paint-brush:before{content:""}.fa-birthday-cake:before{content:""}.fa-area-chart:before{content:""}.fa-pie-chart:before{content:""}.fa-line-chart:before{content:""}.fa-lastfm:before{content:""}.fa-lastfm-square:before{content:""}.fa-toggle-off:before{content:""}.fa-toggle-on:before{content:""}.fa-bicycle:before{content:""}.fa-bus:before{content:""}.fa-ioxhost:before{content:""}.fa-angellist:before{content:""}.fa-cc:before{content:""}.fa-ils:before,.fa-shekel:before,.fa-sheqel:before{content:""}.fa-meanpath:before{content:""}.fa-buysellads:before{content:""}.fa-connectdevelop:before{content:""}.fa-dashcube:before{content:""}.fa-forumbee:before{content:""}.fa-leanpub:before{content:""}.fa-sellsy:before{content:""}.fa-shirtsinbulk:before{content:""}.fa-simplybuilt:before{content:""}.fa-skyatlas:before{content:""}.fa-cart-plus:before{content:""}.fa-cart-arrow-down:before{content:""}.fa-diamond:before{content:""}.fa-ship:before{content:""}.fa-user-secret:before{content:""}.fa-motorcycle:before{content:""}.fa-street-view:before{content:""}.fa-heartbeat:before{content:""}.fa-venus:before{content:""}.fa-mars:before{content:""}.fa-mercury:before{content:""}.fa-intersex:before,.fa-transgender:before{content:""}.fa-transgender-alt:before{content:""}.fa-venus-double:before{content:""}.fa-mars-double:before{content:""}.fa-venus-mars:before{content:""}.fa-mars-stroke:before{content:""}.fa-mars-stroke-v:before{content:""}.fa-mars-stroke-h:before{content:""}.fa-neuter:before{content:""}.fa-genderless:before{content:""}.fa-facebook-official:before{content:""}.fa-pinterest-p:before{content:""}.fa-whatsapp:before{content:""}.fa-server:before{content:""}.fa-user-plus:before{content:""}.fa-user-times:before{content:""}.fa-bed:before,.fa-hotel:before{content:""}.fa-viacoin:before{content:""}.fa-train:before{content:""}.fa-subway:before{content:""}.fa-medium:before{content:""}.fa-y-combinator:before,.fa-yc:before{content:""}.fa-optin-monster:before{content:""}.fa-opencart:before{content:""}.fa-expeditedssl:before{content:""}.fa-battery-4:before,.fa-battery-full:before,.fa-battery:before{content:""}.fa-battery-3:before,.fa-battery-three-quarters:before{content:""}.fa-battery-2:before,.fa-battery-half:before{content:""}.fa-battery-1:before,.fa-battery-quarter:before{content:""}.fa-battery-0:before,.fa-battery-empty:before{content:""}.fa-mouse-pointer:before{content:""}.fa-i-cursor:before{content:""}.fa-object-group:before{content:""}.fa-object-ungroup:before{content:""}.fa-sticky-note:before{content:""}.fa-sticky-note-o:before{content:""}.fa-cc-jcb:before{content:""}.fa-cc-diners-club:before{content:""}.fa-clone:before{content:""}.fa-balance-scale:before{content:""}.fa-hourglass-o:before{content:""}.fa-hourglass-1:before,.fa-hourglass-start:before{content:""}.fa-hourglass-2:before,.fa-hourglass-half:before{content:""}.fa-hourglass-3:before,.fa-hourglass-end:before{content:""}.fa-hourglass:before{content:""}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:""}.fa-hand-paper-o:before,.fa-hand-stop-o:before{content:""}.fa-hand-scissors-o:before{content:""}.fa-hand-lizard-o:before{content:""}.fa-hand-spock-o:before{content:""}.fa-hand-pointer-o:before{content:""}.fa-hand-peace-o:before{content:""}.fa-trademark:before{content:""}.fa-registered:before{content:""}.fa-creative-commons:before{content:""}.fa-gg:before{content:""}.fa-gg-circle:before{content:""}.fa-tripadvisor:before{content:""}.fa-odnoklassniki:before{content:""}.fa-odnoklassniki-square:before{content:""}.fa-get-pocket:before{content:""}.fa-wikipedia-w:before{content:""}.fa-safari:before{content:""}.fa-chrome:before{content:""}.fa-firefox:before{content:""}.fa-opera:before{content:""}.fa-internet-explorer:before{content:""}.fa-television:before,.fa-tv:before{content:""}.fa-contao:before{content:""}.fa-500px:before{content:""}.fa-amazon:before{content:""}.fa-calendar-plus-o:before{content:""}.fa-calendar-minus-o:before{content:""}.fa-calendar-times-o:before{content:""}.fa-calendar-check-o:before{content:""}.fa-industry:before{content:""}.fa-map-pin:before{content:""}.fa-map-signs:before{content:""}.fa-map-o:before{content:""}.fa-map:before{content:""}.fa-commenting:before{content:""}.fa-commenting-o:before{content:""}.fa-houzz:before{content:""}.fa-vimeo:before{content:""}.fa-black-tie:before{content:""}.fa-fonticons:before{content:""}.fa-reddit-alien:before{content:""}.fa-edge:before{content:""}.fa-credit-card-alt:before{content:""}.fa-codiepie:before{content:""}.fa-modx:before{content:""}.fa-fort-awesome:before{content:""}.fa-usb:before{content:""}.fa-product-hunt:before{content:""}.fa-mixcloud:before{content:""}.fa-scribd:before{content:""}.fa-pause-circle:before{content:""}.fa-pause-circle-o:before{content:""}.fa-stop-circle:before{content:""}.fa-stop-circle-o:before{content:""}.fa-shopping-bag:before{content:""}.fa-shopping-basket:before{content:""}.fa-hashtag:before{content:""}.fa-bluetooth:before{content:""}.fa-bluetooth-b:before{content:""}.fa-percent:before{content:""}.fa-gitlab:before,.icon-gitlab:before{content:""}.fa-wpbeginner:before{content:""}.fa-wpforms:before{content:""}.fa-envira:before{content:""}.fa-universal-access:before{content:""}.fa-wheelchair-alt:before{content:""}.fa-question-circle-o:before{content:""}.fa-blind:before{content:""}.fa-audio-description:before{content:""}.fa-volume-control-phone:before{content:""}.fa-braille:before{content:""}.fa-assistive-listening-systems:before{content:""}.fa-american-sign-language-interpreting:before,.fa-asl-interpreting:before{content:""}.fa-deaf:before,.fa-deafness:before,.fa-hard-of-hearing:before{content:""}.fa-glide:before{content:""}.fa-glide-g:before{content:""}.fa-sign-language:before,.fa-signing:before{content:""}.fa-low-vision:before{content:""}.fa-viadeo:before{content:""}.fa-viadeo-square:before{content:""}.fa-snapchat:before{content:""}.fa-snapchat-ghost:before{content:""}.fa-snapchat-square:before{content:""}.fa-pied-piper:before{content:""}.fa-first-order:before{content:""}.fa-yoast:before{content:""}.fa-themeisle:before{content:""}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:""}.fa-fa:before,.fa-font-awesome:before{content:""}.fa-handshake-o:before{content:""}.fa-envelope-open:before{content:""}.fa-envelope-open-o:before{content:""}.fa-linode:before{content:""}.fa-address-book:before{content:""}.fa-address-book-o:before{content:""}.fa-address-card:before,.fa-vcard:before{content:""}.fa-address-card-o:before,.fa-vcard-o:before{content:""}.fa-user-circle:before{content:""}.fa-user-circle-o:before{content:""}.fa-user-o:before{content:""}.fa-id-badge:before{content:""}.fa-drivers-license:before,.fa-id-card:before{content:""}.fa-drivers-license-o:before,.fa-id-card-o:before{content:""}.fa-quora:before{content:""}.fa-free-code-camp:before{content:""}.fa-telegram:before{content:""}.fa-thermometer-4:before,.fa-thermometer-full:before,.fa-thermometer:before{content:""}.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:""}.fa-thermometer-2:before,.fa-thermometer-half:before{content:""}.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:""}.fa-thermometer-0:before,.fa-thermometer-empty:before{content:""}.fa-shower:before{content:""}.fa-bath:before,.fa-bathtub:before,.fa-s15:before{content:""}.fa-podcast:before{content:""}.fa-window-maximize:before{content:""}.fa-window-minimize:before{content:""}.fa-window-restore:before{content:""}.fa-times-rectangle:before,.fa-window-close:before{content:""}.fa-times-rectangle-o:before,.fa-window-close-o:before{content:""}.fa-bandcamp:before{content:""}.fa-grav:before{content:""}.fa-etsy:before{content:""}.fa-imdb:before{content:""}.fa-ravelry:before{content:""}.fa-eercast:before{content:""}.fa-microchip:before{content:""}.fa-snowflake-o:before{content:""}.fa-superpowers:before{content:""}.fa-wpexplorer:before{content:""}.fa-meetup:before{content:""}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}.fa,.icon,.rst-content .admonition-title,.rst-content .code-block-caption .headerlink,.rst-content .eqno .headerlink,.rst-content code.download span:first-child,.rst-content dl dt .headerlink,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content p.caption .headerlink,.rst-content p .headerlink,.rst-content table>caption .headerlink,.rst-content tt.download span:first-child,.wy-dropdown .caret,.wy-inline-validate.wy-inline-validate-danger .wy-input-context,.wy-inline-validate.wy-inline-validate-info .wy-input-context,.wy-inline-validate.wy-inline-validate-success .wy-input-context,.wy-inline-validate.wy-inline-validate-warning .wy-input-context,.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand,.wy-menu-vertical li button.toctree-expand{font-family:inherit}.fa:before,.icon:before,.rst-content .admonition-title:before,.rst-content .code-block-caption .headerlink:before,.rst-content .eqno .headerlink:before,.rst-content code.download span:first-child:before,.rst-content dl dt .headerlink:before,.rst-content h1 .headerlink:before,.rst-content h2 .headerlink:before,.rst-content h3 .headerlink:before,.rst-content h4 .headerlink:before,.rst-content h5 .headerlink:before,.rst-content h6 .headerlink:before,.rst-content p.caption .headerlink:before,.rst-content p .headerlink:before,.rst-content table>caption .headerlink:before,.rst-content tt.download span:first-child:before,.wy-dropdown .caret:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-menu-vertical li.current>a button.toctree-expand:before,.wy-menu-vertical li.on a button.toctree-expand:before,.wy-menu-vertical li button.toctree-expand:before{font-family:FontAwesome;display:inline-block;font-style:normal;font-weight:400;line-height:1;text-decoration:inherit}.rst-content .code-block-caption a .headerlink,.rst-content .eqno a .headerlink,.rst-content a .admonition-title,.rst-content code.download a span:first-child,.rst-content dl dt a .headerlink,.rst-content h1 a .headerlink,.rst-content h2 a .headerlink,.rst-content h3 a .headerlink,.rst-content h4 a .headerlink,.rst-content h5 a .headerlink,.rst-content h6 a .headerlink,.rst-content p.caption a .headerlink,.rst-content p a .headerlink,.rst-content table>caption a .headerlink,.rst-content tt.download a span:first-child,.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand,.wy-menu-vertical li a button.toctree-expand,a .fa,a .icon,a .rst-content .admonition-title,a .rst-content .code-block-caption .headerlink,a .rst-content .eqno .headerlink,a .rst-content code.download span:first-child,a .rst-content dl dt .headerlink,a .rst-content h1 .headerlink,a .rst-content h2 .headerlink,a .rst-content h3 .headerlink,a .rst-content h4 .headerlink,a .rst-content h5 .headerlink,a .rst-content h6 .headerlink,a .rst-content p.caption .headerlink,a .rst-content p .headerlink,a .rst-content table>caption .headerlink,a .rst-content tt.download span:first-child,a .wy-menu-vertical li button.toctree-expand{display:inline-block;text-decoration:inherit}.btn .fa,.btn .icon,.btn .rst-content .admonition-title,.btn .rst-content .code-block-caption .headerlink,.btn .rst-content .eqno .headerlink,.btn .rst-content code.download span:first-child,.btn .rst-content dl dt .headerlink,.btn .rst-content h1 .headerlink,.btn .rst-content h2 .headerlink,.btn .rst-content h3 .headerlink,.btn .rst-content h4 .headerlink,.btn .rst-content h5 .headerlink,.btn .rst-content h6 .headerlink,.btn .rst-content p .headerlink,.btn .rst-content table>caption .headerlink,.btn .rst-content tt.download span:first-child,.btn .wy-menu-vertical li.current>a button.toctree-expand,.btn .wy-menu-vertical li.on a button.toctree-expand,.btn .wy-menu-vertical li button.toctree-expand,.nav .fa,.nav .icon,.nav .rst-content .admonition-title,.nav .rst-content .code-block-caption .headerlink,.nav .rst-content .eqno .headerlink,.nav .rst-content code.download span:first-child,.nav .rst-content dl dt .headerlink,.nav .rst-content h1 .headerlink,.nav .rst-content h2 .headerlink,.nav .rst-content h3 .headerlink,.nav .rst-content h4 .headerlink,.nav .rst-content h5 .headerlink,.nav .rst-content h6 .headerlink,.nav .rst-content p .headerlink,.nav .rst-content table>caption .headerlink,.nav .rst-content tt.download span:first-child,.nav .wy-menu-vertical li.current>a button.toctree-expand,.nav .wy-menu-vertical li.on a button.toctree-expand,.nav .wy-menu-vertical li button.toctree-expand,.rst-content .btn .admonition-title,.rst-content .code-block-caption .btn .headerlink,.rst-content .code-block-caption .nav .headerlink,.rst-content .eqno .btn .headerlink,.rst-content .eqno .nav .headerlink,.rst-content .nav .admonition-title,.rst-content code.download .btn span:first-child,.rst-content code.download .nav span:first-child,.rst-content dl dt .btn .headerlink,.rst-content dl dt .nav .headerlink,.rst-content h1 .btn .headerlink,.rst-content h1 .nav .headerlink,.rst-content h2 .btn .headerlink,.rst-content h2 .nav .headerlink,.rst-content h3 .btn .headerlink,.rst-content h3 .nav .headerlink,.rst-content h4 .btn .headerlink,.rst-content h4 .nav .headerlink,.rst-content h5 .btn .headerlink,.rst-content h5 .nav .headerlink,.rst-content h6 .btn .headerlink,.rst-content h6 .nav .headerlink,.rst-content p .btn .headerlink,.rst-content p .nav .headerlink,.rst-content table>caption .btn .headerlink,.rst-content table>caption .nav .headerlink,.rst-content tt.download .btn span:first-child,.rst-content tt.download .nav span:first-child,.wy-menu-vertical li .btn button.toctree-expand,.wy-menu-vertical li.current>a .btn button.toctree-expand,.wy-menu-vertical li.current>a .nav button.toctree-expand,.wy-menu-vertical li .nav button.toctree-expand,.wy-menu-vertical li.on a .btn button.toctree-expand,.wy-menu-vertical li.on a .nav button.toctree-expand{display:inline}.btn .fa-large.icon,.btn .fa.fa-large,.btn .rst-content .code-block-caption .fa-large.headerlink,.btn .rst-content .eqno .fa-large.headerlink,.btn .rst-content .fa-large.admonition-title,.btn .rst-content code.download span.fa-large:first-child,.btn .rst-content dl dt .fa-large.headerlink,.btn .rst-content h1 .fa-large.headerlink,.btn .rst-content h2 .fa-large.headerlink,.btn .rst-content h3 .fa-large.headerlink,.btn .rst-content h4 .fa-large.headerlink,.btn .rst-content h5 .fa-large.headerlink,.btn .rst-content h6 .fa-large.headerlink,.btn .rst-content p .fa-large.headerlink,.btn .rst-content table>caption .fa-large.headerlink,.btn .rst-content tt.download span.fa-large:first-child,.btn .wy-menu-vertical li button.fa-large.toctree-expand,.nav .fa-large.icon,.nav .fa.fa-large,.nav .rst-content .code-block-caption .fa-large.headerlink,.nav .rst-content .eqno .fa-large.headerlink,.nav .rst-content .fa-large.admonition-title,.nav .rst-content code.download span.fa-large:first-child,.nav .rst-content dl dt .fa-large.headerlink,.nav .rst-content h1 .fa-large.headerlink,.nav .rst-content h2 .fa-large.headerlink,.nav .rst-content h3 .fa-large.headerlink,.nav .rst-content h4 .fa-large.headerlink,.nav .rst-content h5 .fa-large.headerlink,.nav .rst-content h6 .fa-large.headerlink,.nav .rst-content p .fa-large.headerlink,.nav .rst-content table>caption .fa-large.headerlink,.nav .rst-content tt.download span.fa-large:first-child,.nav .wy-menu-vertical li button.fa-large.toctree-expand,.rst-content .btn .fa-large.admonition-title,.rst-content .code-block-caption .btn .fa-large.headerlink,.rst-content .code-block-caption .nav .fa-large.headerlink,.rst-content .eqno .btn .fa-large.headerlink,.rst-content .eqno .nav .fa-large.headerlink,.rst-content .nav .fa-large.admonition-title,.rst-content code.download .btn span.fa-large:first-child,.rst-content code.download .nav span.fa-large:first-child,.rst-content dl dt .btn .fa-large.headerlink,.rst-content dl dt .nav .fa-large.headerlink,.rst-content h1 .btn .fa-large.headerlink,.rst-content h1 .nav .fa-large.headerlink,.rst-content h2 .btn .fa-large.headerlink,.rst-content h2 .nav .fa-large.headerlink,.rst-content h3 .btn .fa-large.headerlink,.rst-content h3 .nav .fa-large.headerlink,.rst-content h4 .btn .fa-large.headerlink,.rst-content h4 .nav .fa-large.headerlink,.rst-content h5 .btn .fa-large.headerlink,.rst-content h5 .nav .fa-large.headerlink,.rst-content h6 .btn .fa-large.headerlink,.rst-content h6 .nav .fa-large.headerlink,.rst-content p .btn .fa-large.headerlink,.rst-content p .nav .fa-large.headerlink,.rst-content table>caption .btn .fa-large.headerlink,.rst-content table>caption .nav .fa-large.headerlink,.rst-content tt.download .btn span.fa-large:first-child,.rst-content tt.download .nav span.fa-large:first-child,.wy-menu-vertical li .btn button.fa-large.toctree-expand,.wy-menu-vertical li .nav button.fa-large.toctree-expand{line-height:.9em}.btn .fa-spin.icon,.btn .fa.fa-spin,.btn .rst-content .code-block-caption .fa-spin.headerlink,.btn .rst-content .eqno .fa-spin.headerlink,.btn .rst-content .fa-spin.admonition-title,.btn .rst-content code.download span.fa-spin:first-child,.btn .rst-content dl dt .fa-spin.headerlink,.btn .rst-content h1 .fa-spin.headerlink,.btn .rst-content h2 .fa-spin.headerlink,.btn .rst-content h3 .fa-spin.headerlink,.btn .rst-content h4 .fa-spin.headerlink,.btn .rst-content h5 .fa-spin.headerlink,.btn .rst-content h6 .fa-spin.headerlink,.btn .rst-content p .fa-spin.headerlink,.btn .rst-content table>caption .fa-spin.headerlink,.btn .rst-content tt.download span.fa-spin:first-child,.btn .wy-menu-vertical li button.fa-spin.toctree-expand,.nav .fa-spin.icon,.nav .fa.fa-spin,.nav .rst-content .code-block-caption .fa-spin.headerlink,.nav .rst-content .eqno .fa-spin.headerlink,.nav .rst-content .fa-spin.admonition-title,.nav .rst-content code.download span.fa-spin:first-child,.nav .rst-content dl dt .fa-spin.headerlink,.nav .rst-content h1 .fa-spin.headerlink,.nav .rst-content h2 .fa-spin.headerlink,.nav .rst-content h3 .fa-spin.headerlink,.nav .rst-content h4 .fa-spin.headerlink,.nav .rst-content h5 .fa-spin.headerlink,.nav .rst-content h6 .fa-spin.headerlink,.nav .rst-content p .fa-spin.headerlink,.nav .rst-content table>caption .fa-spin.headerlink,.nav .rst-content tt.download span.fa-spin:first-child,.nav .wy-menu-vertical li button.fa-spin.toctree-expand,.rst-content .btn .fa-spin.admonition-title,.rst-content .code-block-caption .btn .fa-spin.headerlink,.rst-content .code-block-caption .nav .fa-spin.headerlink,.rst-content .eqno .btn .fa-spin.headerlink,.rst-content .eqno .nav .fa-spin.headerlink,.rst-content .nav .fa-spin.admonition-title,.rst-content code.download .btn span.fa-spin:first-child,.rst-content code.download .nav span.fa-spin:first-child,.rst-content dl dt .btn .fa-spin.headerlink,.rst-content dl dt .nav .fa-spin.headerlink,.rst-content h1 .btn .fa-spin.headerlink,.rst-content h1 .nav .fa-spin.headerlink,.rst-content h2 .btn .fa-spin.headerlink,.rst-content h2 .nav .fa-spin.headerlink,.rst-content h3 .btn .fa-spin.headerlink,.rst-content h3 .nav .fa-spin.headerlink,.rst-content h4 .btn .fa-spin.headerlink,.rst-content h4 .nav .fa-spin.headerlink,.rst-content h5 .btn .fa-spin.headerlink,.rst-content h5 .nav .fa-spin.headerlink,.rst-content h6 .btn .fa-spin.headerlink,.rst-content h6 .nav .fa-spin.headerlink,.rst-content p .btn .fa-spin.headerlink,.rst-content p .nav .fa-spin.headerlink,.rst-content table>caption .btn .fa-spin.headerlink,.rst-content table>caption .nav .fa-spin.headerlink,.rst-content tt.download .btn span.fa-spin:first-child,.rst-content tt.download .nav span.fa-spin:first-child,.wy-menu-vertical li .btn button.fa-spin.toctree-expand,.wy-menu-vertical li .nav button.fa-spin.toctree-expand{display:inline-block}.btn.fa:before,.btn.icon:before,.rst-content .btn.admonition-title:before,.rst-content .code-block-caption .btn.headerlink:before,.rst-content .eqno .btn.headerlink:before,.rst-content code.download span.btn:first-child:before,.rst-content dl dt .btn.headerlink:before,.rst-content h1 .btn.headerlink:before,.rst-content h2 .btn.headerlink:before,.rst-content h3 .btn.headerlink:before,.rst-content h4 .btn.headerlink:before,.rst-content h5 .btn.headerlink:before,.rst-content h6 .btn.headerlink:before,.rst-content p .btn.headerlink:before,.rst-content table>caption .btn.headerlink:before,.rst-content tt.download span.btn:first-child:before,.wy-menu-vertical li button.btn.toctree-expand:before{opacity:.5;-webkit-transition:opacity .05s ease-in;-moz-transition:opacity .05s ease-in;transition:opacity .05s ease-in}.btn.fa:hover:before,.btn.icon:hover:before,.rst-content .btn.admonition-title:hover:before,.rst-content .code-block-caption .btn.headerlink:hover:before,.rst-content .eqno .btn.headerlink:hover:before,.rst-content code.download span.btn:first-child:hover:before,.rst-content dl dt .btn.headerlink:hover:before,.rst-content h1 .btn.headerlink:hover:before,.rst-content h2 .btn.headerlink:hover:before,.rst-content h3 .btn.headerlink:hover:before,.rst-content h4 .btn.headerlink:hover:before,.rst-content h5 .btn.headerlink:hover:before,.rst-content h6 .btn.headerlink:hover:before,.rst-content p .btn.headerlink:hover:before,.rst-content table>caption .btn.headerlink:hover:before,.rst-content tt.download span.btn:first-child:hover:before,.wy-menu-vertical li button.btn.toctree-expand:hover:before{opacity:1}.btn-mini .fa:before,.btn-mini .icon:before,.btn-mini .rst-content .admonition-title:before,.btn-mini .rst-content .code-block-caption .headerlink:before,.btn-mini .rst-content .eqno .headerlink:before,.btn-mini .rst-content code.download span:first-child:before,.btn-mini .rst-content dl dt .headerlink:before,.btn-mini .rst-content h1 .headerlink:before,.btn-mini .rst-content h2 .headerlink:before,.btn-mini .rst-content h3 .headerlink:before,.btn-mini .rst-content h4 .headerlink:before,.btn-mini .rst-content h5 .headerlink:before,.btn-mini .rst-content h6 .headerlink:before,.btn-mini .rst-content p .headerlink:before,.btn-mini .rst-content table>caption .headerlink:before,.btn-mini .rst-content tt.download span:first-child:before,.btn-mini .wy-menu-vertical li button.toctree-expand:before,.rst-content .btn-mini .admonition-title:before,.rst-content .code-block-caption .btn-mini .headerlink:before,.rst-content .eqno .btn-mini .headerlink:before,.rst-content code.download .btn-mini span:first-child:before,.rst-content dl dt .btn-mini .headerlink:before,.rst-content h1 .btn-mini .headerlink:before,.rst-content h2 .btn-mini .headerlink:before,.rst-content h3 .btn-mini .headerlink:before,.rst-content h4 .btn-mini .headerlink:before,.rst-content h5 .btn-mini .headerlink:before,.rst-content h6 .btn-mini .headerlink:before,.rst-content p .btn-mini .headerlink:before,.rst-content table>caption .btn-mini .headerlink:before,.rst-content tt.download .btn-mini span:first-child:before,.wy-menu-vertical li .btn-mini button.toctree-expand:before{font-size:14px;vertical-align:-15%}.rst-content .admonition,.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .danger,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .note,.rst-content .seealso,.rst-content .tip,.rst-content .warning,.wy-alert{padding:12px;line-height:24px;margin-bottom:24px;background:#e7f2fa}.rst-content .admonition-title,.wy-alert-title{font-weight:700;display:block;color:#fff;background:#6ab0de;padding:6px 12px;margin:-12px -12px 12px}.rst-content .danger,.rst-content .error,.rst-content .wy-alert-danger.admonition,.rst-content .wy-alert-danger.admonition-todo,.rst-content .wy-alert-danger.attention,.rst-content .wy-alert-danger.caution,.rst-content .wy-alert-danger.hint,.rst-content .wy-alert-danger.important,.rst-content .wy-alert-danger.note,.rst-content .wy-alert-danger.seealso,.rst-content .wy-alert-danger.tip,.rst-content .wy-alert-danger.warning,.wy-alert.wy-alert-danger{background:#fdf3f2}.rst-content .danger .admonition-title,.rst-content .danger .wy-alert-title,.rst-content .error .admonition-title,.rst-content .error .wy-alert-title,.rst-content .wy-alert-danger.admonition-todo .admonition-title,.rst-content .wy-alert-danger.admonition-todo .wy-alert-title,.rst-content .wy-alert-danger.admonition .admonition-title,.rst-content .wy-alert-danger.admonition .wy-alert-title,.rst-content .wy-alert-danger.attention .admonition-title,.rst-content .wy-alert-danger.attention .wy-alert-title,.rst-content .wy-alert-danger.caution .admonition-title,.rst-content .wy-alert-danger.caution .wy-alert-title,.rst-content .wy-alert-danger.hint .admonition-title,.rst-content .wy-alert-danger.hint .wy-alert-title,.rst-content .wy-alert-danger.important .admonition-title,.rst-content .wy-alert-danger.important .wy-alert-title,.rst-content .wy-alert-danger.note .admonition-title,.rst-content .wy-alert-danger.note .wy-alert-title,.rst-content .wy-alert-danger.seealso .admonition-title,.rst-content .wy-alert-danger.seealso .wy-alert-title,.rst-content .wy-alert-danger.tip .admonition-title,.rst-content .wy-alert-danger.tip .wy-alert-title,.rst-content .wy-alert-danger.warning .admonition-title,.rst-content .wy-alert-danger.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-danger .admonition-title,.wy-alert.wy-alert-danger .rst-content .admonition-title,.wy-alert.wy-alert-danger .wy-alert-title{background:#f29f97}.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .warning,.rst-content .wy-alert-warning.admonition,.rst-content .wy-alert-warning.danger,.rst-content .wy-alert-warning.error,.rst-content .wy-alert-warning.hint,.rst-content .wy-alert-warning.important,.rst-content .wy-alert-warning.note,.rst-content .wy-alert-warning.seealso,.rst-content .wy-alert-warning.tip,.wy-alert.wy-alert-warning{background:#ffedcc}.rst-content .admonition-todo .admonition-title,.rst-content .admonition-todo .wy-alert-title,.rst-content .attention .admonition-title,.rst-content .attention .wy-alert-title,.rst-content .caution .admonition-title,.rst-content .caution .wy-alert-title,.rst-content .warning .admonition-title,.rst-content .warning .wy-alert-title,.rst-content .wy-alert-warning.admonition .admonition-title,.rst-content .wy-alert-warning.admonition .wy-alert-title,.rst-content .wy-alert-warning.danger .admonition-title,.rst-content .wy-alert-warning.danger .wy-alert-title,.rst-content .wy-alert-warning.error .admonition-title,.rst-content .wy-alert-warning.error .wy-alert-title,.rst-content .wy-alert-warning.hint .admonition-title,.rst-content .wy-alert-warning.hint .wy-alert-title,.rst-content .wy-alert-warning.important .admonition-title,.rst-content .wy-alert-warning.important .wy-alert-title,.rst-content .wy-alert-warning.note .admonition-title,.rst-content .wy-alert-warning.note .wy-alert-title,.rst-content .wy-alert-warning.seealso .admonition-title,.rst-content .wy-alert-warning.seealso .wy-alert-title,.rst-content .wy-alert-warning.tip .admonition-title,.rst-content .wy-alert-warning.tip .wy-alert-title,.rst-content .wy-alert.wy-alert-warning .admonition-title,.wy-alert.wy-alert-warning .rst-content .admonition-title,.wy-alert.wy-alert-warning .wy-alert-title{background:#f0b37e}.rst-content .note,.rst-content .seealso,.rst-content .wy-alert-info.admonition,.rst-content .wy-alert-info.admonition-todo,.rst-content .wy-alert-info.attention,.rst-content .wy-alert-info.caution,.rst-content .wy-alert-info.danger,.rst-content .wy-alert-info.error,.rst-content .wy-alert-info.hint,.rst-content .wy-alert-info.important,.rst-content .wy-alert-info.tip,.rst-content .wy-alert-info.warning,.wy-alert.wy-alert-info{background:#e7f2fa}.rst-content .note .admonition-title,.rst-content .note .wy-alert-title,.rst-content .seealso .admonition-title,.rst-content .seealso .wy-alert-title,.rst-content .wy-alert-info.admonition-todo .admonition-title,.rst-content .wy-alert-info.admonition-todo .wy-alert-title,.rst-content .wy-alert-info.admonition .admonition-title,.rst-content .wy-alert-info.admonition .wy-alert-title,.rst-content .wy-alert-info.attention .admonition-title,.rst-content .wy-alert-info.attention .wy-alert-title,.rst-content .wy-alert-info.caution .admonition-title,.rst-content .wy-alert-info.caution .wy-alert-title,.rst-content .wy-alert-info.danger .admonition-title,.rst-content .wy-alert-info.danger .wy-alert-title,.rst-content .wy-alert-info.error .admonition-title,.rst-content .wy-alert-info.error .wy-alert-title,.rst-content .wy-alert-info.hint .admonition-title,.rst-content .wy-alert-info.hint .wy-alert-title,.rst-content .wy-alert-info.important .admonition-title,.rst-content .wy-alert-info.important .wy-alert-title,.rst-content .wy-alert-info.tip .admonition-title,.rst-content .wy-alert-info.tip .wy-alert-title,.rst-content .wy-alert-info.warning .admonition-title,.rst-content .wy-alert-info.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-info .admonition-title,.wy-alert.wy-alert-info .rst-content .admonition-title,.wy-alert.wy-alert-info .wy-alert-title{background:#6ab0de}.rst-content .hint,.rst-content .important,.rst-content .tip,.rst-content .wy-alert-success.admonition,.rst-content .wy-alert-success.admonition-todo,.rst-content .wy-alert-success.attention,.rst-content .wy-alert-success.caution,.rst-content .wy-alert-success.danger,.rst-content .wy-alert-success.error,.rst-content .wy-alert-success.note,.rst-content .wy-alert-success.seealso,.rst-content .wy-alert-success.warning,.wy-alert.wy-alert-success{background:#dbfaf4}.rst-content .hint .admonition-title,.rst-content .hint .wy-alert-title,.rst-content .important .admonition-title,.rst-content .important .wy-alert-title,.rst-content .tip .admonition-title,.rst-content .tip .wy-alert-title,.rst-content .wy-alert-success.admonition-todo .admonition-title,.rst-content .wy-alert-success.admonition-todo .wy-alert-title,.rst-content .wy-alert-success.admonition .admonition-title,.rst-content .wy-alert-success.admonition .wy-alert-title,.rst-content .wy-alert-success.attention .admonition-title,.rst-content .wy-alert-success.attention .wy-alert-title,.rst-content .wy-alert-success.caution .admonition-title,.rst-content .wy-alert-success.caution .wy-alert-title,.rst-content .wy-alert-success.danger .admonition-title,.rst-content .wy-alert-success.danger .wy-alert-title,.rst-content .wy-alert-success.error .admonition-title,.rst-content .wy-alert-success.error .wy-alert-title,.rst-content .wy-alert-success.note .admonition-title,.rst-content .wy-alert-success.note .wy-alert-title,.rst-content .wy-alert-success.seealso .admonition-title,.rst-content .wy-alert-success.seealso .wy-alert-title,.rst-content .wy-alert-success.warning .admonition-title,.rst-content .wy-alert-success.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-success .admonition-title,.wy-alert.wy-alert-success .rst-content .admonition-title,.wy-alert.wy-alert-success .wy-alert-title{background:#1abc9c}.rst-content .wy-alert-neutral.admonition,.rst-content .wy-alert-neutral.admonition-todo,.rst-content .wy-alert-neutral.attention,.rst-content .wy-alert-neutral.caution,.rst-content .wy-alert-neutral.danger,.rst-content .wy-alert-neutral.error,.rst-content .wy-alert-neutral.hint,.rst-content .wy-alert-neutral.important,.rst-content .wy-alert-neutral.note,.rst-content .wy-alert-neutral.seealso,.rst-content .wy-alert-neutral.tip,.rst-content .wy-alert-neutral.warning,.wy-alert.wy-alert-neutral{background:#f3f6f6}.rst-content .wy-alert-neutral.admonition-todo .admonition-title,.rst-content .wy-alert-neutral.admonition-todo .wy-alert-title,.rst-content .wy-alert-neutral.admonition .admonition-title,.rst-content .wy-alert-neutral.admonition .wy-alert-title,.rst-content .wy-alert-neutral.attention .admonition-title,.rst-content .wy-alert-neutral.attention .wy-alert-title,.rst-content .wy-alert-neutral.caution .admonition-title,.rst-content .wy-alert-neutral.caution .wy-alert-title,.rst-content .wy-alert-neutral.danger .admonition-title,.rst-content .wy-alert-neutral.danger .wy-alert-title,.rst-content .wy-alert-neutral.error .admonition-title,.rst-content .wy-alert-neutral.error .wy-alert-title,.rst-content .wy-alert-neutral.hint .admonition-title,.rst-content .wy-alert-neutral.hint .wy-alert-title,.rst-content .wy-alert-neutral.important .admonition-title,.rst-content .wy-alert-neutral.important .wy-alert-title,.rst-content .wy-alert-neutral.note .admonition-title,.rst-content .wy-alert-neutral.note .wy-alert-title,.rst-content .wy-alert-neutral.seealso .admonition-title,.rst-content .wy-alert-neutral.seealso .wy-alert-title,.rst-content .wy-alert-neutral.tip .admonition-title,.rst-content .wy-alert-neutral.tip .wy-alert-title,.rst-content .wy-alert-neutral.warning .admonition-title,.rst-content .wy-alert-neutral.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-neutral .admonition-title,.wy-alert.wy-alert-neutral .rst-content .admonition-title,.wy-alert.wy-alert-neutral .wy-alert-title{color:#404040;background:#e1e4e5}.rst-content .wy-alert-neutral.admonition-todo a,.rst-content .wy-alert-neutral.admonition a,.rst-content .wy-alert-neutral.attention a,.rst-content .wy-alert-neutral.caution a,.rst-content .wy-alert-neutral.danger a,.rst-content .wy-alert-neutral.error a,.rst-content .wy-alert-neutral.hint a,.rst-content .wy-alert-neutral.important a,.rst-content .wy-alert-neutral.note a,.rst-content .wy-alert-neutral.seealso a,.rst-content .wy-alert-neutral.tip a,.rst-content .wy-alert-neutral.warning a,.wy-alert.wy-alert-neutral a{color:#2980b9}.rst-content .admonition-todo p:last-child,.rst-content .admonition p:last-child,.rst-content .attention p:last-child,.rst-content .caution p:last-child,.rst-content .danger p:last-child,.rst-content .error p:last-child,.rst-content .hint p:last-child,.rst-content .important p:last-child,.rst-content .note p:last-child,.rst-content .seealso p:last-child,.rst-content .tip p:last-child,.rst-content .warning p:last-child,.wy-alert p:last-child{margin-bottom:0}.wy-tray-container{position:fixed;bottom:0;left:0;z-index:600}.wy-tray-container li{display:block;width:300px;background:transparent;color:#fff;text-align:center;box-shadow:0 5px 5px 0 rgba(0,0,0,.1);padding:0 24px;min-width:20%;opacity:0;height:0;line-height:56px;overflow:hidden;-webkit-transition:all .3s ease-in;-moz-transition:all .3s ease-in;transition:all .3s ease-in}.wy-tray-container li.wy-tray-item-success{background:#27ae60}.wy-tray-container li.wy-tray-item-info{background:#2980b9}.wy-tray-container li.wy-tray-item-warning{background:#e67e22}.wy-tray-container li.wy-tray-item-danger{background:#e74c3c}.wy-tray-container li.on{opacity:1;height:56px}@media screen and (max-width:768px){.wy-tray-container{bottom:auto;top:0;width:100%}.wy-tray-container li{width:100%}}button{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle;cursor:pointer;line-height:normal;-webkit-appearance:button;*overflow:visible}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}button[disabled]{cursor:default}.btn{display:inline-block;border-radius:2px;line-height:normal;white-space:nowrap;text-align:center;cursor:pointer;font-size:100%;padding:6px 12px 8px;color:#fff;border:1px solid rgba(0,0,0,.1);background-color:#27ae60;text-decoration:none;font-weight:400;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;box-shadow:inset 0 1px 2px -1px hsla(0,0%,100%,.5),inset 0 -2px 0 0 rgba(0,0,0,.1);outline-none:false;vertical-align:middle;*display:inline;zoom:1;-webkit-user-drag:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-transition:all .1s linear;-moz-transition:all .1s linear;transition:all .1s linear}.btn-hover{background:#2e8ece;color:#fff}.btn:hover{background:#2cc36b;color:#fff}.btn:focus{background:#2cc36b;outline:0}.btn:active{box-shadow:inset 0 -1px 0 0 rgba(0,0,0,.05),inset 0 2px 0 0 rgba(0,0,0,.1);padding:8px 12px 6px}.btn:visited{color:#fff}.btn-disabled,.btn-disabled:active,.btn-disabled:focus,.btn-disabled:hover,.btn:disabled{background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);filter:alpha(opacity=40);opacity:.4;cursor:not-allowed;box-shadow:none}.btn::-moz-focus-inner{padding:0;border:0}.btn-small{font-size:80%}.btn-info{background-color:#2980b9!important}.btn-info:hover{background-color:#2e8ece!important}.btn-neutral{background-color:#f3f6f6!important;color:#404040!important}.btn-neutral:hover{background-color:#e5ebeb!important;color:#404040}.btn-neutral:visited{color:#404040!important}.btn-success{background-color:#27ae60!important}.btn-success:hover{background-color:#295!important}.btn-danger{background-color:#e74c3c!important}.btn-danger:hover{background-color:#ea6153!important}.btn-warning{background-color:#e67e22!important}.btn-warning:hover{background-color:#e98b39!important}.btn-invert{background-color:#222}.btn-invert:hover{background-color:#2f2f2f!important}.btn-link{background-color:transparent!important;color:#2980b9;box-shadow:none;border-color:transparent!important}.btn-link:active,.btn-link:hover{background-color:transparent!important;color:#409ad5!important;box-shadow:none}.btn-link:visited{color:#9b59b6}.wy-btn-group .btn,.wy-control .btn{vertical-align:middle}.wy-btn-group{margin-bottom:24px;*zoom:1}.wy-btn-group:after,.wy-btn-group:before{display:table;content:""}.wy-btn-group:after{clear:both}.wy-dropdown{position:relative;display:inline-block}.wy-dropdown-active .wy-dropdown-menu{display:block}.wy-dropdown-menu{position:absolute;left:0;display:none;float:left;top:100%;min-width:100%;background:#fcfcfc;z-index:100;border:1px solid #cfd7dd;box-shadow:0 2px 2px 0 rgba(0,0,0,.1);padding:12px}.wy-dropdown-menu>dd>a{display:block;clear:both;color:#404040;white-space:nowrap;font-size:90%;padding:0 12px;cursor:pointer}.wy-dropdown-menu>dd>a:hover{background:#2980b9;color:#fff}.wy-dropdown-menu>dd.divider{border-top:1px solid #cfd7dd;margin:6px 0}.wy-dropdown-menu>dd.search{padding-bottom:12px}.wy-dropdown-menu>dd.search input[type=search]{width:100%}.wy-dropdown-menu>dd.call-to-action{background:#e3e3e3;text-transform:uppercase;font-weight:500;font-size:80%}.wy-dropdown-menu>dd.call-to-action:hover{background:#e3e3e3}.wy-dropdown-menu>dd.call-to-action .btn{color:#fff}.wy-dropdown.wy-dropdown-up .wy-dropdown-menu{bottom:100%;top:auto;left:auto;right:0}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu{background:#fcfcfc;margin-top:2px}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a{padding:6px 12px}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a:hover{background:#2980b9;color:#fff}.wy-dropdown.wy-dropdown-left .wy-dropdown-menu{right:0;left:auto;text-align:right}.wy-dropdown-arrow:before{content:" ";border-bottom:5px solid #f5f5f5;border-left:5px solid transparent;border-right:5px solid transparent;position:absolute;display:block;top:-4px;left:50%;margin-left:-3px}.wy-dropdown-arrow.wy-dropdown-arrow-left:before{left:11px}.wy-form-stacked select{display:block}.wy-form-aligned .wy-help-inline,.wy-form-aligned input,.wy-form-aligned label,.wy-form-aligned select,.wy-form-aligned textarea{display:inline-block;*display:inline;*zoom:1;vertical-align:middle}.wy-form-aligned .wy-control-group>label{display:inline-block;vertical-align:middle;width:10em;margin:6px 12px 0 0;float:left}.wy-form-aligned .wy-control{float:left}.wy-form-aligned .wy-control label{display:block}.wy-form-aligned .wy-control select{margin-top:6px}fieldset{margin:0}fieldset,legend{border:0;padding:0}legend{width:100%;white-space:normal;margin-bottom:24px;font-size:150%;*margin-left:-7px}label,legend{display:block}label{margin:0 0 .3125em;color:#333;font-size:90%}input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}.wy-control-group{margin-bottom:24px;max-width:1200px;margin-left:auto;margin-right:auto;*zoom:1}.wy-control-group:after,.wy-control-group:before{display:table;content:""}.wy-control-group:after{clear:both}.wy-control-group.wy-control-group-required>label:after{content:" *";color:#e74c3c}.wy-control-group .wy-form-full,.wy-control-group .wy-form-halves,.wy-control-group .wy-form-thirds{padding-bottom:12px}.wy-control-group .wy-form-full input[type=color],.wy-control-group .wy-form-full input[type=date],.wy-control-group .wy-form-full input[type=datetime-local],.wy-control-group .wy-form-full input[type=datetime],.wy-control-group .wy-form-full input[type=email],.wy-control-group .wy-form-full input[type=month],.wy-control-group .wy-form-full input[type=number],.wy-control-group .wy-form-full input[type=password],.wy-control-group .wy-form-full input[type=search],.wy-control-group .wy-form-full input[type=tel],.wy-control-group .wy-form-full input[type=text],.wy-control-group .wy-form-full input[type=time],.wy-control-group .wy-form-full input[type=url],.wy-control-group .wy-form-full input[type=week],.wy-control-group .wy-form-full select,.wy-control-group .wy-form-halves input[type=color],.wy-control-group .wy-form-halves input[type=date],.wy-control-group .wy-form-halves input[type=datetime-local],.wy-control-group .wy-form-halves input[type=datetime],.wy-control-group .wy-form-halves input[type=email],.wy-control-group .wy-form-halves input[type=month],.wy-control-group .wy-form-halves input[type=number],.wy-control-group .wy-form-halves input[type=password],.wy-control-group .wy-form-halves input[type=search],.wy-control-group .wy-form-halves input[type=tel],.wy-control-group .wy-form-halves input[type=text],.wy-control-group .wy-form-halves input[type=time],.wy-control-group .wy-form-halves input[type=url],.wy-control-group .wy-form-halves input[type=week],.wy-control-group .wy-form-halves select,.wy-control-group .wy-form-thirds input[type=color],.wy-control-group .wy-form-thirds input[type=date],.wy-control-group .wy-form-thirds input[type=datetime-local],.wy-control-group .wy-form-thirds input[type=datetime],.wy-control-group .wy-form-thirds input[type=email],.wy-control-group .wy-form-thirds input[type=month],.wy-control-group .wy-form-thirds input[type=number],.wy-control-group .wy-form-thirds input[type=password],.wy-control-group .wy-form-thirds input[type=search],.wy-control-group .wy-form-thirds input[type=tel],.wy-control-group .wy-form-thirds input[type=text],.wy-control-group .wy-form-thirds input[type=time],.wy-control-group .wy-form-thirds input[type=url],.wy-control-group .wy-form-thirds input[type=week],.wy-control-group .wy-form-thirds select{width:100%}.wy-control-group .wy-form-full{float:left;display:block;width:100%;margin-right:0}.wy-control-group .wy-form-full:last-child{margin-right:0}.wy-control-group .wy-form-halves{float:left;display:block;margin-right:2.35765%;width:48.82117%}.wy-control-group .wy-form-halves:last-child,.wy-control-group .wy-form-halves:nth-of-type(2n){margin-right:0}.wy-control-group .wy-form-halves:nth-of-type(odd){clear:left}.wy-control-group .wy-form-thirds{float:left;display:block;margin-right:2.35765%;width:31.76157%}.wy-control-group .wy-form-thirds:last-child,.wy-control-group .wy-form-thirds:nth-of-type(3n){margin-right:0}.wy-control-group .wy-form-thirds:nth-of-type(3n+1){clear:left}.wy-control-group.wy-control-group-no-input .wy-control,.wy-control-no-input{margin:6px 0 0;font-size:90%}.wy-control-no-input{display:inline-block}.wy-control-group.fluid-input input[type=color],.wy-control-group.fluid-input input[type=date],.wy-control-group.fluid-input input[type=datetime-local],.wy-control-group.fluid-input input[type=datetime],.wy-control-group.fluid-input input[type=email],.wy-control-group.fluid-input input[type=month],.wy-control-group.fluid-input input[type=number],.wy-control-group.fluid-input input[type=password],.wy-control-group.fluid-input input[type=search],.wy-control-group.fluid-input input[type=tel],.wy-control-group.fluid-input input[type=text],.wy-control-group.fluid-input input[type=time],.wy-control-group.fluid-input input[type=url],.wy-control-group.fluid-input input[type=week]{width:100%}.wy-form-message-inline{padding-left:.3em;color:#666;font-size:90%}.wy-form-message{display:block;color:#999;font-size:70%;margin-top:.3125em;font-style:italic}.wy-form-message p{font-size:inherit;font-style:italic;margin-bottom:6px}.wy-form-message p:last-child{margin-bottom:0}input{line-height:normal}input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;*overflow:visible}input[type=color],input[type=date],input[type=datetime-local],input[type=datetime],input[type=email],input[type=month],input[type=number],input[type=password],input[type=search],input[type=tel],input[type=text],input[type=time],input[type=url],input[type=week]{-webkit-appearance:none;padding:6px;display:inline-block;border:1px solid #ccc;font-size:80%;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;box-shadow:inset 0 1px 3px #ddd;border-radius:0;-webkit-transition:border .3s linear;-moz-transition:border .3s linear;transition:border .3s linear}input[type=datetime-local]{padding:.34375em .625em}input[disabled]{cursor:default}input[type=checkbox],input[type=radio]{padding:0;margin-right:.3125em;*height:13px;*width:13px}input[type=checkbox],input[type=radio],input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}input[type=color]:focus,input[type=date]:focus,input[type=datetime-local]:focus,input[type=datetime]:focus,input[type=email]:focus,input[type=month]:focus,input[type=number]:focus,input[type=password]:focus,input[type=search]:focus,input[type=tel]:focus,input[type=text]:focus,input[type=time]:focus,input[type=url]:focus,input[type=week]:focus{outline:0;outline:thin dotted\9;border-color:#333}input.no-focus:focus{border-color:#ccc!important}input[type=checkbox]:focus,input[type=file]:focus,input[type=radio]:focus{outline:thin dotted #333;outline:1px auto #129fea}input[type=color][disabled],input[type=date][disabled],input[type=datetime-local][disabled],input[type=datetime][disabled],input[type=email][disabled],input[type=month][disabled],input[type=number][disabled],input[type=password][disabled],input[type=search][disabled],input[type=tel][disabled],input[type=text][disabled],input[type=time][disabled],input[type=url][disabled],input[type=week][disabled]{cursor:not-allowed;background-color:#fafafa}input:focus:invalid,select:focus:invalid,textarea:focus:invalid{color:#e74c3c;border:1px solid #e74c3c}input:focus:invalid:focus,select:focus:invalid:focus,textarea:focus:invalid:focus{border-color:#e74c3c}input[type=checkbox]:focus:invalid:focus,input[type=file]:focus:invalid:focus,input[type=radio]:focus:invalid:focus{outline-color:#e74c3c}input.wy-input-large{padding:12px;font-size:100%}textarea{overflow:auto;vertical-align:top;width:100%;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif}select,textarea{padding:.5em .625em;display:inline-block;border:1px solid #ccc;font-size:80%;box-shadow:inset 0 1px 3px #ddd;-webkit-transition:border .3s linear;-moz-transition:border .3s linear;transition:border .3s linear}select{border:1px solid #ccc;background-color:#fff}select[multiple]{height:auto}select:focus,textarea:focus{outline:0}input[readonly],select[disabled],select[readonly],textarea[disabled],textarea[readonly]{cursor:not-allowed;background-color:#fafafa}input[type=checkbox][disabled],input[type=radio][disabled]{cursor:not-allowed}.wy-checkbox,.wy-radio{margin:6px 0;color:#404040;display:block}.wy-checkbox input,.wy-radio input{vertical-align:baseline}.wy-form-message-inline{display:inline-block;*display:inline;*zoom:1;vertical-align:middle}.wy-input-prefix,.wy-input-suffix{white-space:nowrap;padding:6px}.wy-input-prefix .wy-input-context,.wy-input-suffix .wy-input-context{line-height:27px;padding:0 8px;display:inline-block;font-size:80%;background-color:#f3f6f6;border:1px solid #ccc;color:#999}.wy-input-suffix .wy-input-context{border-left:0}.wy-input-prefix .wy-input-context{border-right:0}.wy-switch{position:relative;display:block;height:24px;margin-top:12px;cursor:pointer}.wy-switch:before{left:0;top:0;width:36px;height:12px;background:#ccc}.wy-switch:after,.wy-switch:before{position:absolute;content:"";display:block;border-radius:4px;-webkit-transition:all .2s ease-in-out;-moz-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.wy-switch:after{width:18px;height:18px;background:#999;left:-3px;top:-3px}.wy-switch span{position:absolute;left:48px;display:block;font-size:12px;color:#ccc;line-height:1}.wy-switch.active:before{background:#1e8449}.wy-switch.active:after{left:24px;background:#27ae60}.wy-switch.disabled{cursor:not-allowed;opacity:.8}.wy-control-group.wy-control-group-error .wy-form-message,.wy-control-group.wy-control-group-error>label{color:#e74c3c}.wy-control-group.wy-control-group-error input[type=color],.wy-control-group.wy-control-group-error input[type=date],.wy-control-group.wy-control-group-error input[type=datetime-local],.wy-control-group.wy-control-group-error input[type=datetime],.wy-control-group.wy-control-group-error input[type=email],.wy-control-group.wy-control-group-error input[type=month],.wy-control-group.wy-control-group-error input[type=number],.wy-control-group.wy-control-group-error input[type=password],.wy-control-group.wy-control-group-error input[type=search],.wy-control-group.wy-control-group-error input[type=tel],.wy-control-group.wy-control-group-error input[type=text],.wy-control-group.wy-control-group-error input[type=time],.wy-control-group.wy-control-group-error input[type=url],.wy-control-group.wy-control-group-error input[type=week],.wy-control-group.wy-control-group-error textarea{border:1px solid #e74c3c}.wy-inline-validate{white-space:nowrap}.wy-inline-validate .wy-input-context{padding:.5em .625em;display:inline-block;font-size:80%}.wy-inline-validate.wy-inline-validate-success .wy-input-context{color:#27ae60}.wy-inline-validate.wy-inline-validate-danger .wy-input-context{color:#e74c3c}.wy-inline-validate.wy-inline-validate-warning .wy-input-context{color:#e67e22}.wy-inline-validate.wy-inline-validate-info .wy-input-context{color:#2980b9}.rotate-90{-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-ms-transform:rotate(90deg);-o-transform:rotate(90deg);transform:rotate(90deg)}.rotate-180{-webkit-transform:rotate(180deg);-moz-transform:rotate(180deg);-ms-transform:rotate(180deg);-o-transform:rotate(180deg);transform:rotate(180deg)}.rotate-270{-webkit-transform:rotate(270deg);-moz-transform:rotate(270deg);-ms-transform:rotate(270deg);-o-transform:rotate(270deg);transform:rotate(270deg)}.mirror{-webkit-transform:scaleX(-1);-moz-transform:scaleX(-1);-ms-transform:scaleX(-1);-o-transform:scaleX(-1);transform:scaleX(-1)}.mirror.rotate-90{-webkit-transform:scaleX(-1) rotate(90deg);-moz-transform:scaleX(-1) rotate(90deg);-ms-transform:scaleX(-1) rotate(90deg);-o-transform:scaleX(-1) rotate(90deg);transform:scaleX(-1) rotate(90deg)}.mirror.rotate-180{-webkit-transform:scaleX(-1) rotate(180deg);-moz-transform:scaleX(-1) rotate(180deg);-ms-transform:scaleX(-1) rotate(180deg);-o-transform:scaleX(-1) rotate(180deg);transform:scaleX(-1) rotate(180deg)}.mirror.rotate-270{-webkit-transform:scaleX(-1) rotate(270deg);-moz-transform:scaleX(-1) rotate(270deg);-ms-transform:scaleX(-1) rotate(270deg);-o-transform:scaleX(-1) rotate(270deg);transform:scaleX(-1) rotate(270deg)}@media only screen and (max-width:480px){.wy-form button[type=submit]{margin:.7em 0 0}.wy-form input[type=color],.wy-form input[type=date],.wy-form input[type=datetime-local],.wy-form input[type=datetime],.wy-form input[type=email],.wy-form input[type=month],.wy-form input[type=number],.wy-form input[type=password],.wy-form input[type=search],.wy-form input[type=tel],.wy-form input[type=text],.wy-form input[type=time],.wy-form input[type=url],.wy-form input[type=week],.wy-form label{margin-bottom:.3em;display:block}.wy-form input[type=color],.wy-form input[type=date],.wy-form input[type=datetime-local],.wy-form input[type=datetime],.wy-form input[type=email],.wy-form input[type=month],.wy-form input[type=number],.wy-form input[type=password],.wy-form input[type=search],.wy-form input[type=tel],.wy-form input[type=time],.wy-form input[type=url],.wy-form input[type=week]{margin-bottom:0}.wy-form-aligned .wy-control-group label{margin-bottom:.3em;text-align:left;display:block;width:100%}.wy-form-aligned .wy-control{margin:1.5em 0 0}.wy-form-message,.wy-form-message-inline,.wy-form .wy-help-inline{display:block;font-size:80%;padding:6px 0}}@media screen and (max-width:768px){.tablet-hide{display:none}}@media screen and (max-width:480px){.mobile-hide{display:none}}.float-left{float:left}.float-right{float:right}.full-width{width:100%}.rst-content table.docutils,.rst-content table.field-list,.wy-table{border-collapse:collapse;border-spacing:0;empty-cells:show;margin-bottom:24px}.rst-content table.docutils caption,.rst-content table.field-list caption,.wy-table caption{color:#000;font:italic 85%/1 arial,sans-serif;padding:1em 0;text-align:center}.rst-content table.docutils td,.rst-content table.docutils th,.rst-content table.field-list td,.rst-content table.field-list th,.wy-table td,.wy-table th{font-size:90%;margin:0;overflow:visible;padding:8px 16px}.rst-content table.docutils td:first-child,.rst-content table.docutils th:first-child,.rst-content table.field-list td:first-child,.rst-content table.field-list th:first-child,.wy-table td:first-child,.wy-table th:first-child{border-left-width:0}.rst-content table.docutils thead,.rst-content table.field-list thead,.wy-table thead{color:#000;text-align:left;vertical-align:bottom;white-space:nowrap}.rst-content table.docutils thead th,.rst-content table.field-list thead th,.wy-table thead th{font-weight:700;border-bottom:2px solid #e1e4e5}.rst-content table.docutils td,.rst-content table.field-list td,.wy-table td{background-color:transparent;vertical-align:middle}.rst-content table.docutils td p,.rst-content table.field-list td p,.wy-table td p{line-height:18px}.rst-content table.docutils td p:last-child,.rst-content table.field-list td p:last-child,.wy-table td p:last-child{margin-bottom:0}.rst-content table.docutils .wy-table-cell-min,.rst-content table.field-list .wy-table-cell-min,.wy-table .wy-table-cell-min{width:1%;padding-right:0}.rst-content table.docutils .wy-table-cell-min input[type=checkbox],.rst-content table.field-list .wy-table-cell-min input[type=checkbox],.wy-table .wy-table-cell-min input[type=checkbox]{margin:0}.wy-table-secondary{color:grey;font-size:90%}.wy-table-tertiary{color:grey;font-size:80%}.rst-content table.docutils:not(.field-list) tr:nth-child(2n-1) td,.wy-table-backed,.wy-table-odd td,.wy-table-striped tr:nth-child(2n-1) td{background-color:#f3f6f6}.rst-content table.docutils,.wy-table-bordered-all{border:1px solid #e1e4e5}.rst-content table.docutils td,.wy-table-bordered-all td{border-bottom:1px solid #e1e4e5;border-left:1px solid #e1e4e5}.rst-content table.docutils tbody>tr:last-child td,.wy-table-bordered-all tbody>tr:last-child td{border-bottom-width:0}.wy-table-bordered{border:1px solid #e1e4e5}.wy-table-bordered-rows td{border-bottom:1px solid #e1e4e5}.wy-table-bordered-rows tbody>tr:last-child td{border-bottom-width:0}.wy-table-horizontal td,.wy-table-horizontal th{border-width:0 0 1px;border-bottom:1px solid #e1e4e5}.wy-table-horizontal tbody>tr:last-child td{border-bottom-width:0}.wy-table-responsive{margin-bottom:24px;max-width:100%;overflow:auto}.wy-table-responsive table{margin-bottom:0!important}.wy-table-responsive table td,.wy-table-responsive table th{white-space:nowrap}a{color:#2980b9;text-decoration:none;cursor:pointer}a:hover{color:#3091d1}a:visited{color:#9b59b6}html{height:100%}body,html{overflow-x:hidden}body{font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;font-weight:400;color:#404040;min-height:100%;background:#edf0f2}.wy-text-left{text-align:left}.wy-text-center{text-align:center}.wy-text-right{text-align:right}.wy-text-large{font-size:120%}.wy-text-normal{font-size:100%}.wy-text-small,small{font-size:80%}.wy-text-strike{text-decoration:line-through}.wy-text-warning{color:#e67e22!important}a.wy-text-warning:hover{color:#eb9950!important}.wy-text-info{color:#2980b9!important}a.wy-text-info:hover{color:#409ad5!important}.wy-text-success{color:#27ae60!important}a.wy-text-success:hover{color:#36d278!important}.wy-text-danger{color:#e74c3c!important}a.wy-text-danger:hover{color:#ed7669!important}.wy-text-neutral{color:#404040!important}a.wy-text-neutral:hover{color:#595959!important}.rst-content .toctree-wrapper>p.caption,h1,h2,h3,h4,h5,h6,legend{margin-top:0;font-weight:700;font-family:Roboto Slab,ff-tisa-web-pro,Georgia,Arial,sans-serif}p{line-height:24px;font-size:16px;margin:0 0 24px}h1{font-size:175%}.rst-content .toctree-wrapper>p.caption,h2{font-size:150%}h3{font-size:125%}h4{font-size:115%}h5{font-size:110%}h6{font-size:100%}hr{display:block;height:1px;border:0;border-top:1px solid #e1e4e5;margin:24px 0;padding:0}.rst-content code,.rst-content tt,code{white-space:nowrap;max-width:100%;background:#fff;border:1px solid #e1e4e5;font-size:75%;padding:0 5px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;color:#e74c3c;overflow-x:auto}.rst-content tt.code-large,code.code-large{font-size:90%}.rst-content .section ul,.rst-content .toctree-wrapper ul,.rst-content section ul,.wy-plain-list-disc,article ul{list-style:disc;line-height:24px;margin-bottom:24px}.rst-content .section ul li,.rst-content .toctree-wrapper ul li,.rst-content section ul li,.wy-plain-list-disc li,article ul li{list-style:disc;margin-left:24px}.rst-content .section ul li p:last-child,.rst-content .section ul li ul,.rst-content .toctree-wrapper ul li p:last-child,.rst-content .toctree-wrapper ul li ul,.rst-content section ul li p:last-child,.rst-content section ul li ul,.wy-plain-list-disc li p:last-child,.wy-plain-list-disc li ul,article ul li p:last-child,article ul li ul{margin-bottom:0}.rst-content .section ul li li,.rst-content .toctree-wrapper ul li li,.rst-content section ul li li,.wy-plain-list-disc li li,article ul li li{list-style:circle}.rst-content .section ul li li li,.rst-content .toctree-wrapper ul li li li,.rst-content section ul li li li,.wy-plain-list-disc li li li,article ul li li li{list-style:square}.rst-content .section ul li ol li,.rst-content .toctree-wrapper ul li ol li,.rst-content section ul li ol li,.wy-plain-list-disc li ol li,article ul li ol li{list-style:decimal}.rst-content .section ol,.rst-content .section ol.arabic,.rst-content .toctree-wrapper ol,.rst-content .toctree-wrapper ol.arabic,.rst-content section ol,.rst-content section ol.arabic,.wy-plain-list-decimal,article ol{list-style:decimal;line-height:24px;margin-bottom:24px}.rst-content .section ol.arabic li,.rst-content .section ol li,.rst-content .toctree-wrapper ol.arabic li,.rst-content .toctree-wrapper ol li,.rst-content section ol.arabic li,.rst-content section ol li,.wy-plain-list-decimal li,article ol li{list-style:decimal;margin-left:24px}.rst-content .section ol.arabic li ul,.rst-content .section ol li p:last-child,.rst-content .section ol li ul,.rst-content .toctree-wrapper ol.arabic li ul,.rst-content .toctree-wrapper ol li p:last-child,.rst-content .toctree-wrapper ol li ul,.rst-content section ol.arabic li ul,.rst-content section ol li p:last-child,.rst-content section ol li ul,.wy-plain-list-decimal li p:last-child,.wy-plain-list-decimal li ul,article ol li p:last-child,article ol li ul{margin-bottom:0}.rst-content .section ol.arabic li ul li,.rst-content .section ol li ul li,.rst-content .toctree-wrapper ol.arabic li ul li,.rst-content .toctree-wrapper ol li ul li,.rst-content section ol.arabic li ul li,.rst-content section ol li ul li,.wy-plain-list-decimal li ul li,article ol li ul li{list-style:disc}.wy-breadcrumbs{*zoom:1}.wy-breadcrumbs:after,.wy-breadcrumbs:before{display:table;content:""}.wy-breadcrumbs:after{clear:both}.wy-breadcrumbs>li{display:inline-block;padding-top:5px}.wy-breadcrumbs>li.wy-breadcrumbs-aside{float:right}.rst-content .wy-breadcrumbs>li code,.rst-content .wy-breadcrumbs>li tt,.wy-breadcrumbs>li .rst-content tt,.wy-breadcrumbs>li code{all:inherit;color:inherit}.breadcrumb-item:before{content:"/";color:#bbb;font-size:13px;padding:0 6px 0 3px}.wy-breadcrumbs-extra{margin-bottom:0;color:#b3b3b3;font-size:80%;display:inline-block}@media screen and (max-width:480px){.wy-breadcrumbs-extra,.wy-breadcrumbs li.wy-breadcrumbs-aside{display:none}}@media print{.wy-breadcrumbs li.wy-breadcrumbs-aside{display:none}}html{font-size:16px}.wy-affix{position:fixed;top:1.618em}.wy-menu a:hover{text-decoration:none}.wy-menu-horiz{*zoom:1}.wy-menu-horiz:after,.wy-menu-horiz:before{display:table;content:""}.wy-menu-horiz:after{clear:both}.wy-menu-horiz li,.wy-menu-horiz ul{display:inline-block}.wy-menu-horiz li:hover{background:hsla(0,0%,100%,.1)}.wy-menu-horiz li.divide-left{border-left:1px solid #404040}.wy-menu-horiz li.divide-right{border-right:1px solid #404040}.wy-menu-horiz a{height:32px;display:inline-block;line-height:32px;padding:0 16px}.wy-menu-vertical{width:300px}.wy-menu-vertical header,.wy-menu-vertical p.caption{color:#55a5d9;height:32px;line-height:32px;padding:0 1.618em;margin:12px 0 0;display:block;font-weight:700;text-transform:uppercase;font-size:85%;white-space:nowrap}.wy-menu-vertical ul{margin-bottom:0}.wy-menu-vertical li.divide-top{border-top:1px solid #404040}.wy-menu-vertical li.divide-bottom{border-bottom:1px solid #404040}.wy-menu-vertical li.current{background:#e3e3e3}.wy-menu-vertical li.current a{color:grey;border-right:1px solid #c9c9c9;padding:.4045em 2.427em}.wy-menu-vertical li.current a:hover{background:#d6d6d6}.rst-content .wy-menu-vertical li tt,.wy-menu-vertical li .rst-content tt,.wy-menu-vertical li code{border:none;background:inherit;color:inherit;padding-left:0;padding-right:0}.wy-menu-vertical li button.toctree-expand{display:block;float:left;margin-left:-1.2em;line-height:18px;color:#4d4d4d;border:none;background:none;padding:0}.wy-menu-vertical li.current>a,.wy-menu-vertical li.on a{color:#404040;font-weight:700;position:relative;background:#fcfcfc;border:none;padding:.4045em 1.618em}.wy-menu-vertical li.current>a:hover,.wy-menu-vertical li.on a:hover{background:#fcfcfc}.wy-menu-vertical li.current>a:hover button.toctree-expand,.wy-menu-vertical li.on a:hover button.toctree-expand{color:grey}.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand{display:block;line-height:18px;color:#333}.wy-menu-vertical li.toctree-l1.current>a{border-bottom:1px solid #c9c9c9;border-top:1px solid #c9c9c9}.wy-menu-vertical .toctree-l1.current .toctree-l2>ul,.wy-menu-vertical .toctree-l2.current .toctree-l3>ul,.wy-menu-vertical .toctree-l3.current .toctree-l4>ul,.wy-menu-vertical .toctree-l4.current .toctree-l5>ul,.wy-menu-vertical .toctree-l5.current .toctree-l6>ul,.wy-menu-vertical .toctree-l6.current .toctree-l7>ul,.wy-menu-vertical .toctree-l7.current .toctree-l8>ul,.wy-menu-vertical .toctree-l8.current .toctree-l9>ul,.wy-menu-vertical .toctree-l9.current .toctree-l10>ul,.wy-menu-vertical .toctree-l10.current .toctree-l11>ul{display:none}.wy-menu-vertical .toctree-l1.current .current.toctree-l2>ul,.wy-menu-vertical .toctree-l2.current .current.toctree-l3>ul,.wy-menu-vertical .toctree-l3.current .current.toctree-l4>ul,.wy-menu-vertical .toctree-l4.current .current.toctree-l5>ul,.wy-menu-vertical .toctree-l5.current .current.toctree-l6>ul,.wy-menu-vertical .toctree-l6.current .current.toctree-l7>ul,.wy-menu-vertical .toctree-l7.current .current.toctree-l8>ul,.wy-menu-vertical .toctree-l8.current .current.toctree-l9>ul,.wy-menu-vertical .toctree-l9.current .current.toctree-l10>ul,.wy-menu-vertical .toctree-l10.current .current.toctree-l11>ul{display:block}.wy-menu-vertical li.toctree-l3,.wy-menu-vertical li.toctree-l4{font-size:.9em}.wy-menu-vertical li.toctree-l2 a,.wy-menu-vertical li.toctree-l3 a,.wy-menu-vertical li.toctree-l4 a,.wy-menu-vertical li.toctree-l5 a,.wy-menu-vertical li.toctree-l6 a,.wy-menu-vertical li.toctree-l7 a,.wy-menu-vertical li.toctree-l8 a,.wy-menu-vertical li.toctree-l9 a,.wy-menu-vertical li.toctree-l10 a{color:#404040}.wy-menu-vertical li.toctree-l2 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l3 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l4 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l5 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l6 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l7 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l8 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l9 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l10 a:hover button.toctree-expand{color:grey}.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a,.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a,.wy-menu-vertical li.toctree-l4.current li.toctree-l5>a,.wy-menu-vertical li.toctree-l5.current li.toctree-l6>a,.wy-menu-vertical li.toctree-l6.current li.toctree-l7>a,.wy-menu-vertical li.toctree-l7.current li.toctree-l8>a,.wy-menu-vertical li.toctree-l8.current li.toctree-l9>a,.wy-menu-vertical li.toctree-l9.current li.toctree-l10>a,.wy-menu-vertical li.toctree-l10.current li.toctree-l11>a{display:block}.wy-menu-vertical li.toctree-l2.current>a{padding:.4045em 2.427em}.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a{padding:.4045em 1.618em .4045em 4.045em}.wy-menu-vertical li.toctree-l3.current>a{padding:.4045em 4.045em}.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a{padding:.4045em 1.618em .4045em 5.663em}.wy-menu-vertical li.toctree-l4.current>a{padding:.4045em 5.663em}.wy-menu-vertical li.toctree-l4.current li.toctree-l5>a{padding:.4045em 1.618em .4045em 7.281em}.wy-menu-vertical li.toctree-l5.current>a{padding:.4045em 7.281em}.wy-menu-vertical li.toctree-l5.current li.toctree-l6>a{padding:.4045em 1.618em .4045em 8.899em}.wy-menu-vertical li.toctree-l6.current>a{padding:.4045em 8.899em}.wy-menu-vertical li.toctree-l6.current li.toctree-l7>a{padding:.4045em 1.618em .4045em 10.517em}.wy-menu-vertical li.toctree-l7.current>a{padding:.4045em 10.517em}.wy-menu-vertical li.toctree-l7.current li.toctree-l8>a{padding:.4045em 1.618em .4045em 12.135em}.wy-menu-vertical li.toctree-l8.current>a{padding:.4045em 12.135em}.wy-menu-vertical li.toctree-l8.current li.toctree-l9>a{padding:.4045em 1.618em .4045em 13.753em}.wy-menu-vertical li.toctree-l9.current>a{padding:.4045em 13.753em}.wy-menu-vertical li.toctree-l9.current li.toctree-l10>a{padding:.4045em 1.618em .4045em 15.371em}.wy-menu-vertical li.toctree-l10.current>a{padding:.4045em 15.371em}.wy-menu-vertical li.toctree-l10.current li.toctree-l11>a{padding:.4045em 1.618em .4045em 16.989em}.wy-menu-vertical li.toctree-l2.current>a,.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a{background:#c9c9c9}.wy-menu-vertical li.toctree-l2 button.toctree-expand{color:#a3a3a3}.wy-menu-vertical li.toctree-l3.current>a,.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a{background:#bdbdbd}.wy-menu-vertical li.toctree-l3 button.toctree-expand{color:#969696}.wy-menu-vertical li.current ul{display:block}.wy-menu-vertical li ul{margin-bottom:0;display:none}.wy-menu-vertical li ul li a{margin-bottom:0;color:#d9d9d9;font-weight:400}.wy-menu-vertical a{line-height:18px;padding:.4045em 1.618em;display:block;position:relative;font-size:90%;color:#d9d9d9}.wy-menu-vertical a:hover{background-color:#4e4a4a;cursor:pointer}.wy-menu-vertical a:hover button.toctree-expand{color:#d9d9d9}.wy-menu-vertical a:active{background-color:#2980b9;cursor:pointer;color:#fff}.wy-menu-vertical a:active button.toctree-expand{color:#fff}.wy-side-nav-search{display:block;width:300px;padding:.809em;margin-bottom:.809em;z-index:200;background-color:#2980b9;text-align:center;color:#fcfcfc}.wy-side-nav-search input[type=text]{width:100%;border-radius:50px;padding:6px 12px;border-color:#2472a4}.wy-side-nav-search img{display:block;margin:auto auto .809em;height:45px;width:45px;background-color:#2980b9;padding:5px;border-radius:100%}.wy-side-nav-search .wy-dropdown>a,.wy-side-nav-search>a{color:#fcfcfc;font-size:100%;font-weight:700;display:inline-block;padding:4px 6px;margin-bottom:.809em;max-width:100%}.wy-side-nav-search .wy-dropdown>a:hover,.wy-side-nav-search .wy-dropdown>aactive,.wy-side-nav-search .wy-dropdown>afocus,.wy-side-nav-search>a:hover,.wy-side-nav-search>aactive,.wy-side-nav-search>afocus{background:hsla(0,0%,100%,.1)}.wy-side-nav-search .wy-dropdown>a img.logo,.wy-side-nav-search>a img.logo{display:block;margin:0 auto;height:auto;width:auto;border-radius:0;max-width:100%;background:transparent}.wy-side-nav-search .wy-dropdown>a.icon,.wy-side-nav-search>a.icon{display:block}.wy-side-nav-search .wy-dropdown>a.icon img.logo,.wy-side-nav-search>a.icon img.logo{margin-top:.85em}.wy-side-nav-search>div.switch-menus{position:relative;display:block;margin-top:-.4045em;margin-bottom:.809em;font-weight:400;color:hsla(0,0%,100%,.3)}.wy-side-nav-search>div.switch-menus>div.language-switch,.wy-side-nav-search>div.switch-menus>div.version-switch{display:inline-block;padding:.2em}.wy-side-nav-search>div.switch-menus>div.language-switch select,.wy-side-nav-search>div.switch-menus>div.version-switch select{display:inline-block;margin-right:-2rem;padding-right:2rem;max-width:240px;text-align-last:center;background:none;border:none;border-radius:0;box-shadow:none;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;font-size:1em;font-weight:400;color:hsla(0,0%,100%,.3);cursor:pointer;appearance:none;-webkit-appearance:none;-moz-appearance:none}.wy-side-nav-search>div.switch-menus>div.language-switch select:active,.wy-side-nav-search>div.switch-menus>div.language-switch select:focus,.wy-side-nav-search>div.switch-menus>div.language-switch select:hover,.wy-side-nav-search>div.switch-menus>div.version-switch select:active,.wy-side-nav-search>div.switch-menus>div.version-switch select:focus,.wy-side-nav-search>div.switch-menus>div.version-switch select:hover{background:hsla(0,0%,100%,.1);color:hsla(0,0%,100%,.5)}.wy-side-nav-search>div.switch-menus>div.language-switch select option,.wy-side-nav-search>div.switch-menus>div.version-switch select option{color:#000}.wy-side-nav-search>div.switch-menus>div.language-switch:has(>select):after,.wy-side-nav-search>div.switch-menus>div.version-switch:has(>select):after{display:inline-block;width:1.5em;height:100%;padding:.1em;content:"\f0d7";font-size:1em;line-height:1.2em;font-family:FontAwesome;text-align:center;pointer-events:none;box-sizing:border-box}.wy-nav .wy-menu-vertical header{color:#2980b9}.wy-nav .wy-menu-vertical a{color:#b3b3b3}.wy-nav .wy-menu-vertical a:hover{background-color:#2980b9;color:#fff}[data-menu-wrap]{-webkit-transition:all .2s ease-in;-moz-transition:all .2s ease-in;transition:all .2s ease-in;position:absolute;opacity:1;width:100%;opacity:0}[data-menu-wrap].move-center{left:0;right:auto;opacity:1}[data-menu-wrap].move-left{right:auto;left:-100%;opacity:0}[data-menu-wrap].move-right{right:-100%;left:auto;opacity:0}.wy-body-for-nav{background:#fcfcfc}.wy-grid-for-nav{position:absolute;width:100%;height:100%}.wy-nav-side{position:fixed;top:0;bottom:0;left:0;padding-bottom:2em;width:300px;overflow-x:hidden;overflow-y:hidden;min-height:100%;color:#9b9b9b;background:#343131;z-index:200}.wy-side-scroll{width:320px;position:relative;overflow-x:hidden;overflow-y:scroll;height:100%}.wy-nav-top{display:none;background:#2980b9;color:#fff;padding:.4045em .809em;position:relative;line-height:50px;text-align:center;font-size:100%;*zoom:1}.wy-nav-top:after,.wy-nav-top:before{display:table;content:""}.wy-nav-top:after{clear:both}.wy-nav-top a{color:#fff;font-weight:700}.wy-nav-top img{margin-right:12px;height:45px;width:45px;background-color:#2980b9;padding:5px;border-radius:100%}.wy-nav-top i{font-size:30px;float:left;cursor:pointer;padding-top:inherit}.wy-nav-content-wrap{margin-left:300px;background:#fcfcfc;min-height:100%}.wy-nav-content{padding:1.618em 3.236em;height:100%;max-width:800px;margin:auto}.wy-body-mask{position:fixed;width:100%;height:100%;background:rgba(0,0,0,.2);display:none;z-index:499}.wy-body-mask.on{display:block}footer{color:grey}footer p{margin-bottom:12px}.rst-content footer span.commit tt,footer span.commit .rst-content tt,footer span.commit code{padding:0;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;font-size:1em;background:none;border:none;color:grey}.rst-footer-buttons{*zoom:1}.rst-footer-buttons:after,.rst-footer-buttons:before{width:100%;display:table;content:""}.rst-footer-buttons:after{clear:both}.rst-breadcrumbs-buttons{margin-top:12px;*zoom:1}.rst-breadcrumbs-buttons:after,.rst-breadcrumbs-buttons:before{display:table;content:""}.rst-breadcrumbs-buttons:after{clear:both}#search-results .search li{margin-bottom:24px;border-bottom:1px solid #e1e4e5;padding-bottom:24px}#search-results .search li:first-child{border-top:1px solid #e1e4e5;padding-top:24px}#search-results .search li a{font-size:120%;margin-bottom:12px;display:inline-block}#search-results .context{color:grey;font-size:90%}.genindextable li>ul{margin-left:24px}@media screen and (max-width:768px){.wy-body-for-nav{background:#fcfcfc}.wy-nav-top{display:block}.wy-nav-side{left:-300px}.wy-nav-side.shift{width:85%;left:0}.wy-menu.wy-menu-vertical,.wy-side-nav-search,.wy-side-scroll{width:auto}.wy-nav-content-wrap{margin-left:0}.wy-nav-content-wrap .wy-nav-content{padding:1.618em}.wy-nav-content-wrap.shift{position:fixed;min-width:100%;left:85%;top:0;height:100%;overflow:hidden}}@media screen and (min-width:1100px){.wy-nav-content-wrap{background:rgba(0,0,0,.05)}.wy-nav-content{margin:0;background:#fcfcfc}}@media print{.rst-versions,.wy-nav-side,footer{display:none}.wy-nav-content-wrap{margin-left:0}}.rst-versions{position:fixed;bottom:0;left:0;width:300px;color:#fcfcfc;background:#1f1d1d;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;z-index:400}.rst-versions a{color:#2980b9;text-decoration:none}.rst-versions .rst-badge-small{display:none}.rst-versions .rst-current-version{padding:12px;background-color:#272525;display:block;text-align:right;font-size:90%;cursor:pointer;color:#27ae60;*zoom:1}.rst-versions .rst-current-version:after,.rst-versions .rst-current-version:before{display:table;content:""}.rst-versions .rst-current-version:after{clear:both}.rst-content .code-block-caption .rst-versions .rst-current-version .headerlink,.rst-content .eqno .rst-versions .rst-current-version .headerlink,.rst-content .rst-versions .rst-current-version .admonition-title,.rst-content code.download .rst-versions .rst-current-version span:first-child,.rst-content dl dt .rst-versions .rst-current-version .headerlink,.rst-content h1 .rst-versions .rst-current-version .headerlink,.rst-content h2 .rst-versions .rst-current-version .headerlink,.rst-content h3 .rst-versions .rst-current-version .headerlink,.rst-content h4 .rst-versions .rst-current-version .headerlink,.rst-content h5 .rst-versions .rst-current-version .headerlink,.rst-content h6 .rst-versions .rst-current-version .headerlink,.rst-content p .rst-versions .rst-current-version .headerlink,.rst-content table>caption .rst-versions .rst-current-version .headerlink,.rst-content tt.download .rst-versions .rst-current-version span:first-child,.rst-versions .rst-current-version .fa,.rst-versions .rst-current-version .icon,.rst-versions .rst-current-version .rst-content .admonition-title,.rst-versions .rst-current-version .rst-content .code-block-caption .headerlink,.rst-versions .rst-current-version .rst-content .eqno .headerlink,.rst-versions .rst-current-version .rst-content code.download span:first-child,.rst-versions .rst-current-version .rst-content dl dt .headerlink,.rst-versions .rst-current-version .rst-content h1 .headerlink,.rst-versions .rst-current-version .rst-content h2 .headerlink,.rst-versions .rst-current-version .rst-content h3 .headerlink,.rst-versions .rst-current-version .rst-content h4 .headerlink,.rst-versions .rst-current-version .rst-content h5 .headerlink,.rst-versions .rst-current-version .rst-content h6 .headerlink,.rst-versions .rst-current-version .rst-content p .headerlink,.rst-versions .rst-current-version .rst-content table>caption .headerlink,.rst-versions .rst-current-version .rst-content tt.download span:first-child,.rst-versions .rst-current-version .wy-menu-vertical li button.toctree-expand,.wy-menu-vertical li .rst-versions .rst-current-version button.toctree-expand{color:#fcfcfc}.rst-versions .rst-current-version .fa-book,.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version.rst-out-of-date{background-color:#e74c3c;color:#fff}.rst-versions .rst-current-version.rst-active-old-version{background-color:#f1c40f;color:#000}.rst-versions.shift-up{height:auto;max-height:100%;overflow-y:scroll}.rst-versions.shift-up .rst-other-versions{display:block}.rst-versions .rst-other-versions{font-size:90%;padding:12px;color:grey;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:1px solid #413d3d}.rst-versions .rst-other-versions dd{display:inline-block;margin:0}.rst-versions .rst-other-versions dd a{display:inline-block;padding:6px;color:#fcfcfc}.rst-versions .rst-other-versions .rtd-current-item{font-weight:700}.rst-versions.rst-badge{width:auto;bottom:20px;right:20px;left:auto;border:none;max-width:300px;max-height:90%}.rst-versions.rst-badge .fa-book,.rst-versions.rst-badge .icon-book{float:none;line-height:30px}.rst-versions.rst-badge.shift-up .rst-current-version{text-align:right}.rst-versions.rst-badge.shift-up .rst-current-version .fa-book,.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge>.rst-current-version{width:auto;height:30px;line-height:30px;padding:0 6px;display:block;text-align:center}@media screen and (max-width:768px){.rst-versions{width:85%;display:none}.rst-versions.shift{display:block}}#flyout-search-form{padding:6px}.rst-content .toctree-wrapper>p.caption,.rst-content h1,.rst-content h2,.rst-content h3,.rst-content h4,.rst-content h5,.rst-content h6{margin-bottom:24px}.rst-content img{max-width:100%;height:auto}.rst-content div.figure,.rst-content figure{margin-bottom:24px}.rst-content div.figure .caption-text,.rst-content figure .caption-text{font-style:italic}.rst-content div.figure p:last-child.caption,.rst-content figure p:last-child.caption{margin-bottom:0}.rst-content div.figure.align-center,.rst-content figure.align-center{text-align:center}.rst-content .section>a>img,.rst-content .section>img,.rst-content section>a>img,.rst-content section>img{margin-bottom:24px}.rst-content abbr[title]{text-decoration:none}.rst-content.style-external-links a.reference.external:after{font-family:FontAwesome;content:"\f08e";color:#b3b3b3;vertical-align:super;font-size:60%;margin:0 .2em}.rst-content blockquote{margin-left:24px;line-height:24px;margin-bottom:24px}.rst-content pre.literal-block{white-space:pre;margin:0;padding:12px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;display:block;overflow:auto}.rst-content div[class^=highlight],.rst-content pre.literal-block{border:1px solid #e1e4e5;overflow-x:auto;margin:1px 0 24px}.rst-content div[class^=highlight] div[class^=highlight],.rst-content pre.literal-block div[class^=highlight]{padding:0;border:none;margin:0}.rst-content div[class^=highlight] td.code{width:100%}.rst-content .linenodiv pre{border-right:1px solid #e6e9ea;margin:0;padding:12px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;user-select:none;pointer-events:none}.rst-content div[class^=highlight] pre{white-space:pre;margin:0;padding:12px;display:block;overflow:auto}.rst-content div[class^=highlight] pre .hll{display:block;margin:0 -12px;padding:0 12px}.rst-content .linenodiv pre,.rst-content div[class^=highlight] pre,.rst-content pre.literal-block{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;font-size:12px;line-height:1.4}.rst-content div.highlight .gp,.rst-content div.highlight span.linenos{user-select:none;pointer-events:none}.rst-content div.highlight span.linenos{display:inline-block;padding-left:0;padding-right:12px;margin-right:12px;border-right:1px solid #e6e9ea}.rst-content .code-block-caption{font-style:italic;font-size:85%;line-height:1;padding:1em 0;text-align:center}@media print{.rst-content .codeblock,.rst-content div[class^=highlight],.rst-content div[class^=highlight] pre{white-space:pre-wrap}}.rst-content .admonition,.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .danger,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .note,.rst-content .seealso,.rst-content .tip,.rst-content .warning{clear:both}.rst-content .admonition-todo .last,.rst-content .admonition-todo>:last-child,.rst-content .admonition .last,.rst-content .admonition>:last-child,.rst-content .attention .last,.rst-content .attention>:last-child,.rst-content .caution .last,.rst-content .caution>:last-child,.rst-content .danger .last,.rst-content .danger>:last-child,.rst-content .error .last,.rst-content .error>:last-child,.rst-content .hint .last,.rst-content .hint>:last-child,.rst-content .important .last,.rst-content .important>:last-child,.rst-content .note .last,.rst-content .note>:last-child,.rst-content .seealso .last,.rst-content .seealso>:last-child,.rst-content .tip .last,.rst-content .tip>:last-child,.rst-content .warning .last,.rst-content .warning>:last-child{margin-bottom:0}.rst-content .admonition-title:before{margin-right:4px}.rst-content .admonition table{border-color:rgba(0,0,0,.1)}.rst-content .admonition table td,.rst-content .admonition table th{background:transparent!important;border-color:rgba(0,0,0,.1)!important}.rst-content .section ol.loweralpha,.rst-content .section ol.loweralpha>li,.rst-content .toctree-wrapper ol.loweralpha,.rst-content .toctree-wrapper ol.loweralpha>li,.rst-content section ol.loweralpha,.rst-content section ol.loweralpha>li{list-style:lower-alpha}.rst-content .section ol.upperalpha,.rst-content .section ol.upperalpha>li,.rst-content .toctree-wrapper ol.upperalpha,.rst-content .toctree-wrapper ol.upperalpha>li,.rst-content section ol.upperalpha,.rst-content section ol.upperalpha>li{list-style:upper-alpha}.rst-content .section ol li>*,.rst-content .section ul li>*,.rst-content .toctree-wrapper ol li>*,.rst-content .toctree-wrapper ul li>*,.rst-content section ol li>*,.rst-content section ul li>*{margin-top:12px;margin-bottom:12px}.rst-content .section ol li>:first-child,.rst-content .section ul li>:first-child,.rst-content .toctree-wrapper ol li>:first-child,.rst-content .toctree-wrapper ul li>:first-child,.rst-content section ol li>:first-child,.rst-content section ul li>:first-child{margin-top:0}.rst-content .section ol li>p,.rst-content .section ol li>p:last-child,.rst-content .section ul li>p,.rst-content .section ul li>p:last-child,.rst-content .toctree-wrapper ol li>p,.rst-content .toctree-wrapper ol li>p:last-child,.rst-content .toctree-wrapper ul li>p,.rst-content .toctree-wrapper ul li>p:last-child,.rst-content section ol li>p,.rst-content section ol li>p:last-child,.rst-content section ul li>p,.rst-content section ul li>p:last-child{margin-bottom:12px}.rst-content .section ol li>p:only-child,.rst-content .section ol li>p:only-child:last-child,.rst-content .section ul li>p:only-child,.rst-content .section ul li>p:only-child:last-child,.rst-content .toctree-wrapper ol li>p:only-child,.rst-content .toctree-wrapper ol li>p:only-child:last-child,.rst-content .toctree-wrapper ul li>p:only-child,.rst-content .toctree-wrapper ul li>p:only-child:last-child,.rst-content section ol li>p:only-child,.rst-content section ol li>p:only-child:last-child,.rst-content section ul li>p:only-child,.rst-content section ul li>p:only-child:last-child{margin-bottom:0}.rst-content .section ol li>ol,.rst-content .section ol li>ul,.rst-content .section ul li>ol,.rst-content .section ul li>ul,.rst-content .toctree-wrapper ol li>ol,.rst-content .toctree-wrapper ol li>ul,.rst-content .toctree-wrapper ul li>ol,.rst-content .toctree-wrapper ul li>ul,.rst-content section ol li>ol,.rst-content section ol li>ul,.rst-content section ul li>ol,.rst-content section ul li>ul{margin-bottom:12px}.rst-content .section ol.simple li>*,.rst-content .section ol.simple li ol,.rst-content .section ol.simple li ul,.rst-content .section ul.simple li>*,.rst-content .section ul.simple li ol,.rst-content .section ul.simple li ul,.rst-content .toctree-wrapper ol.simple li>*,.rst-content .toctree-wrapper ol.simple li ol,.rst-content .toctree-wrapper ol.simple li ul,.rst-content .toctree-wrapper ul.simple li>*,.rst-content .toctree-wrapper ul.simple li ol,.rst-content .toctree-wrapper ul.simple li ul,.rst-content section ol.simple li>*,.rst-content section ol.simple li ol,.rst-content section ol.simple li ul,.rst-content section ul.simple li>*,.rst-content section ul.simple li ol,.rst-content section ul.simple li ul{margin-top:0;margin-bottom:0}.rst-content .line-block{margin-left:0;margin-bottom:24px;line-height:24px}.rst-content .line-block .line-block{margin-left:24px;margin-bottom:0}.rst-content .topic-title{font-weight:700;margin-bottom:12px}.rst-content .toc-backref{color:#404040}.rst-content .align-right{float:right;margin:0 0 24px 24px}.rst-content .align-left{float:left;margin:0 24px 24px 0}.rst-content .align-center{margin:auto}.rst-content .align-center:not(table){display:block}.rst-content .code-block-caption .headerlink,.rst-content .eqno .headerlink,.rst-content .toctree-wrapper>p.caption .headerlink,.rst-content dl dt .headerlink,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content p.caption .headerlink,.rst-content p .headerlink,.rst-content table>caption .headerlink{opacity:0;font-size:14px;font-family:FontAwesome;margin-left:.5em}.rst-content .code-block-caption .headerlink:focus,.rst-content .code-block-caption:hover .headerlink,.rst-content .eqno .headerlink:focus,.rst-content .eqno:hover .headerlink,.rst-content .toctree-wrapper>p.caption .headerlink:focus,.rst-content .toctree-wrapper>p.caption:hover .headerlink,.rst-content dl dt .headerlink:focus,.rst-content dl dt:hover .headerlink,.rst-content h1 .headerlink:focus,.rst-content h1:hover .headerlink,.rst-content h2 .headerlink:focus,.rst-content h2:hover .headerlink,.rst-content h3 .headerlink:focus,.rst-content h3:hover .headerlink,.rst-content h4 .headerlink:focus,.rst-content h4:hover .headerlink,.rst-content h5 .headerlink:focus,.rst-content h5:hover .headerlink,.rst-content h6 .headerlink:focus,.rst-content h6:hover .headerlink,.rst-content p.caption .headerlink:focus,.rst-content p.caption:hover .headerlink,.rst-content p .headerlink:focus,.rst-content p:hover .headerlink,.rst-content table>caption .headerlink:focus,.rst-content table>caption:hover .headerlink{opacity:1}.rst-content p a{overflow-wrap:anywhere}.rst-content .wy-table td p,.rst-content .wy-table td ul,.rst-content .wy-table th p,.rst-content .wy-table th ul,.rst-content table.docutils td p,.rst-content table.docutils td ul,.rst-content table.docutils th p,.rst-content table.docutils th ul,.rst-content table.field-list td p,.rst-content table.field-list td ul,.rst-content table.field-list th p,.rst-content table.field-list th ul{font-size:inherit}.rst-content .btn:focus{outline:2px solid}.rst-content table>caption .headerlink:after{font-size:12px}.rst-content .centered{text-align:center}.rst-content .sidebar{float:right;width:40%;display:block;margin:0 0 24px 24px;padding:24px;background:#f3f6f6;border:1px solid #e1e4e5}.rst-content .sidebar dl,.rst-content .sidebar p,.rst-content .sidebar ul{font-size:90%}.rst-content .sidebar .last,.rst-content .sidebar>:last-child{margin-bottom:0}.rst-content .sidebar .sidebar-title{display:block;font-family:Roboto Slab,ff-tisa-web-pro,Georgia,Arial,sans-serif;font-weight:700;background:#e1e4e5;padding:6px 12px;margin:-24px -24px 24px;font-size:100%}.rst-content .highlighted{background:#f1c40f;box-shadow:0 0 0 2px #f1c40f;display:inline;font-weight:700}.rst-content .citation-reference,.rst-content .footnote-reference{vertical-align:baseline;position:relative;top:-.4em;line-height:0;font-size:90%}.rst-content .citation-reference>span.fn-bracket,.rst-content .footnote-reference>span.fn-bracket{display:none}.rst-content .hlist{width:100%}.rst-content dl dt span.classifier:before{content:" : "}.rst-content dl dt span.classifier-delimiter{display:none!important}html.writer-html4 .rst-content table.docutils.citation,html.writer-html4 .rst-content table.docutils.footnote{background:none;border:none}html.writer-html4 .rst-content table.docutils.citation td,html.writer-html4 .rst-content table.docutils.citation tr,html.writer-html4 .rst-content table.docutils.footnote td,html.writer-html4 .rst-content table.docutils.footnote tr{border:none;background-color:transparent!important;white-space:normal}html.writer-html4 .rst-content table.docutils.citation td.label,html.writer-html4 .rst-content table.docutils.footnote td.label{padding-left:0;padding-right:0;vertical-align:top}html.writer-html5 .rst-content dl.citation,html.writer-html5 .rst-content dl.field-list,html.writer-html5 .rst-content dl.footnote{display:grid;grid-template-columns:auto minmax(80%,95%)}html.writer-html5 .rst-content dl.citation>dt,html.writer-html5 .rst-content dl.field-list>dt,html.writer-html5 .rst-content dl.footnote>dt{display:inline-grid;grid-template-columns:max-content auto}html.writer-html5 .rst-content aside.citation,html.writer-html5 .rst-content aside.footnote,html.writer-html5 .rst-content div.citation{display:grid;grid-template-columns:auto auto minmax(.65rem,auto) minmax(40%,95%)}html.writer-html5 .rst-content aside.citation>span.label,html.writer-html5 .rst-content aside.footnote>span.label,html.writer-html5 .rst-content div.citation>span.label{grid-column-start:1;grid-column-end:2}html.writer-html5 .rst-content aside.citation>span.backrefs,html.writer-html5 .rst-content aside.footnote>span.backrefs,html.writer-html5 .rst-content div.citation>span.backrefs{grid-column-start:2;grid-column-end:3;grid-row-start:1;grid-row-end:3}html.writer-html5 .rst-content aside.citation>p,html.writer-html5 .rst-content aside.footnote>p,html.writer-html5 .rst-content div.citation>p{grid-column-start:4;grid-column-end:5}html.writer-html5 .rst-content dl.citation,html.writer-html5 .rst-content dl.field-list,html.writer-html5 .rst-content dl.footnote{margin-bottom:24px}html.writer-html5 .rst-content dl.citation>dt,html.writer-html5 .rst-content dl.field-list>dt,html.writer-html5 .rst-content dl.footnote>dt{padding-left:1rem}html.writer-html5 .rst-content dl.citation>dd,html.writer-html5 .rst-content dl.citation>dt,html.writer-html5 .rst-content dl.field-list>dd,html.writer-html5 .rst-content dl.field-list>dt,html.writer-html5 .rst-content dl.footnote>dd,html.writer-html5 .rst-content dl.footnote>dt{margin-bottom:0}html.writer-html5 .rst-content dl.citation,html.writer-html5 .rst-content dl.footnote{font-size:.9rem}html.writer-html5 .rst-content dl.citation>dt,html.writer-html5 .rst-content dl.footnote>dt{margin:0 .5rem .5rem 0;line-height:1.2rem;word-break:break-all;font-weight:400}html.writer-html5 .rst-content dl.citation>dt>span.brackets:before,html.writer-html5 .rst-content dl.footnote>dt>span.brackets:before{content:"["}html.writer-html5 .rst-content dl.citation>dt>span.brackets:after,html.writer-html5 .rst-content dl.footnote>dt>span.brackets:after{content:"]"}html.writer-html5 .rst-content dl.citation>dt>span.fn-backref,html.writer-html5 .rst-content dl.footnote>dt>span.fn-backref{text-align:left;font-style:italic;margin-left:.65rem;word-break:break-word;word-spacing:-.1rem;max-width:5rem}html.writer-html5 .rst-content dl.citation>dt>span.fn-backref>a,html.writer-html5 .rst-content dl.footnote>dt>span.fn-backref>a{word-break:keep-all}html.writer-html5 .rst-content dl.citation>dt>span.fn-backref>a:not(:first-child):before,html.writer-html5 .rst-content dl.footnote>dt>span.fn-backref>a:not(:first-child):before{content:" "}html.writer-html5 .rst-content dl.citation>dd,html.writer-html5 .rst-content dl.footnote>dd{margin:0 0 .5rem;line-height:1.2rem}html.writer-html5 .rst-content dl.citation>dd p,html.writer-html5 .rst-content dl.footnote>dd p{font-size:.9rem}html.writer-html5 .rst-content aside.citation,html.writer-html5 .rst-content aside.footnote,html.writer-html5 .rst-content div.citation{padding-left:1rem;padding-right:1rem;font-size:.9rem;line-height:1.2rem}html.writer-html5 .rst-content aside.citation p,html.writer-html5 .rst-content aside.footnote p,html.writer-html5 .rst-content div.citation p{font-size:.9rem;line-height:1.2rem;margin-bottom:12px}html.writer-html5 .rst-content aside.citation span.backrefs,html.writer-html5 .rst-content aside.footnote span.backrefs,html.writer-html5 .rst-content div.citation span.backrefs{text-align:left;font-style:italic;margin-left:.65rem;word-break:break-word;word-spacing:-.1rem;max-width:5rem}html.writer-html5 .rst-content aside.citation span.backrefs>a,html.writer-html5 .rst-content aside.footnote span.backrefs>a,html.writer-html5 .rst-content div.citation span.backrefs>a{word-break:keep-all}html.writer-html5 .rst-content aside.citation span.backrefs>a:not(:first-child):before,html.writer-html5 .rst-content aside.footnote span.backrefs>a:not(:first-child):before,html.writer-html5 .rst-content div.citation span.backrefs>a:not(:first-child):before{content:" "}html.writer-html5 .rst-content aside.citation span.label,html.writer-html5 .rst-content aside.footnote span.label,html.writer-html5 .rst-content div.citation span.label{line-height:1.2rem}html.writer-html5 .rst-content aside.citation-list,html.writer-html5 .rst-content aside.footnote-list,html.writer-html5 .rst-content div.citation-list{margin-bottom:24px}html.writer-html5 .rst-content dl.option-list kbd{font-size:.9rem}.rst-content table.docutils.footnote,html.writer-html4 .rst-content table.docutils.citation,html.writer-html5 .rst-content aside.footnote,html.writer-html5 .rst-content aside.footnote-list aside.footnote,html.writer-html5 .rst-content div.citation-list>div.citation,html.writer-html5 .rst-content dl.citation,html.writer-html5 .rst-content dl.footnote{color:grey}.rst-content table.docutils.footnote code,.rst-content table.docutils.footnote tt,html.writer-html4 .rst-content table.docutils.citation code,html.writer-html4 .rst-content table.docutils.citation tt,html.writer-html5 .rst-content aside.footnote-list aside.footnote code,html.writer-html5 .rst-content aside.footnote-list aside.footnote tt,html.writer-html5 .rst-content aside.footnote code,html.writer-html5 .rst-content aside.footnote tt,html.writer-html5 .rst-content div.citation-list>div.citation code,html.writer-html5 .rst-content div.citation-list>div.citation tt,html.writer-html5 .rst-content dl.citation code,html.writer-html5 .rst-content dl.citation tt,html.writer-html5 .rst-content dl.footnote code,html.writer-html5 .rst-content dl.footnote tt{color:#555}.rst-content .wy-table-responsive.citation,.rst-content .wy-table-responsive.footnote{margin-bottom:0}.rst-content .wy-table-responsive.citation+:not(.citation),.rst-content .wy-table-responsive.footnote+:not(.footnote){margin-top:24px}.rst-content .wy-table-responsive.citation:last-child,.rst-content .wy-table-responsive.footnote:last-child{margin-bottom:24px}.rst-content table.docutils th{border-color:#e1e4e5}html.writer-html5 .rst-content table.docutils th{border:1px solid #e1e4e5}html.writer-html5 .rst-content table.docutils td>p,html.writer-html5 .rst-content table.docutils th>p{line-height:1rem;margin-bottom:0;font-size:.9rem}.rst-content table.docutils td .last,.rst-content table.docutils td .last>:last-child{margin-bottom:0}.rst-content table.field-list,.rst-content table.field-list td{border:none}.rst-content table.field-list td p{line-height:inherit}.rst-content table.field-list td>strong{display:inline-block}.rst-content table.field-list .field-name{padding-right:10px;text-align:left;white-space:nowrap}.rst-content table.field-list .field-body{text-align:left}.rst-content code,.rst-content tt{color:#000;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;padding:2px 5px}.rst-content code big,.rst-content code em,.rst-content tt big,.rst-content tt em{font-size:100%!important;line-height:normal}.rst-content code.literal,.rst-content tt.literal{color:#e74c3c;white-space:normal}.rst-content code.xref,.rst-content tt.xref,a .rst-content code,a .rst-content tt{font-weight:700;color:#404040;overflow-wrap:normal}.rst-content kbd,.rst-content pre,.rst-content samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace}.rst-content a code,.rst-content a tt{color:#2980b9}.rst-content dl{margin-bottom:24px}.rst-content dl dt{font-weight:700;margin-bottom:12px}.rst-content dl ol,.rst-content dl p,.rst-content dl table,.rst-content dl ul{margin-bottom:12px}.rst-content dl dd{margin:0 0 12px 24px;line-height:24px}.rst-content dl dd>ol:last-child,.rst-content dl dd>p:last-child,.rst-content dl dd>table:last-child,.rst-content dl dd>ul:last-child{margin-bottom:0}html.writer-html4 .rst-content dl:not(.docutils),html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple){margin-bottom:24px}html.writer-html4 .rst-content dl:not(.docutils)>dt,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt{display:table;margin:6px 0;font-size:90%;line-height:normal;background:#e7f2fa;color:#2980b9;border-top:3px solid #6ab0de;padding:6px;position:relative}html.writer-html4 .rst-content dl:not(.docutils)>dt:before,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt:before{color:#6ab0de}html.writer-html4 .rst-content dl:not(.docutils)>dt .headerlink,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt .headerlink{color:#404040;font-size:100%!important}html.writer-html4 .rst-content dl:not(.docutils) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt{margin-bottom:6px;border:none;border-left:3px solid #ccc;background:#f0f0f0;color:#555}html.writer-html4 .rst-content dl:not(.docutils) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt .headerlink,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt .headerlink{color:#404040;font-size:100%!important}html.writer-html4 .rst-content dl:not(.docutils)>dt:first-child,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt:first-child{margin-top:0}html.writer-html4 .rst-content dl:not(.docutils) code.descclassname,html.writer-html4 .rst-content dl:not(.docutils) code.descname,html.writer-html4 .rst-content dl:not(.docutils) tt.descclassname,html.writer-html4 .rst-content dl:not(.docutils) tt.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) code.descclassname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) code.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) tt.descclassname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) tt.descname{background-color:transparent;border:none;padding:0;font-size:100%!important}html.writer-html4 .rst-content dl:not(.docutils) code.descname,html.writer-html4 .rst-content dl:not(.docutils) tt.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) code.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) tt.descname{font-weight:700}html.writer-html4 .rst-content dl:not(.docutils) .optional,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .optional{display:inline-block;padding:0 4px;color:#000;font-weight:700}html.writer-html4 .rst-content dl:not(.docutils) .property,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .property{display:inline-block;padding-right:8px;max-width:100%}html.writer-html4 .rst-content dl:not(.docutils) .k,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .k{font-style:italic}html.writer-html4 .rst-content dl:not(.docutils) .descclassname,html.writer-html4 .rst-content dl:not(.docutils) .descname,html.writer-html4 .rst-content dl:not(.docutils) .sig-name,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .descclassname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .sig-name{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;color:#000}.rst-content .viewcode-back,.rst-content .viewcode-link{display:inline-block;color:#27ae60;font-size:80%;padding-left:24px}.rst-content .viewcode-back{display:block;float:right}.rst-content p.rubric{margin-bottom:12px;font-weight:700}.rst-content code.download,.rst-content tt.download{background:inherit;padding:inherit;font-weight:400;font-family:inherit;font-size:inherit;color:inherit;border:inherit;white-space:inherit}.rst-content code.download span:first-child,.rst-content tt.download span:first-child{-webkit-font-smoothing:subpixel-antialiased}.rst-content code.download span:first-child:before,.rst-content tt.download span:first-child:before{margin-right:4px}.rst-content .guilabel,.rst-content .menuselection{font-size:80%;font-weight:700;border-radius:4px;padding:2.4px 6px;margin:auto 2px}.rst-content .guilabel,.rst-content .menuselection{border:1px solid #7fbbe3;background:#e7f2fa}.rst-content :not(dl.option-list)>:not(dt):not(kbd):not(.kbd)>.kbd,.rst-content :not(dl.option-list)>:not(dt):not(kbd):not(.kbd)>kbd{color:inherit;font-size:80%;background-color:#fff;border:1px solid #a6a6a6;border-radius:4px;box-shadow:0 2px grey;padding:2.4px 6px;margin:auto 0}.rst-content .versionmodified{font-style:italic}@media screen and (max-width:480px){.rst-content .sidebar{width:100%}}span[id*=MathJax-Span]{color:#404040}.math{text-align:center}@font-face{font-family:Lato;src:url(fonts/lato-normal.woff2?bd03a2cc277bbbc338d464e679fe9942) format("woff2"),url(fonts/lato-normal.woff?27bd77b9162d388cb8d4c4217c7c5e2a) format("woff");font-weight:400;font-style:normal;font-display:block}@font-face{font-family:Lato;src:url(fonts/lato-bold.woff2?cccb897485813c7c256901dbca54ecf2) format("woff2"),url(fonts/lato-bold.woff?d878b6c29b10beca227e9eef4246111b) format("woff");font-weight:700;font-style:normal;font-display:block}@font-face{font-family:Lato;src:url(fonts/lato-bold-italic.woff2?0b6bb6725576b072c5d0b02ecdd1900d) format("woff2"),url(fonts/lato-bold-italic.woff?9c7e4e9eb485b4a121c760e61bc3707c) format("woff");font-weight:700;font-style:italic;font-display:block}@font-face{font-family:Lato;src:url(fonts/lato-normal-italic.woff2?4eb103b4d12be57cb1d040ed5e162e9d) format("woff2"),url(fonts/lato-normal-italic.woff?f28f2d6482446544ef1ea1ccc6dd5892) format("woff");font-weight:400;font-style:italic;font-display:block}@font-face{font-family:Roboto Slab;font-style:normal;font-weight:400;src:url(fonts/Roboto-Slab-Regular.woff2?7abf5b8d04d26a2cafea937019bca958) format("woff2"),url(fonts/Roboto-Slab-Regular.woff?c1be9284088d487c5e3ff0a10a92e58c) format("woff");font-display:block}@font-face{font-family:Roboto Slab;font-style:normal;font-weight:700;src:url(fonts/Roboto-Slab-Bold.woff2?9984f4a9bda09be08e83f2506954adbe) format("woff2"),url(fonts/Roboto-Slab-Bold.woff?bed5564a116b05148e3b3bea6fb1162a) format("woff");font-display:block} \ No newline at end of file diff --git a/docs/_static/doctools.js b/docs/_static/doctools.js deleted file mode 100644 index 0398ebb..0000000 --- a/docs/_static/doctools.js +++ /dev/null @@ -1,149 +0,0 @@ -/* - * Base JavaScript utilities for all Sphinx HTML documentation. - */ -"use strict"; - -const BLACKLISTED_KEY_CONTROL_ELEMENTS = new Set([ - "TEXTAREA", - "INPUT", - "SELECT", - "BUTTON", -]); - -const _ready = (callback) => { - if (document.readyState !== "loading") { - callback(); - } else { - document.addEventListener("DOMContentLoaded", callback); - } -}; - -/** - * Small JavaScript module for the documentation. - */ -const Documentation = { - init: () => { - Documentation.initDomainIndexTable(); - Documentation.initOnKeyListeners(); - }, - - /** - * i18n support - */ - TRANSLATIONS: {}, - PLURAL_EXPR: (n) => (n === 1 ? 0 : 1), - LOCALE: "unknown", - - // gettext and ngettext don't access this so that the functions - // can safely bound to a different name (_ = Documentation.gettext) - gettext: (string) => { - const translated = Documentation.TRANSLATIONS[string]; - switch (typeof translated) { - case "undefined": - return string; // no translation - case "string": - return translated; // translation exists - default: - return translated[0]; // (singular, plural) translation tuple exists - } - }, - - ngettext: (singular, plural, n) => { - const translated = Documentation.TRANSLATIONS[singular]; - if (typeof translated !== "undefined") - return translated[Documentation.PLURAL_EXPR(n)]; - return n === 1 ? singular : plural; - }, - - addTranslations: (catalog) => { - Object.assign(Documentation.TRANSLATIONS, catalog.messages); - Documentation.PLURAL_EXPR = new Function( - "n", - `return (${catalog.plural_expr})` - ); - Documentation.LOCALE = catalog.locale; - }, - - /** - * helper function to focus on search bar - */ - focusSearchBar: () => { - document.querySelectorAll("input[name=q]")[0]?.focus(); - }, - - /** - * Initialise the domain index toggle buttons - */ - initDomainIndexTable: () => { - const toggler = (el) => { - const idNumber = el.id.substr(7); - const toggledRows = document.querySelectorAll(`tr.cg-${idNumber}`); - if (el.src.substr(-9) === "minus.png") { - el.src = `${el.src.substr(0, el.src.length - 9)}plus.png`; - toggledRows.forEach((el) => (el.style.display = "none")); - } else { - el.src = `${el.src.substr(0, el.src.length - 8)}minus.png`; - toggledRows.forEach((el) => (el.style.display = "")); - } - }; - - const togglerElements = document.querySelectorAll("img.toggler"); - togglerElements.forEach((el) => - el.addEventListener("click", (event) => toggler(event.currentTarget)) - ); - togglerElements.forEach((el) => (el.style.display = "")); - if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) togglerElements.forEach(toggler); - }, - - initOnKeyListeners: () => { - // only install a listener if it is really needed - if ( - !DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS && - !DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS - ) - return; - - document.addEventListener("keydown", (event) => { - // bail for input elements - if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) return; - // bail with special keys - if (event.altKey || event.ctrlKey || event.metaKey) return; - - if (!event.shiftKey) { - switch (event.key) { - case "ArrowLeft": - if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; - - const prevLink = document.querySelector('link[rel="prev"]'); - if (prevLink && prevLink.href) { - window.location.href = prevLink.href; - event.preventDefault(); - } - break; - case "ArrowRight": - if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; - - const nextLink = document.querySelector('link[rel="next"]'); - if (nextLink && nextLink.href) { - window.location.href = nextLink.href; - event.preventDefault(); - } - break; - } - } - - // some keyboard layouts may need Shift to get / - switch (event.key) { - case "/": - if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) break; - Documentation.focusSearchBar(); - event.preventDefault(); - } - }); - }, -}; - -// quick alias for translations -const _ = Documentation.gettext; - -_ready(Documentation.init); diff --git a/docs/_static/documentation_options.js b/docs/_static/documentation_options.js deleted file mode 100644 index 7e4c114..0000000 --- a/docs/_static/documentation_options.js +++ /dev/null @@ -1,13 +0,0 @@ -const DOCUMENTATION_OPTIONS = { - VERSION: '', - LANGUAGE: 'en', - COLLAPSE_INDEX: false, - BUILDER: 'html', - FILE_SUFFIX: '.html', - LINK_SUFFIX: '.html', - HAS_SOURCE: true, - SOURCELINK_SUFFIX: '.txt', - NAVIGATION_WITH_KEYS: false, - SHOW_SEARCH_SUMMARY: true, - ENABLE_SEARCH_SHORTCUTS: true, -}; \ No newline at end of file diff --git a/docs/_static/file.png b/docs/_static/file.png deleted file mode 100644 index a858a41..0000000 Binary files a/docs/_static/file.png and /dev/null differ diff --git a/docs/_static/fonts/Lato/lato-bold.eot b/docs/_static/fonts/Lato/lato-bold.eot deleted file mode 100644 index 3361183..0000000 Binary files a/docs/_static/fonts/Lato/lato-bold.eot and /dev/null differ diff --git a/docs/_static/fonts/Lato/lato-bold.ttf b/docs/_static/fonts/Lato/lato-bold.ttf deleted file mode 100644 index 29f691d..0000000 Binary files a/docs/_static/fonts/Lato/lato-bold.ttf and /dev/null differ diff --git a/docs/_static/fonts/Lato/lato-bold.woff b/docs/_static/fonts/Lato/lato-bold.woff deleted file mode 100644 index c6dff51..0000000 Binary files a/docs/_static/fonts/Lato/lato-bold.woff and /dev/null differ diff --git a/docs/_static/fonts/Lato/lato-bold.woff2 b/docs/_static/fonts/Lato/lato-bold.woff2 deleted file mode 100644 index bb19504..0000000 Binary files a/docs/_static/fonts/Lato/lato-bold.woff2 and /dev/null differ diff --git a/docs/_static/fonts/Lato/lato-bolditalic.eot b/docs/_static/fonts/Lato/lato-bolditalic.eot deleted file mode 100644 index 3d41549..0000000 Binary files a/docs/_static/fonts/Lato/lato-bolditalic.eot and /dev/null differ diff --git a/docs/_static/fonts/Lato/lato-bolditalic.ttf b/docs/_static/fonts/Lato/lato-bolditalic.ttf deleted file mode 100644 index f402040..0000000 Binary files a/docs/_static/fonts/Lato/lato-bolditalic.ttf and /dev/null differ diff --git a/docs/_static/fonts/Lato/lato-bolditalic.woff b/docs/_static/fonts/Lato/lato-bolditalic.woff deleted file mode 100644 index 88ad05b..0000000 Binary files a/docs/_static/fonts/Lato/lato-bolditalic.woff and /dev/null differ diff --git a/docs/_static/fonts/Lato/lato-bolditalic.woff2 b/docs/_static/fonts/Lato/lato-bolditalic.woff2 deleted file mode 100644 index c4e3d80..0000000 Binary files a/docs/_static/fonts/Lato/lato-bolditalic.woff2 and /dev/null differ diff --git a/docs/_static/fonts/Lato/lato-italic.eot b/docs/_static/fonts/Lato/lato-italic.eot deleted file mode 100644 index 3f82642..0000000 Binary files a/docs/_static/fonts/Lato/lato-italic.eot and /dev/null differ diff --git a/docs/_static/fonts/Lato/lato-italic.ttf b/docs/_static/fonts/Lato/lato-italic.ttf deleted file mode 100644 index b4bfc9b..0000000 Binary files a/docs/_static/fonts/Lato/lato-italic.ttf and /dev/null differ diff --git a/docs/_static/fonts/Lato/lato-italic.woff b/docs/_static/fonts/Lato/lato-italic.woff deleted file mode 100644 index 76114bc..0000000 Binary files a/docs/_static/fonts/Lato/lato-italic.woff and /dev/null differ diff --git a/docs/_static/fonts/Lato/lato-italic.woff2 b/docs/_static/fonts/Lato/lato-italic.woff2 deleted file mode 100644 index 3404f37..0000000 Binary files a/docs/_static/fonts/Lato/lato-italic.woff2 and /dev/null differ diff --git a/docs/_static/fonts/Lato/lato-regular.eot b/docs/_static/fonts/Lato/lato-regular.eot deleted file mode 100644 index 11e3f2a..0000000 Binary files a/docs/_static/fonts/Lato/lato-regular.eot and /dev/null differ diff --git a/docs/_static/fonts/Lato/lato-regular.ttf b/docs/_static/fonts/Lato/lato-regular.ttf deleted file mode 100644 index 74decd9..0000000 Binary files a/docs/_static/fonts/Lato/lato-regular.ttf and /dev/null differ diff --git a/docs/_static/fonts/Lato/lato-regular.woff b/docs/_static/fonts/Lato/lato-regular.woff deleted file mode 100644 index ae1307f..0000000 Binary files a/docs/_static/fonts/Lato/lato-regular.woff and /dev/null differ diff --git a/docs/_static/fonts/Lato/lato-regular.woff2 b/docs/_static/fonts/Lato/lato-regular.woff2 deleted file mode 100644 index 3bf9843..0000000 Binary files a/docs/_static/fonts/Lato/lato-regular.woff2 and /dev/null differ diff --git a/docs/_static/fonts/RobotoSlab/roboto-slab-v7-bold.eot b/docs/_static/fonts/RobotoSlab/roboto-slab-v7-bold.eot deleted file mode 100644 index 79dc8ef..0000000 Binary files a/docs/_static/fonts/RobotoSlab/roboto-slab-v7-bold.eot and /dev/null differ diff --git a/docs/_static/fonts/RobotoSlab/roboto-slab-v7-bold.ttf b/docs/_static/fonts/RobotoSlab/roboto-slab-v7-bold.ttf deleted file mode 100644 index df5d1df..0000000 Binary files a/docs/_static/fonts/RobotoSlab/roboto-slab-v7-bold.ttf and /dev/null differ diff --git a/docs/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff b/docs/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff deleted file mode 100644 index 6cb6000..0000000 Binary files a/docs/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff and /dev/null differ diff --git a/docs/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff2 b/docs/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff2 deleted file mode 100644 index 7059e23..0000000 Binary files a/docs/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff2 and /dev/null differ diff --git a/docs/_static/fonts/RobotoSlab/roboto-slab-v7-regular.eot b/docs/_static/fonts/RobotoSlab/roboto-slab-v7-regular.eot deleted file mode 100644 index 2f7ca78..0000000 Binary files a/docs/_static/fonts/RobotoSlab/roboto-slab-v7-regular.eot and /dev/null differ diff --git a/docs/_static/fonts/RobotoSlab/roboto-slab-v7-regular.ttf b/docs/_static/fonts/RobotoSlab/roboto-slab-v7-regular.ttf deleted file mode 100644 index eb52a79..0000000 Binary files a/docs/_static/fonts/RobotoSlab/roboto-slab-v7-regular.ttf and /dev/null differ diff --git a/docs/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff b/docs/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff deleted file mode 100644 index f815f63..0000000 Binary files a/docs/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff and /dev/null differ diff --git a/docs/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff2 b/docs/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff2 deleted file mode 100644 index f2c76e5..0000000 Binary files a/docs/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff2 and /dev/null differ diff --git a/docs/_static/jquery.js b/docs/_static/jquery.js deleted file mode 100644 index c4c6022..0000000 --- a/docs/_static/jquery.js +++ /dev/null @@ -1,2 +0,0 @@ -/*! jQuery v3.6.0 | (c) OpenJS Foundation and other contributors | jquery.org/license */ -!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],r=Object.getPrototypeOf,s=t.slice,g=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},x=function(e){return null!=e&&e===e.window},E=C.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.6.0",S=function(e,t){return new S.fn.init(e,t)};function p(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ye(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=S)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return g(t.replace($,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[S]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e&&e.namespaceURI,n=e&&(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!C.getElementsByName||!C.getElementsByName(S).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+S+"-]").length||v.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||v.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||v.push(".#.+[+~]"),e.querySelectorAll("\\\f"),v.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},j=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&y(p,e)?-1:t==C||t.ownerDocument==p&&y(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!N[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?S.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?S.grep(e,function(e){return e===n!==r}):"string"!=typeof n?S.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(S.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||D,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof S?t[0]:t,S.merge(this,S.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&S.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(S):S.makeArray(e,this)}).prototype=S.fn,D=S(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}S.fn.extend({has:function(e){var t=S(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=E.createDocumentFragment().appendChild(E.createElement("div")),(fe=E.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),y.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="",y.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML="",y.option=!!ce.lastChild;var ge={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;n",""]);var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function je(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function De(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function qe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Le(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var _t,zt=[],Ut=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=zt.pop()||S.expando+"_"+wt.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Ut.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Ut.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Ut,"$1"+r):!1!==e.jsonp&&(e.url+=(Tt.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,zt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((_t=E.implementation.createHTMLDocument("").body).innerHTML="
",2===_t.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return $(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=Fe(y.pixelPosition,function(e,t){if(t)return t=We(e,n),Pe.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return $(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){S.fn[n]=function(e,t){return 0"),n("table.docutils.footnote").wrap("
"),n("table.docutils.citation").wrap("
"),n(".wy-menu-vertical ul").not(".simple").siblings("a").each((function(){var t=n(this);expand=n(''),expand.on("click",(function(n){return e.toggleCurrent(t),n.stopPropagation(),!1})),t.prepend(expand)}))},reset:function(){var n=encodeURI(window.location.hash)||"#";try{var e=$(".wy-menu-vertical"),t=e.find('[href="'+n+'"]');if(0===t.length){var i=$('.document [id="'+n.substring(1)+'"]').closest("div.section");0===(t=e.find('[href="#'+i.attr("id")+'"]')).length&&(t=e.find('[href="#"]'))}if(t.length>0){$(".wy-menu-vertical .current").removeClass("current").attr("aria-expanded","false"),t.addClass("current").attr("aria-expanded","true"),t.closest("li.toctree-l1").parent().addClass("current").attr("aria-expanded","true");for(let n=1;n<=10;n++)t.closest("li.toctree-l"+n).addClass("current").attr("aria-expanded","true");t[0].scrollIntoView()}}catch(n){console.log("Error expanding nav for anchor",n)}},onScroll:function(){this.winScroll=!1;var n=this.win.scrollTop(),e=n+this.winHeight,t=this.navBar.scrollTop()+(n-this.winPosition);n<0||e>this.docHeight||(this.navBar.scrollTop(t),this.winPosition=n)},onResize:function(){this.winResize=!1,this.winHeight=this.win.height(),this.docHeight=$(document).height()},hashChange:function(){this.linkScroll=!0,this.win.one("hashchange",(function(){this.linkScroll=!1}))},toggleCurrent:function(n){var e=n.closest("li");e.siblings("li.current").removeClass("current").attr("aria-expanded","false"),e.siblings().find("li.current").removeClass("current").attr("aria-expanded","false");var t=e.find("> ul li");t.length&&(t.removeClass("current").attr("aria-expanded","false"),e.toggleClass("current").attr("aria-expanded",(function(n,e){return"true"==e?"false":"true"})))}},"undefined"!=typeof window&&(window.SphinxRtdTheme={Navigation:n.exports.ThemeNav,StickyNav:n.exports.ThemeNav}),function(){for(var n=0,e=["ms","moz","webkit","o"],t=0;t a.language.name.localeCompare(b.language.name)); - - const languagesHTML = ` -
-
Languages
- ${languages - .map( - (translation) => ` -
- ${translation.language.code} -
- `, - ) - .join("\n")} -
- `; - return languagesHTML; - } - - function renderVersions(config) { - if (!config.versions.active.length) { - return ""; - } - const versionsHTML = ` -
-
Versions
- ${config.versions.active - .map( - (version) => ` -
- ${version.slug} -
- `, - ) - .join("\n")} -
- `; - return versionsHTML; - } - - function renderDownloads(config) { - if (!Object.keys(config.versions.current.downloads).length) { - return ""; - } - const downloadsNameDisplay = { - pdf: "PDF", - epub: "Epub", - htmlzip: "HTML", - }; - - const downloadsHTML = ` -
-
Downloads
- ${Object.entries(config.versions.current.downloads) - .map( - ([name, url]) => ` -
- ${downloadsNameDisplay[name]} -
- `, - ) - .join("\n")} -
- `; - return downloadsHTML; - } - - document.addEventListener("readthedocs-addons-data-ready", function (event) { - const config = event.detail.data(); - - const flyout = ` -
- - Read the Docs - v: ${config.versions.current.slug} - - -
-
- ${renderLanguages(config)} - ${renderVersions(config)} - ${renderDownloads(config)} -
-
On Read the Docs
-
- Project Home -
-
- Builds -
-
- Downloads -
-
-
-
Search
-
-
- -
-
-
-
- - Hosted by Read the Docs - -
-
- `; - - // Inject the generated flyout into the body HTML element. - document.body.insertAdjacentHTML("beforeend", flyout); - - // Trigger the Read the Docs Addons Search modal when clicking on the "Search docs" input from inside the flyout. - document - .querySelector("#flyout-search-form") - .addEventListener("focusin", () => { - const event = new CustomEvent("readthedocs-search-show"); - document.dispatchEvent(event); - }); - }) -} - -if (themeLanguageSelector || themeVersionSelector) { - function onSelectorSwitch(event) { - const option = event.target.selectedIndex; - const item = event.target.options[option]; - window.location.href = item.dataset.url; - } - - document.addEventListener("readthedocs-addons-data-ready", function (event) { - const config = event.detail.data(); - - const versionSwitch = document.querySelector( - "div.switch-menus > div.version-switch", - ); - if (themeVersionSelector) { - let versions = config.versions.active; - if (config.versions.current.hidden || config.versions.current.type === "external") { - versions.unshift(config.versions.current); - } - const versionSelect = ` - - `; - - versionSwitch.innerHTML = versionSelect; - versionSwitch.firstElementChild.addEventListener("change", onSelectorSwitch); - } - - const languageSwitch = document.querySelector( - "div.switch-menus > div.language-switch", - ); - - if (themeLanguageSelector) { - if (config.projects.translations.length) { - // Add the current language to the options on the selector - let languages = config.projects.translations.concat( - config.projects.current, - ); - languages = languages.sort((a, b) => - a.language.name.localeCompare(b.language.name), - ); - - const languageSelect = ` - - `; - - languageSwitch.innerHTML = languageSelect; - languageSwitch.firstElementChild.addEventListener("change", onSelectorSwitch); - } - else { - languageSwitch.remove(); - } - } - }); -} - -document.addEventListener("readthedocs-addons-data-ready", function (event) { - // Trigger the Read the Docs Addons Search modal when clicking on "Search docs" input from the topnav. - document - .querySelector("[role='search'] input") - .addEventListener("focusin", () => { - const event = new CustomEvent("readthedocs-search-show"); - document.dispatchEvent(event); - }); -}); \ No newline at end of file diff --git a/docs/_static/language_data.js b/docs/_static/language_data.js deleted file mode 100644 index c7fe6c6..0000000 --- a/docs/_static/language_data.js +++ /dev/null @@ -1,192 +0,0 @@ -/* - * This script contains the language-specific data used by searchtools.js, - * namely the list of stopwords, stemmer, scorer and splitter. - */ - -var stopwords = ["a", "and", "are", "as", "at", "be", "but", "by", "for", "if", "in", "into", "is", "it", "near", "no", "not", "of", "on", "or", "such", "that", "the", "their", "then", "there", "these", "they", "this", "to", "was", "will", "with"]; - - -/* Non-minified version is copied as a separate JS file, if available */ - -/** - * Porter Stemmer - */ -var Stemmer = function() { - - var step2list = { - ational: 'ate', - tional: 'tion', - enci: 'ence', - anci: 'ance', - izer: 'ize', - bli: 'ble', - alli: 'al', - entli: 'ent', - eli: 'e', - ousli: 'ous', - ization: 'ize', - ation: 'ate', - ator: 'ate', - alism: 'al', - iveness: 'ive', - fulness: 'ful', - ousness: 'ous', - aliti: 'al', - iviti: 'ive', - biliti: 'ble', - logi: 'log' - }; - - var step3list = { - icate: 'ic', - ative: '', - alize: 'al', - iciti: 'ic', - ical: 'ic', - ful: '', - ness: '' - }; - - var c = "[^aeiou]"; // consonant - var v = "[aeiouy]"; // vowel - var C = c + "[^aeiouy]*"; // consonant sequence - var V = v + "[aeiou]*"; // vowel sequence - - var mgr0 = "^(" + C + ")?" + V + C; // [C]VC... is m>0 - var meq1 = "^(" + C + ")?" + V + C + "(" + V + ")?$"; // [C]VC[V] is m=1 - var mgr1 = "^(" + C + ")?" + V + C + V + C; // [C]VCVC... is m>1 - var s_v = "^(" + C + ")?" + v; // vowel in stem - - this.stemWord = function (w) { - var stem; - var suffix; - var firstch; - var origword = w; - - if (w.length < 3) - return w; - - var re; - var re2; - var re3; - var re4; - - firstch = w.substr(0,1); - if (firstch == "y") - w = firstch.toUpperCase() + w.substr(1); - - // Step 1a - re = /^(.+?)(ss|i)es$/; - re2 = /^(.+?)([^s])s$/; - - if (re.test(w)) - w = w.replace(re,"$1$2"); - else if (re2.test(w)) - w = w.replace(re2,"$1$2"); - - // Step 1b - re = /^(.+?)eed$/; - re2 = /^(.+?)(ed|ing)$/; - if (re.test(w)) { - var fp = re.exec(w); - re = new RegExp(mgr0); - if (re.test(fp[1])) { - re = /.$/; - w = w.replace(re,""); - } - } - else if (re2.test(w)) { - var fp = re2.exec(w); - stem = fp[1]; - re2 = new RegExp(s_v); - if (re2.test(stem)) { - w = stem; - re2 = /(at|bl|iz)$/; - re3 = new RegExp("([^aeiouylsz])\\1$"); - re4 = new RegExp("^" + C + v + "[^aeiouwxy]$"); - if (re2.test(w)) - w = w + "e"; - else if (re3.test(w)) { - re = /.$/; - w = w.replace(re,""); - } - else if (re4.test(w)) - w = w + "e"; - } - } - - // Step 1c - re = /^(.+?)y$/; - if (re.test(w)) { - var fp = re.exec(w); - stem = fp[1]; - re = new RegExp(s_v); - if (re.test(stem)) - w = stem + "i"; - } - - // Step 2 - re = /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/; - if (re.test(w)) { - var fp = re.exec(w); - stem = fp[1]; - suffix = fp[2]; - re = new RegExp(mgr0); - if (re.test(stem)) - w = stem + step2list[suffix]; - } - - // Step 3 - re = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/; - if (re.test(w)) { - var fp = re.exec(w); - stem = fp[1]; - suffix = fp[2]; - re = new RegExp(mgr0); - if (re.test(stem)) - w = stem + step3list[suffix]; - } - - // Step 4 - re = /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/; - re2 = /^(.+?)(s|t)(ion)$/; - if (re.test(w)) { - var fp = re.exec(w); - stem = fp[1]; - re = new RegExp(mgr1); - if (re.test(stem)) - w = stem; - } - else if (re2.test(w)) { - var fp = re2.exec(w); - stem = fp[1] + fp[2]; - re2 = new RegExp(mgr1); - if (re2.test(stem)) - w = stem; - } - - // Step 5 - re = /^(.+?)e$/; - if (re.test(w)) { - var fp = re.exec(w); - stem = fp[1]; - re = new RegExp(mgr1); - re2 = new RegExp(meq1); - re3 = new RegExp("^" + C + v + "[^aeiouwxy]$"); - if (re.test(stem) || (re2.test(stem) && !(re3.test(stem)))) - w = stem; - } - re = /ll$/; - re2 = new RegExp(mgr1); - if (re.test(w) && re2.test(w)) { - re = /.$/; - w = w.replace(re,""); - } - - // and turn initial Y back to y - if (firstch == "y") - w = firstch.toLowerCase() + w.substr(1); - return w; - } -} - diff --git a/docs/_static/minus.png b/docs/_static/minus.png deleted file mode 100644 index d96755f..0000000 Binary files a/docs/_static/minus.png and /dev/null differ diff --git a/docs/_static/plus.png b/docs/_static/plus.png deleted file mode 100644 index 7107cec..0000000 Binary files a/docs/_static/plus.png and /dev/null differ diff --git a/docs/_static/pygments.css b/docs/_static/pygments.css deleted file mode 100644 index 6f8b210..0000000 --- a/docs/_static/pygments.css +++ /dev/null @@ -1,75 +0,0 @@ -pre { line-height: 125%; } -td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } -span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } -td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } -span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } -.highlight .hll { background-color: #ffffcc } -.highlight { background: #f8f8f8; } -.highlight .c { color: #3D7B7B; font-style: italic } /* Comment */ -.highlight .err { border: 1px solid #F00 } /* Error */ -.highlight .k { color: #008000; font-weight: bold } /* Keyword */ -.highlight .o { color: #666 } /* Operator */ -.highlight .ch { color: #3D7B7B; font-style: italic } /* Comment.Hashbang */ -.highlight .cm { color: #3D7B7B; font-style: italic } /* Comment.Multiline */ -.highlight .cp { color: #9C6500 } /* Comment.Preproc */ -.highlight .cpf { color: #3D7B7B; font-style: italic } /* Comment.PreprocFile */ -.highlight .c1 { color: #3D7B7B; font-style: italic } /* Comment.Single */ -.highlight .cs { color: #3D7B7B; font-style: italic } /* Comment.Special */ -.highlight .gd { color: #A00000 } /* Generic.Deleted */ -.highlight .ge { font-style: italic } /* Generic.Emph */ -.highlight .ges { font-weight: bold; font-style: italic } /* Generic.EmphStrong */ -.highlight .gr { color: #E40000 } /* Generic.Error */ -.highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */ -.highlight .gi { color: #008400 } /* Generic.Inserted */ -.highlight .go { color: #717171 } /* Generic.Output */ -.highlight .gp { color: #000080; font-weight: bold } /* Generic.Prompt */ -.highlight .gs { font-weight: bold } /* Generic.Strong */ -.highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ -.highlight .gt { color: #04D } /* Generic.Traceback */ -.highlight .kc { color: #008000; font-weight: bold } /* Keyword.Constant */ -.highlight .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */ -.highlight .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */ -.highlight .kp { color: #008000 } /* Keyword.Pseudo */ -.highlight .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */ -.highlight .kt { color: #B00040 } /* Keyword.Type */ -.highlight .m { color: #666 } /* Literal.Number */ -.highlight .s { color: #BA2121 } /* Literal.String */ -.highlight .na { color: #687822 } /* Name.Attribute */ -.highlight .nb { color: #008000 } /* Name.Builtin */ -.highlight .nc { color: #00F; font-weight: bold } /* Name.Class */ -.highlight .no { color: #800 } /* Name.Constant */ -.highlight .nd { color: #A2F } /* Name.Decorator */ -.highlight .ni { color: #717171; font-weight: bold } /* Name.Entity */ -.highlight .ne { color: #CB3F38; font-weight: bold } /* Name.Exception */ -.highlight .nf { color: #00F } /* Name.Function */ -.highlight .nl { color: #767600 } /* Name.Label */ -.highlight .nn { color: #00F; font-weight: bold } /* Name.Namespace */ -.highlight .nt { color: #008000; font-weight: bold } /* Name.Tag */ -.highlight .nv { color: #19177C } /* Name.Variable */ -.highlight .ow { color: #A2F; font-weight: bold } /* Operator.Word */ -.highlight .w { color: #BBB } /* Text.Whitespace */ -.highlight .mb { color: #666 } /* Literal.Number.Bin */ -.highlight .mf { color: #666 } /* Literal.Number.Float */ -.highlight .mh { color: #666 } /* Literal.Number.Hex */ -.highlight .mi { color: #666 } /* Literal.Number.Integer */ -.highlight .mo { color: #666 } /* Literal.Number.Oct */ -.highlight .sa { color: #BA2121 } /* Literal.String.Affix */ -.highlight .sb { color: #BA2121 } /* Literal.String.Backtick */ -.highlight .sc { color: #BA2121 } /* Literal.String.Char */ -.highlight .dl { color: #BA2121 } /* Literal.String.Delimiter */ -.highlight .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */ -.highlight .s2 { color: #BA2121 } /* Literal.String.Double */ -.highlight .se { color: #AA5D1F; font-weight: bold } /* Literal.String.Escape */ -.highlight .sh { color: #BA2121 } /* Literal.String.Heredoc */ -.highlight .si { color: #A45A77; font-weight: bold } /* Literal.String.Interpol */ -.highlight .sx { color: #008000 } /* Literal.String.Other */ -.highlight .sr { color: #A45A77 } /* Literal.String.Regex */ -.highlight .s1 { color: #BA2121 } /* Literal.String.Single */ -.highlight .ss { color: #19177C } /* Literal.String.Symbol */ -.highlight .bp { color: #008000 } /* Name.Builtin.Pseudo */ -.highlight .fm { color: #00F } /* Name.Function.Magic */ -.highlight .vc { color: #19177C } /* Name.Variable.Class */ -.highlight .vg { color: #19177C } /* Name.Variable.Global */ -.highlight .vi { color: #19177C } /* Name.Variable.Instance */ -.highlight .vm { color: #19177C } /* Name.Variable.Magic */ -.highlight .il { color: #666 } /* Literal.Number.Integer.Long */ \ No newline at end of file diff --git a/docs/_static/searchtools.js b/docs/_static/searchtools.js deleted file mode 100644 index 2c774d1..0000000 --- a/docs/_static/searchtools.js +++ /dev/null @@ -1,632 +0,0 @@ -/* - * Sphinx JavaScript utilities for the full-text search. - */ -"use strict"; - -/** - * Simple result scoring code. - */ -if (typeof Scorer === "undefined") { - var Scorer = { - // Implement the following function to further tweak the score for each result - // The function takes a result array [docname, title, anchor, descr, score, filename] - // and returns the new score. - /* - score: result => { - const [docname, title, anchor, descr, score, filename, kind] = result - return score - }, - */ - - // query matches the full name of an object - objNameMatch: 11, - // or matches in the last dotted part of the object name - objPartialMatch: 6, - // Additive scores depending on the priority of the object - objPrio: { - 0: 15, // used to be importantResults - 1: 5, // used to be objectResults - 2: -5, // used to be unimportantResults - }, - // Used when the priority is not in the mapping. - objPrioDefault: 0, - - // query found in title - title: 15, - partialTitle: 7, - // query found in terms - term: 5, - partialTerm: 2, - }; -} - -// Global search result kind enum, used by themes to style search results. -class SearchResultKind { - static get index() { return "index"; } - static get object() { return "object"; } - static get text() { return "text"; } - static get title() { return "title"; } -} - -const _removeChildren = (element) => { - while (element && element.lastChild) element.removeChild(element.lastChild); -}; - -/** - * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping - */ -const _escapeRegExp = (string) => - string.replace(/[.*+\-?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string - -const _displayItem = (item, searchTerms, highlightTerms) => { - const docBuilder = DOCUMENTATION_OPTIONS.BUILDER; - const docFileSuffix = DOCUMENTATION_OPTIONS.FILE_SUFFIX; - const docLinkSuffix = DOCUMENTATION_OPTIONS.LINK_SUFFIX; - const showSearchSummary = DOCUMENTATION_OPTIONS.SHOW_SEARCH_SUMMARY; - const contentRoot = document.documentElement.dataset.content_root; - - const [docName, title, anchor, descr, score, _filename, kind] = item; - - let listItem = document.createElement("li"); - // Add a class representing the item's type: - // can be used by a theme's CSS selector for styling - // See SearchResultKind for the class names. - listItem.classList.add(`kind-${kind}`); - let requestUrl; - let linkUrl; - if (docBuilder === "dirhtml") { - // dirhtml builder - let dirname = docName + "/"; - if (dirname.match(/\/index\/$/)) - dirname = dirname.substring(0, dirname.length - 6); - else if (dirname === "index/") dirname = ""; - requestUrl = contentRoot + dirname; - linkUrl = requestUrl; - } else { - // normal html builders - requestUrl = contentRoot + docName + docFileSuffix; - linkUrl = docName + docLinkSuffix; - } - let linkEl = listItem.appendChild(document.createElement("a")); - linkEl.href = linkUrl + anchor; - linkEl.dataset.score = score; - linkEl.innerHTML = title; - if (descr) { - listItem.appendChild(document.createElement("span")).innerHTML = - " (" + descr + ")"; - // highlight search terms in the description - if (SPHINX_HIGHLIGHT_ENABLED) // set in sphinx_highlight.js - highlightTerms.forEach((term) => _highlightText(listItem, term, "highlighted")); - } - else if (showSearchSummary) - fetch(requestUrl) - .then((responseData) => responseData.text()) - .then((data) => { - if (data) - listItem.appendChild( - Search.makeSearchSummary(data, searchTerms, anchor) - ); - // highlight search terms in the summary - if (SPHINX_HIGHLIGHT_ENABLED) // set in sphinx_highlight.js - highlightTerms.forEach((term) => _highlightText(listItem, term, "highlighted")); - }); - Search.output.appendChild(listItem); -}; -const _finishSearch = (resultCount) => { - Search.stopPulse(); - Search.title.innerText = _("Search Results"); - if (!resultCount) - Search.status.innerText = Documentation.gettext( - "Your search did not match any documents. Please make sure that all words are spelled correctly and that you've selected enough categories." - ); - else - Search.status.innerText = Documentation.ngettext( - "Search finished, found one page matching the search query.", - "Search finished, found ${resultCount} pages matching the search query.", - resultCount, - ).replace('${resultCount}', resultCount); -}; -const _displayNextItem = ( - results, - resultCount, - searchTerms, - highlightTerms, -) => { - // results left, load the summary and display it - // this is intended to be dynamic (don't sub resultsCount) - if (results.length) { - _displayItem(results.pop(), searchTerms, highlightTerms); - setTimeout( - () => _displayNextItem(results, resultCount, searchTerms, highlightTerms), - 5 - ); - } - // search finished, update title and status message - else _finishSearch(resultCount); -}; -// Helper function used by query() to order search results. -// Each input is an array of [docname, title, anchor, descr, score, filename, kind]. -// Order the results by score (in opposite order of appearance, since the -// `_displayNextItem` function uses pop() to retrieve items) and then alphabetically. -const _orderResultsByScoreThenName = (a, b) => { - const leftScore = a[4]; - const rightScore = b[4]; - if (leftScore === rightScore) { - // same score: sort alphabetically - const leftTitle = a[1].toLowerCase(); - const rightTitle = b[1].toLowerCase(); - if (leftTitle === rightTitle) return 0; - return leftTitle > rightTitle ? -1 : 1; // inverted is intentional - } - return leftScore > rightScore ? 1 : -1; -}; - -/** - * Default splitQuery function. Can be overridden in ``sphinx.search`` with a - * custom function per language. - * - * The regular expression works by splitting the string on consecutive characters - * that are not Unicode letters, numbers, underscores, or emoji characters. - * This is the same as ``\W+`` in Python, preserving the surrogate pair area. - */ -if (typeof splitQuery === "undefined") { - var splitQuery = (query) => query - .split(/[^\p{Letter}\p{Number}_\p{Emoji_Presentation}]+/gu) - .filter(term => term) // remove remaining empty strings -} - -/** - * Search Module - */ -const Search = { - _index: null, - _queued_query: null, - _pulse_status: -1, - - htmlToText: (htmlString, anchor) => { - const htmlElement = new DOMParser().parseFromString(htmlString, 'text/html'); - for (const removalQuery of [".headerlink", "script", "style"]) { - htmlElement.querySelectorAll(removalQuery).forEach((el) => { el.remove() }); - } - if (anchor) { - const anchorContent = htmlElement.querySelector(`[role="main"] ${anchor}`); - if (anchorContent) return anchorContent.textContent; - - console.warn( - `Anchored content block not found. Sphinx search tries to obtain it via DOM query '[role=main] ${anchor}'. Check your theme or template.` - ); - } - - // if anchor not specified or not found, fall back to main content - const docContent = htmlElement.querySelector('[role="main"]'); - if (docContent) return docContent.textContent; - - console.warn( - "Content block not found. Sphinx search tries to obtain it via DOM query '[role=main]'. Check your theme or template." - ); - return ""; - }, - - init: () => { - const query = new URLSearchParams(window.location.search).get("q"); - document - .querySelectorAll('input[name="q"]') - .forEach((el) => (el.value = query)); - if (query) Search.performSearch(query); - }, - - loadIndex: (url) => - (document.body.appendChild(document.createElement("script")).src = url), - - setIndex: (index) => { - Search._index = index; - if (Search._queued_query !== null) { - const query = Search._queued_query; - Search._queued_query = null; - Search.query(query); - } - }, - - hasIndex: () => Search._index !== null, - - deferQuery: (query) => (Search._queued_query = query), - - stopPulse: () => (Search._pulse_status = -1), - - startPulse: () => { - if (Search._pulse_status >= 0) return; - - const pulse = () => { - Search._pulse_status = (Search._pulse_status + 1) % 4; - Search.dots.innerText = ".".repeat(Search._pulse_status); - if (Search._pulse_status >= 0) window.setTimeout(pulse, 500); - }; - pulse(); - }, - - /** - * perform a search for something (or wait until index is loaded) - */ - performSearch: (query) => { - // create the required interface elements - const searchText = document.createElement("h2"); - searchText.textContent = _("Searching"); - const searchSummary = document.createElement("p"); - searchSummary.classList.add("search-summary"); - searchSummary.innerText = ""; - const searchList = document.createElement("ul"); - searchList.setAttribute("role", "list"); - searchList.classList.add("search"); - - const out = document.getElementById("search-results"); - Search.title = out.appendChild(searchText); - Search.dots = Search.title.appendChild(document.createElement("span")); - Search.status = out.appendChild(searchSummary); - Search.output = out.appendChild(searchList); - - const searchProgress = document.getElementById("search-progress"); - // Some themes don't use the search progress node - if (searchProgress) { - searchProgress.innerText = _("Preparing search..."); - } - Search.startPulse(); - - // index already loaded, the browser was quick! - if (Search.hasIndex()) Search.query(query); - else Search.deferQuery(query); - }, - - _parseQuery: (query) => { - // stem the search terms and add them to the correct list - const stemmer = new Stemmer(); - const searchTerms = new Set(); - const excludedTerms = new Set(); - const highlightTerms = new Set(); - const objectTerms = new Set(splitQuery(query.toLowerCase().trim())); - splitQuery(query.trim()).forEach((queryTerm) => { - const queryTermLower = queryTerm.toLowerCase(); - - // maybe skip this "word" - // stopwords array is from language_data.js - if ( - stopwords.indexOf(queryTermLower) !== -1 || - queryTerm.match(/^\d+$/) - ) - return; - - // stem the word - let word = stemmer.stemWord(queryTermLower); - // select the correct list - if (word[0] === "-") excludedTerms.add(word.substr(1)); - else { - searchTerms.add(word); - highlightTerms.add(queryTermLower); - } - }); - - if (SPHINX_HIGHLIGHT_ENABLED) { // set in sphinx_highlight.js - localStorage.setItem("sphinx_highlight_terms", [...highlightTerms].join(" ")) - } - - // console.debug("SEARCH: searching for:"); - // console.info("required: ", [...searchTerms]); - // console.info("excluded: ", [...excludedTerms]); - - return [query, searchTerms, excludedTerms, highlightTerms, objectTerms]; - }, - - /** - * execute search (requires search index to be loaded) - */ - _performSearch: (query, searchTerms, excludedTerms, highlightTerms, objectTerms) => { - const filenames = Search._index.filenames; - const docNames = Search._index.docnames; - const titles = Search._index.titles; - const allTitles = Search._index.alltitles; - const indexEntries = Search._index.indexentries; - - // Collect multiple result groups to be sorted separately and then ordered. - // Each is an array of [docname, title, anchor, descr, score, filename, kind]. - const normalResults = []; - const nonMainIndexResults = []; - - _removeChildren(document.getElementById("search-progress")); - - const queryLower = query.toLowerCase().trim(); - for (const [title, foundTitles] of Object.entries(allTitles)) { - if (title.toLowerCase().trim().includes(queryLower) && (queryLower.length >= title.length/2)) { - for (const [file, id] of foundTitles) { - const score = Math.round(Scorer.title * queryLower.length / title.length); - const boost = titles[file] === title ? 1 : 0; // add a boost for document titles - normalResults.push([ - docNames[file], - titles[file] !== title ? `${titles[file]} > ${title}` : title, - id !== null ? "#" + id : "", - null, - score + boost, - filenames[file], - SearchResultKind.title, - ]); - } - } - } - - // search for explicit entries in index directives - for (const [entry, foundEntries] of Object.entries(indexEntries)) { - if (entry.includes(queryLower) && (queryLower.length >= entry.length/2)) { - for (const [file, id, isMain] of foundEntries) { - const score = Math.round(100 * queryLower.length / entry.length); - const result = [ - docNames[file], - titles[file], - id ? "#" + id : "", - null, - score, - filenames[file], - SearchResultKind.index, - ]; - if (isMain) { - normalResults.push(result); - } else { - nonMainIndexResults.push(result); - } - } - } - } - - // lookup as object - objectTerms.forEach((term) => - normalResults.push(...Search.performObjectSearch(term, objectTerms)) - ); - - // lookup as search terms in fulltext - normalResults.push(...Search.performTermsSearch(searchTerms, excludedTerms)); - - // let the scorer override scores with a custom scoring function - if (Scorer.score) { - normalResults.forEach((item) => (item[4] = Scorer.score(item))); - nonMainIndexResults.forEach((item) => (item[4] = Scorer.score(item))); - } - - // Sort each group of results by score and then alphabetically by name. - normalResults.sort(_orderResultsByScoreThenName); - nonMainIndexResults.sort(_orderResultsByScoreThenName); - - // Combine the result groups in (reverse) order. - // Non-main index entries are typically arbitrary cross-references, - // so display them after other results. - let results = [...nonMainIndexResults, ...normalResults]; - - // remove duplicate search results - // note the reversing of results, so that in the case of duplicates, the highest-scoring entry is kept - let seen = new Set(); - results = results.reverse().reduce((acc, result) => { - let resultStr = result.slice(0, 4).concat([result[5]]).map(v => String(v)).join(','); - if (!seen.has(resultStr)) { - acc.push(result); - seen.add(resultStr); - } - return acc; - }, []); - - return results.reverse(); - }, - - query: (query) => { - const [searchQuery, searchTerms, excludedTerms, highlightTerms, objectTerms] = Search._parseQuery(query); - const results = Search._performSearch(searchQuery, searchTerms, excludedTerms, highlightTerms, objectTerms); - - // for debugging - //Search.lastresults = results.slice(); // a copy - // console.info("search results:", Search.lastresults); - - // print the results - _displayNextItem(results, results.length, searchTerms, highlightTerms); - }, - - /** - * search for object names - */ - performObjectSearch: (object, objectTerms) => { - const filenames = Search._index.filenames; - const docNames = Search._index.docnames; - const objects = Search._index.objects; - const objNames = Search._index.objnames; - const titles = Search._index.titles; - - const results = []; - - const objectSearchCallback = (prefix, match) => { - const name = match[4] - const fullname = (prefix ? prefix + "." : "") + name; - const fullnameLower = fullname.toLowerCase(); - if (fullnameLower.indexOf(object) < 0) return; - - let score = 0; - const parts = fullnameLower.split("."); - - // check for different match types: exact matches of full name or - // "last name" (i.e. last dotted part) - if (fullnameLower === object || parts.slice(-1)[0] === object) - score += Scorer.objNameMatch; - else if (parts.slice(-1)[0].indexOf(object) > -1) - score += Scorer.objPartialMatch; // matches in last name - - const objName = objNames[match[1]][2]; - const title = titles[match[0]]; - - // If more than one term searched for, we require other words to be - // found in the name/title/description - const otherTerms = new Set(objectTerms); - otherTerms.delete(object); - if (otherTerms.size > 0) { - const haystack = `${prefix} ${name} ${objName} ${title}`.toLowerCase(); - if ( - [...otherTerms].some((otherTerm) => haystack.indexOf(otherTerm) < 0) - ) - return; - } - - let anchor = match[3]; - if (anchor === "") anchor = fullname; - else if (anchor === "-") anchor = objNames[match[1]][1] + "-" + fullname; - - const descr = objName + _(", in ") + title; - - // add custom score for some objects according to scorer - if (Scorer.objPrio.hasOwnProperty(match[2])) - score += Scorer.objPrio[match[2]]; - else score += Scorer.objPrioDefault; - - results.push([ - docNames[match[0]], - fullname, - "#" + anchor, - descr, - score, - filenames[match[0]], - SearchResultKind.object, - ]); - }; - Object.keys(objects).forEach((prefix) => - objects[prefix].forEach((array) => - objectSearchCallback(prefix, array) - ) - ); - return results; - }, - - /** - * search for full-text terms in the index - */ - performTermsSearch: (searchTerms, excludedTerms) => { - // prepare search - const terms = Search._index.terms; - const titleTerms = Search._index.titleterms; - const filenames = Search._index.filenames; - const docNames = Search._index.docnames; - const titles = Search._index.titles; - - const scoreMap = new Map(); - const fileMap = new Map(); - - // perform the search on the required terms - searchTerms.forEach((word) => { - const files = []; - const arr = [ - { files: terms[word], score: Scorer.term }, - { files: titleTerms[word], score: Scorer.title }, - ]; - // add support for partial matches - if (word.length > 2) { - const escapedWord = _escapeRegExp(word); - if (!terms.hasOwnProperty(word)) { - Object.keys(terms).forEach((term) => { - if (term.match(escapedWord)) - arr.push({ files: terms[term], score: Scorer.partialTerm }); - }); - } - if (!titleTerms.hasOwnProperty(word)) { - Object.keys(titleTerms).forEach((term) => { - if (term.match(escapedWord)) - arr.push({ files: titleTerms[term], score: Scorer.partialTitle }); - }); - } - } - - // no match but word was a required one - if (arr.every((record) => record.files === undefined)) return; - - // found search word in contents - arr.forEach((record) => { - if (record.files === undefined) return; - - let recordFiles = record.files; - if (recordFiles.length === undefined) recordFiles = [recordFiles]; - files.push(...recordFiles); - - // set score for the word in each file - recordFiles.forEach((file) => { - if (!scoreMap.has(file)) scoreMap.set(file, {}); - scoreMap.get(file)[word] = record.score; - }); - }); - - // create the mapping - files.forEach((file) => { - if (!fileMap.has(file)) fileMap.set(file, [word]); - else if (fileMap.get(file).indexOf(word) === -1) fileMap.get(file).push(word); - }); - }); - - // now check if the files don't contain excluded terms - const results = []; - for (const [file, wordList] of fileMap) { - // check if all requirements are matched - - // as search terms with length < 3 are discarded - const filteredTermCount = [...searchTerms].filter( - (term) => term.length > 2 - ).length; - if ( - wordList.length !== searchTerms.size && - wordList.length !== filteredTermCount - ) - continue; - - // ensure that none of the excluded terms is in the search result - if ( - [...excludedTerms].some( - (term) => - terms[term] === file || - titleTerms[term] === file || - (terms[term] || []).includes(file) || - (titleTerms[term] || []).includes(file) - ) - ) - break; - - // select one (max) score for the file. - const score = Math.max(...wordList.map((w) => scoreMap.get(file)[w])); - // add result to the result list - results.push([ - docNames[file], - titles[file], - "", - null, - score, - filenames[file], - SearchResultKind.text, - ]); - } - return results; - }, - - /** - * helper function to return a node containing the - * search summary for a given text. keywords is a list - * of stemmed words. - */ - makeSearchSummary: (htmlText, keywords, anchor) => { - const text = Search.htmlToText(htmlText, anchor); - if (text === "") return null; - - const textLower = text.toLowerCase(); - const actualStartPosition = [...keywords] - .map((k) => textLower.indexOf(k.toLowerCase())) - .filter((i) => i > -1) - .slice(-1)[0]; - const startWithContext = Math.max(actualStartPosition - 120, 0); - - const top = startWithContext === 0 ? "" : "..."; - const tail = startWithContext + 240 < text.length ? "..." : ""; - - let summary = document.createElement("p"); - summary.classList.add("context"); - summary.textContent = top + text.substr(startWithContext, 240).trim() + tail; - - return summary; - }, -}; - -_ready(Search.init); diff --git a/docs/_static/sphinx_highlight.js b/docs/_static/sphinx_highlight.js deleted file mode 100644 index 8a96c69..0000000 --- a/docs/_static/sphinx_highlight.js +++ /dev/null @@ -1,154 +0,0 @@ -/* Highlighting utilities for Sphinx HTML documentation. */ -"use strict"; - -const SPHINX_HIGHLIGHT_ENABLED = true - -/** - * highlight a given string on a node by wrapping it in - * span elements with the given class name. - */ -const _highlight = (node, addItems, text, className) => { - if (node.nodeType === Node.TEXT_NODE) { - const val = node.nodeValue; - const parent = node.parentNode; - const pos = val.toLowerCase().indexOf(text); - if ( - pos >= 0 && - !parent.classList.contains(className) && - !parent.classList.contains("nohighlight") - ) { - let span; - - const closestNode = parent.closest("body, svg, foreignObject"); - const isInSVG = closestNode && closestNode.matches("svg"); - if (isInSVG) { - span = document.createElementNS("http://www.w3.org/2000/svg", "tspan"); - } else { - span = document.createElement("span"); - span.classList.add(className); - } - - span.appendChild(document.createTextNode(val.substr(pos, text.length))); - const rest = document.createTextNode(val.substr(pos + text.length)); - parent.insertBefore( - span, - parent.insertBefore( - rest, - node.nextSibling - ) - ); - node.nodeValue = val.substr(0, pos); - /* There may be more occurrences of search term in this node. So call this - * function recursively on the remaining fragment. - */ - _highlight(rest, addItems, text, className); - - if (isInSVG) { - const rect = document.createElementNS( - "http://www.w3.org/2000/svg", - "rect" - ); - const bbox = parent.getBBox(); - rect.x.baseVal.value = bbox.x; - rect.y.baseVal.value = bbox.y; - rect.width.baseVal.value = bbox.width; - rect.height.baseVal.value = bbox.height; - rect.setAttribute("class", className); - addItems.push({ parent: parent, target: rect }); - } - } - } else if (node.matches && !node.matches("button, select, textarea")) { - node.childNodes.forEach((el) => _highlight(el, addItems, text, className)); - } -}; -const _highlightText = (thisNode, text, className) => { - let addItems = []; - _highlight(thisNode, addItems, text, className); - addItems.forEach((obj) => - obj.parent.insertAdjacentElement("beforebegin", obj.target) - ); -}; - -/** - * Small JavaScript module for the documentation. - */ -const SphinxHighlight = { - - /** - * highlight the search words provided in localstorage in the text - */ - highlightSearchWords: () => { - if (!SPHINX_HIGHLIGHT_ENABLED) return; // bail if no highlight - - // get and clear terms from localstorage - const url = new URL(window.location); - const highlight = - localStorage.getItem("sphinx_highlight_terms") - || url.searchParams.get("highlight") - || ""; - localStorage.removeItem("sphinx_highlight_terms") - url.searchParams.delete("highlight"); - window.history.replaceState({}, "", url); - - // get individual terms from highlight string - const terms = highlight.toLowerCase().split(/\s+/).filter(x => x); - if (terms.length === 0) return; // nothing to do - - // There should never be more than one element matching "div.body" - const divBody = document.querySelectorAll("div.body"); - const body = divBody.length ? divBody[0] : document.querySelector("body"); - window.setTimeout(() => { - terms.forEach((term) => _highlightText(body, term, "highlighted")); - }, 10); - - const searchBox = document.getElementById("searchbox"); - if (searchBox === null) return; - searchBox.appendChild( - document - .createRange() - .createContextualFragment( - '" - ) - ); - }, - - /** - * helper function to hide the search marks again - */ - hideSearchWords: () => { - document - .querySelectorAll("#searchbox .highlight-link") - .forEach((el) => el.remove()); - document - .querySelectorAll("span.highlighted") - .forEach((el) => el.classList.remove("highlighted")); - localStorage.removeItem("sphinx_highlight_terms") - }, - - initEscapeListener: () => { - // only install a listener if it is really needed - if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) return; - - document.addEventListener("keydown", (event) => { - // bail for input elements - if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) return; - // bail with special keys - if (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey) return; - if (DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS && (event.key === "Escape")) { - SphinxHighlight.hideSearchWords(); - event.preventDefault(); - } - }); - }, -}; - -_ready(() => { - /* Do not call highlightSearchWords() when we are on the search page. - * It will highlight words from the *previous* search query. - */ - if (typeof Search === "undefined") SphinxHighlight.highlightSearchWords(); - SphinxHighlight.initEscapeListener(); -}); diff --git a/docs/appdirectory.html b/docs/appdirectory.html deleted file mode 100644 index 4cbb103..0000000 --- a/docs/appdirectory.html +++ /dev/null @@ -1,120 +0,0 @@ - - - - - - - - - CLAMS App Directory — clams-python documentation - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - -
  • -
  • -
-
-
-
-
- -
-

CLAMS App Directory

-

CLAMS App Directory is a public registry for free and open CLAMS apps, available at http://apps.clams.ai .

-

To submit your app for registration on the directory, please find a submission guideline on the directory website.

-
- - -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/appmetadata.html b/docs/appmetadata.html deleted file mode 100644 index d27d6ba..0000000 --- a/docs/appmetadata.html +++ /dev/null @@ -1,884 +0,0 @@ - - - - - - - - - CLAMS App Metadata — clams-python documentation - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - -
  • -
  • -
-
-
-
-
- -
-

CLAMS App Metadata

-
-

Overview

-

Every CLAMS app must provide information about the app itself. We call this set of information App Metadata.

-
-

Format

-

A CLAMS App Metadata should be able to be serialized into a JSON string.

-
-
-

Input/Output type specification

-

Essentially, all CLAMS apps are designed to take one MMIF file as input and produce another MMIF file as output. In this -section, we will discuss how to specify, in the App Metadata, the semantics of the input and output MMIF files, and -how that information should be formatted in terms of the App Metadata syntax, concretely by using input and -output lists and type vocabularies where @type are defined.

-
-

Note

-

CLAMS App Metadata is encoded in JSON format, but is not part of MMIF specification. -Full json schema for app metadata is available in the below section. -When an app is published to the CLAMS app directory, the app metadata will be rendered as a HTML page, with some -additional information about submission. Visit the CLAMS app directory to see how the app -metadata is rendered.

-
-
-

Annotation types in MMIF

-

As described in the MMIF documentation, MMIF files can contain annotations of various types. -Currently, CLAMS team is using the following vocabularies with pre-defined annotation types:

- -

Each annotation object type in the vocabularies has a unique URI that is used as the value of the @type field. -However, more important part of the type definition in the context of CLAMS app development is the metadata and -properties fields. These fields provide additional information about the annotation type. Semantically, there is -no differences between a metadata field and a property field. The difference is in the intended use of the field. -a metadata field is used to provide common information about a group of annotation objects of the same type, while -a properties field is used to provide information about the individual annotation instance. In practice, metadata -fields are placed in the view metadata (view[].metatadata.contains) and properties fields are placed in the -annotation object itself. Because of this lack of distinction in the semantics, we will use the term “type property” to -refer to both metadata and properties in the context of annotation type (I/O) specifications in the app metadata.

-

Type definitions in the vocabularies are intentionally kept minimal and underspecified. This is because the definitions -are meant to be extended by an app developers. For example, the LAPPS vocabulary defines a type called Token, -primarily to represent a token in a natural language text. However, the usage of the type can be extended to represent -a sub-word token used in a machine learning model, or a minimal unit of a sign language video. If the app developer -needs to add additional information to the type definition, they can do so by adding arbitrary properties to the type -definition in action. In such a case, the app developer is expected to provide the explanation of the extended type in -the app metadata. See below for the syntax of I/O specification in the app metadata.

-
-
-

Syntax for I/O specification in App Metadata

-

In the App Metadata, the input and output types are specified as lists of objects. Each object in the list should have -the following fields:

-
    -
  • @type: The URI of the annotation type. This field is required.

  • -
  • description: A human-readable description of the annotation type. This field is optional.

  • -
  • properties: A key-value pairs of type properties. This field is optional.

  • -
  • required: A boolean value indicating whether the annotation type is required as input. This field is optional and -defaults to true. Not applicable for output types.

  • -
-
-
Simple case - using types as defined in the vocabularies
-

In the simplest case, where a developer merely re-uses an annotation type definition and pre-defined properties, an -input specification can be as simple as the following:

-
{
-  # other app metadata fields,
-  "input":
-  [
-    {
-      "@type": "http://vocab.lappsgrid.org/Token",
-      "properties": {
-        "posTagSet": "https://www.ling.upenn.edu/courses/Fall_2003/ling001/penn_treebank_pos.html"
-      }
-    }
-  ],
-  # and more app metadata fields,
-}
-
-
-

In the above example, the developer is declaring the app is expecting Token annotation objects, with a posTagSet -property of which value is the URL of the Penn Treebank POS tag set, verbatim, in the input MMIF, and all other -existing annotation types in the input MMIF will be ignored during processing. There are some grammar of how this -input list can be written.

-
    -
  • The value of a property specification can be a verbatim string, or "*" to indicate that the property can have -any value.

  • -
  • If the app expects multiple types of annotations, the input field should contain multiple objects in the list.

  • -
  • And if the app expects “one-of” specified types, one can specify the set of those types in a nested list. One nested -list in the input specification means one required type.

  • -
  • And finally, if an input type is optional (i.e., required=false), it indicates that the app can use extra -information from the optional annotations. In such a case, it is recommended to provide a description of differences -in the output MMIF when the extra information is available.

  • -
-

For example, here is a more complex example of the simple case:

-
{
-  # other app metadata fields,
-  "input":
-  [
-    [
-      { "@type": "https://mmif.clams.ai/vocabulary/AudioDocument/v1/" },
-      { "@type": "https://mmif.clams.ai/vocabulary/VideoDocument/v1/" }
-    ],
-    {
-      "@type": "https://mmif.clams.ai/vocabulary/TimeFrame/v5",
-      "properties": {
-        "label": "speech",
-      }
-      "required": false
-    },
-  ],
-  # and more app metadata fields,
-}
-
-
-

This app is a speech-to-text (automatic speech recognition) app that can take either an audio document or a video -document and transcribe the speech in the document. The app can also take a TimeFrame annotation objects with -label="speech" property. When speech time frames are available, app can perform transcription only on the speech -segments, to save time and compute power.

-

Another example with even more complex input specification:

-
{
-  # other app metadata fields,
-  "input":
-  [
-    { "@type": "https://mmif.clams.ai/vocabulary/VideoDocument/v1/" },
-    [
-      {
-        "@type": "https://mmif.clams.ai/vocabulary/TimeFrame/v5",
-        "properties": {
-          "timeUnit": "*"
-          "label": "slate",
-        }
-      },
-      {
-        "@type": "https://mmif.clams.ai/vocabulary/TimeFrame/v5",
-        "properties": {
-          "timeUnit": "*"
-          "label": "chyron",
-        }
-      }
-    ]
-  ],
-  # and more app metadata fields,
-}
-
-
-

This is a text recognition app that can take a video document and TimeFrame annotations that are labels as -either slate or chyron, and have timeUnit properties. The value of the timeUnit property doesn’t matter, -but the input time frames must have it.

-
-

Note

-

Unfortunately, currently there is no way to specify optional properties within the type definition.

-
-

Finally, let’s take a look at the output specification of a scene recognition CLAMS app:

-
{
-  # other app metadata fields,
-  "output":
-  [
-      {
-        "@type": "https://mmif.clams.ai/vocabulary/TimePoint/v4/",
-        "description": "An individual \"still frame\"-level image classification results.",
-        "properties": {
-            "timeUnit": "milliseconds",
-            "labelset": ["slate", "chyron", "talking-heads-no-text"],
-            "classification": "*",
-            "label": "*"
-        }
-      }
-  ],
-  # and more app metadata fields,
-}
-
-
-

Note that in the actual output MMIF, more properties can be stored in the TimePoint objects. The output -specification in the app metadata is a subset of the properties to be produced that are useful for type checking -in the downstream apps, as well as for human readers to understand the output.

-
-
-
Extended case - adding custom properties to the type definition
-

When the type definition is extended on the fly, developers are expected to provide the extended specification in the -form of key-value pairs in the properties field. The grammar of the JSON object does not change, but developers are -expected to provide a verbose description of the type extension in the description field.

-
-
-
-
-

Runtime parameter specification

-

CLAMS apps designed to be run as HTTP servers, preferably as stateless. -When accepting HTTP requests, the app should take the request data payload (body) as the input MMIF, and any exposed -configurations should be read from query strings in the URL.

-

That said, the only allowed data type for users to pass as parameter values at the request time is a string. Hence, the -app developer is responsible for parsing the string values into the appropriate data types. (clams-python SDK -provides some basic parsing functions, automatically called by the web framework wrapper.) At the app metadata level, -developers can specify the expected parameter data types, among integer, number, string, boolean, -map, and also can specify the default value of the parameter (when specified, default values should be properly -typed, not as strings). Noticeably, there’s NO list in the available data types, and that is because a parameter -can be specified as multivalued=True to accept multiple values as a list. For details of how SDK’s built-in -parameter value parsing works, please refer to the App Metadata json scheme (in the below -section).

-
-

Syntax for parameter specification in App Metadata

-
-
-
-

Metadata Schema

-

The schema for app metadata is as follows. -(You can also download the schema in JSON Schema format from here.)

-
-

CLAMS AppMetadata

-

Data model that describes a CLAMS app.

-

Can be initialized by simply passing all required key-value pairs.

-

If you have a pre-generated metadata as an external file, you can read in the file as a dict and use it as -keyword arguments for initialization. But be careful with keys of which values are automatically generated by the -SDK.

-

Please refer to <CLAMS App Metadata> for the metadata specification.

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

type

object

properties

    -
  • name

  • -
-

A short name of the app.

type

string

    -
  • description

  • -
-

A longer description of the app (what it does, how to use, etc.).

type

string

    -
  • app_version

  • -
-

(AUTO-GENERATED, DO NOT SET MANUALLY)

-

Version of the app.

-

When the metadata is generated using clams-python SDK, this field is automatically filled in

-

type

string

default

    -
  • mmif_version

  • -
-

(AUTO-GENERATED, DO NOT SET MANUALLY)

-

Version of MMIF specification the app.

-

When the metadata is generated using clams-python SDK, this field is automatically filled in.

-

type

string

default

    -
  • analyzer_version

  • -
-

(optional) Version of an analyzer software, if the app is working as a wrapper for one.

default

null

anyOf

type

string

type

null

    -
  • app_license

  • -
-

License information of the app.

type

string

    -
  • analyzer_license

  • -
-

(optional) License information of an analyzer software, if the app works as a wrapper for one.

default

null

anyOf

type

string

type

null

    -
  • identifier

  • -
-

(partly AUTO-GENERATED)

-

IRI-formatted unique identifier for the app.

-

If the app is to be published to the CLAMS app-directory, the developer should give a single string value composed with valid URL characters (no /, no whitespace),

-

then when the metadata is generated using clams-python SDK, the app-directory URL is prepended and app_version value will be appended automatically.

-

For example, example-app -> http://apps.clams.ai/example-app/1.0.0

-

Otherwise, only the app_version value is used as suffix, so use an IRI form, but leave the version number out.

-

type

string

minLength

1

format

uri

    -
  • url

  • -
-

A public repository where the app’s source code (git-based) and/or documentation is available.

type

string

minLength

1

format

uri

    -
  • input

  • -
-

List of input types. Must have at least one element.

-

This list should iterate all input types in an exhaustive and meticulous manner, meaning it is recommended for developers to pay extra attention to input and output fields to include 1) all types are listed, 2) if types to have specific properties, include the properties.

-

This list should interpreted conjunctive (AND).

-

However, a nested list in this list means oneOf disjunctive (OR) specification.

-

For example, input = [TypeA (req=True), [TypeB, TypeC]] means``TypeA`` is required and either TypeB or TypeC is additionally required.

-

All input elements in the nested list must not be required=False, and only a single nesting level is allowed (e.g. input = [TypeA, [ [TypeB, TypeC], [TypeD, TypeE] ] ] is not allowed).

-

type

array

default

items

anyOf

#/$defs/Input

type

array

items

#/$defs/Input

    -
  • output

  • -
-

List of output types. Must have at least one element.This list should iterate all output types in an exhaustive and meticulous manner, meaning it is recommended for developers to pay extra attention to input and output fields to include

type

array

default

items

#/$defs/Output

    -
  • parameters

  • -
-

List of runtime parameters. Can be empty.

type

array

default

items

#/$defs/RuntimeParameter

    -
  • dependencies

  • -
-

(optional) List of software dependencies of the app.

-

This list is completely optional, as in most cases such dependencies are specified in a separate file in the codebase of the app (for example, requirements.txt file for a Python app, or pom.xml file for a maven-based Java app).

-

List items must be strings, not any kind of structured data. Thus, it is recommended to include a package name and its version in the string value at the minimum (e.g., clams-python==1.2.3).

-

default

null

anyOf

type

array

items

type

string

type

null

    -
  • more

  • -
-

(optional) A string-to-string map that can be used to store any additional metadata of the app.

default

null

anyOf

type

object

additionalProperties

type

string

type

null

additionalProperties

False

-
-
CLAMS Input Specification
-

Data model that describes input specification of a CLAMS app.

-

CLAMS apps are expected to have at least one input type, and each type must -be defined by a @type URI string. If the type has specific properties and values required by the app, -they can be described in the (optional) properties field. Finally, a human-readable -verbose description can be provided in the (optional) description field for users.

-

Developers should take diligent care to include all input types and their properties in the app metadata.

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

type

object

properties

    -
  • @type

  • -
-

The type of the object. Must be a IRI string.

type

string

minLength

1

format

uri

    -
  • description

  • -
-

A verbose, human-readable description of the type. This is intended to be used for documentation purpose for a particular use case of this annotation type and is not expected to be consumed by software. This description should work as a guideline for users to understand the output type, and also can be used as a expansion specification for the type definition beyond the base vocabulary.

default

null

anyOf

type

string

type

null

    -
  • properties

  • -
-

(optional) Specification for type properties, if any. "*" indicates any value.

type

object

default

additionalProperties

True

    -
  • required

  • -
-

(optional, True by default) Indicating whether this input type is mandatory or optional.

default

null

anyOf

type

boolean

type

null

additionalProperties

False

-
-
-
CLAMS Output Specification
-

Data model that describes output specification of a CLAMS app.

-

CLAMS apps are expected to have at least one output type, and each type must -be defined by a @type URI string. If the type has common properties and values generated by the app, -they can be described in the (optional) properties field. Finally, a human-readable -verbose description can be provided in the (optional) description field for users.

-

Developers should take diligent care to include all output types and their properties in the app metadata. To -specify the property values, developers can use an actual value (for full match) or '*' (for any value).

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

type

object

properties

    -
  • @type

  • -
-

The type of the object. Must be a IRI string.

type

string

minLength

1

format

uri

    -
  • description

  • -
-

A verbose, human-readable description of the type. This is intended to be used for documentation purpose for a particular use case of this annotation type and is not expected to be consumed by software. This description should work as a guideline for users to understand the output type, and also can be used as a expansion specification for the type definition beyond the base vocabulary.

default

null

anyOf

type

string

type

null

    -
  • properties

  • -
-

(optional) Specification for type properties, if any. "*" indicates any value.

type

object

default

additionalProperties

True

additionalProperties

False

-
-
-
CLAMS App Runtime Parameter
-

Defines a data model that describes a single runtime configuration of a CLAMS app. -Usually, an app keeps a list of these configuration specifications in the parameters field. -When initializing a RuntimeParameter object in python the value for the default field must be a string. -For example, if you want to set a default value for a boolean parameter, use any of 'True', 'true', 't', -or their falsy counterpart, instead of True or False

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

type

object

properties

    -
  • name

  • -
-

A short name of the parameter (works as a key).

type

string

    -
  • description

  • -
-

A longer description of the parameter (what it does, how to use, etc.).

type

string

    -
  • type

  • -
-

Type of the parameter value the app expects. Must be one of (‘integer’, ‘number’, ‘string’, ‘boolean’, ‘map’). When type is map, multivalued=true is forced, and when boolean, multivalued=false is forced.

-

Notes for developers:

-

When the type is map, the parameter value (still a single string from the users’ perspective) must be formatted as a KEY:VALUE pair, namely a colon-separated string. To pass multiple key-value pairs, users need to pass the parameter multiple times (remember type=map implies multivalued=true) with pairs in the colon-separated format.

-

Also, the VALUE part of the user input is always expected and handled as a string. If a developers wants to do more text processing on the passed value to accept more complex data types or structures (e.g., map from a string to a list of strings), it is up to the developer. However, any additional form requirements should be precisely described in the description field for users.

-

Finally, the same format is expected for the default value, if any. For example, if the default desired dictionary is {'key1': 'value1', 'key2': 'value2'}, the default value (used when initializing a parameter) should be ['key1:value1','key2:value2'] -.

-

type

string

enum

integer, number, string, boolean, map

    -
  • choices

  • -
-

(optional) List of string values that can be accepted.

default

null

anyOf

type

array

items

anyOf

type

integer

type

number

type

boolean

type

string

type

null

    -
  • default

  • -
-

(optional) Default value for the parameter.

-

Notes for developers:

-

Setting a default value makes a parameter optional.

-

When multivalued=true, the default value should be a list of values.

-

When type=map, the default value should be a list of colon-separated strings.

-

default

null

anyOf

type

integer

type

number

type

boolean

type

string

type

array

items

anyOf

type

integer

type

number

type

boolean

type

string

type

null

    -
  • multivalued

  • -
-

(optional, False by default) Set True if the parameter can have multiple values.

-

Note that, for parameters that allow multiple values, the SDK will pass a singleton list to _annotate() even when one value is passed via HTTP.

-

type

boolean

additionalProperties

False

-
-
-
-
-
- - -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/appmetadata.jsonschema b/docs/appmetadata.jsonschema deleted file mode 100644 index a1fee39..0000000 --- a/docs/appmetadata.jsonschema +++ /dev/null @@ -1,328 +0,0 @@ -{ - "$comment": "clams-python SDK 1.3.3 was used to generate this schema", - "$defs": { - "Input": { - "additionalProperties": false, - "description": "Data model that describes input specification of a CLAMS app. \n\nCLAMS apps are expected to have at least one input type, and each type must\nbe defined by a ``@type`` URI string. If the type has specific properties and values required by the app, \nthey can be described in the (optional) ``properties`` field. Finally, a human-readable \nverbose description can be provided in the (optional) ``description`` field for users.\n\nDevelopers should take diligent care to include all input types and their properties in the app metadata. ", - "properties": { - "@type": { - "description": "The type of the object. Must be a IRI string.", - "format": "uri", - "minLength": 1, - "type": "string" - }, - "description": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "default": null, - "description": "A verbose, human-readable description of the type. This is intended to be used for documentation purpose for a particular use case of this annotation type and is not expected to be consumed by software. This description should work as a guideline for users to understand the output type, and also can be used as a expansion specification for the type definition beyond the base vocabulary." - }, - "properties": { - "type": "object", - "additionalProperties": true, - "default": {}, - "description": "(optional) Specification for type properties, if any. ``\"*\"`` indicates any value." - }, - "required": { - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "null" - } - ], - "default": null, - "description": "(optional, True by default) Indicating whether this input type is mandatory or optional." - } - }, - "required": [ - "@type" - ], - "title": "CLAMS Input Specification", - "type": "object" - }, - "Output": { - "additionalProperties": false, - "description": "Data model that describes output specification of a CLAMS app. \n\nCLAMS apps are expected to have at least one output type, and each type must\nbe defined by a ``@type`` URI string. If the type has common properties and values generated by the app, \nthey can be described in the (optional) ``properties`` field. Finally, a human-readable \nverbose description can be provided in the (optional) ``description`` field for users.\n\nDevelopers should take diligent care to include all output types and their properties in the app metadata. To \nspecify the property values, developers can use an actual value (for full match) or ``'*'`` (for any value).", - "properties": { - "@type": { - "description": "The type of the object. Must be a IRI string.", - "format": "uri", - "minLength": 1, - "type": "string" - }, - "description": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "default": null, - "description": "A verbose, human-readable description of the type. This is intended to be used for documentation purpose for a particular use case of this annotation type and is not expected to be consumed by software. This description should work as a guideline for users to understand the output type, and also can be used as a expansion specification for the type definition beyond the base vocabulary." - }, - "properties": { - "type": "object", - "additionalProperties": true, - "default": {}, - "description": "(optional) Specification for type properties, if any. ``\"*\"`` indicates any value." - } - }, - "required": [ - "@type" - ], - "title": "CLAMS Output Specification", - "type": "object" - }, - "RuntimeParameter": { - "additionalProperties": false, - "description": "Defines a data model that describes a single runtime configuration of a CLAMS app. \nUsually, an app keeps a list of these configuration specifications in the ``parameters`` field. \nWhen initializing a RuntimeParameter object in python the value for the default field must be a string. \nFor example, if you want to set a default value for a boolean parameter, use any of ``'True'``, ``'true'``, ``'t'``,\nor their falsy counterpart, instead of ``True`` or ``False``", - "properties": { - "name": { - "description": "A short name of the parameter (works as a key).", - "type": "string" - }, - "description": { - "description": "A longer description of the parameter (what it does, how to use, etc.).", - "type": "string" - }, - "type": { - "description": "Type of the parameter value the app expects. Must be one of ('integer', 'number', 'string', 'boolean', 'map'). When type is ``map``, ``multivalued=true`` is forced, and when ``boolean``, ``multivalued=false`` is forced. \n\nNotes for developers: \n\nWhen the type is ``map``, the parameter value (still a single string from the users' perspective) must be formatted as a ``KEY:VALUE`` pair, namely a colon-separated string. To pass multiple key-value pairs, users need to pass the parameter multiple times (remember ``type=map`` implies ``multivalued=true``) with pairs in the colon-separated format. \n\nAlso, the `VALUE` part of the user input is always expected and handled as a string. If a developers wants to do more text processing on the passed value to accept more complex data types or structures (e.g., map from a string to a list of strings), it is up to the developer. However, any additional form requirements should be precisely described in the ``description`` field for users. \n\nFinally, the same format is expected for the default value, if any. For example, if the default desired dictionary is ``{'key1': 'value1', 'key2': 'value2'}``, the default value (used when initializing a parameter) should be ``['key1:value1','key2:value2']``\n.", - "enum": [ - "integer", - "number", - "string", - "boolean", - "map" - ], - "type": "string" - }, - "choices": { - "anyOf": [ - { - "items": { - "anyOf": [ - { - "type": "integer" - }, - { - "type": "number" - }, - { - "type": "boolean" - }, - { - "type": "string" - } - ] - }, - "type": "array" - }, - { - "type": "null" - } - ], - "default": null, - "description": "(optional) List of string values that can be accepted." - }, - "default": { - "anyOf": [ - { - "type": "integer" - }, - { - "type": "number" - }, - { - "type": "boolean" - }, - { - "type": "string" - }, - { - "items": { - "anyOf": [ - { - "type": "integer" - }, - { - "type": "number" - }, - { - "type": "boolean" - }, - { - "type": "string" - } - ] - }, - "type": "array" - }, - { - "type": "null" - } - ], - "default": null, - "description": "(optional) Default value for the parameter.\n\nNotes for developers: \n\nSetting a default value makes a parameter `optional`. \n\nWhen ``multivalued=true``, the default value should be a list of values. \n\nWhen ``type=map``, the default value should be a list of colon-separated strings. \n\n" - }, - "multivalued": { - "description": "(optional, False by default) Set True if the parameter can have multiple values.\n\nNote that, for parameters that allow multiple values, the SDK will pass a singleton list to ``_annotate()`` even when one value is passed via HTTP.", - "type": "boolean" - } - }, - "required": [ - "name", - "description", - "type", - "multivalued" - ], - "title": "CLAMS App Runtime Parameter", - "type": "object" - } - }, - "$schema": "https://json-schema.org/draft/2020-12/schema", - "additionalProperties": false, - "description": "Data model that describes a CLAMS app. \n\nCan be initialized by simply passing all required key-value pairs. \n\nIf you have a pre-generated metadata as an external file, you can read in the file as a ``dict`` and use it as \nkeyword arguments for initialization. But be careful with keys of which values are automatically generated by the \nSDK. \n\n\nPlease refer to <:ref:`appmetadata`> for the metadata specification. ", - "properties": { - "name": { - "description": "A short name of the app.", - "type": "string" - }, - "description": { - "description": "A longer description of the app (what it does, how to use, etc.).", - "type": "string" - }, - "app_version": { - "default": "", - "description": "(AUTO-GENERATED, DO NOT SET MANUALLY)\n\nVersion of the app.\n\nWhen the metadata is generated using clams-python SDK, this field is automatically filled in", - "type": "string" - }, - "mmif_version": { - "default": "", - "description": "(AUTO-GENERATED, DO NOT SET MANUALLY)\n\nVersion of MMIF specification the app.\n\nWhen the metadata is generated using clams-python SDK, this field is automatically filled in.", - "type": "string" - }, - "analyzer_version": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "default": null, - "description": "(optional) Version of an analyzer software, if the app is working as a wrapper for one. " - }, - "app_license": { - "description": "License information of the app.", - "type": "string" - }, - "analyzer_license": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "default": null, - "description": "(optional) License information of an analyzer software, if the app works as a wrapper for one. " - }, - "identifier": { - "description": "(partly AUTO-GENERATED)\n\nIRI-formatted unique identifier for the app.\n\nIf the app is to be published to the CLAMS app-directory, the developer should give a single string value composed with valid URL characters (no ``/``, no whitespace),\n\nthen when the metadata is generated using clams-python SDK, the app-directory URL is prepended and ``app_version`` value will be appended automatically.\n\nFor example, ``example-app`` -> ``http://apps.clams.ai/example-app/1.0.0``\n\nOtherwise, only the ``app_version`` value is used as suffix, so use an IRI form, but leave the version number out.", - "format": "uri", - "minLength": 1, - "type": "string" - }, - "url": { - "description": "A public repository where the app's source code (git-based) and/or documentation is available. ", - "format": "uri", - "minLength": 1, - "type": "string" - }, - "input": { - "default": [], - "description": "List of input types. Must have at least one element.\n\nThis list should iterate all input types in an exhaustive and meticulous manner, meaning it is recommended for developers to pay extra attention to ``input`` and ``output`` fields to include 1) all types are listed, 2) if types to have specific properties, include the properties.\n\nThis list should interpreted conjunctive (``AND``).\n\nHowever, a nested list in this list means ``oneOf`` disjunctive (``OR``) specification.\n\nFor example, ``input = [TypeA (req=True), [TypeB, TypeC]]`` means``TypeA`` is required and either ``TypeB`` or ``TypeC`` is additionally required.\n\nAll input elements in the nested list must not be ``required=False``, and only a single nesting level is allowed (e.g. ``input = [TypeA, [ [TypeB, TypeC], [TypeD, TypeE] ] ]`` is not allowed).", - "items": { - "anyOf": [ - { - "$ref": "#/$defs/Input" - }, - { - "items": { - "$ref": "#/$defs/Input" - }, - "type": "array" - } - ] - }, - "type": "array" - }, - "output": { - "default": [], - "description": "List of output types. Must have at least one element.This list should iterate all output types in an exhaustive and meticulous manner, meaning it is recommended for developers to pay extra attention to ``input`` and ``output`` fields to include ", - "items": { - "$ref": "#/$defs/Output" - }, - "type": "array" - }, - "parameters": { - "default": [], - "description": "List of runtime parameters. Can be empty.", - "items": { - "$ref": "#/$defs/RuntimeParameter" - }, - "type": "array" - }, - "dependencies": { - "anyOf": [ - { - "items": { - "type": "string" - }, - "type": "array" - }, - { - "type": "null" - } - ], - "default": null, - "description": "(optional) List of software dependencies of the app. \n\nThis list is completely optional, as in most cases such dependencies are specified in a separate file in the codebase of the app (for example, ``requirements.txt`` file for a Python app, or ``pom.xml`` file for a maven-based Java app).\n\nList items must be strings, not any kind of structured data. Thus, it is recommended to include a package name and its version in the string value at the minimum (e.g., ``clams-python==1.2.3``)." - }, - "more": { - "anyOf": [ - { - "additionalProperties": { - "type": "string" - }, - "type": "object" - }, - { - "type": "null" - } - ], - "default": null, - "description": "(optional) A string-to-string map that can be used to store any additional metadata of the app." - } - }, - "required": [ - "name", - "description", - "app_license", - "identifier", - "url" - ], - "title": "CLAMS AppMetadata", - "type": "object" -} diff --git a/docs/autodoc/clams.app.html b/docs/autodoc/clams.app.html deleted file mode 100644 index 01feed9..0000000 --- a/docs/autodoc/clams.app.html +++ /dev/null @@ -1,360 +0,0 @@ - - - - - - - - - clams.app package — clams-python documentation - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -
-

clams.app package

-

Core package providing classes for representing CLAMS apps.

-
-
-class clams.app.ClamsApp[source]
-

Bases: ABC

-

An abstract class to define API’s for ClamsApps. A CLAMS app should inherit -this class and then can be used with classes in restify to work as -web applications.

-
-
-_RAW_PARAMS_KEY = '#RAW#'[source]
-
- -
-
-_abc_impl = <_abc._abc_data object>[source]
-
- -
-
-abstract _annotate(mmif: Mmif, _raw_parameters=None, **refined_parameters) Mmif[source]
-

An abstract method to generate (or load if stored elsewhere) the app -metadata at runtime. All CLAMS app must implement this.

-

This is where the bulk of your logic will go. -A typical implementation of this method would be

-
    -
  1. Create a new view (or views) by calling new_view() on the input mmif object.

  2. -
  3. Call sign_view() with the input runtime parameters for the record.

  4. -
  5. Call new_contain() on the new view object with any annotation properties specified by the configuration.

  6. -
  7. Process the data and create Annotation objects and add them to the new view.

  8. -
  9. While doing so, get help from DocumentTypes, AnnotationTypes classes to generate @type strings.

  10. -
  11. Return the mmif object

  12. -
-
-
Parameters:
-
    -
  • mmif – An input MMIF object to annotate

  • -
  • runtime_params – An arbitrary set of k-v pairs to configure the app at runtime

  • -
-
-
Returns:
-

A Mmif object of the annotated output, ready for serialization

-
-
-
- -
-
-abstract _appmetadata() AppMetadata[source]
-

An abstract method to generate the app metadata.

-
-
Returns:
-

A Python object of the metadata, must be JSON-serializable

-
-
-
- -
-
-static _check_mmif_compatibility(target_specver, input_specver)[source]
-
- -
-
-_load_appmetadata() AppMetadata[source]
-

A private method to load the app metadata. This is called in __init__, -(only once) and it uses three sources to load the metadata (in the order -of priority):

-
    -
  1. using a metadata.py file (recommended)

  2. -
  3. using self._appmetadata() method (legacy, no longer recommended)

  4. -
-

In any case, AppMetadata class must be useful.

-

For metadata specification, -see https://sdk.clams.ai/appmetadata.jsonschema.

-
- -
-
-_refine_params(**runtime_params: List[str])[source]
-

Method to “fill” the parameter dictionary with default values, when a key-value is not specified in the input. -The input map is not really “filled” as a copy of it is returned with addition of default values. -:param runtime_params: key-value pairs of runtime parameters -:return: a copy of parameter map, with default values added -:raises ValueError: when a value for a required parameter is not found in the input

-
- -
-
-annotate(mmif: str | dict | Mmif, **runtime_params: List[str]) str[source]
-

A public method to invoke the primary app function. It’s essentially a -wrapper around _annotate() method where some common operations -(that are invoked by keyword arguments) are implemented.

-
-
Parameters:
-
    -
  • mmif – An input MMIF object to annotate

  • -
  • runtime_params – An arbitrary set of k-v pairs to configure the app at runtime

  • -
-
-
Returns:
-

Serialized JSON string of the output of the app

-
-
-
- -
-
-appmetadata(**kwargs: List[str]) str[source]
-

A public method to get metadata for this app as a string.

-
-
Returns:
-

Serialized JSON string of the metadata

-
-
-
- -
-
-get_configuration(**runtime_params)[source]
-
- -
-
-static open_document_location(document: str | ~mmif.serialize.annotation.Document, opener: ~typing.Any = <built-in function open>, **openerargs)[source]
-

A context-providing file opener. A user can provide their own opener -class/method and parameters. By default, with will use python built-in -open to open the location of the document.

-
-
Parameters:
-
    -
  • document – A Document object that has location

  • -
  • opener – A Python class or method that can be used to open a file (e.g. PIL.Image for an image file)

  • -
  • openerargs – Parameters that are passed to the opener

  • -
-
-
Returns:
-

-
-
-
- -
-
-record_error(mmif: str | dict | Mmif, **runtime_conf: List[str]) Mmif[source]
-

A method to record an error instead of annotation results in the view -this app generated. For logging purpose, the runtime parameters used -when the error occurred must be passed as well.

-
-
Parameters:
-
    -
  • mmif – input MMIF object

  • -
  • runtime_conf – parameters passed to annotate when the app encountered the error

  • -
-
-
Returns:
-

An output MMIF with a new view with the error encoded in the view metadata

-
-
-
- -
-
-set_error_view(mmif: str | dict | Mmif, **runtime_conf: List[str]) Mmif[source]
-

A method to record an error instead of annotation results in the view -this app generated. For logging purpose, the runtime parameters used -when the error occurred must be passed as well.

-
-
Parameters:
-
    -
  • mmif – input MMIF object

  • -
  • runtime_conf – parameters passed to annotate when the app encountered the error

  • -
-
-
Returns:
-

An output MMIF with a new view with the error encoded in the view metadata

-
-
-
- -
-
-sign_view(view: View, runtime_conf: dict) None[source]
-

A method to “sign” a new view that this app creates at the beginning of annotation. -Signing will populate the view metadata with information and configuration of this app. -The parameters passed to the _annotate() must be -passed to this method. This means all parameters for “common” configuration that -are consumed in annotate() should not be recorded in the -view metadata. -:param view: a view to sign -:param runtime_conf: runtime configuration of the app as k-v pairs

-
- -
-
-universal_parameters = [{'choices': None, 'default': False, 'description': 'The JSON body of the HTTP response will be re-formatted with 2-space indentation', 'multivalued': False, 'name': 'pretty', 'type': 'boolean'}, {'choices': None, 'default': False, 'description': 'The running time of the app will be recorded in the view metadata', 'multivalued': False, 'name': 'runningTime', 'type': 'boolean'}, {'choices': None, 'default': False, 'description': 'The hardware information (architecture, GPU and vRAM) will be recorded in the view metadata', 'multivalued': False, 'name': 'hwFetch', 'type': 'boolean'}][source]
-
- -
-
-static validate_document_locations(mmif: str | Mmif) None[source]
-

Validate files encoded in the input MMIF.

-
-
Parameters:
-

mmif – An input MMIF with zero or more Document

-
-
Raises:
-

FileNotFoundError – When any of files is not found at its location

-
-
-
- -
- -
- - -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/autodoc/clams.appmetadata.html b/docs/autodoc/clams.appmetadata.html deleted file mode 100644 index eefe1ee..0000000 --- a/docs/autodoc/clams.appmetadata.html +++ /dev/null @@ -1,1179 +0,0 @@ - - - - - - - - - clams.appmetadata package — clams-python documentation - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -
-

clams.appmetadata package

-

Package providing classes for representing metadata of CLAMS apps.

-
-
-class clams.appmetadata.AppMetadata(*, name: str, description: str, app_version: str = '', mmif_version: str = '', analyzer_version: str | None = None, app_license: str, analyzer_license: str | None = None, identifier: AnyHttpUrl, url: AnyHttpUrl, input: List[Input | List[Input]] = [], output: List[Output] = [], parameters: List[RuntimeParameter] = [], dependencies: List[str] | None = None, more: Dict[str, str] | None = None)[source]
-

Data model that describes a CLAMS app.

-

Can be initialized by simply passing all required key-value pairs.

-

If you have a pre-generated metadata as an external file, you can read in the file as a dict and use it as -keyword arguments for initialization. But be careful with keys of which values are automatically generated by the -SDK.

-

Please refer to <CLAMS App Metadata> for the metadata specification.

-
-
-add_input(at_type: str | TypesBase, required: bool = True, **properties) Input[source]
-

Helper method to add an element to the input list.

-
-
Parameters:
-
    -
  • at_type@type of the input object

  • -
  • required – whether this type is mandatory or optional

  • -
  • properties – additional property specifications

  • -
-
-
Returns:
-

the newly added Input object

-
-
-
- -
-
-add_more(key: str, value: str)[source]
-

Helper method to add a k-v pair to the more map. -:param key: key of an additional metadata -:param value: value of the additional metadata

-
- -
-
-add_output(at_type: str | TypesBase, **properties) Output[source]
-

Helper method to add an element to the output list.

-
-
Parameters:
-
    -
  • at_type@type of the input object

  • -
  • properties – additional property specifications

  • -
-
-
Returns:
-

the newly added Output object

-
-
-
- -
-
-add_parameter(name: str, description: str, type: Literal['integer', 'number', 'string', 'boolean', 'map'], choices: List[int | float | bool | str] | None = None, multivalued: bool = False, default: None | int | float | bool | str | List[int | float | bool | str] = None)[source]
-

Helper method to add an element to the parameters list.

-
- -
-
-model_config: ClassVar[ConfigDict] = {'extra': 'forbid', 'json_schema_extra': <function AppMetadata.<lambda>>, 'title': 'CLAMS AppMetadata', 'validate_by_name': True}[source]
-

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

-
- -
- -
-
-class clams.appmetadata.Input(*, at_type: AnyHttpUrl, description: str | None = None, properties: Dict[str, Any] = {}, required: bool | None = None)[source]
-

Data model that describes input specification of a CLAMS app.

-

CLAMS apps are expected to have at least one input type, and each type must -be defined by a @type URI string. If the type has specific properties and values required by the app, -they can be described in the (optional) properties field. Finally, a human-readable -verbose description can be provided in the (optional) description field for users.

-

Developers should take diligent care to include all input types and their properties in the app metadata.

-
-
-add_description(description: str)[source]
-

Add description string to the type specification.

-
- -
-
-copy(*, include: AbstractSetIntStr | MappingIntStrAny | None = None, exclude: AbstractSetIntStr | MappingIntStrAny | None = None, update: Dict[str, Any] | None = None, deep: bool = False) Self[source]
-

Returns a copy of the model.

-
-
!!! warning “Deprecated”

This method is now deprecated; use model_copy instead.

-
-
-

If you need include or exclude, use:

-

`python {test="skip" lint="skip"} -data = self.model_dump(include=include, exclude=exclude, round_trip=True) -data = {**data, **(update or {})} -copied = self.model_validate(data) -`

-
-
Args:

include: Optional set or mapping specifying which fields to include in the copied model. -exclude: Optional set or mapping specifying which fields to exclude in the copied model. -update: Optional dictionary of field-value pairs to override field values in the copied model. -deep: If True, the values of fields that are Pydantic models will be deep-copied.

-
-
Returns:

A copy of the model with included, excluded and updated fields as specified.

-
-
-
- -
-
-model_config: ClassVar[ConfigDict] = {'extra': 'forbid', 'json_schema_extra': <function pop_titles>, 'title': 'CLAMS Input Specification', 'validate_by_name': True}[source]
-

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

-
- -
-
-classmethod model_construct(_fields_set: set[str] | None = None, **values: Any) Self[source]
-

Creates a new instance of the Model class with validated data.

-

Creates a new model setting __dict__ and __pydantic_fields_set__ from trusted or pre-validated data. -Default values are respected, but no other validation is performed.

-
-
!!! note

model_construct() generally respects the model_config.extra setting on the provided model. -That is, if model_config.extra == ‘allow’, then all extra passed values are added to the model instance’s __dict__ -and __pydantic_extra__ fields. If model_config.extra == ‘ignore’ (the default), then all extra passed values are ignored. -Because no validation is performed with a call to model_construct(), having model_config.extra == ‘forbid’ does not result in -an error if extra values are passed, but they will be ignored.

-
-
Args:
-
_fields_set: A set of field names that were originally explicitly set during instantiation. If provided,

this is directly used for the [model_fields_set][pydantic.BaseModel.model_fields_set] attribute. -Otherwise, the field names from the values argument will be used.

-
-
-

values: Trusted or pre-validated data dictionary.

-
-
Returns:

A new instance of the Model class with validated data.

-
-
-
- -
-
-model_copy(*, update: Mapping[str, Any] | None = None, deep: bool = False) Self[source]
-
-
!!! abstract “Usage Documentation”

[model_copy](../concepts/serialization.md#model_copy)

-
-
-

Returns a copy of the model.

-
-
!!! note

The underlying instance’s [__dict__][object.__dict__] attribute is copied. This -might have unexpected side effects if you store anything in it, on top of the model -fields (e.g. the value of [cached properties][functools.cached_property]).

-
-
Args:
-
update: Values to change/add in the new model. Note: the data is not validated

before creating the new model. You should trust this data.

-
-
-

deep: Set to True to make a deep copy of the model.

-
-
Returns:

New model instance.

-
-
-
- -
-
-model_dump(*, mode: Literal['json', 'python'] | str = 'python', include: set[int] | set[str] | Mapping[int, set[int] | set[str] | Mapping[int, IncEx | bool] | Mapping[str, IncEx | bool] | bool] | Mapping[str, set[int] | set[str] | Mapping[int, IncEx | bool] | Mapping[str, IncEx | bool] | bool] | None = None, exclude: set[int] | set[str] | Mapping[int, set[int] | set[str] | Mapping[int, IncEx | bool] | Mapping[str, IncEx | bool] | bool] | Mapping[str, set[int] | set[str] | Mapping[int, IncEx | bool] | Mapping[str, IncEx | bool] | bool] | None = None, context: Any | None = None, by_alias: bool | None = None, exclude_unset: bool = False, exclude_defaults: bool = False, exclude_none: bool = False, round_trip: bool = False, warnings: bool | Literal['none', 'warn', 'error'] = True, fallback: Callable[[Any], Any] | None = None, serialize_as_any: bool = False) dict[str, Any][source]
-
-
!!! abstract “Usage Documentation”

[model_dump](../concepts/serialization.md#modelmodel_dump)

-
-
-

Generate a dictionary representation of the model, optionally specifying which fields to include or exclude.

-
-
Args:
-
mode: The mode in which to_python should run.

If mode is ‘json’, the output will only contain JSON serializable types. -If mode is ‘python’, the output may contain non-JSON-serializable Python objects.

-
-
-

include: A set of fields to include in the output. -exclude: A set of fields to exclude from the output. -context: Additional context to pass to the serializer. -by_alias: Whether to use the field’s alias in the dictionary key if defined. -exclude_unset: Whether to exclude fields that have not been explicitly set. -exclude_defaults: Whether to exclude fields that are set to their default value. -exclude_none: Whether to exclude fields that have a value of None. -round_trip: If True, dumped values should be valid as input for non-idempotent types such as Json[T]. -warnings: How to handle serialization errors. False/”none” ignores them, True/”warn” logs errors,

-
-

“error” raises a [PydanticSerializationError][pydantic_core.PydanticSerializationError].

-
-
-
fallback: A function to call when an unknown value is encountered. If not provided,

a [PydanticSerializationError][pydantic_core.PydanticSerializationError] error is raised.

-
-
-

serialize_as_any: Whether to serialize fields with duck-typing serialization behavior.

-
-
Returns:

A dictionary representation of the model.

-
-
-
- -
-
-model_dump_json(*, indent: int | None = None, include: set[int] | set[str] | Mapping[int, set[int] | set[str] | Mapping[int, IncEx | bool] | Mapping[str, IncEx | bool] | bool] | Mapping[str, set[int] | set[str] | Mapping[int, IncEx | bool] | Mapping[str, IncEx | bool] | bool] | None = None, exclude: set[int] | set[str] | Mapping[int, set[int] | set[str] | Mapping[int, IncEx | bool] | Mapping[str, IncEx | bool] | bool] | Mapping[str, set[int] | set[str] | Mapping[int, IncEx | bool] | Mapping[str, IncEx | bool] | bool] | None = None, context: Any | None = None, by_alias: bool | None = None, exclude_unset: bool = False, exclude_defaults: bool = False, exclude_none: bool = False, round_trip: bool = False, warnings: bool | Literal['none', 'warn', 'error'] = True, fallback: Callable[[Any], Any] | None = None, serialize_as_any: bool = False) str[source]
-
-
!!! abstract “Usage Documentation”

[model_dump_json](../concepts/serialization.md#modelmodel_dump_json)

-
-
-

Generates a JSON representation of the model using Pydantic’s to_json method.

-
-
Args:

indent: Indentation to use in the JSON output. If None is passed, the output will be compact. -include: Field(s) to include in the JSON output. -exclude: Field(s) to exclude from the JSON output. -context: Additional context to pass to the serializer. -by_alias: Whether to serialize using field aliases. -exclude_unset: Whether to exclude fields that have not been explicitly set. -exclude_defaults: Whether to exclude fields that are set to their default value. -exclude_none: Whether to exclude fields that have a value of None. -round_trip: If True, dumped values should be valid as input for non-idempotent types such as Json[T]. -warnings: How to handle serialization errors. False/”none” ignores them, True/”warn” logs errors,

-
-

“error” raises a [PydanticSerializationError][pydantic_core.PydanticSerializationError].

-
-
-
fallback: A function to call when an unknown value is encountered. If not provided,

a [PydanticSerializationError][pydantic_core.PydanticSerializationError] error is raised.

-
-
-

serialize_as_any: Whether to serialize fields with duck-typing serialization behavior.

-
-
Returns:

A JSON string representation of the model.

-
-
-
- -
-
-property model_extra: dict[str, Any] | None[source]
-

Get extra fields set during validation.

-
-
Returns:

A dictionary of extra fields, or None if config.extra is not set to “allow”.

-
-
-
- -
-
-property model_fields_set: set[str][source]
-

Returns the set of fields that have been explicitly set on this model instance.

-
-
Returns:
-
A set of strings representing the fields that have been set,

i.e. that were not filled from defaults.

-
-
-
-
-
- -
-
-classmethod model_json_schema(by_alias: bool = True, ref_template: str = '#/$defs/{model}', schema_generator: type[~pydantic.json_schema.GenerateJsonSchema] = <class 'pydantic.json_schema.GenerateJsonSchema'>, mode: ~typing.Literal['validation', 'serialization'] = 'validation') dict[str, Any][source]
-

Generates a JSON schema for a model class.

-
-
Args:

by_alias: Whether to use attribute aliases or not. -ref_template: The reference template. -schema_generator: To override the logic used to generate the JSON schema, as a subclass of

-
-

GenerateJsonSchema with your desired modifications

-
-

mode: The mode in which to generate the schema.

-
-
Returns:

The JSON schema for the given model class.

-
-
-
- -
-
-classmethod model_parametrized_name(params: tuple[type[Any], ...]) str[source]
-

Compute the class name for parametrizations of generic classes.

-

This method can be overridden to achieve a custom naming scheme for generic BaseModels.

-
-
Args:
-
params: Tuple of types of the class. Given a generic class

Model with 2 type variables and a concrete model Model[str, int], -the value (str, int) would be passed to params.

-
-
-
-
Returns:

String representing the new class where params are passed to cls as type variables.

-
-
Raises:

TypeError: Raised when trying to generate concrete names for non-generic models.

-
-
-
- -
-
-model_post_init(context: Any, /) None[source]
-

Override this method to perform additional initialization after __init__ and model_construct. -This is useful if you want to do some validation that requires the entire model to be initialized.

-
- -
-
-classmethod model_rebuild(*, force: bool = False, raise_errors: bool = True, _parent_namespace_depth: int = 2, _types_namespace: MappingNamespace | None = None) bool | None[source]
-

Try to rebuild the pydantic-core schema for the model.

-

This may be necessary when one of the annotations is a ForwardRef which could not be resolved during -the initial attempt to build the schema, and automatic rebuilding fails.

-
-
Args:

force: Whether to force the rebuilding of the model schema, defaults to False. -raise_errors: Whether to raise errors, defaults to True. -_parent_namespace_depth: The depth level of the parent namespace, defaults to 2. -_types_namespace: The types namespace, defaults to None.

-
-
Returns:

Returns None if the schema is already “complete” and rebuilding was not required. -If rebuilding _was_ required, returns True if rebuilding was successful, otherwise False.

-
-
-
- -
-
-classmethod model_validate(obj: Any, *, strict: bool | None = None, from_attributes: bool | None = None, context: Any | None = None, by_alias: bool | None = None, by_name: bool | None = None) Self[source]
-

Validate a pydantic model instance.

-
-
Args:

obj: The object to validate. -strict: Whether to enforce types strictly. -from_attributes: Whether to extract data from object attributes. -context: Additional context to pass to the validator. -by_alias: Whether to use the field’s alias when validating against the provided input data. -by_name: Whether to use the field’s name when validating against the provided input data.

-
-
Raises:

ValidationError: If the object could not be validated.

-
-
Returns:

The validated model instance.

-
-
-
- -
-
-classmethod model_validate_json(json_data: str | bytes | bytearray, *, strict: bool | None = None, context: Any | None = None, by_alias: bool | None = None, by_name: bool | None = None) Self[source]
-
-
!!! abstract “Usage Documentation”

[JSON Parsing](../concepts/json.md#json-parsing)

-
-
-

Validate the given JSON data against the Pydantic model.

-
-
Args:

json_data: The JSON data to validate. -strict: Whether to enforce types strictly. -context: Extra variables to pass to the validator. -by_alias: Whether to use the field’s alias when validating against the provided input data. -by_name: Whether to use the field’s name when validating against the provided input data.

-
-
Returns:

The validated Pydantic model.

-
-
Raises:

ValidationError: If json_data is not a JSON string or the object could not be validated.

-
-
-
- -
-
-classmethod model_validate_strings(obj: Any, *, strict: bool | None = None, context: Any | None = None, by_alias: bool | None = None, by_name: bool | None = None) Self[source]
-

Validate the given object with string data against the Pydantic model.

-
-
Args:

obj: The object containing string data to validate. -strict: Whether to enforce types strictly. -context: Extra variables to pass to the validator. -by_alias: Whether to use the field’s alias when validating against the provided input data. -by_name: Whether to use the field’s name when validating against the provided input data.

-
-
Returns:

The validated Pydantic model.

-
-
-
- -
- -
-
-class clams.appmetadata.Output(*, at_type: AnyHttpUrl, description: str | None = None, properties: Dict[str, Any] = {})[source]
-

Data model that describes output specification of a CLAMS app.

-

CLAMS apps are expected to have at least one output type, and each type must -be defined by a @type URI string. If the type has common properties and values generated by the app, -they can be described in the (optional) properties field. Finally, a human-readable -verbose description can be provided in the (optional) description field for users.

-

Developers should take diligent care to include all output types and their properties in the app metadata. To -specify the property values, developers can use an actual value (for full match) or '*' (for any value).

-
-
-add_description(description: str)[source]
-

Add description string to the type specification.

-
- -
-
-copy(*, include: AbstractSetIntStr | MappingIntStrAny | None = None, exclude: AbstractSetIntStr | MappingIntStrAny | None = None, update: Dict[str, Any] | None = None, deep: bool = False) Self[source]
-

Returns a copy of the model.

-
-
!!! warning “Deprecated”

This method is now deprecated; use model_copy instead.

-
-
-

If you need include or exclude, use:

-

`python {test="skip" lint="skip"} -data = self.model_dump(include=include, exclude=exclude, round_trip=True) -data = {**data, **(update or {})} -copied = self.model_validate(data) -`

-
-
Args:

include: Optional set or mapping specifying which fields to include in the copied model. -exclude: Optional set or mapping specifying which fields to exclude in the copied model. -update: Optional dictionary of field-value pairs to override field values in the copied model. -deep: If True, the values of fields that are Pydantic models will be deep-copied.

-
-
Returns:

A copy of the model with included, excluded and updated fields as specified.

-
-
-
- -
-
-model_config: ClassVar[ConfigDict] = {'extra': 'forbid', 'json_schema_extra': <function pop_titles>, 'title': 'CLAMS Output Specification', 'validate_by_name': True}[source]
-

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

-
- -
-
-classmethod model_construct(_fields_set: set[str] | None = None, **values: Any) Self[source]
-

Creates a new instance of the Model class with validated data.

-

Creates a new model setting __dict__ and __pydantic_fields_set__ from trusted or pre-validated data. -Default values are respected, but no other validation is performed.

-
-
!!! note

model_construct() generally respects the model_config.extra setting on the provided model. -That is, if model_config.extra == ‘allow’, then all extra passed values are added to the model instance’s __dict__ -and __pydantic_extra__ fields. If model_config.extra == ‘ignore’ (the default), then all extra passed values are ignored. -Because no validation is performed with a call to model_construct(), having model_config.extra == ‘forbid’ does not result in -an error if extra values are passed, but they will be ignored.

-
-
Args:
-
_fields_set: A set of field names that were originally explicitly set during instantiation. If provided,

this is directly used for the [model_fields_set][pydantic.BaseModel.model_fields_set] attribute. -Otherwise, the field names from the values argument will be used.

-
-
-

values: Trusted or pre-validated data dictionary.

-
-
Returns:

A new instance of the Model class with validated data.

-
-
-
- -
-
-model_copy(*, update: Mapping[str, Any] | None = None, deep: bool = False) Self[source]
-
-
!!! abstract “Usage Documentation”

[model_copy](../concepts/serialization.md#model_copy)

-
-
-

Returns a copy of the model.

-
-
!!! note

The underlying instance’s [__dict__][object.__dict__] attribute is copied. This -might have unexpected side effects if you store anything in it, on top of the model -fields (e.g. the value of [cached properties][functools.cached_property]).

-
-
Args:
-
update: Values to change/add in the new model. Note: the data is not validated

before creating the new model. You should trust this data.

-
-
-

deep: Set to True to make a deep copy of the model.

-
-
Returns:

New model instance.

-
-
-
- -
-
-model_dump(*, mode: Literal['json', 'python'] | str = 'python', include: set[int] | set[str] | Mapping[int, set[int] | set[str] | Mapping[int, IncEx | bool] | Mapping[str, IncEx | bool] | bool] | Mapping[str, set[int] | set[str] | Mapping[int, IncEx | bool] | Mapping[str, IncEx | bool] | bool] | None = None, exclude: set[int] | set[str] | Mapping[int, set[int] | set[str] | Mapping[int, IncEx | bool] | Mapping[str, IncEx | bool] | bool] | Mapping[str, set[int] | set[str] | Mapping[int, IncEx | bool] | Mapping[str, IncEx | bool] | bool] | None = None, context: Any | None = None, by_alias: bool | None = None, exclude_unset: bool = False, exclude_defaults: bool = False, exclude_none: bool = False, round_trip: bool = False, warnings: bool | Literal['none', 'warn', 'error'] = True, fallback: Callable[[Any], Any] | None = None, serialize_as_any: bool = False) dict[str, Any][source]
-
-
!!! abstract “Usage Documentation”

[model_dump](../concepts/serialization.md#modelmodel_dump)

-
-
-

Generate a dictionary representation of the model, optionally specifying which fields to include or exclude.

-
-
Args:
-
mode: The mode in which to_python should run.

If mode is ‘json’, the output will only contain JSON serializable types. -If mode is ‘python’, the output may contain non-JSON-serializable Python objects.

-
-
-

include: A set of fields to include in the output. -exclude: A set of fields to exclude from the output. -context: Additional context to pass to the serializer. -by_alias: Whether to use the field’s alias in the dictionary key if defined. -exclude_unset: Whether to exclude fields that have not been explicitly set. -exclude_defaults: Whether to exclude fields that are set to their default value. -exclude_none: Whether to exclude fields that have a value of None. -round_trip: If True, dumped values should be valid as input for non-idempotent types such as Json[T]. -warnings: How to handle serialization errors. False/”none” ignores them, True/”warn” logs errors,

-
-

“error” raises a [PydanticSerializationError][pydantic_core.PydanticSerializationError].

-
-
-
fallback: A function to call when an unknown value is encountered. If not provided,

a [PydanticSerializationError][pydantic_core.PydanticSerializationError] error is raised.

-
-
-

serialize_as_any: Whether to serialize fields with duck-typing serialization behavior.

-
-
Returns:

A dictionary representation of the model.

-
-
-
- -
-
-model_dump_json(*, indent: int | None = None, include: set[int] | set[str] | Mapping[int, set[int] | set[str] | Mapping[int, IncEx | bool] | Mapping[str, IncEx | bool] | bool] | Mapping[str, set[int] | set[str] | Mapping[int, IncEx | bool] | Mapping[str, IncEx | bool] | bool] | None = None, exclude: set[int] | set[str] | Mapping[int, set[int] | set[str] | Mapping[int, IncEx | bool] | Mapping[str, IncEx | bool] | bool] | Mapping[str, set[int] | set[str] | Mapping[int, IncEx | bool] | Mapping[str, IncEx | bool] | bool] | None = None, context: Any | None = None, by_alias: bool | None = None, exclude_unset: bool = False, exclude_defaults: bool = False, exclude_none: bool = False, round_trip: bool = False, warnings: bool | Literal['none', 'warn', 'error'] = True, fallback: Callable[[Any], Any] | None = None, serialize_as_any: bool = False) str[source]
-
-
!!! abstract “Usage Documentation”

[model_dump_json](../concepts/serialization.md#modelmodel_dump_json)

-
-
-

Generates a JSON representation of the model using Pydantic’s to_json method.

-
-
Args:

indent: Indentation to use in the JSON output. If None is passed, the output will be compact. -include: Field(s) to include in the JSON output. -exclude: Field(s) to exclude from the JSON output. -context: Additional context to pass to the serializer. -by_alias: Whether to serialize using field aliases. -exclude_unset: Whether to exclude fields that have not been explicitly set. -exclude_defaults: Whether to exclude fields that are set to their default value. -exclude_none: Whether to exclude fields that have a value of None. -round_trip: If True, dumped values should be valid as input for non-idempotent types such as Json[T]. -warnings: How to handle serialization errors. False/”none” ignores them, True/”warn” logs errors,

-
-

“error” raises a [PydanticSerializationError][pydantic_core.PydanticSerializationError].

-
-
-
fallback: A function to call when an unknown value is encountered. If not provided,

a [PydanticSerializationError][pydantic_core.PydanticSerializationError] error is raised.

-
-
-

serialize_as_any: Whether to serialize fields with duck-typing serialization behavior.

-
-
Returns:

A JSON string representation of the model.

-
-
-
- -
-
-property model_extra: dict[str, Any] | None[source]
-

Get extra fields set during validation.

-
-
Returns:

A dictionary of extra fields, or None if config.extra is not set to “allow”.

-
-
-
- -
-
-property model_fields_set: set[str][source]
-

Returns the set of fields that have been explicitly set on this model instance.

-
-
Returns:
-
A set of strings representing the fields that have been set,

i.e. that were not filled from defaults.

-
-
-
-
-
- -
-
-classmethod model_json_schema(by_alias: bool = True, ref_template: str = '#/$defs/{model}', schema_generator: type[~pydantic.json_schema.GenerateJsonSchema] = <class 'pydantic.json_schema.GenerateJsonSchema'>, mode: ~typing.Literal['validation', 'serialization'] = 'validation') dict[str, Any][source]
-

Generates a JSON schema for a model class.

-
-
Args:

by_alias: Whether to use attribute aliases or not. -ref_template: The reference template. -schema_generator: To override the logic used to generate the JSON schema, as a subclass of

-
-

GenerateJsonSchema with your desired modifications

-
-

mode: The mode in which to generate the schema.

-
-
Returns:

The JSON schema for the given model class.

-
-
-
- -
-
-classmethod model_parametrized_name(params: tuple[type[Any], ...]) str[source]
-

Compute the class name for parametrizations of generic classes.

-

This method can be overridden to achieve a custom naming scheme for generic BaseModels.

-
-
Args:
-
params: Tuple of types of the class. Given a generic class

Model with 2 type variables and a concrete model Model[str, int], -the value (str, int) would be passed to params.

-
-
-
-
Returns:

String representing the new class where params are passed to cls as type variables.

-
-
Raises:

TypeError: Raised when trying to generate concrete names for non-generic models.

-
-
-
- -
-
-model_post_init(context: Any, /) None[source]
-

Override this method to perform additional initialization after __init__ and model_construct. -This is useful if you want to do some validation that requires the entire model to be initialized.

-
- -
-
-classmethod model_rebuild(*, force: bool = False, raise_errors: bool = True, _parent_namespace_depth: int = 2, _types_namespace: MappingNamespace | None = None) bool | None[source]
-

Try to rebuild the pydantic-core schema for the model.

-

This may be necessary when one of the annotations is a ForwardRef which could not be resolved during -the initial attempt to build the schema, and automatic rebuilding fails.

-
-
Args:

force: Whether to force the rebuilding of the model schema, defaults to False. -raise_errors: Whether to raise errors, defaults to True. -_parent_namespace_depth: The depth level of the parent namespace, defaults to 2. -_types_namespace: The types namespace, defaults to None.

-
-
Returns:

Returns None if the schema is already “complete” and rebuilding was not required. -If rebuilding _was_ required, returns True if rebuilding was successful, otherwise False.

-
-
-
- -
-
-classmethod model_validate(obj: Any, *, strict: bool | None = None, from_attributes: bool | None = None, context: Any | None = None, by_alias: bool | None = None, by_name: bool | None = None) Self[source]
-

Validate a pydantic model instance.

-
-
Args:

obj: The object to validate. -strict: Whether to enforce types strictly. -from_attributes: Whether to extract data from object attributes. -context: Additional context to pass to the validator. -by_alias: Whether to use the field’s alias when validating against the provided input data. -by_name: Whether to use the field’s name when validating against the provided input data.

-
-
Raises:

ValidationError: If the object could not be validated.

-
-
Returns:

The validated model instance.

-
-
-
- -
-
-classmethod model_validate_json(json_data: str | bytes | bytearray, *, strict: bool | None = None, context: Any | None = None, by_alias: bool | None = None, by_name: bool | None = None) Self[source]
-
-
!!! abstract “Usage Documentation”

[JSON Parsing](../concepts/json.md#json-parsing)

-
-
-

Validate the given JSON data against the Pydantic model.

-
-
Args:

json_data: The JSON data to validate. -strict: Whether to enforce types strictly. -context: Extra variables to pass to the validator. -by_alias: Whether to use the field’s alias when validating against the provided input data. -by_name: Whether to use the field’s name when validating against the provided input data.

-
-
Returns:

The validated Pydantic model.

-
-
Raises:

ValidationError: If json_data is not a JSON string or the object could not be validated.

-
-
-
- -
-
-classmethod model_validate_strings(obj: Any, *, strict: bool | None = None, context: Any | None = None, by_alias: bool | None = None, by_name: bool | None = None) Self[source]
-

Validate the given object with string data against the Pydantic model.

-
-
Args:

obj: The object containing string data to validate. -strict: Whether to enforce types strictly. -context: Extra variables to pass to the validator. -by_alias: Whether to use the field’s alias when validating against the provided input data. -by_name: Whether to use the field’s name when validating against the provided input data.

-
-
Returns:

The validated Pydantic model.

-
-
-
- -
- -
-
-class clams.appmetadata.RuntimeParameter(*, name: str, description: str, type: Literal['integer', 'number', 'string', 'boolean', 'map'], choices: List[int | float | bool | str] | None = None, default: Annotated[int | float | bool | str | List[int | float | bool | str] | None, _PydanticGeneralMetadata(union_mode='left_to_right')] = None, multivalued: bool)[source]
-

Defines a data model that describes a single runtime configuration of a CLAMS app. -Usually, an app keeps a list of these configuration specifications in the parameters field. -When initializing a RuntimeParameter object in python the value for the default field must be a string. -For example, if you want to set a default value for a boolean parameter, use any of 'True', 'true', 't', -or their falsy counterpart, instead of True or False

-
-
-copy(*, include: AbstractSetIntStr | MappingIntStrAny | None = None, exclude: AbstractSetIntStr | MappingIntStrAny | None = None, update: Dict[str, Any] | None = None, deep: bool = False) Self[source]
-

Returns a copy of the model.

-
-
!!! warning “Deprecated”

This method is now deprecated; use model_copy instead.

-
-
-

If you need include or exclude, use:

-

`python {test="skip" lint="skip"} -data = self.model_dump(include=include, exclude=exclude, round_trip=True) -data = {**data, **(update or {})} -copied = self.model_validate(data) -`

-
-
Args:

include: Optional set or mapping specifying which fields to include in the copied model. -exclude: Optional set or mapping specifying which fields to exclude in the copied model. -update: Optional dictionary of field-value pairs to override field values in the copied model. -deep: If True, the values of fields that are Pydantic models will be deep-copied.

-
-
Returns:

A copy of the model with included, excluded and updated fields as specified.

-
-
-
- -
-
-model_config: ClassVar[ConfigDict] = {'extra': 'forbid', 'json_schema_extra': <function pop_titles>, 'title': 'CLAMS App Runtime Parameter'}[source]
-

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

-
- -
-
-classmethod model_construct(_fields_set: set[str] | None = None, **values: Any) Self[source]
-

Creates a new instance of the Model class with validated data.

-

Creates a new model setting __dict__ and __pydantic_fields_set__ from trusted or pre-validated data. -Default values are respected, but no other validation is performed.

-
-
!!! note

model_construct() generally respects the model_config.extra setting on the provided model. -That is, if model_config.extra == ‘allow’, then all extra passed values are added to the model instance’s __dict__ -and __pydantic_extra__ fields. If model_config.extra == ‘ignore’ (the default), then all extra passed values are ignored. -Because no validation is performed with a call to model_construct(), having model_config.extra == ‘forbid’ does not result in -an error if extra values are passed, but they will be ignored.

-
-
Args:
-
_fields_set: A set of field names that were originally explicitly set during instantiation. If provided,

this is directly used for the [model_fields_set][pydantic.BaseModel.model_fields_set] attribute. -Otherwise, the field names from the values argument will be used.

-
-
-

values: Trusted or pre-validated data dictionary.

-
-
Returns:

A new instance of the Model class with validated data.

-
-
-
- -
-
-model_copy(*, update: Mapping[str, Any] | None = None, deep: bool = False) Self[source]
-
-
!!! abstract “Usage Documentation”

[model_copy](../concepts/serialization.md#model_copy)

-
-
-

Returns a copy of the model.

-
-
!!! note

The underlying instance’s [__dict__][object.__dict__] attribute is copied. This -might have unexpected side effects if you store anything in it, on top of the model -fields (e.g. the value of [cached properties][functools.cached_property]).

-
-
Args:
-
update: Values to change/add in the new model. Note: the data is not validated

before creating the new model. You should trust this data.

-
-
-

deep: Set to True to make a deep copy of the model.

-
-
Returns:

New model instance.

-
-
-
- -
-
-model_dump(*, mode: Literal['json', 'python'] | str = 'python', include: set[int] | set[str] | Mapping[int, set[int] | set[str] | Mapping[int, IncEx | bool] | Mapping[str, IncEx | bool] | bool] | Mapping[str, set[int] | set[str] | Mapping[int, IncEx | bool] | Mapping[str, IncEx | bool] | bool] | None = None, exclude: set[int] | set[str] | Mapping[int, set[int] | set[str] | Mapping[int, IncEx | bool] | Mapping[str, IncEx | bool] | bool] | Mapping[str, set[int] | set[str] | Mapping[int, IncEx | bool] | Mapping[str, IncEx | bool] | bool] | None = None, context: Any | None = None, by_alias: bool | None = None, exclude_unset: bool = False, exclude_defaults: bool = False, exclude_none: bool = False, round_trip: bool = False, warnings: bool | Literal['none', 'warn', 'error'] = True, fallback: Callable[[Any], Any] | None = None, serialize_as_any: bool = False) dict[str, Any][source]
-
-
!!! abstract “Usage Documentation”

[model_dump](../concepts/serialization.md#modelmodel_dump)

-
-
-

Generate a dictionary representation of the model, optionally specifying which fields to include or exclude.

-
-
Args:
-
mode: The mode in which to_python should run.

If mode is ‘json’, the output will only contain JSON serializable types. -If mode is ‘python’, the output may contain non-JSON-serializable Python objects.

-
-
-

include: A set of fields to include in the output. -exclude: A set of fields to exclude from the output. -context: Additional context to pass to the serializer. -by_alias: Whether to use the field’s alias in the dictionary key if defined. -exclude_unset: Whether to exclude fields that have not been explicitly set. -exclude_defaults: Whether to exclude fields that are set to their default value. -exclude_none: Whether to exclude fields that have a value of None. -round_trip: If True, dumped values should be valid as input for non-idempotent types such as Json[T]. -warnings: How to handle serialization errors. False/”none” ignores them, True/”warn” logs errors,

-
-

“error” raises a [PydanticSerializationError][pydantic_core.PydanticSerializationError].

-
-
-
fallback: A function to call when an unknown value is encountered. If not provided,

a [PydanticSerializationError][pydantic_core.PydanticSerializationError] error is raised.

-
-
-

serialize_as_any: Whether to serialize fields with duck-typing serialization behavior.

-
-
Returns:

A dictionary representation of the model.

-
-
-
- -
-
-model_dump_json(*, indent: int | None = None, include: set[int] | set[str] | Mapping[int, set[int] | set[str] | Mapping[int, IncEx | bool] | Mapping[str, IncEx | bool] | bool] | Mapping[str, set[int] | set[str] | Mapping[int, IncEx | bool] | Mapping[str, IncEx | bool] | bool] | None = None, exclude: set[int] | set[str] | Mapping[int, set[int] | set[str] | Mapping[int, IncEx | bool] | Mapping[str, IncEx | bool] | bool] | Mapping[str, set[int] | set[str] | Mapping[int, IncEx | bool] | Mapping[str, IncEx | bool] | bool] | None = None, context: Any | None = None, by_alias: bool | None = None, exclude_unset: bool = False, exclude_defaults: bool = False, exclude_none: bool = False, round_trip: bool = False, warnings: bool | Literal['none', 'warn', 'error'] = True, fallback: Callable[[Any], Any] | None = None, serialize_as_any: bool = False) str[source]
-
-
!!! abstract “Usage Documentation”

[model_dump_json](../concepts/serialization.md#modelmodel_dump_json)

-
-
-

Generates a JSON representation of the model using Pydantic’s to_json method.

-
-
Args:

indent: Indentation to use in the JSON output. If None is passed, the output will be compact. -include: Field(s) to include in the JSON output. -exclude: Field(s) to exclude from the JSON output. -context: Additional context to pass to the serializer. -by_alias: Whether to serialize using field aliases. -exclude_unset: Whether to exclude fields that have not been explicitly set. -exclude_defaults: Whether to exclude fields that are set to their default value. -exclude_none: Whether to exclude fields that have a value of None. -round_trip: If True, dumped values should be valid as input for non-idempotent types such as Json[T]. -warnings: How to handle serialization errors. False/”none” ignores them, True/”warn” logs errors,

-
-

“error” raises a [PydanticSerializationError][pydantic_core.PydanticSerializationError].

-
-
-
fallback: A function to call when an unknown value is encountered. If not provided,

a [PydanticSerializationError][pydantic_core.PydanticSerializationError] error is raised.

-
-
-

serialize_as_any: Whether to serialize fields with duck-typing serialization behavior.

-
-
Returns:

A JSON string representation of the model.

-
-
-
- -
-
-property model_extra: dict[str, Any] | None[source]
-

Get extra fields set during validation.

-
-
Returns:

A dictionary of extra fields, or None if config.extra is not set to “allow”.

-
-
-
- -
-
-property model_fields_set: set[str][source]
-

Returns the set of fields that have been explicitly set on this model instance.

-
-
Returns:
-
A set of strings representing the fields that have been set,

i.e. that were not filled from defaults.

-
-
-
-
-
- -
-
-classmethod model_json_schema(by_alias: bool = True, ref_template: str = '#/$defs/{model}', schema_generator: type[~pydantic.json_schema.GenerateJsonSchema] = <class 'pydantic.json_schema.GenerateJsonSchema'>, mode: ~typing.Literal['validation', 'serialization'] = 'validation') dict[str, Any][source]
-

Generates a JSON schema for a model class.

-
-
Args:

by_alias: Whether to use attribute aliases or not. -ref_template: The reference template. -schema_generator: To override the logic used to generate the JSON schema, as a subclass of

-
-

GenerateJsonSchema with your desired modifications

-
-

mode: The mode in which to generate the schema.

-
-
Returns:

The JSON schema for the given model class.

-
-
-
- -
-
-classmethod model_parametrized_name(params: tuple[type[Any], ...]) str[source]
-

Compute the class name for parametrizations of generic classes.

-

This method can be overridden to achieve a custom naming scheme for generic BaseModels.

-
-
Args:
-
params: Tuple of types of the class. Given a generic class

Model with 2 type variables and a concrete model Model[str, int], -the value (str, int) would be passed to params.

-
-
-
-
Returns:

String representing the new class where params are passed to cls as type variables.

-
-
Raises:

TypeError: Raised when trying to generate concrete names for non-generic models.

-
-
-
- -
-
-model_post_init(context: Any, /) None[source]
-

Override this method to perform additional initialization after __init__ and model_construct. -This is useful if you want to do some validation that requires the entire model to be initialized.

-
- -
-
-classmethod model_rebuild(*, force: bool = False, raise_errors: bool = True, _parent_namespace_depth: int = 2, _types_namespace: MappingNamespace | None = None) bool | None[source]
-

Try to rebuild the pydantic-core schema for the model.

-

This may be necessary when one of the annotations is a ForwardRef which could not be resolved during -the initial attempt to build the schema, and automatic rebuilding fails.

-
-
Args:

force: Whether to force the rebuilding of the model schema, defaults to False. -raise_errors: Whether to raise errors, defaults to True. -_parent_namespace_depth: The depth level of the parent namespace, defaults to 2. -_types_namespace: The types namespace, defaults to None.

-
-
Returns:

Returns None if the schema is already “complete” and rebuilding was not required. -If rebuilding _was_ required, returns True if rebuilding was successful, otherwise False.

-
-
-
- -
-
-classmethod model_validate(obj: Any, *, strict: bool | None = None, from_attributes: bool | None = None, context: Any | None = None, by_alias: bool | None = None, by_name: bool | None = None) Self[source]
-

Validate a pydantic model instance.

-
-
Args:

obj: The object to validate. -strict: Whether to enforce types strictly. -from_attributes: Whether to extract data from object attributes. -context: Additional context to pass to the validator. -by_alias: Whether to use the field’s alias when validating against the provided input data. -by_name: Whether to use the field’s name when validating against the provided input data.

-
-
Raises:

ValidationError: If the object could not be validated.

-
-
Returns:

The validated model instance.

-
-
-
- -
-
-classmethod model_validate_json(json_data: str | bytes | bytearray, *, strict: bool | None = None, context: Any | None = None, by_alias: bool | None = None, by_name: bool | None = None) Self[source]
-
-
!!! abstract “Usage Documentation”

[JSON Parsing](../concepts/json.md#json-parsing)

-
-
-

Validate the given JSON data against the Pydantic model.

-
-
Args:

json_data: The JSON data to validate. -strict: Whether to enforce types strictly. -context: Extra variables to pass to the validator. -by_alias: Whether to use the field’s alias when validating against the provided input data. -by_name: Whether to use the field’s name when validating against the provided input data.

-
-
Returns:

The validated Pydantic model.

-
-
Raises:

ValidationError: If json_data is not a JSON string or the object could not be validated.

-
-
-
- -
-
-classmethod model_validate_strings(obj: Any, *, strict: bool | None = None, context: Any | None = None, by_alias: bool | None = None, by_name: bool | None = None) Self[source]
-

Validate the given object with string data against the Pydantic model.

-
-
Args:

obj: The object containing string data to validate. -strict: Whether to enforce types strictly. -context: Extra variables to pass to the validator. -by_alias: Whether to use the field’s alias when validating against the provided input data. -by_name: Whether to use the field’s name when validating against the provided input data.

-
-
Returns:

The validated Pydantic model.

-
-
-
- -
- -
- - -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/autodoc/clams.mmif_utils.html b/docs/autodoc/clams.mmif_utils.html deleted file mode 100644 index 26e0495..0000000 --- a/docs/autodoc/clams.mmif_utils.html +++ /dev/null @@ -1,134 +0,0 @@ - - - - - - - - - clams.mmif_utils package — clams-python documentation - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -
-

clams.mmif_utils package

-

Package providing utility functions for working with MMIF data.

-
-

rewind module

-
-
-

source module

-
-
- - -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/autodoc/clams.restify.html b/docs/autodoc/clams.restify.html deleted file mode 100644 index c2e9fba..0000000 --- a/docs/autodoc/clams.restify.html +++ /dev/null @@ -1,270 +0,0 @@ - - - - - - - - - clams.restify package — clams-python documentation - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -
-

clams.restify package

-

Package providing wrapper class for running a CLAMS app as a HTTP app.

-
-
-class clams.restify.ClamsHTTPApi(cla_instance: ClamsApp)[source]
-

Bases: Resource

-

ClamsHTTPApi provides mapping from HTTP verbs to Python API defined in ClamsApp.

-

Constructor takes an instance of ClamsApp.

-
-
-get() Response[source]
-

Maps HTTP GET verb to appmetadata().

-
-
Returns:
-

Returns app metadata in a HTTP response.

-
-
-
- -
-
-static json_to_response(json_str: str, status=200) Response[source]
-

Helper method to convert JSON output from a ClamsApp to a HTTP response.

-
-
Parameters:
-
    -
  • json_str – a serialized JSON .

  • -
  • status – a numerical HTTP code to respond.

  • -
-
-
Returns:
-

A HTTP response ready to send.

-
-
-
- -
-
-methods: t.ClassVar[t.Collection[str] | None] = {'GET', 'POST', 'PUT'}[source]
-

The methods this view is registered for. Uses the same default -(["GET", "HEAD", "OPTIONS"]) as route and -add_url_rule by default.

-
- -
-
-post() Response[source]
-

Maps HTTP POST verb to annotate(). -Note that for now HTTP PUT verbs is also mapped to annotate().

-
-
Returns:
-

Returns MMIF output from a ClamsApp in a HTTP response.

-
-
-
- -
-
-put() Response[source]
-

Maps HTTP POST verb to annotate(). -Note that for now HTTP PUT verbs is also mapped to annotate().

-
-
Returns:
-

Returns MMIF output from a ClamsApp in a HTTP response.

-
-
-
- -
- -
-
-class clams.restify.Restifier(app_instance: ClamsApp, loopback: bool = False, port: int = 5000, debug: bool = True)[source]
-

Bases: object

-

Restifier is a simple wrapper that takes a ClamsApp object and -turn it into a flaks-based HTTP server. For mapping between Python API and -HTTP API, it relies on ClamsHTTPApi class.

-

Constructor takes a ClamsApp instance and some options for flask configuration.

-
-
Parameters:
-
    -
  • app_instance – A ClamsApp to wrap.

  • -
  • loopback – when True, the flask wrapper only listens to requests from localhost (used for debugging).

  • -
  • port – Port number for the flask app to listen (used for debugging).

  • -
  • debug – When True, the flask wrapper will run in debug mode.

  • -
-
-
-
-
-run(**options)[source]
-

Starts a development server. See serve_development().

-
-
Parameters:
-

options – any additional options to pass to the web server.

-
-
-
- -
-
-serve_development(**options)[source]
-

Runs the CLAMS app as a flask webapp, using flask built-in development server (https://werkzeug.palletsprojects.com/en/2.0.x/).

-
-
Parameters:
-

options – any additional options to pass to the web server.

-
-
-
- -
-
-serve_production(**options)[source]
-

Runs the CLAMS app as a flask webapp, using a production-ready web server (gunicorn, https://docs.gunicorn.org/en/stable/#).

-
-
Parameters:
-

options – any additional options to pass to the web server.

-
-
-
- -
-
-test_client()[source]
-

Returns flask test client.

-
- -
- -
- - -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/clamsapp.html b/docs/clamsapp.html deleted file mode 100644 index d4509f8..0000000 --- a/docs/clamsapp.html +++ /dev/null @@ -1,348 +0,0 @@ - - - - - - - - - Using CLAMS App — clams-python documentation - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - -
  • -
  • -
-
-
-
-
- -
-

Using CLAMS App

-

This document provides general instructions for installing and using CLAMS Apps. -App developers may provide additional information specific to their app, -hence it’s advised also to look up the app website (or code repository) to get the additional information.

-
-

Requirements

-

Generally, a CLAMS App requires

-
    -
  • To run the app in a container (as an HTTP server), container management software such as docker or podman. This is the recommended way to use CLAMS Apps.

    -
      -
    • (the CLAMS team is using docker for development and testing, hence the instructions are based on docker commands.)

    • -
    -
  • -
  • To run the app locally, Python3 with the clams-python module installed. Python 3.8 or higher is required.

  • -
  • To invoke and execute analysis, HTTP client utility (such as curl).

  • -
-

For Python dependencies, usually CLAMS Apps come with requirements.txt files that list up the Python library. -However, there could be other non-Python software/library that are required by the app.

-
-
-

Installation

-

CLAMS Apps available on the CLAMS App Directory. Currently, all CLAMS Apps are open-source projects and are distributed as

-
    -
  1. source code downloadable from code repository

  2. -
  3. pre-built container image

  4. -
-

Please visit the app-directory to see which apps are available and where you can download them.

-

In most cases, you can “install” a CLAMS App by either

-
    -
  1. downloading pre-built container image directly (quick-and-easy way)

  2. -
  3. downloading source code from the app code repository and manually building a container image (more flexible way if you want to modify the app, or have to build for a specific HW)

  4. -
-
-

Download prebuilt image

-

This is the quickest (and recommended) way to get started with a CLAMS App. -CLAMS apps in the App Directory come with public prebuilt container images, available in a container registry.

-
docker pull <prebulit_image_name>
-
-
-

The image name can be found on the App Directory entry of the app.

-
-
-

Build an image from source code

-

Alternatively, you can build a container image from the source code. -This is useful when you want to modify the app itself, or when you want to change the image building process to adjust to your hardware environment (e.g., specific compute engine), or additional software dependencies (e.g. MMIF plugins). -To download the source code, you can either use git clone command or download a zip file from the source code repository. -The source code repository address can be found on the App Directory entry of the app.

-

From the locally downloaded project directory, run the following in your terminal to build an image from the included container specification file.

-

(Assuming you are using docker as your container manager)

-
$ docker build . -f Containerfile -t <IMAGE_NAME_YOU_PICK>
-
-
-
-
-
-

Running CLAMS App

-

CLAMS Apps are primarily designed to run as an HTTP server, but some apps written based on clams-python SDK additionally provide CLI equivalent to the HTTP requests. -In this session, we will first cover the usage of CLAMS apps as an HTTP server, and then cover the (optional) CLI.

-
-

Starting the HTTP server as a container

-

Once the image is built (by docker build) or downloaded (by docker pull), to create and start a container, run:

-
$ docker run -v /path/to/data/directory:/data -p <PORT>:5000 <IMAGE_NAME>
-
-
-

where /path/to/data/directory is the local location of your media files or MMIF objects and PORT is the host port number you want your container to be listening to. -The HTTP inside the container will be listening to 5000 by default, so the second part of the -p argument is always 5000. -Usually any number above 1024 is fine for the host port number, and you can use the same 5000 number for the host port number.

-

The mount point for the data directory inside the container can be any path, and we used /data just as an example. -However, it is very important to understand that the file location in the input MMIF file must be a valid and available path inside the container (see below for more details).

-
-

Note -If you are using a Mac, on recent versions of macOS, port 5000 is used by Airplay Receiver by default. So you may need to use a different port number, or turn off the Airplay Receiver in the System Preferences to release 5000. -For more information on safe port numbers, see IANA Port Number Registry or Wikipedia.

-

Note -Another note for users of recent Macs with Apple Silicon (M1, M2, etc) CPU: you might see the following error message when you run the container image.

-
The requested image's platform (linux/amd64) does not match the detected host platform (linux/arm64/v8) and no specific platform was requested
-
-
-

This is because the image you are trying to run is built for Intel/AMD x64 CPUs. To force the container to run on an emulation layer, you can add --platform linux/amd64 option to the docker run command.

-
-

Additionally, you can mount a directory to /cache/ inside the container to persist the cache data between container runs. -This is particularly handy when the app you are using downloads a fairly large pretrained model file on the first run, and you want to keep it for the next run.

-

Unlike the data directory, the cache directory is not required to be mounted, but if you want to persist the cache data, you can mount a local directory to /cache/ inside the container (fixed path).

-
docker run -v /path/to/data/directory:/data -v /path/to/cache/directory:/cache -p <port>:5000 <image_name>
-
-
-
-

Note -One might be tempted bind-mount their entire local cache directory (usually ~/.cache in Linux systems) to re-use locally downloaded model files, across different apps. -However, doing so will expose all the cached data, not just model files, to the container. -This can include sensitive information such as browser cache, authentication tokens, etc, hence will pose a great security risk. -It is recommended to create a separate directory to use as a cache directory for CLAMS containers.

-
-
-
-
-

Invoking the app server

-
-

To get app metadata

-

Once the app is running as an HTTP server, visit the server address (localhost:5000, or the remote host name if running on a remote computer) to get the app metadata. -App metadata is also available at the App Directory entry of the app if the app is published on the App Directory. -App metadata contains important information about the app that we will use in the following sections.

-
-
-

To process input media

-

To actually run the app and process input media through computational analysis, simply send a POST request to the app with a MMIF input as the request body.

-

MMIF input files can be obtained from outputs of other CLAMS apps, or you can create an empty MMIF only with source media locations using clams source command. See the help message for a more detailed instructions. -(Make sure you have installed ``clams-python` package <https://pypi.org/project/clams-python/>`_ version from PyPI.)

-
$ pip install clams-python
-$ clams source --help
-
-
-

For example; by running

-
$ clams source audio:/data/audio/some-audio-file.mp3
-
-
-

You will get

-
{
-  "metadata": {
-    "mmif": "http://mmif.clams.ai/X.Y.Z"
-  },
-  "documents": [
-    {
-      "@type": "http://mmif.clams.ai/vocabulary/AudioDocument/v1",
-      "properties": {
-        "mime": "audio",
-        "id": "d1",
-        "location": "file:///data/audio/some-audio-file.mp3"
-      }
-    }
-  ],
-  "views": []
-}
-
-
-

If an app requires just Document inputs (see input section of the app metadata), an empty MMIF with required media file locations will suffice. -The location has to be a URL or an absolute path, and it is important to ensure that it exists. -Especially when running the app in a container, and the document location is specified as a file system path, the file must be available inside the container. -In the above, we bind-mounted /path/to/data/directory (host) to /data (container). -That is why we used /data/audio/some-audio-file.mp3 as the location when generating this MMIF input. -So in this example, the file /path/to/data/directory/audio/some-audio-file.mp3 must exist on the host side, so that inside the container, it can be accessed as /data/audio/some-audio-file.mp3.

-

Some apps only works with input MMIF that already contains some annotations of specific types. To run such apps, you need to run different apps in a sequence.

-

(TODO: added CLAMS workflow documentation link here.)

-

When an input MMIF is ready, you can send it to the app server. -Here’s an example of how to use the curl command, and store the response in a file output.mmif.

-
$ clams source audio:/data/audio/some-audio-file.mp3 > input.mmif
-$ curl -H "Accept: application/json" -X POST -d@input.mmif -s http://localhost:5000 > output.mmif
-
-# or using a bash pipeline
-$ clams source audio:/data/audio/some-audio-file.mp3 | curl -X POST -d@- -s http://localhost:5000 > output.mmif
-
-
-

Windows PowerShell users may encounter an Invoke-WebRequest exception when attempting to send an input file with curl. -This can be resolved for the duration of the current session by using the command remove-item alias:curl before proceeding to use curl.

-
-
-

Configuring the app

-

Running as an HTTP server, CLAMS Apps are stateless, but can be configured for each HTTP request by providing configuration parameters as query string.

-

For example, appending ?pretty=True to the URL will result in a JSON output with indentation for better readability.

-
-

Note -When you’re using curl from a shell session, you need to escape the ? or & characters with \ to prevent the shell from interpreting it as a special character.

-
-

Different apps have different configurability. For configuration parameters of an app, please refer to parameter section of the app metadata.

-
-
-
-

Using CLAMS App as a CLI program

-

First and foremost, not all CLAMS Apps support command line interface (CLI). -At the minimum, a CLAMS app is required to support HTTP interfaces described in the previous section. -If any of the following instructions do not work for an app, it is likely that the app does not support CLI.

-
-

Python entry points

-

Apps written on clams-python SDK have three python entry points by default: app.py, metadata.py, and cli.py.

-
-
-

app.py: Running app as a local HTTP server

-

app.py is the main entry point for running the app as an HTTP server. -To run the app as a local HTTP server without containerization, you can run the following command from the source code directory.

-
$ python app.py
-
-
-
    -
  • By default, the app will be listening to port 5000, but you can change the port number by passing --port <NUMBER> option.

  • -
  • Be default, the app will be running in debugging mode, but you can change it to production mode by passing --production option to support larger traffic volume.

  • -
  • As you might have noticed, the default CMD in the prebuilt containers is python app.py --production --port 5000.

  • -
-
-
-

metadata.py: Getting app metadata

-

Running metadata.py will print out the app metadata in JSON format.

-
-
-

cli.py: Running as a CLI program

-

cli.py is completely optional for app developers, and unlike the other two above that are guaranteed to be available, cli.py may not be available for some apps. -When running an app as a HTTP app, the input MMIF must be passed as POST request’s body, and the output MMIF will be returned as the response body. -To mimic this behavior in a CLI, cli.py has two positional arguments;

-
$ python cli.py <INPUT_MMIF> <OUTPUT_MMIF>  # will read INPUT_MMIF file, process it, and write the result to OUTPUT_MMIF file
-
-
-

<INPUT_MMIF> and <OUTPUT_MMIF> are file paths to the input and output MMIF files, respectively. -Following the common unix CLI practice, you can use - to represent STDIN and/or STDOUT

-
# will read from STDIN, process it, and write the result to STDOUT
-$ python cli.py - -
-
-# or equivalently
-$ python cli.py
-
-# read from a file, write to STDOUT
-$ python cli.py input.mmif -
-
-# or equivalently
-$ python cli.py input.mmif
-
-# read from STDIN, write to a file
-$ cat input.mmif | python cli.py - output.mmif
-
-
-

As with the HTTP server, you can pass configuration parameters to the CLI program. -All parameter names are the same as the HTTP query parameters, but you need to use -- prefix to indicate that it is a parameter.

-
$ python cli.py --pretty True input.mmif output.mmif
-
-
-

Finally, when running the app as a container, you can override the default CMD (app.py) by passing a cli.py command to the docker run command.

-
$ cat input.mmif | docker run -i -v /path/to/data/directory:/data <IMAGE_NAME> python cli.py
-
-
-

Note that input.mmif is in the host machine, and the container is reading it from the STDIN. -You can also pass the input MMIF file as a volume to the container. -However, when you do so, you need to make sure that the file path in the MMIF is correctly set to the file path in the container.

-
-

Note -Here, make sure to pass `-i`` option to the docker run <https://docs.docker.com/reference/cli/docker/container/run/#interactive>`_ command to make host’s STDIN work properly with the container.

-
-
-
-
- - -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/cli.html b/docs/cli.html deleted file mode 100644 index 60bd2fc..0000000 --- a/docs/cli.html +++ /dev/null @@ -1,127 +0,0 @@ - - - - - - - - - clams shell command — clams-python documentation - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - -
  • -
  • -
-
-
-
-
- -
-

clams shell command

-

clams-python comes with a command line interface (CLI) that allows you to

-
    -
  1. create a new CLAMS app from a template

  2. -
  3. create a new MMIF file with selected source documents and an empty view

  4. -
-

The CLI is installed as clams shell command. To see the available commands, run

-
clams --help
-
-
-
- - -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/genindex.html b/docs/genindex.html deleted file mode 100644 index d537fb0..0000000 --- a/docs/genindex.html +++ /dev/null @@ -1,454 +0,0 @@ - - - - - - - - Index — clams-python documentation - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - -
  • -
  • -
-
-
-
-
- - -

Index

- -
- _ - | A - | C - | G - | I - | J - | M - | O - | P - | R - | S - | T - | U - | V - -
-

_

- - - -
- -

A

- - - -
- -

C

- - - -
    -
  • - clams.app - -
  • -
  • - clams.restify - -
  • -
- -

G

- - - -
- -

I

- - -
- -

J

- - -
- -

M

- - - -
- -

O

- - - -
- -

P

- - - -
- -

R

- - - -
- -

S

- - - -
- -

T

- - -
- -

U

- - -
- -

V

- - -
- - - -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/index.html b/docs/index.html deleted file mode 100644 index 52fc962..0000000 --- a/docs/index.html +++ /dev/null @@ -1,196 +0,0 @@ - - - - - - - - - Welcome to CLAMS Python SDK documentation! — clams-python documentation - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - -
  • -
  • -
-
-
-
-
- -
-

Welcome to CLAMS Python SDK documentation!

-
-

CLAMS project

-

CLAMS project aims at free and open-source software platform for computational analysis and metadata generation applications for multimedia material. We believe free and open AI-based multimedia processing platform can bring many benefits of technical advancement to libraries, archives, and museums. To achieve interoperability between various computational applications developed be different vendors, which is absolutely necessary for using applications together supported by user-friendly workflow engines, we are also developing JSON-based MultiMedia Interchange Format (MMIF)

-
-
-

clams-python

-

clams-python is a Python implementation of the CLAMS SDK. clams-python provides CLAMS app developers with the following assistance ;

-
    -
  1. handling MMIF files for input and output specification for CLAMS apps

  2. -
  3. a base Python class to start developing a CLAMS app

  4. -
  5. a flask-based wrapper to run a Python CLAMS app as an HTTP web app

  6. -
  7. cookie-cutter shell command that sets up everything and starts a new CLAMS app project

  8. -
-
-

Start now from Getting Started!

-
-
-
-

For more …

- - - -
-
-
-

Indices and tables

- -
- - -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/input-output.html b/docs/input-output.html deleted file mode 100644 index 30a0916..0000000 --- a/docs/input-output.html +++ /dev/null @@ -1,375 +0,0 @@ - - - - - - - - - I/O Specification — clams-python documentation - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - -
  • -
  • -
-
-
-
-
- -
-

I/O Specification

-

A CLAMS app must be able to take a MMIF JSON as input as well as to return a MMIF JSON as output. MMIF is a JSON-based open source data format (with some JSON-LD components). For more details and discussions, please visit the MMIF website and the issue tracker.

-

To learn more about MMIF, please visit MMIF specification documentation.

-
-

mmif package

-

mmif-python PyPI package comes together with the installation of clams-python, and with it, you can use mmif python package.

-
import mmif
-from mmif.serialize import Mmif
-new_mmif = Mmif()
-# this will fail because an empty MMIF will fail to validate under the MMIF JSON schema
-
-
-

Because API’s of the mmif package is well documented in the mmif-python website, we won’t go into more details here. Please refer to the website.

-
-
-

MMIF version and CLAMS apps

-

As some parts of clams-python implementation is relying on structure of MMIF, it is possible that a MMIF file of a specific version does not work with a CLAMS app that is based on a incompatible version of mmif-python.

-

As we saw in the above, when using Mmif class, the MMIF JSON string is automatically validated under the MMIF JSON schema (shipped with the mmif-python). -So in most cases you don’t have to worry about the compatibility issue, but it is still important to understand the versioning scheme of MMIF + mmif-python and clams-python, because all clams-python distributions are depending on specific versions of mmif-python as a Python library.

-
import mmif
-mmif.__specver__
-
-
-

And when an app targets a specific version, it means:

-
    -
  1. The app can only process input MMIF files that are valid under the jsonschema of mmif-python version.

  2. -
  3. The app will output MMIF files exactly versioned as the target version.

  4. -
-

However, also take these into consideration

-
    -
  1. Not all MMIF updates are about the jsonschema. That means, some MMIF versions share the same schema, hence the syntactic validation process can pass for differently versioned MMIF files.

  2. -
  3. Changing jsonschema is actually a big change in MMIF, and we will try to avoid it as much as possible. When it’s unavoidable, we will release the new MMIF and mmif-python with the major version number incremented.

  4. -
-

For more information on the relation between mmif-python versions and MMIF specification versions, or MMIF version compatibility, please take time to read our decision on the subject here. You can also find a table with all public clams-python packages and their target MMIF versions in the below. As we mentioned, developers don’t usually need to worry about MMIF versions.

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Target Specification Versions

clams-python version

mmif-python version

Target MMIF Specification

1.3.3

1.1.2

1.1.0

1.3.2

1.1.1

1.1.0

1.3.1

1.0.19

1.0.5

1.3.0

1.0.18

1.0.5

1.2.6

1.0.18

1.0.5

1.2.5

1.0.17

1.0.5

1.2.4

1.0.16

1.0.4

1.2.3

1.0.15

1.0.4

1.2.2

1.0.14

1.0.4

1.2.1

1.0.13

1.0.4

1.2.0

1.0.13

1.0.4

1.1.6

1.0.13

1.0.4

1.1.5

1.0.11

1.0.4

1.1.4

1.0.11

1.0.4

1.1.3

1.0.10

1.0.2

1.1.2

1.0.9

1.1.0

1.1.1

1.0.8

1.0.0

1.1.0

1.0.8

1.0.0

1.0.9

1.0.8

1.0.0

1.0.8

1.0.8

1.0.0

1.0.7

1.0.6

1.0.0

1.0.6

1.0.6

1.0.0

1.0.5

1.0.5

1.0.0

1.0.4

1.0.4

1.0.0

1.0.3

1.0.1

1.0.0

1.0.2

1.0.1

1.0.0

1.0.1

1.0.1

1.0.0

1.0.0

1.0.0

0.5.0

0.6.3

0.5.2

0.5.0

0.6.2

0.5.2

0.5.0

0.6.1

0.5.2

0.5.0

0.6.0

0.5.1

0.5.0

0.5.3

0.4.8

0.4.2

0.5.2

0.4.6

0.4.0

0.5.1

0.4.6

0.4.0

0.5.0

0.4.5

0.4.0

0.4.4

0.4.5

0.4.0

0.4.3

0.4.4

0.4.0

0.4.2

0.4.2

0.4.0

0.4.1

0.4.1

0.4.0

0.4.0

0.4.1

0.4.0

0.3.0

0.3.5

0.3.1

0.2.4

0.3.3

0.3.1

0.2.3

0.3.1

0.3.0

0.2.2

0.3.1

0.3.0

0.2.1

0.3.0

0.3.0

0.2.0

0.2.2

0.2.1

0.1.8

0.2.2

0.2.1

0.1.7

0.2.2

0.2.1

0.1.6

0.2.1

0.2.1

0.1.5

0.2.1

0.2.1

0.1.4

0.2.1

0.2.1

-
-
- - -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/introduction.html b/docs/introduction.html deleted file mode 100644 index d91d723..0000000 --- a/docs/introduction.html +++ /dev/null @@ -1,267 +0,0 @@ - - - - - - - - - Getting started — clams-python documentation - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - -
  • -
  • -
-
-
-
-
- -
-

Getting started

-
-

Overview

-

The CLAMS project has many moving parts to make various computational analysis tools talk to each other to create customized workflows. However the most important part of the project must be the apps published for the CLAMS platform. The CLAMS Python SDK will help app developers handling MMIF data format with high-level classes and methods in Python, and publishing their code as a CLAMS app that can be easily deployed to the site via CLAMS workflow engines, such as the CLAMS appliance.

-

A CLAMS app can be any software that performs automated contents analysis on text, audio, and/or image/video data stream, while using MMIF as I/O format. When deployed into a CLAMS workflow engine, an app needs be running as a webapp wrapped in a container. In this documentation, we will explain what Python API’s and HTTP API’s an app must implement.

-
-
-

Prerequisites

-
    -
  • Python: the latest clams-python requires Python 3.8 or newer. We have no plan to support Python 2.7.

  • -
  • Containerization software: when deployed to a CLAMS workflow engine, an app must be containerized. Developers can choose any containerization software for doing so, but clams-python SDK is developed with Docker in mind.

  • -
-
-
-

Installation

-

clams-python distribution package is available at PyPI. You can use pip to install the latest version.

-
pip install clams-python
-
-
-

Note that installing clams-python will also install mmif-python PyPI package, which is a companion python library related to the MMIF data format. More information regarding MMIF specifications can be found here. Documentation on mmif-python is available at the Python API documentation website.

-
-
-

Quick Start with the App Template

-

clams-python comes with a cookiecutter template for creating a CLAMS app. You can use it to create a new app project.

-
clams develop --help
-
-
-

The newly created project will have a README.md file that explains how to develop and deploy the app. Here we will explain the basic structure of the project. Developing a CLAMS app has three (or four depending on the underlying analyzer) major components.

-
    -
  1. (Writing computational analysis code, or use existing code in Python)

  2. -
  3. Make the analyzer to speak MMIF by wrapping with clams.app.ClamsApp.

  4. -
  5. Make the app into a web app by wrapping with clams.restify.Restifier.

  6. -
  7. Containerize the app by writing a Containerfile or Dockerfile.

  8. -
-
-

CLAMS App API

-

A CLAMS Python app is a python class that implements and exposes two core methods: annotate(), appmetadata(). In essence, these methods (discussed further below) are wrappers around _annotate() and _appmetadata(), and they provide some common functionality such as making sure the output is serialized into a string.

-
    -
  • annotate(): Takes a MMIF as input and processes the MMIF input, then returns serialized MMIF str.

  • -
  • appmetadata(): Returns JSON-formatted str that contains metadata about the app.

  • -
-

A good place to start writing a CLAMS app is to start with inheriting clams.app.ClamsApp. Let’s talk about the two methods in detail when inheriting the class.

-
-

annotate()

-

The annotate() method is the core method of a CLAMS app. It takes a MMIF JSON string as the main input, along with other kwargs for runtime configurations, and analyzes the MMIF input, then returns analysis results in a serialized MMIF str. -When you inherit ClamsApp, you need to implement

-
    -
  • _annotate() instead of annotate() (read the docstrings as they contain important information about the app implementation).

  • -
  • at a high level, _annotate() is mostly concerned with

    -
    -
      -
    • finding processable documents and relevant annotations from previous views,

    • -
    • creating new views,

    • -
    • and calling the code that runs over the documents and inserts the results to the new views.

    • -
    -
    -
  • -
-

As a developer you can expose different behaviors of the annotate() method by providing configurable parameters as keyword arguments of the method. For example, you can have the user specify a re-sample rate of an audio file to be analyzed by providing a resample_rate parameter.

-
-

Note

-

These runtime configurations are not part of the MMIF input, but for reproducible analysis, you should record these configurations in the output MMIF.

-
-
-

Note

-

There are universal parameters defined at the SDK-level that all CLAMS apps commonly use. See clams.app.ClamsApp.universal_parameters.

-
-
-

Warning

-

All the runtime configurations should be pre-announced in the app metadata.

-
-

Also see <Tutorial: Wrapping an NLP Application> for a step-by-step tutorial on how to write the _annotate() method with a simple example NLP tool.

-
-
-

appmetadata()

-

App metadata is a map where important information about the app itself is stored as key-value pairs. That said, the appmetadata() method should not perform any analysis on the input MMIF. In fact, it shouldn’t take any input at all.

-

When using clams.app.ClamsApp, you have different options to implement information source for the metadata. See _load_appmetadata() for the options, and <CLAMS App Metadata> for the metadata specification.

-
-

Note

-

In the future, the app metadata will be used for automatic generation of CLAMS App Directory.

-
-
-
-
-

HTTP webapp

-

To be integrated into CLAMS workflow engines, a CLAMS app needs to serve as a webapp. Once your application class is ready, you can use clams.restify.Restifier to wrap your app as a Flask-based web application.

-
from clams.app import ClamsApp
-from clams.restify import Restifier
-
-class AnApp(ClamsApp):
-    # Implements an app that does this and that.
-
-if __name__ == "__main__":
-    app = AnApp()
-    webapp = Restifier(app)
-    webapp.run()
-
-
-

When running the above code, Python will start a web server and host your CLAMS app. By default the serve will listen to 0.0.0.0:5000, but you can adjust hostname and port number. In this webapp, appmetadata and annotate will be respectively mapped to GET, and POST to the root route. Hence, for example, you can POST a MMIF file to the web app and get a response with the annotated MMIF string in the body.

-
-

Note

-

Now with HTTP interface, users can pass runtime configuration as URL query strings. As the values of query string parameters are always strings, Restifier will try to convert the values to the types specified in the app metadata, using clams.restify.ParameterCaster.

-
-

In the above example, clams.restify.Restifier.run() will start the webapp in debug mode on a Werkzeug server, which is not always suitable for a production server. For a more robust server that can handle multiple requests asynchronously, you might want to use a production-ready HTTP server. In such a case you can use serve_production(), which will spin up a multi-worker Gunicorn server. If you don’t like it (because, for example, gunicorn does not support Windows OS), you can write your own HTTP wrapper. At the end of the day, all you need is a webapp that maps appmetadata and annotate on GET and POST requests.

-

To test the behavior of the application in a Flask server, you should run the app as a webapp in a terminal (shell) session:

-
$ python app.py --develop --port 5000
-# default port number is 5000
-
-
-

And poke at it from a new shell session:

-
# in a new terminal session
-$ curl http://localhost:5000/
-$ curl -H "Accept: application/json" -X POST -d@input/example-1.mmif "http://0.0.0.0:5000?pretty=True"
-
-
-

The first command prints the metadata, and the second prints the output MMIF file. Appending ?pretty=True to the URL will result in a pretty printed output. Note that with the --develop option we started a Flask development server. Without this option, a production server will be started. To get more information about the input file format (the contents of input/example-1.mmif), please refer to the user manual.

-
-
-

Containerization

-

In addition to the HTTP service, a CLAMS app is expected to be containerized for seamless deployment to CLAMS workflow engines. Also, independently from being compatible with the CLAMS platform, containerization of your app is recommended especially when your app processes video streams and is dependent on complicated system-level video processing libraries (e.g. OpenCV, FFmpeg).

-

When you start developing an app with clams develop command, the command will create a Containerfile with some instructions as inline comments for you (you can always start from scratch with any containerization tool you like).

-
-

Note

-

If you are part of CLAMS team and you want to publish your app to the https://github.com/clamsproject organization, clams develop command will also create a GitHub Actions files to automatically build and push an app image to the organization’s container registry. For the actions to work, you must use the name Containerfile instead of Dockerfile.

-
-

If you are not familiar with Containerfile or Dockerfile, refer to the official documentation to learn how to write one. To integrate to the CLAMS workflow engines, a containerized CLAMS app must automatically start itself as a webapp when instantiated as a container, and listen to 5000 port.

-

We have a public GitHub Container Repository, and publishing Debian-based base images to help developers write Containerfile and save build time to install common libraries. At the moment we have various basic images with Python 3.8, clams-python, and commonly used video and audio processing libraries installed.

-

Once you finished writing your Containerfile, you can build and test the containerized app locally. If you are not familiar with building and running container images To build a Docker image, these documentation will be helpful.

- -
-
-
- - -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/modules.html b/docs/modules.html deleted file mode 100644 index fac7cf1..0000000 --- a/docs/modules.html +++ /dev/null @@ -1,241 +0,0 @@ - - - - - - - - - clams package — clams-python documentation - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - -
  • -
  • -
-
-
-
-
- -
-

clams package

-

API documentation

-
- -
-
- - -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/objects.inv b/docs/objects.inv deleted file mode 100644 index 76779ff..0000000 Binary files a/docs/objects.inv and /dev/null differ diff --git a/docs/py-modindex.html b/docs/py-modindex.html deleted file mode 100644 index 5f8a1f2..0000000 --- a/docs/py-modindex.html +++ /dev/null @@ -1,140 +0,0 @@ - - - - - - - - Python Module Index — clams-python documentation - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - -
  • -
  • -
-
-
-
-
- - -

Python Module Index

- -
- c -
- - - - - - - - - - - - - -
 
- c
- clams -
    - clams.app -
    - clams.restify -
- - -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/runtime-params.html b/docs/runtime-params.html deleted file mode 100644 index 71b8fa2..0000000 --- a/docs/runtime-params.html +++ /dev/null @@ -1,168 +0,0 @@ - - - - - - - - - Runtime Configuration — clams-python documentation - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - -
  • -
  • -
-
-
-
-
- -
-

Runtime Configuration

-
-

As Python API

-

Using keyword arguments in the annotate() method, you -can make your app configurable at the runtime.

-

For example, an app can be configured to use a different combination of optional -input annotation types, or to use a different unit for the output time-based -annotations. See an example below.

-
class NamedEntityRecognizerApp(ClamsApp):
-    def __init__(self)
-        super().__init__()
-        self.ner_model = self._load_model()
-
-    def annotate(self, input_mmif, **parameters)
-        ...  # preamble to "sign" the view and prepare a new view to use
-        labels_to_use = parameters.get('labels', ['PERSON', 'ORG', 'GPE'])
-        text = self.get_text(input_mmif)
-        ne = self.ner_model(text)
-        for ent in ne:
-            if ent.label_ in labels_to_use:
-                self.add_annotation(input_mmif, ent.start_char, ent.end_char, ent.label_)
-        return input_mmif
-
-    ...
-
-
-

When you use a configuration parameter in your app, you should also expose it -to the user via the app metadata. See CLAMS App Metadata section for more details.

-
-
-

As HTTP Server

-

When running as a HTTP server, a CLAMS app should be stateless (or always set to -default states), and all the state should be “configured” by the client for each -request, via the runtime configuration parameters we described above if necessary. -For HTTP interface, users can enter configuration values via -query strings as part of the -request URL. For example, if the user wants to use the above app as a server -with the labels parameter only set to PERSON and ORG, then the user -can send a POST request to the server with the following URL:

-
http://app-server:5000?labels=PERSON&labels=ORG
-
-
-

Note that for this example to work, the parameter must be specified as -multivalued=True in the app metadata, so that the SDK can collect multiple -values for the same parameter name in a single python list and pass to the -annotate() method. Otherwise, only the first value will be passed.

-
-
- - -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/search.html b/docs/search.html deleted file mode 100644 index edc484a..0000000 --- a/docs/search.html +++ /dev/null @@ -1,130 +0,0 @@ - - - - - - - - Search — clams-python documentation - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - -
  • -
  • -
-
-
-
-
- - - - -
- -
- -
-
- -
-
-
-
- - - - - - - - - \ No newline at end of file diff --git a/docs/searchindex.js b/docs/searchindex.js deleted file mode 100644 index 92839e3..0000000 --- a/docs/searchindex.js +++ /dev/null @@ -1 +0,0 @@ -Search.setIndex({"alltitles": {"API documentation:": [[8, null]], "Adding annotations:": [[13, "adding-annotations"]], "Annotation types in MMIF": [[1, "annotation-types-in-mmif"]], "App Metadata": [[13, "app-metadata"]], "As HTTP Server": [[12, "as-http-server"]], "As Python API": [[12, "as-python-api"]], "Build an image from source code": [[6, "build-an-image-from-source-code"]], "CLAMS App API": [[10, "clams-app-api"]], "CLAMS App Directory": [[0, null]], "CLAMS App Metadata": [[1, null]], "CLAMS App Runtime Parameter": [[1, "clams-app-runtime-parameter"]], "CLAMS AppMetadata": [[1, "clams-appmetadata"]], "CLAMS Input Specification": [[1, "clams-input-specification"]], "CLAMS Output Specification": [[1, "clams-output-specification"]], "CLAMS project": [[8, "clams-project"]], "Configuring the app": [[6, "configuring-the-app"]], "Containerization": [[10, "containerization"]], "Containerization with Docker": [[13, "containerization-with-docker"]], "Contents": [[8, null]], "Creating a new view:": [[13, "creating-a-new-view"]], "Download prebuilt image": [[6, "download-prebuilt-image"]], "Extended case - adding custom properties to the type definition": [[1, "extended-case-adding-custom-properties-to-the-type-definition"]], "For more \u2026": [[8, "for-more"]], "Format": [[1, "format"]], "Getting started": [[10, null]], "HTTP webapp": [[10, "http-webapp"]], "I/O Specification": [[9, null]], "Imports": [[13, "imports"]], "Indices and tables": [[8, "indices-and-tables"]], "Input/Output type specification": [[1, "input-output-type-specification"]], "Installation": [[6, "installation"], [10, "installation"]], "Invoking the app server": [[6, "invoking-the-app-server"]], "MMIF version and CLAMS apps": [[9, "mmif-version-and-clams-apps"]], "Metadata Schema": [[1, "metadata-schema"]], "Overview": [[1, "overview"], [10, "overview"]], "Prerequisites": [[10, "prerequisites"]], "Python entry points": [[6, "python-entry-points"]], "Quick Start with the App Template": [[10, "quick-start-with-the-app-template"]], "Requirements": [[6, "requirements"]], "Running CLAMS App": [[6, "running-clams-app"]], "Runtime Configuration": [[12, null]], "Runtime parameter specification": [[1, "runtime-parameter-specification"]], "Simple case - using types as defined in the vocabularies": [[1, "simple-case-using-types-as-defined-in-the-vocabularies"]], "Start now from Getting Started!": [[8, "start-now-from-getting-started"]], "Starting the HTTP server as a container": [[6, "starting-the-http-server-as-a-container"]], "Syntax for I/O specification in App Metadata": [[1, "syntax-for-i-o-specification-in-app-metadata"]], "Syntax for parameter specification in App Metadata": [[1, "syntax-for-parameter-specification-in-app-metadata"]], "Target Specification Versions": [[9, "id1"]], "The NLP tool": [[13, "the-nlp-tool"]], "The application class": [[13, "the-application-class"]], "To get app metadata": [[6, "to-get-app-metadata"]], "To process input media": [[6, "to-process-input-media"]], "Tutorial: Wrapping an NLP Application": [[13, null]], "Using CLAMS App": [[6, null]], "Using CLAMS App as a CLI program": [[6, "using-clams-app-as-a-cli-program"]], "Using the document.location property": [[13, "using-the-document-location-property"]], "Welcome to CLAMS Python SDK documentation!": [[8, null]], "Wrapping the tokenizer": [[13, "wrapping-the-tokenizer"]], "_annotate()": [[13, "annotate"]], "annotate()": [[10, "annotate"]], "app.py: Running app as a local HTTP server": [[6, "app-py-running-app-as-a-local-http-server"]], "appmetadata()": [[10, "appmetadata"]], "clams package": [[11, null]], "clams shell command": [[7, null]], "clams-python": [[8, "clams-python"]], "clams.app package": [[2, null]], "clams.appmetadata package": [[3, null]], "clams.mmif_utils package": [[4, null]], "clams.restify package": [[5, null]], "cli.py: Running as a CLI program": [[6, "cli-py-running-as-a-cli-program"]], "metadata.py: Getting app metadata": [[6, "metadata-py-getting-app-metadata"]], "mmif package": [[9, "mmif-package"]], "rewind module": [[4, "rewind-module"]], "source module": [[4, "source-module"]]}, "docnames": ["appdirectory", "appmetadata", "autodoc/clams.app", "autodoc/clams.appmetadata", "autodoc/clams.mmif_utils", "autodoc/clams.restify", "clamsapp", "cli", "index", "input-output", "introduction", "modules", "runtime-params", "tutorial"], "envversion": {"sphinx": 64, "sphinx.domains.c": 3, "sphinx.domains.changeset": 1, "sphinx.domains.citation": 1, "sphinx.domains.cpp": 9, "sphinx.domains.index": 1, "sphinx.domains.javascript": 3, "sphinx.domains.math": 2, "sphinx.domains.python": 4, "sphinx.domains.rst": 2, "sphinx.domains.std": 2, "sphinx.ext.intersphinx": 1}, "filenames": ["appdirectory.rst", "appmetadata.rst", "autodoc/clams.app.rst", "autodoc/clams.appmetadata.rst", "autodoc/clams.mmif_utils.rst", "autodoc/clams.restify.rst", "clamsapp.md", "cli.rst", "index.rst", "input-output.rst", "introduction.rst", "modules.rst", "runtime-params.rst", "tutorial.md"], "indexentries": {"_abc_impl (clams.app.clamsapp attribute)": [[2, "clams.app.ClamsApp._abc_impl", false]], "_annotate() (clams.app.clamsapp method)": [[2, "clams.app.ClamsApp._annotate", false]], "_appmetadata() (clams.app.clamsapp method)": [[2, "clams.app.ClamsApp._appmetadata", false]], "_check_mmif_compatibility() (clams.app.clamsapp static method)": [[2, "clams.app.ClamsApp._check_mmif_compatibility", false]], "_load_appmetadata() (clams.app.clamsapp method)": [[2, "clams.app.ClamsApp._load_appmetadata", false]], "_raw_params_key (clams.app.clamsapp attribute)": [[2, "clams.app.ClamsApp._RAW_PARAMS_KEY", false]], "_refine_params() (clams.app.clamsapp method)": [[2, "clams.app.ClamsApp._refine_params", false]], "add_description() (clams.appmetadata.input method)": [[3, "clams.appmetadata.Input.add_description", false]], "add_description() (clams.appmetadata.output method)": [[3, "clams.appmetadata.Output.add_description", false]], "add_input() (clams.appmetadata.appmetadata method)": [[3, "clams.appmetadata.AppMetadata.add_input", false]], "add_more() (clams.appmetadata.appmetadata method)": [[3, "clams.appmetadata.AppMetadata.add_more", false]], "add_output() (clams.appmetadata.appmetadata method)": [[3, "clams.appmetadata.AppMetadata.add_output", false]], "add_parameter() (clams.appmetadata.appmetadata method)": [[3, "clams.appmetadata.AppMetadata.add_parameter", false]], "annotate() (clams.app.clamsapp method)": [[2, "clams.app.ClamsApp.annotate", false]], "appmetadata (class in clams.appmetadata)": [[3, "clams.appmetadata.AppMetadata", false]], "appmetadata() (clams.app.clamsapp method)": [[2, "clams.app.ClamsApp.appmetadata", false]], "clams.app": [[2, "module-clams.app", false]], "clams.restify": [[5, "module-clams.restify", false]], "clamsapp (class in clams.app)": [[2, "clams.app.ClamsApp", false]], "clamshttpapi (class in clams.restify)": [[5, "clams.restify.ClamsHTTPApi", false]], "copy() (clams.appmetadata.input method)": [[3, "clams.appmetadata.Input.copy", false]], "copy() (clams.appmetadata.output method)": [[3, "clams.appmetadata.Output.copy", false]], "copy() (clams.appmetadata.runtimeparameter method)": [[3, "clams.appmetadata.RuntimeParameter.copy", false]], "get() (clams.restify.clamshttpapi method)": [[5, "clams.restify.ClamsHTTPApi.get", false]], "get_configuration() (clams.app.clamsapp method)": [[2, "clams.app.ClamsApp.get_configuration", false]], "input (class in clams.appmetadata)": [[3, "clams.appmetadata.Input", false]], "json_to_response() (clams.restify.clamshttpapi static method)": [[5, "clams.restify.ClamsHTTPApi.json_to_response", false]], "methods (clams.restify.clamshttpapi attribute)": [[5, "clams.restify.ClamsHTTPApi.methods", false]], "model_config (clams.appmetadata.appmetadata attribute)": [[3, "clams.appmetadata.AppMetadata.model_config", false]], "model_config (clams.appmetadata.input attribute)": [[3, "clams.appmetadata.Input.model_config", false]], "model_config (clams.appmetadata.output attribute)": [[3, "clams.appmetadata.Output.model_config", false]], "model_config (clams.appmetadata.runtimeparameter attribute)": [[3, "clams.appmetadata.RuntimeParameter.model_config", false]], "model_construct() (clams.appmetadata.input class method)": [[3, "clams.appmetadata.Input.model_construct", false]], "model_construct() (clams.appmetadata.output class method)": [[3, "clams.appmetadata.Output.model_construct", false]], "model_construct() (clams.appmetadata.runtimeparameter class method)": [[3, "clams.appmetadata.RuntimeParameter.model_construct", false]], "model_copy() (clams.appmetadata.input method)": [[3, "clams.appmetadata.Input.model_copy", false]], "model_copy() (clams.appmetadata.output method)": [[3, "clams.appmetadata.Output.model_copy", false]], "model_copy() (clams.appmetadata.runtimeparameter method)": [[3, "clams.appmetadata.RuntimeParameter.model_copy", false]], "model_dump() (clams.appmetadata.input method)": [[3, "clams.appmetadata.Input.model_dump", false]], "model_dump() (clams.appmetadata.output method)": [[3, "clams.appmetadata.Output.model_dump", false]], "model_dump() (clams.appmetadata.runtimeparameter method)": [[3, "clams.appmetadata.RuntimeParameter.model_dump", false]], "model_dump_json() (clams.appmetadata.input method)": [[3, "clams.appmetadata.Input.model_dump_json", false]], "model_dump_json() (clams.appmetadata.output method)": [[3, "clams.appmetadata.Output.model_dump_json", false]], "model_dump_json() (clams.appmetadata.runtimeparameter method)": [[3, "clams.appmetadata.RuntimeParameter.model_dump_json", false]], "model_extra (clams.appmetadata.input property)": [[3, "clams.appmetadata.Input.model_extra", false]], "model_extra (clams.appmetadata.output property)": [[3, "clams.appmetadata.Output.model_extra", false]], "model_extra (clams.appmetadata.runtimeparameter property)": [[3, "clams.appmetadata.RuntimeParameter.model_extra", false]], "model_fields_set (clams.appmetadata.input property)": [[3, "clams.appmetadata.Input.model_fields_set", false]], "model_fields_set (clams.appmetadata.output property)": [[3, "clams.appmetadata.Output.model_fields_set", false]], "model_fields_set (clams.appmetadata.runtimeparameter property)": [[3, "clams.appmetadata.RuntimeParameter.model_fields_set", false]], "model_json_schema() (clams.appmetadata.input class method)": [[3, "clams.appmetadata.Input.model_json_schema", false]], "model_json_schema() (clams.appmetadata.output class method)": [[3, "clams.appmetadata.Output.model_json_schema", false]], "model_json_schema() (clams.appmetadata.runtimeparameter class method)": [[3, "clams.appmetadata.RuntimeParameter.model_json_schema", false]], "model_parametrized_name() (clams.appmetadata.input class method)": [[3, "clams.appmetadata.Input.model_parametrized_name", false]], "model_parametrized_name() (clams.appmetadata.output class method)": [[3, "clams.appmetadata.Output.model_parametrized_name", false]], "model_parametrized_name() (clams.appmetadata.runtimeparameter class method)": [[3, "clams.appmetadata.RuntimeParameter.model_parametrized_name", false]], "model_post_init() (clams.appmetadata.input method)": [[3, "clams.appmetadata.Input.model_post_init", false]], "model_post_init() (clams.appmetadata.output method)": [[3, "clams.appmetadata.Output.model_post_init", false]], "model_post_init() (clams.appmetadata.runtimeparameter method)": [[3, "clams.appmetadata.RuntimeParameter.model_post_init", false]], "model_rebuild() (clams.appmetadata.input class method)": [[3, "clams.appmetadata.Input.model_rebuild", false]], "model_rebuild() (clams.appmetadata.output class method)": [[3, "clams.appmetadata.Output.model_rebuild", false]], "model_rebuild() (clams.appmetadata.runtimeparameter class method)": [[3, "clams.appmetadata.RuntimeParameter.model_rebuild", false]], "model_validate() (clams.appmetadata.input class method)": [[3, "clams.appmetadata.Input.model_validate", false]], "model_validate() (clams.appmetadata.output class method)": [[3, "clams.appmetadata.Output.model_validate", false]], "model_validate() (clams.appmetadata.runtimeparameter class method)": [[3, "clams.appmetadata.RuntimeParameter.model_validate", false]], "model_validate_json() (clams.appmetadata.input class method)": [[3, "clams.appmetadata.Input.model_validate_json", false]], "model_validate_json() (clams.appmetadata.output class method)": [[3, "clams.appmetadata.Output.model_validate_json", false]], "model_validate_json() (clams.appmetadata.runtimeparameter class method)": [[3, "clams.appmetadata.RuntimeParameter.model_validate_json", false]], "model_validate_strings() (clams.appmetadata.input class method)": [[3, "clams.appmetadata.Input.model_validate_strings", false]], "model_validate_strings() (clams.appmetadata.output class method)": [[3, "clams.appmetadata.Output.model_validate_strings", false]], "model_validate_strings() (clams.appmetadata.runtimeparameter class method)": [[3, "clams.appmetadata.RuntimeParameter.model_validate_strings", false]], "module": [[2, "module-clams.app", false], [5, "module-clams.restify", false]], "open_document_location() (clams.app.clamsapp static method)": [[2, "clams.app.ClamsApp.open_document_location", false]], "output (class in clams.appmetadata)": [[3, "clams.appmetadata.Output", false]], "post() (clams.restify.clamshttpapi method)": [[5, "clams.restify.ClamsHTTPApi.post", false]], "put() (clams.restify.clamshttpapi method)": [[5, "clams.restify.ClamsHTTPApi.put", false]], "record_error() (clams.app.clamsapp method)": [[2, "clams.app.ClamsApp.record_error", false]], "restifier (class in clams.restify)": [[5, "clams.restify.Restifier", false]], "run() (clams.restify.restifier method)": [[5, "clams.restify.Restifier.run", false]], "runtimeparameter (class in clams.appmetadata)": [[3, "clams.appmetadata.RuntimeParameter", false]], "serve_development() (clams.restify.restifier method)": [[5, "clams.restify.Restifier.serve_development", false]], "serve_production() (clams.restify.restifier method)": [[5, "clams.restify.Restifier.serve_production", false]], "set_error_view() (clams.app.clamsapp method)": [[2, "clams.app.ClamsApp.set_error_view", false]], "sign_view() (clams.app.clamsapp method)": [[2, "clams.app.ClamsApp.sign_view", false]], "test_client() (clams.restify.restifier method)": [[5, "clams.restify.Restifier.test_client", false]], "universal_parameters (clams.app.clamsapp attribute)": [[2, "clams.app.ClamsApp.universal_parameters", false]], "validate_document_locations() (clams.app.clamsapp static method)": [[2, "clams.app.ClamsApp.validate_document_locations", false]]}, "objects": {"clams": [[2, 0, 0, "-", "app"], [5, 0, 0, "-", "restify"]], "clams.app": [[2, 1, 1, "", "ClamsApp"]], "clams.app.ClamsApp": [[2, 2, 1, "", "_RAW_PARAMS_KEY"], [2, 2, 1, "", "_abc_impl"], [2, 3, 1, "", "_annotate"], [2, 3, 1, "", "_appmetadata"], [2, 3, 1, "", "_check_mmif_compatibility"], [2, 3, 1, "", "_load_appmetadata"], [2, 3, 1, "", "_refine_params"], [2, 3, 1, "", "annotate"], [2, 3, 1, "", "appmetadata"], [2, 3, 1, "", "get_configuration"], [2, 3, 1, "", "open_document_location"], [2, 3, 1, "", "record_error"], [2, 3, 1, "", "set_error_view"], [2, 3, 1, "", "sign_view"], [2, 2, 1, "", "universal_parameters"], [2, 3, 1, "", "validate_document_locations"]], "clams.appmetadata": [[3, 1, 1, "", "AppMetadata"], [3, 1, 1, "", "Input"], [3, 1, 1, "", "Output"], [3, 1, 1, "", "RuntimeParameter"]], "clams.appmetadata.AppMetadata": [[3, 3, 1, "", "add_input"], [3, 3, 1, "", "add_more"], [3, 3, 1, "", "add_output"], [3, 3, 1, "", "add_parameter"], [3, 2, 1, "", "model_config"]], "clams.appmetadata.Input": [[3, 3, 1, "", "add_description"], [3, 3, 1, "", "copy"], [3, 2, 1, "", "model_config"], [3, 3, 1, "", "model_construct"], [3, 3, 1, "", "model_copy"], [3, 3, 1, "", "model_dump"], [3, 3, 1, "", "model_dump_json"], [3, 4, 1, "", "model_extra"], [3, 4, 1, "", "model_fields_set"], [3, 3, 1, "", "model_json_schema"], [3, 3, 1, "", "model_parametrized_name"], [3, 3, 1, "", "model_post_init"], [3, 3, 1, "", "model_rebuild"], [3, 3, 1, "", "model_validate"], [3, 3, 1, "", "model_validate_json"], [3, 3, 1, "", "model_validate_strings"]], "clams.appmetadata.Output": [[3, 3, 1, "", "add_description"], [3, 3, 1, "", "copy"], [3, 2, 1, "", "model_config"], [3, 3, 1, "", "model_construct"], [3, 3, 1, "", "model_copy"], [3, 3, 1, "", "model_dump"], [3, 3, 1, "", "model_dump_json"], [3, 4, 1, "", "model_extra"], [3, 4, 1, "", "model_fields_set"], [3, 3, 1, "", "model_json_schema"], [3, 3, 1, "", "model_parametrized_name"], [3, 3, 1, "", "model_post_init"], [3, 3, 1, "", "model_rebuild"], [3, 3, 1, "", "model_validate"], [3, 3, 1, "", "model_validate_json"], [3, 3, 1, "", "model_validate_strings"]], "clams.appmetadata.RuntimeParameter": [[3, 3, 1, "", "copy"], [3, 2, 1, "", "model_config"], [3, 3, 1, "", "model_construct"], [3, 3, 1, "", "model_copy"], [3, 3, 1, "", "model_dump"], [3, 3, 1, "", "model_dump_json"], [3, 4, 1, "", "model_extra"], [3, 4, 1, "", "model_fields_set"], [3, 3, 1, "", "model_json_schema"], [3, 3, 1, "", "model_parametrized_name"], [3, 3, 1, "", "model_post_init"], [3, 3, 1, "", "model_rebuild"], [3, 3, 1, "", "model_validate"], [3, 3, 1, "", "model_validate_json"], [3, 3, 1, "", "model_validate_strings"]], "clams.restify": [[5, 1, 1, "", "ClamsHTTPApi"], [5, 1, 1, "", "Restifier"]], "clams.restify.ClamsHTTPApi": [[5, 3, 1, "", "get"], [5, 3, 1, "", "json_to_response"], [5, 2, 1, "", "methods"], [5, 3, 1, "", "post"], [5, 3, 1, "", "put"]], "clams.restify.Restifier": [[5, 3, 1, "", "run"], [5, 3, 1, "", "serve_development"], [5, 3, 1, "", "serve_production"], [5, 3, 1, "", "test_client"]]}, "objnames": {"0": ["py", "module", "Python module"], "1": ["py", "class", "Python class"], "2": ["py", "attribute", "Python attribute"], "3": ["py", "method", "Python method"], "4": ["py", "property", "Python property"]}, "objtypes": {"0": "py:module", "1": "py:class", "2": "py:attribute", "3": "py:method", "4": "py:property"}, "terms": {"": [1, 2, 3, 6, 9, 10, 13], "0": [1, 5, 9, 10, 13], "1": [1, 9, 10, 13], "10": [9, 13], "1024": 6, "11": 9, "13": 9, "14": 9, "15": 9, "16": 9, "17": 9, "18": 9, "19": 9, "2": [1, 2, 3, 5, 9, 10, 13], "200": 5, "3": [1, 6, 9, 10, 13], "4": [9, 13], "4000": 13, "5": [9, 13], "5000": [5, 6, 10, 12, 13], "6": 9, "7": [9, 10], "8": [6, 9, 10], "9": 9, "A": [1, 2, 3, 5, 9, 10, 13], "AND": 1, "And": [1, 9, 10, 13], "As": [1, 6, 8, 9, 10, 13], "At": [1, 6, 10], "Be": 6, "But": [1, 3], "By": [2, 6, 10, 13], "For": [1, 2, 3, 5, 6, 9, 10, 12, 13], "If": [1, 3, 6, 10, 13], "In": [1, 2, 6, 10, 13], "It": [2, 6, 10], "NO": 1, "NOT": 1, "Not": [1, 9], "OR": 1, "One": [1, 6, 13], "That": [1, 3, 6, 9, 10], "The": [1, 2, 3, 5, 6, 7, 8, 9, 10], "There": [1, 10, 13], "These": [1, 10], "To": [0, 1, 3, 7, 8, 9, 10, 13], "With": 13, "_": 6, "__": 13, "__dict__": 3, "__init__": [2, 3, 12], "__main__": [10, 13], "__name__": 10, "__pydantic_extra__": 3, "__pydantic_fields_set__": 3, "__specver__": 9, "__version__": 13, "_abc": 2, "_abc_data": 2, "_abc_impl": [2, 11], "_annot": [1, 2, 10, 11], "_appmetadata": [2, 10, 11, 13], "_check_mmif_compat": [2, 11], "_fields_set": 3, "_load_appmetadata": [2, 10, 11], "_load_model": 12, "_new_view": 13, "_parent_namespace_depth": 3, "_pydanticgeneralmetadata": 3, "_raw_paramet": 2, "_raw_params_kei": [2, 11], "_refine_param": [2, 11], "_run_nlp_tool": 13, "_types_namespac": 3, "_view_": 13, "_was_": 3, "abc": 2, "abl": [1, 9], "about": [1, 6, 9, 10, 13], "abov": [1, 6, 9, 10, 12, 13], "absolut": [6, 8], "abstract": [2, 3], "abstractsetintstr": 3, "accept": [1, 6, 10, 13], "access": [6, 13], "achiev": [3, 8], "across": 6, "action": [1, 10], "actual": [1, 3, 6, 9, 13], "ad": [2, 3, 6], "add": [1, 2, 3, 6, 13], "add_annot": 12, "add_descript": [3, 11], "add_input": [3, 11, 13], "add_mor": [3, 11], "add_output": [3, 11, 13], "add_paramet": [3, 11, 13], "add_properti": 13, "add_url_rul": 5, "addit": [1, 2, 3, 5, 6, 10, 13], "addition": [1, 6], "additionalproperti": 1, "address": 6, "adjust": [6, 10], "advanc": 8, "advis": 6, "after": [3, 13], "again": 13, "against": 3, "ai": [0, 1, 2, 6, 8, 10, 13], "aim": 8, "airplai": 6, "alia": [3, 6], "alias": 3, "all": [1, 2, 3, 6, 9, 10, 12, 13], "allow": [1, 3, 7, 13], "along": 10, "alreadi": [3, 6, 13], "also": [1, 5, 6, 8, 9, 10, 12, 13], "altern": 6, "alwai": [1, 6, 10, 12, 13], "amd": 6, "amd64": 6, "among": 1, "an": [1, 2, 3, 5, 7, 8, 9, 10, 12], "analysi": [6, 8, 10], "analyz": [1, 10], "analyzer_licens": [1, 3, 13], "analyzer_vers": [1, 3, 13], "anapp": 10, "anchor": 13, "ani": [1, 2, 3, 5, 6, 10, 13], "annot": [2, 3, 5, 6, 11, 12], "annotationtyp": [2, 13], "announc": 10, "anoth": [1, 6, 13], "anyhttpurl": 3, "anyof": 1, "anyth": 3, "apach": 13, "api": [2, 5, 9, 11, 13], "app": [3, 5, 7, 8, 11, 12], "app_inst": 5, "app_licens": [1, 3, 13], "app_vers": [1, 3], "append": [1, 6, 10], "appl": 6, "appli": 13, "applianc": 10, "applic": [1, 2, 6, 8, 10], "appmetadata": [2, 5, 8, 11, 13], "appmetdata": 13, "appropri": 1, "apt": 13, "ar": [1, 2, 3, 6, 8, 9, 10, 13], "arbitrari": [1, 2], "architectur": 2, "archiv": 8, "area": 13, "arg": [3, 13], "argument": [1, 2, 3, 6, 10, 12, 13], "arm64": 6, "around": [2, 10, 13], "arrai": 1, "articl": 13, "asid": 13, "assist": 8, "assum": 6, "asynchron": 10, "at_typ": 3, "attempt": [3, 6], "attent": 1, "attr": 13, "attribut": [3, 13], "audio": [1, 6, 10], "audiodocu": [1, 6], "authent": 6, "auto": 1, "autom": 10, "automat": [1, 3, 9, 10, 13], "avail": [0, 1, 6, 7, 10, 13], "avoid": 9, "back": 13, "background": 13, "bark": 13, "base": [1, 2, 5, 6, 8, 9, 10, 12, 13], "basemodel": 3, "bash": 6, "basic": [1, 10], "becaus": [1, 3, 6, 9, 10, 13], "becom": 13, "been": 3, "befor": [3, 6, 13], "begin": 2, "behavior": [3, 6, 10], "being": [10, 13], "believ": 8, "below": [1, 6, 9, 10, 12, 13], "benefit": 8, "better": 6, "between": [1, 5, 6, 8, 9], "beyond": [1, 13], "big": 9, "bind": 6, "block": 13, "bodi": [1, 2, 6, 10], "bool": [3, 5], "boolean": [1, 2, 3, 13], "both": [1, 13], "bother": 13, "boundari": 13, "break": 13, "bring": 8, "browser": 6, "build": [3, 10, 13], "built": [1, 2, 5, 6], "bulk": [2, 13], "by_alia": 3, "by_nam": 3, "byte": 3, "bytearrai": 3, "cach": [3, 6, 13], "cached_properti": 3, "call": [1, 2, 3, 10, 13], "callabl": 3, "caller": 13, "came": 13, "can": [1, 2, 3, 6, 8, 9, 10, 12, 13], "care": [1, 3], "case": [2, 6, 9, 10, 13], "cat": [6, 13], "chang": [1, 3, 6, 9, 13], "charact": [1, 6], "check": [1, 13], "choic": [1, 2, 3], "choos": 10, "chunk": 13, "chyron": 1, "cl": 3, "cla_inst": 5, "clam": [12, 13], "clamsapp": [2, 5, 10, 11, 12, 13], "clamshttpapi": [5, 11], "clamsproject": [10, 13], "class": [2, 3, 5, 8, 9, 10, 12], "classif": 1, "classmethod": 3, "classvar": [3, 5], "cli": 7, "client": [5, 6, 12], "clone": 6, "cmd": [6, 13], "code": [1, 5, 10, 13], "codebas": 1, "collect": [5, 12], "colon": 1, "com": [5, 6, 10, 13], "combin": 12, "come": [6, 7, 9, 10], "command": [6, 8, 10, 13], "commandlin": 10, "comment": 10, "common": [1, 2, 3, 6, 10, 13], "commonli": 10, "compact": 3, "companion": 10, "compat": [9, 10], "complet": [1, 3, 6], "complex": 1, "complic": 10, "compon": [9, 10], "compos": 1, "comput": [1, 3, 6, 8, 10], "concept": 3, "concern": [10, 13], "concret": [1, 3], "config": 3, "configdict": 3, "configur": [1, 2, 3, 5, 8, 10, 13], "conform": 3, "conjunct": 1, "connect": 13, "consider": 9, "constitu": 13, "constructor": 5, "consum": [1, 2], "contain": [1, 3, 10, 13], "container": [6, 8], "containerfil": [6, 10, 13], "content": 10, "context": [1, 2, 3], "continu": 13, "conveni": 13, "convent": 13, "convert": [5, 10], "cooki": [8, 13], "cookiecutt": 10, "copi": [2, 3, 11, 13], "core": [2, 3, 10], "coref": 13, "correct": 13, "correctli": 6, "could": [3, 6, 13], "counterpart": [1, 3], "cours": 1, "cover": 6, "cpu": 6, "creat": [2, 3, 6, 7, 10], "curl": [6, 10, 13], "current": [1, 6, 13], "custom": [3, 10], "cutter": [8, 13], "d": [6, 10, 13], "d1": [6, 13], "dai": 10, "data": [1, 2, 3, 4, 6, 9, 10, 13], "date": 13, "debian": [10, 13], "debug": [5, 6, 10], "decis": 9, "declar": 1, "deep": 3, "def": [1, 3, 12, 13], "default": [1, 2, 3, 5, 6, 10, 12, 13], "defin": [2, 3, 5, 10, 13], "depend": [1, 3, 6, 9, 10, 13], "dependency_structur": 13, "deploi": 10, "deploy": 10, "deprec": 3, "depth": 3, "descipt": 13, "describ": [1, 3, 6, 12, 13], "descript": [1, 2, 3, 13], "design": [1, 6], "desir": [1, 3], "detail": [1, 6, 9, 10, 12, 13], "detect": 6, "develop": [1, 3, 5, 6, 8, 9, 10, 13], "dict": [1, 2, 3], "dictionari": [1, 2, 3], "differ": [1, 6, 8, 9, 10, 12, 13], "dilig": [1, 3], "dir": 13, "directli": [3, 6], "directori": [1, 6, 8, 10, 13], "discrimin": 13, "discuss": [1, 9, 10], "disjunct": 1, "distinct": 1, "distribut": [6, 9, 10], "do": [1, 2, 3, 6, 10, 13], "doc": [5, 6, 10, 13], "docid": 13, "docker": [6, 8, 10], "dockerfil": [10, 13], "dockerignor": 13, "docstr": 10, "document": [1, 2, 3, 6, 7, 9, 10, 11], "documenttyp": [2, 13], "doe": [1, 3, 6, 9, 10, 13], "doesn": [1, 13], "dog": 13, "don": [9, 10, 13], "done": 13, "down": 13, "download": 1, "downstream": 1, "duck": 3, "dump": 3, "durat": 6, "dure": [1, 3], "e": [1, 2, 3, 6, 10], "each": [1, 3, 6, 10, 12, 13], "easi": [6, 13], "easili": 10, "edit": 13, "edu": 1, "effect": 3, "eighti": 13, "either": [1, 6, 13], "element": [1, 3], "els": 13, "elsewher": 2, "empti": [1, 6, 7, 9, 13], "emul": 6, "en": 5, "encod": [1, 2], "encount": [2, 3, 6], "end": [10, 13], "end_char": 12, "enforc": 3, "engin": [6, 8, 10], "ensur": 6, "ent": 12, "enter": 12, "entir": [3, 6], "enum": 1, "environ": 6, "eol": 13, "equival": 6, "error": [2, 3, 6, 13], "escap": 6, "especi": [6, 10], "essenc": [10, 13], "essenti": [1, 2], "etc": [1, 6], "even": [1, 13], "everi": 1, "everyth": 8, "exactli": 9, "exampl": [1, 3, 6, 10, 12, 13], "except": [6, 13], "exclud": 3, "exclude_default": 3, "exclude_non": 3, "exclude_unset": 3, "execut": 6, "exhaust": 1, "exist": [1, 6, 10], "expans": 1, "expect": [1, 3, 10, 13], "explain": [10, 13], "explan": 1, "explanatori": 13, "explicitli": 3, "expos": [1, 6, 10, 12], "express": 13, "extens": 1, "extern": [1, 3], "extra": [1, 3], "extract": 3, "f": [6, 13], "fact": [10, 13], "fail": [3, 9], "fairli": 6, "fall_2003": 1, "fallback": 3, "fals": [1, 2, 3, 5], "falsi": [1, 3], "familiar": 10, "few": 13, "ffmpeg": 10, "fido": 13, "field": [1, 3, 13], "file": [1, 2, 3, 6, 7, 8, 9, 10, 13], "filenotfounderror": 2, "fill": [1, 2, 3, 13], "final": [1, 3, 6, 13], "find": [0, 9, 10, 13], "findit": 13, "fine": 6, "finish": 10, "first": [6, 10, 12, 13], "fix": 6, "flag": 13, "flak": 5, "flask": [5, 8, 10, 13], "flexibl": 6, "float": 3, "fly": 1, "focus": 13, "follow": [1, 6, 8, 12, 13], "forbid": 3, "forc": [1, 3, 6], "foremost": 6, "form": 1, "format": [2, 6, 8, 9, 10], "forwardref": 3, "found": [2, 6, 10], "four": 10, "frame": 1, "framework": 1, "free": [0, 8], "friendli": 8, "from": [1, 2, 3, 5, 7, 9, 10, 13], "from_attribut": 3, "full": [1, 3, 13], "function": [1, 2, 3, 4, 10, 13], "functool": 3, "further": 10, "futur": 10, "g": [1, 2, 3, 6, 10], "gener": [1, 2, 3, 6, 8, 10, 13], "generatejsonschema": 3, "generic_rel": 13, "get": [2, 3, 5, 11, 12, 13], "get_configur": [2, 11], "get_documents_by_typ": 13, "get_text": 12, "getattr": 13, "ghcr": 13, "git": [1, 6], "github": [10, 13], "give": [1, 13], "given": [3, 13], "glanc": 13, "go": [2, 9, 13], "good": [10, 13], "gpe": 12, "gpu": 2, "grammar": 1, "great": 6, "group": 1, "guarante": 6, "guidelin": [0, 1], "gunicorn": [5, 10, 13], "h": [6, 10], "ha": [1, 2, 3, 6, 10, 13], "hand": 13, "handi": 6, "handl": [1, 3, 8, 10], "hardwar": [2, 6], "have": [1, 3, 6, 9, 10, 13], "head": [1, 5], "hello": 13, "help": [2, 6, 7, 10, 13], "helper": [3, 5], "henc": [1, 6, 9, 10], "here": [1, 6, 9, 10, 13], "high": 10, "higher": 6, "histori": 8, "host": [6, 10, 13], "hostnam": 10, "how": [1, 3, 6, 10, 13], "howev": [1, 6, 9, 10], "html": 1, "http": [0, 1, 2, 5, 8, 13], "human": [1, 3], "hw": 6, "hwfetch": 2, "i": [0, 2, 3, 5, 6, 7, 8, 10, 13], "iana": 6, "id": [6, 13], "idea": 13, "idempot": 3, "identifi": [1, 3, 13], "ignor": [1, 3], "imag": [1, 2, 10, 13], "image_nam": 6, "image_name_you_pick": 6, "implement": [2, 8, 9, 10, 13], "impli": 1, "import": [1, 6, 9, 10], "incex": 3, "includ": [1, 3, 6, 13], "incompat": 9, "increasingli": 13, "increment": 9, "indent": [2, 3, 6], "independ": 10, "index": 8, "indic": [1, 6], "individu": 1, "inform": [1, 2, 6, 9, 10], "inherit": [2, 10], "initi": [1, 3, 13], "inlin": 10, "input": [2, 3, 8, 9, 10, 11, 12, 13], "input_mmif": [6, 12], "input_specv": 2, "insert": [10, 13], "insid": [6, 13], "instal": [7, 8, 9, 13], "instanc": [1, 3, 5], "instanti": [3, 10], "instead": [1, 2, 3, 10, 13], "instruct": [6, 10, 13], "int": [3, 5], "integ": [1, 3], "integr": 10, "intel": 6, "intend": 1, "intention": 1, "interact": 6, "interchang": 8, "interest": 13, "interfac": [6, 7, 10, 12], "interoper": 8, "interpret": [1, 6], "introduc": 13, "invoc": 13, "invok": 2, "io": 13, "iri": 1, "isinst": 13, "issu": 9, "item": [1, 6, 13], "iter": 1, "its": [1, 2, 13], "itself": [1, 6, 10, 13], "java": 1, "jim": 13, "join": 13, "json": [1, 2, 3, 5, 6, 8, 9, 10], "json_data": 3, "json_schema": 3, "json_schema_extra": 3, "json_str": 5, "json_to_respons": [5, 11], "jsonschema": [2, 9], "just": [6, 13], "k": [2, 3], "keep": [1, 3, 6, 13], "kei": [1, 2, 3, 10], "kept": 1, "key1": 1, "key2": 1, "keyword": [1, 2, 3, 10, 12, 13], "kind": 1, "kwarg": [2, 10], "label": [1, 12], "label_": 12, "labels_to_us": 12, "labelset": 1, "lack": 1, "laid": 13, "lambda": 3, "languag": [1, 13], "lapp": [1, 13], "lappsgrid": [1, 13], "larg": [6, 13], "larger": 6, "later": 13, "latest": 10, "layer": 6, "ld": 9, "learn": [1, 9, 10], "least": [1, 3], "leav": [1, 13], "left": 13, "left_to_right": 3, "legaci": 2, "lehrer": 13, "lemma": 13, "let": [1, 10, 13], "level": [1, 3, 10], "librari": [6, 8, 9, 10], "licens": 1, "like": [6, 10, 13], "line": [6, 7, 13], "ling": 1, "ling001": 1, "linguist": 13, "link": 6, "lint": 3, "linux": 6, "list": [1, 2, 3, 6, 12, 13], "listen": [5, 6, 10], "liter": 3, "live": 13, "ll": 13, "load": 2, "local": [10, 13], "localhost": [5, 6, 10, 13], "locat": [2, 6], "log": [2, 3], "logic": [2, 3, 13], "long": 13, "long_id": 13, "longer": [1, 2], "look": [1, 6, 13], "lookup": 13, "loop": 13, "loopback": 5, "lot": 13, "m1": [6, 13], "m2": [6, 13], "mac": 6, "machin": [1, 6, 13], "maco": 6, "mai": [3, 6, 13], "main": [6, 10], "major": [9, 10], "make": [1, 3, 6, 10, 12, 13], "manag": 6, "mandatori": [1, 3], "mani": [8, 10, 13], "manner": 1, "manual": [1, 6, 10], "map": [1, 2, 3, 5, 10, 13], "mappingintstrani": 3, "mappingnamespac": 3, "markabl": 13, "match": [1, 3, 6, 13], "materi": 8, "matter": 1, "maven": 1, "md": [3, 10], "mean": [1, 2, 9, 13], "meant": 1, "mention": 9, "mere": 1, "messag": 6, "metadata": [2, 3, 5, 8, 10, 12], "metatadata": 1, "method": [2, 3, 5, 10, 11, 12, 13], "meticul": 1, "might": [3, 6, 10], "millisecond": 1, "mime": [6, 13], "mimic": 6, "mind": 10, "minim": [1, 13], "minimum": [1, 6], "minlength": 1, "mmif": [2, 4, 5, 6, 7, 8, 10, 13], "mmif_util": [8, 11], "mmif_vers": [1, 3], "mode": [3, 5, 6, 10], "model": [1, 3, 6], "model_config": [3, 11], "model_construct": [3, 11], "model_copi": [3, 11], "model_dump": [3, 11], "model_dump_json": [3, 11], "model_extra": [3, 11], "model_fields_set": [3, 11], "model_json_schema": [3, 11], "model_parametrized_nam": [3, 11], "model_post_init": [3, 11], "model_rebuild": [3, 11], "model_valid": [3, 11], "model_validate_json": [3, 11], "model_validate_str": [3, 11], "modelmodel_dump": 3, "modelmodel_dump_json": 3, "modif": 3, "modifi": 6, "modul": [6, 8, 11, 13], "moment": 10, "more": [1, 2, 3, 6, 9, 10, 12, 13], "most": [1, 6, 9, 10, 13], "mostli": [10, 13], "mount": [6, 13], "move": [10, 13], "mp3": 6, "much": 9, "multi": 10, "multimedia": 8, "multipl": [1, 10, 12], "multivalu": [1, 2, 3, 12], "museum": 8, "must": [1, 2, 3, 6, 9, 10, 12, 13], "n": 13, "name": [1, 2, 3, 6, 10, 12, 13], "namedentityrecognizerapp": 12, "namespac": 3, "natur": 1, "nchunk": 13, "ne": [12, 13], "necessari": [3, 8, 12, 13], "need": [1, 3, 6, 9, 10, 13], "ner_model": 12, "nest": 1, "new": [2, 3, 7, 8, 9, 10, 12], "new_annot": 13, "new_contain": [2, 13], "new_mmif": 9, "new_view": [2, 13], "newer": 10, "newli": [3, 10], "newshour": 13, "next": 6, "nilli": 13, "nineteen": 13, "nlp": [8, 10], "non": [3, 6, 13], "none": [2, 3, 5], "note": [1, 3, 5, 6, 8, 10, 12, 13], "noth": 13, "notic": [1, 6, 13], "now": [3, 5, 10, 13], "null": 1, "number": [1, 3, 5, 6, 9, 10, 13], "numer": 5, "o": [8, 10, 13], "obj": 3, "object": [1, 2, 3, 5, 6, 13], "obtain": 6, "occur": 2, "off": 6, "offici": [10, 13], "offset": 13, "onc": [2, 6, 10], "one": [1, 3, 10, 13], "oneof": 1, "onli": [1, 2, 3, 5, 6, 9, 12, 13], "open": [0, 2, 6, 8, 9], "open_document_loc": [2, 11], "opencv": 10, "openerarg": 2, "oper": 2, "option": [1, 3, 5, 6, 10, 12, 13], "order": 2, "org": [1, 5, 6, 12, 13], "organ": [10, 13], "origin": 3, "other": [1, 3, 6, 10, 13], "otherwis": [1, 3, 12], "our": [9, 13], "out": [1, 6, 13], "output": [2, 3, 5, 6, 8, 9, 10, 11, 12, 13], "output_mmif": 6, "over": [10, 13], "overrid": [3, 6], "overridden": 3, "overview": [8, 13], "own": [2, 10], "p": [6, 13], "p1": 13, "p2": 13, "packag": [1, 6, 8, 10, 13], "page": [1, 8, 13], "pai": 1, "pair": [1, 2, 3, 10, 13], "palletsproject": 5, "paragraph": 13, "param": [2, 3], "paramet": [2, 3, 5, 6, 10, 12, 13], "parametercast": 10, "parametr": 3, "parent": [3, 13], "pars": [1, 3], "part": [1, 6, 9, 10, 12, 13], "particular": 1, "particularli": [6, 13], "partli": 1, "pass": [1, 2, 3, 5, 6, 9, 10, 12, 13], "patch": 8, "path": [6, 13], "payload": 1, "pb": 13, "penn": 1, "penn_treebank_po": 1, "perform": [1, 3, 10], "persist": 6, "person": [12, 13], "perspect": 1, "phrase_structur": 13, "pick": 13, "pil": 2, "pip": [6, 10], "pip3": 13, "pipelin": 6, "place": [1, 10, 13], "plain": 13, "plan": 10, "platform": [6, 8, 10], "pleas": [0, 1, 3, 6, 9, 10], "plugin": 6, "po": [1, 13], "podman": 6, "point": 13, "poke": 10, "pom": 1, "pop_titl": 3, "popul": 2, "port": [5, 6, 10, 13], "pose": 6, "posit": 6, "possibl": [9, 13], "possibli": 13, "post": [5, 6, 10, 11, 12, 13], "postagset": 1, "power": 1, "powershel": 6, "practic": [1, 6], "pre": [1, 3, 6, 10, 13], "preambl": [12, 13], "prebulit_image_nam": 6, "precis": 1, "prefer": [1, 6], "prefix": [6, 13], "prepar": [12, 13], "prepend": 1, "prerequisit": 8, "pretrain": 6, "pretti": [2, 6, 10, 13], "prevent": 6, "previou": [6, 10], "primari": 2, "primarili": [1, 6], "print": [6, 10, 13], "prioriti": 2, "privat": 2, "problem": 13, "proceed": 6, "process": [1, 2, 8, 9, 10, 13], "produc": [1, 13], "product": [5, 6, 10, 13], "project": [6, 10], "properli": [1, 6], "properti": [2, 3, 6], "provid": [1, 2, 3, 4, 5, 6, 8, 10, 13], "public": [0, 1, 2, 6, 8, 9, 10, 13], "publish": [1, 6, 10], "pull": 6, "purpos": [1, 2], "push": 10, "put": [5, 11, 13], "pwd": 13, "py": [2, 10, 13], "pydant": 3, "pydantic_cor": 3, "pydanticserializationerror": 3, "pypi": [6, 9, 10], "python": [1, 2, 3, 5, 7, 9, 10, 13], "python3": [6, 13], "queri": [1, 6, 10, 12], "quick": [6, 8], "quickest": 6, "r": 13, "rais": [2, 3, 13], "raise_error": 3, "rate": 10, "raw": 2, "re": [1, 2, 6, 10, 13], "read": [1, 3, 6, 9, 10, 13], "readabl": [1, 3, 6], "reader": 1, "readi": [2, 5, 6, 10], "readm": 10, "realli": 2, "rebuild": 3, "receiv": 6, "recent": [6, 13], "recognit": 1, "recommend": [1, 2, 6, 10, 13], "record": [2, 10, 13], "record_error": [2, 11], "reduc": 13, "ref_templ": 3, "refer": [1, 3, 6, 9, 10, 13], "refin": 13, "refined_paramet": 2, "regard": [10, 13], "regist": 5, "registr": 0, "registri": [0, 6, 10], "regular": 13, "relat": [9, 10, 13], "releas": [6, 9], "relev": [10, 13], "reli": [5, 9], "rememb": 1, "remot": 6, "remov": [6, 13], "render": 1, "repeat": 13, "repositori": [1, 6, 10, 13], "repres": [1, 2, 3, 6], "represent": 3, "reproduc": [10, 13], "req": 1, "request": [1, 5, 6, 10, 12, 13], "requir": [1, 2, 3, 10, 13], "resample_r": 10, "resolv": [3, 6], "resourc": 5, "respect": [3, 6, 10, 13], "respond": 5, "respons": [1, 2, 5, 6, 10, 13], "rest": 13, "restifi": [2, 8, 10, 11, 13], "result": [1, 2, 3, 6, 10, 13], "retriev": 13, "return": [2, 3, 5, 6, 9, 10, 12, 13], "rewind": 11, "risk": 6, "rm": 13, "robust": 10, "root": 10, "round_trip": 3, "rout": [5, 10], "run": [1, 2, 3, 5, 7, 8, 10, 11, 12, 13], "runningtim": 2, "runtim": [2, 3, 8, 10, 13], "runtime_conf": 2, "runtime_config": 13, "runtime_param": 2, "runtimeparamet": [1, 3, 11], "safe": 6, "said": [1, 10], "same": [1, 5, 6, 9, 12, 13], "sampl": 10, "save": [1, 10, 13], "saw": 9, "scene": 1, "schema": [3, 9], "schema_gener": 3, "scheme": [1, 3, 9], "scratch": 10, "script": 13, "sdk": [1, 2, 3, 6, 10, 12, 13], "seamless": 10, "search": 8, "second": [6, 10, 13], "section": [1, 6, 12], "secur": 6, "see": [1, 2, 5, 6, 7, 10, 12, 13], "segment": 1, "select": 7, "self": [2, 3, 12, 13], "semant": 1, "semantic_rol": 13, "send": [5, 6, 12], "sens": 13, "sensit": 6, "sentenc": 13, "separ": [1, 6, 13], "sequenc": 6, "serial": [1, 2, 3, 5, 9, 10, 13], "serializ": [2, 3], "serialize_as_ani": 3, "serv": 10, "serve_develop": [5, 11], "serve_product": [5, 10, 11], "server": [1, 5, 8, 10, 13], "servic": 10, "session": [6, 10], "set": [1, 2, 3, 6, 8, 12, 13], "set_error_view": [2, 11], "setup": 13, "sever": 13, "share": 9, "shell": [6, 8, 10, 13], "ship": 9, "short": 1, "should": [1, 2, 3, 10, 12, 13], "shouldn": 10, "shown": 13, "side": [3, 6], "sign": [1, 2, 12, 13], "sign_view": [2, 11, 13], "silicon": 6, "simpl": [5, 10, 13], "simplest": [1, 13], "simpli": [1, 3, 6], "sinc": 13, "singl": [1, 3, 12], "singleton": 1, "site": 10, "size": 13, "skeleton": 13, "skip": 3, "slate": 1, "small": 13, "snippet": 13, "so": [1, 2, 6, 9, 10, 12, 13], "softwar": [1, 6, 8, 10], "some": [1, 2, 3, 5, 6, 9, 10, 13], "somewher": 13, "sourc": [1, 2, 3, 5, 7, 8, 9, 10, 11, 13], "space": [2, 13], "span": 13, "speak": 10, "special": 6, "specif": [2, 3, 6, 8, 10, 13], "specifi": [1, 2, 3, 6, 10, 12, 13], "speech": 1, "spin": 10, "stabl": 5, "standard": 13, "start": [5, 13], "start_char": 12, "starter": 13, "startswith": 13, "state": 12, "stateless": [1, 6, 12], "statement": 13, "static": [2, 5], "statu": 5, "stdin": 6, "stdout": 6, "step": 10, "still": [1, 9, 13], "stop": 13, "store": [1, 2, 3, 6, 10, 13], "str": [2, 3, 5, 10], "straight": 13, "stream": 10, "strict": [3, 13], "strictli": 3, "string": [1, 2, 3, 6, 9, 10, 12, 13], "structur": [1, 9, 10], "sub": 1, "subclass": [3, 13], "subject": 9, "submiss": [0, 1], "submit": 0, "subset": 1, "success": 3, "suffic": 6, "suffix": 1, "suitabl": 10, "super": 12, "support": [6, 8, 10], "sure": [6, 10, 13], "syntact": 9, "system": [6, 10, 13], "t": [1, 3, 5, 6, 9, 10, 13], "tabl": 9, "tag": [1, 13], "take": [1, 3, 5, 9, 10, 13], "talk": [1, 10], "target": 13, "target_specv": 2, "task": 13, "team": [1, 6, 10], "technic": 8, "tell": 13, "templat": [3, 7, 8, 13], "tempt": 6, "term": 1, "termin": [6, 10, 13], "test": [3, 5, 6, 10, 13], "test_client": [5, 11], "text": [1, 10, 12, 13], "text_valu": 13, "textdocu": 13, "thei": [1, 3, 10, 13], "them": [2, 3, 6, 13], "thi": [1, 2, 3, 5, 6, 9, 10, 12, 13], "thing": 13, "think": 13, "third": 13, "those": [1, 13], "three": [2, 6, 10, 13], "through": [6, 13], "throw": 13, "thu": [1, 13], "time": [1, 2, 9, 10, 12], "timefram": 1, "timepoint": 1, "timestamp": 13, "timeunit": 1, "titl": 3, "to_json": 3, "to_python": 3, "todo": 6, "togeth": [8, 9], "tok": 13, "token": [1, 6, 8], "tokenizer_licens": 13, "tokenizer_vers": 13, "tokenizerwrapp": 13, "tool": [8, 10], "top": 3, "tracker": 9, "traffic": 6, "transcrib": 1, "transcript": 1, "treebank": 1, "true": [1, 3, 5, 6, 10, 12, 13], "trust": 3, "try": [3, 6, 9, 10], "tupl": 3, "turn": [5, 6], "tutori": [8, 10], "two": [6, 10, 13], "txt": [1, 6, 13], "type": [2, 3, 6, 10, 12, 13], "typea": 1, "typeb": 1, "typec": 1, "typeerror": 3, "typesbas": 3, "typic": [2, 13], "u": 13, "ultra": 13, "unavoid": 9, "under": 9, "underli": [3, 10], "underspecifi": 1, "understand": [1, 6, 9], "unexpect": 3, "unfortun": 1, "union_mod": 3, "uniqu": [1, 13], "unit": [1, 12], "univers": 10, "universal_paramet": [2, 10, 11], "unix": 6, "unknown": 3, "unlik": 6, "up": [1, 6, 8, 10, 13], "updat": [3, 9], "upenn": 1, "urban": 13, "uri": [1, 3, 13], "url": [1, 3, 6, 10, 12, 13], "us": [2, 3, 5, 8, 9, 10, 12], "usag": [1, 3, 6, 13], "user": [1, 2, 3, 6, 8, 10, 12, 13], "usual": [1, 3, 6, 9, 13], "util": [4, 6], "v": [2, 3, 6, 13], "v1": [1, 6, 13], "v4": 1, "v5": 1, "v8": 6, "val": 13, "valid": [1, 2, 3, 6, 9], "validate_by_nam": 3, "validate_document_loc": [2, 11], "validationerror": 3, "valu": [1, 2, 3, 10, 12, 13], "value1": 1, "value2": 1, "valueerror": 2, "variabl": [3, 13], "variou": [1, 8, 10], "vchunk": 13, "vendor": 8, "verb": 5, "verbatim": 1, "verbos": [1, 3], "veri": 6, "version": [1, 6, 8, 10, 13], "via": [1, 10, 12, 13], "video": [1, 10], "videodocu": 1, "view": [1, 2, 5, 6, 7, 10, 12], "visit": [1, 6, 9], "vocab": [1, 13], "vocabulari": [6, 13], "volum": 6, "vram": 2, "w": 13, "wa": [3, 6], "wai": [1, 6, 13], "want": [1, 3, 6, 10, 12, 13], "warn": [3, 13], "we": [1, 6, 8, 9, 10, 12, 13], "web": [1, 2, 5, 8, 10], "webapp": 5, "webrequest": 6, "websit": [0, 6, 9, 10], "well": [1, 2, 9, 13], "were": 3, "werkzeug": [5, 10], "what": [1, 10, 13], "when": [1, 2, 3, 5, 6, 9, 10, 12, 13], "where": [1, 2, 3, 6, 10, 13], "whether": [1, 3], "which": [1, 3, 6, 8, 10, 13], "while": [1, 2, 10], "whitespac": 1, "why": 6, "wikipedia": 6, "willi": 13, "window": [6, 10], "within": [1, 13], "without": [6, 10, 13], "won": 9, "word": [1, 13], "work": [1, 2, 4, 6, 9, 10, 12, 13], "workdir": 13, "worker": 10, "workflow": [6, 8, 10], "worri": 9, "would": [2, 3, 13], "wrap": [5, 8, 10], "wrapper": [1, 2, 5, 8, 10, 13], "write": [6, 10, 13], "written": [1, 6], "www": 1, "x": [5, 6, 10, 13], "x64": 6, "xml": 1, "y": [6, 13], "you": [1, 3, 6, 7, 9, 10, 12, 13], "your": [0, 2, 3, 6, 10, 12, 13], "z": [6, 13], "zero": 2, "zip": 6}, "titles": ["CLAMS App Directory", "CLAMS App Metadata", "clams.app package", "clams.appmetadata package", "clams.mmif_utils package", "clams.restify package", "Using CLAMS App", "clams shell command", "Welcome to CLAMS Python SDK documentation!", "I/O Specification", "Getting started", "clams package", "Runtime Configuration", "Tutorial: Wrapping an NLP Application"], "titleterms": {"As": 12, "For": 8, "The": 13, "To": 6, "_annot": 13, "ad": [1, 13], "an": [6, 13], "annot": [1, 10, 13], "api": [8, 10, 12], "app": [0, 1, 2, 6, 9, 10, 13], "applic": 13, "appmetadata": [1, 3, 10], "build": 6, "case": 1, "clam": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], "class": 13, "cli": 6, "code": 6, "command": 7, "configur": [6, 12], "contain": 6, "container": [10, 13], "content": 8, "creat": 13, "custom": 1, "defin": 1, "definit": 1, "directori": 0, "docker": 13, "document": [8, 13], "download": 6, "entri": 6, "extend": 1, "format": 1, "from": [6, 8], "get": [6, 8, 10], "http": [6, 10, 12], "i": [1, 9], "imag": 6, "import": 13, "indic": 8, "input": [1, 6], "instal": [6, 10], "invok": 6, "local": 6, "locat": 13, "media": 6, "metadata": [1, 6, 13], "mmif": [1, 9], "mmif_util": 4, "modul": 4, "more": 8, "new": 13, "nlp": 13, "now": 8, "o": [1, 9], "output": 1, "overview": [1, 10], "packag": [2, 3, 4, 5, 9, 11], "paramet": 1, "point": 6, "prebuilt": 6, "prerequisit": 10, "process": 6, "program": 6, "project": 8, "properti": [1, 13], "py": 6, "python": [6, 8, 12], "quick": 10, "requir": 6, "restifi": 5, "rewind": 4, "run": 6, "runtim": [1, 12], "schema": 1, "sdk": 8, "server": [6, 12], "shell": 7, "simpl": 1, "sourc": [4, 6], "specif": [1, 9], "start": [6, 8, 10], "syntax": 1, "tabl": 8, "target": 9, "templat": 10, "token": 13, "tool": 13, "tutori": 13, "type": 1, "us": [1, 6, 13], "version": 9, "view": 13, "vocabulari": 1, "webapp": 10, "welcom": 8, "wrap": 13}}) \ No newline at end of file diff --git a/docs/tutorial.html b/docs/tutorial.html deleted file mode 100644 index 4b30a8a..0000000 --- a/docs/tutorial.html +++ /dev/null @@ -1,476 +0,0 @@ - - - - - - - - - Tutorial: Wrapping an NLP Application — clams-python documentation - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - -
  • -
  • -
-
-
-
-
- -
-

Tutorial: Wrapping an NLP Application

-

The following is a tutorial on how to wrap a simple NLP tool as a CLAMS application, using app template generated by clams develop command. -Particularly, this article is focused on writing _annotate() method in a CLAMS app (app.py). -This may not make a lot of sense without glancing over recent MMIF specifications and CLAMS SDK overview.

-
-

The NLP tool

-

We use an ultra simple tokenizer as tokenizer.py as the example NLP tool. All it does is define a tokenize function that uses a simple regular expression and returns a list of offset pairs.

-
$ cat tokenizer.py
-
-
-import re
-
-__VERSION__ = 'v1'
-
-def tokenize(text):
-    return [tok.span() for tok in re.finditer("\w+", text)]
-
-
-
$ python
->>> import tokenizer
->>> tokenizer.tokenize('Fido barks.')
-[(0, 4), (5, 10)]
-
-
-
-
-

Wrapping the tokenizer

-

First, it is recommended to use clams develop in the command line and follow the instructions there to generate the necessary skeleton templates for developing the app. -In the rest of this tutorial, we will use TokenizerWrapper as the class name -(To generate a starter code with that class name, you need to use -n tokenizer-wrapper flag).

-

By convention, all the “app” code is in a script named app.py (this is not a strict requirement and you can give it another name). The app.py script in general does several things:

-
    -
  1. importing the necessary code (preamble)

  2. -
  3. creating a subclass of ClamsApp that defines the metadata (_appmetadata()) and provides a method to run the wrapped tool (_annotate())

  4. -
  5. providing a way to run the code as a HTTP Flask server (__main__ block).

  6. -
-

The clams develop cookie cutter will generate the third part. Thus, the first and second parts of the code are explained here.

-
-

Imports

-

Aside from a few standard modules we need the following imports:

-
from clams import ClamsApp, Restifier
-from mmif import Mmif, View, Annotation, Document, AnnotationTypes, DocumentTypes
-
-# For an NLP tool we need to import the LAPPS vocabulary items
-from lapps.discriminators import Uri
-# --- came from the starter code
-
-import tokenizer  # THIS MEANS you put the `tokenizer.py` in the same directory with `app.py`
-
-
-

For non-NLP CLAMS applications we would also do from mmif.vocabulary import AnnotationTypes, but this is not needed for NLP applications because they do not need the CLAMS vocabulary. What we do need to import are the URIs of all LAPPS annotation types and the NLP tool itself.

-
-

Note -MMIF uses LAPPS vocabulary for linguistic annotation types.

-
-

Importing lapps.discriminators.Uri is for convenience since it gives us easy access to the URIs of annotation types and some of their attributes. The following code prints a list of available variables that point to URIs:

-
>>> from lapps.discriminators import Uri
->>> attrs = [x for x in dir(Uri) if not x.startswith('__')]
->>> attrs = [a for a in attrs if not getattr(Uri, a).find('org/ns') > -1]
->>> print(' '.join(attrs))
-ANNOTATION CHUNK CONSTITUENT COREF DATE DEPENDENCY DEPENDENCY_STRUCTURE DOCUMENT GENERIC_RELATION LEMMA LOCATION LOOKUP MARKABLE MATCHES NCHUNK NE ORGANIZATION PARAGRAPH PERSON PHRASE_STRUCTURE POS RELATION SEMANTIC_ROLE SENTENCE TOKEN VCHUNK
->>> print(Uri.TOKEN)
-http://vocab.lappsgrid.org/Token
-
-
-
-
-

The application class

-

With the imports in place we define a subclass of ClamsApp which needs two methods:

-
class TokenizerWrapper(ClamsApp):
-    def _appmetadata(self): pass
-
-    def _annotate(self, mmif): pass
-
-
-

Here it is useful to introduce some background. The CLAMS HTTP API connects the GET and POST requests to the appmetdata() and annotate() methods on the app respectively, and those methods are both defined in ClamsApp. In essence, they are wrappers around _appmetadata() and _annotate() and provide some common functionality like making sure the output is serialized into a string.

-
-

App Metadata

-

The _appmetadata() method should return an AppMetadata object that defines the relevant metadata for the app:

-

(If you are using the app template, use metadata.py instead of app.py to define the metadata.)

-
APP_LICENSE = 'Apache 2.0'
-TOKENIZER_LICENSE = 'Apache 2.0'
-TOKENIZER_VERSION = tokenizer.__VERSION__
-
-def _appmetadata(self):
-    metadata = AppMetadata(
-        identifier='tokenizer-wrapper',
-        name="Tokenizer Wrapper",
-        url='https://github.com/clamsproject/app-nlp-example',
-        description="Apply simple tokenization to all text documents in a MMIF file.",
-        app_license=APP_LICENSE,
-        analyzer_version=TOKENIZER_VERSION,
-        analyzer_license=TOKENIZER_LICENSE,
-    )
-    metadata.add_input(DocumentTypes.TextDocument)
-    metadata.add_output(Uri.TOKEN)
-    metadata.add_parameter('error', 'Throw error if set to True', 'boolean')
-    metadata.add_parameter('eol', 'Insert sentence boundaries', 'boolean')
-    return metadata
-
-
-
-

Warning -When using the separately generated metadata.py created via clams develop, this method within app.py should be left empty with a pass statement as shown below:

-
def _appmetadata(self):
-    # When using the ``metadata.py`` leave this do-nothing "pass" method here.
-        pass
-
-
-

And the appmetadata() within metadata.py should be implemented instead. Follow the instructions in the template.

-
-

As we see in the code above, the AppMetadata object is created with the following fields: -identifier, name, url, description, app_license, analyzer_version, and analyzer_license. If you used clams develop to generate the app template, you’ll also notice that some of these fields are already filled in for you based on the -n argument you provided.

-

More interesting are the add_input(), add_output(), and add_parameter() parts. -The add_input() method is used to specify the input annotation type(s) that the app expects. Here, we specify that the app just expects text documents. -The add_output() method is used to specify the output annotation type(s) that the app produces. -So our first CLAMS app will take text documents as input and produce token annotations as output. -Note that the I/O types must be specified using the URIs. We are using URIs defined in CLAMS and LAPPS vocabularies, but as long as the type is defined somewhere, any URI can be used.

-

Finally, the add_parameter() method is used to specify the parameters that the app accepts. -For usage of these parameters, you’ll find Runtime Configuration page helpful. -Now, as a developer, you can specify the parameters that the app accepts, so the users can set these parameters when running the app, using add_parameter() method. -Here, we are defining two parameters: error and eol, and in addition to the name of parameter, we also specify the description and the type of the parameter. With the desciption and the type, the parameters should be pretty self-explanatory to the user. -One think to note in this code snippet is that both parameters don’t have default values. -This means that if the user doesn’t specify the value for these parameters at the runtime, the app will not run and throw an error. -If you want to make a parameter “optional” by providing a default value, you can do so by adding a default argument to the add_parameter() method.

-
-

Note -Also refer to CLAMS App Metadata for more details regarding what fields need to be specified.

-
-
-
-

_annotate()

-

The _annotate() method should accept a MMIF file/string/object as its first parameter and always returns a MMIF object with an additional view containing annotation results. This is where the bulk of your logic will go. For a text processing app, it is mostly concerned with finding text documents, calling the code that runs over the text, creating new views and inserting the results.

-

In addition to the input MMIF, this method can accept any number of keyword arguments, which are the parameters set by the user/caller. Note that when this method is called inside the annotate() public method in the ClamsApp class (which is the usual case when running as a CLAMS app), the keyword arguments are automatically “refined” before being passed here. The refinement includes

-
    -
  1. inserting “default” values for parameters that are not set by the user

  2. -
  3. checking that the values are of the correct type and value, based on the parameter specification in the app metadata

  4. -
-
def _annotate(self, mmif, **parameters):
-    # then, access the parameters: here to just print
-    # them and to willy-nilly throw an error if the caller wants that
-    for arg, val in parameters.items():
-        print(f"Parameter {arg}={val}")
-        # as we defined this `error` parameter in the app metadata
-        if arg == 'error' and val is True:
-            raise Exception(f"Exception - {parameters['error']}")
-    # Initialize the MMIF object from the string if needed
-    self.mmif = mmif if isinstance(mmif, Mmif) else Mmif(mmif)
-    # process the text documents in the documents list
-    for doc in self.mmif.get_documents_by_type(DocumentTypes.TextDocument):
-        # prepare a new _View_ object to store output annotations
-        new_view = self._new_view(doc.long_id, parameters)  # continue reading to see what `long_id` does
-        # _run_nlp_tool() is the method that does the actual work
-        self._run_nlp_tool(doc, new_view)
-    # return the MMIF object
-    return self.mmif
-
-
-

For language processing applications, one task is to retrieve all text documents from both the documents list and the views. Annotations generated by the NLP tool need to be anchored to the text documents, which in the case of text documents in the documents list is done by using the text document identifier, but for text documents in views we also need the view identifier. A view may have many text documents and typically all annotations created will be put in one view.

-

For each text document from the document list, there is one invocation of _new_view() which gets handed a document identifier, so it can be put in the view metadata. And for each view with text documents there is also one invocation of _new_view(), but no document identifier is handed in so the identifier will not be put into the view metadata.

-

The method _run_nlp_tool() is responsible for running the NLP tool and adding annotations to the new view. The third argument allows us to anchor annotations created by the tool by handing over the document identifier, possibly prefixed by the view the document lives in.

-

One thing about _annotate() as it is defined above is that it will most likely be the same for each NLP application, all the application specific details are in the code that creates new views and the code that adds annotations.

-
-
Creating a new view:
-
def _new_view(self, docid, runtime_config):
-    view = self.mmif.new_view()
-    view.metadata.app = self.metadata.identifier
-    # first thing you need to do after creating a new view is "sign" the view
-    # the sign_view() method will record the app's identifier and the timestamp
-    # as well as the user parameter inputs. This is important for reproducibility.
-    self.sign_view(view, runtime_config)
-    # then record what annotations you want to create in this view
-    view.new_contain(Uri.TOKEN, document=docid)
-    return view
-
-
-

This is the simplest NLP view possible since there is only one annotation type, and it has no metadata properties beyond the document property. Other applications may have more annotation types, which results in repeated invocations of new_contain(), and may define other metadata properties for those types.

-
-
-
Adding annotations:
-
def _run_nlp_tool(self, doc, new_view):
-    """
-    Run the NLP tool over the document and add annotations to the view, using the
-    full document identifier (which may include a view identifier) for the document
-    property.
-    """
-    text = doc.text_value
-    tokens = tokenizer.tokenize(text)
-    for p1, p2 in tokens:
-        a = new_view.new_annotation(Uri.TOKEN)
-        # `long_id` will give you the annotation object's ID, prefixed by its parents view's ID (if it has one)
-        # so that when the targeting document is in a different view, we can still have back references
-        a.add_property('document', doc.long_id)
-        a.add_property('start', p1)
-        a.add_property('end', p2)
-        a.add_property('word', text[p1:p2])
-        # see what properties are required / available in the LAPPS vocabulary https://vocab.lappsgrid.org/Token
-
-
-

First, with text_value we get the text from the text document, either from its location property or from its text property. Second, we apply the tokenizer to the text. And third, we loop over the token offsets in the tokenizer result and create annotations of type Uri.TOKEN with an identifier that is automatically generated by the SDK. All that is needed for adding an annotation is the new_annotation() method on the view object and the add_property() method on the annotation object.

-
-
-
-
-
-

Containerization with Docker

-

Apps within CLAMS typically run as Flask servers in Docker containers, and after an app is tested as a local Flask application, it should be containerized. In fact, in some cases we don’t even bother running a local Flask server and move straight to the container set up.

-

Three configuration files for building a container image should be automatically generated through the clams develop command:

- - - - - - - - - - - - - - - - - -

file

description

Containerfile

Describes how to create a container image for this application.

.dockerignore

Specifies which files are not needed for running this application.

requirements.txt

File with all Python modules that need to be installed.

-

Here is the minimal Containerfile included with this example:

-
# make sure to use a specific version number here
-FROM ghcr.io/clamsproject/clams-python:x.y.z
-COPY ./ /app
-WORKDIR ./app
-
-CMD ["python3", "app.py", "--production"]
-
-
-

This starts from the base CLAMS image which is created from an official Python image (Debian-based) with the clams-python package and the code it depends on added. The Containerfile only needs to be edited if additional installations are required to run the NLP tool. In that case the Containerfile will have a few more lines:

-
FROM ghcr.io/clamsproject/clams-python:x.y.z
-RUN apt install -y <system-packages>
-WORKDIR ./app
-COPY ./requirements.txt .
-RUN pip3 install --no-cache-dir -r requirements.txt  # no-cache-dir will save some space and reduce final image size
-COPY ./ ./
-CMD ["python3", "app.py"]
-
-
-
-

Note -Containerfile in the app starter template has more pre-configuration lines.

-
-

With this Containerfile you typically only need to make changes to the requirements file for additional python installs.

-

This repository also includes a .dockerignore file. Editing it is optional, but with large repositories with lots of documentation and images you may want to add some file paths just to keep the image as small as possible.

-

Use one of the following commands to build the image, the first one builds an image with a production server using Gunicorn, the second one builds a development server using Flask.

-
$ docker build -t clams-nlp-example:0.0.1 -f Containerfile .
-
-
-

The -t option lets you pick a name and a tag for the image. You can use another name if you like. You do not have to add a tag and you could just use -t nlp-clams-example, but it is usually a good idea to use the version name as the tag. -The -f option lets you specify a different Containerfile. If you do not specify a file then docker will look for a file called Dockerfile in the current directory (Note that in this tutorial we’re using Containerfile as the name, not Dockerfile).

-
-

Note -For full details on the docker build command see the docker-build documentation.

-
-

To test the Flask app in the container from your local machine do

-
$ docker run --name clams-nlp-example --rm -d -p 4000:5000 clams-nlp-example:0.0.1
-
-
-

There are a lot going on in this command, so let’s break it down: ---name clams-nlp-example: This is the name of the container. You can use any name you like, but it has to be unique. You will need this name to stop the container later if you run it with -d (see below). ---rm: This option tells Docker to remove the container when it is stopped. --d: This option tells Docker to run the container in the background. If you leave this option out you will see the output of the container in your terminal. --p 5000:5000: This option tells Docker to map port 5000 on the container to port 4000 on your local machine. This is the port that the Flask server is running on in the container. -clams-nlp-example:0.0.1: This is the name and tag of the image that you want to run in a container.

-
-

Note -For full details on the docker run command see the docker-run documentation.

-
-

Now you can call the server with curl command from your terminal, but first you need an input MMIF file.

-
{
-  "metadata": {
-    "mmif": "http://mmif.clams.ai/0.4.0"
-  },
-  "documents": [
-    {
-      "@type": "http://mmif.clams.ai/0.4.0/vocabulary/TextDocument",
-      "properties": {
-        "id": "m2",
-    "text": {
-      "@value": "Hello, this is Jim Lehrer with the NewsHour on PBS. In the nineteen eighties, barking dogs have increasingly become a problem in urban areas."
-    }
-      }
-    }
-  ],
-  "views": []
-}
-
-
-

Save this as example.mmif and run the following curl command:

-
$ curl http://localhost:5000/
-$ curl -X POST -d@example.mmif http://localhost:5000/
-
-
-
-

Using the document.location property

-

Typically TextDocument in a MMIF use the location property to point to a text file. This will not work with the setup laid out above because that’s dependent on having a local path on your machine and the container has no access to that path. What you need to do is to make sure that the container can see the data on your local machine and you can use the -v option for that:

-
$ docker run --name clams-nlp-example --rm -d -p 5000:5000 -v $PWD/input/data:/data clams-nlp-example:0.0.1
-
-
-

We now have specified that the /data directory on the container is a mount of the ./input/data directory in the “host” machine. Given that ./input/data contains a example.txt text file, now you need to make sure that the input MMIF file uses the path on the container:

-
{
-  "@type": "http://mmif.clams.ai/vocabulary/TextDocument/v1",
-  "properties": {
-    "id": "m1",
-    "mime": "text/plain",
-    "location": "file:///data/text/example.txt"
-  }
-}
-
-
-

To generate a MMIF file like this, you can use clams source command from your shell.

-
$ clams source --prefix /data/input text:example.txt
-{
-  "metadata": {
-    "mmif": "http://mmif.clams.ai/1.0.0"
-  },
-  "documents": [
-    {
-      "@type": "http://mmif.clams.ai/vocabulary/TextDocument/v1",
-      "properties": {
-        "mime": "text",
-        "id": "d1",
-        "location": "file:///data/input/example.txt"
-      }
-    }
-  ],
-  "views": []
-}
-
-
-

And now you can use curl again

-
$ curl -X POST -d@input/example-3.mmif http://0.0.0.0:5000/
-
-
-
-
-
- - -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/documentation/clamsapp.md b/documentation/clamsapp.md index 27d1a6e..5ec697f 100644 --- a/documentation/clamsapp.md +++ b/documentation/clamsapp.md @@ -183,8 +183,8 @@ For example, appending `?pretty=True` to the URL will result in a JSON output wi > **Note** > When you're using `curl` from a shell session, you need to escape the `?` or `&` characters with `\` to prevent the shell from interpreting it as a special character. - -Different apps have different configurability. For configuration parameters of an app, please refer to `parameter` section of the app metadata. + +Different apps have different configurability. For configuration parameters of an app, please refer to `parameter` section of the app metadata. In addition to app-specific parameters, all apps support universal parameters (e.g., `pretty` for formatted output). Check the app metadata for the complete and up-to-date list. ### Using CLAMS App as a CLI program @@ -209,6 +209,17 @@ $ python app.py * Be default, the app will be running in *debugging* mode, but you can change it to *production* mode by passing `--production` option to support larger traffic volume. * As you might have noticed, the default `CMD` in the prebuilt containers is `python app.py --production --port 5000`. +##### Environment variables for production mode + +When running in production mode, the following environment variables can be used to configure the app server: + +| Variable | Description | Default | +|----------|-------------|---------| +| `CLAMS_GUNICORN_WORKERS` | Number of gunicorn worker processes | Auto-calculated based on CPU cores and GPU memory | +| `CLAMS_LOGLEVEL` | Logging verbosity level (`debug`, `info`, `warning`, `error`) | `warning` | + +By default, the number of workers is calculated as `(CPU cores × 2) + 1`. For GPU-based apps, see [GPU Memory Management](gpu-apps.md) for details on automatic worker scaling and VRAM management. + #### `metadata.py`: Getting app metadata Running `metadata.py` will print out the app metadata in JSON format. diff --git a/documentation/conf.py b/documentation/conf.py index 56cd583..d55cb09 100644 --- a/documentation/conf.py +++ b/documentation/conf.py @@ -14,6 +14,8 @@ from pathlib import Path import shutil import sys +import inspect +import os import mmif @@ -24,9 +26,14 @@ # -- Project information ----------------------------------------------------- project = proj_root_dir.name +blob_base_url = f'https://github.com/clamsproject/{project}/blob' copyright = f'{datetime.date.today().year}, Brandeis LLC' author = 'Brandeis LLC' -version = open(proj_root_dir / 'VERSION').read().strip() +try: + version = open(proj_root_dir / 'VERSION').read().strip() +except FileNotFoundError: + print("WARNING: VERSION file not found, using 'dev' as version.") + version = 'dev' root_doc = 'index' @@ -39,7 +46,6 @@ 'sphinx.ext.autodoc', 'sphinx.ext.linkcode', 'sphinx.ext.intersphinx', - 'sphinx_rtd_theme', 'sphinx-jsonschema', 'm2r2' ] @@ -53,13 +59,10 @@ } -# Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -# This pattern also affects html_static_path and html_extra_path. exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] +# dynamically generated files +exclude_patterns.extend(['cli_help.rst', 'whatsnew.md']) # -- Options for HTML output ------------------------------------------------- @@ -67,30 +70,127 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -html_theme = 'sphinx_rtd_theme' - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -# html_static_path = ['_static'] - -# hide document source view link at the top -html_show_sourcelink = False +html_theme = 'furo' +html_extra_path = ['appmetadata.jsonschema'] +html_static_path = [] # No static path for now, can be created if needed +html_show_sourcelink = True # Furo handles this well, no need to hide + +# Theme options for visual consistency with CLAMS branding +html_theme_options = { + # "light_logo": "logo.png", # TODO: Add logo files if available + # "dark_logo": "logo.png", + "sidebar_hide_name": False, + "navigation_with_keys": True, + "source_repository": "https://github.com/clamsproject/clams-python", + "source_branch": "main", # Default branch for "Edit on GitHub" links + "source_directory": "documentation/", + + # CLAMS brand colors + "light_css_variables": { + "color-brand-primary": "#008AFF", + "color-brand-content": "#0085A1", + "color-link": "#008AFF", + "color-link-hover": "#0085A1", + }, + # Dark mode variables can be added here if needed +} # function used by `linkcode` extension def linkcode_resolve(domain, info): - if domain != 'py': + if domain != 'py' or not info.get('module'): return None - if not info['module']: + + try: + # Find the Python object + obj = sys.modules.get(info['module']) + if obj is None: return None + for part in info['fullname'].split('.'): + obj = getattr(obj, part) + + # Get the source file and line numbers + # Use inspect.unwrap to handle decorated objects + unwrapped_obj = inspect.unwrap(obj) + filename = inspect.getsourcefile(unwrapped_obj) + if not filename: return None + + lines, start_lineno = inspect.getsourcelines(unwrapped_obj) + end_lineno = start_lineno + len(lines) - 1 + + # clams-python docs are single-version, always pointing to main + git_ref = 'main' + + # Get file path relative to repository root + repo_root = Path(__file__).parent.parent + rel_path = Path(filename).relative_to(repo_root) + + return f"{blob_base_url}/{git_ref}/{rel_path}#L{start_lineno}-L{end_lineno}" + + except Exception: + # Don't fail the entire build if one link fails, just return None return None - filename = info['module'].replace('.', '/') - return f"https://github.com/clamsproject/clams-python/tree/main/{filename}/__init__.py" -def update_target_spec(): +def generate_whatsnew_rst(app): + changelog_path = proj_root_dir / 'CHANGELOG.md' + output_path = proj_root_dir / 'documentation' / 'whatsnew.md' + if not changelog_path.exists(): + print(f"WARNING: CHANGELOG.md not found at {changelog_path}") + with open(output_path, 'w') as f: + f.write("") + return + + import re + + content = [] + found_version = False + version_header_re = re.compile(r'^## releasing\s+([^\s]+)\s*(\(.*\))?') + + print(f"DEBUG: Looking for version '{version}' in CHANGELOG.md") + + with open(changelog_path, 'r') as f: + lines = f.readlines() + + for line in lines: + match = version_header_re.match(line) + if match: + header_version = match.group(1) + if header_version == version: + found_version = True + # We don't include the header line itself in the content we want to wrap + continue + elif found_version: + break + + if found_version: + content.append(line) + + if not found_version: + print(f"NOTE: No changelog entry found for version {version}") + with open(output_path, 'w') as f: + f.write("") + else: + # Dump matched markdown content directly to whatsnew.md + with open(output_path, 'w') as f: + f.write(f"## What's New in {version}\n\n(Full changelog available in the [CHANGELOG.md]({blob_base_url}/main/CHANGELOG.md))\n") + f.writelines(content) + + +def generate_jsonschema(app): + import json + from clams.appmetadata import AppMetadata + + # Generate schema using Pydantic v2 API + schema_dict = AppMetadata.model_json_schema() + + output_path = Path(app.srcdir) / 'appmetadata.jsonschema' + with open(output_path, 'w') as f: + json.dump(schema_dict, f, indent=2) + + +def update_target_spec(app): target_vers_csv = Path(__file__).parent / 'target-versions.csv' - with open("../VERSION", 'r') as version_f: + with open(proj_root_dir / "VERSION", 'r') as version_f: version = version_f.read().strip() mmifver = mmif.__version__ specver = mmif.__specver__ @@ -102,4 +202,8 @@ def update_target_spec(): out_f.write(line) shutil.move(out_f.name, in_f.name) -update_target_spec() + +def setup(app): + app.connect('builder-inited', generate_whatsnew_rst) + app.connect('builder-inited', generate_jsonschema) + app.connect('builder-inited', update_target_spec) diff --git a/documentation/gpu-apps.rst b/documentation/gpu-apps.rst new file mode 100644 index 0000000..2f2025e --- /dev/null +++ b/documentation/gpu-apps.rst @@ -0,0 +1,127 @@ +GPU Memory Management for CLAMS Apps +===================================== + +This document covers GPU memory management features in the CLAMS SDK for developers building CUDA-based applications. + +Overview +-------- + +CLAMS apps that use GPU acceleration face memory management challenges when running as HTTP servers with multiple workers. Each gunicorn worker loads models independently into GPU VRAM, which can cause out-of-memory (OOM) errors. + +The CLAMS SDK provides: + +1. **Metadata fields** for declaring GPU memory requirements +2. **Automatic worker scaling** based on available VRAM +3. **Worker recycling** to release GPU memory between requests +4. **Memory monitoring** via ``hwFetch`` parameter + +.. note:: + Memory profiling features require **PyTorch** (``torch.cuda`` APIs). Worker calculation uses ``nvidia-smi`` and works with any framework. + +Declaring GPU Memory Requirements +--------------------------------- + +Declare GPU memory requirements in app metadata: + +.. list-table:: + :header-rows: 1 + :widths: 15 10 10 65 + + * - Field + - Type + - Default + - Description + * - ``est_gpu_mem_min`` + - int + - 0 + - Memory usage with parameters set for least computation (e.g., smallest model). 0 means no GPU. + * - ``est_gpu_mem_typ`` + - int + - 0 + - Memory usage with default parameters. Used for worker calculation. + +These values don't need to be precise. A reasonable estimate from development experience (e.g., observing ``nvidia-smi`` during runs) is sufficient. + +Example: + +.. code-block:: python + + metadata = AppMetadata( + name="My GPU App", + # ... other fields + est_gpu_mem_min=4000, # 4GB minimum + est_gpu_mem_typ=6000, # 6GB typical + ) + +Gunicorn Integration +-------------------- + +Running ``python app.py --production`` starts a gunicorn server with automatic GPU-aware configuration. + +Worker Calculation +~~~~~~~~~~~~~~~~~~ + +Worker count is the minimum of: + +- CPU-based: ``(cores × 2) + 1`` +- VRAM-based: ``total_vram / est_gpu_mem_typ`` + +Override with ``CLAMS_GUNICORN_WORKERS`` environment variable if needed. + +Worker Recycling +~~~~~~~~~~~~~~~~ + +By default, workers are recycled after each request (``max_requests=1``) to fully release GPU memory. For single-model apps, disable recycling for better performance: + +.. code-block:: python + + restifier.serve_production(max_requests=0) # Workers persist + +NVIDIA Memory Oversubscription +------------------------------ + +.. warning:: + **NVIDIA drivers R535+ include "System Memory Fallback"** - when VRAM is exhausted, the GPU swaps to system RAM via PCIe. This prevents OOM errors but causes **severe performance degradation (5-10x slower)**. + + This feature is convenient for development but can mask memory issues in production. Monitor actual VRAM usage with ``hwFetch`` to ensure your app fits in GPU memory. + +Disabling Oversubscription +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To force OOM errors instead of silent performance degradation: + +**PyTorch:** + +.. code-block:: python + + import torch + # Limit to 90% of VRAM - will raise OOM if exceeded + torch.cuda.set_per_process_memory_fraction(0.9) + +**TensorFlow:** + +.. code-block:: python + + import tensorflow as tf + gpus = tf.config.list_physical_devices('GPU') + if gpus: + # Set hard memory limit (in MB) + tf.config.set_logical_device_configuration( + gpus[0], + [tf.config.LogicalDeviceConfiguration(memory_limit=8000)] + ) + +Monitoring with hwFetch +----------------------- + +Enable ``hwFetch`` parameter to include GPU info in responses: + +.. code-block:: bash + + curl -X POST "http://localhost:5000/?hwFetch=true" -d@input.mmif + +Response includes:: + + NVIDIA RTX 4090, 23.65 GiB total, 20.00 GiB available, 3.50 GiB peak used + +Use this to verify your app's actual VRAM usage and tune ``est_gpu_mem_typ`` accordingly. diff --git a/documentation/index.rst b/documentation/index.rst index 135da15..109647d 100644 --- a/documentation/index.rst +++ b/documentation/index.rst @@ -3,6 +3,12 @@ Welcome to CLAMS Python SDK documentation! .. mdinclude:: ../README.md +---- + +.. mdinclude:: whatsnew.md + +---- + .. toctree:: :maxdepth: 2 :caption: Contents @@ -10,6 +16,7 @@ Welcome to CLAMS Python SDK documentation! introduction input-output runtime-params + gpu-apps appmetadata appdirectory cli diff --git a/requirements.txt b/requirements.txt index 8a44892..6394805 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -mmif-python==1.1.2 +mmif-python==1.2.1 Flask>=2 Flask-RESTful>=0.3.9 diff --git a/tests/test_clamsapp.py b/tests/test_clamsapp.py index 8318f1b..00faff8 100644 --- a/tests/test_clamsapp.py +++ b/tests/test_clamsapp.py @@ -85,7 +85,7 @@ def _annotate(self, mmif, **kwargs): class TestClamsApp(unittest.TestCase): def setUp(self): - self.appmetadataschema = json.loads(AppMetadata.schema_json()) + self.appmetadataschema = AppMetadata.model_json_schema() self.app = ExampleClamsApp() self.in_mmif = ExampleInputMMIF.get_mmif() @@ -234,12 +234,12 @@ def test_sign_view(self): m = Mmif(self.in_mmif) v1 = m.new_view() self.app.sign_view(v1, {}) - self.assertEqual(v1.metadata.app, self.app.metadata.identifier) + self.assertEqual(v1.metadata.app, str(self.app.metadata.identifier)) self.assertEqual(len(v1.metadata.parameters), 0) v2 = m.new_view() args2 = {'undefined_param1': ['value1']} # values are lists as our restifier uses `to_dict(flat=False)` self.app.sign_view(v2, self.app._refine_params(**args2)) - self.assertEqual(v2.metadata.app, self.app.metadata.identifier) + self.assertEqual(v2.metadata.app, str(self.app.metadata.identifier)) self.assertEqual(len(v2.metadata.parameters), 1) self.assertFalse(clams.ClamsApp._RAW_PARAMS_KEY in v2.metadata.appConfiguration) for param in self.app.metadata.parameters: @@ -271,19 +271,41 @@ def test_annotate(self): out_mmif = Mmif(out_mmif) self.assertEqual(len(out_mmif.views), 2) for v in out_mmif.views: - if v.metadata.app == self.app.metadata.identifier: + if v.metadata.app == str(self.app.metadata.identifier): self.assertEqual(len(v.metadata.parameters), 0) # no params were passed when `annotate()` was called out_mmif = self.app.annotate(self.in_mmif, pretty=[str(False)]) out_mmif = Mmif(out_mmif) for v in out_mmif.views: - if v.metadata.app == self.app.metadata.identifier: + if v.metadata.app == str(self.app.metadata.identifier): self.assertEqual(len(v.metadata.parameters), 1) # 'pretty` parameter was passed out_mmif = Mmif(self.app.annotate(out_mmif)) self.assertEqual(len(out_mmif.views), 4) views = list(out_mmif.views) # insertion order is kept - self.assertTrue(views[0].metadata.timestamp < views[1].metadata.timestamp) - + self.assertEqual(views[0].metadata.timestamp, views[1].metadata.timestamp) + self.assertEqual(views[2].metadata.timestamp, views[3].metadata.timestamp) + self.assertTrue(views[1].metadata.timestamp < views[2].metadata.timestamp) + + def test_run_id(self): + # first run + out_mmif = Mmif(self.app.annotate(self.in_mmif)) + app_views = [v for v in out_mmif.views if v.metadata.app == str(self.app.metadata.identifier)] + self.assertTrue(len(app_views) > 0) + first_timestamp = app_views[0].metadata.timestamp + for view in app_views[1:]: + self.assertEqual(first_timestamp, view.metadata.timestamp) + # second run + out_mmif2 = Mmif(self.app.annotate(out_mmif)) + app_views2 = [v for v in out_mmif2.views if v.metadata.app == str(self.app.metadata.identifier)] + self.assertEqual(len(app_views2), len(app_views) * 2) + second_timestamp = app_views2[-1].metadata.timestamp + self.assertNotEqual(first_timestamp, second_timestamp) + for view in app_views2: + if view.id in [v.id for v in app_views]: + self.assertEqual(first_timestamp, view.metadata.timestamp) + else: + self.assertEqual(second_timestamp, view.metadata.timestamp) + def test_annotate_returns_invalid_mmif(self): m = Mmif(self.in_mmif) v = m.new_view() @@ -297,13 +319,13 @@ def test_annotate_returns_invalid_mmif(self): def test_open_document_location(self): mmif = ExampleInputMMIF.get_rawmmif() - with self.app.open_document_location(mmif.documents['t1']) as f: + with self.app.open_document_location(mmif['t1']) as f: self.assertEqual(f.read(), ExampleInputMMIF.EXAMPLE_TEXT) def test_open_document_location_custom_opener(self): from PIL import Image mmif = ExampleInputMMIF.get_rawmmif() - with self.app.open_document_location(mmif.documents['i1'], Image.open) as f: + with self.app.open_document_location(mmif['i1'], Image.open) as f: self.assertEqual(f.size, (200, 71)) def test_refine_parameters(self): @@ -355,20 +377,89 @@ def test_refine_parameters(self): def test_error_handling(self): params = {'raise_error': ['true'], 'pretty': ['true']} in_mmif = Mmif(self.in_mmif) - try: + try: out_mmif = self.app.annotate(in_mmif, **params) except Exception as e: out_mmif_from_str = self.app.set_error_view(self.in_mmif, **params) out_mmif_from_mmif = self.app.set_error_view(in_mmif, **params) self.assertEqual( - out_mmif_from_mmif.views.get_last(), - out_mmif_from_str.views.get_last()) + out_mmif_from_mmif.views.get_last_contentful_view(), + out_mmif_from_str.views.get_last_contentful_view()) out_mmif = out_mmif_from_str self.assertIsNotNone(out_mmif) last_view: View = next(reversed(out_mmif.views)) self.assertEqual(len(last_view.metadata.contains), 0) self.assertEqual(len(last_view.metadata.error), 2) + def test_gpu_mem_fields_default_zero(self): + """GPU memory fields default to 0.""" + metadata = AppMetadata( + name="Test App", + description="Test", + app_license="MIT", + identifier="test-app", + url="https://example.com", + ) + metadata.add_input(DocumentTypes.TextDocument) + metadata.add_output(AnnotationTypes.TimeFrame) + + self.assertEqual(metadata.est_gpu_mem_min, 0) + self.assertEqual(metadata.est_gpu_mem_typ, 0) + + def test_est_gpu_mem_typ_validation(self): + """Warning issued when est_gpu_mem_typ < est_gpu_mem_min, autocorrected.""" + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + + metadata = AppMetadata( + name="Test App", + description="Test", + app_license="MIT", + identifier="test-app", + url="https://example.com", + est_gpu_mem_min=4000, # 4GB min + est_gpu_mem_typ=2000, # 2GB typical (less than min!) + ) + metadata.add_input(DocumentTypes.TextDocument) + metadata.add_output(AnnotationTypes.TimeFrame) + + # Should have issued a warning + self.assertEqual(len(w), 1) + self.assertIn('est_gpu_mem_typ', str(w[0].message)) + self.assertIn('est_gpu_mem_min', str(w[0].message)) + + # Should have auto-corrected + self.assertEqual(metadata.est_gpu_mem_typ, metadata.est_gpu_mem_min) + + def test_analyzer_versions_default_none(self): + """analyzer_versions field defaults to None.""" + metadata = AppMetadata( + name="Test App", + description="Test", + app_license="MIT", + identifier="test-app", + url="https://example.com", + ) + metadata.add_input(DocumentTypes.TextDocument) + metadata.add_output(AnnotationTypes.TimeFrame) + + self.assertIsNone(metadata.analyzer_versions) + + def test_analyzer_versions_with_value(self): + """analyzer_versions can be set with a dictionary.""" + test_versions = {"model_a": "1.0", "model_b": "2.1"} + metadata = AppMetadata( + name="Test App", + description="Test", + app_license="MIT", + identifier="test-app", + url="https://example.com", + analyzer_versions=test_versions, + ) + metadata.add_input(DocumentTypes.TextDocument) + metadata.add_output(AnnotationTypes.TimeFrame) + + self.assertEqual(metadata.analyzer_versions, test_versions) class TestRestifier(unittest.TestCase):