diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 00000000..e69de29b diff --git a/00_test_files/figure-commonmark/cell-35-output-1.png b/00_test_files/figure-commonmark/cell-35-output-1.png new file mode 100644 index 00000000..7b80ee81 Binary files /dev/null and b/00_test_files/figure-commonmark/cell-35-output-1.png differ diff --git a/00_test_files/figure-commonmark/cell-36-output-1.png b/00_test_files/figure-commonmark/cell-36-output-1.png new file mode 100644 index 00000000..4ddbcd3e Binary files /dev/null and b/00_test_files/figure-commonmark/cell-36-output-1.png differ diff --git a/00_test_files/figure-commonmark/cell-38-output-1.png b/00_test_files/figure-commonmark/cell-38-output-1.png new file mode 100644 index 00000000..b8e59c91 Binary files /dev/null and b/00_test_files/figure-commonmark/cell-38-output-1.png differ diff --git a/00_test_files/figure-html/cell-35-output-1.png b/00_test_files/figure-html/cell-35-output-1.png new file mode 100644 index 00000000..7b80ee81 Binary files /dev/null and b/00_test_files/figure-html/cell-35-output-1.png differ diff --git a/00_test_files/figure-html/cell-36-output-1.png b/00_test_files/figure-html/cell-36-output-1.png new file mode 100644 index 00000000..4ddbcd3e Binary files /dev/null and b/00_test_files/figure-html/cell-36-output-1.png differ diff --git a/00_test_files/figure-html/cell-38-output-1.png b/00_test_files/figure-html/cell-38-output-1.png new file mode 100644 index 00000000..b8e59c91 Binary files /dev/null and b/00_test_files/figure-html/cell-38-output-1.png differ diff --git a/05_transform_files/figure-commonmark/cell-73-output-1.png b/05_transform_files/figure-commonmark/cell-73-output-1.png new file mode 100644 index 00000000..45f2e4cf Binary files /dev/null and b/05_transform_files/figure-commonmark/cell-73-output-1.png differ diff --git a/05_transform_files/figure-html/cell-73-output-1.png b/05_transform_files/figure-html/cell-73-output-1.png new file mode 100644 index 00000000..45f2e4cf Binary files /dev/null and b/05_transform_files/figure-html/cell-73-output-1.png differ diff --git a/CNAME b/CNAME new file mode 100644 index 00000000..d9bb41c6 --- /dev/null +++ b/CNAME @@ -0,0 +1 @@ +fastcore.fast.ai diff --git a/apilist.txt b/apilist.txt new file mode 100644 index 00000000..d56559ee --- /dev/null +++ b/apilist.txt @@ -0,0 +1,1356 @@ +# fastcore Module Documentation + +## fastcore.basics + +> Basic functionality used in the fastai library + +- `def ifnone(a, b)` + `b` if `a` is None else `a` + +- `def maybe_attr(o, attr)` + `getattr(o,attr,o)` + +- `def basic_repr(flds)` + Minimal `__repr__` + +- `class BasicRepr` + Base class for objects needing a basic `__repr__` + + +- `def is_array(x)` + `True` if `x` supports `__array__` or `iloc` + +- `def listify(o, *rest)` + Convert `o` to a `list` + +- `def tuplify(o, use_list, match)` + Make `o` a tuple + +- `def true(x)` + Test whether `x` is truthy; collections with >0 elements are considered `True` + +- `class NullType` + An object that is `False` and can be called, chained, and indexed + + - `def __getattr__(self, *args)` + - `def __call__(self, *args, **kwargs)` + - `def __getitem__(self, *args)` + - `def __bool__(self)` + +- `def tonull(x)` + Convert `None` to `null` + +- `def get_class(nm, *fld_names, **flds)` + Dynamically create a class, optionally inheriting from `sup`, containing `fld_names` + +- `def mk_class(nm, *fld_names, **flds)` + Create a class using `get_class` and add to the caller's module + +- `def wrap_class(nm, *fld_names, **flds)` + Decorator: makes function a method of a new class `nm` passing parameters to `mk_class` + +- `class ignore_exceptions` + Context manager to ignore exceptions + + - `def __enter__(self)` + - `def __exit__(self, *args)` + +- `def exec_local(code, var_name)` + Call `exec` on `code` and return the var `var_name` + +- `def risinstance(types, obj)` + Curried `isinstance` but with args reversed + +- `class Inf` + Infinite lists + + +- `def in_(x, a)` + `True` if `x in a` + +- `def ret_true(*args, **kwargs)` + Predicate: always `True` + +- `def ret_false(*args, **kwargs)` + Predicate: always `False` + +- `def stop(e)` + Raises exception `e` (by default `StopIteration`) + +- `def gen(func, seq, cond)` + Like `(func(o) for o in seq if cond(func(o)))` but handles `StopIteration` + +- `def chunked(it, chunk_sz, drop_last, n_chunks)` + Return batches from iterator `it` of size `chunk_sz` (or return `n_chunks` total) + +- `def otherwise(x, tst, y)` + `y if tst(x) else x` + +- `def custom_dir(c, add)` + Implement custom `__dir__`, adding `add` to `cls` + +- `class AttrDict` + `dict` subclass that also provides access to keys as attrs + + - `def __getattr__(self, k)` + - `def __setattr__(self, k, v)` + - `def __dir__(self)` + - `def copy(self)` + +- `class AttrDictDefault` + `AttrDict` subclass that returns `None` for missing attrs + + - `def __init__(self, *args, **kwargs)` + - `def __getattr__(self, k)` + +- `class NS` + `SimpleNamespace` subclass that also adds `iter` and `dict` support + + - `def __iter__(self)` + - `def __getitem__(self, x)` + - `def __setitem__(self, x, y)` + +- `def get_annotations_ex(obj)` + Backport of py3.10 `get_annotations` that returns globals/locals + +- `def eval_type(t, glb, loc)` + `eval` a type or collection of types, if needed, for annotations in py3.10+ + +- `def type_hints(f)` + Like `typing.get_type_hints` but returns `{}` if not allowed type + +- `def annotations(o)` + Annotations for `o`, or `type(o)` + +- `def anno_ret(func)` + Get the return annotation of `func` + +- `def signature_ex(obj, eval_str)` + Backport of `inspect.signature(..., eval_str=True` to `True`) + +- `def str2int(s)` + Convert `s` to an `int` + +- `def str2float(s)` + Convert `s` to a float + +- `def str2list(s)` + Convert `s` to a list + +- `def str2date(s)` + `date.fromisoformat` with empty string handling + +- `def typed(_func)` + Decorator to check param and return types at runtime, with optional casting + +- `def exec_new(code)` + Execute `code` in a new environment and return it + +- `def exec_import(mod, sym)` + Import `sym` from `mod` in a new environment + +## fastcore.dispatch + +> Basic single and dual parameter dispatch + +- `def lenient_issubclass(cls, types)` + If possible return whether `cls` is a subclass of `types`, otherwise return False. + +- `def sorted_topologically(iterable)` + Return a new list containing all items from the iterable sorted topologically + +- `class TypeDispatch` + Dictionary-like object; `__getitem__` matches keys of types using `issubclass` + + - `def __init__(self, funcs, bases)` + - `def add(self, f)` + Add type `t` and function `f` + + - `def first(self)` + Get first function in ordered dict of type:func. + + - `def returns(self, x)` + Get the return type of annotation of `x`. + + - `def __repr__(self)` + - `def __call__(self, *args, **kwargs)` + - `def __get__(self, inst, owner)` + - `def __getitem__(self, k)` + Find first matching type that is a super-class of `k` + + +- `class DispatchReg` + A global registry for `TypeDispatch` objects keyed by function name + + - `def __init__(self)` + - `def __call__(self, f)` + +- `def retain_meta(x, res, as_copy)` + Call `res.set_meta(x)`, if it exists + +- `def default_set_meta(self, x, as_copy)` + Copy over `_meta` from `x` to `res`, if it's missing + +- `@typedispatch def cast(x, typ)` + cast `x` to type `typ` (may also change `x` inplace) + +- `def retain_type(new, old, typ, as_copy)` + Cast `new` to type of `old` or `typ` if it's a superclass + +- `def retain_types(new, old, typs)` + Cast each item of `new` to type of matching item in `old` if it's a superclass + +- `def explode_types(o)` + Return the type of `o`, potentially in nested dictionaries for thing that are listy + +## fastcore.docments + +> Document parameters using comments. + +- `def docstring(sym)` + Get docstring for `sym` for functions ad classes + +- `def parse_docstring(sym)` + Parse a numpy-style docstring in `sym` + +- `def isdataclass(s)` + Check if `s` is a dataclass but not a dataclass' instance + +- `def get_dataclass_source(s)` + Get source code for dataclass `s` + +- `def get_source(s)` + Get source code for string, function object or dataclass `s` + +- `def get_name(obj)` + Get the name of `obj` + +- `def qual_name(obj)` + Get the qualified name of `obj` + +- `@delegates(_docments) def docments(elt, full, **kwargs)` + Generates a `docment` + +- `def extract_docstrings(code)` + Create a dict from function/class/method names to tuples of docstrings and param lists + +## fastcore.docscrape + +> Parse numpy-style docstrings + +- `def strip_blank_lines(l)` + Remove leading and trailing blank lines from a list of lines + +- `class Reader` + A line-based string reader. + + - `def __init__(self, data)` + - `def __getitem__(self, n)` + - `def reset(self)` + - `def read(self)` + - `def seek_next_non_empty_line(self)` + - `def eof(self)` + - `def read_to_condition(self, condition_func)` + - `def read_to_next_empty_line(self)` + - `def read_to_next_unindented_line(self)` + - `def peek(self, n)` + - `def is_empty(self)` + +- `class ParseError` + - `def __str__(self)` + +- `class NumpyDocString` + Parses a numpydoc string to an abstract representation + + - `def __init__(self, docstring, config)` + - `def __iter__(self)` + - `def __len__(self)` + - `def __getitem__(self, key)` + - `def __setitem__(self, key, val)` + +- `def dedent_lines(lines, split)` + Deindent a list of lines maximally + +## fastcore.foundation + +> The `L` class and helpers for it + +- `@contextmanager def working_directory(path)` + Change working directory to `path` and return to previous on exit. + +- `def add_docs(cls, cls_doc, **docs)` + Copy values from `docs` to `cls` docstrings, and confirm all public methods are documented + +- `def docs(cls)` + Decorator version of `add_docs`, using `_docs` dict + +- `def coll_repr(c, max_n)` + String repr of up to `max_n` items of (possibly lazy) collection `c` + +- `def is_bool(x)` + Check whether `x` is a bool or None + +- `def mask2idxs(mask)` + Convert bool mask or index list to index `L` + +- `def is_indexer(idx)` + Test whether `idx` will index a single item in a list + +- `class CollBase` + Base class for composing a list of `items` + + - `def __init__(self, items)` + - `def __len__(self)` + - `def __getitem__(self, k)` + - `def __setitem__(self, k, v)` + - `def __delitem__(self, i)` + - `def __repr__(self)` + - `def __iter__(self)` + +- `class L` + Behaves like a list of `items` but can also index with list of indices or masks + + - `def __init__(self, items, *rest)` + - `def __getitem__(self, idx)` + - `def copy(self)` + - `def __setitem__(self, idx, o)` + Set `idx` (can be list of indices, or mask, or int) items to `o` (which is broadcast if not iterable) + + - `def __eq__(self, b)` + - `def sorted(self, key, reverse)` + - `def __iter__(self)` + - `def __contains__(self, b)` + - `def __reversed__(self)` + - `def __invert__(self)` + - `def __repr__(self)` + - `def __mul__(a, b)` + - `def __add__(a, b)` + - `def __radd__(a, b)` + - `def __addi__(a, b)` + - `@classmethod def split(cls, s, sep, maxsplit)` + - `@classmethod def range(cls, a, b, step)` + - `def map(self, f, *args, **kwargs)` + - `def argwhere(self, f, negate, **kwargs)` + - `def argfirst(self, f, negate)` + - `def filter(self, f, negate, **kwargs)` + - `def enumerate(self)` + - `def renumerate(self)` + - `def unique(self, sort, bidir, start)` + - `def val2idx(self)` + - `def cycle(self)` + - `def map_dict(self, f, *args, **kwargs)` + - `def map_first(self, f, g, *args, **kwargs)` + - `def itemgot(self, *idxs)` + - `def attrgot(self, k, default)` + - `def starmap(self, f, *args, **kwargs)` + - `def zip(self, cycled)` + - `def zipwith(self, *rest)` + - `def map_zip(self, f, *args, **kwargs)` + - `def map_zipwith(self, f, *rest, **kwargs)` + - `def shuffle(self)` + - `def concat(self)` + - `def reduce(self, f, initial)` + - `def sum(self)` + - `def product(self)` + - `def setattrs(self, attr, val)` + +- `def save_config_file(file, d, **kwargs)` + Write settings dict to a new config file, or overwrite the existing one. + +- `class Config` + Reading and writing `ConfigParser` ini files + + - `def __init__(self, cfg_path, cfg_name, create, save, extra_files, types)` + - `def __repr__(self)` + - `def __setitem__(self, k, v)` + - `def __contains__(self, k)` + - `def save(self)` + - `def __getattr__(self, k)` + - `def __getitem__(self, k)` + - `def get(self, k, default)` + - `def path(self, k, default)` + - `@classmethod def find(cls, cfg_name, cfg_path, **kwargs)` + Search `cfg_path` and its parents to find `cfg_name` + + +## fastcore.imghdr + +> Recognize image file formats based on their first few bytes. + +- `def test_jpeg(h, f)` + JPEG data with JFIF or Exif markers; and raw JPEG + +- `def test_gif(h, f)` + GIF ('87 and '89 variants) + +- `def test_tiff(h, f)` + TIFF (can be in Motorola or Intel byte order) + +- `def test_rgb(h, f)` + SGI image library + +- `def test_pbm(h, f)` + PBM (portable bitmap) + +- `def test_pgm(h, f)` + PGM (portable graymap) + +- `def test_ppm(h, f)` + PPM (portable pixmap) + +- `def test_rast(h, f)` + Sun raster file + +- `def test_xbm(h, f)` + X bitmap (X10 or X11) + +## fastcore.imports + +- `def is_iter(o)` + Test whether `o` can be used in a `for` loop + +- `def is_coll(o)` + Test whether `o` is a collection (i.e. has a usable `len`) + +- `def all_equal(a, b)` + Compares whether `a` and `b` are the same length and have the same contents + +- `def noop(x, *args, **kwargs)` + Do nothing + +- `def noops(self, x, *args, **kwargs)` + Do nothing (method) + +- `def isinstance_str(x, cls_name)` + Like `isinstance`, except takes a type name instead of a type + +- `def equals(a, b)` + Compares `a` and `b` for equality; supports sublists, tensors and arrays too + +- `def ipython_shell()` + Same as `get_ipython` but returns `False` if not in IPython + +- `def in_ipython()` + Check if code is running in some kind of IPython environment + +- `def in_colab()` + Check if the code is running in Google Colaboratory + +- `def in_jupyter()` + Check if the code is running in a jupyter notebook + +- `def in_notebook()` + Check if the code is running in a jupyter notebook + +- `def remove_prefix(text, prefix)` + Temporary until py39 is a prereq + +- `def remove_suffix(text, suffix)` + Temporary until py39 is a prereq + +## fastcore.meta + +> Metaclasses + +- `def test_sig(f, b)` + Test the signature of an object + +- `class FixSigMeta` + A metaclass that fixes the signature on classes that override `__new__` + + - `def __new__(cls, name, bases, dict)` + +- `class PrePostInitMeta` + A metaclass that calls optional `__pre_init__` and `__post_init__` methods + + - `def __call__(cls, *args, **kwargs)` + +- `class AutoInit` + Same as `object`, but no need for subclasses to call `super().__init__` + + - `def __pre_init__(self, *args, **kwargs)` + +- `class NewChkMeta` + Metaclass to avoid recreating object passed to constructor + + - `def __call__(cls, x, *args, **kwargs)` + +- `class BypassNewMeta` + Metaclass: casts `x` to this class if it's of type `cls._bypass_type` + + - `def __call__(cls, x, *args, **kwargs)` + +- `def empty2none(p)` + Replace `Parameter.empty` with `None` + +- `def anno_dict(f)` + `__annotation__ dictionary with `empty` cast to `None`, returning empty if doesn't exist + +- `def use_kwargs_dict(keep, **kwargs)` + Decorator: replace `**kwargs` in signature with `names` params + +- `def use_kwargs(names, keep)` + Decorator: replace `**kwargs` in signature with `names` params + +- `def delegates(to, keep, but)` + Decorator: replace `**kwargs` in signature with params from `to` + +- `def method(f)` + Mark `f` as a method + +- `def funcs_kwargs(as_method)` + Replace methods in `cls._methods` with those from `kwargs` + +## fastcore.net + +> Network, HTTP, and URL functions + +- `def urlquote(url)` + Update url's path with `urllib.parse.quote` + +- `def urlwrap(url, data, headers)` + Wrap `url` in a urllib `Request` with `urlquote` + +- `class HTTP4xxClientError` + Base class for client exceptions (code 4xx) from `url*` functions + + +- `class HTTP5xxServerError` + Base class for server exceptions (code 5xx) from `url*` functions + + +- `def urlopen(url, data, headers, timeout, **kwargs)` + Like `urllib.request.urlopen`, but first `urlwrap` the `url`, and encode `data` + +- `def urlread(url, data, headers, decode, return_json, return_headers, timeout, **kwargs)` + Retrieve `url`, using `data` dict or `kwargs` to `POST` if present + +- `def urljson(url, data, timeout)` + Retrieve `url` and decode json + +- `def urlclean(url)` + Remove fragment, params, and querystring from `url` if present + +- `def urlsave(url, dest, reporthook, headers, timeout)` + Retrieve `url` and save based on its name + +- `def urlvalid(x)` + Test if `x` is a valid URL + +- `def urlrequest(url, verb, headers, route, query, data, json_data)` + `Request` for `url` with optional route params replaced by `route`, plus `query` string, and post `data` + +- `@patch def summary(self, skip)` + Summary containing full_url, headers, method, and data, removing `skip` from headers + +- `def urlsend(url, verb, headers, decode, route, query, data, json_data, return_json, return_headers, debug, timeout)` + Send request with `urlrequest`, converting result to json if `return_json` + +- `def do_request(url, post, headers, **data)` + Call GET or json-encoded POST on `url`, depending on `post` + +- `def start_server(port, host, dgram, reuse_addr, n_queue)` + Create a `socket` server on `port`, with optional `host`, of type `dgram` + +- `def start_client(port, host, dgram)` + Create a `socket` client on `port`, with optional `host`, of type `dgram` + +- `def tobytes(s)` + Convert `s` into HTTP-ready bytes format + +- `def http_response(body, status, hdrs, **kwargs)` + Create an HTTP-ready response, adding `kwargs` to `hdrs` + +- `@threaded def recv_once(host, port)` + Spawn a thread to receive a single HTTP request and store in `d['r']` + +## fastcore.parallel + +> Threading and multiprocessing functions + +- `def threaded(process)` + Run `f` in a `Thread` (or `Process` if `process=True`), and returns it + +- `def startthread(f)` + Like `threaded`, but start thread immediately + +- `def startproc(f)` + Like `threaded(True)`, but start Process immediately + +- `class ThreadPoolExecutor` + Same as Python's ThreadPoolExecutor, except can pass `max_workers==0` for serial execution + + - `def __init__(self, max_workers, on_exc, pause, **kwargs)` + - `def map(self, f, items, *args, **kwargs)` + +- `@delegates() class ProcessPoolExecutor` + Same as Python's ProcessPoolExecutor, except can pass `max_workers==0` for serial execution + + - `def __init__(self, max_workers, on_exc, pause, **kwargs)` + - `def map(self, f, items, *args, **kwargs)` + +- `def parallel(f, items, *args, **kwargs)` + Applies `func` in parallel to `items`, using `n_workers` + +- `def parallel_async(f, items, *args, **kwargs)` + Applies `f` to `items` in parallel using asyncio and a semaphore to limit concurrency. + +- `def run_procs(f, f_done, args)` + Call `f` for each item in `args` in parallel, yielding `f_done` + +- `def parallel_gen(cls, items, n_workers, **kwargs)` + Instantiate `cls` in `n_workers` procs & call each on a subset of `items` in parallel. + +## fastcore.py2pyi + +- `def imp_mod(module_path, package)` + Import dynamically the module referenced in `fn` + +- `def has_deco(node, name)` + Check if a function node `node` has a decorator named `name` + +- `def create_pyi(fn, package)` + Convert `fname.py` to `fname.pyi` by removing function bodies and expanding `delegates` kwargs + +- `@call_parse def py2pyi(fname, package)` + Convert `fname.py` to `fname.pyi` by removing function bodies and expanding `delegates` kwargs + +- `@call_parse def replace_wildcards(path)` + Expand wildcard imports in the specified Python file. + +## fastcore.script + +> A fast way to turn your python function into a script. + +- `def store_true()` + Placeholder to pass to `Param` for `store_true` action + +- `def store_false()` + Placeholder to pass to `Param` for `store_false` action + +- `def bool_arg(v)` + Use as `type` for `Param` to get `bool` behavior + +- `class Param` + A parameter in a function used in `anno_parser` or `call_parse` + + - `def __init__(self, help, type, opt, action, nargs, const, choices, required, default)` + - `def set_default(self, d)` + - `@property def pre(self)` + - `@property def kwargs(self)` + - `def __repr__(self)` + +- `def anno_parser(func, prog)` + Look at params (annotated with `Param`) in func and return an `ArgumentParser` + +- `def args_from_prog(func, prog)` + Extract args from `prog` + +- `def call_parse(func, nested)` + Decorator to create a simple CLI from `func` using `anno_parser` + +## fastcore.style + +> Fast styling for friendly CLIs. + +- `class StyleCode` + An escape sequence for styling terminal text. + + - `def __init__(self, name, code, typ)` + - `def __str__(self)` + +- `class Style` + A minimal terminal text styler. + + - `def __init__(self, codes)` + - `def __dir__(self)` + - `def __getattr__(self, k)` + - `def __call__(self, obj)` + - `def __repr__(self)` + +- `def demo()` + Demonstrate all available styles and their codes. + +## fastcore.test + +> Helper functions to quickly write tests in notebooks + +- `def test_fail(f, msg, contains, args, kwargs)` + Fails with `msg` unless `f()` raises an exception and (optionally) has `contains` in `e.args` + +- `def test(a, b, cmp, cname)` + `assert` that `cmp(a,b)`; display inputs and `cname or cmp.__name__` if it fails + +- `def nequals(a, b)` + Compares `a` and `b` for `not equals` + +- `def test_eq(a, b)` + `test` that `a==b` + +- `def test_eq_type(a, b)` + `test` that `a==b` and are same type + +- `def test_ne(a, b)` + `test` that `a!=b` + +- `def is_close(a, b, eps)` + Is `a` within `eps` of `b` + +- `def test_close(a, b, eps)` + `test` that `a` is within `eps` of `b` + +- `def test_is(a, b)` + `test` that `a is b` + +- `def test_shuffled(a, b)` + `test` that `a` and `b` are shuffled versions of the same sequence of items + +- `def test_stdout(f, exp, regex)` + Test that `f` prints `exp` to stdout, optionally checking as `regex` + +- `def test_fig_exists(ax)` + Test there is a figure displayed in `ax` + +- `class ExceptionExpected` + Context manager that tests if an exception is raised + + - `def __init__(self, ex, regex)` + - `def __enter__(self)` + - `def __exit__(self, type, value, traceback)` + +## fastcore.transform + +> Definition of `Transform` and `Pipeline` + +- `class Transform` + Delegates (`__call__`,`decode`,`setup`) to (encodes,decodes,setups) if `split_idx` matches + + - `def __init__(self, enc, dec, split_idx, order)` + - `@property def name(self)` + - `def __call__(self, x, **kwargs)` + - `def decode(self, x, **kwargs)` + - `def __repr__(self)` + - `def setup(self, items, train_setup)` + +- `class InplaceTransform` + A `Transform` that modifies in-place and just returns whatever it's passed + + +- `class DisplayedTransform` + A transform with a `__repr__` that shows its attrs + + - `@property def name(self)` + +- `class ItemTransform` + A transform that always take tuples as items + + - `def __call__(self, x, **kwargs)` + - `def decode(self, x, **kwargs)` + +- `def get_func(t, name, *args, **kwargs)` + Get the `t.name` (potentially partial-ized with `args` and `kwargs`) or `noop` if not defined + +- `class Func` + Basic wrapper around a `name` with `args` and `kwargs` to call on a given type + + - `def __init__(self, name, *args, **kwargs)` + - `def __repr__(self)` + - `def __call__(self, t)` + +- `def compose_tfms(x, tfms, is_enc, reverse, **kwargs)` + Apply all `func_nm` attribute of `tfms` on `x`, maybe in `reverse` order + +- `def mk_transform(f)` + Convert function `f` to `Transform` if it isn't already one + +- `def gather_attrs(o, k, nm)` + Used in __getattr__ to collect all attrs `k` from `self.{nm}` + +- `def gather_attr_names(o, nm)` + Used in __dir__ to collect all attrs `k` from `self.{nm}` + +- `class Pipeline` + A pipeline of composed (for encode/decode) transforms, setup with types + + - `def __init__(self, funcs, split_idx)` + - `def setup(self, items, train_setup)` + - `def add(self, ts, items, train_setup)` + - `def __call__(self, o)` + - `def __repr__(self)` + - `def __getitem__(self, i)` + - `def __setstate__(self, data)` + - `def __getattr__(self, k)` + - `def __dir__(self)` + - `def decode(self, o, full)` + - `def show(self, o, ctx, **kwargs)` + +## fastcore.xdg + +> XDG Base Directory Specification helpers. + +- `def xdg_cache_home()` + Path corresponding to `XDG_CACHE_HOME` + +- `def xdg_config_dirs()` + Paths corresponding to `XDG_CONFIG_DIRS` + +- `def xdg_config_home()` + Path corresponding to `XDG_CONFIG_HOME` + +- `def xdg_data_dirs()` + Paths corresponding to XDG_DATA_DIRS` + +- `def xdg_data_home()` + Path corresponding to `XDG_DATA_HOME` + +- `def xdg_runtime_dir()` + Path corresponding to `XDG_RUNTIME_DIR` + +- `def xdg_state_home()` + Path corresponding to `XDG_STATE_HOME` + +## fastcore.xml + +> Concise generation of XML. + +- `class FT` + A 'Fast Tag' structure, containing `tag`,`children`,and `attrs` + + - `def __init__(self, tag, cs, attrs, void_, **kwargs)` + - `def on(self, f)` + - `def changed(self)` + - `def __setattr__(self, k, v)` + - `def __getattr__(self, k)` + - `@property def list(self)` + - `def get(self, k, default)` + - `def __repr__(self)` + - `def __iter__(self)` + - `def __getitem__(self, idx)` + - `def __setitem__(self, i, o)` + - `def __call__(self, *c, **kw)` + - `def set(self, *c, **kw)` + Set children and/or attributes (chainable) + + +- `def ft(tag, *c, **kw)` + Create an `FT` structure for `to_xml()` + +- `def Html(*c, **kwargs)` + An HTML tag, optionally preceeded by `!DOCTYPE HTML` + +- `class Safe` + - `def __html__(self)` + +- `def to_xml(elm, lvl, indent, do_escape)` + Convert `ft` element tree into an XML string + +- `def highlight(s, lang)` + Markdown to syntax-highlight `s` in language `lang` + +## fastcore.xtras + +> Utility functions used in the fastai library + +- `def walk(path, symlinks, keep_file, keep_folder, skip_folder, func, ret_folders)` + Generator version of `os.walk`, using functions to filter files and folders + +- `def globtastic(path, recursive, symlinks, file_glob, file_re, folder_re, skip_file_glob, skip_file_re, skip_folder_re, func, ret_folders)` + A more powerful `glob`, including regex matches, symlink handling, and skip parameters + +- `@contextmanager def maybe_open(f, mode, **kwargs)` + Context manager: open `f` if it is a path (and close on exit) + +- `def mkdir(path, exist_ok, parents, overwrite, **kwargs)` + Creates and returns a directory defined by `path`, optionally removing previous existing directory if `overwrite` is `True` + +- `def image_size(fn)` + Tuple of (w,h) for png, gif, or jpg; `None` otherwise + +- `def bunzip(fn)` + bunzip `fn`, raising exception if output already exists + +- `def loads(s, **kw)` + Same as `json.loads`, but handles `None` + +- `def loads_multi(s)` + Generator of >=0 decoded json dicts, possibly with non-json ignored text at start and end + +- `def dumps(obj, **kw)` + Same as `json.dumps`, but uses `ujson` if available + +- `def untar_dir(fname, dest, rename, overwrite)` + untar `file` into `dest`, creating a directory if the root contains more than one item + +- `def repo_details(url)` + Tuple of `owner,name` from ssh or https git repo `url` + +- `def run(cmd, *rest)` + Pass `cmd` (splitting with `shlex` if string) to `subprocess.run`; return `stdout`; raise `IOError` if fails + +- `def open_file(fn, mode, **kwargs)` + Open a file, with optional compression if gz or bz2 suffix + +- `def save_pickle(fn, o)` + Save a pickle file, to a file name or opened file + +- `def load_pickle(fn)` + Load a pickle file from a file name or opened file + +- `def parse_env(s, fn)` + Parse a shell-style environment string or file + +- `def expand_wildcards(code)` + Expand all wildcard imports in the given code string. + +- `def dict2obj(d, list_func, dict_func)` + Convert (possibly nested) dicts (or lists of dicts) to `AttrDict` + +- `def obj2dict(d)` + Convert (possibly nested) AttrDicts (or lists of AttrDicts) to `dict` + +- `def repr_dict(d)` + Print nested dicts and lists, such as returned by `dict2obj` + +- `def is_listy(x)` + `isinstance(x, (tuple,list,L,slice,Generator))` + +- `def mapped(f, it)` + map `f` over `it`, unless it's not listy, in which case return `f(it)` + +- `@patch def readlines(self, hint, encoding)` + Read the content of `self` + +- `@patch def read_json(self, encoding, errors)` + Same as `read_text` followed by `loads` + +- `@patch def mk_write(self, data, encoding, errors, mode)` + Make all parent dirs of `self`, and write `data` + +- `@patch def relpath(self, start)` + Same as `os.path.relpath`, but returns a `Path`, and resolves symlinks + +- `@patch def ls(self, n_max, file_type, file_exts)` + Contents of path as a list + +- `@patch def delete(self)` + Delete a file, symlink, or directory tree + +- `class IterLen` + Base class to add iteration to anything supporting `__len__` and `__getitem__` + + - `def __iter__(self)` + +- `@docs class ReindexCollection` + Reindexes collection `coll` with indices `idxs` and optional LRU cache of size `cache` + + - `def __init__(self, coll, idxs, cache, tfm)` + - `def __getitem__(self, i)` + - `def __len__(self)` + - `def reindex(self, idxs)` + - `def shuffle(self)` + - `def cache_clear(self)` + - `def __getstate__(self)` + - `def __setstate__(self, s)` + +- `def get_source_link(func)` + Return link to `func` in source code + +- `def truncstr(s, maxlen, suf, space)` + Truncate `s` to length `maxlen`, adding suffix `suf` if truncated + +- `def sparkline(data, mn, mx, empty_zero)` + Sparkline for `data`, with `None`s (and zero, if `empty_zero`) shown as empty column + +- `def modify_exception(e, msg, replace)` + Modifies `e` with a custom message attached + +- `def round_multiple(x, mult, round_down)` + Round `x` to nearest multiple of `mult` + +- `def set_num_threads(nt)` + Get numpy (and others) to use `nt` threads + +- `def join_path_file(file, path, ext)` + Return `path/file` if file is a string or a `Path`, file otherwise + +- `def autostart(g)` + Decorator that automatically starts a generator + +- `class EventTimer` + An event timer with history of `store` items of time `span` + + - `def __init__(self, store, span)` + - `def add(self, n)` + Record `n` events + + - `@property def duration(self)` + - `@property def freq(self)` + +- `def stringfmt_names(s)` + Unique brace-delimited names in `s` + +- `class PartialFormatter` + A `string.Formatter` that doesn't error on missing fields, and tracks missing fields and unused args + + - `def __init__(self)` + - `def get_field(self, nm, args, kwargs)` + - `def check_unused_args(self, used, args, kwargs)` + +- `def partial_format(s, **kwargs)` + string format `s`, ignoring missing field errors, returning missing and extra fields + +- `def utc2local(dt)` + Convert `dt` from UTC to local time + +- `def local2utc(dt)` + Convert `dt` from local to UTC time + +- `def trace(f)` + Add `set_trace` to an existing function `f` + +- `@contextmanager def modified_env(*delete, **replace)` + Context manager temporarily modifying `os.environ` by deleting `delete` and replacing `replace` + +- `class ContextManagers` + Wrapper for `contextlib.ExitStack` which enters a collection of context managers + + - `def __init__(self, mgrs)` + - `def __enter__(self)` + - `def __exit__(self, *args, **kwargs)` + +- `def shufflish(x, pct)` + Randomly relocate items of `x` up to `pct` of `len(x)` from their starting location + +- `def console_help(libname)` + Show help for all console scripts from `libname` + +- `def hl_md(s, lang, show)` + Syntax highlight `s` using `lang`. + +- `def type2str(typ)` + Stringify `typ` + +- `class Unset` + - `def __repr__(self)` + - `def __str__(self)` + - `def __bool__(self)` + - `@property def name(self)` + +- `def nullable_dc(cls)` + Like `dataclass`, but default of `UNSET` added to fields without defaults + +- `def flexiclass(cls)` + Convert `cls` into a `dataclass` like `make_nullable`. Converts in place and also returns the result. + +- `def asdict(o)` + Convert `o` to a `dict`, supporting dataclasses, namedtuples, iterables, and `__dict__` attrs. + +- `def is_typeddict(cls)` + Check if `cls` is a `TypedDict` + +- `def is_namedtuple(cls)` + `True` if `cls` is a namedtuple type + +- `def flexicache(*funcs)` + Like `lru_cache`, but customisable with policy `funcs` + +- `def time_policy(seconds)` + A `flexicache` policy that expires cached items after `seconds` have passed + +- `def mtime_policy(filepath)` + A `flexicache` policy that expires cached items after `filepath` modified-time changes + +- `def timed_cache(seconds, maxsize)` + Like `lru_cache`, but also with time-based eviction + diff --git a/basics.html b/basics.html new file mode 100644 index 00000000..9370bc0c --- /dev/null +++ b/basics.html @@ -0,0 +1,3644 @@ + + + + + + + + + + +Basic functionality – fastcore + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ +
+ + +
+ + + +
+ +
+
+

Basic functionality

+
+ +
+
+ Basic functionality used in the fastai library +
+
+ + +
+ + + + +
+ + + +
+ + + +
+

Basics

+
+

source

+
+

ifnone

+
+
 ifnone (a, b)
+
+

b if a is None else a

+

Since b if a is None else a is such a common pattern, we wrap it in a function. However, be careful, because python will evaluate both a and b when calling ifnone (which it doesn’t do if using the if version directly).

+
+
test_eq(ifnone(None,1), 1)
+test_eq(ifnone(2   ,1), 2)
+
+
+

source

+
+
+

maybe_attr

+
+
 maybe_attr (o, attr)
+
+

getattr(o,attr,o)

+

Return the attribute attr for object o. If the attribute doesn’t exist, then return the object o instead.

+
+
class myobj: myattr='foo'
+
+test_eq(maybe_attr(myobj, 'myattr'), 'foo')
+test_eq(maybe_attr(myobj, 'another_attr'), myobj)
+
+
+

source

+
+
+

basic_repr

+
+
 basic_repr (flds=None)
+
+

Minimal __repr__

+

In types which provide rich display functionality in Jupyter, their __repr__ is also called in order to provide a fallback text representation. Unfortunately, this includes a memory address which changes on every invocation, making it non-deterministic. This causes diffs to get messy and creates conflicts in git. To fix this, put __repr__=basic_repr() inside your class.

+
+
class SomeClass: __repr__=basic_repr()
+repr(SomeClass())
+
+
'SomeClass()'
+
+
+

If you pass a list of attributes (flds) of an object, then this will generate a string with the name of each attribute and its corresponding value. The format of this string is key=value, where key is the name of the attribute, and value is the value of the attribute. For each value, attempt to use the __name__ attribute, otherwise fall back to using the value’s __repr__ when constructing the string.

+
+
class SomeClass:
+    a=1
+    b='foo'
+    __repr__=basic_repr('a,b')
+    __name__='some-class'
+
+repr(SomeClass())
+
+
"SomeClass(a=1, b='foo')"
+
+
+

Nested objects work too:

+
+
class AnotherClass:
+    c=SomeClass()
+    d='bar'
+    __repr__=basic_repr(['c', 'd'])
+
+repr(AnotherClass())
+
+
"AnotherClass(c=SomeClass(a=1, b='foo'), d='bar')"
+
+
+

Instance variables (but not class variables) are shown if basic_repr is called with no arguments:

+
+
class SomeClass:
+    def __init__(self, a=1, b='foo'): self.a,self.b = a,b
+    __repr__=basic_repr()
+
+repr(SomeClass())
+
+
"SomeClass(a=1, b='foo')"
+
+
+
+

source

+
+
+

BasicRepr

+
+
 BasicRepr ()
+
+

Base class for objects needing a basic __repr__

+

As a shortcut for creating a __repr__ for instance variables, you can inherit from BasicRepr:

+
+
class SomeClass(BasicRepr):
+    def __init__(self, a=1, b='foo'): self.a,self.b = a,b
+
+repr(SomeClass())
+
+
"SomeClass(a=1, b='foo')"
+
+
+
+

source

+
+
+

is_array

+
+
 is_array (x)
+
+

True if x supports __array__ or iloc

+
+
is_array(np.array(1)),is_array([1])
+
+
(True, False)
+
+
+
+

source

+
+
+

listify

+
+
 listify (o=None, *rest, use_list=False, match=None)
+
+

Convert o to a list

+

Conversion is designed to “do what you mean”, e.g:

+
+
test_eq(listify('hi'), ['hi'])
+test_eq(listify(b'hi'), [b'hi'])
+test_eq(listify(array(1)), [array(1)])
+test_eq(listify(1), [1])
+test_eq(listify([1,2]), [1,2])
+test_eq(listify(range(3)), [0,1,2])
+test_eq(listify(None), [])
+test_eq(listify(1,2), [1,2])
+
+
+
arr = np.arange(9).reshape(3,3)
+listify(arr)
+
+
[array([[0, 1, 2],
+        [3, 4, 5],
+        [6, 7, 8]])]
+
+
+
+
listify(array([1,2]))
+
+
[array([1, 2])]
+
+
+

Generators are turned into lists too:

+
+
gen = (o for o in range(3))
+test_eq(listify(gen), [0,1,2])
+
+

Use match to provide a length to match:

+
+
test_eq(listify(1,match=3), [1,1,1])
+
+

If match is a sequence, it’s length is used:

+
+
test_eq(listify(1,match=range(3)), [1,1,1])
+
+

If the listified item is not of length 1, it must be the same length as match:

+
+
test_eq(listify([1,1,1],match=3), [1,1,1])
+test_fail(lambda: listify([1,1],match=3))
+
+
+

source

+
+
+

tuplify

+
+
 tuplify (o, use_list=False, match=None)
+
+

Make o a tuple

+
+
test_eq(tuplify(None),())
+test_eq(tuplify([1,2,3]),(1,2,3))
+test_eq(tuplify(1,match=[1,2,3]),(1,1,1))
+
+
+

source

+
+
+

true

+
+
 true (x)
+
+

Test whether x is truthy; collections with >0 elements are considered True

+
+
[(o,true(o)) for o in
+ (array(0),array(1),array([0]),array([0,1]),1,0,'',None)]
+
+
[(array(0), False),
+ (array(1), True),
+ (array([0]), True),
+ (array([0, 1]), True),
+ (1, True),
+ (0, False),
+ ('', False),
+ (None, False)]
+
+
+
+

source

+
+
+

NullType

+
+
 NullType ()
+
+

An object that is False and can be called, chained, and indexed

+
+
bool(null.hi().there[3])
+
+
False
+
+
+
+

source

+
+
+

tonull

+
+
 tonull (x)
+
+

Convert None to null

+
+
bool(tonull(None).hi().there[3])
+
+
False
+
+
+
+

source

+
+
+

get_class

+
+
 get_class (nm, *fld_names, sup=None, doc=None, funcs=None, anno=None,
+            **flds)
+
+

Dynamically create a class, optionally inheriting from sup, containing fld_names

+
+
_t = get_class('_t', 'a', b=2, anno={'b':int})
+t = _t()
+test_eq(t.a, None)
+test_eq(t.b, 2)
+t = _t(1, b=3)
+test_eq(t.a, 1)
+test_eq(t.b, 3)
+t = _t(1, 3)
+test_eq(t.a, 1)
+test_eq(t.b, 3)
+test_eq(t, pickle.loads(pickle.dumps(t)))
+test_eq(_t.__annotations__, {'b':int, 'a':typing.Any})
+repr(t)
+
+
'__main__._t(a=1, b=3)'
+
+
+

Most often you’ll want to call mk_class, since it adds the class to your module. See mk_class for more details and examples of use (which also apply to get_class).

+
+

source

+
+
+

mk_class

+
+
 mk_class (nm, *fld_names, sup=None, doc=None, funcs=None, mod=None,
+           anno=None, **flds)
+
+

Create a class using get_class and add to the caller’s module

+

Any kwargs will be added as class attributes, and sup is an optional (tuple of) base classes.

+
+
mk_class('_t', a=1, sup=dict)
+t = _t()
+test_eq(t.a, 1)
+assert(isinstance(t,dict))
+
+

A __init__ is provided that sets attrs for any kwargs, and for any args (matching by position to fields), along with a __repr__ which prints all attrs. The docstring is set to doc. You can pass funcs which will be added as attrs with the function names.

+
+
def foo(self): return 1
+mk_class('_t', 'a', sup=dict, doc='test doc', funcs=foo)
+
+t = _t(3, b=2)
+test_eq(t.a, 3)
+test_eq(t.b, 2)
+test_eq(t.foo(), 1)
+test_eq(t.__doc__, 'test doc')
+t
+
+
{}
+
+
+
+

source

+
+
+

wrap_class

+
+
 wrap_class (nm, *fld_names, sup=None, doc=None, funcs=None, **flds)
+
+

Decorator: makes function a method of a new class nm passing parameters to mk_class

+
+
@wrap_class('_t', a=2)
+def bar(self,x): return x+1
+
+t = _t()
+test_eq(t.a, 2)
+test_eq(t.bar(3), 4)
+
+
+

source

+
+

ignore_exceptions

+
+
 ignore_exceptions ()
+
+

Context manager to ignore exceptions

+
+
with ignore_exceptions(): 
+    # Exception will be ignored
+    raise Exception
+
+
+

source

+
+
+
+

exec_local

+
+
 exec_local (code, var_name)
+
+

Call exec on code and return the var var_name

+
+
test_eq(exec_local("a=1", "a"), 1)
+
+
+

source

+
+
+

risinstance

+
+
 risinstance (types, obj=None)
+
+

Curried isinstance but with args reversed

+
+
assert risinstance(int, 1)
+assert not risinstance(str, 0)
+assert risinstance(int)(1)
+assert not risinstance(int)(None)
+
+

types can also be strings:

+
+
assert risinstance(('str','int'), 'a')
+assert risinstance('str', 'a')
+assert not risinstance('int', 'a')
+
+
+

source

+
+
+

ver2tuple

+
+
 ver2tuple (v:str)
+
+
+
test_eq(ver2tuple('3.8.1'), (3,8,1))
+test_eq(ver2tuple('3.1'), (3,1,0))
+test_eq(ver2tuple('3.'), (3,0,0))
+test_eq(ver2tuple('3'), (3,0,0))
+
+
+
+
+

NoOp

+

These are used when you need a pass-through function.

+
+
+

noop

+
+
 noop (x=None, *args, **kwargs)
+
+

Do nothing

+
+
noop()
+test_eq(noop(1),1)
+
+
+
+
+

noops

+
+
 noops (x=None, *args, **kwargs)
+
+

Do nothing (method)

+
+
class _t: foo=noops
+test_eq(_t().foo(1),1)
+
+
+
+
+

Infinite Lists

+

These lists are useful for things like padding an array or adding index column(s) to arrays.

+

Inf defines the following properties:

+
    +
  • count: itertools.count()
  • +
  • zeros: itertools.cycle([0])
  • +
  • ones : itertools.cycle([1])
  • +
  • nones: itertools.cycle([None])
  • +
+
+
test_eq([o for i,o in zip(range(5), Inf.count)],
+        [0, 1, 2, 3, 4])
+
+test_eq([o for i,o in zip(range(5), Inf.zeros)],
+        [0]*5)
+
+test_eq([o for i,o in zip(range(5), Inf.ones)],
+        [1]*5)
+
+test_eq([o for i,o in zip(range(5), Inf.nones)],
+        [None]*5)
+
+
+
+

Operator Functions

+
+

source

+
+

in_

+
+
 in_ (x, a)
+
+

True if x in a

+
+
# test if element is in another
+assert in_('c', ('b', 'c', 'a'))
+assert in_(4, [2,3,4,5])
+assert in_('t', 'fastai')
+test_fail(in_('h', 'fastai'))
+
+# use in_ as a partial
+assert in_('fastai')('t')
+assert in_([2,3,4,5])(4)
+test_fail(in_('fastai')('h'))
+
+

In addition to in_, the following functions are provided matching the behavior of the equivalent versions in operator: lt gt le ge eq ne add sub mul truediv is_ is_not mod.

+
+
lt(3,5),gt(3,5),is_(None,None),in_(0,[1,2]),mod(3,2)
+
+
(True, False, True, False, 1)
+
+
+

Similarly to _in, they also have additional functionality: if you only pass one param, they return a partial function that passes that param as the second positional parameter.

+
+
lt(5)(3),gt(5)(3),is_(None)(None),in_([1,2])(0),mod(2)(3)
+
+
(True, False, True, False, 1)
+
+
+
+

source

+
+
+

ret_true

+
+
 ret_true (*args, **kwargs)
+
+

Predicate: always True

+
+
assert ret_true(1,2,3)
+assert ret_true(False)
+
+
+

source

+
+
+

ret_false

+
+
 ret_false (*args, **kwargs)
+
+

Predicate: always False

+
+

source

+
+
+

stop

+
+
 stop (e=<class 'StopIteration'>)
+
+

Raises exception e (by default StopIteration)

+
+

source

+
+
+

gen

+
+
 gen (func, seq, cond=<function ret_true>)
+
+

Like (func(o) for o in seq if cond(func(o))) but handles StopIteration

+
+
test_eq(gen(noop, Inf.count, lt(5)),
+        range(5))
+test_eq(gen(operator.neg, Inf.count, gt(-5)),
+        [0,-1,-2,-3,-4])
+test_eq(gen(lambda o:o if o<5 else stop(), Inf.count),
+        range(5))
+
+
+

source

+
+
+

chunked

+
+
 chunked (it, chunk_sz=None, drop_last=False, n_chunks=None)
+
+

Return batches from iterator it of size chunk_sz (or return n_chunks total)

+

Note that you must pass either chunk_sz, or n_chunks, but not both.

+
+
t = list(range(10))
+test_eq(chunked(t,3),      [[0,1,2], [3,4,5], [6,7,8], [9]])
+test_eq(chunked(t,3,True), [[0,1,2], [3,4,5], [6,7,8],    ])
+
+t = map(lambda o:stop() if o==6 else o, Inf.count)
+test_eq(chunked(t,3), [[0, 1, 2], [3, 4, 5]])
+t = map(lambda o:stop() if o==7 else o, Inf.count)
+test_eq(chunked(t,3), [[0, 1, 2], [3, 4, 5], [6]])
+
+t = np.arange(10)
+test_eq(chunked(t,3),      [[0,1,2], [3,4,5], [6,7,8], [9]])
+test_eq(chunked(t,3,True), [[0,1,2], [3,4,5], [6,7,8],    ])
+
+test_eq(chunked([], 3),          [])
+test_eq(chunked([], n_chunks=3), [])
+
+
+

source

+
+
+

otherwise

+
+
 otherwise (x, tst, y)
+
+

y if tst(x) else x

+
+
test_eq(otherwise(2+1, gt(3), 4), 3)
+test_eq(otherwise(2+1, gt(2), 4), 4)
+
+
+
+
+

Attribute Helpers

+

These functions reduce boilerplate when setting or manipulating attributes or properties of objects.

+
+

source

+
+

custom_dir

+
+
 custom_dir (c, add)
+
+

Implement custom __dir__, adding add to cls

+

custom_dir allows you extract the __dict__ property of a class and appends the list add to it.

+
+
class _T: 
+    def f(): pass
+
+s = custom_dir(_T(), add=['foo', 'bar'])
+assert {'foo', 'bar', 'f'}.issubset(s)
+
+
+

source

+
+
+

AttrDict

+

dict subclass that also provides access to keys as attrs

+
+
d = AttrDict(a=1,b="two")
+test_eq(d.a, 1)
+test_eq(d['b'], 'two')
+test_eq(d.get('c','nope'), 'nope')
+d.b = 2
+test_eq(d.b, 2)
+test_eq(d['b'], 2)
+d['b'] = 3
+test_eq(d['b'], 3)
+test_eq(d.b, 3)
+assert 'a' in dir(d)
+
+

AttrDict will pretty print in Jupyter Notebooks:

+
+
_test_dict = {'a':1, 'b': {'c':1, 'd':2}, 'c': {'c':1, 'd':2}, 'd': {'c':1, 'd':2},
+              'e': {'c':1, 'd':2}, 'f': {'c':1, 'd':2, 'e': 4, 'f':[1,2,3,4,5]}}
+AttrDict(_test_dict)
+
+
{ 'a': 1,
+  'b': {'c': 1, 'd': 2},
+  'c': {'c': 1, 'd': 2},
+  'd': {'c': 1, 'd': 2},
+  'e': {'c': 1, 'd': 2},
+  'f': {'c': 1, 'd': 2, 'e': 4, 'f': [1, 2, 3, 4, 5]}}
+
+
+
+

source

+
+
+

AttrDictDefault

+
+
 AttrDictDefault (*args, default_=None, **kwargs)
+
+

AttrDict subclass that returns None for missing attrs

+
+
d = AttrDictDefault(a=1,b="two", default_='nope')
+test_eq(d.a, 1)
+test_eq(d['b'], 'two')
+test_eq(d.c, 'nope')
+
+
+

source

+
+
+

NS

+

SimpleNamespace subclass that also adds iter and dict support

+

This is very similar to AttrDict, but since it starts with SimpleNamespace, it has some differences in behavior. You can use it just like SimpleNamespace:

+
+
d = NS(**_test_dict)
+d
+
+
namespace(a=1,
+          b={'c': 1, 'd': 2},
+          c={'c': 1, 'd': 2},
+          d={'c': 1, 'd': 2},
+          e={'c': 1, 'd': 2},
+          f={'c': 1, 'd': 2, 'e': 4, 'f': [1, 2, 3, 4, 5]})
+
+
+

…but you can also index it to get/set:

+
+
d['a']
+
+
1
+
+
+

…and iterate t:

+
+
list(d)
+
+
['a', 'b', 'c', 'd', 'e', 'f']
+
+
+
+

source

+
+
+

get_annotations_ex

+
+
 get_annotations_ex (obj, globals=None, locals=None)
+
+

Backport of py3.10 get_annotations that returns globals/locals

+

In Python 3.10 inspect.get_annotations was added. However previous versions of Python are unable to evaluate type annotations correctly if from future import __annotations__ is used. Furthermore, all annotations are evaluated, even if only some subset are needed. get_annotations_ex provides the same functionality as inspect.get_annotations, but works on earlier versions of Python, and returns the globals and locals needed to evaluate types.

+
+

source

+
+
+

eval_type

+
+
 eval_type (t, glb, loc)
+
+

eval a type or collection of types, if needed, for annotations in py3.10+

+

In py3.10, or if from future import __annotations__ is used, a is a str:

+
+
class _T2a: pass
+def func(a: _T2a): pass
+ann,glb,loc = get_annotations_ex(func)
+
+eval_type(ann['a'], glb, loc)
+
+
__main__._T2a
+
+
+

| is supported for defining Union types when using eval_type even for python versions prior to 3.9:

+
+
class _T2b: pass
+def func(a: _T2a|_T2b): pass
+ann,glb,loc = get_annotations_ex(func)
+
+eval_type(ann['a'], glb, loc)
+
+
typing.Union[__main__._T2a, __main__._T2b]
+
+
+
+

source

+
+
+

type_hints

+
+
 type_hints (f)
+
+

Like typing.get_type_hints but returns {} if not allowed type

+

Below is a list of allowed types for type hints in python:

+
+
list(typing._allowed_types)
+
+
[function,
+ builtin_function_or_method,
+ method,
+ module,
+ wrapper_descriptor,
+ method-wrapper,
+ method_descriptor]
+
+
+

For example, type func is allowed so type_hints returns the same value as typing.get_hints:

+
+
def f(a:int)->bool: ... # a function with type hints (allowed)
+exp = {'a':int,'return':bool}
+test_eq(type_hints(f), typing.get_type_hints(f))
+test_eq(type_hints(f), exp)
+
+

However, class is not an allowed type, so type_hints returns {}:

+
+
class _T:
+    def __init__(self, a:int=0)->bool: ...
+assert not type_hints(_T)
+
+
+

source

+
+
+

annotations

+
+
 annotations (o)
+
+

Annotations for o, or type(o)

+

This supports a wider range of situations than type_hints, by checking type() and __init__ for annotations too:

+
+
for o in _T,_T(),_T.__init__,f: test_eq(annotations(o), exp)
+assert not annotations(int)
+assert not annotations(print)
+
+
+

source

+
+
+

anno_ret

+
+
 anno_ret (func)
+
+

Get the return annotation of func

+
+
def f(x) -> float: return x
+test_eq(anno_ret(f), float)
+
+def f(x) -> typing.Tuple[float,float]: return x
+assert anno_ret(f)==typing.Tuple[float,float]
+
+

If your return annotation is None, anno_ret will return NoneType (and not None):

+
+
def f(x) -> None: return x
+
+test_eq(anno_ret(f), NoneType)
+assert anno_ret(f) is not None # returns NoneType instead of None
+
+

If your function does not have a return type, or if you pass in None instead of a function, then anno_ret returns None:

+
+
def f(x): return x
+
+test_eq(anno_ret(f), None)
+test_eq(anno_ret(None), None) # instead of passing in a func, pass in None
+
+
+

source

+
+
+

signature_ex

+
+
 signature_ex (obj, eval_str:bool=False)
+
+

Backport of inspect.signature(..., eval_str=True to <py310

+
+

source

+
+
+

union2tuple

+
+
 union2tuple (t)
+
+
+
test_eq(union2tuple(Union[int,str]), (int,str))
+test_eq(union2tuple(int), int)
+assert union2tuple(Tuple[int,str])==Tuple[int,str]
+test_eq(union2tuple((int,str)), (int,str))
+if UnionType: test_eq(union2tuple(int|str), (int,str))
+
+
+

source

+
+
+

argnames

+
+
 argnames (f, frame=False)
+
+

Names of arguments to function or frame f

+
+
test_eq(argnames(f), ['x'])
+
+
+

source

+
+
+

with_cast

+
+
 with_cast (f)
+
+

Decorator which uses any parameter annotations as preprocessing functions

+
+
@with_cast
+def _f(a, b:Path, c:str='', d=0): return (a,b,c,d)
+
+test_eq(_f(1, '.', 3), (1,Path('.'),'3',0))
+test_eq(_f(1, '.'), (1,Path('.'),'',0))
+
+@with_cast
+def _g(a:int=0)->str: return a
+
+test_eq(_g(4.0), '4')
+test_eq(_g(4.4), '4')
+test_eq(_g(2), '2')
+
+
+

source

+
+
+

store_attr

+
+
 store_attr (names=None, but='', cast=False, store_args=None, **attrs)
+
+

Store params named in comma-separated names from calling context into attrs in self

+

In it’s most basic form, you can use store_attr to shorten code like this:

+
+
class T:
+    def __init__(self, a,b,c): self.a,self.b,self.c = a,b,c
+
+

…to this:

+
+
class T:
+    def __init__(self, a,b,c): store_attr('a,b,c', self)
+
+

This class behaves as if we’d used the first form:

+
+
t = T(1,c=2,b=3)
+assert t.a==1 and t.b==3 and t.c==2
+
+
+
class T1:
+    def __init__(self, a,b,c): store_attr()
+
+

In addition, it stores the attrs as a dict in __stored_args__, which you can use for display, logging, and so forth.

+
+
test_eq(t.__stored_args__, {'a':1, 'b':3, 'c':2})
+
+

Since you normally want to use the first argument (often called self) for storing attributes, it’s optional:

+
+
class T:
+    def __init__(self, a,b,c:str): store_attr('a,b,c')
+
+t = T(1,c=2,b=3)
+assert t.a==1 and t.b==3 and t.c==2
+
+

With cast=True any parameter annotations will be used as preprocessing functions for the corresponding arguments:

+
+
class T:
+    def __init__(self, a:listify, b, c:str): store_attr('a,b,c', cast=True)
+
+t = T(1,c=2,b=3)
+assert t.a==[1] and t.b==3 and t.c=='2'
+
+

You can inherit from a class using store_attr, and just call it again to add in any new attributes added in the derived class:

+
+
class T2(T):
+    def __init__(self, d, **kwargs):
+        super().__init__(**kwargs)
+        store_attr('d')
+
+t = T2(d=1,a=2,b=3,c=4)
+assert t.a==2 and t.b==3 and t.c==4 and t.d==1
+
+

You can skip passing a list of attrs to store. In this case, all arguments passed to the method are stored:

+
+
class T:
+    def __init__(self, a,b,c): store_attr()
+
+t = T(1,c=2,b=3)
+assert t.a==1 and t.b==3 and t.c==2
+
+
+
class T4(T):
+    def __init__(self, d, **kwargs):
+        super().__init__(**kwargs)
+        store_attr()
+
+t = T4(4, a=1,c=2,b=3)
+assert t.a==1 and t.b==3 and t.c==2 and t.d==4
+
+
+
class T4:
+    def __init__(self, *, a: int, b: float = 1):
+        store_attr()
+        
+t = T4(a=3)
+assert t.a==3 and t.b==1
+t = T4(a=3, b=2)
+assert t.a==3 and t.b==2
+
+

You can skip some attrs by passing but:

+
+
class T:
+    def __init__(self, a,b,c): store_attr(but='a')
+
+t = T(1,c=2,b=3)
+assert t.b==3 and t.c==2
+assert not hasattr(t,'a')
+
+

You can also pass keywords to store_attr, which is identical to setting the attrs directly, but also stores them in __stored_args__.

+
+
class T:
+    def __init__(self): store_attr(a=1)
+
+t = T()
+assert t.a==1
+
+

You can also use store_attr inside functions.

+
+
def create_T(a, b):
+    t = SimpleNamespace()
+    store_attr(self=t)
+    return t
+
+t = create_T(a=1, b=2)
+assert t.a==1 and t.b==2
+
+
+

source

+
+
+

attrdict

+
+
 attrdict (o, *ks, default=None)
+
+

Dict from each k in ks to getattr(o,k)

+
+
class T:
+    def __init__(self, a,b,c): store_attr()
+
+t = T(1,c=2,b=3)
+test_eq(attrdict(t,'b','c'), {'b':3, 'c':2})
+
+
+

source

+
+
+

properties

+
+
 properties (cls, *ps)
+
+

Change attrs in cls with names in ps to properties

+
+
class T:
+    def a(self): return 1
+    def b(self): return 2
+properties(T,'a')
+
+test_eq(T().a,1)
+test_eq(T().b(),2)
+
+
+

source

+
+
+

camel2words

+
+
 camel2words (s, space=' ')
+
+

Convert CamelCase to ‘spaced words’

+
+
test_eq(camel2words('ClassAreCamel'), 'Class Are Camel')
+
+
+

source

+
+
+

camel2snake

+
+
 camel2snake (name)
+
+

Convert CamelCase to snake_case

+
+
test_eq(camel2snake('ClassAreCamel'), 'class_are_camel')
+test_eq(camel2snake('Already_Snake'), 'already__snake')
+
+
+

source

+
+
+

snake2camel

+
+
 snake2camel (s)
+
+

Convert snake_case to CamelCase

+
+
test_eq(snake2camel('a_b_cc'), 'ABCc')
+
+
+

source

+
+
+

class2attr

+
+
 class2attr (cls_name)
+
+

Return the snake-cased name of the class; strip ending cls_name if it exists.

+
+
class Parent:
+    @property
+    def name(self): return class2attr(self, 'Parent')
+
+class ChildOfParent(Parent): pass
+class ParentChildOf(Parent): pass
+
+p = Parent()
+cp = ChildOfParent()
+cp2 = ParentChildOf()
+
+test_eq(p.name, 'parent')
+test_eq(cp.name, 'child_of')
+test_eq(cp2.name, 'parent_child_of')
+
+
+

source

+
+
+

getcallable

+
+
 getcallable (o, attr)
+
+

Calls getattr with a default of noop

+
+
class Math:
+    def addition(self,a,b): return a+b
+
+m = Math()
+
+test_eq(getcallable(m, "addition")(a=1,b=2), 3)
+test_eq(getcallable(m, "subtraction")(a=1,b=2), None)
+
+
+

source

+
+
+

getattrs

+
+
 getattrs (o, *attrs, default=None)
+
+

List of all attrs in o

+
+
from fractions import Fraction
+
+
+
getattrs(Fraction(1,2), 'numerator', 'denominator')
+
+
[1, 2]
+
+
+
+

source

+
+
+

hasattrs

+
+
 hasattrs (o, attrs)
+
+

Test whether o contains all attrs

+
+
assert hasattrs(1,('imag','real'))
+assert not hasattrs(1,('imag','foo'))
+
+
+

source

+
+
+

setattrs

+
+
 setattrs (dest, flds, src)
+
+
+
d = dict(a=1,bb="2",ignore=3)
+o = SimpleNamespace()
+setattrs(o, "a,bb", d)
+test_eq(o.a, 1)
+test_eq(o.bb, "2")
+
+
+
d = SimpleNamespace(a=1,bb="2",ignore=3)
+o = SimpleNamespace()
+setattrs(o, "a,bb", d)
+test_eq(o.a, 1)
+test_eq(o.bb, "2")
+
+
+

source

+
+
+

try_attrs

+
+
 try_attrs (obj, *attrs)
+
+

Return first attr that exists in obj

+
+
test_eq(try_attrs(1, 'real'), 1)
+test_eq(try_attrs(1, 'foobar', 'real'), 1)
+
+
+
+
+

Attribute Delegation

+
+

source

+
+

GetAttrBase

+
+
 GetAttrBase ()
+
+

Basic delegation of __getattr__ and __dir__

+
+

source

+
+

GetAttr

+
+
 GetAttr ()
+
+

Inherit from this to have all attr accesses in self._xtra passed down to self.default

+

Inherit from GetAttr to have attr access passed down to an instance attribute. This makes it easy to create composites that don’t require callers to know about their components. For a more detailed discussion of how this works as well as relevant context, we suggest reading the delegated composition section of this blog article.

+

You can customise the behaviour of GetAttr in subclasses via; - _default - By default, this is set to 'default', so attr access is passed down to self.default - _default can be set to the name of any instance attribute that does not start with dunder __ - _xtra - By default, this is None, so all attr access is passed down - You can limit which attrs get passed down by setting _xtra to a list of attribute names

+

To illuminate the utility of GetAttr, suppose we have the following two classes, _WebPage which is a superclass of _ProductPage, which we wish to compose like so:

+
+
class _WebPage:
+    def __init__(self, title, author="Jeremy"):
+        self.title,self.author = title,author
+
+class _ProductPage:
+    def __init__(self, page, price): self.page,self.price = page,price
+        
+page = _WebPage('Soap', author="Sylvain")
+p = _ProductPage(page, 15.0)
+
+

How do we make it so we can just write p.author, instead of p.page.author to access the author attribute? We can use GetAttr, of course! First, we subclass GetAttr when defining _ProductPage. Next, we set self.default to the object whose attributes we want to be able to access directly, which in this case is the page argument passed on initialization:

+
+
class _ProductPage(GetAttr):
+    def __init__(self, page, price): self.default,self.price = page,price #self.default allows you to access page directly.
+
+p = _ProductPage(page, 15.0)
+
+

Now, we can access the author attribute directly from the instance:

+
+
test_eq(p.author, 'Sylvain')
+
+

If you wish to store the object you are composing in an attribute other than self.default, you can set the class attribute _data as shown below. This is useful in the case where you might have a name collision with self.default:

+
+
class _C(GetAttr):
+    _default = '_data' # use different component name; `self._data` rather than `self.default`
+    def __init__(self,a): self._data = a
+    def foo(self): noop
+
+t = _C('Hi')
+test_eq(t._data, 'Hi') 
+test_fail(lambda: t.default) # we no longer have self.default
+test_eq(t.lower(), 'hi')
+test_eq(t.upper(), 'HI')
+assert 'lower' in dir(t)
+assert 'upper' in dir(t)
+
+

By default, all attributes and methods of the object you are composing are retained. In the below example, we compose a str object with the class _C. This allows us to directly call string methods on instances of class _C, such as str.lower() or str.upper():

+
+
class _C(GetAttr):
+    # allow all attributes and methods to get passed to `self.default` (by leaving _xtra=None)
+    def __init__(self,a): self.default = a
+    def foo(self): noop
+
+t = _C('Hi')
+test_eq(t.lower(), 'hi')
+test_eq(t.upper(), 'HI')
+assert 'lower' in dir(t)
+assert 'upper' in dir(t)
+
+

However, you can choose which attributes or methods to retain by defining a class attribute _xtra, which is a list of allowed attribute and method names to delegate. In the below example, we only delegate the lower method from the composed str object when defining class _C:

+
+
class _C(GetAttr):
+    _xtra = ['lower'] # specify which attributes get passed to `self.default`
+    def __init__(self,a): self.default = a
+    def foo(self): noop
+
+t = _C('Hi')
+test_eq(t.default, 'Hi')
+test_eq(t.lower(), 'hi')
+test_fail(lambda: t.upper()) # upper wasn't in _xtra, so it isn't available to be called
+assert 'lower' in dir(t)
+assert 'upper' not in dir(t)
+
+

You must be careful to properly set an instance attribute in __init__ that corresponds to the class attribute _default. The below example sets the class attribute _default to data, but erroneously fails to define self.data (and instead defines self.default).

+

Failing to properly set instance attributes leads to errors when you try to access methods directly:

+
+
class _C(GetAttr):
+    _default = 'data' # use a bad component name; i.e. self.data does not exist
+    def __init__(self,a): self.default = a
+    def foo(self): noop
+        
+# TODO: should we raise an error when we create a new instance ...
+t = _C('Hi')
+test_eq(t.default, 'Hi')
+# ... or is it enough for all GetAttr features to raise errors
+test_fail(lambda: t.data)
+test_fail(lambda: t.lower())
+test_fail(lambda: t.upper())
+test_fail(lambda: dir(t))
+
+
+

source

+
+
+
+

delegate_attr

+
+
 delegate_attr (k, to)
+
+

Use in __getattr__ to delegate to attr to without inheriting from GetAttr

+

delegate_attr is a functional way to delegate attributes, and is an alternative to GetAttr. We recommend reading the documentation of GetAttr for more details around delegation.

+

You can use achieve delegation when you define __getattr__ by using delegate_attr:

+
+
class _C:
+    def __init__(self, o): self.o = o # self.o corresponds to the `to` argument in delegate_attr.
+    def __getattr__(self, k): return delegate_attr(self, k, to='o')
+    
+
+t = _C('HELLO') # delegates to a string
+test_eq(t.lower(), 'hello')
+
+t = _C(np.array([5,4,3])) # delegates to a numpy array
+test_eq(t.sum(), 12)
+
+t = _C(pd.DataFrame({'a': [1,2], 'b': [3,4]})) # delegates to a pandas.DataFrame
+test_eq(t.b.max(), 4)
+
+
+
+
+

Extensible Types

+

ShowPrint is a base class that defines a show method, which is used primarily for callbacks in fastai that expect this method to be defined.

+

Int, Float, and Str extend int, float and str respectively by adding an additional show method by inheriting from ShowPrint.

+

The code for Int is shown below:

+

Examples:

+
+
Int(0).show()
+Float(2.0).show()
+Str('Hello').show()
+
+
0
+2.0
+Hello
+
+
+
+
+

Collection functions

+

Functions that manipulate popular python collections.

+
+

source

+
+

partition

+
+
 partition (coll, f)
+
+

Partition a collection by a predicate

+
+
ts,fs = partition(range(10), mod(2))
+test_eq(fs, [0,2,4,6,8])
+test_eq(ts, [1,3,5,7,9])
+
+
+

source

+
+
+

flatten

+
+
 flatten (o)
+
+

Concatenate all collections and items as a generator

+
+

source

+
+
+

concat

+
+
 concat (colls)
+
+

Concatenate all collections and items as a list

+
+
concat([(o for o in range(2)),[2,3,4], 5])
+
+
[0, 1, 2, 3, 4, 5]
+
+
+
+
concat([["abc", "xyz"], ["foo", "bar"]])
+
+
['abc', 'xyz', 'foo', 'bar']
+
+
+
+

source

+
+
+

strcat

+
+
 strcat (its, sep:str='')
+
+

Concatenate stringified items its

+
+
test_eq(strcat(['a',2]), 'a2')
+test_eq(strcat(['a',2], ';'), 'a;2')
+
+
+

source

+
+
+

detuplify

+
+
 detuplify (x)
+
+

If x is a tuple with one thing, extract it

+
+
test_eq(detuplify(()),None)
+test_eq(detuplify([1]),1)
+test_eq(detuplify([1,2]), [1,2])
+test_eq(detuplify(np.array([[1,2]])), np.array([[1,2]]))
+
+
+

source

+
+
+

replicate

+
+
 replicate (item, match)
+
+

Create tuple of item copied len(match) times

+
+
t = [1,1]
+test_eq(replicate([1,2], t),([1,2],[1,2]))
+test_eq(replicate(1, t),(1,1))
+
+
+

source

+
+
+

setify

+
+
 setify (o)
+
+

Turn any list like-object into a set.

+
+
# test
+test_eq(setify(None),set())
+test_eq(setify('abc'),{'abc'})
+test_eq(setify([1,2,2]),{1,2})
+test_eq(setify(range(0,3)),{0,1,2})
+test_eq(setify({1,2}),{1,2})
+
+
+

source

+
+
+

merge

+
+
 merge (*ds)
+
+

Merge all dictionaries in ds

+
+
test_eq(merge(), {})
+test_eq(merge(dict(a=1,b=2)), dict(a=1,b=2))
+test_eq(merge(dict(a=1,b=2), dict(b=3,c=4), None), dict(a=1, b=3, c=4))
+
+
+

source

+
+
+

range_of

+
+
 range_of (x)
+
+

All indices of collection x (i.e. list(range(len(x))))

+
+
test_eq(range_of([1,1,1,1]), [0,1,2,3])
+
+
+

source

+
+
+

groupby

+
+
 groupby (x, key, val=<function noop>)
+
+

Like itertools.groupby but doesn’t need to be sorted, and isn’t lazy, plus some extensions

+
+
test_eq(groupby('aa ab bb'.split(), itemgetter(0)), {'a':['aa','ab'], 'b':['bb']})
+
+

Here’s an example of how to invert a grouping, using an int as key (which uses itemgetter; passing a str will use attrgetter), and using a val function:

+
+
d = {0: [1, 3, 7], 2: [3], 3: [5], 4: [8], 5: [4], 7: [5]}
+groupby(((o,k) for k,v in d.items() for o in v), 0, 1)
+
+
{1: [0], 3: [0, 2], 7: [0], 5: [3, 7], 8: [4], 4: [5]}
+
+
+
+

source

+
+
+

last_index

+
+
 last_index (x, o)
+
+

Finds the last index of occurence of x in o (returns -1 if no occurence)

+
+
test_eq(last_index(9, [1, 2, 9, 3, 4, 9, 10]), 5)
+test_eq(last_index(6, [1, 2, 9, 3, 4, 9, 10]), -1)
+
+
+

source

+
+
+

filter_dict

+
+
 filter_dict (d, func)
+
+

Filter a dict using func, applied to keys and values

+
+
letters = {o:chr(o) for o in range(65,73)}
+letters
+
+
{65: 'A', 66: 'B', 67: 'C', 68: 'D', 69: 'E', 70: 'F', 71: 'G', 72: 'H'}
+
+
+
+
filter_dict(letters, lambda k,v: k<67 or v in 'FG')
+
+
{65: 'A', 66: 'B', 70: 'F', 71: 'G'}
+
+
+
+

source

+
+
+

filter_keys

+
+
 filter_keys (d, func)
+
+

Filter a dict using func, applied to keys

+
+
filter_keys(letters, lt(67))
+
+
{65: 'A', 66: 'B'}
+
+
+
+

source

+
+
+

filter_values

+
+
 filter_values (d, func)
+
+

Filter a dict using func, applied to values

+
+
filter_values(letters, in_('FG'))
+
+
{70: 'F', 71: 'G'}
+
+
+
+

source

+
+
+

cycle

+
+
 cycle (o)
+
+

Like itertools.cycle except creates list of Nones if o is empty

+
+
test_eq(itertools.islice(cycle([1,2,3]),5), [1,2,3,1,2])
+test_eq(itertools.islice(cycle([]),3), [None]*3)
+test_eq(itertools.islice(cycle(None),3), [None]*3)
+test_eq(itertools.islice(cycle(1),3), [1,1,1])
+
+
+

source

+
+
+

zip_cycle

+
+
 zip_cycle (x, *args)
+
+

Like itertools.zip_longest but cycles through elements of all but first argument

+
+
test_eq(zip_cycle([1,2,3,4],list('abc')), [(1, 'a'), (2, 'b'), (3, 'c'), (4, 'a')])
+
+
+

source

+
+
+

sorted_ex

+
+
 sorted_ex (iterable, key=None, reverse=False)
+
+

Like sorted, but if key is str use attrgetter; if int use itemgetter

+
+

source

+
+
+

not_

+
+
 not_ (f)
+
+

Create new function that negates result of f

+
+
def f(a): return a>0
+test_eq(f(1),True)
+test_eq(not_(f)(1),False)
+test_eq(not_(f)(a=-1),True)
+
+
+

source

+
+
+

argwhere

+
+
 argwhere (iterable, f, negate=False, **kwargs)
+
+

Like filter_ex, but return indices for matching items

+
+

source

+
+
+

filter_ex

+
+
 filter_ex (iterable, f=<function noop>, negate=False, gen=False,
+            **kwargs)
+
+

Like filter, but passing kwargs to f, defaulting f to noop, and adding negate and gen

+
+

source

+
+
+

range_of

+
+
 range_of (a, b=None, step=None)
+
+

All indices of collection a, if a is a collection, otherwise range

+
+
test_eq(range_of([1,1,1,1]), [0,1,2,3])
+test_eq(range_of(4), [0,1,2,3])
+
+
+

source

+
+
+

renumerate

+
+
 renumerate (iterable, start=0)
+
+

Same as enumerate, but returns index as 2nd element instead of 1st

+
+
test_eq(renumerate('abc'), (('a',0),('b',1),('c',2)))
+
+
+

source

+
+
+

first

+
+
 first (x, f=None, negate=False, **kwargs)
+
+

First element of x, optionally filtered by f, or None if missing

+
+
test_eq(first(['a', 'b', 'c', 'd', 'e']), 'a')
+test_eq(first([False]), False)
+test_eq(first([False], noop), None)
+
+
+

source

+
+
+

only

+
+
 only (o)
+
+

Return the only item of o, raise if o doesn’t have exactly one item

+
+

source

+
+
+

nested_attr

+
+
 nested_attr (o, attr, default=None)
+
+

Same as getattr, but if attr includes a ., then looks inside nested objects

+
+
class CustomIndexable:
+    def __init__(self): self.data = {'a':1,'b':'v','c':{'d':5}}
+    def __getitem__(self, key): return self.data[key]
+
+custom_indexable = CustomIndexable()
+test_eq(nested_attr(custom_indexable,'a'),1)
+test_eq(nested_attr(custom_indexable,'c.d'),5)
+test_eq(nested_attr(custom_indexable,'e'),None)
+
+

class TestObj: def init(self): self.nested = {‘key’: [1, 2, {‘inner’: ‘value’}]} test_obj = TestObj()

+

test_eq(nested_attr(test_obj, ‘nested.key.2.inner’),‘value’) test_eq(nested_attr([1, 2, 3], ‘1’),2)

+
+
b = {'a':1,'b':'v','c':{'d':5}}
+test_eq(nested_attr(b,'b'),'v')
+test_eq(nested_attr(b,'c.d'),5)
+
+
+
a = SimpleNamespace(b=(SimpleNamespace(c=1)))
+test_eq(nested_attr(a, 'b.c'), getattr(getattr(a, 'b'), 'c'))
+test_eq(nested_attr(a, 'b.d'), None)
+test_eq(nested_attr(b, 'a'), 1)
+
+
+

source

+
+
+

nested_setdefault

+
+
 nested_setdefault (o, attr, default)
+
+

Same as setdefault, but if attr includes a ., then looks inside nested objects

+
+

source

+
+
+

nested_callable

+
+
 nested_callable (o, attr)
+
+

Same as nested_attr but if not found will return noop

+
+
a = SimpleNamespace(b=(SimpleNamespace(c=1)))
+test_eq(nested_callable(a, 'b.c'), getattr(getattr(a, 'b'), 'c'))
+test_eq(nested_callable(a, 'b.d'), noop)
+
+
+

source

+
+
+

nested_idx

+
+
 nested_idx (coll, *idxs)
+
+

Index into nested collections, dicts, etc, with idxs

+
+
a = {'b':[1,{'c':2}]}
+test_eq(nested_idx(a, 'nope'), None)
+test_eq(nested_idx(a, 'nope', 'nup'), None)
+test_eq(nested_idx(a, 'b', 3), None)
+test_eq(nested_idx(a), a)
+test_eq(nested_idx(a, 'b'), [1,{'c':2}])
+test_eq(nested_idx(a, 'b', 1), {'c':2})
+test_eq(nested_idx(a, 'b', 1, 'c'), 2)
+
+
+
a = SimpleNamespace(b=[1,{'c':2}])
+test_eq(nested_idx(a, 'nope'), None)
+test_eq(nested_idx(a, 'nope', 'nup'), None)
+test_eq(nested_idx(a, 'b', 3), None)
+test_eq(nested_idx(a), a)
+test_eq(nested_idx(a, 'b'), [1,{'c':2}])
+test_eq(nested_idx(a, 'b', 1), {'c':2})
+test_eq(nested_idx(a, 'b', 1, 'c'), 2)
+
+
+

source

+
+
+

set_nested_idx

+
+
 set_nested_idx (coll, value, *idxs)
+
+

Set value indexed like `nested_idx

+
+
set_nested_idx(a, 3, 'b', 0)
+test_eq(nested_idx(a, 'b', 0), 3)
+
+
+

source

+
+
+

val2idx

+
+
 val2idx (x)
+
+

Dict from value to index

+
+
test_eq(val2idx([1,2,3]), {3:2,1:0,2:1})
+
+
+

source

+
+
+

uniqueify

+
+
 uniqueify (x, sort=False, bidir=False, start=None)
+
+

Unique elements in x, optional sort, optional return reverse correspondence, optional prepend with elements.

+
+
t = [1,1,0,5,0,3]
+test_eq(uniqueify(t),[1,0,5,3])
+test_eq(uniqueify(t, sort=True),[0,1,3,5])
+test_eq(uniqueify(t, start=[7,8,6]), [7,8,6,1,0,5,3])
+v,o = uniqueify(t, bidir=True)
+test_eq(v,[1,0,5,3])
+test_eq(o,{1:0, 0: 1, 5: 2, 3: 3})
+v,o = uniqueify(t, sort=True, bidir=True)
+test_eq(v,[0,1,3,5])
+test_eq(o,{0:0, 1: 1, 3: 2, 5: 3})
+
+
+

source

+
+
+

loop_first_last

+
+
 loop_first_last (values)
+
+

Iterate and generate a tuple with a flag for first and last value.

+
+
test_eq(loop_first_last(range(3)), [(True,False,0), (False,False,1), (False,True,2)])
+
+
+

source

+
+
+

loop_first

+
+
 loop_first (values)
+
+

Iterate and generate a tuple with a flag for first value.

+
+
test_eq(loop_first(range(3)), [(True,0), (False,1), (False,2)])
+
+
+

source

+
+
+

loop_last

+
+
 loop_last (values)
+
+

Iterate and generate a tuple with a flag for last value.

+
+
test_eq(loop_last(range(3)), [(False,0), (False,1), (True,2)])
+
+
+

source

+
+
+

first_match

+
+
 first_match (lst, f, default=None)
+
+

First element of lst matching predicate f, or default if none

+
+
a = [0,2,4,5,6,7,10]
+test_eq(first_match(a, lambda o:o%2), 3)
+
+
+

source

+
+
+

last_match

+
+
 last_match (lst, f, default=None)
+
+

Last element of lst matching predicate f, or default if none

+
+
test_eq(last_match(a, lambda o:o%2), 5)
+
+
+
+
+

fastuple

+

A tuple with extended functionality.

+
+

source

+
+

fastuple

+
+
 fastuple (x=None, *rest)
+
+

A tuple with elementwise ops and more friendly init behavior

+
+
+

Friendly init behavior

+

Common failure modes when trying to initialize a tuple in python:

+
tuple(3)
+> TypeError: 'int' object is not iterable
+

or

+
tuple(3, 4)
+> TypeError: tuple expected at most 1 arguments, got 2
+

However, fastuple allows you to define tuples like this and in the usual way:

+
+
test_eq(fastuple(3), (3,))
+test_eq(fastuple(3,4), (3, 4))
+test_eq(fastuple((3,4)), (3, 4))
+
+
+
+

Elementwise operations

+
+

source

+
+
fastuple.add
+
+
 fastuple.add (*args)
+
+

+ is already defined in tuple for concat, so use add instead

+
+
test_eq(fastuple.add((1,1),(2,2)), (3,3))
+test_eq_type(fastuple(1,1).add(2), fastuple(3,3))
+test_eq(fastuple('1','2').add('2'), fastuple('12','22'))
+
+
+

source

+
+
+
fastuple.mul
+
+
 fastuple.mul (*args)
+
+

* is already defined in tuple for replicating, so use mul instead

+
+
test_eq_type(fastuple(1,1).mul(2), fastuple(2,2))
+
+
+
+
+

Other Elementwise Operations

+

Additionally, the following elementwise operations are available: - le: less than or equal - eq: equal - gt: greater than - min: minimum of

+
+
test_eq(fastuple(3,1).le(1), (False, True))
+test_eq(fastuple(3,1).eq(1), (False, True))
+test_eq(fastuple(3,1).gt(1), (True, False))
+test_eq(fastuple(3,1).min(2), (2,1))
+
+

You can also do other elementwise operations like negate a fastuple, or subtract two fastuples:

+
+
test_eq(-fastuple(1,2), (-1,-2))
+test_eq(~fastuple(1,0,1), (False,True,False))
+
+test_eq(fastuple(1,1)-fastuple(2,2), (-1,-1))
+
+
+
test_eq(type(fastuple(1)), fastuple)
+test_eq_type(fastuple(1,2), fastuple(1,2))
+test_ne(fastuple(1,2), fastuple(1,3))
+test_eq(fastuple(), ())
+
+
+
+
+

Functions on Functions

+

Utilities for functional programming or for defining, modifying, or debugging functions.

+
+

source

+
+

bind

+
+
 bind (func, *pargs, **pkwargs)
+
+

Same as partial, except you can use arg0 arg1 etc param placeholders

+

bind is the same as partial, but also allows you to reorder positional arguments using variable name(s) arg{i} where i refers to the zero-indexed positional argument. bind as implemented currently only supports reordering of up to the first 5 positional arguments.

+

Consider the function myfunc below, which has 3 positional arguments. These arguments can be referenced as arg0, arg1, and arg1, respectively.

+
+
def myfn(a,b,c,d=1,e=2): return(a,b,c,d,e)
+
+

In the below example we bind the positional arguments of myfn as follows:

+
    +
  • The second input 14, referenced by arg1, is substituted for the first positional argument.
  • +
  • We supply a default value of 17 for the second positional argument.
  • +
  • The first input 19, referenced by arg0, is subsituted for the third positional argument.
  • +
+
+
test_eq(bind(myfn, arg1, 17, arg0, e=3)(19,14), (14,17,19,1,3))
+
+

In this next example:

+
    +
  • We set the default value to 17 for the first positional argument.
  • +
  • The first input 19 refrenced by arg0, becomes the second positional argument.
  • +
  • The second input 14 becomes the third positional argument.
  • +
  • We override the default the value for named argument e to 3.
  • +
+
+
test_eq(bind(myfn, 17, arg0, e=3)(19,14), (17,19,14,1,3))
+
+

This is an example of using bind like partial and do not reorder any arguments:

+
+
test_eq(bind(myfn)(17,19,14), (17,19,14,1,2))
+
+

bind can also be used to change default values. In the below example, we use the first input 3 to override the default value of the named argument e, and supply default values for the first three positional arguments:

+
+
test_eq(bind(myfn, 17,19,14,e=arg0)(3), (17,19,14,1,3))
+
+
+

source

+
+
+

mapt

+
+
 mapt (func, *iterables)
+
+

Tuplified map

+
+
t = [0,1,2,3]
+test_eq(mapt(operator.neg, t), (0,-1,-2,-3))
+
+
+

source

+
+
+

map_ex

+
+
 map_ex (iterable, f, *args, gen=False, **kwargs)
+
+

Like map, but use bind, and supports str and indexing

+
+
test_eq(map_ex(t,operator.neg), [0,-1,-2,-3])
+
+

If f is a string then it is treated as a format string to create the mapping:

+
+
test_eq(map_ex(t, '#{}#'), ['#0#','#1#','#2#','#3#'])
+
+

If f is a dictionary (or anything supporting __getitem__) then it is indexed to create the mapping:

+
+
test_eq(map_ex(t, list('abcd')), list('abcd'))
+
+

You can also pass the same arg params that bind accepts:

+
+
def f(a=None,b=None): return b
+test_eq(map_ex(t, f, b=arg0), range(4))
+
+
+

source

+
+
+

compose

+
+
 compose (*funcs, order=None)
+
+

Create a function that composes all functions in funcs, passing along remaining *args and **kwargs to all

+
+
f1 = lambda o,p=0: (o*2)+p
+f2 = lambda o,p=1: (o+1)/p
+test_eq(f2(f1(3)), compose(f1,f2)(3))
+test_eq(f2(f1(3,p=3),p=3), compose(f1,f2)(3,p=3))
+test_eq(f2(f1(3,  3),  3), compose(f1,f2)(3,  3))
+
+f1.order = 1
+test_eq(f1(f2(3)), compose(f1,f2, order="order")(3))
+
+
+

source

+
+
+

maps

+
+
 maps (*args, retain=<function noop>)
+
+

Like map, except funcs are composed first

+
+
test_eq(maps([1]), [1])
+test_eq(maps(operator.neg, [1,2]), [-1,-2])
+test_eq(maps(operator.neg, operator.neg, [1,2]), [1,2])
+
+
+

source

+
+
+

partialler

+
+
 partialler (f, *args, order=None, **kwargs)
+
+

Like functools.partial but also copies over docstring

+
+
def _f(x,a=1):
+    "test func"
+    return x-a
+_f.order=1
+
+f = partialler(_f, 2)
+test_eq(f.order, 1)
+test_eq(f(3), -1)
+f = partialler(_f, a=2, order=3)
+test_eq(f.__doc__, "test func")
+test_eq(f.order, 3)
+test_eq(f(3), _f(3,2))
+
+
+
class partial0:
+    "Like `partialler`, but args passed to callable are inserted at started, instead of at end"
+    def __init__(self, f, *args, order=None, **kwargs):
+        self.f,self.args,self.kwargs = f,args,kwargs
+        self.order = ifnone(order, getattr(f,'order',None))
+        self.__doc__ = f.__doc__
+
+    def __call__(self, *args, **kwargs): return self.f(*args, *self.args, **kwargs, **self.kwargs)
+
+
+
f = partial0(_f, 2)
+test_eq(f.order, 1)
+test_eq(f(3), 1) # NB: different to `partialler` example
+
+
+

source

+
+
+

instantiate

+
+
 instantiate (t)
+
+

Instantiate t if it’s a type, otherwise do nothing

+
+
test_eq_type(instantiate(int), 0)
+test_eq_type(instantiate(1), 1)
+
+
+

source

+
+
+

using_attr

+
+
 using_attr (f, attr)
+
+

Construct a function which applies f to the argument’s attribute attr

+
+
t = Path('/a/b.txt')
+f = using_attr(str.upper, 'name')
+test_eq(f(t), 'B.TXT')
+
+
+
+

Self (with an uppercase S)

+

A Concise Way To Create Lambdas

+

This is a concise way to create lambdas that are calling methods on an object (note the capitalization!)

+

Self.sum(), for instance, is a shortcut for lambda o: o.sum().

+
+
f = Self.sum()
+x = np.array([3.,1])
+test_eq(f(x), 4.)
+
+# This is equivalent to above
+f = lambda o: o.sum()
+x = np.array([3.,1])
+test_eq(f(x), 4.)
+
+f = Self.argmin()
+arr = np.array([1,2,3,4,5])
+test_eq(f(arr), arr.argmin())
+
+f = Self.sum().is_integer()
+x = np.array([3.,1])
+test_eq(f(x), True)
+
+f = Self.sum().real.is_integer()
+x = np.array([3.,1])
+test_eq(f(x), True)
+
+f = Self.imag()
+test_eq(f(3), 0)
+
+f = Self[1]
+test_eq(f(x), 1)
+
+

Self is also callable, which creates a function which calls any function passed to it, using the arguments passed to Self:

+
+
def f(a, b=3): return a+b+2
+def g(a, b=3): return a*b
+fg = Self(1,b=2)
+list(map(fg, [f,g]))
+
+
[5, 2]
+
+
+
+
+
+

Patching

+
+

source

+
+

copy_func

+
+
 copy_func (f)
+
+

Copy a non-builtin function (NB copy.copy does not work for this)

+

Sometimes it may be desirable to make a copy of a function that doesn’t point to the original object. When you use Python’s built in copy.copy or copy.deepcopy to copy a function, you get a reference to the original object:

+
+
import copy as cp
+
+
+
def foo(): pass
+a = cp.copy(foo)
+b = cp.deepcopy(foo)
+
+a.someattr = 'hello' # since a and b point at the same object, updating a will update b
+test_eq(b.someattr, 'hello')
+
+assert a is foo and b is foo
+
+

However, with copy_func, you can retrieve a copy of a function without a reference to the original object:

+
+
c = copy_func(foo) # c is an indpendent object
+assert c is not foo
+
+
+
def g(x, *, y=3): return x+y
+test_eq(copy_func(g)(4), 7)
+
+
+

source

+
+
+

patch_to

+
+
 patch_to (cls, as_prop=False, cls_method=False)
+
+

Decorator: add f to cls

+

The @patch_to decorator allows you to monkey patch a function into a class as a method:

+
+
class _T3(int): pass  
+
+@patch_to(_T3)
+def func1(self, a): return self+a
+
+t = _T3(1) # we initialized `t` to a type int = 1
+test_eq(t.func1(2), 3) # we add 2 to `t`, so 2 + 1 = 3
+
+

You can access instance properties in the usual way via self:

+
+
class _T4():
+    def __init__(self, g): self.g = g
+        
+@patch_to(_T4)
+def greet(self, x): return self.g + x
+        
+t = _T4('hello ') # this sets self.g = 'hello '
+test_eq(t.greet('world'), 'hello world') #t.greet('world') will append 'world' to 'hello '
+
+

You can instead specify that the method should be a class method by setting cls_method=True:

+
+
class _T5(int): attr = 3 # attr is a class attribute we will access in a later method
+    
+@patch_to(_T5, cls_method=True)
+def func(cls, x): return cls.attr + x # you can access class attributes in the normal way
+
+test_eq(_T5.func(4), 7)
+
+

Additionally you can specify that the function you want to patch should be a class attribute with as_prop=True:

+
+
@patch_to(_T5, as_prop=True)
+def add_ten(self): return self + 10
+
+t = _T5(4)
+test_eq(t.add_ten, 14)
+
+

Instead of passing one class to the @patch_to decorator, you can pass multiple classes in a tuple to simulteanously patch more than one class with the same method:

+
+
class _T6(int): pass
+class _T7(int): pass
+
+@patch_to((_T6,_T7))
+def func_mult(self, a): return self*a
+
+t = _T6(2)
+test_eq(t.func_mult(4), 8)
+t = _T7(2)
+test_eq(t.func_mult(4), 8)
+
+
+

source

+
+
+

patch

+
+
 patch (f=None, as_prop=False, cls_method=False)
+
+

Decorator: add f to the first parameter’s class (based on f’s type annotations)

+

@patch is an alternative to @patch_to that allows you similarly monkey patch class(es) by using type annotations:

+
+
class _T8(int): pass  
+
+@patch
+def func(self:_T8, a): return self+a
+
+t = _T8(1)  # we initilized `t` to a type int = 1
+test_eq(t.func(3), 4) # we add 3 to `t`, so 3 + 1 = 4
+test_eq(t.func.__qualname__, '_T8.func')
+
+

Similarly to patch_to, you can supply a union of classes instead of a single class in your type annotations to patch multiple classes:

+
+
class _T9(int): pass 
+
+@patch
+def func2(x:_T8|_T9, a): return x*a # will patch both _T8 and _T9
+
+t = _T8(2)
+test_eq(t.func2(4), 8)
+test_eq(t.func2.__qualname__, '_T8.func2')
+
+t = _T9(2)
+test_eq(t.func2(4), 8)
+test_eq(t.func2.__qualname__, '_T9.func2')
+
+

Just like patch_to decorator you can use as_prop and cls_method parameters with patch decorator:

+
+
@patch(as_prop=True)
+def add_ten(self:_T5): return self + 10
+
+t = _T5(4)
+test_eq(t.add_ten, 14)
+
+
+
class _T5(int): attr = 3 # attr is a class attribute we will access in a later method
+    
+@patch(cls_method=True)
+def func(cls:_T5, x): return cls.attr + x # you can access class attributes in the normal way
+
+test_eq(_T5.func(4), 7)
+
+
+

source

+
+
+

patch_property

+
+
 patch_property (f)
+
+

Deprecated; use patch(as_prop=True) instead

+

Patching classmethod shouldn’t affect how python’s inheritance works

+
+
class FastParent: pass
+
+@patch(cls_method=True)
+def type_cls(cls: FastParent): return cls
+
+class FastChild(FastParent): pass
+
+parent = FastParent()
+test_eq(parent.type_cls(), FastParent)
+
+child = FastChild()
+test_eq(child.type_cls(), FastChild)
+
+
+
+
+

Other Helpers

+
+

source

+
+

compile_re

+
+
 compile_re (pat)
+
+

Compile pat if it’s not None

+
+
assert compile_re(None) is None
+assert compile_re('a').match('ab')
+
+
+

source

+
+

ImportEnum

+
+
 ImportEnum (value, names=None, module=None, qualname=None, type=None,
+             start=1)
+
+

An Enum that can have its values imported

+
+
_T = ImportEnum('_T', {'foobar':1, 'goobar':2})
+_T.imports()
+test_eq(foobar, _T.foobar)
+test_eq(goobar, _T.goobar)
+
+
+

source

+
+
+

StrEnum

+
+
 StrEnum (value, names=None, module=None, qualname=None, type=None,
+          start=1)
+
+

An ImportEnum that behaves like a str

+
+

source

+
+
+
+

str_enum

+
+
 str_enum (name, *vals)
+
+

Simplified creation of StrEnum types

+
+

source

+
+

ValEnum

+
+
 ValEnum (value, names=None, module=None, qualname=None, type=None,
+          start=1)
+
+

An ImportEnum that stringifies using values

+
+
_T = str_enum('_T', 'a', 'b')
+test_eq(f'{_T.a}', 'a')
+test_eq(_T.a, 'a')
+test_eq(list(_T.__members__), ['a','b'])
+print(_T.a, _T.a.upper())
+
+
a A
+
+
+
+

source

+
+
+

Stateful

+
+
 Stateful (*args, **kwargs)
+
+

A base class/mixin for objects that should not serialize all their state

+
+
class _T(Stateful):
+    def __init__(self):
+        super().__init__()
+        self.a=1
+        self._state['test']=2
+
+t = _T()
+t2 = pickle.loads(pickle.dumps(t))
+test_eq(t.a,1)
+test_eq(t._state['test'],2)
+test_eq(t2.a,1)
+test_eq(t2._state,{})
+
+

Override _init_state to do any necessary setup steps that are required during __init__ or during deserialization (e.g. pickle.load). Here’s an example of how Stateful simplifies the official Python example for Handling Stateful Objects.

+
+
class TextReader(Stateful):
+    """Print and number lines in a text file."""
+    _stateattrs=('file',)
+    def __init__(self, filename):
+        self.filename,self.lineno = filename,0
+        super().__init__()
+
+    def readline(self):
+        self.lineno += 1
+        line = self.file.readline()
+        if line: return f"{self.lineno}: {line.strip()}"
+
+    def _init_state(self):
+        self.file = open(self.filename)
+        for _ in range(self.lineno): self.file.readline()
+
+
+
reader = TextReader("00_test.ipynb")
+print(reader.readline())
+print(reader.readline())
+
+new_reader = pickle.loads(pickle.dumps(reader))
+print(reader.readline())
+
+
1: {
+2: "cells": [
+3: {
+
+
+
+

source

+
+
+
+

NotStr

+
+
 NotStr (s)
+
+

Behaves like a str, but isn’t an instance of one

+
+
s = NotStr("hello")
+assert not isinstance(s, str)
+test_eq(s, 'hello')
+test_eq(s*2, 'hellohello')
+test_eq(len(s), 5)
+
+
+

source

+
+

PrettyString

+

Little hack to get strings to show properly in Jupyter.

+

Allow strings with special characters to render properly in Jupyter. Without calling print() strings with special characters are displayed like so:

+
+
with_special_chars='a string\nwith\nnew\nlines and\ttabs'
+with_special_chars
+
+
'a string\nwith\nnew\nlines and\ttabs'
+
+
+

We can correct this with PrettyString:

+
+
PrettyString(with_special_chars)
+
+
a string
+with
+new
+lines and   tabs
+
+
+
+

source

+
+
+
+

even_mults

+
+
 even_mults (start, stop, n)
+
+

Build log-stepped array from start to stop in n steps.

+
+
test_eq(even_mults(2,8,3), [2,4,8])
+test_eq(even_mults(2,32,5), [2,4,8,16,32])
+test_eq(even_mults(2,8,1), 8)
+
+
+

source

+
+
+

num_cpus

+
+
 num_cpus ()
+
+

Get number of cpus

+
+
num_cpus()
+
+
10
+
+
+
+

source

+
+
+

add_props

+
+
 add_props (f, g=None, n=2)
+
+

Create properties passing each of range(n) to f

+
+
class _T(): a,b = add_props(lambda i,x:i*2)
+
+t = _T()
+test_eq(t.a,0)
+test_eq(t.b,2)
+
+
+
class _T(): 
+    def __init__(self, v): self.v=v
+    def _set(i, self, v): self.v[i] = v
+    a,b = add_props(lambda i,x: x.v[i], _set)
+
+t = _T([0,2])
+test_eq(t.a,0)
+test_eq(t.b,2)
+t.a = t.a+1
+t.b = 3
+test_eq(t.a,1)
+test_eq(t.b,3)
+
+
+

source

+
+
+

str2bool

+
+
 str2bool (s)
+
+

Case-insensitive convert string s too a bool (y,yes,t,true,on,1->True)

+

True values are ‘y’, ‘yes’, ‘t’, ‘true’, ‘on’, and ‘1’; false values are ‘n’, ‘no’, ‘f’, ‘false’, ‘off’, and ‘0’. Raises ValueError if ‘val’ is anything else.

+
+
for o in "y YES t True on 1".split(): assert str2bool(o)
+for o in "n no FALSE off 0".split(): assert not str2bool(o)
+for o in 0,None,'',False: assert not str2bool(o)
+for o in 1,True: assert str2bool(o)
+
+
+

source

+
+
+

str2int

+
+
 str2int (s)
+
+

Convert s to an int

+
+

source

+
+
+

str2float

+
+
 str2float (s:str)
+
+

Convert s to a float

+
+

source

+
+
+

str2list

+
+
 str2list (s:str)
+
+

Convert s to a list

+
+

source

+
+
+

str2date

+
+
 str2date (s:str)
+
+

date.fromisoformat with empty string handling

+
+

source

+
+
+

to_date

+
+
 to_date (arg)
+
+
+

source

+
+
+

to_list

+
+
 to_list (arg)
+
+
+

source

+
+
+

to_float

+
+
 to_float (arg)
+
+
+

source

+
+
+

to_int

+
+
 to_int (arg)
+
+
+

source

+
+
+

to_bool

+
+
 to_bool (arg)
+
+
+

source

+
+
+

typed

+
+
 typed (_func=None, cast=False)
+
+

Decorator to check param and return types at runtime, with optional casting

+

typed validates argument types at runtime. This is in contrast to MyPy which only offers static type checking.

+

For example, a TypeError will be raised if we try to pass an integer into the first argument of the below function:

+
+
@typed
+def discount(price:int, pct:float) -> float:
+    return (1-pct) * price
+
+with ExceptionExpected(TypeError): discount(100.0, .1)
+
+

You can have automatic casting based on heuristics by specifying typed(cast=True). If casting is not possible, a TypeError is raised.

+
+
@typed(cast=True)
+def discount(price:int, pct:float) -> float:
+    return (1-pct) * price
+
+assert 90.0 == discount(100.5, .1) # will auto cast 100.5 to the int 100
+assert 90.0 == discount(' 100 ', .1) # will auto cast the str "100" to the int 100
+with ExceptionExpected(TypeError): discount("a", .1)
+
+

We can also optionally allow multiple types by enumarating the types in a tuple as illustrated below:

+
+
@typed
+def discount(price:int|float, pct:float): 
+    return (1-pct) * price
+
+assert 90.0 == discount(100.0, .1)
+
+@typed(cast=True)
+def discount(price:int|None, pct:float):
+    return (1-pct) * price
+
+assert 90.0 == discount(100.0, .1)
+
+

We currently do not support union types when casting.

+
+
@typed(cast=True)
+def discount(price:int|float, pct:float):
+    return (1-pct) * price
+
+with ExceptionExpected(AssertionError): assert 90.0 == discount("100.0", .1)
+
+

typed works with classes, too:

+
+
class Foo:
+    @typed
+    def __init__(self, a:int, b: int, c:str): pass
+    @typed(cast=True)
+    def test(cls, d:str): return d
+
+with ExceptionExpected(TypeError): Foo(1, 2, 3) 
+assert isinstance(Foo(1,2, 'a string').test(10), str)
+
+

It also works with custom types.

+
+
@typed
+def test_foo(foo: Foo): pass
+
+with ExceptionExpected(TypeError): test_foo(1)
+test_foo(Foo(1, 2, 'a string'))
+
+
+
class Bar:
+    @typed
+    def __init__(self, a:int): self.a = a
+@typed(cast=True)
+def test_bar(bar: Bar): return bar
+
+assert isinstance(test_bar(1), Bar)
+test_eq(test_bar(1).a, 1)
+with ExceptionExpected(TypeError): test_bar("foobar")
+
+
+

source

+
+
+

exec_new

+
+
 exec_new (code)
+
+

Execute code in a new environment and return it

+
+
g = exec_new('a=1')
+test_eq(g['a'], 1)
+
+
+

source

+
+
+

exec_import

+
+
 exec_import (mod, sym)
+
+

Import sym from mod in a new environment

+
+
+
+

Notebook functions

+
+
+

ipython_shell

+
+
 ipython_shell ()
+
+

Same as get_ipython but returns False if not in IPython

+
+
+
+

in_ipython

+
+
 in_ipython ()
+
+

Check if code is running in some kind of IPython environment

+
+
+
+

in_colab

+
+
 in_colab ()
+
+

Check if the code is running in Google Colaboratory

+
+
+
+

in_jupyter

+
+
 in_jupyter ()
+
+

Check if the code is running in a jupyter notebook

+
+
+
+

in_notebook

+
+
 in_notebook ()
+
+

Check if the code is running in a jupyter notebook

+

These variables are available as booleans in fastcore.basics as IN_IPYTHON, IN_JUPYTER, IN_COLAB and IN_NOTEBOOK.

+
+
IN_IPYTHON, IN_JUPYTER, IN_COLAB, IN_NOTEBOOK
+
+
(True, True, False, True)
+
+
+ + +
+
+ +
+ +
+ + + + + \ No newline at end of file diff --git a/basics.html.md b/basics.html.md new file mode 100644 index 00000000..6ac844a3 --- /dev/null +++ b/basics.html.md @@ -0,0 +1,3622 @@ +# Basic functionality + + + + +## Basics + +------------------------------------------------------------------------ + +source + +### ifnone + +> ifnone (a, b) + +*`b` if `a` is None else `a`* + +Since `b if a is None else a` is such a common pattern, we wrap it in a +function. However, be careful, because python will evaluate *both* `a` +and `b` when calling +[`ifnone`](https://fastcore.fast.ai/basics.html#ifnone) (which it +doesn’t do if using the `if` version directly). + +``` python +test_eq(ifnone(None,1), 1) +test_eq(ifnone(2 ,1), 2) +``` + +------------------------------------------------------------------------ + +source + +### maybe_attr + +> maybe_attr (o, attr) + +*`getattr(o,attr,o)`* + +Return the attribute `attr` for object `o`. If the attribute doesn’t +exist, then return the object `o` instead. + +``` python +class myobj: myattr='foo' + +test_eq(maybe_attr(myobj, 'myattr'), 'foo') +test_eq(maybe_attr(myobj, 'another_attr'), myobj) +``` + +------------------------------------------------------------------------ + +source + +### basic_repr + +> basic_repr (flds=None) + +*Minimal `__repr__`* + +In types which provide rich display functionality in Jupyter, their +`__repr__` is also called in order to provide a fallback text +representation. Unfortunately, this includes a memory address which +changes on every invocation, making it non-deterministic. This causes +diffs to get messy and creates conflicts in git. To fix this, put +`__repr__=basic_repr()` inside your class. + +``` python +class SomeClass: __repr__=basic_repr() +repr(SomeClass()) +``` + + 'SomeClass()' + +If you pass a list of attributes (`flds`) of an object, then this will +generate a string with the name of each attribute and its corresponding +value. The format of this string is `key=value`, where `key` is the name +of the attribute, and `value` is the value of the attribute. For each +value, attempt to use the `__name__` attribute, otherwise fall back to +using the value’s `__repr__` when constructing the string. + +``` python +class SomeClass: + a=1 + b='foo' + __repr__=basic_repr('a,b') + __name__='some-class' + +repr(SomeClass()) +``` + + "SomeClass(a=1, b='foo')" + +Nested objects work too: + +``` python +class AnotherClass: + c=SomeClass() + d='bar' + __repr__=basic_repr(['c', 'd']) + +repr(AnotherClass()) +``` + + "AnotherClass(c=SomeClass(a=1, b='foo'), d='bar')" + +Instance variables (but not class variables) are shown if +[`basic_repr`](https://fastcore.fast.ai/basics.html#basic_repr) is +called with no arguments: + +``` python +class SomeClass: + def __init__(self, a=1, b='foo'): self.a,self.b = a,b + __repr__=basic_repr() + +repr(SomeClass()) +``` + + "SomeClass(a=1, b='foo')" + +------------------------------------------------------------------------ + +source + +### BasicRepr + +> BasicRepr () + +*Base class for objects needing a basic `__repr__`* + +As a shortcut for creating a `__repr__` for instance variables, you can +inherit from +[`BasicRepr`](https://fastcore.fast.ai/basics.html#basicrepr): + +``` python +class SomeClass(BasicRepr): + def __init__(self, a=1, b='foo'): self.a,self.b = a,b + +repr(SomeClass()) +``` + + "SomeClass(a=1, b='foo')" + +------------------------------------------------------------------------ + +source + +### is_array + +> is_array (x) + +*`True` if `x` supports `__array__` or `iloc`* + +``` python +is_array(np.array(1)),is_array([1]) +``` + + (True, False) + +------------------------------------------------------------------------ + +source + +### listify + +> listify (o=None, *rest, use_list=False, match=None) + +*Convert `o` to a `list`* + +Conversion is designed to “do what you mean”, e.g: + +``` python +test_eq(listify('hi'), ['hi']) +test_eq(listify(b'hi'), [b'hi']) +test_eq(listify(array(1)), [array(1)]) +test_eq(listify(1), [1]) +test_eq(listify([1,2]), [1,2]) +test_eq(listify(range(3)), [0,1,2]) +test_eq(listify(None), []) +test_eq(listify(1,2), [1,2]) +``` + +``` python +arr = np.arange(9).reshape(3,3) +listify(arr) +``` + + [array([[0, 1, 2], + [3, 4, 5], + [6, 7, 8]])] + +``` python +listify(array([1,2])) +``` + + [array([1, 2])] + +Generators are turned into lists too: + +``` python +gen = (o for o in range(3)) +test_eq(listify(gen), [0,1,2]) +``` + +Use `match` to provide a length to match: + +``` python +test_eq(listify(1,match=3), [1,1,1]) +``` + +If `match` is a sequence, it’s length is used: + +``` python +test_eq(listify(1,match=range(3)), [1,1,1]) +``` + +If the listified item is not of length `1`, it must be the same length +as `match`: + +``` python +test_eq(listify([1,1,1],match=3), [1,1,1]) +test_fail(lambda: listify([1,1],match=3)) +``` + +------------------------------------------------------------------------ + +source + +### tuplify + +> tuplify (o, use_list=False, match=None) + +*Make `o` a tuple* + +``` python +test_eq(tuplify(None),()) +test_eq(tuplify([1,2,3]),(1,2,3)) +test_eq(tuplify(1,match=[1,2,3]),(1,1,1)) +``` + +------------------------------------------------------------------------ + +source + +### true + +> true (x) + +*Test whether `x` is truthy; collections with \>0 elements are +considered `True`* + +``` python +[(o,true(o)) for o in + (array(0),array(1),array([0]),array([0,1]),1,0,'',None)] +``` + + [(array(0), False), + (array(1), True), + (array([0]), True), + (array([0, 1]), True), + (1, True), + (0, False), + ('', False), + (None, False)] + +------------------------------------------------------------------------ + +source + +### NullType + +> NullType () + +*An object that is `False` and can be called, chained, and indexed* + +``` python +bool(null.hi().there[3]) +``` + + False + +------------------------------------------------------------------------ + +source + +### tonull + +> tonull (x) + +*Convert `None` to `null`* + +``` python +bool(tonull(None).hi().there[3]) +``` + + False + +------------------------------------------------------------------------ + +source + +### get_class + +> get_class (nm, *fld_names, sup=None, doc=None, funcs=None, anno=None, +> **flds) + +*Dynamically create a class, optionally inheriting from `sup`, +containing `fld_names`* + +``` python +_t = get_class('_t', 'a', b=2, anno={'b':int}) +t = _t() +test_eq(t.a, None) +test_eq(t.b, 2) +t = _t(1, b=3) +test_eq(t.a, 1) +test_eq(t.b, 3) +t = _t(1, 3) +test_eq(t.a, 1) +test_eq(t.b, 3) +test_eq(t, pickle.loads(pickle.dumps(t))) +test_eq(_t.__annotations__, {'b':int, 'a':typing.Any}) +repr(t) +``` + + '__main__._t(a=1, b=3)' + +Most often you’ll want to call +[`mk_class`](https://fastcore.fast.ai/basics.html#mk_class), since it +adds the class to your module. See +[`mk_class`](https://fastcore.fast.ai/basics.html#mk_class) for more +details and examples of use (which also apply to +[`get_class`](https://fastcore.fast.ai/basics.html#get_class)). + +------------------------------------------------------------------------ + +source + +### mk_class + +> mk_class (nm, *fld_names, sup=None, doc=None, funcs=None, mod=None, +> anno=None, **flds) + +*Create a class using +[`get_class`](https://fastcore.fast.ai/basics.html#get_class) and add to +the caller’s module* + +Any `kwargs` will be added as class attributes, and `sup` is an optional +(tuple of) base classes. + +``` python +mk_class('_t', a=1, sup=dict) +t = _t() +test_eq(t.a, 1) +assert(isinstance(t,dict)) +``` + +A `__init__` is provided that sets attrs for any `kwargs`, and for any +`args` (matching by position to fields), along with a `__repr__` which +prints all attrs. The docstring is set to `doc`. You can pass `funcs` +which will be added as attrs with the function names. + +``` python +def foo(self): return 1 +mk_class('_t', 'a', sup=dict, doc='test doc', funcs=foo) + +t = _t(3, b=2) +test_eq(t.a, 3) +test_eq(t.b, 2) +test_eq(t.foo(), 1) +test_eq(t.__doc__, 'test doc') +t +``` + + {} + +------------------------------------------------------------------------ + +source + +### wrap_class + +> wrap_class (nm, *fld_names, sup=None, doc=None, funcs=None, **flds) + +*Decorator: makes function a method of a new class `nm` passing +parameters to +[`mk_class`](https://fastcore.fast.ai/basics.html#mk_class)* + +``` python +@wrap_class('_t', a=2) +def bar(self,x): return x+1 + +t = _t() +test_eq(t.a, 2) +test_eq(t.bar(3), 4) +``` + +------------------------------------------------------------------------ + +source + +#### ignore_exceptions + +> ignore_exceptions () + +*Context manager to ignore exceptions* + +``` python +with ignore_exceptions(): + # Exception will be ignored + raise Exception +``` + +------------------------------------------------------------------------ + +source + +### exec_local + +> exec_local (code, var_name) + +*Call `exec` on `code` and return the var `var_name`* + +``` python +test_eq(exec_local("a=1", "a"), 1) +``` + +------------------------------------------------------------------------ + +source + +### risinstance + +> risinstance (types, obj=None) + +*Curried `isinstance` but with args reversed* + +``` python +assert risinstance(int, 1) +assert not risinstance(str, 0) +assert risinstance(int)(1) +assert not risinstance(int)(None) +``` + +`types` can also be strings: + +``` python +assert risinstance(('str','int'), 'a') +assert risinstance('str', 'a') +assert not risinstance('int', 'a') +``` + +------------------------------------------------------------------------ + +source + +### ver2tuple + +> ver2tuple (v:str) + +``` python +test_eq(ver2tuple('3.8.1'), (3,8,1)) +test_eq(ver2tuple('3.1'), (3,1,0)) +test_eq(ver2tuple('3.'), (3,0,0)) +test_eq(ver2tuple('3'), (3,0,0)) +``` + +## NoOp + +These are used when you need a pass-through function. + +------------------------------------------------------------------------ + +### noop + +> noop (x=None, *args, **kwargs) + +*Do nothing* + +``` python +noop() +test_eq(noop(1),1) +``` + +------------------------------------------------------------------------ + +### noops + +> noops (x=None, *args, **kwargs) + +*Do nothing (method)* + +``` python +class _t: foo=noops +test_eq(_t().foo(1),1) +``` + +## Infinite Lists + +These lists are useful for things like padding an array or adding index +column(s) to arrays. + +[`Inf`](https://fastcore.fast.ai/basics.html#inf) defines the following +properties: + +- `count: itertools.count()` +- `zeros: itertools.cycle([0])` +- `ones : itertools.cycle([1])` +- `nones: itertools.cycle([None])` + +``` python +test_eq([o for i,o in zip(range(5), Inf.count)], + [0, 1, 2, 3, 4]) + +test_eq([o for i,o in zip(range(5), Inf.zeros)], + [0]*5) + +test_eq([o for i,o in zip(range(5), Inf.ones)], + [1]*5) + +test_eq([o for i,o in zip(range(5), Inf.nones)], + [None]*5) +``` + +## Operator Functions + +------------------------------------------------------------------------ + +source + +### in\_ + +> in_ (x, a) + +*`True` if `x in a`* + +``` python +# test if element is in another +assert in_('c', ('b', 'c', 'a')) +assert in_(4, [2,3,4,5]) +assert in_('t', 'fastai') +test_fail(in_('h', 'fastai')) + +# use in_ as a partial +assert in_('fastai')('t') +assert in_([2,3,4,5])(4) +test_fail(in_('fastai')('h')) +``` + +In addition to [`in_`](https://fastcore.fast.ai/basics.html#in_), the +following functions are provided matching the behavior of the equivalent +versions in `operator`: *lt gt le ge eq ne add sub mul truediv is\_ +is_not mod*. + +``` python +lt(3,5),gt(3,5),is_(None,None),in_(0,[1,2]),mod(3,2) +``` + + (True, False, True, False, 1) + +Similarly to `_in`, they also have additional functionality: if you only +pass one param, they return a partial function that passes that param as +the second positional parameter. + +``` python +lt(5)(3),gt(5)(3),is_(None)(None),in_([1,2])(0),mod(2)(3) +``` + + (True, False, True, False, 1) + +------------------------------------------------------------------------ + +source + +### ret_true + +> ret_true (*args, **kwargs) + +*Predicate: always `True`* + +``` python +assert ret_true(1,2,3) +assert ret_true(False) +``` + +------------------------------------------------------------------------ + +source + +### ret_false + +> ret_false (*args, **kwargs) + +*Predicate: always `False`* + +------------------------------------------------------------------------ + +source + +### stop + +> stop (e=) + +*Raises exception `e` (by default `StopIteration`)* + +------------------------------------------------------------------------ + +source + +### gen + +> gen (func, seq, cond=) + +*Like `(func(o) for o in seq if cond(func(o)))` but handles +`StopIteration`* + +``` python +test_eq(gen(noop, Inf.count, lt(5)), + range(5)) +test_eq(gen(operator.neg, Inf.count, gt(-5)), + [0,-1,-2,-3,-4]) +test_eq(gen(lambda o:o if o<5 else stop(), Inf.count), + range(5)) +``` + +------------------------------------------------------------------------ + +source + +### chunked + +> chunked (it, chunk_sz=None, drop_last=False, n_chunks=None) + +*Return batches from iterator `it` of size `chunk_sz` (or return +`n_chunks` total)* + +Note that you must pass either `chunk_sz`, or `n_chunks`, but not both. + +``` python +t = list(range(10)) +test_eq(chunked(t,3), [[0,1,2], [3,4,5], [6,7,8], [9]]) +test_eq(chunked(t,3,True), [[0,1,2], [3,4,5], [6,7,8], ]) + +t = map(lambda o:stop() if o==6 else o, Inf.count) +test_eq(chunked(t,3), [[0, 1, 2], [3, 4, 5]]) +t = map(lambda o:stop() if o==7 else o, Inf.count) +test_eq(chunked(t,3), [[0, 1, 2], [3, 4, 5], [6]]) + +t = np.arange(10) +test_eq(chunked(t,3), [[0,1,2], [3,4,5], [6,7,8], [9]]) +test_eq(chunked(t,3,True), [[0,1,2], [3,4,5], [6,7,8], ]) + +test_eq(chunked([], 3), []) +test_eq(chunked([], n_chunks=3), []) +``` + +------------------------------------------------------------------------ + +source + +### otherwise + +> otherwise (x, tst, y) + +*`y if tst(x) else x`* + +``` python +test_eq(otherwise(2+1, gt(3), 4), 3) +test_eq(otherwise(2+1, gt(2), 4), 4) +``` + +## Attribute Helpers + +These functions reduce boilerplate when setting or manipulating +attributes or properties of objects. + +------------------------------------------------------------------------ + +source + +### custom_dir + +> custom_dir (c, add) + +*Implement custom `__dir__`, adding `add` to `cls`* + +[`custom_dir`](https://fastcore.fast.ai/basics.html#custom_dir) allows +you extract the [`__dict__` property of a +class](https://stackoverflow.com/questions/19907442/explain-dict-attribute) +and appends the list `add` to it. + +``` python +class _T: + def f(): pass + +s = custom_dir(_T(), add=['foo', 'bar']) +assert {'foo', 'bar', 'f'}.issubset(s) +``` + +------------------------------------------------------------------------ + +source + +### AttrDict + +*`dict` subclass that also provides access to keys as attrs* + +``` python +d = AttrDict(a=1,b="two") +test_eq(d.a, 1) +test_eq(d['b'], 'two') +test_eq(d.get('c','nope'), 'nope') +d.b = 2 +test_eq(d.b, 2) +test_eq(d['b'], 2) +d['b'] = 3 +test_eq(d['b'], 3) +test_eq(d.b, 3) +assert 'a' in dir(d) +``` + +[`AttrDict`](https://fastcore.fast.ai/basics.html#attrdict) will pretty +print in Jupyter Notebooks: + +``` python +_test_dict = {'a':1, 'b': {'c':1, 'd':2}, 'c': {'c':1, 'd':2}, 'd': {'c':1, 'd':2}, + 'e': {'c':1, 'd':2}, 'f': {'c':1, 'd':2, 'e': 4, 'f':[1,2,3,4,5]}} +AttrDict(_test_dict) +``` + +``` json +{ 'a': 1, + 'b': {'c': 1, 'd': 2}, + 'c': {'c': 1, 'd': 2}, + 'd': {'c': 1, 'd': 2}, + 'e': {'c': 1, 'd': 2}, + 'f': {'c': 1, 'd': 2, 'e': 4, 'f': [1, 2, 3, 4, 5]}} +``` + +------------------------------------------------------------------------ + +source + +### AttrDictDefault + +> AttrDictDefault (*args, default_=None, **kwargs) + +*[`AttrDict`](https://fastcore.fast.ai/basics.html#attrdict) subclass +that returns `None` for missing attrs* + +``` python +d = AttrDictDefault(a=1,b="two", default_='nope') +test_eq(d.a, 1) +test_eq(d['b'], 'two') +test_eq(d.c, 'nope') +``` + +------------------------------------------------------------------------ + +source + +### NS + +*`SimpleNamespace` subclass that also adds `iter` and `dict` support* + +This is very similar to +[`AttrDict`](https://fastcore.fast.ai/basics.html#attrdict), but since +it starts with `SimpleNamespace`, it has some differences in behavior. +You can use it just like `SimpleNamespace`: + +``` python +d = NS(**_test_dict) +d +``` + + namespace(a=1, + b={'c': 1, 'd': 2}, + c={'c': 1, 'd': 2}, + d={'c': 1, 'd': 2}, + e={'c': 1, 'd': 2}, + f={'c': 1, 'd': 2, 'e': 4, 'f': [1, 2, 3, 4, 5]}) + +…but you can also index it to get/set: + +``` python +d['a'] +``` + + 1 + +…and iterate t: + +``` python +list(d) +``` + + ['a', 'b', 'c', 'd', 'e', 'f'] + +------------------------------------------------------------------------ + +source + +### get_annotations_ex + +> get_annotations_ex (obj, globals=None, locals=None) + +*Backport of py3.10 `get_annotations` that returns globals/locals* + +In Python 3.10 `inspect.get_annotations` was added. However previous +versions of Python are unable to evaluate type annotations correctly if +`from future import __annotations__` is used. Furthermore, *all* +annotations are evaluated, even if only some subset are needed. +[`get_annotations_ex`](https://fastcore.fast.ai/basics.html#get_annotations_ex) +provides the same functionality as `inspect.get_annotations`, but works +on earlier versions of Python, and returns the `globals` and `locals` +needed to evaluate types. + +------------------------------------------------------------------------ + +source + +### eval_type + +> eval_type (t, glb, loc) + +*`eval` a type or collection of types, if needed, for annotations in +py3.10+* + +In py3.10, or if `from future import __annotations__` is used, `a` is a +`str`: + +``` python +class _T2a: pass +def func(a: _T2a): pass +ann,glb,loc = get_annotations_ex(func) + +eval_type(ann['a'], glb, loc) +``` + + __main__._T2a + +`|` is supported for defining `Union` types when using +[`eval_type`](https://fastcore.fast.ai/basics.html#eval_type) even for +python versions prior to 3.9: + +``` python +class _T2b: pass +def func(a: _T2a|_T2b): pass +ann,glb,loc = get_annotations_ex(func) + +eval_type(ann['a'], glb, loc) +``` + + typing.Union[__main__._T2a, __main__._T2b] + +------------------------------------------------------------------------ + +source + +### type_hints + +> type_hints (f) + +*Like `typing.get_type_hints` but returns `{}` if not allowed type* + +Below is a list of allowed types for type hints in python: + +``` python +list(typing._allowed_types) +``` + + [function, + builtin_function_or_method, + method, + module, + wrapper_descriptor, + method-wrapper, + method_descriptor] + +For example, type `func` is allowed so +[`type_hints`](https://fastcore.fast.ai/basics.html#type_hints) returns +the same value as `typing.get_hints`: + +``` python +def f(a:int)->bool: ... # a function with type hints (allowed) +exp = {'a':int,'return':bool} +test_eq(type_hints(f), typing.get_type_hints(f)) +test_eq(type_hints(f), exp) +``` + +However, `class` is not an allowed type, so +[`type_hints`](https://fastcore.fast.ai/basics.html#type_hints) returns +`{}`: + +``` python +class _T: + def __init__(self, a:int=0)->bool: ... +assert not type_hints(_T) +``` + +------------------------------------------------------------------------ + +source + +### annotations + +> annotations (o) + +*Annotations for `o`, or `type(o)`* + +This supports a wider range of situations than +[`type_hints`](https://fastcore.fast.ai/basics.html#type_hints), by +checking `type()` and `__init__` for annotations too: + +``` python +for o in _T,_T(),_T.__init__,f: test_eq(annotations(o), exp) +assert not annotations(int) +assert not annotations(print) +``` + +------------------------------------------------------------------------ + +source + +### anno_ret + +> anno_ret (func) + +*Get the return annotation of `func`* + +``` python +def f(x) -> float: return x +test_eq(anno_ret(f), float) + +def f(x) -> typing.Tuple[float,float]: return x +assert anno_ret(f)==typing.Tuple[float,float] +``` + +If your return annotation is `None`, +[`anno_ret`](https://fastcore.fast.ai/basics.html#anno_ret) will return +`NoneType` (and not `None`): + +``` python +def f(x) -> None: return x + +test_eq(anno_ret(f), NoneType) +assert anno_ret(f) is not None # returns NoneType instead of None +``` + +If your function does not have a return type, or if you pass in `None` +instead of a function, then +[`anno_ret`](https://fastcore.fast.ai/basics.html#anno_ret) returns +`None`: + +``` python +def f(x): return x + +test_eq(anno_ret(f), None) +test_eq(anno_ret(None), None) # instead of passing in a func, pass in None +``` + +------------------------------------------------------------------------ + +source + +### signature_ex + +> signature_ex (obj, eval_str:bool=False) + +*Backport of `inspect.signature(..., eval_str=True` to \source + +### union2tuple + +> union2tuple (t) + +``` python +test_eq(union2tuple(Union[int,str]), (int,str)) +test_eq(union2tuple(int), int) +assert union2tuple(Tuple[int,str])==Tuple[int,str] +test_eq(union2tuple((int,str)), (int,str)) +if UnionType: test_eq(union2tuple(int|str), (int,str)) +``` + +------------------------------------------------------------------------ + +source + +### argnames + +> argnames (f, frame=False) + +*Names of arguments to function or frame `f`* + +``` python +test_eq(argnames(f), ['x']) +``` + +------------------------------------------------------------------------ + +source + +### with_cast + +> with_cast (f) + +*Decorator which uses any parameter annotations as preprocessing +functions* + +``` python +@with_cast +def _f(a, b:Path, c:str='', d=0): return (a,b,c,d) + +test_eq(_f(1, '.', 3), (1,Path('.'),'3',0)) +test_eq(_f(1, '.'), (1,Path('.'),'',0)) + +@with_cast +def _g(a:int=0)->str: return a + +test_eq(_g(4.0), '4') +test_eq(_g(4.4), '4') +test_eq(_g(2), '2') +``` + +------------------------------------------------------------------------ + +source + +### store_attr + +> store_attr (names=None, but='', cast=False, store_args=None, **attrs) + +*Store params named in comma-separated `names` from calling context into +attrs in `self`* + +In it’s most basic form, you can use +[`store_attr`](https://fastcore.fast.ai/basics.html#store_attr) to +shorten code like this: + +``` python +class T: + def __init__(self, a,b,c): self.a,self.b,self.c = a,b,c +``` + +…to this: + +``` python +class T: + def __init__(self, a,b,c): store_attr('a,b,c', self) +``` + +This class behaves as if we’d used the first form: + +``` python +t = T(1,c=2,b=3) +assert t.a==1 and t.b==3 and t.c==2 +``` + +``` python +class T1: + def __init__(self, a,b,c): store_attr() +``` + +In addition, it stores the attrs as a `dict` in `__stored_args__`, which +you can use for display, logging, and so forth. + +``` python +test_eq(t.__stored_args__, {'a':1, 'b':3, 'c':2}) +``` + +Since you normally want to use the first argument (often called `self`) +for storing attributes, it’s optional: + +``` python +class T: + def __init__(self, a,b,c:str): store_attr('a,b,c') + +t = T(1,c=2,b=3) +assert t.a==1 and t.b==3 and t.c==2 +``` + +With `cast=True` any parameter annotations will be used as preprocessing +functions for the corresponding arguments: + +``` python +class T: + def __init__(self, a:listify, b, c:str): store_attr('a,b,c', cast=True) + +t = T(1,c=2,b=3) +assert t.a==[1] and t.b==3 and t.c=='2' +``` + +You can inherit from a class using +[`store_attr`](https://fastcore.fast.ai/basics.html#store_attr), and +just call it again to add in any new attributes added in the derived +class: + +``` python +class T2(T): + def __init__(self, d, **kwargs): + super().__init__(**kwargs) + store_attr('d') + +t = T2(d=1,a=2,b=3,c=4) +assert t.a==2 and t.b==3 and t.c==4 and t.d==1 +``` + +You can skip passing a list of attrs to store. In this case, all +arguments passed to the method are stored: + +``` python +class T: + def __init__(self, a,b,c): store_attr() + +t = T(1,c=2,b=3) +assert t.a==1 and t.b==3 and t.c==2 +``` + +``` python +class T4(T): + def __init__(self, d, **kwargs): + super().__init__(**kwargs) + store_attr() + +t = T4(4, a=1,c=2,b=3) +assert t.a==1 and t.b==3 and t.c==2 and t.d==4 +``` + +``` python +class T4: + def __init__(self, *, a: int, b: float = 1): + store_attr() + +t = T4(a=3) +assert t.a==3 and t.b==1 +t = T4(a=3, b=2) +assert t.a==3 and t.b==2 +``` + +You can skip some attrs by passing `but`: + +``` python +class T: + def __init__(self, a,b,c): store_attr(but='a') + +t = T(1,c=2,b=3) +assert t.b==3 and t.c==2 +assert not hasattr(t,'a') +``` + +You can also pass keywords to +[`store_attr`](https://fastcore.fast.ai/basics.html#store_attr), which +is identical to setting the attrs directly, but also stores them in +`__stored_args__`. + +``` python +class T: + def __init__(self): store_attr(a=1) + +t = T() +assert t.a==1 +``` + +You can also use store_attr inside functions. + +``` python +def create_T(a, b): + t = SimpleNamespace() + store_attr(self=t) + return t + +t = create_T(a=1, b=2) +assert t.a==1 and t.b==2 +``` + +------------------------------------------------------------------------ + +source + +### attrdict + +> attrdict (o, *ks, default=None) + +*Dict from each `k` in `ks` to `getattr(o,k)`* + +``` python +class T: + def __init__(self, a,b,c): store_attr() + +t = T(1,c=2,b=3) +test_eq(attrdict(t,'b','c'), {'b':3, 'c':2}) +``` + +------------------------------------------------------------------------ + +source + +### properties + +> properties (cls, *ps) + +*Change attrs in `cls` with names in `ps` to properties* + +``` python +class T: + def a(self): return 1 + def b(self): return 2 +properties(T,'a') + +test_eq(T().a,1) +test_eq(T().b(),2) +``` + +------------------------------------------------------------------------ + +source + +### camel2words + +> camel2words (s, space=' ') + +*Convert CamelCase to ‘spaced words’* + +``` python +test_eq(camel2words('ClassAreCamel'), 'Class Are Camel') +``` + +------------------------------------------------------------------------ + +source + +### camel2snake + +> camel2snake (name) + +*Convert CamelCase to snake_case* + +``` python +test_eq(camel2snake('ClassAreCamel'), 'class_are_camel') +test_eq(camel2snake('Already_Snake'), 'already__snake') +``` + +------------------------------------------------------------------------ + +source + +### snake2camel + +> snake2camel (s) + +*Convert snake_case to CamelCase* + +``` python +test_eq(snake2camel('a_b_cc'), 'ABCc') +``` + +------------------------------------------------------------------------ + +source + +### class2attr + +> class2attr (cls_name) + +*Return the snake-cased name of the class; strip ending `cls_name` if it +exists.* + +``` python +class Parent: + @property + def name(self): return class2attr(self, 'Parent') + +class ChildOfParent(Parent): pass +class ParentChildOf(Parent): pass + +p = Parent() +cp = ChildOfParent() +cp2 = ParentChildOf() + +test_eq(p.name, 'parent') +test_eq(cp.name, 'child_of') +test_eq(cp2.name, 'parent_child_of') +``` + +------------------------------------------------------------------------ + +source + +### getcallable + +> getcallable (o, attr) + +*Calls `getattr` with a default of `noop`* + +``` python +class Math: + def addition(self,a,b): return a+b + +m = Math() + +test_eq(getcallable(m, "addition")(a=1,b=2), 3) +test_eq(getcallable(m, "subtraction")(a=1,b=2), None) +``` + +------------------------------------------------------------------------ + +source + +### getattrs + +> getattrs (o, *attrs, default=None) + +*List of all `attrs` in `o`* + +``` python +from fractions import Fraction +``` + +``` python +getattrs(Fraction(1,2), 'numerator', 'denominator') +``` + + [1, 2] + +------------------------------------------------------------------------ + +source + +### hasattrs + +> hasattrs (o, attrs) + +*Test whether `o` contains all `attrs`* + +``` python +assert hasattrs(1,('imag','real')) +assert not hasattrs(1,('imag','foo')) +``` + +------------------------------------------------------------------------ + +source + +### setattrs + +> setattrs (dest, flds, src) + +``` python +d = dict(a=1,bb="2",ignore=3) +o = SimpleNamespace() +setattrs(o, "a,bb", d) +test_eq(o.a, 1) +test_eq(o.bb, "2") +``` + +``` python +d = SimpleNamespace(a=1,bb="2",ignore=3) +o = SimpleNamespace() +setattrs(o, "a,bb", d) +test_eq(o.a, 1) +test_eq(o.bb, "2") +``` + +------------------------------------------------------------------------ + +source + +### try_attrs + +> try_attrs (obj, *attrs) + +*Return first attr that exists in `obj`* + +``` python +test_eq(try_attrs(1, 'real'), 1) +test_eq(try_attrs(1, 'foobar', 'real'), 1) +``` + +## Attribute Delegation + +------------------------------------------------------------------------ + +source + +### GetAttrBase + +> GetAttrBase () + +*Basic delegation of +[`__getattr__`](https://fastcore.fast.ai/xml.html#__getattr__) and +`__dir__`* + +------------------------------------------------------------------------ + +source + +#### GetAttr + +> GetAttr () + +*Inherit from this to have all attr accesses in `self._xtra` passed down +to `self.default`* + +Inherit from [`GetAttr`](https://fastcore.fast.ai/basics.html#getattr) +to have attr access passed down to an instance attribute. This makes it +easy to create composites that don’t require callers to know about their +components. For a more detailed discussion of how this works as well as +relevant context, we suggest reading the [delegated composition section +of this blog article](https://www.fast.ai/2019/08/06/delegation/). + +You can customise the behaviour of +[`GetAttr`](https://fastcore.fast.ai/basics.html#getattr) in subclasses +via; - `_default` - By default, this is set to `'default'`, so attr +access is passed down to `self.default` - `_default` can be set to the +name of any instance attribute that does not start with dunder `__` - +`_xtra` - By default, this is `None`, so all attr access is passed +down - You can limit which attrs get passed down by setting `_xtra` to a +list of attribute names + +To illuminate the utility of +[`GetAttr`](https://fastcore.fast.ai/basics.html#getattr), suppose we +have the following two classes, `_WebPage` which is a superclass of +`_ProductPage`, which we wish to compose like so: + +``` python +class _WebPage: + def __init__(self, title, author="Jeremy"): + self.title,self.author = title,author + +class _ProductPage: + def __init__(self, page, price): self.page,self.price = page,price + +page = _WebPage('Soap', author="Sylvain") +p = _ProductPage(page, 15.0) +``` + +How do we make it so we can just write `p.author`, instead of +`p.page.author` to access the `author` attribute? We can use +[`GetAttr`](https://fastcore.fast.ai/basics.html#getattr), of course! +First, we subclass +[`GetAttr`](https://fastcore.fast.ai/basics.html#getattr) when defining +`_ProductPage`. Next, we set `self.default` to the object whose +attributes we want to be able to access directly, which in this case is +the `page` argument passed on initialization: + +``` python +class _ProductPage(GetAttr): + def __init__(self, page, price): self.default,self.price = page,price #self.default allows you to access page directly. + +p = _ProductPage(page, 15.0) +``` + +Now, we can access the `author` attribute directly from the instance: + +``` python +test_eq(p.author, 'Sylvain') +``` + +If you wish to store the object you are composing in an attribute other +than `self.default`, you can set the class attribute `_data` as shown +below. This is useful in the case where you might have a name collision +with `self.default`: + +``` python +class _C(GetAttr): + _default = '_data' # use different component name; `self._data` rather than `self.default` + def __init__(self,a): self._data = a + def foo(self): noop + +t = _C('Hi') +test_eq(t._data, 'Hi') +test_fail(lambda: t.default) # we no longer have self.default +test_eq(t.lower(), 'hi') +test_eq(t.upper(), 'HI') +assert 'lower' in dir(t) +assert 'upper' in dir(t) +``` + +By default, all attributes and methods of the object you are composing +are retained. In the below example, we compose a `str` object with the +class `_C`. This allows us to directly call string methods on instances +of class `_C`, such as `str.lower()` or `str.upper()`: + +``` python +class _C(GetAttr): + # allow all attributes and methods to get passed to `self.default` (by leaving _xtra=None) + def __init__(self,a): self.default = a + def foo(self): noop + +t = _C('Hi') +test_eq(t.lower(), 'hi') +test_eq(t.upper(), 'HI') +assert 'lower' in dir(t) +assert 'upper' in dir(t) +``` + +However, you can choose which attributes or methods to retain by +defining a class attribute `_xtra`, which is a list of allowed attribute +and method names to delegate. In the below example, we only delegate the +`lower` method from the composed `str` object when defining class `_C`: + +``` python +class _C(GetAttr): + _xtra = ['lower'] # specify which attributes get passed to `self.default` + def __init__(self,a): self.default = a + def foo(self): noop + +t = _C('Hi') +test_eq(t.default, 'Hi') +test_eq(t.lower(), 'hi') +test_fail(lambda: t.upper()) # upper wasn't in _xtra, so it isn't available to be called +assert 'lower' in dir(t) +assert 'upper' not in dir(t) +``` + +You must be careful to properly set an instance attribute in `__init__` +that corresponds to the class attribute `_default`. The below example +sets the class attribute `_default` to `data`, but erroneously fails to +define `self.data` (and instead defines `self.default`). + +Failing to properly set instance attributes leads to errors when you try +to access methods directly: + +``` python +class _C(GetAttr): + _default = 'data' # use a bad component name; i.e. self.data does not exist + def __init__(self,a): self.default = a + def foo(self): noop + +# TODO: should we raise an error when we create a new instance ... +t = _C('Hi') +test_eq(t.default, 'Hi') +# ... or is it enough for all GetAttr features to raise errors +test_fail(lambda: t.data) +test_fail(lambda: t.lower()) +test_fail(lambda: t.upper()) +test_fail(lambda: dir(t)) +``` + +------------------------------------------------------------------------ + +source + +### delegate_attr + +> delegate_attr (k, to) + +*Use in [`__getattr__`](https://fastcore.fast.ai/xml.html#__getattr__) +to delegate to attr `to` without inheriting from +[`GetAttr`](https://fastcore.fast.ai/basics.html#getattr)* + +[`delegate_attr`](https://fastcore.fast.ai/basics.html#delegate_attr) is +a functional way to delegate attributes, and is an alternative to +[`GetAttr`](https://fastcore.fast.ai/basics.html#getattr). We recommend +reading the documentation of +[`GetAttr`](https://fastcore.fast.ai/basics.html#getattr) for more +details around delegation. + +You can use achieve delegation when you define +[`__getattr__`](https://fastcore.fast.ai/xml.html#__getattr__) by using +[`delegate_attr`](https://fastcore.fast.ai/basics.html#delegate_attr): + +``` python +class _C: + def __init__(self, o): self.o = o # self.o corresponds to the `to` argument in delegate_attr. + def __getattr__(self, k): return delegate_attr(self, k, to='o') + + +t = _C('HELLO') # delegates to a string +test_eq(t.lower(), 'hello') + +t = _C(np.array([5,4,3])) # delegates to a numpy array +test_eq(t.sum(), 12) + +t = _C(pd.DataFrame({'a': [1,2], 'b': [3,4]})) # delegates to a pandas.DataFrame +test_eq(t.b.max(), 4) +``` + +## Extensible Types + +[`ShowPrint`](https://fastcore.fast.ai/basics.html#showprint) is a base +class that defines a `show` method, which is used primarily for +callbacks in fastai that expect this method to be defined. + +[`Int`](https://fastcore.fast.ai/basics.html#int), +[`Float`](https://fastcore.fast.ai/basics.html#float), and +[`Str`](https://fastcore.fast.ai/basics.html#str) extend `int`, `float` +and `str` respectively by adding an additional `show` method by +inheriting from +[`ShowPrint`](https://fastcore.fast.ai/basics.html#showprint). + +The code for [`Int`](https://fastcore.fast.ai/basics.html#int) is shown +below: + +Examples: + +``` python +Int(0).show() +Float(2.0).show() +Str('Hello').show() +``` + + 0 + 2.0 + Hello + +## Collection functions + +Functions that manipulate popular python collections. + +------------------------------------------------------------------------ + +source + +### partition + +> partition (coll, f) + +*Partition a collection by a predicate* + +``` python +ts,fs = partition(range(10), mod(2)) +test_eq(fs, [0,2,4,6,8]) +test_eq(ts, [1,3,5,7,9]) +``` + +------------------------------------------------------------------------ + +source + +### flatten + +> flatten (o) + +*Concatenate all collections and items as a generator* + +------------------------------------------------------------------------ + +source + +### concat + +> concat (colls) + +*Concatenate all collections and items as a list* + +``` python +concat([(o for o in range(2)),[2,3,4], 5]) +``` + + [0, 1, 2, 3, 4, 5] + +``` python +concat([["abc", "xyz"], ["foo", "bar"]]) +``` + + ['abc', 'xyz', 'foo', 'bar'] + +------------------------------------------------------------------------ + +source + +### strcat + +> strcat (its, sep:str='') + +*Concatenate stringified items `its`* + +``` python +test_eq(strcat(['a',2]), 'a2') +test_eq(strcat(['a',2], ';'), 'a;2') +``` + +------------------------------------------------------------------------ + +source + +### detuplify + +> detuplify (x) + +*If `x` is a tuple with one thing, extract it* + +``` python +test_eq(detuplify(()),None) +test_eq(detuplify([1]),1) +test_eq(detuplify([1,2]), [1,2]) +test_eq(detuplify(np.array([[1,2]])), np.array([[1,2]])) +``` + +------------------------------------------------------------------------ + +source + +### replicate + +> replicate (item, match) + +*Create tuple of `item` copied `len(match)` times* + +``` python +t = [1,1] +test_eq(replicate([1,2], t),([1,2],[1,2])) +test_eq(replicate(1, t),(1,1)) +``` + +------------------------------------------------------------------------ + +source + +### setify + +> setify (o) + +*Turn any list like-object into a set.* + +``` python +# test +test_eq(setify(None),set()) +test_eq(setify('abc'),{'abc'}) +test_eq(setify([1,2,2]),{1,2}) +test_eq(setify(range(0,3)),{0,1,2}) +test_eq(setify({1,2}),{1,2}) +``` + +------------------------------------------------------------------------ + +source + +### merge + +> merge (*ds) + +*Merge all dictionaries in `ds`* + +``` python +test_eq(merge(), {}) +test_eq(merge(dict(a=1,b=2)), dict(a=1,b=2)) +test_eq(merge(dict(a=1,b=2), dict(b=3,c=4), None), dict(a=1, b=3, c=4)) +``` + +------------------------------------------------------------------------ + +source + +### range_of + +> range_of (x) + +*All indices of collection `x` (i.e. `list(range(len(x)))`)* + +``` python +test_eq(range_of([1,1,1,1]), [0,1,2,3]) +``` + +------------------------------------------------------------------------ + +source + +### groupby + +> groupby (x, key, val=) + +*Like `itertools.groupby` but doesn’t need to be sorted, and isn’t lazy, +plus some extensions* + +``` python +test_eq(groupby('aa ab bb'.split(), itemgetter(0)), {'a':['aa','ab'], 'b':['bb']}) +``` + +Here’s an example of how to *invert* a grouping, using an `int` as `key` +(which uses `itemgetter`; passing a `str` will use `attrgetter`), and +using a `val` function: + +``` python +d = {0: [1, 3, 7], 2: [3], 3: [5], 4: [8], 5: [4], 7: [5]} +groupby(((o,k) for k,v in d.items() for o in v), 0, 1) +``` + + {1: [0], 3: [0, 2], 7: [0], 5: [3, 7], 8: [4], 4: [5]} + +------------------------------------------------------------------------ + +source + +### last_index + +> last_index (x, o) + +*Finds the last index of occurence of `x` in `o` (returns -1 if no +occurence)* + +``` python +test_eq(last_index(9, [1, 2, 9, 3, 4, 9, 10]), 5) +test_eq(last_index(6, [1, 2, 9, 3, 4, 9, 10]), -1) +``` + +------------------------------------------------------------------------ + +source + +### filter_dict + +> filter_dict (d, func) + +*Filter a `dict` using `func`, applied to keys and values* + +``` python +letters = {o:chr(o) for o in range(65,73)} +letters +``` + + {65: 'A', 66: 'B', 67: 'C', 68: 'D', 69: 'E', 70: 'F', 71: 'G', 72: 'H'} + +``` python +filter_dict(letters, lambda k,v: k<67 or v in 'FG') +``` + + {65: 'A', 66: 'B', 70: 'F', 71: 'G'} + +------------------------------------------------------------------------ + +source + +### filter_keys + +> filter_keys (d, func) + +*Filter a `dict` using `func`, applied to keys* + +``` python +filter_keys(letters, lt(67)) +``` + + {65: 'A', 66: 'B'} + +------------------------------------------------------------------------ + +source + +### filter_values + +> filter_values (d, func) + +*Filter a `dict` using `func`, applied to values* + +``` python +filter_values(letters, in_('FG')) +``` + + {70: 'F', 71: 'G'} + +------------------------------------------------------------------------ + +source + +### cycle + +> cycle (o) + +*Like `itertools.cycle` except creates list of `None`s if `o` is empty* + +``` python +test_eq(itertools.islice(cycle([1,2,3]),5), [1,2,3,1,2]) +test_eq(itertools.islice(cycle([]),3), [None]*3) +test_eq(itertools.islice(cycle(None),3), [None]*3) +test_eq(itertools.islice(cycle(1),3), [1,1,1]) +``` + +------------------------------------------------------------------------ + +source + +### zip_cycle + +> zip_cycle (x, *args) + +*Like `itertools.zip_longest` but +[`cycle`](https://fastcore.fast.ai/foundation.html#cycle)s through +elements of all but first argument* + +``` python +test_eq(zip_cycle([1,2,3,4],list('abc')), [(1, 'a'), (2, 'b'), (3, 'c'), (4, 'a')]) +``` + +------------------------------------------------------------------------ + +source + +### sorted_ex + +> sorted_ex (iterable, key=None, reverse=False) + +*Like `sorted`, but if key is str use `attrgetter`; if int use +`itemgetter`* + +------------------------------------------------------------------------ + +source + +### not\_ + +> not_ (f) + +*Create new function that negates result of `f`* + +``` python +def f(a): return a>0 +test_eq(f(1),True) +test_eq(not_(f)(1),False) +test_eq(not_(f)(a=-1),True) +``` + +------------------------------------------------------------------------ + +source + +### argwhere + +> argwhere (iterable, f, negate=False, **kwargs) + +*Like [`filter_ex`](https://fastcore.fast.ai/basics.html#filter_ex), but +return indices for matching items* + +------------------------------------------------------------------------ + +source + +### filter_ex + +> filter_ex (iterable, f=, negate=False, gen=False, +> **kwargs) + +*Like `filter`, but passing `kwargs` to `f`, defaulting `f` to `noop`, +and adding `negate` and +[`gen`](https://fastcore.fast.ai/basics.html#gen)* + +------------------------------------------------------------------------ + +source + +### range_of + +> range_of (a, b=None, step=None) + +*All indices of collection `a`, if `a` is a collection, otherwise +`range`* + +``` python +test_eq(range_of([1,1,1,1]), [0,1,2,3]) +test_eq(range_of(4), [0,1,2,3]) +``` + +------------------------------------------------------------------------ + +source + +### renumerate + +> renumerate (iterable, start=0) + +*Same as `enumerate`, but returns index as 2nd element instead of 1st* + +``` python +test_eq(renumerate('abc'), (('a',0),('b',1),('c',2))) +``` + +------------------------------------------------------------------------ + +source + +### first + +> first (x, f=None, negate=False, **kwargs) + +*First element of `x`, optionally filtered by `f`, or None if missing* + +``` python +test_eq(first(['a', 'b', 'c', 'd', 'e']), 'a') +test_eq(first([False]), False) +test_eq(first([False], noop), None) +``` + +------------------------------------------------------------------------ + +source + +### only + +> only (o) + +*Return the only item of `o`, raise if `o` doesn’t have exactly one +item* + +------------------------------------------------------------------------ + +source + +### nested_attr + +> nested_attr (o, attr, default=None) + +*Same as `getattr`, but if `attr` includes a `.`, then looks inside +nested objects* + +``` python +class CustomIndexable: + def __init__(self): self.data = {'a':1,'b':'v','c':{'d':5}} + def __getitem__(self, key): return self.data[key] + +custom_indexable = CustomIndexable() +test_eq(nested_attr(custom_indexable,'a'),1) +test_eq(nested_attr(custom_indexable,'c.d'),5) +test_eq(nested_attr(custom_indexable,'e'),None) +``` + +class TestObj: def **init**(self): self.nested = {‘key’: \[1, 2, +{‘inner’: ‘value’}\]} test_obj = TestObj() + +test_eq(nested_attr(test_obj, ‘nested.key.2.inner’),‘value’) +test_eq(nested_attr(\[1, 2, 3\], ‘1’),2) + +``` python +b = {'a':1,'b':'v','c':{'d':5}} +test_eq(nested_attr(b,'b'),'v') +test_eq(nested_attr(b,'c.d'),5) +``` + +``` python +a = SimpleNamespace(b=(SimpleNamespace(c=1))) +test_eq(nested_attr(a, 'b.c'), getattr(getattr(a, 'b'), 'c')) +test_eq(nested_attr(a, 'b.d'), None) +test_eq(nested_attr(b, 'a'), 1) +``` + +------------------------------------------------------------------------ + +source + +### nested_setdefault + +> nested_setdefault (o, attr, default) + +*Same as `setdefault`, but if `attr` includes a `.`, then looks inside +nested objects* + +------------------------------------------------------------------------ + +source + +### nested_callable + +> nested_callable (o, attr) + +*Same as +[`nested_attr`](https://fastcore.fast.ai/basics.html#nested_attr) but if +not found will return `noop`* + +``` python +a = SimpleNamespace(b=(SimpleNamespace(c=1))) +test_eq(nested_callable(a, 'b.c'), getattr(getattr(a, 'b'), 'c')) +test_eq(nested_callable(a, 'b.d'), noop) +``` + +------------------------------------------------------------------------ + +source + +### nested_idx + +> nested_idx (coll, *idxs) + +*Index into nested collections, dicts, etc, with `idxs`* + +``` python +a = {'b':[1,{'c':2}]} +test_eq(nested_idx(a, 'nope'), None) +test_eq(nested_idx(a, 'nope', 'nup'), None) +test_eq(nested_idx(a, 'b', 3), None) +test_eq(nested_idx(a), a) +test_eq(nested_idx(a, 'b'), [1,{'c':2}]) +test_eq(nested_idx(a, 'b', 1), {'c':2}) +test_eq(nested_idx(a, 'b', 1, 'c'), 2) +``` + +``` python +a = SimpleNamespace(b=[1,{'c':2}]) +test_eq(nested_idx(a, 'nope'), None) +test_eq(nested_idx(a, 'nope', 'nup'), None) +test_eq(nested_idx(a, 'b', 3), None) +test_eq(nested_idx(a), a) +test_eq(nested_idx(a, 'b'), [1,{'c':2}]) +test_eq(nested_idx(a, 'b', 1), {'c':2}) +test_eq(nested_idx(a, 'b', 1, 'c'), 2) +``` + +------------------------------------------------------------------------ + +source + +### set_nested_idx + +> set_nested_idx (coll, value, *idxs) + +*Set value indexed like \`nested_idx* + +``` python +set_nested_idx(a, 3, 'b', 0) +test_eq(nested_idx(a, 'b', 0), 3) +``` + +------------------------------------------------------------------------ + +source + +### val2idx + +> val2idx (x) + +*Dict from value to index* + +``` python +test_eq(val2idx([1,2,3]), {3:2,1:0,2:1}) +``` + +------------------------------------------------------------------------ + +source + +### uniqueify + +> uniqueify (x, sort=False, bidir=False, start=None) + +*Unique elements in `x`, optional `sort`, optional return reverse +correspondence, optional prepend with elements.* + +``` python +t = [1,1,0,5,0,3] +test_eq(uniqueify(t),[1,0,5,3]) +test_eq(uniqueify(t, sort=True),[0,1,3,5]) +test_eq(uniqueify(t, start=[7,8,6]), [7,8,6,1,0,5,3]) +v,o = uniqueify(t, bidir=True) +test_eq(v,[1,0,5,3]) +test_eq(o,{1:0, 0: 1, 5: 2, 3: 3}) +v,o = uniqueify(t, sort=True, bidir=True) +test_eq(v,[0,1,3,5]) +test_eq(o,{0:0, 1: 1, 3: 2, 5: 3}) +``` + +------------------------------------------------------------------------ + +source + +### loop_first_last + +> loop_first_last (values) + +*Iterate and generate a tuple with a flag for first and last value.* + +``` python +test_eq(loop_first_last(range(3)), [(True,False,0), (False,False,1), (False,True,2)]) +``` + +------------------------------------------------------------------------ + +source + +### loop_first + +> loop_first (values) + +*Iterate and generate a tuple with a flag for first value.* + +``` python +test_eq(loop_first(range(3)), [(True,0), (False,1), (False,2)]) +``` + +------------------------------------------------------------------------ + +source + +### loop_last + +> loop_last (values) + +*Iterate and generate a tuple with a flag for last value.* + +``` python +test_eq(loop_last(range(3)), [(False,0), (False,1), (True,2)]) +``` + +------------------------------------------------------------------------ + +source + +### first_match + +> first_match (lst, f, default=None) + +*First element of `lst` matching predicate `f`, or `default` if none* + +``` python +a = [0,2,4,5,6,7,10] +test_eq(first_match(a, lambda o:o%2), 3) +``` + +------------------------------------------------------------------------ + +source + +### last_match + +> last_match (lst, f, default=None) + +*Last element of `lst` matching predicate `f`, or `default` if none* + +``` python +test_eq(last_match(a, lambda o:o%2), 5) +``` + +## fastuple + +A tuple with extended functionality. + +------------------------------------------------------------------------ + +source + +#### fastuple + +> fastuple (x=None, *rest) + +*A `tuple` with elementwise ops and more friendly **init** behavior* + +#### Friendly init behavior + +Common failure modes when trying to initialize a tuple in python: + +``` py +tuple(3) +> TypeError: 'int' object is not iterable +``` + +or + +``` py +tuple(3, 4) +> TypeError: tuple expected at most 1 arguments, got 2 +``` + +However, [`fastuple`](https://fastcore.fast.ai/basics.html#fastuple) +allows you to define tuples like this and in the usual way: + +``` python +test_eq(fastuple(3), (3,)) +test_eq(fastuple(3,4), (3, 4)) +test_eq(fastuple((3,4)), (3, 4)) +``` + +#### Elementwise operations + +------------------------------------------------------------------------ + +source + +##### fastuple.add + +> fastuple.add (*args) + +*`+` is already defined in `tuple` for concat, so use `add` instead* + +``` python +test_eq(fastuple.add((1,1),(2,2)), (3,3)) +test_eq_type(fastuple(1,1).add(2), fastuple(3,3)) +test_eq(fastuple('1','2').add('2'), fastuple('12','22')) +``` + +------------------------------------------------------------------------ + +source + +##### fastuple.mul + +> fastuple.mul (*args) + +*`*` is already defined in `tuple` for replicating, so use `mul` +instead* + +``` python +test_eq_type(fastuple(1,1).mul(2), fastuple(2,2)) +``` + +#### Other Elementwise Operations + +Additionally, the following elementwise operations are available: - +`le`: less than or equal - `eq`: equal - `gt`: greater than - `min`: +minimum of + +``` python +test_eq(fastuple(3,1).le(1), (False, True)) +test_eq(fastuple(3,1).eq(1), (False, True)) +test_eq(fastuple(3,1).gt(1), (True, False)) +test_eq(fastuple(3,1).min(2), (2,1)) +``` + +You can also do other elementwise operations like negate a +[`fastuple`](https://fastcore.fast.ai/basics.html#fastuple), or subtract +two [`fastuple`](https://fastcore.fast.ai/basics.html#fastuple)s: + +``` python +test_eq(-fastuple(1,2), (-1,-2)) +test_eq(~fastuple(1,0,1), (False,True,False)) + +test_eq(fastuple(1,1)-fastuple(2,2), (-1,-1)) +``` + +``` python +test_eq(type(fastuple(1)), fastuple) +test_eq_type(fastuple(1,2), fastuple(1,2)) +test_ne(fastuple(1,2), fastuple(1,3)) +test_eq(fastuple(), ()) +``` + +## Functions on Functions + +Utilities for functional programming or for defining, modifying, or +debugging functions. + +------------------------------------------------------------------------ + +source + +### bind + +> bind (func, *pargs, **pkwargs) + +*Same as `partial`, except you can use `arg0` `arg1` etc param +placeholders* + +[`bind`](https://fastcore.fast.ai/basics.html#bind) is the same as +`partial`, but also allows you to reorder positional arguments using +variable name(s) `arg{i}` where i refers to the zero-indexed positional +argument. [`bind`](https://fastcore.fast.ai/basics.html#bind) as +implemented currently only supports reordering of up to the first 5 +positional arguments. + +Consider the function `myfunc` below, which has 3 positional arguments. +These arguments can be referenced as `arg0`, `arg1`, and `arg1`, +respectively. + +``` python +def myfn(a,b,c,d=1,e=2): return(a,b,c,d,e) +``` + +In the below example we bind the positional arguments of `myfn` as +follows: + +- The second input `14`, referenced by `arg1`, is substituted for the + first positional argument. +- We supply a default value of `17` for the second positional argument. +- The first input `19`, referenced by `arg0`, is subsituted for the + third positional argument. + +``` python +test_eq(bind(myfn, arg1, 17, arg0, e=3)(19,14), (14,17,19,1,3)) +``` + +In this next example: + +- We set the default value to `17` for the first positional argument. +- The first input `19` refrenced by `arg0`, becomes the second + positional argument. +- The second input `14` becomes the third positional argument. +- We override the default the value for named argument `e` to `3`. + +``` python +test_eq(bind(myfn, 17, arg0, e=3)(19,14), (17,19,14,1,3)) +``` + +This is an example of using +[`bind`](https://fastcore.fast.ai/basics.html#bind) like `partial` and +do not reorder any arguments: + +``` python +test_eq(bind(myfn)(17,19,14), (17,19,14,1,2)) +``` + +[`bind`](https://fastcore.fast.ai/basics.html#bind) can also be used to +change default values. In the below example, we use the first input `3` +to override the default value of the named argument `e`, and supply +default values for the first three positional arguments: + +``` python +test_eq(bind(myfn, 17,19,14,e=arg0)(3), (17,19,14,1,3)) +``` + +------------------------------------------------------------------------ + +source + +### mapt + +> mapt (func, *iterables) + +*Tuplified `map`* + +``` python +t = [0,1,2,3] +test_eq(mapt(operator.neg, t), (0,-1,-2,-3)) +``` + +------------------------------------------------------------------------ + +source + +### map_ex + +> map_ex (iterable, f, *args, gen=False, **kwargs) + +*Like `map`, but use +[`bind`](https://fastcore.fast.ai/basics.html#bind), and supports `str` +and indexing* + +``` python +test_eq(map_ex(t,operator.neg), [0,-1,-2,-3]) +``` + +If `f` is a string then it is treated as a format string to create the +mapping: + +``` python +test_eq(map_ex(t, '#{}#'), ['#0#','#1#','#2#','#3#']) +``` + +If `f` is a dictionary (or anything supporting `__getitem__`) then it is +indexed to create the mapping: + +``` python +test_eq(map_ex(t, list('abcd')), list('abcd')) +``` + +You can also pass the same `arg` params that +[`bind`](https://fastcore.fast.ai/basics.html#bind) accepts: + +``` python +def f(a=None,b=None): return b +test_eq(map_ex(t, f, b=arg0), range(4)) +``` + +------------------------------------------------------------------------ + +source + +### compose + +> compose (*funcs, order=None) + +*Create a function that composes all functions in `funcs`, passing along +remaining `*args` and `**kwargs` to all* + +``` python +f1 = lambda o,p=0: (o*2)+p +f2 = lambda o,p=1: (o+1)/p +test_eq(f2(f1(3)), compose(f1,f2)(3)) +test_eq(f2(f1(3,p=3),p=3), compose(f1,f2)(3,p=3)) +test_eq(f2(f1(3, 3), 3), compose(f1,f2)(3, 3)) + +f1.order = 1 +test_eq(f1(f2(3)), compose(f1,f2, order="order")(3)) +``` + +------------------------------------------------------------------------ + +source + +### maps + +> maps (*args, retain=) + +*Like `map`, except funcs are composed first* + +``` python +test_eq(maps([1]), [1]) +test_eq(maps(operator.neg, [1,2]), [-1,-2]) +test_eq(maps(operator.neg, operator.neg, [1,2]), [1,2]) +``` + +------------------------------------------------------------------------ + +source + +### partialler + +> partialler (f, *args, order=None, **kwargs) + +*Like `functools.partial` but also copies over docstring* + +``` python +def _f(x,a=1): + "test func" + return x-a +_f.order=1 + +f = partialler(_f, 2) +test_eq(f.order, 1) +test_eq(f(3), -1) +f = partialler(_f, a=2, order=3) +test_eq(f.__doc__, "test func") +test_eq(f.order, 3) +test_eq(f(3), _f(3,2)) +``` + +``` python +class partial0: + "Like `partialler`, but args passed to callable are inserted at started, instead of at end" + def __init__(self, f, *args, order=None, **kwargs): + self.f,self.args,self.kwargs = f,args,kwargs + self.order = ifnone(order, getattr(f,'order',None)) + self.__doc__ = f.__doc__ + + def __call__(self, *args, **kwargs): return self.f(*args, *self.args, **kwargs, **self.kwargs) +``` + +``` python +f = partial0(_f, 2) +test_eq(f.order, 1) +test_eq(f(3), 1) # NB: different to `partialler` example +``` + +------------------------------------------------------------------------ + +source + +### instantiate + +> instantiate (t) + +*Instantiate `t` if it’s a type, otherwise do nothing* + +``` python +test_eq_type(instantiate(int), 0) +test_eq_type(instantiate(1), 1) +``` + +------------------------------------------------------------------------ + +source + +### using_attr + +> using_attr (f, attr) + +*Construct a function which applies `f` to the argument’s attribute +`attr`* + +``` python +t = Path('/a/b.txt') +f = using_attr(str.upper, 'name') +test_eq(f(t), 'B.TXT') +``` + +### Self (with an *uppercase* S) + +A Concise Way To Create Lambdas + +This is a concise way to create lambdas that are calling methods on an +object (note the capitalization!) + +`Self.sum()`, for instance, is a shortcut for `lambda o: o.sum()`. + +``` python +f = Self.sum() +x = np.array([3.,1]) +test_eq(f(x), 4.) + +# This is equivalent to above +f = lambda o: o.sum() +x = np.array([3.,1]) +test_eq(f(x), 4.) + +f = Self.argmin() +arr = np.array([1,2,3,4,5]) +test_eq(f(arr), arr.argmin()) + +f = Self.sum().is_integer() +x = np.array([3.,1]) +test_eq(f(x), True) + +f = Self.sum().real.is_integer() +x = np.array([3.,1]) +test_eq(f(x), True) + +f = Self.imag() +test_eq(f(3), 0) + +f = Self[1] +test_eq(f(x), 1) +``` + +`Self` is also callable, which creates a function which calls any +function passed to it, using the arguments passed to `Self`: + +``` python +def f(a, b=3): return a+b+2 +def g(a, b=3): return a*b +fg = Self(1,b=2) +list(map(fg, [f,g])) +``` + + [5, 2] + +## Patching + +------------------------------------------------------------------------ + +source + +### copy_func + +> copy_func (f) + +*Copy a non-builtin function (NB `copy.copy` does not work for this)* + +Sometimes it may be desirable to make a copy of a function that doesn’t +point to the original object. When you use Python’s built in `copy.copy` +or `copy.deepcopy` to copy a function, you get a reference to the +original object: + +``` python +import copy as cp +``` + +``` python +def foo(): pass +a = cp.copy(foo) +b = cp.deepcopy(foo) + +a.someattr = 'hello' # since a and b point at the same object, updating a will update b +test_eq(b.someattr, 'hello') + +assert a is foo and b is foo +``` + +However, with +[`copy_func`](https://fastcore.fast.ai/basics.html#copy_func), you can +retrieve a copy of a function without a reference to the original +object: + +``` python +c = copy_func(foo) # c is an indpendent object +assert c is not foo +``` + +``` python +def g(x, *, y=3): return x+y +test_eq(copy_func(g)(4), 7) +``` + +------------------------------------------------------------------------ + +source + +### patch_to + +> patch_to (cls, as_prop=False, cls_method=False) + +*Decorator: add `f` to `cls`* + +The `@patch_to` decorator allows you to [monkey +patch](https://stackoverflow.com/questions/5626193/what-is-monkey-patching) +a function into a class as a method: + +``` python +class _T3(int): pass + +@patch_to(_T3) +def func1(self, a): return self+a + +t = _T3(1) # we initialized `t` to a type int = 1 +test_eq(t.func1(2), 3) # we add 2 to `t`, so 2 + 1 = 3 +``` + +You can access instance properties in the usual way via `self`: + +``` python +class _T4(): + def __init__(self, g): self.g = g + +@patch_to(_T4) +def greet(self, x): return self.g + x + +t = _T4('hello ') # this sets self.g = 'hello ' +test_eq(t.greet('world'), 'hello world') #t.greet('world') will append 'world' to 'hello ' +``` + +You can instead specify that the method should be a class method by +setting `cls_method=True`: + +``` python +class _T5(int): attr = 3 # attr is a class attribute we will access in a later method + +@patch_to(_T5, cls_method=True) +def func(cls, x): return cls.attr + x # you can access class attributes in the normal way + +test_eq(_T5.func(4), 7) +``` + +Additionally you can specify that the function you want to patch should +be a class attribute with `as_prop=True`: + +``` python +@patch_to(_T5, as_prop=True) +def add_ten(self): return self + 10 + +t = _T5(4) +test_eq(t.add_ten, 14) +``` + +Instead of passing one class to the `@patch_to` decorator, you can pass +multiple classes in a tuple to simulteanously patch more than one class +with the same method: + +``` python +class _T6(int): pass +class _T7(int): pass + +@patch_to((_T6,_T7)) +def func_mult(self, a): return self*a + +t = _T6(2) +test_eq(t.func_mult(4), 8) +t = _T7(2) +test_eq(t.func_mult(4), 8) +``` + +------------------------------------------------------------------------ + +source + +### patch + +> patch (f=None, as_prop=False, cls_method=False) + +*Decorator: add `f` to the first parameter’s class (based on f’s type +annotations)* + +`@patch` is an alternative to `@patch_to` that allows you similarly +monkey patch class(es) by using [type +annotations](https://docs.python.org/3/library/typing.html): + +``` python +class _T8(int): pass + +@patch +def func(self:_T8, a): return self+a + +t = _T8(1) # we initilized `t` to a type int = 1 +test_eq(t.func(3), 4) # we add 3 to `t`, so 3 + 1 = 4 +test_eq(t.func.__qualname__, '_T8.func') +``` + +Similarly to +[`patch_to`](https://fastcore.fast.ai/basics.html#patch_to), you can +supply a union of classes instead of a single class in your type +annotations to patch multiple classes: + +``` python +class _T9(int): pass + +@patch +def func2(x:_T8|_T9, a): return x*a # will patch both _T8 and _T9 + +t = _T8(2) +test_eq(t.func2(4), 8) +test_eq(t.func2.__qualname__, '_T8.func2') + +t = _T9(2) +test_eq(t.func2(4), 8) +test_eq(t.func2.__qualname__, '_T9.func2') +``` + +Just like [`patch_to`](https://fastcore.fast.ai/basics.html#patch_to) +decorator you can use `as_prop` and `cls_method` parameters with +[`patch`](https://fastcore.fast.ai/basics.html#patch) decorator: + +``` python +@patch(as_prop=True) +def add_ten(self:_T5): return self + 10 + +t = _T5(4) +test_eq(t.add_ten, 14) +``` + +``` python +class _T5(int): attr = 3 # attr is a class attribute we will access in a later method + +@patch(cls_method=True) +def func(cls:_T5, x): return cls.attr + x # you can access class attributes in the normal way + +test_eq(_T5.func(4), 7) +``` + +------------------------------------------------------------------------ + +source + +### patch_property + +> patch_property (f) + +*Deprecated; use `patch(as_prop=True)` instead* + +Patching `classmethod` shouldn’t affect how python’s inheritance works + +``` python +class FastParent: pass + +@patch(cls_method=True) +def type_cls(cls: FastParent): return cls + +class FastChild(FastParent): pass + +parent = FastParent() +test_eq(parent.type_cls(), FastParent) + +child = FastChild() +test_eq(child.type_cls(), FastChild) +``` + +## Other Helpers + +------------------------------------------------------------------------ + +source + +### compile_re + +> compile_re (pat) + +*Compile `pat` if it’s not None* + +``` python +assert compile_re(None) is None +assert compile_re('a').match('ab') +``` + +------------------------------------------------------------------------ + +source + +#### ImportEnum + +> ImportEnum (value, names=None, module=None, qualname=None, type=None, +> start=1) + +*An `Enum` that can have its values imported* + +``` python +_T = ImportEnum('_T', {'foobar':1, 'goobar':2}) +_T.imports() +test_eq(foobar, _T.foobar) +test_eq(goobar, _T.goobar) +``` + +------------------------------------------------------------------------ + +source + +#### StrEnum + +> StrEnum (value, names=None, module=None, qualname=None, type=None, +> start=1) + +*An [`ImportEnum`](https://fastcore.fast.ai/basics.html#importenum) that +behaves like a `str`* + +------------------------------------------------------------------------ + +source + +### str_enum + +> str_enum (name, *vals) + +*Simplified creation of +[`StrEnum`](https://fastcore.fast.ai/basics.html#strenum) types* + +------------------------------------------------------------------------ + +source + +#### ValEnum + +> ValEnum (value, names=None, module=None, qualname=None, type=None, +> start=1) + +*An [`ImportEnum`](https://fastcore.fast.ai/basics.html#importenum) that +stringifies using values* + +``` python +_T = str_enum('_T', 'a', 'b') +test_eq(f'{_T.a}', 'a') +test_eq(_T.a, 'a') +test_eq(list(_T.__members__), ['a','b']) +print(_T.a, _T.a.upper()) +``` + + a A + +------------------------------------------------------------------------ + +source + +#### Stateful + +> Stateful (*args, **kwargs) + +*A base class/mixin for objects that should not serialize all their +state* + +``` python +class _T(Stateful): + def __init__(self): + super().__init__() + self.a=1 + self._state['test']=2 + +t = _T() +t2 = pickle.loads(pickle.dumps(t)) +test_eq(t.a,1) +test_eq(t._state['test'],2) +test_eq(t2.a,1) +test_eq(t2._state,{}) +``` + +Override `_init_state` to do any necessary setup steps that are required +during `__init__` or during deserialization (e.g. `pickle.load`). Here’s +an example of how +[`Stateful`](https://fastcore.fast.ai/basics.html#stateful) simplifies +the official Python example for [Handling Stateful +Objects](https://docs.python.org/3/library/pickle.html#handling-stateful-objects). + +``` python +class TextReader(Stateful): + """Print and number lines in a text file.""" + _stateattrs=('file',) + def __init__(self, filename): + self.filename,self.lineno = filename,0 + super().__init__() + + def readline(self): + self.lineno += 1 + line = self.file.readline() + if line: return f"{self.lineno}: {line.strip()}" + + def _init_state(self): + self.file = open(self.filename) + for _ in range(self.lineno): self.file.readline() +``` + +``` python +reader = TextReader("00_test.ipynb") +print(reader.readline()) +print(reader.readline()) + +new_reader = pickle.loads(pickle.dumps(reader)) +print(reader.readline()) +``` + + 1: { + 2: "cells": [ + 3: { + +------------------------------------------------------------------------ + +source + +### NotStr + +> NotStr (s) + +*Behaves like a `str`, but isn’t an instance of one* + +``` python +s = NotStr("hello") +assert not isinstance(s, str) +test_eq(s, 'hello') +test_eq(s*2, 'hellohello') +test_eq(len(s), 5) +``` + +------------------------------------------------------------------------ + +source + +#### PrettyString + +*Little hack to get strings to show properly in Jupyter.* + +Allow strings with special characters to render properly in Jupyter. +Without calling `print()` strings with special characters are displayed +like so: + +``` python +with_special_chars='a string\nwith\nnew\nlines and\ttabs' +with_special_chars +``` + + 'a string\nwith\nnew\nlines and\ttabs' + +We can correct this with +[`PrettyString`](https://fastcore.fast.ai/basics.html#prettystring): + +``` python +PrettyString(with_special_chars) +``` + + a string + with + new + lines and tabs + +------------------------------------------------------------------------ + +source + +### even_mults + +> even_mults (start, stop, n) + +*Build log-stepped array from `start` to +[`stop`](https://fastcore.fast.ai/basics.html#stop) in `n` steps.* + +``` python +test_eq(even_mults(2,8,3), [2,4,8]) +test_eq(even_mults(2,32,5), [2,4,8,16,32]) +test_eq(even_mults(2,8,1), 8) +``` + +------------------------------------------------------------------------ + +source + +### num_cpus + +> num_cpus () + +*Get number of cpus* + +``` python +num_cpus() +``` + + 10 + +------------------------------------------------------------------------ + +source + +### add_props + +> add_props (f, g=None, n=2) + +*Create properties passing each of `range(n)` to f* + +``` python +class _T(): a,b = add_props(lambda i,x:i*2) + +t = _T() +test_eq(t.a,0) +test_eq(t.b,2) +``` + +``` python +class _T(): + def __init__(self, v): self.v=v + def _set(i, self, v): self.v[i] = v + a,b = add_props(lambda i,x: x.v[i], _set) + +t = _T([0,2]) +test_eq(t.a,0) +test_eq(t.b,2) +t.a = t.a+1 +t.b = 3 +test_eq(t.a,1) +test_eq(t.b,3) +``` + +------------------------------------------------------------------------ + +source + +### str2bool + +> str2bool (s) + +*Case-insensitive convert string `s` too a bool +(`y`,`yes`,`t`,[`true`](https://fastcore.fast.ai/basics.html#true),`on`,`1`-\>`True`)* + +True values are ‘y’, ‘yes’, ‘t’, ‘true’, ‘on’, and ‘1’; false values are +‘n’, ‘no’, ‘f’, ‘false’, ‘off’, and ‘0’. Raises `ValueError` if ‘val’ is +anything else. + +``` python +for o in "y YES t True on 1".split(): assert str2bool(o) +for o in "n no FALSE off 0".split(): assert not str2bool(o) +for o in 0,None,'',False: assert not str2bool(o) +for o in 1,True: assert str2bool(o) +``` + +------------------------------------------------------------------------ + +source + +### str2int + +> str2int (s) + +*Convert `s` to an `int`* + +------------------------------------------------------------------------ + +source + +### str2float + +> str2float (s:str) + +*Convert `s` to a float* + +------------------------------------------------------------------------ + +source + +### str2list + +> str2list (s:str) + +*Convert `s` to a list* + +------------------------------------------------------------------------ + +source + +### str2date + +> str2date (s:str) + +*`date.fromisoformat` with empty string handling* + +------------------------------------------------------------------------ + +source + +### to_date + +> to_date (arg) + +------------------------------------------------------------------------ + +source + +### to_list + +> to_list (arg) + +------------------------------------------------------------------------ + +source + +### to_float + +> to_float (arg) + +------------------------------------------------------------------------ + +source + +### to_int + +> to_int (arg) + +------------------------------------------------------------------------ + +source + +### to_bool + +> to_bool (arg) + +------------------------------------------------------------------------ + +source + +### typed + +> typed (_func=None, cast=False) + +*Decorator to check param and return types at runtime, with optional +casting* + +[`typed`](https://fastcore.fast.ai/basics.html#typed) validates argument +types at **runtime**. This is in contrast to +[MyPy](http://mypy-lang.org/) which only offers static type checking. + +For example, a `TypeError` will be raised if we try to pass an integer +into the first argument of the below function: + +``` python +@typed +def discount(price:int, pct:float) -> float: + return (1-pct) * price + +with ExceptionExpected(TypeError): discount(100.0, .1) +``` + +You can have automatic casting based on heuristics by specifying +`typed(cast=True)`. If casting is not possible, a `TypeError` is raised. + +``` python +@typed(cast=True) +def discount(price:int, pct:float) -> float: + return (1-pct) * price + +assert 90.0 == discount(100.5, .1) # will auto cast 100.5 to the int 100 +assert 90.0 == discount(' 100 ', .1) # will auto cast the str "100" to the int 100 +with ExceptionExpected(TypeError): discount("a", .1) +``` + +We can also optionally allow multiple types by enumarating the types in +a tuple as illustrated below: + +``` python +@typed +def discount(price:int|float, pct:float): + return (1-pct) * price + +assert 90.0 == discount(100.0, .1) + +@typed(cast=True) +def discount(price:int|None, pct:float): + return (1-pct) * price + +assert 90.0 == discount(100.0, .1) +``` + +We currently do not support union types when casting. + +``` python +@typed(cast=True) +def discount(price:int|float, pct:float): + return (1-pct) * price + +with ExceptionExpected(AssertionError): assert 90.0 == discount("100.0", .1) +``` + +[`typed`](https://fastcore.fast.ai/basics.html#typed) works with +classes, too: + +``` python +class Foo: + @typed + def __init__(self, a:int, b: int, c:str): pass + @typed(cast=True) + def test(cls, d:str): return d + +with ExceptionExpected(TypeError): Foo(1, 2, 3) +assert isinstance(Foo(1,2, 'a string').test(10), str) +``` + +It also works with custom types. + +``` python +@typed +def test_foo(foo: Foo): pass + +with ExceptionExpected(TypeError): test_foo(1) +test_foo(Foo(1, 2, 'a string')) +``` + +``` python +class Bar: + @typed + def __init__(self, a:int): self.a = a +@typed(cast=True) +def test_bar(bar: Bar): return bar + +assert isinstance(test_bar(1), Bar) +test_eq(test_bar(1).a, 1) +with ExceptionExpected(TypeError): test_bar("foobar") +``` + +------------------------------------------------------------------------ + +source + +### exec_new + +> exec_new (code) + +*Execute `code` in a new environment and return it* + +``` python +g = exec_new('a=1') +test_eq(g['a'], 1) +``` + +------------------------------------------------------------------------ + +source + +### exec_import + +> exec_import (mod, sym) + +*Import `sym` from `mod` in a new environment* + +## Notebook functions + +------------------------------------------------------------------------ + +### ipython_shell + +> ipython_shell () + +*Same as `get_ipython` but returns `False` if not in IPython* + +------------------------------------------------------------------------ + +### in_ipython + +> in_ipython () + +*Check if code is running in some kind of IPython environment* + +------------------------------------------------------------------------ + +### in_colab + +> in_colab () + +*Check if the code is running in Google Colaboratory* + +------------------------------------------------------------------------ + +### in_jupyter + +> in_jupyter () + +*Check if the code is running in a jupyter notebook* + +------------------------------------------------------------------------ + +### in_notebook + +> in_notebook () + +*Check if the code is running in a jupyter notebook* + +These variables are available as booleans in `fastcore.basics` as +`IN_IPYTHON`, `IN_JUPYTER`, `IN_COLAB` and `IN_NOTEBOOK`. + +``` python +IN_IPYTHON, IN_JUPYTER, IN_COLAB, IN_NOTEBOOK +``` + + (True, True, False, True) diff --git a/dispatch.html b/dispatch.html new file mode 100644 index 00000000..cc906abc --- /dev/null +++ b/dispatch.html @@ -0,0 +1,1283 @@ + + + + + + + + + + +Type dispatch – fastcore + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ +
+ + +
+ + + +
+ +
+
+

Type dispatch

+
+ +
+
+ Basic single and dual parameter dispatch +
+
+ + +
+ + + + +
+ + + +
+ + + +
+
from nbdev.showdoc import *
+from fastcore.test import *
+from fastcore.nb_imports import *
+
+
+

Helpers

+
+

source

+
+

lenient_issubclass

+
+
 lenient_issubclass (cls, types)
+
+

If possible return whether cls is a subclass of types, otherwise return False.

+
+
assert not lenient_issubclass(typing.Collection, list)
+assert lenient_issubclass(list, typing.Collection)
+assert lenient_issubclass(typing.Collection, object)
+assert lenient_issubclass(typing.List, typing.Collection)
+assert not lenient_issubclass(typing.Collection, typing.List)
+assert not lenient_issubclass(object, typing.Callable)
+
+
+

source

+
+
+

sorted_topologically

+
+
 sorted_topologically (iterable, cmp=<built-in function lt>,
+                       reverse=False)
+
+

Return a new list containing all items from the iterable sorted topologically

+
+
td = [3, 1, 2, 5]
+test_eq(sorted_topologically(td), [1, 2, 3, 5])
+test_eq(sorted_topologically(td, reverse=True), [5, 3, 2, 1])
+
+
+
td = {int:1, numbers.Number:2, numbers.Integral:3}
+test_eq(sorted_topologically(td, cmp=lenient_issubclass), [int, numbers.Integral, numbers.Number])
+
+
+
td = [numbers.Integral, tuple, list, int, dict]
+td = sorted_topologically(td, cmp=lenient_issubclass)
+assert td.index(int) < td.index(numbers.Integral)
+
+
+
+
+

TypeDispatch

+

Type dispatch, or Multiple dispatch, allows you to change the way a function behaves based upon the input types it recevies. This is a prominent feature in some programming languages like Julia. For example, this is a conceptual example of how multiple dispatch works in Julia, returning different values depending on the input types of x and y:

+
collide_with(x::Asteroid, y::Asteroid) = ... 
+# deal with asteroid hitting asteroid
+
+collide_with(x::Asteroid, y::Spaceship) = ... 
+# deal with asteroid hitting spaceship
+
+collide_with(x::Spaceship, y::Asteroid) = ... 
+# deal with spaceship hitting asteroid
+
+collide_with(x::Spaceship, y::Spaceship) = ... 
+# deal with spaceship hitting spaceship
+

Type dispatch can be especially useful in data science, where you might allow different input types (i.e. numpy arrays and pandas dataframes) to function that processes data. Type dispatch allows you to have a common API for functions that do similar tasks.

+

The TypeDispatch class allows us to achieve type dispatch in Python. It contains a dictionary that maps types from type annotations to functions, which ensures that the proper function is called when passed inputs.

+
+

source

+
+

TypeDispatch

+
+
 TypeDispatch (funcs=(), bases=())
+
+

Dictionary-like object; __getitem__ matches keys of types using issubclass

+

To demonstrate how TypeDispatch works, we define a set of functions that accept a variety of input types, specified with different type annotations:

+
+
def f2(x:int, y:float): return x+y              #int and float for 2nd arg
+def f_nin(x:numbers.Integral)->int:  return x+1 #integral numeric
+def f_ni2(x:int): return x                      #integer
+def f_bll(x:bool|list): return x              #bool or list
+def f_num(x:numbers.Number): return x           #Number (root of numerics)
+
+

We can optionally initialize TypeDispatch with a list of functions we want to search. Printing an instance of TypeDispatch will display convenient mapping of types -> functions:

+
+
t = TypeDispatch([f_nin,f_ni2,f_num,f_bll,None])
+t
+
+
(bool,object) -> f_bll
+(int,object) -> f_ni2
+(Integral,object) -> f_nin
+(Number,object) -> f_num
+(list,object) -> f_bll
+(object,object) -> NoneType
+
+
+

Note that only the first two arguments are used for TypeDispatch. If your function only contains one argument, the second parameter will be shown as object. If you pass None into TypeDispatch, then this will be displayed as (object, object) -> NoneType.

+

TypeDispatch is a dictionary-like object, which means that you can retrieve a function by the associated type annotation. For example, the statement:

+
t[float]
+

Will return f_num because that is the matching function that has a type annotation that is a super-class of of float - numbers.Number:

+
+
assert issubclass(float, numbers.Number)
+test_eq(t[float], f_num)
+
+

The same is true for other types as well:

+
+
test_eq(t[np.int32], f_nin)
+test_eq(t[bool], f_bll)
+test_eq(t[list], f_bll)
+test_eq(t[np.int32], f_nin)
+
+

If you try to get a type that doesn’t match, TypeDispatch will return None:

+
+
test_eq(t[str], None)
+
+
+

source

+
+
+

TypeDispatch.add

+
+
 TypeDispatch.add (f)
+
+

Add type t and function f

+

This method allows you to add an additional function to an existing TypeDispatch instance :

+
+
def f_col(x:typing.Collection): return x
+t.add(f_col)
+test_eq(t[str], f_col)
+t
+
+
(bool,object) -> f_bll
+(int,object) -> f_ni2
+(Integral,object) -> f_nin
+(Number,object) -> f_num
+(list,object) -> f_bll
+(typing.Collection,object) -> f_col
+(object,object) -> NoneType
+
+
+

If you accidentally add the same function more than once things will still work as expected:

+
+
t.add(f_ni2) 
+test_eq(t[int], f_ni2)
+
+

However, if you add a function that has a type collision that raises an ambiguity, this will automatically resolve to the latest function added:

+
+
def f_ni3(z:int): return z # collides with f_ni2 with same type annotations
+t.add(f_ni3) 
+test_eq(t[int], f_ni3)
+
+
+

Using bases:

+

The argument bases can optionally accept a single instance of TypeDispatch or a collection (i.e. a tuple or list) of TypeDispatch objects. This can provide functionality similar to multiple inheritance.

+

These are searched for matching functions if no match in your list of functions:

+
+
def f_str(x:str): return x+'1'
+
+t = TypeDispatch([f_nin,f_ni2,f_num,f_bll,None])
+t2 = TypeDispatch(f_str, bases=t) # you can optionally supply a list of TypeDispatch objects for `bases`.
+t2
+
+
(str,object) -> f_str
+(bool,object) -> f_bll
+(int,object) -> f_ni2
+(Integral,object) -> f_nin
+(Number,object) -> f_num
+(list,object) -> f_bll
+(object,object) -> NoneType
+
+
+
+
test_eq(t2[int], f_ni2)       # searches `t` b/c not found in `t2`
+test_eq(t2[np.int32], f_nin)  # searches `t` b/c not found in `t2`
+test_eq(t2[float], f_num)     # searches `t` b/c not found in `t2`
+test_eq(t2[bool], f_bll)      # searches `t` b/c not found in `t2`
+test_eq(t2[str], f_str)       # found in `t`!
+test_eq(t2('a'), 'a1')        # found in `t`!, and uses __call__
+
+o = np.int32(1)
+test_eq(t2(o), 2)             # found in `t2` and uses __call__
+
+
+
+

Up To Two Arguments

+

TypeDispatch supports up to two arguments when searching for the appropriate function. The following functions f1 and f2 both have two parameters:

+
+
def f1(x:numbers.Integral, y): return x+1  #Integral is a numeric type
+def f2(x:int, y:float): return x+y
+t = TypeDispatch([f1,f2])
+t
+
+
(int,float) -> f2
+(Integral,object) -> f1
+
+
+

You can lookup functions from a TypeDispatch instance with two parameters like this:

+
+
test_eq(t[np.int32], f1)
+test_eq(t[int,float], f2)
+
+

Keep in mind that anything beyond the first two parameters are ignored, and any collisions will be resolved in favor of the most recent function added. In the below example, f1 is ignored in favor of f2 because the first two parameters have identical type hints:

+
+
def f1(a:str, b:int, c:list): return a
+def f2(a: str, b:int): return b
+t = TypeDispatch([f1,f2])
+test_eq(t[str, int], f2)
+t
+
+
(str,int) -> f2
+
+
+
+
+

Matching

+

Type Dispatch matches types with functions according to whether the supplied class is a subclass or the same class of the type annotation(s) of associated functions.

+

Let’s consider an example where we try to retrieve the function corresponding to types of [np.int32, float].

+

In this scenario, f2 will not be matched. This is because the first type annotation of f2, int, is not a superclass (or the same class) of np.int32:

+
+
def f1(x:numbers.Integral, y): return x+1
+def f2(x:int, y:float): return x+y
+t = TypeDispatch([f1,f2])
+
+assert not issubclass(np.int32, int)
+
+

Instead, f1 is a valid match, as its first argument is annoted with the type numbers.Integeral, which np.int32 is a subclass of:

+
+
assert issubclass(np.int32, numbers.Integral)
+test_eq(t[np.int32,float], f1)
+
+

In f1 , the 2nd parameter y is not annotated, which means TypeDispatch will match anything where the first argument matches int that is not matched with anything else:

+
+
assert issubclass(int, numbers.Integral) # int is a subclass of numbers.Integral
+test_eq(t[int], f1)
+test_eq(t[int,int], f1)
+
+

If no match is possible, None is returned:

+
+
test_eq(t[float,float], None)
+
+
+

source

+
+
+
+

TypeDispatch.__call__

+
+
 TypeDispatch.__call__ (*args, **kwargs)
+
+

Call self as a function.

+

TypeDispatch is also callable. When you call an instance of TypeDispatch, it will execute the relevant function:

+
+
def f_arr(x:np.ndarray): return x.sum()
+def f_int(x:np.int32): return x+1
+t = TypeDispatch([f_arr, f_int])
+
+arr = np.array([5,4,3,2,1])
+test_eq(t(arr), 15) # dispatches to f_arr
+
+o = np.int32(1)
+test_eq(t(o), 2) # dispatches to f_int
+assert t.first() is not None
+
+

You can also call an instance of of TypeDispatch when there are two parameters:

+
+
def f1(x:numbers.Integral, y): return x+1
+def f2(x:int, y:float): return x+y
+t = TypeDispatch([f1,f2])
+
+test_eq(t(3,2.0), 5)
+test_eq(t(3,2), 4)
+
+

When no match is found, a TypeDispatch instance becomes an identity function. This default behavior is leveraged by fasatai for data transformations to provide a sensible default when a matching function cannot be found.

+
+
test_eq(t('a'), 'a')
+
+
+

source

+
+
+

TypeDispatch.returns

+
+
 TypeDispatch.returns (x)
+
+

Get the return type of annotation of x.

+

You can optionally pass an object to TypeDispatch.returns and get the return type annotation back:

+
+
def f1(x:int) -> np.ndarray: return np.array(x)
+def f2(x:str) -> float: return List
+def f3(x:float): return List # f3 has no return type annotation
+
+t = TypeDispatch([f1, f2, f3])
+
+test_eq(t.returns(1), np.ndarray)  # dispatched to f1
+test_eq(t.returns('Hello'), float) # dispatched to f2
+test_eq(t.returns(1.0), None)      # dispatched to f3
+
+class _Test: pass
+_test = _Test()
+test_eq(t.returns(_test), None) # type `_Test` not found, so None returned
+
+
+

Using TypeDispatch With Methods

+

You can use TypeDispatch when defining methods as well:

+
+
def m_nin(self, x:str|numbers.Integral): return str(x)+'1'
+def m_bll(self, x:bool): self.foo='a'
+def m_num(self, x:numbers.Number): return x*2
+
+t = TypeDispatch([m_nin,m_num,m_bll])
+class A: f = t # set class attribute `f` equal to a TypeDispatch instance
+    
+a = A()
+test_eq(a.f(1), '11')  #dispatch to m_nin
+test_eq(a.f(1.), 2.)   #dispatch to m_num
+test_is(a.f.inst, a)
+
+a.f(False) # this triggers t.m_bll to run, which sets self.foo to 'a'
+test_eq(a.foo, 'a')
+
+

As discussed in TypeDispatch.__call__, when there is not a match, TypeDispatch.__call__ becomes an identity function. In the below example, a tuple does not match any type annotations so a tuple is returned:

+
+
test_eq(a.f(()), ())
+
+

We extend the previous example by using bases to add an additional method that supports tuples:

+
+
def m_tup(self, x:tuple): return x+(1,)
+t2 = TypeDispatch(m_tup, bases=t)
+
+class A2: f = t2
+a2 = A2()
+test_eq(a2.f(1), '11')
+test_eq(a2.f(1.), 2.)
+test_is(a2.f.inst, a2)
+a2.f(False)
+test_eq(a2.foo, 'a')
+test_eq(a2.f(()), (1,))
+
+
+
+

Using TypeDispatch With Class Methods

+

You can use TypeDispatch when defining class methods too:

+
+
def m_nin(cls, x:str|numbers.Integral): return str(x)+'1'
+def m_bll(cls, x:bool): cls.foo='a'
+def m_num(cls, x:numbers.Number): return x*2
+
+t = TypeDispatch([m_nin,m_num,m_bll])
+class A: f = t # set class attribute `f` equal to a TypeDispatch
+
+test_eq(A.f(1), '11')  #dispatch to m_nin
+test_eq(A.f(1.), 2.)   #dispatch to m_num
+test_is(A.f.owner, A)
+
+A.f(False) # this triggers t.m_bll to run, which sets A.foo to 'a'
+test_eq(A.foo, 'a')
+
+
+
+
+
+

typedispatch Decorator

+
+

source

+
+

DispatchReg

+
+
 DispatchReg ()
+
+

A global registry for TypeDispatch objects keyed by function name

+
+
@typedispatch
+def f_td_test(x, y): return f'{x}{y}'
+@typedispatch
+def f_td_test(x:numbers.Integral|int, y): return x+1
+@typedispatch
+def f_td_test(x:int, y:float): return x+y
+@typedispatch
+def f_td_test(x:int, y:int): return x*y
+
+test_eq(f_td_test(3,2.0), 5)
+assert issubclass(int, numbers.Integral)
+test_eq(f_td_test(3,2), 6)
+
+test_eq(f_td_test('a','b'), 'ab')
+
+
+

Using typedispatch With other decorators

+

You can use typedispatch with classmethod and staticmethod decorator

+
+
class A:
+    @typedispatch
+    def f_td_test(self, x:numbers.Integral, y): return x+1
+    @typedispatch
+    @classmethod
+    def f_td_test(cls, x:int, y:float): return x+y
+    @typedispatch
+    @staticmethod
+    def f_td_test(x:int, y:int): return x*y
+    
+test_eq(A.f_td_test(3,2), 6)
+test_eq(A.f_td_test(3,2.0), 5)
+test_eq(A().f_td_test(3,'2.0'), 4)
+
+
+
+
+
+

Casting

+

Now that we can dispatch on types, let’s make it easier to cast objects to a different type.

+
+

source

+
+

retain_meta

+
+
 retain_meta (x, res, as_copy=False)
+
+

Call res.set_meta(x), if it exists

+
+

source

+
+
+

default_set_meta

+
+
 default_set_meta (x, as_copy=False)
+
+

Copy over _meta from x to res, if it’s missing

+
+
+
+

(object,object) -> cast

+

Dictionary-like object; __getitem__ matches keys of types using issubclass

+

This works both for plain python classes:…

+
+
mk_class('_T1', 'a')   # mk_class is a fastai utility that constructs a class.
+class _T2(_T1): pass
+
+t = _T1(a=1)
+t2 = cast(t, _T2)        
+assert t2 is t            # t2 refers to the same object as t
+assert isinstance(t, _T2) # t also changed in-place
+assert isinstance(t2, _T2)
+
+test_eq_type(_T2(a=1), t2)
+
+

…as well as for arrays and tensors.

+
+
class _T1(ndarray): pass
+
+t = array([1])
+t2 = cast(t, _T1)
+test_eq(array([1]), t2)
+test_eq(_T1, type(t2))
+
+

To customize casting for other types, define a separate cast function with typedispatch for your type.

+
+

source

+
+
+

retain_type

+
+
 retain_type (new, old=None, typ=None, as_copy=False)
+
+

Cast new to type of old or typ if it’s a superclass

+
+
class _T(tuple): pass
+a = _T((1,2))
+b = tuple((1,2))
+c = retain_type(b, typ=_T)
+test_eq_type(c, a)
+
+

If old has a _meta attribute, its content is passed when casting new to the type of old. In the below example, only the attribute a, but not other_attr is kept, because other_attr is not in _meta:

+
+
class _A():
+    set_meta = default_set_meta
+    def __init__(self, t): self.t=t
+
+class _B1(_A):
+    def __init__(self, t, a=1):
+        super().__init__(t)
+        self._meta = {'a':a}
+        self.other_attr = 'Hello' # will not be kept after casting.
+        
+x = _B1(1, a=2)
+b = _A(1)
+c = retain_type(b, old=x)
+test_eq(c._meta, {'a': 2})
+assert not getattr(c, 'other_attr', None)
+
+
+

source

+
+
+

retain_types

+
+
 retain_types (new, old=None, typs=None)
+
+

Cast each item of new to type of matching item in old if it’s a superclass

+
+
class T(tuple): pass
+
+t1,t2 = retain_types((1,(1,(1,1))), (2,T((2,T((3,4))))))
+test_eq_type(t1, 1)
+test_eq_type(t2, T((1,T((1,1)))))
+
+t1,t2 = retain_types((1,(1,(1,1))), typs = {tuple: [int, {T: [int, {T: [int,int]}]}]})
+test_eq_type(t1, 1)
+test_eq_type(t2, T((1,T((1,1)))))
+
+
+

source

+
+
+

explode_types

+
+
 explode_types (o)
+
+

Return the type of o, potentially in nested dictionaries for thing that are listy

+
+
test_eq(explode_types((2,T((2,T((3,4)))))), {tuple: [int, {T: [int, {T: [int,int]}]}]})
+
+ + +
+
+ +
+ +
+ + + + + \ No newline at end of file diff --git a/dispatch.html.md b/dispatch.html.md new file mode 100644 index 00000000..7a9f8195 --- /dev/null +++ b/dispatch.html.md @@ -0,0 +1,722 @@ +# Type dispatch + + + + +``` python +from nbdev.showdoc import * +from fastcore.test import * +from fastcore.nb_imports import * +``` + +## Helpers + +------------------------------------------------------------------------ + +source + +### lenient_issubclass + +> lenient_issubclass (cls, types) + +*If possible return whether `cls` is a subclass of `types`, otherwise +return False.* + +``` python +assert not lenient_issubclass(typing.Collection, list) +assert lenient_issubclass(list, typing.Collection) +assert lenient_issubclass(typing.Collection, object) +assert lenient_issubclass(typing.List, typing.Collection) +assert not lenient_issubclass(typing.Collection, typing.List) +assert not lenient_issubclass(object, typing.Callable) +``` + +------------------------------------------------------------------------ + +source + +### sorted_topologically + +> sorted_topologically (iterable, cmp=, +> reverse=False) + +*Return a new list containing all items from the iterable sorted +topologically* + +``` python +td = [3, 1, 2, 5] +test_eq(sorted_topologically(td), [1, 2, 3, 5]) +test_eq(sorted_topologically(td, reverse=True), [5, 3, 2, 1]) +``` + +``` python +td = {int:1, numbers.Number:2, numbers.Integral:3} +test_eq(sorted_topologically(td, cmp=lenient_issubclass), [int, numbers.Integral, numbers.Number]) +``` + +``` python +td = [numbers.Integral, tuple, list, int, dict] +td = sorted_topologically(td, cmp=lenient_issubclass) +assert td.index(int) < td.index(numbers.Integral) +``` + +## TypeDispatch + +Type dispatch, or [Multiple +dispatch](https://en.wikipedia.org/wiki/Multiple_dispatch#Julia), allows +you to change the way a function behaves based upon the input types it +recevies. This is a prominent feature in some programming languages like +Julia. For example, this is a [conceptual +example](https://en.wikipedia.org/wiki/Multiple_dispatch#Julia) of how +multiple dispatch works in Julia, returning different values depending +on the input types of x and y: + +``` julia +collide_with(x::Asteroid, y::Asteroid) = ... +# deal with asteroid hitting asteroid + +collide_with(x::Asteroid, y::Spaceship) = ... +# deal with asteroid hitting spaceship + +collide_with(x::Spaceship, y::Asteroid) = ... +# deal with spaceship hitting asteroid + +collide_with(x::Spaceship, y::Spaceship) = ... +# deal with spaceship hitting spaceship +``` + +Type dispatch can be especially useful in data science, where you might +allow different input types (i.e. numpy arrays and pandas dataframes) to +function that processes data. Type dispatch allows you to have a common +API for functions that do similar tasks. + +The +[`TypeDispatch`](https://fastcore.fast.ai/dispatch.html#typedispatch) +class allows us to achieve type dispatch in Python. It contains a +dictionary that maps types from type annotations to functions, which +ensures that the proper function is called when passed inputs. + +------------------------------------------------------------------------ + +source + +### TypeDispatch + +> TypeDispatch (funcs=(), bases=()) + +*Dictionary-like object; `__getitem__` matches keys of types using +`issubclass`* + +To demonstrate how +[`TypeDispatch`](https://fastcore.fast.ai/dispatch.html#typedispatch) +works, we define a set of functions that accept a variety of input +types, specified with different type annotations: + +``` python +def f2(x:int, y:float): return x+y #int and float for 2nd arg +def f_nin(x:numbers.Integral)->int: return x+1 #integral numeric +def f_ni2(x:int): return x #integer +def f_bll(x:bool|list): return x #bool or list +def f_num(x:numbers.Number): return x #Number (root of numerics) +``` + +We can optionally initialize +[`TypeDispatch`](https://fastcore.fast.ai/dispatch.html#typedispatch) +with a list of functions we want to search. Printing an instance of +[`TypeDispatch`](https://fastcore.fast.ai/dispatch.html#typedispatch) +will display convenient mapping of types -\> functions: + +``` python +t = TypeDispatch([f_nin,f_ni2,f_num,f_bll,None]) +t +``` + + (bool,object) -> f_bll + (int,object) -> f_ni2 + (Integral,object) -> f_nin + (Number,object) -> f_num + (list,object) -> f_bll + (object,object) -> NoneType + +Note that only the first two arguments are used for +[`TypeDispatch`](https://fastcore.fast.ai/dispatch.html#typedispatch). +If your function only contains one argument, the second parameter will +be shown as `object`. If you pass `None` into +[`TypeDispatch`](https://fastcore.fast.ai/dispatch.html#typedispatch), +then this will be displayed as `(object, object) -> NoneType`. + +[`TypeDispatch`](https://fastcore.fast.ai/dispatch.html#typedispatch) is +a dictionary-like object, which means that you can retrieve a function +by the associated type annotation. For example, the statement: + +``` py +t[float] +``` + +Will return `f_num` because that is the matching function that has a +type annotation that is a super-class of of `float` - `numbers.Number`: + +``` python +assert issubclass(float, numbers.Number) +test_eq(t[float], f_num) +``` + +The same is true for other types as well: + +``` python +test_eq(t[np.int32], f_nin) +test_eq(t[bool], f_bll) +test_eq(t[list], f_bll) +test_eq(t[np.int32], f_nin) +``` + +If you try to get a type that doesn’t match, +[`TypeDispatch`](https://fastcore.fast.ai/dispatch.html#typedispatch) +will return `None`: + +``` python +test_eq(t[str], None) +``` + +------------------------------------------------------------------------ + +source + +### TypeDispatch.add + +> TypeDispatch.add (f) + +*Add type `t` and function `f`* + +This method allows you to add an additional function to an existing +[`TypeDispatch`](https://fastcore.fast.ai/dispatch.html#typedispatch) +instance : + +``` python +def f_col(x:typing.Collection): return x +t.add(f_col) +test_eq(t[str], f_col) +t +``` + + (bool,object) -> f_bll + (int,object) -> f_ni2 + (Integral,object) -> f_nin + (Number,object) -> f_num + (list,object) -> f_bll + (typing.Collection,object) -> f_col + (object,object) -> NoneType + +If you accidentally add the same function more than once things will +still work as expected: + +``` python +t.add(f_ni2) +test_eq(t[int], f_ni2) +``` + +However, if you add a function that has a type collision that raises an +ambiguity, this will automatically resolve to the latest function added: + +``` python +def f_ni3(z:int): return z # collides with f_ni2 with same type annotations +t.add(f_ni3) +test_eq(t[int], f_ni3) +``` + +#### Using `bases`: + +The argument `bases` can optionally accept a single instance of +[`TypeDispatch`](https://fastcore.fast.ai/dispatch.html#typedispatch) or +a collection (i.e. a tuple or list) of +[`TypeDispatch`](https://fastcore.fast.ai/dispatch.html#typedispatch) +objects. This can provide functionality similar to multiple inheritance. + +These are searched for matching functions if no match in your list of +functions: + +``` python +def f_str(x:str): return x+'1' + +t = TypeDispatch([f_nin,f_ni2,f_num,f_bll,None]) +t2 = TypeDispatch(f_str, bases=t) # you can optionally supply a list of TypeDispatch objects for `bases`. +t2 +``` + + (str,object) -> f_str + (bool,object) -> f_bll + (int,object) -> f_ni2 + (Integral,object) -> f_nin + (Number,object) -> f_num + (list,object) -> f_bll + (object,object) -> NoneType + +``` python +test_eq(t2[int], f_ni2) # searches `t` b/c not found in `t2` +test_eq(t2[np.int32], f_nin) # searches `t` b/c not found in `t2` +test_eq(t2[float], f_num) # searches `t` b/c not found in `t2` +test_eq(t2[bool], f_bll) # searches `t` b/c not found in `t2` +test_eq(t2[str], f_str) # found in `t`! +test_eq(t2('a'), 'a1') # found in `t`!, and uses __call__ + +o = np.int32(1) +test_eq(t2(o), 2) # found in `t2` and uses __call__ +``` + +#### Up To Two Arguments + +[`TypeDispatch`](https://fastcore.fast.ai/dispatch.html#typedispatch) +supports up to two arguments when searching for the appropriate +function. The following functions `f1` and `f2` both have two +parameters: + +``` python +def f1(x:numbers.Integral, y): return x+1 #Integral is a numeric type +def f2(x:int, y:float): return x+y +t = TypeDispatch([f1,f2]) +t +``` + + (int,float) -> f2 + (Integral,object) -> f1 + +You can lookup functions from a +[`TypeDispatch`](https://fastcore.fast.ai/dispatch.html#typedispatch) +instance with two parameters like this: + +``` python +test_eq(t[np.int32], f1) +test_eq(t[int,float], f2) +``` + +Keep in mind that anything beyond the first two parameters are ignored, +and any collisions will be resolved in favor of the most recent function +added. In the below example, `f1` is ignored in favor of `f2` because +the first two parameters have identical type hints: + +``` python +def f1(a:str, b:int, c:list): return a +def f2(a: str, b:int): return b +t = TypeDispatch([f1,f2]) +test_eq(t[str, int], f2) +t +``` + + (str,int) -> f2 + +#### Matching + +`Type Dispatch` matches types with functions according to whether the +supplied class is a subclass or the same class of the type annotation(s) +of associated functions. + +Let’s consider an example where we try to retrieve the function +corresponding to types of `[np.int32, float]`. + +In this scenario, `f2` will not be matched. This is because the first +type annotation of `f2`, `int`, is not a superclass (or the same class) +of `np.int32`: + +``` python +def f1(x:numbers.Integral, y): return x+1 +def f2(x:int, y:float): return x+y +t = TypeDispatch([f1,f2]) + +assert not issubclass(np.int32, int) +``` + +Instead, `f1` is a valid match, as its first argument is annoted with +the type `numbers.Integeral`, which `np.int32` is a subclass of: + +``` python +assert issubclass(np.int32, numbers.Integral) +test_eq(t[np.int32,float], f1) +``` + +In `f1` , the 2nd parameter `y` is not annotated, which means +[`TypeDispatch`](https://fastcore.fast.ai/dispatch.html#typedispatch) +will match anything where the first argument matches `int` that is not +matched with anything else: + +``` python +assert issubclass(int, numbers.Integral) # int is a subclass of numbers.Integral +test_eq(t[int], f1) +test_eq(t[int,int], f1) +``` + +If no match is possible, `None` is returned: + +``` python +test_eq(t[float,float], None) +``` + +------------------------------------------------------------------------ + +source + +### TypeDispatch.\_\_call\_\_ + +> TypeDispatch.__call__ (*args, **kwargs) + +*Call self as a function.* + +[`TypeDispatch`](https://fastcore.fast.ai/dispatch.html#typedispatch) is +also callable. When you call an instance of +[`TypeDispatch`](https://fastcore.fast.ai/dispatch.html#typedispatch), +it will execute the relevant function: + +``` python +def f_arr(x:np.ndarray): return x.sum() +def f_int(x:np.int32): return x+1 +t = TypeDispatch([f_arr, f_int]) + +arr = np.array([5,4,3,2,1]) +test_eq(t(arr), 15) # dispatches to f_arr + +o = np.int32(1) +test_eq(t(o), 2) # dispatches to f_int +assert t.first() is not None +``` + +You can also call an instance of of +[`TypeDispatch`](https://fastcore.fast.ai/dispatch.html#typedispatch) +when there are two parameters: + +``` python +def f1(x:numbers.Integral, y): return x+1 +def f2(x:int, y:float): return x+y +t = TypeDispatch([f1,f2]) + +test_eq(t(3,2.0), 5) +test_eq(t(3,2), 4) +``` + +When no match is found, a +[`TypeDispatch`](https://fastcore.fast.ai/dispatch.html#typedispatch) +instance becomes an identity function. This default behavior is +leveraged by fasatai for data transformations to provide a sensible +default when a matching function cannot be found. + +``` python +test_eq(t('a'), 'a') +``` + +------------------------------------------------------------------------ + +source + +### TypeDispatch.returns + +> TypeDispatch.returns (x) + +*Get the return type of annotation of `x`.* + +You can optionally pass an object to +[`TypeDispatch.returns`](https://fastcore.fast.ai/dispatch.html#typedispatch.returns) +and get the return type annotation back: + +``` python +def f1(x:int) -> np.ndarray: return np.array(x) +def f2(x:str) -> float: return List +def f3(x:float): return List # f3 has no return type annotation + +t = TypeDispatch([f1, f2, f3]) + +test_eq(t.returns(1), np.ndarray) # dispatched to f1 +test_eq(t.returns('Hello'), float) # dispatched to f2 +test_eq(t.returns(1.0), None) # dispatched to f3 + +class _Test: pass +_test = _Test() +test_eq(t.returns(_test), None) # type `_Test` not found, so None returned +``` + +#### Using TypeDispatch With Methods + +You can use +[`TypeDispatch`](https://fastcore.fast.ai/dispatch.html#typedispatch) +when defining methods as well: + +``` python +def m_nin(self, x:str|numbers.Integral): return str(x)+'1' +def m_bll(self, x:bool): self.foo='a' +def m_num(self, x:numbers.Number): return x*2 + +t = TypeDispatch([m_nin,m_num,m_bll]) +class A: f = t # set class attribute `f` equal to a TypeDispatch instance + +a = A() +test_eq(a.f(1), '11') #dispatch to m_nin +test_eq(a.f(1.), 2.) #dispatch to m_num +test_is(a.f.inst, a) + +a.f(False) # this triggers t.m_bll to run, which sets self.foo to 'a' +test_eq(a.foo, 'a') +``` + +As discussed in +[`TypeDispatch.__call__`](https://fastcore.fast.ai/dispatch.html#typedispatch.__call__), +when there is not a match, +[`TypeDispatch.__call__`](https://fastcore.fast.ai/dispatch.html#typedispatch.__call__) +becomes an identity function. In the below example, a tuple does not +match any type annotations so a tuple is returned: + +``` python +test_eq(a.f(()), ()) +``` + +We extend the previous example by using `bases` to add an additional +method that supports tuples: + +``` python +def m_tup(self, x:tuple): return x+(1,) +t2 = TypeDispatch(m_tup, bases=t) + +class A2: f = t2 +a2 = A2() +test_eq(a2.f(1), '11') +test_eq(a2.f(1.), 2.) +test_is(a2.f.inst, a2) +a2.f(False) +test_eq(a2.foo, 'a') +test_eq(a2.f(()), (1,)) +``` + +#### Using TypeDispatch With Class Methods + +You can use +[`TypeDispatch`](https://fastcore.fast.ai/dispatch.html#typedispatch) +when defining class methods too: + +``` python +def m_nin(cls, x:str|numbers.Integral): return str(x)+'1' +def m_bll(cls, x:bool): cls.foo='a' +def m_num(cls, x:numbers.Number): return x*2 + +t = TypeDispatch([m_nin,m_num,m_bll]) +class A: f = t # set class attribute `f` equal to a TypeDispatch + +test_eq(A.f(1), '11') #dispatch to m_nin +test_eq(A.f(1.), 2.) #dispatch to m_num +test_is(A.f.owner, A) + +A.f(False) # this triggers t.m_bll to run, which sets A.foo to 'a' +test_eq(A.foo, 'a') +``` + +## typedispatch Decorator + +------------------------------------------------------------------------ + +source + +### DispatchReg + +> DispatchReg () + +*A global registry for +[`TypeDispatch`](https://fastcore.fast.ai/dispatch.html#typedispatch) +objects keyed by function name* + +``` python +@typedispatch +def f_td_test(x, y): return f'{x}{y}' +@typedispatch +def f_td_test(x:numbers.Integral|int, y): return x+1 +@typedispatch +def f_td_test(x:int, y:float): return x+y +@typedispatch +def f_td_test(x:int, y:int): return x*y + +test_eq(f_td_test(3,2.0), 5) +assert issubclass(int, numbers.Integral) +test_eq(f_td_test(3,2), 6) + +test_eq(f_td_test('a','b'), 'ab') +``` + +#### Using typedispatch With other decorators + +You can use `typedispatch` with `classmethod` and `staticmethod` +decorator + +``` python +class A: + @typedispatch + def f_td_test(self, x:numbers.Integral, y): return x+1 + @typedispatch + @classmethod + def f_td_test(cls, x:int, y:float): return x+y + @typedispatch + @staticmethod + def f_td_test(x:int, y:int): return x*y + +test_eq(A.f_td_test(3,2), 6) +test_eq(A.f_td_test(3,2.0), 5) +test_eq(A().f_td_test(3,'2.0'), 4) +``` + +## Casting + +Now that we can dispatch on types, let’s make it easier to cast objects +to a different type. + +------------------------------------------------------------------------ + +source + +### retain_meta + +> retain_meta (x, res, as_copy=False) + +*Call `res.set_meta(x)`, if it exists* + +------------------------------------------------------------------------ + +source + +### default_set_meta + +> default_set_meta (x, as_copy=False) + +*Copy over `_meta` from `x` to `res`, if it’s missing* + +------------------------------------------------------------------------ + +### (object,object) -\> cast + +*Dictionary-like object; `__getitem__` matches keys of types using +`issubclass`* + +This works both for plain python classes:… + +``` python +mk_class('_T1', 'a') # mk_class is a fastai utility that constructs a class. +class _T2(_T1): pass + +t = _T1(a=1) +t2 = cast(t, _T2) +assert t2 is t # t2 refers to the same object as t +assert isinstance(t, _T2) # t also changed in-place +assert isinstance(t2, _T2) + +test_eq_type(_T2(a=1), t2) +``` + +…as well as for arrays and tensors. + +``` python +class _T1(ndarray): pass + +t = array([1]) +t2 = cast(t, _T1) +test_eq(array([1]), t2) +test_eq(_T1, type(t2)) +``` + +To customize casting for other types, define a separate +[`cast`](https://fastcore.fast.ai/dispatch.html#cast) function with +`typedispatch` for your type. + +------------------------------------------------------------------------ + +source + +### retain_type + +> retain_type (new, old=None, typ=None, as_copy=False) + +*Cast `new` to type of `old` or `typ` if it’s a superclass* + +``` python +class _T(tuple): pass +a = _T((1,2)) +b = tuple((1,2)) +c = retain_type(b, typ=_T) +test_eq_type(c, a) +``` + +If `old` has a `_meta` attribute, its content is passed when casting +`new` to the type of `old`. In the below example, only the attribute +`a`, but not `other_attr` is kept, because `other_attr` is not in +`_meta`: + +``` python +class _A(): + set_meta = default_set_meta + def __init__(self, t): self.t=t + +class _B1(_A): + def __init__(self, t, a=1): + super().__init__(t) + self._meta = {'a':a} + self.other_attr = 'Hello' # will not be kept after casting. + +x = _B1(1, a=2) +b = _A(1) +c = retain_type(b, old=x) +test_eq(c._meta, {'a': 2}) +assert not getattr(c, 'other_attr', None) +``` + +------------------------------------------------------------------------ + +source + +### retain_types + +> retain_types (new, old=None, typs=None) + +*Cast each item of `new` to type of matching item in `old` if it’s a +superclass* + +``` python +class T(tuple): pass + +t1,t2 = retain_types((1,(1,(1,1))), (2,T((2,T((3,4)))))) +test_eq_type(t1, 1) +test_eq_type(t2, T((1,T((1,1))))) + +t1,t2 = retain_types((1,(1,(1,1))), typs = {tuple: [int, {T: [int, {T: [int,int]}]}]}) +test_eq_type(t1, 1) +test_eq_type(t2, T((1,T((1,1))))) +``` + +------------------------------------------------------------------------ + +source + +### explode_types + +> explode_types (o) + +*Return the type of `o`, potentially in nested dictionaries for thing +that are listy* + +``` python +test_eq(explode_types((2,T((2,T((3,4)))))), {tuple: [int, {T: [int, {T: [int,int]}]}]}) +``` diff --git a/docments.html b/docments.html new file mode 100644 index 00000000..1e416a38 --- /dev/null +++ b/docments.html @@ -0,0 +1,1104 @@ + + + + + + + + + + +Docments – fastcore + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ +
+ + +
+ + + +
+ +
+
+

Docments

+
+ +
+
+ Document parameters using comments. +
+
+ + +
+ + + + +
+ + + +
+ + + +

docments provides programmatic access to comments in function parameters and return types. It can be used to create more developer-friendly documentation, CLI, etc tools.

+
+

Why?

+

Without docments, if you want to document your parameters, you have to repeat param names in docstrings, since they’re already in the function signature. The parameters have to be kept synchronized in the two places as you change your code. Readers of your code have to look back and forth between two places to understand what’s happening. So it’s more work for you, and for your users.

+

Furthermore, to have parameter documentation formatted nicely without docments, you have to use special magic docstring formatting, often with odd quirks, which is a pain to create and maintain, and awkward to read in code. For instance, using numpy-style documentation:

+
+
def add_np(a:int, b:int=0)->int:
+    """The sum of two numbers.
+    
+    Used to demonstrate numpy-style docstrings.
+
+Parameters
+----------
+a : int
+    the 1st number to add
+b : int
+    the 2nd number to add (default: 0)
+
+Returns
+-------
+int
+    the result of adding `a` to `b`"""
+    return a+b
+
+

By comparison, here’s the same thing using docments:

+
+
def add(
+    a:int, # the 1st number to add
+    b=0,   # the 2nd number to add
+)->int:    # the result of adding `a` to `b`
+    "The sum of two numbers."
+    return a+b
+
+
+
+

Numpy docstring helper functions

+

docments also supports numpy-style docstrings, or a mix or numpy-style and docments parameter documentation. The functions in this section help get and parse this information.

+
+

source

+
+

docstring

+
+
 docstring (sym)
+
+

Get docstring for sym for functions ad classes

+
+
test_eq(docstring(add), "The sum of two numbers.")
+
+
+

source

+
+
+

parse_docstring

+
+
 parse_docstring (sym)
+
+

Parse a numpy-style docstring in sym

+
+
# parse_docstring(add_np)
+
+
+

source

+
+
+

isdataclass

+
+
 isdataclass (s)
+
+

Check if s is a dataclass but not a dataclass’ instance

+
+

source

+
+
+

get_dataclass_source

+
+
 get_dataclass_source (s)
+
+

Get source code for dataclass s

+
+

source

+
+
+

get_source

+
+
 get_source (s)
+
+

Get source code for string, function object or dataclass s

+
+

source

+
+
+

get_name

+
+
 get_name (obj)
+
+

Get the name of obj

+
+
test_eq(get_name(in_ipython), 'in_ipython')
+test_eq(get_name(L.map), 'map')
+
+
+

source

+
+
+

qual_name

+
+
 qual_name (obj)
+
+

Get the qualified name of obj

+
+
assert qual_name(docscrape) == 'fastcore.docscrape'
+
+
+
+
+

Docments

+
+

source

+
+

docments

+
+
 docments (elt, full=False, returns=True, eval_str=False)
+
+

Generates a docment

+

The returned dict has parameter names as keys, docments as values. The return value comment appears in the return, unless returns=False. Using the add definition above, we get:

+
+
def add(
+    a:int, # the 1st number to add
+    b=0,   # the 2nd number to add
+)->int:    # the result of adding `a` to `b`
+    "The sum of two numbers."
+    return a+b
+
+docments(add)
+
+
{ 'a': 'the 1st number to add',
+  'b': 'the 2nd number to add',
+  'return': 'the result of adding `a` to `b`'}
+
+
+

If you pass full=True, the values are dict of defaults, types, and docments as values. Note that the type annotation is inferred from the default value, if the annotation is empty and a default is supplied.

+
+
docments(add, full=True)
+
+
{ 'a': { 'anno': <class 'int'>,
+         'default': <class 'inspect._empty'>,
+         'docment': 'the 1st number to add'},
+  'b': { 'anno': <class 'int'>,
+         'default': 0,
+         'docment': 'the 2nd number to add'},
+  'return': { 'anno': <class 'int'>,
+              'default': <class 'inspect._empty'>,
+              'docment': 'the result of adding `a` to `b`'}}
+
+
+

To evaluate stringified annotations (from python 3.10), use eval_str:

+
+
docments(add, full=True, eval_str=True)['a']
+
+
{ 'anno': <class 'int'>,
+  'default': <class 'inspect._empty'>,
+  'docment': 'the 1st number to add'}
+
+
+

If you need more space to document a parameter, place one or more lines of comments above the parameter, or above the return type. You can mix-and-match these docment styles:

+
+
def add(
+    # The first operand
+    a:int,
+    # This is the second of the operands to the *addition* operator.
+    # Note that passing a negative value here is the equivalent of the *subtraction* operator.
+    b:int,
+)->int: # The result is calculated using Python's builtin `+` operator.
+    "Add `a` to `b`"
+    return a+b
+
+
+
docments(add)
+
+
{ 'a': 'The first operand',
+  'b': 'This is the second of the operands to the *addition* operator.\n'
+       'Note that passing a negative value here is the equivalent of the '
+       '*subtraction* operator.',
+  'return': "The result is calculated using Python's builtin `+` operator."}
+
+
+

Docments works with async functions, too:

+
+
async def add_async(
+    # The first operand
+    a:int,
+    # This is the second of the operands to the *addition* operator.
+    # Note that passing a negative value here is the equivalent of the *subtraction* operator.
+    b:int,
+)->int: # The result is calculated using Python's builtin `+` operator.
+    "Add `a` to `b`"
+    return a+b
+
+
+
test_eq(docments(add_async), docments(add))
+
+

You can also use docments with classes and methods:

+
+
class Adder:
+    "An addition calculator"
+    def __init__(self,
+        a:int, # First operand
+        b:int, # 2nd operand
+    ): self.a,self.b = a,b
+    
+    def calculate(self
+                 )->int: # Integral result of addition operator
+        "Add `a` to `b`"
+        return a+b
+
+
+
docments(Adder)
+
+
{'a': 'First operand', 'b': '2nd operand', 'return': None}
+
+
+
+
docments(Adder.calculate)
+
+
{'return': 'Integral result of addition operator', 'self': None}
+
+
+

docments can also be extracted from numpy-style docstrings:

+
+
print(add_np.__doc__)
+
+
The sum of two numbers.
+    
+    Used to demonstrate numpy-style docstrings.
+
+Parameters
+----------
+a : int
+    the 1st number to add
+b : int
+    the 2nd number to add (default: 0)
+
+Returns
+-------
+int
+    the result of adding `a` to `b`
+
+
+
+
docments(add_np)
+
+
{ 'a': 'the 1st number to add',
+  'b': 'the 2nd number to add (default: 0)',
+  'return': 'the result of adding `a` to `b`'}
+
+
+

You can even mix and match docments and numpy parameters:

+
+
def add_mixed(a:int, # the first number to add
+              b
+             )->int: # the result
+    """The sum of two numbers.
+
+Parameters
+----------
+b : int
+    the 2nd number to add (default: 0)"""
+    return a+b
+
+
+
docments(add_mixed, full=True)
+
+
{ 'a': { 'anno': <class 'int'>,
+         'default': <class 'inspect._empty'>,
+         'docment': 'the first number to add'},
+  'b': { 'anno': 'int',
+         'default': <class 'inspect._empty'>,
+         'docment': 'the 2nd number to add (default: 0)'},
+  'return': { 'anno': <class 'int'>,
+              'default': <class 'inspect._empty'>,
+              'docment': 'the result'}}
+
+
+

You can use docments with dataclasses, however if the class was defined in online notebook, docments will not contain parameters’ comments. This is because the source code is not available in the notebook. After converting the notebook to a module, the docments will be available. Thus, documentation will have correct parameters’ comments.

+

Docments even works with delegates:

+
+
from fastcore.meta import delegates
+
+
+
def _a(a:int=2): return a # First
+
+@delegates(_a)
+def _b(b:str, **kwargs): return b, (_a(**kwargs)) # Second
+
+docments(_b)
+
+
{'a': 'First', 'b': 'Second', 'return': None}
+
+
+
+
+
+

Extract docstrings

+
+

source

+
+

extract_docstrings

+
+
 extract_docstrings (code)
+
+

Create a dict from function/class/method names to tuples of docstrings and param lists

+
+
sample_code = """
+"This is a module."
+
+def top_func(a, b, *args, **kw):
+    "This is top-level."
+    pass
+
+class SampleClass:
+    "This is a class."
+
+    def __init__(self, x, y):
+        "Constructor for SampleClass."
+        pass
+
+    def method1(self, param1):
+        "This is method1."
+        pass
+
+    def _private_method(self):
+        "This should not be included."
+        pass
+
+class AnotherClass:
+    def __init__(self, a, b):
+        "This class has no separate docstring."
+        pass"""
+
+exp = {'_module': ('This is a module.', ''),
+       'top_func': ('This is top-level.', 'a, b, *args, **kw'),
+       'SampleClass': ('This is a class.', 'self, x, y'),
+       'SampleClass.method1': ('This is method1.', 'self, param1'),
+       'AnotherClass': ('This class has no separate docstring.', 'self, a, b')}
+test_eq(extract_docstrings(sample_code), exp)
+
+ + +
+
+ +
+ +
+ + + + + \ No newline at end of file diff --git a/docments.html.md b/docments.html.md new file mode 100644 index 00000000..b8ca5147 --- /dev/null +++ b/docments.html.md @@ -0,0 +1,450 @@ +# Docments + + + + +[`docments`](https://fastcore.fast.ai/docments.html#docments) provides +programmatic access to comments in function parameters and return types. +It can be used to create more developer-friendly documentation, CLI, etc +tools. + +## Why? + +Without docments, if you want to document your parameters, you have to +repeat param names in docstrings, since they’re already in the function +signature. The parameters have to be kept synchronized in the two places +as you change your code. Readers of your code have to look back and +forth between two places to understand what’s happening. So it’s more +work for you, and for your users. + +Furthermore, to have parameter documentation formatted nicely without +docments, you have to use special magic docstring formatting, often with +[odd +quirks](https://stackoverflow.com/questions/62167540/why-do-definitions-have-a-space-before-the-colon-in-numpy-docstring-sections), +which is a pain to create and maintain, and awkward to read in code. For +instance, using [numpy-style +documentation](https://numpydoc.readthedocs.io/en/latest/format.html): + +``` python +def add_np(a:int, b:int=0)->int: + """The sum of two numbers. + + Used to demonstrate numpy-style docstrings. + +Parameters +---------- +a : int + the 1st number to add +b : int + the 2nd number to add (default: 0) + +Returns +------- +int + the result of adding `a` to `b`""" + return a+b +``` + +By comparison, here’s the same thing using docments: + +``` python +def add( + a:int, # the 1st number to add + b=0, # the 2nd number to add +)->int: # the result of adding `a` to `b` + "The sum of two numbers." + return a+b +``` + +## Numpy docstring helper functions + +[`docments`](https://fastcore.fast.ai/docments.html#docments) also +supports numpy-style docstrings, or a mix or numpy-style and docments +parameter documentation. The functions in this section help get and +parse this information. + +------------------------------------------------------------------------ + +source + +### docstring + +> docstring (sym) + +*Get docstring for `sym` for functions ad classes* + +``` python +test_eq(docstring(add), "The sum of two numbers.") +``` + +------------------------------------------------------------------------ + +source + +### parse_docstring + +> parse_docstring (sym) + +*Parse a numpy-style docstring in `sym`* + +``` python +# parse_docstring(add_np) +``` + +------------------------------------------------------------------------ + +source + +### isdataclass + +> isdataclass (s) + +*Check if `s` is a dataclass but not a dataclass’ instance* + +------------------------------------------------------------------------ + +source + +### get_dataclass_source + +> get_dataclass_source (s) + +*Get source code for dataclass `s`* + +------------------------------------------------------------------------ + +source + +### get_source + +> get_source (s) + +*Get source code for string, function object or dataclass `s`* + +------------------------------------------------------------------------ + +source + +### get_name + +> get_name (obj) + +*Get the name of `obj`* + +``` python +test_eq(get_name(in_ipython), 'in_ipython') +test_eq(get_name(L.map), 'map') +``` + +------------------------------------------------------------------------ + +source + +### qual_name + +> qual_name (obj) + +*Get the qualified name of `obj`* + +``` python +assert qual_name(docscrape) == 'fastcore.docscrape' +``` + +## Docments + +------------------------------------------------------------------------ + +source + +### docments + +> docments (elt, full=False, returns=True, eval_str=False) + +*Generates a `docment`* + +The returned `dict` has parameter names as keys, docments as values. The +return value comment appears in the `return`, unless `returns=False`. +Using the `add` definition above, we get: + +``` python +def add( + a:int, # the 1st number to add + b=0, # the 2nd number to add +)->int: # the result of adding `a` to `b` + "The sum of two numbers." + return a+b + +docments(add) +``` + +``` json +{ 'a': 'the 1st number to add', + 'b': 'the 2nd number to add', + 'return': 'the result of adding `a` to `b`'} +``` + +If you pass `full=True`, the values are `dict` of defaults, types, and +docments as values. Note that the type annotation is inferred from the +default value, if the annotation is empty and a default is supplied. + +``` python +docments(add, full=True) +``` + +``` json +{ 'a': { 'anno': , + 'default': , + 'docment': 'the 1st number to add'}, + 'b': { 'anno': , + 'default': 0, + 'docment': 'the 2nd number to add'}, + 'return': { 'anno': , + 'default': , + 'docment': 'the result of adding `a` to `b`'}} +``` + +To evaluate stringified annotations (from python 3.10), use `eval_str`: + +``` python +docments(add, full=True, eval_str=True)['a'] +``` + +``` json +{ 'anno': , + 'default': , + 'docment': 'the 1st number to add'} +``` + +If you need more space to document a parameter, place one or more lines +of comments above the parameter, or above the return type. You can +mix-and-match these docment styles: + +``` python +def add( + # The first operand + a:int, + # This is the second of the operands to the *addition* operator. + # Note that passing a negative value here is the equivalent of the *subtraction* operator. + b:int, +)->int: # The result is calculated using Python's builtin `+` operator. + "Add `a` to `b`" + return a+b +``` + +``` python +docments(add) +``` + +``` json +{ 'a': 'The first operand', + 'b': 'This is the second of the operands to the *addition* operator.\n' + 'Note that passing a negative value here is the equivalent of the ' + '*subtraction* operator.', + 'return': "The result is calculated using Python's builtin `+` operator."} +``` + +Docments works with async functions, too: + +``` python +async def add_async( + # The first operand + a:int, + # This is the second of the operands to the *addition* operator. + # Note that passing a negative value here is the equivalent of the *subtraction* operator. + b:int, +)->int: # The result is calculated using Python's builtin `+` operator. + "Add `a` to `b`" + return a+b +``` + +``` python +test_eq(docments(add_async), docments(add)) +``` + +You can also use docments with classes and methods: + +``` python +class Adder: + "An addition calculator" + def __init__(self, + a:int, # First operand + b:int, # 2nd operand + ): self.a,self.b = a,b + + def calculate(self + )->int: # Integral result of addition operator + "Add `a` to `b`" + return a+b +``` + +``` python +docments(Adder) +``` + +``` json +{'a': 'First operand', 'b': '2nd operand', 'return': None} +``` + +``` python +docments(Adder.calculate) +``` + +``` json +{'return': 'Integral result of addition operator', 'self': None} +``` + +docments can also be extracted from numpy-style docstrings: + +``` python +print(add_np.__doc__) +``` + + The sum of two numbers. + + Used to demonstrate numpy-style docstrings. + + Parameters + ---------- + a : int + the 1st number to add + b : int + the 2nd number to add (default: 0) + + Returns + ------- + int + the result of adding `a` to `b` + +``` python +docments(add_np) +``` + +``` json +{ 'a': 'the 1st number to add', + 'b': 'the 2nd number to add (default: 0)', + 'return': 'the result of adding `a` to `b`'} +``` + +You can even mix and match docments and numpy parameters: + +``` python +def add_mixed(a:int, # the first number to add + b + )->int: # the result + """The sum of two numbers. + +Parameters +---------- +b : int + the 2nd number to add (default: 0)""" + return a+b +``` + +``` python +docments(add_mixed, full=True) +``` + +``` json +{ 'a': { 'anno': , + 'default': , + 'docment': 'the first number to add'}, + 'b': { 'anno': 'int', + 'default': , + 'docment': 'the 2nd number to add (default: 0)'}, + 'return': { 'anno': , + 'default': , + 'docment': 'the result'}} +``` + +You can use docments with dataclasses, however if the class was defined +in online notebook, docments will not contain parameters’ comments. This +is because the source code is not available in the notebook. After +converting the notebook to a module, the docments will be available. +Thus, documentation will have correct parameters’ comments. + +Docments even works with +[`delegates`](https://fastcore.fast.ai/meta.html#delegates): + +``` python +from fastcore.meta import delegates +``` + +``` python +def _a(a:int=2): return a # First + +@delegates(_a) +def _b(b:str, **kwargs): return b, (_a(**kwargs)) # Second + +docments(_b) +``` + +``` json +{'a': 'First', 'b': 'Second', 'return': None} +``` + +## Extract docstrings + +------------------------------------------------------------------------ + +source + +### extract_docstrings + +> extract_docstrings (code) + +*Create a dict from function/class/method names to tuples of docstrings +and param lists* + +``` python +sample_code = """ +"This is a module." + +def top_func(a, b, *args, **kw): + "This is top-level." + pass + +class SampleClass: + "This is a class." + + def __init__(self, x, y): + "Constructor for SampleClass." + pass + + def method1(self, param1): + "This is method1." + pass + + def _private_method(self): + "This should not be included." + pass + +class AnotherClass: + def __init__(self, a, b): + "This class has no separate docstring." + pass""" + +exp = {'_module': ('This is a module.', ''), + 'top_func': ('This is top-level.', 'a, b, *args, **kw'), + 'SampleClass': ('This is a class.', 'self, x, y'), + 'SampleClass.method1': ('This is method1.', 'self, param1'), + 'AnotherClass': ('This class has no separate docstring.', 'self, a, b')} +test_eq(extract_docstrings(sample_code), exp) +``` diff --git a/foundation.html b/foundation.html new file mode 100644 index 00000000..2bd611d8 --- /dev/null +++ b/foundation.html @@ -0,0 +1,1565 @@ + + + + + + + + + + +Foundation – fastcore + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ +
+ + +
+ + + +
+ +
+
+

Foundation

+
+ +
+
+ The L class and helpers for it +
+
+ + +
+ + + + +
+ + + +
+ + + +
+

Foundational Functions

+
+

source

+
+

working_directory

+
+
 working_directory (path)
+
+

Change working directory to path and return to previous on exit.

+
+

source

+
+
+

add_docs

+
+
 add_docs (cls, cls_doc=None, **docs)
+
+

Copy values from docs to cls docstrings, and confirm all public methods are documented

+

add_docs allows you to add docstrings to a class and its associated methods. This function allows you to group docstrings together seperate from your code, which enables you to define one-line functions as well as organize your code more succintly. We believe this confers a number of benefits which we discuss in our style guide.

+

Suppose you have the following undocumented class:

+
+
class T:
+    def foo(self): pass
+    def bar(self): pass
+
+

You can add documentation to this class like so:

+
+
add_docs(T, cls_doc="A docstring for the class.",
+            foo="The foo method.",
+            bar="The bar method.")
+
+

Now, docstrings will appear as expected:

+
+
test_eq(T.__doc__, "A docstring for the class.")
+test_eq(T.foo.__doc__, "The foo method.")
+test_eq(T.bar.__doc__, "The bar method.")
+
+

add_docs also validates that all of your public methods contain a docstring. If one of your methods is not documented, it will raise an error:

+
+
class T:
+    def foo(self): pass
+    def bar(self): pass
+
+f=lambda: add_docs(T, "A docstring for the class.", foo="The foo method.")
+test_fail(f, contains="Missing docs")
+
+
+

source

+
+
+

docs

+
+
 docs (cls)
+
+

Decorator version of add_docs, using _docs dict

+

Instead of using add_docs, you can use the decorator docs as shown below. Note that the docstring for the class can be set with the argument cls_doc:

+
+
@docs
+class _T:
+    def f(self): pass
+    def g(cls): pass
+    
+    _docs = dict(cls_doc="The class docstring", 
+                 f="The docstring for method f.",
+                 g="A different docstring for method g.")
+
+    
+test_eq(_T.__doc__, "The class docstring")
+test_eq(_T.f.__doc__, "The docstring for method f.")
+test_eq(_T.g.__doc__, "A different docstring for method g.")
+
+

For either the docs decorator or the add_docs function, you can still define your docstrings in the normal way. Below we set the docstring for the class as usual, but define the method docstrings through the _docs attribute:

+
+
@docs
+class _T:
+    "The class docstring"
+    def f(self): pass
+    _docs = dict(f="The docstring for method f.")
+
+    
+test_eq(_T.__doc__, "The class docstring")
+test_eq(_T.f.__doc__, "The docstring for method f.")
+
+
+
+
+

is_iter

+
+
 is_iter (o)
+
+

Test whether o can be used in a for loop

+
+
assert is_iter([1])
+assert not is_iter(array(1))
+assert is_iter(array([1,2]))
+assert (o for o in range(3))
+
+
+

source

+
+
+

coll_repr

+
+
 coll_repr (c, max_n=10)
+
+

String repr of up to max_n items of (possibly lazy) collection c

+

coll_repr is used to provide a more informative __repr__ about list-like objects. coll_repr and is used by L to build a __repr__ that displays the length of a list in addition to a preview of a list.

+

Below is an example of the __repr__ string created for a list of 1000 elements:

+
+
test_eq(coll_repr(range(1000)),    '(#1000) [0,1,2,3,4,5,6,7,8,9...]')
+test_eq(coll_repr(range(1000), 5), '(#1000) [0,1,2,3,4...]')
+test_eq(coll_repr(range(10),   5),   '(#10) [0,1,2,3,4...]')
+test_eq(coll_repr(range(5),    5),    '(#5) [0,1,2,3,4]')
+
+

We can set the option max_n to optionally preview a specified number of items instead of the default:

+
+
test_eq(coll_repr(range(1000), max_n=5), '(#1000) [0,1,2,3,4...]')
+
+
+

source

+
+
+

is_bool

+
+
 is_bool (x)
+
+

Check whether x is a bool or None

+
+

source

+
+
+

mask2idxs

+
+
 mask2idxs (mask)
+
+

Convert bool mask or index list to index L

+
+
test_eq(mask2idxs([False,True,False,True]), [1,3])
+test_eq(mask2idxs(array([False,True,False,True])), [1,3])
+test_eq(mask2idxs(array([1,2,3])), [1,2,3])
+
+
+

source

+
+
+

cycle

+
+
 cycle (o)
+
+

Like itertools.cycle except creates list of Nones if o is empty

+
+
test_eq(itertools.islice(cycle([1,2,3]),5), [1,2,3,1,2])
+test_eq(itertools.islice(cycle([]),3), [None]*3)
+test_eq(itertools.islice(cycle(None),3), [None]*3)
+test_eq(itertools.islice(cycle(1),3), [1,1,1])
+
+
+

source

+
+
+

zip_cycle

+
+
 zip_cycle (x, *args)
+
+

Like itertools.zip_longest but cycles through elements of all but first argument

+
+
test_eq(zip_cycle([1,2,3,4],list('abc')), [(1, 'a'), (2, 'b'), (3, 'c'), (4, 'a')])
+
+
+

source

+
+
+

is_indexer

+
+
 is_indexer (idx)
+
+

Test whether idx will index a single item in a list

+

You can, for example index a single item in a list with an integer or a 0-dimensional numpy array:

+
+
assert is_indexer(1)
+assert is_indexer(np.array(1))
+
+

However, you cannot index into single item in a list with another list or a numpy array with ndim > 0.

+
+
assert not is_indexer([1, 2])
+assert not is_indexer(np.array([[1, 2], [3, 4]]))
+
+
+
+
+

L helpers

+
+

source

+
+

CollBase

+
+
 CollBase (items)
+
+

Base class for composing a list of items

+

ColBase is a base class that emulates the functionality of a python list:

+
+
class _T(CollBase): pass
+l = _T([1,2,3,4,5])
+
+test_eq(len(l), 5) # __len__
+test_eq(l[-1], 5); test_eq(l[0], 1) #__getitem__
+l[2] = 100; test_eq(l[2], 100)      # __set_item__
+del l[0]; test_eq(len(l), 4)        # __delitem__
+test_eq(str(l), '[2, 100, 4, 5]')   # __repr__
+
+
+

source

+
+
+

L

+
+
 L (x=None, *args, **kwargs)
+
+

Behaves like a list of items but can also index with list of indices or masks

+

L is a drop in replacement for a python list. Inspired by NumPy, L, supports advanced indexing and has additional methods (outlined below) that provide additional functionality and encourage simple expressive code. For example, the code below takes a list of pairs, selects the second item of each pair, takes its absolute value, filters items greater than 4, and adds them up:

+
+
from fastcore.utils import gt
+
+
+
d = dict(a=1,b=-5,d=6,e=9).items()
+test_eq(L(d).itemgot(1).map(abs).filter(gt(4)).sum(), 20) # abs(-5) + abs(6) + abs(9) = 20; 1 was filtered out.
+
+

Read this overview section for a quick tutorial of L, as well as background on the name.

+

You can create an L from an existing iterable (e.g. a list, range, etc) and access or modify it with an int list/tuple index, mask, int, or slice. All list methods can also be used with L.

+
+
t = L(range(12))
+test_eq(t, list(range(12)))
+test_ne(t, list(range(11)))
+t.reverse()
+test_eq(t[0], 11)
+t[3] = "h"
+test_eq(t[3], "h")
+t[3,5] = ("j","k")
+test_eq(t[3,5], ["j","k"])
+test_eq(t, L(t))
+test_eq(L(L(1,2),[3,4]), ([1,2],[3,4]))
+t
+
+
(#12) [11,10,9,'j',7,'k',5,4,3,2...]
+
+
+

Any L is a Sequence so you can use it with methods like random.sample:

+
+
assert isinstance(t, Sequence)
+
+
+
import random
+
+
+
random.seed(0)
+random.sample(t, 3)
+
+
[5, 0, 11]
+
+
+

There are optimized indexers for arrays, tensors, and DataFrames.

+
+
import pandas as pd
+
+
+
arr = np.arange(9).reshape(3,3)
+t = L(arr, use_list=None)
+test_eq(t[1,2], arr[[1,2]])
+
+df = pd.DataFrame({'a':[1,2,3]})
+t = L(df, use_list=None)
+test_eq(t[1,2], L(pd.DataFrame({'a':[2,3]}, index=[1,2]), use_list=None))
+
+

You can also modify an L with append, +, and *.

+
+
t = L()
+test_eq(t, [])
+t.append(1)
+test_eq(t, [1])
+t += [3,2]
+test_eq(t, [1,3,2])
+t = t + [4]
+test_eq(t, [1,3,2,4])
+t = 5 + t
+test_eq(t, [5,1,3,2,4])
+test_eq(L(1,2,3), [1,2,3])
+test_eq(L(1,2,3), L(1,2,3))
+t = L(1)*5
+t = t.map(operator.neg)
+test_eq(t,[-1]*5)
+test_eq(~L([True,False,False]), L([False,True,True]))
+t = L(range(4))
+test_eq(zip(t, L(1).cycle()), zip(range(4),(1,1,1,1)))
+t = L.range(100)
+test_shuffled(t,t.shuffle())
+
+
+
test_eq(L([]).sum(), 0)
+test_eq(L([]).product(), 1)
+
+
+
def _f(x,a=0): return x+a
+t = L(1)*5
+test_eq(t.map(_f), t)
+test_eq(t.map(_f,1), [2]*5)
+test_eq(t.map(_f,a=2), [3]*5)
+
+

An L can be constructed from anything iterable, although tensors and arrays will not be iterated over on construction, unless you pass use_list to the constructor.

+
+
test_eq(L([1,2,3]),[1,2,3])
+test_eq(L(L([1,2,3])),[1,2,3])
+test_ne(L([1,2,3]),[1,2,])
+test_eq(L('abc'),['abc'])
+test_eq(L(range(0,3)),[0,1,2])
+test_eq(L(o for o in range(0,3)),[0,1,2])
+test_eq(L(array(0)),[array(0)])
+test_eq(L([array(0),array(1)]),[array(0),array(1)])
+test_eq(L(array([0.,1.1]))[0],array([0.,1.1]))
+test_eq(L(array([0.,1.1]), use_list=True), [array(0.),array(1.1)])  # `use_list=True` to unwrap arrays/arrays
+
+

If match is not None then the created list is same len as match, either by:

+
    +
  • If len(items)==1 then items is replicated,
  • +
  • Otherwise an error is raised if match and items are not already the same size.
  • +
+
+
test_eq(L(1,match=[1,2,3]),[1,1,1])
+test_eq(L([1,2],match=[2,3]),[1,2])
+test_fail(lambda: L([1,2],match=[1,2,3]))
+
+

If you create an L from an existing L then you’ll get back the original object (since L uses the NewChkMeta metaclass).

+
+
test_is(L(t), t)
+
+

An L is considred equal to a list if they have the same elements. It’s never considered equal to a str a set or a dict even if they have the same elements/keys.

+
+
test_eq(L(['a', 'b']), ['a', 'b'])
+test_ne(L(['a', 'b']), 'ab')
+test_ne(L(['a', 'b']), {'a':1, 'b':2})
+
+
+
+

L Methods

+
+

source

+
+
+

L.__getitem__

+
+
 L.__getitem__ (idx)
+
+

Retrieve idx (can be list of indices, or mask, or int) items

+
+
t = L(range(12))
+test_eq(t[1,2], [1,2])                # implicit tuple
+test_eq(t[[1,2]], [1,2])              # list
+test_eq(t[:3], [0,1,2])               # slice
+test_eq(t[[False]*11 + [True]], [11]) # mask
+test_eq(t[array(3)], 3)
+
+
+

source

+
+
+

L.__setitem__

+
+
 L.__setitem__ (idx, o)
+
+

Set idx (can be list of indices, or mask, or int) items to o (which is broadcast if not iterable)

+
+
t[4,6] = 0
+test_eq(t[4,6], [0,0])
+t[4,6] = [1,2]
+test_eq(t[4,6], [1,2])
+
+
+

source

+
+
+

L.unique

+
+
 L.unique (sort=False, bidir=False, start=None)
+
+

Unique items, in stable order

+
+
test_eq(L(4,1,2,3,4,4).unique(), [4,1,2,3])
+
+
+

source

+
+
+

L.val2idx

+
+
 L.val2idx ()
+
+

Dict from value to index

+
+
test_eq(L(1,2,3).val2idx(), {3:2,1:0,2:1})
+
+
+

source

+
+
+

L.filter

+
+
 L.filter (f=<function noop>, negate=False, **kwargs)
+
+

Create new L filtered by predicate f, passing args and kwargs to f

+
+
list(t)
+
+
[0, 1, 2, 3, 1, 5, 2, 7, 8, 9, 10, 11]
+
+
+
+
test_eq(t.filter(lambda o:o<5), [0,1,2,3,1,2])
+test_eq(t.filter(lambda o:o<5, negate=True), [5,7,8,9,10,11])
+
+
+

source

+
+
+

L.argwhere

+
+
 L.argwhere (f, negate=False, **kwargs)
+
+

Like filter, but return indices for matching items

+
+
test_eq(t.argwhere(lambda o:o<5), [0,1,2,3,4,6])
+
+
+

source

+
+
+

L.argfirst

+
+
 L.argfirst (f, negate=False)
+
+

Return index of first matching item

+
+
test_eq(t.argfirst(lambda o:o>4), 5)
+test_eq(t.argfirst(lambda o:o>4,negate=True),0)
+
+
+

source

+
+
+

L.map

+
+
 L.map (f, *args, **kwargs)
+
+

Create new L with f applied to all items, passing args and kwargs to f

+
+
test_eq(L.range(4).map(operator.neg), [0,-1,-2,-3])
+
+

If f is a string then it is treated as a format string to create the mapping:

+
+
test_eq(L.range(4).map('#{}#'), ['#0#','#1#','#2#','#3#'])
+
+

If f is a dictionary (or anything supporting __getitem__) then it is indexed to create the mapping:

+
+
test_eq(L.range(4).map(list('abcd')), list('abcd'))
+
+

You can also pass the same arg params that bind accepts:

+
+
def f(a=None,b=None): return b
+test_eq(L.range(4).map(f, b=arg0), range(4))
+
+
+

source

+
+
+

L.map_dict

+
+
 L.map_dict (f=<function noop>, *args, **kwargs)
+
+

Like map, but creates a dict from items to function results

+
+
test_eq(L(range(1,5)).map_dict(), {1:1, 2:2, 3:3, 4:4})
+test_eq(L(range(1,5)).map_dict(operator.neg), {1:-1, 2:-2, 3:-3, 4:-4})
+
+
+

source

+
+
+

L.zip

+
+
 L.zip (cycled=False)
+
+

Create new L with zip(*items)

+
+
t = L([[1,2,3],'abc'])
+test_eq(t.zip(), [(1, 'a'),(2, 'b'),(3, 'c')])
+
+
+
t = L([[1,2,3,4],['a','b','c']])
+test_eq(t.zip(cycled=True ), [(1, 'a'),(2, 'b'),(3, 'c'),(4, 'a')])
+test_eq(t.zip(cycled=False), [(1, 'a'),(2, 'b'),(3, 'c')])
+
+
+

source

+
+
+

L.map_zip

+
+
 L.map_zip (f, *args, cycled=False, **kwargs)
+
+

Combine zip and starmap

+
+
t = L([1,2,3],[2,3,4])
+test_eq(t.map_zip(operator.mul), [2,6,12])
+
+
+

source

+
+
+

L.zipwith

+
+
 L.zipwith (*rest, cycled=False)
+
+

Create new L with self zip with each of *rest

+
+
b = [[0],[1],[2,2]]
+t = L([1,2,3]).zipwith(b)
+test_eq(t, [(1,[0]), (2,[1]), (3,[2,2])])
+
+
+

source

+
+
+

L.map_zipwith

+
+
 L.map_zipwith (f, *rest, cycled=False, **kwargs)
+
+

Combine zipwith and starmap

+
+
test_eq(L(1,2,3).map_zipwith(operator.mul, [2,3,4]), [2,6,12])
+
+
+

source

+
+
+

L.itemgot

+
+
 L.itemgot (*idxs)
+
+

Create new L with item idx of all items

+
+
test_eq(t.itemgot(1), b)
+
+
+

source

+
+
+

L.attrgot

+
+
 L.attrgot (k, default=None)
+
+

Create new L with attr k (or value k for dicts) of all items.

+
+
# Example when items are not a dict
+a = [SimpleNamespace(a=3,b=4),SimpleNamespace(a=1,b=2)]
+test_eq(L(a).attrgot('b'), [4,2])
+
+#Example of when items are a dict
+b =[{'id': 15, 'name': 'nbdev'}, {'id': 17, 'name': 'fastcore'}]
+test_eq(L(b).attrgot('id'), [15, 17])
+
+
+

source

+
+
+

L.sorted

+
+
 L.sorted (key=None, reverse=False)
+
+

New L sorted by key. If key is str use attrgetter; if int use itemgetter

+
+
test_eq(L(a).sorted('a').attrgot('b'), [2,4])
+
+
+

source

+
+
+

L.split

+
+
 L.split (s, sep=None, maxsplit=-1)
+
+

Class Method: Same as str.split, but returns an L

+
+
test_eq(L.split('a b c'), list('abc'))
+
+
+

source

+
+
+

L.range

+
+
 L.range (a, b=None, step=None)
+
+

Class Method: Same as range, but returns L. Can pass collection for a, to use len(a)

+
+
test_eq_type(L.range([1,1,1]), L(range(3)))
+test_eq_type(L.range(5,2,2), L(range(5,2,2)))
+
+
+

source

+
+
+

L.concat

+
+
 L.concat ()
+
+

Concatenate all elements of list

+
+
test_eq(L([0,1,2,3],4,L(5,6)).concat(), range(7))
+
+
+

source

+
+
+

L.copy

+
+
 L.copy ()
+
+

Same as list.copy, but returns an L

+
+
t = L([0,1,2,3],4,L(5,6)).copy()
+test_eq(t.concat(), range(7))
+
+
+

source

+
+
+

L.map_first

+
+
 L.map_first (f=<function noop>, g=<function noop>, *args, **kwargs)
+
+

First element of map_filter

+
+
t = L(0,1,2,3)
+test_eq(t.map_first(lambda o:o*2 if o>2 else None), 6)
+
+
+

source

+
+
+

L.setattrs

+
+
 L.setattrs (attr, val)
+
+

Call setattr on all items

+
+
t = L(SimpleNamespace(),SimpleNamespace())
+t.setattrs('foo', 'bar')
+test_eq(t.attrgot('foo'), ['bar','bar'])
+
+
+
+
+

Config

+
+

source

+
+

save_config_file

+
+
 save_config_file (file, d, **kwargs)
+
+

Write settings dict to a new config file, or overwrite the existing one.

+
+

source

+
+
+

read_config_file

+
+
 read_config_file (file, **kwargs)
+
+

Config files are saved and read using Python’s configparser.ConfigParser, inside the DEFAULT section.

+
+
_d = dict(user='fastai', lib_name='fastcore', some_path='test', some_bool=True, some_num=3)
+try:
+    save_config_file('tmp.ini', _d)
+    res = read_config_file('tmp.ini')
+finally: os.unlink('tmp.ini')
+dict(res)
+
+
{'user': 'fastai',
+ 'lib_name': 'fastcore',
+ 'some_path': 'test',
+ 'some_bool': 'True',
+ 'some_num': '3'}
+
+
+
+

source

+
+
+

Config

+
+
 Config (cfg_path, cfg_name, create=None, save=True, extra_files=None,
+         types=None)
+
+

Reading and writing ConfigParser ini files

+

Config is a convenient wrapper around ConfigParser ini files with a single section (DEFAULT).

+

Instantiate a Config from an ini file at cfg_path/cfg_name:

+
+
save_config_file('../tmp.ini', _d)
+try: cfg = Config('..', 'tmp.ini')
+finally: os.unlink('../tmp.ini')
+cfg
+
+
{'user': 'fastai', 'lib_name': 'fastcore', 'some_path': 'test', 'some_bool': 'True', 'some_num': '3'}
+
+
+

You can create a new file if one doesn’t exist by providing a create dict:

+
+
try: cfg = Config('..', 'tmp.ini', create=_d)
+finally: os.unlink('../tmp.ini')
+cfg
+
+
{'user': 'fastai', 'lib_name': 'fastcore', 'some_path': 'test', 'some_bool': 'True', 'some_num': '3'}
+
+
+

If you additionally pass save=False, the Config will contain the items from create without writing a new file:

+
+
cfg = Config('..', 'tmp.ini', create=_d, save=False)
+test_eq(cfg.user,'fastai')
+assert not Path('../tmp.ini').exists()
+
+
+

source

+
+
+

Config.get

+
+
 Config.get (k, default=None)
+
+

Keys can be accessed as attributes, items, or with get and an optional default:

+
+
test_eq(cfg.user,'fastai')
+test_eq(cfg['some_path'], 'test')
+test_eq(cfg.get('foo','bar'),'bar')
+
+

Extra files can be read before cfg_path/cfg_name using extra_files, in the order they appear:

+
+
with tempfile.TemporaryDirectory() as d:
+    a = Config(d, 'a.ini', {'a':0,'b':0})
+    b = Config(d, 'b.ini', {'a':1,'c':0})
+    c = Config(d, 'c.ini', {'a':2,'d':0}, extra_files=[a.config_file,b.config_file])
+    test_eq(c.d, {'a':'2','b':'0','c':'0','d':'0'})
+
+

If you pass a dict types, then the values of that dict will be used as types to instantiate all values returned. Path is a special case – in that case, the path returned will be relative to the path containing the config file (assuming the value is relative). bool types use str2bool to convert to boolean.

+
+
_types = dict(some_path=Path, some_bool=bool, some_num=int)
+cfg = Config('..', 'tmp.ini', create=_d, save=False, types=_types)
+
+test_eq(cfg.user,'fastai')
+test_eq(cfg['some_path'].resolve(), (Path('..')/'test').resolve())
+test_eq(cfg.get('some_num'), 3)
+
+
+

source

+
+
+

Config.find

+
+
 Config.find (cfg_name, cfg_path=None, **kwargs)
+
+

Search cfg_path and its parents to find cfg_name

+

You can use Config.find to search subdirectories for a config file, starting in the current path if no path is specified:

+
+
Config.find('settings.ini').repo
+
+
'fastcore'
+
+
+ + +
+
+ +
+ +
+ + + + + \ No newline at end of file diff --git a/foundation.html.md b/foundation.html.md new file mode 100644 index 00000000..db9b1606 --- /dev/null +++ b/foundation.html.md @@ -0,0 +1,1081 @@ +# Foundation + + + + +## Foundational Functions + +------------------------------------------------------------------------ + +source + +### working_directory + +> working_directory (path) + +*Change working directory to `path` and return to previous on exit.* + +------------------------------------------------------------------------ + +source + +### add_docs + +> add_docs (cls, cls_doc=None, **docs) + +*Copy values from +[`docs`](https://fastcore.fast.ai/foundation.html#docs) to `cls` +docstrings, and confirm all public methods are documented* + +[`add_docs`](https://fastcore.fast.ai/foundation.html#add_docs) allows +you to add docstrings to a class and its associated methods. This +function allows you to group docstrings together seperate from your +code, which enables you to define one-line functions as well as organize +your code more succintly. We believe this confers a number of benefits +which we discuss in [our style +guide](https://docs.fast.ai/dev/style.html). + +Suppose you have the following undocumented class: + +``` python +class T: + def foo(self): pass + def bar(self): pass +``` + +You can add documentation to this class like so: + +``` python +add_docs(T, cls_doc="A docstring for the class.", + foo="The foo method.", + bar="The bar method.") +``` + +Now, docstrings will appear as expected: + +``` python +test_eq(T.__doc__, "A docstring for the class.") +test_eq(T.foo.__doc__, "The foo method.") +test_eq(T.bar.__doc__, "The bar method.") +``` + +[`add_docs`](https://fastcore.fast.ai/foundation.html#add_docs) also +validates that all of your public methods contain a docstring. If one of +your methods is not documented, it will raise an error: + +``` python +class T: + def foo(self): pass + def bar(self): pass + +f=lambda: add_docs(T, "A docstring for the class.", foo="The foo method.") +test_fail(f, contains="Missing docs") +``` + +------------------------------------------------------------------------ + +source + +### docs + +> docs (cls) + +*Decorator version of +[`add_docs`](https://fastcore.fast.ai/foundation.html#add_docs), using +`_docs` dict* + +Instead of using +[`add_docs`](https://fastcore.fast.ai/foundation.html#add_docs), you can +use the decorator +[`docs`](https://fastcore.fast.ai/foundation.html#docs) as shown below. +Note that the docstring for the class can be set with the argument +`cls_doc`: + +``` python +@docs +class _T: + def f(self): pass + def g(cls): pass + + _docs = dict(cls_doc="The class docstring", + f="The docstring for method f.", + g="A different docstring for method g.") + + +test_eq(_T.__doc__, "The class docstring") +test_eq(_T.f.__doc__, "The docstring for method f.") +test_eq(_T.g.__doc__, "A different docstring for method g.") +``` + +For either the [`docs`](https://fastcore.fast.ai/foundation.html#docs) +decorator or the +[`add_docs`](https://fastcore.fast.ai/foundation.html#add_docs) +function, you can still define your docstrings in the normal way. Below +we set the docstring for the class as usual, but define the method +docstrings through the `_docs` attribute: + +``` python +@docs +class _T: + "The class docstring" + def f(self): pass + _docs = dict(f="The docstring for method f.") + + +test_eq(_T.__doc__, "The class docstring") +test_eq(_T.f.__doc__, "The docstring for method f.") +``` + +------------------------------------------------------------------------ + +### is_iter + +> is_iter (o) + +*Test whether `o` can be used in a `for` loop* + +``` python +assert is_iter([1]) +assert not is_iter(array(1)) +assert is_iter(array([1,2])) +assert (o for o in range(3)) +``` + +------------------------------------------------------------------------ + +source + +### coll_repr + +> coll_repr (c, max_n=10) + +*String repr of up to `max_n` items of (possibly lazy) collection `c`* + +[`coll_repr`](https://fastcore.fast.ai/foundation.html#coll_repr) is +used to provide a more informative +[`__repr__`](https://stackoverflow.com/questions/1984162/purpose-of-pythons-repr) +about list-like objects. +[`coll_repr`](https://fastcore.fast.ai/foundation.html#coll_repr) and is +used by [`L`](https://fastcore.fast.ai/foundation.html#l) to build a +`__repr__` that displays the length of a list in addition to a preview +of a list. + +Below is an example of the `__repr__` string created for a list of 1000 +elements: + +``` python +test_eq(coll_repr(range(1000)), '(#1000) [0,1,2,3,4,5,6,7,8,9...]') +test_eq(coll_repr(range(1000), 5), '(#1000) [0,1,2,3,4...]') +test_eq(coll_repr(range(10), 5), '(#10) [0,1,2,3,4...]') +test_eq(coll_repr(range(5), 5), '(#5) [0,1,2,3,4]') +``` + +We can set the option `max_n` to optionally preview a specified number +of items instead of the default: + +``` python +test_eq(coll_repr(range(1000), max_n=5), '(#1000) [0,1,2,3,4...]') +``` + +------------------------------------------------------------------------ + +source + +### is_bool + +> is_bool (x) + +*Check whether `x` is a bool or None* + +------------------------------------------------------------------------ + +source + +### mask2idxs + +> mask2idxs (mask) + +*Convert bool mask or index list to index +[`L`](https://fastcore.fast.ai/foundation.html#l)* + +``` python +test_eq(mask2idxs([False,True,False,True]), [1,3]) +test_eq(mask2idxs(array([False,True,False,True])), [1,3]) +test_eq(mask2idxs(array([1,2,3])), [1,2,3]) +``` + +------------------------------------------------------------------------ + +source + +### cycle + +> cycle (o) + +*Like `itertools.cycle` except creates list of `None`s if `o` is empty* + +``` python +test_eq(itertools.islice(cycle([1,2,3]),5), [1,2,3,1,2]) +test_eq(itertools.islice(cycle([]),3), [None]*3) +test_eq(itertools.islice(cycle(None),3), [None]*3) +test_eq(itertools.islice(cycle(1),3), [1,1,1]) +``` + +------------------------------------------------------------------------ + +source + +### zip_cycle + +> zip_cycle (x, *args) + +*Like `itertools.zip_longest` but +[`cycle`](https://fastcore.fast.ai/foundation.html#cycle)s through +elements of all but first argument* + +``` python +test_eq(zip_cycle([1,2,3,4],list('abc')), [(1, 'a'), (2, 'b'), (3, 'c'), (4, 'a')]) +``` + +------------------------------------------------------------------------ + +source + +### is_indexer + +> is_indexer (idx) + +*Test whether `idx` will index a single item in a list* + +You can, for example index a single item in a list with an integer or a +0-dimensional numpy array: + +``` python +assert is_indexer(1) +assert is_indexer(np.array(1)) +``` + +However, you cannot index into single item in a list with another list +or a numpy array with ndim \> 0. + +``` python +assert not is_indexer([1, 2]) +assert not is_indexer(np.array([[1, 2], [3, 4]])) +``` + +## [`L`](https://fastcore.fast.ai/foundation.html#l) helpers + +------------------------------------------------------------------------ + +source + +### CollBase + +> CollBase (items) + +*Base class for composing a list of `items`* + +`ColBase` is a base class that emulates the functionality of a python +`list`: + +``` python +class _T(CollBase): pass +l = _T([1,2,3,4,5]) + +test_eq(len(l), 5) # __len__ +test_eq(l[-1], 5); test_eq(l[0], 1) #__getitem__ +l[2] = 100; test_eq(l[2], 100) # __set_item__ +del l[0]; test_eq(len(l), 4) # __delitem__ +test_eq(str(l), '[2, 100, 4, 5]') # __repr__ +``` + +------------------------------------------------------------------------ + +source + +### L + +> L (x=None, *args, **kwargs) + +*Behaves like a list of `items` but can also index with list of indices +or masks* + +[`L`](https://fastcore.fast.ai/foundation.html#l) is a drop in +replacement for a python `list`. Inspired by +[NumPy](http://www.numpy.org/), +[`L`](https://fastcore.fast.ai/foundation.html#l), supports advanced +indexing and has additional methods (outlined below) that provide +additional functionality and encourage simple expressive code. For +example, the code below takes a list of pairs, selects the second item +of each pair, takes its absolute value, filters items greater than 4, +and adds them up: + +``` python +from fastcore.utils import gt +``` + +``` python +d = dict(a=1,b=-5,d=6,e=9).items() +test_eq(L(d).itemgot(1).map(abs).filter(gt(4)).sum(), 20) # abs(-5) + abs(6) + abs(9) = 20; 1 was filtered out. +``` + +Read [this overview section](https://fastcore.fast.ai/tour.html#L) for a +quick tutorial of [`L`](https://fastcore.fast.ai/foundation.html#l), as +well as background on the name. + +You can create an [`L`](https://fastcore.fast.ai/foundation.html#l) from +an existing iterable (e.g. a list, range, etc) and access or modify it +with an int list/tuple index, mask, int, or slice. All `list` methods +can also be used with [`L`](https://fastcore.fast.ai/foundation.html#l). + +``` python +t = L(range(12)) +test_eq(t, list(range(12))) +test_ne(t, list(range(11))) +t.reverse() +test_eq(t[0], 11) +t[3] = "h" +test_eq(t[3], "h") +t[3,5] = ("j","k") +test_eq(t[3,5], ["j","k"]) +test_eq(t, L(t)) +test_eq(L(L(1,2),[3,4]), ([1,2],[3,4])) +t +``` + + (#12) [11,10,9,'j',7,'k',5,4,3,2...] + +Any [`L`](https://fastcore.fast.ai/foundation.html#l) is a `Sequence` so +you can use it with methods like `random.sample`: + +``` python +assert isinstance(t, Sequence) +``` + +``` python +import random +``` + +``` python +random.seed(0) +random.sample(t, 3) +``` + + [5, 0, 11] + +There are optimized indexers for arrays, tensors, and DataFrames. + +``` python +import pandas as pd +``` + +``` python +arr = np.arange(9).reshape(3,3) +t = L(arr, use_list=None) +test_eq(t[1,2], arr[[1,2]]) + +df = pd.DataFrame({'a':[1,2,3]}) +t = L(df, use_list=None) +test_eq(t[1,2], L(pd.DataFrame({'a':[2,3]}, index=[1,2]), use_list=None)) +``` + +You can also modify an [`L`](https://fastcore.fast.ai/foundation.html#l) +with `append`, `+`, and `*`. + +``` python +t = L() +test_eq(t, []) +t.append(1) +test_eq(t, [1]) +t += [3,2] +test_eq(t, [1,3,2]) +t = t + [4] +test_eq(t, [1,3,2,4]) +t = 5 + t +test_eq(t, [5,1,3,2,4]) +test_eq(L(1,2,3), [1,2,3]) +test_eq(L(1,2,3), L(1,2,3)) +t = L(1)*5 +t = t.map(operator.neg) +test_eq(t,[-1]*5) +test_eq(~L([True,False,False]), L([False,True,True])) +t = L(range(4)) +test_eq(zip(t, L(1).cycle()), zip(range(4),(1,1,1,1))) +t = L.range(100) +test_shuffled(t,t.shuffle()) +``` + +``` python +test_eq(L([]).sum(), 0) +test_eq(L([]).product(), 1) +``` + +``` python +def _f(x,a=0): return x+a +t = L(1)*5 +test_eq(t.map(_f), t) +test_eq(t.map(_f,1), [2]*5) +test_eq(t.map(_f,a=2), [3]*5) +``` + +An [`L`](https://fastcore.fast.ai/foundation.html#l) can be constructed +from anything iterable, although tensors and arrays will not be iterated +over on construction, unless you pass `use_list` to the constructor. + +``` python +test_eq(L([1,2,3]),[1,2,3]) +test_eq(L(L([1,2,3])),[1,2,3]) +test_ne(L([1,2,3]),[1,2,]) +test_eq(L('abc'),['abc']) +test_eq(L(range(0,3)),[0,1,2]) +test_eq(L(o for o in range(0,3)),[0,1,2]) +test_eq(L(array(0)),[array(0)]) +test_eq(L([array(0),array(1)]),[array(0),array(1)]) +test_eq(L(array([0.,1.1]))[0],array([0.,1.1])) +test_eq(L(array([0.,1.1]), use_list=True), [array(0.),array(1.1)]) # `use_list=True` to unwrap arrays/arrays +``` + +If `match` is not `None` then the created list is same len as `match`, +either by: + +- If `len(items)==1` then `items` is replicated, +- Otherwise an error is raised if `match` and `items` are not already + the same size. + +``` python +test_eq(L(1,match=[1,2,3]),[1,1,1]) +test_eq(L([1,2],match=[2,3]),[1,2]) +test_fail(lambda: L([1,2],match=[1,2,3])) +``` + +If you create an [`L`](https://fastcore.fast.ai/foundation.html#l) from +an existing [`L`](https://fastcore.fast.ai/foundation.html#l) then +you’ll get back the original object (since +[`L`](https://fastcore.fast.ai/foundation.html#l) uses the +[`NewChkMeta`](https://fastcore.fast.ai/meta.html#newchkmeta) +metaclass). + +``` python +test_is(L(t), t) +``` + +An [`L`](https://fastcore.fast.ai/foundation.html#l) is considred equal +to a list if they have the same elements. It’s never considered equal to +a `str` a `set` or a `dict` even if they have the same elements/keys. + +``` python +test_eq(L(['a', 'b']), ['a', 'b']) +test_ne(L(['a', 'b']), 'ab') +test_ne(L(['a', 'b']), {'a':1, 'b':2}) +``` + +### [`L`](https://fastcore.fast.ai/foundation.html#l) Methods + +------------------------------------------------------------------------ + +source + +### L.\_\_getitem\_\_ + +> L.__getitem__ (idx) + +*Retrieve `idx` (can be list of indices, or mask, or int) items* + +``` python +t = L(range(12)) +test_eq(t[1,2], [1,2]) # implicit tuple +test_eq(t[[1,2]], [1,2]) # list +test_eq(t[:3], [0,1,2]) # slice +test_eq(t[[False]*11 + [True]], [11]) # mask +test_eq(t[array(3)], 3) +``` + +------------------------------------------------------------------------ + +source + +### L.\_\_setitem\_\_ + +> L.__setitem__ (idx, o) + +*Set `idx` (can be list of indices, or mask, or int) items to `o` (which +is broadcast if not iterable)* + +``` python +t[4,6] = 0 +test_eq(t[4,6], [0,0]) +t[4,6] = [1,2] +test_eq(t[4,6], [1,2]) +``` + +------------------------------------------------------------------------ + +source + +### L.unique + +> L.unique (sort=False, bidir=False, start=None) + +*Unique items, in stable order* + +``` python +test_eq(L(4,1,2,3,4,4).unique(), [4,1,2,3]) +``` + +------------------------------------------------------------------------ + +source + +### L.val2idx + +> L.val2idx () + +*Dict from value to index* + +``` python +test_eq(L(1,2,3).val2idx(), {3:2,1:0,2:1}) +``` + +------------------------------------------------------------------------ + +source + +### L.filter + +> L.filter (f=, negate=False, **kwargs) + +*Create new [`L`](https://fastcore.fast.ai/foundation.html#l) filtered +by predicate `f`, passing `args` and `kwargs` to `f`* + +``` python +list(t) +``` + + [0, 1, 2, 3, 1, 5, 2, 7, 8, 9, 10, 11] + +``` python +test_eq(t.filter(lambda o:o<5), [0,1,2,3,1,2]) +test_eq(t.filter(lambda o:o<5, negate=True), [5,7,8,9,10,11]) +``` + +------------------------------------------------------------------------ + +source + +### L.argwhere + +> L.argwhere (f, negate=False, **kwargs) + +*Like `filter`, but return indices for matching items* + +``` python +test_eq(t.argwhere(lambda o:o<5), [0,1,2,3,4,6]) +``` + +------------------------------------------------------------------------ + +source + +### L.argfirst + +> L.argfirst (f, negate=False) + +*Return index of first matching item* + +``` python +test_eq(t.argfirst(lambda o:o>4), 5) +test_eq(t.argfirst(lambda o:o>4,negate=True),0) +``` + +------------------------------------------------------------------------ + +source + +### L.map + +> L.map (f, *args, **kwargs) + +*Create new [`L`](https://fastcore.fast.ai/foundation.html#l) with `f` +applied to all `items`, passing `args` and `kwargs` to `f`* + +``` python +test_eq(L.range(4).map(operator.neg), [0,-1,-2,-3]) +``` + +If `f` is a string then it is treated as a format string to create the +mapping: + +``` python +test_eq(L.range(4).map('#{}#'), ['#0#','#1#','#2#','#3#']) +``` + +If `f` is a dictionary (or anything supporting `__getitem__`) then it is +indexed to create the mapping: + +``` python +test_eq(L.range(4).map(list('abcd')), list('abcd')) +``` + +You can also pass the same `arg` params that +[`bind`](https://fastcore.fast.ai/basics.html#bind) accepts: + +``` python +def f(a=None,b=None): return b +test_eq(L.range(4).map(f, b=arg0), range(4)) +``` + +------------------------------------------------------------------------ + +source + +### L.map_dict + +> L.map_dict (f=, *args, **kwargs) + +*Like `map`, but creates a dict from `items` to function results* + +``` python +test_eq(L(range(1,5)).map_dict(), {1:1, 2:2, 3:3, 4:4}) +test_eq(L(range(1,5)).map_dict(operator.neg), {1:-1, 2:-2, 3:-3, 4:-4}) +``` + +------------------------------------------------------------------------ + +source + +### L.zip + +> L.zip (cycled=False) + +*Create new [`L`](https://fastcore.fast.ai/foundation.html#l) with +`zip(*items)`* + +``` python +t = L([[1,2,3],'abc']) +test_eq(t.zip(), [(1, 'a'),(2, 'b'),(3, 'c')]) +``` + +``` python +t = L([[1,2,3,4],['a','b','c']]) +test_eq(t.zip(cycled=True ), [(1, 'a'),(2, 'b'),(3, 'c'),(4, 'a')]) +test_eq(t.zip(cycled=False), [(1, 'a'),(2, 'b'),(3, 'c')]) +``` + +------------------------------------------------------------------------ + +source + +### L.map_zip + +> L.map_zip (f, *args, cycled=False, **kwargs) + +*Combine `zip` and `starmap`* + +``` python +t = L([1,2,3],[2,3,4]) +test_eq(t.map_zip(operator.mul), [2,6,12]) +``` + +------------------------------------------------------------------------ + +source + +### L.zipwith + +> L.zipwith (*rest, cycled=False) + +*Create new [`L`](https://fastcore.fast.ai/foundation.html#l) with +`self` zip with each of `*rest`* + +``` python +b = [[0],[1],[2,2]] +t = L([1,2,3]).zipwith(b) +test_eq(t, [(1,[0]), (2,[1]), (3,[2,2])]) +``` + +------------------------------------------------------------------------ + +source + +### L.map_zipwith + +> L.map_zipwith (f, *rest, cycled=False, **kwargs) + +*Combine `zipwith` and `starmap`* + +``` python +test_eq(L(1,2,3).map_zipwith(operator.mul, [2,3,4]), [2,6,12]) +``` + +------------------------------------------------------------------------ + +source + +### L.itemgot + +> L.itemgot (*idxs) + +*Create new [`L`](https://fastcore.fast.ai/foundation.html#l) with item +`idx` of all `items`* + +``` python +test_eq(t.itemgot(1), b) +``` + +------------------------------------------------------------------------ + +source + +### L.attrgot + +> L.attrgot (k, default=None) + +*Create new [`L`](https://fastcore.fast.ai/foundation.html#l) with attr +`k` (or value `k` for dicts) of all `items`.* + +``` python +# Example when items are not a dict +a = [SimpleNamespace(a=3,b=4),SimpleNamespace(a=1,b=2)] +test_eq(L(a).attrgot('b'), [4,2]) + +#Example of when items are a dict +b =[{'id': 15, 'name': 'nbdev'}, {'id': 17, 'name': 'fastcore'}] +test_eq(L(b).attrgot('id'), [15, 17]) +``` + +------------------------------------------------------------------------ + +source + +### L.sorted + +> L.sorted (key=None, reverse=False) + +*New [`L`](https://fastcore.fast.ai/foundation.html#l) sorted by `key`. +If key is str use `attrgetter`; if int use `itemgetter`* + +``` python +test_eq(L(a).sorted('a').attrgot('b'), [2,4]) +``` + +------------------------------------------------------------------------ + +source + +### L.split + +> L.split (s, sep=None, maxsplit=-1) + +*Class Method: Same as `str.split`, but returns an +[`L`](https://fastcore.fast.ai/foundation.html#l)* + +``` python +test_eq(L.split('a b c'), list('abc')) +``` + +------------------------------------------------------------------------ + +source + +### L.range + +> L.range (a, b=None, step=None) + +*Class Method: Same as `range`, but returns +[`L`](https://fastcore.fast.ai/foundation.html#l). Can pass collection +for `a`, to use `len(a)`* + +``` python +test_eq_type(L.range([1,1,1]), L(range(3))) +test_eq_type(L.range(5,2,2), L(range(5,2,2))) +``` + +------------------------------------------------------------------------ + +source + +### L.concat + +> L.concat () + +*Concatenate all elements of list* + +``` python +test_eq(L([0,1,2,3],4,L(5,6)).concat(), range(7)) +``` + +------------------------------------------------------------------------ + +source + +### L.copy + +> L.copy () + +*Same as `list.copy`, but returns an +[`L`](https://fastcore.fast.ai/foundation.html#l)* + +``` python +t = L([0,1,2,3],4,L(5,6)).copy() +test_eq(t.concat(), range(7)) +``` + +------------------------------------------------------------------------ + +source + +### L.map_first + +> L.map_first (f=, g=, *args, **kwargs) + +*First element of `map_filter`* + +``` python +t = L(0,1,2,3) +test_eq(t.map_first(lambda o:o*2 if o>2 else None), 6) +``` + +------------------------------------------------------------------------ + +source + +### L.setattrs + +> L.setattrs (attr, val) + +*Call `setattr` on all items* + +``` python +t = L(SimpleNamespace(),SimpleNamespace()) +t.setattrs('foo', 'bar') +test_eq(t.attrgot('foo'), ['bar','bar']) +``` + +## Config + +------------------------------------------------------------------------ + +source + +### save_config_file + +> save_config_file (file, d, **kwargs) + +*Write settings dict to a new config file, or overwrite the existing +one.* + +------------------------------------------------------------------------ + +source + +### read_config_file + +> read_config_file (file, **kwargs) + +Config files are saved and read using Python’s +`configparser.ConfigParser`, inside the `DEFAULT` section. + +``` python +_d = dict(user='fastai', lib_name='fastcore', some_path='test', some_bool=True, some_num=3) +try: + save_config_file('tmp.ini', _d) + res = read_config_file('tmp.ini') +finally: os.unlink('tmp.ini') +dict(res) +``` + + {'user': 'fastai', + 'lib_name': 'fastcore', + 'some_path': 'test', + 'some_bool': 'True', + 'some_num': '3'} + +------------------------------------------------------------------------ + +source + +### Config + +> Config (cfg_path, cfg_name, create=None, save=True, extra_files=None, +> types=None) + +*Reading and writing `ConfigParser` ini files* + +[`Config`](https://fastcore.fast.ai/foundation.html#config) is a +convenient wrapper around `ConfigParser` ini files with a single section +(`DEFAULT`). + +Instantiate a +[`Config`](https://fastcore.fast.ai/foundation.html#config) from an ini +file at `cfg_path/cfg_name`: + +``` python +save_config_file('../tmp.ini', _d) +try: cfg = Config('..', 'tmp.ini') +finally: os.unlink('../tmp.ini') +cfg +``` + + {'user': 'fastai', 'lib_name': 'fastcore', 'some_path': 'test', 'some_bool': 'True', 'some_num': '3'} + +You can create a new file if one doesn’t exist by providing a `create` +dict: + +``` python +try: cfg = Config('..', 'tmp.ini', create=_d) +finally: os.unlink('../tmp.ini') +cfg +``` + + {'user': 'fastai', 'lib_name': 'fastcore', 'some_path': 'test', 'some_bool': 'True', 'some_num': '3'} + +If you additionally pass `save=False`, the +[`Config`](https://fastcore.fast.ai/foundation.html#config) will contain +the items from `create` without writing a new file: + +``` python +cfg = Config('..', 'tmp.ini', create=_d, save=False) +test_eq(cfg.user,'fastai') +assert not Path('../tmp.ini').exists() +``` + +------------------------------------------------------------------------ + +source + +### Config.get + +> Config.get (k, default=None) + +Keys can be accessed as attributes, items, or with `get` and an optional +default: + +``` python +test_eq(cfg.user,'fastai') +test_eq(cfg['some_path'], 'test') +test_eq(cfg.get('foo','bar'),'bar') +``` + +Extra files can be read *before* `cfg_path/cfg_name` using +`extra_files`, in the order they appear: + +``` python +with tempfile.TemporaryDirectory() as d: + a = Config(d, 'a.ini', {'a':0,'b':0}) + b = Config(d, 'b.ini', {'a':1,'c':0}) + c = Config(d, 'c.ini', {'a':2,'d':0}, extra_files=[a.config_file,b.config_file]) + test_eq(c.d, {'a':'2','b':'0','c':'0','d':'0'}) +``` + +If you pass a dict `types`, then the values of that dict will be used as +types to instantiate all values returned. `Path` is a special case – in +that case, the path returned will be relative to the path containing the +config file (assuming the value is relative). `bool` types use +[`str2bool`](https://fastcore.fast.ai/basics.html#str2bool) to convert +to boolean. + +``` python +_types = dict(some_path=Path, some_bool=bool, some_num=int) +cfg = Config('..', 'tmp.ini', create=_d, save=False, types=_types) + +test_eq(cfg.user,'fastai') +test_eq(cfg['some_path'].resolve(), (Path('..')/'test').resolve()) +test_eq(cfg.get('some_num'), 3) +``` + +------------------------------------------------------------------------ + +source + +### Config.find + +> Config.find (cfg_name, cfg_path=None, **kwargs) + +*Search `cfg_path` and its parents to find `cfg_name`* + +You can use +[`Config.find`](https://fastcore.fast.ai/foundation.html#config.find) to +search subdirectories for a config file, starting in the current path if +no path is specified: + +``` python +Config.find('settings.ini').repo +``` + + 'fastcore' diff --git a/images/att_00000.png b/images/att_00000.png new file mode 100644 index 00000000..7d44d3f4 Binary files /dev/null and b/images/att_00000.png differ diff --git a/images/att_00005.png b/images/att_00005.png new file mode 100644 index 00000000..de857c98 Binary files /dev/null and b/images/att_00005.png differ diff --git a/images/att_00006.png b/images/att_00006.png new file mode 100644 index 00000000..eaa1d257 Binary files /dev/null and b/images/att_00006.png differ diff --git a/images/att_00007.png b/images/att_00007.png new file mode 100644 index 00000000..b21a11b0 Binary files /dev/null and b/images/att_00007.png differ diff --git a/index.html b/index.html new file mode 100644 index 00000000..e64ab8a7 --- /dev/null +++ b/index.html @@ -0,0 +1,729 @@ + + + + + + + + + + +Welcome to fastcore – fastcore + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ +
+ + +
+ + + +
+ +
+
+

Welcome to fastcore

+
+ +
+
+ Python goodies to make your coding faster, easier, and more maintainable +
+
+ + +
+ + + + +
+ + + +
+ + + +

Python is a powerful, dynamic language. Rather than bake everything into the language, it lets the programmer customize it to make it work for them. fastcore uses this flexibility to add to Python features inspired by other languages we’ve loved, like multiple dispatch from Julia, mixins from Ruby, and currying, binding, and more from Haskell. It also adds some “missing features” and clean up some rough edges in the Python standard library, such as simplifying parallel processing, and bringing ideas from NumPy over to Python’s list type.

+
+

Getting started

+

To install fastcore run: conda install fastcore -c fastai (if you use Anaconda, which we recommend) or pip install fastcore. For an editable install, clone this repo and run: pip install -e ".[dev]". fastcore is tested to work on Ubuntu, macOS and Windows (versions tested are those shown with the -latest suffix here).

+

fastcore contains many features, including:

+
    +
  • fastcore.test: Simple testing functions
  • +
  • fastcore.foundation: Mixins, delegation, composition, and more
  • +
  • fastcore.xtras: Utility functions to help with functional-style programming, parallel processing, and more
  • +
  • fastcore.dispatch: Multiple dispatch methods
  • +
  • fastcore.transform: Pipelines of composed partially reversible transformations
  • +
+

To get started, we recommend you read through the fastcore tour.

+
+
+

Contributing

+

After you clone this repository, please run nbdev_install_hooks in your terminal. This sets up git hooks, which clean up the notebooks to remove the extraneous stuff stored in the notebooks (e.g. which cells you ran) which causes unnecessary merge conflicts.

+

To run the tests in parallel, launch nbdev_test.

+

Before submitting a PR, check that the local library and notebooks match.

+
    +
  • If you made a change to the notebooks in one of the exported cells, you can export it to the library with nbdev_prepare.
  • +
  • If you made a change to the library, you can export it back to the notebooks with nbdev_update.
  • +
+ + +
+ +
+ +
+ + + + + \ No newline at end of file diff --git a/index.html.md b/index.html.md new file mode 100644 index 00000000..b6472b0d --- /dev/null +++ b/index.html.md @@ -0,0 +1,54 @@ +# Welcome to fastcore + + + + +Python is a powerful, dynamic language. Rather than bake everything into +the language, it lets the programmer customize it to make it work for +them. `fastcore` uses this flexibility to add to Python features +inspired by other languages we’ve loved, like multiple dispatch from +Julia, mixins from Ruby, and currying, binding, and more from Haskell. +It also adds some “missing features” and clean up some rough edges in +the Python standard library, such as simplifying parallel processing, +and bringing ideas from NumPy over to Python’s `list` type. + +## Getting started + +To install fastcore run: `conda install fastcore -c fastai` (if you use +Anaconda, which we recommend) or `pip install fastcore`. For an +[editable +install](https://stackoverflow.com/questions/35064426/when-would-the-e-editable-option-be-useful-with-pip-install), +clone this repo and run: `pip install -e ".[dev]"`. fastcore is tested +to work on Ubuntu, macOS and Windows (versions tested are those shown +with the `-latest` suffix +[here](https://docs.github.com/en/actions/reference/specifications-for-github-hosted-runners#supported-runners-and-hardware-resources)). + +`fastcore` contains many features, including: + +- `fastcore.test`: Simple testing functions +- `fastcore.foundation`: Mixins, delegation, composition, and more +- `fastcore.xtras`: Utility functions to help with functional-style + programming, parallel processing, and more +- `fastcore.dispatch`: Multiple dispatch methods +- `fastcore.transform`: Pipelines of composed partially reversible + transformations + +To get started, we recommend you read through [the fastcore +tour](https://fastcore.fast.ai/tour.html). + +## Contributing + +After you clone this repository, please run `nbdev_install_hooks` in +your terminal. This sets up git hooks, which clean up the notebooks to +remove the extraneous stuff stored in the notebooks (e.g. which cells +you ran) which causes unnecessary merge conflicts. + +To run the tests in parallel, launch `nbdev_test`. + +Before submitting a PR, check that the local library and notebooks +match. + +- If you made a change to the notebooks in one of the exported cells, + you can export it to the library with `nbdev_prepare`. +- If you made a change to the library, you can export it back to the + notebooks with `nbdev_update`. diff --git a/llms-ctx-full.txt b/llms-ctx-full.txt new file mode 100644 index 00000000..bafbe93e --- /dev/null +++ b/llms-ctx-full.txt @@ -0,0 +1,12213 @@ +fastcore adds to Python features inspired by other languages, like multiple dispatch from Julia, mixins from Ruby, and currying, binding, and more from Haskell. It also adds some “missing features” and clean up some rough edges in the Python standard library, such as simplifying parallel processing, and bringing ideas from NumPy over to Python’s list type. + +Here are some tips on using fastcore: + +- **Liberal imports**: Utilize `from fastcore.module import *` freely. The library is designed for safe wildcard imports. +- **Enhanced list operations**: Substitute `list` with `L`. This provides advanced indexing, method chaining, and additional functionality while maintaining list-like behavior. +- **Extend existing classes**: Apply the `@patch` decorator to add methods to classes, including built-ins, without subclassing. This enables more flexible code organization. +- **Streamline class initialization**: In `__init__` methods, use `store_attr()` to efficiently set multiple attributes, reducing repetitive assignment code. +- **Explicit keyword arguments**: Apply the `delegates` decorator to functions to replace `**kwargs` with specific parameters, enhancing IDE support and documentation. +- **Optimize parallel execution**: Leverage fastcore's enhanced `ThreadPoolExecutor` and `ProcessPoolExecutor` for simplified concurrent processing. +- **Expressive testing**: Prefer fastcore's testing functions like `test_eq`, `test_ne`, `test_close` for more readable and informative test assertions. +- **Advanced file operations**: Use the extended `Path` class, which adds methods like `ls()`, `read_json()`, and others to `pathlib.Path`. +- **Flexible data structures**: Convert between dictionaries and attribute-access objects using `dict2obj` and `obj2dict` for more intuitive data handling. +- **Data pipeline construction**: Employ `Transform` and `Pipeline` classes to create modular, composable data processing workflows. +- **Functional programming paradigms**: Utilize tools like `compose`, `maps`, and `filter_ex` to write more functional-style Python code. +- **Documentation**: Use `docments` where possible to document parameters of functions and methods. +- **Time-aware caching**: Apply the `timed_cache` decorator to add time-based expiration to the standard `lru_cache` functionality. +- **Simplified CLI creation**: Use fastcore's console script utilities to easily transform Python functions into command-line interfaces.# A tour of fastcore + + + +Here’s a (somewhat) quick tour of a few higlights from fastcore. + +### Documentation + +All fast.ai projects, including this one, are built with +[nbdev](https://nbdev.fast.ai), which is a full literate programming +environment built on Jupyter Notebooks. That means that every piece of +documentation, including the page you’re reading now, can be accessed as +interactive Jupyter notebooks. In fact, you can even grab a link +directly to a notebook running interactively on Google Colab - if you +want to follow along with this tour, click the link below: + +``` python +colab_link('index') +``` + +[Open `index` in +Colab](https://colab.research.google.com/github/fastai/fastcore/blob/master/nbs/index.ipynb) + +The full docs are available at +[fastcore.fast.ai](https://fastcore.fast.ai). The code in the examples +and in all fast.ai libraries follow the [fast.ai style +guide](https://docs.fast.ai/dev/style.html). In order to support +interactive programming, all fast.ai libraries are designed to allow for +`import *` to be used safely, particular by ensuring that +[`__all__`](https://riptutorial.com/python/example/2894/the---all---special-variable) +is defined in all packages. In order to see where a function is from, +just type it: + +``` python +coll_repr +``` + + + +For more details, including a link to the full documentation and source +code, use `doc`, which pops up a window with this information: + +``` python +doc(coll_repr) +``` + + + +The documentation also contains links to any related functions or +classes, which appear like this: +[`coll_repr`](https://fastcore.fast.ai/foundation.html#coll_repr) (in +the notebook itself you will just see a word with back-ticks around it; +the links are auto-generated in the documentation site). The +documentation will generally show one or more examples of use, along +with any background context necessary to understand them. As you’ll see, +the examples for each function and method are shown as tests, rather +than example outputs, so let’s start by explaining that. + +### Testing + +fastcore’s testing module is designed to work well with +[nbdev](https://nbdev.fast.ai), which is a full literate programming +environment built on Jupyter Notebooks. That means that your tests, +docs, and code all live together in the same notebook. fastcore and +nbdev’s approach to testing starts with the premise that all your tests +should pass. If one fails, no more tests in a notebook are run. + +Tests look like this: + +``` python +test_eq(coll_repr(range(1000), 5), '(#1000) [0,1,2,3,4...]') +``` + +That’s an example from the docs for +[`coll_repr`](https://fastcore.fast.ai/foundation.html#coll_repr). As +you see, it’s not showing you the output directly. Here’s what that +would look like: + +``` python +coll_repr(range(1000), 5) +``` + + '(#1000) [0,1,2,3,4...]' + +So, the test is actually showing you what the output looks like, because +if the function call didn’t return `'(#1000) [0,1,2,3,4...]'`, then the +test would have failed. + +So every test shown in the docs is also showing you the behavior of the +library — and vice versa! + +Test functions always start with `test_`, and then follow with the +operation being tested. So +[`test_eq`](https://fastcore.fast.ai/test.html#test_eq) tests for +equality (as you saw in the example above). This includes tests for +equality of arrays and tensors, lists and generators, and many more: + +``` python +test_eq([0,1,2,3], np.arange(4)) +``` + +When a test fails, it prints out information about what was expected: + +``` python +test_eq([0,1,2,3], np.arange(3)) +``` + + ---- + AssertionError: ==: + [0, 1, 2, 3] + [0 1 2] + +If you want to check that objects are the same type, rather than the +just contain the same collection, use +[`test_eq_type`](https://fastcore.fast.ai/test.html#test_eq_type). + +You can test with any comparison function using +[`test`](https://fastcore.fast.ai/test.html#test), e.g test whether an +object is less than: + +``` python +test(2, 3, operator.lt) +``` + +You can even test that exceptions are raised: + +``` python +def divide_zero(): return 1/0 +test_fail(divide_zero) +``` + +…and test that things are printed to stdout: + +``` python +test_stdout(lambda: print('hi'), 'hi') +``` + +### Foundations + +fast.ai is unusual in that we often use +[mixins](https://en.wikipedia.org/wiki/Mixin) in our code. Mixins are +widely used in many programming languages, such as Ruby, but not so much +in Python. We use mixins to attach new behavior to existing libraries, +or to allow modules to add new behavior to our own classes, such as in +extension modules. One useful example of a mixin we define is +[`Path.ls`](https://fastcore.fast.ai/xtras.html#path.ls), which lists a +directory and returns an +[`L`](https://fastcore.fast.ai/foundation.html#l) (an extended list +class which we’ll discuss shortly): + +``` python +p = Path('images') +p.ls() +``` + + (#6) [Path('images/mnist3.png'),Path('images/att_00000.png'),Path('images/puppy.jpg'),Path('images/att_00005.png'),Path('images/att_00007.png'),Path('images/att_00006.png')] + +You can easily add you own mixins with the +[`patch`](https://fastcore.fast.ai/basics.html#patch) +[decorator](https://realpython.com/primer-on-python-decorators/), which +takes advantage of Python 3 [function +annotations](https://www.python.org/dev/peps/pep-3107/#parameters) to +say what class to patch: + +``` python +@patch +def num_items(self:Path): return len(self.ls()) + +p.num_items() +``` + + 6 + +We also use `**kwargs` frequently. In python `**kwargs` in a parameter +like means “*put any additional keyword arguments into a dict called +`kwargs`*”. Normally, using `kwargs` makes an API quite difficult to +work with, because it breaks things like tab-completion and popup lists +of signatures. `utils` provides +[`use_kwargs`](https://fastcore.fast.ai/meta.html#use_kwargs) and +[`delegates`](https://fastcore.fast.ai/meta.html#delegates) to avoid +this problem. See our [detailed article on +delegation](https://www.fast.ai/2019/08/06/delegation/) on this topic. + +[`GetAttr`](https://fastcore.fast.ai/basics.html#getattr) solves a +similar problem (and is also discussed in the article linked above): +it’s allows you to use Python’s exceptionally useful +[`__getattr__`](https://fastcore.fast.ai/xml.html#__getattr__) magic +method, but avoids the problem that normally in Python tab-completion +and docs break when using this. For instance, you can see here that +Python’s `dir` function, which is used to find the attributes of a +python object, finds everything inside the `self.default` attribute +here: + +``` python +class Author: + def __init__(self, name): self.name = name + +class ProductPage(GetAttr): + _default = 'author' + def __init__(self,author,price,cost): self.author,self.price,self.cost = author,price,cost + +p = ProductPage(Author("Jeremy"), 1.50, 0.50) +[o for o in dir(p) if not o.startswith('_')] +``` + + ['author', 'cost', 'name', 'price'] + +Looking at that `ProductPage` example, it’s rather verbose and +duplicates a lot of attribute names, which can lead to bugs later if you +change them only in one place. `fastcore` provides +[`store_attr`](https://fastcore.fast.ai/basics.html#store_attr) to +simplify this common pattern. It also provides +[`basic_repr`](https://fastcore.fast.ai/basics.html#basic_repr) to give +simple objects a useful `repr`: + +``` python +class ProductPage: + def __init__(self,author,price,cost): store_attr() + __repr__ = basic_repr('author,price,cost') + +ProductPage("Jeremy", 1.50, 0.50) +``` + + __main__.ProductPage(author='Jeremy', price=1.5, cost=0.5) + +One of the most interesting `fastcore` functions is the +[`funcs_kwargs`](https://fastcore.fast.ai/meta.html#funcs_kwargs) +decorator. This allows class behavior to be modified without +sub-classing. This can allow folks that aren’t familiar with +object-oriented programming to customize your class more easily. Here’s +an example of a class that uses +[`funcs_kwargs`](https://fastcore.fast.ai/meta.html#funcs_kwargs): + +``` python +@funcs_kwargs +class T: + _methods=['some_method'] + def __init__(self, **kwargs): assert not kwargs, f'Passed unknown args: {kwargs}' + +p = T(some_method = print) +p.some_method("hello") +``` + + hello + +The `assert not kwargs` above is used to ensure that the user doesn’t +pass an unknown parameter (i.e one that’s not in `_methods`). `fastai` +uses [`funcs_kwargs`](https://fastcore.fast.ai/meta.html#funcs_kwargs) +in many places, for instance, you can customize any part of a +`DataLoader` by passing your own methods. + +`fastcore` also provides many utility functions that make a Python +programmer’s life easier, in `fastcore.utils`. We won’t look at many +here, since you can easily look at the docs yourself. To get you +started, have a look at the docs for +[`chunked`](https://fastcore.fast.ai/basics.html#chunked) (remember, if +you’re in a notebook, type `doc(chunked)`), which is a handy function +for creating lazily generated batches from a collection. + +Python’s +[`ProcessPoolExecutor`](https://fastcore.fast.ai/parallel.html#processpoolexecutor) +is extended to allow `max_workers` to be set to `0`, to easily turn off +parallel processing. This makes it easy to debug your code in serial, +then run it in parallel. It also allows you to pass arguments to your +parallel function, and to ensure there’s a pause between calls, in case +the process you are running has race conditions. +[`parallel`](https://fastcore.fast.ai/parallel.html#parallel) makes +parallel processing even easier to use, and even adds an optional +progress bar. + +### L + +Like most languages, Python allows for very concise syntax for some very +common types, such as `list`, which can be constructed with `[1,2,3]`. +Perl’s designer Larry Wall explained the reasoning for this kind of +syntax: + +> In metaphorical honor of Huffman’s compression code that assigns +> smaller numbers of bits to more common bytes. In terms of syntax, it +> simply means that commonly used things should be shorter, but you +> shouldn’t waste short sequences on less common constructs. + +On this basis, `fastcore` has just one type that has a single letter +name: [`L`](https://fastcore.fast.ai/foundation.html#l). The reason for +this is that it is designed to be a replacement for `list`, so we want +it to be just as easy to use as `[1,2,3]`. Here’s how to create that as +an [`L`](https://fastcore.fast.ai/foundation.html#l): + +``` python +L(1,2,3) +``` + + (#3) [1,2,3] + +The first thing to notice is that an +[`L`](https://fastcore.fast.ai/foundation.html#l) object includes in its +representation its number of elements; that’s the `(#3)` in the output +above. If there’s more than 10 elements, it will automatically truncate +the list: + +``` python +p = L.range(20).shuffle() +p +``` + + (#20) [5,1,9,10,18,13,6,17,3,16...] + +[`L`](https://fastcore.fast.ai/foundation.html#l) contains many of the +same indexing ideas that NumPy’s `array` does, including indexing with a +list of indexes, or a boolean mask list: + +``` python +p[2,4,6] +``` + + (#3) [9,18,6] + +It also contains other methods used in `array`, such as +[`L.argwhere`](https://fastcore.fast.ai/foundation.html#l.argwhere): + +``` python +p.argwhere(ge(15)) +``` + + (#5) [4,7,9,18,19] + +As you can see from this example, `fastcore` also includes a number of +features that make a functional style of programming easier, such as a +full range of boolean functions (e.g `ge`, `gt`, etc) which give the +same answer as the functions from Python’s `operator` module if given +two parameters, but return a [curried +function](https://en.wikipedia.org/wiki/Currying) if given one +parameter. + +There’s too much functionality to show it all here, so be sure to check +the docs. Many little things are added that we thought should have been +in `list` in the first place, such as making this do what you’d expect +(which is an error with `list`, but works fine with +[`L`](https://fastcore.fast.ai/foundation.html#l)): + +``` python +1 + L(2,3,4) +``` + + (#4) [1,2,3,4] + +### Transforms + +A [`Transform`](https://fastcore.fast.ai/transform.html#transform) is +the main building block of the fastai data pipelines. In the most +general terms a transform can be any function you want to apply to your +data, however the +[`Transform`](https://fastcore.fast.ai/transform.html#transform) class +provides several mechanisms that make the process of building them easy +and flexible (see the docs for information about each of these): + +- Type dispatch +- Dispatch over tuples +- Reversability +- Type propagation +- Preprocessing +- Filtering based on the dataset type +- Ordering +- Appending new behavior with decorators + +[`Transform`](https://fastcore.fast.ai/transform.html#transform) looks +for three special methods, encodes, decodes, +and setups, which provide the implementation for +[`__call__`](https://www.python-course.eu/python3_magic_methods.php), +`decode`, and `setup` respectively. For instance: + +``` python +class A(Transform): + def encodes(self, x): return x+1 + +A()(1) +``` + + 2 + +For simple transforms like this, you can also use +[`Transform`](https://fastcore.fast.ai/transform.html#transform) as a +decorator: + +``` python +@Transform +def f(x): return x+1 + +f(1) +``` + + 2 + +Transforms can be composed into a +[`Pipeline`](https://fastcore.fast.ai/transform.html#pipeline): + +``` python +@Transform +def g(x): return x/2 + +pipe = Pipeline([f,g]) +pipe(3) +``` + + 2.0 + +The power of +[`Transform`](https://fastcore.fast.ai/transform.html#transform) and +[`Pipeline`](https://fastcore.fast.ai/transform.html#pipeline) is best +understood by seeing how they’re used to create a complete data +processing pipeline. This is explained in [chapter +11](https://github.com/fastai/fastbook/blob/master/11_midlevel_data.ipynb) +of the [fastai +book](https://www.amazon.com/Deep-Learning-Coders-fastai-PyTorch/dp/1492045527), +which is [available for free](https://github.com/fastai/fastbook) in +Jupyter Notebook format.# fastcore: An Underrated Python Library + +A unique python library that extends the python programming language and provides utilities that enhance productivity. + +Sep 1, 2020 • Hamel Husain • 14 min read + +__fastcore fastai + +# Background __ + +I recently embarked on a journey to sharpen my python skills: I wanted to learn advanced patterns, idioms, and techniques. I started with reading books on advanced Python, however, the information didn't seem to stick without having somewhere to apply it. I also wanted the ability to ask questions from an expert while I was learning -- which is an arrangement that is hard to find! That's when it occurred to me: What if I could find an open source project that has fairly advanced python code and write documentation and tests? I made a bet that if I did this it would force me to learn everything very deeply, and the maintainers would be appreciative of my work and be willing to answer my questions. + +And that's exactly what I did over the past month! I'm pleased to report that it has been the most efficient learning experience I've ever experienced. I've discovered that writing documentation forced me to deeply understand not just what the code does but also _why the code works the way it does_ , and to explore edge cases while writing tests. Most importantly, I was able to ask questions when I was stuck, and maintainers were willing to devote extra time knowing that their mentorship was in service of making their code more accessible! It turns out the library I choose, fastcore is some of the most fascinating Python I have ever encountered as its purpose and goals are fairly unique. + +For the uninitiated, fastcore is a library on top of which many fast.ai projects are built on. Most importantly, fastcore extends the python programming language and strives to eliminate boilerplate and add useful functionality for common tasks. In this blog post, I'm going to highlight some of my favorite tools that fastcore provides, rather than sharing what I learned about python. My goal is to pique your interest in this library, and hopefully motivate you to check out the documentation after you are done to learn more! + +# Why fastcore is interesting __ + + 1. **Get exposed to ideas from other languages without leaving python:** I’ve always heard that it is beneficial to learn other languages in order to become a better programmer. From a pragmatic point of view, I’ve found it difficult to learn other languages because I could never use them at work. Fastcore extends python to include patterns found in languages as diverse as Julia, Ruby and Haskell. Now that I understand these tools I am motivated to learn other languages. + 2. **You get a new set of pragmatic tools** : fastcore includes utilities that will allow you to write more concise expressive code, and perhaps solve new problems. + 3. **Learn more about the Python programming language:** Because fastcore extends the python programming language, many advanced concepts are exposed during the process. For the motivated, this is a great way to see how many of the internals of python work. + +# A whirlwind tour through fastcore __ + +Here are some things you can do with fastcore that immediately caught my attention. + +* * * + +## Making **kwargs transparent __ + +Whenever I see a function that has the argument****kwargs** , I cringe a little. This is because it means the API is obfuscated and I have to read the source code to figure out what valid parameters might be. Consider the below example: + +``` +def baz(a, b=2, c=3, d=4): return a + b + c + +def foo(c, a, **kwargs): + return c + baz(a, **kwargs) + +inspect.signature(foo) + +``` + +``` + +``` + +Without reading the source code, it might be hard for me to know that `foo` also accepts and additional parameters `b` and `d`. We can fix this with `delegates`: + +``` +def baz(a, b=2, c=3, d=4): return a + b + c + +@delegates(baz) # this decorator will pass down keyword arguments from baz +def foo(c, a, **kwargs): + return c + baz(a, **kwargs) + +inspect.signature(foo) + +``` + +``` + +``` + +You can customize the behavior of this decorator. For example, you can have your cake and eat it too by passing down your arguments and also keeping `**kwargs`: + +``` +@delegates(baz, keep=True) +def foo(c, a, **kwargs): + return c + baz(a, **kwargs) + +inspect.signature(foo) + +``` + +``` + +``` + +You can also exclude arguments. For example, we exclude argument `d` from delegation: + +``` +def basefoo(a, b=2, c=3, d=4): pass + +@delegates(basefoo, but=['d']) # exclude `d` +def foo(c, a, **kwargs): pass + +inspect.signature(foo) + +``` + +``` + +``` + +You can also delegate between classes: + +``` +class BaseFoo: + def __init__(self, e, c=2): pass + +@delegates()# since no argument was passsed here we delegate to the superclass +class Foo(BaseFoo): + def __init__(self, a, b=1, **kwargs): super().__init__(**kwargs) + +inspect.signature(Foo) + +``` + +``` + +``` + +For more information, read the docs on delegates. + +* * * + +## Avoid boilerplate when setting instance attributes __ + +Have you ever wondered if it was possible to avoid the boilerplate involved with setting attributes in`__init__`? + +``` +class Test: + def __init__(self, a, b ,c): + self.a, self.b, self.c = a, b, c + +``` + +Ouch! That was painful. Look at all the repeated variable names. Do I really have to repeat myself like this when defining a class? Not Anymore! Checkout store_attr: + +``` +class Test: + def __init__(self, a, b, c): + store_attr() + +t = Test(5,4,3) +assert t.b == 4 + +``` + +You can also exclude certain attributes: + +``` +class Test: + def __init__(self, a, b, c): + store_attr(but=['c']) + +t = Test(5,4,3) +assert t.b == 4 +assert not hasattr(t, 'c') + +``` + +There are many more ways of customizing and using `store_attr` than I highlighted here. Check out the docs for more detail. + +P.S. you might be thinking that Python dataclasses also allow you to avoid this boilerplate. While true in some cases, `store_attr` is more flexible.1 + +1\. For example, store_attr does not rely on inheritance, which means you won't get stuck using multiple inheritance when using this with your own classes. Also, unlike dataclasses, store_attr does not require python 3.7 or higher. Furthermore, you can use store_attr anytime in the object lifecycle, and in any location in your class to customize the behavior of how and when variables are stored.↩ + +* * * + +## Avoiding subclassing boilerplate __ + +One thing I hate about python is the`__super__().__init__()` boilerplate associated with subclassing. For example: + +``` +class ParentClass: + def __init__(self): self.some_attr = 'hello' + +class ChildClass(ParentClass): + def __init__(self): + super().__init__() + +cc = ChildClass() +assert cc.some_attr == 'hello' # only accessible b/c you used super + +``` + +We can avoid this boilerplate by using the metaclass PrePostInitMeta. We define a new class called `NewParent` that is a wrapper around the `ParentClass`: + +``` +class NewParent(ParentClass, metaclass=PrePostInitMeta): + def __pre_init__(self, *args, **kwargs): super().__init__() + +class ChildClass(NewParent): + def __init__(self):pass + +sc = ChildClass() +assert sc.some_attr == 'hello' + +``` + +* * * + +## Type Dispatch __ + +Type dispatch, orMultiple dispatch, allows you to change the way a function behaves based upon the input types it receives. This is a prominent feature in some programming languages like Julia. For example, this is a conceptual example of how multiple dispatch works in Julia, returning different values depending on the input types of x and y: + +``` +collide_with(x::Asteroid, y::Asteroid) = ... +# deal with asteroid hitting asteroid + +collide_with(x::Asteroid, y::Spaceship) = ... +# deal with asteroid hitting spaceship + +collide_with(x::Spaceship, y::Asteroid) = ... +# deal with spaceship hitting asteroid + +collide_with(x::Spaceship, y::Spaceship) = ... +# deal with spaceship hitting spaceship + +``` + +Type dispatch can be especially useful in data science, where you might allow different input types (i.e. Numpy arrays and Pandas dataframes) to a function that processes data. Type dispatch allows you to have a common API for functions that do similar tasks. + +Unfortunately, Python does not support this out-of-the box. Fortunately, there is the @typedispatch decorator to the rescue. This decorator relies upon type hints in order to route inputs the correct version of the function: + +``` +@typedispatch +def f(x:str, y:str): return f'{x}{y}' + +@typedispatch +def f(x:np.ndarray): return x.sum() + +@typedispatch +def f(x:int, y:int): return x+y + +``` + +Below is a demonstration of type dispatch at work for the function `f`: + +``` +f('Hello ', 'World!') + +``` + +``` +'Hello World!' +``` + +``` +f(2,3) + +``` + +``` +5 +``` + +``` +f(np.array([5,5,5,5])) + +``` + +``` +20 +``` + +There are limitations of this feature, as well as other ways of using this functionality that you can read about here. In the process of learning about typed dispatch, I also found a python library called multipledispatch made by Mathhew Rocklin (the creator of Dask). + +After using this feature, I am now motivated to learn languages like Julia to discover what other paradigms I might be missing. + +* * * + +## A better version of functools.partial __ + +`functools.partial` is a great utility that creates functions from other functions that lets you set default values. Lets take this function for example that filters a list to only contain values >= `val`: + +``` +test_input = [1,2,3,4,5,6] +def f(arr, val): + "Filter a list to remove any values that are less than val." + return [x for x in arr if x >= val] + +f(test_input, 3) + +``` + +``` +[3, 4, 5, 6] +``` + +You can create a new function out of this function using `partial` that sets the default value to 5: + +``` +filter5 = partial(f, val=5) +filter5(test_input) + +``` + +``` +[5, 6] +``` + +One problem with `partial` is that it removes the original docstring and replaces it with a generic docstring: + +``` +filter5.__doc__ + +``` + +``` +'partial(func, *args, **keywords) - new function with partial application\n of the given arguments and keywords.\n' +``` + +fastcore.utils.partialler fixes this, and makes sure the docstring is retained such that the new API is transparent: + +``` +filter5 = partialler(f, val=5) +filter5.__doc__ + +``` + +``` +'Filter a list to remove any values that are less than val.' +``` + +* * * + +## Composition of functions __ + +A technique that is pervasive in functional programming languages is function composition, whereby you chain a bunch of functions together to achieve some kind of result. This is especially useful when applying various data transformations. Consider a toy example where I have three functions: (1) Removes elements of a list less than 5 (from the prior section) (2) adds 2 to each number (3) sums all the numbers: + +``` +def add(arr, val): return [x + val for x in arr] +def arrsum(arr): return sum(arr) + +# See the previous section on partialler +add2 = partialler(add, val=2) + +transform = compose(filter5, add2, arrsum) +transform([1,2,3,4,5,6]) + +``` + +``` +15 +``` + +But why is this useful? You might me thinking, I can accomplish the same thing with: + +``` +arrsum(add2(filter5([1,2,3,4,5,6]))) + +``` + +You are not wrong! However, composition gives you a convenient interface in case you want to do something like the following: + +``` +def fit(x, transforms:list): + "fit a model after performing transformations" + x = compose(*transforms)(x) + y = [np.mean(x)] * len(x) # its a dumb model. Don't judge me + return y + +# filters out elements < 5, adds 2, then predicts the mean +fit(x=[1,2,3,4,5,6], transforms=[filter5, add2]) + +``` + +``` +[7.5, 7.5] +``` + +For more information about `compose`, read the docs. + +* * * + +## A more useful `__repr__`__ + +In python,`__repr__` helps you get information about an object for logging and debugging. Below is what you get by default when you define a new class. (Note: we are using `store_attr`, which was discussed earlier). + +``` +class Test: + def __init__(self, a, b=2, c=3): store_attr() # `store_attr` was discussed previously + +Test(1) + +``` + +``` +<__main__.Test at 0x7ffcd766cee0> +``` + +We can use basic_repr to quickly give us a more sensible default: + +``` +class Test: + def __init__(self, a, b=2, c=3): store_attr() + __repr__ = basic_repr('a,b,c') + +Test(2) + +``` + +``` +Test(a=2, b=2, c=3) +``` + +* * * + +## Monkey Patching With A Decorator __ + +It can be convenient tomonkey patch with a decorator, which is especially helpful when you want to patch an external library you are importing. We can use the decorator @patch from `fastcore.foundation` along with type hints like so: + +``` +class MyClass(int): pass + +@patch +def func(self:MyClass, a): return self+a + +mc = MyClass(3) + +``` + +Now, `MyClass` has an additional method named `func`: + +``` +mc.func(10) + +``` + +``` +13 +``` + +Still not convinced? I'll show you another example of this kind of patching in the next section. + +* * * + +## A better pathlib.Path __ + +When you seethese extensions to pathlib.path you won't ever use vanilla pathlib again! A number of additional methods have been added to pathlib, such as: + + * `Path.readlines`: same as `with open('somefile', 'r') as f: f.readlines()` + * `Path.read`: same as `with open('somefile', 'r') as f: f.read()` + * `Path.save`: saves file as pickle + * `Path.load`: loads pickle file + * `Path.ls`: shows the contents of the path as a list. + * etc. + +Read more about this here. Here is a demonstration of `ls`: + +``` +from fastcore.utils import * +from pathlib import Path +p = Path('.') +p.ls() # you don't get this with vanilla Pathlib.Path!! + +``` + +``` +(#7) [Path('2020-09-01-fastcore.ipynb'),Path('README.md'),Path('fastcore_imgs'),Path('2020-02-20-test.ipynb'),Path('.ipynb_checkpoints'),Path('2020-02-21-introducing-fastpages.ipynb'),Path('my_icons')] +``` + +Wait! What's going on here? We just imported `pathlib.Path` \- why are we getting this new functionality? Thats because we imported the `fastcore.utils` module, which patches this module via the `@patch` decorator discussed earlier. Just to drive the point home on why the `@patch` decorator is useful, I'll go ahead and add another method to `Path` right now: + +``` +@patch +def fun(self:Path): return "This is fun!" + +p.fun() + +``` + +``` +'This is fun!' +``` + +That is magical, right? I know! That's why I'm writing about it! + +* * * + +## An Even More Concise Way To Create Lambdas __ + +`Self`, with an uppercase S, is an even more concise way to create lambdas that are calling methods on an object. For example, let's create a lambda for taking the sum of a Numpy array: + +``` +arr=np.array([5,4,3,2,1]) +f = lambda a: a.sum() +assert f(arr) == 15 + +``` + +You can use `Self` in the same way: + +``` +f = Self.sum() +assert f(arr) == 15 + +``` + +Let's create a lambda that does a groupby and max of a Pandas dataframe: + +``` +import pandas as pd +df=pd.DataFrame({'Some Column': ['a', 'a', 'b', 'b', ], + 'Another Column': [5, 7, 50, 70]}) + +f = Self.groupby('Some Column').mean() +f(df) + +``` + +| Another Column +---|--- +Some Column | +a | 6 +b | 60 + +Read more about `Self` in the docs). + +* * * + +## Notebook Functions __ + +These are simple but handy, and allow you to know whether or not code is executing in a Jupyter Notebook, Colab, or an Ipython Shell: + +``` +from fastcore.imports import in_notebook, in_colab, in_ipython +in_notebook(), in_colab(), in_ipython() + +``` + +``` +(True, False, True) +``` + +This is useful if you are displaying certain types of visualizations, progress bars or animations in your code that you may want to modify or toggle depending on the environment. + +* * * + +## A Drop-In Replacement For List __ + +You might be pretty happy with Python's`list`. This is one of those situations that you don't know you needed a better list until someone showed one to you. Enter `L`, a list like object with many extra goodies. + +The best way I can describe `L` is to pretend that `list` and `numpy` had a pretty baby: + +define a list (check out the nice `__repr__` that shows the length of the list!) + +``` +L(1,2,3) + +``` + +``` +(#3) [1,2,3] +``` + +Shuffle a list: + +``` +p = L.range(20).shuffle() +p + +``` + +``` +(#20) [8,7,5,12,14,16,2,15,19,6...] +``` + +Index into a list: + +``` +p[2,4,6] + +``` + +``` +(#3) [5,14,2] +``` + +L has sensible defaults, for example appending an element to a list: + +``` +1 + L(2,3,4) + +``` + +``` +(#4) [1,2,3,4] +``` + +There is much more `L` has to offer. Read the docs to learn more. + +# But Wait ... There's More!__ + +There are more things I would like to show you about fastcore, but there is no way they would reasonably fit into a blog post. Here is a list of some of my favorite things that I didn't demo in this blog post: + +## Utilities __ + +TheBasics section contain many shortcuts to perform common tasks or provide an additional interface to what standard python provides. + + * mk_class: quickly add a bunch of attributes to a class + * wrap_class: add new methods to a class with a simple decorator + * groupby: similar to Scala's groupby + * merge: merge dicts + * fasttuple: a tuple on steroids + * Infinite Lists: useful for padding and testing + * chunked: for batching and organizing stuff + +## Multiprocessing __ + +TheMultiprocessing section extends python's multiprocessing library by offering features like: + + * progress bars + * ability to pause to mitigate race conditions with external services + * processing things in batches on each worker, ex: if you have a vectorized operation to perform in chunks + +## Functional Programming __ + +Thefunctional programming section is my favorite part of this library. + + * maps: a map that also composes functions + * mapped: A more robust `map` + * using_attr: compose a function that operates on an attribute + +## Transforms __ + +Transforms is a collection of utilities for creating data transformations and associated pipelines. These transformation utilities build upon many of the building blocks discussed in this blog post. + +## Further Reading __ + +**It should be noted that you should read themain page of the docs first, followed by the section on tests to fully understand the documentation.** + + * The fastcore documentation site. + * The fastcore GitHub repo. + * Blog post on delegation. + +# Shameless plug: fastpages __ + +This blog post was written entirely in a Jupyter Notebook, which GitHub automatically converted into to a blog post! Sound interesting?Check out fastpages.# fastcore Module Documentation + +## fastcore.basics + +> Basic functionality used in the fastai library + +- `def ifnone(a, b)` + `b` if `a` is None else `a` + +- `def maybe_attr(o, attr)` + `getattr(o,attr,o)` + +- `def basic_repr(flds)` + Minimal `__repr__` + +- `class BasicRepr` + Base class for objects needing a basic `__repr__` + + +- `def is_array(x)` + `True` if `x` supports `__array__` or `iloc` + +- `def listify(o, *rest)` + Convert `o` to a `list` + +- `def tuplify(o, use_list, match)` + Make `o` a tuple + +- `def true(x)` + Test whether `x` is truthy; collections with >0 elements are considered `True` + +- `class NullType` + An object that is `False` and can be called, chained, and indexed + + - `def __getattr__(self, *args)` + - `def __call__(self, *args, **kwargs)` + - `def __getitem__(self, *args)` + - `def __bool__(self)` + +- `def tonull(x)` + Convert `None` to `null` + +- `def get_class(nm, *fld_names, **flds)` + Dynamically create a class, optionally inheriting from `sup`, containing `fld_names` + +- `def mk_class(nm, *fld_names, **flds)` + Create a class using `get_class` and add to the caller's module + +- `def wrap_class(nm, *fld_names, **flds)` + Decorator: makes function a method of a new class `nm` passing parameters to `mk_class` + +- `class ignore_exceptions` + Context manager to ignore exceptions + + - `def __enter__(self)` + - `def __exit__(self, *args)` + +- `def exec_local(code, var_name)` + Call `exec` on `code` and return the var `var_name` + +- `def risinstance(types, obj)` + Curried `isinstance` but with args reversed + +- `class Inf` + Infinite lists + + +- `def in_(x, a)` + `True` if `x in a` + +- `def ret_true(*args, **kwargs)` + Predicate: always `True` + +- `def ret_false(*args, **kwargs)` + Predicate: always `False` + +- `def stop(e)` + Raises exception `e` (by default `StopIteration`) + +- `def gen(func, seq, cond)` + Like `(func(o) for o in seq if cond(func(o)))` but handles `StopIteration` + +- `def chunked(it, chunk_sz, drop_last, n_chunks)` + Return batches from iterator `it` of size `chunk_sz` (or return `n_chunks` total) + +- `def otherwise(x, tst, y)` + `y if tst(x) else x` + +- `def custom_dir(c, add)` + Implement custom `__dir__`, adding `add` to `cls` + +- `class AttrDict` + `dict` subclass that also provides access to keys as attrs + + - `def __getattr__(self, k)` + - `def __setattr__(self, k, v)` + - `def __dir__(self)` + - `def copy(self)` + +- `class AttrDictDefault` + `AttrDict` subclass that returns `None` for missing attrs + + - `def __init__(self, *args, **kwargs)` + - `def __getattr__(self, k)` + +- `class NS` + `SimpleNamespace` subclass that also adds `iter` and `dict` support + + - `def __iter__(self)` + - `def __getitem__(self, x)` + - `def __setitem__(self, x, y)` + +- `def get_annotations_ex(obj)` + Backport of py3.10 `get_annotations` that returns globals/locals + +- `def eval_type(t, glb, loc)` + `eval` a type or collection of types, if needed, for annotations in py3.10+ + +- `def type_hints(f)` + Like `typing.get_type_hints` but returns `{}` if not allowed type + +- `def annotations(o)` + Annotations for `o`, or `type(o)` + +- `def anno_ret(func)` + Get the return annotation of `func` + +- `def signature_ex(obj, eval_str)` + Backport of `inspect.signature(..., eval_str=True` to `True`) + +- `def str2int(s)` + Convert `s` to an `int` + +- `def str2float(s)` + Convert `s` to a float + +- `def str2list(s)` + Convert `s` to a list + +- `def str2date(s)` + `date.fromisoformat` with empty string handling + +- `def typed(_func)` + Decorator to check param and return types at runtime, with optional casting + +- `def exec_new(code)` + Execute `code` in a new environment and return it + +- `def exec_import(mod, sym)` + Import `sym` from `mod` in a new environment + +## fastcore.dispatch + +> Basic single and dual parameter dispatch + +- `def lenient_issubclass(cls, types)` + If possible return whether `cls` is a subclass of `types`, otherwise return False. + +- `def sorted_topologically(iterable)` + Return a new list containing all items from the iterable sorted topologically + +- `class TypeDispatch` + Dictionary-like object; `__getitem__` matches keys of types using `issubclass` + + - `def __init__(self, funcs, bases)` + - `def add(self, f)` + Add type `t` and function `f` + + - `def first(self)` + Get first function in ordered dict of type:func. + + - `def returns(self, x)` + Get the return type of annotation of `x`. + + - `def __repr__(self)` + - `def __call__(self, *args, **kwargs)` + - `def __get__(self, inst, owner)` + - `def __getitem__(self, k)` + Find first matching type that is a super-class of `k` + + +- `class DispatchReg` + A global registry for `TypeDispatch` objects keyed by function name + + - `def __init__(self)` + - `def __call__(self, f)` + +- `def retain_meta(x, res, as_copy)` + Call `res.set_meta(x)`, if it exists + +- `def default_set_meta(self, x, as_copy)` + Copy over `_meta` from `x` to `res`, if it's missing + +- `@typedispatch def cast(x, typ)` + cast `x` to type `typ` (may also change `x` inplace) + +- `def retain_type(new, old, typ, as_copy)` + Cast `new` to type of `old` or `typ` if it's a superclass + +- `def retain_types(new, old, typs)` + Cast each item of `new` to type of matching item in `old` if it's a superclass + +- `def explode_types(o)` + Return the type of `o`, potentially in nested dictionaries for thing that are listy + +## fastcore.docments + +> Document parameters using comments. + +- `def docstring(sym)` + Get docstring for `sym` for functions ad classes + +- `def parse_docstring(sym)` + Parse a numpy-style docstring in `sym` + +- `def isdataclass(s)` + Check if `s` is a dataclass but not a dataclass' instance + +- `def get_dataclass_source(s)` + Get source code for dataclass `s` + +- `def get_source(s)` + Get source code for string, function object or dataclass `s` + +- `def get_name(obj)` + Get the name of `obj` + +- `def qual_name(obj)` + Get the qualified name of `obj` + +- `@delegates(_docments) def docments(elt, full, **kwargs)` + Generates a `docment` + +- `def extract_docstrings(code)` + Create a dict from function/class/method names to tuples of docstrings and param lists + +## fastcore.docscrape + +> Parse numpy-style docstrings + +- `def strip_blank_lines(l)` + Remove leading and trailing blank lines from a list of lines + +- `class Reader` + A line-based string reader. + + - `def __init__(self, data)` + - `def __getitem__(self, n)` + - `def reset(self)` + - `def read(self)` + - `def seek_next_non_empty_line(self)` + - `def eof(self)` + - `def read_to_condition(self, condition_func)` + - `def read_to_next_empty_line(self)` + - `def read_to_next_unindented_line(self)` + - `def peek(self, n)` + - `def is_empty(self)` + +- `class ParseError` + - `def __str__(self)` + +- `class NumpyDocString` + Parses a numpydoc string to an abstract representation + + - `def __init__(self, docstring, config)` + - `def __iter__(self)` + - `def __len__(self)` + - `def __getitem__(self, key)` + - `def __setitem__(self, key, val)` + +- `def dedent_lines(lines, split)` + Deindent a list of lines maximally + +## fastcore.foundation + +> The `L` class and helpers for it + +- `@contextmanager def working_directory(path)` + Change working directory to `path` and return to previous on exit. + +- `def add_docs(cls, cls_doc, **docs)` + Copy values from `docs` to `cls` docstrings, and confirm all public methods are documented + +- `def docs(cls)` + Decorator version of `add_docs`, using `_docs` dict + +- `def coll_repr(c, max_n)` + String repr of up to `max_n` items of (possibly lazy) collection `c` + +- `def is_bool(x)` + Check whether `x` is a bool or None + +- `def mask2idxs(mask)` + Convert bool mask or index list to index `L` + +- `def is_indexer(idx)` + Test whether `idx` will index a single item in a list + +- `class CollBase` + Base class for composing a list of `items` + + - `def __init__(self, items)` + - `def __len__(self)` + - `def __getitem__(self, k)` + - `def __setitem__(self, k, v)` + - `def __delitem__(self, i)` + - `def __repr__(self)` + - `def __iter__(self)` + +- `class L` + Behaves like a list of `items` but can also index with list of indices or masks + + - `def __init__(self, items, *rest)` + - `def __getitem__(self, idx)` + - `def copy(self)` + - `def __setitem__(self, idx, o)` + Set `idx` (can be list of indices, or mask, or int) items to `o` (which is broadcast if not iterable) + + - `def __eq__(self, b)` + - `def sorted(self, key, reverse)` + - `def __iter__(self)` + - `def __contains__(self, b)` + - `def __reversed__(self)` + - `def __invert__(self)` + - `def __repr__(self)` + - `def __mul__(a, b)` + - `def __add__(a, b)` + - `def __radd__(a, b)` + - `def __addi__(a, b)` + - `@classmethod def split(cls, s, sep, maxsplit)` + - `@classmethod def range(cls, a, b, step)` + - `def map(self, f, *args, **kwargs)` + - `def argwhere(self, f, negate, **kwargs)` + - `def argfirst(self, f, negate)` + - `def filter(self, f, negate, **kwargs)` + - `def enumerate(self)` + - `def renumerate(self)` + - `def unique(self, sort, bidir, start)` + - `def val2idx(self)` + - `def cycle(self)` + - `def map_dict(self, f, *args, **kwargs)` + - `def map_first(self, f, g, *args, **kwargs)` + - `def itemgot(self, *idxs)` + - `def attrgot(self, k, default)` + - `def starmap(self, f, *args, **kwargs)` + - `def zip(self, cycled)` + - `def zipwith(self, *rest)` + - `def map_zip(self, f, *args, **kwargs)` + - `def map_zipwith(self, f, *rest, **kwargs)` + - `def shuffle(self)` + - `def concat(self)` + - `def reduce(self, f, initial)` + - `def sum(self)` + - `def product(self)` + - `def setattrs(self, attr, val)` + +- `def save_config_file(file, d, **kwargs)` + Write settings dict to a new config file, or overwrite the existing one. + +- `class Config` + Reading and writing `ConfigParser` ini files + + - `def __init__(self, cfg_path, cfg_name, create, save, extra_files, types)` + - `def __repr__(self)` + - `def __setitem__(self, k, v)` + - `def __contains__(self, k)` + - `def save(self)` + - `def __getattr__(self, k)` + - `def __getitem__(self, k)` + - `def get(self, k, default)` + - `def path(self, k, default)` + - `@classmethod def find(cls, cfg_name, cfg_path, **kwargs)` + Search `cfg_path` and its parents to find `cfg_name` + + +## fastcore.imghdr + +> Recognize image file formats based on their first few bytes. + +- `def test_jpeg(h, f)` + JPEG data with JFIF or Exif markers; and raw JPEG + +- `def test_gif(h, f)` + GIF ('87 and '89 variants) + +- `def test_tiff(h, f)` + TIFF (can be in Motorola or Intel byte order) + +- `def test_rgb(h, f)` + SGI image library + +- `def test_pbm(h, f)` + PBM (portable bitmap) + +- `def test_pgm(h, f)` + PGM (portable graymap) + +- `def test_ppm(h, f)` + PPM (portable pixmap) + +- `def test_rast(h, f)` + Sun raster file + +- `def test_xbm(h, f)` + X bitmap (X10 or X11) + +## fastcore.imports + +- `def is_iter(o)` + Test whether `o` can be used in a `for` loop + +- `def is_coll(o)` + Test whether `o` is a collection (i.e. has a usable `len`) + +- `def all_equal(a, b)` + Compares whether `a` and `b` are the same length and have the same contents + +- `def noop(x, *args, **kwargs)` + Do nothing + +- `def noops(self, x, *args, **kwargs)` + Do nothing (method) + +- `def isinstance_str(x, cls_name)` + Like `isinstance`, except takes a type name instead of a type + +- `def equals(a, b)` + Compares `a` and `b` for equality; supports sublists, tensors and arrays too + +- `def ipython_shell()` + Same as `get_ipython` but returns `False` if not in IPython + +- `def in_ipython()` + Check if code is running in some kind of IPython environment + +- `def in_colab()` + Check if the code is running in Google Colaboratory + +- `def in_jupyter()` + Check if the code is running in a jupyter notebook + +- `def in_notebook()` + Check if the code is running in a jupyter notebook + +- `def remove_prefix(text, prefix)` + Temporary until py39 is a prereq + +- `def remove_suffix(text, suffix)` + Temporary until py39 is a prereq + +## fastcore.meta + +> Metaclasses + +- `def test_sig(f, b)` + Test the signature of an object + +- `class FixSigMeta` + A metaclass that fixes the signature on classes that override `__new__` + + - `def __new__(cls, name, bases, dict)` + +- `class PrePostInitMeta` + A metaclass that calls optional `__pre_init__` and `__post_init__` methods + + - `def __call__(cls, *args, **kwargs)` + +- `class AutoInit` + Same as `object`, but no need for subclasses to call `super().__init__` + + - `def __pre_init__(self, *args, **kwargs)` + +- `class NewChkMeta` + Metaclass to avoid recreating object passed to constructor + + - `def __call__(cls, x, *args, **kwargs)` + +- `class BypassNewMeta` + Metaclass: casts `x` to this class if it's of type `cls._bypass_type` + + - `def __call__(cls, x, *args, **kwargs)` + +- `def empty2none(p)` + Replace `Parameter.empty` with `None` + +- `def anno_dict(f)` + `__annotation__ dictionary with `empty` cast to `None`, returning empty if doesn't exist + +- `def use_kwargs_dict(keep, **kwargs)` + Decorator: replace `**kwargs` in signature with `names` params + +- `def use_kwargs(names, keep)` + Decorator: replace `**kwargs` in signature with `names` params + +- `def delegates(to, keep, but)` + Decorator: replace `**kwargs` in signature with params from `to` + +- `def method(f)` + Mark `f` as a method + +- `def funcs_kwargs(as_method)` + Replace methods in `cls._methods` with those from `kwargs` + +## fastcore.net + +> Network, HTTP, and URL functions + +- `def urlquote(url)` + Update url's path with `urllib.parse.quote` + +- `def urlwrap(url, data, headers)` + Wrap `url` in a urllib `Request` with `urlquote` + +- `class HTTP4xxClientError` + Base class for client exceptions (code 4xx) from `url*` functions + + +- `class HTTP5xxServerError` + Base class for server exceptions (code 5xx) from `url*` functions + + +- `def urlopen(url, data, headers, timeout, **kwargs)` + Like `urllib.request.urlopen`, but first `urlwrap` the `url`, and encode `data` + +- `def urlread(url, data, headers, decode, return_json, return_headers, timeout, **kwargs)` + Retrieve `url`, using `data` dict or `kwargs` to `POST` if present + +- `def urljson(url, data, timeout)` + Retrieve `url` and decode json + +- `def urlclean(url)` + Remove fragment, params, and querystring from `url` if present + +- `def urlsave(url, dest, reporthook, headers, timeout)` + Retrieve `url` and save based on its name + +- `def urlvalid(x)` + Test if `x` is a valid URL + +- `def urlrequest(url, verb, headers, route, query, data, json_data)` + `Request` for `url` with optional route params replaced by `route`, plus `query` string, and post `data` + +- `@patch def summary(self, skip)` + Summary containing full_url, headers, method, and data, removing `skip` from headers + +- `def urlsend(url, verb, headers, decode, route, query, data, json_data, return_json, return_headers, debug, timeout)` + Send request with `urlrequest`, converting result to json if `return_json` + +- `def do_request(url, post, headers, **data)` + Call GET or json-encoded POST on `url`, depending on `post` + +- `def start_server(port, host, dgram, reuse_addr, n_queue)` + Create a `socket` server on `port`, with optional `host`, of type `dgram` + +- `def start_client(port, host, dgram)` + Create a `socket` client on `port`, with optional `host`, of type `dgram` + +- `def tobytes(s)` + Convert `s` into HTTP-ready bytes format + +- `def http_response(body, status, hdrs, **kwargs)` + Create an HTTP-ready response, adding `kwargs` to `hdrs` + +- `@threaded def recv_once(host, port)` + Spawn a thread to receive a single HTTP request and store in `d['r']` + +## fastcore.parallel + +> Threading and multiprocessing functions + +- `def threaded(process)` + Run `f` in a `Thread` (or `Process` if `process=True`), and returns it + +- `def startthread(f)` + Like `threaded`, but start thread immediately + +- `def startproc(f)` + Like `threaded(True)`, but start Process immediately + +- `class ThreadPoolExecutor` + Same as Python's ThreadPoolExecutor, except can pass `max_workers==0` for serial execution + + - `def __init__(self, max_workers, on_exc, pause, **kwargs)` + - `def map(self, f, items, *args, **kwargs)` + +- `@delegates() class ProcessPoolExecutor` + Same as Python's ProcessPoolExecutor, except can pass `max_workers==0` for serial execution + + - `def __init__(self, max_workers, on_exc, pause, **kwargs)` + - `def map(self, f, items, *args, **kwargs)` + +- `def parallel(f, items, *args, **kwargs)` + Applies `func` in parallel to `items`, using `n_workers` + +- `def parallel_async(f, items, *args, **kwargs)` + Applies `f` to `items` in parallel using asyncio and a semaphore to limit concurrency. + +- `def run_procs(f, f_done, args)` + Call `f` for each item in `args` in parallel, yielding `f_done` + +- `def parallel_gen(cls, items, n_workers, **kwargs)` + Instantiate `cls` in `n_workers` procs & call each on a subset of `items` in parallel. + +## fastcore.py2pyi + +- `def imp_mod(module_path, package)` + Import dynamically the module referenced in `fn` + +- `def has_deco(node, name)` + Check if a function node `node` has a decorator named `name` + +- `def create_pyi(fn, package)` + Convert `fname.py` to `fname.pyi` by removing function bodies and expanding `delegates` kwargs + +- `@call_parse def py2pyi(fname, package)` + Convert `fname.py` to `fname.pyi` by removing function bodies and expanding `delegates` kwargs + +- `@call_parse def replace_wildcards(path)` + Expand wildcard imports in the specified Python file. + +## fastcore.script + +> A fast way to turn your python function into a script. + +- `def store_true()` + Placeholder to pass to `Param` for `store_true` action + +- `def store_false()` + Placeholder to pass to `Param` for `store_false` action + +- `def bool_arg(v)` + Use as `type` for `Param` to get `bool` behavior + +- `class Param` + A parameter in a function used in `anno_parser` or `call_parse` + + - `def __init__(self, help, type, opt, action, nargs, const, choices, required, default)` + - `def set_default(self, d)` + - `@property def pre(self)` + - `@property def kwargs(self)` + - `def __repr__(self)` + +- `def anno_parser(func, prog)` + Look at params (annotated with `Param`) in func and return an `ArgumentParser` + +- `def args_from_prog(func, prog)` + Extract args from `prog` + +- `def call_parse(func, nested)` + Decorator to create a simple CLI from `func` using `anno_parser` + +## fastcore.style + +> Fast styling for friendly CLIs. + +- `class StyleCode` + An escape sequence for styling terminal text. + + - `def __init__(self, name, code, typ)` + - `def __str__(self)` + +- `class Style` + A minimal terminal text styler. + + - `def __init__(self, codes)` + - `def __dir__(self)` + - `def __getattr__(self, k)` + - `def __call__(self, obj)` + - `def __repr__(self)` + +- `def demo()` + Demonstrate all available styles and their codes. + +## fastcore.test + +> Helper functions to quickly write tests in notebooks + +- `def test_fail(f, msg, contains, args, kwargs)` + Fails with `msg` unless `f()` raises an exception and (optionally) has `contains` in `e.args` + +- `def test(a, b, cmp, cname)` + `assert` that `cmp(a,b)`; display inputs and `cname or cmp.__name__` if it fails + +- `def nequals(a, b)` + Compares `a` and `b` for `not equals` + +- `def test_eq(a, b)` + `test` that `a==b` + +- `def test_eq_type(a, b)` + `test` that `a==b` and are same type + +- `def test_ne(a, b)` + `test` that `a!=b` + +- `def is_close(a, b, eps)` + Is `a` within `eps` of `b` + +- `def test_close(a, b, eps)` + `test` that `a` is within `eps` of `b` + +- `def test_is(a, b)` + `test` that `a is b` + +- `def test_shuffled(a, b)` + `test` that `a` and `b` are shuffled versions of the same sequence of items + +- `def test_stdout(f, exp, regex)` + Test that `f` prints `exp` to stdout, optionally checking as `regex` + +- `def test_fig_exists(ax)` + Test there is a figure displayed in `ax` + +- `class ExceptionExpected` + Context manager that tests if an exception is raised + + - `def __init__(self, ex, regex)` + - `def __enter__(self)` + - `def __exit__(self, type, value, traceback)` + +## fastcore.transform + +> Definition of `Transform` and `Pipeline` + +- `class Transform` + Delegates (`__call__`,`decode`,`setup`) to (encodes,decodes,setups) if `split_idx` matches + + - `def __init__(self, enc, dec, split_idx, order)` + - `@property def name(self)` + - `def __call__(self, x, **kwargs)` + - `def decode(self, x, **kwargs)` + - `def __repr__(self)` + - `def setup(self, items, train_setup)` + +- `class InplaceTransform` + A `Transform` that modifies in-place and just returns whatever it's passed + + +- `class DisplayedTransform` + A transform with a `__repr__` that shows its attrs + + - `@property def name(self)` + +- `class ItemTransform` + A transform that always take tuples as items + + - `def __call__(self, x, **kwargs)` + - `def decode(self, x, **kwargs)` + +- `def get_func(t, name, *args, **kwargs)` + Get the `t.name` (potentially partial-ized with `args` and `kwargs`) or `noop` if not defined + +- `class Func` + Basic wrapper around a `name` with `args` and `kwargs` to call on a given type + + - `def __init__(self, name, *args, **kwargs)` + - `def __repr__(self)` + - `def __call__(self, t)` + +- `def compose_tfms(x, tfms, is_enc, reverse, **kwargs)` + Apply all `func_nm` attribute of `tfms` on `x`, maybe in `reverse` order + +- `def mk_transform(f)` + Convert function `f` to `Transform` if it isn't already one + +- `def gather_attrs(o, k, nm)` + Used in __getattr__ to collect all attrs `k` from `self.{nm}` + +- `def gather_attr_names(o, nm)` + Used in __dir__ to collect all attrs `k` from `self.{nm}` + +- `class Pipeline` + A pipeline of composed (for encode/decode) transforms, setup with types + + - `def __init__(self, funcs, split_idx)` + - `def setup(self, items, train_setup)` + - `def add(self, ts, items, train_setup)` + - `def __call__(self, o)` + - `def __repr__(self)` + - `def __getitem__(self, i)` + - `def __setstate__(self, data)` + - `def __getattr__(self, k)` + - `def __dir__(self)` + - `def decode(self, o, full)` + - `def show(self, o, ctx, **kwargs)` + +## fastcore.xdg + +> XDG Base Directory Specification helpers. + +- `def xdg_cache_home()` + Path corresponding to `XDG_CACHE_HOME` + +- `def xdg_config_dirs()` + Paths corresponding to `XDG_CONFIG_DIRS` + +- `def xdg_config_home()` + Path corresponding to `XDG_CONFIG_HOME` + +- `def xdg_data_dirs()` + Paths corresponding to XDG_DATA_DIRS` + +- `def xdg_data_home()` + Path corresponding to `XDG_DATA_HOME` + +- `def xdg_runtime_dir()` + Path corresponding to `XDG_RUNTIME_DIR` + +- `def xdg_state_home()` + Path corresponding to `XDG_STATE_HOME` + +## fastcore.xml + +> Concise generation of XML. + +- `class FT` + A 'Fast Tag' structure, containing `tag`,`children`,and `attrs` + + - `def __init__(self, tag, cs, attrs, void_, **kwargs)` + - `def on(self, f)` + - `def changed(self)` + - `def __setattr__(self, k, v)` + - `def __getattr__(self, k)` + - `@property def list(self)` + - `def get(self, k, default)` + - `def __repr__(self)` + - `def __iter__(self)` + - `def __getitem__(self, idx)` + - `def __setitem__(self, i, o)` + - `def __call__(self, *c, **kw)` + - `def set(self, *c, **kw)` + Set children and/or attributes (chainable) + + +- `def ft(tag, *c, **kw)` + Create an `FT` structure for `to_xml()` + +- `def Html(*c, **kwargs)` + An HTML tag, optionally preceeded by `!DOCTYPE HTML` + +- `class Safe` + - `def __html__(self)` + +- `def to_xml(elm, lvl, indent, do_escape)` + Convert `ft` element tree into an XML string + +- `def highlight(s, lang)` + Markdown to syntax-highlight `s` in language `lang` + +## fastcore.xtras + +> Utility functions used in the fastai library + +- `def walk(path, symlinks, keep_file, keep_folder, skip_folder, func, ret_folders)` + Generator version of `os.walk`, using functions to filter files and folders + +- `def globtastic(path, recursive, symlinks, file_glob, file_re, folder_re, skip_file_glob, skip_file_re, skip_folder_re, func, ret_folders)` + A more powerful `glob`, including regex matches, symlink handling, and skip parameters + +- `@contextmanager def maybe_open(f, mode, **kwargs)` + Context manager: open `f` if it is a path (and close on exit) + +- `def mkdir(path, exist_ok, parents, overwrite, **kwargs)` + Creates and returns a directory defined by `path`, optionally removing previous existing directory if `overwrite` is `True` + +- `def image_size(fn)` + Tuple of (w,h) for png, gif, or jpg; `None` otherwise + +- `def bunzip(fn)` + bunzip `fn`, raising exception if output already exists + +- `def loads(s, **kw)` + Same as `json.loads`, but handles `None` + +- `def loads_multi(s)` + Generator of >=0 decoded json dicts, possibly with non-json ignored text at start and end + +- `def dumps(obj, **kw)` + Same as `json.dumps`, but uses `ujson` if available + +- `def untar_dir(fname, dest, rename, overwrite)` + untar `file` into `dest`, creating a directory if the root contains more than one item + +- `def repo_details(url)` + Tuple of `owner,name` from ssh or https git repo `url` + +- `def run(cmd, *rest)` + Pass `cmd` (splitting with `shlex` if string) to `subprocess.run`; return `stdout`; raise `IOError` if fails + +- `def open_file(fn, mode, **kwargs)` + Open a file, with optional compression if gz or bz2 suffix + +- `def save_pickle(fn, o)` + Save a pickle file, to a file name or opened file + +- `def load_pickle(fn)` + Load a pickle file from a file name or opened file + +- `def parse_env(s, fn)` + Parse a shell-style environment string or file + +- `def expand_wildcards(code)` + Expand all wildcard imports in the given code string. + +- `def dict2obj(d, list_func, dict_func)` + Convert (possibly nested) dicts (or lists of dicts) to `AttrDict` + +- `def obj2dict(d)` + Convert (possibly nested) AttrDicts (or lists of AttrDicts) to `dict` + +- `def repr_dict(d)` + Print nested dicts and lists, such as returned by `dict2obj` + +- `def is_listy(x)` + `isinstance(x, (tuple,list,L,slice,Generator))` + +- `def mapped(f, it)` + map `f` over `it`, unless it's not listy, in which case return `f(it)` + +- `@patch def readlines(self, hint, encoding)` + Read the content of `self` + +- `@patch def read_json(self, encoding, errors)` + Same as `read_text` followed by `loads` + +- `@patch def mk_write(self, data, encoding, errors, mode)` + Make all parent dirs of `self`, and write `data` + +- `@patch def relpath(self, start)` + Same as `os.path.relpath`, but returns a `Path`, and resolves symlinks + +- `@patch def ls(self, n_max, file_type, file_exts)` + Contents of path as a list + +- `@patch def delete(self)` + Delete a file, symlink, or directory tree + +- `class IterLen` + Base class to add iteration to anything supporting `__len__` and `__getitem__` + + - `def __iter__(self)` + +- `@docs class ReindexCollection` + Reindexes collection `coll` with indices `idxs` and optional LRU cache of size `cache` + + - `def __init__(self, coll, idxs, cache, tfm)` + - `def __getitem__(self, i)` + - `def __len__(self)` + - `def reindex(self, idxs)` + - `def shuffle(self)` + - `def cache_clear(self)` + - `def __getstate__(self)` + - `def __setstate__(self, s)` + +- `def get_source_link(func)` + Return link to `func` in source code + +- `def truncstr(s, maxlen, suf, space)` + Truncate `s` to length `maxlen`, adding suffix `suf` if truncated + +- `def sparkline(data, mn, mx, empty_zero)` + Sparkline for `data`, with `None`s (and zero, if `empty_zero`) shown as empty column + +- `def modify_exception(e, msg, replace)` + Modifies `e` with a custom message attached + +- `def round_multiple(x, mult, round_down)` + Round `x` to nearest multiple of `mult` + +- `def set_num_threads(nt)` + Get numpy (and others) to use `nt` threads + +- `def join_path_file(file, path, ext)` + Return `path/file` if file is a string or a `Path`, file otherwise + +- `def autostart(g)` + Decorator that automatically starts a generator + +- `class EventTimer` + An event timer with history of `store` items of time `span` + + - `def __init__(self, store, span)` + - `def add(self, n)` + Record `n` events + + - `@property def duration(self)` + - `@property def freq(self)` + +- `def stringfmt_names(s)` + Unique brace-delimited names in `s` + +- `class PartialFormatter` + A `string.Formatter` that doesn't error on missing fields, and tracks missing fields and unused args + + - `def __init__(self)` + - `def get_field(self, nm, args, kwargs)` + - `def check_unused_args(self, used, args, kwargs)` + +- `def partial_format(s, **kwargs)` + string format `s`, ignoring missing field errors, returning missing and extra fields + +- `def utc2local(dt)` + Convert `dt` from UTC to local time + +- `def local2utc(dt)` + Convert `dt` from local to UTC time + +- `def trace(f)` + Add `set_trace` to an existing function `f` + +- `@contextmanager def modified_env(*delete, **replace)` + Context manager temporarily modifying `os.environ` by deleting `delete` and replacing `replace` + +- `class ContextManagers` + Wrapper for `contextlib.ExitStack` which enters a collection of context managers + + - `def __init__(self, mgrs)` + - `def __enter__(self)` + - `def __exit__(self, *args, **kwargs)` + +- `def shufflish(x, pct)` + Randomly relocate items of `x` up to `pct` of `len(x)` from their starting location + +- `def console_help(libname)` + Show help for all console scripts from `libname` + +- `def hl_md(s, lang, show)` + Syntax highlight `s` using `lang`. + +- `def type2str(typ)` + Stringify `typ` + +- `class Unset` + - `def __repr__(self)` + - `def __str__(self)` + - `def __bool__(self)` + - `@property def name(self)` + +- `def nullable_dc(cls)` + Like `dataclass`, but default of `UNSET` added to fields without defaults + +- `def flexiclass(cls)` + Convert `cls` into a `dataclass` like `make_nullable`. Converts in place and also returns the result. + +- `def asdict(o)` + Convert `o` to a `dict`, supporting dataclasses, namedtuples, iterables, and `__dict__` attrs. + +- `def is_typeddict(cls)` + Check if `cls` is a `TypedDict` + +- `def is_namedtuple(cls)` + `True` if `cls` is a namedtuple type + +- `def flexicache(*funcs)` + Like `lru_cache`, but customisable with policy `funcs` + +- `def time_policy(seconds)` + A `flexicache` policy that expires cached items after `seconds` have passed + +- `def mtime_policy(filepath)` + A `flexicache` policy that expires cached items after `filepath` modified-time changes + +- `def timed_cache(seconds, maxsize)` + Like `lru_cache`, but also with time-based eviction +# Test + + + +## Simple test functions + +We can check that code raises an exception when that’s expected +([`test_fail`](https://fastcore.fast.ai/test.html#test_fail)). + +To test for equality or inequality (with different types of things) we +define a simple function +[`test`](https://fastcore.fast.ai/test.html#test) that compares two +objects with a given `cmp` operator. + +------------------------------------------------------------------------ + +source + +### test_fail + +> test_fail (f, msg='', contains='', args=None, kwargs=None) + +*Fails with `msg` unless `f()` raises an exception and (optionally) has +`contains` in `e.args`* + +``` python +def _fail(): raise Exception("foobar") +test_fail(_fail, contains="foo") + +def _fail(): raise Exception() +test_fail(_fail) +``` + +We can also pass `args` and `kwargs` to function to check if it fails +with special inputs. + +``` python +def _fail_args(a): + if a == 5: + raise ValueError +test_fail(_fail_args, args=(5,)) +test_fail(_fail_args, kwargs=dict(a=5)) +``` + +------------------------------------------------------------------------ + +source + +### test + +> test (a, b, cmp, cname=None) + +*`assert` that `cmp(a,b)`; display inputs and `cname or cmp.__name__` if +it fails* + +``` python +test([1,2],[1,2], operator.eq) +test_fail(lambda: test([1,2],[1], operator.eq)) +test([1,2],[1], operator.ne) +test_fail(lambda: test([1,2],[1,2], operator.ne)) +``` + +------------------------------------------------------------------------ + +### all_equal + +> all_equal (a, b) + +*Compares whether `a` and `b` are the same length and have the same +contents* + +``` python +test(['abc'], ['abc'], all_equal) +test_fail(lambda: test(['abc'],['cab'], all_equal)) +``` + +------------------------------------------------------------------------ + +### equals + +> equals (a, b) + +*Compares `a` and `b` for equality; supports sublists, tensors and +arrays too* + +``` python +test([['abc'],['a']], [['abc'],['a']], equals) +test([['abc'],['a'],'b', [['x']]], [['abc'],['a'],'b', [['x']]], equals) # supports any depth and nested structure +``` + +------------------------------------------------------------------------ + +source + +### nequals + +> nequals (a, b) + +*Compares `a` and `b` for `not equals`* + +``` python +test(['abc'], ['ab' ], nequals) +``` + +## test_eq test_ne, etc… + +Just use +[`test_eq`](https://fastcore.fast.ai/test.html#test_eq)/[`test_ne`](https://fastcore.fast.ai/test.html#test_ne) +to test for `==`/`!=`. +[`test_eq_type`](https://fastcore.fast.ai/test.html#test_eq_type) checks +things are equal and of the same type. We define them using +[`test`](https://fastcore.fast.ai/test.html#test): + +------------------------------------------------------------------------ + +source + +### test_eq + +> test_eq (a, b) + +*[`test`](https://fastcore.fast.ai/test.html#test) that `a==b`* + +``` python +test_eq([1,2],[1,2]) +test_eq([1,2],map(int,[1,2])) +test_eq(array([1,2]),array([1,2])) +test_eq(array([1,2]),array([1,2])) +test_eq([array([1,2]),3],[array([1,2]),3]) +test_eq(dict(a=1,b=2), dict(b=2,a=1)) +test_fail(lambda: test_eq([1,2], 1), contains="==") +test_fail(lambda: test_eq(None, np.array([1,2])), contains="==") +test_eq({'a', 'b', 'c'}, {'c', 'a', 'b'}) +``` + +``` python +df1 = pd.DataFrame(dict(a=[1,2],b=['a','b'])) +df2 = pd.DataFrame(dict(a=[1,2],b=['a','b'])) +df3 = pd.DataFrame(dict(a=[1,2],b=['a','c'])) + +test_eq(df1,df2) +test_eq(df1.a,df2.a) +test_fail(lambda: test_eq(df1,df3), contains='==') +class T(pd.Series): pass +test_eq(df1.iloc[0], T(df2.iloc[0])) # works with subclasses +``` + +``` python +test_eq(torch.zeros(10), torch.zeros(10, dtype=torch.float64)) +test_eq(torch.zeros(10), torch.ones(10)-1) +test_fail(lambda:test_eq(torch.zeros(10), torch.ones(1, 10)), contains='==') +test_eq(torch.zeros(3), [0,0,0]) +``` + +------------------------------------------------------------------------ + +source + +### test_eq_type + +> test_eq_type (a, b) + +*[`test`](https://fastcore.fast.ai/test.html#test) that `a==b` and are +same type* + +``` python +test_eq_type(1,1) +test_fail(lambda: test_eq_type(1,1.)) +test_eq_type([1,1],[1,1]) +test_fail(lambda: test_eq_type([1,1],(1,1))) +test_fail(lambda: test_eq_type([1,1],[1,1.])) +``` + +------------------------------------------------------------------------ + +source + +### test_ne + +> test_ne (a, b) + +*[`test`](https://fastcore.fast.ai/test.html#test) that `a!=b`* + +``` python +test_ne([1,2],[1]) +test_ne([1,2],[1,3]) +test_ne(array([1,2]),array([1,1])) +test_ne(array([1,2]),array([1,1])) +test_ne([array([1,2]),3],[array([1,2])]) +test_ne([3,4],array([3])) +test_ne([3,4],array([3,5])) +test_ne(dict(a=1,b=2), ['a', 'b']) +test_ne(['a', 'b'], dict(a=1,b=2)) +``` + +------------------------------------------------------------------------ + +source + +### is_close + +> is_close (a, b, eps=1e-05) + +*Is `a` within `eps` of `b`* + +------------------------------------------------------------------------ + +source + +### test_close + +> test_close (a, b, eps=1e-05) + +*[`test`](https://fastcore.fast.ai/test.html#test) that `a` is within +`eps` of `b`* + +``` python +test_close(1,1.001,eps=1e-2) +test_fail(lambda: test_close(1,1.001)) +test_close([-0.001,1.001], [0.,1.], eps=1e-2) +test_close(np.array([-0.001,1.001]), np.array([0.,1.]), eps=1e-2) +test_close(array([-0.001,1.001]), array([0.,1.]), eps=1e-2) +``` + +------------------------------------------------------------------------ + +source + +### test_is + +> test_is (a, b) + +*[`test`](https://fastcore.fast.ai/test.html#test) that `a is b`* + +``` python +test_fail(lambda: test_is([1], [1])) +a = [1] +test_is(a, a) +b = [2]; test_fail(lambda: test_is(a, b)) +``` + +------------------------------------------------------------------------ + +source + +### test_shuffled + +> test_shuffled (a, b) + +*[`test`](https://fastcore.fast.ai/test.html#test) that `a` and `b` are +shuffled versions of the same sequence of items* + +``` python +a = list(range(50)) +b = copy(a) +random.shuffle(b) +test_shuffled(a,b) +test_fail(lambda:test_shuffled(a,a)) +``` + +``` python +a = 'abc' +b = 'abcabc' +test_fail(lambda:test_shuffled(a,b)) +``` + +``` python +a = ['a', 42, True] +b = [42, True, 'a'] +test_shuffled(a,b) +``` + +------------------------------------------------------------------------ + +source + +### test_stdout + +> test_stdout (f, exp, regex=False) + +*Test that `f` prints `exp` to stdout, optionally checking as `regex`* + +``` python +test_stdout(lambda: print('hi'), 'hi') +test_fail(lambda: test_stdout(lambda: print('hi'), 'ho')) +test_stdout(lambda: 1+1, '') +test_stdout(lambda: print('hi there!'), r'^hi.*!$', regex=True) +``` + +------------------------------------------------------------------------ + +source + +### test_warns + +> test_warns (f, show=False) + +``` python +test_warns(lambda: warnings.warn("Oh no!")) +test_fail(lambda: test_warns(lambda: 2+2), contains='No warnings raised') +``` + +``` python +test_warns(lambda: warnings.warn("Oh no!"), show=True) +``` + + : Oh no! + +``` python +im = Image.open(TEST_IMAGE).resize((128,128)); im +``` + +![](00_test_files/figure-commonmark/cell-35-output-1.png) + +``` python +im = Image.open(TEST_IMAGE_BW).resize((128,128)); im +``` + +![](00_test_files/figure-commonmark/cell-36-output-1.png) + +------------------------------------------------------------------------ + +source + +### test_fig_exists + +> test_fig_exists (ax) + +*Test there is a figure displayed in `ax`* + +``` python +fig,ax = plt.subplots() +ax.imshow(array(im)); +``` + +![](00_test_files/figure-commonmark/cell-38-output-1.png) + +``` python +test_fig_exists(ax) +``` + +------------------------------------------------------------------------ + +source + +### ExceptionExpected + +> ExceptionExpected (ex=, regex='') + +*Context manager that tests if an exception is raised* + +``` python +def _tst_1(): assert False, "This is a test" +def _tst_2(): raise SyntaxError + +with ExceptionExpected(): _tst_1() +with ExceptionExpected(ex=AssertionError, regex="This is a test"): _tst_1() +with ExceptionExpected(ex=SyntaxError): _tst_2() +``` + +`exception` is an abbreviation for `ExceptionExpected()`. + +``` python +with exception: _tst_1() +```# Basic functionality + + + +## Basics + +------------------------------------------------------------------------ + +source + +### ifnone + +> ifnone (a, b) + +*`b` if `a` is None else `a`* + +Since `b if a is None else a` is such a common pattern, we wrap it in a +function. However, be careful, because python will evaluate *both* `a` +and `b` when calling +[`ifnone`](https://fastcore.fast.ai/basics.html#ifnone) (which it +doesn’t do if using the `if` version directly). + +``` python +test_eq(ifnone(None,1), 1) +test_eq(ifnone(2 ,1), 2) +``` + +------------------------------------------------------------------------ + +source + +### maybe_attr + +> maybe_attr (o, attr) + +*`getattr(o,attr,o)`* + +Return the attribute `attr` for object `o`. If the attribute doesn’t +exist, then return the object `o` instead. + +``` python +class myobj: myattr='foo' + +test_eq(maybe_attr(myobj, 'myattr'), 'foo') +test_eq(maybe_attr(myobj, 'another_attr'), myobj) +``` + +------------------------------------------------------------------------ + +source + +### basic_repr + +> basic_repr (flds=None) + +*Minimal `__repr__`* + +In types which provide rich display functionality in Jupyter, their +`__repr__` is also called in order to provide a fallback text +representation. Unfortunately, this includes a memory address which +changes on every invocation, making it non-deterministic. This causes +diffs to get messy and creates conflicts in git. To fix this, put +`__repr__=basic_repr()` inside your class. + +``` python +class SomeClass: __repr__=basic_repr() +repr(SomeClass()) +``` + + 'SomeClass()' + +If you pass a list of attributes (`flds`) of an object, then this will +generate a string with the name of each attribute and its corresponding +value. The format of this string is `key=value`, where `key` is the name +of the attribute, and `value` is the value of the attribute. For each +value, attempt to use the `__name__` attribute, otherwise fall back to +using the value’s `__repr__` when constructing the string. + +``` python +class SomeClass: + a=1 + b='foo' + __repr__=basic_repr('a,b') + __name__='some-class' + +repr(SomeClass()) +``` + + "SomeClass(a=1, b='foo')" + +Nested objects work too: + +``` python +class AnotherClass: + c=SomeClass() + d='bar' + __repr__=basic_repr(['c', 'd']) + +repr(AnotherClass()) +``` + + "AnotherClass(c=SomeClass(a=1, b='foo'), d='bar')" + +Instance variables (but not class variables) are shown if +[`basic_repr`](https://fastcore.fast.ai/basics.html#basic_repr) is +called with no arguments: + +``` python +class SomeClass: + def __init__(self, a=1, b='foo'): self.a,self.b = a,b + __repr__=basic_repr() + +repr(SomeClass()) +``` + + "SomeClass(a=1, b='foo')" + +------------------------------------------------------------------------ + +source + +### BasicRepr + +> BasicRepr () + +*Base class for objects needing a basic `__repr__`* + +As a shortcut for creating a `__repr__` for instance variables, you can +inherit from +[`BasicRepr`](https://fastcore.fast.ai/basics.html#basicrepr): + +``` python +class SomeClass(BasicRepr): + def __init__(self, a=1, b='foo'): self.a,self.b = a,b + +repr(SomeClass()) +``` + + "SomeClass(a=1, b='foo')" + +------------------------------------------------------------------------ + +source + +### is_array + +> is_array (x) + +*`True` if `x` supports `__array__` or `iloc`* + +``` python +is_array(np.array(1)),is_array([1]) +``` + + (True, False) + +------------------------------------------------------------------------ + +source + +### listify + +> listify (o=None, *rest, use_list=False, match=None) + +*Convert `o` to a `list`* + +Conversion is designed to “do what you mean”, e.g: + +``` python +test_eq(listify('hi'), ['hi']) +test_eq(listify(b'hi'), [b'hi']) +test_eq(listify(array(1)), [array(1)]) +test_eq(listify(1), [1]) +test_eq(listify([1,2]), [1,2]) +test_eq(listify(range(3)), [0,1,2]) +test_eq(listify(None), []) +test_eq(listify(1,2), [1,2]) +``` + +``` python +arr = np.arange(9).reshape(3,3) +listify(arr) +``` + + [array([[0, 1, 2], + [3, 4, 5], + [6, 7, 8]])] + +``` python +listify(array([1,2])) +``` + + [array([1, 2])] + +Generators are turned into lists too: + +``` python +gen = (o for o in range(3)) +test_eq(listify(gen), [0,1,2]) +``` + +Use `match` to provide a length to match: + +``` python +test_eq(listify(1,match=3), [1,1,1]) +``` + +If `match` is a sequence, it’s length is used: + +``` python +test_eq(listify(1,match=range(3)), [1,1,1]) +``` + +If the listified item is not of length `1`, it must be the same length +as `match`: + +``` python +test_eq(listify([1,1,1],match=3), [1,1,1]) +test_fail(lambda: listify([1,1],match=3)) +``` + +------------------------------------------------------------------------ + +source + +### tuplify + +> tuplify (o, use_list=False, match=None) + +*Make `o` a tuple* + +``` python +test_eq(tuplify(None),()) +test_eq(tuplify([1,2,3]),(1,2,3)) +test_eq(tuplify(1,match=[1,2,3]),(1,1,1)) +``` + +------------------------------------------------------------------------ + +source + +### true + +> true (x) + +*Test whether `x` is truthy; collections with \>0 elements are +considered `True`* + +``` python +[(o,true(o)) for o in + (array(0),array(1),array([0]),array([0,1]),1,0,'',None)] +``` + + [(array(0), False), + (array(1), True), + (array([0]), True), + (array([0, 1]), True), + (1, True), + (0, False), + ('', False), + (None, False)] + +------------------------------------------------------------------------ + +source + +### NullType + +> NullType () + +*An object that is `False` and can be called, chained, and indexed* + +``` python +bool(null.hi().there[3]) +``` + + False + +------------------------------------------------------------------------ + +source + +### tonull + +> tonull (x) + +*Convert `None` to `null`* + +``` python +bool(tonull(None).hi().there[3]) +``` + + False + +------------------------------------------------------------------------ + +source + +### get_class + +> get_class (nm, *fld_names, sup=None, doc=None, funcs=None, anno=None, +> **flds) + +*Dynamically create a class, optionally inheriting from `sup`, +containing `fld_names`* + +``` python +_t = get_class('_t', 'a', b=2, anno={'b':int}) +t = _t() +test_eq(t.a, None) +test_eq(t.b, 2) +t = _t(1, b=3) +test_eq(t.a, 1) +test_eq(t.b, 3) +t = _t(1, 3) +test_eq(t.a, 1) +test_eq(t.b, 3) +test_eq(t, pickle.loads(pickle.dumps(t))) +test_eq(_t.__annotations__, {'b':int, 'a':typing.Any}) +repr(t) +``` + + '__main__._t(a=1, b=3)' + +Most often you’ll want to call +[`mk_class`](https://fastcore.fast.ai/basics.html#mk_class), since it +adds the class to your module. See +[`mk_class`](https://fastcore.fast.ai/basics.html#mk_class) for more +details and examples of use (which also apply to +[`get_class`](https://fastcore.fast.ai/basics.html#get_class)). + +------------------------------------------------------------------------ + +source + +### mk_class + +> mk_class (nm, *fld_names, sup=None, doc=None, funcs=None, mod=None, +> anno=None, **flds) + +*Create a class using +[`get_class`](https://fastcore.fast.ai/basics.html#get_class) and add to +the caller’s module* + +Any `kwargs` will be added as class attributes, and `sup` is an optional +(tuple of) base classes. + +``` python +mk_class('_t', a=1, sup=dict) +t = _t() +test_eq(t.a, 1) +assert(isinstance(t,dict)) +``` + +A `__init__` is provided that sets attrs for any `kwargs`, and for any +`args` (matching by position to fields), along with a `__repr__` which +prints all attrs. The docstring is set to `doc`. You can pass `funcs` +which will be added as attrs with the function names. + +``` python +def foo(self): return 1 +mk_class('_t', 'a', sup=dict, doc='test doc', funcs=foo) + +t = _t(3, b=2) +test_eq(t.a, 3) +test_eq(t.b, 2) +test_eq(t.foo(), 1) +test_eq(t.__doc__, 'test doc') +t +``` + + {} + +------------------------------------------------------------------------ + +source + +### wrap_class + +> wrap_class (nm, *fld_names, sup=None, doc=None, funcs=None, **flds) + +*Decorator: makes function a method of a new class `nm` passing +parameters to +[`mk_class`](https://fastcore.fast.ai/basics.html#mk_class)* + +``` python +@wrap_class('_t', a=2) +def bar(self,x): return x+1 + +t = _t() +test_eq(t.a, 2) +test_eq(t.bar(3), 4) +``` + +------------------------------------------------------------------------ + +source + +#### ignore_exceptions + +> ignore_exceptions () + +*Context manager to ignore exceptions* + +``` python +with ignore_exceptions(): + # Exception will be ignored + raise Exception +``` + +------------------------------------------------------------------------ + +source + +### exec_local + +> exec_local (code, var_name) + +*Call `exec` on `code` and return the var `var_name`* + +``` python +test_eq(exec_local("a=1", "a"), 1) +``` + +------------------------------------------------------------------------ + +source + +### risinstance + +> risinstance (types, obj=None) + +*Curried `isinstance` but with args reversed* + +``` python +assert risinstance(int, 1) +assert not risinstance(str, 0) +assert risinstance(int)(1) +assert not risinstance(int)(None) +``` + +`types` can also be strings: + +``` python +assert risinstance(('str','int'), 'a') +assert risinstance('str', 'a') +assert not risinstance('int', 'a') +``` + +------------------------------------------------------------------------ + +source + +### ver2tuple + +> ver2tuple (v:str) + +``` python +test_eq(ver2tuple('3.8.1'), (3,8,1)) +test_eq(ver2tuple('3.1'), (3,1,0)) +test_eq(ver2tuple('3.'), (3,0,0)) +test_eq(ver2tuple('3'), (3,0,0)) +``` + +## NoOp + +These are used when you need a pass-through function. + +------------------------------------------------------------------------ + +### noop + +> noop (x=None, *args, **kwargs) + +*Do nothing* + +``` python +noop() +test_eq(noop(1),1) +``` + +------------------------------------------------------------------------ + +### noops + +> noops (x=None, *args, **kwargs) + +*Do nothing (method)* + +``` python +class _t: foo=noops +test_eq(_t().foo(1),1) +``` + +## Infinite Lists + +These lists are useful for things like padding an array or adding index +column(s) to arrays. + +[`Inf`](https://fastcore.fast.ai/basics.html#inf) defines the following +properties: + +- `count: itertools.count()` +- `zeros: itertools.cycle([0])` +- `ones : itertools.cycle([1])` +- `nones: itertools.cycle([None])` + +``` python +test_eq([o for i,o in zip(range(5), Inf.count)], + [0, 1, 2, 3, 4]) + +test_eq([o for i,o in zip(range(5), Inf.zeros)], + [0]*5) + +test_eq([o for i,o in zip(range(5), Inf.ones)], + [1]*5) + +test_eq([o for i,o in zip(range(5), Inf.nones)], + [None]*5) +``` + +## Operator Functions + +------------------------------------------------------------------------ + +source + +### in\_ + +> in_ (x, a) + +*`True` if `x in a`* + +``` python +# test if element is in another +assert in_('c', ('b', 'c', 'a')) +assert in_(4, [2,3,4,5]) +assert in_('t', 'fastai') +test_fail(in_('h', 'fastai')) + +# use in_ as a partial +assert in_('fastai')('t') +assert in_([2,3,4,5])(4) +test_fail(in_('fastai')('h')) +``` + +In addition to [`in_`](https://fastcore.fast.ai/basics.html#in_), the +following functions are provided matching the behavior of the equivalent +versions in `operator`: *lt gt le ge eq ne add sub mul truediv is\_ +is_not mod*. + +``` python +lt(3,5),gt(3,5),is_(None,None),in_(0,[1,2]),mod(3,2) +``` + + (True, False, True, False, 1) + +Similarly to `_in`, they also have additional functionality: if you only +pass one param, they return a partial function that passes that param as +the second positional parameter. + +``` python +lt(5)(3),gt(5)(3),is_(None)(None),in_([1,2])(0),mod(2)(3) +``` + + (True, False, True, False, 1) + +------------------------------------------------------------------------ + +source + +### ret_true + +> ret_true (*args, **kwargs) + +*Predicate: always `True`* + +``` python +assert ret_true(1,2,3) +assert ret_true(False) +``` + +------------------------------------------------------------------------ + +source + +### ret_false + +> ret_false (*args, **kwargs) + +*Predicate: always `False`* + +------------------------------------------------------------------------ + +source + +### stop + +> stop (e=) + +*Raises exception `e` (by default `StopIteration`)* + +------------------------------------------------------------------------ + +source + +### gen + +> gen (func, seq, cond=) + +*Like `(func(o) for o in seq if cond(func(o)))` but handles +`StopIteration`* + +``` python +test_eq(gen(noop, Inf.count, lt(5)), + range(5)) +test_eq(gen(operator.neg, Inf.count, gt(-5)), + [0,-1,-2,-3,-4]) +test_eq(gen(lambda o:o if o<5 else stop(), Inf.count), + range(5)) +``` + +------------------------------------------------------------------------ + +source + +### chunked + +> chunked (it, chunk_sz=None, drop_last=False, n_chunks=None) + +*Return batches from iterator `it` of size `chunk_sz` (or return +`n_chunks` total)* + +Note that you must pass either `chunk_sz`, or `n_chunks`, but not both. + +``` python +t = list(range(10)) +test_eq(chunked(t,3), [[0,1,2], [3,4,5], [6,7,8], [9]]) +test_eq(chunked(t,3,True), [[0,1,2], [3,4,5], [6,7,8], ]) + +t = map(lambda o:stop() if o==6 else o, Inf.count) +test_eq(chunked(t,3), [[0, 1, 2], [3, 4, 5]]) +t = map(lambda o:stop() if o==7 else o, Inf.count) +test_eq(chunked(t,3), [[0, 1, 2], [3, 4, 5], [6]]) + +t = np.arange(10) +test_eq(chunked(t,3), [[0,1,2], [3,4,5], [6,7,8], [9]]) +test_eq(chunked(t,3,True), [[0,1,2], [3,4,5], [6,7,8], ]) + +test_eq(chunked([], 3), []) +test_eq(chunked([], n_chunks=3), []) +``` + +------------------------------------------------------------------------ + +source + +### otherwise + +> otherwise (x, tst, y) + +*`y if tst(x) else x`* + +``` python +test_eq(otherwise(2+1, gt(3), 4), 3) +test_eq(otherwise(2+1, gt(2), 4), 4) +``` + +## Attribute Helpers + +These functions reduce boilerplate when setting or manipulating +attributes or properties of objects. + +------------------------------------------------------------------------ + +source + +### custom_dir + +> custom_dir (c, add) + +*Implement custom `__dir__`, adding `add` to `cls`* + +[`custom_dir`](https://fastcore.fast.ai/basics.html#custom_dir) allows +you extract the [`__dict__` property of a +class](https://stackoverflow.com/questions/19907442/explain-dict-attribute) +and appends the list `add` to it. + +``` python +class _T: + def f(): pass + +s = custom_dir(_T(), add=['foo', 'bar']) +assert {'foo', 'bar', 'f'}.issubset(s) +``` + +------------------------------------------------------------------------ + +source + +### AttrDict + +*`dict` subclass that also provides access to keys as attrs* + +``` python +d = AttrDict(a=1,b="two") +test_eq(d.a, 1) +test_eq(d['b'], 'two') +test_eq(d.get('c','nope'), 'nope') +d.b = 2 +test_eq(d.b, 2) +test_eq(d['b'], 2) +d['b'] = 3 +test_eq(d['b'], 3) +test_eq(d.b, 3) +assert 'a' in dir(d) +``` + +[`AttrDict`](https://fastcore.fast.ai/basics.html#attrdict) will pretty +print in Jupyter Notebooks: + +``` python +_test_dict = {'a':1, 'b': {'c':1, 'd':2}, 'c': {'c':1, 'd':2}, 'd': {'c':1, 'd':2}, + 'e': {'c':1, 'd':2}, 'f': {'c':1, 'd':2, 'e': 4, 'f':[1,2,3,4,5]}} +AttrDict(_test_dict) +``` + +``` json +{ 'a': 1, + 'b': {'c': 1, 'd': 2}, + 'c': {'c': 1, 'd': 2}, + 'd': {'c': 1, 'd': 2}, + 'e': {'c': 1, 'd': 2}, + 'f': {'c': 1, 'd': 2, 'e': 4, 'f': [1, 2, 3, 4, 5]}} +``` + +------------------------------------------------------------------------ + +source + +### AttrDictDefault + +> AttrDictDefault (*args, default_=None, **kwargs) + +*[`AttrDict`](https://fastcore.fast.ai/basics.html#attrdict) subclass +that returns `None` for missing attrs* + +``` python +d = AttrDictDefault(a=1,b="two", default_='nope') +test_eq(d.a, 1) +test_eq(d['b'], 'two') +test_eq(d.c, 'nope') +``` + +------------------------------------------------------------------------ + +source + +### NS + +*`SimpleNamespace` subclass that also adds `iter` and `dict` support* + +This is very similar to +[`AttrDict`](https://fastcore.fast.ai/basics.html#attrdict), but since +it starts with `SimpleNamespace`, it has some differences in behavior. +You can use it just like `SimpleNamespace`: + +``` python +d = NS(**_test_dict) +d +``` + + namespace(a=1, + b={'c': 1, 'd': 2}, + c={'c': 1, 'd': 2}, + d={'c': 1, 'd': 2}, + e={'c': 1, 'd': 2}, + f={'c': 1, 'd': 2, 'e': 4, 'f': [1, 2, 3, 4, 5]}) + +…but you can also index it to get/set: + +``` python +d['a'] +``` + + 1 + +…and iterate t: + +``` python +list(d) +``` + + ['a', 'b', 'c', 'd', 'e', 'f'] + +------------------------------------------------------------------------ + +source + +### get_annotations_ex + +> get_annotations_ex (obj, globals=None, locals=None) + +*Backport of py3.10 `get_annotations` that returns globals/locals* + +In Python 3.10 `inspect.get_annotations` was added. However previous +versions of Python are unable to evaluate type annotations correctly if +`from future import __annotations__` is used. Furthermore, *all* +annotations are evaluated, even if only some subset are needed. +[`get_annotations_ex`](https://fastcore.fast.ai/basics.html#get_annotations_ex) +provides the same functionality as `inspect.get_annotations`, but works +on earlier versions of Python, and returns the `globals` and `locals` +needed to evaluate types. + +------------------------------------------------------------------------ + +source + +### eval_type + +> eval_type (t, glb, loc) + +*`eval` a type or collection of types, if needed, for annotations in +py3.10+* + +In py3.10, or if `from future import __annotations__` is used, `a` is a +`str`: + +``` python +class _T2a: pass +def func(a: _T2a): pass +ann,glb,loc = get_annotations_ex(func) + +eval_type(ann['a'], glb, loc) +``` + + __main__._T2a + +`|` is supported for defining `Union` types when using +[`eval_type`](https://fastcore.fast.ai/basics.html#eval_type) even for +python versions prior to 3.9: + +``` python +class _T2b: pass +def func(a: _T2a|_T2b): pass +ann,glb,loc = get_annotations_ex(func) + +eval_type(ann['a'], glb, loc) +``` + + typing.Union[__main__._T2a, __main__._T2b] + +------------------------------------------------------------------------ + +source + +### type_hints + +> type_hints (f) + +*Like `typing.get_type_hints` but returns `{}` if not allowed type* + +Below is a list of allowed types for type hints in python: + +``` python +list(typing._allowed_types) +``` + + [function, + builtin_function_or_method, + method, + module, + wrapper_descriptor, + method-wrapper, + method_descriptor] + +For example, type `func` is allowed so +[`type_hints`](https://fastcore.fast.ai/basics.html#type_hints) returns +the same value as `typing.get_hints`: + +``` python +def f(a:int)->bool: ... # a function with type hints (allowed) +exp = {'a':int,'return':bool} +test_eq(type_hints(f), typing.get_type_hints(f)) +test_eq(type_hints(f), exp) +``` + +However, `class` is not an allowed type, so +[`type_hints`](https://fastcore.fast.ai/basics.html#type_hints) returns +`{}`: + +``` python +class _T: + def __init__(self, a:int=0)->bool: ... +assert not type_hints(_T) +``` + +------------------------------------------------------------------------ + +source + +### annotations + +> annotations (o) + +*Annotations for `o`, or `type(o)`* + +This supports a wider range of situations than +[`type_hints`](https://fastcore.fast.ai/basics.html#type_hints), by +checking `type()` and `__init__` for annotations too: + +``` python +for o in _T,_T(),_T.__init__,f: test_eq(annotations(o), exp) +assert not annotations(int) +assert not annotations(print) +``` + +------------------------------------------------------------------------ + +source + +### anno_ret + +> anno_ret (func) + +*Get the return annotation of `func`* + +``` python +def f(x) -> float: return x +test_eq(anno_ret(f), float) + +def f(x) -> typing.Tuple[float,float]: return x +assert anno_ret(f)==typing.Tuple[float,float] +``` + +If your return annotation is `None`, +[`anno_ret`](https://fastcore.fast.ai/basics.html#anno_ret) will return +`NoneType` (and not `None`): + +``` python +def f(x) -> None: return x + +test_eq(anno_ret(f), NoneType) +assert anno_ret(f) is not None # returns NoneType instead of None +``` + +If your function does not have a return type, or if you pass in `None` +instead of a function, then +[`anno_ret`](https://fastcore.fast.ai/basics.html#anno_ret) returns +`None`: + +``` python +def f(x): return x + +test_eq(anno_ret(f), None) +test_eq(anno_ret(None), None) # instead of passing in a func, pass in None +``` + +------------------------------------------------------------------------ + +source + +### signature_ex + +> signature_ex (obj, eval_str:bool=False) + +*Backport of `inspect.signature(..., eval_str=True` to \source + +### union2tuple + +> union2tuple (t) + +``` python +test_eq(union2tuple(Union[int,str]), (int,str)) +test_eq(union2tuple(int), int) +assert union2tuple(Tuple[int,str])==Tuple[int,str] +test_eq(union2tuple((int,str)), (int,str)) +if UnionType: test_eq(union2tuple(int|str), (int,str)) +``` + +------------------------------------------------------------------------ + +source + +### argnames + +> argnames (f, frame=False) + +*Names of arguments to function or frame `f`* + +``` python +test_eq(argnames(f), ['x']) +``` + +------------------------------------------------------------------------ + +source + +### with_cast + +> with_cast (f) + +*Decorator which uses any parameter annotations as preprocessing +functions* + +``` python +@with_cast +def _f(a, b:Path, c:str='', d=0): return (a,b,c,d) + +test_eq(_f(1, '.', 3), (1,Path('.'),'3',0)) +test_eq(_f(1, '.'), (1,Path('.'),'',0)) + +@with_cast +def _g(a:int=0)->str: return a + +test_eq(_g(4.0), '4') +test_eq(_g(4.4), '4') +test_eq(_g(2), '2') +``` + +------------------------------------------------------------------------ + +source + +### store_attr + +> store_attr (names=None, but='', cast=False, store_args=None, **attrs) + +*Store params named in comma-separated `names` from calling context into +attrs in `self`* + +In it’s most basic form, you can use +[`store_attr`](https://fastcore.fast.ai/basics.html#store_attr) to +shorten code like this: + +``` python +class T: + def __init__(self, a,b,c): self.a,self.b,self.c = a,b,c +``` + +…to this: + +``` python +class T: + def __init__(self, a,b,c): store_attr('a,b,c', self) +``` + +This class behaves as if we’d used the first form: + +``` python +t = T(1,c=2,b=3) +assert t.a==1 and t.b==3 and t.c==2 +``` + +``` python +class T1: + def __init__(self, a,b,c): store_attr() +``` + +In addition, it stores the attrs as a `dict` in `__stored_args__`, which +you can use for display, logging, and so forth. + +``` python +test_eq(t.__stored_args__, {'a':1, 'b':3, 'c':2}) +``` + +Since you normally want to use the first argument (often called `self`) +for storing attributes, it’s optional: + +``` python +class T: + def __init__(self, a,b,c:str): store_attr('a,b,c') + +t = T(1,c=2,b=3) +assert t.a==1 and t.b==3 and t.c==2 +``` + +With `cast=True` any parameter annotations will be used as preprocessing +functions for the corresponding arguments: + +``` python +class T: + def __init__(self, a:listify, b, c:str): store_attr('a,b,c', cast=True) + +t = T(1,c=2,b=3) +assert t.a==[1] and t.b==3 and t.c=='2' +``` + +You can inherit from a class using +[`store_attr`](https://fastcore.fast.ai/basics.html#store_attr), and +just call it again to add in any new attributes added in the derived +class: + +``` python +class T2(T): + def __init__(self, d, **kwargs): + super().__init__(**kwargs) + store_attr('d') + +t = T2(d=1,a=2,b=3,c=4) +assert t.a==2 and t.b==3 and t.c==4 and t.d==1 +``` + +You can skip passing a list of attrs to store. In this case, all +arguments passed to the method are stored: + +``` python +class T: + def __init__(self, a,b,c): store_attr() + +t = T(1,c=2,b=3) +assert t.a==1 and t.b==3 and t.c==2 +``` + +``` python +class T4(T): + def __init__(self, d, **kwargs): + super().__init__(**kwargs) + store_attr() + +t = T4(4, a=1,c=2,b=3) +assert t.a==1 and t.b==3 and t.c==2 and t.d==4 +``` + +``` python +class T4: + def __init__(self, *, a: int, b: float = 1): + store_attr() + +t = T4(a=3) +assert t.a==3 and t.b==1 +t = T4(a=3, b=2) +assert t.a==3 and t.b==2 +``` + +You can skip some attrs by passing `but`: + +``` python +class T: + def __init__(self, a,b,c): store_attr(but='a') + +t = T(1,c=2,b=3) +assert t.b==3 and t.c==2 +assert not hasattr(t,'a') +``` + +You can also pass keywords to +[`store_attr`](https://fastcore.fast.ai/basics.html#store_attr), which +is identical to setting the attrs directly, but also stores them in +`__stored_args__`. + +``` python +class T: + def __init__(self): store_attr(a=1) + +t = T() +assert t.a==1 +``` + +You can also use store_attr inside functions. + +``` python +def create_T(a, b): + t = SimpleNamespace() + store_attr(self=t) + return t + +t = create_T(a=1, b=2) +assert t.a==1 and t.b==2 +``` + +------------------------------------------------------------------------ + +source + +### attrdict + +> attrdict (o, *ks, default=None) + +*Dict from each `k` in `ks` to `getattr(o,k)`* + +``` python +class T: + def __init__(self, a,b,c): store_attr() + +t = T(1,c=2,b=3) +test_eq(attrdict(t,'b','c'), {'b':3, 'c':2}) +``` + +------------------------------------------------------------------------ + +source + +### properties + +> properties (cls, *ps) + +*Change attrs in `cls` with names in `ps` to properties* + +``` python +class T: + def a(self): return 1 + def b(self): return 2 +properties(T,'a') + +test_eq(T().a,1) +test_eq(T().b(),2) +``` + +------------------------------------------------------------------------ + +source + +### camel2words + +> camel2words (s, space=' ') + +*Convert CamelCase to ‘spaced words’* + +``` python +test_eq(camel2words('ClassAreCamel'), 'Class Are Camel') +``` + +------------------------------------------------------------------------ + +source + +### camel2snake + +> camel2snake (name) + +*Convert CamelCase to snake_case* + +``` python +test_eq(camel2snake('ClassAreCamel'), 'class_are_camel') +test_eq(camel2snake('Already_Snake'), 'already__snake') +``` + +------------------------------------------------------------------------ + +source + +### snake2camel + +> snake2camel (s) + +*Convert snake_case to CamelCase* + +``` python +test_eq(snake2camel('a_b_cc'), 'ABCc') +``` + +------------------------------------------------------------------------ + +source + +### class2attr + +> class2attr (cls_name) + +*Return the snake-cased name of the class; strip ending `cls_name` if it +exists.* + +``` python +class Parent: + @property + def name(self): return class2attr(self, 'Parent') + +class ChildOfParent(Parent): pass +class ParentChildOf(Parent): pass + +p = Parent() +cp = ChildOfParent() +cp2 = ParentChildOf() + +test_eq(p.name, 'parent') +test_eq(cp.name, 'child_of') +test_eq(cp2.name, 'parent_child_of') +``` + +------------------------------------------------------------------------ + +source + +### getcallable + +> getcallable (o, attr) + +*Calls `getattr` with a default of `noop`* + +``` python +class Math: + def addition(self,a,b): return a+b + +m = Math() + +test_eq(getcallable(m, "addition")(a=1,b=2), 3) +test_eq(getcallable(m, "subtraction")(a=1,b=2), None) +``` + +------------------------------------------------------------------------ + +source + +### getattrs + +> getattrs (o, *attrs, default=None) + +*List of all `attrs` in `o`* + +``` python +from fractions import Fraction +``` + +``` python +getattrs(Fraction(1,2), 'numerator', 'denominator') +``` + + [1, 2] + +------------------------------------------------------------------------ + +source + +### hasattrs + +> hasattrs (o, attrs) + +*Test whether `o` contains all `attrs`* + +``` python +assert hasattrs(1,('imag','real')) +assert not hasattrs(1,('imag','foo')) +``` + +------------------------------------------------------------------------ + +source + +### setattrs + +> setattrs (dest, flds, src) + +``` python +d = dict(a=1,bb="2",ignore=3) +o = SimpleNamespace() +setattrs(o, "a,bb", d) +test_eq(o.a, 1) +test_eq(o.bb, "2") +``` + +``` python +d = SimpleNamespace(a=1,bb="2",ignore=3) +o = SimpleNamespace() +setattrs(o, "a,bb", d) +test_eq(o.a, 1) +test_eq(o.bb, "2") +``` + +------------------------------------------------------------------------ + +source + +### try_attrs + +> try_attrs (obj, *attrs) + +*Return first attr that exists in `obj`* + +``` python +test_eq(try_attrs(1, 'real'), 1) +test_eq(try_attrs(1, 'foobar', 'real'), 1) +``` + +## Attribute Delegation + +------------------------------------------------------------------------ + +source + +### GetAttrBase + +> GetAttrBase () + +*Basic delegation of +[`__getattr__`](https://fastcore.fast.ai/xml.html#__getattr__) and +`__dir__`* + +------------------------------------------------------------------------ + +source + +#### GetAttr + +> GetAttr () + +*Inherit from this to have all attr accesses in `self._xtra` passed down +to `self.default`* + +Inherit from [`GetAttr`](https://fastcore.fast.ai/basics.html#getattr) +to have attr access passed down to an instance attribute. This makes it +easy to create composites that don’t require callers to know about their +components. For a more detailed discussion of how this works as well as +relevant context, we suggest reading the [delegated composition section +of this blog article](https://www.fast.ai/2019/08/06/delegation/). + +You can customise the behaviour of +[`GetAttr`](https://fastcore.fast.ai/basics.html#getattr) in subclasses +via; - `_default` - By default, this is set to `'default'`, so attr +access is passed down to `self.default` - `_default` can be set to the +name of any instance attribute that does not start with dunder `__` - +`_xtra` - By default, this is `None`, so all attr access is passed +down - You can limit which attrs get passed down by setting `_xtra` to a +list of attribute names + +To illuminate the utility of +[`GetAttr`](https://fastcore.fast.ai/basics.html#getattr), suppose we +have the following two classes, `_WebPage` which is a superclass of +`_ProductPage`, which we wish to compose like so: + +``` python +class _WebPage: + def __init__(self, title, author="Jeremy"): + self.title,self.author = title,author + +class _ProductPage: + def __init__(self, page, price): self.page,self.price = page,price + +page = _WebPage('Soap', author="Sylvain") +p = _ProductPage(page, 15.0) +``` + +How do we make it so we can just write `p.author`, instead of +`p.page.author` to access the `author` attribute? We can use +[`GetAttr`](https://fastcore.fast.ai/basics.html#getattr), of course! +First, we subclass +[`GetAttr`](https://fastcore.fast.ai/basics.html#getattr) when defining +`_ProductPage`. Next, we set `self.default` to the object whose +attributes we want to be able to access directly, which in this case is +the `page` argument passed on initialization: + +``` python +class _ProductPage(GetAttr): + def __init__(self, page, price): self.default,self.price = page,price #self.default allows you to access page directly. + +p = _ProductPage(page, 15.0) +``` + +Now, we can access the `author` attribute directly from the instance: + +``` python +test_eq(p.author, 'Sylvain') +``` + +If you wish to store the object you are composing in an attribute other +than `self.default`, you can set the class attribute `_data` as shown +below. This is useful in the case where you might have a name collision +with `self.default`: + +``` python +class _C(GetAttr): + _default = '_data' # use different component name; `self._data` rather than `self.default` + def __init__(self,a): self._data = a + def foo(self): noop + +t = _C('Hi') +test_eq(t._data, 'Hi') +test_fail(lambda: t.default) # we no longer have self.default +test_eq(t.lower(), 'hi') +test_eq(t.upper(), 'HI') +assert 'lower' in dir(t) +assert 'upper' in dir(t) +``` + +By default, all attributes and methods of the object you are composing +are retained. In the below example, we compose a `str` object with the +class `_C`. This allows us to directly call string methods on instances +of class `_C`, such as `str.lower()` or `str.upper()`: + +``` python +class _C(GetAttr): + # allow all attributes and methods to get passed to `self.default` (by leaving _xtra=None) + def __init__(self,a): self.default = a + def foo(self): noop + +t = _C('Hi') +test_eq(t.lower(), 'hi') +test_eq(t.upper(), 'HI') +assert 'lower' in dir(t) +assert 'upper' in dir(t) +``` + +However, you can choose which attributes or methods to retain by +defining a class attribute `_xtra`, which is a list of allowed attribute +and method names to delegate. In the below example, we only delegate the +`lower` method from the composed `str` object when defining class `_C`: + +``` python +class _C(GetAttr): + _xtra = ['lower'] # specify which attributes get passed to `self.default` + def __init__(self,a): self.default = a + def foo(self): noop + +t = _C('Hi') +test_eq(t.default, 'Hi') +test_eq(t.lower(), 'hi') +test_fail(lambda: t.upper()) # upper wasn't in _xtra, so it isn't available to be called +assert 'lower' in dir(t) +assert 'upper' not in dir(t) +``` + +You must be careful to properly set an instance attribute in `__init__` +that corresponds to the class attribute `_default`. The below example +sets the class attribute `_default` to `data`, but erroneously fails to +define `self.data` (and instead defines `self.default`). + +Failing to properly set instance attributes leads to errors when you try +to access methods directly: + +``` python +class _C(GetAttr): + _default = 'data' # use a bad component name; i.e. self.data does not exist + def __init__(self,a): self.default = a + def foo(self): noop + +# TODO: should we raise an error when we create a new instance ... +t = _C('Hi') +test_eq(t.default, 'Hi') +# ... or is it enough for all GetAttr features to raise errors +test_fail(lambda: t.data) +test_fail(lambda: t.lower()) +test_fail(lambda: t.upper()) +test_fail(lambda: dir(t)) +``` + +------------------------------------------------------------------------ + +source + +### delegate_attr + +> delegate_attr (k, to) + +*Use in [`__getattr__`](https://fastcore.fast.ai/xml.html#__getattr__) +to delegate to attr `to` without inheriting from +[`GetAttr`](https://fastcore.fast.ai/basics.html#getattr)* + +[`delegate_attr`](https://fastcore.fast.ai/basics.html#delegate_attr) is +a functional way to delegate attributes, and is an alternative to +[`GetAttr`](https://fastcore.fast.ai/basics.html#getattr). We recommend +reading the documentation of +[`GetAttr`](https://fastcore.fast.ai/basics.html#getattr) for more +details around delegation. + +You can use achieve delegation when you define +[`__getattr__`](https://fastcore.fast.ai/xml.html#__getattr__) by using +[`delegate_attr`](https://fastcore.fast.ai/basics.html#delegate_attr): + +``` python +class _C: + def __init__(self, o): self.o = o # self.o corresponds to the `to` argument in delegate_attr. + def __getattr__(self, k): return delegate_attr(self, k, to='o') + + +t = _C('HELLO') # delegates to a string +test_eq(t.lower(), 'hello') + +t = _C(np.array([5,4,3])) # delegates to a numpy array +test_eq(t.sum(), 12) + +t = _C(pd.DataFrame({'a': [1,2], 'b': [3,4]})) # delegates to a pandas.DataFrame +test_eq(t.b.max(), 4) +``` + +## Extensible Types + +[`ShowPrint`](https://fastcore.fast.ai/basics.html#showprint) is a base +class that defines a `show` method, which is used primarily for +callbacks in fastai that expect this method to be defined. + +[`Int`](https://fastcore.fast.ai/basics.html#int), +[`Float`](https://fastcore.fast.ai/basics.html#float), and +[`Str`](https://fastcore.fast.ai/basics.html#str) extend `int`, `float` +and `str` respectively by adding an additional `show` method by +inheriting from +[`ShowPrint`](https://fastcore.fast.ai/basics.html#showprint). + +The code for [`Int`](https://fastcore.fast.ai/basics.html#int) is shown +below: + +Examples: + +``` python +Int(0).show() +Float(2.0).show() +Str('Hello').show() +``` + + 0 + 2.0 + Hello + +## Collection functions + +Functions that manipulate popular python collections. + +------------------------------------------------------------------------ + +source + +### partition + +> partition (coll, f) + +*Partition a collection by a predicate* + +``` python +ts,fs = partition(range(10), mod(2)) +test_eq(fs, [0,2,4,6,8]) +test_eq(ts, [1,3,5,7,9]) +``` + +------------------------------------------------------------------------ + +source + +### flatten + +> flatten (o) + +*Concatenate all collections and items as a generator* + +------------------------------------------------------------------------ + +source + +### concat + +> concat (colls) + +*Concatenate all collections and items as a list* + +``` python +concat([(o for o in range(2)),[2,3,4], 5]) +``` + + [0, 1, 2, 3, 4, 5] + +``` python +concat([["abc", "xyz"], ["foo", "bar"]]) +``` + + ['abc', 'xyz', 'foo', 'bar'] + +------------------------------------------------------------------------ + +source + +### strcat + +> strcat (its, sep:str='') + +*Concatenate stringified items `its`* + +``` python +test_eq(strcat(['a',2]), 'a2') +test_eq(strcat(['a',2], ';'), 'a;2') +``` + +------------------------------------------------------------------------ + +source + +### detuplify + +> detuplify (x) + +*If `x` is a tuple with one thing, extract it* + +``` python +test_eq(detuplify(()),None) +test_eq(detuplify([1]),1) +test_eq(detuplify([1,2]), [1,2]) +test_eq(detuplify(np.array([[1,2]])), np.array([[1,2]])) +``` + +------------------------------------------------------------------------ + +source + +### replicate + +> replicate (item, match) + +*Create tuple of `item` copied `len(match)` times* + +``` python +t = [1,1] +test_eq(replicate([1,2], t),([1,2],[1,2])) +test_eq(replicate(1, t),(1,1)) +``` + +------------------------------------------------------------------------ + +source + +### setify + +> setify (o) + +*Turn any list like-object into a set.* + +``` python +# test +test_eq(setify(None),set()) +test_eq(setify('abc'),{'abc'}) +test_eq(setify([1,2,2]),{1,2}) +test_eq(setify(range(0,3)),{0,1,2}) +test_eq(setify({1,2}),{1,2}) +``` + +------------------------------------------------------------------------ + +source + +### merge + +> merge (*ds) + +*Merge all dictionaries in `ds`* + +``` python +test_eq(merge(), {}) +test_eq(merge(dict(a=1,b=2)), dict(a=1,b=2)) +test_eq(merge(dict(a=1,b=2), dict(b=3,c=4), None), dict(a=1, b=3, c=4)) +``` + +------------------------------------------------------------------------ + +source + +### range_of + +> range_of (x) + +*All indices of collection `x` (i.e. `list(range(len(x)))`)* + +``` python +test_eq(range_of([1,1,1,1]), [0,1,2,3]) +``` + +------------------------------------------------------------------------ + +source + +### groupby + +> groupby (x, key, val=) + +*Like `itertools.groupby` but doesn’t need to be sorted, and isn’t lazy, +plus some extensions* + +``` python +test_eq(groupby('aa ab bb'.split(), itemgetter(0)), {'a':['aa','ab'], 'b':['bb']}) +``` + +Here’s an example of how to *invert* a grouping, using an `int` as `key` +(which uses `itemgetter`; passing a `str` will use `attrgetter`), and +using a `val` function: + +``` python +d = {0: [1, 3, 7], 2: [3], 3: [5], 4: [8], 5: [4], 7: [5]} +groupby(((o,k) for k,v in d.items() for o in v), 0, 1) +``` + + {1: [0], 3: [0, 2], 7: [0], 5: [3, 7], 8: [4], 4: [5]} + +------------------------------------------------------------------------ + +source + +### last_index + +> last_index (x, o) + +*Finds the last index of occurence of `x` in `o` (returns -1 if no +occurence)* + +``` python +test_eq(last_index(9, [1, 2, 9, 3, 4, 9, 10]), 5) +test_eq(last_index(6, [1, 2, 9, 3, 4, 9, 10]), -1) +``` + +------------------------------------------------------------------------ + +source + +### filter_dict + +> filter_dict (d, func) + +*Filter a `dict` using `func`, applied to keys and values* + +``` python +letters = {o:chr(o) for o in range(65,73)} +letters +``` + + {65: 'A', 66: 'B', 67: 'C', 68: 'D', 69: 'E', 70: 'F', 71: 'G', 72: 'H'} + +``` python +filter_dict(letters, lambda k,v: k<67 or v in 'FG') +``` + + {65: 'A', 66: 'B', 70: 'F', 71: 'G'} + +------------------------------------------------------------------------ + +source + +### filter_keys + +> filter_keys (d, func) + +*Filter a `dict` using `func`, applied to keys* + +``` python +filter_keys(letters, lt(67)) +``` + + {65: 'A', 66: 'B'} + +------------------------------------------------------------------------ + +source + +### filter_values + +> filter_values (d, func) + +*Filter a `dict` using `func`, applied to values* + +``` python +filter_values(letters, in_('FG')) +``` + + {70: 'F', 71: 'G'} + +------------------------------------------------------------------------ + +source + +### cycle + +> cycle (o) + +*Like `itertools.cycle` except creates list of `None`s if `o` is empty* + +``` python +test_eq(itertools.islice(cycle([1,2,3]),5), [1,2,3,1,2]) +test_eq(itertools.islice(cycle([]),3), [None]*3) +test_eq(itertools.islice(cycle(None),3), [None]*3) +test_eq(itertools.islice(cycle(1),3), [1,1,1]) +``` + +------------------------------------------------------------------------ + +source + +### zip_cycle + +> zip_cycle (x, *args) + +*Like `itertools.zip_longest` but +[`cycle`](https://fastcore.fast.ai/foundation.html#cycle)s through +elements of all but first argument* + +``` python +test_eq(zip_cycle([1,2,3,4],list('abc')), [(1, 'a'), (2, 'b'), (3, 'c'), (4, 'a')]) +``` + +------------------------------------------------------------------------ + +source + +### sorted_ex + +> sorted_ex (iterable, key=None, reverse=False) + +*Like `sorted`, but if key is str use `attrgetter`; if int use +`itemgetter`* + +------------------------------------------------------------------------ + +source + +### not\_ + +> not_ (f) + +*Create new function that negates result of `f`* + +``` python +def f(a): return a>0 +test_eq(f(1),True) +test_eq(not_(f)(1),False) +test_eq(not_(f)(a=-1),True) +``` + +------------------------------------------------------------------------ + +source + +### argwhere + +> argwhere (iterable, f, negate=False, **kwargs) + +*Like [`filter_ex`](https://fastcore.fast.ai/basics.html#filter_ex), but +return indices for matching items* + +------------------------------------------------------------------------ + +source + +### filter_ex + +> filter_ex (iterable, f=, negate=False, gen=False, +> **kwargs) + +*Like `filter`, but passing `kwargs` to `f`, defaulting `f` to `noop`, +and adding `negate` and +[`gen`](https://fastcore.fast.ai/basics.html#gen)* + +------------------------------------------------------------------------ + +source + +### range_of + +> range_of (a, b=None, step=None) + +*All indices of collection `a`, if `a` is a collection, otherwise +`range`* + +``` python +test_eq(range_of([1,1,1,1]), [0,1,2,3]) +test_eq(range_of(4), [0,1,2,3]) +``` + +------------------------------------------------------------------------ + +source + +### renumerate + +> renumerate (iterable, start=0) + +*Same as `enumerate`, but returns index as 2nd element instead of 1st* + +``` python +test_eq(renumerate('abc'), (('a',0),('b',1),('c',2))) +``` + +------------------------------------------------------------------------ + +source + +### first + +> first (x, f=None, negate=False, **kwargs) + +*First element of `x`, optionally filtered by `f`, or None if missing* + +``` python +test_eq(first(['a', 'b', 'c', 'd', 'e']), 'a') +test_eq(first([False]), False) +test_eq(first([False], noop), None) +``` + +------------------------------------------------------------------------ + +source + +### only + +> only (o) + +*Return the only item of `o`, raise if `o` doesn’t have exactly one +item* + +------------------------------------------------------------------------ + +source + +### nested_attr + +> nested_attr (o, attr, default=None) + +*Same as `getattr`, but if `attr` includes a `.`, then looks inside +nested objects* + +``` python +class CustomIndexable: + def __init__(self): self.data = {'a':1,'b':'v','c':{'d':5}} + def __getitem__(self, key): return self.data[key] + +custom_indexable = CustomIndexable() +test_eq(nested_attr(custom_indexable,'a'),1) +test_eq(nested_attr(custom_indexable,'c.d'),5) +test_eq(nested_attr(custom_indexable,'e'),None) +``` + +class TestObj: def **init**(self): self.nested = {‘key’: \[1, 2, +{‘inner’: ‘value’}\]} test_obj = TestObj() + +test_eq(nested_attr(test_obj, ‘nested.key.2.inner’),‘value’) +test_eq(nested_attr(\[1, 2, 3\], ‘1’),2) + +``` python +b = {'a':1,'b':'v','c':{'d':5}} +test_eq(nested_attr(b,'b'),'v') +test_eq(nested_attr(b,'c.d'),5) +``` + +``` python +a = SimpleNamespace(b=(SimpleNamespace(c=1))) +test_eq(nested_attr(a, 'b.c'), getattr(getattr(a, 'b'), 'c')) +test_eq(nested_attr(a, 'b.d'), None) +test_eq(nested_attr(b, 'a'), 1) +``` + +------------------------------------------------------------------------ + +source + +### nested_setdefault + +> nested_setdefault (o, attr, default) + +*Same as `setdefault`, but if `attr` includes a `.`, then looks inside +nested objects* + +------------------------------------------------------------------------ + +source + +### nested_callable + +> nested_callable (o, attr) + +*Same as +[`nested_attr`](https://fastcore.fast.ai/basics.html#nested_attr) but if +not found will return `noop`* + +``` python +a = SimpleNamespace(b=(SimpleNamespace(c=1))) +test_eq(nested_callable(a, 'b.c'), getattr(getattr(a, 'b'), 'c')) +test_eq(nested_callable(a, 'b.d'), noop) +``` + +------------------------------------------------------------------------ + +source + +### nested_idx + +> nested_idx (coll, *idxs) + +*Index into nested collections, dicts, etc, with `idxs`* + +``` python +a = {'b':[1,{'c':2}]} +test_eq(nested_idx(a, 'nope'), None) +test_eq(nested_idx(a, 'nope', 'nup'), None) +test_eq(nested_idx(a, 'b', 3), None) +test_eq(nested_idx(a), a) +test_eq(nested_idx(a, 'b'), [1,{'c':2}]) +test_eq(nested_idx(a, 'b', 1), {'c':2}) +test_eq(nested_idx(a, 'b', 1, 'c'), 2) +``` + +``` python +a = SimpleNamespace(b=[1,{'c':2}]) +test_eq(nested_idx(a, 'nope'), None) +test_eq(nested_idx(a, 'nope', 'nup'), None) +test_eq(nested_idx(a, 'b', 3), None) +test_eq(nested_idx(a), a) +test_eq(nested_idx(a, 'b'), [1,{'c':2}]) +test_eq(nested_idx(a, 'b', 1), {'c':2}) +test_eq(nested_idx(a, 'b', 1, 'c'), 2) +``` + +------------------------------------------------------------------------ + +source + +### set_nested_idx + +> set_nested_idx (coll, value, *idxs) + +*Set value indexed like \`nested_idx* + +``` python +set_nested_idx(a, 3, 'b', 0) +test_eq(nested_idx(a, 'b', 0), 3) +``` + +------------------------------------------------------------------------ + +source + +### val2idx + +> val2idx (x) + +*Dict from value to index* + +``` python +test_eq(val2idx([1,2,3]), {3:2,1:0,2:1}) +``` + +------------------------------------------------------------------------ + +source + +### uniqueify + +> uniqueify (x, sort=False, bidir=False, start=None) + +*Unique elements in `x`, optional `sort`, optional return reverse +correspondence, optional prepend with elements.* + +``` python +t = [1,1,0,5,0,3] +test_eq(uniqueify(t),[1,0,5,3]) +test_eq(uniqueify(t, sort=True),[0,1,3,5]) +test_eq(uniqueify(t, start=[7,8,6]), [7,8,6,1,0,5,3]) +v,o = uniqueify(t, bidir=True) +test_eq(v,[1,0,5,3]) +test_eq(o,{1:0, 0: 1, 5: 2, 3: 3}) +v,o = uniqueify(t, sort=True, bidir=True) +test_eq(v,[0,1,3,5]) +test_eq(o,{0:0, 1: 1, 3: 2, 5: 3}) +``` + +------------------------------------------------------------------------ + +source + +### loop_first_last + +> loop_first_last (values) + +*Iterate and generate a tuple with a flag for first and last value.* + +``` python +test_eq(loop_first_last(range(3)), [(True,False,0), (False,False,1), (False,True,2)]) +``` + +------------------------------------------------------------------------ + +source + +### loop_first + +> loop_first (values) + +*Iterate and generate a tuple with a flag for first value.* + +``` python +test_eq(loop_first(range(3)), [(True,0), (False,1), (False,2)]) +``` + +------------------------------------------------------------------------ + +source + +### loop_last + +> loop_last (values) + +*Iterate and generate a tuple with a flag for last value.* + +``` python +test_eq(loop_last(range(3)), [(False,0), (False,1), (True,2)]) +``` + +------------------------------------------------------------------------ + +source + +### first_match + +> first_match (lst, f, default=None) + +*First element of `lst` matching predicate `f`, or `default` if none* + +``` python +a = [0,2,4,5,6,7,10] +test_eq(first_match(a, lambda o:o%2), 3) +``` + +------------------------------------------------------------------------ + +source + +### last_match + +> last_match (lst, f, default=None) + +*Last element of `lst` matching predicate `f`, or `default` if none* + +``` python +test_eq(last_match(a, lambda o:o%2), 5) +``` + +## fastuple + +A tuple with extended functionality. + +------------------------------------------------------------------------ + +source + +#### fastuple + +> fastuple (x=None, *rest) + +*A `tuple` with elementwise ops and more friendly **init** behavior* + +#### Friendly init behavior + +Common failure modes when trying to initialize a tuple in python: + +``` py +tuple(3) +> TypeError: 'int' object is not iterable +``` + +or + +``` py +tuple(3, 4) +> TypeError: tuple expected at most 1 arguments, got 2 +``` + +However, [`fastuple`](https://fastcore.fast.ai/basics.html#fastuple) +allows you to define tuples like this and in the usual way: + +``` python +test_eq(fastuple(3), (3,)) +test_eq(fastuple(3,4), (3, 4)) +test_eq(fastuple((3,4)), (3, 4)) +``` + +#### Elementwise operations + +------------------------------------------------------------------------ + +source + +##### fastuple.add + +> fastuple.add (*args) + +*`+` is already defined in `tuple` for concat, so use `add` instead* + +``` python +test_eq(fastuple.add((1,1),(2,2)), (3,3)) +test_eq_type(fastuple(1,1).add(2), fastuple(3,3)) +test_eq(fastuple('1','2').add('2'), fastuple('12','22')) +``` + +------------------------------------------------------------------------ + +source + +##### fastuple.mul + +> fastuple.mul (*args) + +*`*` is already defined in `tuple` for replicating, so use `mul` +instead* + +``` python +test_eq_type(fastuple(1,1).mul(2), fastuple(2,2)) +``` + +#### Other Elementwise Operations + +Additionally, the following elementwise operations are available: - +`le`: less than or equal - `eq`: equal - `gt`: greater than - `min`: +minimum of + +``` python +test_eq(fastuple(3,1).le(1), (False, True)) +test_eq(fastuple(3,1).eq(1), (False, True)) +test_eq(fastuple(3,1).gt(1), (True, False)) +test_eq(fastuple(3,1).min(2), (2,1)) +``` + +You can also do other elementwise operations like negate a +[`fastuple`](https://fastcore.fast.ai/basics.html#fastuple), or subtract +two [`fastuple`](https://fastcore.fast.ai/basics.html#fastuple)s: + +``` python +test_eq(-fastuple(1,2), (-1,-2)) +test_eq(~fastuple(1,0,1), (False,True,False)) + +test_eq(fastuple(1,1)-fastuple(2,2), (-1,-1)) +``` + +``` python +test_eq(type(fastuple(1)), fastuple) +test_eq_type(fastuple(1,2), fastuple(1,2)) +test_ne(fastuple(1,2), fastuple(1,3)) +test_eq(fastuple(), ()) +``` + +## Functions on Functions + +Utilities for functional programming or for defining, modifying, or +debugging functions. + +------------------------------------------------------------------------ + +source + +### bind + +> bind (func, *pargs, **pkwargs) + +*Same as `partial`, except you can use `arg0` `arg1` etc param +placeholders* + +[`bind`](https://fastcore.fast.ai/basics.html#bind) is the same as +`partial`, but also allows you to reorder positional arguments using +variable name(s) `arg{i}` where i refers to the zero-indexed positional +argument. [`bind`](https://fastcore.fast.ai/basics.html#bind) as +implemented currently only supports reordering of up to the first 5 +positional arguments. + +Consider the function `myfunc` below, which has 3 positional arguments. +These arguments can be referenced as `arg0`, `arg1`, and `arg1`, +respectively. + +``` python +def myfn(a,b,c,d=1,e=2): return(a,b,c,d,e) +``` + +In the below example we bind the positional arguments of `myfn` as +follows: + +- The second input `14`, referenced by `arg1`, is substituted for the + first positional argument. +- We supply a default value of `17` for the second positional argument. +- The first input `19`, referenced by `arg0`, is subsituted for the + third positional argument. + +``` python +test_eq(bind(myfn, arg1, 17, arg0, e=3)(19,14), (14,17,19,1,3)) +``` + +In this next example: + +- We set the default value to `17` for the first positional argument. +- The first input `19` refrenced by `arg0`, becomes the second + positional argument. +- The second input `14` becomes the third positional argument. +- We override the default the value for named argument `e` to `3`. + +``` python +test_eq(bind(myfn, 17, arg0, e=3)(19,14), (17,19,14,1,3)) +``` + +This is an example of using +[`bind`](https://fastcore.fast.ai/basics.html#bind) like `partial` and +do not reorder any arguments: + +``` python +test_eq(bind(myfn)(17,19,14), (17,19,14,1,2)) +``` + +[`bind`](https://fastcore.fast.ai/basics.html#bind) can also be used to +change default values. In the below example, we use the first input `3` +to override the default value of the named argument `e`, and supply +default values for the first three positional arguments: + +``` python +test_eq(bind(myfn, 17,19,14,e=arg0)(3), (17,19,14,1,3)) +``` + +------------------------------------------------------------------------ + +source + +### mapt + +> mapt (func, *iterables) + +*Tuplified `map`* + +``` python +t = [0,1,2,3] +test_eq(mapt(operator.neg, t), (0,-1,-2,-3)) +``` + +------------------------------------------------------------------------ + +source + +### map_ex + +> map_ex (iterable, f, *args, gen=False, **kwargs) + +*Like `map`, but use +[`bind`](https://fastcore.fast.ai/basics.html#bind), and supports `str` +and indexing* + +``` python +test_eq(map_ex(t,operator.neg), [0,-1,-2,-3]) +``` + +If `f` is a string then it is treated as a format string to create the +mapping: + +``` python +test_eq(map_ex(t, '#{}#'), ['#0#','#1#','#2#','#3#']) +``` + +If `f` is a dictionary (or anything supporting `__getitem__`) then it is +indexed to create the mapping: + +``` python +test_eq(map_ex(t, list('abcd')), list('abcd')) +``` + +You can also pass the same `arg` params that +[`bind`](https://fastcore.fast.ai/basics.html#bind) accepts: + +``` python +def f(a=None,b=None): return b +test_eq(map_ex(t, f, b=arg0), range(4)) +``` + +------------------------------------------------------------------------ + +source + +### compose + +> compose (*funcs, order=None) + +*Create a function that composes all functions in `funcs`, passing along +remaining `*args` and `**kwargs` to all* + +``` python +f1 = lambda o,p=0: (o*2)+p +f2 = lambda o,p=1: (o+1)/p +test_eq(f2(f1(3)), compose(f1,f2)(3)) +test_eq(f2(f1(3,p=3),p=3), compose(f1,f2)(3,p=3)) +test_eq(f2(f1(3, 3), 3), compose(f1,f2)(3, 3)) + +f1.order = 1 +test_eq(f1(f2(3)), compose(f1,f2, order="order")(3)) +``` + +------------------------------------------------------------------------ + +source + +### maps + +> maps (*args, retain=) + +*Like `map`, except funcs are composed first* + +``` python +test_eq(maps([1]), [1]) +test_eq(maps(operator.neg, [1,2]), [-1,-2]) +test_eq(maps(operator.neg, operator.neg, [1,2]), [1,2]) +``` + +------------------------------------------------------------------------ + +source + +### partialler + +> partialler (f, *args, order=None, **kwargs) + +*Like `functools.partial` but also copies over docstring* + +``` python +def _f(x,a=1): + "test func" + return x-a +_f.order=1 + +f = partialler(_f, 2) +test_eq(f.order, 1) +test_eq(f(3), -1) +f = partialler(_f, a=2, order=3) +test_eq(f.__doc__, "test func") +test_eq(f.order, 3) +test_eq(f(3), _f(3,2)) +``` + +``` python +class partial0: + "Like `partialler`, but args passed to callable are inserted at started, instead of at end" + def __init__(self, f, *args, order=None, **kwargs): + self.f,self.args,self.kwargs = f,args,kwargs + self.order = ifnone(order, getattr(f,'order',None)) + self.__doc__ = f.__doc__ + + def __call__(self, *args, **kwargs): return self.f(*args, *self.args, **kwargs, **self.kwargs) +``` + +``` python +f = partial0(_f, 2) +test_eq(f.order, 1) +test_eq(f(3), 1) # NB: different to `partialler` example +``` + +------------------------------------------------------------------------ + +source + +### instantiate + +> instantiate (t) + +*Instantiate `t` if it’s a type, otherwise do nothing* + +``` python +test_eq_type(instantiate(int), 0) +test_eq_type(instantiate(1), 1) +``` + +------------------------------------------------------------------------ + +source + +### using_attr + +> using_attr (f, attr) + +*Construct a function which applies `f` to the argument’s attribute +`attr`* + +``` python +t = Path('/a/b.txt') +f = using_attr(str.upper, 'name') +test_eq(f(t), 'B.TXT') +``` + +### Self (with an *uppercase* S) + +A Concise Way To Create Lambdas + +This is a concise way to create lambdas that are calling methods on an +object (note the capitalization!) + +`Self.sum()`, for instance, is a shortcut for `lambda o: o.sum()`. + +``` python +f = Self.sum() +x = np.array([3.,1]) +test_eq(f(x), 4.) + +# This is equivalent to above +f = lambda o: o.sum() +x = np.array([3.,1]) +test_eq(f(x), 4.) + +f = Self.argmin() +arr = np.array([1,2,3,4,5]) +test_eq(f(arr), arr.argmin()) + +f = Self.sum().is_integer() +x = np.array([3.,1]) +test_eq(f(x), True) + +f = Self.sum().real.is_integer() +x = np.array([3.,1]) +test_eq(f(x), True) + +f = Self.imag() +test_eq(f(3), 0) + +f = Self[1] +test_eq(f(x), 1) +``` + +`Self` is also callable, which creates a function which calls any +function passed to it, using the arguments passed to `Self`: + +``` python +def f(a, b=3): return a+b+2 +def g(a, b=3): return a*b +fg = Self(1,b=2) +list(map(fg, [f,g])) +``` + + [5, 2] + +## Patching + +------------------------------------------------------------------------ + +source + +### copy_func + +> copy_func (f) + +*Copy a non-builtin function (NB `copy.copy` does not work for this)* + +Sometimes it may be desirable to make a copy of a function that doesn’t +point to the original object. When you use Python’s built in `copy.copy` +or `copy.deepcopy` to copy a function, you get a reference to the +original object: + +``` python +import copy as cp +``` + +``` python +def foo(): pass +a = cp.copy(foo) +b = cp.deepcopy(foo) + +a.someattr = 'hello' # since a and b point at the same object, updating a will update b +test_eq(b.someattr, 'hello') + +assert a is foo and b is foo +``` + +However, with +[`copy_func`](https://fastcore.fast.ai/basics.html#copy_func), you can +retrieve a copy of a function without a reference to the original +object: + +``` python +c = copy_func(foo) # c is an indpendent object +assert c is not foo +``` + +``` python +def g(x, *, y=3): return x+y +test_eq(copy_func(g)(4), 7) +``` + +------------------------------------------------------------------------ + +source + +### patch_to + +> patch_to (cls, as_prop=False, cls_method=False) + +*Decorator: add `f` to `cls`* + +The `@patch_to` decorator allows you to [monkey +patch](https://stackoverflow.com/questions/5626193/what-is-monkey-patching) +a function into a class as a method: + +``` python +class _T3(int): pass + +@patch_to(_T3) +def func1(self, a): return self+a + +t = _T3(1) # we initialized `t` to a type int = 1 +test_eq(t.func1(2), 3) # we add 2 to `t`, so 2 + 1 = 3 +``` + +You can access instance properties in the usual way via `self`: + +``` python +class _T4(): + def __init__(self, g): self.g = g + +@patch_to(_T4) +def greet(self, x): return self.g + x + +t = _T4('hello ') # this sets self.g = 'hello ' +test_eq(t.greet('world'), 'hello world') #t.greet('world') will append 'world' to 'hello ' +``` + +You can instead specify that the method should be a class method by +setting `cls_method=True`: + +``` python +class _T5(int): attr = 3 # attr is a class attribute we will access in a later method + +@patch_to(_T5, cls_method=True) +def func(cls, x): return cls.attr + x # you can access class attributes in the normal way + +test_eq(_T5.func(4), 7) +``` + +Additionally you can specify that the function you want to patch should +be a class attribute with `as_prop=True`: + +``` python +@patch_to(_T5, as_prop=True) +def add_ten(self): return self + 10 + +t = _T5(4) +test_eq(t.add_ten, 14) +``` + +Instead of passing one class to the `@patch_to` decorator, you can pass +multiple classes in a tuple to simulteanously patch more than one class +with the same method: + +``` python +class _T6(int): pass +class _T7(int): pass + +@patch_to((_T6,_T7)) +def func_mult(self, a): return self*a + +t = _T6(2) +test_eq(t.func_mult(4), 8) +t = _T7(2) +test_eq(t.func_mult(4), 8) +``` + +------------------------------------------------------------------------ + +source + +### patch + +> patch (f=None, as_prop=False, cls_method=False) + +*Decorator: add `f` to the first parameter’s class (based on f’s type +annotations)* + +`@patch` is an alternative to `@patch_to` that allows you similarly +monkey patch class(es) by using [type +annotations](https://docs.python.org/3/library/typing.html): + +``` python +class _T8(int): pass + +@patch +def func(self:_T8, a): return self+a + +t = _T8(1) # we initilized `t` to a type int = 1 +test_eq(t.func(3), 4) # we add 3 to `t`, so 3 + 1 = 4 +test_eq(t.func.__qualname__, '_T8.func') +``` + +Similarly to +[`patch_to`](https://fastcore.fast.ai/basics.html#patch_to), you can +supply a union of classes instead of a single class in your type +annotations to patch multiple classes: + +``` python +class _T9(int): pass + +@patch +def func2(x:_T8|_T9, a): return x*a # will patch both _T8 and _T9 + +t = _T8(2) +test_eq(t.func2(4), 8) +test_eq(t.func2.__qualname__, '_T8.func2') + +t = _T9(2) +test_eq(t.func2(4), 8) +test_eq(t.func2.__qualname__, '_T9.func2') +``` + +Just like [`patch_to`](https://fastcore.fast.ai/basics.html#patch_to) +decorator you can use `as_prop` and `cls_method` parameters with +[`patch`](https://fastcore.fast.ai/basics.html#patch) decorator: + +``` python +@patch(as_prop=True) +def add_ten(self:_T5): return self + 10 + +t = _T5(4) +test_eq(t.add_ten, 14) +``` + +``` python +class _T5(int): attr = 3 # attr is a class attribute we will access in a later method + +@patch(cls_method=True) +def func(cls:_T5, x): return cls.attr + x # you can access class attributes in the normal way + +test_eq(_T5.func(4), 7) +``` + +------------------------------------------------------------------------ + +source + +### patch_property + +> patch_property (f) + +*Deprecated; use `patch(as_prop=True)` instead* + +Patching `classmethod` shouldn’t affect how python’s inheritance works + +``` python +class FastParent: pass + +@patch(cls_method=True) +def type_cls(cls: FastParent): return cls + +class FastChild(FastParent): pass + +parent = FastParent() +test_eq(parent.type_cls(), FastParent) + +child = FastChild() +test_eq(child.type_cls(), FastChild) +``` + +## Other Helpers + +------------------------------------------------------------------------ + +source + +### compile_re + +> compile_re (pat) + +*Compile `pat` if it’s not None* + +``` python +assert compile_re(None) is None +assert compile_re('a').match('ab') +``` + +------------------------------------------------------------------------ + +source + +#### ImportEnum + +> ImportEnum (value, names=None, module=None, qualname=None, type=None, +> start=1) + +*An `Enum` that can have its values imported* + +``` python +_T = ImportEnum('_T', {'foobar':1, 'goobar':2}) +_T.imports() +test_eq(foobar, _T.foobar) +test_eq(goobar, _T.goobar) +``` + +------------------------------------------------------------------------ + +source + +#### StrEnum + +> StrEnum (value, names=None, module=None, qualname=None, type=None, +> start=1) + +*An [`ImportEnum`](https://fastcore.fast.ai/basics.html#importenum) that +behaves like a `str`* + +------------------------------------------------------------------------ + +source + +### str_enum + +> str_enum (name, *vals) + +*Simplified creation of +[`StrEnum`](https://fastcore.fast.ai/basics.html#strenum) types* + +------------------------------------------------------------------------ + +source + +#### ValEnum + +> ValEnum (value, names=None, module=None, qualname=None, type=None, +> start=1) + +*An [`ImportEnum`](https://fastcore.fast.ai/basics.html#importenum) that +stringifies using values* + +``` python +_T = str_enum('_T', 'a', 'b') +test_eq(f'{_T.a}', 'a') +test_eq(_T.a, 'a') +test_eq(list(_T.__members__), ['a','b']) +print(_T.a, _T.a.upper()) +``` + + a A + +------------------------------------------------------------------------ + +source + +#### Stateful + +> Stateful (*args, **kwargs) + +*A base class/mixin for objects that should not serialize all their +state* + +``` python +class _T(Stateful): + def __init__(self): + super().__init__() + self.a=1 + self._state['test']=2 + +t = _T() +t2 = pickle.loads(pickle.dumps(t)) +test_eq(t.a,1) +test_eq(t._state['test'],2) +test_eq(t2.a,1) +test_eq(t2._state,{}) +``` + +Override `_init_state` to do any necessary setup steps that are required +during `__init__` or during deserialization (e.g. `pickle.load`). Here’s +an example of how +[`Stateful`](https://fastcore.fast.ai/basics.html#stateful) simplifies +the official Python example for [Handling Stateful +Objects](https://docs.python.org/3/library/pickle.html#handling-stateful-objects). + +``` python +class TextReader(Stateful): + """Print and number lines in a text file.""" + _stateattrs=('file',) + def __init__(self, filename): + self.filename,self.lineno = filename,0 + super().__init__() + + def readline(self): + self.lineno += 1 + line = self.file.readline() + if line: return f"{self.lineno}: {line.strip()}" + + def _init_state(self): + self.file = open(self.filename) + for _ in range(self.lineno): self.file.readline() +``` + +``` python +reader = TextReader("00_test.ipynb") +print(reader.readline()) +print(reader.readline()) + +new_reader = pickle.loads(pickle.dumps(reader)) +print(reader.readline()) +``` + + 1: { + 2: "cells": [ + 3: { + +------------------------------------------------------------------------ + +source + +### NotStr + +> NotStr (s) + +*Behaves like a `str`, but isn’t an instance of one* + +``` python +s = NotStr("hello") +assert not isinstance(s, str) +test_eq(s, 'hello') +test_eq(s*2, 'hellohello') +test_eq(len(s), 5) +``` + +------------------------------------------------------------------------ + +source + +#### PrettyString + +*Little hack to get strings to show properly in Jupyter.* + +Allow strings with special characters to render properly in Jupyter. +Without calling `print()` strings with special characters are displayed +like so: + +``` python +with_special_chars='a string\nwith\nnew\nlines and\ttabs' +with_special_chars +``` + + 'a string\nwith\nnew\nlines and\ttabs' + +We can correct this with +[`PrettyString`](https://fastcore.fast.ai/basics.html#prettystring): + +``` python +PrettyString(with_special_chars) +``` + + a string + with + new + lines and tabs + +------------------------------------------------------------------------ + +source + +### even_mults + +> even_mults (start, stop, n) + +*Build log-stepped array from `start` to +[`stop`](https://fastcore.fast.ai/basics.html#stop) in `n` steps.* + +``` python +test_eq(even_mults(2,8,3), [2,4,8]) +test_eq(even_mults(2,32,5), [2,4,8,16,32]) +test_eq(even_mults(2,8,1), 8) +``` + +------------------------------------------------------------------------ + +source + +### num_cpus + +> num_cpus () + +*Get number of cpus* + +``` python +num_cpus() +``` + + 10 + +------------------------------------------------------------------------ + +source + +### add_props + +> add_props (f, g=None, n=2) + +*Create properties passing each of `range(n)` to f* + +``` python +class _T(): a,b = add_props(lambda i,x:i*2) + +t = _T() +test_eq(t.a,0) +test_eq(t.b,2) +``` + +``` python +class _T(): + def __init__(self, v): self.v=v + def _set(i, self, v): self.v[i] = v + a,b = add_props(lambda i,x: x.v[i], _set) + +t = _T([0,2]) +test_eq(t.a,0) +test_eq(t.b,2) +t.a = t.a+1 +t.b = 3 +test_eq(t.a,1) +test_eq(t.b,3) +``` + +------------------------------------------------------------------------ + +source + +### str2bool + +> str2bool (s) + +*Case-insensitive convert string `s` too a bool +(`y`,`yes`,`t`,[`true`](https://fastcore.fast.ai/basics.html#true),`on`,`1`-\>`True`)* + +True values are ‘y’, ‘yes’, ‘t’, ‘true’, ‘on’, and ‘1’; false values are +‘n’, ‘no’, ‘f’, ‘false’, ‘off’, and ‘0’. Raises `ValueError` if ‘val’ is +anything else. + +``` python +for o in "y YES t True on 1".split(): assert str2bool(o) +for o in "n no FALSE off 0".split(): assert not str2bool(o) +for o in 0,None,'',False: assert not str2bool(o) +for o in 1,True: assert str2bool(o) +``` + +------------------------------------------------------------------------ + +source + +### str2int + +> str2int (s) + +*Convert `s` to an `int`* + +------------------------------------------------------------------------ + +source + +### str2float + +> str2float (s:str) + +*Convert `s` to a float* + +------------------------------------------------------------------------ + +source + +### str2list + +> str2list (s:str) + +*Convert `s` to a list* + +------------------------------------------------------------------------ + +source + +### str2date + +> str2date (s:str) + +*`date.fromisoformat` with empty string handling* + +------------------------------------------------------------------------ + +source + +### to_date + +> to_date (arg) + +------------------------------------------------------------------------ + +source + +### to_list + +> to_list (arg) + +------------------------------------------------------------------------ + +source + +### to_float + +> to_float (arg) + +------------------------------------------------------------------------ + +source + +### to_int + +> to_int (arg) + +------------------------------------------------------------------------ + +source + +### to_bool + +> to_bool (arg) + +------------------------------------------------------------------------ + +source + +### typed + +> typed (_func=None, cast=False) + +*Decorator to check param and return types at runtime, with optional +casting* + +[`typed`](https://fastcore.fast.ai/basics.html#typed) validates argument +types at **runtime**. This is in contrast to +[MyPy](http://mypy-lang.org/) which only offers static type checking. + +For example, a `TypeError` will be raised if we try to pass an integer +into the first argument of the below function: + +``` python +@typed +def discount(price:int, pct:float) -> float: + return (1-pct) * price + +with ExceptionExpected(TypeError): discount(100.0, .1) +``` + +You can have automatic casting based on heuristics by specifying +`typed(cast=True)`. If casting is not possible, a `TypeError` is raised. + +``` python +@typed(cast=True) +def discount(price:int, pct:float) -> float: + return (1-pct) * price + +assert 90.0 == discount(100.5, .1) # will auto cast 100.5 to the int 100 +assert 90.0 == discount(' 100 ', .1) # will auto cast the str "100" to the int 100 +with ExceptionExpected(TypeError): discount("a", .1) +``` + +We can also optionally allow multiple types by enumarating the types in +a tuple as illustrated below: + +``` python +@typed +def discount(price:int|float, pct:float): + return (1-pct) * price + +assert 90.0 == discount(100.0, .1) + +@typed(cast=True) +def discount(price:int|None, pct:float): + return (1-pct) * price + +assert 90.0 == discount(100.0, .1) +``` + +We currently do not support union types when casting. + +``` python +@typed(cast=True) +def discount(price:int|float, pct:float): + return (1-pct) * price + +with ExceptionExpected(AssertionError): assert 90.0 == discount("100.0", .1) +``` + +[`typed`](https://fastcore.fast.ai/basics.html#typed) works with +classes, too: + +``` python +class Foo: + @typed + def __init__(self, a:int, b: int, c:str): pass + @typed(cast=True) + def test(cls, d:str): return d + +with ExceptionExpected(TypeError): Foo(1, 2, 3) +assert isinstance(Foo(1,2, 'a string').test(10), str) +``` + +It also works with custom types. + +``` python +@typed +def test_foo(foo: Foo): pass + +with ExceptionExpected(TypeError): test_foo(1) +test_foo(Foo(1, 2, 'a string')) +``` + +``` python +class Bar: + @typed + def __init__(self, a:int): self.a = a +@typed(cast=True) +def test_bar(bar: Bar): return bar + +assert isinstance(test_bar(1), Bar) +test_eq(test_bar(1).a, 1) +with ExceptionExpected(TypeError): test_bar("foobar") +``` + +------------------------------------------------------------------------ + +source + +### exec_new + +> exec_new (code) + +*Execute `code` in a new environment and return it* + +``` python +g = exec_new('a=1') +test_eq(g['a'], 1) +``` + +------------------------------------------------------------------------ + +source + +### exec_import + +> exec_import (mod, sym) + +*Import `sym` from `mod` in a new environment* + +## Notebook functions + +------------------------------------------------------------------------ + +### ipython_shell + +> ipython_shell () + +*Same as `get_ipython` but returns `False` if not in IPython* + +------------------------------------------------------------------------ + +### in_ipython + +> in_ipython () + +*Check if code is running in some kind of IPython environment* + +------------------------------------------------------------------------ + +### in_colab + +> in_colab () + +*Check if the code is running in Google Colaboratory* + +------------------------------------------------------------------------ + +### in_jupyter + +> in_jupyter () + +*Check if the code is running in a jupyter notebook* + +------------------------------------------------------------------------ + +### in_notebook + +> in_notebook () + +*Check if the code is running in a jupyter notebook* + +These variables are available as booleans in `fastcore.basics` as +`IN_IPYTHON`, `IN_JUPYTER`, `IN_COLAB` and `IN_NOTEBOOK`. + +``` python +IN_IPYTHON, IN_JUPYTER, IN_COLAB, IN_NOTEBOOK +``` + + (True, True, False, True)# Foundation + + + +## Foundational Functions + +------------------------------------------------------------------------ + +source + +### working_directory + +> working_directory (path) + +*Change working directory to `path` and return to previous on exit.* + +------------------------------------------------------------------------ + +source + +### add_docs + +> add_docs (cls, cls_doc=None, **docs) + +*Copy values from +[`docs`](https://fastcore.fast.ai/foundation.html#docs) to `cls` +docstrings, and confirm all public methods are documented* + +[`add_docs`](https://fastcore.fast.ai/foundation.html#add_docs) allows +you to add docstrings to a class and its associated methods. This +function allows you to group docstrings together seperate from your +code, which enables you to define one-line functions as well as organize +your code more succintly. We believe this confers a number of benefits +which we discuss in [our style +guide](https://docs.fast.ai/dev/style.html). + +Suppose you have the following undocumented class: + +``` python +class T: + def foo(self): pass + def bar(self): pass +``` + +You can add documentation to this class like so: + +``` python +add_docs(T, cls_doc="A docstring for the class.", + foo="The foo method.", + bar="The bar method.") +``` + +Now, docstrings will appear as expected: + +``` python +test_eq(T.__doc__, "A docstring for the class.") +test_eq(T.foo.__doc__, "The foo method.") +test_eq(T.bar.__doc__, "The bar method.") +``` + +[`add_docs`](https://fastcore.fast.ai/foundation.html#add_docs) also +validates that all of your public methods contain a docstring. If one of +your methods is not documented, it will raise an error: + +``` python +class T: + def foo(self): pass + def bar(self): pass + +f=lambda: add_docs(T, "A docstring for the class.", foo="The foo method.") +test_fail(f, contains="Missing docs") +``` + +------------------------------------------------------------------------ + +source + +### docs + +> docs (cls) + +*Decorator version of +[`add_docs`](https://fastcore.fast.ai/foundation.html#add_docs), using +`_docs` dict* + +Instead of using +[`add_docs`](https://fastcore.fast.ai/foundation.html#add_docs), you can +use the decorator +[`docs`](https://fastcore.fast.ai/foundation.html#docs) as shown below. +Note that the docstring for the class can be set with the argument +`cls_doc`: + +``` python +@docs +class _T: + def f(self): pass + def g(cls): pass + + _docs = dict(cls_doc="The class docstring", + f="The docstring for method f.", + g="A different docstring for method g.") + + +test_eq(_T.__doc__, "The class docstring") +test_eq(_T.f.__doc__, "The docstring for method f.") +test_eq(_T.g.__doc__, "A different docstring for method g.") +``` + +For either the [`docs`](https://fastcore.fast.ai/foundation.html#docs) +decorator or the +[`add_docs`](https://fastcore.fast.ai/foundation.html#add_docs) +function, you can still define your docstrings in the normal way. Below +we set the docstring for the class as usual, but define the method +docstrings through the `_docs` attribute: + +``` python +@docs +class _T: + "The class docstring" + def f(self): pass + _docs = dict(f="The docstring for method f.") + + +test_eq(_T.__doc__, "The class docstring") +test_eq(_T.f.__doc__, "The docstring for method f.") +``` + +------------------------------------------------------------------------ + +### is_iter + +> is_iter (o) + +*Test whether `o` can be used in a `for` loop* + +``` python +assert is_iter([1]) +assert not is_iter(array(1)) +assert is_iter(array([1,2])) +assert (o for o in range(3)) +``` + +------------------------------------------------------------------------ + +source + +### coll_repr + +> coll_repr (c, max_n=10) + +*String repr of up to `max_n` items of (possibly lazy) collection `c`* + +[`coll_repr`](https://fastcore.fast.ai/foundation.html#coll_repr) is +used to provide a more informative +[`__repr__`](https://stackoverflow.com/questions/1984162/purpose-of-pythons-repr) +about list-like objects. +[`coll_repr`](https://fastcore.fast.ai/foundation.html#coll_repr) and is +used by [`L`](https://fastcore.fast.ai/foundation.html#l) to build a +`__repr__` that displays the length of a list in addition to a preview +of a list. + +Below is an example of the `__repr__` string created for a list of 1000 +elements: + +``` python +test_eq(coll_repr(range(1000)), '(#1000) [0,1,2,3,4,5,6,7,8,9...]') +test_eq(coll_repr(range(1000), 5), '(#1000) [0,1,2,3,4...]') +test_eq(coll_repr(range(10), 5), '(#10) [0,1,2,3,4...]') +test_eq(coll_repr(range(5), 5), '(#5) [0,1,2,3,4]') +``` + +We can set the option `max_n` to optionally preview a specified number +of items instead of the default: + +``` python +test_eq(coll_repr(range(1000), max_n=5), '(#1000) [0,1,2,3,4...]') +``` + +------------------------------------------------------------------------ + +source + +### is_bool + +> is_bool (x) + +*Check whether `x` is a bool or None* + +------------------------------------------------------------------------ + +source + +### mask2idxs + +> mask2idxs (mask) + +*Convert bool mask or index list to index +[`L`](https://fastcore.fast.ai/foundation.html#l)* + +``` python +test_eq(mask2idxs([False,True,False,True]), [1,3]) +test_eq(mask2idxs(array([False,True,False,True])), [1,3]) +test_eq(mask2idxs(array([1,2,3])), [1,2,3]) +``` + +------------------------------------------------------------------------ + +source + +### cycle + +> cycle (o) + +*Like `itertools.cycle` except creates list of `None`s if `o` is empty* + +``` python +test_eq(itertools.islice(cycle([1,2,3]),5), [1,2,3,1,2]) +test_eq(itertools.islice(cycle([]),3), [None]*3) +test_eq(itertools.islice(cycle(None),3), [None]*3) +test_eq(itertools.islice(cycle(1),3), [1,1,1]) +``` + +------------------------------------------------------------------------ + +source + +### zip_cycle + +> zip_cycle (x, *args) + +*Like `itertools.zip_longest` but +[`cycle`](https://fastcore.fast.ai/foundation.html#cycle)s through +elements of all but first argument* + +``` python +test_eq(zip_cycle([1,2,3,4],list('abc')), [(1, 'a'), (2, 'b'), (3, 'c'), (4, 'a')]) +``` + +------------------------------------------------------------------------ + +source + +### is_indexer + +> is_indexer (idx) + +*Test whether `idx` will index a single item in a list* + +You can, for example index a single item in a list with an integer or a +0-dimensional numpy array: + +``` python +assert is_indexer(1) +assert is_indexer(np.array(1)) +``` + +However, you cannot index into single item in a list with another list +or a numpy array with ndim \> 0. + +``` python +assert not is_indexer([1, 2]) +assert not is_indexer(np.array([[1, 2], [3, 4]])) +``` + +## [`L`](https://fastcore.fast.ai/foundation.html#l) helpers + +------------------------------------------------------------------------ + +source + +### CollBase + +> CollBase (items) + +*Base class for composing a list of `items`* + +`ColBase` is a base class that emulates the functionality of a python +`list`: + +``` python +class _T(CollBase): pass +l = _T([1,2,3,4,5]) + +test_eq(len(l), 5) # __len__ +test_eq(l[-1], 5); test_eq(l[0], 1) #__getitem__ +l[2] = 100; test_eq(l[2], 100) # __set_item__ +del l[0]; test_eq(len(l), 4) # __delitem__ +test_eq(str(l), '[2, 100, 4, 5]') # __repr__ +``` + +------------------------------------------------------------------------ + +source + +### L + +> L (x=None, *args, **kwargs) + +*Behaves like a list of `items` but can also index with list of indices +or masks* + +[`L`](https://fastcore.fast.ai/foundation.html#l) is a drop in +replacement for a python `list`. Inspired by +[NumPy](http://www.numpy.org/), +[`L`](https://fastcore.fast.ai/foundation.html#l), supports advanced +indexing and has additional methods (outlined below) that provide +additional functionality and encourage simple expressive code. For +example, the code below takes a list of pairs, selects the second item +of each pair, takes its absolute value, filters items greater than 4, +and adds them up: + +``` python +from fastcore.utils import gt +``` + +``` python +d = dict(a=1,b=-5,d=6,e=9).items() +test_eq(L(d).itemgot(1).map(abs).filter(gt(4)).sum(), 20) # abs(-5) + abs(6) + abs(9) = 20; 1 was filtered out. +``` + +Read [this overview section](https://fastcore.fast.ai/tour.html#L) for a +quick tutorial of [`L`](https://fastcore.fast.ai/foundation.html#l), as +well as background on the name. + +You can create an [`L`](https://fastcore.fast.ai/foundation.html#l) from +an existing iterable (e.g. a list, range, etc) and access or modify it +with an int list/tuple index, mask, int, or slice. All `list` methods +can also be used with [`L`](https://fastcore.fast.ai/foundation.html#l). + +``` python +t = L(range(12)) +test_eq(t, list(range(12))) +test_ne(t, list(range(11))) +t.reverse() +test_eq(t[0], 11) +t[3] = "h" +test_eq(t[3], "h") +t[3,5] = ("j","k") +test_eq(t[3,5], ["j","k"]) +test_eq(t, L(t)) +test_eq(L(L(1,2),[3,4]), ([1,2],[3,4])) +t +``` + + (#12) [11,10,9,'j',7,'k',5,4,3,2...] + +Any [`L`](https://fastcore.fast.ai/foundation.html#l) is a `Sequence` so +you can use it with methods like `random.sample`: + +``` python +assert isinstance(t, Sequence) +``` + +``` python +import random +``` + +``` python +random.seed(0) +random.sample(t, 3) +``` + + [5, 0, 11] + +There are optimized indexers for arrays, tensors, and DataFrames. + +``` python +import pandas as pd +``` + +``` python +arr = np.arange(9).reshape(3,3) +t = L(arr, use_list=None) +test_eq(t[1,2], arr[[1,2]]) + +df = pd.DataFrame({'a':[1,2,3]}) +t = L(df, use_list=None) +test_eq(t[1,2], L(pd.DataFrame({'a':[2,3]}, index=[1,2]), use_list=None)) +``` + +You can also modify an [`L`](https://fastcore.fast.ai/foundation.html#l) +with `append`, `+`, and `*`. + +``` python +t = L() +test_eq(t, []) +t.append(1) +test_eq(t, [1]) +t += [3,2] +test_eq(t, [1,3,2]) +t = t + [4] +test_eq(t, [1,3,2,4]) +t = 5 + t +test_eq(t, [5,1,3,2,4]) +test_eq(L(1,2,3), [1,2,3]) +test_eq(L(1,2,3), L(1,2,3)) +t = L(1)*5 +t = t.map(operator.neg) +test_eq(t,[-1]*5) +test_eq(~L([True,False,False]), L([False,True,True])) +t = L(range(4)) +test_eq(zip(t, L(1).cycle()), zip(range(4),(1,1,1,1))) +t = L.range(100) +test_shuffled(t,t.shuffle()) +``` + +``` python +test_eq(L([]).sum(), 0) +test_eq(L([]).product(), 1) +``` + +``` python +def _f(x,a=0): return x+a +t = L(1)*5 +test_eq(t.map(_f), t) +test_eq(t.map(_f,1), [2]*5) +test_eq(t.map(_f,a=2), [3]*5) +``` + +An [`L`](https://fastcore.fast.ai/foundation.html#l) can be constructed +from anything iterable, although tensors and arrays will not be iterated +over on construction, unless you pass `use_list` to the constructor. + +``` python +test_eq(L([1,2,3]),[1,2,3]) +test_eq(L(L([1,2,3])),[1,2,3]) +test_ne(L([1,2,3]),[1,2,]) +test_eq(L('abc'),['abc']) +test_eq(L(range(0,3)),[0,1,2]) +test_eq(L(o for o in range(0,3)),[0,1,2]) +test_eq(L(array(0)),[array(0)]) +test_eq(L([array(0),array(1)]),[array(0),array(1)]) +test_eq(L(array([0.,1.1]))[0],array([0.,1.1])) +test_eq(L(array([0.,1.1]), use_list=True), [array(0.),array(1.1)]) # `use_list=True` to unwrap arrays/arrays +``` + +If `match` is not `None` then the created list is same len as `match`, +either by: + +- If `len(items)==1` then `items` is replicated, +- Otherwise an error is raised if `match` and `items` are not already + the same size. + +``` python +test_eq(L(1,match=[1,2,3]),[1,1,1]) +test_eq(L([1,2],match=[2,3]),[1,2]) +test_fail(lambda: L([1,2],match=[1,2,3])) +``` + +If you create an [`L`](https://fastcore.fast.ai/foundation.html#l) from +an existing [`L`](https://fastcore.fast.ai/foundation.html#l) then +you’ll get back the original object (since +[`L`](https://fastcore.fast.ai/foundation.html#l) uses the +[`NewChkMeta`](https://fastcore.fast.ai/meta.html#newchkmeta) +metaclass). + +``` python +test_is(L(t), t) +``` + +An [`L`](https://fastcore.fast.ai/foundation.html#l) is considred equal +to a list if they have the same elements. It’s never considered equal to +a `str` a `set` or a `dict` even if they have the same elements/keys. + +``` python +test_eq(L(['a', 'b']), ['a', 'b']) +test_ne(L(['a', 'b']), 'ab') +test_ne(L(['a', 'b']), {'a':1, 'b':2}) +``` + +### [`L`](https://fastcore.fast.ai/foundation.html#l) Methods + +------------------------------------------------------------------------ + +source + +### L.\_\_getitem\_\_ + +> L.__getitem__ (idx) + +*Retrieve `idx` (can be list of indices, or mask, or int) items* + +``` python +t = L(range(12)) +test_eq(t[1,2], [1,2]) # implicit tuple +test_eq(t[[1,2]], [1,2]) # list +test_eq(t[:3], [0,1,2]) # slice +test_eq(t[[False]*11 + [True]], [11]) # mask +test_eq(t[array(3)], 3) +``` + +------------------------------------------------------------------------ + +source + +### L.\_\_setitem\_\_ + +> L.__setitem__ (idx, o) + +*Set `idx` (can be list of indices, or mask, or int) items to `o` (which +is broadcast if not iterable)* + +``` python +t[4,6] = 0 +test_eq(t[4,6], [0,0]) +t[4,6] = [1,2] +test_eq(t[4,6], [1,2]) +``` + +------------------------------------------------------------------------ + +source + +### L.unique + +> L.unique (sort=False, bidir=False, start=None) + +*Unique items, in stable order* + +``` python +test_eq(L(4,1,2,3,4,4).unique(), [4,1,2,3]) +``` + +------------------------------------------------------------------------ + +source + +### L.val2idx + +> L.val2idx () + +*Dict from value to index* + +``` python +test_eq(L(1,2,3).val2idx(), {3:2,1:0,2:1}) +``` + +------------------------------------------------------------------------ + +source + +### L.filter + +> L.filter (f=, negate=False, **kwargs) + +*Create new [`L`](https://fastcore.fast.ai/foundation.html#l) filtered +by predicate `f`, passing `args` and `kwargs` to `f`* + +``` python +list(t) +``` + + [0, 1, 2, 3, 1, 5, 2, 7, 8, 9, 10, 11] + +``` python +test_eq(t.filter(lambda o:o<5), [0,1,2,3,1,2]) +test_eq(t.filter(lambda o:o<5, negate=True), [5,7,8,9,10,11]) +``` + +------------------------------------------------------------------------ + +source + +### L.argwhere + +> L.argwhere (f, negate=False, **kwargs) + +*Like `filter`, but return indices for matching items* + +``` python +test_eq(t.argwhere(lambda o:o<5), [0,1,2,3,4,6]) +``` + +------------------------------------------------------------------------ + +source + +### L.argfirst + +> L.argfirst (f, negate=False) + +*Return index of first matching item* + +``` python +test_eq(t.argfirst(lambda o:o>4), 5) +test_eq(t.argfirst(lambda o:o>4,negate=True),0) +``` + +------------------------------------------------------------------------ + +source + +### L.map + +> L.map (f, *args, **kwargs) + +*Create new [`L`](https://fastcore.fast.ai/foundation.html#l) with `f` +applied to all `items`, passing `args` and `kwargs` to `f`* + +``` python +test_eq(L.range(4).map(operator.neg), [0,-1,-2,-3]) +``` + +If `f` is a string then it is treated as a format string to create the +mapping: + +``` python +test_eq(L.range(4).map('#{}#'), ['#0#','#1#','#2#','#3#']) +``` + +If `f` is a dictionary (or anything supporting `__getitem__`) then it is +indexed to create the mapping: + +``` python +test_eq(L.range(4).map(list('abcd')), list('abcd')) +``` + +You can also pass the same `arg` params that +[`bind`](https://fastcore.fast.ai/basics.html#bind) accepts: + +``` python +def f(a=None,b=None): return b +test_eq(L.range(4).map(f, b=arg0), range(4)) +``` + +------------------------------------------------------------------------ + +source + +### L.map_dict + +> L.map_dict (f=, *args, **kwargs) + +*Like `map`, but creates a dict from `items` to function results* + +``` python +test_eq(L(range(1,5)).map_dict(), {1:1, 2:2, 3:3, 4:4}) +test_eq(L(range(1,5)).map_dict(operator.neg), {1:-1, 2:-2, 3:-3, 4:-4}) +``` + +------------------------------------------------------------------------ + +source + +### L.zip + +> L.zip (cycled=False) + +*Create new [`L`](https://fastcore.fast.ai/foundation.html#l) with +`zip(*items)`* + +``` python +t = L([[1,2,3],'abc']) +test_eq(t.zip(), [(1, 'a'),(2, 'b'),(3, 'c')]) +``` + +``` python +t = L([[1,2,3,4],['a','b','c']]) +test_eq(t.zip(cycled=True ), [(1, 'a'),(2, 'b'),(3, 'c'),(4, 'a')]) +test_eq(t.zip(cycled=False), [(1, 'a'),(2, 'b'),(3, 'c')]) +``` + +------------------------------------------------------------------------ + +source + +### L.map_zip + +> L.map_zip (f, *args, cycled=False, **kwargs) + +*Combine `zip` and `starmap`* + +``` python +t = L([1,2,3],[2,3,4]) +test_eq(t.map_zip(operator.mul), [2,6,12]) +``` + +------------------------------------------------------------------------ + +source + +### L.zipwith + +> L.zipwith (*rest, cycled=False) + +*Create new [`L`](https://fastcore.fast.ai/foundation.html#l) with +`self` zip with each of `*rest`* + +``` python +b = [[0],[1],[2,2]] +t = L([1,2,3]).zipwith(b) +test_eq(t, [(1,[0]), (2,[1]), (3,[2,2])]) +``` + +------------------------------------------------------------------------ + +source + +### L.map_zipwith + +> L.map_zipwith (f, *rest, cycled=False, **kwargs) + +*Combine `zipwith` and `starmap`* + +``` python +test_eq(L(1,2,3).map_zipwith(operator.mul, [2,3,4]), [2,6,12]) +``` + +------------------------------------------------------------------------ + +source + +### L.itemgot + +> L.itemgot (*idxs) + +*Create new [`L`](https://fastcore.fast.ai/foundation.html#l) with item +`idx` of all `items`* + +``` python +test_eq(t.itemgot(1), b) +``` + +------------------------------------------------------------------------ + +source + +### L.attrgot + +> L.attrgot (k, default=None) + +*Create new [`L`](https://fastcore.fast.ai/foundation.html#l) with attr +`k` (or value `k` for dicts) of all `items`.* + +``` python +# Example when items are not a dict +a = [SimpleNamespace(a=3,b=4),SimpleNamespace(a=1,b=2)] +test_eq(L(a).attrgot('b'), [4,2]) + +#Example of when items are a dict +b =[{'id': 15, 'name': 'nbdev'}, {'id': 17, 'name': 'fastcore'}] +test_eq(L(b).attrgot('id'), [15, 17]) +``` + +------------------------------------------------------------------------ + +source + +### L.sorted + +> L.sorted (key=None, reverse=False) + +*New [`L`](https://fastcore.fast.ai/foundation.html#l) sorted by `key`. +If key is str use `attrgetter`; if int use `itemgetter`* + +``` python +test_eq(L(a).sorted('a').attrgot('b'), [2,4]) +``` + +------------------------------------------------------------------------ + +source + +### L.split + +> L.split (s, sep=None, maxsplit=-1) + +*Class Method: Same as `str.split`, but returns an +[`L`](https://fastcore.fast.ai/foundation.html#l)* + +``` python +test_eq(L.split('a b c'), list('abc')) +``` + +------------------------------------------------------------------------ + +source + +### L.range + +> L.range (a, b=None, step=None) + +*Class Method: Same as `range`, but returns +[`L`](https://fastcore.fast.ai/foundation.html#l). Can pass collection +for `a`, to use `len(a)`* + +``` python +test_eq_type(L.range([1,1,1]), L(range(3))) +test_eq_type(L.range(5,2,2), L(range(5,2,2))) +``` + +------------------------------------------------------------------------ + +source + +### L.concat + +> L.concat () + +*Concatenate all elements of list* + +``` python +test_eq(L([0,1,2,3],4,L(5,6)).concat(), range(7)) +``` + +------------------------------------------------------------------------ + +source + +### L.copy + +> L.copy () + +*Same as `list.copy`, but returns an +[`L`](https://fastcore.fast.ai/foundation.html#l)* + +``` python +t = L([0,1,2,3],4,L(5,6)).copy() +test_eq(t.concat(), range(7)) +``` + +------------------------------------------------------------------------ + +source + +### L.map_first + +> L.map_first (f=, g=, *args, **kwargs) + +*First element of `map_filter`* + +``` python +t = L(0,1,2,3) +test_eq(t.map_first(lambda o:o*2 if o>2 else None), 6) +``` + +------------------------------------------------------------------------ + +source + +### L.setattrs + +> L.setattrs (attr, val) + +*Call `setattr` on all items* + +``` python +t = L(SimpleNamespace(),SimpleNamespace()) +t.setattrs('foo', 'bar') +test_eq(t.attrgot('foo'), ['bar','bar']) +``` + +## Config + +------------------------------------------------------------------------ + +source + +### save_config_file + +> save_config_file (file, d, **kwargs) + +*Write settings dict to a new config file, or overwrite the existing +one.* + +------------------------------------------------------------------------ + +source + +### read_config_file + +> read_config_file (file, **kwargs) + +Config files are saved and read using Python’s +`configparser.ConfigParser`, inside the `DEFAULT` section. + +``` python +_d = dict(user='fastai', lib_name='fastcore', some_path='test', some_bool=True, some_num=3) +try: + save_config_file('tmp.ini', _d) + res = read_config_file('tmp.ini') +finally: os.unlink('tmp.ini') +dict(res) +``` + + {'user': 'fastai', + 'lib_name': 'fastcore', + 'some_path': 'test', + 'some_bool': 'True', + 'some_num': '3'} + +------------------------------------------------------------------------ + +source + +### Config + +> Config (cfg_path, cfg_name, create=None, save=True, extra_files=None, +> types=None) + +*Reading and writing `ConfigParser` ini files* + +[`Config`](https://fastcore.fast.ai/foundation.html#config) is a +convenient wrapper around `ConfigParser` ini files with a single section +(`DEFAULT`). + +Instantiate a +[`Config`](https://fastcore.fast.ai/foundation.html#config) from an ini +file at `cfg_path/cfg_name`: + +``` python +save_config_file('../tmp.ini', _d) +try: cfg = Config('..', 'tmp.ini') +finally: os.unlink('../tmp.ini') +cfg +``` + + {'user': 'fastai', 'lib_name': 'fastcore', 'some_path': 'test', 'some_bool': 'True', 'some_num': '3'} + +You can create a new file if one doesn’t exist by providing a `create` +dict: + +``` python +try: cfg = Config('..', 'tmp.ini', create=_d) +finally: os.unlink('../tmp.ini') +cfg +``` + + {'user': 'fastai', 'lib_name': 'fastcore', 'some_path': 'test', 'some_bool': 'True', 'some_num': '3'} + +If you additionally pass `save=False`, the +[`Config`](https://fastcore.fast.ai/foundation.html#config) will contain +the items from `create` without writing a new file: + +``` python +cfg = Config('..', 'tmp.ini', create=_d, save=False) +test_eq(cfg.user,'fastai') +assert not Path('../tmp.ini').exists() +``` + +------------------------------------------------------------------------ + +source + +### Config.get + +> Config.get (k, default=None) + +Keys can be accessed as attributes, items, or with `get` and an optional +default: + +``` python +test_eq(cfg.user,'fastai') +test_eq(cfg['some_path'], 'test') +test_eq(cfg.get('foo','bar'),'bar') +``` + +Extra files can be read *before* `cfg_path/cfg_name` using +`extra_files`, in the order they appear: + +``` python +with tempfile.TemporaryDirectory() as d: + a = Config(d, 'a.ini', {'a':0,'b':0}) + b = Config(d, 'b.ini', {'a':1,'c':0}) + c = Config(d, 'c.ini', {'a':2,'d':0}, extra_files=[a.config_file,b.config_file]) + test_eq(c.d, {'a':'2','b':'0','c':'0','d':'0'}) +``` + +If you pass a dict `types`, then the values of that dict will be used as +types to instantiate all values returned. `Path` is a special case – in +that case, the path returned will be relative to the path containing the +config file (assuming the value is relative). `bool` types use +[`str2bool`](https://fastcore.fast.ai/basics.html#str2bool) to convert +to boolean. + +``` python +_types = dict(some_path=Path, some_bool=bool, some_num=int) +cfg = Config('..', 'tmp.ini', create=_d, save=False, types=_types) + +test_eq(cfg.user,'fastai') +test_eq(cfg['some_path'].resolve(), (Path('..')/'test').resolve()) +test_eq(cfg.get('some_num'), 3) +``` + +------------------------------------------------------------------------ + +source + +### Config.find + +> Config.find (cfg_name, cfg_path=None, **kwargs) + +*Search `cfg_path` and its parents to find `cfg_name`* + +You can use +[`Config.find`](https://fastcore.fast.ai/foundation.html#config.find) to +search subdirectories for a config file, starting in the current path if +no path is specified: + +``` python +Config.find('settings.ini').repo +``` + + 'fastcore'# Utility functions + + + +## File Functions + +Utilities (other than extensions to Pathlib.Path) for dealing with IO. + +------------------------------------------------------------------------ + +source + +### walk + +> walk (path:pathlib.Path|str, symlinks:bool=True, keep_file: infunctioncallable>=, keep_folder: infunctioncallable>=, skip_folder: infunctioncallable>=, func: infunctioncallable>=, ret_folders:bool=False) + +*Generator version of `os.walk`, using functions to filter files and +folders* + + ++++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TypeDefaultDetails
pathpathlib.Path | strpath to start searching
symlinksboolTruefollow symlinks?
keep_filecallableret_truefunction that returns True for wanted files
keep_foldercallableret_truefunction that returns True for folders to enter
skip_foldercallableret_falsefunction that returns True for folders to skip
funccallablejoinfunction to apply to each matched file
ret_foldersboolFalsereturn folders, not just files
+ +------------------------------------------------------------------------ + +source + +### globtastic + +> globtastic (path:pathlib.Path|str, recursive:bool=True, +> symlinks:bool=True, file_glob:str=None, file_re:str=None, +> folder_re:str=None, skip_file_glob:str=None, +> skip_file_re:str=None, skip_folder_re:str=None, func: infunctioncallable>=, ret_folders:bool=False) + +*A more powerful `glob`, including regex matches, symlink handling, and +skip parameters* + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TypeDefaultDetails
pathpathlib.Path | strpath to start searching
recursiveboolTruesearch subfolders
symlinksboolTruefollow symlinks?
file_globstrNoneOnly include files matching glob
file_restrNoneOnly include files matching regex
folder_restrNoneOnly enter folders matching regex
skip_file_globstrNoneSkip files matching glob
skip_file_restrNoneSkip files matching regex
skip_folder_restrNoneSkip folders matching regex,
funccallablejoinfunction to apply to each matched file
ret_foldersboolFalsereturn folders, not just files
ReturnsLPaths to matched files
+ +``` python +globtastic('.', skip_folder_re='^[_.]', folder_re='core', file_glob='*.*py*', file_re='c') +``` + + (#5) ['./fastcore/docments.py','./fastcore/dispatch.py','./fastcore/basics.py','./fastcore/docscrape.py','./fastcore/script.py'] + +------------------------------------------------------------------------ + +source + +### maybe_open + +> maybe_open (f, mode='r', **kwargs) + +*Context manager: open `f` if it is a path (and close on exit)* + +This is useful for functions where you want to accept a path *or* file. +[`maybe_open`](https://fastcore.fast.ai/xtras.html#maybe_open) will not +close your file handle if you pass one in. + +``` python +def _f(fn): + with maybe_open(fn) as f: return f.encoding + +fname = '00_test.ipynb' +sys_encoding = 'cp1252' if sys.platform == 'win32' else 'UTF-8' +test_eq(_f(fname), sys_encoding) +with open(fname) as fh: test_eq(_f(fh), sys_encoding) +``` + +For example, we can use this to reimplement +[`imghdr.what`](https://docs.python.org/3/library/imghdr.html#imghdr.what) +from the Python standard library, which is [written in Python +3.9](https://github.com/python/cpython/blob/3.9/Lib/imghdr.py#L11) as: + +``` python +from fastcore import imghdr +``` + +``` python +def what(file, h=None): + f = None + try: + if h is None: + if isinstance(file, (str,os.PathLike)): + f = open(file, 'rb') + h = f.read(32) + else: + location = file.tell() + h = file.read(32) + file.seek(location) + for tf in imghdr.tests: + res = tf(h, f) + if res: return res + finally: + if f: f.close() + return None +``` + +Here’s an example of the use of this function: + +``` python +fname = 'images/puppy.jpg' +what(fname) +``` + + 'jpeg' + +With [`maybe_open`](https://fastcore.fast.ai/xtras.html#maybe_open), +`Self`, and +[`L.map_first`](https://fastcore.fast.ai/foundation.html#l.map_first), +we can rewrite this in a much more concise and (in our opinion) clear +way: + +``` python +def what(file, h=None): + if h is None: + with maybe_open(file, 'rb') as f: h = f.peek(32) + return L(imghdr.tests).map_first(Self(h,file)) +``` + +…and we can check that it still works: + +``` python +test_eq(what(fname), 'jpeg') +``` + +…along with the version passing a file handle: + +``` python +with open(fname,'rb') as f: test_eq(what(f), 'jpeg') +``` + +…along with the `h` parameter version: + +``` python +with open(fname,'rb') as f: test_eq(what(None, h=f.read(32)), 'jpeg') +``` + +------------------------------------------------------------------------ + +source + +### mkdir + +> mkdir (path, exist_ok=False, parents=False, overwrite=False, **kwargs) + +*Creates and returns a directory defined by `path`, optionally removing +previous existing directory if `overwrite` is `True`* + +``` python +with tempfile.TemporaryDirectory() as d: + path = Path(os.path.join(d, 'new_dir')) + new_dir = mkdir(path) + assert new_dir.exists() + test_eq(new_dir, path) + + # test overwrite + with open(new_dir/'test.txt', 'w') as f: f.writelines('test') + test_eq(len(list(walk(new_dir))), 1) # assert file is present + new_dir = mkdir(new_dir, overwrite=True) + test_eq(len(list(walk(new_dir))), 0) # assert file was deleted +``` + +------------------------------------------------------------------------ + +source + +### image_size + +> image_size (fn) + +*Tuple of (w,h) for png, gif, or jpg; `None` otherwise* + +``` python +test_eq(image_size(fname), (1200,803)) +``` + +------------------------------------------------------------------------ + +source + +### bunzip + +> bunzip (fn) + +*bunzip `fn`, raising exception if output already exists* + +``` python +f = Path('files/test.txt') +if f.exists(): f.unlink() +bunzip('files/test.txt.bz2') +t = f.open().readlines() +test_eq(len(t),1) +test_eq(t[0], 'test\n') +f.unlink() +``` + +------------------------------------------------------------------------ + +source + +### loads + +> loads (s, **kw) + +*Same as `json.loads`, but handles `None`* + +------------------------------------------------------------------------ + +source + +### loads_multi + +> loads_multi (s:str) + +*Generator of \>=0 decoded json dicts, possibly with non-json ignored +text at start and end* + +``` python +tst = """ +# ignored +{ "a":1 } +hello +{ +"b":2 +} +""" + +test_eq(list(loads_multi(tst)), [{'a': 1}, {'b': 2}]) +``` + +------------------------------------------------------------------------ + +source + +### dumps + +> dumps (obj, **kw) + +*Same as `json.dumps`, but uses `ujson` if available* + +------------------------------------------------------------------------ + +source + +### untar_dir + +> untar_dir (fname, dest, rename=False, overwrite=False) + +*untar `file` into `dest`, creating a directory if the root contains +more than one item* + +``` python +def test_untar(foldername, rename=False, **kwargs): + with tempfile.TemporaryDirectory() as d: + nm = os.path.join(d, 'a') + shutil.make_archive(nm, 'gztar', **kwargs) + with tempfile.TemporaryDirectory() as d2: + d2 = Path(d2) + untar_dir(nm+'.tar.gz', d2, rename=rename) + test_eq(d2.ls(), [d2/foldername]) +``` + +If the contents of `fname` contain just one file or directory, it is +placed directly in `dest`: + +``` python +# using `base_dir` in `make_archive` results in `images` directory included in file names +test_untar('images', base_dir='images') +``` + +If `rename` then the directory created is named based on the archive, +without extension: + +``` python +test_untar('a', base_dir='images', rename=True) +``` + +If the contents of `fname` contain multiple files and directories, a new +folder in `dest` is created with the same name as `fname` (but without +extension): + +``` python +# using `root_dir` in `make_archive` results in `images` directory *not* included in file names +test_untar('a', root_dir='images') +``` + +------------------------------------------------------------------------ + +source + +### repo_details + +> repo_details (url) + +*Tuple of `owner,name` from ssh or https git repo `url`* + +``` python +test_eq(repo_details('https://github.com/fastai/fastai.git'), ['fastai', 'fastai']) +test_eq(repo_details('git@github.com:fastai/nbdev.git\n'), ['fastai', 'nbdev']) +``` + +------------------------------------------------------------------------ + +source + +### run + +> run (cmd, *rest, same_in_win=False, ignore_ex=False, as_bytes=False, +> stderr=False) + +*Pass `cmd` (splitting with `shlex` if string) to `subprocess.run`; +return `stdout`; raise `IOError` if fails* + +You can pass a string (which will be split based on standard shell +rules), a list, or pass args directly: + +``` python +run('echo', same_in_win=True) +run('pip', '--version', same_in_win=True) +run(['pip', '--version'], same_in_win=True) +``` + + 'pip 23.3.1 from /Users/jhoward/miniconda3/lib/python3.11/site-packages/pip (python 3.11)' + +``` python +if sys.platform == 'win32': + assert 'ipynb' in run('cmd /c dir /p') + assert 'ipynb' in run(['cmd', '/c', 'dir', '/p']) + assert 'ipynb' in run('cmd', '/c', 'dir', '/p') +else: + assert 'ipynb' in run('ls -ls') + assert 'ipynb' in run(['ls', '-l']) + assert 'ipynb' in run('ls', '-l') +``` + +Some commands fail in non-error situations, like `grep`. Use `ignore_ex` +in those cases, which will return a tuple of stdout and returncode: + +``` python +if sys.platform == 'win32': + test_eq(run('cmd /c findstr asdfds 00_test.ipynb', ignore_ex=True)[0], 1) +else: + test_eq(run('grep asdfds 00_test.ipynb', ignore_ex=True)[0], 1) +``` + +[`run`](https://fastcore.fast.ai/xtras.html#run) automatically decodes +returned bytes to a `str`. Use `as_bytes` to skip that: + +``` python +if sys.platform == 'win32': + test_eq(run('cmd /c echo hi'), 'hi') +else: + test_eq(run('echo hi', as_bytes=True), b'hi\n') +``` + +------------------------------------------------------------------------ + +source + +### open_file + +> open_file (fn, mode='r', **kwargs) + +*Open a file, with optional compression if gz or bz2 suffix* + +------------------------------------------------------------------------ + +source + +### save_pickle + +> save_pickle (fn, o) + +*Save a pickle file, to a file name or opened file* + +------------------------------------------------------------------------ + +source + +### load_pickle + +> load_pickle (fn) + +*Load a pickle file from a file name or opened file* + +``` python +for suf in '.pkl','.bz2','.gz': + # delete=False is added for Windows + # https://stackoverflow.com/questions/23212435/permission-denied-to-write-to-my-temporary-file + with tempfile.NamedTemporaryFile(suffix=suf, delete=False) as f: + fn = Path(f.name) + save_pickle(fn, 't') + t = load_pickle(fn) + f.close() + test_eq(t,'t') +``` + +------------------------------------------------------------------------ + +source + +### parse_env + +> parse_env (s:str=None, fn:Union[str,pathlib.Path]=None) + +*Parse a shell-style environment string or file* + +``` python +testf = """# comment + # another comment + export FOO="bar#baz" +BAR=thing # comment "ok" + baz='thong' +QUX=quux +export ZAP = "zip" # more comments + FOOBAR = 42 # trailing space and comment""" + +exp = dict(FOO='bar#baz', BAR='thing', baz='thong', QUX='quux', ZAP='zip', FOOBAR='42') + +test_eq(parse_env(testf), exp) +``` + +------------------------------------------------------------------------ + +source + +### expand_wildcards + +> expand_wildcards (code) + +*Expand all wildcard imports in the given code string.* + +``` python +inp = """from math import * +from os import * +from random import * +def func(): return sin(pi) + path.join('a', 'b') + randint(1, 10)""" + +exp = """from math import pi, sin +from os import path +from random import randint +def func(): return sin(pi) + path.join('a', 'b') + randint(1, 10)""" + +test_eq(expand_wildcards(inp), exp) + +inp = """from itertools import * +def func(): pass""" +test_eq(expand_wildcards(inp), inp) + +inp = """def outer(): + from math import * + def inner(): + from os import * + return sin(pi) + path.join('a', 'b')""" + +exp = """def outer(): + from math import pi, sin + def inner(): + from os import path + return sin(pi) + path.join('a', 'b')""" + +test_eq(expand_wildcards(inp), exp) +``` + +## Collections + +------------------------------------------------------------------------ + +source + +### dict2obj + +> dict2obj (d, list_func=, dict_func= 'fastcore.basics.AttrDict'>) + +*Convert (possibly nested) dicts (or lists of dicts) to +[`AttrDict`](https://fastcore.fast.ai/basics.html#attrdict)* + +This is a convenience to give you “dotted” access to (possibly nested) +dictionaries, e.g: + +``` python +d1 = dict(a=1, b=dict(c=2,d=3)) +d2 = dict2obj(d1) +test_eq(d2.b.c, 2) +test_eq(d2.b['c'], 2) +``` + +It can also be used on lists of dicts. + +``` python +_list_of_dicts = [d1, d1] +ds = dict2obj(_list_of_dicts) +test_eq(ds[0].b.c, 2) +``` + +------------------------------------------------------------------------ + +source + +### obj2dict + +> obj2dict (d) + +*Convert (possibly nested) AttrDicts (or lists of AttrDicts) to `dict`* + +[`obj2dict`](https://fastcore.fast.ai/xtras.html#obj2dict) can be used +to reverse what is done by +[`dict2obj`](https://fastcore.fast.ai/xtras.html#dict2obj): + +``` python +test_eq(obj2dict(d2), d1) +test_eq(obj2dict(ds), _list_of_dicts) +``` + +------------------------------------------------------------------------ + +source + +### repr_dict + +> repr_dict (d) + +*Print nested dicts and lists, such as returned by +[`dict2obj`](https://fastcore.fast.ai/xtras.html#dict2obj)* + +``` python +print(repr_dict(d2)) +``` + + - a: 1 + - b: + - c: 2 + - d: 3 + +------------------------------------------------------------------------ + +source + +### is_listy + +> is_listy (x) + +*`isinstance(x, (tuple,list,L,slice,Generator))`* + +``` python +assert is_listy((1,)) +assert is_listy([1]) +assert is_listy(L([1])) +assert is_listy(slice(2)) +assert not is_listy(array([1])) +``` + +------------------------------------------------------------------------ + +source + +### mapped + +> mapped (f, it) + +*map `f` over `it`, unless it’s not listy, in which case return `f(it)`* + +``` python +def _f(x,a=1): return x-a + +test_eq(mapped(_f,1),0) +test_eq(mapped(_f,[1,2]),[0,1]) +test_eq(mapped(_f,(1,)),(0,)) +``` + +## Extensions to Pathlib.Path + +The following methods are added to the standard python libary +[Pathlib.Path](https://docs.python.org/3/library/pathlib.html#basic-use). + +------------------------------------------------------------------------ + +source + +### Path.readlines + +> Path.readlines (hint=-1, encoding='utf8') + +*Read the content of `self`* + +------------------------------------------------------------------------ + +source + +### Path.read_json + +> Path.read_json (encoding=None, errors=None) + +*Same as `read_text` followed by +[`loads`](https://fastcore.fast.ai/xtras.html#loads)* + +------------------------------------------------------------------------ + +source + +### Path.mk_write + +> Path.mk_write (data, encoding=None, errors=None, mode=511) + +*Make all parent dirs of `self`, and write `data`* + +------------------------------------------------------------------------ + +source + +### Path.relpath + +> Path.relpath (start=None) + +*Same as `os.path.relpath`, but returns a `Path`, and resolves symlinks* + +``` python +p = Path('../fastcore/').resolve() +p +``` + + Path('/Users/daniel.roy.greenfeld/fh/fastcore/fastcore') + +``` python +p.relpath(Path.cwd()) +``` + + Path('../fastcore') + +------------------------------------------------------------------------ + +source + +### Path.ls + +> Path.ls (n_max=None, file_type=None, file_exts=None) + +*Contents of path as a list* + +We add an `ls()` method to `pathlib.Path` which is simply defined as +`list(Path.iterdir())`, mainly for convenience in REPL environments such +as notebooks. + +``` python +path = Path() +t = path.ls() +assert len(t)>0 +t1 = path.ls(10) +test_eq(len(t1), 10) +t2 = path.ls(file_exts='.ipynb') +assert len(t)>len(t2) +t[0] +``` + + Path('000_tour.ipynb') + +You can also pass an optional `file_type` MIME prefix and/or a list of +file extensions. + +``` python +lib_path = (path/'../fastcore') +txt_files=lib_path.ls(file_type='text') +assert len(txt_files) > 0 and txt_files[0].suffix=='.py' +ipy_files=path.ls(file_exts=['.ipynb']) +assert len(ipy_files) > 0 and ipy_files[0].suffix=='.ipynb' +txt_files[0],ipy_files[0] +``` + + (Path('../fastcore/shutil.py'), Path('000_tour.ipynb')) + +------------------------------------------------------------------------ + +source + +### Path.\_\_repr\_\_ + +> Path.__repr__ () + +*Return repr(self).* + +fastai also updates the `repr` of `Path` such that, if `Path.BASE_PATH` +is defined, all paths are printed relative to that path (as long as they +are contained in `Path.BASE_PATH`: + +``` python +t = ipy_files[0].absolute() +try: + Path.BASE_PATH = t.parent.parent + test_eq(repr(t), f"Path('nbs/{t.name}')") +finally: Path.BASE_PATH = None +``` + +------------------------------------------------------------------------ + +source + +### Path.delete + +> Path.delete () + +*Delete a file, symlink, or directory tree* + +## Reindexing Collections + +------------------------------------------------------------------------ + +source + +#### ReindexCollection + +> ReindexCollection (coll, idxs=None, cache=None, tfm=) + +*Reindexes collection `coll` with indices `idxs` and optional LRU cache +of size `cache`* + +This is useful when constructing batches or organizing data in a +particular manner (i.e. for deep learning). This class is primarly used +in organizing data for language models in fastai. + +You can supply a custom index upon instantiation with the `idxs` +argument, or you can call the `reindex` method to supply a new index for +your collection. + +Here is how you can reindex a list such that the elements are reversed: + +``` python +rc=ReindexCollection(['a', 'b', 'c', 'd', 'e'], idxs=[4,3,2,1,0]) +list(rc) +``` + + ['e', 'd', 'c', 'b', 'a'] + +Alternatively, you can use the `reindex` method: + +------------------------------------------------------------------------ + +source + +###### ReindexCollection.reindex + +> ReindexCollection.reindex (idxs) + +*Replace `self.idxs` with idxs* + +``` python +rc=ReindexCollection(['a', 'b', 'c', 'd', 'e']) +rc.reindex([4,3,2,1,0]) +list(rc) +``` + + ['e', 'd', 'c', 'b', 'a'] + +You can optionally specify a LRU cache, which uses +[functools.lru_cache](https://docs.python.org/3/library/functools.html#functools.lru_cache) +upon instantiation: + +``` python +sz = 50 +t = ReindexCollection(L.range(sz), cache=2) + +#trigger a cache hit by indexing into the same element multiple times +t[0], t[0] +t._get.cache_info() +``` + + CacheInfo(hits=1, misses=1, maxsize=2, currsize=1) + +You can optionally clear the LRU cache by calling the `cache_clear` +method: + +------------------------------------------------------------------------ + +source + +##### ReindexCollection.cache_clear + +> ReindexCollection.cache_clear () + +*Clear LRU cache* + +``` python +sz = 50 +t = ReindexCollection(L.range(sz), cache=2) + +#trigger a cache hit by indexing into the same element multiple times +t[0], t[0] +t.cache_clear() +t._get.cache_info() +``` + + CacheInfo(hits=0, misses=0, maxsize=2, currsize=0) + +------------------------------------------------------------------------ + +source + +##### ReindexCollection.shuffle + +> ReindexCollection.shuffle () + +*Randomly shuffle indices* + +Note that an ordered index is automatically constructed for the data +structure even if one is not supplied. + +``` python +rc=ReindexCollection(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']) +rc.shuffle() +list(rc) +``` + + ['a', 'd', 'h', 'c', 'e', 'b', 'f', 'g'] + +``` python +sz = 50 +t = ReindexCollection(L.range(sz), cache=2) +test_eq(list(t), range(sz)) +test_eq(t[sz-1], sz-1) +test_eq(t._get.cache_info().hits, 1) +t.shuffle() +test_eq(t._get.cache_info().hits, 1) +test_ne(list(t), range(sz)) +test_eq(set(t), set(range(sz))) +t.cache_clear() +test_eq(t._get.cache_info().hits, 0) +test_eq(t.count(0), 1) +``` + +## Other Helpers + +------------------------------------------------------------------------ + +source + +### get_source_link + +> get_source_link (func) + +*Return link to `func` in source code* + +[`get_source_link`](https://fastcore.fast.ai/xtras.html#get_source_link) +allows you get a link to source code related to an object. For +[nbdev](https://github.com/fastai/nbdev) related projects such as +fastcore, we can get the full link to a GitHub repo. For `nbdev` +projects, be sure to properly set the `git_url` in `settings.ini` +(derived from `lib_name` and `branch` on top of the prefix you will need +to adapt) so that those links are correct. + +For example, below we get the link to +[`fastcore.test.test_eq`](https://fastcore.fast.ai/test.html#test_eq): + +``` python +from fastcore.test import test_eq +``` + +``` python +assert 'fastcore/test.py' in get_source_link(test_eq) +assert get_source_link(test_eq).startswith('https://github.com/fastai/fastcore') +get_source_link(test_eq) +``` + + 'https://github.com/fastai/fastcore/tree/master/fastcore/test.py#L35' + +------------------------------------------------------------------------ + +source + +### truncstr + +> truncstr (s:str, maxlen:int, suf:str='…', space='') + +*Truncate `s` to length `maxlen`, adding suffix `suf` if truncated* + +``` python +w = 'abacadabra' +test_eq(truncstr(w, 10), w) +test_eq(truncstr(w, 5), 'abac…') +test_eq(truncstr(w, 5, suf=''), 'abaca') +test_eq(truncstr(w, 11, space='_'), w+"_") +test_eq(truncstr(w, 10, space='_'), w[:-1]+'…') +test_eq(truncstr(w, 5, suf='!!'), 'aba!!') +``` + +------------------------------------------------------------------------ + +source + +### sparkline + +> sparkline (data, mn=None, mx=None, empty_zero=False) + +*Sparkline for `data`, with `None`s (and zero, if `empty_zero`) shown as +empty column* + +``` python +data = [9,6,None,1,4,0,8,15,10] +print(f'without "empty_zero": {sparkline(data, empty_zero=False)}') +print(f' with "empty_zero": {sparkline(data, empty_zero=True )}') +``` + + without "empty_zero": ▅▂ ▁▂▁▃▇▅ + with "empty_zero": ▅▂ ▁▂ ▃▇▅ + +You can set a maximum and minimum for the y-axis of the sparkline with +the arguments `mn` and `mx` respectively: + +``` python +sparkline([1,2,3,400], mn=0, mx=3) +``` + + '▂▅▇▇' + +------------------------------------------------------------------------ + +source + +### modify_exception + +> modify_exception (e:Exception, msg:str=None, replace:bool=False) + +*Modifies `e` with a custom message attached* + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TypeDefaultDetails
eExceptionAn exception
msgstrNoneA custom message
replaceboolFalseWhether to replace e.args with [msg]
ReturnsException
+ +``` python +msg = "This is my custom message!" + +test_fail(lambda: (_ for _ in ()).throw(modify_exception(Exception(), None)), contains='') +test_fail(lambda: (_ for _ in ()).throw(modify_exception(Exception(), msg)), contains=msg) +test_fail(lambda: (_ for _ in ()).throw(modify_exception(Exception("The first message"), msg)), contains="The first message This is my custom message!") +test_fail(lambda: (_ for _ in ()).throw(modify_exception(Exception("The first message"), msg, True)), contains="This is my custom message!") +``` + +------------------------------------------------------------------------ + +source + +### round_multiple + +> round_multiple (x, mult, round_down=False) + +*Round `x` to nearest multiple of `mult`* + +``` python +test_eq(round_multiple(63,32), 64) +test_eq(round_multiple(50,32), 64) +test_eq(round_multiple(40,32), 32) +test_eq(round_multiple( 0,32), 0) +test_eq(round_multiple(63,32, round_down=True), 32) +test_eq(round_multiple((63,40),32), (64,32)) +``` + +------------------------------------------------------------------------ + +source + +### set_num_threads + +> set_num_threads (nt) + +*Get numpy (and others) to use `nt` threads* + +This sets the number of threads consistently for many tools, by: + +1. Set the following environment variables equal to `nt`: + `OPENBLAS_NUM_THREADS`,`NUMEXPR_NUM_THREADS`,`OMP_NUM_THREADS`,`MKL_NUM_THREADS` +2. Sets `nt` threads for numpy and pytorch. + +------------------------------------------------------------------------ + +source + +### join_path_file + +> join_path_file (file, path, ext='') + +*Return `path/file` if file is a string or a `Path`, file otherwise* + +``` python +path = Path.cwd()/'_tmp'/'tst' +f = join_path_file('tst.txt', path) +assert path.exists() +test_eq(f, path/'tst.txt') +with open(f, 'w') as f_: assert join_path_file(f_, path) == f_ +shutil.rmtree(Path.cwd()/'_tmp') +``` + +------------------------------------------------------------------------ + +source + +### autostart + +> autostart (g) + +*Decorator that automatically starts a generator* + +------------------------------------------------------------------------ + +source + +#### EventTimer + +> EventTimer (store=5, span=60) + +*An event timer with history of `store` items of time `span`* + +Add events with `add`, and get number of `events` and their frequency +(`freq`). + +``` python +# Random wait function for testing +def _randwait(): yield from (sleep(random.random()/200) for _ in range(100)) + +c = EventTimer(store=5, span=0.03) +for o in _randwait(): c.add(1) +print(f'Num Events: {c.events}, Freq/sec: {c.freq:.01f}') +print('Most recent: ', sparkline(c.hist), *L(c.hist).map('{:.01f}')) +``` + + Num Events: 3, Freq/sec: 205.6 + Most recent: ▁▁▃▁▇ 254.1 263.2 284.5 259.9 315.7 + +------------------------------------------------------------------------ + +source + +### stringfmt_names + +> stringfmt_names (s:str) + +*Unique brace-delimited names in `s`* + +``` python +s = '/pulls/{pull_number}/reviews/{review_id}' +test_eq(stringfmt_names(s), ['pull_number','review_id']) +``` + +------------------------------------------------------------------------ + +source + +#### PartialFormatter + +> PartialFormatter () + +*A `string.Formatter` that doesn’t error on missing fields, and tracks +missing fields and unused args* + +------------------------------------------------------------------------ + +source + +### partial_format + +> partial_format (s:str, **kwargs) + +*string format `s`, ignoring missing field errors, returning missing and +extra fields* + +The result is a tuple of +`(formatted_string,missing_fields,extra_fields)`, e.g: + +``` python +res,missing,xtra = partial_format(s, pull_number=1, foo=2) +test_eq(res, '/pulls/1/reviews/{review_id}') +test_eq(missing, ['review_id']) +test_eq(xtra, {'foo':2}) +``` + +------------------------------------------------------------------------ + +source + +### utc2local + +> utc2local (dt:datetime.datetime) + +*Convert `dt` from UTC to local time* + +``` python +dt = datetime(2000,1,1,12) +print(f'{dt} UTC is {utc2local(dt)} local time') +``` + + 2000-01-01 12:00:00 UTC is 2000-01-01 22:00:00+10:00 local time + +------------------------------------------------------------------------ + +source + +### local2utc + +> local2utc (dt:datetime.datetime) + +*Convert `dt` from local to UTC time* + +``` python +print(f'{dt} local is {local2utc(dt)} UTC time') +``` + + 2000-01-01 12:00:00 local is 2000-01-01 02:00:00+00:00 UTC time + +------------------------------------------------------------------------ + +source + +### trace + +> trace (f) + +*Add `set_trace` to an existing function `f`* + +You can add a breakpoint to an existing function, e.g: + +``` python +Path.cwd = trace(Path.cwd) +Path.cwd() +``` + +Now, when the function is called it will drop you into the debugger. +Note, you must issue the `s` command when you begin to step into the +function that is being traced. + +------------------------------------------------------------------------ + +source + +### modified_env + +> modified_env (*delete, **replace) + +*Context manager temporarily modifying `os.environ` by deleting `delete` +and replacing `replace`* + +``` python +# USER isn't in Cloud Linux Environments +env_test = 'USERNAME' if sys.platform == "win32" else 'SHELL' +oldusr = os.environ[env_test] + +replace_param = {env_test: 'a'} +with modified_env('PATH', **replace_param): + test_eq(os.environ[env_test], 'a') + assert 'PATH' not in os.environ + +assert 'PATH' in os.environ +test_eq(os.environ[env_test], oldusr) +``` + +------------------------------------------------------------------------ + +source + +#### ContextManagers + +> ContextManagers (mgrs) + +*Wrapper for `contextlib.ExitStack` which enters a collection of context +managers* + +------------------------------------------------------------------------ + +source + +### shufflish + +> shufflish (x, pct=0.04) + +*Randomly relocate items of `x` up to `pct` of `len(x)` from their +starting location* + +------------------------------------------------------------------------ + +source + +### console_help + +> console_help (libname:str) + +*Show help for all console scripts from `libname`* + + + + + + + + + + + + + + + + +
TypeDetails
libnamestrname of library for console script listing
+ +------------------------------------------------------------------------ + +source + +### hl_md + +> hl_md (s, lang='xml', show=True) + +*Syntax highlight `s` using `lang`.* + +When we display code in a notebook, it’s nice to highlight it, so we +create a function to simplify that: + +``` python +hl_md('a child') +``` + +``` xml +a child +``` + +------------------------------------------------------------------------ + +source + +### type2str + +> type2str (typ:type) + +*Stringify `typ`* + +``` python +test_eq(type2str(Optional[float]), 'Union[float, None]') +``` + +------------------------------------------------------------------------ + +source + +### dataclass_src + +> dataclass_src (cls) + +``` python +DC = make_dataclass('DC', [('x', int), ('y', Optional[float], None), ('z', float, None)]) +print(dataclass_src(DC)) +``` + + @dataclass + class DC: + x: int + y: Union[float, None] = None + z: float = None + +------------------------------------------------------------------------ + +source + +### Unset + +> Unset (value, names=None, module=None, qualname=None, type=None, start=1) + +*An enumeration.* + +------------------------------------------------------------------------ + +source + +### nullable_dc + +> nullable_dc (cls) + +*Like `dataclass`, but default of `UNSET` added to fields without +defaults* + +``` python +@nullable_dc +class Person: name: str; age: int; city: str = "Unknown" +Person(name="Bob") +``` + + Person(name='Bob', age=UNSET, city='Unknown') + +------------------------------------------------------------------------ + +source + +### make_nullable + +> make_nullable (clas) + +``` python +@dataclass +class Person: name: str; age: int; city: str = "Unknown" + +make_nullable(Person) +Person("Bob", city='NY') +``` + + Person(name='Bob', age=UNSET, city='NY') + +``` python +Person(name="Bob") +``` + + Person(name='Bob', age=UNSET, city='Unknown') + +``` python +Person("Bob", 34) +``` + + Person(name='Bob', age=34, city='Unknown') + +------------------------------------------------------------------------ + +source + +### flexiclass + +> flexiclass (cls) + +*Convert `cls` into a `dataclass` like +[`make_nullable`](https://fastcore.fast.ai/xtras.html#make_nullable). +Converts in place and also returns the result.* + + + + + + + + + + + + + + + + + + + + + +
TypeDetails
clsThe class to convert
Returnsdataclass
+ +This can be used as a decorator… + +``` python +@flexiclass +class Person: name: str; age: int; city: str = "Unknown" + +bob = Person(name="Bob") +bob +``` + + Person(name='Bob', age=UNSET, city='Unknown') + +…or can update the behavior of an existing class (or dataclass): + +``` python +class Person: name: str; age: int; city: str = "Unknown" + +flexiclass(Person) +bob = Person(name="Bob") +bob +``` + + Person(name='Bob', age=UNSET, city='Unknown') + +Action occurs in-place: + +``` python +class Person: name: str; age: int; city: str = "Unknown" + +flexiclass(Person) +is_dataclass(Person) +``` + + True + +------------------------------------------------------------------------ + +source + +### asdict + +> asdict (o) + +*Convert `o` to a `dict`, supporting dataclasses, namedtuples, +iterables, and `__dict__` attrs.* + +Any `UNSET` values are not included. + +``` python +asdict(bob) +``` + + {'name': 'Bob', 'city': 'Unknown'} + +To customise dict conversion behavior for a class, implement the +`_asdict` method (this is used in the Python stdlib for named tuples). + +------------------------------------------------------------------------ + +source + +### is_typeddict + +> is_typeddict (cls:type) + +*Check if `cls` is a `TypedDict`* + +``` python +class MyDict(TypedDict): name:str + +assert is_typeddict(MyDict) +assert not is_typeddict({'a':1}) +``` + +------------------------------------------------------------------------ + +source + +### is_namedtuple + +> is_namedtuple (cls) + +*`True` if `cls` is a namedtuple type* + +``` python +assert is_namedtuple(namedtuple('tst', ['a'])) +assert not is_namedtuple(tuple) +``` + +------------------------------------------------------------------------ + +source + +### flexicache + +> flexicache (*funcs, maxsize=128) + +*Like `lru_cache`, but customisable with policy `funcs`* + +This is a flexible lru cache function that you can pass a list of +functions to. Those functions define the cache eviction policy. For +instance, +[`time_policy`](https://fastcore.fast.ai/xtras.html#time_policy) is +provided for time-based cache eviction, and +[`mtime_policy`](https://fastcore.fast.ai/xtras.html#mtime_policy) +evicts based on a file’s modified-time changing. The policy functions +are passed the last value that function returned was (initially `None`), +and return a new value to indicate the cache has expired. When the cache +expires, all functions are called with `None` to force getting new +values. + +------------------------------------------------------------------------ + +source + +### time_policy + +> time_policy (seconds) + +*A [`flexicache`](https://fastcore.fast.ai/xtras.html#flexicache) policy +that expires cached items after `seconds` have passed* + +------------------------------------------------------------------------ + +source + +### mtime_policy + +> mtime_policy (filepath) + +*A [`flexicache`](https://fastcore.fast.ai/xtras.html#flexicache) policy +that expires cached items after `filepath` modified-time changes* + +``` python +@flexicache(time_policy(10), mtime_policy('000_tour.ipynb')) +def cached_func(x, y): return x+y + +cached_func(1,2) +``` + + 3 + +``` python +@flexicache(time_policy(10), mtime_policy('000_tour.ipynb')) +async def cached_func(x, y): return x+y + +await cached_func(1,2) +await cached_func(1,2) +``` + + 3 + +------------------------------------------------------------------------ + +source + +### timed_cache + +> timed_cache (seconds=60, maxsize=128) + +*Like `lru_cache`, but also with time-based eviction* + +This function is a small convenience wrapper for using +[`flexicache`](https://fastcore.fast.ai/xtras.html#flexicache) with +[`time_policy`](https://fastcore.fast.ai/xtras.html#time_policy). + +``` python +@timed_cache(seconds=0.05, maxsize=2) +def cached_func(x): return x * 2, time() + +# basic caching +result1, time1 = cached_func(2) +test_eq(result1, 4) +sleep(0.001) +result2, time2 = cached_func(2) +test_eq(result2, 4) +test_eq(time1, time2) + +# caching different values +result3, _ = cached_func(3) +test_eq(result3, 6) + +# maxsize +_, time4 = cached_func(4) +_, time2_new = cached_func(2) +test_close(time2, time2_new, eps=0.1) +_, time3_new = cached_func(3) +test_ne(time3_new, time()) + +# time expiration +sleep(0.05) +_, time4_new = cached_func(4) +test_ne(time4_new, time()) +```
# Parallel + + + +``` python +from fastcore.test import * +from nbdev.showdoc import * +from fastcore.nb_imports import * +``` + +------------------------------------------------------------------------ + +source + +### threaded + +> threaded (process=False) + +*Run `f` in a `Thread` (or `Process` if `process=True`), and returns it* + +``` python +@threaded +def _1(): + time.sleep(0.05) + print("second") + return 5 + +@threaded +def _2(): + time.sleep(0.01) + print("first") + +a = _1() +_2() +time.sleep(0.1) +``` + + first + second + +After the thread is complete, the return value is stored in the `result` +attr. + +``` python +a.result +``` + + 5 + +------------------------------------------------------------------------ + +source + +### startthread + +> startthread (f) + +*Like [`threaded`](https://fastcore.fast.ai/parallel.html#threaded), but +start thread immediately* + +``` python +@startthread +def _(): + time.sleep(0.05) + print("second") + +@startthread +def _(): + time.sleep(0.01) + print("first") + +time.sleep(0.1) +``` + + first + second + +------------------------------------------------------------------------ + +source + +### startproc + +> startproc (f) + +*Like `threaded(True)`, but start Process immediately* + +``` python +@startproc +def _(): + time.sleep(0.05) + print("second") + +@startproc +def _(): + time.sleep(0.01) + print("first") + +time.sleep(0.1) +``` + + first + second + +------------------------------------------------------------------------ + +source + +### parallelable + +> parallelable (param_name, num_workers, f=None) + +------------------------------------------------------------------------ + +source + +#### ThreadPoolExecutor + +> ThreadPoolExecutor (max_workers=4, on_exc=, +> pause=0, **kwargs) + +*Same as Python’s ThreadPoolExecutor, except can pass `max_workers==0` +for serial execution* + +------------------------------------------------------------------------ + +source + +#### ProcessPoolExecutor + +> ProcessPoolExecutor (max_workers=4, on_exc=, +> pause=0, mp_context=None, initializer=None, +> initargs=()) + +*Same as Python’s ProcessPoolExecutor, except can pass `max_workers==0` +for serial execution* + +------------------------------------------------------------------------ + +source + +### parallel + +> parallel (f, items, *args, n_workers=4, total=None, progress=None, +> pause=0, method=None, threadpool=False, timeout=None, +> chunksize=1, **kwargs) + +*Applies `func` in parallel to `items`, using `n_workers`* + +``` python +inp,exp = range(50),range(1,51) + +test_eq(parallel(_add_one, inp, n_workers=2), exp) +test_eq(parallel(_add_one, inp, threadpool=True, n_workers=2), exp) +test_eq(parallel(_add_one, inp, n_workers=1, a=2), range(2,52)) +test_eq(parallel(_add_one, inp, n_workers=0), exp) +test_eq(parallel(_add_one, inp, n_workers=0, a=2), range(2,52)) +``` + +Use the `pause` parameter to ensure a pause of `pause` seconds between +processes starting. This is in case there are race conditions in +starting some process, or to stagger the time each process starts, for +example when making many requests to a webserver. Set `threadpool=True` +to use +[`ThreadPoolExecutor`](https://fastcore.fast.ai/parallel.html#threadpoolexecutor) +instead of +[`ProcessPoolExecutor`](https://fastcore.fast.ai/parallel.html#processpoolexecutor). + +``` python +from datetime import datetime +``` + +``` python +def print_time(i): + time.sleep(random.random()/1000) + print(i, datetime.now()) + +parallel(print_time, range(5), n_workers=2, pause=0.25); +``` + + 0 2024-10-11 23:06:05.920741 + 1 2024-10-11 23:06:06.171470 + 2 2024-10-11 23:06:06.431925 + 3 2024-10-11 23:06:06.689940 + 4 2024-10-11 23:06:06.937109 + +------------------------------------------------------------------------ + +source + +### parallel_async + +> parallel_async (f, items, *args, n_workers=16, timeout=None, chunksize=1, +> on_exc=, **kwargs) + +*Applies `f` to `items` in parallel using asyncio and a semaphore to +limit concurrency.* + +``` python +import asyncio +``` + +``` python +async def print_time_async(i): + wait = random.random() + await asyncio.sleep(wait) + print(i, datetime.now(), wait) + +await parallel_async(print_time_async, range(6), n_workers=3); +``` + + 0 2024-10-11 23:06:39.545583 0.10292732609738675 + 3 2024-10-11 23:06:39.900393 0.3516179734831676 + 4 2024-10-11 23:06:39.941094 0.03699593757956876 + 2 2024-10-11 23:06:39.957677 0.5148658606540902 + 1 2024-10-11 23:06:40.099716 0.6574035385815227 + 5 2024-10-11 23:06:40.654097 0.7116319667399102 + +------------------------------------------------------------------------ + +source + +### run_procs + +> run_procs (f, f_done, args) + +*Call `f` for each item in `args` in parallel, yielding `f_done`* + +------------------------------------------------------------------------ + +source + +### parallel_gen + +> parallel_gen (cls, items, n_workers=4, **kwargs) + +*Instantiate `cls` in `n_workers` procs & call each on a subset of +`items` in parallel.* + +``` python +# class _C: +# def __call__(self, o): return ((i+1) for i in o) + +# items = range(5) + +# res = L(parallel_gen(_C, items, n_workers=0)) +# idxs,dat1 = zip(*res.sorted(itemgetter(0))) +# test_eq(dat1, range(1,6)) + +# res = L(parallel_gen(_C, items, n_workers=3)) +# idxs,dat2 = zip(*res.sorted(itemgetter(0))) +# test_eq(dat2, dat1) +``` + +`cls` is any class with `__call__`. It will be passed `args` and +`kwargs` when initialized. Note that `n_workers` instances of `cls` are +created, one in each process. `items` are then split in `n_workers` +batches and one is sent to each `cls`. The function then returns a +generator of tuples of item indices and results. + +``` python +class TestSleepyBatchFunc: + "For testing parallel processes that run at different speeds" + def __init__(self): self.a=1 + def __call__(self, batch): + for k in batch: + time.sleep(random.random()/4) + yield k+self.a + +x = np.linspace(0,0.99,20) + +res = L(parallel_gen(TestSleepyBatchFunc, x, n_workers=2)) +test_eq(res.sorted().itemgot(1), x+1) +``` + + + +``` python +# #|hide +# from subprocess import Popen, PIPE +# # test num_workers > 0 in scripts works when python process start method is spawn +# process = Popen(["python", "parallel_test.py"], stdout=PIPE) +# _, err = process.communicate(timeout=10) +# exit_code = process.wait() +# test_eq(exit_code, 0) +```# Network functionality + + + +``` python +from fastcore.test import * +from nbdev.showdoc import * +from fastcore.nb_imports import * +``` + +## URLs + +------------------------------------------------------------------------ + +source + +### urlquote + +> urlquote (url) + +*Update url’s path with `urllib.parse.quote`* + +``` python +urlquote("https://github.com/fastai/fastai/compare/master@{1.day.ago}…master") +``` + + 'https://github.com/fastai/fastai/compare/master@%7B1.day.ago%7D%E2%80%A6master' + +``` python +urlquote("https://www.google.com/search?q=你好") +``` + + 'https://www.google.com/search?q=%E4%BD%A0%E5%A5%BD' + +------------------------------------------------------------------------ + +source + +### urlwrap + +> urlwrap (url, data=None, headers=None) + +*Wrap `url` in a urllib `Request` with +[`urlquote`](https://fastcore.fast.ai/net.html#urlquote)* + +------------------------------------------------------------------------ + +source + +#### HTTP4xxClientError + +> HTTP4xxClientError (url, code, msg, hdrs, fp) + +*Base class for client exceptions (code 4xx) from `url*` functions* + +------------------------------------------------------------------------ + +source + +#### HTTP5xxServerError + +> HTTP5xxServerError (url, code, msg, hdrs, fp) + +*Base class for server exceptions (code 5xx) from `url*` functions* + +------------------------------------------------------------------------ + +source + +### urlopener + +> urlopener () + +------------------------------------------------------------------------ + +source + +### urlopen + +> urlopen (url, data=None, headers=None, timeout=None, **kwargs) + +*Like `urllib.request.urlopen`, but first +[`urlwrap`](https://fastcore.fast.ai/net.html#urlwrap) the `url`, and +encode `data`* + +With [`urlopen`](https://fastcore.fast.ai/net.html#urlopen), the body of +the response will also be returned in addition to the message if there +is an error: + +``` python +try: urlopen('https://api.github.com/v3') +except HTTPError as e: + print(e.code, e.msg) + assert 'documentation_url' in e.msg +``` + + 404 Not Found + ====Error Body==== + { + "message": "Not Found", + "documentation_url": "https://docs.github.com/rest" + } + +------------------------------------------------------------------------ + +source + +### urlread + +> urlread (url, data=None, headers=None, decode=True, return_json=False, +> return_headers=False, timeout=None, **kwargs) + +*Retrieve `url`, using `data` dict or `kwargs` to `POST` if present* + +------------------------------------------------------------------------ + +source + +### urljson + +> urljson (url, data=None, timeout=None) + +*Retrieve `url` and decode json* + +``` python +test_eq(urljson('https://httpbin.org/get')['headers']['User-Agent'], url_default_headers['User-Agent']) +``` + +------------------------------------------------------------------------ + +source + +### urlcheck + +> urlcheck (url, headers=None, timeout=10) + +------------------------------------------------------------------------ + +source + +### urlclean + +> urlclean (url) + +*Remove fragment, params, and querystring from `url` if present* + +``` python +test_eq(urlclean('http://a.com/b?c=1#d'), 'http://a.com/b') +``` + +------------------------------------------------------------------------ + +source + +### urlretrieve + +> urlretrieve (url, filename=None, reporthook=None, data=None, +> headers=None, timeout=None) + +*Same as `urllib.request.urlretrieve` but also works with `Request` +objects* + +------------------------------------------------------------------------ + +source + +### urldest + +> urldest (url, dest=None) + +------------------------------------------------------------------------ + +source + +### urlsave + +> urlsave (url, dest=None, reporthook=None, headers=None, timeout=None) + +*Retrieve `url` and save based on its name* + +``` python +#skip +with tempfile.TemporaryDirectory() as d: urlsave('http://www.google.com/index.html', d) +``` + +------------------------------------------------------------------------ + +source + +### urlvalid + +> urlvalid (x) + +*Test if `x` is a valid URL* + +``` python +assert urlvalid('http://www.google.com/') +assert not urlvalid('www.google.com/') +assert not urlvalid(1) +``` + +------------------------------------------------------------------------ + +source + +### urlrequest + +> urlrequest (url, verb, headers=None, route=None, query=None, data=None, +> json_data=True) + +*`Request` for `url` with optional route params replaced by `route`, +plus `query` string, and post `data`* + +``` python +hdr = {'Hdr1':'1', 'Hdr2':'2'} +req = urlrequest('http://example.com/{foo}/1', 'POST', + headers=hdr, route={'foo':'3'}, query={'q':'4'}, data={'d':'5'}) + +test_eq(req.headers, hdr) +test_eq(req.full_url, 'http://example.com/3/1?q=4') +test_eq(req.method, 'POST') +test_eq(req.data, b'{"d": "5"}') +``` + +``` python +req = urlrequest('http://example.com/{foo}/1', 'POST', data={'d':'5','e':'6'}, headers=hdr, json_data=False) +test_eq(req.data, b'd=5&e=6') +``` + +------------------------------------------------------------------------ + +source + +### Request.summary + +> Request.summary (skip=None) + +*Summary containing full_url, headers, method, and data, removing `skip` +from headers* + +``` python +req.summary(skip='Hdr1') +``` + + {'full_url': 'http://example.com/{foo}/1', + 'method': 'POST', + 'data': b'd=5&e=6', + 'headers': {'Hdr2': '2'}} + +------------------------------------------------------------------------ + +source + +### urlsend + +> urlsend (url, verb, headers=None, decode=True, route=None, query=None, +> data=None, json_data=True, return_json=True, +> return_headers=False, debug=None, timeout=None) + +*Send request with +[`urlrequest`](https://fastcore.fast.ai/net.html#urlrequest), converting +result to json if `return_json`* + +------------------------------------------------------------------------ + +source + +### do_request + +> do_request (url, post=False, headers=None, **data) + +*Call GET or json-encoded POST on `url`, depending on `post`* + +## Basic client/server + +------------------------------------------------------------------------ + +source + +### start_server + +> start_server (port, host=None, dgram=False, reuse_addr=True, +> n_queue=None) + +*Create a `socket` server on `port`, with optional `host`, of type +`dgram`* + +You can create a TCP client and server pass an int as `port` and +optional `host`. `host` defaults to your main network interface if not +provided. You can create a Unix socket client and server by passing a +string to `port`. A `SOCK_STREAM` socket is created by default, unless +you pass `dgram=True`, in which case a `SOCK_DGRAM` socket is created. +`n_queue` sets the listening queue size. + +------------------------------------------------------------------------ + +source + +### start_client + +> start_client (port, host=None, dgram=False) + +*Create a `socket` client on `port`, with optional `host`, of type +`dgram`* + +------------------------------------------------------------------------ + +source + +### tobytes + +> tobytes (s:str) + +*Convert `s` into HTTP-ready bytes format* + +``` python +test_eq(tobytes('foo\nbar'), b'foo\r\nbar') +``` + +------------------------------------------------------------------------ + +source + +### http_response + +> http_response (body=None, status=200, hdrs=None, **kwargs) + +*Create an HTTP-ready response, adding `kwargs` to `hdrs`* + +``` python +exp = b'HTTP/1.1 200 OK\r\nUser-Agent: me\r\nContent-Length: 4\r\n\r\nbody' +test_eq(http_response('body', 200, User_Agent='me'), exp) +``` + +------------------------------------------------------------------------ + +source + +### recv_once + +> recv_once (host:str='localhost', port:int=8000) + +*Spawn a thread to receive a single HTTP request and store in `d['r']`*# Docments + + + +[`docments`](https://fastcore.fast.ai/docments.html#docments) provides +programmatic access to comments in function parameters and return types. +It can be used to create more developer-friendly documentation, CLI, etc +tools. + +## Why? + +Without docments, if you want to document your parameters, you have to +repeat param names in docstrings, since they’re already in the function +signature. The parameters have to be kept synchronized in the two places +as you change your code. Readers of your code have to look back and +forth between two places to understand what’s happening. So it’s more +work for you, and for your users. + +Furthermore, to have parameter documentation formatted nicely without +docments, you have to use special magic docstring formatting, often with +[odd +quirks](https://stackoverflow.com/questions/62167540/why-do-definitions-have-a-space-before-the-colon-in-numpy-docstring-sections), +which is a pain to create and maintain, and awkward to read in code. For +instance, using [numpy-style +documentation](https://numpydoc.readthedocs.io/en/latest/format.html): + +``` python +def add_np(a:int, b:int=0)->int: + """The sum of two numbers. + + Used to demonstrate numpy-style docstrings. + +Parameters +---------- +a : int + the 1st number to add +b : int + the 2nd number to add (default: 0) + +Returns +------- +int + the result of adding `a` to `b`""" + return a+b +``` + +By comparison, here’s the same thing using docments: + +``` python +def add( + a:int, # the 1st number to add + b=0, # the 2nd number to add +)->int: # the result of adding `a` to `b` + "The sum of two numbers." + return a+b +``` + +## Numpy docstring helper functions + +[`docments`](https://fastcore.fast.ai/docments.html#docments) also +supports numpy-style docstrings, or a mix or numpy-style and docments +parameter documentation. The functions in this section help get and +parse this information. + +------------------------------------------------------------------------ + +source + +### docstring + +> docstring (sym) + +*Get docstring for `sym` for functions ad classes* + +``` python +test_eq(docstring(add), "The sum of two numbers.") +``` + +------------------------------------------------------------------------ + +source + +### parse_docstring + +> parse_docstring (sym) + +*Parse a numpy-style docstring in `sym`* + +``` python +# parse_docstring(add_np) +``` + +------------------------------------------------------------------------ + +source + +### isdataclass + +> isdataclass (s) + +*Check if `s` is a dataclass but not a dataclass’ instance* + +------------------------------------------------------------------------ + +source + +### get_dataclass_source + +> get_dataclass_source (s) + +*Get source code for dataclass `s`* + +------------------------------------------------------------------------ + +source + +### get_source + +> get_source (s) + +*Get source code for string, function object or dataclass `s`* + +------------------------------------------------------------------------ + +source + +### get_name + +> get_name (obj) + +*Get the name of `obj`* + +``` python +test_eq(get_name(in_ipython), 'in_ipython') +test_eq(get_name(L.map), 'map') +``` + +------------------------------------------------------------------------ + +source + +### qual_name + +> qual_name (obj) + +*Get the qualified name of `obj`* + +``` python +assert qual_name(docscrape) == 'fastcore.docscrape' +``` + +## Docments + +------------------------------------------------------------------------ + +source + +### docments + +> docments (elt, full=False, returns=True, eval_str=False) + +*Generates a `docment`* + +The returned `dict` has parameter names as keys, docments as values. The +return value comment appears in the `return`, unless `returns=False`. +Using the `add` definition above, we get: + +``` python +def add( + a:int, # the 1st number to add + b=0, # the 2nd number to add +)->int: # the result of adding `a` to `b` + "The sum of two numbers." + return a+b + +docments(add) +``` + +``` json +{ 'a': 'the 1st number to add', + 'b': 'the 2nd number to add', + 'return': 'the result of adding `a` to `b`'} +``` + +If you pass `full=True`, the values are `dict` of defaults, types, and +docments as values. Note that the type annotation is inferred from the +default value, if the annotation is empty and a default is supplied. + +``` python +docments(add, full=True) +``` + +``` json +{ 'a': { 'anno': , + 'default': , + 'docment': 'the 1st number to add'}, + 'b': { 'anno': , + 'default': 0, + 'docment': 'the 2nd number to add'}, + 'return': { 'anno': , + 'default': , + 'docment': 'the result of adding `a` to `b`'}} +``` + +To evaluate stringified annotations (from python 3.10), use `eval_str`: + +``` python +docments(add, full=True, eval_str=True)['a'] +``` + +``` json +{ 'anno': , + 'default': , + 'docment': 'the 1st number to add'} +``` + +If you need more space to document a parameter, place one or more lines +of comments above the parameter, or above the return type. You can +mix-and-match these docment styles: + +``` python +def add( + # The first operand + a:int, + # This is the second of the operands to the *addition* operator. + # Note that passing a negative value here is the equivalent of the *subtraction* operator. + b:int, +)->int: # The result is calculated using Python's builtin `+` operator. + "Add `a` to `b`" + return a+b +``` + +``` python +docments(add) +``` + +``` json +{ 'a': 'The first operand', + 'b': 'This is the second of the operands to the *addition* operator.\n' + 'Note that passing a negative value here is the equivalent of the ' + '*subtraction* operator.', + 'return': "The result is calculated using Python's builtin `+` operator."} +``` + +Docments works with async functions, too: + +``` python +async def add_async( + # The first operand + a:int, + # This is the second of the operands to the *addition* operator. + # Note that passing a negative value here is the equivalent of the *subtraction* operator. + b:int, +)->int: # The result is calculated using Python's builtin `+` operator. + "Add `a` to `b`" + return a+b +``` + +``` python +test_eq(docments(add_async), docments(add)) +``` + +You can also use docments with classes and methods: + +``` python +class Adder: + "An addition calculator" + def __init__(self, + a:int, # First operand + b:int, # 2nd operand + ): self.a,self.b = a,b + + def calculate(self + )->int: # Integral result of addition operator + "Add `a` to `b`" + return a+b +``` + +``` python +docments(Adder) +``` + +``` json +{'a': 'First operand', 'b': '2nd operand', 'return': None} +``` + +``` python +docments(Adder.calculate) +``` + +``` json +{'return': 'Integral result of addition operator', 'self': None} +``` + +docments can also be extracted from numpy-style docstrings: + +``` python +print(add_np.__doc__) +``` + + The sum of two numbers. + + Used to demonstrate numpy-style docstrings. + + Parameters + ---------- + a : int + the 1st number to add + b : int + the 2nd number to add (default: 0) + + Returns + ------- + int + the result of adding `a` to `b` + +``` python +docments(add_np) +``` + +``` json +{ 'a': 'the 1st number to add', + 'b': 'the 2nd number to add (default: 0)', + 'return': 'the result of adding `a` to `b`'} +``` + +You can even mix and match docments and numpy parameters: + +``` python +def add_mixed(a:int, # the first number to add + b + )->int: # the result + """The sum of two numbers. + +Parameters +---------- +b : int + the 2nd number to add (default: 0)""" + return a+b +``` + +``` python +docments(add_mixed, full=True) +``` + +``` json +{ 'a': { 'anno': , + 'default': , + 'docment': 'the first number to add'}, + 'b': { 'anno': 'int', + 'default': , + 'docment': 'the 2nd number to add (default: 0)'}, + 'return': { 'anno': , + 'default': , + 'docment': 'the result'}} +``` + +You can use docments with dataclasses, however if the class was defined +in online notebook, docments will not contain parameters’ comments. This +is because the source code is not available in the notebook. After +converting the notebook to a module, the docments will be available. +Thus, documentation will have correct parameters’ comments. + +Docments even works with +[`delegates`](https://fastcore.fast.ai/meta.html#delegates): + +``` python +from fastcore.meta import delegates +``` + +``` python +def _a(a:int=2): return a # First + +@delegates(_a) +def _b(b:str, **kwargs): return b, (_a(**kwargs)) # Second + +docments(_b) +``` + +``` json +{'a': 'First', 'b': 'Second', 'return': None} +``` + +## Extract docstrings + +------------------------------------------------------------------------ + +source + +### extract_docstrings + +> extract_docstrings (code) + +*Create a dict from function/class/method names to tuples of docstrings +and param lists* + +``` python +sample_code = """ +"This is a module." + +def top_func(a, b, *args, **kw): + "This is top-level." + pass + +class SampleClass: + "This is a class." + + def __init__(self, x, y): + "Constructor for SampleClass." + pass + + def method1(self, param1): + "This is method1." + pass + + def _private_method(self): + "This should not be included." + pass + +class AnotherClass: + def __init__(self, a, b): + "This class has no separate docstring." + pass""" + +exp = {'_module': ('This is a module.', ''), + 'top_func': ('This is top-level.', 'a, b, *args, **kw'), + 'SampleClass': ('This is a class.', 'self, x, y'), + 'SampleClass.method1': ('This is method1.', 'self, param1'), + 'AnotherClass': ('This class has no separate docstring.', 'self, a, b')} +test_eq(extract_docstrings(sample_code), exp) +```# Meta + + + +``` python +from fastcore.foundation import * +from nbdev.showdoc import * +from fastcore.nb_imports import * +``` + +See this [blog post](https://realpython.com/python-metaclasses/) for +more information about metaclasses. + +- [`FixSigMeta`](https://fastcore.fast.ai/meta.html#fixsigmeta) + preserves information that enables [intropsection of + signatures](https://www.python.org/dev/peps/pep-0362/#:~:text=Python%20has%20always%20supported%20powerful,fully%20reconstruct%20the%20function's%20signature.) + (i.e. tab completion in IDEs) when certain types of inheritence would + otherwise obfuscate this introspection. +- [`PrePostInitMeta`](https://fastcore.fast.ai/meta.html#prepostinitmeta) + ensures that the classes defined with it run `__pre_init__` and + `__post_init__` (without having to write `self.__pre_init__()` and + `self.__post_init__()` in the actual `init` +- [`NewChkMeta`](https://fastcore.fast.ai/meta.html#newchkmeta) gives + the + [`PrePostInitMeta`](https://fastcore.fast.ai/meta.html#prepostinitmeta) + functionality and ensures classes defined with it don’t re-create an + object of their type whenever it’s passed to the constructor +- [`BypassNewMeta`](https://fastcore.fast.ai/meta.html#bypassnewmeta) + ensures classes defined with it can easily be casted form objects they + subclass. + +------------------------------------------------------------------------ + +source + +### test_sig + +> test_sig (f, b) + +*Test the signature of an object* + +``` python +def func_1(h,i,j): pass +def func_2(h,i=3, j=[5,6]): pass + +class T: + def __init__(self, a, b): pass + +test_sig(func_1, '(h, i, j)') +test_sig(func_2, '(h, i=3, j=[5, 6])') +test_sig(T, '(a, b)') +``` + +------------------------------------------------------------------------ + +source + +### FixSigMeta + +> FixSigMeta (name, bases, dict) + +*A metaclass that fixes the signature on classes that override +`__new__`* + +When you inherit from a class that defines `__new__`, or a metaclass +that defines `__call__`, the signature of your `__init__` method is +obfuscated such that tab completion no longer works. +[`FixSigMeta`](https://fastcore.fast.ai/meta.html#fixsigmeta) fixes this +issue and restores signatures. + +To understand what +[`FixSigMeta`](https://fastcore.fast.ai/meta.html#fixsigmeta) does, it +is useful to inspect an object’s signature. You can inspect the +signature of an object with `inspect.signature`: + +``` python +class T: + def __init__(self, a, b, c): pass + +inspect.signature(T) +``` + + + +This corresponds to tab completion working in the normal way: + +Tab completion in a Jupyter Notebook. + +However, when you inherhit from a class that defines `__new__` or a +metaclass that defines `__call__` this obfuscates the signature by +overriding your class with the signature of `__new__`, which prevents +tab completion from displaying useful information: + +``` python +class Foo: + def __new__(self, **args): pass + +class Bar(Foo): + def __init__(self, d, e, f): pass + +inspect.signature(Bar) +``` + + + +Tab completion in a Jupyter Notebook. + +Finally, the signature and tab completion can be restored by inheriting +from the metaclass +[`FixSigMeta`](https://fastcore.fast.ai/meta.html#fixsigmeta) as shown +below: + +``` python +class Bar(Foo, metaclass=FixSigMeta): + def __init__(self, d, e, f): pass + +test_sig(Bar, '(d, e, f)') +inspect.signature(Bar) +``` + + + +Tab completion in a Jupyter Notebook. + +If you need to define a metaclass that overrides `__call__` (as done in +[`PrePostInitMeta`](https://fastcore.fast.ai/meta.html#prepostinitmeta)), +you need to inherit from +[`FixSigMeta`](https://fastcore.fast.ai/meta.html#fixsigmeta) instead of +`type` when constructing the metaclass to preserve the signature in +`__init__`. Be careful not to override `__new__` when doing this: + +``` python +class TestMeta(FixSigMeta): + # __new__ comes from FixSigMeta + def __call__(cls, *args, **kwargs): pass + +class T(metaclass=TestMeta): + def __init__(self, a, b): pass + +test_sig(T, '(a, b)') +``` + +On the other hand, if you fail to inherit from +[`FixSigMeta`](https://fastcore.fast.ai/meta.html#fixsigmeta) when +inheriting from a metaclass that overrides `__call__`, your signature +will reflect that of `__call__` instead (which is often undesirable): + +``` python +class GenericMeta(type): + "A boilerplate metaclass that doesn't do anything for testing." + def __new__(cls, name, bases, dict): + return super().__new__(cls, name, bases, dict) + def __call__(cls, *args, **kwargs): pass + +class T2(metaclass=GenericMeta): + def __init__(self, a, b): pass + +# We can avoid this by inheriting from the metaclass `FixSigMeta` +test_sig(T2, '(*args, **kwargs)') +``` + +------------------------------------------------------------------------ + +source + +### PrePostInitMeta + +> PrePostInitMeta (name, bases, dict) + +*A metaclass that calls optional `__pre_init__` and `__post_init__` +methods* + +`__pre_init__` and `__post_init__` are useful for initializing variables +or performing tasks prior to or after `__init__` being called, +respectively. Fore example: + +``` python +class _T(metaclass=PrePostInitMeta): + def __pre_init__(self): self.a = 0; + def __init__(self,b=0): self.b = self.a + 1; assert self.b==1 + def __post_init__(self): self.c = self.b + 2; assert self.c==3 + +t = _T() +test_eq(t.a, 0) # set with __pre_init__ +test_eq(t.b, 1) # set with __init__ +test_eq(t.c, 3) # set with __post_init__ +``` + +One use for +[`PrePostInitMeta`](https://fastcore.fast.ai/meta.html#prepostinitmeta) +is avoiding the `__super__().__init__()` boilerplate associated with +subclassing, such as used in +[`AutoInit`](https://fastcore.fast.ai/meta.html#autoinit). + +------------------------------------------------------------------------ + +source + +### AutoInit + +> AutoInit (*args, **kwargs) + +*Same as `object`, but no need for subclasses to call +`super().__init__`* + +This is normally used as a +[mixin](https://www.residentmar.io/2019/07/07/python-mixins.html), eg: + +``` python +class TestParent(): + def __init__(self): self.h = 10 + +class TestChild(AutoInit, TestParent): + def __init__(self): self.k = self.h + 2 + +t = TestChild() +test_eq(t.h, 10) # h=10 is initialized in the parent class +test_eq(t.k, 12) +``` + +------------------------------------------------------------------------ + +source + +### NewChkMeta + +> NewChkMeta (name, bases, dict) + +*Metaclass to avoid recreating object passed to constructor* + +[`NewChkMeta`](https://fastcore.fast.ai/meta.html#newchkmeta) is used +when an object of the same type is the first argument to your class’s +constructor (i.e. the `__init__` function), and you would rather it not +create a new object but point to the same exact object. + +This is used in [`L`](https://fastcore.fast.ai/foundation.html#l), for +example, to avoid creating a new object when the object is already of +type [`L`](https://fastcore.fast.ai/foundation.html#l). This allows the +users to defenisvely instantiate an +[`L`](https://fastcore.fast.ai/foundation.html#l) object and just return +a reference to the same object if it already happens to be of type +[`L`](https://fastcore.fast.ai/foundation.html#l). + +For example, the below class `_T` **optionally** accepts an object `o` +as its first argument. A new object is returned upon instantiation per +usual: + +``` python +class _T(): + "Testing" + def __init__(self, o): + # if `o` is not an object without an attribute `foo`, set foo = 1 + self.foo = getattr(o,'foo',1) +``` + +``` python +t = _T(3) +test_eq(t.foo,1) # 1 was not of type _T, so foo = 1 + +t2 = _T(t) #t1 is of type _T +assert t is not t2 # t1 and t2 are different objects +``` + +However, if we want `_T` to return a reference to the same object when +passed an an object of type `_T` we can inherit from the +[`NewChkMeta`](https://fastcore.fast.ai/meta.html#newchkmeta) class as +illustrated below: + +``` python +class _T(metaclass=NewChkMeta): + "Testing with metaclass NewChkMeta" + def __init__(self, o=None, b=1): + # if `o` is not an object without an attribute `foo`, set foo = 1 + self.foo = getattr(o,'foo',1) + self.b = b +``` + +We can now test `t` and `t2` are now pointing at the same object when +using this new definition of `_T`: + +``` python +t = _T(3) +test_eq(t.foo,1) # 1 was not of type _T, so foo = 1 + +t2 = _T(t) # t2 will now reference t + +test_is(t, t2) # t and t2 are the same object +t2.foo = 5 # this will also change t.foo to 5 because it is the same object +test_eq(t.foo, 5) +test_eq(t2.foo, 5) +``` + +However, there is one exception to how +[`NewChkMeta`](https://fastcore.fast.ai/meta.html#newchkmeta) works. +**If you pass any additional arguments in the constructor a new object +is returned**, even if the first object is of the same type. For +example, consider the below example where we pass the additional +argument `b` into the constructor: + +``` python +t3 = _T(t, b=1) +assert t3 is not t + +t4 = _T(t) # without any arguments the constructor will return a reference to the same object +assert t4 is t +``` + +Finally, it should be noted that +[`NewChkMeta`](https://fastcore.fast.ai/meta.html#newchkmeta) as well as +all other metaclases in this section, inherit from +[`FixSigMeta`](https://fastcore.fast.ai/meta.html#fixsigmeta). This +means class signatures will always be preserved when inheriting from +this metaclass (see docs for +[`FixSigMeta`](https://fastcore.fast.ai/meta.html#fixsigmeta) for more +details): + +``` python +test_sig(_T, '(o=None, b=1)') +``` + +------------------------------------------------------------------------ + +source + +### BypassNewMeta + +> BypassNewMeta (name, bases, dict) + +*Metaclass: casts `x` to this class if it’s of type `cls._bypass_type`* + +[`BypassNewMeta`](https://fastcore.fast.ai/meta.html#bypassnewmeta) is +identical to +[`NewChkMeta`](https://fastcore.fast.ai/meta.html#newchkmeta), except +for checking for a class as the same type, we instead check for a class +of type specified in attribute `_bypass_type`. + +In NewChkMeta, objects of the same type passed to the constructor +(without arguments) would result into a new variable referencing the +same object. However, with +[`BypassNewMeta`](https://fastcore.fast.ai/meta.html#bypassnewmeta) this +only occurs if the type matches the `_bypass_type` of the class you are +defining: + +``` python +class _TestA: pass +class _TestB: pass + +class _T(_TestA, metaclass=BypassNewMeta): + _bypass_type=_TestB + def __init__(self,x): self.x=x +``` + +In the below example, `t` does not refer to `t2` because `t` is of type +`_TestA` while `_T._bypass_type` is of type `TestB`: + +``` python +t = _TestA() +t2 = _T(t) +assert t is not t2 +``` + +However, if `t` is set to `_TestB` to match `_T._bypass_type`, then both +`t` and `t2` will refer to the same object. + +``` python +t = _TestB() +t2 = _T(t) +t2.new_attr = 15 + +test_is(t, t2) +# since t2 just references t these will be the same +test_eq(t.new_attr, t2.new_attr) + +# likewise, chaning an attribute on t will also affect t2 because they both point to the same object. +t.new_attr = 9 +test_eq(t2.new_attr, 9) +``` + +## Metaprogramming + +------------------------------------------------------------------------ + +source + +### empty2none + +> empty2none (p) + +*Replace `Parameter.empty` with `None`* + +------------------------------------------------------------------------ + +source + +### anno_dict + +> anno_dict (f) + +*`__annotation__ dictionary with`empty`cast to`None\`, returning empty +if doesn’t exist* + +``` python +def _f(a:int, b:L)->str: ... +test_eq(anno_dict(_f), {'a': int, 'b': L, 'return': str}) +``` + +------------------------------------------------------------------------ + +source + +### use_kwargs_dict + +> use_kwargs_dict (keep=False, **kwargs) + +*Decorator: replace `**kwargs` in signature with `names` params* + +Replace all `**kwargs` with named arguments like so: + +``` python +@use_kwargs_dict(y=1,z=None) +def foo(a, b=1, **kwargs): pass + +test_sig(foo, '(a, b=1, *, y=1, z=None)') +``` + +Add named arguments, but optionally keep `**kwargs` by setting +`keep=True`: + +``` python +@use_kwargs_dict(y=1,z=None, keep=True) +def foo(a, b=1, **kwargs): pass + +test_sig(foo, '(a, b=1, *, y=1, z=None, **kwargs)') +``` + +------------------------------------------------------------------------ + +source + +### use_kwargs + +> use_kwargs (names, keep=False) + +*Decorator: replace `**kwargs` in signature with `names` params* + +[`use_kwargs`](https://fastcore.fast.ai/meta.html#use_kwargs) is +different than +[`use_kwargs_dict`](https://fastcore.fast.ai/meta.html#use_kwargs_dict) +as it only replaces `**kwargs` with named parameters without any default +values: + +``` python +@use_kwargs(['y', 'z']) +def foo(a, b=1, **kwargs): pass + +test_sig(foo, '(a, b=1, *, y=None, z=None)') +``` + +You may optionally keep the `**kwargs` argument in your signature by +setting `keep=True`: + +``` python +@use_kwargs(['y', 'z'], keep=True) +def foo(a, *args, b=1, **kwargs): pass +test_sig(foo, '(a, *args, b=1, y=None, z=None, **kwargs)') +``` + +------------------------------------------------------------------------ + +source + +### delegates + +> delegates (to:function=None, keep=False, but:list=None) + +*Decorator: replace `**kwargs` in signature with params from `to`* + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TypeDefaultDetails
tofunctionNoneDelegatee
keepboolFalseKeep kwargs in decorated function?
butlistNoneExclude these parameters from signature
+ +A common Python idiom is to accept `**kwargs` in addition to named +parameters that are passed onto other function calls. It is especially +common to use `**kwargs` when you want to give the user an option to +override default parameters of any functions or methods being called by +the parent function. + +For example, suppose we have have a function `foo` that passes arguments +to `baz` like so: + +``` python +def baz(a, b:int=2, c:int=3): return a + b + c + +def foo(c, a, **kwargs): + return c + baz(a, **kwargs) + +assert foo(c=1, a=1) == 7 +``` + +The problem with this approach is the api for `foo` is obfuscated. Users +cannot introspect what the valid arguments for `**kwargs` are without +reading the source code. When a user tries tries to introspect the +signature of `foo`, they are presented with this: + +``` python +inspect.signature(foo) +``` + + + +We can address this issue by using the decorator +[`delegates`](https://fastcore.fast.ai/meta.html#delegates) to include +parameters from other functions. For example, if we apply the +[`delegates`](https://fastcore.fast.ai/meta.html#delegates) decorator to +`foo` to include parameters from `baz`: + +``` python +@delegates(baz) +def foo(c, a, **kwargs): + return c + baz(a, **kwargs) + +test_sig(foo, '(c, a, *, b: int = 2)') +inspect.signature(foo) +``` + + + +We can optionally decide to keep `**kwargs` by setting `keep=True`: + +``` python +@delegates(baz, keep=True) +def foo(c, a, **kwargs): + return c + baz(a, **kwargs) + +inspect.signature(foo) +``` + + + +It is important to note that **only parameters with default parameters +are included**. For example, in the below scenario only `c`, but NOT `e` +and `d` are included in the signature of `foo` after applying +[`delegates`](https://fastcore.fast.ai/meta.html#delegates): + +``` python +def basefoo(e, d, c=2): pass + +@delegates(basefoo) +def foo(a, b=1, **kwargs): pass +inspect.signature(foo) # e and d are not included b/c they don't have default parameters. +``` + + + +The reason that required arguments (i.e. those without default +parameters) are automatically excluded is that you should be explicitly +implementing required arguments into your function’s signature rather +than relying on +[`delegates`](https://fastcore.fast.ai/meta.html#delegates). + +Additionally, you can exclude specific parameters from being included in +the signature with the `but` parameter. In the example below, we exclude +the parameter `d`: + +``` python +def basefoo(e, c=2, d=3): pass + +@delegates(basefoo, but= ['d']) +def foo(a, b=1, **kwargs): pass + +test_sig(foo, '(a, b=1, *, c=2)') +inspect.signature(foo) +``` + + + +You can also use +[`delegates`](https://fastcore.fast.ai/meta.html#delegates) between +methods in a class. Here is an example of +[`delegates`](https://fastcore.fast.ai/meta.html#delegates) with class +methods: + +``` python +# example 1: class methods +class _T(): + @classmethod + def foo(cls, a=1, b=2): + pass + + @classmethod + @delegates(foo) + def bar(cls, c=3, **kwargs): + pass + +test_sig(_T.bar, '(c=3, *, a=1, b=2)') +``` + +Here is the same example with instance methods: + +``` python +# example 2: instance methods +class _T(): + def foo(self, a=1, b=2): + pass + + @delegates(foo) + def bar(self, c=3, **kwargs): + pass + +t = _T() +test_sig(t.bar, '(c=3, *, a=1, b=2)') +``` + +You can also delegate between classes. By default, the +[`delegates`](https://fastcore.fast.ai/meta.html#delegates) decorator +will delegate to the superclass: + +``` python +class BaseFoo: + def __init__(self, e, c=2): pass + +@delegates()# since no argument was passsed here we delegate to the superclass +class Foo(BaseFoo): + def __init__(self, a, b=1, **kwargs): super().__init__(**kwargs) + +test_sig(Foo, '(a, b=1, *, c=2)') +``` + +------------------------------------------------------------------------ + +source + +### method + +> method (f) + +*Mark `f` as a method* + +The [`method`](https://fastcore.fast.ai/meta.html#method) function is +used to change a function’s type to a method. In the below example we +change the type of `a` from a function to a method: + +``` python +def a(x=2): return x + 1 +assert type(a).__name__ == 'function' + +a = method(a) +assert type(a).__name__ == 'method' +``` + +------------------------------------------------------------------------ + +source + +### funcs_kwargs + +> funcs_kwargs (as_method=False) + +*Replace methods in `cls._methods` with those from `kwargs`* + +The `func_kwargs` decorator allows you to add a list of functions or +methods to an existing class. You must set this list as a class +attribute named `_methods` when defining your class. Additionally, you +must incldue the `**kwargs` argument in the `___init__` method of your +class. + +After defining your class this way, you can add functions to your class +upon instantation as illusrated below. + +For example, we define class `T` to allow adding the function `b` to +class `T` as follows (note that this function is stored as an attribute +of `T` and doesn’t have access to `cls` or `self`): + +``` python +@funcs_kwargs +class T: + _methods=['b'] # allows you to add method b upon instantiation + def __init__(self, f=1, **kwargs): pass # don't forget to include **kwargs in __init__ + def a(self): return 1 + def b(self): return 2 + +t = T() +test_eq(t.a(), 1) +test_eq(t.b(), 2) +``` + +Because we defined the class `T` this way, the signature of `T` +indicates the option to add the function or method(s) specified in +`_methods`. In this example, `b` is added to the signature: + +``` python +test_sig(T, '(f=1, *, b=None)') +inspect.signature(T) +``` + + + +You can now add the function `b` to class `T` upon instantiation: + +``` python +def _new_func(): return 5 + +t = T(b = _new_func) +test_eq(t.b(), 5) +``` + +If you try to add a function with a name not listed in `_methods` it +will be ignored. In the below example, the attempt to add a function +named `a` is ignored: + +``` python +t = T(a = lambda:3) +test_eq(t.a(), 1) # the attempt to add a is ignored and uses the original method instead. +``` + +Note that you can also add methods not defined in the original class as +long it is specified in the `_methods` attribute: + +``` python +@funcs_kwargs +class T: + _methods=['c'] + def __init__(self, f=1, **kwargs): pass + +t = T(c = lambda: 4) +test_eq(t.c(), 4) +``` + +Until now, these examples showed how to add functions stored as an +instance attribute without access to `self`. However, if you need access +to `self` you can set `as_method=True` in the `func_kwargs` decorator to +add a method instead: + +``` python +def _f(self,a=1): return self.num + a # access the num attribute from the instance + +@funcs_kwargs(as_method=True) +class T: + _methods=['b'] + num = 5 + +t = T(b = _f) # adds method b +test_eq(t.b(5), 10) # self.num + 5 = 10 +``` + +Here is an example of how you might use this functionality with +inheritence: + +``` python +def _f(self,a=1): return self.num * a #multiply instead of add + +class T2(T): + def __init__(self,num): + super().__init__(b = _f) # add method b from the super class + self.num=num + +t = T2(num=3) +test_eq(t.b(a=5), 15) # 3 * 5 = 15 +test_sig(T2, '(num)') +```
# Script - CLI + + + +Part of [fast.ai](https://www.fast.ai)’s toolkit for delightful +developer experiences. + +## Overview + +Sometimes, you want to create a quick script, either for yourself, or +for others. But in Python, that involves a whole lot of boilerplate and +ceremony, especially if you want to support command line arguments, +provide help, and other niceties. You can use +[argparse](https://docs.python.org/3/library/argparse.html) for this +purpose, which comes with Python, but it’s complex and verbose. + +`fastcore.script` makes life easier. There are much fancier modules to +help you write scripts (we recommend [Python +Fire](https://github.com/google/python-fire), and +[Click](https://click.palletsprojects.com/en/7.x/) is also popular), but +fastcore.script is very fast and very simple. In fact, it’s \<50 lines +of code! Basically, it’s just a little wrapper around `argparse` that +uses modern Python features and some thoughtful defaults to get rid of +the boilerplate. + +For full details, see the [docs](https://fastcore.script.fast.ai) for +`core`. + +## Example + +Here’s a complete example (available in `examples/test_fastcore.py`): + +``` python +from fastcore.script import * +@call_parse +def main(msg:str, # The message + upper:bool): # Convert to uppercase? + "Print `msg`, optionally converting to uppercase" + print(msg.upper() if upper else msg) +``` + +If you copy that info a file and run it, you’ll see: + + $ examples/test_fastcore.py --help + usage: test_fastcore.py [-h] [--upper] msg + + Print `msg`, optionally converting to uppercase + + positional arguments: + msg The message + + optional arguments: + -h, --help show this help message and exit + --upper Convert to uppercase? (default: False) + +As you see, we didn’t need any `if __name__ == "__main__"`, we didn’t +have to parse arguments, we just wrote a function, added a decorator to +it, and added some annotations to our function’s parameters. As a bonus, +we can also use this function directly from a REPL such as Jupyter +Notebook - it’s not just for command line scripts! + +You should provide a default (after the `=`) for any *optional* +parameters. If you don’t provide a default for a parameter, then it will +be a *positional* parameter. + +## Param annotations + +If you want to use the full power of `argparse`, you can do so by using +[`Param`](https://fastcore.fast.ai/script.html#param) annotations +instead of type annotations and +[docments](https://fastcore.fast.ai/docments.html), like so: + +``` python +from fastcore.script import * +@call_parse +def main(msg:Param("The message", str), + upper:Param("Convert to uppercase?", store_true)): + "Print `msg`, optionally converting to uppercase" + print(msg.upper() if upper else msg) +``` + +If you use this approach, then each parameter in your function should +have an annotation `Param(...)` (as in the example above). You can pass +the following when calling +[`Param`](https://fastcore.fast.ai/script.html#param): +`help`,`type`,`opt`,`action`,`nargs`,`const`,`choices`,`required` . +Except for `opt`, all of these are just passed directly to `argparse`, +so you have all the power of that module at your disposal. Generally +you’ll want to pass at least `help` (since this is provided as the help +string for that parameter) and `type` (to ensure that you get the type +of data you expect). `opt` is a bool that defines whether a param is +optional or required (positional) - but you’ll generally not need to set +this manually, because fastcore.script will set it for you automatically +based on *default* values. + +## setuptools scripts + +There’s a really nice feature of pip/setuptools that lets you create +commandline scripts directly from functions, makes them available in the +`PATH`, and even makes your scripts cross-platform (e.g. in Windows it +creates an exe). fastcore.script supports this feature too. The trick to +making a function available as a script is to add a `console_scripts` +section to your setup file, of the form: +`script_name=module:function_name`. E.g. in this case we use: +`test_fastcore.script=fastcore.script.test_cli:main`. With this, you can +then just type `test_fastcore.script` at any time, from any directory, +and your script will be called (once it’s installed using one of the +methods below). + +You don’t actually have to write a `setup.py` yourself. Instead, just +use [nbdev](https://nbdev.fast.ai). Then modify `settings.ini` as +appropriate for your module/script. To install your script directly, you +can type `pip install -e .`. Your script, when installed this way (it’s +called an [editable +install](http://codumentary.blogspot.com/2014/11/python-tip-of-year-pip-install-editable.html)), +will automatically be up to date even if you edit it - there’s no need +to reinstall it after editing. With nbdev you can even make your module +and script available for installation directly from pip and conda by +running `make release`. + +## API details + +------------------------------------------------------------------------ + +source + +### store_true + +> store_true () + +*Placeholder to pass to +[`Param`](https://fastcore.fast.ai/script.html#param) for +[`store_true`](https://fastcore.fast.ai/script.html#store_true) action* + +------------------------------------------------------------------------ + +source + +### store_false + +> store_false () + +*Placeholder to pass to +[`Param`](https://fastcore.fast.ai/script.html#param) for +[`store_false`](https://fastcore.fast.ai/script.html#store_false) +action* + +------------------------------------------------------------------------ + +source + +### bool_arg + +> bool_arg (v) + +*Use as `type` for [`Param`](https://fastcore.fast.ai/script.html#param) +to get `bool` behavior* + +------------------------------------------------------------------------ + +source + +### clean_type_str + +> clean_type_str (x:str) + +``` python +class Test: pass + +test_eq(clean_type_str(argparse.ArgumentParser), 'argparse.ArgumentParser') +test_eq(clean_type_str(Test), 'Test') +test_eq(clean_type_str(int), 'int') +test_eq(clean_type_str(float), 'float') +test_eq(clean_type_str(store_false), 'store_false') +``` + +------------------------------------------------------------------------ + +source + +### Param + +> Param (help='', type=None, opt=True, action=None, nargs=None, const=None, +> choices=None, required=None, default=None) + +*A parameter in a function used in +[`anno_parser`](https://fastcore.fast.ai/script.html#anno_parser) or +[`call_parse`](https://fastcore.fast.ai/script.html#call_parse)* + +``` python +test_eq(repr(Param("Help goes here")), '') +test_eq(repr(Param("Help", int)), 'int ') +test_eq(repr(Param(help=None, type=int)), 'int') +test_eq(repr(Param(help=None, type=None)), '') +``` + +Each parameter in your function should have an annotation `Param(...)`. +You can pass the following when calling +[`Param`](https://fastcore.fast.ai/script.html#param): +`help`,`type`,`opt`,`action`,`nargs`,`const`,`choices`,`required` +(i.e. it takes the same parameters as +`argparse.ArgumentParser.add_argument`, plus `opt`). Except for `opt`, +all of these are just passed directly to `argparse`, so you have all the +power of that module at your disposal. Generally you’ll want to pass at +least `help` (since this is provided as the help string for that +parameter) and `type` (to ensure that you get the type of data you +expect). + +`opt` is a bool that defines whether a param is optional or required +(positional) - but you’ll generally not need to set this manually, +because fastcore.script will set it for you automatically based on +*default* values. You should provide a default (after the `=`) for any +*optional* parameters. If you don’t provide a default for a parameter, +then it will be a *positional* parameter. + +Param’s `__repr__` also allows for more informative function annotation +when looking up the function’s doc using shift+tab. You see the type +annotation (if there is one) and the accompanying help documentation +with it. + +``` python +def f(required:Param("Required param", int), + a:Param("param 1", bool_arg), + b:Param("param 2", str)="test"): + "my docs" + ... +``` + +``` python +help(f) +``` + + Help on function f in module __main__: + + f(required: int , a: bool_arg , b: str = 'test') + my docs + +``` python +p = Param(help="help", type=int) +p.set_default(1) +test_eq(p.kwargs, {'help': 'help (default: 1)', 'type': int, 'default': 1}) +``` + +------------------------------------------------------------------------ + +source + +### anno_parser + +> anno_parser (func, prog:str=None) + +*Look at params (annotated with +[`Param`](https://fastcore.fast.ai/script.html#param)) in func and +return an `ArgumentParser`* + + + + + + + + + + + + + + + + + + + + + + + + +
TypeDefaultDetails
funcFunction to get arguments from
progstrNoneThe name of the program
+ +This converts a function with parameter annotations of type +[`Param`](https://fastcore.fast.ai/script.html#param) into an +`argparse.ArgumentParser` object. Function arguments with a default +provided are optional, and other arguments are positional. + +``` python +_en = str_enum('_en', 'aa','bb','cc') +def f(required:Param("Required param", int), + a:Param("param 1", bool_arg), + b:Param("param 2", str)="test", + c:Param("param 3", _en)=_en.aa): + "my docs" + ... + +p = anno_parser(f, 'progname') +p.print_help() +``` + + usage: progname [-h] [--b B] [--c {aa,bb,cc}] required a + + my docs + + positional arguments: + required Required param + a param 1 + + optional arguments: + -h, --help show this help message and exit + --b B param 2 (default: test) + --c {aa,bb,cc} param 3 (default: aa) + +It also works with type annotations and docments: + +``` python +def g(required:int, # Required param + a:bool_arg, # param 1 + b="test", # param 2 + c:_en=_en.aa): # param 3 + "my docs" + ... + +p = anno_parser(g, 'progname') +p.print_help() +``` + + usage: progname [-h] [--b B] [--c {aa,bb,cc}] required a + + my docs + + positional arguments: + required Required param + a param 1 + + optional arguments: + -h, --help show this help message and exit + --b B param 2 (default: test) + --c {aa,bb,cc} param 3 (default: aa) + +------------------------------------------------------------------------ + +source + +### args_from_prog + +> args_from_prog (func, prog) + +*Extract args from `prog`* + +Sometimes it’s convenient to extract arguments from the actual name of +the called program. +[`args_from_prog`](https://fastcore.fast.ai/script.html#args_from_prog) +will do this, assuming that names and values of the params are separated +by a `#`. Optionally there can also be a prefix separated by `##` +(double underscore). + +``` python +exp = {'a': False, 'b': 'baa'} +test_eq(args_from_prog(f, 'foo##a#0#b#baa'), exp) +test_eq(args_from_prog(f, 'a#0#b#baa'), exp) +``` + +------------------------------------------------------------------------ + +source + +### call_parse + +> call_parse (func=None, nested=False) + +*Decorator to create a simple CLI from `func` using +[`anno_parser`](https://fastcore.fast.ai/script.html#anno_parser)* + +``` python +@call_parse +def test_add( + a:int=0, # param a + b:int=0 # param 1 +): + "Add up `a` and `b`" + return a + b +``` + +[`call_parse`](https://fastcore.fast.ai/script.html#call_parse) +decorated functions work as regular functions and also as command-line +interface functions. + +``` python +test_eq(test_add(1,2), 3) +``` + +This is the main way to use `fastcore.script`; decorate your function +with [`call_parse`](https://fastcore.fast.ai/script.html#call_parse), +add [`Param`](https://fastcore.fast.ai/script.html#param) annotations +(as shown above) or type annotations and docments, and it can then be +used as a script. + +Use the `nested` keyword argument to create nested parsers, where +earlier parsers consume only their known args from `sys.argv` before +later parsers are used. This is useful to create one command line +application that executes another. For example: + +``` sh +myrunner --keyword 1 script.py -- +``` + +A separating `--` after the first application’s args is recommended +though not always required, otherwise args may be parsed in unexpected +ways. For example: + +``` sh +myrunner script.py -h +``` + +would display `myrunner`’s help and not `script.py`’s.
# XDG + + + +See the [XDG Base Directory +Specification](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html) +for more information. + +## Overview + +[`xdg_cache_home`](https://fastcore.fast.ai/xdg.html#xdg_cache_home), +[`xdg_config_home`](https://fastcore.fast.ai/xdg.html#xdg_config_home), +[`xdg_data_home`](https://fastcore.fast.ai/xdg.html#xdg_data_home), and +[`xdg_state_home`](https://fastcore.fast.ai/xdg.html#xdg_state_home) +return `pathlib.Path` objects containing the value of the environment +variable named `XDG_CACHE_HOME`, `XDG_CONFIG_HOME`, `XDG_DATA_HOME`, and +`XDG_STATE_HOME` respectively, or the default defined in the +specification if the environment variable is unset, empty, or contains a +relative path rather than absolute path. + +[`xdg_config_dirs`](https://fastcore.fast.ai/xdg.html#xdg_config_dirs) +and [`xdg_data_dirs`](https://fastcore.fast.ai/xdg.html#xdg_data_dirs) +return a list of `pathlib.Path` objects containing the value, split on +colons, of the environment variable named `XDG_CONFIG_DIRS` and +`XDG_DATA_DIRS` respectively, or the default defined in the +specification if the environment variable is unset or empty. Relative +paths are ignored, as per the specification. + +[`xdg_runtime_dir`](https://fastcore.fast.ai/xdg.html#xdg_runtime_dir) +returns a `pathlib.Path` object containing the value of the +`XDG_RUNTIME_DIR` environment variable, or `None` if the environment +variable is not set, or contains a relative path rather than absolute +path. + +## Helpers + +We’ll start by defining a context manager that temporarily sets an +environment variable to demonstrate the behaviour of each helper +function: + +``` python +from contextlib import contextmanager +``` + +``` python +@contextmanager +def env(variable, value): + old = os.environ.get(variable, None) + try: + os.environ[variable] = value + yield + finally: + if old is None: del os.environ[variable] + else: os.environ[variable] = old +``` + +------------------------------------------------------------------------ + +source + +### xdg_cache_home + +> xdg_cache_home () + +*Path corresponding to `XDG_CACHE_HOME`* + +``` python +from fastcore.test import * +``` + +``` python +test_eq(xdg_cache_home(), Path.home()/'.cache') +with env('XDG_CACHE_HOME', '/home/fastai/.cache'): + test_eq(xdg_cache_home(), Path('/home/fastai/.cache')) +``` + +------------------------------------------------------------------------ + +source + +### xdg_config_dirs + +> xdg_config_dirs () + +*Paths corresponding to `XDG_CONFIG_DIRS`* + +``` python +test_eq(xdg_config_dirs(), [Path('/etc/xdg')]) +with env('XDG_CONFIG_DIRS', '/home/fastai/.xdg:/home/fastai/.config'): + test_eq(xdg_config_dirs(), [Path('/home/fastai/.xdg'), Path('/home/fastai/.config')]) +``` + +------------------------------------------------------------------------ + +source + +### xdg_config_home + +> xdg_config_home () + +*Path corresponding to `XDG_CONFIG_HOME`* + +``` python +test_eq(xdg_config_home(), Path.home()/'.config') +with env('XDG_CONFIG_HOME', '/home/fastai/.config'): + test_eq(xdg_config_home(), Path('/home/fastai/.config')) +``` + +------------------------------------------------------------------------ + +source + +### xdg_data_dirs + +> xdg_data_dirs () + +*Paths corresponding to XDG_DATA_DIRS\`* + +------------------------------------------------------------------------ + +source + +### xdg_data_home + +> xdg_data_home () + +*Path corresponding to `XDG_DATA_HOME`* + +``` python +test_eq(xdg_data_home(), Path.home()/'.local/share') +with env('XDG_DATA_HOME', '/home/fastai/.data'): + test_eq(xdg_data_home(), Path('/home/fastai/.data')) +``` + +------------------------------------------------------------------------ + +source + +### xdg_runtime_dir + +> xdg_runtime_dir () + +*Path corresponding to `XDG_RUNTIME_DIR`* + +------------------------------------------------------------------------ + +source + +### xdg_state_home + +> xdg_state_home () + +*Path corresponding to `XDG_STATE_HOME`* + +``` python +test_eq(xdg_state_home(), Path.home()/'.local/state') +with env('XDG_STATE_HOME', '/home/fastai/.state'): + test_eq(xdg_state_home(), Path('/home/fastai/.state')) +``` + +------------------------------------------------------------------------ + +Copyright © 2016-2021 Scott Stevenson + +Modifications copyright © 2022 onwards Jeremy Howard# XML + + + +``` python +from IPython.display import Markdown +from pprint import pprint + +from fastcore.test import test_eq +``` + +## FT functions + +------------------------------------------------------------------------ + +source + +### attrmap + +> attrmap (o) + +------------------------------------------------------------------------ + +source + +### valmap + +> valmap (o) + +------------------------------------------------------------------------ + +source + +### FT + +> FT (tag:str, cs:tuple, attrs:dict=None, void_=False, **kwargs) + +*A ‘Fast Tag’ structure, containing `tag`,`children`,and `attrs`* + +------------------------------------------------------------------------ + +source + +### ft + +> ft (tag:str, *c, void_:bool=False, attrmap: infunctioncallable>=, valmap: infunctioncallable>=, ft_cls=, +> **kw) + +*Create an [`FT`](https://fastcore.fast.ai/xml.html#ft) structure for +`to_xml()`* + +The main HTML tags are exported as +[`ft`](https://fastcore.fast.ai/xml.html#ft) partials. + +Attributes are passed as keywords. Use ‘klass’ and ‘fr’ instead of +‘class’ and ‘for’, to avoid Python reserved word clashes. + +------------------------------------------------------------------------ + +source + +### Html + +> Html (*c, doctype=True, **kwargs) + +*An HTML tag, optionally preceeded by `!DOCTYPE HTML`* + +``` python +samp = Html( + Head(Title('Some page')), + Body(Div('Some text\nanother line', (Input(name="jph's"), Img(src="filename", data=1)), + cls=['myclass', 'another'], + style={'padding':1, 'margin':2})) +) +pprint(samp) +``` + + (!doctype((),{'html': True}), + html((head((title(('Some page',),{}),),{}), body((div(('Some text\nanother line', input((),{'name': "jph's"}), img((),{'src': 'filename', 'data': 1})),{'class': 'myclass another', 'style': 'padding:1; margin:2'}),),{})),{})) + +``` python +elem = P('Some text', id="myid") +print(elem.tag) +print(elem.children) +print(elem.attrs) +``` + + p + ('Some text',) + {'id': 'myid'} + +You can get and set attrs directly: + +``` python +elem.id = 'newid' +print(elem.id, elem.get('id'), elem.get('foo', 'missing')) +elem +``` + + newid newid missing + + p(('Some text',),{'id': 'newid'}) + +------------------------------------------------------------------------ + +source + +### Safe + +\*str(object=’’) -\> str str(bytes_or_buffer\[, encoding\[, errors\]\]) +-\> str + +Create a new string object from the given object. If encoding or errors +is specified, then the object must expose a data buffer that will be +decoded using the given encoding and error handler. Otherwise, returns +the result of object.\_\_str\_\_() (if defined) or repr(object). +encoding defaults to sys.getdefaultencoding(). errors defaults to +‘strict’.\* + +## Conversion to XML/HTML + +------------------------------------------------------------------------ + +source + +### to_xml + +> to_xml (elm, lvl=0, indent=True, do_escape=True) + +*Convert [`ft`](https://fastcore.fast.ai/xml.html#ft) element tree into +an XML string* + +``` python +h = to_xml(samp, do_escape=False) +print(h) +``` + + + + + Some page + + +
+ Some text + another line +
+ + + +``` python +class PageTitle: + def __ft__(self): return H1("Hello") + +class HomePage: + def __ft__(self): return Div(PageTitle(), Div('hello')) + +h = to_xml(Div(HomePage())) +expected_output = """
+
+

Hello

+
hello
+
+
+""" +assert h == expected_output +``` + +``` python +print(h) +``` + +
+
+

Hello

+
hello
+
+
+ +``` python +h = to_xml(samp, indent=False) +print(h) +``` + + Some page
Some text + another line
+ +Interoperability both directions with Django and Jinja using the +[**html**() +protocol](https://jinja.palletsprojects.com/en/3.1.x/templates/#jinja-filters.escape): + +``` python +def _esc(s): return s.__html__() if hasattr(s, '__html__') else Safe(escape(s)) + +r = Safe('Hello from Django') +print(to_xml(Div(r))) +print(_esc(Div(P('Hello from fastcore <3')))) +``` + +
Hello from Django
+ +
+

Hello from fastcore <3

+
+ +## Display + +------------------------------------------------------------------------ + +source + +### highlight + +> highlight (s, lang='html') + +*Markdown to syntax-highlight `s` in language `lang`* + +------------------------------------------------------------------------ + +source + +### showtags + +> showtags (s) + +You can also reorder the children to come *after* the attrs, if you use +this alternative syntax for [`FT`](https://fastcore.fast.ai/xml.html#ft) +where the children are in a second pair of `()` (behind the scenes this +is because [`FT`](https://fastcore.fast.ai/xml.html#ft) implements +`__call__` to add children). + +``` python +Body(klass='myclass')( + Div(style='padding:3px')( + 'Some text 1<2', + I(spurious=True)('in italics'), + Input(name='me'), + Img(src="filename", data=1) + ) +) +``` + +``` html + +
+Some text 1<2in italics +
+ +``` + +------------------------------------------------------------------------ + +source + +### **getattr** + +> __getattr__ (tag)
\ No newline at end of file diff --git a/llms-ctx.txt b/llms-ctx.txt new file mode 100644 index 00000000..9b549b05 --- /dev/null +++ b/llms-ctx.txt @@ -0,0 +1,2418 @@ +fastcore adds to Python features inspired by other languages, like multiple dispatch from Julia, mixins from Ruby, and currying, binding, and more from Haskell. It also adds some “missing features” and clean up some rough edges in the Python standard library, such as simplifying parallel processing, and bringing ideas from NumPy over to Python’s list type. + +Here are some tips on using fastcore: + +- **Liberal imports**: Utilize `from fastcore.module import *` freely. The library is designed for safe wildcard imports. +- **Enhanced list operations**: Substitute `list` with `L`. This provides advanced indexing, method chaining, and additional functionality while maintaining list-like behavior. +- **Extend existing classes**: Apply the `@patch` decorator to add methods to classes, including built-ins, without subclassing. This enables more flexible code organization. +- **Streamline class initialization**: In `__init__` methods, use `store_attr()` to efficiently set multiple attributes, reducing repetitive assignment code. +- **Explicit keyword arguments**: Apply the `delegates` decorator to functions to replace `**kwargs` with specific parameters, enhancing IDE support and documentation. +- **Optimize parallel execution**: Leverage fastcore's enhanced `ThreadPoolExecutor` and `ProcessPoolExecutor` for simplified concurrent processing. +- **Expressive testing**: Prefer fastcore's testing functions like `test_eq`, `test_ne`, `test_close` for more readable and informative test assertions. +- **Advanced file operations**: Use the extended `Path` class, which adds methods like `ls()`, `read_json()`, and others to `pathlib.Path`. +- **Flexible data structures**: Convert between dictionaries and attribute-access objects using `dict2obj` and `obj2dict` for more intuitive data handling. +- **Data pipeline construction**: Employ `Transform` and `Pipeline` classes to create modular, composable data processing workflows. +- **Functional programming paradigms**: Utilize tools like `compose`, `maps`, and `filter_ex` to write more functional-style Python code. +- **Documentation**: Use `docments` where possible to document parameters of functions and methods. +- **Time-aware caching**: Apply the `timed_cache` decorator to add time-based expiration to the standard `lru_cache` functionality. +- **Simplified CLI creation**: Use fastcore's console script utilities to easily transform Python functions into command-line interfaces.# A tour of fastcore + + + +Here’s a (somewhat) quick tour of a few higlights from fastcore. + +### Documentation + +All fast.ai projects, including this one, are built with +[nbdev](https://nbdev.fast.ai), which is a full literate programming +environment built on Jupyter Notebooks. That means that every piece of +documentation, including the page you’re reading now, can be accessed as +interactive Jupyter notebooks. In fact, you can even grab a link +directly to a notebook running interactively on Google Colab - if you +want to follow along with this tour, click the link below: + +``` python +colab_link('index') +``` + +[Open `index` in +Colab](https://colab.research.google.com/github/fastai/fastcore/blob/master/nbs/index.ipynb) + +The full docs are available at +[fastcore.fast.ai](https://fastcore.fast.ai). The code in the examples +and in all fast.ai libraries follow the [fast.ai style +guide](https://docs.fast.ai/dev/style.html). In order to support +interactive programming, all fast.ai libraries are designed to allow for +`import *` to be used safely, particular by ensuring that +[`__all__`](https://riptutorial.com/python/example/2894/the---all---special-variable) +is defined in all packages. In order to see where a function is from, +just type it: + +``` python +coll_repr +``` + + + +For more details, including a link to the full documentation and source +code, use `doc`, which pops up a window with this information: + +``` python +doc(coll_repr) +``` + + + +The documentation also contains links to any related functions or +classes, which appear like this: +[`coll_repr`](https://fastcore.fast.ai/foundation.html#coll_repr) (in +the notebook itself you will just see a word with back-ticks around it; +the links are auto-generated in the documentation site). The +documentation will generally show one or more examples of use, along +with any background context necessary to understand them. As you’ll see, +the examples for each function and method are shown as tests, rather +than example outputs, so let’s start by explaining that. + +### Testing + +fastcore’s testing module is designed to work well with +[nbdev](https://nbdev.fast.ai), which is a full literate programming +environment built on Jupyter Notebooks. That means that your tests, +docs, and code all live together in the same notebook. fastcore and +nbdev’s approach to testing starts with the premise that all your tests +should pass. If one fails, no more tests in a notebook are run. + +Tests look like this: + +``` python +test_eq(coll_repr(range(1000), 5), '(#1000) [0,1,2,3,4...]') +``` + +That’s an example from the docs for +[`coll_repr`](https://fastcore.fast.ai/foundation.html#coll_repr). As +you see, it’s not showing you the output directly. Here’s what that +would look like: + +``` python +coll_repr(range(1000), 5) +``` + + '(#1000) [0,1,2,3,4...]' + +So, the test is actually showing you what the output looks like, because +if the function call didn’t return `'(#1000) [0,1,2,3,4...]'`, then the +test would have failed. + +So every test shown in the docs is also showing you the behavior of the +library — and vice versa! + +Test functions always start with `test_`, and then follow with the +operation being tested. So +[`test_eq`](https://fastcore.fast.ai/test.html#test_eq) tests for +equality (as you saw in the example above). This includes tests for +equality of arrays and tensors, lists and generators, and many more: + +``` python +test_eq([0,1,2,3], np.arange(4)) +``` + +When a test fails, it prints out information about what was expected: + +``` python +test_eq([0,1,2,3], np.arange(3)) +``` + + ---- + AssertionError: ==: + [0, 1, 2, 3] + [0 1 2] + +If you want to check that objects are the same type, rather than the +just contain the same collection, use +[`test_eq_type`](https://fastcore.fast.ai/test.html#test_eq_type). + +You can test with any comparison function using +[`test`](https://fastcore.fast.ai/test.html#test), e.g test whether an +object is less than: + +``` python +test(2, 3, operator.lt) +``` + +You can even test that exceptions are raised: + +``` python +def divide_zero(): return 1/0 +test_fail(divide_zero) +``` + +…and test that things are printed to stdout: + +``` python +test_stdout(lambda: print('hi'), 'hi') +``` + +### Foundations + +fast.ai is unusual in that we often use +[mixins](https://en.wikipedia.org/wiki/Mixin) in our code. Mixins are +widely used in many programming languages, such as Ruby, but not so much +in Python. We use mixins to attach new behavior to existing libraries, +or to allow modules to add new behavior to our own classes, such as in +extension modules. One useful example of a mixin we define is +[`Path.ls`](https://fastcore.fast.ai/xtras.html#path.ls), which lists a +directory and returns an +[`L`](https://fastcore.fast.ai/foundation.html#l) (an extended list +class which we’ll discuss shortly): + +``` python +p = Path('images') +p.ls() +``` + + (#6) [Path('images/mnist3.png'),Path('images/att_00000.png'),Path('images/puppy.jpg'),Path('images/att_00005.png'),Path('images/att_00007.png'),Path('images/att_00006.png')] + +You can easily add you own mixins with the +[`patch`](https://fastcore.fast.ai/basics.html#patch) +[decorator](https://realpython.com/primer-on-python-decorators/), which +takes advantage of Python 3 [function +annotations](https://www.python.org/dev/peps/pep-3107/#parameters) to +say what class to patch: + +``` python +@patch +def num_items(self:Path): return len(self.ls()) + +p.num_items() +``` + + 6 + +We also use `**kwargs` frequently. In python `**kwargs` in a parameter +like means “*put any additional keyword arguments into a dict called +`kwargs`*”. Normally, using `kwargs` makes an API quite difficult to +work with, because it breaks things like tab-completion and popup lists +of signatures. `utils` provides +[`use_kwargs`](https://fastcore.fast.ai/meta.html#use_kwargs) and +[`delegates`](https://fastcore.fast.ai/meta.html#delegates) to avoid +this problem. See our [detailed article on +delegation](https://www.fast.ai/2019/08/06/delegation/) on this topic. + +[`GetAttr`](https://fastcore.fast.ai/basics.html#getattr) solves a +similar problem (and is also discussed in the article linked above): +it’s allows you to use Python’s exceptionally useful +[`__getattr__`](https://fastcore.fast.ai/xml.html#__getattr__) magic +method, but avoids the problem that normally in Python tab-completion +and docs break when using this. For instance, you can see here that +Python’s `dir` function, which is used to find the attributes of a +python object, finds everything inside the `self.default` attribute +here: + +``` python +class Author: + def __init__(self, name): self.name = name + +class ProductPage(GetAttr): + _default = 'author' + def __init__(self,author,price,cost): self.author,self.price,self.cost = author,price,cost + +p = ProductPage(Author("Jeremy"), 1.50, 0.50) +[o for o in dir(p) if not o.startswith('_')] +``` + + ['author', 'cost', 'name', 'price'] + +Looking at that `ProductPage` example, it’s rather verbose and +duplicates a lot of attribute names, which can lead to bugs later if you +change them only in one place. `fastcore` provides +[`store_attr`](https://fastcore.fast.ai/basics.html#store_attr) to +simplify this common pattern. It also provides +[`basic_repr`](https://fastcore.fast.ai/basics.html#basic_repr) to give +simple objects a useful `repr`: + +``` python +class ProductPage: + def __init__(self,author,price,cost): store_attr() + __repr__ = basic_repr('author,price,cost') + +ProductPage("Jeremy", 1.50, 0.50) +``` + + __main__.ProductPage(author='Jeremy', price=1.5, cost=0.5) + +One of the most interesting `fastcore` functions is the +[`funcs_kwargs`](https://fastcore.fast.ai/meta.html#funcs_kwargs) +decorator. This allows class behavior to be modified without +sub-classing. This can allow folks that aren’t familiar with +object-oriented programming to customize your class more easily. Here’s +an example of a class that uses +[`funcs_kwargs`](https://fastcore.fast.ai/meta.html#funcs_kwargs): + +``` python +@funcs_kwargs +class T: + _methods=['some_method'] + def __init__(self, **kwargs): assert not kwargs, f'Passed unknown args: {kwargs}' + +p = T(some_method = print) +p.some_method("hello") +``` + + hello + +The `assert not kwargs` above is used to ensure that the user doesn’t +pass an unknown parameter (i.e one that’s not in `_methods`). `fastai` +uses [`funcs_kwargs`](https://fastcore.fast.ai/meta.html#funcs_kwargs) +in many places, for instance, you can customize any part of a +`DataLoader` by passing your own methods. + +`fastcore` also provides many utility functions that make a Python +programmer’s life easier, in `fastcore.utils`. We won’t look at many +here, since you can easily look at the docs yourself. To get you +started, have a look at the docs for +[`chunked`](https://fastcore.fast.ai/basics.html#chunked) (remember, if +you’re in a notebook, type `doc(chunked)`), which is a handy function +for creating lazily generated batches from a collection. + +Python’s +[`ProcessPoolExecutor`](https://fastcore.fast.ai/parallel.html#processpoolexecutor) +is extended to allow `max_workers` to be set to `0`, to easily turn off +parallel processing. This makes it easy to debug your code in serial, +then run it in parallel. It also allows you to pass arguments to your +parallel function, and to ensure there’s a pause between calls, in case +the process you are running has race conditions. +[`parallel`](https://fastcore.fast.ai/parallel.html#parallel) makes +parallel processing even easier to use, and even adds an optional +progress bar. + +### L + +Like most languages, Python allows for very concise syntax for some very +common types, such as `list`, which can be constructed with `[1,2,3]`. +Perl’s designer Larry Wall explained the reasoning for this kind of +syntax: + +> In metaphorical honor of Huffman’s compression code that assigns +> smaller numbers of bits to more common bytes. In terms of syntax, it +> simply means that commonly used things should be shorter, but you +> shouldn’t waste short sequences on less common constructs. + +On this basis, `fastcore` has just one type that has a single letter +name: [`L`](https://fastcore.fast.ai/foundation.html#l). The reason for +this is that it is designed to be a replacement for `list`, so we want +it to be just as easy to use as `[1,2,3]`. Here’s how to create that as +an [`L`](https://fastcore.fast.ai/foundation.html#l): + +``` python +L(1,2,3) +``` + + (#3) [1,2,3] + +The first thing to notice is that an +[`L`](https://fastcore.fast.ai/foundation.html#l) object includes in its +representation its number of elements; that’s the `(#3)` in the output +above. If there’s more than 10 elements, it will automatically truncate +the list: + +``` python +p = L.range(20).shuffle() +p +``` + + (#20) [5,1,9,10,18,13,6,17,3,16...] + +[`L`](https://fastcore.fast.ai/foundation.html#l) contains many of the +same indexing ideas that NumPy’s `array` does, including indexing with a +list of indexes, or a boolean mask list: + +``` python +p[2,4,6] +``` + + (#3) [9,18,6] + +It also contains other methods used in `array`, such as +[`L.argwhere`](https://fastcore.fast.ai/foundation.html#l.argwhere): + +``` python +p.argwhere(ge(15)) +``` + + (#5) [4,7,9,18,19] + +As you can see from this example, `fastcore` also includes a number of +features that make a functional style of programming easier, such as a +full range of boolean functions (e.g `ge`, `gt`, etc) which give the +same answer as the functions from Python’s `operator` module if given +two parameters, but return a [curried +function](https://en.wikipedia.org/wiki/Currying) if given one +parameter. + +There’s too much functionality to show it all here, so be sure to check +the docs. Many little things are added that we thought should have been +in `list` in the first place, such as making this do what you’d expect +(which is an error with `list`, but works fine with +[`L`](https://fastcore.fast.ai/foundation.html#l)): + +``` python +1 + L(2,3,4) +``` + + (#4) [1,2,3,4] + +### Transforms + +A [`Transform`](https://fastcore.fast.ai/transform.html#transform) is +the main building block of the fastai data pipelines. In the most +general terms a transform can be any function you want to apply to your +data, however the +[`Transform`](https://fastcore.fast.ai/transform.html#transform) class +provides several mechanisms that make the process of building them easy +and flexible (see the docs for information about each of these): + +- Type dispatch +- Dispatch over tuples +- Reversability +- Type propagation +- Preprocessing +- Filtering based on the dataset type +- Ordering +- Appending new behavior with decorators + +[`Transform`](https://fastcore.fast.ai/transform.html#transform) looks +for three special methods, encodes, decodes, +and setups, which provide the implementation for +[`__call__`](https://www.python-course.eu/python3_magic_methods.php), +`decode`, and `setup` respectively. For instance: + +``` python +class A(Transform): + def encodes(self, x): return x+1 + +A()(1) +``` + + 2 + +For simple transforms like this, you can also use +[`Transform`](https://fastcore.fast.ai/transform.html#transform) as a +decorator: + +``` python +@Transform +def f(x): return x+1 + +f(1) +``` + + 2 + +Transforms can be composed into a +[`Pipeline`](https://fastcore.fast.ai/transform.html#pipeline): + +``` python +@Transform +def g(x): return x/2 + +pipe = Pipeline([f,g]) +pipe(3) +``` + + 2.0 + +The power of +[`Transform`](https://fastcore.fast.ai/transform.html#transform) and +[`Pipeline`](https://fastcore.fast.ai/transform.html#pipeline) is best +understood by seeing how they’re used to create a complete data +processing pipeline. This is explained in [chapter +11](https://github.com/fastai/fastbook/blob/master/11_midlevel_data.ipynb) +of the [fastai +book](https://www.amazon.com/Deep-Learning-Coders-fastai-PyTorch/dp/1492045527), +which is [available for free](https://github.com/fastai/fastbook) in +Jupyter Notebook format.# fastcore: An Underrated Python Library + +A unique python library that extends the python programming language and provides utilities that enhance productivity. + +Sep 1, 2020 • Hamel Husain • 14 min read + +__fastcore fastai + +# Background __ + +I recently embarked on a journey to sharpen my python skills: I wanted to learn advanced patterns, idioms, and techniques. I started with reading books on advanced Python, however, the information didn't seem to stick without having somewhere to apply it. I also wanted the ability to ask questions from an expert while I was learning -- which is an arrangement that is hard to find! That's when it occurred to me: What if I could find an open source project that has fairly advanced python code and write documentation and tests? I made a bet that if I did this it would force me to learn everything very deeply, and the maintainers would be appreciative of my work and be willing to answer my questions. + +And that's exactly what I did over the past month! I'm pleased to report that it has been the most efficient learning experience I've ever experienced. I've discovered that writing documentation forced me to deeply understand not just what the code does but also _why the code works the way it does_ , and to explore edge cases while writing tests. Most importantly, I was able to ask questions when I was stuck, and maintainers were willing to devote extra time knowing that their mentorship was in service of making their code more accessible! It turns out the library I choose, fastcore is some of the most fascinating Python I have ever encountered as its purpose and goals are fairly unique. + +For the uninitiated, fastcore is a library on top of which many fast.ai projects are built on. Most importantly, fastcore extends the python programming language and strives to eliminate boilerplate and add useful functionality for common tasks. In this blog post, I'm going to highlight some of my favorite tools that fastcore provides, rather than sharing what I learned about python. My goal is to pique your interest in this library, and hopefully motivate you to check out the documentation after you are done to learn more! + +# Why fastcore is interesting __ + + 1. **Get exposed to ideas from other languages without leaving python:** I’ve always heard that it is beneficial to learn other languages in order to become a better programmer. From a pragmatic point of view, I’ve found it difficult to learn other languages because I could never use them at work. Fastcore extends python to include patterns found in languages as diverse as Julia, Ruby and Haskell. Now that I understand these tools I am motivated to learn other languages. + 2. **You get a new set of pragmatic tools** : fastcore includes utilities that will allow you to write more concise expressive code, and perhaps solve new problems. + 3. **Learn more about the Python programming language:** Because fastcore extends the python programming language, many advanced concepts are exposed during the process. For the motivated, this is a great way to see how many of the internals of python work. + +# A whirlwind tour through fastcore __ + +Here are some things you can do with fastcore that immediately caught my attention. + +* * * + +## Making **kwargs transparent __ + +Whenever I see a function that has the argument****kwargs** , I cringe a little. This is because it means the API is obfuscated and I have to read the source code to figure out what valid parameters might be. Consider the below example: + +``` +def baz(a, b=2, c=3, d=4): return a + b + c + +def foo(c, a, **kwargs): + return c + baz(a, **kwargs) + +inspect.signature(foo) + +``` + +``` + +``` + +Without reading the source code, it might be hard for me to know that `foo` also accepts and additional parameters `b` and `d`. We can fix this with `delegates`: + +``` +def baz(a, b=2, c=3, d=4): return a + b + c + +@delegates(baz) # this decorator will pass down keyword arguments from baz +def foo(c, a, **kwargs): + return c + baz(a, **kwargs) + +inspect.signature(foo) + +``` + +``` + +``` + +You can customize the behavior of this decorator. For example, you can have your cake and eat it too by passing down your arguments and also keeping `**kwargs`: + +``` +@delegates(baz, keep=True) +def foo(c, a, **kwargs): + return c + baz(a, **kwargs) + +inspect.signature(foo) + +``` + +``` + +``` + +You can also exclude arguments. For example, we exclude argument `d` from delegation: + +``` +def basefoo(a, b=2, c=3, d=4): pass + +@delegates(basefoo, but=['d']) # exclude `d` +def foo(c, a, **kwargs): pass + +inspect.signature(foo) + +``` + +``` + +``` + +You can also delegate between classes: + +``` +class BaseFoo: + def __init__(self, e, c=2): pass + +@delegates()# since no argument was passsed here we delegate to the superclass +class Foo(BaseFoo): + def __init__(self, a, b=1, **kwargs): super().__init__(**kwargs) + +inspect.signature(Foo) + +``` + +``` + +``` + +For more information, read the docs on delegates. + +* * * + +## Avoid boilerplate when setting instance attributes __ + +Have you ever wondered if it was possible to avoid the boilerplate involved with setting attributes in`__init__`? + +``` +class Test: + def __init__(self, a, b ,c): + self.a, self.b, self.c = a, b, c + +``` + +Ouch! That was painful. Look at all the repeated variable names. Do I really have to repeat myself like this when defining a class? Not Anymore! Checkout store_attr: + +``` +class Test: + def __init__(self, a, b, c): + store_attr() + +t = Test(5,4,3) +assert t.b == 4 + +``` + +You can also exclude certain attributes: + +``` +class Test: + def __init__(self, a, b, c): + store_attr(but=['c']) + +t = Test(5,4,3) +assert t.b == 4 +assert not hasattr(t, 'c') + +``` + +There are many more ways of customizing and using `store_attr` than I highlighted here. Check out the docs for more detail. + +P.S. you might be thinking that Python dataclasses also allow you to avoid this boilerplate. While true in some cases, `store_attr` is more flexible.1 + +1\. For example, store_attr does not rely on inheritance, which means you won't get stuck using multiple inheritance when using this with your own classes. Also, unlike dataclasses, store_attr does not require python 3.7 or higher. Furthermore, you can use store_attr anytime in the object lifecycle, and in any location in your class to customize the behavior of how and when variables are stored.↩ + +* * * + +## Avoiding subclassing boilerplate __ + +One thing I hate about python is the`__super__().__init__()` boilerplate associated with subclassing. For example: + +``` +class ParentClass: + def __init__(self): self.some_attr = 'hello' + +class ChildClass(ParentClass): + def __init__(self): + super().__init__() + +cc = ChildClass() +assert cc.some_attr == 'hello' # only accessible b/c you used super + +``` + +We can avoid this boilerplate by using the metaclass PrePostInitMeta. We define a new class called `NewParent` that is a wrapper around the `ParentClass`: + +``` +class NewParent(ParentClass, metaclass=PrePostInitMeta): + def __pre_init__(self, *args, **kwargs): super().__init__() + +class ChildClass(NewParent): + def __init__(self):pass + +sc = ChildClass() +assert sc.some_attr == 'hello' + +``` + +* * * + +## Type Dispatch __ + +Type dispatch, orMultiple dispatch, allows you to change the way a function behaves based upon the input types it receives. This is a prominent feature in some programming languages like Julia. For example, this is a conceptual example of how multiple dispatch works in Julia, returning different values depending on the input types of x and y: + +``` +collide_with(x::Asteroid, y::Asteroid) = ... +# deal with asteroid hitting asteroid + +collide_with(x::Asteroid, y::Spaceship) = ... +# deal with asteroid hitting spaceship + +collide_with(x::Spaceship, y::Asteroid) = ... +# deal with spaceship hitting asteroid + +collide_with(x::Spaceship, y::Spaceship) = ... +# deal with spaceship hitting spaceship + +``` + +Type dispatch can be especially useful in data science, where you might allow different input types (i.e. Numpy arrays and Pandas dataframes) to a function that processes data. Type dispatch allows you to have a common API for functions that do similar tasks. + +Unfortunately, Python does not support this out-of-the box. Fortunately, there is the @typedispatch decorator to the rescue. This decorator relies upon type hints in order to route inputs the correct version of the function: + +``` +@typedispatch +def f(x:str, y:str): return f'{x}{y}' + +@typedispatch +def f(x:np.ndarray): return x.sum() + +@typedispatch +def f(x:int, y:int): return x+y + +``` + +Below is a demonstration of type dispatch at work for the function `f`: + +``` +f('Hello ', 'World!') + +``` + +``` +'Hello World!' +``` + +``` +f(2,3) + +``` + +``` +5 +``` + +``` +f(np.array([5,5,5,5])) + +``` + +``` +20 +``` + +There are limitations of this feature, as well as other ways of using this functionality that you can read about here. In the process of learning about typed dispatch, I also found a python library called multipledispatch made by Mathhew Rocklin (the creator of Dask). + +After using this feature, I am now motivated to learn languages like Julia to discover what other paradigms I might be missing. + +* * * + +## A better version of functools.partial __ + +`functools.partial` is a great utility that creates functions from other functions that lets you set default values. Lets take this function for example that filters a list to only contain values >= `val`: + +``` +test_input = [1,2,3,4,5,6] +def f(arr, val): + "Filter a list to remove any values that are less than val." + return [x for x in arr if x >= val] + +f(test_input, 3) + +``` + +``` +[3, 4, 5, 6] +``` + +You can create a new function out of this function using `partial` that sets the default value to 5: + +``` +filter5 = partial(f, val=5) +filter5(test_input) + +``` + +``` +[5, 6] +``` + +One problem with `partial` is that it removes the original docstring and replaces it with a generic docstring: + +``` +filter5.__doc__ + +``` + +``` +'partial(func, *args, **keywords) - new function with partial application\n of the given arguments and keywords.\n' +``` + +fastcore.utils.partialler fixes this, and makes sure the docstring is retained such that the new API is transparent: + +``` +filter5 = partialler(f, val=5) +filter5.__doc__ + +``` + +``` +'Filter a list to remove any values that are less than val.' +``` + +* * * + +## Composition of functions __ + +A technique that is pervasive in functional programming languages is function composition, whereby you chain a bunch of functions together to achieve some kind of result. This is especially useful when applying various data transformations. Consider a toy example where I have three functions: (1) Removes elements of a list less than 5 (from the prior section) (2) adds 2 to each number (3) sums all the numbers: + +``` +def add(arr, val): return [x + val for x in arr] +def arrsum(arr): return sum(arr) + +# See the previous section on partialler +add2 = partialler(add, val=2) + +transform = compose(filter5, add2, arrsum) +transform([1,2,3,4,5,6]) + +``` + +``` +15 +``` + +But why is this useful? You might me thinking, I can accomplish the same thing with: + +``` +arrsum(add2(filter5([1,2,3,4,5,6]))) + +``` + +You are not wrong! However, composition gives you a convenient interface in case you want to do something like the following: + +``` +def fit(x, transforms:list): + "fit a model after performing transformations" + x = compose(*transforms)(x) + y = [np.mean(x)] * len(x) # its a dumb model. Don't judge me + return y + +# filters out elements < 5, adds 2, then predicts the mean +fit(x=[1,2,3,4,5,6], transforms=[filter5, add2]) + +``` + +``` +[7.5, 7.5] +``` + +For more information about `compose`, read the docs. + +* * * + +## A more useful `__repr__`__ + +In python,`__repr__` helps you get information about an object for logging and debugging. Below is what you get by default when you define a new class. (Note: we are using `store_attr`, which was discussed earlier). + +``` +class Test: + def __init__(self, a, b=2, c=3): store_attr() # `store_attr` was discussed previously + +Test(1) + +``` + +``` +<__main__.Test at 0x7ffcd766cee0> +``` + +We can use basic_repr to quickly give us a more sensible default: + +``` +class Test: + def __init__(self, a, b=2, c=3): store_attr() + __repr__ = basic_repr('a,b,c') + +Test(2) + +``` + +``` +Test(a=2, b=2, c=3) +``` + +* * * + +## Monkey Patching With A Decorator __ + +It can be convenient tomonkey patch with a decorator, which is especially helpful when you want to patch an external library you are importing. We can use the decorator @patch from `fastcore.foundation` along with type hints like so: + +``` +class MyClass(int): pass + +@patch +def func(self:MyClass, a): return self+a + +mc = MyClass(3) + +``` + +Now, `MyClass` has an additional method named `func`: + +``` +mc.func(10) + +``` + +``` +13 +``` + +Still not convinced? I'll show you another example of this kind of patching in the next section. + +* * * + +## A better pathlib.Path __ + +When you seethese extensions to pathlib.path you won't ever use vanilla pathlib again! A number of additional methods have been added to pathlib, such as: + + * `Path.readlines`: same as `with open('somefile', 'r') as f: f.readlines()` + * `Path.read`: same as `with open('somefile', 'r') as f: f.read()` + * `Path.save`: saves file as pickle + * `Path.load`: loads pickle file + * `Path.ls`: shows the contents of the path as a list. + * etc. + +Read more about this here. Here is a demonstration of `ls`: + +``` +from fastcore.utils import * +from pathlib import Path +p = Path('.') +p.ls() # you don't get this with vanilla Pathlib.Path!! + +``` + +``` +(#7) [Path('2020-09-01-fastcore.ipynb'),Path('README.md'),Path('fastcore_imgs'),Path('2020-02-20-test.ipynb'),Path('.ipynb_checkpoints'),Path('2020-02-21-introducing-fastpages.ipynb'),Path('my_icons')] +``` + +Wait! What's going on here? We just imported `pathlib.Path` \- why are we getting this new functionality? Thats because we imported the `fastcore.utils` module, which patches this module via the `@patch` decorator discussed earlier. Just to drive the point home on why the `@patch` decorator is useful, I'll go ahead and add another method to `Path` right now: + +``` +@patch +def fun(self:Path): return "This is fun!" + +p.fun() + +``` + +``` +'This is fun!' +``` + +That is magical, right? I know! That's why I'm writing about it! + +* * * + +## An Even More Concise Way To Create Lambdas __ + +`Self`, with an uppercase S, is an even more concise way to create lambdas that are calling methods on an object. For example, let's create a lambda for taking the sum of a Numpy array: + +``` +arr=np.array([5,4,3,2,1]) +f = lambda a: a.sum() +assert f(arr) == 15 + +``` + +You can use `Self` in the same way: + +``` +f = Self.sum() +assert f(arr) == 15 + +``` + +Let's create a lambda that does a groupby and max of a Pandas dataframe: + +``` +import pandas as pd +df=pd.DataFrame({'Some Column': ['a', 'a', 'b', 'b', ], + 'Another Column': [5, 7, 50, 70]}) + +f = Self.groupby('Some Column').mean() +f(df) + +``` + +| Another Column +---|--- +Some Column | +a | 6 +b | 60 + +Read more about `Self` in the docs). + +* * * + +## Notebook Functions __ + +These are simple but handy, and allow you to know whether or not code is executing in a Jupyter Notebook, Colab, or an Ipython Shell: + +``` +from fastcore.imports import in_notebook, in_colab, in_ipython +in_notebook(), in_colab(), in_ipython() + +``` + +``` +(True, False, True) +``` + +This is useful if you are displaying certain types of visualizations, progress bars or animations in your code that you may want to modify or toggle depending on the environment. + +* * * + +## A Drop-In Replacement For List __ + +You might be pretty happy with Python's`list`. This is one of those situations that you don't know you needed a better list until someone showed one to you. Enter `L`, a list like object with many extra goodies. + +The best way I can describe `L` is to pretend that `list` and `numpy` had a pretty baby: + +define a list (check out the nice `__repr__` that shows the length of the list!) + +``` +L(1,2,3) + +``` + +``` +(#3) [1,2,3] +``` + +Shuffle a list: + +``` +p = L.range(20).shuffle() +p + +``` + +``` +(#20) [8,7,5,12,14,16,2,15,19,6...] +``` + +Index into a list: + +``` +p[2,4,6] + +``` + +``` +(#3) [5,14,2] +``` + +L has sensible defaults, for example appending an element to a list: + +``` +1 + L(2,3,4) + +``` + +``` +(#4) [1,2,3,4] +``` + +There is much more `L` has to offer. Read the docs to learn more. + +# But Wait ... There's More!__ + +There are more things I would like to show you about fastcore, but there is no way they would reasonably fit into a blog post. Here is a list of some of my favorite things that I didn't demo in this blog post: + +## Utilities __ + +TheBasics section contain many shortcuts to perform common tasks or provide an additional interface to what standard python provides. + + * mk_class: quickly add a bunch of attributes to a class + * wrap_class: add new methods to a class with a simple decorator + * groupby: similar to Scala's groupby + * merge: merge dicts + * fasttuple: a tuple on steroids + * Infinite Lists: useful for padding and testing + * chunked: for batching and organizing stuff + +## Multiprocessing __ + +TheMultiprocessing section extends python's multiprocessing library by offering features like: + + * progress bars + * ability to pause to mitigate race conditions with external services + * processing things in batches on each worker, ex: if you have a vectorized operation to perform in chunks + +## Functional Programming __ + +Thefunctional programming section is my favorite part of this library. + + * maps: a map that also composes functions + * mapped: A more robust `map` + * using_attr: compose a function that operates on an attribute + +## Transforms __ + +Transforms is a collection of utilities for creating data transformations and associated pipelines. These transformation utilities build upon many of the building blocks discussed in this blog post. + +## Further Reading __ + +**It should be noted that you should read themain page of the docs first, followed by the section on tests to fully understand the documentation.** + + * The fastcore documentation site. + * The fastcore GitHub repo. + * Blog post on delegation. + +# Shameless plug: fastpages __ + +This blog post was written entirely in a Jupyter Notebook, which GitHub automatically converted into to a blog post! Sound interesting?Check out fastpages.# fastcore Module Documentation + +## fastcore.basics + +> Basic functionality used in the fastai library + +- `def ifnone(a, b)` + `b` if `a` is None else `a` + +- `def maybe_attr(o, attr)` + `getattr(o,attr,o)` + +- `def basic_repr(flds)` + Minimal `__repr__` + +- `class BasicRepr` + Base class for objects needing a basic `__repr__` + + +- `def is_array(x)` + `True` if `x` supports `__array__` or `iloc` + +- `def listify(o, *rest)` + Convert `o` to a `list` + +- `def tuplify(o, use_list, match)` + Make `o` a tuple + +- `def true(x)` + Test whether `x` is truthy; collections with >0 elements are considered `True` + +- `class NullType` + An object that is `False` and can be called, chained, and indexed + + - `def __getattr__(self, *args)` + - `def __call__(self, *args, **kwargs)` + - `def __getitem__(self, *args)` + - `def __bool__(self)` + +- `def tonull(x)` + Convert `None` to `null` + +- `def get_class(nm, *fld_names, **flds)` + Dynamically create a class, optionally inheriting from `sup`, containing `fld_names` + +- `def mk_class(nm, *fld_names, **flds)` + Create a class using `get_class` and add to the caller's module + +- `def wrap_class(nm, *fld_names, **flds)` + Decorator: makes function a method of a new class `nm` passing parameters to `mk_class` + +- `class ignore_exceptions` + Context manager to ignore exceptions + + - `def __enter__(self)` + - `def __exit__(self, *args)` + +- `def exec_local(code, var_name)` + Call `exec` on `code` and return the var `var_name` + +- `def risinstance(types, obj)` + Curried `isinstance` but with args reversed + +- `class Inf` + Infinite lists + + +- `def in_(x, a)` + `True` if `x in a` + +- `def ret_true(*args, **kwargs)` + Predicate: always `True` + +- `def ret_false(*args, **kwargs)` + Predicate: always `False` + +- `def stop(e)` + Raises exception `e` (by default `StopIteration`) + +- `def gen(func, seq, cond)` + Like `(func(o) for o in seq if cond(func(o)))` but handles `StopIteration` + +- `def chunked(it, chunk_sz, drop_last, n_chunks)` + Return batches from iterator `it` of size `chunk_sz` (or return `n_chunks` total) + +- `def otherwise(x, tst, y)` + `y if tst(x) else x` + +- `def custom_dir(c, add)` + Implement custom `__dir__`, adding `add` to `cls` + +- `class AttrDict` + `dict` subclass that also provides access to keys as attrs + + - `def __getattr__(self, k)` + - `def __setattr__(self, k, v)` + - `def __dir__(self)` + - `def copy(self)` + +- `class AttrDictDefault` + `AttrDict` subclass that returns `None` for missing attrs + + - `def __init__(self, *args, **kwargs)` + - `def __getattr__(self, k)` + +- `class NS` + `SimpleNamespace` subclass that also adds `iter` and `dict` support + + - `def __iter__(self)` + - `def __getitem__(self, x)` + - `def __setitem__(self, x, y)` + +- `def get_annotations_ex(obj)` + Backport of py3.10 `get_annotations` that returns globals/locals + +- `def eval_type(t, glb, loc)` + `eval` a type or collection of types, if needed, for annotations in py3.10+ + +- `def type_hints(f)` + Like `typing.get_type_hints` but returns `{}` if not allowed type + +- `def annotations(o)` + Annotations for `o`, or `type(o)` + +- `def anno_ret(func)` + Get the return annotation of `func` + +- `def signature_ex(obj, eval_str)` + Backport of `inspect.signature(..., eval_str=True` to `True`) + +- `def str2int(s)` + Convert `s` to an `int` + +- `def str2float(s)` + Convert `s` to a float + +- `def str2list(s)` + Convert `s` to a list + +- `def str2date(s)` + `date.fromisoformat` with empty string handling + +- `def typed(_func)` + Decorator to check param and return types at runtime, with optional casting + +- `def exec_new(code)` + Execute `code` in a new environment and return it + +- `def exec_import(mod, sym)` + Import `sym` from `mod` in a new environment + +## fastcore.dispatch + +> Basic single and dual parameter dispatch + +- `def lenient_issubclass(cls, types)` + If possible return whether `cls` is a subclass of `types`, otherwise return False. + +- `def sorted_topologically(iterable)` + Return a new list containing all items from the iterable sorted topologically + +- `class TypeDispatch` + Dictionary-like object; `__getitem__` matches keys of types using `issubclass` + + - `def __init__(self, funcs, bases)` + - `def add(self, f)` + Add type `t` and function `f` + + - `def first(self)` + Get first function in ordered dict of type:func. + + - `def returns(self, x)` + Get the return type of annotation of `x`. + + - `def __repr__(self)` + - `def __call__(self, *args, **kwargs)` + - `def __get__(self, inst, owner)` + - `def __getitem__(self, k)` + Find first matching type that is a super-class of `k` + + +- `class DispatchReg` + A global registry for `TypeDispatch` objects keyed by function name + + - `def __init__(self)` + - `def __call__(self, f)` + +- `def retain_meta(x, res, as_copy)` + Call `res.set_meta(x)`, if it exists + +- `def default_set_meta(self, x, as_copy)` + Copy over `_meta` from `x` to `res`, if it's missing + +- `@typedispatch def cast(x, typ)` + cast `x` to type `typ` (may also change `x` inplace) + +- `def retain_type(new, old, typ, as_copy)` + Cast `new` to type of `old` or `typ` if it's a superclass + +- `def retain_types(new, old, typs)` + Cast each item of `new` to type of matching item in `old` if it's a superclass + +- `def explode_types(o)` + Return the type of `o`, potentially in nested dictionaries for thing that are listy + +## fastcore.docments + +> Document parameters using comments. + +- `def docstring(sym)` + Get docstring for `sym` for functions ad classes + +- `def parse_docstring(sym)` + Parse a numpy-style docstring in `sym` + +- `def isdataclass(s)` + Check if `s` is a dataclass but not a dataclass' instance + +- `def get_dataclass_source(s)` + Get source code for dataclass `s` + +- `def get_source(s)` + Get source code for string, function object or dataclass `s` + +- `def get_name(obj)` + Get the name of `obj` + +- `def qual_name(obj)` + Get the qualified name of `obj` + +- `@delegates(_docments) def docments(elt, full, **kwargs)` + Generates a `docment` + +- `def extract_docstrings(code)` + Create a dict from function/class/method names to tuples of docstrings and param lists + +## fastcore.docscrape + +> Parse numpy-style docstrings + +- `def strip_blank_lines(l)` + Remove leading and trailing blank lines from a list of lines + +- `class Reader` + A line-based string reader. + + - `def __init__(self, data)` + - `def __getitem__(self, n)` + - `def reset(self)` + - `def read(self)` + - `def seek_next_non_empty_line(self)` + - `def eof(self)` + - `def read_to_condition(self, condition_func)` + - `def read_to_next_empty_line(self)` + - `def read_to_next_unindented_line(self)` + - `def peek(self, n)` + - `def is_empty(self)` + +- `class ParseError` + - `def __str__(self)` + +- `class NumpyDocString` + Parses a numpydoc string to an abstract representation + + - `def __init__(self, docstring, config)` + - `def __iter__(self)` + - `def __len__(self)` + - `def __getitem__(self, key)` + - `def __setitem__(self, key, val)` + +- `def dedent_lines(lines, split)` + Deindent a list of lines maximally + +## fastcore.foundation + +> The `L` class and helpers for it + +- `@contextmanager def working_directory(path)` + Change working directory to `path` and return to previous on exit. + +- `def add_docs(cls, cls_doc, **docs)` + Copy values from `docs` to `cls` docstrings, and confirm all public methods are documented + +- `def docs(cls)` + Decorator version of `add_docs`, using `_docs` dict + +- `def coll_repr(c, max_n)` + String repr of up to `max_n` items of (possibly lazy) collection `c` + +- `def is_bool(x)` + Check whether `x` is a bool or None + +- `def mask2idxs(mask)` + Convert bool mask or index list to index `L` + +- `def is_indexer(idx)` + Test whether `idx` will index a single item in a list + +- `class CollBase` + Base class for composing a list of `items` + + - `def __init__(self, items)` + - `def __len__(self)` + - `def __getitem__(self, k)` + - `def __setitem__(self, k, v)` + - `def __delitem__(self, i)` + - `def __repr__(self)` + - `def __iter__(self)` + +- `class L` + Behaves like a list of `items` but can also index with list of indices or masks + + - `def __init__(self, items, *rest)` + - `def __getitem__(self, idx)` + - `def copy(self)` + - `def __setitem__(self, idx, o)` + Set `idx` (can be list of indices, or mask, or int) items to `o` (which is broadcast if not iterable) + + - `def __eq__(self, b)` + - `def sorted(self, key, reverse)` + - `def __iter__(self)` + - `def __contains__(self, b)` + - `def __reversed__(self)` + - `def __invert__(self)` + - `def __repr__(self)` + - `def __mul__(a, b)` + - `def __add__(a, b)` + - `def __radd__(a, b)` + - `def __addi__(a, b)` + - `@classmethod def split(cls, s, sep, maxsplit)` + - `@classmethod def range(cls, a, b, step)` + - `def map(self, f, *args, **kwargs)` + - `def argwhere(self, f, negate, **kwargs)` + - `def argfirst(self, f, negate)` + - `def filter(self, f, negate, **kwargs)` + - `def enumerate(self)` + - `def renumerate(self)` + - `def unique(self, sort, bidir, start)` + - `def val2idx(self)` + - `def cycle(self)` + - `def map_dict(self, f, *args, **kwargs)` + - `def map_first(self, f, g, *args, **kwargs)` + - `def itemgot(self, *idxs)` + - `def attrgot(self, k, default)` + - `def starmap(self, f, *args, **kwargs)` + - `def zip(self, cycled)` + - `def zipwith(self, *rest)` + - `def map_zip(self, f, *args, **kwargs)` + - `def map_zipwith(self, f, *rest, **kwargs)` + - `def shuffle(self)` + - `def concat(self)` + - `def reduce(self, f, initial)` + - `def sum(self)` + - `def product(self)` + - `def setattrs(self, attr, val)` + +- `def save_config_file(file, d, **kwargs)` + Write settings dict to a new config file, or overwrite the existing one. + +- `class Config` + Reading and writing `ConfigParser` ini files + + - `def __init__(self, cfg_path, cfg_name, create, save, extra_files, types)` + - `def __repr__(self)` + - `def __setitem__(self, k, v)` + - `def __contains__(self, k)` + - `def save(self)` + - `def __getattr__(self, k)` + - `def __getitem__(self, k)` + - `def get(self, k, default)` + - `def path(self, k, default)` + - `@classmethod def find(cls, cfg_name, cfg_path, **kwargs)` + Search `cfg_path` and its parents to find `cfg_name` + + +## fastcore.imghdr + +> Recognize image file formats based on their first few bytes. + +- `def test_jpeg(h, f)` + JPEG data with JFIF or Exif markers; and raw JPEG + +- `def test_gif(h, f)` + GIF ('87 and '89 variants) + +- `def test_tiff(h, f)` + TIFF (can be in Motorola or Intel byte order) + +- `def test_rgb(h, f)` + SGI image library + +- `def test_pbm(h, f)` + PBM (portable bitmap) + +- `def test_pgm(h, f)` + PGM (portable graymap) + +- `def test_ppm(h, f)` + PPM (portable pixmap) + +- `def test_rast(h, f)` + Sun raster file + +- `def test_xbm(h, f)` + X bitmap (X10 or X11) + +## fastcore.imports + +- `def is_iter(o)` + Test whether `o` can be used in a `for` loop + +- `def is_coll(o)` + Test whether `o` is a collection (i.e. has a usable `len`) + +- `def all_equal(a, b)` + Compares whether `a` and `b` are the same length and have the same contents + +- `def noop(x, *args, **kwargs)` + Do nothing + +- `def noops(self, x, *args, **kwargs)` + Do nothing (method) + +- `def isinstance_str(x, cls_name)` + Like `isinstance`, except takes a type name instead of a type + +- `def equals(a, b)` + Compares `a` and `b` for equality; supports sublists, tensors and arrays too + +- `def ipython_shell()` + Same as `get_ipython` but returns `False` if not in IPython + +- `def in_ipython()` + Check if code is running in some kind of IPython environment + +- `def in_colab()` + Check if the code is running in Google Colaboratory + +- `def in_jupyter()` + Check if the code is running in a jupyter notebook + +- `def in_notebook()` + Check if the code is running in a jupyter notebook + +- `def remove_prefix(text, prefix)` + Temporary until py39 is a prereq + +- `def remove_suffix(text, suffix)` + Temporary until py39 is a prereq + +## fastcore.meta + +> Metaclasses + +- `def test_sig(f, b)` + Test the signature of an object + +- `class FixSigMeta` + A metaclass that fixes the signature on classes that override `__new__` + + - `def __new__(cls, name, bases, dict)` + +- `class PrePostInitMeta` + A metaclass that calls optional `__pre_init__` and `__post_init__` methods + + - `def __call__(cls, *args, **kwargs)` + +- `class AutoInit` + Same as `object`, but no need for subclasses to call `super().__init__` + + - `def __pre_init__(self, *args, **kwargs)` + +- `class NewChkMeta` + Metaclass to avoid recreating object passed to constructor + + - `def __call__(cls, x, *args, **kwargs)` + +- `class BypassNewMeta` + Metaclass: casts `x` to this class if it's of type `cls._bypass_type` + + - `def __call__(cls, x, *args, **kwargs)` + +- `def empty2none(p)` + Replace `Parameter.empty` with `None` + +- `def anno_dict(f)` + `__annotation__ dictionary with `empty` cast to `None`, returning empty if doesn't exist + +- `def use_kwargs_dict(keep, **kwargs)` + Decorator: replace `**kwargs` in signature with `names` params + +- `def use_kwargs(names, keep)` + Decorator: replace `**kwargs` in signature with `names` params + +- `def delegates(to, keep, but)` + Decorator: replace `**kwargs` in signature with params from `to` + +- `def method(f)` + Mark `f` as a method + +- `def funcs_kwargs(as_method)` + Replace methods in `cls._methods` with those from `kwargs` + +## fastcore.net + +> Network, HTTP, and URL functions + +- `def urlquote(url)` + Update url's path with `urllib.parse.quote` + +- `def urlwrap(url, data, headers)` + Wrap `url` in a urllib `Request` with `urlquote` + +- `class HTTP4xxClientError` + Base class for client exceptions (code 4xx) from `url*` functions + + +- `class HTTP5xxServerError` + Base class for server exceptions (code 5xx) from `url*` functions + + +- `def urlopen(url, data, headers, timeout, **kwargs)` + Like `urllib.request.urlopen`, but first `urlwrap` the `url`, and encode `data` + +- `def urlread(url, data, headers, decode, return_json, return_headers, timeout, **kwargs)` + Retrieve `url`, using `data` dict or `kwargs` to `POST` if present + +- `def urljson(url, data, timeout)` + Retrieve `url` and decode json + +- `def urlclean(url)` + Remove fragment, params, and querystring from `url` if present + +- `def urlsave(url, dest, reporthook, headers, timeout)` + Retrieve `url` and save based on its name + +- `def urlvalid(x)` + Test if `x` is a valid URL + +- `def urlrequest(url, verb, headers, route, query, data, json_data)` + `Request` for `url` with optional route params replaced by `route`, plus `query` string, and post `data` + +- `@patch def summary(self, skip)` + Summary containing full_url, headers, method, and data, removing `skip` from headers + +- `def urlsend(url, verb, headers, decode, route, query, data, json_data, return_json, return_headers, debug, timeout)` + Send request with `urlrequest`, converting result to json if `return_json` + +- `def do_request(url, post, headers, **data)` + Call GET or json-encoded POST on `url`, depending on `post` + +- `def start_server(port, host, dgram, reuse_addr, n_queue)` + Create a `socket` server on `port`, with optional `host`, of type `dgram` + +- `def start_client(port, host, dgram)` + Create a `socket` client on `port`, with optional `host`, of type `dgram` + +- `def tobytes(s)` + Convert `s` into HTTP-ready bytes format + +- `def http_response(body, status, hdrs, **kwargs)` + Create an HTTP-ready response, adding `kwargs` to `hdrs` + +- `@threaded def recv_once(host, port)` + Spawn a thread to receive a single HTTP request and store in `d['r']` + +## fastcore.parallel + +> Threading and multiprocessing functions + +- `def threaded(process)` + Run `f` in a `Thread` (or `Process` if `process=True`), and returns it + +- `def startthread(f)` + Like `threaded`, but start thread immediately + +- `def startproc(f)` + Like `threaded(True)`, but start Process immediately + +- `class ThreadPoolExecutor` + Same as Python's ThreadPoolExecutor, except can pass `max_workers==0` for serial execution + + - `def __init__(self, max_workers, on_exc, pause, **kwargs)` + - `def map(self, f, items, *args, **kwargs)` + +- `@delegates() class ProcessPoolExecutor` + Same as Python's ProcessPoolExecutor, except can pass `max_workers==0` for serial execution + + - `def __init__(self, max_workers, on_exc, pause, **kwargs)` + - `def map(self, f, items, *args, **kwargs)` + +- `def parallel(f, items, *args, **kwargs)` + Applies `func` in parallel to `items`, using `n_workers` + +- `def parallel_async(f, items, *args, **kwargs)` + Applies `f` to `items` in parallel using asyncio and a semaphore to limit concurrency. + +- `def run_procs(f, f_done, args)` + Call `f` for each item in `args` in parallel, yielding `f_done` + +- `def parallel_gen(cls, items, n_workers, **kwargs)` + Instantiate `cls` in `n_workers` procs & call each on a subset of `items` in parallel. + +## fastcore.py2pyi + +- `def imp_mod(module_path, package)` + Import dynamically the module referenced in `fn` + +- `def has_deco(node, name)` + Check if a function node `node` has a decorator named `name` + +- `def create_pyi(fn, package)` + Convert `fname.py` to `fname.pyi` by removing function bodies and expanding `delegates` kwargs + +- `@call_parse def py2pyi(fname, package)` + Convert `fname.py` to `fname.pyi` by removing function bodies and expanding `delegates` kwargs + +- `@call_parse def replace_wildcards(path)` + Expand wildcard imports in the specified Python file. + +## fastcore.script + +> A fast way to turn your python function into a script. + +- `def store_true()` + Placeholder to pass to `Param` for `store_true` action + +- `def store_false()` + Placeholder to pass to `Param` for `store_false` action + +- `def bool_arg(v)` + Use as `type` for `Param` to get `bool` behavior + +- `class Param` + A parameter in a function used in `anno_parser` or `call_parse` + + - `def __init__(self, help, type, opt, action, nargs, const, choices, required, default)` + - `def set_default(self, d)` + - `@property def pre(self)` + - `@property def kwargs(self)` + - `def __repr__(self)` + +- `def anno_parser(func, prog)` + Look at params (annotated with `Param`) in func and return an `ArgumentParser` + +- `def args_from_prog(func, prog)` + Extract args from `prog` + +- `def call_parse(func, nested)` + Decorator to create a simple CLI from `func` using `anno_parser` + +## fastcore.style + +> Fast styling for friendly CLIs. + +- `class StyleCode` + An escape sequence for styling terminal text. + + - `def __init__(self, name, code, typ)` + - `def __str__(self)` + +- `class Style` + A minimal terminal text styler. + + - `def __init__(self, codes)` + - `def __dir__(self)` + - `def __getattr__(self, k)` + - `def __call__(self, obj)` + - `def __repr__(self)` + +- `def demo()` + Demonstrate all available styles and their codes. + +## fastcore.test + +> Helper functions to quickly write tests in notebooks + +- `def test_fail(f, msg, contains, args, kwargs)` + Fails with `msg` unless `f()` raises an exception and (optionally) has `contains` in `e.args` + +- `def test(a, b, cmp, cname)` + `assert` that `cmp(a,b)`; display inputs and `cname or cmp.__name__` if it fails + +- `def nequals(a, b)` + Compares `a` and `b` for `not equals` + +- `def test_eq(a, b)` + `test` that `a==b` + +- `def test_eq_type(a, b)` + `test` that `a==b` and are same type + +- `def test_ne(a, b)` + `test` that `a!=b` + +- `def is_close(a, b, eps)` + Is `a` within `eps` of `b` + +- `def test_close(a, b, eps)` + `test` that `a` is within `eps` of `b` + +- `def test_is(a, b)` + `test` that `a is b` + +- `def test_shuffled(a, b)` + `test` that `a` and `b` are shuffled versions of the same sequence of items + +- `def test_stdout(f, exp, regex)` + Test that `f` prints `exp` to stdout, optionally checking as `regex` + +- `def test_fig_exists(ax)` + Test there is a figure displayed in `ax` + +- `class ExceptionExpected` + Context manager that tests if an exception is raised + + - `def __init__(self, ex, regex)` + - `def __enter__(self)` + - `def __exit__(self, type, value, traceback)` + +## fastcore.transform + +> Definition of `Transform` and `Pipeline` + +- `class Transform` + Delegates (`__call__`,`decode`,`setup`) to (encodes,decodes,setups) if `split_idx` matches + + - `def __init__(self, enc, dec, split_idx, order)` + - `@property def name(self)` + - `def __call__(self, x, **kwargs)` + - `def decode(self, x, **kwargs)` + - `def __repr__(self)` + - `def setup(self, items, train_setup)` + +- `class InplaceTransform` + A `Transform` that modifies in-place and just returns whatever it's passed + + +- `class DisplayedTransform` + A transform with a `__repr__` that shows its attrs + + - `@property def name(self)` + +- `class ItemTransform` + A transform that always take tuples as items + + - `def __call__(self, x, **kwargs)` + - `def decode(self, x, **kwargs)` + +- `def get_func(t, name, *args, **kwargs)` + Get the `t.name` (potentially partial-ized with `args` and `kwargs`) or `noop` if not defined + +- `class Func` + Basic wrapper around a `name` with `args` and `kwargs` to call on a given type + + - `def __init__(self, name, *args, **kwargs)` + - `def __repr__(self)` + - `def __call__(self, t)` + +- `def compose_tfms(x, tfms, is_enc, reverse, **kwargs)` + Apply all `func_nm` attribute of `tfms` on `x`, maybe in `reverse` order + +- `def mk_transform(f)` + Convert function `f` to `Transform` if it isn't already one + +- `def gather_attrs(o, k, nm)` + Used in __getattr__ to collect all attrs `k` from `self.{nm}` + +- `def gather_attr_names(o, nm)` + Used in __dir__ to collect all attrs `k` from `self.{nm}` + +- `class Pipeline` + A pipeline of composed (for encode/decode) transforms, setup with types + + - `def __init__(self, funcs, split_idx)` + - `def setup(self, items, train_setup)` + - `def add(self, ts, items, train_setup)` + - `def __call__(self, o)` + - `def __repr__(self)` + - `def __getitem__(self, i)` + - `def __setstate__(self, data)` + - `def __getattr__(self, k)` + - `def __dir__(self)` + - `def decode(self, o, full)` + - `def show(self, o, ctx, **kwargs)` + +## fastcore.xdg + +> XDG Base Directory Specification helpers. + +- `def xdg_cache_home()` + Path corresponding to `XDG_CACHE_HOME` + +- `def xdg_config_dirs()` + Paths corresponding to `XDG_CONFIG_DIRS` + +- `def xdg_config_home()` + Path corresponding to `XDG_CONFIG_HOME` + +- `def xdg_data_dirs()` + Paths corresponding to XDG_DATA_DIRS` + +- `def xdg_data_home()` + Path corresponding to `XDG_DATA_HOME` + +- `def xdg_runtime_dir()` + Path corresponding to `XDG_RUNTIME_DIR` + +- `def xdg_state_home()` + Path corresponding to `XDG_STATE_HOME` + +## fastcore.xml + +> Concise generation of XML. + +- `class FT` + A 'Fast Tag' structure, containing `tag`,`children`,and `attrs` + + - `def __init__(self, tag, cs, attrs, void_, **kwargs)` + - `def on(self, f)` + - `def changed(self)` + - `def __setattr__(self, k, v)` + - `def __getattr__(self, k)` + - `@property def list(self)` + - `def get(self, k, default)` + - `def __repr__(self)` + - `def __iter__(self)` + - `def __getitem__(self, idx)` + - `def __setitem__(self, i, o)` + - `def __call__(self, *c, **kw)` + - `def set(self, *c, **kw)` + Set children and/or attributes (chainable) + + +- `def ft(tag, *c, **kw)` + Create an `FT` structure for `to_xml()` + +- `def Html(*c, **kwargs)` + An HTML tag, optionally preceeded by `!DOCTYPE HTML` + +- `class Safe` + - `def __html__(self)` + +- `def to_xml(elm, lvl, indent, do_escape)` + Convert `ft` element tree into an XML string + +- `def highlight(s, lang)` + Markdown to syntax-highlight `s` in language `lang` + +## fastcore.xtras + +> Utility functions used in the fastai library + +- `def walk(path, symlinks, keep_file, keep_folder, skip_folder, func, ret_folders)` + Generator version of `os.walk`, using functions to filter files and folders + +- `def globtastic(path, recursive, symlinks, file_glob, file_re, folder_re, skip_file_glob, skip_file_re, skip_folder_re, func, ret_folders)` + A more powerful `glob`, including regex matches, symlink handling, and skip parameters + +- `@contextmanager def maybe_open(f, mode, **kwargs)` + Context manager: open `f` if it is a path (and close on exit) + +- `def mkdir(path, exist_ok, parents, overwrite, **kwargs)` + Creates and returns a directory defined by `path`, optionally removing previous existing directory if `overwrite` is `True` + +- `def image_size(fn)` + Tuple of (w,h) for png, gif, or jpg; `None` otherwise + +- `def bunzip(fn)` + bunzip `fn`, raising exception if output already exists + +- `def loads(s, **kw)` + Same as `json.loads`, but handles `None` + +- `def loads_multi(s)` + Generator of >=0 decoded json dicts, possibly with non-json ignored text at start and end + +- `def dumps(obj, **kw)` + Same as `json.dumps`, but uses `ujson` if available + +- `def untar_dir(fname, dest, rename, overwrite)` + untar `file` into `dest`, creating a directory if the root contains more than one item + +- `def repo_details(url)` + Tuple of `owner,name` from ssh or https git repo `url` + +- `def run(cmd, *rest)` + Pass `cmd` (splitting with `shlex` if string) to `subprocess.run`; return `stdout`; raise `IOError` if fails + +- `def open_file(fn, mode, **kwargs)` + Open a file, with optional compression if gz or bz2 suffix + +- `def save_pickle(fn, o)` + Save a pickle file, to a file name or opened file + +- `def load_pickle(fn)` + Load a pickle file from a file name or opened file + +- `def parse_env(s, fn)` + Parse a shell-style environment string or file + +- `def expand_wildcards(code)` + Expand all wildcard imports in the given code string. + +- `def dict2obj(d, list_func, dict_func)` + Convert (possibly nested) dicts (or lists of dicts) to `AttrDict` + +- `def obj2dict(d)` + Convert (possibly nested) AttrDicts (or lists of AttrDicts) to `dict` + +- `def repr_dict(d)` + Print nested dicts and lists, such as returned by `dict2obj` + +- `def is_listy(x)` + `isinstance(x, (tuple,list,L,slice,Generator))` + +- `def mapped(f, it)` + map `f` over `it`, unless it's not listy, in which case return `f(it)` + +- `@patch def readlines(self, hint, encoding)` + Read the content of `self` + +- `@patch def read_json(self, encoding, errors)` + Same as `read_text` followed by `loads` + +- `@patch def mk_write(self, data, encoding, errors, mode)` + Make all parent dirs of `self`, and write `data` + +- `@patch def relpath(self, start)` + Same as `os.path.relpath`, but returns a `Path`, and resolves symlinks + +- `@patch def ls(self, n_max, file_type, file_exts)` + Contents of path as a list + +- `@patch def delete(self)` + Delete a file, symlink, or directory tree + +- `class IterLen` + Base class to add iteration to anything supporting `__len__` and `__getitem__` + + - `def __iter__(self)` + +- `@docs class ReindexCollection` + Reindexes collection `coll` with indices `idxs` and optional LRU cache of size `cache` + + - `def __init__(self, coll, idxs, cache, tfm)` + - `def __getitem__(self, i)` + - `def __len__(self)` + - `def reindex(self, idxs)` + - `def shuffle(self)` + - `def cache_clear(self)` + - `def __getstate__(self)` + - `def __setstate__(self, s)` + +- `def get_source_link(func)` + Return link to `func` in source code + +- `def truncstr(s, maxlen, suf, space)` + Truncate `s` to length `maxlen`, adding suffix `suf` if truncated + +- `def sparkline(data, mn, mx, empty_zero)` + Sparkline for `data`, with `None`s (and zero, if `empty_zero`) shown as empty column + +- `def modify_exception(e, msg, replace)` + Modifies `e` with a custom message attached + +- `def round_multiple(x, mult, round_down)` + Round `x` to nearest multiple of `mult` + +- `def set_num_threads(nt)` + Get numpy (and others) to use `nt` threads + +- `def join_path_file(file, path, ext)` + Return `path/file` if file is a string or a `Path`, file otherwise + +- `def autostart(g)` + Decorator that automatically starts a generator + +- `class EventTimer` + An event timer with history of `store` items of time `span` + + - `def __init__(self, store, span)` + - `def add(self, n)` + Record `n` events + + - `@property def duration(self)` + - `@property def freq(self)` + +- `def stringfmt_names(s)` + Unique brace-delimited names in `s` + +- `class PartialFormatter` + A `string.Formatter` that doesn't error on missing fields, and tracks missing fields and unused args + + - `def __init__(self)` + - `def get_field(self, nm, args, kwargs)` + - `def check_unused_args(self, used, args, kwargs)` + +- `def partial_format(s, **kwargs)` + string format `s`, ignoring missing field errors, returning missing and extra fields + +- `def utc2local(dt)` + Convert `dt` from UTC to local time + +- `def local2utc(dt)` + Convert `dt` from local to UTC time + +- `def trace(f)` + Add `set_trace` to an existing function `f` + +- `@contextmanager def modified_env(*delete, **replace)` + Context manager temporarily modifying `os.environ` by deleting `delete` and replacing `replace` + +- `class ContextManagers` + Wrapper for `contextlib.ExitStack` which enters a collection of context managers + + - `def __init__(self, mgrs)` + - `def __enter__(self)` + - `def __exit__(self, *args, **kwargs)` + +- `def shufflish(x, pct)` + Randomly relocate items of `x` up to `pct` of `len(x)` from their starting location + +- `def console_help(libname)` + Show help for all console scripts from `libname` + +- `def hl_md(s, lang, show)` + Syntax highlight `s` using `lang`. + +- `def type2str(typ)` + Stringify `typ` + +- `class Unset` + - `def __repr__(self)` + - `def __str__(self)` + - `def __bool__(self)` + - `@property def name(self)` + +- `def nullable_dc(cls)` + Like `dataclass`, but default of `UNSET` added to fields without defaults + +- `def flexiclass(cls)` + Convert `cls` into a `dataclass` like `make_nullable`. Converts in place and also returns the result. + +- `def asdict(o)` + Convert `o` to a `dict`, supporting dataclasses, namedtuples, iterables, and `__dict__` attrs. + +- `def is_typeddict(cls)` + Check if `cls` is a `TypedDict` + +- `def is_namedtuple(cls)` + `True` if `cls` is a namedtuple type + +- `def flexicache(*funcs)` + Like `lru_cache`, but customisable with policy `funcs` + +- `def time_policy(seconds)` + A `flexicache` policy that expires cached items after `seconds` have passed + +- `def mtime_policy(filepath)` + A `flexicache` policy that expires cached items after `filepath` modified-time changes + +- `def timed_cache(seconds, maxsize)` + Like `lru_cache`, but also with time-based eviction + \ No newline at end of file diff --git a/llms.txt b/llms.txt new file mode 100644 index 00000000..034ac932 --- /dev/null +++ b/llms.txt @@ -0,0 +1,43 @@ +# fastcore + +fastcore adds to Python features inspired by other languages, like multiple dispatch from Julia, mixins from Ruby, and currying, binding, and more from Haskell. It also adds some “missing features” and clean up some rough edges in the Python standard library, such as simplifying parallel processing, and bringing ideas from NumPy over to Python’s list type. + +Here are some tips on using fastcore: + +- **Liberal imports**: Utilize `from fastcore.module import *` freely. The library is designed for safe wildcard imports. +- **Enhanced list operations**: Substitute `list` with `L`. This provides advanced indexing, method chaining, and additional functionality while maintaining list-like behavior. +- **Extend existing classes**: Apply the `@patch` decorator to add methods to classes, including built-ins, without subclassing. This enables more flexible code organization. +- **Streamline class initialization**: In `__init__` methods, use `store_attr()` to efficiently set multiple attributes, reducing repetitive assignment code. +- **Explicit keyword arguments**: Apply the `delegates` decorator to functions to replace `**kwargs` with specific parameters, enhancing IDE support and documentation. +- **Optimize parallel execution**: Leverage fastcore's enhanced `ThreadPoolExecutor` and `ProcessPoolExecutor` for simplified concurrent processing. +- **Expressive testing**: Prefer fastcore's testing functions like `test_eq`, `test_ne`, `test_close` for more readable and informative test assertions. +- **Advanced file operations**: Use the extended `Path` class, which adds methods like `ls()`, `read_json()`, and others to `pathlib.Path`. +- **Flexible data structures**: Convert between dictionaries and attribute-access objects using `dict2obj` and `obj2dict` for more intuitive data handling. +- **Data pipeline construction**: Employ `Transform` and `Pipeline` classes to create modular, composable data processing workflows. +- **Functional programming paradigms**: Utilize tools like `compose`, `maps`, and `filter_ex` to write more functional-style Python code. +- **Documentation**: Use `docments` where possible to document parameters of functions and methods. +- **Time-aware caching**: Apply the `timed_cache` decorator to add time-based expiration to the standard `lru_cache` functionality. +- **Simplified CLI creation**: Use fastcore's console script utilities to easily transform Python functions into command-line interfaces. + +## Tutorials + +- [Fastcore Quick Tour](https://fastcore.fast.ai/tour.html.md): A quick tour of a few higlights from fastcore. +- [Blog Post](https://gist.githubusercontent.com/hamelsmu/ea9e0519d9a94a4203bcc36043eb01c5/raw/6c0c96a2823d67aecc103206d6ab21c05dcd520a/fastcore:_an_underrated_python_library.md): A tour of some of the features of fastcore. + +## API + +- [API List](https://fastcore.fast.ai/apilist.txt): A succint list of all functions and methods in fastcore. + +## Optional + +- [fastcore.test](https://fastcore.fast.ai/test.html.md): Simple testing functions +- [fastcore.basics](https://fastcore.fast.ai/basics.html.md): Basic functionality used in the fastai library. +- [fastcore.foundation](https://fastcore.fast.ai/foundation.html.md): The L class and helpers for it +- [fastcore.xtras](https://fastcore.fast.ai/xtras.html.md): Utility functions used in the fastai library +- [fastcore.parallel](https://fastcore.fast.ai/parallel.html.md):parallel processing +- [fastcore.net](https://fastcore.fast.ai/net.html.md): testing utilities +- [fastcore.docments](https://fastcore.fast.ai/docments.html.md): documentation utilities +- [fastcore.meta](https://fastcore.fast.ai/meta.html.md): metaclasses +- [fastcore.script](https://fastcore.fast.ai/script.html.md): CLI script utilities +- [fastcore.xdg](https://fastcore.fast.ai/xdg.html.md): XDG Base Directory Specification helpers. +- [fastcore.xml](https://fastcore.fast.ai/xml.html.md): concise generation of XML \ No newline at end of file diff --git a/meta.html b/meta.html new file mode 100644 index 00000000..aa4e1a79 --- /dev/null +++ b/meta.html @@ -0,0 +1,1309 @@ + + + + + + + + + + +Meta – fastcore + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ +
+ + +
+ + + +
+ +
+
+

Meta

+
+ +
+
+ Metaclasses +
+
+ + +
+ + + + +
+ + + +
+ + + +
+
from fastcore.foundation import *
+from nbdev.showdoc import *
+from fastcore.nb_imports import *
+
+

See this blog post for more information about metaclasses.

+
    +
  • FixSigMeta preserves information that enables intropsection of signatures (i.e. tab completion in IDEs) when certain types of inheritence would otherwise obfuscate this introspection.
  • +
  • PrePostInitMeta ensures that the classes defined with it run __pre_init__ and __post_init__ (without having to write self.__pre_init__() and self.__post_init__() in the actual init
  • +
  • NewChkMeta gives the PrePostInitMeta functionality and ensures classes defined with it don’t re-create an object of their type whenever it’s passed to the constructor
  • +
  • BypassNewMeta ensures classes defined with it can easily be casted form objects they subclass.
  • +
+
+

source

+
+

test_sig

+
+
 test_sig (f, b)
+
+

Test the signature of an object

+
+
def func_1(h,i,j): pass
+def func_2(h,i=3, j=[5,6]): pass
+
+class T:
+    def __init__(self, a, b): pass
+
+test_sig(func_1, '(h, i, j)')
+test_sig(func_2, '(h, i=3, j=[5, 6])')
+test_sig(T, '(a, b)')
+
+
+

source

+
+
+

FixSigMeta

+
+
 FixSigMeta (name, bases, dict)
+
+

A metaclass that fixes the signature on classes that override __new__

+

When you inherit from a class that defines __new__, or a metaclass that defines __call__, the signature of your __init__ method is obfuscated such that tab completion no longer works. FixSigMeta fixes this issue and restores signatures.

+

To understand what FixSigMeta does, it is useful to inspect an object’s signature. You can inspect the signature of an object with inspect.signature:

+
+
class T:
+    def __init__(self, a, b, c): pass
+    
+inspect.signature(T)
+
+
<Signature (a, b, c)>
+
+
+

This corresponds to tab completion working in the normal way:

+

Tab completion in a Jupyter Notebook.

+

However, when you inherhit from a class that defines __new__ or a metaclass that defines __call__ this obfuscates the signature by overriding your class with the signature of __new__, which prevents tab completion from displaying useful information:

+
+
class Foo:
+    def __new__(self, **args): pass
+
+class Bar(Foo):
+    def __init__(self, d, e, f): pass
+    
+inspect.signature(Bar)
+
+
<Signature (d, e, f)>
+
+
+

Tab completion in a Jupyter Notebook.

+

Finally, the signature and tab completion can be restored by inheriting from the metaclass FixSigMeta as shown below:

+
+
class Bar(Foo, metaclass=FixSigMeta):
+    def __init__(self, d, e, f): pass
+    
+test_sig(Bar, '(d, e, f)')
+inspect.signature(Bar)
+
+
<Signature (d, e, f)>
+
+
+

Tab completion in a Jupyter Notebook.

+

If you need to define a metaclass that overrides __call__ (as done in PrePostInitMeta), you need to inherit from FixSigMeta instead of type when constructing the metaclass to preserve the signature in __init__. Be careful not to override __new__ when doing this:

+
+
class TestMeta(FixSigMeta):
+    # __new__ comes from FixSigMeta
+    def __call__(cls, *args, **kwargs): pass
+    
+class T(metaclass=TestMeta):
+    def __init__(self, a, b): pass
+    
+test_sig(T, '(a, b)')
+
+

On the other hand, if you fail to inherit from FixSigMeta when inheriting from a metaclass that overrides __call__, your signature will reflect that of __call__ instead (which is often undesirable):

+
+
class GenericMeta(type):
+    "A boilerplate metaclass that doesn't do anything for testing."
+    def __new__(cls, name, bases, dict):
+        return super().__new__(cls, name, bases, dict)
+    def __call__(cls, *args, **kwargs): pass
+
+class T2(metaclass=GenericMeta):
+    def __init__(self, a, b): pass
+
+# We can avoid this by inheriting from the metaclass `FixSigMeta`
+test_sig(T2, '(*args, **kwargs)')
+
+
+

source

+
+
+

PrePostInitMeta

+
+
 PrePostInitMeta (name, bases, dict)
+
+

A metaclass that calls optional __pre_init__ and __post_init__ methods

+

__pre_init__ and __post_init__ are useful for initializing variables or performing tasks prior to or after __init__ being called, respectively. Fore example:

+
+
class _T(metaclass=PrePostInitMeta):
+    def __pre_init__(self):  self.a  = 0; 
+    def __init__(self,b=0):  self.b = self.a + 1; assert self.b==1
+    def __post_init__(self): self.c = self.b + 2; assert self.c==3
+
+t = _T()
+test_eq(t.a, 0) # set with __pre_init__
+test_eq(t.b, 1) # set with __init__
+test_eq(t.c, 3) # set with __post_init__
+
+

One use for PrePostInitMeta is avoiding the __super__().__init__() boilerplate associated with subclassing, such as used in AutoInit.

+
+

source

+
+
+

AutoInit

+
+
 AutoInit (*args, **kwargs)
+
+

Same as object, but no need for subclasses to call super().__init__

+

This is normally used as a mixin, eg:

+
+
class TestParent():
+    def __init__(self): self.h = 10
+        
+class TestChild(AutoInit, TestParent):
+    def __init__(self): self.k = self.h + 2
+    
+t = TestChild()
+test_eq(t.h, 10) # h=10 is initialized in the parent class
+test_eq(t.k, 12)
+
+
+

source

+
+
+

NewChkMeta

+
+
 NewChkMeta (name, bases, dict)
+
+

Metaclass to avoid recreating object passed to constructor

+

NewChkMeta is used when an object of the same type is the first argument to your class’s constructor (i.e. the __init__ function), and you would rather it not create a new object but point to the same exact object.

+

This is used in L, for example, to avoid creating a new object when the object is already of type L. This allows the users to defenisvely instantiate an L object and just return a reference to the same object if it already happens to be of type L.

+

For example, the below class _T optionally accepts an object o as its first argument. A new object is returned upon instantiation per usual:

+
+
class _T():
+    "Testing"
+    def __init__(self, o): 
+        # if `o` is not an object without an attribute `foo`, set foo = 1
+        self.foo = getattr(o,'foo',1)
+
+
+
t = _T(3)
+test_eq(t.foo,1) # 1 was not of type _T, so foo = 1
+
+t2 = _T(t) #t1 is of type _T
+assert t is not t2 # t1 and t2 are different objects
+
+

However, if we want _T to return a reference to the same object when passed an an object of type _T we can inherit from the NewChkMeta class as illustrated below:

+
+
class _T(metaclass=NewChkMeta):
+    "Testing with metaclass NewChkMeta"
+    def __init__(self, o=None, b=1):
+        # if `o` is not an object without an attribute `foo`, set foo = 1
+        self.foo = getattr(o,'foo',1)
+        self.b = b
+
+

We can now test t and t2 are now pointing at the same object when using this new definition of _T:

+
+
t = _T(3)
+test_eq(t.foo,1) # 1 was not of type _T, so foo = 1
+
+t2 = _T(t) # t2 will now reference t
+
+test_is(t, t2) # t and t2 are the same object
+t2.foo = 5 # this will also change t.foo to 5 because it is the same object
+test_eq(t.foo, 5)
+test_eq(t2.foo, 5)
+
+

However, there is one exception to how NewChkMeta works. If you pass any additional arguments in the constructor a new object is returned, even if the first object is of the same type. For example, consider the below example where we pass the additional argument b into the constructor:

+
+
t3 = _T(t, b=1)
+assert t3 is not t
+
+t4 = _T(t) # without any arguments the constructor will return a reference to the same object
+assert t4 is t
+
+

Finally, it should be noted that NewChkMeta as well as all other metaclases in this section, inherit from FixSigMeta. This means class signatures will always be preserved when inheriting from this metaclass (see docs for FixSigMeta for more details):

+
+
test_sig(_T, '(o=None, b=1)')
+
+
+

source

+
+
+

BypassNewMeta

+
+
 BypassNewMeta (name, bases, dict)
+
+

Metaclass: casts x to this class if it’s of type cls._bypass_type

+

BypassNewMeta is identical to NewChkMeta, except for checking for a class as the same type, we instead check for a class of type specified in attribute _bypass_type.

+

In NewChkMeta, objects of the same type passed to the constructor (without arguments) would result into a new variable referencing the same object. However, with BypassNewMeta this only occurs if the type matches the _bypass_type of the class you are defining:

+
+
class _TestA: pass
+class _TestB: pass
+
+class _T(_TestA, metaclass=BypassNewMeta):
+    _bypass_type=_TestB
+    def __init__(self,x): self.x=x
+
+

In the below example, t does not refer to t2 because t is of type _TestA while _T._bypass_type is of type TestB:

+
+
t = _TestA()
+t2 = _T(t)
+assert t is not t2
+
+

However, if t is set to _TestB to match _T._bypass_type, then both t and t2 will refer to the same object.

+
+
t = _TestB()
+t2 = _T(t)
+t2.new_attr = 15
+
+test_is(t, t2)
+# since t2 just references t these will be the same
+test_eq(t.new_attr, t2.new_attr)
+
+# likewise, chaning an attribute on t will also affect t2 because they both point to the same object.
+t.new_attr = 9
+test_eq(t2.new_attr, 9)
+
+
+
+

Metaprogramming

+
+

source

+
+

empty2none

+
+
 empty2none (p)
+
+

Replace Parameter.empty with None

+
+

source

+
+
+

anno_dict

+
+
 anno_dict (f)
+
+

__annotation__ dictionary withemptycast toNone`, returning empty if doesn’t exist

+
+
def _f(a:int, b:L)->str: ...
+test_eq(anno_dict(_f), {'a': int, 'b': L, 'return': str})
+
+
+

source

+
+
+

use_kwargs_dict

+
+
 use_kwargs_dict (keep=False, **kwargs)
+
+

Decorator: replace **kwargs in signature with names params

+

Replace all **kwargs with named arguments like so:

+
+
@use_kwargs_dict(y=1,z=None)
+def foo(a, b=1, **kwargs): pass
+
+test_sig(foo, '(a, b=1, *, y=1, z=None)')
+
+

Add named arguments, but optionally keep **kwargs by setting keep=True:

+
+
@use_kwargs_dict(y=1,z=None, keep=True)
+def foo(a, b=1, **kwargs): pass
+
+test_sig(foo, '(a, b=1, *, y=1, z=None, **kwargs)')
+
+
+

source

+
+
+

use_kwargs

+
+
 use_kwargs (names, keep=False)
+
+

Decorator: replace **kwargs in signature with names params

+

use_kwargs is different than use_kwargs_dict as it only replaces **kwargs with named parameters without any default values:

+
+
@use_kwargs(['y', 'z'])
+def foo(a, b=1, **kwargs): pass
+
+test_sig(foo, '(a, b=1, *, y=None, z=None)')
+
+

You may optionally keep the **kwargs argument in your signature by setting keep=True:

+
+
@use_kwargs(['y', 'z'], keep=True)
+def foo(a, *args, b=1, **kwargs): pass
+test_sig(foo, '(a, *args, b=1, y=None, z=None, **kwargs)')
+
+
+

source

+
+
+

delegates

+
+
 delegates (to:function=None, keep=False, but:list=None)
+
+

Decorator: replace **kwargs in signature with params from to

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TypeDefaultDetails
tofunctionNoneDelegatee
keepboolFalseKeep kwargs in decorated function?
butlistNoneExclude these parameters from signature
+

A common Python idiom is to accept **kwargs in addition to named parameters that are passed onto other function calls. It is especially common to use **kwargs when you want to give the user an option to override default parameters of any functions or methods being called by the parent function.

+

For example, suppose we have have a function foo that passes arguments to baz like so:

+
+
def baz(a, b:int=2, c:int=3): return a + b + c
+
+def foo(c, a, **kwargs):
+    return c + baz(a, **kwargs)
+
+assert foo(c=1, a=1) == 7
+
+

The problem with this approach is the api for foo is obfuscated. Users cannot introspect what the valid arguments for **kwargs are without reading the source code. When a user tries tries to introspect the signature of foo, they are presented with this:

+
+
inspect.signature(foo)
+
+
<Signature (c, a, **kwargs)>
+
+
+

We can address this issue by using the decorator delegates to include parameters from other functions. For example, if we apply the delegates decorator to foo to include parameters from baz:

+
+
@delegates(baz)
+def foo(c, a, **kwargs):
+    return c + baz(a, **kwargs)
+
+test_sig(foo, '(c, a, *, b: int = 2)')
+inspect.signature(foo)
+
+
<Signature (c, a, *, b: int = 2)>
+
+
+

We can optionally decide to keep **kwargs by setting keep=True:

+
+
@delegates(baz, keep=True)
+def foo(c, a, **kwargs):
+    return c + baz(a, **kwargs)
+
+inspect.signature(foo)
+
+
<Signature (c, a, *, b: int = 2, **kwargs)>
+
+
+

It is important to note that only parameters with default parameters are included. For example, in the below scenario only c, but NOT e and d are included in the signature of foo after applying delegates:

+
+
def basefoo(e, d, c=2): pass
+
+@delegates(basefoo)
+def foo(a, b=1, **kwargs): pass
+inspect.signature(foo) # e and d are not included b/c they don't have default parameters.
+
+
<Signature (a, b=1, *, c=2)>
+
+
+

The reason that required arguments (i.e. those without default parameters) are automatically excluded is that you should be explicitly implementing required arguments into your function’s signature rather than relying on delegates.

+

Additionally, you can exclude specific parameters from being included in the signature with the but parameter. In the example below, we exclude the parameter d:

+
+
def basefoo(e, c=2, d=3): pass
+
+@delegates(basefoo, but= ['d'])
+def foo(a, b=1, **kwargs): pass
+
+test_sig(foo, '(a, b=1, *, c=2)')
+inspect.signature(foo)
+
+
<Signature (a, b=1, *, c=2)>
+
+
+

You can also use delegates between methods in a class. Here is an example of delegates with class methods:

+
+
# example 1: class methods
+class _T():
+    @classmethod
+    def foo(cls, a=1, b=2):
+        pass
+    
+    @classmethod
+    @delegates(foo)
+    def bar(cls, c=3, **kwargs):
+        pass
+
+test_sig(_T.bar, '(c=3, *, a=1, b=2)')
+
+

Here is the same example with instance methods:

+
+
# example 2: instance methods
+class _T():
+    def foo(self, a=1, b=2):
+        pass
+    
+    @delegates(foo)
+    def bar(self, c=3, **kwargs):
+        pass
+
+t = _T()
+test_sig(t.bar, '(c=3, *, a=1, b=2)')
+
+

You can also delegate between classes. By default, the delegates decorator will delegate to the superclass:

+
+
class BaseFoo:
+    def __init__(self, e, c=2): pass
+
+@delegates()# since no argument was passsed here we delegate to the superclass
+class Foo(BaseFoo):
+    def __init__(self, a, b=1, **kwargs): super().__init__(**kwargs)
+
+test_sig(Foo, '(a, b=1, *, c=2)')
+
+
+

source

+
+
+

method

+
+
 method (f)
+
+

Mark f as a method

+

The method function is used to change a function’s type to a method. In the below example we change the type of a from a function to a method:

+
+
def a(x=2): return x + 1
+assert type(a).__name__ == 'function'
+
+a = method(a)
+assert type(a).__name__ == 'method'
+
+
+

source

+
+
+

funcs_kwargs

+
+
 funcs_kwargs (as_method=False)
+
+

Replace methods in cls._methods with those from kwargs

+

The func_kwargs decorator allows you to add a list of functions or methods to an existing class. You must set this list as a class attribute named _methods when defining your class. Additionally, you must incldue the **kwargs argument in the ___init__ method of your class.

+

After defining your class this way, you can add functions to your class upon instantation as illusrated below.

+

For example, we define class T to allow adding the function b to class T as follows (note that this function is stored as an attribute of T and doesn’t have access to cls or self):

+
+
@funcs_kwargs
+class T:
+    _methods=['b'] # allows you to add method b upon instantiation
+    def __init__(self, f=1, **kwargs): pass # don't forget to include **kwargs in __init__
+    def a(self): return 1
+    def b(self): return 2
+    
+t = T()
+test_eq(t.a(), 1)
+test_eq(t.b(), 2)
+
+

Because we defined the class T this way, the signature of T indicates the option to add the function or method(s) specified in _methods. In this example, b is added to the signature:

+
+
test_sig(T, '(f=1, *, b=None)')
+inspect.signature(T)
+
+
<Signature (f=1, *, b=None)>
+
+
+

You can now add the function b to class T upon instantiation:

+
+
def _new_func(): return 5
+
+t = T(b = _new_func)
+test_eq(t.b(), 5)
+
+

If you try to add a function with a name not listed in _methods it will be ignored. In the below example, the attempt to add a function named a is ignored:

+
+
t = T(a = lambda:3)
+test_eq(t.a(), 1) # the attempt to add a is ignored and uses the original method instead.
+
+

Note that you can also add methods not defined in the original class as long it is specified in the _methods attribute:

+
+
@funcs_kwargs
+class T:
+    _methods=['c']
+    def __init__(self, f=1, **kwargs): pass
+
+t = T(c = lambda: 4)
+test_eq(t.c(), 4)
+
+

Until now, these examples showed how to add functions stored as an instance attribute without access to self. However, if you need access to self you can set as_method=True in the func_kwargs decorator to add a method instead:

+
+
def _f(self,a=1): return self.num + a # access the num attribute from the instance
+
+@funcs_kwargs(as_method=True)
+class T: 
+    _methods=['b']
+    num = 5
+    
+t = T(b = _f) # adds method b
+test_eq(t.b(5), 10) # self.num + 5 = 10
+
+

Here is an example of how you might use this functionality with inheritence:

+
+
def _f(self,a=1): return self.num * a #multiply instead of add 
+
+class T2(T):
+    def __init__(self,num):
+        super().__init__(b = _f) # add method b from the super class
+        self.num=num
+        
+t = T2(num=3)
+test_eq(t.b(a=5), 15) # 3 * 5 = 15
+test_sig(T2, '(num)')
+
+ + +
+
+ +
+ +
+ + + + + \ No newline at end of file diff --git a/meta.html.md b/meta.html.md new file mode 100644 index 00000000..a0d4c388 --- /dev/null +++ b/meta.html.md @@ -0,0 +1,814 @@ +# Meta + + + + +``` python +from fastcore.foundation import * +from nbdev.showdoc import * +from fastcore.nb_imports import * +``` + +See this [blog post](https://realpython.com/python-metaclasses/) for +more information about metaclasses. + +- [`FixSigMeta`](https://fastcore.fast.ai/meta.html#fixsigmeta) + preserves information that enables [intropsection of + signatures](https://www.python.org/dev/peps/pep-0362/#:~:text=Python%20has%20always%20supported%20powerful,fully%20reconstruct%20the%20function's%20signature.) + (i.e. tab completion in IDEs) when certain types of inheritence would + otherwise obfuscate this introspection. +- [`PrePostInitMeta`](https://fastcore.fast.ai/meta.html#prepostinitmeta) + ensures that the classes defined with it run `__pre_init__` and + `__post_init__` (without having to write `self.__pre_init__()` and + `self.__post_init__()` in the actual `init` +- [`NewChkMeta`](https://fastcore.fast.ai/meta.html#newchkmeta) gives + the + [`PrePostInitMeta`](https://fastcore.fast.ai/meta.html#prepostinitmeta) + functionality and ensures classes defined with it don’t re-create an + object of their type whenever it’s passed to the constructor +- [`BypassNewMeta`](https://fastcore.fast.ai/meta.html#bypassnewmeta) + ensures classes defined with it can easily be casted form objects they + subclass. + +------------------------------------------------------------------------ + +source + +### test_sig + +> test_sig (f, b) + +*Test the signature of an object* + +``` python +def func_1(h,i,j): pass +def func_2(h,i=3, j=[5,6]): pass + +class T: + def __init__(self, a, b): pass + +test_sig(func_1, '(h, i, j)') +test_sig(func_2, '(h, i=3, j=[5, 6])') +test_sig(T, '(a, b)') +``` + +------------------------------------------------------------------------ + +source + +### FixSigMeta + +> FixSigMeta (name, bases, dict) + +*A metaclass that fixes the signature on classes that override +`__new__`* + +When you inherit from a class that defines `__new__`, or a metaclass +that defines `__call__`, the signature of your `__init__` method is +obfuscated such that tab completion no longer works. +[`FixSigMeta`](https://fastcore.fast.ai/meta.html#fixsigmeta) fixes this +issue and restores signatures. + +To understand what +[`FixSigMeta`](https://fastcore.fast.ai/meta.html#fixsigmeta) does, it +is useful to inspect an object’s signature. You can inspect the +signature of an object with `inspect.signature`: + +``` python +class T: + def __init__(self, a, b, c): pass + +inspect.signature(T) +``` + + + +This corresponds to tab completion working in the normal way: + +Tab completion in a Jupyter Notebook. + +However, when you inherhit from a class that defines `__new__` or a +metaclass that defines `__call__` this obfuscates the signature by +overriding your class with the signature of `__new__`, which prevents +tab completion from displaying useful information: + +``` python +class Foo: + def __new__(self, **args): pass + +class Bar(Foo): + def __init__(self, d, e, f): pass + +inspect.signature(Bar) +``` + + + +Tab completion in a Jupyter Notebook. + +Finally, the signature and tab completion can be restored by inheriting +from the metaclass +[`FixSigMeta`](https://fastcore.fast.ai/meta.html#fixsigmeta) as shown +below: + +``` python +class Bar(Foo, metaclass=FixSigMeta): + def __init__(self, d, e, f): pass + +test_sig(Bar, '(d, e, f)') +inspect.signature(Bar) +``` + + + +Tab completion in a Jupyter Notebook. + +If you need to define a metaclass that overrides `__call__` (as done in +[`PrePostInitMeta`](https://fastcore.fast.ai/meta.html#prepostinitmeta)), +you need to inherit from +[`FixSigMeta`](https://fastcore.fast.ai/meta.html#fixsigmeta) instead of +`type` when constructing the metaclass to preserve the signature in +`__init__`. Be careful not to override `__new__` when doing this: + +``` python +class TestMeta(FixSigMeta): + # __new__ comes from FixSigMeta + def __call__(cls, *args, **kwargs): pass + +class T(metaclass=TestMeta): + def __init__(self, a, b): pass + +test_sig(T, '(a, b)') +``` + +On the other hand, if you fail to inherit from +[`FixSigMeta`](https://fastcore.fast.ai/meta.html#fixsigmeta) when +inheriting from a metaclass that overrides `__call__`, your signature +will reflect that of `__call__` instead (which is often undesirable): + +``` python +class GenericMeta(type): + "A boilerplate metaclass that doesn't do anything for testing." + def __new__(cls, name, bases, dict): + return super().__new__(cls, name, bases, dict) + def __call__(cls, *args, **kwargs): pass + +class T2(metaclass=GenericMeta): + def __init__(self, a, b): pass + +# We can avoid this by inheriting from the metaclass `FixSigMeta` +test_sig(T2, '(*args, **kwargs)') +``` + +------------------------------------------------------------------------ + +source + +### PrePostInitMeta + +> PrePostInitMeta (name, bases, dict) + +*A metaclass that calls optional `__pre_init__` and `__post_init__` +methods* + +`__pre_init__` and `__post_init__` are useful for initializing variables +or performing tasks prior to or after `__init__` being called, +respectively. Fore example: + +``` python +class _T(metaclass=PrePostInitMeta): + def __pre_init__(self): self.a = 0; + def __init__(self,b=0): self.b = self.a + 1; assert self.b==1 + def __post_init__(self): self.c = self.b + 2; assert self.c==3 + +t = _T() +test_eq(t.a, 0) # set with __pre_init__ +test_eq(t.b, 1) # set with __init__ +test_eq(t.c, 3) # set with __post_init__ +``` + +One use for +[`PrePostInitMeta`](https://fastcore.fast.ai/meta.html#prepostinitmeta) +is avoiding the `__super__().__init__()` boilerplate associated with +subclassing, such as used in +[`AutoInit`](https://fastcore.fast.ai/meta.html#autoinit). + +------------------------------------------------------------------------ + +source + +### AutoInit + +> AutoInit (*args, **kwargs) + +*Same as `object`, but no need for subclasses to call +`super().__init__`* + +This is normally used as a +[mixin](https://www.residentmar.io/2019/07/07/python-mixins.html), eg: + +``` python +class TestParent(): + def __init__(self): self.h = 10 + +class TestChild(AutoInit, TestParent): + def __init__(self): self.k = self.h + 2 + +t = TestChild() +test_eq(t.h, 10) # h=10 is initialized in the parent class +test_eq(t.k, 12) +``` + +------------------------------------------------------------------------ + +source + +### NewChkMeta + +> NewChkMeta (name, bases, dict) + +*Metaclass to avoid recreating object passed to constructor* + +[`NewChkMeta`](https://fastcore.fast.ai/meta.html#newchkmeta) is used +when an object of the same type is the first argument to your class’s +constructor (i.e. the `__init__` function), and you would rather it not +create a new object but point to the same exact object. + +This is used in [`L`](https://fastcore.fast.ai/foundation.html#l), for +example, to avoid creating a new object when the object is already of +type [`L`](https://fastcore.fast.ai/foundation.html#l). This allows the +users to defenisvely instantiate an +[`L`](https://fastcore.fast.ai/foundation.html#l) object and just return +a reference to the same object if it already happens to be of type +[`L`](https://fastcore.fast.ai/foundation.html#l). + +For example, the below class `_T` **optionally** accepts an object `o` +as its first argument. A new object is returned upon instantiation per +usual: + +``` python +class _T(): + "Testing" + def __init__(self, o): + # if `o` is not an object without an attribute `foo`, set foo = 1 + self.foo = getattr(o,'foo',1) +``` + +``` python +t = _T(3) +test_eq(t.foo,1) # 1 was not of type _T, so foo = 1 + +t2 = _T(t) #t1 is of type _T +assert t is not t2 # t1 and t2 are different objects +``` + +However, if we want `_T` to return a reference to the same object when +passed an an object of type `_T` we can inherit from the +[`NewChkMeta`](https://fastcore.fast.ai/meta.html#newchkmeta) class as +illustrated below: + +``` python +class _T(metaclass=NewChkMeta): + "Testing with metaclass NewChkMeta" + def __init__(self, o=None, b=1): + # if `o` is not an object without an attribute `foo`, set foo = 1 + self.foo = getattr(o,'foo',1) + self.b = b +``` + +We can now test `t` and `t2` are now pointing at the same object when +using this new definition of `_T`: + +``` python +t = _T(3) +test_eq(t.foo,1) # 1 was not of type _T, so foo = 1 + +t2 = _T(t) # t2 will now reference t + +test_is(t, t2) # t and t2 are the same object +t2.foo = 5 # this will also change t.foo to 5 because it is the same object +test_eq(t.foo, 5) +test_eq(t2.foo, 5) +``` + +However, there is one exception to how +[`NewChkMeta`](https://fastcore.fast.ai/meta.html#newchkmeta) works. +**If you pass any additional arguments in the constructor a new object +is returned**, even if the first object is of the same type. For +example, consider the below example where we pass the additional +argument `b` into the constructor: + +``` python +t3 = _T(t, b=1) +assert t3 is not t + +t4 = _T(t) # without any arguments the constructor will return a reference to the same object +assert t4 is t +``` + +Finally, it should be noted that +[`NewChkMeta`](https://fastcore.fast.ai/meta.html#newchkmeta) as well as +all other metaclases in this section, inherit from +[`FixSigMeta`](https://fastcore.fast.ai/meta.html#fixsigmeta). This +means class signatures will always be preserved when inheriting from +this metaclass (see docs for +[`FixSigMeta`](https://fastcore.fast.ai/meta.html#fixsigmeta) for more +details): + +``` python +test_sig(_T, '(o=None, b=1)') +``` + +------------------------------------------------------------------------ + +source + +### BypassNewMeta + +> BypassNewMeta (name, bases, dict) + +*Metaclass: casts `x` to this class if it’s of type `cls._bypass_type`* + +[`BypassNewMeta`](https://fastcore.fast.ai/meta.html#bypassnewmeta) is +identical to +[`NewChkMeta`](https://fastcore.fast.ai/meta.html#newchkmeta), except +for checking for a class as the same type, we instead check for a class +of type specified in attribute `_bypass_type`. + +In NewChkMeta, objects of the same type passed to the constructor +(without arguments) would result into a new variable referencing the +same object. However, with +[`BypassNewMeta`](https://fastcore.fast.ai/meta.html#bypassnewmeta) this +only occurs if the type matches the `_bypass_type` of the class you are +defining: + +``` python +class _TestA: pass +class _TestB: pass + +class _T(_TestA, metaclass=BypassNewMeta): + _bypass_type=_TestB + def __init__(self,x): self.x=x +``` + +In the below example, `t` does not refer to `t2` because `t` is of type +`_TestA` while `_T._bypass_type` is of type `TestB`: + +``` python +t = _TestA() +t2 = _T(t) +assert t is not t2 +``` + +However, if `t` is set to `_TestB` to match `_T._bypass_type`, then both +`t` and `t2` will refer to the same object. + +``` python +t = _TestB() +t2 = _T(t) +t2.new_attr = 15 + +test_is(t, t2) +# since t2 just references t these will be the same +test_eq(t.new_attr, t2.new_attr) + +# likewise, chaning an attribute on t will also affect t2 because they both point to the same object. +t.new_attr = 9 +test_eq(t2.new_attr, 9) +``` + +## Metaprogramming + +------------------------------------------------------------------------ + +source + +### empty2none + +> empty2none (p) + +*Replace `Parameter.empty` with `None`* + +------------------------------------------------------------------------ + +source + +### anno_dict + +> anno_dict (f) + +*`__annotation__ dictionary with`empty`cast to`None\`, returning empty +if doesn’t exist* + +``` python +def _f(a:int, b:L)->str: ... +test_eq(anno_dict(_f), {'a': int, 'b': L, 'return': str}) +``` + +------------------------------------------------------------------------ + +source + +### use_kwargs_dict + +> use_kwargs_dict (keep=False, **kwargs) + +*Decorator: replace `**kwargs` in signature with `names` params* + +Replace all `**kwargs` with named arguments like so: + +``` python +@use_kwargs_dict(y=1,z=None) +def foo(a, b=1, **kwargs): pass + +test_sig(foo, '(a, b=1, *, y=1, z=None)') +``` + +Add named arguments, but optionally keep `**kwargs` by setting +`keep=True`: + +``` python +@use_kwargs_dict(y=1,z=None, keep=True) +def foo(a, b=1, **kwargs): pass + +test_sig(foo, '(a, b=1, *, y=1, z=None, **kwargs)') +``` + +------------------------------------------------------------------------ + +source + +### use_kwargs + +> use_kwargs (names, keep=False) + +*Decorator: replace `**kwargs` in signature with `names` params* + +[`use_kwargs`](https://fastcore.fast.ai/meta.html#use_kwargs) is +different than +[`use_kwargs_dict`](https://fastcore.fast.ai/meta.html#use_kwargs_dict) +as it only replaces `**kwargs` with named parameters without any default +values: + +``` python +@use_kwargs(['y', 'z']) +def foo(a, b=1, **kwargs): pass + +test_sig(foo, '(a, b=1, *, y=None, z=None)') +``` + +You may optionally keep the `**kwargs` argument in your signature by +setting `keep=True`: + +``` python +@use_kwargs(['y', 'z'], keep=True) +def foo(a, *args, b=1, **kwargs): pass +test_sig(foo, '(a, *args, b=1, y=None, z=None, **kwargs)') +``` + +------------------------------------------------------------------------ + +source + +### delegates + +> delegates (to:function=None, keep=False, but:list=None) + +*Decorator: replace `**kwargs` in signature with params from `to`* + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TypeDefaultDetails
tofunctionNoneDelegatee
keepboolFalseKeep kwargs in decorated function?
butlistNoneExclude these parameters from signature
+ +A common Python idiom is to accept `**kwargs` in addition to named +parameters that are passed onto other function calls. It is especially +common to use `**kwargs` when you want to give the user an option to +override default parameters of any functions or methods being called by +the parent function. + +For example, suppose we have have a function `foo` that passes arguments +to `baz` like so: + +``` python +def baz(a, b:int=2, c:int=3): return a + b + c + +def foo(c, a, **kwargs): + return c + baz(a, **kwargs) + +assert foo(c=1, a=1) == 7 +``` + +The problem with this approach is the api for `foo` is obfuscated. Users +cannot introspect what the valid arguments for `**kwargs` are without +reading the source code. When a user tries tries to introspect the +signature of `foo`, they are presented with this: + +``` python +inspect.signature(foo) +``` + + + +We can address this issue by using the decorator +[`delegates`](https://fastcore.fast.ai/meta.html#delegates) to include +parameters from other functions. For example, if we apply the +[`delegates`](https://fastcore.fast.ai/meta.html#delegates) decorator to +`foo` to include parameters from `baz`: + +``` python +@delegates(baz) +def foo(c, a, **kwargs): + return c + baz(a, **kwargs) + +test_sig(foo, '(c, a, *, b: int = 2)') +inspect.signature(foo) +``` + + + +We can optionally decide to keep `**kwargs` by setting `keep=True`: + +``` python +@delegates(baz, keep=True) +def foo(c, a, **kwargs): + return c + baz(a, **kwargs) + +inspect.signature(foo) +``` + + + +It is important to note that **only parameters with default parameters +are included**. For example, in the below scenario only `c`, but NOT `e` +and `d` are included in the signature of `foo` after applying +[`delegates`](https://fastcore.fast.ai/meta.html#delegates): + +``` python +def basefoo(e, d, c=2): pass + +@delegates(basefoo) +def foo(a, b=1, **kwargs): pass +inspect.signature(foo) # e and d are not included b/c they don't have default parameters. +``` + + + +The reason that required arguments (i.e. those without default +parameters) are automatically excluded is that you should be explicitly +implementing required arguments into your function’s signature rather +than relying on +[`delegates`](https://fastcore.fast.ai/meta.html#delegates). + +Additionally, you can exclude specific parameters from being included in +the signature with the `but` parameter. In the example below, we exclude +the parameter `d`: + +``` python +def basefoo(e, c=2, d=3): pass + +@delegates(basefoo, but= ['d']) +def foo(a, b=1, **kwargs): pass + +test_sig(foo, '(a, b=1, *, c=2)') +inspect.signature(foo) +``` + + + +You can also use +[`delegates`](https://fastcore.fast.ai/meta.html#delegates) between +methods in a class. Here is an example of +[`delegates`](https://fastcore.fast.ai/meta.html#delegates) with class +methods: + +``` python +# example 1: class methods +class _T(): + @classmethod + def foo(cls, a=1, b=2): + pass + + @classmethod + @delegates(foo) + def bar(cls, c=3, **kwargs): + pass + +test_sig(_T.bar, '(c=3, *, a=1, b=2)') +``` + +Here is the same example with instance methods: + +``` python +# example 2: instance methods +class _T(): + def foo(self, a=1, b=2): + pass + + @delegates(foo) + def bar(self, c=3, **kwargs): + pass + +t = _T() +test_sig(t.bar, '(c=3, *, a=1, b=2)') +``` + +You can also delegate between classes. By default, the +[`delegates`](https://fastcore.fast.ai/meta.html#delegates) decorator +will delegate to the superclass: + +``` python +class BaseFoo: + def __init__(self, e, c=2): pass + +@delegates()# since no argument was passsed here we delegate to the superclass +class Foo(BaseFoo): + def __init__(self, a, b=1, **kwargs): super().__init__(**kwargs) + +test_sig(Foo, '(a, b=1, *, c=2)') +``` + +------------------------------------------------------------------------ + +source + +### method + +> method (f) + +*Mark `f` as a method* + +The [`method`](https://fastcore.fast.ai/meta.html#method) function is +used to change a function’s type to a method. In the below example we +change the type of `a` from a function to a method: + +``` python +def a(x=2): return x + 1 +assert type(a).__name__ == 'function' + +a = method(a) +assert type(a).__name__ == 'method' +``` + +------------------------------------------------------------------------ + +source + +### funcs_kwargs + +> funcs_kwargs (as_method=False) + +*Replace methods in `cls._methods` with those from `kwargs`* + +The `func_kwargs` decorator allows you to add a list of functions or +methods to an existing class. You must set this list as a class +attribute named `_methods` when defining your class. Additionally, you +must incldue the `**kwargs` argument in the `___init__` method of your +class. + +After defining your class this way, you can add functions to your class +upon instantation as illusrated below. + +For example, we define class `T` to allow adding the function `b` to +class `T` as follows (note that this function is stored as an attribute +of `T` and doesn’t have access to `cls` or `self`): + +``` python +@funcs_kwargs +class T: + _methods=['b'] # allows you to add method b upon instantiation + def __init__(self, f=1, **kwargs): pass # don't forget to include **kwargs in __init__ + def a(self): return 1 + def b(self): return 2 + +t = T() +test_eq(t.a(), 1) +test_eq(t.b(), 2) +``` + +Because we defined the class `T` this way, the signature of `T` +indicates the option to add the function or method(s) specified in +`_methods`. In this example, `b` is added to the signature: + +``` python +test_sig(T, '(f=1, *, b=None)') +inspect.signature(T) +``` + + + +You can now add the function `b` to class `T` upon instantiation: + +``` python +def _new_func(): return 5 + +t = T(b = _new_func) +test_eq(t.b(), 5) +``` + +If you try to add a function with a name not listed in `_methods` it +will be ignored. In the below example, the attempt to add a function +named `a` is ignored: + +``` python +t = T(a = lambda:3) +test_eq(t.a(), 1) # the attempt to add a is ignored and uses the original method instead. +``` + +Note that you can also add methods not defined in the original class as +long it is specified in the `_methods` attribute: + +``` python +@funcs_kwargs +class T: + _methods=['c'] + def __init__(self, f=1, **kwargs): pass + +t = T(c = lambda: 4) +test_eq(t.c(), 4) +``` + +Until now, these examples showed how to add functions stored as an +instance attribute without access to `self`. However, if you need access +to `self` you can set `as_method=True` in the `func_kwargs` decorator to +add a method instead: + +``` python +def _f(self,a=1): return self.num + a # access the num attribute from the instance + +@funcs_kwargs(as_method=True) +class T: + _methods=['b'] + num = 5 + +t = T(b = _f) # adds method b +test_eq(t.b(5), 10) # self.num + 5 = 10 +``` + +Here is an example of how you might use this functionality with +inheritence: + +``` python +def _f(self,a=1): return self.num * a #multiply instead of add + +class T2(T): + def __init__(self,num): + super().__init__(b = _f) # add method b from the super class + self.num=num + +t = T2(num=3) +test_eq(t.b(a=5), 15) # 3 * 5 = 15 +test_sig(T2, '(num)') +``` diff --git a/net.html b/net.html new file mode 100644 index 00000000..9cf8afe7 --- /dev/null +++ b/net.html @@ -0,0 +1,1059 @@ + + + + + + + + + + +Network functionality – fastcore + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ +
+ + +
+ + + +
+ +
+
+

Network functionality

+
+ +
+
+ Network, HTTP, and URL functions +
+
+ + +
+ + + + +
+ + + +
+ + + +
+
from fastcore.test import *
+from nbdev.showdoc import *
+from fastcore.nb_imports import *
+
+
+

URLs

+
+

source

+
+

urlquote

+
+
 urlquote (url)
+
+

Update url’s path with urllib.parse.quote

+
+
urlquote("https://github.com/fastai/fastai/compare/master@{1.day.ago}…master")
+
+
'https://github.com/fastai/fastai/compare/master@%7B1.day.ago%7D%E2%80%A6master'
+
+
+
+
urlquote("https://www.google.com/search?q=你好")
+
+
'https://www.google.com/search?q=%E4%BD%A0%E5%A5%BD'
+
+
+
+

source

+
+
+

urlwrap

+
+
 urlwrap (url, data=None, headers=None)
+
+

Wrap url in a urllib Request with urlquote

+
+

source

+
+

HTTP4xxClientError

+
+
 HTTP4xxClientError (url, code, msg, hdrs, fp)
+
+

Base class for client exceptions (code 4xx) from url* functions

+
+

source

+
+
+

HTTP5xxServerError

+
+
 HTTP5xxServerError (url, code, msg, hdrs, fp)
+
+

Base class for server exceptions (code 5xx) from url* functions

+
+

source

+
+
+
+

urlopener

+
+
 urlopener ()
+
+
+

source

+
+
+

urlopen

+
+
 urlopen (url, data=None, headers=None, timeout=None, **kwargs)
+
+

Like urllib.request.urlopen, but first urlwrap the url, and encode data

+

With urlopen, the body of the response will also be returned in addition to the message if there is an error:

+
+
try: urlopen('https://api.github.com/v3')
+except HTTPError as e: 
+    print(e.code, e.msg)
+    assert 'documentation_url' in e.msg
+
+
404 Not Found
+====Error Body====
+{
+  "message": "Not Found",
+  "documentation_url": "https://docs.github.com/rest"
+}
+
+
+
+
+

source

+
+
+

urlread

+
+
 urlread (url, data=None, headers=None, decode=True, return_json=False,
+          return_headers=False, timeout=None, **kwargs)
+
+

Retrieve url, using data dict or kwargs to POST if present

+
+

source

+
+
+

urljson

+
+
 urljson (url, data=None, timeout=None)
+
+

Retrieve url and decode json

+
+
test_eq(urljson('https://httpbin.org/get')['headers']['User-Agent'], url_default_headers['User-Agent'])
+
+
+

source

+
+
+

urlcheck

+
+
 urlcheck (url, headers=None, timeout=10)
+
+
+

source

+
+
+

urlclean

+
+
 urlclean (url)
+
+

Remove fragment, params, and querystring from url if present

+
+
test_eq(urlclean('http://a.com/b?c=1#d'), 'http://a.com/b')
+
+
+

source

+
+
+

urlretrieve

+
+
 urlretrieve (url, filename=None, reporthook=None, data=None,
+              headers=None, timeout=None)
+
+

Same as urllib.request.urlretrieve but also works with Request objects

+
+

source

+
+
+

urldest

+
+
 urldest (url, dest=None)
+
+
+

source

+
+
+

urlsave

+
+
 urlsave (url, dest=None, reporthook=None, headers=None, timeout=None)
+
+

Retrieve url and save based on its name

+
+
#skip
+with tempfile.TemporaryDirectory() as d: urlsave('http://www.google.com/index.html', d)
+
+
+

source

+
+
+

urlvalid

+
+
 urlvalid (x)
+
+

Test if x is a valid URL

+
+
assert urlvalid('http://www.google.com/')
+assert not urlvalid('www.google.com/')
+assert not urlvalid(1)
+
+
+

source

+
+
+

urlrequest

+
+
 urlrequest (url, verb, headers=None, route=None, query=None, data=None,
+             json_data=True)
+
+

Request for url with optional route params replaced by route, plus query string, and post data

+
+
hdr = {'Hdr1':'1', 'Hdr2':'2'}
+req = urlrequest('http://example.com/{foo}/1', 'POST',
+                 headers=hdr, route={'foo':'3'}, query={'q':'4'}, data={'d':'5'})
+
+test_eq(req.headers, hdr)
+test_eq(req.full_url, 'http://example.com/3/1?q=4')
+test_eq(req.method, 'POST')
+test_eq(req.data, b'{"d": "5"}')
+
+
+
req = urlrequest('http://example.com/{foo}/1', 'POST', data={'d':'5','e':'6'}, headers=hdr, json_data=False)
+test_eq(req.data, b'd=5&e=6')
+
+
+

source

+
+
+

Request.summary

+
+
 Request.summary (skip=None)
+
+

Summary containing full_url, headers, method, and data, removing skip from headers

+
+
req.summary(skip='Hdr1')
+
+
{'full_url': 'http://example.com/{foo}/1',
+ 'method': 'POST',
+ 'data': b'd=5&e=6',
+ 'headers': {'Hdr2': '2'}}
+
+
+
+

source

+
+
+

urlsend

+
+
 urlsend (url, verb, headers=None, decode=True, route=None, query=None,
+          data=None, json_data=True, return_json=True,
+          return_headers=False, debug=None, timeout=None)
+
+

Send request with urlrequest, converting result to json if return_json

+
+

source

+
+
+

do_request

+
+
 do_request (url, post=False, headers=None, **data)
+
+

Call GET or json-encoded POST on url, depending on post

+
+
+
+

Basic client/server

+
+

source

+
+

start_server

+
+
 start_server (port, host=None, dgram=False, reuse_addr=True,
+               n_queue=None)
+
+

Create a socket server on port, with optional host, of type dgram

+

You can create a TCP client and server pass an int as port and optional host. host defaults to your main network interface if not provided. You can create a Unix socket client and server by passing a string to port. A SOCK_STREAM socket is created by default, unless you pass dgram=True, in which case a SOCK_DGRAM socket is created. n_queue sets the listening queue size.

+
+

source

+
+
+

start_client

+
+
 start_client (port, host=None, dgram=False)
+
+

Create a socket client on port, with optional host, of type dgram

+
+

source

+
+
+

tobytes

+
+
 tobytes (s:str)
+
+

Convert s into HTTP-ready bytes format

+
+
test_eq(tobytes('foo\nbar'), b'foo\r\nbar')
+
+
+

source

+
+
+

http_response

+
+
 http_response (body=None, status=200, hdrs=None, **kwargs)
+
+

Create an HTTP-ready response, adding kwargs to hdrs

+
+
exp = b'HTTP/1.1 200 OK\r\nUser-Agent: me\r\nContent-Length: 4\r\n\r\nbody'
+test_eq(http_response('body', 200, User_Agent='me'), exp)
+
+
+

source

+
+
+

recv_once

+
+
 recv_once (host:str='localhost', port:int=8000)
+
+

Spawn a thread to receive a single HTTP request and store in d['r']

+ + +
+
+ +
+ +
+ + + + + \ No newline at end of file diff --git a/net.html.md b/net.html.md new file mode 100644 index 00000000..8b00bdb1 --- /dev/null +++ b/net.html.md @@ -0,0 +1,390 @@ +# Network functionality + + + + +``` python +from fastcore.test import * +from nbdev.showdoc import * +from fastcore.nb_imports import * +``` + +## URLs + +------------------------------------------------------------------------ + +source + +### urlquote + +> urlquote (url) + +*Update url’s path with `urllib.parse.quote`* + +``` python +urlquote("https://github.com/fastai/fastai/compare/master@{1.day.ago}…master") +``` + + 'https://github.com/fastai/fastai/compare/master@%7B1.day.ago%7D%E2%80%A6master' + +``` python +urlquote("https://www.google.com/search?q=你好") +``` + + 'https://www.google.com/search?q=%E4%BD%A0%E5%A5%BD' + +------------------------------------------------------------------------ + +source + +### urlwrap + +> urlwrap (url, data=None, headers=None) + +*Wrap `url` in a urllib `Request` with +[`urlquote`](https://fastcore.fast.ai/net.html#urlquote)* + +------------------------------------------------------------------------ + +source + +#### HTTP4xxClientError + +> HTTP4xxClientError (url, code, msg, hdrs, fp) + +*Base class for client exceptions (code 4xx) from `url*` functions* + +------------------------------------------------------------------------ + +source + +#### HTTP5xxServerError + +> HTTP5xxServerError (url, code, msg, hdrs, fp) + +*Base class for server exceptions (code 5xx) from `url*` functions* + +------------------------------------------------------------------------ + +source + +### urlopener + +> urlopener () + +------------------------------------------------------------------------ + +source + +### urlopen + +> urlopen (url, data=None, headers=None, timeout=None, **kwargs) + +*Like `urllib.request.urlopen`, but first +[`urlwrap`](https://fastcore.fast.ai/net.html#urlwrap) the `url`, and +encode `data`* + +With [`urlopen`](https://fastcore.fast.ai/net.html#urlopen), the body of +the response will also be returned in addition to the message if there +is an error: + +``` python +try: urlopen('https://api.github.com/v3') +except HTTPError as e: + print(e.code, e.msg) + assert 'documentation_url' in e.msg +``` + + 404 Not Found + ====Error Body==== + { + "message": "Not Found", + "documentation_url": "https://docs.github.com/rest" + } + +------------------------------------------------------------------------ + +source + +### urlread + +> urlread (url, data=None, headers=None, decode=True, return_json=False, +> return_headers=False, timeout=None, **kwargs) + +*Retrieve `url`, using `data` dict or `kwargs` to `POST` if present* + +------------------------------------------------------------------------ + +source + +### urljson + +> urljson (url, data=None, timeout=None) + +*Retrieve `url` and decode json* + +``` python +test_eq(urljson('https://httpbin.org/get')['headers']['User-Agent'], url_default_headers['User-Agent']) +``` + +------------------------------------------------------------------------ + +source + +### urlcheck + +> urlcheck (url, headers=None, timeout=10) + +------------------------------------------------------------------------ + +source + +### urlclean + +> urlclean (url) + +*Remove fragment, params, and querystring from `url` if present* + +``` python +test_eq(urlclean('http://a.com/b?c=1#d'), 'http://a.com/b') +``` + +------------------------------------------------------------------------ + +source + +### urlretrieve + +> urlretrieve (url, filename=None, reporthook=None, data=None, +> headers=None, timeout=None) + +*Same as `urllib.request.urlretrieve` but also works with `Request` +objects* + +------------------------------------------------------------------------ + +source + +### urldest + +> urldest (url, dest=None) + +------------------------------------------------------------------------ + +source + +### urlsave + +> urlsave (url, dest=None, reporthook=None, headers=None, timeout=None) + +*Retrieve `url` and save based on its name* + +``` python +#skip +with tempfile.TemporaryDirectory() as d: urlsave('http://www.google.com/index.html', d) +``` + +------------------------------------------------------------------------ + +source + +### urlvalid + +> urlvalid (x) + +*Test if `x` is a valid URL* + +``` python +assert urlvalid('http://www.google.com/') +assert not urlvalid('www.google.com/') +assert not urlvalid(1) +``` + +------------------------------------------------------------------------ + +source + +### urlrequest + +> urlrequest (url, verb, headers=None, route=None, query=None, data=None, +> json_data=True) + +*`Request` for `url` with optional route params replaced by `route`, +plus `query` string, and post `data`* + +``` python +hdr = {'Hdr1':'1', 'Hdr2':'2'} +req = urlrequest('http://example.com/{foo}/1', 'POST', + headers=hdr, route={'foo':'3'}, query={'q':'4'}, data={'d':'5'}) + +test_eq(req.headers, hdr) +test_eq(req.full_url, 'http://example.com/3/1?q=4') +test_eq(req.method, 'POST') +test_eq(req.data, b'{"d": "5"}') +``` + +``` python +req = urlrequest('http://example.com/{foo}/1', 'POST', data={'d':'5','e':'6'}, headers=hdr, json_data=False) +test_eq(req.data, b'd=5&e=6') +``` + +------------------------------------------------------------------------ + +source + +### Request.summary + +> Request.summary (skip=None) + +*Summary containing full_url, headers, method, and data, removing `skip` +from headers* + +``` python +req.summary(skip='Hdr1') +``` + + {'full_url': 'http://example.com/{foo}/1', + 'method': 'POST', + 'data': b'd=5&e=6', + 'headers': {'Hdr2': '2'}} + +------------------------------------------------------------------------ + +source + +### urlsend + +> urlsend (url, verb, headers=None, decode=True, route=None, query=None, +> data=None, json_data=True, return_json=True, +> return_headers=False, debug=None, timeout=None) + +*Send request with +[`urlrequest`](https://fastcore.fast.ai/net.html#urlrequest), converting +result to json if `return_json`* + +------------------------------------------------------------------------ + +source + +### do_request + +> do_request (url, post=False, headers=None, **data) + +*Call GET or json-encoded POST on `url`, depending on `post`* + +## Basic client/server + +------------------------------------------------------------------------ + +source + +### start_server + +> start_server (port, host=None, dgram=False, reuse_addr=True, +> n_queue=None) + +*Create a `socket` server on `port`, with optional `host`, of type +`dgram`* + +You can create a TCP client and server pass an int as `port` and +optional `host`. `host` defaults to your main network interface if not +provided. You can create a Unix socket client and server by passing a +string to `port`. A `SOCK_STREAM` socket is created by default, unless +you pass `dgram=True`, in which case a `SOCK_DGRAM` socket is created. +`n_queue` sets the listening queue size. + +------------------------------------------------------------------------ + +source + +### start_client + +> start_client (port, host=None, dgram=False) + +*Create a `socket` client on `port`, with optional `host`, of type +`dgram`* + +------------------------------------------------------------------------ + +source + +### tobytes + +> tobytes (s:str) + +*Convert `s` into HTTP-ready bytes format* + +``` python +test_eq(tobytes('foo\nbar'), b'foo\r\nbar') +``` + +------------------------------------------------------------------------ + +source + +### http_response + +> http_response (body=None, status=200, hdrs=None, **kwargs) + +*Create an HTTP-ready response, adding `kwargs` to `hdrs`* + +``` python +exp = b'HTTP/1.1 200 OK\r\nUser-Agent: me\r\nContent-Length: 4\r\n\r\nbody' +test_eq(http_response('body', 200, User_Agent='me'), exp) +``` + +------------------------------------------------------------------------ + +source + +### recv_once + +> recv_once (host:str='localhost', port:int=8000) + +*Spawn a thread to receive a single HTTP request and store in `d['r']`* diff --git a/parallel.html b/parallel.html new file mode 100644 index 00000000..1e12a98b --- /dev/null +++ b/parallel.html @@ -0,0 +1,1014 @@ + + + + + + + + + + +Parallel – fastcore + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ +
+ + +
+ + + +
+ +
+
+

Parallel

+
+ +
+
+ Threading and multiprocessing functions +
+
+ + +
+ + + + +
+ + + +
+ + + +
+
from fastcore.test import *
+from nbdev.showdoc import *
+from fastcore.nb_imports import *
+
+
+

source

+
+

threaded

+
+
 threaded (process=False)
+
+

Run f in a Thread (or Process if process=True), and returns it

+
+
@threaded
+def _1():
+    time.sleep(0.05)
+    print("second")
+    return 5
+
+@threaded
+def _2():
+    time.sleep(0.01)
+    print("first")
+
+a = _1()
+_2()
+time.sleep(0.1)
+
+
first
+second
+
+
+

After the thread is complete, the return value is stored in the result attr.

+
+
a.result
+
+
5
+
+
+
+

source

+
+
+

startthread

+
+
 startthread (f)
+
+

Like threaded, but start thread immediately

+
+
@startthread
+def _():
+    time.sleep(0.05)
+    print("second")
+
+@startthread
+def _():
+    time.sleep(0.01)
+    print("first")
+
+time.sleep(0.1)
+
+
first
+second
+
+
+
+

source

+
+
+

startproc

+
+
 startproc (f)
+
+

Like threaded(True), but start Process immediately

+
+
@startproc
+def _():
+    time.sleep(0.05)
+    print("second")
+
+@startproc
+def _():
+    time.sleep(0.01)
+    print("first")
+
+time.sleep(0.1)
+
+
first
+second
+
+
+
+

source

+
+
+

parallelable

+
+
 parallelable (param_name, num_workers, f=None)
+
+
+

source

+
+

ThreadPoolExecutor

+
+
 ThreadPoolExecutor (max_workers=4, on_exc=<built-in function print>,
+                     pause=0, **kwargs)
+
+

Same as Python’s ThreadPoolExecutor, except can pass max_workers==0 for serial execution

+
+

source

+
+
+

ProcessPoolExecutor

+
+
 ProcessPoolExecutor (max_workers=4, on_exc=<built-in function print>,
+                      pause=0, mp_context=None, initializer=None,
+                      initargs=())
+
+

Same as Python’s ProcessPoolExecutor, except can pass max_workers==0 for serial execution

+
+

source

+
+
+
+

parallel

+
+
 parallel (f, items, *args, n_workers=4, total=None, progress=None,
+           pause=0, method=None, threadpool=False, timeout=None,
+           chunksize=1, **kwargs)
+
+

Applies func in parallel to items, using n_workers

+
+
inp,exp = range(50),range(1,51)
+
+test_eq(parallel(_add_one, inp, n_workers=2), exp)
+test_eq(parallel(_add_one, inp, threadpool=True, n_workers=2), exp)
+test_eq(parallel(_add_one, inp, n_workers=1, a=2), range(2,52))
+test_eq(parallel(_add_one, inp, n_workers=0), exp)
+test_eq(parallel(_add_one, inp, n_workers=0, a=2), range(2,52))
+
+

Use the pause parameter to ensure a pause of pause seconds between processes starting. This is in case there are race conditions in starting some process, or to stagger the time each process starts, for example when making many requests to a webserver. Set threadpool=True to use ThreadPoolExecutor instead of ProcessPoolExecutor.

+
+
from datetime import datetime
+
+
+
def print_time(i): 
+    time.sleep(random.random()/1000)
+    print(i, datetime.now())
+
+parallel(print_time, range(5), n_workers=2, pause=0.25);
+
+
0 2024-10-11 23:06:05.920741
+1 2024-10-11 23:06:06.171470
+2 2024-10-11 23:06:06.431925
+3 2024-10-11 23:06:06.689940
+4 2024-10-11 23:06:06.937109
+
+
+
+

source

+
+
+

parallel_async

+
+
 parallel_async (f, items, *args, n_workers=16, timeout=None, chunksize=1,
+                 on_exc=<built-in function print>, **kwargs)
+
+

Applies f to items in parallel using asyncio and a semaphore to limit concurrency.

+
+
import asyncio
+
+
+
async def print_time_async(i): 
+    wait = random.random()
+    await asyncio.sleep(wait)
+    print(i, datetime.now(), wait)
+
+await parallel_async(print_time_async, range(6), n_workers=3);
+
+
0 2024-10-11 23:06:39.545583 0.10292732609738675
+3 2024-10-11 23:06:39.900393 0.3516179734831676
+4 2024-10-11 23:06:39.941094 0.03699593757956876
+2 2024-10-11 23:06:39.957677 0.5148658606540902
+1 2024-10-11 23:06:40.099716 0.6574035385815227
+5 2024-10-11 23:06:40.654097 0.7116319667399102
+
+
+
+

source

+
+
+

run_procs

+
+
 run_procs (f, f_done, args)
+
+

Call f for each item in args in parallel, yielding f_done

+
+

source

+
+
+

parallel_gen

+
+
 parallel_gen (cls, items, n_workers=4, **kwargs)
+
+

Instantiate cls in n_workers procs & call each on a subset of items in parallel.

+
+
# class _C:
+#     def __call__(self, o): return ((i+1) for i in o)
+
+# items = range(5)
+
+# res = L(parallel_gen(_C, items, n_workers=0))
+# idxs,dat1 = zip(*res.sorted(itemgetter(0)))
+# test_eq(dat1, range(1,6))
+
+# res = L(parallel_gen(_C, items, n_workers=3))
+# idxs,dat2 = zip(*res.sorted(itemgetter(0)))
+# test_eq(dat2, dat1)
+
+

cls is any class with __call__. It will be passed args and kwargs when initialized. Note that n_workers instances of cls are created, one in each process. items are then split in n_workers batches and one is sent to each cls. The function then returns a generator of tuples of item indices and results.

+
+
class TestSleepyBatchFunc:
+    "For testing parallel processes that run at different speeds"
+    def __init__(self): self.a=1
+    def __call__(self, batch):
+        for k in batch:
+            time.sleep(random.random()/4)
+            yield k+self.a
+
+x = np.linspace(0,0.99,20)
+
+res = L(parallel_gen(TestSleepyBatchFunc, x, n_workers=2))
+test_eq(res.sorted().itemgot(1), x+1)
+
+ + +
+
+ +
+
+
+
# #|hide
+# from subprocess import Popen, PIPE
+# # test num_workers > 0 in scripts works when python process start method is spawn
+# process = Popen(["python", "parallel_test.py"], stdout=PIPE)
+# _, err = process.communicate(timeout=10)
+# exit_code = process.wait()
+# test_eq(exit_code, 0)
+
+ + +
+ +
+ +
+ + + + + \ No newline at end of file diff --git a/parallel.html.md b/parallel.html.md new file mode 100644 index 00000000..efcf9b15 --- /dev/null +++ b/parallel.html.md @@ -0,0 +1,321 @@ +# Parallel + + + + +``` python +from fastcore.test import * +from nbdev.showdoc import * +from fastcore.nb_imports import * +``` + +------------------------------------------------------------------------ + +source + +### threaded + +> threaded (process=False) + +*Run `f` in a `Thread` (or `Process` if `process=True`), and returns it* + +``` python +@threaded +def _1(): + time.sleep(0.05) + print("second") + return 5 + +@threaded +def _2(): + time.sleep(0.01) + print("first") + +a = _1() +_2() +time.sleep(0.1) +``` + + first + second + +After the thread is complete, the return value is stored in the `result` +attr. + +``` python +a.result +``` + + 5 + +------------------------------------------------------------------------ + +source + +### startthread + +> startthread (f) + +*Like [`threaded`](https://fastcore.fast.ai/parallel.html#threaded), but +start thread immediately* + +``` python +@startthread +def _(): + time.sleep(0.05) + print("second") + +@startthread +def _(): + time.sleep(0.01) + print("first") + +time.sleep(0.1) +``` + + first + second + +------------------------------------------------------------------------ + +source + +### startproc + +> startproc (f) + +*Like `threaded(True)`, but start Process immediately* + +``` python +@startproc +def _(): + time.sleep(0.05) + print("second") + +@startproc +def _(): + time.sleep(0.01) + print("first") + +time.sleep(0.1) +``` + + first + second + +------------------------------------------------------------------------ + +source + +### parallelable + +> parallelable (param_name, num_workers, f=None) + +------------------------------------------------------------------------ + +source + +#### ThreadPoolExecutor + +> ThreadPoolExecutor (max_workers=4, on_exc=, +> pause=0, **kwargs) + +*Same as Python’s ThreadPoolExecutor, except can pass `max_workers==0` +for serial execution* + +------------------------------------------------------------------------ + +source + +#### ProcessPoolExecutor + +> ProcessPoolExecutor (max_workers=4, on_exc=, +> pause=0, mp_context=None, initializer=None, +> initargs=()) + +*Same as Python’s ProcessPoolExecutor, except can pass `max_workers==0` +for serial execution* + +------------------------------------------------------------------------ + +source + +### parallel + +> parallel (f, items, *args, n_workers=4, total=None, progress=None, +> pause=0, method=None, threadpool=False, timeout=None, +> chunksize=1, **kwargs) + +*Applies `func` in parallel to `items`, using `n_workers`* + +``` python +inp,exp = range(50),range(1,51) + +test_eq(parallel(_add_one, inp, n_workers=2), exp) +test_eq(parallel(_add_one, inp, threadpool=True, n_workers=2), exp) +test_eq(parallel(_add_one, inp, n_workers=1, a=2), range(2,52)) +test_eq(parallel(_add_one, inp, n_workers=0), exp) +test_eq(parallel(_add_one, inp, n_workers=0, a=2), range(2,52)) +``` + +Use the `pause` parameter to ensure a pause of `pause` seconds between +processes starting. This is in case there are race conditions in +starting some process, or to stagger the time each process starts, for +example when making many requests to a webserver. Set `threadpool=True` +to use +[`ThreadPoolExecutor`](https://fastcore.fast.ai/parallel.html#threadpoolexecutor) +instead of +[`ProcessPoolExecutor`](https://fastcore.fast.ai/parallel.html#processpoolexecutor). + +``` python +from datetime import datetime +``` + +``` python +def print_time(i): + time.sleep(random.random()/1000) + print(i, datetime.now()) + +parallel(print_time, range(5), n_workers=2, pause=0.25); +``` + + 0 2024-10-11 23:06:05.920741 + 1 2024-10-11 23:06:06.171470 + 2 2024-10-11 23:06:06.431925 + 3 2024-10-11 23:06:06.689940 + 4 2024-10-11 23:06:06.937109 + +------------------------------------------------------------------------ + +source + +### parallel_async + +> parallel_async (f, items, *args, n_workers=16, timeout=None, chunksize=1, +> on_exc=, **kwargs) + +*Applies `f` to `items` in parallel using asyncio and a semaphore to +limit concurrency.* + +``` python +import asyncio +``` + +``` python +async def print_time_async(i): + wait = random.random() + await asyncio.sleep(wait) + print(i, datetime.now(), wait) + +await parallel_async(print_time_async, range(6), n_workers=3); +``` + + 0 2024-10-11 23:06:39.545583 0.10292732609738675 + 3 2024-10-11 23:06:39.900393 0.3516179734831676 + 4 2024-10-11 23:06:39.941094 0.03699593757956876 + 2 2024-10-11 23:06:39.957677 0.5148658606540902 + 1 2024-10-11 23:06:40.099716 0.6574035385815227 + 5 2024-10-11 23:06:40.654097 0.7116319667399102 + +------------------------------------------------------------------------ + +source + +### run_procs + +> run_procs (f, f_done, args) + +*Call `f` for each item in `args` in parallel, yielding `f_done`* + +------------------------------------------------------------------------ + +source + +### parallel_gen + +> parallel_gen (cls, items, n_workers=4, **kwargs) + +*Instantiate `cls` in `n_workers` procs & call each on a subset of +`items` in parallel.* + +``` python +# class _C: +# def __call__(self, o): return ((i+1) for i in o) + +# items = range(5) + +# res = L(parallel_gen(_C, items, n_workers=0)) +# idxs,dat1 = zip(*res.sorted(itemgetter(0))) +# test_eq(dat1, range(1,6)) + +# res = L(parallel_gen(_C, items, n_workers=3)) +# idxs,dat2 = zip(*res.sorted(itemgetter(0))) +# test_eq(dat2, dat1) +``` + +`cls` is any class with `__call__`. It will be passed `args` and +`kwargs` when initialized. Note that `n_workers` instances of `cls` are +created, one in each process. `items` are then split in `n_workers` +batches and one is sent to each `cls`. The function then returns a +generator of tuples of item indices and results. + +``` python +class TestSleepyBatchFunc: + "For testing parallel processes that run at different speeds" + def __init__(self): self.a=1 + def __call__(self, batch): + for k in batch: + time.sleep(random.random()/4) + yield k+self.a + +x = np.linspace(0,0.99,20) + +res = L(parallel_gen(TestSleepyBatchFunc, x, n_workers=2)) +test_eq(res.sorted().itemgot(1), x+1) +``` + + + +``` python +# #|hide +# from subprocess import Popen, PIPE +# # test num_workers > 0 in scripts works when python process start method is spawn +# process = Popen(["python", "parallel_test.py"], stdout=PIPE) +# _, err = process.communicate(timeout=10) +# exit_code = process.wait() +# test_eq(exit_code, 0) +``` diff --git a/py2pyi.html b/py2pyi.html new file mode 100644 index 00000000..c44de310 --- /dev/null +++ b/py2pyi.html @@ -0,0 +1,1236 @@ + + + + + + + + + +Create delegated pyi – fastcore + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ +
+ + +
+ + + +
+ +
+
+

Create delegated pyi

+
+ + + +
+ + + + +
+ + + +
+ + + +
+

Setup

+
+
+

Basics

+
+

source

+
+

imp_mod

+
+
 imp_mod (module_path, package=None)
+
+

Import dynamically the module referenced in fn

+
+
fn = Path('test_py2pyi.py')
+
+
+
mod = imp_mod(fn)
+a = mod.A()
+a.h()
+
+
1
+
+
+
+
tree = _get_tree(mod)
+
+
+
+
+

AST.__repr__

+
+
 AST.__repr__ ()
+
+
+
# for o in enumerate(tree.body): print(o)
+
+
+
node = tree.body[4]
+node
+
+
def f(a: int, b: str='a') -> str:
+    """I am f"""
+    return 1
+
+
+
+
isinstance(node, functypes)
+
+
True
+
+
+
+

source

+
+
+

has_deco

+
+
 has_deco (node:Union[ast.FunctionDef,ast.AsyncFunctionDef], name:str)
+
+

Check if a function node node has a decorator named name

+
+
nm = 'delegates'
+has_deco(node, nm)
+
+
False
+
+
+
+
node = tree.body[5]
+node
+
+
@delegates(f)
+def g(c, d: X, **kwargs) -> str:
+    """I am g"""
+    return 2
+
+
+
+
has_deco(node, nm)
+
+
True
+
+
+
+
+
+

Function processing

+
+
def _proc_body   (node, mod): print('_proc_body', type(node))
+def _proc_func   (node, mod): print('_proc_func', type(node))
+def _proc_class  (node, mod): print('_proc_class', type(node))
+def _proc_patched(node, mod): print('_proc_patched', type(node))
+
+
+
parent_node = copy.deepcopy(tree.body[7])
+patched_node = copy.deepcopy(tree.body[10])
+test_is(has_deco(patched_node, "patch"), True)
+test_eq(str(patched_node.args.args[0].annotation), parent_node.name)
+
+_clean_patched_node(patched_node)
+test_is(has_deco(patched_node, "patch"), False)
+test_eq(patched_node.args.args[0].annotation, None)
+
+
+
empty_cls1, empty_cls2, empty_cls3 = ast.parse('''
+class A: 
+    """An empty class."""
+class B: 
+    pass
+class C: 
+    ...
+''').body
+
+test_is(_is_empty_class(empty_cls1), True)
+test_is(_is_empty_class(empty_cls2), True)
+test_is(_is_empty_class(empty_cls3), True)
+
+non_empty_cls, empty_func = ast.parse('''
+class A: 
+    a = 1
+def f():
+    ...
+''').body
+test_is(_is_empty_class(non_empty_cls), False)
+test_is(_is_empty_class(empty_func), False)
+
+
+
# we could have reused `parent_node` and `patched_node` from the previous cells.
+# copying them here allows us to run this cell multiple times which makes it a little easier to write tests.
+
+parent_node = copy.deepcopy(tree.body[7])
+patched_node = copy.deepcopy(tree.body[11])
+test_eq(len(parent_node.body),1)
+_add_patched_node_to_parent(patched_node, parent_node)
+test_eq(len(parent_node.body),2)
+test_eq(parent_node.body[-1], patched_node)
+
+# patched node replaces an existing class method (A.h)
+patched_h_node = ast.parse("""
+@patch
+def h(self: A, *args, **kwargs):
+    ...
+""", mode='single').body[0]
+
+_add_patched_node_to_parent(patched_h_node, parent_node)
+test_eq(len(parent_node.body), 2)
+test_eq(parent_node.body[0], patched_h_node)
+
+# patched node is added to an empty class
+empty_cls, patched_node = ast.parse('''
+class Z: 
+    """An empty class."""
+
+@patch
+def a(self: Z, *args, **kwargs):
+    ...
+''').body
+
+test_eq(len(empty_cls.body), 1)
+test_ne(empty_cls.body[0], patched_node)
+_add_patched_node_to_parent(patched_node, empty_cls)
+test_eq(len(empty_cls.body), 1)
+test_eq(empty_cls.body[0], patched_node)
+
+
+
raw_tree = _get_tree(mod)
+processed_tree = _proc_mod(mod)
+n_raw_tree_nodes = len(raw_tree.body)
+# mod contains 3 patch methods so our processed_tree should have 3 less nodes 
+test_eq(len(processed_tree.body), n_raw_tree_nodes-3)
+
+
_proc_class <class 'ast.ClassDef'>
+_proc_body <class 'ast.FunctionDef'>
+_proc_func <class 'ast.FunctionDef'>
+_proc_body <class 'ast.FunctionDef'>
+_proc_class <class 'ast.ClassDef'>
+_proc_class <class 'ast.ClassDef'>
+_proc_patched <class 'ast.FunctionDef'>
+_proc_patched <class 'ast.FunctionDef'>
+_proc_body <class 'ast.FunctionDef'>
+
+
+
+
_proc_mod(mod);
+
+
_proc_class <class 'ast.ClassDef'>
+_proc_body <class 'ast.FunctionDef'>
+_proc_func <class 'ast.FunctionDef'>
+_proc_body <class 'ast.FunctionDef'>
+_proc_class <class 'ast.ClassDef'>
+_proc_class <class 'ast.ClassDef'>
+_proc_patched <class 'ast.FunctionDef'>
+_proc_patched <class 'ast.FunctionDef'>
+_proc_body <class 'ast.FunctionDef'>
+
+
+
+
node.name
+
+
'g'
+
+
+
+
sym = getattr(mod, node.name)
+sym
+
+
<function test_py2pyi.g(c, d: test_py2pyi.X, *, b: str = 'a') -> str>
+
+
+
+
sig = signature(sym)
+print(sig)
+
+
(c, d: test_py2pyi.X, *, b: str = 'a') -> str
+
+
+
+

source

+
+

sig2str

+
+
 sig2str (sig)
+
+
+

source

+
+
+

ast_args

+
+
 ast_args (func)
+
+
+
newargs = ast_args(sym)
+newargs
+
+
c, d: test_py2pyi.X, *, b: str='a'
+
+
+
+
node.args
+
+
c, d: X, **kwargs
+
+
+
+
node.args = newargs
+node
+
+
@delegates(f)
+def g(c, d: test_py2pyi.X, *, b: str='a') -> str:
+    """I am g"""
+    return 2
+
+
+
+
_body_ellip(node)
+node
+
+
@delegates(f)
+def g(c, d: test_py2pyi.X, *, b: str='a') -> str:
+    """I am g"""
+    ...
+
+
+
+
tree = _get_tree(mod)
+node = tree.body[5]
+node
+
+
@delegates(f)
+def g(c, d: X, **kwargs) -> str:
+    """I am g"""
+    return 2
+
+
+
+
_update_func(node, sym)
+node
+
+
def g(c, d: X, *, b: str='a') -> str:
+    """I am g"""
+    ...
+
+
+
+
tree = _proc_mod(mod)
+tree.body[5]
+
+
_proc_class <class 'ast.ClassDef'>
+_proc_class <class 'ast.ClassDef'>
+_proc_class <class 'ast.ClassDef'>
+_proc_patched <class 'ast.FunctionDef'>
+_proc_patched <class 'ast.FunctionDef'>
+
+
+
def g(c, d: X, *, b: str='a') -> str:
+    """I am g"""
+    ...
+
+
+
+
+
+

Patch

+
+
tree = _get_tree(mod)
+node = tree.body[9]
+node
+
+
@patch
+@delegates(j)
+def k(self: (A, B), b: bool=False, **kwargs):
+    return 1
+
+
+
+
ann = node.args.args[0].annotation
+
+
+
if hasattr(ann, 'elts'): ann = ann.elts[0]
+
+
+
nm = ann.id
+nm
+
+
'A'
+
+
+
+
cls = getattr(mod, nm)
+sym = getattr(cls, node.name)
+
+
+
sig2str(signature(sym))
+
+
"(self: (test_py2pyi.A, test_py2pyi.B), b: bool = False, *, d: str = 'a')"
+
+
+
+
_update_func(node, sym)
+
+
+
node
+
+
@patch
+def k(self: (A, B), b: bool=False, *, d: str='a'):
+    ...
+
+
+
+
tree = _get_tree(mod)
+tree.body[9]
+
+
@patch
+@delegates(j)
+def k(self: (A, B), b: bool=False, **kwargs):
+    return 1
+
+
+
+
+

Class and file

+
+
tree = _get_tree(mod)
+node = tree.body[7]
+node
+
+
class A:
+
+    @delegates(j)
+    def h(self, b: bool=False, **kwargs):
+        a = 1
+        return a
+
+
+
+
node.body
+
+
[@delegates(j)
+ def h(self, b: bool=False, **kwargs):
+     a = 1
+     return a]
+
+
+
+
tree = _proc_mod(mod)
+tree.body[7]
+
+
class A:
+
+    def h(self, b: bool=False, *, d: str='a'):
+        ...
+
+    def k(self, b: bool=False, *, d: str='a'):
+        ...
+
+    def m(self, b: bool=False, *, d: str='a'):
+        ...
+
+    def n(self, b: bool=False, **kwargs):
+        """No delegates here mmm'k?"""
+        ...
+
+
+
+

source

+
+

create_pyi

+
+
 create_pyi (fn, package=None)
+
+

Convert fname.py to fname.pyi by removing function bodies and expanding delegates kwargs

+
+
create_pyi(fn)
+
+
+
# fn = Path('/Users/jhoward/git/fastcore/fastcore/docments.py')
+# create_pyi(fn, 'fastcore')
+
+
+
+
+

Script

+
+

source

+
+

py2pyi

+
+
 py2pyi (fname:str, package:str=None)
+
+

Convert fname.py to fname.pyi by removing function bodies and expanding delegates kwargs

+ + + + + + + + + + + + + + + + + + + + + + + +
TypeDefaultDetails
fnamestrThe file name to convert
packagestrNoneThe parent package
+
+

source

+
+
+

replace_wildcards

+
+
 replace_wildcards (path:str)
+
+

Expand wildcard imports in the specified Python file.

+ + + + + + + + + + + + + + + +
TypeDetails
pathstrPath to the Python file to process
+ + +
+
+ +
+ +
+ + + + + \ No newline at end of file diff --git a/py2pyi.html.md b/py2pyi.html.md new file mode 100644 index 00000000..ce22e5ce --- /dev/null +++ b/py2pyi.html.md @@ -0,0 +1,545 @@ +# Create delegated pyi + + + + +## Setup + +## Basics + +------------------------------------------------------------------------ + +source + +### imp_mod + +> imp_mod (module_path, package=None) + +*Import dynamically the module referenced in `fn`* + +``` python +fn = Path('test_py2pyi.py') +``` + +``` python +mod = imp_mod(fn) +a = mod.A() +a.h() +``` + + 1 + +``` python +tree = _get_tree(mod) +``` + +------------------------------------------------------------------------ + +### AST.\_\_repr\_\_ + +> AST.__repr__ () + +``` python +# for o in enumerate(tree.body): print(o) +``` + +``` python +node = tree.body[4] +node +``` + +``` python +def f(a: int, b: str='a') -> str: + """I am f""" + return 1 +``` + +``` python +isinstance(node, functypes) +``` + + True + +------------------------------------------------------------------------ + +source + +### has_deco + +> has_deco (node:Union[ast.FunctionDef,ast.AsyncFunctionDef], name:str) + +*Check if a function node `node` has a decorator named `name`* + +``` python +nm = 'delegates' +has_deco(node, nm) +``` + + False + +``` python +node = tree.body[5] +node +``` + +``` python +@delegates(f) +def g(c, d: X, **kwargs) -> str: + """I am g""" + return 2 +``` + +``` python +has_deco(node, nm) +``` + + True + +## Function processing + +``` python +def _proc_body (node, mod): print('_proc_body', type(node)) +def _proc_func (node, mod): print('_proc_func', type(node)) +def _proc_class (node, mod): print('_proc_class', type(node)) +def _proc_patched(node, mod): print('_proc_patched', type(node)) +``` + +``` python +parent_node = copy.deepcopy(tree.body[7]) +patched_node = copy.deepcopy(tree.body[10]) +test_is(has_deco(patched_node, "patch"), True) +test_eq(str(patched_node.args.args[0].annotation), parent_node.name) + +_clean_patched_node(patched_node) +test_is(has_deco(patched_node, "patch"), False) +test_eq(patched_node.args.args[0].annotation, None) +``` + +``` python +empty_cls1, empty_cls2, empty_cls3 = ast.parse(''' +class A: + """An empty class.""" +class B: + pass +class C: + ... +''').body + +test_is(_is_empty_class(empty_cls1), True) +test_is(_is_empty_class(empty_cls2), True) +test_is(_is_empty_class(empty_cls3), True) + +non_empty_cls, empty_func = ast.parse(''' +class A: + a = 1 +def f(): + ... +''').body +test_is(_is_empty_class(non_empty_cls), False) +test_is(_is_empty_class(empty_func), False) +``` + +``` python +# we could have reused `parent_node` and `patched_node` from the previous cells. +# copying them here allows us to run this cell multiple times which makes it a little easier to write tests. + +parent_node = copy.deepcopy(tree.body[7]) +patched_node = copy.deepcopy(tree.body[11]) +test_eq(len(parent_node.body),1) +_add_patched_node_to_parent(patched_node, parent_node) +test_eq(len(parent_node.body),2) +test_eq(parent_node.body[-1], patched_node) + +# patched node replaces an existing class method (A.h) +patched_h_node = ast.parse(""" +@patch +def h(self: A, *args, **kwargs): + ... +""", mode='single').body[0] + +_add_patched_node_to_parent(patched_h_node, parent_node) +test_eq(len(parent_node.body), 2) +test_eq(parent_node.body[0], patched_h_node) + +# patched node is added to an empty class +empty_cls, patched_node = ast.parse(''' +class Z: + """An empty class.""" + +@patch +def a(self: Z, *args, **kwargs): + ... +''').body + +test_eq(len(empty_cls.body), 1) +test_ne(empty_cls.body[0], patched_node) +_add_patched_node_to_parent(patched_node, empty_cls) +test_eq(len(empty_cls.body), 1) +test_eq(empty_cls.body[0], patched_node) +``` + +``` python +raw_tree = _get_tree(mod) +processed_tree = _proc_mod(mod) +n_raw_tree_nodes = len(raw_tree.body) +# mod contains 3 patch methods so our processed_tree should have 3 less nodes +test_eq(len(processed_tree.body), n_raw_tree_nodes-3) +``` + + _proc_class + _proc_body + _proc_func + _proc_body + _proc_class + _proc_class + _proc_patched + _proc_patched + _proc_body + +``` python +_proc_mod(mod); +``` + + _proc_class + _proc_body + _proc_func + _proc_body + _proc_class + _proc_class + _proc_patched + _proc_patched + _proc_body + +``` python +node.name +``` + + 'g' + +``` python +sym = getattr(mod, node.name) +sym +``` + + str> + +``` python +sig = signature(sym) +print(sig) +``` + + (c, d: test_py2pyi.X, *, b: str = 'a') -> str + +------------------------------------------------------------------------ + +source + +### sig2str + +> sig2str (sig) + +------------------------------------------------------------------------ + +source + +### ast_args + +> ast_args (func) + +``` python +newargs = ast_args(sym) +newargs +``` + +``` python +c, d: test_py2pyi.X, *, b: str='a' +``` + +``` python +node.args +``` + +``` python +c, d: X, **kwargs +``` + +``` python +node.args = newargs +node +``` + +``` python +@delegates(f) +def g(c, d: test_py2pyi.X, *, b: str='a') -> str: + """I am g""" + return 2 +``` + +``` python +_body_ellip(node) +node +``` + +``` python +@delegates(f) +def g(c, d: test_py2pyi.X, *, b: str='a') -> str: + """I am g""" + ... +``` + +``` python +tree = _get_tree(mod) +node = tree.body[5] +node +``` + +``` python +@delegates(f) +def g(c, d: X, **kwargs) -> str: + """I am g""" + return 2 +``` + +``` python +_update_func(node, sym) +node +``` + +``` python +def g(c, d: X, *, b: str='a') -> str: + """I am g""" + ... +``` + +``` python +tree = _proc_mod(mod) +tree.body[5] +``` + + _proc_class + _proc_class + _proc_class + _proc_patched + _proc_patched + +``` python +def g(c, d: X, *, b: str='a') -> str: + """I am g""" + ... +``` + +## Patch + +``` python +tree = _get_tree(mod) +node = tree.body[9] +node +``` + +``` python +@patch +@delegates(j) +def k(self: (A, B), b: bool=False, **kwargs): + return 1 +``` + +``` python +ann = node.args.args[0].annotation +``` + +``` python +if hasattr(ann, 'elts'): ann = ann.elts[0] +``` + +``` python +nm = ann.id +nm +``` + + 'A' + +``` python +cls = getattr(mod, nm) +sym = getattr(cls, node.name) +``` + +``` python +sig2str(signature(sym)) +``` + + "(self: (test_py2pyi.A, test_py2pyi.B), b: bool = False, *, d: str = 'a')" + +``` python +_update_func(node, sym) +``` + +``` python +node +``` + +``` python +@patch +def k(self: (A, B), b: bool=False, *, d: str='a'): + ... +``` + +``` python +tree = _get_tree(mod) +tree.body[9] +``` + +``` python +@patch +@delegates(j) +def k(self: (A, B), b: bool=False, **kwargs): + return 1 +``` + +## Class and file + +``` python +tree = _get_tree(mod) +node = tree.body[7] +node +``` + +``` python +class A: + + @delegates(j) + def h(self, b: bool=False, **kwargs): + a = 1 + return a +``` + +``` python +node.body +``` + + [@delegates(j) + def h(self, b: bool=False, **kwargs): + a = 1 + return a] + +``` python +tree = _proc_mod(mod) +tree.body[7] +``` + +``` python +class A: + + def h(self, b: bool=False, *, d: str='a'): + ... + + def k(self, b: bool=False, *, d: str='a'): + ... + + def m(self, b: bool=False, *, d: str='a'): + ... + + def n(self, b: bool=False, **kwargs): + """No delegates here mmm'k?""" + ... +``` + +------------------------------------------------------------------------ + +source + +### create_pyi + +> create_pyi (fn, package=None) + +*Convert `fname.py` to `fname.pyi` by removing function bodies and +expanding [`delegates`](https://fastcore.fast.ai/meta.html#delegates) +kwargs* + +``` python +create_pyi(fn) +``` + +``` python +# fn = Path('/Users/jhoward/git/fastcore/fastcore/docments.py') +# create_pyi(fn, 'fastcore') +``` + +## Script + +------------------------------------------------------------------------ + +source + +### py2pyi + +> py2pyi (fname:str, package:str=None) + +*Convert `fname.py` to `fname.pyi` by removing function bodies and +expanding [`delegates`](https://fastcore.fast.ai/meta.html#delegates) +kwargs* + + + + + + + + + + + + + + + + + + + + + + + + +
TypeDefaultDetails
fnamestrThe file name to convert
packagestrNoneThe parent package
+ +------------------------------------------------------------------------ + +source + +### replace_wildcards + +> replace_wildcards (path:str) + +*Expand wildcard imports in the specified Python file.* + + + + + + + + + + + + + + + + +
TypeDetails
pathstrPath to the Python file to process
diff --git a/robots.txt b/robots.txt new file mode 100644 index 00000000..7b04fdf9 --- /dev/null +++ b/robots.txt @@ -0,0 +1 @@ +Sitemap: https://fastcore.fast.ai/sitemap.xml diff --git a/script.html b/script.html new file mode 100644 index 00000000..301d19d7 --- /dev/null +++ b/script.html @@ -0,0 +1,1016 @@ + + + + + + + + + + +Script - CLI – fastcore + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ +
+ + +
+ + + +
+ +
+
+

Script - CLI

+
+ +
+
+ A fast way to turn your python function into a script. +
+
+ + +
+ + + + +
+ + + +
+ + + +

Part of fast.ai’s toolkit for delightful developer experiences.

+
+

Overview

+

Sometimes, you want to create a quick script, either for yourself, or for others. But in Python, that involves a whole lot of boilerplate and ceremony, especially if you want to support command line arguments, provide help, and other niceties. You can use argparse for this purpose, which comes with Python, but it’s complex and verbose.

+

fastcore.script makes life easier. There are much fancier modules to help you write scripts (we recommend Python Fire, and Click is also popular), but fastcore.script is very fast and very simple. In fact, it’s <50 lines of code! Basically, it’s just a little wrapper around argparse that uses modern Python features and some thoughtful defaults to get rid of the boilerplate.

+

For full details, see the docs for core.

+
+
+

Example

+

Here’s a complete example (available in examples/test_fastcore.py):

+
from fastcore.script import *
+@call_parse
+def main(msg:str,     # The message
+         upper:bool): # Convert to uppercase?
+    "Print `msg`, optionally converting to uppercase"
+    print(msg.upper() if upper else msg)
+

If you copy that info a file and run it, you’ll see:

+
$ examples/test_fastcore.py --help
+usage: test_fastcore.py [-h] [--upper] msg
+
+Print `msg`, optionally converting to uppercase
+
+positional arguments:
+  msg          The message
+
+optional arguments:
+  -h, --help   show this help message and exit
+  --upper      Convert to uppercase? (default: False)
+

As you see, we didn’t need any if __name__ == "__main__", we didn’t have to parse arguments, we just wrote a function, added a decorator to it, and added some annotations to our function’s parameters. As a bonus, we can also use this function directly from a REPL such as Jupyter Notebook - it’s not just for command line scripts!

+

You should provide a default (after the =) for any optional parameters. If you don’t provide a default for a parameter, then it will be a positional parameter.

+
+
+

Param annotations

+

If you want to use the full power of argparse, you can do so by using Param annotations instead of type annotations and docments, like so:

+
from fastcore.script import *
+@call_parse
+def main(msg:Param("The message", str),
+         upper:Param("Convert to uppercase?", store_true)):
+    "Print `msg`, optionally converting to uppercase"
+    print(msg.upper() if upper else msg)
+

If you use this approach, then each parameter in your function should have an annotation Param(...) (as in the example above). You can pass the following when calling Param: help,type,opt,action,nargs,const,choices,required . Except for opt, all of these are just passed directly to argparse, so you have all the power of that module at your disposal. Generally you’ll want to pass at least help (since this is provided as the help string for that parameter) and type (to ensure that you get the type of data you expect). opt is a bool that defines whether a param is optional or required (positional) - but you’ll generally not need to set this manually, because fastcore.script will set it for you automatically based on default values.

+
+
+

setuptools scripts

+

There’s a really nice feature of pip/setuptools that lets you create commandline scripts directly from functions, makes them available in the PATH, and even makes your scripts cross-platform (e.g. in Windows it creates an exe). fastcore.script supports this feature too. The trick to making a function available as a script is to add a console_scripts section to your setup file, of the form: script_name=module:function_name. E.g. in this case we use: test_fastcore.script=fastcore.script.test_cli:main. With this, you can then just type test_fastcore.script at any time, from any directory, and your script will be called (once it’s installed using one of the methods below).

+

You don’t actually have to write a setup.py yourself. Instead, just use nbdev. Then modify settings.ini as appropriate for your module/script. To install your script directly, you can type pip install -e .. Your script, when installed this way (it’s called an editable install), will automatically be up to date even if you edit it - there’s no need to reinstall it after editing. With nbdev you can even make your module and script available for installation directly from pip and conda by running make release.

+
+
+

API details

+
+

source

+
+

store_true

+
+
 store_true ()
+
+

Placeholder to pass to Param for store_true action

+
+

source

+
+
+

store_false

+
+
 store_false ()
+
+

Placeholder to pass to Param for store_false action

+
+

source

+
+
+

bool_arg

+
+
 bool_arg (v)
+
+

Use as type for Param to get bool behavior

+
+

source

+
+
+

clean_type_str

+
+
 clean_type_str (x:str)
+
+
+
class Test: pass
+
+test_eq(clean_type_str(argparse.ArgumentParser), 'argparse.ArgumentParser')
+test_eq(clean_type_str(Test), 'Test')
+test_eq(clean_type_str(int), 'int')
+test_eq(clean_type_str(float), 'float')
+test_eq(clean_type_str(store_false), 'store_false')
+
+
+

source

+
+
+

Param

+
+
 Param (help='', type=None, opt=True, action=None, nargs=None, const=None,
+        choices=None, required=None, default=None)
+
+

A parameter in a function used in anno_parser or call_parse

+
+
test_eq(repr(Param("Help goes here")), '<Help goes here>')
+test_eq(repr(Param("Help", int)), 'int <Help>')
+test_eq(repr(Param(help=None, type=int)), 'int')
+test_eq(repr(Param(help=None, type=None)), '')
+
+

Each parameter in your function should have an annotation Param(...). You can pass the following when calling Param: help,type,opt,action,nargs,const,choices,required (i.e. it takes the same parameters as argparse.ArgumentParser.add_argument, plus opt). Except for opt, all of these are just passed directly to argparse, so you have all the power of that module at your disposal. Generally you’ll want to pass at least help (since this is provided as the help string for that parameter) and type (to ensure that you get the type of data you expect).

+

opt is a bool that defines whether a param is optional or required (positional) - but you’ll generally not need to set this manually, because fastcore.script will set it for you automatically based on default values. You should provide a default (after the =) for any optional parameters. If you don’t provide a default for a parameter, then it will be a positional parameter.

+

Param’s __repr__ also allows for more informative function annotation when looking up the function’s doc using shift+tab. You see the type annotation (if there is one) and the accompanying help documentation with it.

+
+
def f(required:Param("Required param", int),
+      a:Param("param 1", bool_arg),
+      b:Param("param 2", str)="test"):
+    "my docs"
+    ...
+
+
+
help(f)
+
+
Help on function f in module __main__:
+
+f(required: int <Required param>, a: bool_arg <param 1>, b: str <param 2> = 'test')
+    my docs
+
+
+
+
+
p = Param(help="help", type=int)
+p.set_default(1)
+test_eq(p.kwargs, {'help': 'help (default: 1)', 'type': int, 'default': 1})
+
+
+

source

+
+
+

anno_parser

+
+
 anno_parser (func, prog:str=None)
+
+

Look at params (annotated with Param) in func and return an ArgumentParser

+ + + + + + + + + + + + + + + + + + + + + + + +
TypeDefaultDetails
funcFunction to get arguments from
progstrNoneThe name of the program
+

This converts a function with parameter annotations of type Param into an argparse.ArgumentParser object. Function arguments with a default provided are optional, and other arguments are positional.

+
+
_en = str_enum('_en', 'aa','bb','cc')
+def f(required:Param("Required param", int),
+      a:Param("param 1", bool_arg),
+      b:Param("param 2", str)="test",
+      c:Param("param 3", _en)=_en.aa):
+    "my docs"
+    ...
+
+p = anno_parser(f, 'progname')
+p.print_help()
+
+
usage: progname [-h] [--b B] [--c {aa,bb,cc}] required a
+
+my docs
+
+positional arguments:
+  required        Required param
+  a               param 1
+
+optional arguments:
+  -h, --help      show this help message and exit
+  --b B           param 2 (default: test)
+  --c {aa,bb,cc}  param 3 (default: aa)
+
+
+

It also works with type annotations and docments:

+
+
def g(required:int,  # Required param
+      a:bool_arg,    # param 1
+      b="test",      # param 2
+      c:_en=_en.aa): # param 3
+    "my docs"
+    ...
+
+p = anno_parser(g, 'progname')
+p.print_help()
+
+
usage: progname [-h] [--b B] [--c {aa,bb,cc}] required a
+
+my docs
+
+positional arguments:
+  required        Required param
+  a               param 1
+
+optional arguments:
+  -h, --help      show this help message and exit
+  --b B           param 2 (default: test)
+  --c {aa,bb,cc}  param 3 (default: aa)
+
+
+
+

source

+
+
+

args_from_prog

+
+
 args_from_prog (func, prog)
+
+

Extract args from prog

+

Sometimes it’s convenient to extract arguments from the actual name of the called program. args_from_prog will do this, assuming that names and values of the params are separated by a #. Optionally there can also be a prefix separated by ## (double underscore).

+
+
exp = {'a': False, 'b': 'baa'}
+test_eq(args_from_prog(f, 'foo##a#0#b#baa'), exp)
+test_eq(args_from_prog(f, 'a#0#b#baa'), exp)
+
+
+

source

+
+
+

call_parse

+
+
 call_parse (func=None, nested=False)
+
+

Decorator to create a simple CLI from func using anno_parser

+
+
@call_parse
+def test_add(
+    a:int=0,  # param a
+    b:int=0  # param 1
+):
+    "Add up `a` and `b`"
+    return a + b
+
+

call_parse decorated functions work as regular functions and also as command-line interface functions.

+
+
test_eq(test_add(1,2), 3)
+
+

This is the main way to use fastcore.script; decorate your function with call_parse, add Param annotations (as shown above) or type annotations and docments, and it can then be used as a script.

+

Use the nested keyword argument to create nested parsers, where earlier parsers consume only their known args from sys.argv before later parsers are used. This is useful to create one command line application that executes another. For example:

+
myrunner --keyword 1 script.py -- <script.py args>
+

A separating -- after the first application’s args is recommended though not always required, otherwise args may be parsed in unexpected ways. For example:

+
myrunner script.py -h
+

would display myrunner’s help and not script.py’s.

+ + +
+
+ +
+ +
+ + + + + \ No newline at end of file diff --git a/script.html.md b/script.html.md new file mode 100644 index 00000000..85fa87ee --- /dev/null +++ b/script.html.md @@ -0,0 +1,431 @@ +# Script - CLI + + + + +Part of [fast.ai](https://www.fast.ai)’s toolkit for delightful +developer experiences. + +## Overview + +Sometimes, you want to create a quick script, either for yourself, or +for others. But in Python, that involves a whole lot of boilerplate and +ceremony, especially if you want to support command line arguments, +provide help, and other niceties. You can use +[argparse](https://docs.python.org/3/library/argparse.html) for this +purpose, which comes with Python, but it’s complex and verbose. + +`fastcore.script` makes life easier. There are much fancier modules to +help you write scripts (we recommend [Python +Fire](https://github.com/google/python-fire), and +[Click](https://click.palletsprojects.com/en/7.x/) is also popular), but +fastcore.script is very fast and very simple. In fact, it’s \<50 lines +of code! Basically, it’s just a little wrapper around `argparse` that +uses modern Python features and some thoughtful defaults to get rid of +the boilerplate. + +For full details, see the [docs](https://fastcore.script.fast.ai) for +`core`. + +## Example + +Here’s a complete example (available in `examples/test_fastcore.py`): + +``` python +from fastcore.script import * +@call_parse +def main(msg:str, # The message + upper:bool): # Convert to uppercase? + "Print `msg`, optionally converting to uppercase" + print(msg.upper() if upper else msg) +``` + +If you copy that info a file and run it, you’ll see: + + $ examples/test_fastcore.py --help + usage: test_fastcore.py [-h] [--upper] msg + + Print `msg`, optionally converting to uppercase + + positional arguments: + msg The message + + optional arguments: + -h, --help show this help message and exit + --upper Convert to uppercase? (default: False) + +As you see, we didn’t need any `if __name__ == "__main__"`, we didn’t +have to parse arguments, we just wrote a function, added a decorator to +it, and added some annotations to our function’s parameters. As a bonus, +we can also use this function directly from a REPL such as Jupyter +Notebook - it’s not just for command line scripts! + +You should provide a default (after the `=`) for any *optional* +parameters. If you don’t provide a default for a parameter, then it will +be a *positional* parameter. + +## Param annotations + +If you want to use the full power of `argparse`, you can do so by using +[`Param`](https://fastcore.fast.ai/script.html#param) annotations +instead of type annotations and +[docments](https://fastcore.fast.ai/docments.html), like so: + +``` python +from fastcore.script import * +@call_parse +def main(msg:Param("The message", str), + upper:Param("Convert to uppercase?", store_true)): + "Print `msg`, optionally converting to uppercase" + print(msg.upper() if upper else msg) +``` + +If you use this approach, then each parameter in your function should +have an annotation `Param(...)` (as in the example above). You can pass +the following when calling +[`Param`](https://fastcore.fast.ai/script.html#param): +`help`,`type`,`opt`,`action`,`nargs`,`const`,`choices`,`required` . +Except for `opt`, all of these are just passed directly to `argparse`, +so you have all the power of that module at your disposal. Generally +you’ll want to pass at least `help` (since this is provided as the help +string for that parameter) and `type` (to ensure that you get the type +of data you expect). `opt` is a bool that defines whether a param is +optional or required (positional) - but you’ll generally not need to set +this manually, because fastcore.script will set it for you automatically +based on *default* values. + +## setuptools scripts + +There’s a really nice feature of pip/setuptools that lets you create +commandline scripts directly from functions, makes them available in the +`PATH`, and even makes your scripts cross-platform (e.g. in Windows it +creates an exe). fastcore.script supports this feature too. The trick to +making a function available as a script is to add a `console_scripts` +section to your setup file, of the form: +`script_name=module:function_name`. E.g. in this case we use: +`test_fastcore.script=fastcore.script.test_cli:main`. With this, you can +then just type `test_fastcore.script` at any time, from any directory, +and your script will be called (once it’s installed using one of the +methods below). + +You don’t actually have to write a `setup.py` yourself. Instead, just +use [nbdev](https://nbdev.fast.ai). Then modify `settings.ini` as +appropriate for your module/script. To install your script directly, you +can type `pip install -e .`. Your script, when installed this way (it’s +called an [editable +install](http://codumentary.blogspot.com/2014/11/python-tip-of-year-pip-install-editable.html)), +will automatically be up to date even if you edit it - there’s no need +to reinstall it after editing. With nbdev you can even make your module +and script available for installation directly from pip and conda by +running `make release`. + +## API details + +------------------------------------------------------------------------ + +source + +### store_true + +> store_true () + +*Placeholder to pass to +[`Param`](https://fastcore.fast.ai/script.html#param) for +[`store_true`](https://fastcore.fast.ai/script.html#store_true) action* + +------------------------------------------------------------------------ + +source + +### store_false + +> store_false () + +*Placeholder to pass to +[`Param`](https://fastcore.fast.ai/script.html#param) for +[`store_false`](https://fastcore.fast.ai/script.html#store_false) +action* + +------------------------------------------------------------------------ + +source + +### bool_arg + +> bool_arg (v) + +*Use as `type` for [`Param`](https://fastcore.fast.ai/script.html#param) +to get `bool` behavior* + +------------------------------------------------------------------------ + +source + +### clean_type_str + +> clean_type_str (x:str) + +``` python +class Test: pass + +test_eq(clean_type_str(argparse.ArgumentParser), 'argparse.ArgumentParser') +test_eq(clean_type_str(Test), 'Test') +test_eq(clean_type_str(int), 'int') +test_eq(clean_type_str(float), 'float') +test_eq(clean_type_str(store_false), 'store_false') +``` + +------------------------------------------------------------------------ + +source + +### Param + +> Param (help='', type=None, opt=True, action=None, nargs=None, const=None, +> choices=None, required=None, default=None) + +*A parameter in a function used in +[`anno_parser`](https://fastcore.fast.ai/script.html#anno_parser) or +[`call_parse`](https://fastcore.fast.ai/script.html#call_parse)* + +``` python +test_eq(repr(Param("Help goes here")), '') +test_eq(repr(Param("Help", int)), 'int ') +test_eq(repr(Param(help=None, type=int)), 'int') +test_eq(repr(Param(help=None, type=None)), '') +``` + +Each parameter in your function should have an annotation `Param(...)`. +You can pass the following when calling +[`Param`](https://fastcore.fast.ai/script.html#param): +`help`,`type`,`opt`,`action`,`nargs`,`const`,`choices`,`required` +(i.e. it takes the same parameters as +`argparse.ArgumentParser.add_argument`, plus `opt`). Except for `opt`, +all of these are just passed directly to `argparse`, so you have all the +power of that module at your disposal. Generally you’ll want to pass at +least `help` (since this is provided as the help string for that +parameter) and `type` (to ensure that you get the type of data you +expect). + +`opt` is a bool that defines whether a param is optional or required +(positional) - but you’ll generally not need to set this manually, +because fastcore.script will set it for you automatically based on +*default* values. You should provide a default (after the `=`) for any +*optional* parameters. If you don’t provide a default for a parameter, +then it will be a *positional* parameter. + +Param’s `__repr__` also allows for more informative function annotation +when looking up the function’s doc using shift+tab. You see the type +annotation (if there is one) and the accompanying help documentation +with it. + +``` python +def f(required:Param("Required param", int), + a:Param("param 1", bool_arg), + b:Param("param 2", str)="test"): + "my docs" + ... +``` + +``` python +help(f) +``` + + Help on function f in module __main__: + + f(required: int , a: bool_arg , b: str = 'test') + my docs + +``` python +p = Param(help="help", type=int) +p.set_default(1) +test_eq(p.kwargs, {'help': 'help (default: 1)', 'type': int, 'default': 1}) +``` + +------------------------------------------------------------------------ + +source + +### anno_parser + +> anno_parser (func, prog:str=None) + +*Look at params (annotated with +[`Param`](https://fastcore.fast.ai/script.html#param)) in func and +return an `ArgumentParser`* + + + + + + + + + + + + + + + + + + + + + + + + +
TypeDefaultDetails
funcFunction to get arguments from
progstrNoneThe name of the program
+ +This converts a function with parameter annotations of type +[`Param`](https://fastcore.fast.ai/script.html#param) into an +`argparse.ArgumentParser` object. Function arguments with a default +provided are optional, and other arguments are positional. + +``` python +_en = str_enum('_en', 'aa','bb','cc') +def f(required:Param("Required param", int), + a:Param("param 1", bool_arg), + b:Param("param 2", str)="test", + c:Param("param 3", _en)=_en.aa): + "my docs" + ... + +p = anno_parser(f, 'progname') +p.print_help() +``` + + usage: progname [-h] [--b B] [--c {aa,bb,cc}] required a + + my docs + + positional arguments: + required Required param + a param 1 + + optional arguments: + -h, --help show this help message and exit + --b B param 2 (default: test) + --c {aa,bb,cc} param 3 (default: aa) + +It also works with type annotations and docments: + +``` python +def g(required:int, # Required param + a:bool_arg, # param 1 + b="test", # param 2 + c:_en=_en.aa): # param 3 + "my docs" + ... + +p = anno_parser(g, 'progname') +p.print_help() +``` + + usage: progname [-h] [--b B] [--c {aa,bb,cc}] required a + + my docs + + positional arguments: + required Required param + a param 1 + + optional arguments: + -h, --help show this help message and exit + --b B param 2 (default: test) + --c {aa,bb,cc} param 3 (default: aa) + +------------------------------------------------------------------------ + +source + +### args_from_prog + +> args_from_prog (func, prog) + +*Extract args from `prog`* + +Sometimes it’s convenient to extract arguments from the actual name of +the called program. +[`args_from_prog`](https://fastcore.fast.ai/script.html#args_from_prog) +will do this, assuming that names and values of the params are separated +by a `#`. Optionally there can also be a prefix separated by `##` +(double underscore). + +``` python +exp = {'a': False, 'b': 'baa'} +test_eq(args_from_prog(f, 'foo##a#0#b#baa'), exp) +test_eq(args_from_prog(f, 'a#0#b#baa'), exp) +``` + +------------------------------------------------------------------------ + +source + +### call_parse + +> call_parse (func=None, nested=False) + +*Decorator to create a simple CLI from `func` using +[`anno_parser`](https://fastcore.fast.ai/script.html#anno_parser)* + +``` python +@call_parse +def test_add( + a:int=0, # param a + b:int=0 # param 1 +): + "Add up `a` and `b`" + return a + b +``` + +[`call_parse`](https://fastcore.fast.ai/script.html#call_parse) +decorated functions work as regular functions and also as command-line +interface functions. + +``` python +test_eq(test_add(1,2), 3) +``` + +This is the main way to use `fastcore.script`; decorate your function +with [`call_parse`](https://fastcore.fast.ai/script.html#call_parse), +add [`Param`](https://fastcore.fast.ai/script.html#param) annotations +(as shown above) or type annotations and docments, and it can then be +used as a script. + +Use the `nested` keyword argument to create nested parsers, where +earlier parsers consume only their known args from `sys.argv` before +later parsers are used. This is useful to create one command line +application that executes another. For example: + +``` sh +myrunner --keyword 1 script.py -- +``` + +A separating `--` after the first application’s args is recommended +though not always required, otherwise args may be parsed in unexpected +ways. For example: + +``` sh +myrunner script.py -h +``` + +would display `myrunner`’s help and not `script.py`’s. diff --git a/search.json b/search.json new file mode 100644 index 00000000..5983eeff --- /dev/null +++ b/search.json @@ -0,0 +1,672 @@ +[ + { + "objectID": "style.html", + "href": "style.html", + "title": "Style", + "section": "", + "text": "Note\n\n\n\nStyled outputs don’t show in Quarto documentation. Please use a notebook editor to correctly view this page.\n\n\n\nsource\n\nStyleCode\n\n StyleCode (name, code, typ)\n\nAn escape sequence for styling terminal text.\nThe primary building block of the S API.\n\nprint(str(StyleCode('blue', 34, 'fg')) + 'hello' + str(StyleCode('default', 39, 'fg')) + ' world')\n\nhello world\n\n\n\nsource\n\n\nStyle\n\n Style (codes=None)\n\nA minimal terminal text styler.\nThe main way to use it is via the exported S object.\n\n\nExported source\nS = Style()\n\n\nWe start with an empty style:\n\nS\n\n<Style: none>\n\n\nDefine a new style by chaining attributes:\n\ns = S.blue.bold.underline\ns\n\n<Style: blue bold underline>\n\n\nYou can see a full list of available styles with auto-complete by typing S . Tab.\nApply a style by calling it with a string:\n\ns('hello world')\n\n'\\x1b[34m\\x1b[1m\\x1b[4mhello world\\x1b[22m\\x1b[24m\\x1b[39m'\n\n\nThat’s a raw string with the underlying escape sequences that tell the terminal how to format text. To see the styled version we have to print it:\n\nprint(s('hello world'))\n\nhello world\n\n\nYou can also nest styles:\n\nprint(S.bold(S.blue('key') + ' = value ') + S.light_gray(' ' + S.underline('# With a comment')) + ' and unstyled text')\n\nkey = value # With a comment and unstyled text\n\n\n\nprint(S.blue('this '+S.bold('is')+' a test'))\n\nthis is a test\n\n\n\nsource\n\n\ndemo\n\n demo ()\n\nDemonstrate all available styles and their codes.\n\ndemo()\n\n 30 black \n 31 red \n 32 green \n 33 yellow \n 34 blue \n 35 magenta \n 36 cyan \n 37 light_gray \n 39 default \n 90 dark_gray \n 91 light_red \n 92 light_green \n 93 light_yellow \n 94 light_blue \n 95 light_magenta \n 96 light_cyan \n 97 white \n 40 black_bg \n 41 red_bg \n 42 green_bg \n 43 yellow_bg \n 44 blue_bg \n 45 magenta_bg \n 46 cyan_bg \n 47 light_gray_bg \n 49 default_bg \n100 dark_gray_bg \n101 light_red_bg \n102 light_green_bg \n103 light_yellow_bg \n104 light_blue_bg \n105 light_magenta_bg\n106 light_cyan_bg \n107 white_bg \n 1 bold \n 2 dim \n 3 italic \n 4 underline \n 5 blink \n 7 invert \n 8 hidden \n 9 strikethrough \n 22 reset_bold \n 22 reset_dim \n 23 reset_italic \n 24 reset_underline \n 25 reset_blink \n 27 reset_invert \n 28 reset_hidden \n 29 reset_strikethrough\n 0 reset", + "crumbs": [ + "Style" + ] + }, + { + "objectID": "xtras.html", + "href": "xtras.html", + "title": "Utility functions", + "section": "", + "text": "Utilities (other than extensions to Pathlib.Path) for dealing with IO.\n\nsource\n\n\n\n walk (path:pathlib.Path|str, symlinks:bool=True, keep_file:<built-\n infunctioncallable>=<function ret_true>, keep_folder:<built-\n infunctioncallable>=<function ret_true>, skip_folder:<built-\n infunctioncallable>=<function ret_false>, func:<built-\n infunctioncallable>=<function join>, ret_folders:bool=False)\n\nGenerator version of os.walk, using functions to filter files and folders\n\n\n\n\n\n\n\n\n\n\nType\nDefault\nDetails\n\n\n\n\npath\npathlib.Path | str\n\npath to start searching\n\n\nsymlinks\nbool\nTrue\nfollow symlinks?\n\n\nkeep_file\ncallable\nret_true\nfunction that returns True for wanted files\n\n\nkeep_folder\ncallable\nret_true\nfunction that returns True for folders to enter\n\n\nskip_folder\ncallable\nret_false\nfunction that returns True for folders to skip\n\n\nfunc\ncallable\njoin\nfunction to apply to each matched file\n\n\nret_folders\nbool\nFalse\nreturn folders, not just files\n\n\n\n\nsource\n\n\n\n\n globtastic (path:pathlib.Path|str, recursive:bool=True,\n symlinks:bool=True, file_glob:str=None, file_re:str=None,\n folder_re:str=None, skip_file_glob:str=None,\n skip_file_re:str=None, skip_folder_re:str=None, func:<built-\n infunctioncallable>=<function join>, ret_folders:bool=False)\n\nA more powerful glob, including regex matches, symlink handling, and skip parameters\n\n\n\n\nType\nDefault\nDetails\n\n\n\n\npath\npathlib.Path | str\n\npath to start searching\n\n\nrecursive\nbool\nTrue\nsearch subfolders\n\n\nsymlinks\nbool\nTrue\nfollow symlinks?\n\n\nfile_glob\nstr\nNone\nOnly include files matching glob\n\n\nfile_re\nstr\nNone\nOnly include files matching regex\n\n\nfolder_re\nstr\nNone\nOnly enter folders matching regex\n\n\nskip_file_glob\nstr\nNone\nSkip files matching glob\n\n\nskip_file_re\nstr\nNone\nSkip files matching regex\n\n\nskip_folder_re\nstr\nNone\nSkip folders matching regex,\n\n\nfunc\ncallable\njoin\nfunction to apply to each matched file\n\n\nret_folders\nbool\nFalse\nreturn folders, not just files\n\n\nReturns\nL\n\nPaths to matched files\n\n\n\n\nglobtastic('.', skip_folder_re='^[_.]', folder_re='core', file_glob='*.*py*', file_re='c')\n\n(#5) ['./fastcore/docments.py','./fastcore/dispatch.py','./fastcore/basics.py','./fastcore/docscrape.py','./fastcore/script.py']\n\n\n\nsource\n\n\n\n\n maybe_open (f, mode='r', **kwargs)\n\nContext manager: open f if it is a path (and close on exit)\nThis is useful for functions where you want to accept a path or file. maybe_open will not close your file handle if you pass one in.\n\ndef _f(fn):\n with maybe_open(fn) as f: return f.encoding\n\nfname = '00_test.ipynb'\nsys_encoding = 'cp1252' if sys.platform == 'win32' else 'UTF-8'\ntest_eq(_f(fname), sys_encoding)\nwith open(fname) as fh: test_eq(_f(fh), sys_encoding)\n\nFor example, we can use this to reimplement imghdr.what from the Python standard library, which is written in Python 3.9 as:\n\nfrom fastcore import imghdr\n\n\ndef what(file, h=None):\n f = None\n try:\n if h is None:\n if isinstance(file, (str,os.PathLike)):\n f = open(file, 'rb')\n h = f.read(32)\n else:\n location = file.tell()\n h = file.read(32)\n file.seek(location)\n for tf in imghdr.tests:\n res = tf(h, f)\n if res: return res\n finally:\n if f: f.close()\n return None\n\nHere’s an example of the use of this function:\n\nfname = 'images/puppy.jpg'\nwhat(fname)\n\n'jpeg'\n\n\nWith maybe_open, Self, and L.map_first, we can rewrite this in a much more concise and (in our opinion) clear way:\n\ndef what(file, h=None):\n if h is None:\n with maybe_open(file, 'rb') as f: h = f.peek(32)\n return L(imghdr.tests).map_first(Self(h,file))\n\n…and we can check that it still works:\n\ntest_eq(what(fname), 'jpeg')\n\n…along with the version passing a file handle:\n\nwith open(fname,'rb') as f: test_eq(what(f), 'jpeg')\n\n…along with the h parameter version:\n\nwith open(fname,'rb') as f: test_eq(what(None, h=f.read(32)), 'jpeg')\n\n\nsource\n\n\n\n\n mkdir (path, exist_ok=False, parents=False, overwrite=False, **kwargs)\n\nCreates and returns a directory defined by path, optionally removing previous existing directory if overwrite is True\n\nwith tempfile.TemporaryDirectory() as d:\n path = Path(os.path.join(d, 'new_dir'))\n new_dir = mkdir(path)\n assert new_dir.exists()\n test_eq(new_dir, path)\n \n # test overwrite\n with open(new_dir/'test.txt', 'w') as f: f.writelines('test')\n test_eq(len(list(walk(new_dir))), 1) # assert file is present\n new_dir = mkdir(new_dir, overwrite=True)\n test_eq(len(list(walk(new_dir))), 0) # assert file was deleted\n\n\nsource\n\n\n\n\n image_size (fn)\n\nTuple of (w,h) for png, gif, or jpg; None otherwise\n\ntest_eq(image_size(fname), (1200,803))\n\n\nsource\n\n\n\n\n bunzip (fn)\n\nbunzip fn, raising exception if output already exists\n\nf = Path('files/test.txt')\nif f.exists(): f.unlink()\nbunzip('files/test.txt.bz2')\nt = f.open().readlines()\ntest_eq(len(t),1)\ntest_eq(t[0], 'test\\n')\nf.unlink()\n\n\nsource\n\n\n\n\n loads (s, **kw)\n\nSame as json.loads, but handles None\n\nsource\n\n\n\n\n loads_multi (s:str)\n\nGenerator of >=0 decoded json dicts, possibly with non-json ignored text at start and end\n\ntst = \"\"\"\n# ignored\n{ \"a\":1 }\nhello\n{\n\"b\":2\n}\n\"\"\"\n\ntest_eq(list(loads_multi(tst)), [{'a': 1}, {'b': 2}])\n\n\nsource\n\n\n\n\n dumps (obj, **kw)\n\nSame as json.dumps, but uses ujson if available\n\nsource\n\n\n\n\n untar_dir (fname, dest, rename=False, overwrite=False)\n\nuntar file into dest, creating a directory if the root contains more than one item\n\ndef test_untar(foldername, rename=False, **kwargs):\n with tempfile.TemporaryDirectory() as d:\n nm = os.path.join(d, 'a')\n shutil.make_archive(nm, 'gztar', **kwargs)\n with tempfile.TemporaryDirectory() as d2:\n d2 = Path(d2)\n untar_dir(nm+'.tar.gz', d2, rename=rename)\n test_eq(d2.ls(), [d2/foldername])\n\nIf the contents of fname contain just one file or directory, it is placed directly in dest:\n\n# using `base_dir` in `make_archive` results in `images` directory included in file names\ntest_untar('images', base_dir='images')\n\nIf rename then the directory created is named based on the archive, without extension:\n\ntest_untar('a', base_dir='images', rename=True)\n\nIf the contents of fname contain multiple files and directories, a new folder in dest is created with the same name as fname (but without extension):\n\n# using `root_dir` in `make_archive` results in `images` directory *not* included in file names\ntest_untar('a', root_dir='images')\n\n\nsource\n\n\n\n\n repo_details (url)\n\nTuple of owner,name from ssh or https git repo url\n\ntest_eq(repo_details('https://github.com/fastai/fastai.git'), ['fastai', 'fastai'])\ntest_eq(repo_details('git@github.com:fastai/nbdev.git\\n'), ['fastai', 'nbdev'])\n\n\nsource\n\n\n\n\n run (cmd, *rest, same_in_win=False, ignore_ex=False, as_bytes=False,\n stderr=False)\n\nPass cmd (splitting with shlex if string) to subprocess.run; return stdout; raise IOError if fails\nYou can pass a string (which will be split based on standard shell rules), a list, or pass args directly:\n\nrun('echo', same_in_win=True)\nrun('pip', '--version', same_in_win=True)\nrun(['pip', '--version'], same_in_win=True)\n\n'pip 23.3.1 from /Users/jhoward/miniconda3/lib/python3.11/site-packages/pip (python 3.11)'\n\n\n\nif sys.platform == 'win32':\n assert 'ipynb' in run('cmd /c dir /p')\n assert 'ipynb' in run(['cmd', '/c', 'dir', '/p'])\n assert 'ipynb' in run('cmd', '/c', 'dir', '/p')\nelse:\n assert 'ipynb' in run('ls -ls')\n assert 'ipynb' in run(['ls', '-l'])\n assert 'ipynb' in run('ls', '-l')\n\nSome commands fail in non-error situations, like grep. Use ignore_ex in those cases, which will return a tuple of stdout and returncode:\n\nif sys.platform == 'win32':\n test_eq(run('cmd /c findstr asdfds 00_test.ipynb', ignore_ex=True)[0], 1)\nelse:\n test_eq(run('grep asdfds 00_test.ipynb', ignore_ex=True)[0], 1)\n\nrun automatically decodes returned bytes to a str. Use as_bytes to skip that:\n\nif sys.platform == 'win32':\n test_eq(run('cmd /c echo hi'), 'hi')\nelse:\n test_eq(run('echo hi', as_bytes=True), b'hi\\n')\n\n\nsource\n\n\n\n\n open_file (fn, mode='r', **kwargs)\n\nOpen a file, with optional compression if gz or bz2 suffix\n\nsource\n\n\n\n\n save_pickle (fn, o)\n\nSave a pickle file, to a file name or opened file\n\nsource\n\n\n\n\n load_pickle (fn)\n\nLoad a pickle file from a file name or opened file\n\nfor suf in '.pkl','.bz2','.gz':\n # delete=False is added for Windows\n # https://stackoverflow.com/questions/23212435/permission-denied-to-write-to-my-temporary-file\n with tempfile.NamedTemporaryFile(suffix=suf, delete=False) as f:\n fn = Path(f.name)\n save_pickle(fn, 't')\n t = load_pickle(fn)\n f.close()\n test_eq(t,'t')\n\n\nsource\n\n\n\n\n parse_env (s:str=None, fn:Union[str,pathlib.Path]=None)\n\nParse a shell-style environment string or file\n\ntestf = \"\"\"# comment\n # another comment\n export FOO=\"bar#baz\"\nBAR=thing # comment \"ok\"\n baz='thong'\nQUX=quux\nexport ZAP = \"zip\" # more comments\n FOOBAR = 42 # trailing space and comment\"\"\"\n\nexp = dict(FOO='bar#baz', BAR='thing', baz='thong', QUX='quux', ZAP='zip', FOOBAR='42')\n\ntest_eq(parse_env(testf), exp)\n\n\nsource\n\n\n\n\n expand_wildcards (code)\n\nExpand all wildcard imports in the given code string.\n\ninp = \"\"\"from math import *\nfrom os import *\nfrom random import *\ndef func(): return sin(pi) + path.join('a', 'b') + randint(1, 10)\"\"\"\n\nexp = \"\"\"from math import pi, sin\nfrom os import path\nfrom random import randint\ndef func(): return sin(pi) + path.join('a', 'b') + randint(1, 10)\"\"\"\n\ntest_eq(expand_wildcards(inp), exp)\n\ninp = \"\"\"from itertools import *\ndef func(): pass\"\"\"\ntest_eq(expand_wildcards(inp), inp)\n\ninp = \"\"\"def outer():\n from math import *\n def inner():\n from os import *\n return sin(pi) + path.join('a', 'b')\"\"\"\n\nexp = \"\"\"def outer():\n from math import pi, sin\n def inner():\n from os import path\n return sin(pi) + path.join('a', 'b')\"\"\"\n\ntest_eq(expand_wildcards(inp), exp)", + "crumbs": [ + "Utility functions" + ] + }, + { + "objectID": "xtras.html#file-functions", + "href": "xtras.html#file-functions", + "title": "Utility functions", + "section": "", + "text": "Utilities (other than extensions to Pathlib.Path) for dealing with IO.\n\nsource\n\n\n\n walk (path:pathlib.Path|str, symlinks:bool=True, keep_file:<built-\n infunctioncallable>=<function ret_true>, keep_folder:<built-\n infunctioncallable>=<function ret_true>, skip_folder:<built-\n infunctioncallable>=<function ret_false>, func:<built-\n infunctioncallable>=<function join>, ret_folders:bool=False)\n\nGenerator version of os.walk, using functions to filter files and folders\n\n\n\n\n\n\n\n\n\n\nType\nDefault\nDetails\n\n\n\n\npath\npathlib.Path | str\n\npath to start searching\n\n\nsymlinks\nbool\nTrue\nfollow symlinks?\n\n\nkeep_file\ncallable\nret_true\nfunction that returns True for wanted files\n\n\nkeep_folder\ncallable\nret_true\nfunction that returns True for folders to enter\n\n\nskip_folder\ncallable\nret_false\nfunction that returns True for folders to skip\n\n\nfunc\ncallable\njoin\nfunction to apply to each matched file\n\n\nret_folders\nbool\nFalse\nreturn folders, not just files\n\n\n\n\nsource\n\n\n\n\n globtastic (path:pathlib.Path|str, recursive:bool=True,\n symlinks:bool=True, file_glob:str=None, file_re:str=None,\n folder_re:str=None, skip_file_glob:str=None,\n skip_file_re:str=None, skip_folder_re:str=None, func:<built-\n infunctioncallable>=<function join>, ret_folders:bool=False)\n\nA more powerful glob, including regex matches, symlink handling, and skip parameters\n\n\n\n\nType\nDefault\nDetails\n\n\n\n\npath\npathlib.Path | str\n\npath to start searching\n\n\nrecursive\nbool\nTrue\nsearch subfolders\n\n\nsymlinks\nbool\nTrue\nfollow symlinks?\n\n\nfile_glob\nstr\nNone\nOnly include files matching glob\n\n\nfile_re\nstr\nNone\nOnly include files matching regex\n\n\nfolder_re\nstr\nNone\nOnly enter folders matching regex\n\n\nskip_file_glob\nstr\nNone\nSkip files matching glob\n\n\nskip_file_re\nstr\nNone\nSkip files matching regex\n\n\nskip_folder_re\nstr\nNone\nSkip folders matching regex,\n\n\nfunc\ncallable\njoin\nfunction to apply to each matched file\n\n\nret_folders\nbool\nFalse\nreturn folders, not just files\n\n\nReturns\nL\n\nPaths to matched files\n\n\n\n\nglobtastic('.', skip_folder_re='^[_.]', folder_re='core', file_glob='*.*py*', file_re='c')\n\n(#5) ['./fastcore/docments.py','./fastcore/dispatch.py','./fastcore/basics.py','./fastcore/docscrape.py','./fastcore/script.py']\n\n\n\nsource\n\n\n\n\n maybe_open (f, mode='r', **kwargs)\n\nContext manager: open f if it is a path (and close on exit)\nThis is useful for functions where you want to accept a path or file. maybe_open will not close your file handle if you pass one in.\n\ndef _f(fn):\n with maybe_open(fn) as f: return f.encoding\n\nfname = '00_test.ipynb'\nsys_encoding = 'cp1252' if sys.platform == 'win32' else 'UTF-8'\ntest_eq(_f(fname), sys_encoding)\nwith open(fname) as fh: test_eq(_f(fh), sys_encoding)\n\nFor example, we can use this to reimplement imghdr.what from the Python standard library, which is written in Python 3.9 as:\n\nfrom fastcore import imghdr\n\n\ndef what(file, h=None):\n f = None\n try:\n if h is None:\n if isinstance(file, (str,os.PathLike)):\n f = open(file, 'rb')\n h = f.read(32)\n else:\n location = file.tell()\n h = file.read(32)\n file.seek(location)\n for tf in imghdr.tests:\n res = tf(h, f)\n if res: return res\n finally:\n if f: f.close()\n return None\n\nHere’s an example of the use of this function:\n\nfname = 'images/puppy.jpg'\nwhat(fname)\n\n'jpeg'\n\n\nWith maybe_open, Self, and L.map_first, we can rewrite this in a much more concise and (in our opinion) clear way:\n\ndef what(file, h=None):\n if h is None:\n with maybe_open(file, 'rb') as f: h = f.peek(32)\n return L(imghdr.tests).map_first(Self(h,file))\n\n…and we can check that it still works:\n\ntest_eq(what(fname), 'jpeg')\n\n…along with the version passing a file handle:\n\nwith open(fname,'rb') as f: test_eq(what(f), 'jpeg')\n\n…along with the h parameter version:\n\nwith open(fname,'rb') as f: test_eq(what(None, h=f.read(32)), 'jpeg')\n\n\nsource\n\n\n\n\n mkdir (path, exist_ok=False, parents=False, overwrite=False, **kwargs)\n\nCreates and returns a directory defined by path, optionally removing previous existing directory if overwrite is True\n\nwith tempfile.TemporaryDirectory() as d:\n path = Path(os.path.join(d, 'new_dir'))\n new_dir = mkdir(path)\n assert new_dir.exists()\n test_eq(new_dir, path)\n \n # test overwrite\n with open(new_dir/'test.txt', 'w') as f: f.writelines('test')\n test_eq(len(list(walk(new_dir))), 1) # assert file is present\n new_dir = mkdir(new_dir, overwrite=True)\n test_eq(len(list(walk(new_dir))), 0) # assert file was deleted\n\n\nsource\n\n\n\n\n image_size (fn)\n\nTuple of (w,h) for png, gif, or jpg; None otherwise\n\ntest_eq(image_size(fname), (1200,803))\n\n\nsource\n\n\n\n\n bunzip (fn)\n\nbunzip fn, raising exception if output already exists\n\nf = Path('files/test.txt')\nif f.exists(): f.unlink()\nbunzip('files/test.txt.bz2')\nt = f.open().readlines()\ntest_eq(len(t),1)\ntest_eq(t[0], 'test\\n')\nf.unlink()\n\n\nsource\n\n\n\n\n loads (s, **kw)\n\nSame as json.loads, but handles None\n\nsource\n\n\n\n\n loads_multi (s:str)\n\nGenerator of >=0 decoded json dicts, possibly with non-json ignored text at start and end\n\ntst = \"\"\"\n# ignored\n{ \"a\":1 }\nhello\n{\n\"b\":2\n}\n\"\"\"\n\ntest_eq(list(loads_multi(tst)), [{'a': 1}, {'b': 2}])\n\n\nsource\n\n\n\n\n dumps (obj, **kw)\n\nSame as json.dumps, but uses ujson if available\n\nsource\n\n\n\n\n untar_dir (fname, dest, rename=False, overwrite=False)\n\nuntar file into dest, creating a directory if the root contains more than one item\n\ndef test_untar(foldername, rename=False, **kwargs):\n with tempfile.TemporaryDirectory() as d:\n nm = os.path.join(d, 'a')\n shutil.make_archive(nm, 'gztar', **kwargs)\n with tempfile.TemporaryDirectory() as d2:\n d2 = Path(d2)\n untar_dir(nm+'.tar.gz', d2, rename=rename)\n test_eq(d2.ls(), [d2/foldername])\n\nIf the contents of fname contain just one file or directory, it is placed directly in dest:\n\n# using `base_dir` in `make_archive` results in `images` directory included in file names\ntest_untar('images', base_dir='images')\n\nIf rename then the directory created is named based on the archive, without extension:\n\ntest_untar('a', base_dir='images', rename=True)\n\nIf the contents of fname contain multiple files and directories, a new folder in dest is created with the same name as fname (but without extension):\n\n# using `root_dir` in `make_archive` results in `images` directory *not* included in file names\ntest_untar('a', root_dir='images')\n\n\nsource\n\n\n\n\n repo_details (url)\n\nTuple of owner,name from ssh or https git repo url\n\ntest_eq(repo_details('https://github.com/fastai/fastai.git'), ['fastai', 'fastai'])\ntest_eq(repo_details('git@github.com:fastai/nbdev.git\\n'), ['fastai', 'nbdev'])\n\n\nsource\n\n\n\n\n run (cmd, *rest, same_in_win=False, ignore_ex=False, as_bytes=False,\n stderr=False)\n\nPass cmd (splitting with shlex if string) to subprocess.run; return stdout; raise IOError if fails\nYou can pass a string (which will be split based on standard shell rules), a list, or pass args directly:\n\nrun('echo', same_in_win=True)\nrun('pip', '--version', same_in_win=True)\nrun(['pip', '--version'], same_in_win=True)\n\n'pip 23.3.1 from /Users/jhoward/miniconda3/lib/python3.11/site-packages/pip (python 3.11)'\n\n\n\nif sys.platform == 'win32':\n assert 'ipynb' in run('cmd /c dir /p')\n assert 'ipynb' in run(['cmd', '/c', 'dir', '/p'])\n assert 'ipynb' in run('cmd', '/c', 'dir', '/p')\nelse:\n assert 'ipynb' in run('ls -ls')\n assert 'ipynb' in run(['ls', '-l'])\n assert 'ipynb' in run('ls', '-l')\n\nSome commands fail in non-error situations, like grep. Use ignore_ex in those cases, which will return a tuple of stdout and returncode:\n\nif sys.platform == 'win32':\n test_eq(run('cmd /c findstr asdfds 00_test.ipynb', ignore_ex=True)[0], 1)\nelse:\n test_eq(run('grep asdfds 00_test.ipynb', ignore_ex=True)[0], 1)\n\nrun automatically decodes returned bytes to a str. Use as_bytes to skip that:\n\nif sys.platform == 'win32':\n test_eq(run('cmd /c echo hi'), 'hi')\nelse:\n test_eq(run('echo hi', as_bytes=True), b'hi\\n')\n\n\nsource\n\n\n\n\n open_file (fn, mode='r', **kwargs)\n\nOpen a file, with optional compression if gz or bz2 suffix\n\nsource\n\n\n\n\n save_pickle (fn, o)\n\nSave a pickle file, to a file name or opened file\n\nsource\n\n\n\n\n load_pickle (fn)\n\nLoad a pickle file from a file name or opened file\n\nfor suf in '.pkl','.bz2','.gz':\n # delete=False is added for Windows\n # https://stackoverflow.com/questions/23212435/permission-denied-to-write-to-my-temporary-file\n with tempfile.NamedTemporaryFile(suffix=suf, delete=False) as f:\n fn = Path(f.name)\n save_pickle(fn, 't')\n t = load_pickle(fn)\n f.close()\n test_eq(t,'t')\n\n\nsource\n\n\n\n\n parse_env (s:str=None, fn:Union[str,pathlib.Path]=None)\n\nParse a shell-style environment string or file\n\ntestf = \"\"\"# comment\n # another comment\n export FOO=\"bar#baz\"\nBAR=thing # comment \"ok\"\n baz='thong'\nQUX=quux\nexport ZAP = \"zip\" # more comments\n FOOBAR = 42 # trailing space and comment\"\"\"\n\nexp = dict(FOO='bar#baz', BAR='thing', baz='thong', QUX='quux', ZAP='zip', FOOBAR='42')\n\ntest_eq(parse_env(testf), exp)\n\n\nsource\n\n\n\n\n expand_wildcards (code)\n\nExpand all wildcard imports in the given code string.\n\ninp = \"\"\"from math import *\nfrom os import *\nfrom random import *\ndef func(): return sin(pi) + path.join('a', 'b') + randint(1, 10)\"\"\"\n\nexp = \"\"\"from math import pi, sin\nfrom os import path\nfrom random import randint\ndef func(): return sin(pi) + path.join('a', 'b') + randint(1, 10)\"\"\"\n\ntest_eq(expand_wildcards(inp), exp)\n\ninp = \"\"\"from itertools import *\ndef func(): pass\"\"\"\ntest_eq(expand_wildcards(inp), inp)\n\ninp = \"\"\"def outer():\n from math import *\n def inner():\n from os import *\n return sin(pi) + path.join('a', 'b')\"\"\"\n\nexp = \"\"\"def outer():\n from math import pi, sin\n def inner():\n from os import path\n return sin(pi) + path.join('a', 'b')\"\"\"\n\ntest_eq(expand_wildcards(inp), exp)", + "crumbs": [ + "Utility functions" + ] + }, + { + "objectID": "xtras.html#collections", + "href": "xtras.html#collections", + "title": "Utility functions", + "section": "Collections", + "text": "Collections\n\nsource\n\ndict2obj\n\n dict2obj (d, list_func=<class 'fastcore.foundation.L'>, dict_func=<class\n 'fastcore.basics.AttrDict'>)\n\nConvert (possibly nested) dicts (or lists of dicts) to AttrDict\nThis is a convenience to give you “dotted” access to (possibly nested) dictionaries, e.g:\n\nd1 = dict(a=1, b=dict(c=2,d=3))\nd2 = dict2obj(d1)\ntest_eq(d2.b.c, 2)\ntest_eq(d2.b['c'], 2)\n\nIt can also be used on lists of dicts.\n\n_list_of_dicts = [d1, d1]\nds = dict2obj(_list_of_dicts)\ntest_eq(ds[0].b.c, 2)\n\n\nsource\n\n\nobj2dict\n\n obj2dict (d)\n\nConvert (possibly nested) AttrDicts (or lists of AttrDicts) to dict\nobj2dict can be used to reverse what is done by dict2obj:\n\ntest_eq(obj2dict(d2), d1)\ntest_eq(obj2dict(ds), _list_of_dicts)\n\n\nsource\n\n\nrepr_dict\n\n repr_dict (d)\n\nPrint nested dicts and lists, such as returned by dict2obj\n\nprint(repr_dict(d2))\n\n- a: 1\n- b: \n - c: 2\n - d: 3\n\n\n\nsource\n\n\nis_listy\n\n is_listy (x)\n\nisinstance(x, (tuple,list,L,slice,Generator))\n\nassert is_listy((1,))\nassert is_listy([1])\nassert is_listy(L([1]))\nassert is_listy(slice(2))\nassert not is_listy(array([1]))\n\n\nsource\n\n\nmapped\n\n mapped (f, it)\n\nmap f over it, unless it’s not listy, in which case return f(it)\n\ndef _f(x,a=1): return x-a\n\ntest_eq(mapped(_f,1),0)\ntest_eq(mapped(_f,[1,2]),[0,1])\ntest_eq(mapped(_f,(1,)),(0,))", + "crumbs": [ + "Utility functions" + ] + }, + { + "objectID": "xtras.html#extensions-to-pathlib.path", + "href": "xtras.html#extensions-to-pathlib.path", + "title": "Utility functions", + "section": "Extensions to Pathlib.Path", + "text": "Extensions to Pathlib.Path\nThe following methods are added to the standard python libary Pathlib.Path.\n\nsource\n\nPath.readlines\n\n Path.readlines (hint=-1, encoding='utf8')\n\nRead the content of self\n\nsource\n\n\nPath.read_json\n\n Path.read_json (encoding=None, errors=None)\n\nSame as read_text followed by loads\n\nsource\n\n\nPath.mk_write\n\n Path.mk_write (data, encoding=None, errors=None, mode=511)\n\nMake all parent dirs of self, and write data\n\nsource\n\n\nPath.relpath\n\n Path.relpath (start=None)\n\nSame as os.path.relpath, but returns a Path, and resolves symlinks\n\np = Path('../fastcore/').resolve()\np\n\nPath('/Users/daniel.roy.greenfeld/fh/fastcore/fastcore')\n\n\n\np.relpath(Path.cwd())\n\nPath('../fastcore')\n\n\n\nsource\n\n\nPath.ls\n\n Path.ls (n_max=None, file_type=None, file_exts=None)\n\nContents of path as a list\nWe add an ls() method to pathlib.Path which is simply defined as list(Path.iterdir()), mainly for convenience in REPL environments such as notebooks.\n\npath = Path()\nt = path.ls()\nassert len(t)>0\nt1 = path.ls(10)\ntest_eq(len(t1), 10)\nt2 = path.ls(file_exts='.ipynb')\nassert len(t)>len(t2)\nt[0]\n\nPath('000_tour.ipynb')\n\n\nYou can also pass an optional file_type MIME prefix and/or a list of file extensions.\n\nlib_path = (path/'../fastcore')\ntxt_files=lib_path.ls(file_type='text')\nassert len(txt_files) > 0 and txt_files[0].suffix=='.py'\nipy_files=path.ls(file_exts=['.ipynb'])\nassert len(ipy_files) > 0 and ipy_files[0].suffix=='.ipynb'\ntxt_files[0],ipy_files[0]\n\n(Path('../fastcore/shutil.py'), Path('000_tour.ipynb'))\n\n\n\nsource\n\n\nPath.__repr__\n\n Path.__repr__ ()\n\nReturn repr(self).\nfastai also updates the repr of Path such that, if Path.BASE_PATH is defined, all paths are printed relative to that path (as long as they are contained in Path.BASE_PATH:\n\nt = ipy_files[0].absolute()\ntry:\n Path.BASE_PATH = t.parent.parent\n test_eq(repr(t), f\"Path('nbs/{t.name}')\")\nfinally: Path.BASE_PATH = None\n\n\nsource\n\n\nPath.delete\n\n Path.delete ()\n\nDelete a file, symlink, or directory tree", + "crumbs": [ + "Utility functions" + ] + }, + { + "objectID": "xtras.html#reindexing-collections", + "href": "xtras.html#reindexing-collections", + "title": "Utility functions", + "section": "Reindexing Collections", + "text": "Reindexing Collections\n\nsource\n\nReindexCollection\n\n ReindexCollection (coll, idxs=None, cache=None, tfm=<function noop>)\n\nReindexes collection coll with indices idxs and optional LRU cache of size cache\nThis is useful when constructing batches or organizing data in a particular manner (i.e. for deep learning). This class is primarly used in organizing data for language models in fastai.\nYou can supply a custom index upon instantiation with the idxs argument, or you can call the reindex method to supply a new index for your collection.\nHere is how you can reindex a list such that the elements are reversed:\n\nrc=ReindexCollection(['a', 'b', 'c', 'd', 'e'], idxs=[4,3,2,1,0])\nlist(rc)\n\n['e', 'd', 'c', 'b', 'a']\n\n\nAlternatively, you can use the reindex method:\n\nsource\n\nReindexCollection.reindex\n\n ReindexCollection.reindex (idxs)\n\nReplace self.idxs with idxs\n\nrc=ReindexCollection(['a', 'b', 'c', 'd', 'e'])\nrc.reindex([4,3,2,1,0])\nlist(rc)\n\n['e', 'd', 'c', 'b', 'a']\n\n\nYou can optionally specify a LRU cache, which uses functools.lru_cache upon instantiation:\n\nsz = 50\nt = ReindexCollection(L.range(sz), cache=2)\n\n#trigger a cache hit by indexing into the same element multiple times\nt[0], t[0]\nt._get.cache_info()\n\nCacheInfo(hits=1, misses=1, maxsize=2, currsize=1)\n\n\nYou can optionally clear the LRU cache by calling the cache_clear method:\n\nsource\n\n\nReindexCollection.cache_clear\n\n ReindexCollection.cache_clear ()\n\nClear LRU cache\n\nsz = 50\nt = ReindexCollection(L.range(sz), cache=2)\n\n#trigger a cache hit by indexing into the same element multiple times\nt[0], t[0]\nt.cache_clear()\nt._get.cache_info()\n\nCacheInfo(hits=0, misses=0, maxsize=2, currsize=0)\n\n\n\nsource\n\n\nReindexCollection.shuffle\n\n ReindexCollection.shuffle ()\n\nRandomly shuffle indices\nNote that an ordered index is automatically constructed for the data structure even if one is not supplied.\n\nrc=ReindexCollection(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'])\nrc.shuffle()\nlist(rc)\n\n['a', 'd', 'h', 'c', 'e', 'b', 'f', 'g']\n\n\n\nsz = 50\nt = ReindexCollection(L.range(sz), cache=2)\ntest_eq(list(t), range(sz))\ntest_eq(t[sz-1], sz-1)\ntest_eq(t._get.cache_info().hits, 1)\nt.shuffle()\ntest_eq(t._get.cache_info().hits, 1)\ntest_ne(list(t), range(sz))\ntest_eq(set(t), set(range(sz)))\nt.cache_clear()\ntest_eq(t._get.cache_info().hits, 0)\ntest_eq(t.count(0), 1)", + "crumbs": [ + "Utility functions" + ] + }, + { + "objectID": "xtras.html#other-helpers", + "href": "xtras.html#other-helpers", + "title": "Utility functions", + "section": "Other Helpers", + "text": "Other Helpers\n\nsource\n\nget_source_link\n\n get_source_link (func)\n\nReturn link to func in source code\nget_source_link allows you get a link to source code related to an object. For nbdev related projects such as fastcore, we can get the full link to a GitHub repo. For nbdev projects, be sure to properly set the git_url in settings.ini (derived from lib_name and branch on top of the prefix you will need to adapt) so that those links are correct.\nFor example, below we get the link to fastcore.test.test_eq:\n\nfrom fastcore.test import test_eq\n\n\nassert 'fastcore/test.py' in get_source_link(test_eq)\nassert get_source_link(test_eq).startswith('https://github.com/fastai/fastcore')\nget_source_link(test_eq)\n\n'https://github.com/fastai/fastcore/tree/master/fastcore/test.py#L35'\n\n\n\nsource\n\n\ntruncstr\n\n truncstr (s:str, maxlen:int, suf:str='…', space='')\n\nTruncate s to length maxlen, adding suffix suf if truncated\n\nw = 'abacadabra'\ntest_eq(truncstr(w, 10), w)\ntest_eq(truncstr(w, 5), 'abac…')\ntest_eq(truncstr(w, 5, suf=''), 'abaca')\ntest_eq(truncstr(w, 11, space='_'), w+\"_\")\ntest_eq(truncstr(w, 10, space='_'), w[:-1]+'…')\ntest_eq(truncstr(w, 5, suf='!!'), 'aba!!')\n\n\nsource\n\n\nsparkline\n\n sparkline (data, mn=None, mx=None, empty_zero=False)\n\nSparkline for data, with Nones (and zero, if empty_zero) shown as empty column\n\ndata = [9,6,None,1,4,0,8,15,10]\nprint(f'without \"empty_zero\": {sparkline(data, empty_zero=False)}')\nprint(f' with \"empty_zero\": {sparkline(data, empty_zero=True )}')\n\nwithout \"empty_zero\": ▅▂ ▁▂▁▃▇▅\n with \"empty_zero\": ▅▂ ▁▂ ▃▇▅\n\n\nYou can set a maximum and minimum for the y-axis of the sparkline with the arguments mn and mx respectively:\n\nsparkline([1,2,3,400], mn=0, mx=3)\n\n'▂▅▇▇'\n\n\n\nsource\n\n\nmodify_exception\n\n modify_exception (e:Exception, msg:str=None, replace:bool=False)\n\nModifies e with a custom message attached\n\n\n\n\nType\nDefault\nDetails\n\n\n\n\ne\nException\n\nAn exception\n\n\nmsg\nstr\nNone\nA custom message\n\n\nreplace\nbool\nFalse\nWhether to replace e.args with [msg]\n\n\nReturns\nException\n\n\n\n\n\n\nmsg = \"This is my custom message!\"\n\ntest_fail(lambda: (_ for _ in ()).throw(modify_exception(Exception(), None)), contains='')\ntest_fail(lambda: (_ for _ in ()).throw(modify_exception(Exception(), msg)), contains=msg)\ntest_fail(lambda: (_ for _ in ()).throw(modify_exception(Exception(\"The first message\"), msg)), contains=\"The first message This is my custom message!\")\ntest_fail(lambda: (_ for _ in ()).throw(modify_exception(Exception(\"The first message\"), msg, True)), contains=\"This is my custom message!\")\n\n\nsource\n\n\nround_multiple\n\n round_multiple (x, mult, round_down=False)\n\nRound x to nearest multiple of mult\n\ntest_eq(round_multiple(63,32), 64)\ntest_eq(round_multiple(50,32), 64)\ntest_eq(round_multiple(40,32), 32)\ntest_eq(round_multiple( 0,32), 0)\ntest_eq(round_multiple(63,32, round_down=True), 32)\ntest_eq(round_multiple((63,40),32), (64,32))\n\n\nsource\n\n\nset_num_threads\n\n set_num_threads (nt)\n\nGet numpy (and others) to use nt threads\nThis sets the number of threads consistently for many tools, by:\n\nSet the following environment variables equal to nt: OPENBLAS_NUM_THREADS,NUMEXPR_NUM_THREADS,OMP_NUM_THREADS,MKL_NUM_THREADS\nSets nt threads for numpy and pytorch.\n\n\nsource\n\n\njoin_path_file\n\n join_path_file (file, path, ext='')\n\nReturn path/file if file is a string or a Path, file otherwise\n\npath = Path.cwd()/'_tmp'/'tst'\nf = join_path_file('tst.txt', path)\nassert path.exists()\ntest_eq(f, path/'tst.txt')\nwith open(f, 'w') as f_: assert join_path_file(f_, path) == f_\nshutil.rmtree(Path.cwd()/'_tmp')\n\n\nsource\n\n\nautostart\n\n autostart (g)\n\nDecorator that automatically starts a generator\n\nsource\n\nEventTimer\n\n EventTimer (store=5, span=60)\n\nAn event timer with history of store items of time span\nAdd events with add, and get number of events and their frequency (freq).\n\n# Random wait function for testing\ndef _randwait(): yield from (sleep(random.random()/200) for _ in range(100))\n\nc = EventTimer(store=5, span=0.03)\nfor o in _randwait(): c.add(1)\nprint(f'Num Events: {c.events}, Freq/sec: {c.freq:.01f}')\nprint('Most recent: ', sparkline(c.hist), *L(c.hist).map('{:.01f}'))\n\nNum Events: 3, Freq/sec: 205.6\nMost recent: ▁▁▃▁▇ 254.1 263.2 284.5 259.9 315.7\n\n\n\nsource\n\n\n\nstringfmt_names\n\n stringfmt_names (s:str)\n\nUnique brace-delimited names in s\n\ns = '/pulls/{pull_number}/reviews/{review_id}'\ntest_eq(stringfmt_names(s), ['pull_number','review_id'])\n\n\nsource\n\nPartialFormatter\n\n PartialFormatter ()\n\nA string.Formatter that doesn’t error on missing fields, and tracks missing fields and unused args\n\nsource\n\n\n\npartial_format\n\n partial_format (s:str, **kwargs)\n\nstring format s, ignoring missing field errors, returning missing and extra fields\nThe result is a tuple of (formatted_string,missing_fields,extra_fields), e.g:\n\nres,missing,xtra = partial_format(s, pull_number=1, foo=2)\ntest_eq(res, '/pulls/1/reviews/{review_id}')\ntest_eq(missing, ['review_id'])\ntest_eq(xtra, {'foo':2})\n\n\nsource\n\n\nutc2local\n\n utc2local (dt:datetime.datetime)\n\nConvert dt from UTC to local time\n\ndt = datetime(2000,1,1,12)\nprint(f'{dt} UTC is {utc2local(dt)} local time')\n\n2000-01-01 12:00:00 UTC is 2000-01-01 22:00:00+10:00 local time\n\n\n\nsource\n\n\nlocal2utc\n\n local2utc (dt:datetime.datetime)\n\nConvert dt from local to UTC time\n\nprint(f'{dt} local is {local2utc(dt)} UTC time')\n\n2000-01-01 12:00:00 local is 2000-01-01 02:00:00+00:00 UTC time\n\n\n\nsource\n\n\ntrace\n\n trace (f)\n\nAdd set_trace to an existing function f\nYou can add a breakpoint to an existing function, e.g:\nPath.cwd = trace(Path.cwd)\nPath.cwd()\nNow, when the function is called it will drop you into the debugger. Note, you must issue the s command when you begin to step into the function that is being traced.\n\nsource\n\n\nmodified_env\n\n modified_env (*delete, **replace)\n\nContext manager temporarily modifying os.environ by deleting delete and replacing replace\n\n# USER isn't in Cloud Linux Environments\nenv_test = 'USERNAME' if sys.platform == \"win32\" else 'SHELL'\noldusr = os.environ[env_test]\n\nreplace_param = {env_test: 'a'}\nwith modified_env('PATH', **replace_param):\n test_eq(os.environ[env_test], 'a')\n assert 'PATH' not in os.environ\n\nassert 'PATH' in os.environ\ntest_eq(os.environ[env_test], oldusr)\n\n\nsource\n\nContextManagers\n\n ContextManagers (mgrs)\n\nWrapper for contextlib.ExitStack which enters a collection of context managers\n\nsource\n\n\n\nshufflish\n\n shufflish (x, pct=0.04)\n\nRandomly relocate items of x up to pct of len(x) from their starting location\n\nsource\n\n\nconsole_help\n\n console_help (libname:str)\n\nShow help for all console scripts from libname\n\n\n\n\nType\nDetails\n\n\n\n\nlibname\nstr\nname of library for console script listing\n\n\n\n\nsource\n\n\nhl_md\n\n hl_md (s, lang='xml', show=True)\n\nSyntax highlight s using lang.\nWhen we display code in a notebook, it’s nice to highlight it, so we create a function to simplify that:\n\nhl_md('<test><xml foo=\"bar\">a child</xml></test>')\n\n<test><xml foo=\"bar\">a child</xml></test>\n\n\n\nsource\n\n\ntype2str\n\n type2str (typ:type)\n\nStringify typ\n\ntest_eq(type2str(Optional[float]), 'Union[float, None]')\n\n\nsource\n\n\ndataclass_src\n\n dataclass_src (cls)\n\n\nDC = make_dataclass('DC', [('x', int), ('y', Optional[float], None), ('z', float, None)])\nprint(dataclass_src(DC))\n\n@dataclass\nclass DC:\n x: int\n y: Union[float, None] = None\n z: float = None\n\n\n\n\nsource\n\n\nUnset\n\n Unset (value, names=None, module=None, qualname=None, type=None, start=1)\n\nAn enumeration.\n\nsource\n\n\nnullable_dc\n\n nullable_dc (cls)\n\nLike dataclass, but default of UNSET added to fields without defaults\n\n@nullable_dc\nclass Person: name: str; age: int; city: str = \"Unknown\"\nPerson(name=\"Bob\")\n\nPerson(name='Bob', age=UNSET, city='Unknown')\n\n\n\nsource\n\n\nmake_nullable\n\n make_nullable (clas)\n\n\n@dataclass\nclass Person: name: str; age: int; city: str = \"Unknown\"\n\nmake_nullable(Person)\nPerson(\"Bob\", city='NY')\n\nPerson(name='Bob', age=UNSET, city='NY')\n\n\n\nPerson(name=\"Bob\")\n\nPerson(name='Bob', age=UNSET, city='Unknown')\n\n\n\nPerson(\"Bob\", 34)\n\nPerson(name='Bob', age=34, city='Unknown')\n\n\n\nsource\n\n\nflexiclass\n\n flexiclass (cls)\n\nConvert cls into a dataclass like make_nullable. Converts in place and also returns the result.\n\n\n\n\nType\nDetails\n\n\n\n\ncls\n\nThe class to convert\n\n\nReturns\ndataclass\n\n\n\n\nThis can be used as a decorator…\n\n@flexiclass\nclass Person: name: str; age: int; city: str = \"Unknown\"\n\nbob = Person(name=\"Bob\")\nbob\n\nPerson(name='Bob', age=UNSET, city='Unknown')\n\n\n…or can update the behavior of an existing class (or dataclass):\n\nclass Person: name: str; age: int; city: str = \"Unknown\"\n\nflexiclass(Person)\nbob = Person(name=\"Bob\")\nbob\n\nPerson(name='Bob', age=UNSET, city='Unknown')\n\n\nAction occurs in-place:\n\nclass Person: name: str; age: int; city: str = \"Unknown\"\n\nflexiclass(Person)\nis_dataclass(Person)\n\nTrue\n\n\n\nsource\n\n\nasdict\n\n asdict (o)\n\nConvert o to a dict, supporting dataclasses, namedtuples, iterables, and __dict__ attrs.\nAny UNSET values are not included.\n\nasdict(bob)\n\n{'name': 'Bob', 'city': 'Unknown'}\n\n\nTo customise dict conversion behavior for a class, implement the _asdict method (this is used in the Python stdlib for named tuples).\n\nsource\n\n\nis_typeddict\n\n is_typeddict (cls:type)\n\nCheck if cls is a TypedDict\n\nclass MyDict(TypedDict): name:str\n\nassert is_typeddict(MyDict)\nassert not is_typeddict({'a':1})\n\n\nsource\n\n\nis_namedtuple\n\n is_namedtuple (cls)\n\nTrue if cls is a namedtuple type\n\nassert is_namedtuple(namedtuple('tst', ['a']))\nassert not is_namedtuple(tuple)\n\n\nsource\n\n\nflexicache\n\n flexicache (*funcs, maxsize=128)\n\nLike lru_cache, but customisable with policy funcs\nThis is a flexible lru cache function that you can pass a list of functions to. Those functions define the cache eviction policy. For instance, time_policy is provided for time-based cache eviction, and mtime_policy evicts based on a file’s modified-time changing. The policy functions are passed the last value that function returned was (initially None), and return a new value to indicate the cache has expired. When the cache expires, all functions are called with None to force getting new values.\n\nsource\n\n\ntime_policy\n\n time_policy (seconds)\n\nA flexicache policy that expires cached items after seconds have passed\n\nsource\n\n\nmtime_policy\n\n mtime_policy (filepath)\n\nA flexicache policy that expires cached items after filepath modified-time changes\n\n@flexicache(time_policy(10), mtime_policy('000_tour.ipynb'))\ndef cached_func(x, y): return x+y\n\ncached_func(1,2)\n\n3\n\n\n\n@flexicache(time_policy(10), mtime_policy('000_tour.ipynb'))\nasync def cached_func(x, y): return x+y\n\nawait cached_func(1,2)\nawait cached_func(1,2)\n\n3\n\n\n\nsource\n\n\ntimed_cache\n\n timed_cache (seconds=60, maxsize=128)\n\nLike lru_cache, but also with time-based eviction\nThis function is a small convenience wrapper for using flexicache with time_policy.\n\n@timed_cache(seconds=0.05, maxsize=2)\ndef cached_func(x): return x * 2, time()\n\n# basic caching\nresult1, time1 = cached_func(2)\ntest_eq(result1, 4)\nsleep(0.001)\nresult2, time2 = cached_func(2)\ntest_eq(result2, 4)\ntest_eq(time1, time2)\n\n# caching different values\nresult3, _ = cached_func(3)\ntest_eq(result3, 6)\n\n# maxsize\n_, time4 = cached_func(4)\n_, time2_new = cached_func(2)\ntest_close(time2, time2_new, eps=0.1)\n_, time3_new = cached_func(3)\ntest_ne(time3_new, time())\n\n# time expiration\nsleep(0.05)\n_, time4_new = cached_func(4)\ntest_ne(time4_new, time())", + "crumbs": [ + "Utility functions" + ] + }, + { + "objectID": "foundation.html", + "href": "foundation.html", + "title": "Foundation", + "section": "", + "text": "source\n\n\n\n working_directory (path)\n\nChange working directory to path and return to previous on exit.\n\nsource\n\n\n\n\n add_docs (cls, cls_doc=None, **docs)\n\nCopy values from docs to cls docstrings, and confirm all public methods are documented\nadd_docs allows you to add docstrings to a class and its associated methods. This function allows you to group docstrings together seperate from your code, which enables you to define one-line functions as well as organize your code more succintly. We believe this confers a number of benefits which we discuss in our style guide.\nSuppose you have the following undocumented class:\n\nclass T:\n def foo(self): pass\n def bar(self): pass\n\nYou can add documentation to this class like so:\n\nadd_docs(T, cls_doc=\"A docstring for the class.\",\n foo=\"The foo method.\",\n bar=\"The bar method.\")\n\nNow, docstrings will appear as expected:\n\ntest_eq(T.__doc__, \"A docstring for the class.\")\ntest_eq(T.foo.__doc__, \"The foo method.\")\ntest_eq(T.bar.__doc__, \"The bar method.\")\n\nadd_docs also validates that all of your public methods contain a docstring. If one of your methods is not documented, it will raise an error:\n\nclass T:\n def foo(self): pass\n def bar(self): pass\n\nf=lambda: add_docs(T, \"A docstring for the class.\", foo=\"The foo method.\")\ntest_fail(f, contains=\"Missing docs\")\n\n\nsource\n\n\n\n\n docs (cls)\n\nDecorator version of add_docs, using _docs dict\nInstead of using add_docs, you can use the decorator docs as shown below. Note that the docstring for the class can be set with the argument cls_doc:\n\n@docs\nclass _T:\n def f(self): pass\n def g(cls): pass\n \n _docs = dict(cls_doc=\"The class docstring\", \n f=\"The docstring for method f.\",\n g=\"A different docstring for method g.\")\n\n \ntest_eq(_T.__doc__, \"The class docstring\")\ntest_eq(_T.f.__doc__, \"The docstring for method f.\")\ntest_eq(_T.g.__doc__, \"A different docstring for method g.\")\n\nFor either the docs decorator or the add_docs function, you can still define your docstrings in the normal way. Below we set the docstring for the class as usual, but define the method docstrings through the _docs attribute:\n\n@docs\nclass _T:\n \"The class docstring\"\n def f(self): pass\n _docs = dict(f=\"The docstring for method f.\")\n\n \ntest_eq(_T.__doc__, \"The class docstring\")\ntest_eq(_T.f.__doc__, \"The docstring for method f.\")\n\n\n\n\n\n\n is_iter (o)\n\nTest whether o can be used in a for loop\n\nassert is_iter([1])\nassert not is_iter(array(1))\nassert is_iter(array([1,2]))\nassert (o for o in range(3))\n\n\nsource\n\n\n\n\n coll_repr (c, max_n=10)\n\nString repr of up to max_n items of (possibly lazy) collection c\ncoll_repr is used to provide a more informative __repr__ about list-like objects. coll_repr and is used by L to build a __repr__ that displays the length of a list in addition to a preview of a list.\nBelow is an example of the __repr__ string created for a list of 1000 elements:\n\ntest_eq(coll_repr(range(1000)), '(#1000) [0,1,2,3,4,5,6,7,8,9...]')\ntest_eq(coll_repr(range(1000), 5), '(#1000) [0,1,2,3,4...]')\ntest_eq(coll_repr(range(10), 5), '(#10) [0,1,2,3,4...]')\ntest_eq(coll_repr(range(5), 5), '(#5) [0,1,2,3,4]')\n\nWe can set the option max_n to optionally preview a specified number of items instead of the default:\n\ntest_eq(coll_repr(range(1000), max_n=5), '(#1000) [0,1,2,3,4...]')\n\n\nsource\n\n\n\n\n is_bool (x)\n\nCheck whether x is a bool or None\n\nsource\n\n\n\n\n mask2idxs (mask)\n\nConvert bool mask or index list to index L\n\ntest_eq(mask2idxs([False,True,False,True]), [1,3])\ntest_eq(mask2idxs(array([False,True,False,True])), [1,3])\ntest_eq(mask2idxs(array([1,2,3])), [1,2,3])\n\n\nsource\n\n\n\n\n cycle (o)\n\nLike itertools.cycle except creates list of Nones if o is empty\n\ntest_eq(itertools.islice(cycle([1,2,3]),5), [1,2,3,1,2])\ntest_eq(itertools.islice(cycle([]),3), [None]*3)\ntest_eq(itertools.islice(cycle(None),3), [None]*3)\ntest_eq(itertools.islice(cycle(1),3), [1,1,1])\n\n\nsource\n\n\n\n\n zip_cycle (x, *args)\n\nLike itertools.zip_longest but cycles through elements of all but first argument\n\ntest_eq(zip_cycle([1,2,3,4],list('abc')), [(1, 'a'), (2, 'b'), (3, 'c'), (4, 'a')])\n\n\nsource\n\n\n\n\n is_indexer (idx)\n\nTest whether idx will index a single item in a list\nYou can, for example index a single item in a list with an integer or a 0-dimensional numpy array:\n\nassert is_indexer(1)\nassert is_indexer(np.array(1))\n\nHowever, you cannot index into single item in a list with another list or a numpy array with ndim > 0.\n\nassert not is_indexer([1, 2])\nassert not is_indexer(np.array([[1, 2], [3, 4]]))", + "crumbs": [ + "Foundation" + ] + }, + { + "objectID": "foundation.html#foundational-functions", + "href": "foundation.html#foundational-functions", + "title": "Foundation", + "section": "", + "text": "source\n\n\n\n working_directory (path)\n\nChange working directory to path and return to previous on exit.\n\nsource\n\n\n\n\n add_docs (cls, cls_doc=None, **docs)\n\nCopy values from docs to cls docstrings, and confirm all public methods are documented\nadd_docs allows you to add docstrings to a class and its associated methods. This function allows you to group docstrings together seperate from your code, which enables you to define one-line functions as well as organize your code more succintly. We believe this confers a number of benefits which we discuss in our style guide.\nSuppose you have the following undocumented class:\n\nclass T:\n def foo(self): pass\n def bar(self): pass\n\nYou can add documentation to this class like so:\n\nadd_docs(T, cls_doc=\"A docstring for the class.\",\n foo=\"The foo method.\",\n bar=\"The bar method.\")\n\nNow, docstrings will appear as expected:\n\ntest_eq(T.__doc__, \"A docstring for the class.\")\ntest_eq(T.foo.__doc__, \"The foo method.\")\ntest_eq(T.bar.__doc__, \"The bar method.\")\n\nadd_docs also validates that all of your public methods contain a docstring. If one of your methods is not documented, it will raise an error:\n\nclass T:\n def foo(self): pass\n def bar(self): pass\n\nf=lambda: add_docs(T, \"A docstring for the class.\", foo=\"The foo method.\")\ntest_fail(f, contains=\"Missing docs\")\n\n\nsource\n\n\n\n\n docs (cls)\n\nDecorator version of add_docs, using _docs dict\nInstead of using add_docs, you can use the decorator docs as shown below. Note that the docstring for the class can be set with the argument cls_doc:\n\n@docs\nclass _T:\n def f(self): pass\n def g(cls): pass\n \n _docs = dict(cls_doc=\"The class docstring\", \n f=\"The docstring for method f.\",\n g=\"A different docstring for method g.\")\n\n \ntest_eq(_T.__doc__, \"The class docstring\")\ntest_eq(_T.f.__doc__, \"The docstring for method f.\")\ntest_eq(_T.g.__doc__, \"A different docstring for method g.\")\n\nFor either the docs decorator or the add_docs function, you can still define your docstrings in the normal way. Below we set the docstring for the class as usual, but define the method docstrings through the _docs attribute:\n\n@docs\nclass _T:\n \"The class docstring\"\n def f(self): pass\n _docs = dict(f=\"The docstring for method f.\")\n\n \ntest_eq(_T.__doc__, \"The class docstring\")\ntest_eq(_T.f.__doc__, \"The docstring for method f.\")\n\n\n\n\n\n\n is_iter (o)\n\nTest whether o can be used in a for loop\n\nassert is_iter([1])\nassert not is_iter(array(1))\nassert is_iter(array([1,2]))\nassert (o for o in range(3))\n\n\nsource\n\n\n\n\n coll_repr (c, max_n=10)\n\nString repr of up to max_n items of (possibly lazy) collection c\ncoll_repr is used to provide a more informative __repr__ about list-like objects. coll_repr and is used by L to build a __repr__ that displays the length of a list in addition to a preview of a list.\nBelow is an example of the __repr__ string created for a list of 1000 elements:\n\ntest_eq(coll_repr(range(1000)), '(#1000) [0,1,2,3,4,5,6,7,8,9...]')\ntest_eq(coll_repr(range(1000), 5), '(#1000) [0,1,2,3,4...]')\ntest_eq(coll_repr(range(10), 5), '(#10) [0,1,2,3,4...]')\ntest_eq(coll_repr(range(5), 5), '(#5) [0,1,2,3,4]')\n\nWe can set the option max_n to optionally preview a specified number of items instead of the default:\n\ntest_eq(coll_repr(range(1000), max_n=5), '(#1000) [0,1,2,3,4...]')\n\n\nsource\n\n\n\n\n is_bool (x)\n\nCheck whether x is a bool or None\n\nsource\n\n\n\n\n mask2idxs (mask)\n\nConvert bool mask or index list to index L\n\ntest_eq(mask2idxs([False,True,False,True]), [1,3])\ntest_eq(mask2idxs(array([False,True,False,True])), [1,3])\ntest_eq(mask2idxs(array([1,2,3])), [1,2,3])\n\n\nsource\n\n\n\n\n cycle (o)\n\nLike itertools.cycle except creates list of Nones if o is empty\n\ntest_eq(itertools.islice(cycle([1,2,3]),5), [1,2,3,1,2])\ntest_eq(itertools.islice(cycle([]),3), [None]*3)\ntest_eq(itertools.islice(cycle(None),3), [None]*3)\ntest_eq(itertools.islice(cycle(1),3), [1,1,1])\n\n\nsource\n\n\n\n\n zip_cycle (x, *args)\n\nLike itertools.zip_longest but cycles through elements of all but first argument\n\ntest_eq(zip_cycle([1,2,3,4],list('abc')), [(1, 'a'), (2, 'b'), (3, 'c'), (4, 'a')])\n\n\nsource\n\n\n\n\n is_indexer (idx)\n\nTest whether idx will index a single item in a list\nYou can, for example index a single item in a list with an integer or a 0-dimensional numpy array:\n\nassert is_indexer(1)\nassert is_indexer(np.array(1))\n\nHowever, you cannot index into single item in a list with another list or a numpy array with ndim > 0.\n\nassert not is_indexer([1, 2])\nassert not is_indexer(np.array([[1, 2], [3, 4]]))", + "crumbs": [ + "Foundation" + ] + }, + { + "objectID": "foundation.html#l-helpers", + "href": "foundation.html#l-helpers", + "title": "Foundation", + "section": "L helpers", + "text": "L helpers\n\nsource\n\nCollBase\n\n CollBase (items)\n\nBase class for composing a list of items\nColBase is a base class that emulates the functionality of a python list:\n\nclass _T(CollBase): pass\nl = _T([1,2,3,4,5])\n\ntest_eq(len(l), 5) # __len__\ntest_eq(l[-1], 5); test_eq(l[0], 1) #__getitem__\nl[2] = 100; test_eq(l[2], 100) # __set_item__\ndel l[0]; test_eq(len(l), 4) # __delitem__\ntest_eq(str(l), '[2, 100, 4, 5]') # __repr__\n\n\nsource\n\n\nL\n\n L (x=None, *args, **kwargs)\n\nBehaves like a list of items but can also index with list of indices or masks\nL is a drop in replacement for a python list. Inspired by NumPy, L, supports advanced indexing and has additional methods (outlined below) that provide additional functionality and encourage simple expressive code. For example, the code below takes a list of pairs, selects the second item of each pair, takes its absolute value, filters items greater than 4, and adds them up:\n\nfrom fastcore.utils import gt\n\n\nd = dict(a=1,b=-5,d=6,e=9).items()\ntest_eq(L(d).itemgot(1).map(abs).filter(gt(4)).sum(), 20) # abs(-5) + abs(6) + abs(9) = 20; 1 was filtered out.\n\nRead this overview section for a quick tutorial of L, as well as background on the name.\nYou can create an L from an existing iterable (e.g. a list, range, etc) and access or modify it with an int list/tuple index, mask, int, or slice. All list methods can also be used with L.\n\nt = L(range(12))\ntest_eq(t, list(range(12)))\ntest_ne(t, list(range(11)))\nt.reverse()\ntest_eq(t[0], 11)\nt[3] = \"h\"\ntest_eq(t[3], \"h\")\nt[3,5] = (\"j\",\"k\")\ntest_eq(t[3,5], [\"j\",\"k\"])\ntest_eq(t, L(t))\ntest_eq(L(L(1,2),[3,4]), ([1,2],[3,4]))\nt\n\n(#12) [11,10,9,'j',7,'k',5,4,3,2...]\n\n\nAny L is a Sequence so you can use it with methods like random.sample:\n\nassert isinstance(t, Sequence)\n\n\nimport random\n\n\nrandom.seed(0)\nrandom.sample(t, 3)\n\n[5, 0, 11]\n\n\nThere are optimized indexers for arrays, tensors, and DataFrames.\n\nimport pandas as pd\n\n\narr = np.arange(9).reshape(3,3)\nt = L(arr, use_list=None)\ntest_eq(t[1,2], arr[[1,2]])\n\ndf = pd.DataFrame({'a':[1,2,3]})\nt = L(df, use_list=None)\ntest_eq(t[1,2], L(pd.DataFrame({'a':[2,3]}, index=[1,2]), use_list=None))\n\nYou can also modify an L with append, +, and *.\n\nt = L()\ntest_eq(t, [])\nt.append(1)\ntest_eq(t, [1])\nt += [3,2]\ntest_eq(t, [1,3,2])\nt = t + [4]\ntest_eq(t, [1,3,2,4])\nt = 5 + t\ntest_eq(t, [5,1,3,2,4])\ntest_eq(L(1,2,3), [1,2,3])\ntest_eq(L(1,2,3), L(1,2,3))\nt = L(1)*5\nt = t.map(operator.neg)\ntest_eq(t,[-1]*5)\ntest_eq(~L([True,False,False]), L([False,True,True]))\nt = L(range(4))\ntest_eq(zip(t, L(1).cycle()), zip(range(4),(1,1,1,1)))\nt = L.range(100)\ntest_shuffled(t,t.shuffle())\n\n\ntest_eq(L([]).sum(), 0)\ntest_eq(L([]).product(), 1)\n\n\ndef _f(x,a=0): return x+a\nt = L(1)*5\ntest_eq(t.map(_f), t)\ntest_eq(t.map(_f,1), [2]*5)\ntest_eq(t.map(_f,a=2), [3]*5)\n\nAn L can be constructed from anything iterable, although tensors and arrays will not be iterated over on construction, unless you pass use_list to the constructor.\n\ntest_eq(L([1,2,3]),[1,2,3])\ntest_eq(L(L([1,2,3])),[1,2,3])\ntest_ne(L([1,2,3]),[1,2,])\ntest_eq(L('abc'),['abc'])\ntest_eq(L(range(0,3)),[0,1,2])\ntest_eq(L(o for o in range(0,3)),[0,1,2])\ntest_eq(L(array(0)),[array(0)])\ntest_eq(L([array(0),array(1)]),[array(0),array(1)])\ntest_eq(L(array([0.,1.1]))[0],array([0.,1.1]))\ntest_eq(L(array([0.,1.1]), use_list=True), [array(0.),array(1.1)]) # `use_list=True` to unwrap arrays/arrays\n\nIf match is not None then the created list is same len as match, either by:\n\nIf len(items)==1 then items is replicated,\nOtherwise an error is raised if match and items are not already the same size.\n\n\ntest_eq(L(1,match=[1,2,3]),[1,1,1])\ntest_eq(L([1,2],match=[2,3]),[1,2])\ntest_fail(lambda: L([1,2],match=[1,2,3]))\n\nIf you create an L from an existing L then you’ll get back the original object (since L uses the NewChkMeta metaclass).\n\ntest_is(L(t), t)\n\nAn L is considred equal to a list if they have the same elements. It’s never considered equal to a str a set or a dict even if they have the same elements/keys.\n\ntest_eq(L(['a', 'b']), ['a', 'b'])\ntest_ne(L(['a', 'b']), 'ab')\ntest_ne(L(['a', 'b']), {'a':1, 'b':2})\n\n\n\nL Methods\n\nsource\n\n\nL.__getitem__\n\n L.__getitem__ (idx)\n\nRetrieve idx (can be list of indices, or mask, or int) items\n\nt = L(range(12))\ntest_eq(t[1,2], [1,2]) # implicit tuple\ntest_eq(t[[1,2]], [1,2]) # list\ntest_eq(t[:3], [0,1,2]) # slice\ntest_eq(t[[False]*11 + [True]], [11]) # mask\ntest_eq(t[array(3)], 3)\n\n\nsource\n\n\nL.__setitem__\n\n L.__setitem__ (idx, o)\n\nSet idx (can be list of indices, or mask, or int) items to o (which is broadcast if not iterable)\n\nt[4,6] = 0\ntest_eq(t[4,6], [0,0])\nt[4,6] = [1,2]\ntest_eq(t[4,6], [1,2])\n\n\nsource\n\n\nL.unique\n\n L.unique (sort=False, bidir=False, start=None)\n\nUnique items, in stable order\n\ntest_eq(L(4,1,2,3,4,4).unique(), [4,1,2,3])\n\n\nsource\n\n\nL.val2idx\n\n L.val2idx ()\n\nDict from value to index\n\ntest_eq(L(1,2,3).val2idx(), {3:2,1:0,2:1})\n\n\nsource\n\n\nL.filter\n\n L.filter (f=<function noop>, negate=False, **kwargs)\n\nCreate new L filtered by predicate f, passing args and kwargs to f\n\nlist(t)\n\n[0, 1, 2, 3, 1, 5, 2, 7, 8, 9, 10, 11]\n\n\n\ntest_eq(t.filter(lambda o:o<5), [0,1,2,3,1,2])\ntest_eq(t.filter(lambda o:o<5, negate=True), [5,7,8,9,10,11])\n\n\nsource\n\n\nL.argwhere\n\n L.argwhere (f, negate=False, **kwargs)\n\nLike filter, but return indices for matching items\n\ntest_eq(t.argwhere(lambda o:o<5), [0,1,2,3,4,6])\n\n\nsource\n\n\nL.argfirst\n\n L.argfirst (f, negate=False)\n\nReturn index of first matching item\n\ntest_eq(t.argfirst(lambda o:o>4), 5)\ntest_eq(t.argfirst(lambda o:o>4,negate=True),0)\n\n\nsource\n\n\nL.map\n\n L.map (f, *args, **kwargs)\n\nCreate new L with f applied to all items, passing args and kwargs to f\n\ntest_eq(L.range(4).map(operator.neg), [0,-1,-2,-3])\n\nIf f is a string then it is treated as a format string to create the mapping:\n\ntest_eq(L.range(4).map('#{}#'), ['#0#','#1#','#2#','#3#'])\n\nIf f is a dictionary (or anything supporting __getitem__) then it is indexed to create the mapping:\n\ntest_eq(L.range(4).map(list('abcd')), list('abcd'))\n\nYou can also pass the same arg params that bind accepts:\n\ndef f(a=None,b=None): return b\ntest_eq(L.range(4).map(f, b=arg0), range(4))\n\n\nsource\n\n\nL.map_dict\n\n L.map_dict (f=<function noop>, *args, **kwargs)\n\nLike map, but creates a dict from items to function results\n\ntest_eq(L(range(1,5)).map_dict(), {1:1, 2:2, 3:3, 4:4})\ntest_eq(L(range(1,5)).map_dict(operator.neg), {1:-1, 2:-2, 3:-3, 4:-4})\n\n\nsource\n\n\nL.zip\n\n L.zip (cycled=False)\n\nCreate new L with zip(*items)\n\nt = L([[1,2,3],'abc'])\ntest_eq(t.zip(), [(1, 'a'),(2, 'b'),(3, 'c')])\n\n\nt = L([[1,2,3,4],['a','b','c']])\ntest_eq(t.zip(cycled=True ), [(1, 'a'),(2, 'b'),(3, 'c'),(4, 'a')])\ntest_eq(t.zip(cycled=False), [(1, 'a'),(2, 'b'),(3, 'c')])\n\n\nsource\n\n\nL.map_zip\n\n L.map_zip (f, *args, cycled=False, **kwargs)\n\nCombine zip and starmap\n\nt = L([1,2,3],[2,3,4])\ntest_eq(t.map_zip(operator.mul), [2,6,12])\n\n\nsource\n\n\nL.zipwith\n\n L.zipwith (*rest, cycled=False)\n\nCreate new L with self zip with each of *rest\n\nb = [[0],[1],[2,2]]\nt = L([1,2,3]).zipwith(b)\ntest_eq(t, [(1,[0]), (2,[1]), (3,[2,2])])\n\n\nsource\n\n\nL.map_zipwith\n\n L.map_zipwith (f, *rest, cycled=False, **kwargs)\n\nCombine zipwith and starmap\n\ntest_eq(L(1,2,3).map_zipwith(operator.mul, [2,3,4]), [2,6,12])\n\n\nsource\n\n\nL.itemgot\n\n L.itemgot (*idxs)\n\nCreate new L with item idx of all items\n\ntest_eq(t.itemgot(1), b)\n\n\nsource\n\n\nL.attrgot\n\n L.attrgot (k, default=None)\n\nCreate new L with attr k (or value k for dicts) of all items.\n\n# Example when items are not a dict\na = [SimpleNamespace(a=3,b=4),SimpleNamespace(a=1,b=2)]\ntest_eq(L(a).attrgot('b'), [4,2])\n\n#Example of when items are a dict\nb =[{'id': 15, 'name': 'nbdev'}, {'id': 17, 'name': 'fastcore'}]\ntest_eq(L(b).attrgot('id'), [15, 17])\n\n\nsource\n\n\nL.sorted\n\n L.sorted (key=None, reverse=False)\n\nNew L sorted by key. If key is str use attrgetter; if int use itemgetter\n\ntest_eq(L(a).sorted('a').attrgot('b'), [2,4])\n\n\nsource\n\n\nL.split\n\n L.split (s, sep=None, maxsplit=-1)\n\nClass Method: Same as str.split, but returns an L\n\ntest_eq(L.split('a b c'), list('abc'))\n\n\nsource\n\n\nL.range\n\n L.range (a, b=None, step=None)\n\nClass Method: Same as range, but returns L. Can pass collection for a, to use len(a)\n\ntest_eq_type(L.range([1,1,1]), L(range(3)))\ntest_eq_type(L.range(5,2,2), L(range(5,2,2)))\n\n\nsource\n\n\nL.concat\n\n L.concat ()\n\nConcatenate all elements of list\n\ntest_eq(L([0,1,2,3],4,L(5,6)).concat(), range(7))\n\n\nsource\n\n\nL.copy\n\n L.copy ()\n\nSame as list.copy, but returns an L\n\nt = L([0,1,2,3],4,L(5,6)).copy()\ntest_eq(t.concat(), range(7))\n\n\nsource\n\n\nL.map_first\n\n L.map_first (f=<function noop>, g=<function noop>, *args, **kwargs)\n\nFirst element of map_filter\n\nt = L(0,1,2,3)\ntest_eq(t.map_first(lambda o:o*2 if o>2 else None), 6)\n\n\nsource\n\n\nL.setattrs\n\n L.setattrs (attr, val)\n\nCall setattr on all items\n\nt = L(SimpleNamespace(),SimpleNamespace())\nt.setattrs('foo', 'bar')\ntest_eq(t.attrgot('foo'), ['bar','bar'])", + "crumbs": [ + "Foundation" + ] + }, + { + "objectID": "foundation.html#config", + "href": "foundation.html#config", + "title": "Foundation", + "section": "Config", + "text": "Config\n\nsource\n\nsave_config_file\n\n save_config_file (file, d, **kwargs)\n\nWrite settings dict to a new config file, or overwrite the existing one.\n\nsource\n\n\nread_config_file\n\n read_config_file (file, **kwargs)\n\nConfig files are saved and read using Python’s configparser.ConfigParser, inside the DEFAULT section.\n\n_d = dict(user='fastai', lib_name='fastcore', some_path='test', some_bool=True, some_num=3)\ntry:\n save_config_file('tmp.ini', _d)\n res = read_config_file('tmp.ini')\nfinally: os.unlink('tmp.ini')\ndict(res)\n\n{'user': 'fastai',\n 'lib_name': 'fastcore',\n 'some_path': 'test',\n 'some_bool': 'True',\n 'some_num': '3'}\n\n\n\nsource\n\n\nConfig\n\n Config (cfg_path, cfg_name, create=None, save=True, extra_files=None,\n types=None)\n\nReading and writing ConfigParser ini files\nConfig is a convenient wrapper around ConfigParser ini files with a single section (DEFAULT).\nInstantiate a Config from an ini file at cfg_path/cfg_name:\n\nsave_config_file('../tmp.ini', _d)\ntry: cfg = Config('..', 'tmp.ini')\nfinally: os.unlink('../tmp.ini')\ncfg\n\n{'user': 'fastai', 'lib_name': 'fastcore', 'some_path': 'test', 'some_bool': 'True', 'some_num': '3'}\n\n\nYou can create a new file if one doesn’t exist by providing a create dict:\n\ntry: cfg = Config('..', 'tmp.ini', create=_d)\nfinally: os.unlink('../tmp.ini')\ncfg\n\n{'user': 'fastai', 'lib_name': 'fastcore', 'some_path': 'test', 'some_bool': 'True', 'some_num': '3'}\n\n\nIf you additionally pass save=False, the Config will contain the items from create without writing a new file:\n\ncfg = Config('..', 'tmp.ini', create=_d, save=False)\ntest_eq(cfg.user,'fastai')\nassert not Path('../tmp.ini').exists()\n\n\nsource\n\n\nConfig.get\n\n Config.get (k, default=None)\n\nKeys can be accessed as attributes, items, or with get and an optional default:\n\ntest_eq(cfg.user,'fastai')\ntest_eq(cfg['some_path'], 'test')\ntest_eq(cfg.get('foo','bar'),'bar')\n\nExtra files can be read before cfg_path/cfg_name using extra_files, in the order they appear:\n\nwith tempfile.TemporaryDirectory() as d:\n a = Config(d, 'a.ini', {'a':0,'b':0})\n b = Config(d, 'b.ini', {'a':1,'c':0})\n c = Config(d, 'c.ini', {'a':2,'d':0}, extra_files=[a.config_file,b.config_file])\n test_eq(c.d, {'a':'2','b':'0','c':'0','d':'0'})\n\nIf you pass a dict types, then the values of that dict will be used as types to instantiate all values returned. Path is a special case – in that case, the path returned will be relative to the path containing the config file (assuming the value is relative). bool types use str2bool to convert to boolean.\n\n_types = dict(some_path=Path, some_bool=bool, some_num=int)\ncfg = Config('..', 'tmp.ini', create=_d, save=False, types=_types)\n\ntest_eq(cfg.user,'fastai')\ntest_eq(cfg['some_path'].resolve(), (Path('..')/'test').resolve())\ntest_eq(cfg.get('some_num'), 3)\n\n\nsource\n\n\nConfig.find\n\n Config.find (cfg_name, cfg_path=None, **kwargs)\n\nSearch cfg_path and its parents to find cfg_name\nYou can use Config.find to search subdirectories for a config file, starting in the current path if no path is specified:\n\nConfig.find('settings.ini').repo\n\n'fastcore'", + "crumbs": [ + "Foundation" + ] + }, + { + "objectID": "tour.html", + "href": "tour.html", + "title": "A tour of fastcore", + "section": "", + "text": "Here’s a (somewhat) quick tour of a few higlights from fastcore.\n\nDocumentation\nAll fast.ai projects, including this one, are built with nbdev, which is a full literate programming environment built on Jupyter Notebooks. That means that every piece of documentation, including the page you’re reading now, can be accessed as interactive Jupyter notebooks. In fact, you can even grab a link directly to a notebook running interactively on Google Colab - if you want to follow along with this tour, click the link below:\n\ncolab_link('index')\n\nOpen index in Colab\n\n\nThe full docs are available at fastcore.fast.ai. The code in the examples and in all fast.ai libraries follow the fast.ai style guide. In order to support interactive programming, all fast.ai libraries are designed to allow for import * to be used safely, particular by ensuring that __all__ is defined in all packages. In order to see where a function is from, just type it:\n\ncoll_repr\n\n<function fastcore.foundation.coll_repr(c, max_n=10)>\n\n\nFor more details, including a link to the full documentation and source code, use doc, which pops up a window with this information:\ndoc(coll_repr)\n\nThe documentation also contains links to any related functions or classes, which appear like this: coll_repr (in the notebook itself you will just see a word with back-ticks around it; the links are auto-generated in the documentation site). The documentation will generally show one or more examples of use, along with any background context necessary to understand them. As you’ll see, the examples for each function and method are shown as tests, rather than example outputs, so let’s start by explaining that.\n\n\nTesting\nfastcore’s testing module is designed to work well with nbdev, which is a full literate programming environment built on Jupyter Notebooks. That means that your tests, docs, and code all live together in the same notebook. fastcore and nbdev’s approach to testing starts with the premise that all your tests should pass. If one fails, no more tests in a notebook are run.\nTests look like this:\n\ntest_eq(coll_repr(range(1000), 5), '(#1000) [0,1,2,3,4...]')\n\nThat’s an example from the docs for coll_repr. As you see, it’s not showing you the output directly. Here’s what that would look like:\n\ncoll_repr(range(1000), 5)\n\n'(#1000) [0,1,2,3,4...]'\n\n\nSo, the test is actually showing you what the output looks like, because if the function call didn’t return '(#1000) [0,1,2,3,4...]', then the test would have failed.\nSo every test shown in the docs is also showing you the behavior of the library — and vice versa!\nTest functions always start with test_, and then follow with the operation being tested. So test_eq tests for equality (as you saw in the example above). This includes tests for equality of arrays and tensors, lists and generators, and many more:\n\ntest_eq([0,1,2,3], np.arange(4))\n\nWhen a test fails, it prints out information about what was expected:\ntest_eq([0,1,2,3], np.arange(3))\n----\n AssertionError: ==:\n [0, 1, 2, 3]\n [0 1 2]\nIf you want to check that objects are the same type, rather than the just contain the same collection, use test_eq_type.\nYou can test with any comparison function using test, e.g test whether an object is less than:\n\ntest(2, 3, operator.lt)\n\nYou can even test that exceptions are raised:\n\ndef divide_zero(): return 1/0\ntest_fail(divide_zero)\n\n…and test that things are printed to stdout:\n\ntest_stdout(lambda: print('hi'), 'hi')\n\n\n\nFoundations\nfast.ai is unusual in that we often use mixins in our code. Mixins are widely used in many programming languages, such as Ruby, but not so much in Python. We use mixins to attach new behavior to existing libraries, or to allow modules to add new behavior to our own classes, such as in extension modules. One useful example of a mixin we define is Path.ls, which lists a directory and returns an L (an extended list class which we’ll discuss shortly):\n\np = Path('images')\np.ls()\n\n(#6) [Path('images/mnist3.png'),Path('images/att_00000.png'),Path('images/puppy.jpg'),Path('images/att_00005.png'),Path('images/att_00007.png'),Path('images/att_00006.png')]\n\n\nYou can easily add you own mixins with the patch decorator, which takes advantage of Python 3 function annotations to say what class to patch:\n\n@patch\ndef num_items(self:Path): return len(self.ls())\n\np.num_items()\n\n6\n\n\nWe also use **kwargs frequently. In python **kwargs in a parameter like means “put any additional keyword arguments into a dict called kwargs”. Normally, using kwargs makes an API quite difficult to work with, because it breaks things like tab-completion and popup lists of signatures. utils provides use_kwargs and delegates to avoid this problem. See our detailed article on delegation on this topic.\nGetAttr solves a similar problem (and is also discussed in the article linked above): it’s allows you to use Python’s exceptionally useful __getattr__ magic method, but avoids the problem that normally in Python tab-completion and docs break when using this. For instance, you can see here that Python’s dir function, which is used to find the attributes of a python object, finds everything inside the self.default attribute here:\n\nclass Author:\n def __init__(self, name): self.name = name\n\nclass ProductPage(GetAttr):\n _default = 'author'\n def __init__(self,author,price,cost): self.author,self.price,self.cost = author,price,cost\n\np = ProductPage(Author(\"Jeremy\"), 1.50, 0.50)\n[o for o in dir(p) if not o.startswith('_')]\n\n['author', 'cost', 'name', 'price']\n\n\nLooking at that ProductPage example, it’s rather verbose and duplicates a lot of attribute names, which can lead to bugs later if you change them only in one place. fastcore provides store_attr to simplify this common pattern. It also provides basic_repr to give simple objects a useful repr:\n\nclass ProductPage:\n def __init__(self,author,price,cost): store_attr()\n __repr__ = basic_repr('author,price,cost')\n\nProductPage(\"Jeremy\", 1.50, 0.50)\n\n__main__.ProductPage(author='Jeremy', price=1.5, cost=0.5)\n\n\nOne of the most interesting fastcore functions is the funcs_kwargs decorator. This allows class behavior to be modified without sub-classing. This can allow folks that aren’t familiar with object-oriented programming to customize your class more easily. Here’s an example of a class that uses funcs_kwargs:\n\n@funcs_kwargs\nclass T:\n _methods=['some_method']\n def __init__(self, **kwargs): assert not kwargs, f'Passed unknown args: {kwargs}'\n\np = T(some_method = print)\np.some_method(\"hello\")\n\nhello\n\n\nThe assert not kwargs above is used to ensure that the user doesn’t pass an unknown parameter (i.e one that’s not in _methods). fastai uses funcs_kwargs in many places, for instance, you can customize any part of a DataLoader by passing your own methods.\nfastcore also provides many utility functions that make a Python programmer’s life easier, in fastcore.utils. We won’t look at many here, since you can easily look at the docs yourself. To get you started, have a look at the docs for chunked (remember, if you’re in a notebook, type doc(chunked)), which is a handy function for creating lazily generated batches from a collection.\nPython’s ProcessPoolExecutor is extended to allow max_workers to be set to 0, to easily turn off parallel processing. This makes it easy to debug your code in serial, then run it in parallel. It also allows you to pass arguments to your parallel function, and to ensure there’s a pause between calls, in case the process you are running has race conditions. parallel makes parallel processing even easier to use, and even adds an optional progress bar.\n\n\nL\nLike most languages, Python allows for very concise syntax for some very common types, such as list, which can be constructed with [1,2,3]. Perl’s designer Larry Wall explained the reasoning for this kind of syntax:\n\nIn metaphorical honor of Huffman’s compression code that assigns smaller numbers of bits to more common bytes. In terms of syntax, it simply means that commonly used things should be shorter, but you shouldn’t waste short sequences on less common constructs.\n\nOn this basis, fastcore has just one type that has a single letter name: L. The reason for this is that it is designed to be a replacement for list, so we want it to be just as easy to use as [1,2,3]. Here’s how to create that as an L:\n\nL(1,2,3)\n\n(#3) [1,2,3]\n\n\nThe first thing to notice is that an L object includes in its representation its number of elements; that’s the (#3) in the output above. If there’s more than 10 elements, it will automatically truncate the list:\n\np = L.range(20).shuffle()\np\n\n(#20) [5,1,9,10,18,13,6,17,3,16...]\n\n\nL contains many of the same indexing ideas that NumPy’s array does, including indexing with a list of indexes, or a boolean mask list:\n\np[2,4,6]\n\n(#3) [9,18,6]\n\n\nIt also contains other methods used in array, such as L.argwhere:\n\np.argwhere(ge(15))\n\n(#5) [4,7,9,18,19]\n\n\nAs you can see from this example, fastcore also includes a number of features that make a functional style of programming easier, such as a full range of boolean functions (e.g ge, gt, etc) which give the same answer as the functions from Python’s operator module if given two parameters, but return a curried function if given one parameter.\nThere’s too much functionality to show it all here, so be sure to check the docs. Many little things are added that we thought should have been in list in the first place, such as making this do what you’d expect (which is an error with list, but works fine with L):\n\n1 + L(2,3,4)\n\n(#4) [1,2,3,4]\n\n\n\n\nTransforms\nA Transform is the main building block of the fastai data pipelines. In the most general terms a transform can be any function you want to apply to your data, however the Transform class provides several mechanisms that make the process of building them easy and flexible (see the docs for information about each of these):\n\nType dispatch\nDispatch over tuples\nReversability\nType propagation\nPreprocessing\nFiltering based on the dataset type\nOrdering\nAppending new behavior with decorators\n\nTransform looks for three special methods, encodes, decodes, and setups, which provide the implementation for __call__, decode, and setup respectively. For instance:\n\nclass A(Transform):\n def encodes(self, x): return x+1\n\nA()(1)\n\n2\n\n\nFor simple transforms like this, you can also use Transform as a decorator:\n\n@Transform\ndef f(x): return x+1\n\nf(1)\n\n2\n\n\nTransforms can be composed into a Pipeline:\n\n@Transform\ndef g(x): return x/2\n\npipe = Pipeline([f,g])\npipe(3)\n\n2.0\n\n\nThe power of Transform and Pipeline is best understood by seeing how they’re used to create a complete data processing pipeline. This is explained in chapter 11 of the fastai book, which is available for free in Jupyter Notebook format.", + "crumbs": [ + "A tour of fastcore" + ] + }, + { + "objectID": "net.html", + "href": "net.html", + "title": "Network functionality", + "section": "", + "text": "from fastcore.test import *\nfrom nbdev.showdoc import *\nfrom fastcore.nb_imports import *", + "crumbs": [ + "Network functionality" + ] + }, + { + "objectID": "net.html#urls", + "href": "net.html#urls", + "title": "Network functionality", + "section": "URLs", + "text": "URLs\n\nsource\n\nurlquote\n\n urlquote (url)\n\nUpdate url’s path with urllib.parse.quote\n\nurlquote(\"https://github.com/fastai/fastai/compare/master@{1.day.ago}…master\")\n\n'https://github.com/fastai/fastai/compare/master@%7B1.day.ago%7D%E2%80%A6master'\n\n\n\nurlquote(\"https://www.google.com/search?q=你好\")\n\n'https://www.google.com/search?q=%E4%BD%A0%E5%A5%BD'\n\n\n\nsource\n\n\nurlwrap\n\n urlwrap (url, data=None, headers=None)\n\nWrap url in a urllib Request with urlquote\n\nsource\n\nHTTP4xxClientError\n\n HTTP4xxClientError (url, code, msg, hdrs, fp)\n\nBase class for client exceptions (code 4xx) from url* functions\n\nsource\n\n\nHTTP5xxServerError\n\n HTTP5xxServerError (url, code, msg, hdrs, fp)\n\nBase class for server exceptions (code 5xx) from url* functions\n\nsource\n\n\n\nurlopener\n\n urlopener ()\n\n\nsource\n\n\nurlopen\n\n urlopen (url, data=None, headers=None, timeout=None, **kwargs)\n\nLike urllib.request.urlopen, but first urlwrap the url, and encode data\nWith urlopen, the body of the response will also be returned in addition to the message if there is an error:\n\ntry: urlopen('https://api.github.com/v3')\nexcept HTTPError as e: \n print(e.code, e.msg)\n assert 'documentation_url' in e.msg\n\n404 Not Found\n====Error Body====\n{\n \"message\": \"Not Found\",\n \"documentation_url\": \"https://docs.github.com/rest\"\n}\n\n\n\n\nsource\n\n\nurlread\n\n urlread (url, data=None, headers=None, decode=True, return_json=False,\n return_headers=False, timeout=None, **kwargs)\n\nRetrieve url, using data dict or kwargs to POST if present\n\nsource\n\n\nurljson\n\n urljson (url, data=None, timeout=None)\n\nRetrieve url and decode json\n\ntest_eq(urljson('https://httpbin.org/get')['headers']['User-Agent'], url_default_headers['User-Agent'])\n\n\nsource\n\n\nurlcheck\n\n urlcheck (url, headers=None, timeout=10)\n\n\nsource\n\n\nurlclean\n\n urlclean (url)\n\nRemove fragment, params, and querystring from url if present\n\ntest_eq(urlclean('http://a.com/b?c=1#d'), 'http://a.com/b')\n\n\nsource\n\n\nurlretrieve\n\n urlretrieve (url, filename=None, reporthook=None, data=None,\n headers=None, timeout=None)\n\nSame as urllib.request.urlretrieve but also works with Request objects\n\nsource\n\n\nurldest\n\n urldest (url, dest=None)\n\n\nsource\n\n\nurlsave\n\n urlsave (url, dest=None, reporthook=None, headers=None, timeout=None)\n\nRetrieve url and save based on its name\n\n#skip\nwith tempfile.TemporaryDirectory() as d: urlsave('http://www.google.com/index.html', d)\n\n\nsource\n\n\nurlvalid\n\n urlvalid (x)\n\nTest if x is a valid URL\n\nassert urlvalid('http://www.google.com/')\nassert not urlvalid('www.google.com/')\nassert not urlvalid(1)\n\n\nsource\n\n\nurlrequest\n\n urlrequest (url, verb, headers=None, route=None, query=None, data=None,\n json_data=True)\n\nRequest for url with optional route params replaced by route, plus query string, and post data\n\nhdr = {'Hdr1':'1', 'Hdr2':'2'}\nreq = urlrequest('http://example.com/{foo}/1', 'POST',\n headers=hdr, route={'foo':'3'}, query={'q':'4'}, data={'d':'5'})\n\ntest_eq(req.headers, hdr)\ntest_eq(req.full_url, 'http://example.com/3/1?q=4')\ntest_eq(req.method, 'POST')\ntest_eq(req.data, b'{\"d\": \"5\"}')\n\n\nreq = urlrequest('http://example.com/{foo}/1', 'POST', data={'d':'5','e':'6'}, headers=hdr, json_data=False)\ntest_eq(req.data, b'd=5&e=6')\n\n\nsource\n\n\nRequest.summary\n\n Request.summary (skip=None)\n\nSummary containing full_url, headers, method, and data, removing skip from headers\n\nreq.summary(skip='Hdr1')\n\n{'full_url': 'http://example.com/{foo}/1',\n 'method': 'POST',\n 'data': b'd=5&e=6',\n 'headers': {'Hdr2': '2'}}\n\n\n\nsource\n\n\nurlsend\n\n urlsend (url, verb, headers=None, decode=True, route=None, query=None,\n data=None, json_data=True, return_json=True,\n return_headers=False, debug=None, timeout=None)\n\nSend request with urlrequest, converting result to json if return_json\n\nsource\n\n\ndo_request\n\n do_request (url, post=False, headers=None, **data)\n\nCall GET or json-encoded POST on url, depending on post", + "crumbs": [ + "Network functionality" + ] + }, + { + "objectID": "net.html#basic-clientserver", + "href": "net.html#basic-clientserver", + "title": "Network functionality", + "section": "Basic client/server", + "text": "Basic client/server\n\nsource\n\nstart_server\n\n start_server (port, host=None, dgram=False, reuse_addr=True,\n n_queue=None)\n\nCreate a socket server on port, with optional host, of type dgram\nYou can create a TCP client and server pass an int as port and optional host. host defaults to your main network interface if not provided. You can create a Unix socket client and server by passing a string to port. A SOCK_STREAM socket is created by default, unless you pass dgram=True, in which case a SOCK_DGRAM socket is created. n_queue sets the listening queue size.\n\nsource\n\n\nstart_client\n\n start_client (port, host=None, dgram=False)\n\nCreate a socket client on port, with optional host, of type dgram\n\nsource\n\n\ntobytes\n\n tobytes (s:str)\n\nConvert s into HTTP-ready bytes format\n\ntest_eq(tobytes('foo\\nbar'), b'foo\\r\\nbar')\n\n\nsource\n\n\nhttp_response\n\n http_response (body=None, status=200, hdrs=None, **kwargs)\n\nCreate an HTTP-ready response, adding kwargs to hdrs\n\nexp = b'HTTP/1.1 200 OK\\r\\nUser-Agent: me\\r\\nContent-Length: 4\\r\\n\\r\\nbody'\ntest_eq(http_response('body', 200, User_Agent='me'), exp)\n\n\nsource\n\n\nrecv_once\n\n recv_once (host:str='localhost', port:int=8000)\n\nSpawn a thread to receive a single HTTP request and store in d['r']", + "crumbs": [ + "Network functionality" + ] + }, + { + "objectID": "index.html", + "href": "index.html", + "title": "Welcome to fastcore", + "section": "", + "text": "Python is a powerful, dynamic language. Rather than bake everything into the language, it lets the programmer customize it to make it work for them. fastcore uses this flexibility to add to Python features inspired by other languages we’ve loved, like multiple dispatch from Julia, mixins from Ruby, and currying, binding, and more from Haskell. It also adds some “missing features” and clean up some rough edges in the Python standard library, such as simplifying parallel processing, and bringing ideas from NumPy over to Python’s list type.", + "crumbs": [ + "Welcome to fastcore" + ] + }, + { + "objectID": "index.html#getting-started", + "href": "index.html#getting-started", + "title": "Welcome to fastcore", + "section": "Getting started", + "text": "Getting started\nTo install fastcore run: conda install fastcore -c fastai (if you use Anaconda, which we recommend) or pip install fastcore. For an editable install, clone this repo and run: pip install -e \".[dev]\". fastcore is tested to work on Ubuntu, macOS and Windows (versions tested are those shown with the -latest suffix here).\nfastcore contains many features, including:\n\nfastcore.test: Simple testing functions\nfastcore.foundation: Mixins, delegation, composition, and more\nfastcore.xtras: Utility functions to help with functional-style programming, parallel processing, and more\nfastcore.dispatch: Multiple dispatch methods\nfastcore.transform: Pipelines of composed partially reversible transformations\n\nTo get started, we recommend you read through the fastcore tour.", + "crumbs": [ + "Welcome to fastcore" + ] + }, + { + "objectID": "index.html#contributing", + "href": "index.html#contributing", + "title": "Welcome to fastcore", + "section": "Contributing", + "text": "Contributing\nAfter you clone this repository, please run nbdev_install_hooks in your terminal. This sets up git hooks, which clean up the notebooks to remove the extraneous stuff stored in the notebooks (e.g. which cells you ran) which causes unnecessary merge conflicts.\nTo run the tests in parallel, launch nbdev_test.\nBefore submitting a PR, check that the local library and notebooks match.\n\nIf you made a change to the notebooks in one of the exported cells, you can export it to the library with nbdev_prepare.\nIf you made a change to the library, you can export it back to the notebooks with nbdev_update.", + "crumbs": [ + "Welcome to fastcore" + ] + }, + { + "objectID": "basics.html", + "href": "basics.html", + "title": "Basic functionality", + "section": "", + "text": "source\n\n\n\n ifnone (a, b)\n\nb if a is None else a\nSince b if a is None else a is such a common pattern, we wrap it in a function. However, be careful, because python will evaluate both a and b when calling ifnone (which it doesn’t do if using the if version directly).\n\ntest_eq(ifnone(None,1), 1)\ntest_eq(ifnone(2 ,1), 2)\n\n\nsource\n\n\n\n\n maybe_attr (o, attr)\n\ngetattr(o,attr,o)\nReturn the attribute attr for object o. If the attribute doesn’t exist, then return the object o instead.\n\nclass myobj: myattr='foo'\n\ntest_eq(maybe_attr(myobj, 'myattr'), 'foo')\ntest_eq(maybe_attr(myobj, 'another_attr'), myobj)\n\n\nsource\n\n\n\n\n basic_repr (flds=None)\n\nMinimal __repr__\nIn types which provide rich display functionality in Jupyter, their __repr__ is also called in order to provide a fallback text representation. Unfortunately, this includes a memory address which changes on every invocation, making it non-deterministic. This causes diffs to get messy and creates conflicts in git. To fix this, put __repr__=basic_repr() inside your class.\n\nclass SomeClass: __repr__=basic_repr()\nrepr(SomeClass())\n\n'SomeClass()'\n\n\nIf you pass a list of attributes (flds) of an object, then this will generate a string with the name of each attribute and its corresponding value. The format of this string is key=value, where key is the name of the attribute, and value is the value of the attribute. For each value, attempt to use the __name__ attribute, otherwise fall back to using the value’s __repr__ when constructing the string.\n\nclass SomeClass:\n a=1\n b='foo'\n __repr__=basic_repr('a,b')\n __name__='some-class'\n\nrepr(SomeClass())\n\n\"SomeClass(a=1, b='foo')\"\n\n\nNested objects work too:\n\nclass AnotherClass:\n c=SomeClass()\n d='bar'\n __repr__=basic_repr(['c', 'd'])\n\nrepr(AnotherClass())\n\n\"AnotherClass(c=SomeClass(a=1, b='foo'), d='bar')\"\n\n\nInstance variables (but not class variables) are shown if basic_repr is called with no arguments:\n\nclass SomeClass:\n def __init__(self, a=1, b='foo'): self.a,self.b = a,b\n __repr__=basic_repr()\n\nrepr(SomeClass())\n\n\"SomeClass(a=1, b='foo')\"\n\n\n\nsource\n\n\n\n\n BasicRepr ()\n\nBase class for objects needing a basic __repr__\nAs a shortcut for creating a __repr__ for instance variables, you can inherit from BasicRepr:\n\nclass SomeClass(BasicRepr):\n def __init__(self, a=1, b='foo'): self.a,self.b = a,b\n\nrepr(SomeClass())\n\n\"SomeClass(a=1, b='foo')\"\n\n\n\nsource\n\n\n\n\n is_array (x)\n\nTrue if x supports __array__ or iloc\n\nis_array(np.array(1)),is_array([1])\n\n(True, False)\n\n\n\nsource\n\n\n\n\n listify (o=None, *rest, use_list=False, match=None)\n\nConvert o to a list\nConversion is designed to “do what you mean”, e.g:\n\ntest_eq(listify('hi'), ['hi'])\ntest_eq(listify(b'hi'), [b'hi'])\ntest_eq(listify(array(1)), [array(1)])\ntest_eq(listify(1), [1])\ntest_eq(listify([1,2]), [1,2])\ntest_eq(listify(range(3)), [0,1,2])\ntest_eq(listify(None), [])\ntest_eq(listify(1,2), [1,2])\n\n\narr = np.arange(9).reshape(3,3)\nlistify(arr)\n\n[array([[0, 1, 2],\n [3, 4, 5],\n [6, 7, 8]])]\n\n\n\nlistify(array([1,2]))\n\n[array([1, 2])]\n\n\nGenerators are turned into lists too:\n\ngen = (o for o in range(3))\ntest_eq(listify(gen), [0,1,2])\n\nUse match to provide a length to match:\n\ntest_eq(listify(1,match=3), [1,1,1])\n\nIf match is a sequence, it’s length is used:\n\ntest_eq(listify(1,match=range(3)), [1,1,1])\n\nIf the listified item is not of length 1, it must be the same length as match:\n\ntest_eq(listify([1,1,1],match=3), [1,1,1])\ntest_fail(lambda: listify([1,1],match=3))\n\n\nsource\n\n\n\n\n tuplify (o, use_list=False, match=None)\n\nMake o a tuple\n\ntest_eq(tuplify(None),())\ntest_eq(tuplify([1,2,3]),(1,2,3))\ntest_eq(tuplify(1,match=[1,2,3]),(1,1,1))\n\n\nsource\n\n\n\n\n true (x)\n\nTest whether x is truthy; collections with >0 elements are considered True\n\n[(o,true(o)) for o in\n (array(0),array(1),array([0]),array([0,1]),1,0,'',None)]\n\n[(array(0), False),\n (array(1), True),\n (array([0]), True),\n (array([0, 1]), True),\n (1, True),\n (0, False),\n ('', False),\n (None, False)]\n\n\n\nsource\n\n\n\n\n NullType ()\n\nAn object that is False and can be called, chained, and indexed\n\nbool(null.hi().there[3])\n\nFalse\n\n\n\nsource\n\n\n\n\n tonull (x)\n\nConvert None to null\n\nbool(tonull(None).hi().there[3])\n\nFalse\n\n\n\nsource\n\n\n\n\n get_class (nm, *fld_names, sup=None, doc=None, funcs=None, anno=None,\n **flds)\n\nDynamically create a class, optionally inheriting from sup, containing fld_names\n\n_t = get_class('_t', 'a', b=2, anno={'b':int})\nt = _t()\ntest_eq(t.a, None)\ntest_eq(t.b, 2)\nt = _t(1, b=3)\ntest_eq(t.a, 1)\ntest_eq(t.b, 3)\nt = _t(1, 3)\ntest_eq(t.a, 1)\ntest_eq(t.b, 3)\ntest_eq(t, pickle.loads(pickle.dumps(t)))\ntest_eq(_t.__annotations__, {'b':int, 'a':typing.Any})\nrepr(t)\n\n'__main__._t(a=1, b=3)'\n\n\nMost often you’ll want to call mk_class, since it adds the class to your module. See mk_class for more details and examples of use (which also apply to get_class).\n\nsource\n\n\n\n\n mk_class (nm, *fld_names, sup=None, doc=None, funcs=None, mod=None,\n anno=None, **flds)\n\nCreate a class using get_class and add to the caller’s module\nAny kwargs will be added as class attributes, and sup is an optional (tuple of) base classes.\n\nmk_class('_t', a=1, sup=dict)\nt = _t()\ntest_eq(t.a, 1)\nassert(isinstance(t,dict))\n\nA __init__ is provided that sets attrs for any kwargs, and for any args (matching by position to fields), along with a __repr__ which prints all attrs. The docstring is set to doc. You can pass funcs which will be added as attrs with the function names.\n\ndef foo(self): return 1\nmk_class('_t', 'a', sup=dict, doc='test doc', funcs=foo)\n\nt = _t(3, b=2)\ntest_eq(t.a, 3)\ntest_eq(t.b, 2)\ntest_eq(t.foo(), 1)\ntest_eq(t.__doc__, 'test doc')\nt\n\n{}\n\n\n\nsource\n\n\n\n\n wrap_class (nm, *fld_names, sup=None, doc=None, funcs=None, **flds)\n\nDecorator: makes function a method of a new class nm passing parameters to mk_class\n\n@wrap_class('_t', a=2)\ndef bar(self,x): return x+1\n\nt = _t()\ntest_eq(t.a, 2)\ntest_eq(t.bar(3), 4)\n\n\nsource\n\n\n\n ignore_exceptions ()\n\nContext manager to ignore exceptions\n\nwith ignore_exceptions(): \n # Exception will be ignored\n raise Exception\n\n\nsource\n\n\n\n\n\n exec_local (code, var_name)\n\nCall exec on code and return the var var_name\n\ntest_eq(exec_local(\"a=1\", \"a\"), 1)\n\n\nsource\n\n\n\n\n risinstance (types, obj=None)\n\nCurried isinstance but with args reversed\n\nassert risinstance(int, 1)\nassert not risinstance(str, 0)\nassert risinstance(int)(1)\nassert not risinstance(int)(None)\n\ntypes can also be strings:\n\nassert risinstance(('str','int'), 'a')\nassert risinstance('str', 'a')\nassert not risinstance('int', 'a')\n\n\nsource\n\n\n\n\n ver2tuple (v:str)\n\n\ntest_eq(ver2tuple('3.8.1'), (3,8,1))\ntest_eq(ver2tuple('3.1'), (3,1,0))\ntest_eq(ver2tuple('3.'), (3,0,0))\ntest_eq(ver2tuple('3'), (3,0,0))", + "crumbs": [ + "Basic functionality" + ] + }, + { + "objectID": "basics.html#basics", + "href": "basics.html#basics", + "title": "Basic functionality", + "section": "", + "text": "source\n\n\n\n ifnone (a, b)\n\nb if a is None else a\nSince b if a is None else a is such a common pattern, we wrap it in a function. However, be careful, because python will evaluate both a and b when calling ifnone (which it doesn’t do if using the if version directly).\n\ntest_eq(ifnone(None,1), 1)\ntest_eq(ifnone(2 ,1), 2)\n\n\nsource\n\n\n\n\n maybe_attr (o, attr)\n\ngetattr(o,attr,o)\nReturn the attribute attr for object o. If the attribute doesn’t exist, then return the object o instead.\n\nclass myobj: myattr='foo'\n\ntest_eq(maybe_attr(myobj, 'myattr'), 'foo')\ntest_eq(maybe_attr(myobj, 'another_attr'), myobj)\n\n\nsource\n\n\n\n\n basic_repr (flds=None)\n\nMinimal __repr__\nIn types which provide rich display functionality in Jupyter, their __repr__ is also called in order to provide a fallback text representation. Unfortunately, this includes a memory address which changes on every invocation, making it non-deterministic. This causes diffs to get messy and creates conflicts in git. To fix this, put __repr__=basic_repr() inside your class.\n\nclass SomeClass: __repr__=basic_repr()\nrepr(SomeClass())\n\n'SomeClass()'\n\n\nIf you pass a list of attributes (flds) of an object, then this will generate a string with the name of each attribute and its corresponding value. The format of this string is key=value, where key is the name of the attribute, and value is the value of the attribute. For each value, attempt to use the __name__ attribute, otherwise fall back to using the value’s __repr__ when constructing the string.\n\nclass SomeClass:\n a=1\n b='foo'\n __repr__=basic_repr('a,b')\n __name__='some-class'\n\nrepr(SomeClass())\n\n\"SomeClass(a=1, b='foo')\"\n\n\nNested objects work too:\n\nclass AnotherClass:\n c=SomeClass()\n d='bar'\n __repr__=basic_repr(['c', 'd'])\n\nrepr(AnotherClass())\n\n\"AnotherClass(c=SomeClass(a=1, b='foo'), d='bar')\"\n\n\nInstance variables (but not class variables) are shown if basic_repr is called with no arguments:\n\nclass SomeClass:\n def __init__(self, a=1, b='foo'): self.a,self.b = a,b\n __repr__=basic_repr()\n\nrepr(SomeClass())\n\n\"SomeClass(a=1, b='foo')\"\n\n\n\nsource\n\n\n\n\n BasicRepr ()\n\nBase class for objects needing a basic __repr__\nAs a shortcut for creating a __repr__ for instance variables, you can inherit from BasicRepr:\n\nclass SomeClass(BasicRepr):\n def __init__(self, a=1, b='foo'): self.a,self.b = a,b\n\nrepr(SomeClass())\n\n\"SomeClass(a=1, b='foo')\"\n\n\n\nsource\n\n\n\n\n is_array (x)\n\nTrue if x supports __array__ or iloc\n\nis_array(np.array(1)),is_array([1])\n\n(True, False)\n\n\n\nsource\n\n\n\n\n listify (o=None, *rest, use_list=False, match=None)\n\nConvert o to a list\nConversion is designed to “do what you mean”, e.g:\n\ntest_eq(listify('hi'), ['hi'])\ntest_eq(listify(b'hi'), [b'hi'])\ntest_eq(listify(array(1)), [array(1)])\ntest_eq(listify(1), [1])\ntest_eq(listify([1,2]), [1,2])\ntest_eq(listify(range(3)), [0,1,2])\ntest_eq(listify(None), [])\ntest_eq(listify(1,2), [1,2])\n\n\narr = np.arange(9).reshape(3,3)\nlistify(arr)\n\n[array([[0, 1, 2],\n [3, 4, 5],\n [6, 7, 8]])]\n\n\n\nlistify(array([1,2]))\n\n[array([1, 2])]\n\n\nGenerators are turned into lists too:\n\ngen = (o for o in range(3))\ntest_eq(listify(gen), [0,1,2])\n\nUse match to provide a length to match:\n\ntest_eq(listify(1,match=3), [1,1,1])\n\nIf match is a sequence, it’s length is used:\n\ntest_eq(listify(1,match=range(3)), [1,1,1])\n\nIf the listified item is not of length 1, it must be the same length as match:\n\ntest_eq(listify([1,1,1],match=3), [1,1,1])\ntest_fail(lambda: listify([1,1],match=3))\n\n\nsource\n\n\n\n\n tuplify (o, use_list=False, match=None)\n\nMake o a tuple\n\ntest_eq(tuplify(None),())\ntest_eq(tuplify([1,2,3]),(1,2,3))\ntest_eq(tuplify(1,match=[1,2,3]),(1,1,1))\n\n\nsource\n\n\n\n\n true (x)\n\nTest whether x is truthy; collections with >0 elements are considered True\n\n[(o,true(o)) for o in\n (array(0),array(1),array([0]),array([0,1]),1,0,'',None)]\n\n[(array(0), False),\n (array(1), True),\n (array([0]), True),\n (array([0, 1]), True),\n (1, True),\n (0, False),\n ('', False),\n (None, False)]\n\n\n\nsource\n\n\n\n\n NullType ()\n\nAn object that is False and can be called, chained, and indexed\n\nbool(null.hi().there[3])\n\nFalse\n\n\n\nsource\n\n\n\n\n tonull (x)\n\nConvert None to null\n\nbool(tonull(None).hi().there[3])\n\nFalse\n\n\n\nsource\n\n\n\n\n get_class (nm, *fld_names, sup=None, doc=None, funcs=None, anno=None,\n **flds)\n\nDynamically create a class, optionally inheriting from sup, containing fld_names\n\n_t = get_class('_t', 'a', b=2, anno={'b':int})\nt = _t()\ntest_eq(t.a, None)\ntest_eq(t.b, 2)\nt = _t(1, b=3)\ntest_eq(t.a, 1)\ntest_eq(t.b, 3)\nt = _t(1, 3)\ntest_eq(t.a, 1)\ntest_eq(t.b, 3)\ntest_eq(t, pickle.loads(pickle.dumps(t)))\ntest_eq(_t.__annotations__, {'b':int, 'a':typing.Any})\nrepr(t)\n\n'__main__._t(a=1, b=3)'\n\n\nMost often you’ll want to call mk_class, since it adds the class to your module. See mk_class for more details and examples of use (which also apply to get_class).\n\nsource\n\n\n\n\n mk_class (nm, *fld_names, sup=None, doc=None, funcs=None, mod=None,\n anno=None, **flds)\n\nCreate a class using get_class and add to the caller’s module\nAny kwargs will be added as class attributes, and sup is an optional (tuple of) base classes.\n\nmk_class('_t', a=1, sup=dict)\nt = _t()\ntest_eq(t.a, 1)\nassert(isinstance(t,dict))\n\nA __init__ is provided that sets attrs for any kwargs, and for any args (matching by position to fields), along with a __repr__ which prints all attrs. The docstring is set to doc. You can pass funcs which will be added as attrs with the function names.\n\ndef foo(self): return 1\nmk_class('_t', 'a', sup=dict, doc='test doc', funcs=foo)\n\nt = _t(3, b=2)\ntest_eq(t.a, 3)\ntest_eq(t.b, 2)\ntest_eq(t.foo(), 1)\ntest_eq(t.__doc__, 'test doc')\nt\n\n{}\n\n\n\nsource\n\n\n\n\n wrap_class (nm, *fld_names, sup=None, doc=None, funcs=None, **flds)\n\nDecorator: makes function a method of a new class nm passing parameters to mk_class\n\n@wrap_class('_t', a=2)\ndef bar(self,x): return x+1\n\nt = _t()\ntest_eq(t.a, 2)\ntest_eq(t.bar(3), 4)\n\n\nsource\n\n\n\n ignore_exceptions ()\n\nContext manager to ignore exceptions\n\nwith ignore_exceptions(): \n # Exception will be ignored\n raise Exception\n\n\nsource\n\n\n\n\n\n exec_local (code, var_name)\n\nCall exec on code and return the var var_name\n\ntest_eq(exec_local(\"a=1\", \"a\"), 1)\n\n\nsource\n\n\n\n\n risinstance (types, obj=None)\n\nCurried isinstance but with args reversed\n\nassert risinstance(int, 1)\nassert not risinstance(str, 0)\nassert risinstance(int)(1)\nassert not risinstance(int)(None)\n\ntypes can also be strings:\n\nassert risinstance(('str','int'), 'a')\nassert risinstance('str', 'a')\nassert not risinstance('int', 'a')\n\n\nsource\n\n\n\n\n ver2tuple (v:str)\n\n\ntest_eq(ver2tuple('3.8.1'), (3,8,1))\ntest_eq(ver2tuple('3.1'), (3,1,0))\ntest_eq(ver2tuple('3.'), (3,0,0))\ntest_eq(ver2tuple('3'), (3,0,0))", + "crumbs": [ + "Basic functionality" + ] + }, + { + "objectID": "basics.html#noop", + "href": "basics.html#noop", + "title": "Basic functionality", + "section": "NoOp", + "text": "NoOp\nThese are used when you need a pass-through function.\n\n\nnoop\n\n noop (x=None, *args, **kwargs)\n\nDo nothing\n\nnoop()\ntest_eq(noop(1),1)\n\n\n\n\nnoops\n\n noops (x=None, *args, **kwargs)\n\nDo nothing (method)\n\nclass _t: foo=noops\ntest_eq(_t().foo(1),1)", + "crumbs": [ + "Basic functionality" + ] + }, + { + "objectID": "basics.html#infinite-lists", + "href": "basics.html#infinite-lists", + "title": "Basic functionality", + "section": "Infinite Lists", + "text": "Infinite Lists\nThese lists are useful for things like padding an array or adding index column(s) to arrays.\nInf defines the following properties:\n\ncount: itertools.count()\nzeros: itertools.cycle([0])\nones : itertools.cycle([1])\nnones: itertools.cycle([None])\n\n\ntest_eq([o for i,o in zip(range(5), Inf.count)],\n [0, 1, 2, 3, 4])\n\ntest_eq([o for i,o in zip(range(5), Inf.zeros)],\n [0]*5)\n\ntest_eq([o for i,o in zip(range(5), Inf.ones)],\n [1]*5)\n\ntest_eq([o for i,o in zip(range(5), Inf.nones)],\n [None]*5)", + "crumbs": [ + "Basic functionality" + ] + }, + { + "objectID": "basics.html#operator-functions", + "href": "basics.html#operator-functions", + "title": "Basic functionality", + "section": "Operator Functions", + "text": "Operator Functions\n\nsource\n\nin_\n\n in_ (x, a)\n\nTrue if x in a\n\n# test if element is in another\nassert in_('c', ('b', 'c', 'a'))\nassert in_(4, [2,3,4,5])\nassert in_('t', 'fastai')\ntest_fail(in_('h', 'fastai'))\n\n# use in_ as a partial\nassert in_('fastai')('t')\nassert in_([2,3,4,5])(4)\ntest_fail(in_('fastai')('h'))\n\nIn addition to in_, the following functions are provided matching the behavior of the equivalent versions in operator: lt gt le ge eq ne add sub mul truediv is_ is_not mod.\n\nlt(3,5),gt(3,5),is_(None,None),in_(0,[1,2]),mod(3,2)\n\n(True, False, True, False, 1)\n\n\nSimilarly to _in, they also have additional functionality: if you only pass one param, they return a partial function that passes that param as the second positional parameter.\n\nlt(5)(3),gt(5)(3),is_(None)(None),in_([1,2])(0),mod(2)(3)\n\n(True, False, True, False, 1)\n\n\n\nsource\n\n\nret_true\n\n ret_true (*args, **kwargs)\n\nPredicate: always True\n\nassert ret_true(1,2,3)\nassert ret_true(False)\n\n\nsource\n\n\nret_false\n\n ret_false (*args, **kwargs)\n\nPredicate: always False\n\nsource\n\n\nstop\n\n stop (e=<class 'StopIteration'>)\n\nRaises exception e (by default StopIteration)\n\nsource\n\n\ngen\n\n gen (func, seq, cond=<function ret_true>)\n\nLike (func(o) for o in seq if cond(func(o))) but handles StopIteration\n\ntest_eq(gen(noop, Inf.count, lt(5)),\n range(5))\ntest_eq(gen(operator.neg, Inf.count, gt(-5)),\n [0,-1,-2,-3,-4])\ntest_eq(gen(lambda o:o if o<5 else stop(), Inf.count),\n range(5))\n\n\nsource\n\n\nchunked\n\n chunked (it, chunk_sz=None, drop_last=False, n_chunks=None)\n\nReturn batches from iterator it of size chunk_sz (or return n_chunks total)\nNote that you must pass either chunk_sz, or n_chunks, but not both.\n\nt = list(range(10))\ntest_eq(chunked(t,3), [[0,1,2], [3,4,5], [6,7,8], [9]])\ntest_eq(chunked(t,3,True), [[0,1,2], [3,4,5], [6,7,8], ])\n\nt = map(lambda o:stop() if o==6 else o, Inf.count)\ntest_eq(chunked(t,3), [[0, 1, 2], [3, 4, 5]])\nt = map(lambda o:stop() if o==7 else o, Inf.count)\ntest_eq(chunked(t,3), [[0, 1, 2], [3, 4, 5], [6]])\n\nt = np.arange(10)\ntest_eq(chunked(t,3), [[0,1,2], [3,4,5], [6,7,8], [9]])\ntest_eq(chunked(t,3,True), [[0,1,2], [3,4,5], [6,7,8], ])\n\ntest_eq(chunked([], 3), [])\ntest_eq(chunked([], n_chunks=3), [])\n\n\nsource\n\n\notherwise\n\n otherwise (x, tst, y)\n\ny if tst(x) else x\n\ntest_eq(otherwise(2+1, gt(3), 4), 3)\ntest_eq(otherwise(2+1, gt(2), 4), 4)", + "crumbs": [ + "Basic functionality" + ] + }, + { + "objectID": "basics.html#attribute-helpers", + "href": "basics.html#attribute-helpers", + "title": "Basic functionality", + "section": "Attribute Helpers", + "text": "Attribute Helpers\nThese functions reduce boilerplate when setting or manipulating attributes or properties of objects.\n\nsource\n\ncustom_dir\n\n custom_dir (c, add)\n\nImplement custom __dir__, adding add to cls\ncustom_dir allows you extract the __dict__ property of a class and appends the list add to it.\n\nclass _T: \n def f(): pass\n\ns = custom_dir(_T(), add=['foo', 'bar'])\nassert {'foo', 'bar', 'f'}.issubset(s)\n\n\nsource\n\n\nAttrDict\ndict subclass that also provides access to keys as attrs\n\nd = AttrDict(a=1,b=\"two\")\ntest_eq(d.a, 1)\ntest_eq(d['b'], 'two')\ntest_eq(d.get('c','nope'), 'nope')\nd.b = 2\ntest_eq(d.b, 2)\ntest_eq(d['b'], 2)\nd['b'] = 3\ntest_eq(d['b'], 3)\ntest_eq(d.b, 3)\nassert 'a' in dir(d)\n\nAttrDict will pretty print in Jupyter Notebooks:\n\n_test_dict = {'a':1, 'b': {'c':1, 'd':2}, 'c': {'c':1, 'd':2}, 'd': {'c':1, 'd':2},\n 'e': {'c':1, 'd':2}, 'f': {'c':1, 'd':2, 'e': 4, 'f':[1,2,3,4,5]}}\nAttrDict(_test_dict)\n\n{ 'a': 1,\n 'b': {'c': 1, 'd': 2},\n 'c': {'c': 1, 'd': 2},\n 'd': {'c': 1, 'd': 2},\n 'e': {'c': 1, 'd': 2},\n 'f': {'c': 1, 'd': 2, 'e': 4, 'f': [1, 2, 3, 4, 5]}}\n\n\n\nsource\n\n\nAttrDictDefault\n\n AttrDictDefault (*args, default_=None, **kwargs)\n\nAttrDict subclass that returns None for missing attrs\n\nd = AttrDictDefault(a=1,b=\"two\", default_='nope')\ntest_eq(d.a, 1)\ntest_eq(d['b'], 'two')\ntest_eq(d.c, 'nope')\n\n\nsource\n\n\nNS\nSimpleNamespace subclass that also adds iter and dict support\nThis is very similar to AttrDict, but since it starts with SimpleNamespace, it has some differences in behavior. You can use it just like SimpleNamespace:\n\nd = NS(**_test_dict)\nd\n\nnamespace(a=1,\n b={'c': 1, 'd': 2},\n c={'c': 1, 'd': 2},\n d={'c': 1, 'd': 2},\n e={'c': 1, 'd': 2},\n f={'c': 1, 'd': 2, 'e': 4, 'f': [1, 2, 3, 4, 5]})\n\n\n…but you can also index it to get/set:\n\nd['a']\n\n1\n\n\n…and iterate t:\n\nlist(d)\n\n['a', 'b', 'c', 'd', 'e', 'f']\n\n\n\nsource\n\n\nget_annotations_ex\n\n get_annotations_ex (obj, globals=None, locals=None)\n\nBackport of py3.10 get_annotations that returns globals/locals\nIn Python 3.10 inspect.get_annotations was added. However previous versions of Python are unable to evaluate type annotations correctly if from future import __annotations__ is used. Furthermore, all annotations are evaluated, even if only some subset are needed. get_annotations_ex provides the same functionality as inspect.get_annotations, but works on earlier versions of Python, and returns the globals and locals needed to evaluate types.\n\nsource\n\n\neval_type\n\n eval_type (t, glb, loc)\n\neval a type or collection of types, if needed, for annotations in py3.10+\nIn py3.10, or if from future import __annotations__ is used, a is a str:\n\nclass _T2a: pass\ndef func(a: _T2a): pass\nann,glb,loc = get_annotations_ex(func)\n\neval_type(ann['a'], glb, loc)\n\n__main__._T2a\n\n\n| is supported for defining Union types when using eval_type even for python versions prior to 3.9:\n\nclass _T2b: pass\ndef func(a: _T2a|_T2b): pass\nann,glb,loc = get_annotations_ex(func)\n\neval_type(ann['a'], glb, loc)\n\ntyping.Union[__main__._T2a, __main__._T2b]\n\n\n\nsource\n\n\ntype_hints\n\n type_hints (f)\n\nLike typing.get_type_hints but returns {} if not allowed type\nBelow is a list of allowed types for type hints in python:\n\nlist(typing._allowed_types)\n\n[function,\n builtin_function_or_method,\n method,\n module,\n wrapper_descriptor,\n method-wrapper,\n method_descriptor]\n\n\nFor example, type func is allowed so type_hints returns the same value as typing.get_hints:\n\ndef f(a:int)->bool: ... # a function with type hints (allowed)\nexp = {'a':int,'return':bool}\ntest_eq(type_hints(f), typing.get_type_hints(f))\ntest_eq(type_hints(f), exp)\n\nHowever, class is not an allowed type, so type_hints returns {}:\n\nclass _T:\n def __init__(self, a:int=0)->bool: ...\nassert not type_hints(_T)\n\n\nsource\n\n\nannotations\n\n annotations (o)\n\nAnnotations for o, or type(o)\nThis supports a wider range of situations than type_hints, by checking type() and __init__ for annotations too:\n\nfor o in _T,_T(),_T.__init__,f: test_eq(annotations(o), exp)\nassert not annotations(int)\nassert not annotations(print)\n\n\nsource\n\n\nanno_ret\n\n anno_ret (func)\n\nGet the return annotation of func\n\ndef f(x) -> float: return x\ntest_eq(anno_ret(f), float)\n\ndef f(x) -> typing.Tuple[float,float]: return x\nassert anno_ret(f)==typing.Tuple[float,float]\n\nIf your return annotation is None, anno_ret will return NoneType (and not None):\n\ndef f(x) -> None: return x\n\ntest_eq(anno_ret(f), NoneType)\nassert anno_ret(f) is not None # returns NoneType instead of None\n\nIf your function does not have a return type, or if you pass in None instead of a function, then anno_ret returns None:\n\ndef f(x): return x\n\ntest_eq(anno_ret(f), None)\ntest_eq(anno_ret(None), None) # instead of passing in a func, pass in None\n\n\nsource\n\n\nsignature_ex\n\n signature_ex (obj, eval_str:bool=False)\n\nBackport of inspect.signature(..., eval_str=True to <py310\n\nsource\n\n\nunion2tuple\n\n union2tuple (t)\n\n\ntest_eq(union2tuple(Union[int,str]), (int,str))\ntest_eq(union2tuple(int), int)\nassert union2tuple(Tuple[int,str])==Tuple[int,str]\ntest_eq(union2tuple((int,str)), (int,str))\nif UnionType: test_eq(union2tuple(int|str), (int,str))\n\n\nsource\n\n\nargnames\n\n argnames (f, frame=False)\n\nNames of arguments to function or frame f\n\ntest_eq(argnames(f), ['x'])\n\n\nsource\n\n\nwith_cast\n\n with_cast (f)\n\nDecorator which uses any parameter annotations as preprocessing functions\n\n@with_cast\ndef _f(a, b:Path, c:str='', d=0): return (a,b,c,d)\n\ntest_eq(_f(1, '.', 3), (1,Path('.'),'3',0))\ntest_eq(_f(1, '.'), (1,Path('.'),'',0))\n\n@with_cast\ndef _g(a:int=0)->str: return a\n\ntest_eq(_g(4.0), '4')\ntest_eq(_g(4.4), '4')\ntest_eq(_g(2), '2')\n\n\nsource\n\n\nstore_attr\n\n store_attr (names=None, but='', cast=False, store_args=None, **attrs)\n\nStore params named in comma-separated names from calling context into attrs in self\nIn it’s most basic form, you can use store_attr to shorten code like this:\n\nclass T:\n def __init__(self, a,b,c): self.a,self.b,self.c = a,b,c\n\n…to this:\n\nclass T:\n def __init__(self, a,b,c): store_attr('a,b,c', self)\n\nThis class behaves as if we’d used the first form:\n\nt = T(1,c=2,b=3)\nassert t.a==1 and t.b==3 and t.c==2\n\n\nclass T1:\n def __init__(self, a,b,c): store_attr()\n\nIn addition, it stores the attrs as a dict in __stored_args__, which you can use for display, logging, and so forth.\n\ntest_eq(t.__stored_args__, {'a':1, 'b':3, 'c':2})\n\nSince you normally want to use the first argument (often called self) for storing attributes, it’s optional:\n\nclass T:\n def __init__(self, a,b,c:str): store_attr('a,b,c')\n\nt = T(1,c=2,b=3)\nassert t.a==1 and t.b==3 and t.c==2\n\nWith cast=True any parameter annotations will be used as preprocessing functions for the corresponding arguments:\n\nclass T:\n def __init__(self, a:listify, b, c:str): store_attr('a,b,c', cast=True)\n\nt = T(1,c=2,b=3)\nassert t.a==[1] and t.b==3 and t.c=='2'\n\nYou can inherit from a class using store_attr, and just call it again to add in any new attributes added in the derived class:\n\nclass T2(T):\n def __init__(self, d, **kwargs):\n super().__init__(**kwargs)\n store_attr('d')\n\nt = T2(d=1,a=2,b=3,c=4)\nassert t.a==2 and t.b==3 and t.c==4 and t.d==1\n\nYou can skip passing a list of attrs to store. In this case, all arguments passed to the method are stored:\n\nclass T:\n def __init__(self, a,b,c): store_attr()\n\nt = T(1,c=2,b=3)\nassert t.a==1 and t.b==3 and t.c==2\n\n\nclass T4(T):\n def __init__(self, d, **kwargs):\n super().__init__(**kwargs)\n store_attr()\n\nt = T4(4, a=1,c=2,b=3)\nassert t.a==1 and t.b==3 and t.c==2 and t.d==4\n\n\nclass T4:\n def __init__(self, *, a: int, b: float = 1):\n store_attr()\n \nt = T4(a=3)\nassert t.a==3 and t.b==1\nt = T4(a=3, b=2)\nassert t.a==3 and t.b==2\n\nYou can skip some attrs by passing but:\n\nclass T:\n def __init__(self, a,b,c): store_attr(but='a')\n\nt = T(1,c=2,b=3)\nassert t.b==3 and t.c==2\nassert not hasattr(t,'a')\n\nYou can also pass keywords to store_attr, which is identical to setting the attrs directly, but also stores them in __stored_args__.\n\nclass T:\n def __init__(self): store_attr(a=1)\n\nt = T()\nassert t.a==1\n\nYou can also use store_attr inside functions.\n\ndef create_T(a, b):\n t = SimpleNamespace()\n store_attr(self=t)\n return t\n\nt = create_T(a=1, b=2)\nassert t.a==1 and t.b==2\n\n\nsource\n\n\nattrdict\n\n attrdict (o, *ks, default=None)\n\nDict from each k in ks to getattr(o,k)\n\nclass T:\n def __init__(self, a,b,c): store_attr()\n\nt = T(1,c=2,b=3)\ntest_eq(attrdict(t,'b','c'), {'b':3, 'c':2})\n\n\nsource\n\n\nproperties\n\n properties (cls, *ps)\n\nChange attrs in cls with names in ps to properties\n\nclass T:\n def a(self): return 1\n def b(self): return 2\nproperties(T,'a')\n\ntest_eq(T().a,1)\ntest_eq(T().b(),2)\n\n\nsource\n\n\ncamel2words\n\n camel2words (s, space=' ')\n\nConvert CamelCase to ‘spaced words’\n\ntest_eq(camel2words('ClassAreCamel'), 'Class Are Camel')\n\n\nsource\n\n\ncamel2snake\n\n camel2snake (name)\n\nConvert CamelCase to snake_case\n\ntest_eq(camel2snake('ClassAreCamel'), 'class_are_camel')\ntest_eq(camel2snake('Already_Snake'), 'already__snake')\n\n\nsource\n\n\nsnake2camel\n\n snake2camel (s)\n\nConvert snake_case to CamelCase\n\ntest_eq(snake2camel('a_b_cc'), 'ABCc')\n\n\nsource\n\n\nclass2attr\n\n class2attr (cls_name)\n\nReturn the snake-cased name of the class; strip ending cls_name if it exists.\n\nclass Parent:\n @property\n def name(self): return class2attr(self, 'Parent')\n\nclass ChildOfParent(Parent): pass\nclass ParentChildOf(Parent): pass\n\np = Parent()\ncp = ChildOfParent()\ncp2 = ParentChildOf()\n\ntest_eq(p.name, 'parent')\ntest_eq(cp.name, 'child_of')\ntest_eq(cp2.name, 'parent_child_of')\n\n\nsource\n\n\ngetcallable\n\n getcallable (o, attr)\n\nCalls getattr with a default of noop\n\nclass Math:\n def addition(self,a,b): return a+b\n\nm = Math()\n\ntest_eq(getcallable(m, \"addition\")(a=1,b=2), 3)\ntest_eq(getcallable(m, \"subtraction\")(a=1,b=2), None)\n\n\nsource\n\n\ngetattrs\n\n getattrs (o, *attrs, default=None)\n\nList of all attrs in o\n\nfrom fractions import Fraction\n\n\ngetattrs(Fraction(1,2), 'numerator', 'denominator')\n\n[1, 2]\n\n\n\nsource\n\n\nhasattrs\n\n hasattrs (o, attrs)\n\nTest whether o contains all attrs\n\nassert hasattrs(1,('imag','real'))\nassert not hasattrs(1,('imag','foo'))\n\n\nsource\n\n\nsetattrs\n\n setattrs (dest, flds, src)\n\n\nd = dict(a=1,bb=\"2\",ignore=3)\no = SimpleNamespace()\nsetattrs(o, \"a,bb\", d)\ntest_eq(o.a, 1)\ntest_eq(o.bb, \"2\")\n\n\nd = SimpleNamespace(a=1,bb=\"2\",ignore=3)\no = SimpleNamespace()\nsetattrs(o, \"a,bb\", d)\ntest_eq(o.a, 1)\ntest_eq(o.bb, \"2\")\n\n\nsource\n\n\ntry_attrs\n\n try_attrs (obj, *attrs)\n\nReturn first attr that exists in obj\n\ntest_eq(try_attrs(1, 'real'), 1)\ntest_eq(try_attrs(1, 'foobar', 'real'), 1)", + "crumbs": [ + "Basic functionality" + ] + }, + { + "objectID": "basics.html#attribute-delegation", + "href": "basics.html#attribute-delegation", + "title": "Basic functionality", + "section": "Attribute Delegation", + "text": "Attribute Delegation\n\nsource\n\nGetAttrBase\n\n GetAttrBase ()\n\nBasic delegation of __getattr__ and __dir__\n\nsource\n\nGetAttr\n\n GetAttr ()\n\nInherit from this to have all attr accesses in self._xtra passed down to self.default\nInherit from GetAttr to have attr access passed down to an instance attribute. This makes it easy to create composites that don’t require callers to know about their components. For a more detailed discussion of how this works as well as relevant context, we suggest reading the delegated composition section of this blog article.\nYou can customise the behaviour of GetAttr in subclasses via; - _default - By default, this is set to 'default', so attr access is passed down to self.default - _default can be set to the name of any instance attribute that does not start with dunder __ - _xtra - By default, this is None, so all attr access is passed down - You can limit which attrs get passed down by setting _xtra to a list of attribute names\nTo illuminate the utility of GetAttr, suppose we have the following two classes, _WebPage which is a superclass of _ProductPage, which we wish to compose like so:\n\nclass _WebPage:\n def __init__(self, title, author=\"Jeremy\"):\n self.title,self.author = title,author\n\nclass _ProductPage:\n def __init__(self, page, price): self.page,self.price = page,price\n \npage = _WebPage('Soap', author=\"Sylvain\")\np = _ProductPage(page, 15.0)\n\nHow do we make it so we can just write p.author, instead of p.page.author to access the author attribute? We can use GetAttr, of course! First, we subclass GetAttr when defining _ProductPage. Next, we set self.default to the object whose attributes we want to be able to access directly, which in this case is the page argument passed on initialization:\n\nclass _ProductPage(GetAttr):\n def __init__(self, page, price): self.default,self.price = page,price #self.default allows you to access page directly.\n\np = _ProductPage(page, 15.0)\n\nNow, we can access the author attribute directly from the instance:\n\ntest_eq(p.author, 'Sylvain')\n\nIf you wish to store the object you are composing in an attribute other than self.default, you can set the class attribute _data as shown below. This is useful in the case where you might have a name collision with self.default:\n\nclass _C(GetAttr):\n _default = '_data' # use different component name; `self._data` rather than `self.default`\n def __init__(self,a): self._data = a\n def foo(self): noop\n\nt = _C('Hi')\ntest_eq(t._data, 'Hi') \ntest_fail(lambda: t.default) # we no longer have self.default\ntest_eq(t.lower(), 'hi')\ntest_eq(t.upper(), 'HI')\nassert 'lower' in dir(t)\nassert 'upper' in dir(t)\n\nBy default, all attributes and methods of the object you are composing are retained. In the below example, we compose a str object with the class _C. This allows us to directly call string methods on instances of class _C, such as str.lower() or str.upper():\n\nclass _C(GetAttr):\n # allow all attributes and methods to get passed to `self.default` (by leaving _xtra=None)\n def __init__(self,a): self.default = a\n def foo(self): noop\n\nt = _C('Hi')\ntest_eq(t.lower(), 'hi')\ntest_eq(t.upper(), 'HI')\nassert 'lower' in dir(t)\nassert 'upper' in dir(t)\n\nHowever, you can choose which attributes or methods to retain by defining a class attribute _xtra, which is a list of allowed attribute and method names to delegate. In the below example, we only delegate the lower method from the composed str object when defining class _C:\n\nclass _C(GetAttr):\n _xtra = ['lower'] # specify which attributes get passed to `self.default`\n def __init__(self,a): self.default = a\n def foo(self): noop\n\nt = _C('Hi')\ntest_eq(t.default, 'Hi')\ntest_eq(t.lower(), 'hi')\ntest_fail(lambda: t.upper()) # upper wasn't in _xtra, so it isn't available to be called\nassert 'lower' in dir(t)\nassert 'upper' not in dir(t)\n\nYou must be careful to properly set an instance attribute in __init__ that corresponds to the class attribute _default. The below example sets the class attribute _default to data, but erroneously fails to define self.data (and instead defines self.default).\nFailing to properly set instance attributes leads to errors when you try to access methods directly:\n\nclass _C(GetAttr):\n _default = 'data' # use a bad component name; i.e. self.data does not exist\n def __init__(self,a): self.default = a\n def foo(self): noop\n \n# TODO: should we raise an error when we create a new instance ...\nt = _C('Hi')\ntest_eq(t.default, 'Hi')\n# ... or is it enough for all GetAttr features to raise errors\ntest_fail(lambda: t.data)\ntest_fail(lambda: t.lower())\ntest_fail(lambda: t.upper())\ntest_fail(lambda: dir(t))\n\n\nsource\n\n\n\ndelegate_attr\n\n delegate_attr (k, to)\n\nUse in __getattr__ to delegate to attr to without inheriting from GetAttr\ndelegate_attr is a functional way to delegate attributes, and is an alternative to GetAttr. We recommend reading the documentation of GetAttr for more details around delegation.\nYou can use achieve delegation when you define __getattr__ by using delegate_attr:\n\nclass _C:\n def __init__(self, o): self.o = o # self.o corresponds to the `to` argument in delegate_attr.\n def __getattr__(self, k): return delegate_attr(self, k, to='o')\n \n\nt = _C('HELLO') # delegates to a string\ntest_eq(t.lower(), 'hello')\n\nt = _C(np.array([5,4,3])) # delegates to a numpy array\ntest_eq(t.sum(), 12)\n\nt = _C(pd.DataFrame({'a': [1,2], 'b': [3,4]})) # delegates to a pandas.DataFrame\ntest_eq(t.b.max(), 4)", + "crumbs": [ + "Basic functionality" + ] + }, + { + "objectID": "basics.html#extensible-types", + "href": "basics.html#extensible-types", + "title": "Basic functionality", + "section": "Extensible Types", + "text": "Extensible Types\nShowPrint is a base class that defines a show method, which is used primarily for callbacks in fastai that expect this method to be defined.\nInt, Float, and Str extend int, float and str respectively by adding an additional show method by inheriting from ShowPrint.\nThe code for Int is shown below:\nExamples:\n\nInt(0).show()\nFloat(2.0).show()\nStr('Hello').show()\n\n0\n2.0\nHello", + "crumbs": [ + "Basic functionality" + ] + }, + { + "objectID": "basics.html#collection-functions", + "href": "basics.html#collection-functions", + "title": "Basic functionality", + "section": "Collection functions", + "text": "Collection functions\nFunctions that manipulate popular python collections.\n\nsource\n\npartition\n\n partition (coll, f)\n\nPartition a collection by a predicate\n\nts,fs = partition(range(10), mod(2))\ntest_eq(fs, [0,2,4,6,8])\ntest_eq(ts, [1,3,5,7,9])\n\n\nsource\n\n\nflatten\n\n flatten (o)\n\nConcatenate all collections and items as a generator\n\nsource\n\n\nconcat\n\n concat (colls)\n\nConcatenate all collections and items as a list\n\nconcat([(o for o in range(2)),[2,3,4], 5])\n\n[0, 1, 2, 3, 4, 5]\n\n\n\nconcat([[\"abc\", \"xyz\"], [\"foo\", \"bar\"]])\n\n['abc', 'xyz', 'foo', 'bar']\n\n\n\nsource\n\n\nstrcat\n\n strcat (its, sep:str='')\n\nConcatenate stringified items its\n\ntest_eq(strcat(['a',2]), 'a2')\ntest_eq(strcat(['a',2], ';'), 'a;2')\n\n\nsource\n\n\ndetuplify\n\n detuplify (x)\n\nIf x is a tuple with one thing, extract it\n\ntest_eq(detuplify(()),None)\ntest_eq(detuplify([1]),1)\ntest_eq(detuplify([1,2]), [1,2])\ntest_eq(detuplify(np.array([[1,2]])), np.array([[1,2]]))\n\n\nsource\n\n\nreplicate\n\n replicate (item, match)\n\nCreate tuple of item copied len(match) times\n\nt = [1,1]\ntest_eq(replicate([1,2], t),([1,2],[1,2]))\ntest_eq(replicate(1, t),(1,1))\n\n\nsource\n\n\nsetify\n\n setify (o)\n\nTurn any list like-object into a set.\n\n# test\ntest_eq(setify(None),set())\ntest_eq(setify('abc'),{'abc'})\ntest_eq(setify([1,2,2]),{1,2})\ntest_eq(setify(range(0,3)),{0,1,2})\ntest_eq(setify({1,2}),{1,2})\n\n\nsource\n\n\nmerge\n\n merge (*ds)\n\nMerge all dictionaries in ds\n\ntest_eq(merge(), {})\ntest_eq(merge(dict(a=1,b=2)), dict(a=1,b=2))\ntest_eq(merge(dict(a=1,b=2), dict(b=3,c=4), None), dict(a=1, b=3, c=4))\n\n\nsource\n\n\nrange_of\n\n range_of (x)\n\nAll indices of collection x (i.e. list(range(len(x))))\n\ntest_eq(range_of([1,1,1,1]), [0,1,2,3])\n\n\nsource\n\n\ngroupby\n\n groupby (x, key, val=<function noop>)\n\nLike itertools.groupby but doesn’t need to be sorted, and isn’t lazy, plus some extensions\n\ntest_eq(groupby('aa ab bb'.split(), itemgetter(0)), {'a':['aa','ab'], 'b':['bb']})\n\nHere’s an example of how to invert a grouping, using an int as key (which uses itemgetter; passing a str will use attrgetter), and using a val function:\n\nd = {0: [1, 3, 7], 2: [3], 3: [5], 4: [8], 5: [4], 7: [5]}\ngroupby(((o,k) for k,v in d.items() for o in v), 0, 1)\n\n{1: [0], 3: [0, 2], 7: [0], 5: [3, 7], 8: [4], 4: [5]}\n\n\n\nsource\n\n\nlast_index\n\n last_index (x, o)\n\nFinds the last index of occurence of x in o (returns -1 if no occurence)\n\ntest_eq(last_index(9, [1, 2, 9, 3, 4, 9, 10]), 5)\ntest_eq(last_index(6, [1, 2, 9, 3, 4, 9, 10]), -1)\n\n\nsource\n\n\nfilter_dict\n\n filter_dict (d, func)\n\nFilter a dict using func, applied to keys and values\n\nletters = {o:chr(o) for o in range(65,73)}\nletters\n\n{65: 'A', 66: 'B', 67: 'C', 68: 'D', 69: 'E', 70: 'F', 71: 'G', 72: 'H'}\n\n\n\nfilter_dict(letters, lambda k,v: k<67 or v in 'FG')\n\n{65: 'A', 66: 'B', 70: 'F', 71: 'G'}\n\n\n\nsource\n\n\nfilter_keys\n\n filter_keys (d, func)\n\nFilter a dict using func, applied to keys\n\nfilter_keys(letters, lt(67))\n\n{65: 'A', 66: 'B'}\n\n\n\nsource\n\n\nfilter_values\n\n filter_values (d, func)\n\nFilter a dict using func, applied to values\n\nfilter_values(letters, in_('FG'))\n\n{70: 'F', 71: 'G'}\n\n\n\nsource\n\n\ncycle\n\n cycle (o)\n\nLike itertools.cycle except creates list of Nones if o is empty\n\ntest_eq(itertools.islice(cycle([1,2,3]),5), [1,2,3,1,2])\ntest_eq(itertools.islice(cycle([]),3), [None]*3)\ntest_eq(itertools.islice(cycle(None),3), [None]*3)\ntest_eq(itertools.islice(cycle(1),3), [1,1,1])\n\n\nsource\n\n\nzip_cycle\n\n zip_cycle (x, *args)\n\nLike itertools.zip_longest but cycles through elements of all but first argument\n\ntest_eq(zip_cycle([1,2,3,4],list('abc')), [(1, 'a'), (2, 'b'), (3, 'c'), (4, 'a')])\n\n\nsource\n\n\nsorted_ex\n\n sorted_ex (iterable, key=None, reverse=False)\n\nLike sorted, but if key is str use attrgetter; if int use itemgetter\n\nsource\n\n\nnot_\n\n not_ (f)\n\nCreate new function that negates result of f\n\ndef f(a): return a>0\ntest_eq(f(1),True)\ntest_eq(not_(f)(1),False)\ntest_eq(not_(f)(a=-1),True)\n\n\nsource\n\n\nargwhere\n\n argwhere (iterable, f, negate=False, **kwargs)\n\nLike filter_ex, but return indices for matching items\n\nsource\n\n\nfilter_ex\n\n filter_ex (iterable, f=<function noop>, negate=False, gen=False,\n **kwargs)\n\nLike filter, but passing kwargs to f, defaulting f to noop, and adding negate and gen\n\nsource\n\n\nrange_of\n\n range_of (a, b=None, step=None)\n\nAll indices of collection a, if a is a collection, otherwise range\n\ntest_eq(range_of([1,1,1,1]), [0,1,2,3])\ntest_eq(range_of(4), [0,1,2,3])\n\n\nsource\n\n\nrenumerate\n\n renumerate (iterable, start=0)\n\nSame as enumerate, but returns index as 2nd element instead of 1st\n\ntest_eq(renumerate('abc'), (('a',0),('b',1),('c',2)))\n\n\nsource\n\n\nfirst\n\n first (x, f=None, negate=False, **kwargs)\n\nFirst element of x, optionally filtered by f, or None if missing\n\ntest_eq(first(['a', 'b', 'c', 'd', 'e']), 'a')\ntest_eq(first([False]), False)\ntest_eq(first([False], noop), None)\n\n\nsource\n\n\nonly\n\n only (o)\n\nReturn the only item of o, raise if o doesn’t have exactly one item\n\nsource\n\n\nnested_attr\n\n nested_attr (o, attr, default=None)\n\nSame as getattr, but if attr includes a ., then looks inside nested objects\n\nclass CustomIndexable:\n def __init__(self): self.data = {'a':1,'b':'v','c':{'d':5}}\n def __getitem__(self, key): return self.data[key]\n\ncustom_indexable = CustomIndexable()\ntest_eq(nested_attr(custom_indexable,'a'),1)\ntest_eq(nested_attr(custom_indexable,'c.d'),5)\ntest_eq(nested_attr(custom_indexable,'e'),None)\n\nclass TestObj: def init(self): self.nested = {‘key’: [1, 2, {‘inner’: ‘value’}]} test_obj = TestObj()\ntest_eq(nested_attr(test_obj, ‘nested.key.2.inner’),‘value’) test_eq(nested_attr([1, 2, 3], ‘1’),2)\n\nb = {'a':1,'b':'v','c':{'d':5}}\ntest_eq(nested_attr(b,'b'),'v')\ntest_eq(nested_attr(b,'c.d'),5)\n\n\na = SimpleNamespace(b=(SimpleNamespace(c=1)))\ntest_eq(nested_attr(a, 'b.c'), getattr(getattr(a, 'b'), 'c'))\ntest_eq(nested_attr(a, 'b.d'), None)\ntest_eq(nested_attr(b, 'a'), 1)\n\n\nsource\n\n\nnested_setdefault\n\n nested_setdefault (o, attr, default)\n\nSame as setdefault, but if attr includes a ., then looks inside nested objects\n\nsource\n\n\nnested_callable\n\n nested_callable (o, attr)\n\nSame as nested_attr but if not found will return noop\n\na = SimpleNamespace(b=(SimpleNamespace(c=1)))\ntest_eq(nested_callable(a, 'b.c'), getattr(getattr(a, 'b'), 'c'))\ntest_eq(nested_callable(a, 'b.d'), noop)\n\n\nsource\n\n\nnested_idx\n\n nested_idx (coll, *idxs)\n\nIndex into nested collections, dicts, etc, with idxs\n\na = {'b':[1,{'c':2}]}\ntest_eq(nested_idx(a, 'nope'), None)\ntest_eq(nested_idx(a, 'nope', 'nup'), None)\ntest_eq(nested_idx(a, 'b', 3), None)\ntest_eq(nested_idx(a), a)\ntest_eq(nested_idx(a, 'b'), [1,{'c':2}])\ntest_eq(nested_idx(a, 'b', 1), {'c':2})\ntest_eq(nested_idx(a, 'b', 1, 'c'), 2)\n\n\na = SimpleNamespace(b=[1,{'c':2}])\ntest_eq(nested_idx(a, 'nope'), None)\ntest_eq(nested_idx(a, 'nope', 'nup'), None)\ntest_eq(nested_idx(a, 'b', 3), None)\ntest_eq(nested_idx(a), a)\ntest_eq(nested_idx(a, 'b'), [1,{'c':2}])\ntest_eq(nested_idx(a, 'b', 1), {'c':2})\ntest_eq(nested_idx(a, 'b', 1, 'c'), 2)\n\n\nsource\n\n\nset_nested_idx\n\n set_nested_idx (coll, value, *idxs)\n\nSet value indexed like `nested_idx\n\nset_nested_idx(a, 3, 'b', 0)\ntest_eq(nested_idx(a, 'b', 0), 3)\n\n\nsource\n\n\nval2idx\n\n val2idx (x)\n\nDict from value to index\n\ntest_eq(val2idx([1,2,3]), {3:2,1:0,2:1})\n\n\nsource\n\n\nuniqueify\n\n uniqueify (x, sort=False, bidir=False, start=None)\n\nUnique elements in x, optional sort, optional return reverse correspondence, optional prepend with elements.\n\nt = [1,1,0,5,0,3]\ntest_eq(uniqueify(t),[1,0,5,3])\ntest_eq(uniqueify(t, sort=True),[0,1,3,5])\ntest_eq(uniqueify(t, start=[7,8,6]), [7,8,6,1,0,5,3])\nv,o = uniqueify(t, bidir=True)\ntest_eq(v,[1,0,5,3])\ntest_eq(o,{1:0, 0: 1, 5: 2, 3: 3})\nv,o = uniqueify(t, sort=True, bidir=True)\ntest_eq(v,[0,1,3,5])\ntest_eq(o,{0:0, 1: 1, 3: 2, 5: 3})\n\n\nsource\n\n\nloop_first_last\n\n loop_first_last (values)\n\nIterate and generate a tuple with a flag for first and last value.\n\ntest_eq(loop_first_last(range(3)), [(True,False,0), (False,False,1), (False,True,2)])\n\n\nsource\n\n\nloop_first\n\n loop_first (values)\n\nIterate and generate a tuple with a flag for first value.\n\ntest_eq(loop_first(range(3)), [(True,0), (False,1), (False,2)])\n\n\nsource\n\n\nloop_last\n\n loop_last (values)\n\nIterate and generate a tuple with a flag for last value.\n\ntest_eq(loop_last(range(3)), [(False,0), (False,1), (True,2)])\n\n\nsource\n\n\nfirst_match\n\n first_match (lst, f, default=None)\n\nFirst element of lst matching predicate f, or default if none\n\na = [0,2,4,5,6,7,10]\ntest_eq(first_match(a, lambda o:o%2), 3)\n\n\nsource\n\n\nlast_match\n\n last_match (lst, f, default=None)\n\nLast element of lst matching predicate f, or default if none\n\ntest_eq(last_match(a, lambda o:o%2), 5)", + "crumbs": [ + "Basic functionality" + ] + }, + { + "objectID": "basics.html#fastuple", + "href": "basics.html#fastuple", + "title": "Basic functionality", + "section": "fastuple", + "text": "fastuple\nA tuple with extended functionality.\n\nsource\n\nfastuple\n\n fastuple (x=None, *rest)\n\nA tuple with elementwise ops and more friendly init behavior\n\n\nFriendly init behavior\nCommon failure modes when trying to initialize a tuple in python:\ntuple(3)\n> TypeError: 'int' object is not iterable\nor\ntuple(3, 4)\n> TypeError: tuple expected at most 1 arguments, got 2\nHowever, fastuple allows you to define tuples like this and in the usual way:\n\ntest_eq(fastuple(3), (3,))\ntest_eq(fastuple(3,4), (3, 4))\ntest_eq(fastuple((3,4)), (3, 4))\n\n\n\nElementwise operations\n\nsource\n\nfastuple.add\n\n fastuple.add (*args)\n\n+ is already defined in tuple for concat, so use add instead\n\ntest_eq(fastuple.add((1,1),(2,2)), (3,3))\ntest_eq_type(fastuple(1,1).add(2), fastuple(3,3))\ntest_eq(fastuple('1','2').add('2'), fastuple('12','22'))\n\n\nsource\n\n\nfastuple.mul\n\n fastuple.mul (*args)\n\n* is already defined in tuple for replicating, so use mul instead\n\ntest_eq_type(fastuple(1,1).mul(2), fastuple(2,2))\n\n\n\n\nOther Elementwise Operations\nAdditionally, the following elementwise operations are available: - le: less than or equal - eq: equal - gt: greater than - min: minimum of\n\ntest_eq(fastuple(3,1).le(1), (False, True))\ntest_eq(fastuple(3,1).eq(1), (False, True))\ntest_eq(fastuple(3,1).gt(1), (True, False))\ntest_eq(fastuple(3,1).min(2), (2,1))\n\nYou can also do other elementwise operations like negate a fastuple, or subtract two fastuples:\n\ntest_eq(-fastuple(1,2), (-1,-2))\ntest_eq(~fastuple(1,0,1), (False,True,False))\n\ntest_eq(fastuple(1,1)-fastuple(2,2), (-1,-1))\n\n\ntest_eq(type(fastuple(1)), fastuple)\ntest_eq_type(fastuple(1,2), fastuple(1,2))\ntest_ne(fastuple(1,2), fastuple(1,3))\ntest_eq(fastuple(), ())", + "crumbs": [ + "Basic functionality" + ] + }, + { + "objectID": "basics.html#functions-on-functions", + "href": "basics.html#functions-on-functions", + "title": "Basic functionality", + "section": "Functions on Functions", + "text": "Functions on Functions\nUtilities for functional programming or for defining, modifying, or debugging functions.\n\nsource\n\nbind\n\n bind (func, *pargs, **pkwargs)\n\nSame as partial, except you can use arg0 arg1 etc param placeholders\nbind is the same as partial, but also allows you to reorder positional arguments using variable name(s) arg{i} where i refers to the zero-indexed positional argument. bind as implemented currently only supports reordering of up to the first 5 positional arguments.\nConsider the function myfunc below, which has 3 positional arguments. These arguments can be referenced as arg0, arg1, and arg1, respectively.\n\ndef myfn(a,b,c,d=1,e=2): return(a,b,c,d,e)\n\nIn the below example we bind the positional arguments of myfn as follows:\n\nThe second input 14, referenced by arg1, is substituted for the first positional argument.\nWe supply a default value of 17 for the second positional argument.\nThe first input 19, referenced by arg0, is subsituted for the third positional argument.\n\n\ntest_eq(bind(myfn, arg1, 17, arg0, e=3)(19,14), (14,17,19,1,3))\n\nIn this next example:\n\nWe set the default value to 17 for the first positional argument.\nThe first input 19 refrenced by arg0, becomes the second positional argument.\nThe second input 14 becomes the third positional argument.\nWe override the default the value for named argument e to 3.\n\n\ntest_eq(bind(myfn, 17, arg0, e=3)(19,14), (17,19,14,1,3))\n\nThis is an example of using bind like partial and do not reorder any arguments:\n\ntest_eq(bind(myfn)(17,19,14), (17,19,14,1,2))\n\nbind can also be used to change default values. In the below example, we use the first input 3 to override the default value of the named argument e, and supply default values for the first three positional arguments:\n\ntest_eq(bind(myfn, 17,19,14,e=arg0)(3), (17,19,14,1,3))\n\n\nsource\n\n\nmapt\n\n mapt (func, *iterables)\n\nTuplified map\n\nt = [0,1,2,3]\ntest_eq(mapt(operator.neg, t), (0,-1,-2,-3))\n\n\nsource\n\n\nmap_ex\n\n map_ex (iterable, f, *args, gen=False, **kwargs)\n\nLike map, but use bind, and supports str and indexing\n\ntest_eq(map_ex(t,operator.neg), [0,-1,-2,-3])\n\nIf f is a string then it is treated as a format string to create the mapping:\n\ntest_eq(map_ex(t, '#{}#'), ['#0#','#1#','#2#','#3#'])\n\nIf f is a dictionary (or anything supporting __getitem__) then it is indexed to create the mapping:\n\ntest_eq(map_ex(t, list('abcd')), list('abcd'))\n\nYou can also pass the same arg params that bind accepts:\n\ndef f(a=None,b=None): return b\ntest_eq(map_ex(t, f, b=arg0), range(4))\n\n\nsource\n\n\ncompose\n\n compose (*funcs, order=None)\n\nCreate a function that composes all functions in funcs, passing along remaining *args and **kwargs to all\n\nf1 = lambda o,p=0: (o*2)+p\nf2 = lambda o,p=1: (o+1)/p\ntest_eq(f2(f1(3)), compose(f1,f2)(3))\ntest_eq(f2(f1(3,p=3),p=3), compose(f1,f2)(3,p=3))\ntest_eq(f2(f1(3, 3), 3), compose(f1,f2)(3, 3))\n\nf1.order = 1\ntest_eq(f1(f2(3)), compose(f1,f2, order=\"order\")(3))\n\n\nsource\n\n\nmaps\n\n maps (*args, retain=<function noop>)\n\nLike map, except funcs are composed first\n\ntest_eq(maps([1]), [1])\ntest_eq(maps(operator.neg, [1,2]), [-1,-2])\ntest_eq(maps(operator.neg, operator.neg, [1,2]), [1,2])\n\n\nsource\n\n\npartialler\n\n partialler (f, *args, order=None, **kwargs)\n\nLike functools.partial but also copies over docstring\n\ndef _f(x,a=1):\n \"test func\"\n return x-a\n_f.order=1\n\nf = partialler(_f, 2)\ntest_eq(f.order, 1)\ntest_eq(f(3), -1)\nf = partialler(_f, a=2, order=3)\ntest_eq(f.__doc__, \"test func\")\ntest_eq(f.order, 3)\ntest_eq(f(3), _f(3,2))\n\n\nclass partial0:\n \"Like `partialler`, but args passed to callable are inserted at started, instead of at end\"\n def __init__(self, f, *args, order=None, **kwargs):\n self.f,self.args,self.kwargs = f,args,kwargs\n self.order = ifnone(order, getattr(f,'order',None))\n self.__doc__ = f.__doc__\n\n def __call__(self, *args, **kwargs): return self.f(*args, *self.args, **kwargs, **self.kwargs)\n\n\nf = partial0(_f, 2)\ntest_eq(f.order, 1)\ntest_eq(f(3), 1) # NB: different to `partialler` example\n\n\nsource\n\n\ninstantiate\n\n instantiate (t)\n\nInstantiate t if it’s a type, otherwise do nothing\n\ntest_eq_type(instantiate(int), 0)\ntest_eq_type(instantiate(1), 1)\n\n\nsource\n\n\nusing_attr\n\n using_attr (f, attr)\n\nConstruct a function which applies f to the argument’s attribute attr\n\nt = Path('/a/b.txt')\nf = using_attr(str.upper, 'name')\ntest_eq(f(t), 'B.TXT')\n\n\n\nSelf (with an uppercase S)\nA Concise Way To Create Lambdas\nThis is a concise way to create lambdas that are calling methods on an object (note the capitalization!)\nSelf.sum(), for instance, is a shortcut for lambda o: o.sum().\n\nf = Self.sum()\nx = np.array([3.,1])\ntest_eq(f(x), 4.)\n\n# This is equivalent to above\nf = lambda o: o.sum()\nx = np.array([3.,1])\ntest_eq(f(x), 4.)\n\nf = Self.argmin()\narr = np.array([1,2,3,4,5])\ntest_eq(f(arr), arr.argmin())\n\nf = Self.sum().is_integer()\nx = np.array([3.,1])\ntest_eq(f(x), True)\n\nf = Self.sum().real.is_integer()\nx = np.array([3.,1])\ntest_eq(f(x), True)\n\nf = Self.imag()\ntest_eq(f(3), 0)\n\nf = Self[1]\ntest_eq(f(x), 1)\n\nSelf is also callable, which creates a function which calls any function passed to it, using the arguments passed to Self:\n\ndef f(a, b=3): return a+b+2\ndef g(a, b=3): return a*b\nfg = Self(1,b=2)\nlist(map(fg, [f,g]))\n\n[5, 2]", + "crumbs": [ + "Basic functionality" + ] + }, + { + "objectID": "basics.html#patching", + "href": "basics.html#patching", + "title": "Basic functionality", + "section": "Patching", + "text": "Patching\n\nsource\n\ncopy_func\n\n copy_func (f)\n\nCopy a non-builtin function (NB copy.copy does not work for this)\nSometimes it may be desirable to make a copy of a function that doesn’t point to the original object. When you use Python’s built in copy.copy or copy.deepcopy to copy a function, you get a reference to the original object:\n\nimport copy as cp\n\n\ndef foo(): pass\na = cp.copy(foo)\nb = cp.deepcopy(foo)\n\na.someattr = 'hello' # since a and b point at the same object, updating a will update b\ntest_eq(b.someattr, 'hello')\n\nassert a is foo and b is foo\n\nHowever, with copy_func, you can retrieve a copy of a function without a reference to the original object:\n\nc = copy_func(foo) # c is an indpendent object\nassert c is not foo\n\n\ndef g(x, *, y=3): return x+y\ntest_eq(copy_func(g)(4), 7)\n\n\nsource\n\n\npatch_to\n\n patch_to (cls, as_prop=False, cls_method=False)\n\nDecorator: add f to cls\nThe @patch_to decorator allows you to monkey patch a function into a class as a method:\n\nclass _T3(int): pass \n\n@patch_to(_T3)\ndef func1(self, a): return self+a\n\nt = _T3(1) # we initialized `t` to a type int = 1\ntest_eq(t.func1(2), 3) # we add 2 to `t`, so 2 + 1 = 3\n\nYou can access instance properties in the usual way via self:\n\nclass _T4():\n def __init__(self, g): self.g = g\n \n@patch_to(_T4)\ndef greet(self, x): return self.g + x\n \nt = _T4('hello ') # this sets self.g = 'hello '\ntest_eq(t.greet('world'), 'hello world') #t.greet('world') will append 'world' to 'hello '\n\nYou can instead specify that the method should be a class method by setting cls_method=True:\n\nclass _T5(int): attr = 3 # attr is a class attribute we will access in a later method\n \n@patch_to(_T5, cls_method=True)\ndef func(cls, x): return cls.attr + x # you can access class attributes in the normal way\n\ntest_eq(_T5.func(4), 7)\n\nAdditionally you can specify that the function you want to patch should be a class attribute with as_prop=True:\n\n@patch_to(_T5, as_prop=True)\ndef add_ten(self): return self + 10\n\nt = _T5(4)\ntest_eq(t.add_ten, 14)\n\nInstead of passing one class to the @patch_to decorator, you can pass multiple classes in a tuple to simulteanously patch more than one class with the same method:\n\nclass _T6(int): pass\nclass _T7(int): pass\n\n@patch_to((_T6,_T7))\ndef func_mult(self, a): return self*a\n\nt = _T6(2)\ntest_eq(t.func_mult(4), 8)\nt = _T7(2)\ntest_eq(t.func_mult(4), 8)\n\n\nsource\n\n\npatch\n\n patch (f=None, as_prop=False, cls_method=False)\n\nDecorator: add f to the first parameter’s class (based on f’s type annotations)\n@patch is an alternative to @patch_to that allows you similarly monkey patch class(es) by using type annotations:\n\nclass _T8(int): pass \n\n@patch\ndef func(self:_T8, a): return self+a\n\nt = _T8(1) # we initilized `t` to a type int = 1\ntest_eq(t.func(3), 4) # we add 3 to `t`, so 3 + 1 = 4\ntest_eq(t.func.__qualname__, '_T8.func')\n\nSimilarly to patch_to, you can supply a union of classes instead of a single class in your type annotations to patch multiple classes:\n\nclass _T9(int): pass \n\n@patch\ndef func2(x:_T8|_T9, a): return x*a # will patch both _T8 and _T9\n\nt = _T8(2)\ntest_eq(t.func2(4), 8)\ntest_eq(t.func2.__qualname__, '_T8.func2')\n\nt = _T9(2)\ntest_eq(t.func2(4), 8)\ntest_eq(t.func2.__qualname__, '_T9.func2')\n\nJust like patch_to decorator you can use as_prop and cls_method parameters with patch decorator:\n\n@patch(as_prop=True)\ndef add_ten(self:_T5): return self + 10\n\nt = _T5(4)\ntest_eq(t.add_ten, 14)\n\n\nclass _T5(int): attr = 3 # attr is a class attribute we will access in a later method\n \n@patch(cls_method=True)\ndef func(cls:_T5, x): return cls.attr + x # you can access class attributes in the normal way\n\ntest_eq(_T5.func(4), 7)\n\n\nsource\n\n\npatch_property\n\n patch_property (f)\n\nDeprecated; use patch(as_prop=True) instead\nPatching classmethod shouldn’t affect how python’s inheritance works\n\nclass FastParent: pass\n\n@patch(cls_method=True)\ndef type_cls(cls: FastParent): return cls\n\nclass FastChild(FastParent): pass\n\nparent = FastParent()\ntest_eq(parent.type_cls(), FastParent)\n\nchild = FastChild()\ntest_eq(child.type_cls(), FastChild)", + "crumbs": [ + "Basic functionality" + ] + }, + { + "objectID": "basics.html#other-helpers", + "href": "basics.html#other-helpers", + "title": "Basic functionality", + "section": "Other Helpers", + "text": "Other Helpers\n\nsource\n\ncompile_re\n\n compile_re (pat)\n\nCompile pat if it’s not None\n\nassert compile_re(None) is None\nassert compile_re('a').match('ab')\n\n\nsource\n\nImportEnum\n\n ImportEnum (value, names=None, module=None, qualname=None, type=None,\n start=1)\n\nAn Enum that can have its values imported\n\n_T = ImportEnum('_T', {'foobar':1, 'goobar':2})\n_T.imports()\ntest_eq(foobar, _T.foobar)\ntest_eq(goobar, _T.goobar)\n\n\nsource\n\n\nStrEnum\n\n StrEnum (value, names=None, module=None, qualname=None, type=None,\n start=1)\n\nAn ImportEnum that behaves like a str\n\nsource\n\n\n\nstr_enum\n\n str_enum (name, *vals)\n\nSimplified creation of StrEnum types\n\nsource\n\nValEnum\n\n ValEnum (value, names=None, module=None, qualname=None, type=None,\n start=1)\n\nAn ImportEnum that stringifies using values\n\n_T = str_enum('_T', 'a', 'b')\ntest_eq(f'{_T.a}', 'a')\ntest_eq(_T.a, 'a')\ntest_eq(list(_T.__members__), ['a','b'])\nprint(_T.a, _T.a.upper())\n\na A\n\n\n\nsource\n\n\nStateful\n\n Stateful (*args, **kwargs)\n\nA base class/mixin for objects that should not serialize all their state\n\nclass _T(Stateful):\n def __init__(self):\n super().__init__()\n self.a=1\n self._state['test']=2\n\nt = _T()\nt2 = pickle.loads(pickle.dumps(t))\ntest_eq(t.a,1)\ntest_eq(t._state['test'],2)\ntest_eq(t2.a,1)\ntest_eq(t2._state,{})\n\nOverride _init_state to do any necessary setup steps that are required during __init__ or during deserialization (e.g. pickle.load). Here’s an example of how Stateful simplifies the official Python example for Handling Stateful Objects.\n\nclass TextReader(Stateful):\n \"\"\"Print and number lines in a text file.\"\"\"\n _stateattrs=('file',)\n def __init__(self, filename):\n self.filename,self.lineno = filename,0\n super().__init__()\n\n def readline(self):\n self.lineno += 1\n line = self.file.readline()\n if line: return f\"{self.lineno}: {line.strip()}\"\n\n def _init_state(self):\n self.file = open(self.filename)\n for _ in range(self.lineno): self.file.readline()\n\n\nreader = TextReader(\"00_test.ipynb\")\nprint(reader.readline())\nprint(reader.readline())\n\nnew_reader = pickle.loads(pickle.dumps(reader))\nprint(reader.readline())\n\n1: {\n2: \"cells\": [\n3: {\n\n\n\nsource\n\n\n\nNotStr\n\n NotStr (s)\n\nBehaves like a str, but isn’t an instance of one\n\ns = NotStr(\"hello\")\nassert not isinstance(s, str)\ntest_eq(s, 'hello')\ntest_eq(s*2, 'hellohello')\ntest_eq(len(s), 5)\n\n\nsource\n\nPrettyString\nLittle hack to get strings to show properly in Jupyter.\nAllow strings with special characters to render properly in Jupyter. Without calling print() strings with special characters are displayed like so:\n\nwith_special_chars='a string\\nwith\\nnew\\nlines and\\ttabs'\nwith_special_chars\n\n'a string\\nwith\\nnew\\nlines and\\ttabs'\n\n\nWe can correct this with PrettyString:\n\nPrettyString(with_special_chars)\n\na string\nwith\nnew\nlines and tabs\n\n\n\nsource\n\n\n\neven_mults\n\n even_mults (start, stop, n)\n\nBuild log-stepped array from start to stop in n steps.\n\ntest_eq(even_mults(2,8,3), [2,4,8])\ntest_eq(even_mults(2,32,5), [2,4,8,16,32])\ntest_eq(even_mults(2,8,1), 8)\n\n\nsource\n\n\nnum_cpus\n\n num_cpus ()\n\nGet number of cpus\n\nnum_cpus()\n\n10\n\n\n\nsource\n\n\nadd_props\n\n add_props (f, g=None, n=2)\n\nCreate properties passing each of range(n) to f\n\nclass _T(): a,b = add_props(lambda i,x:i*2)\n\nt = _T()\ntest_eq(t.a,0)\ntest_eq(t.b,2)\n\n\nclass _T(): \n def __init__(self, v): self.v=v\n def _set(i, self, v): self.v[i] = v\n a,b = add_props(lambda i,x: x.v[i], _set)\n\nt = _T([0,2])\ntest_eq(t.a,0)\ntest_eq(t.b,2)\nt.a = t.a+1\nt.b = 3\ntest_eq(t.a,1)\ntest_eq(t.b,3)\n\n\nsource\n\n\nstr2bool\n\n str2bool (s)\n\nCase-insensitive convert string s too a bool (y,yes,t,true,on,1->True)\nTrue values are ‘y’, ‘yes’, ‘t’, ‘true’, ‘on’, and ‘1’; false values are ‘n’, ‘no’, ‘f’, ‘false’, ‘off’, and ‘0’. Raises ValueError if ‘val’ is anything else.\n\nfor o in \"y YES t True on 1\".split(): assert str2bool(o)\nfor o in \"n no FALSE off 0\".split(): assert not str2bool(o)\nfor o in 0,None,'',False: assert not str2bool(o)\nfor o in 1,True: assert str2bool(o)\n\n\nsource\n\n\nstr2int\n\n str2int (s)\n\nConvert s to an int\n\nsource\n\n\nstr2float\n\n str2float (s:str)\n\nConvert s to a float\n\nsource\n\n\nstr2list\n\n str2list (s:str)\n\nConvert s to a list\n\nsource\n\n\nstr2date\n\n str2date (s:str)\n\ndate.fromisoformat with empty string handling\n\nsource\n\n\nto_date\n\n to_date (arg)\n\n\nsource\n\n\nto_list\n\n to_list (arg)\n\n\nsource\n\n\nto_float\n\n to_float (arg)\n\n\nsource\n\n\nto_int\n\n to_int (arg)\n\n\nsource\n\n\nto_bool\n\n to_bool (arg)\n\n\nsource\n\n\ntyped\n\n typed (_func=None, cast=False)\n\nDecorator to check param and return types at runtime, with optional casting\ntyped validates argument types at runtime. This is in contrast to MyPy which only offers static type checking.\nFor example, a TypeError will be raised if we try to pass an integer into the first argument of the below function:\n\n@typed\ndef discount(price:int, pct:float) -> float:\n return (1-pct) * price\n\nwith ExceptionExpected(TypeError): discount(100.0, .1)\n\nYou can have automatic casting based on heuristics by specifying typed(cast=True). If casting is not possible, a TypeError is raised.\n\n@typed(cast=True)\ndef discount(price:int, pct:float) -> float:\n return (1-pct) * price\n\nassert 90.0 == discount(100.5, .1) # will auto cast 100.5 to the int 100\nassert 90.0 == discount(' 100 ', .1) # will auto cast the str \"100\" to the int 100\nwith ExceptionExpected(TypeError): discount(\"a\", .1)\n\nWe can also optionally allow multiple types by enumarating the types in a tuple as illustrated below:\n\n@typed\ndef discount(price:int|float, pct:float): \n return (1-pct) * price\n\nassert 90.0 == discount(100.0, .1)\n\n@typed(cast=True)\ndef discount(price:int|None, pct:float):\n return (1-pct) * price\n\nassert 90.0 == discount(100.0, .1)\n\nWe currently do not support union types when casting.\n\n@typed(cast=True)\ndef discount(price:int|float, pct:float):\n return (1-pct) * price\n\nwith ExceptionExpected(AssertionError): assert 90.0 == discount(\"100.0\", .1)\n\ntyped works with classes, too:\n\nclass Foo:\n @typed\n def __init__(self, a:int, b: int, c:str): pass\n @typed(cast=True)\n def test(cls, d:str): return d\n\nwith ExceptionExpected(TypeError): Foo(1, 2, 3) \nassert isinstance(Foo(1,2, 'a string').test(10), str)\n\nIt also works with custom types.\n\n@typed\ndef test_foo(foo: Foo): pass\n\nwith ExceptionExpected(TypeError): test_foo(1)\ntest_foo(Foo(1, 2, 'a string'))\n\n\nclass Bar:\n @typed\n def __init__(self, a:int): self.a = a\n@typed(cast=True)\ndef test_bar(bar: Bar): return bar\n\nassert isinstance(test_bar(1), Bar)\ntest_eq(test_bar(1).a, 1)\nwith ExceptionExpected(TypeError): test_bar(\"foobar\")\n\n\nsource\n\n\nexec_new\n\n exec_new (code)\n\nExecute code in a new environment and return it\n\ng = exec_new('a=1')\ntest_eq(g['a'], 1)\n\n\nsource\n\n\nexec_import\n\n exec_import (mod, sym)\n\nImport sym from mod in a new environment", + "crumbs": [ + "Basic functionality" + ] + }, + { + "objectID": "basics.html#notebook-functions", + "href": "basics.html#notebook-functions", + "title": "Basic functionality", + "section": "Notebook functions", + "text": "Notebook functions\n\n\nipython_shell\n\n ipython_shell ()\n\nSame as get_ipython but returns False if not in IPython\n\n\n\nin_ipython\n\n in_ipython ()\n\nCheck if code is running in some kind of IPython environment\n\n\n\nin_colab\n\n in_colab ()\n\nCheck if the code is running in Google Colaboratory\n\n\n\nin_jupyter\n\n in_jupyter ()\n\nCheck if the code is running in a jupyter notebook\n\n\n\nin_notebook\n\n in_notebook ()\n\nCheck if the code is running in a jupyter notebook\nThese variables are available as booleans in fastcore.basics as IN_IPYTHON, IN_JUPYTER, IN_COLAB and IN_NOTEBOOK.\n\nIN_IPYTHON, IN_JUPYTER, IN_COLAB, IN_NOTEBOOK\n\n(True, True, False, True)", + "crumbs": [ + "Basic functionality" + ] + }, + { + "objectID": "dispatch.html", + "href": "dispatch.html", + "title": "Type dispatch", + "section": "", + "text": "from nbdev.showdoc import *\nfrom fastcore.test import *\nfrom fastcore.nb_imports import *", + "crumbs": [ + "Type dispatch" + ] + }, + { + "objectID": "dispatch.html#helpers", + "href": "dispatch.html#helpers", + "title": "Type dispatch", + "section": "Helpers", + "text": "Helpers\n\nsource\n\nlenient_issubclass\n\n lenient_issubclass (cls, types)\n\nIf possible return whether cls is a subclass of types, otherwise return False.\n\nassert not lenient_issubclass(typing.Collection, list)\nassert lenient_issubclass(list, typing.Collection)\nassert lenient_issubclass(typing.Collection, object)\nassert lenient_issubclass(typing.List, typing.Collection)\nassert not lenient_issubclass(typing.Collection, typing.List)\nassert not lenient_issubclass(object, typing.Callable)\n\n\nsource\n\n\nsorted_topologically\n\n sorted_topologically (iterable, cmp=<built-in function lt>,\n reverse=False)\n\nReturn a new list containing all items from the iterable sorted topologically\n\ntd = [3, 1, 2, 5]\ntest_eq(sorted_topologically(td), [1, 2, 3, 5])\ntest_eq(sorted_topologically(td, reverse=True), [5, 3, 2, 1])\n\n\ntd = {int:1, numbers.Number:2, numbers.Integral:3}\ntest_eq(sorted_topologically(td, cmp=lenient_issubclass), [int, numbers.Integral, numbers.Number])\n\n\ntd = [numbers.Integral, tuple, list, int, dict]\ntd = sorted_topologically(td, cmp=lenient_issubclass)\nassert td.index(int) < td.index(numbers.Integral)", + "crumbs": [ + "Type dispatch" + ] + }, + { + "objectID": "dispatch.html#typedispatch", + "href": "dispatch.html#typedispatch", + "title": "Type dispatch", + "section": "TypeDispatch", + "text": "TypeDispatch\nType dispatch, or Multiple dispatch, allows you to change the way a function behaves based upon the input types it recevies. This is a prominent feature in some programming languages like Julia. For example, this is a conceptual example of how multiple dispatch works in Julia, returning different values depending on the input types of x and y:\ncollide_with(x::Asteroid, y::Asteroid) = ... \n# deal with asteroid hitting asteroid\n\ncollide_with(x::Asteroid, y::Spaceship) = ... \n# deal with asteroid hitting spaceship\n\ncollide_with(x::Spaceship, y::Asteroid) = ... \n# deal with spaceship hitting asteroid\n\ncollide_with(x::Spaceship, y::Spaceship) = ... \n# deal with spaceship hitting spaceship\nType dispatch can be especially useful in data science, where you might allow different input types (i.e. numpy arrays and pandas dataframes) to function that processes data. Type dispatch allows you to have a common API for functions that do similar tasks.\nThe TypeDispatch class allows us to achieve type dispatch in Python. It contains a dictionary that maps types from type annotations to functions, which ensures that the proper function is called when passed inputs.\n\nsource\n\nTypeDispatch\n\n TypeDispatch (funcs=(), bases=())\n\nDictionary-like object; __getitem__ matches keys of types using issubclass\nTo demonstrate how TypeDispatch works, we define a set of functions that accept a variety of input types, specified with different type annotations:\n\ndef f2(x:int, y:float): return x+y #int and float for 2nd arg\ndef f_nin(x:numbers.Integral)->int: return x+1 #integral numeric\ndef f_ni2(x:int): return x #integer\ndef f_bll(x:bool|list): return x #bool or list\ndef f_num(x:numbers.Number): return x #Number (root of numerics)\n\nWe can optionally initialize TypeDispatch with a list of functions we want to search. Printing an instance of TypeDispatch will display convenient mapping of types -> functions:\n\nt = TypeDispatch([f_nin,f_ni2,f_num,f_bll,None])\nt\n\n(bool,object) -> f_bll\n(int,object) -> f_ni2\n(Integral,object) -> f_nin\n(Number,object) -> f_num\n(list,object) -> f_bll\n(object,object) -> NoneType\n\n\nNote that only the first two arguments are used for TypeDispatch. If your function only contains one argument, the second parameter will be shown as object. If you pass None into TypeDispatch, then this will be displayed as (object, object) -> NoneType.\nTypeDispatch is a dictionary-like object, which means that you can retrieve a function by the associated type annotation. For example, the statement:\nt[float]\nWill return f_num because that is the matching function that has a type annotation that is a super-class of of float - numbers.Number:\n\nassert issubclass(float, numbers.Number)\ntest_eq(t[float], f_num)\n\nThe same is true for other types as well:\n\ntest_eq(t[np.int32], f_nin)\ntest_eq(t[bool], f_bll)\ntest_eq(t[list], f_bll)\ntest_eq(t[np.int32], f_nin)\n\nIf you try to get a type that doesn’t match, TypeDispatch will return None:\n\ntest_eq(t[str], None)\n\n\nsource\n\n\nTypeDispatch.add\n\n TypeDispatch.add (f)\n\nAdd type t and function f\nThis method allows you to add an additional function to an existing TypeDispatch instance :\n\ndef f_col(x:typing.Collection): return x\nt.add(f_col)\ntest_eq(t[str], f_col)\nt\n\n(bool,object) -> f_bll\n(int,object) -> f_ni2\n(Integral,object) -> f_nin\n(Number,object) -> f_num\n(list,object) -> f_bll\n(typing.Collection,object) -> f_col\n(object,object) -> NoneType\n\n\nIf you accidentally add the same function more than once things will still work as expected:\n\nt.add(f_ni2) \ntest_eq(t[int], f_ni2)\n\nHowever, if you add a function that has a type collision that raises an ambiguity, this will automatically resolve to the latest function added:\n\ndef f_ni3(z:int): return z # collides with f_ni2 with same type annotations\nt.add(f_ni3) \ntest_eq(t[int], f_ni3)\n\n\nUsing bases:\nThe argument bases can optionally accept a single instance of TypeDispatch or a collection (i.e. a tuple or list) of TypeDispatch objects. This can provide functionality similar to multiple inheritance.\nThese are searched for matching functions if no match in your list of functions:\n\ndef f_str(x:str): return x+'1'\n\nt = TypeDispatch([f_nin,f_ni2,f_num,f_bll,None])\nt2 = TypeDispatch(f_str, bases=t) # you can optionally supply a list of TypeDispatch objects for `bases`.\nt2\n\n(str,object) -> f_str\n(bool,object) -> f_bll\n(int,object) -> f_ni2\n(Integral,object) -> f_nin\n(Number,object) -> f_num\n(list,object) -> f_bll\n(object,object) -> NoneType\n\n\n\ntest_eq(t2[int], f_ni2) # searches `t` b/c not found in `t2`\ntest_eq(t2[np.int32], f_nin) # searches `t` b/c not found in `t2`\ntest_eq(t2[float], f_num) # searches `t` b/c not found in `t2`\ntest_eq(t2[bool], f_bll) # searches `t` b/c not found in `t2`\ntest_eq(t2[str], f_str) # found in `t`!\ntest_eq(t2('a'), 'a1') # found in `t`!, and uses __call__\n\no = np.int32(1)\ntest_eq(t2(o), 2) # found in `t2` and uses __call__\n\n\n\nUp To Two Arguments\nTypeDispatch supports up to two arguments when searching for the appropriate function. The following functions f1 and f2 both have two parameters:\n\ndef f1(x:numbers.Integral, y): return x+1 #Integral is a numeric type\ndef f2(x:int, y:float): return x+y\nt = TypeDispatch([f1,f2])\nt\n\n(int,float) -> f2\n(Integral,object) -> f1\n\n\nYou can lookup functions from a TypeDispatch instance with two parameters like this:\n\ntest_eq(t[np.int32], f1)\ntest_eq(t[int,float], f2)\n\nKeep in mind that anything beyond the first two parameters are ignored, and any collisions will be resolved in favor of the most recent function added. In the below example, f1 is ignored in favor of f2 because the first two parameters have identical type hints:\n\ndef f1(a:str, b:int, c:list): return a\ndef f2(a: str, b:int): return b\nt = TypeDispatch([f1,f2])\ntest_eq(t[str, int], f2)\nt\n\n(str,int) -> f2\n\n\n\n\nMatching\nType Dispatch matches types with functions according to whether the supplied class is a subclass or the same class of the type annotation(s) of associated functions.\nLet’s consider an example where we try to retrieve the function corresponding to types of [np.int32, float].\nIn this scenario, f2 will not be matched. This is because the first type annotation of f2, int, is not a superclass (or the same class) of np.int32:\n\ndef f1(x:numbers.Integral, y): return x+1\ndef f2(x:int, y:float): return x+y\nt = TypeDispatch([f1,f2])\n\nassert not issubclass(np.int32, int)\n\nInstead, f1 is a valid match, as its first argument is annoted with the type numbers.Integeral, which np.int32 is a subclass of:\n\nassert issubclass(np.int32, numbers.Integral)\ntest_eq(t[np.int32,float], f1)\n\nIn f1 , the 2nd parameter y is not annotated, which means TypeDispatch will match anything where the first argument matches int that is not matched with anything else:\n\nassert issubclass(int, numbers.Integral) # int is a subclass of numbers.Integral\ntest_eq(t[int], f1)\ntest_eq(t[int,int], f1)\n\nIf no match is possible, None is returned:\n\ntest_eq(t[float,float], None)\n\n\nsource\n\n\n\nTypeDispatch.__call__\n\n TypeDispatch.__call__ (*args, **kwargs)\n\nCall self as a function.\nTypeDispatch is also callable. When you call an instance of TypeDispatch, it will execute the relevant function:\n\ndef f_arr(x:np.ndarray): return x.sum()\ndef f_int(x:np.int32): return x+1\nt = TypeDispatch([f_arr, f_int])\n\narr = np.array([5,4,3,2,1])\ntest_eq(t(arr), 15) # dispatches to f_arr\n\no = np.int32(1)\ntest_eq(t(o), 2) # dispatches to f_int\nassert t.first() is not None\n\nYou can also call an instance of of TypeDispatch when there are two parameters:\n\ndef f1(x:numbers.Integral, y): return x+1\ndef f2(x:int, y:float): return x+y\nt = TypeDispatch([f1,f2])\n\ntest_eq(t(3,2.0), 5)\ntest_eq(t(3,2), 4)\n\nWhen no match is found, a TypeDispatch instance becomes an identity function. This default behavior is leveraged by fasatai for data transformations to provide a sensible default when a matching function cannot be found.\n\ntest_eq(t('a'), 'a')\n\n\nsource\n\n\nTypeDispatch.returns\n\n TypeDispatch.returns (x)\n\nGet the return type of annotation of x.\nYou can optionally pass an object to TypeDispatch.returns and get the return type annotation back:\n\ndef f1(x:int) -> np.ndarray: return np.array(x)\ndef f2(x:str) -> float: return List\ndef f3(x:float): return List # f3 has no return type annotation\n\nt = TypeDispatch([f1, f2, f3])\n\ntest_eq(t.returns(1), np.ndarray) # dispatched to f1\ntest_eq(t.returns('Hello'), float) # dispatched to f2\ntest_eq(t.returns(1.0), None) # dispatched to f3\n\nclass _Test: pass\n_test = _Test()\ntest_eq(t.returns(_test), None) # type `_Test` not found, so None returned\n\n\nUsing TypeDispatch With Methods\nYou can use TypeDispatch when defining methods as well:\n\ndef m_nin(self, x:str|numbers.Integral): return str(x)+'1'\ndef m_bll(self, x:bool): self.foo='a'\ndef m_num(self, x:numbers.Number): return x*2\n\nt = TypeDispatch([m_nin,m_num,m_bll])\nclass A: f = t # set class attribute `f` equal to a TypeDispatch instance\n \na = A()\ntest_eq(a.f(1), '11') #dispatch to m_nin\ntest_eq(a.f(1.), 2.) #dispatch to m_num\ntest_is(a.f.inst, a)\n\na.f(False) # this triggers t.m_bll to run, which sets self.foo to 'a'\ntest_eq(a.foo, 'a')\n\nAs discussed in TypeDispatch.__call__, when there is not a match, TypeDispatch.__call__ becomes an identity function. In the below example, a tuple does not match any type annotations so a tuple is returned:\n\ntest_eq(a.f(()), ())\n\nWe extend the previous example by using bases to add an additional method that supports tuples:\n\ndef m_tup(self, x:tuple): return x+(1,)\nt2 = TypeDispatch(m_tup, bases=t)\n\nclass A2: f = t2\na2 = A2()\ntest_eq(a2.f(1), '11')\ntest_eq(a2.f(1.), 2.)\ntest_is(a2.f.inst, a2)\na2.f(False)\ntest_eq(a2.foo, 'a')\ntest_eq(a2.f(()), (1,))\n\n\n\nUsing TypeDispatch With Class Methods\nYou can use TypeDispatch when defining class methods too:\n\ndef m_nin(cls, x:str|numbers.Integral): return str(x)+'1'\ndef m_bll(cls, x:bool): cls.foo='a'\ndef m_num(cls, x:numbers.Number): return x*2\n\nt = TypeDispatch([m_nin,m_num,m_bll])\nclass A: f = t # set class attribute `f` equal to a TypeDispatch\n\ntest_eq(A.f(1), '11') #dispatch to m_nin\ntest_eq(A.f(1.), 2.) #dispatch to m_num\ntest_is(A.f.owner, A)\n\nA.f(False) # this triggers t.m_bll to run, which sets A.foo to 'a'\ntest_eq(A.foo, 'a')", + "crumbs": [ + "Type dispatch" + ] + }, + { + "objectID": "dispatch.html#typedispatch-decorator", + "href": "dispatch.html#typedispatch-decorator", + "title": "Type dispatch", + "section": "typedispatch Decorator", + "text": "typedispatch Decorator\n\nsource\n\nDispatchReg\n\n DispatchReg ()\n\nA global registry for TypeDispatch objects keyed by function name\n\n@typedispatch\ndef f_td_test(x, y): return f'{x}{y}'\n@typedispatch\ndef f_td_test(x:numbers.Integral|int, y): return x+1\n@typedispatch\ndef f_td_test(x:int, y:float): return x+y\n@typedispatch\ndef f_td_test(x:int, y:int): return x*y\n\ntest_eq(f_td_test(3,2.0), 5)\nassert issubclass(int, numbers.Integral)\ntest_eq(f_td_test(3,2), 6)\n\ntest_eq(f_td_test('a','b'), 'ab')\n\n\nUsing typedispatch With other decorators\nYou can use typedispatch with classmethod and staticmethod decorator\n\nclass A:\n @typedispatch\n def f_td_test(self, x:numbers.Integral, y): return x+1\n @typedispatch\n @classmethod\n def f_td_test(cls, x:int, y:float): return x+y\n @typedispatch\n @staticmethod\n def f_td_test(x:int, y:int): return x*y\n \ntest_eq(A.f_td_test(3,2), 6)\ntest_eq(A.f_td_test(3,2.0), 5)\ntest_eq(A().f_td_test(3,'2.0'), 4)", + "crumbs": [ + "Type dispatch" + ] + }, + { + "objectID": "dispatch.html#casting", + "href": "dispatch.html#casting", + "title": "Type dispatch", + "section": "Casting", + "text": "Casting\nNow that we can dispatch on types, let’s make it easier to cast objects to a different type.\n\nsource\n\nretain_meta\n\n retain_meta (x, res, as_copy=False)\n\nCall res.set_meta(x), if it exists\n\nsource\n\n\ndefault_set_meta\n\n default_set_meta (x, as_copy=False)\n\nCopy over _meta from x to res, if it’s missing\n\n\n\n(object,object) -> cast\nDictionary-like object; __getitem__ matches keys of types using issubclass\nThis works both for plain python classes:…\n\nmk_class('_T1', 'a') # mk_class is a fastai utility that constructs a class.\nclass _T2(_T1): pass\n\nt = _T1(a=1)\nt2 = cast(t, _T2) \nassert t2 is t # t2 refers to the same object as t\nassert isinstance(t, _T2) # t also changed in-place\nassert isinstance(t2, _T2)\n\ntest_eq_type(_T2(a=1), t2)\n\n…as well as for arrays and tensors.\n\nclass _T1(ndarray): pass\n\nt = array([1])\nt2 = cast(t, _T1)\ntest_eq(array([1]), t2)\ntest_eq(_T1, type(t2))\n\nTo customize casting for other types, define a separate cast function with typedispatch for your type.\n\nsource\n\n\nretain_type\n\n retain_type (new, old=None, typ=None, as_copy=False)\n\nCast new to type of old or typ if it’s a superclass\n\nclass _T(tuple): pass\na = _T((1,2))\nb = tuple((1,2))\nc = retain_type(b, typ=_T)\ntest_eq_type(c, a)\n\nIf old has a _meta attribute, its content is passed when casting new to the type of old. In the below example, only the attribute a, but not other_attr is kept, because other_attr is not in _meta:\n\nclass _A():\n set_meta = default_set_meta\n def __init__(self, t): self.t=t\n\nclass _B1(_A):\n def __init__(self, t, a=1):\n super().__init__(t)\n self._meta = {'a':a}\n self.other_attr = 'Hello' # will not be kept after casting.\n \nx = _B1(1, a=2)\nb = _A(1)\nc = retain_type(b, old=x)\ntest_eq(c._meta, {'a': 2})\nassert not getattr(c, 'other_attr', None)\n\n\nsource\n\n\nretain_types\n\n retain_types (new, old=None, typs=None)\n\nCast each item of new to type of matching item in old if it’s a superclass\n\nclass T(tuple): pass\n\nt1,t2 = retain_types((1,(1,(1,1))), (2,T((2,T((3,4))))))\ntest_eq_type(t1, 1)\ntest_eq_type(t2, T((1,T((1,1)))))\n\nt1,t2 = retain_types((1,(1,(1,1))), typs = {tuple: [int, {T: [int, {T: [int,int]}]}]})\ntest_eq_type(t1, 1)\ntest_eq_type(t2, T((1,T((1,1)))))\n\n\nsource\n\n\nexplode_types\n\n explode_types (o)\n\nReturn the type of o, potentially in nested dictionaries for thing that are listy\n\ntest_eq(explode_types((2,T((2,T((3,4)))))), {tuple: [int, {T: [int, {T: [int,int]}]}]})", + "crumbs": [ + "Type dispatch" + ] + }, + { + "objectID": "meta.html", + "href": "meta.html", + "title": "Meta", + "section": "", + "text": "from fastcore.foundation import *\nfrom nbdev.showdoc import *\nfrom fastcore.nb_imports import *\nSee this blog post for more information about metaclasses.\nsource", + "crumbs": [ + "Meta" + ] + }, + { + "objectID": "meta.html#metaprogramming", + "href": "meta.html#metaprogramming", + "title": "Meta", + "section": "Metaprogramming", + "text": "Metaprogramming\n\nsource\n\nempty2none\n\n empty2none (p)\n\nReplace Parameter.empty with None\n\nsource\n\n\nanno_dict\n\n anno_dict (f)\n\n__annotation__ dictionary withemptycast toNone`, returning empty if doesn’t exist\n\ndef _f(a:int, b:L)->str: ...\ntest_eq(anno_dict(_f), {'a': int, 'b': L, 'return': str})\n\n\nsource\n\n\nuse_kwargs_dict\n\n use_kwargs_dict (keep=False, **kwargs)\n\nDecorator: replace **kwargs in signature with names params\nReplace all **kwargs with named arguments like so:\n\n@use_kwargs_dict(y=1,z=None)\ndef foo(a, b=1, **kwargs): pass\n\ntest_sig(foo, '(a, b=1, *, y=1, z=None)')\n\nAdd named arguments, but optionally keep **kwargs by setting keep=True:\n\n@use_kwargs_dict(y=1,z=None, keep=True)\ndef foo(a, b=1, **kwargs): pass\n\ntest_sig(foo, '(a, b=1, *, y=1, z=None, **kwargs)')\n\n\nsource\n\n\nuse_kwargs\n\n use_kwargs (names, keep=False)\n\nDecorator: replace **kwargs in signature with names params\nuse_kwargs is different than use_kwargs_dict as it only replaces **kwargs with named parameters without any default values:\n\n@use_kwargs(['y', 'z'])\ndef foo(a, b=1, **kwargs): pass\n\ntest_sig(foo, '(a, b=1, *, y=None, z=None)')\n\nYou may optionally keep the **kwargs argument in your signature by setting keep=True:\n\n@use_kwargs(['y', 'z'], keep=True)\ndef foo(a, *args, b=1, **kwargs): pass\ntest_sig(foo, '(a, *args, b=1, y=None, z=None, **kwargs)')\n\n\nsource\n\n\ndelegates\n\n delegates (to:function=None, keep=False, but:list=None)\n\nDecorator: replace **kwargs in signature with params from to\n\n\n\n\nType\nDefault\nDetails\n\n\n\n\nto\nfunction\nNone\nDelegatee\n\n\nkeep\nbool\nFalse\nKeep kwargs in decorated function?\n\n\nbut\nlist\nNone\nExclude these parameters from signature\n\n\n\nA common Python idiom is to accept **kwargs in addition to named parameters that are passed onto other function calls. It is especially common to use **kwargs when you want to give the user an option to override default parameters of any functions or methods being called by the parent function.\nFor example, suppose we have have a function foo that passes arguments to baz like so:\n\ndef baz(a, b:int=2, c:int=3): return a + b + c\n\ndef foo(c, a, **kwargs):\n return c + baz(a, **kwargs)\n\nassert foo(c=1, a=1) == 7\n\nThe problem with this approach is the api for foo is obfuscated. Users cannot introspect what the valid arguments for **kwargs are without reading the source code. When a user tries tries to introspect the signature of foo, they are presented with this:\n\ninspect.signature(foo)\n\n<Signature (c, a, **kwargs)>\n\n\nWe can address this issue by using the decorator delegates to include parameters from other functions. For example, if we apply the delegates decorator to foo to include parameters from baz:\n\n@delegates(baz)\ndef foo(c, a, **kwargs):\n return c + baz(a, **kwargs)\n\ntest_sig(foo, '(c, a, *, b: int = 2)')\ninspect.signature(foo)\n\n<Signature (c, a, *, b: int = 2)>\n\n\nWe can optionally decide to keep **kwargs by setting keep=True:\n\n@delegates(baz, keep=True)\ndef foo(c, a, **kwargs):\n return c + baz(a, **kwargs)\n\ninspect.signature(foo)\n\n<Signature (c, a, *, b: int = 2, **kwargs)>\n\n\nIt is important to note that only parameters with default parameters are included. For example, in the below scenario only c, but NOT e and d are included in the signature of foo after applying delegates:\n\ndef basefoo(e, d, c=2): pass\n\n@delegates(basefoo)\ndef foo(a, b=1, **kwargs): pass\ninspect.signature(foo) # e and d are not included b/c they don't have default parameters.\n\n<Signature (a, b=1, *, c=2)>\n\n\nThe reason that required arguments (i.e. those without default parameters) are automatically excluded is that you should be explicitly implementing required arguments into your function’s signature rather than relying on delegates.\nAdditionally, you can exclude specific parameters from being included in the signature with the but parameter. In the example below, we exclude the parameter d:\n\ndef basefoo(e, c=2, d=3): pass\n\n@delegates(basefoo, but= ['d'])\ndef foo(a, b=1, **kwargs): pass\n\ntest_sig(foo, '(a, b=1, *, c=2)')\ninspect.signature(foo)\n\n<Signature (a, b=1, *, c=2)>\n\n\nYou can also use delegates between methods in a class. Here is an example of delegates with class methods:\n\n# example 1: class methods\nclass _T():\n @classmethod\n def foo(cls, a=1, b=2):\n pass\n \n @classmethod\n @delegates(foo)\n def bar(cls, c=3, **kwargs):\n pass\n\ntest_sig(_T.bar, '(c=3, *, a=1, b=2)')\n\nHere is the same example with instance methods:\n\n# example 2: instance methods\nclass _T():\n def foo(self, a=1, b=2):\n pass\n \n @delegates(foo)\n def bar(self, c=3, **kwargs):\n pass\n\nt = _T()\ntest_sig(t.bar, '(c=3, *, a=1, b=2)')\n\nYou can also delegate between classes. By default, the delegates decorator will delegate to the superclass:\n\nclass BaseFoo:\n def __init__(self, e, c=2): pass\n\n@delegates()# since no argument was passsed here we delegate to the superclass\nclass Foo(BaseFoo):\n def __init__(self, a, b=1, **kwargs): super().__init__(**kwargs)\n\ntest_sig(Foo, '(a, b=1, *, c=2)')\n\n\nsource\n\n\nmethod\n\n method (f)\n\nMark f as a method\nThe method function is used to change a function’s type to a method. In the below example we change the type of a from a function to a method:\n\ndef a(x=2): return x + 1\nassert type(a).__name__ == 'function'\n\na = method(a)\nassert type(a).__name__ == 'method'\n\n\nsource\n\n\nfuncs_kwargs\n\n funcs_kwargs (as_method=False)\n\nReplace methods in cls._methods with those from kwargs\nThe func_kwargs decorator allows you to add a list of functions or methods to an existing class. You must set this list as a class attribute named _methods when defining your class. Additionally, you must incldue the **kwargs argument in the ___init__ method of your class.\nAfter defining your class this way, you can add functions to your class upon instantation as illusrated below.\nFor example, we define class T to allow adding the function b to class T as follows (note that this function is stored as an attribute of T and doesn’t have access to cls or self):\n\n@funcs_kwargs\nclass T:\n _methods=['b'] # allows you to add method b upon instantiation\n def __init__(self, f=1, **kwargs): pass # don't forget to include **kwargs in __init__\n def a(self): return 1\n def b(self): return 2\n \nt = T()\ntest_eq(t.a(), 1)\ntest_eq(t.b(), 2)\n\nBecause we defined the class T this way, the signature of T indicates the option to add the function or method(s) specified in _methods. In this example, b is added to the signature:\n\ntest_sig(T, '(f=1, *, b=None)')\ninspect.signature(T)\n\n<Signature (f=1, *, b=None)>\n\n\nYou can now add the function b to class T upon instantiation:\n\ndef _new_func(): return 5\n\nt = T(b = _new_func)\ntest_eq(t.b(), 5)\n\nIf you try to add a function with a name not listed in _methods it will be ignored. In the below example, the attempt to add a function named a is ignored:\n\nt = T(a = lambda:3)\ntest_eq(t.a(), 1) # the attempt to add a is ignored and uses the original method instead.\n\nNote that you can also add methods not defined in the original class as long it is specified in the _methods attribute:\n\n@funcs_kwargs\nclass T:\n _methods=['c']\n def __init__(self, f=1, **kwargs): pass\n\nt = T(c = lambda: 4)\ntest_eq(t.c(), 4)\n\nUntil now, these examples showed how to add functions stored as an instance attribute without access to self. However, if you need access to self you can set as_method=True in the func_kwargs decorator to add a method instead:\n\ndef _f(self,a=1): return self.num + a # access the num attribute from the instance\n\n@funcs_kwargs(as_method=True)\nclass T: \n _methods=['b']\n num = 5\n \nt = T(b = _f) # adds method b\ntest_eq(t.b(5), 10) # self.num + 5 = 10\n\nHere is an example of how you might use this functionality with inheritence:\n\ndef _f(self,a=1): return self.num * a #multiply instead of add \n\nclass T2(T):\n def __init__(self,num):\n super().__init__(b = _f) # add method b from the super class\n self.num=num\n \nt = T2(num=3)\ntest_eq(t.b(a=5), 15) # 3 * 5 = 15\ntest_sig(T2, '(num)')", + "crumbs": [ + "Meta" + ] + }, + { + "objectID": "xml.html", + "href": "xml.html", + "title": "XML", + "section": "", + "text": "from IPython.display import Markdown\nfrom pprint import pprint\n\nfrom fastcore.test import test_eq", + "crumbs": [ + "XML" + ] + }, + { + "objectID": "xml.html#ft-functions", + "href": "xml.html#ft-functions", + "title": "XML", + "section": "FT functions", + "text": "FT functions\n\nsource\n\nattrmap\n\n attrmap (o)\n\n\nsource\n\n\nvalmap\n\n valmap (o)\n\n\nsource\n\n\nFT\n\n FT (tag:str, cs:tuple, attrs:dict=None, void_=False, **kwargs)\n\nA ‘Fast Tag’ structure, containing tag,children,and attrs\n\nsource\n\n\nft\n\n ft (tag:str, *c, void_:bool=False, attrmap:<built-\n infunctioncallable>=<function attrmap>, valmap:<built-\n infunctioncallable>=<function valmap>, ft_cls=<class '__main__.FT'>,\n **kw)\n\nCreate an FT structure for to_xml()\nThe main HTML tags are exported as ft partials.\nAttributes are passed as keywords. Use ‘klass’ and ‘fr’ instead of ‘class’ and ‘for’, to avoid Python reserved word clashes.\n\nsource\n\n\nHtml\n\n Html (*c, doctype=True, **kwargs)\n\nAn HTML tag, optionally preceeded by !DOCTYPE HTML\n\nsamp = Html(\n Head(Title('Some page')),\n Body(Div('Some text\\nanother line', (Input(name=\"jph's\"), Img(src=\"filename\", data=1)),\n cls=['myclass', 'another'],\n style={'padding':1, 'margin':2}))\n)\npprint(samp)\n\n(!doctype((),{'html': True}),\n html((head((title(('Some page',),{}),),{}), body((div(('Some text\\nanother line', input((),{'name': \"jph's\"}), img((),{'src': 'filename', 'data': 1})),{'class': 'myclass another', 'style': 'padding:1; margin:2'}),),{})),{}))\n\n\n\nelem = P('Some text', id=\"myid\")\nprint(elem.tag)\nprint(elem.children)\nprint(elem.attrs)\n\np\n('Some text',)\n{'id': 'myid'}\n\n\nYou can get and set attrs directly:\n\nelem.id = 'newid'\nprint(elem.id, elem.get('id'), elem.get('foo', 'missing'))\nelem\n\nnewid newid missing\n\n\np(('Some text',),{'id': 'newid'})\n\n\n\nsource\n\n\nSafe\n*str(object=’’) -> str str(bytes_or_buffer[, encoding[, errors]]) -> str\nCreate a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.__str__() (if defined) or repr(object). encoding defaults to sys.getdefaultencoding(). errors defaults to ‘strict’.*", + "crumbs": [ + "XML" + ] + }, + { + "objectID": "xml.html#conversion-to-xmlhtml", + "href": "xml.html#conversion-to-xmlhtml", + "title": "XML", + "section": "Conversion to XML/HTML", + "text": "Conversion to XML/HTML\n\nsource\n\nto_xml\n\n to_xml (elm, lvl=0, indent=True, do_escape=True)\n\nConvert ft element tree into an XML string\n\nh = to_xml(samp, do_escape=False)\nprint(h)\n\n<!doctype html>\n<html>\n <head>\n <title>Some page</title>\n </head>\n <body>\n <div class=\"myclass another\" style=\"padding:1; margin:2\">\nSome text\nanother line <input name=\"jph's\">\n<img src=\"filename\" data=\"1\"> </div>\n </body>\n</html>\n\n\n\n\nclass PageTitle:\n def __ft__(self): return H1(\"Hello\")\n\nclass HomePage:\n def __ft__(self): return Div(PageTitle(), Div('hello'))\n\nh = to_xml(Div(HomePage()))\nexpected_output = \"\"\"<div>\n <div>\n <h1>Hello</h1>\n <div>hello</div>\n </div>\n</div>\n\"\"\"\nassert h == expected_output\n\n\nprint(h)\n\n<div>\n <div>\n <h1>Hello</h1>\n <div>hello</div>\n </div>\n</div>\n\n\n\n\nh = to_xml(samp, indent=False)\nprint(h)\n\n<!doctype html><html><head><title>Some page</title></head><body><div class=\"myclass another\" style=\"padding:1; margin:2\">Some text\nanother line<input name=\"jph's\"><img src=\"filename\" data=\"1\"></div></body></html>\n\n\nInteroperability both directions with Django and Jinja using the html() protocol:\n\ndef _esc(s): return s.__html__() if hasattr(s, '__html__') else Safe(escape(s))\n\nr = Safe('<b>Hello from Django</b>')\nprint(to_xml(Div(r)))\nprint(_esc(Div(P('Hello from fastcore <3'))))\n\n<div><b>Hello from Django</b></div>\n\n<div>\n <p>Hello from fastcore <3</p>\n</div>", + "crumbs": [ + "XML" + ] + }, + { + "objectID": "xml.html#display", + "href": "xml.html#display", + "title": "XML", + "section": "Display", + "text": "Display\n\nsource\n\nhighlight\n\n highlight (s, lang='html')\n\nMarkdown to syntax-highlight s in language lang\n\nsource\n\n\nshowtags\n\n showtags (s)\n\nYou can also reorder the children to come after the attrs, if you use this alternative syntax for FT where the children are in a second pair of () (behind the scenes this is because FT implements __call__ to add children).\n\nBody(klass='myclass')(\n Div(style='padding:3px')(\n 'Some text 1<2',\n I(spurious=True)('in italics'),\n Input(name='me'),\n Img(src=\"filename\", data=1)\n )\n)\n\n<body class=\"myclass\">\n <div style=\"padding:3px\">\nSome text 1<2<i spurious>in italics</i> <input name=\"me\">\n<img src=\"filename\" data=\"1\"> </div>\n</body>\n\n\n\nsource\n\n\ngetattr\n\n __getattr__ (tag)", + "crumbs": [ + "XML" + ] + }, + { + "objectID": "transform.html", + "href": "transform.html", + "title": "Transforms", + "section": "", + "text": "from __future__ import annotations\nfrom nbdev.showdoc import *\nfrom fastcore.test import *\nfrom fastcore.nb_imports import *\n\nThe classes here provide functionality for creating a composition of partially reversible functions. By “partially reversible” we mean that a transform can be decoded, creating a form suitable for display. This is not necessarily identical to the original form (e.g. a transform that changes a byte tensor to a float tensor does not recreate a byte tensor when decoded, since that may lose precision, and a float tensor can be displayed already).\nClasses are also provided and for composing transforms, and mapping them over collections. Pipeline is a transform which composes several Transform, knowing how to decode them or show an encoded item.\n\nsource\n\nTransform\n\n Transform (enc=None, dec=None, split_idx=None, order=None)\n\nDelegates (__call__,decode,setup) to (encodes,decodes,setups) if split_idx matches\nA Transform is the main building block of the fastai data pipelines. In the most general terms a transform can be any function you want to apply to your data, however the Transform class provides several mechanisms that make the process of building them easy and flexible.\n\n\nThe main Transform features:\n\nType dispatch - Type annotations are used to determine if a transform should be applied to the given argument. It also gives an option to provide several implementations and it choses the one to run based on the type. This is useful for example when running both independent and dependent variables through the pipeline where some transforms only make sense for one and not the other. Another usecase is designing a transform that handles different data formats. Note that if a transform takes multiple arguments only the type of the first one is used for dispatch.\nHandling of tuples - When a tuple (or a subclass of tuple) of data is passed to a transform it will get applied to each element separately. You can opt out of this behavior by passing a list or an L, as only tuples gets this specific behavior. An alternative is to use ItemTransform defined below, which will always take the input as a whole.\nReversability - A transform can be made reversible by implementing the decodes method. This is mainly used to turn something like a category which is encoded as a number back into a label understandable by humans for showing purposes. Like the regular call method, the decode method that is used to decode will be applied over each element of a tuple separately.\nType propagation - Whenever possible a transform tries to return data of the same type it received. Mainly used to maintain semantics of things like ArrayImage which is a thin wrapper of pytorch’s Tensor. You can opt out of this behavior by adding ->None return type annotation.\nPreprocessing - The setup method can be used to perform any one-time calculations to be later used by the transform, for example generating a vocabulary to encode categorical data.\nFiltering based on the dataset type - By setting the split_idx flag you can make the transform be used only in a specific DataSource subset like in training, but not validation.\nOrdering - You can set the order attribute which the Pipeline uses when it needs to merge two lists of transforms.\nAppending new behavior with decorators - You can easily extend an existing Transform by creating encodes or decodes methods for new data types. You can put those new methods outside the original transform definition and decorate them with the class you wish them patched into. This can be used by the fastai library users to add their own behavior, or multiple modules contributing to the same transform.\n\n\n\nDefining a Transform\nThere are a few ways to create a transform with different ratios of simplicity to flexibility. - Extending the Transform class - Use inheritence to implement the methods you want. - Passing methods to the constructor - Instantiate the Transform class and pass your functions as enc and dec arguments. - @Transform decorator - Turn any function into a Transform by just adding a decorator - very straightforward if all you need is a single encodes implementation. - Passing a function to fastai APIs - Same as above, but when passing a function to other transform aware classes like Pipeline or TfmdDS you don’t even need a decorator. Your function will get converted to a Transform automatically.\nA simple way to create a Transform is to pass a function to the constructor. In the below example, we pass an anonymous function that does integer division by 2:\n\nf = Transform(lambda o:o//2)\n\nIf you call this transform, it will apply the transformation:\n\ntest_eq_type(f(2), 1)\n\nAnother way to define a Transform is to extend the Transform class:\n\nclass A(Transform): pass\n\nHowever, to enable your transform to do something, you have to define an encodes method. Note that we can use the class name as a decorator to add this method to the original class.\n\n@A\ndef encodes(self, x): return x+1\n\nf1 = A()\ntest_eq(f1(1), 2) # f1(1) is the same as f1.encode(1)\n\nIn addition to adding an encodes method, we can also add a decodes method. This enables you to call the decode method (without an s). For more information about the purpose of decodes, see the discussion about Reversibility in the above section.\nJust like with encodes, you can add a decodes method to the original class by using the class name as a decorator:\n\nclass B(A): pass\n\n@B\ndef decodes(self, x): return x-1\n\nf2 = B()\ntest_eq(f2.decode(2), 1)\n\ntest_eq(f2(1), 2) # uses A's encode method from the parent class\n\nIf you do not define an encodes or decodes method the original value will be returned:\n\nclass _Tst(Transform): pass \n\nf3 = _Tst() # no encodes or decodes method have been defined\ntest_eq_type(f3.decode(2.0), 2.0)\ntest_eq_type(f3(2), 2)\n\nTransforms can be created from class methods too:\n\nclass A:\n @classmethod\n def create(cls, x:int): return x+1\ntest_eq(Transform(A.create)(1), 2)\n\n\nDefining Transforms With A Decorator\nTransform can be used as a decorator to turn a function into a Transform.\n\n@Transform\ndef f(x): return x//2\ntest_eq_type(f(2), 1)\ntest_eq_type(f.decode(2.0), 2.0)\n\n@Transform\ndef f(x): return x*2\ntest_eq_type(f(2), 4)\ntest_eq_type(f.decode(2.0), 2.0)\n\n\n\nTyped Dispatch and Transforms\nWe can also apply different transformations depending on the type of the input passed by using TypedDispatch. TypedDispatch automatically works with Transform when using type hints:\n\nclass A(Transform): pass\n\n@A\ndef encodes(self, x:int): return x//2\n\n@A\ndef encodes(self, x:float): return x+1\n\nWhen we pass in an int, this calls the first encodes method:\n\nf = A()\ntest_eq_type(f(3), 1)\n\nWhen we pass in a float, this calls the second encodes method:\n\ntest_eq_type(f(2.), 3.)\n\nWhen we pass in a type that is not specified in encodes, the original value is returned:\n\ntest_eq(f('a'), 'a')\n\nIf the type annotation is a tuple, then any type in the tuple will match:\n\nclass MyClass(int): pass\n\nclass A(Transform):\n def encodes(self, x:MyClass|float): return x/2\n def encodes(self, x:str|list): return str(x)+'_1'\n\nf = A()\n\nThe below two examples match the first encodes, with a type of MyClass and float, respectively:\n\ntest_eq(f(MyClass(2)), 1.) # input is of type MyClass \ntest_eq(f(6.0), 3.0) # input is of type float\n\nThe next two examples match the second encodes method, with a type of str and list, respectively:\n\ntest_eq(f('a'), 'a_1') # input is of type str\ntest_eq(f(['a','b','c']), \"['a', 'b', 'c']_1\") # input is of type list\n\n\n\nCasting Types With Transform\nWithout any intervention it is easy for operations to change types in Python. For example, FloatSubclass (defined below) becomes a float after performing multiplication:\n\nclass FloatSubclass(float): pass\ntest_eq_type(FloatSubclass(3.0) * 2, 6.0)\n\nThis behavior is often not desirable when performing transformations on data. Therefore, Transform will attempt to cast the output to be of the same type as the input by default. In the below example, the output will be cast to a FloatSubclass type to match the type of the input:\n\n@Transform\ndef f(x): return x*2\n\ntest_eq_type(f(FloatSubclass(3.0)), FloatSubclass(6.0))\n\nWe can optionally turn off casting by annotating the transform function with a return type of None:\n\n@Transform\ndef f(x)-> None: return x*2 # Same transform as above, but with a -> None annotation\n\ntest_eq_type(f(FloatSubclass(3.0)), 6.0) # Casting is turned off because of -> None annotation\n\nHowever, Transform will only cast output back to the input type when the input is a subclass of the output. In the below example, the input is of type FloatSubclass which is not a subclass of the output which is of type str. Therefore, the output doesn’t get cast back to FloatSubclass and stays as type str:\n\n@Transform\ndef f(x): return str(x)\n \ntest_eq_type(f(Float(2.)), '2.0')\n\nJust like encodes, the decodes method will cast outputs to match the input type in the same way. In the below example, the output of decodes remains of type MySubclass:\n\nclass MySubclass(int): pass\n\ndef enc(x): return MySubclass(x+1)\ndef dec(x): return x-1\n\n\nf = Transform(enc,dec)\nt = f(1) # t is of type MySubclass\ntest_eq_type(f.decode(t), MySubclass(1)) # the output of decode is cast to MySubclass to match the input type.\n\n\n\nApply Transforms On Subsets With split_idx\nYou can apply transformations to subsets of data by specifying a split_idx property. If a transform has a split_idx then it’s only applied if the split_idx param matches. In the below example, we set split_idx equal to 1:\n\ndef enc(x): return x+1\ndef dec(x): return x-1\nf = Transform(enc,dec)\nf.split_idx = 1\n\nThe transformations are applied when a matching split_idx parameter is passed:\n\ntest_eq(f(1, split_idx=1),2)\ntest_eq(f.decode(2, split_idx=1),1)\n\nOn the other hand, transformations are ignored when the split_idx parameter does not match:\n\ntest_eq(f(1, split_idx=0), 1)\ntest_eq(f.decode(2, split_idx=0), 2)\n\n\n\nTransforms on Lists\nTransform operates on lists as a whole, not element-wise:\n\nclass A(Transform):\n def encodes(self, x): return dict(x)\n def decodes(self, x): return list(x.items())\n \nf = A()\n_inp = [(1,2), (3,4)]\nt = f(_inp)\n\ntest_eq(t, dict(_inp))\ntest_eq(f.decodes(t), _inp)\n\nIf you want a transform to operate on a list elementwise, you must implement this appropriately in the encodes and decodes methods:\n\nclass AL(Transform): pass\n\n@AL\ndef encodes(self, x): return [x_+1 for x_ in x]\n\n@AL\ndef decodes(self, x): return [x_-1 for x_ in x]\n\nf = AL()\nt = f([1,2])\n\ntest_eq(t, [2,3])\ntest_eq(f.decode(t), [1,2])\n\n\n\nTransforms on Tuples\nUnlike lists, Transform operates on tuples element-wise.\n\ndef neg_int(x): return -x\nf = Transform(neg_int)\n\ntest_eq(f((1,2,3)), (-1,-2,-3))\n\nTransforms will also apply TypedDispatch element-wise on tuples when an input type annotation is specified. In the below example, the values 1.0 and 3.0 are ignored because they are of type float, not int:\n\ndef neg_int(x:int): return -x\nf = Transform(neg_int)\n\ntest_eq(f((1.0, 2, 3.0)), (1.0, -2, 3.0))\n\nAnother example of how Transform can use TypedDispatch with tuples is shown below:\n\nclass B(Transform): pass\n\n@B\ndef encodes(self, x:int): return x+1\n\n@B\ndef encodes(self, x:str): return x+'hello'\n\n@B\ndef encodes(self, x): return str(x)+'!'\n\nIf the input is not an int or str, the third encodes method will apply:\n\nb = B()\ntest_eq(b([1]), '[1]!') \ntest_eq(b([1.0]), '[1.0]!')\n\nHowever, if the input is a tuple, then the appropriate method will apply according to the type of each element in the tuple:\n\ntest_eq(b(('1',)), ('1hello',))\ntest_eq(b((1,2)), (2,3))\ntest_eq(b(('a',1.0)), ('ahello','1.0!'))\n\nDispatching over tuples works recursively, by the way:\n\nclass B(Transform):\n def encodes(self, x:int): return x+1\n def encodes(self, x:str): return x+'_hello'\n def decodes(self, x:int): return x-1\n def decodes(self, x:str): return x.replace('_hello', '')\n\nf = B()\nstart = (1.,(2,'3'))\nt = f(start)\ntest_eq_type(t, (1.,(3,'3_hello')))\ntest_eq(f.decode(t), start)\n\nDispatching also works with typing module type classes, like numbers.integral:\n\n@Transform\ndef f(x:numbers.Integral): return x+1\n\nt = f((1,'1',1))\ntest_eq(t, (2, '1', 2))\n\n\nsource\n\n\n\nInplaceTransform\n\n InplaceTransform (enc=None, dec=None, split_idx=None, order=None)\n\nA Transform that modifies in-place and just returns whatever it’s passed\n\nclass A(InplaceTransform): pass\n\n@A\ndef encodes(self, x:pd.Series): x.fillna(10, inplace=True)\n \nf = A()\n\ntest_eq_type(f(pd.Series([1,2,None])),pd.Series([1,2,10],dtype=np.float64)) #fillna fills with floats.\n\n\nsource\n\n\nDisplayedTransform\n\n DisplayedTransform (enc=None, dec=None, split_idx=None, order=None)\n\nA transform with a __repr__ that shows its attrs\nTransforms normally are represented by just their class name and a list of encodes and decodes implementations:\n\nclass A(Transform): encodes,decodes = noop,noop\nf = A()\nf\n\nA:\nencodes: (object,object) -> noop\ndecodes: (object,object) -> noop\n\n\nA DisplayedTransform will in addition show the contents of all attributes listed in the comma-delimited string self.store_attrs:\n\nclass A(DisplayedTransform):\n encodes = noop\n def __init__(self, a, b=2):\n super().__init__()\n store_attr()\n \nA(a=1,b=2)\n\nA -- {'a': 1, 'b': 2}:\nencodes: (object,object) -> noop\ndecodes: \n\n\n\nsource\n\n\nItemTransform\n\n ItemTransform (enc=None, dec=None, split_idx=None, order=None)\n\nA transform that always take tuples as items\nItemTransform is the class to use to opt out of the default behavior of Transform.\n\nclass AIT(ItemTransform): \n def encodes(self, xy): x,y=xy; return (x+y,y)\n def decodes(self, xy): x,y=xy; return (x-y,y)\n \nf = AIT()\ntest_eq(f((1,2)), (3,2))\ntest_eq(f.decode((3,2)), (1,2))\n\nIf you pass a special tuple subclass, the usual retain type behavior of Transform will keep it:\n\nclass _T(tuple): pass\nx = _T((1,2))\ntest_eq_type(f(x), _T((3,2)))\n\n\nsource\n\n\nget_func\n\n get_func (t, name, *args, **kwargs)\n\nGet the t.name (potentially partial-ized with args and kwargs) or noop if not defined\nThis works for any kind of t supporting getattr, so a class or a module.\n\ntest_eq(get_func(operator, 'neg', 2)(), -2)\ntest_eq(get_func(operator.neg, '__call__')(2), -2)\ntest_eq(get_func(list, 'foobar')([2]), [2])\na = [2,1]\nget_func(list, 'sort')(a)\ntest_eq(a, [1,2])\n\nTransforms are built with multiple-dispatch: a given function can have several methods depending on the type of the object received. This is done directly with the TypeDispatch module and type-annotation in Transform, but you can also use the following class.\n\nsource\n\n\nFunc\n\n Func (name, *args, **kwargs)\n\nBasic wrapper around a name with args and kwargs to call on a given type\nYou can call the Func object on any module name or type, even a list of types. It will return the corresponding function (with a default to noop if nothing is found) or list of functions.\n\ntest_eq(Func('sqrt')(math), math.sqrt)\n\n\n\n\nSig\n\n Sig (*args, **kwargs)\n\nSig is just sugar-syntax to create a Func object more easily with the syntax Sig.name(*args, **kwargs).\n\nf = Sig.sqrt()\ntest_eq(f(math), math.sqrt)\n\n\nsource\n\n\ncompose_tfms\n\n compose_tfms (x, tfms, is_enc=True, reverse=False, **kwargs)\n\nApply all func_nm attribute of tfms on x, maybe in reverse order\n\ndef to_int (x): return Int(x)\ndef to_float(x): return Float(x)\ndef double (x): return x*2\ndef half(x)->None: return x/2\n\n\ndef test_compose(a, b, *fs): test_eq_type(compose_tfms(a, tfms=map(Transform,fs)), b)\n\ntest_compose(1, Int(1), to_int)\ntest_compose(1, Float(1), to_int,to_float)\ntest_compose(1, Float(2), to_int,to_float,double)\ntest_compose(2.0, 2.0, to_int,double,half)\n\n\nclass A(Transform):\n def encodes(self, x:float): return Float(x+1)\n def decodes(self, x): return x-1\n \ntfms = [A(), Transform(math.sqrt)]\nt = compose_tfms(3., tfms=tfms)\ntest_eq_type(t, Float(2.))\ntest_eq(compose_tfms(t, tfms=tfms, is_enc=False), 1.)\ntest_eq(compose_tfms(4., tfms=tfms, reverse=True), 3.)\n\n\ntfms = [A(), Transform(math.sqrt)]\ntest_eq(compose_tfms((9,3.), tfms=tfms), (3,2.))\n\n\nsource\n\n\nmk_transform\n\n mk_transform (f)\n\nConvert function f to Transform if it isn’t already one\n\nsource\n\n\ngather_attrs\n\n gather_attrs (o, k, nm)\n\nUsed in getattr to collect all attrs k from self.{nm}\n\nsource\n\n\ngather_attr_names\n\n gather_attr_names (o, nm)\n\nUsed in dir to collect all attrs k from self.{nm}\n\nsource\n\n\nPipeline\n\n Pipeline (funcs=None, split_idx=None)\n\nA pipeline of composed (for encode/decode) transforms, setup with types\n\nadd_docs(Pipeline,\n __call__=\"Compose `__call__` of all `fs` on `o`\",\n decode=\"Compose `decode` of all `fs` on `o`\",\n show=\"Show `o`, a single item from a tuple, decoding as needed\",\n add=\"Add transforms `ts`\",\n setup=\"Call each tfm's `setup` in order\")\n\nPipeline is a wrapper for compose_tfms. You can pass instances of Transform or regular functions in funcs, the Pipeline will wrap them all in Transform (and instantiate them if needed) during the initialization. It handles the transform setup by adding them one at a time and calling setup on each, goes through them in order in __call__ or decode and can show an object by applying decoding the transforms up until the point it gets an object that knows how to show itself.\n\n# Empty pipeline is noop\npipe = Pipeline()\ntest_eq(pipe(1), 1)\ntest_eq(pipe((1,)), (1,))\n# Check pickle works\nassert pickle.loads(pickle.dumps(pipe))\n\n\nclass IntFloatTfm(Transform):\n def encodes(self, x): return Int(x)\n def decodes(self, x): return Float(x)\n foo=1\n\nint_tfm=IntFloatTfm()\n\ndef neg(x): return -x\nneg_tfm = Transform(neg, neg)\n\n\npipe = Pipeline([neg_tfm, int_tfm])\n\nstart = 2.0\nt = pipe(start)\ntest_eq_type(t, Int(-2))\ntest_eq_type(pipe.decode(t), Float(start))\ntest_stdout(lambda:pipe.show(t), '-2')\n\n\npipe = Pipeline([neg_tfm, int_tfm])\nt = pipe(start)\ntest_stdout(lambda:pipe.show(pipe((1.,2.))), '-1\\n-2')\ntest_eq(pipe.foo, 1)\nassert 'foo' in dir(pipe)\nassert 'int_float_tfm' in dir(pipe)\n\nYou can add a single transform or multiple transforms ts using Pipeline.add. Transforms will be ordered by Transform.order.\n\npipe = Pipeline([neg_tfm, int_tfm])\nclass SqrtTfm(Transform):\n order=-1\n def encodes(self, x): \n return x**(.5)\n def decodes(self, x): return x**2\npipe.add(SqrtTfm())\ntest_eq(pipe(4),-2)\ntest_eq(pipe.decode(-2),4)\npipe.add([SqrtTfm(),SqrtTfm()])\ntest_eq(pipe(256),-2)\ntest_eq(pipe.decode(-2),256)\n\nTransforms are available as attributes named with the snake_case version of the names of their types. Attributes in transforms can be directly accessed as attributes of the pipeline.\n\ntest_eq(pipe.int_float_tfm, int_tfm)\ntest_eq(pipe.foo, 1)\n\npipe = Pipeline([int_tfm, int_tfm])\npipe.int_float_tfm\ntest_eq(pipe.int_float_tfm[0], int_tfm)\ntest_eq(pipe.foo, [1,1])\n\n\n# Check opposite order\npipe = Pipeline([int_tfm,neg_tfm])\nt = pipe(start)\ntest_eq(t, -2)\ntest_stdout(lambda:pipe.show(t), '-2')\n\n\nclass A(Transform):\n def encodes(self, x): return int(x)\n def decodes(self, x): return Float(x)\n\npipe = Pipeline([neg_tfm, A])\nt = pipe(start)\ntest_eq_type(t, -2)\ntest_eq_type(pipe.decode(t), Float(start))\ntest_stdout(lambda:pipe.show(t), '-2.0')\n\n\ns2 = (1,2)\npipe = Pipeline([neg_tfm, A])\nt = pipe(s2)\ntest_eq_type(t, (-1,-2))\ntest_eq_type(pipe.decode(t), (Float(1.),Float(2.)))\ntest_stdout(lambda:pipe.show(t), '-1.0\\n-2.0')\n\n\nfrom PIL import Image\n\n\nclass ArrayImage(ndarray):\n _show_args = {'cmap':'viridis'}\n def __new__(cls, x, *args, **kwargs):\n if isinstance(x,tuple): super().__new__(cls, x, *args, **kwargs)\n if args or kwargs: raise RuntimeError('Unknown array init args')\n if not isinstance(x,ndarray): x = array(x)\n return x.view(cls)\n \n def show(self, ctx=None, figsize=None, **kwargs):\n if ctx is None: _,ctx = plt.subplots(figsize=figsize)\n ctx.imshow(im, **{**self._show_args, **kwargs})\n ctx.axis('off')\n return ctx\n \nim = Image.open(TEST_IMAGE)\nim_t = ArrayImage(im)\n\n\ndef f1(x:ArrayImage): return -x\ndef f2(x): return Image.open(x).resize((128,128))\ndef f3(x:Image.Image): return(ArrayImage(array(x)))\n\n\npipe = Pipeline([f2,f3,f1])\nt = pipe(TEST_IMAGE)\ntest_eq(type(t), ArrayImage)\ntest_eq(t, -array(f3(f2(TEST_IMAGE))))\n\n\npipe = Pipeline([f2,f3])\nt = pipe(TEST_IMAGE)\nax = pipe.show(t)\n\n\n\n\n\n\n\n\n\n#test_fig_exists(ax)\n\n\n#Check filtering is properly applied\nadd1 = B()\nadd1.split_idx = 1\npipe = Pipeline([neg_tfm, A(), add1])\ntest_eq(pipe(start), -2)\npipe.split_idx=1\ntest_eq(pipe(start), -1)\npipe.split_idx=0\ntest_eq(pipe(start), -2)\nfor t in [None, 0, 1]:\n pipe.split_idx=t\n test_eq(pipe.decode(pipe(start)), start)\n test_stdout(lambda: pipe.show(pipe(start)), \"-2.0\")\n\n\ndef neg(x): return -x\ntest_eq(type(mk_transform(neg)), Transform)\ntest_eq(type(mk_transform(math.sqrt)), Transform)\ntest_eq(type(mk_transform(lambda a:a*2)), Transform)\ntest_eq(type(mk_transform(Pipeline([neg]))), Pipeline)\n\n\n\nMethods\n\n#TODO: method examples\n\n\nsource\n\n\nPipeline.__call__\n\n Pipeline.__call__ (o)\n\nCall self as a function.\n\nsource\n\n\nPipeline.decode\n\n Pipeline.decode (o, full=True)\n\n\nsource\n\n\nPipeline.setup\n\n Pipeline.setup (items=None, train_setup=False)\n\nDuring the setup, the Pipeline starts with no transform and adds them one at a time, so that during its setup, each transform gets the items processed up to its point and not after.", + "crumbs": [ + "Transforms" + ] + }, + { + "objectID": "parallel.html", + "href": "parallel.html", + "title": "Parallel", + "section": "", + "text": "from fastcore.test import *\nfrom nbdev.showdoc import *\nfrom fastcore.nb_imports import *\n\n\nsource\n\nthreaded\n\n threaded (process=False)\n\nRun f in a Thread (or Process if process=True), and returns it\n\n@threaded\ndef _1():\n time.sleep(0.05)\n print(\"second\")\n return 5\n\n@threaded\ndef _2():\n time.sleep(0.01)\n print(\"first\")\n\na = _1()\n_2()\ntime.sleep(0.1)\n\nfirst\nsecond\n\n\nAfter the thread is complete, the return value is stored in the result attr.\n\na.result\n\n5\n\n\n\nsource\n\n\nstartthread\n\n startthread (f)\n\nLike threaded, but start thread immediately\n\n@startthread\ndef _():\n time.sleep(0.05)\n print(\"second\")\n\n@startthread\ndef _():\n time.sleep(0.01)\n print(\"first\")\n\ntime.sleep(0.1)\n\nfirst\nsecond\n\n\n\nsource\n\n\nstartproc\n\n startproc (f)\n\nLike threaded(True), but start Process immediately\n\n@startproc\ndef _():\n time.sleep(0.05)\n print(\"second\")\n\n@startproc\ndef _():\n time.sleep(0.01)\n print(\"first\")\n\ntime.sleep(0.1)\n\nfirst\nsecond\n\n\n\nsource\n\n\nparallelable\n\n parallelable (param_name, num_workers, f=None)\n\n\nsource\n\nThreadPoolExecutor\n\n ThreadPoolExecutor (max_workers=4, on_exc=<built-in function print>,\n pause=0, **kwargs)\n\nSame as Python’s ThreadPoolExecutor, except can pass max_workers==0 for serial execution\n\nsource\n\n\nProcessPoolExecutor\n\n ProcessPoolExecutor (max_workers=4, on_exc=<built-in function print>,\n pause=0, mp_context=None, initializer=None,\n initargs=())\n\nSame as Python’s ProcessPoolExecutor, except can pass max_workers==0 for serial execution\n\nsource\n\n\n\nparallel\n\n parallel (f, items, *args, n_workers=4, total=None, progress=None,\n pause=0, method=None, threadpool=False, timeout=None,\n chunksize=1, **kwargs)\n\nApplies func in parallel to items, using n_workers\n\ninp,exp = range(50),range(1,51)\n\ntest_eq(parallel(_add_one, inp, n_workers=2), exp)\ntest_eq(parallel(_add_one, inp, threadpool=True, n_workers=2), exp)\ntest_eq(parallel(_add_one, inp, n_workers=1, a=2), range(2,52))\ntest_eq(parallel(_add_one, inp, n_workers=0), exp)\ntest_eq(parallel(_add_one, inp, n_workers=0, a=2), range(2,52))\n\nUse the pause parameter to ensure a pause of pause seconds between processes starting. This is in case there are race conditions in starting some process, or to stagger the time each process starts, for example when making many requests to a webserver. Set threadpool=True to use ThreadPoolExecutor instead of ProcessPoolExecutor.\n\nfrom datetime import datetime\n\n\ndef print_time(i): \n time.sleep(random.random()/1000)\n print(i, datetime.now())\n\nparallel(print_time, range(5), n_workers=2, pause=0.25);\n\n0 2024-10-11 23:06:05.920741\n1 2024-10-11 23:06:06.171470\n2 2024-10-11 23:06:06.431925\n3 2024-10-11 23:06:06.689940\n4 2024-10-11 23:06:06.937109\n\n\n\nsource\n\n\nparallel_async\n\n parallel_async (f, items, *args, n_workers=16, timeout=None, chunksize=1,\n on_exc=<built-in function print>, **kwargs)\n\nApplies f to items in parallel using asyncio and a semaphore to limit concurrency.\n\nimport asyncio\n\n\nasync def print_time_async(i): \n wait = random.random()\n await asyncio.sleep(wait)\n print(i, datetime.now(), wait)\n\nawait parallel_async(print_time_async, range(6), n_workers=3);\n\n0 2024-10-11 23:06:39.545583 0.10292732609738675\n3 2024-10-11 23:06:39.900393 0.3516179734831676\n4 2024-10-11 23:06:39.941094 0.03699593757956876\n2 2024-10-11 23:06:39.957677 0.5148658606540902\n1 2024-10-11 23:06:40.099716 0.6574035385815227\n5 2024-10-11 23:06:40.654097 0.7116319667399102\n\n\n\nsource\n\n\nrun_procs\n\n run_procs (f, f_done, args)\n\nCall f for each item in args in parallel, yielding f_done\n\nsource\n\n\nparallel_gen\n\n parallel_gen (cls, items, n_workers=4, **kwargs)\n\nInstantiate cls in n_workers procs & call each on a subset of items in parallel.\n\n# class _C:\n# def __call__(self, o): return ((i+1) for i in o)\n\n# items = range(5)\n\n# res = L(parallel_gen(_C, items, n_workers=0))\n# idxs,dat1 = zip(*res.sorted(itemgetter(0)))\n# test_eq(dat1, range(1,6))\n\n# res = L(parallel_gen(_C, items, n_workers=3))\n# idxs,dat2 = zip(*res.sorted(itemgetter(0)))\n# test_eq(dat2, dat1)\n\ncls is any class with __call__. It will be passed args and kwargs when initialized. Note that n_workers instances of cls are created, one in each process. items are then split in n_workers batches and one is sent to each cls. The function then returns a generator of tuples of item indices and results.\n\nclass TestSleepyBatchFunc:\n \"For testing parallel processes that run at different speeds\"\n def __init__(self): self.a=1\n def __call__(self, batch):\n for k in batch:\n time.sleep(random.random()/4)\n yield k+self.a\n\nx = np.linspace(0,0.99,20)\n\nres = L(parallel_gen(TestSleepyBatchFunc, x, n_workers=2))\ntest_eq(res.sorted().itemgot(1), x+1)\n\n\n\n\n\n\n\n\n\n# #|hide\n# from subprocess import Popen, PIPE\n# # test num_workers > 0 in scripts works when python process start method is spawn\n# process = Popen([\"python\", \"parallel_test.py\"], stdout=PIPE)\n# _, err = process.communicate(timeout=10)\n# exit_code = process.wait()\n# test_eq(exit_code, 0)", + "crumbs": [ + "Parallel" + ] + }, + { + "objectID": "py2pyi.html#basics", + "href": "py2pyi.html#basics", + "title": "Create delegated pyi", + "section": "Basics", + "text": "Basics\n\nsource\n\nimp_mod\n\n imp_mod (module_path, package=None)\n\nImport dynamically the module referenced in fn\n\nfn = Path('test_py2pyi.py')\n\n\nmod = imp_mod(fn)\na = mod.A()\na.h()\n\n1\n\n\n\ntree = _get_tree(mod)\n\n\n\n\nAST.__repr__\n\n AST.__repr__ ()\n\n\n# for o in enumerate(tree.body): print(o)\n\n\nnode = tree.body[4]\nnode\n\ndef f(a: int, b: str='a') -> str:\n \"\"\"I am f\"\"\"\n return 1\n\n\n\nisinstance(node, functypes)\n\nTrue\n\n\n\nsource\n\n\nhas_deco\n\n has_deco (node:Union[ast.FunctionDef,ast.AsyncFunctionDef], name:str)\n\nCheck if a function node node has a decorator named name\n\nnm = 'delegates'\nhas_deco(node, nm)\n\nFalse\n\n\n\nnode = tree.body[5]\nnode\n\n@delegates(f)\ndef g(c, d: X, **kwargs) -> str:\n \"\"\"I am g\"\"\"\n return 2\n\n\n\nhas_deco(node, nm)\n\nTrue", + "crumbs": [ + "Create delegated pyi" + ] + }, + { + "objectID": "py2pyi.html#function-processing", + "href": "py2pyi.html#function-processing", + "title": "Create delegated pyi", + "section": "Function processing", + "text": "Function processing\n\ndef _proc_body (node, mod): print('_proc_body', type(node))\ndef _proc_func (node, mod): print('_proc_func', type(node))\ndef _proc_class (node, mod): print('_proc_class', type(node))\ndef _proc_patched(node, mod): print('_proc_patched', type(node))\n\n\nparent_node = copy.deepcopy(tree.body[7])\npatched_node = copy.deepcopy(tree.body[10])\ntest_is(has_deco(patched_node, \"patch\"), True)\ntest_eq(str(patched_node.args.args[0].annotation), parent_node.name)\n\n_clean_patched_node(patched_node)\ntest_is(has_deco(patched_node, \"patch\"), False)\ntest_eq(patched_node.args.args[0].annotation, None)\n\n\nempty_cls1, empty_cls2, empty_cls3 = ast.parse('''\nclass A: \n \"\"\"An empty class.\"\"\"\nclass B: \n pass\nclass C: \n ...\n''').body\n\ntest_is(_is_empty_class(empty_cls1), True)\ntest_is(_is_empty_class(empty_cls2), True)\ntest_is(_is_empty_class(empty_cls3), True)\n\nnon_empty_cls, empty_func = ast.parse('''\nclass A: \n a = 1\ndef f():\n ...\n''').body\ntest_is(_is_empty_class(non_empty_cls), False)\ntest_is(_is_empty_class(empty_func), False)\n\n\n# we could have reused `parent_node` and `patched_node` from the previous cells.\n# copying them here allows us to run this cell multiple times which makes it a little easier to write tests.\n\nparent_node = copy.deepcopy(tree.body[7])\npatched_node = copy.deepcopy(tree.body[11])\ntest_eq(len(parent_node.body),1)\n_add_patched_node_to_parent(patched_node, parent_node)\ntest_eq(len(parent_node.body),2)\ntest_eq(parent_node.body[-1], patched_node)\n\n# patched node replaces an existing class method (A.h)\npatched_h_node = ast.parse(\"\"\"\n@patch\ndef h(self: A, *args, **kwargs):\n ...\n\"\"\", mode='single').body[0]\n\n_add_patched_node_to_parent(patched_h_node, parent_node)\ntest_eq(len(parent_node.body), 2)\ntest_eq(parent_node.body[0], patched_h_node)\n\n# patched node is added to an empty class\nempty_cls, patched_node = ast.parse('''\nclass Z: \n \"\"\"An empty class.\"\"\"\n\n@patch\ndef a(self: Z, *args, **kwargs):\n ...\n''').body\n\ntest_eq(len(empty_cls.body), 1)\ntest_ne(empty_cls.body[0], patched_node)\n_add_patched_node_to_parent(patched_node, empty_cls)\ntest_eq(len(empty_cls.body), 1)\ntest_eq(empty_cls.body[0], patched_node)\n\n\nraw_tree = _get_tree(mod)\nprocessed_tree = _proc_mod(mod)\nn_raw_tree_nodes = len(raw_tree.body)\n# mod contains 3 patch methods so our processed_tree should have 3 less nodes \ntest_eq(len(processed_tree.body), n_raw_tree_nodes-3)\n\n_proc_class <class 'ast.ClassDef'>\n_proc_body <class 'ast.FunctionDef'>\n_proc_func <class 'ast.FunctionDef'>\n_proc_body <class 'ast.FunctionDef'>\n_proc_class <class 'ast.ClassDef'>\n_proc_class <class 'ast.ClassDef'>\n_proc_patched <class 'ast.FunctionDef'>\n_proc_patched <class 'ast.FunctionDef'>\n_proc_body <class 'ast.FunctionDef'>\n\n\n\n_proc_mod(mod);\n\n_proc_class <class 'ast.ClassDef'>\n_proc_body <class 'ast.FunctionDef'>\n_proc_func <class 'ast.FunctionDef'>\n_proc_body <class 'ast.FunctionDef'>\n_proc_class <class 'ast.ClassDef'>\n_proc_class <class 'ast.ClassDef'>\n_proc_patched <class 'ast.FunctionDef'>\n_proc_patched <class 'ast.FunctionDef'>\n_proc_body <class 'ast.FunctionDef'>\n\n\n\nnode.name\n\n'g'\n\n\n\nsym = getattr(mod, node.name)\nsym\n\n<function test_py2pyi.g(c, d: test_py2pyi.X, *, b: str = 'a') -> str>\n\n\n\nsig = signature(sym)\nprint(sig)\n\n(c, d: test_py2pyi.X, *, b: str = 'a') -> str\n\n\n\nsource\n\nsig2str\n\n sig2str (sig)\n\n\nsource\n\n\nast_args\n\n ast_args (func)\n\n\nnewargs = ast_args(sym)\nnewargs\n\nc, d: test_py2pyi.X, *, b: str='a'\n\n\n\nnode.args\n\nc, d: X, **kwargs\n\n\n\nnode.args = newargs\nnode\n\n@delegates(f)\ndef g(c, d: test_py2pyi.X, *, b: str='a') -> str:\n \"\"\"I am g\"\"\"\n return 2\n\n\n\n_body_ellip(node)\nnode\n\n@delegates(f)\ndef g(c, d: test_py2pyi.X, *, b: str='a') -> str:\n \"\"\"I am g\"\"\"\n ...\n\n\n\ntree = _get_tree(mod)\nnode = tree.body[5]\nnode\n\n@delegates(f)\ndef g(c, d: X, **kwargs) -> str:\n \"\"\"I am g\"\"\"\n return 2\n\n\n\n_update_func(node, sym)\nnode\n\ndef g(c, d: X, *, b: str='a') -> str:\n \"\"\"I am g\"\"\"\n ...\n\n\n\ntree = _proc_mod(mod)\ntree.body[5]\n\n_proc_class <class 'ast.ClassDef'>\n_proc_class <class 'ast.ClassDef'>\n_proc_class <class 'ast.ClassDef'>\n_proc_patched <class 'ast.FunctionDef'>\n_proc_patched <class 'ast.FunctionDef'>\n\n\ndef g(c, d: X, *, b: str='a') -> str:\n \"\"\"I am g\"\"\"\n ...", + "crumbs": [ + "Create delegated pyi" + ] + }, + { + "objectID": "py2pyi.html#patch", + "href": "py2pyi.html#patch", + "title": "Create delegated pyi", + "section": "Patch", + "text": "Patch\n\ntree = _get_tree(mod)\nnode = tree.body[9]\nnode\n\n@patch\n@delegates(j)\ndef k(self: (A, B), b: bool=False, **kwargs):\n return 1\n\n\n\nann = node.args.args[0].annotation\n\n\nif hasattr(ann, 'elts'): ann = ann.elts[0]\n\n\nnm = ann.id\nnm\n\n'A'\n\n\n\ncls = getattr(mod, nm)\nsym = getattr(cls, node.name)\n\n\nsig2str(signature(sym))\n\n\"(self: (test_py2pyi.A, test_py2pyi.B), b: bool = False, *, d: str = 'a')\"\n\n\n\n_update_func(node, sym)\n\n\nnode\n\n@patch\ndef k(self: (A, B), b: bool=False, *, d: str='a'):\n ...\n\n\n\ntree = _get_tree(mod)\ntree.body[9]\n\n@patch\n@delegates(j)\ndef k(self: (A, B), b: bool=False, **kwargs):\n return 1", + "crumbs": [ + "Create delegated pyi" + ] + }, + { + "objectID": "py2pyi.html#class-and-file", + "href": "py2pyi.html#class-and-file", + "title": "Create delegated pyi", + "section": "Class and file", + "text": "Class and file\n\ntree = _get_tree(mod)\nnode = tree.body[7]\nnode\n\nclass A:\n\n @delegates(j)\n def h(self, b: bool=False, **kwargs):\n a = 1\n return a\n\n\n\nnode.body\n\n[@delegates(j)\n def h(self, b: bool=False, **kwargs):\n a = 1\n return a]\n\n\n\ntree = _proc_mod(mod)\ntree.body[7]\n\nclass A:\n\n def h(self, b: bool=False, *, d: str='a'):\n ...\n\n def k(self, b: bool=False, *, d: str='a'):\n ...\n\n def m(self, b: bool=False, *, d: str='a'):\n ...\n\n def n(self, b: bool=False, **kwargs):\n \"\"\"No delegates here mmm'k?\"\"\"\n ...\n\n\n\nsource\n\ncreate_pyi\n\n create_pyi (fn, package=None)\n\nConvert fname.py to fname.pyi by removing function bodies and expanding delegates kwargs\n\ncreate_pyi(fn)\n\n\n# fn = Path('/Users/jhoward/git/fastcore/fastcore/docments.py')\n# create_pyi(fn, 'fastcore')", + "crumbs": [ + "Create delegated pyi" + ] + }, + { + "objectID": "py2pyi.html#script", + "href": "py2pyi.html#script", + "title": "Create delegated pyi", + "section": "Script", + "text": "Script\n\nsource\n\npy2pyi\n\n py2pyi (fname:str, package:str=None)\n\nConvert fname.py to fname.pyi by removing function bodies and expanding delegates kwargs\n\n\n\n\nType\nDefault\nDetails\n\n\n\n\nfname\nstr\n\nThe file name to convert\n\n\npackage\nstr\nNone\nThe parent package\n\n\n\n\nsource\n\n\nreplace_wildcards\n\n replace_wildcards (path:str)\n\nExpand wildcard imports in the specified Python file.\n\n\n\n\nType\nDetails\n\n\n\n\npath\nstr\nPath to the Python file to process", + "crumbs": [ + "Create delegated pyi" + ] + }, + { + "objectID": "test.html", + "href": "test.html", + "title": "Test", + "section": "", + "text": "We can check that code raises an exception when that’s expected (test_fail).\nTo test for equality or inequality (with different types of things) we define a simple function test that compares two objects with a given cmp operator.\n\nsource\n\n\n\n test_fail (f, msg='', contains='', args=None, kwargs=None)\n\nFails with msg unless f() raises an exception and (optionally) has contains in e.args\n\ndef _fail(): raise Exception(\"foobar\")\ntest_fail(_fail, contains=\"foo\")\n\ndef _fail(): raise Exception()\ntest_fail(_fail)\n\nWe can also pass args and kwargs to function to check if it fails with special inputs.\n\ndef _fail_args(a):\n if a == 5:\n raise ValueError\ntest_fail(_fail_args, args=(5,))\ntest_fail(_fail_args, kwargs=dict(a=5))\n\n\nsource\n\n\n\n\n test (a, b, cmp, cname=None)\n\nassert that cmp(a,b); display inputs and cname or cmp.__name__ if it fails\n\ntest([1,2],[1,2], operator.eq)\ntest_fail(lambda: test([1,2],[1], operator.eq))\ntest([1,2],[1], operator.ne)\ntest_fail(lambda: test([1,2],[1,2], operator.ne))\n\n\n\n\n\n\n all_equal (a, b)\n\nCompares whether a and b are the same length and have the same contents\n\ntest(['abc'], ['abc'], all_equal)\ntest_fail(lambda: test(['abc'],['cab'], all_equal))\n\n\n\n\n\n\n equals (a, b)\n\nCompares a and b for equality; supports sublists, tensors and arrays too\n\ntest([['abc'],['a']], [['abc'],['a']], equals)\ntest([['abc'],['a'],'b', [['x']]], [['abc'],['a'],'b', [['x']]], equals) # supports any depth and nested structure\n\n\nsource\n\n\n\n\n nequals (a, b)\n\nCompares a and b for not equals\n\ntest(['abc'], ['ab' ], nequals)", + "crumbs": [ + "Test" + ] + }, + { + "objectID": "test.html#simple-test-functions", + "href": "test.html#simple-test-functions", + "title": "Test", + "section": "", + "text": "We can check that code raises an exception when that’s expected (test_fail).\nTo test for equality or inequality (with different types of things) we define a simple function test that compares two objects with a given cmp operator.\n\nsource\n\n\n\n test_fail (f, msg='', contains='', args=None, kwargs=None)\n\nFails with msg unless f() raises an exception and (optionally) has contains in e.args\n\ndef _fail(): raise Exception(\"foobar\")\ntest_fail(_fail, contains=\"foo\")\n\ndef _fail(): raise Exception()\ntest_fail(_fail)\n\nWe can also pass args and kwargs to function to check if it fails with special inputs.\n\ndef _fail_args(a):\n if a == 5:\n raise ValueError\ntest_fail(_fail_args, args=(5,))\ntest_fail(_fail_args, kwargs=dict(a=5))\n\n\nsource\n\n\n\n\n test (a, b, cmp, cname=None)\n\nassert that cmp(a,b); display inputs and cname or cmp.__name__ if it fails\n\ntest([1,2],[1,2], operator.eq)\ntest_fail(lambda: test([1,2],[1], operator.eq))\ntest([1,2],[1], operator.ne)\ntest_fail(lambda: test([1,2],[1,2], operator.ne))\n\n\n\n\n\n\n all_equal (a, b)\n\nCompares whether a and b are the same length and have the same contents\n\ntest(['abc'], ['abc'], all_equal)\ntest_fail(lambda: test(['abc'],['cab'], all_equal))\n\n\n\n\n\n\n equals (a, b)\n\nCompares a and b for equality; supports sublists, tensors and arrays too\n\ntest([['abc'],['a']], [['abc'],['a']], equals)\ntest([['abc'],['a'],'b', [['x']]], [['abc'],['a'],'b', [['x']]], equals) # supports any depth and nested structure\n\n\nsource\n\n\n\n\n nequals (a, b)\n\nCompares a and b for not equals\n\ntest(['abc'], ['ab' ], nequals)", + "crumbs": [ + "Test" + ] + }, + { + "objectID": "test.html#test_eq-test_ne-etc", + "href": "test.html#test_eq-test_ne-etc", + "title": "Test", + "section": "test_eq test_ne, etc…", + "text": "test_eq test_ne, etc…\nJust use test_eq/test_ne to test for ==/!=. test_eq_type checks things are equal and of the same type. We define them using test:\n\nsource\n\ntest_eq\n\n test_eq (a, b)\n\ntest that a==b\n\ntest_eq([1,2],[1,2])\ntest_eq([1,2],map(int,[1,2]))\ntest_eq(array([1,2]),array([1,2]))\ntest_eq(array([1,2]),array([1,2]))\ntest_eq([array([1,2]),3],[array([1,2]),3])\ntest_eq(dict(a=1,b=2), dict(b=2,a=1))\ntest_fail(lambda: test_eq([1,2], 1), contains=\"==\")\ntest_fail(lambda: test_eq(None, np.array([1,2])), contains=\"==\")\ntest_eq({'a', 'b', 'c'}, {'c', 'a', 'b'})\n\n\ndf1 = pd.DataFrame(dict(a=[1,2],b=['a','b']))\ndf2 = pd.DataFrame(dict(a=[1,2],b=['a','b']))\ndf3 = pd.DataFrame(dict(a=[1,2],b=['a','c']))\n\ntest_eq(df1,df2)\ntest_eq(df1.a,df2.a)\ntest_fail(lambda: test_eq(df1,df3), contains='==')\nclass T(pd.Series): pass\ntest_eq(df1.iloc[0], T(df2.iloc[0])) # works with subclasses\n\n\ntest_eq(torch.zeros(10), torch.zeros(10, dtype=torch.float64))\ntest_eq(torch.zeros(10), torch.ones(10)-1)\ntest_fail(lambda:test_eq(torch.zeros(10), torch.ones(1, 10)), contains='==')\ntest_eq(torch.zeros(3), [0,0,0])\n\n\nsource\n\n\ntest_eq_type\n\n test_eq_type (a, b)\n\ntest that a==b and are same type\n\ntest_eq_type(1,1)\ntest_fail(lambda: test_eq_type(1,1.))\ntest_eq_type([1,1],[1,1])\ntest_fail(lambda: test_eq_type([1,1],(1,1)))\ntest_fail(lambda: test_eq_type([1,1],[1,1.]))\n\n\nsource\n\n\ntest_ne\n\n test_ne (a, b)\n\ntest that a!=b\n\ntest_ne([1,2],[1])\ntest_ne([1,2],[1,3])\ntest_ne(array([1,2]),array([1,1]))\ntest_ne(array([1,2]),array([1,1]))\ntest_ne([array([1,2]),3],[array([1,2])])\ntest_ne([3,4],array([3]))\ntest_ne([3,4],array([3,5]))\ntest_ne(dict(a=1,b=2), ['a', 'b'])\ntest_ne(['a', 'b'], dict(a=1,b=2))\n\n\nsource\n\n\nis_close\n\n is_close (a, b, eps=1e-05)\n\nIs a within eps of b\n\nsource\n\n\ntest_close\n\n test_close (a, b, eps=1e-05)\n\ntest that a is within eps of b\n\ntest_close(1,1.001,eps=1e-2)\ntest_fail(lambda: test_close(1,1.001))\ntest_close([-0.001,1.001], [0.,1.], eps=1e-2)\ntest_close(np.array([-0.001,1.001]), np.array([0.,1.]), eps=1e-2)\ntest_close(array([-0.001,1.001]), array([0.,1.]), eps=1e-2)\n\n\nsource\n\n\ntest_is\n\n test_is (a, b)\n\ntest that a is b\n\ntest_fail(lambda: test_is([1], [1]))\na = [1]\ntest_is(a, a)\nb = [2]; test_fail(lambda: test_is(a, b))\n\n\nsource\n\n\ntest_shuffled\n\n test_shuffled (a, b)\n\ntest that a and b are shuffled versions of the same sequence of items\n\na = list(range(50))\nb = copy(a)\nrandom.shuffle(b)\ntest_shuffled(a,b)\ntest_fail(lambda:test_shuffled(a,a))\n\n\na = 'abc'\nb = 'abcabc'\ntest_fail(lambda:test_shuffled(a,b))\n\n\na = ['a', 42, True] \nb = [42, True, 'a']\ntest_shuffled(a,b)\n\n\nsource\n\n\ntest_stdout\n\n test_stdout (f, exp, regex=False)\n\nTest that f prints exp to stdout, optionally checking as regex\n\ntest_stdout(lambda: print('hi'), 'hi')\ntest_fail(lambda: test_stdout(lambda: print('hi'), 'ho'))\ntest_stdout(lambda: 1+1, '')\ntest_stdout(lambda: print('hi there!'), r'^hi.*!$', regex=True)\n\n\nsource\n\n\ntest_warns\n\n test_warns (f, show=False)\n\n\ntest_warns(lambda: warnings.warn(\"Oh no!\"))\ntest_fail(lambda: test_warns(lambda: 2+2), contains='No warnings raised')\n\n\ntest_warns(lambda: warnings.warn(\"Oh no!\"), show=True)\n\n<class 'UserWarning'>: Oh no!\n\n\n\nim = Image.open(TEST_IMAGE).resize((128,128)); im\n\n\n\n\n\n\n\n\n\nim = Image.open(TEST_IMAGE_BW).resize((128,128)); im\n\n\n\n\n\n\n\n\n\nsource\n\n\ntest_fig_exists\n\n test_fig_exists (ax)\n\nTest there is a figure displayed in ax\n\nfig,ax = plt.subplots()\nax.imshow(array(im));\n\n\n\n\n\n\n\n\n\ntest_fig_exists(ax)\n\n\nsource\n\n\nExceptionExpected\n\n ExceptionExpected (ex=<class 'Exception'>, regex='')\n\nContext manager that tests if an exception is raised\n\ndef _tst_1(): assert False, \"This is a test\"\ndef _tst_2(): raise SyntaxError\n\nwith ExceptionExpected(): _tst_1()\nwith ExceptionExpected(ex=AssertionError, regex=\"This is a test\"): _tst_1()\nwith ExceptionExpected(ex=SyntaxError): _tst_2()\n\nexception is an abbreviation for ExceptionExpected().\n\nwith exception: _tst_1()", + "crumbs": [ + "Test" + ] + }, + { + "objectID": "script.html", + "href": "script.html", + "title": "Script - CLI", + "section": "", + "text": "Part of fast.ai’s toolkit for delightful developer experiences.", + "crumbs": [ + "Script - CLI" + ] + }, + { + "objectID": "script.html#overview", + "href": "script.html#overview", + "title": "Script - CLI", + "section": "Overview", + "text": "Overview\nSometimes, you want to create a quick script, either for yourself, or for others. But in Python, that involves a whole lot of boilerplate and ceremony, especially if you want to support command line arguments, provide help, and other niceties. You can use argparse for this purpose, which comes with Python, but it’s complex and verbose.\nfastcore.script makes life easier. There are much fancier modules to help you write scripts (we recommend Python Fire, and Click is also popular), but fastcore.script is very fast and very simple. In fact, it’s <50 lines of code! Basically, it’s just a little wrapper around argparse that uses modern Python features and some thoughtful defaults to get rid of the boilerplate.\nFor full details, see the docs for core.", + "crumbs": [ + "Script - CLI" + ] + }, + { + "objectID": "script.html#example", + "href": "script.html#example", + "title": "Script - CLI", + "section": "Example", + "text": "Example\nHere’s a complete example (available in examples/test_fastcore.py):\nfrom fastcore.script import *\n@call_parse\ndef main(msg:str, # The message\n upper:bool): # Convert to uppercase?\n \"Print `msg`, optionally converting to uppercase\"\n print(msg.upper() if upper else msg)\nIf you copy that info a file and run it, you’ll see:\n$ examples/test_fastcore.py --help\nusage: test_fastcore.py [-h] [--upper] msg\n\nPrint `msg`, optionally converting to uppercase\n\npositional arguments:\n msg The message\n\noptional arguments:\n -h, --help show this help message and exit\n --upper Convert to uppercase? (default: False)\nAs you see, we didn’t need any if __name__ == \"__main__\", we didn’t have to parse arguments, we just wrote a function, added a decorator to it, and added some annotations to our function’s parameters. As a bonus, we can also use this function directly from a REPL such as Jupyter Notebook - it’s not just for command line scripts!\nYou should provide a default (after the =) for any optional parameters. If you don’t provide a default for a parameter, then it will be a positional parameter.", + "crumbs": [ + "Script - CLI" + ] + }, + { + "objectID": "script.html#param-annotations", + "href": "script.html#param-annotations", + "title": "Script - CLI", + "section": "Param annotations", + "text": "Param annotations\nIf you want to use the full power of argparse, you can do so by using Param annotations instead of type annotations and docments, like so:\nfrom fastcore.script import *\n@call_parse\ndef main(msg:Param(\"The message\", str),\n upper:Param(\"Convert to uppercase?\", store_true)):\n \"Print `msg`, optionally converting to uppercase\"\n print(msg.upper() if upper else msg)\nIf you use this approach, then each parameter in your function should have an annotation Param(...) (as in the example above). You can pass the following when calling Param: help,type,opt,action,nargs,const,choices,required . Except for opt, all of these are just passed directly to argparse, so you have all the power of that module at your disposal. Generally you’ll want to pass at least help (since this is provided as the help string for that parameter) and type (to ensure that you get the type of data you expect). opt is a bool that defines whether a param is optional or required (positional) - but you’ll generally not need to set this manually, because fastcore.script will set it for you automatically based on default values.", + "crumbs": [ + "Script - CLI" + ] + }, + { + "objectID": "script.html#setuptools-scripts", + "href": "script.html#setuptools-scripts", + "title": "Script - CLI", + "section": "setuptools scripts", + "text": "setuptools scripts\nThere’s a really nice feature of pip/setuptools that lets you create commandline scripts directly from functions, makes them available in the PATH, and even makes your scripts cross-platform (e.g. in Windows it creates an exe). fastcore.script supports this feature too. The trick to making a function available as a script is to add a console_scripts section to your setup file, of the form: script_name=module:function_name. E.g. in this case we use: test_fastcore.script=fastcore.script.test_cli:main. With this, you can then just type test_fastcore.script at any time, from any directory, and your script will be called (once it’s installed using one of the methods below).\nYou don’t actually have to write a setup.py yourself. Instead, just use nbdev. Then modify settings.ini as appropriate for your module/script. To install your script directly, you can type pip install -e .. Your script, when installed this way (it’s called an editable install), will automatically be up to date even if you edit it - there’s no need to reinstall it after editing. With nbdev you can even make your module and script available for installation directly from pip and conda by running make release.", + "crumbs": [ + "Script - CLI" + ] + }, + { + "objectID": "script.html#api-details", + "href": "script.html#api-details", + "title": "Script - CLI", + "section": "API details", + "text": "API details\n\nsource\n\nstore_true\n\n store_true ()\n\nPlaceholder to pass to Param for store_true action\n\nsource\n\n\nstore_false\n\n store_false ()\n\nPlaceholder to pass to Param for store_false action\n\nsource\n\n\nbool_arg\n\n bool_arg (v)\n\nUse as type for Param to get bool behavior\n\nsource\n\n\nclean_type_str\n\n clean_type_str (x:str)\n\n\nclass Test: pass\n\ntest_eq(clean_type_str(argparse.ArgumentParser), 'argparse.ArgumentParser')\ntest_eq(clean_type_str(Test), 'Test')\ntest_eq(clean_type_str(int), 'int')\ntest_eq(clean_type_str(float), 'float')\ntest_eq(clean_type_str(store_false), 'store_false')\n\n\nsource\n\n\nParam\n\n Param (help='', type=None, opt=True, action=None, nargs=None, const=None,\n choices=None, required=None, default=None)\n\nA parameter in a function used in anno_parser or call_parse\n\ntest_eq(repr(Param(\"Help goes here\")), '<Help goes here>')\ntest_eq(repr(Param(\"Help\", int)), 'int <Help>')\ntest_eq(repr(Param(help=None, type=int)), 'int')\ntest_eq(repr(Param(help=None, type=None)), '')\n\nEach parameter in your function should have an annotation Param(...). You can pass the following when calling Param: help,type,opt,action,nargs,const,choices,required (i.e. it takes the same parameters as argparse.ArgumentParser.add_argument, plus opt). Except for opt, all of these are just passed directly to argparse, so you have all the power of that module at your disposal. Generally you’ll want to pass at least help (since this is provided as the help string for that parameter) and type (to ensure that you get the type of data you expect).\nopt is a bool that defines whether a param is optional or required (positional) - but you’ll generally not need to set this manually, because fastcore.script will set it for you automatically based on default values. You should provide a default (after the =) for any optional parameters. If you don’t provide a default for a parameter, then it will be a positional parameter.\nParam’s __repr__ also allows for more informative function annotation when looking up the function’s doc using shift+tab. You see the type annotation (if there is one) and the accompanying help documentation with it.\n\ndef f(required:Param(\"Required param\", int),\n a:Param(\"param 1\", bool_arg),\n b:Param(\"param 2\", str)=\"test\"):\n \"my docs\"\n ...\n\n\nhelp(f)\n\nHelp on function f in module __main__:\n\nf(required: int <Required param>, a: bool_arg <param 1>, b: str <param 2> = 'test')\n my docs\n\n\n\n\np = Param(help=\"help\", type=int)\np.set_default(1)\ntest_eq(p.kwargs, {'help': 'help (default: 1)', 'type': int, 'default': 1})\n\n\nsource\n\n\nanno_parser\n\n anno_parser (func, prog:str=None)\n\nLook at params (annotated with Param) in func and return an ArgumentParser\n\n\n\n\nType\nDefault\nDetails\n\n\n\n\nfunc\n\n\nFunction to get arguments from\n\n\nprog\nstr\nNone\nThe name of the program\n\n\n\nThis converts a function with parameter annotations of type Param into an argparse.ArgumentParser object. Function arguments with a default provided are optional, and other arguments are positional.\n\n_en = str_enum('_en', 'aa','bb','cc')\ndef f(required:Param(\"Required param\", int),\n a:Param(\"param 1\", bool_arg),\n b:Param(\"param 2\", str)=\"test\",\n c:Param(\"param 3\", _en)=_en.aa):\n \"my docs\"\n ...\n\np = anno_parser(f, 'progname')\np.print_help()\n\nusage: progname [-h] [--b B] [--c {aa,bb,cc}] required a\n\nmy docs\n\npositional arguments:\n required Required param\n a param 1\n\noptional arguments:\n -h, --help show this help message and exit\n --b B param 2 (default: test)\n --c {aa,bb,cc} param 3 (default: aa)\n\n\nIt also works with type annotations and docments:\n\ndef g(required:int, # Required param\n a:bool_arg, # param 1\n b=\"test\", # param 2\n c:_en=_en.aa): # param 3\n \"my docs\"\n ...\n\np = anno_parser(g, 'progname')\np.print_help()\n\nusage: progname [-h] [--b B] [--c {aa,bb,cc}] required a\n\nmy docs\n\npositional arguments:\n required Required param\n a param 1\n\noptional arguments:\n -h, --help show this help message and exit\n --b B param 2 (default: test)\n --c {aa,bb,cc} param 3 (default: aa)\n\n\n\nsource\n\n\nargs_from_prog\n\n args_from_prog (func, prog)\n\nExtract args from prog\nSometimes it’s convenient to extract arguments from the actual name of the called program. args_from_prog will do this, assuming that names and values of the params are separated by a #. Optionally there can also be a prefix separated by ## (double underscore).\n\nexp = {'a': False, 'b': 'baa'}\ntest_eq(args_from_prog(f, 'foo##a#0#b#baa'), exp)\ntest_eq(args_from_prog(f, 'a#0#b#baa'), exp)\n\n\nsource\n\n\ncall_parse\n\n call_parse (func=None, nested=False)\n\nDecorator to create a simple CLI from func using anno_parser\n\n@call_parse\ndef test_add(\n a:int=0, # param a\n b:int=0 # param 1\n):\n \"Add up `a` and `b`\"\n return a + b\n\ncall_parse decorated functions work as regular functions and also as command-line interface functions.\n\ntest_eq(test_add(1,2), 3)\n\nThis is the main way to use fastcore.script; decorate your function with call_parse, add Param annotations (as shown above) or type annotations and docments, and it can then be used as a script.\nUse the nested keyword argument to create nested parsers, where earlier parsers consume only their known args from sys.argv before later parsers are used. This is useful to create one command line application that executes another. For example:\nmyrunner --keyword 1 script.py -- <script.py args>\nA separating -- after the first application’s args is recommended though not always required, otherwise args may be parsed in unexpected ways. For example:\nmyrunner script.py -h\nwould display myrunner’s help and not script.py’s.", + "crumbs": [ + "Script - CLI" + ] + }, + { + "objectID": "xdg.html", + "href": "xdg.html", + "title": "XDG", + "section": "", + "text": "See the XDG Base Directory Specification for more information.", + "crumbs": [ + "XDG" + ] + }, + { + "objectID": "xdg.html#overview", + "href": "xdg.html#overview", + "title": "XDG", + "section": "Overview", + "text": "Overview\nxdg_cache_home, xdg_config_home, xdg_data_home, and xdg_state_home return pathlib.Path objects containing the value of the environment variable named XDG_CACHE_HOME, XDG_CONFIG_HOME, XDG_DATA_HOME, and XDG_STATE_HOME respectively, or the default defined in the specification if the environment variable is unset, empty, or contains a relative path rather than absolute path.\nxdg_config_dirs and xdg_data_dirs return a list of pathlib.Path objects containing the value, split on colons, of the environment variable named XDG_CONFIG_DIRS and XDG_DATA_DIRS respectively, or the default defined in the specification if the environment variable is unset or empty. Relative paths are ignored, as per the specification.\nxdg_runtime_dir returns a pathlib.Path object containing the value of the XDG_RUNTIME_DIR environment variable, or None if the environment variable is not set, or contains a relative path rather than absolute path.", + "crumbs": [ + "XDG" + ] + }, + { + "objectID": "xdg.html#helpers", + "href": "xdg.html#helpers", + "title": "XDG", + "section": "Helpers", + "text": "Helpers\nWe’ll start by defining a context manager that temporarily sets an environment variable to demonstrate the behaviour of each helper function:\n\nfrom contextlib import contextmanager\n\n\n@contextmanager\ndef env(variable, value):\n old = os.environ.get(variable, None)\n try:\n os.environ[variable] = value\n yield\n finally:\n if old is None: del os.environ[variable]\n else: os.environ[variable] = old\n\n\nsource\n\nxdg_cache_home\n\n xdg_cache_home ()\n\nPath corresponding to XDG_CACHE_HOME\n\nfrom fastcore.test import *\n\n\ntest_eq(xdg_cache_home(), Path.home()/'.cache')\nwith env('XDG_CACHE_HOME', '/home/fastai/.cache'):\n test_eq(xdg_cache_home(), Path('/home/fastai/.cache'))\n\n\nsource\n\n\nxdg_config_dirs\n\n xdg_config_dirs ()\n\nPaths corresponding to XDG_CONFIG_DIRS\n\ntest_eq(xdg_config_dirs(), [Path('/etc/xdg')])\nwith env('XDG_CONFIG_DIRS', '/home/fastai/.xdg:/home/fastai/.config'):\n test_eq(xdg_config_dirs(), [Path('/home/fastai/.xdg'), Path('/home/fastai/.config')])\n\n\nsource\n\n\nxdg_config_home\n\n xdg_config_home ()\n\nPath corresponding to XDG_CONFIG_HOME\n\ntest_eq(xdg_config_home(), Path.home()/'.config')\nwith env('XDG_CONFIG_HOME', '/home/fastai/.config'):\n test_eq(xdg_config_home(), Path('/home/fastai/.config'))\n\n\nsource\n\n\nxdg_data_dirs\n\n xdg_data_dirs ()\n\nPaths corresponding to XDG_DATA_DIRS`\n\nsource\n\n\nxdg_data_home\n\n xdg_data_home ()\n\nPath corresponding to XDG_DATA_HOME\n\ntest_eq(xdg_data_home(), Path.home()/'.local/share')\nwith env('XDG_DATA_HOME', '/home/fastai/.data'):\n test_eq(xdg_data_home(), Path('/home/fastai/.data'))\n\n\nsource\n\n\nxdg_runtime_dir\n\n xdg_runtime_dir ()\n\nPath corresponding to XDG_RUNTIME_DIR\n\nsource\n\n\nxdg_state_home\n\n xdg_state_home ()\n\nPath corresponding to XDG_STATE_HOME\n\ntest_eq(xdg_state_home(), Path.home()/'.local/state')\nwith env('XDG_STATE_HOME', '/home/fastai/.state'):\n test_eq(xdg_state_home(), Path('/home/fastai/.state'))\n\n\nCopyright © 2016-2021 Scott Stevenson scott@stevenson.io\nModifications copyright © 2022 onwards Jeremy Howard", + "crumbs": [ + "XDG" + ] + }, + { + "objectID": "docments.html", + "href": "docments.html", + "title": "Docments", + "section": "", + "text": "docments provides programmatic access to comments in function parameters and return types. It can be used to create more developer-friendly documentation, CLI, etc tools.", + "crumbs": [ + "Docments" + ] + }, + { + "objectID": "docments.html#why", + "href": "docments.html#why", + "title": "Docments", + "section": "Why?", + "text": "Why?\nWithout docments, if you want to document your parameters, you have to repeat param names in docstrings, since they’re already in the function signature. The parameters have to be kept synchronized in the two places as you change your code. Readers of your code have to look back and forth between two places to understand what’s happening. So it’s more work for you, and for your users.\nFurthermore, to have parameter documentation formatted nicely without docments, you have to use special magic docstring formatting, often with odd quirks, which is a pain to create and maintain, and awkward to read in code. For instance, using numpy-style documentation:\n\ndef add_np(a:int, b:int=0)->int:\n \"\"\"The sum of two numbers.\n \n Used to demonstrate numpy-style docstrings.\n\nParameters\n----------\na : int\n the 1st number to add\nb : int\n the 2nd number to add (default: 0)\n\nReturns\n-------\nint\n the result of adding `a` to `b`\"\"\"\n return a+b\n\nBy comparison, here’s the same thing using docments:\n\ndef add(\n a:int, # the 1st number to add\n b=0, # the 2nd number to add\n)->int: # the result of adding `a` to `b`\n \"The sum of two numbers.\"\n return a+b", + "crumbs": [ + "Docments" + ] + }, + { + "objectID": "docments.html#numpy-docstring-helper-functions", + "href": "docments.html#numpy-docstring-helper-functions", + "title": "Docments", + "section": "Numpy docstring helper functions", + "text": "Numpy docstring helper functions\ndocments also supports numpy-style docstrings, or a mix or numpy-style and docments parameter documentation. The functions in this section help get and parse this information.\n\nsource\n\ndocstring\n\n docstring (sym)\n\nGet docstring for sym for functions ad classes\n\ntest_eq(docstring(add), \"The sum of two numbers.\")\n\n\nsource\n\n\nparse_docstring\n\n parse_docstring (sym)\n\nParse a numpy-style docstring in sym\n\n# parse_docstring(add_np)\n\n\nsource\n\n\nisdataclass\n\n isdataclass (s)\n\nCheck if s is a dataclass but not a dataclass’ instance\n\nsource\n\n\nget_dataclass_source\n\n get_dataclass_source (s)\n\nGet source code for dataclass s\n\nsource\n\n\nget_source\n\n get_source (s)\n\nGet source code for string, function object or dataclass s\n\nsource\n\n\nget_name\n\n get_name (obj)\n\nGet the name of obj\n\ntest_eq(get_name(in_ipython), 'in_ipython')\ntest_eq(get_name(L.map), 'map')\n\n\nsource\n\n\nqual_name\n\n qual_name (obj)\n\nGet the qualified name of obj\n\nassert qual_name(docscrape) == 'fastcore.docscrape'", + "crumbs": [ + "Docments" + ] + }, + { + "objectID": "docments.html#docments", + "href": "docments.html#docments", + "title": "Docments", + "section": "Docments", + "text": "Docments\n\nsource\n\ndocments\n\n docments (elt, full=False, returns=True, eval_str=False)\n\nGenerates a docment\nThe returned dict has parameter names as keys, docments as values. The return value comment appears in the return, unless returns=False. Using the add definition above, we get:\n\ndef add(\n a:int, # the 1st number to add\n b=0, # the 2nd number to add\n)->int: # the result of adding `a` to `b`\n \"The sum of two numbers.\"\n return a+b\n\ndocments(add)\n\n{ 'a': 'the 1st number to add',\n 'b': 'the 2nd number to add',\n 'return': 'the result of adding `a` to `b`'}\n\n\nIf you pass full=True, the values are dict of defaults, types, and docments as values. Note that the type annotation is inferred from the default value, if the annotation is empty and a default is supplied.\n\ndocments(add, full=True)\n\n{ 'a': { 'anno': <class 'int'>,\n 'default': <class 'inspect._empty'>,\n 'docment': 'the 1st number to add'},\n 'b': { 'anno': <class 'int'>,\n 'default': 0,\n 'docment': 'the 2nd number to add'},\n 'return': { 'anno': <class 'int'>,\n 'default': <class 'inspect._empty'>,\n 'docment': 'the result of adding `a` to `b`'}}\n\n\nTo evaluate stringified annotations (from python 3.10), use eval_str:\n\ndocments(add, full=True, eval_str=True)['a']\n\n{ 'anno': <class 'int'>,\n 'default': <class 'inspect._empty'>,\n 'docment': 'the 1st number to add'}\n\n\nIf you need more space to document a parameter, place one or more lines of comments above the parameter, or above the return type. You can mix-and-match these docment styles:\n\ndef add(\n # The first operand\n a:int,\n # This is the second of the operands to the *addition* operator.\n # Note that passing a negative value here is the equivalent of the *subtraction* operator.\n b:int,\n)->int: # The result is calculated using Python's builtin `+` operator.\n \"Add `a` to `b`\"\n return a+b\n\n\ndocments(add)\n\n{ 'a': 'The first operand',\n 'b': 'This is the second of the operands to the *addition* operator.\\n'\n 'Note that passing a negative value here is the equivalent of the '\n '*subtraction* operator.',\n 'return': \"The result is calculated using Python's builtin `+` operator.\"}\n\n\nDocments works with async functions, too:\n\nasync def add_async(\n # The first operand\n a:int,\n # This is the second of the operands to the *addition* operator.\n # Note that passing a negative value here is the equivalent of the *subtraction* operator.\n b:int,\n)->int: # The result is calculated using Python's builtin `+` operator.\n \"Add `a` to `b`\"\n return a+b\n\n\ntest_eq(docments(add_async), docments(add))\n\nYou can also use docments with classes and methods:\n\nclass Adder:\n \"An addition calculator\"\n def __init__(self,\n a:int, # First operand\n b:int, # 2nd operand\n ): self.a,self.b = a,b\n \n def calculate(self\n )->int: # Integral result of addition operator\n \"Add `a` to `b`\"\n return a+b\n\n\ndocments(Adder)\n\n{'a': 'First operand', 'b': '2nd operand', 'return': None}\n\n\n\ndocments(Adder.calculate)\n\n{'return': 'Integral result of addition operator', 'self': None}\n\n\ndocments can also be extracted from numpy-style docstrings:\n\nprint(add_np.__doc__)\n\nThe sum of two numbers.\n \n Used to demonstrate numpy-style docstrings.\n\nParameters\n----------\na : int\n the 1st number to add\nb : int\n the 2nd number to add (default: 0)\n\nReturns\n-------\nint\n the result of adding `a` to `b`\n\n\n\ndocments(add_np)\n\n{ 'a': 'the 1st number to add',\n 'b': 'the 2nd number to add (default: 0)',\n 'return': 'the result of adding `a` to `b`'}\n\n\nYou can even mix and match docments and numpy parameters:\n\ndef add_mixed(a:int, # the first number to add\n b\n )->int: # the result\n \"\"\"The sum of two numbers.\n\nParameters\n----------\nb : int\n the 2nd number to add (default: 0)\"\"\"\n return a+b\n\n\ndocments(add_mixed, full=True)\n\n{ 'a': { 'anno': <class 'int'>,\n 'default': <class 'inspect._empty'>,\n 'docment': 'the first number to add'},\n 'b': { 'anno': 'int',\n 'default': <class 'inspect._empty'>,\n 'docment': 'the 2nd number to add (default: 0)'},\n 'return': { 'anno': <class 'int'>,\n 'default': <class 'inspect._empty'>,\n 'docment': 'the result'}}\n\n\nYou can use docments with dataclasses, however if the class was defined in online notebook, docments will not contain parameters’ comments. This is because the source code is not available in the notebook. After converting the notebook to a module, the docments will be available. Thus, documentation will have correct parameters’ comments.\nDocments even works with delegates:\n\nfrom fastcore.meta import delegates\n\n\ndef _a(a:int=2): return a # First\n\n@delegates(_a)\ndef _b(b:str, **kwargs): return b, (_a(**kwargs)) # Second\n\ndocments(_b)\n\n{'a': 'First', 'b': 'Second', 'return': None}", + "crumbs": [ + "Docments" + ] + }, + { + "objectID": "docments.html#extract-docstrings", + "href": "docments.html#extract-docstrings", + "title": "Docments", + "section": "Extract docstrings", + "text": "Extract docstrings\n\nsource\n\nextract_docstrings\n\n extract_docstrings (code)\n\nCreate a dict from function/class/method names to tuples of docstrings and param lists\n\nsample_code = \"\"\"\n\"This is a module.\"\n\ndef top_func(a, b, *args, **kw):\n \"This is top-level.\"\n pass\n\nclass SampleClass:\n \"This is a class.\"\n\n def __init__(self, x, y):\n \"Constructor for SampleClass.\"\n pass\n\n def method1(self, param1):\n \"This is method1.\"\n pass\n\n def _private_method(self):\n \"This should not be included.\"\n pass\n\nclass AnotherClass:\n def __init__(self, a, b):\n \"This class has no separate docstring.\"\n pass\"\"\"\n\nexp = {'_module': ('This is a module.', ''),\n 'top_func': ('This is top-level.', 'a, b, *args, **kw'),\n 'SampleClass': ('This is a class.', 'self, x, y'),\n 'SampleClass.method1': ('This is method1.', 'self, param1'),\n 'AnotherClass': ('This class has no separate docstring.', 'self, a, b')}\ntest_eq(extract_docstrings(sample_code), exp)", + "crumbs": [ + "Docments" + ] + } +] \ No newline at end of file diff --git a/site_libs/bootstrap/bootstrap-icons.css b/site_libs/bootstrap/bootstrap-icons.css new file mode 100644 index 00000000..285e4448 --- /dev/null +++ b/site_libs/bootstrap/bootstrap-icons.css @@ -0,0 +1,2078 @@ +/*! + * Bootstrap Icons v1.11.1 (https://icons.getbootstrap.com/) + * Copyright 2019-2023 The Bootstrap Authors + * Licensed under MIT (https://github.com/twbs/icons/blob/main/LICENSE) + */ + +@font-face { + font-display: block; + font-family: "bootstrap-icons"; + src: +url("./bootstrap-icons.woff?2820a3852bdb9a5832199cc61cec4e65") format("woff"); +} + +.bi::before, +[class^="bi-"]::before, +[class*=" bi-"]::before { + display: inline-block; + font-family: bootstrap-icons !important; + font-style: normal; + font-weight: normal !important; + font-variant: normal; + text-transform: none; + line-height: 1; + vertical-align: -.125em; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.bi-123::before { content: "\f67f"; } +.bi-alarm-fill::before { content: "\f101"; } +.bi-alarm::before { content: "\f102"; } +.bi-align-bottom::before { content: "\f103"; } +.bi-align-center::before { content: "\f104"; } +.bi-align-end::before { content: "\f105"; } +.bi-align-middle::before { content: "\f106"; } +.bi-align-start::before { content: "\f107"; } +.bi-align-top::before { content: "\f108"; } +.bi-alt::before { content: "\f109"; } +.bi-app-indicator::before { content: "\f10a"; } +.bi-app::before { content: "\f10b"; } +.bi-archive-fill::before { content: "\f10c"; } +.bi-archive::before { content: "\f10d"; } +.bi-arrow-90deg-down::before { content: "\f10e"; } +.bi-arrow-90deg-left::before { content: "\f10f"; } +.bi-arrow-90deg-right::before { content: "\f110"; } +.bi-arrow-90deg-up::before { content: "\f111"; } +.bi-arrow-bar-down::before { content: "\f112"; } +.bi-arrow-bar-left::before { content: "\f113"; } +.bi-arrow-bar-right::before { content: "\f114"; } +.bi-arrow-bar-up::before { content: "\f115"; } +.bi-arrow-clockwise::before { content: "\f116"; } +.bi-arrow-counterclockwise::before { content: "\f117"; } +.bi-arrow-down-circle-fill::before { content: "\f118"; } +.bi-arrow-down-circle::before { content: "\f119"; } +.bi-arrow-down-left-circle-fill::before { content: "\f11a"; } +.bi-arrow-down-left-circle::before { content: "\f11b"; } +.bi-arrow-down-left-square-fill::before { content: "\f11c"; } +.bi-arrow-down-left-square::before { content: "\f11d"; } +.bi-arrow-down-left::before { content: "\f11e"; } +.bi-arrow-down-right-circle-fill::before { content: "\f11f"; } +.bi-arrow-down-right-circle::before { content: "\f120"; } +.bi-arrow-down-right-square-fill::before { content: "\f121"; } +.bi-arrow-down-right-square::before { content: "\f122"; } +.bi-arrow-down-right::before { content: "\f123"; } +.bi-arrow-down-short::before { content: "\f124"; } +.bi-arrow-down-square-fill::before { content: "\f125"; } +.bi-arrow-down-square::before { content: "\f126"; } +.bi-arrow-down-up::before { content: "\f127"; } +.bi-arrow-down::before { content: "\f128"; } +.bi-arrow-left-circle-fill::before { content: "\f129"; } +.bi-arrow-left-circle::before { content: "\f12a"; } +.bi-arrow-left-right::before { content: "\f12b"; } +.bi-arrow-left-short::before { content: "\f12c"; } +.bi-arrow-left-square-fill::before { content: "\f12d"; } +.bi-arrow-left-square::before { content: "\f12e"; } +.bi-arrow-left::before { content: "\f12f"; } +.bi-arrow-repeat::before { content: "\f130"; } +.bi-arrow-return-left::before { content: "\f131"; } +.bi-arrow-return-right::before { content: "\f132"; } +.bi-arrow-right-circle-fill::before { content: "\f133"; } +.bi-arrow-right-circle::before { content: "\f134"; } +.bi-arrow-right-short::before { content: "\f135"; } +.bi-arrow-right-square-fill::before { content: "\f136"; } +.bi-arrow-right-square::before { content: "\f137"; } +.bi-arrow-right::before { content: "\f138"; } +.bi-arrow-up-circle-fill::before { content: "\f139"; } +.bi-arrow-up-circle::before { content: "\f13a"; } +.bi-arrow-up-left-circle-fill::before { content: "\f13b"; } +.bi-arrow-up-left-circle::before { content: "\f13c"; } +.bi-arrow-up-left-square-fill::before { content: "\f13d"; } +.bi-arrow-up-left-square::before { content: "\f13e"; } +.bi-arrow-up-left::before { content: "\f13f"; } +.bi-arrow-up-right-circle-fill::before { content: "\f140"; } +.bi-arrow-up-right-circle::before { content: "\f141"; } +.bi-arrow-up-right-square-fill::before { content: "\f142"; } +.bi-arrow-up-right-square::before { content: "\f143"; } +.bi-arrow-up-right::before { content: "\f144"; } +.bi-arrow-up-short::before { content: "\f145"; } +.bi-arrow-up-square-fill::before { content: "\f146"; } +.bi-arrow-up-square::before { content: "\f147"; } +.bi-arrow-up::before { content: "\f148"; } +.bi-arrows-angle-contract::before { content: "\f149"; } +.bi-arrows-angle-expand::before { content: "\f14a"; } +.bi-arrows-collapse::before { content: "\f14b"; } +.bi-arrows-expand::before { content: "\f14c"; } +.bi-arrows-fullscreen::before { content: "\f14d"; } +.bi-arrows-move::before { content: "\f14e"; } +.bi-aspect-ratio-fill::before { content: "\f14f"; } +.bi-aspect-ratio::before { content: "\f150"; } +.bi-asterisk::before { content: "\f151"; } +.bi-at::before { content: "\f152"; } +.bi-award-fill::before { content: "\f153"; } +.bi-award::before { content: "\f154"; } +.bi-back::before { content: "\f155"; } +.bi-backspace-fill::before { content: "\f156"; } +.bi-backspace-reverse-fill::before { content: "\f157"; } +.bi-backspace-reverse::before { content: "\f158"; } +.bi-backspace::before { content: "\f159"; } +.bi-badge-3d-fill::before { content: "\f15a"; } +.bi-badge-3d::before { content: "\f15b"; } +.bi-badge-4k-fill::before { content: "\f15c"; } +.bi-badge-4k::before { content: "\f15d"; } +.bi-badge-8k-fill::before { content: "\f15e"; } +.bi-badge-8k::before { content: "\f15f"; } +.bi-badge-ad-fill::before { content: "\f160"; } +.bi-badge-ad::before { content: "\f161"; } +.bi-badge-ar-fill::before { content: "\f162"; } +.bi-badge-ar::before { content: "\f163"; } +.bi-badge-cc-fill::before { content: "\f164"; } +.bi-badge-cc::before { content: "\f165"; } +.bi-badge-hd-fill::before { content: "\f166"; } +.bi-badge-hd::before { content: "\f167"; } +.bi-badge-tm-fill::before { content: "\f168"; } +.bi-badge-tm::before { content: "\f169"; } +.bi-badge-vo-fill::before { content: "\f16a"; } +.bi-badge-vo::before { content: "\f16b"; } +.bi-badge-vr-fill::before { content: "\f16c"; } +.bi-badge-vr::before { content: "\f16d"; } +.bi-badge-wc-fill::before { content: "\f16e"; } +.bi-badge-wc::before { content: "\f16f"; } +.bi-bag-check-fill::before { content: "\f170"; } +.bi-bag-check::before { content: "\f171"; } +.bi-bag-dash-fill::before { content: "\f172"; } +.bi-bag-dash::before { content: "\f173"; } +.bi-bag-fill::before { content: "\f174"; } +.bi-bag-plus-fill::before { content: "\f175"; } +.bi-bag-plus::before { content: "\f176"; } +.bi-bag-x-fill::before { content: "\f177"; } +.bi-bag-x::before { content: "\f178"; } +.bi-bag::before { content: "\f179"; } +.bi-bar-chart-fill::before { content: "\f17a"; } +.bi-bar-chart-line-fill::before { content: "\f17b"; } +.bi-bar-chart-line::before { content: "\f17c"; } +.bi-bar-chart-steps::before { content: "\f17d"; } +.bi-bar-chart::before { content: "\f17e"; } +.bi-basket-fill::before { content: "\f17f"; } +.bi-basket::before { content: "\f180"; } +.bi-basket2-fill::before { content: "\f181"; } +.bi-basket2::before { content: "\f182"; } +.bi-basket3-fill::before { content: "\f183"; } +.bi-basket3::before { content: "\f184"; } +.bi-battery-charging::before { content: "\f185"; } +.bi-battery-full::before { content: "\f186"; } +.bi-battery-half::before { content: "\f187"; } +.bi-battery::before { content: "\f188"; } +.bi-bell-fill::before { content: "\f189"; } +.bi-bell::before { content: "\f18a"; } +.bi-bezier::before { content: "\f18b"; } +.bi-bezier2::before { content: "\f18c"; } +.bi-bicycle::before { content: "\f18d"; } +.bi-binoculars-fill::before { content: "\f18e"; } +.bi-binoculars::before { content: "\f18f"; } +.bi-blockquote-left::before { content: "\f190"; } +.bi-blockquote-right::before { content: "\f191"; } +.bi-book-fill::before { content: "\f192"; } +.bi-book-half::before { content: "\f193"; } +.bi-book::before { content: "\f194"; } +.bi-bookmark-check-fill::before { content: "\f195"; } +.bi-bookmark-check::before { content: "\f196"; } +.bi-bookmark-dash-fill::before { content: "\f197"; } +.bi-bookmark-dash::before { content: "\f198"; } +.bi-bookmark-fill::before { content: "\f199"; } +.bi-bookmark-heart-fill::before { content: "\f19a"; } +.bi-bookmark-heart::before { content: "\f19b"; } +.bi-bookmark-plus-fill::before { content: "\f19c"; } +.bi-bookmark-plus::before { content: "\f19d"; } +.bi-bookmark-star-fill::before { content: "\f19e"; } +.bi-bookmark-star::before { content: "\f19f"; } +.bi-bookmark-x-fill::before { content: "\f1a0"; } +.bi-bookmark-x::before { content: "\f1a1"; } +.bi-bookmark::before { content: "\f1a2"; } +.bi-bookmarks-fill::before { content: "\f1a3"; } +.bi-bookmarks::before { content: "\f1a4"; } +.bi-bookshelf::before { content: "\f1a5"; } +.bi-bootstrap-fill::before { content: "\f1a6"; } +.bi-bootstrap-reboot::before { content: "\f1a7"; } +.bi-bootstrap::before { content: "\f1a8"; } +.bi-border-all::before { content: "\f1a9"; } +.bi-border-bottom::before { content: "\f1aa"; } +.bi-border-center::before { content: "\f1ab"; } +.bi-border-inner::before { content: "\f1ac"; } +.bi-border-left::before { content: "\f1ad"; } +.bi-border-middle::before { content: "\f1ae"; } +.bi-border-outer::before { content: "\f1af"; } +.bi-border-right::before { content: "\f1b0"; } +.bi-border-style::before { content: "\f1b1"; } +.bi-border-top::before { content: "\f1b2"; } +.bi-border-width::before { content: "\f1b3"; } +.bi-border::before { content: "\f1b4"; } +.bi-bounding-box-circles::before { content: "\f1b5"; } +.bi-bounding-box::before { content: "\f1b6"; } +.bi-box-arrow-down-left::before { content: "\f1b7"; } +.bi-box-arrow-down-right::before { content: "\f1b8"; } +.bi-box-arrow-down::before { content: "\f1b9"; } +.bi-box-arrow-in-down-left::before { content: "\f1ba"; } +.bi-box-arrow-in-down-right::before { content: "\f1bb"; } +.bi-box-arrow-in-down::before { content: "\f1bc"; } +.bi-box-arrow-in-left::before { content: "\f1bd"; } +.bi-box-arrow-in-right::before { content: "\f1be"; } +.bi-box-arrow-in-up-left::before { content: "\f1bf"; } +.bi-box-arrow-in-up-right::before { content: "\f1c0"; } +.bi-box-arrow-in-up::before { content: "\f1c1"; } +.bi-box-arrow-left::before { content: "\f1c2"; } +.bi-box-arrow-right::before { content: "\f1c3"; } +.bi-box-arrow-up-left::before { content: "\f1c4"; } +.bi-box-arrow-up-right::before { content: "\f1c5"; } +.bi-box-arrow-up::before { content: "\f1c6"; } +.bi-box-seam::before { content: "\f1c7"; } +.bi-box::before { content: "\f1c8"; } +.bi-braces::before { content: "\f1c9"; } +.bi-bricks::before { content: "\f1ca"; } +.bi-briefcase-fill::before { content: "\f1cb"; } +.bi-briefcase::before { content: "\f1cc"; } +.bi-brightness-alt-high-fill::before { content: "\f1cd"; } +.bi-brightness-alt-high::before { content: "\f1ce"; } +.bi-brightness-alt-low-fill::before { content: "\f1cf"; } +.bi-brightness-alt-low::before { content: "\f1d0"; } +.bi-brightness-high-fill::before { content: "\f1d1"; } +.bi-brightness-high::before { content: "\f1d2"; } +.bi-brightness-low-fill::before { content: "\f1d3"; } +.bi-brightness-low::before { content: "\f1d4"; } +.bi-broadcast-pin::before { content: "\f1d5"; } +.bi-broadcast::before { content: "\f1d6"; } +.bi-brush-fill::before { content: "\f1d7"; } +.bi-brush::before { content: "\f1d8"; } +.bi-bucket-fill::before { content: "\f1d9"; } +.bi-bucket::before { content: "\f1da"; } +.bi-bug-fill::before { content: "\f1db"; } +.bi-bug::before { content: "\f1dc"; } +.bi-building::before { content: "\f1dd"; } +.bi-bullseye::before { content: "\f1de"; } +.bi-calculator-fill::before { content: "\f1df"; } +.bi-calculator::before { content: "\f1e0"; } +.bi-calendar-check-fill::before { content: "\f1e1"; } +.bi-calendar-check::before { content: "\f1e2"; } +.bi-calendar-date-fill::before { content: "\f1e3"; } +.bi-calendar-date::before { content: "\f1e4"; } +.bi-calendar-day-fill::before { content: "\f1e5"; } +.bi-calendar-day::before { content: "\f1e6"; } +.bi-calendar-event-fill::before { content: "\f1e7"; } +.bi-calendar-event::before { content: "\f1e8"; } +.bi-calendar-fill::before { content: "\f1e9"; } +.bi-calendar-minus-fill::before { content: "\f1ea"; } +.bi-calendar-minus::before { content: "\f1eb"; } +.bi-calendar-month-fill::before { content: "\f1ec"; } +.bi-calendar-month::before { content: "\f1ed"; } +.bi-calendar-plus-fill::before { content: "\f1ee"; } +.bi-calendar-plus::before { content: "\f1ef"; } +.bi-calendar-range-fill::before { content: "\f1f0"; } +.bi-calendar-range::before { content: "\f1f1"; } +.bi-calendar-week-fill::before { content: "\f1f2"; } +.bi-calendar-week::before { content: "\f1f3"; } +.bi-calendar-x-fill::before { content: "\f1f4"; } +.bi-calendar-x::before { content: "\f1f5"; } +.bi-calendar::before { content: "\f1f6"; } +.bi-calendar2-check-fill::before { content: "\f1f7"; } +.bi-calendar2-check::before { content: "\f1f8"; } +.bi-calendar2-date-fill::before { content: "\f1f9"; } +.bi-calendar2-date::before { content: "\f1fa"; } +.bi-calendar2-day-fill::before { content: "\f1fb"; } +.bi-calendar2-day::before { content: "\f1fc"; } +.bi-calendar2-event-fill::before { content: "\f1fd"; } +.bi-calendar2-event::before { content: "\f1fe"; } +.bi-calendar2-fill::before { content: "\f1ff"; } +.bi-calendar2-minus-fill::before { content: "\f200"; } +.bi-calendar2-minus::before { content: "\f201"; } +.bi-calendar2-month-fill::before { content: "\f202"; } +.bi-calendar2-month::before { content: "\f203"; } +.bi-calendar2-plus-fill::before { content: "\f204"; } +.bi-calendar2-plus::before { content: "\f205"; } +.bi-calendar2-range-fill::before { content: "\f206"; } +.bi-calendar2-range::before { content: "\f207"; } +.bi-calendar2-week-fill::before { content: "\f208"; } +.bi-calendar2-week::before { content: "\f209"; } +.bi-calendar2-x-fill::before { content: "\f20a"; } +.bi-calendar2-x::before { content: "\f20b"; } +.bi-calendar2::before { content: "\f20c"; } +.bi-calendar3-event-fill::before { content: "\f20d"; } +.bi-calendar3-event::before { content: "\f20e"; } +.bi-calendar3-fill::before { content: "\f20f"; } +.bi-calendar3-range-fill::before { content: "\f210"; } +.bi-calendar3-range::before { content: "\f211"; } +.bi-calendar3-week-fill::before { content: "\f212"; } +.bi-calendar3-week::before { content: "\f213"; } +.bi-calendar3::before { content: "\f214"; } +.bi-calendar4-event::before { content: "\f215"; } +.bi-calendar4-range::before { content: "\f216"; } +.bi-calendar4-week::before { content: "\f217"; } +.bi-calendar4::before { content: "\f218"; } +.bi-camera-fill::before { content: "\f219"; } +.bi-camera-reels-fill::before { content: "\f21a"; } +.bi-camera-reels::before { content: "\f21b"; } +.bi-camera-video-fill::before { content: "\f21c"; } +.bi-camera-video-off-fill::before { content: "\f21d"; } +.bi-camera-video-off::before { content: "\f21e"; } +.bi-camera-video::before { content: "\f21f"; } +.bi-camera::before { content: "\f220"; } +.bi-camera2::before { content: "\f221"; } +.bi-capslock-fill::before { content: "\f222"; } +.bi-capslock::before { content: "\f223"; } +.bi-card-checklist::before { content: "\f224"; } +.bi-card-heading::before { content: "\f225"; } +.bi-card-image::before { content: "\f226"; } +.bi-card-list::before { content: "\f227"; } +.bi-card-text::before { content: "\f228"; } +.bi-caret-down-fill::before { content: "\f229"; } +.bi-caret-down-square-fill::before { content: "\f22a"; } +.bi-caret-down-square::before { content: "\f22b"; } +.bi-caret-down::before { content: "\f22c"; } +.bi-caret-left-fill::before { content: "\f22d"; } +.bi-caret-left-square-fill::before { content: "\f22e"; } +.bi-caret-left-square::before { content: "\f22f"; } +.bi-caret-left::before { content: "\f230"; } +.bi-caret-right-fill::before { content: "\f231"; } +.bi-caret-right-square-fill::before { content: "\f232"; } +.bi-caret-right-square::before { content: "\f233"; } +.bi-caret-right::before { content: "\f234"; } +.bi-caret-up-fill::before { content: "\f235"; } +.bi-caret-up-square-fill::before { content: "\f236"; } +.bi-caret-up-square::before { content: "\f237"; } +.bi-caret-up::before { content: "\f238"; } +.bi-cart-check-fill::before { content: "\f239"; } +.bi-cart-check::before { content: "\f23a"; } +.bi-cart-dash-fill::before { content: "\f23b"; } +.bi-cart-dash::before { content: "\f23c"; } +.bi-cart-fill::before { content: "\f23d"; } +.bi-cart-plus-fill::before { content: "\f23e"; } +.bi-cart-plus::before { content: "\f23f"; } +.bi-cart-x-fill::before { content: "\f240"; } +.bi-cart-x::before { content: "\f241"; } +.bi-cart::before { content: "\f242"; } +.bi-cart2::before { content: "\f243"; } +.bi-cart3::before { content: "\f244"; } +.bi-cart4::before { content: "\f245"; } +.bi-cash-stack::before { content: "\f246"; } +.bi-cash::before { content: "\f247"; } +.bi-cast::before { content: "\f248"; } +.bi-chat-dots-fill::before { content: "\f249"; } +.bi-chat-dots::before { content: "\f24a"; } +.bi-chat-fill::before { content: "\f24b"; } +.bi-chat-left-dots-fill::before { content: "\f24c"; } +.bi-chat-left-dots::before { content: "\f24d"; } +.bi-chat-left-fill::before { content: "\f24e"; } +.bi-chat-left-quote-fill::before { content: "\f24f"; } +.bi-chat-left-quote::before { content: "\f250"; } +.bi-chat-left-text-fill::before { content: "\f251"; } +.bi-chat-left-text::before { content: "\f252"; } +.bi-chat-left::before { content: "\f253"; } +.bi-chat-quote-fill::before { content: "\f254"; } +.bi-chat-quote::before { content: "\f255"; } +.bi-chat-right-dots-fill::before { content: "\f256"; } +.bi-chat-right-dots::before { content: "\f257"; } +.bi-chat-right-fill::before { content: "\f258"; } +.bi-chat-right-quote-fill::before { content: "\f259"; } +.bi-chat-right-quote::before { content: "\f25a"; } +.bi-chat-right-text-fill::before { content: "\f25b"; } +.bi-chat-right-text::before { content: "\f25c"; } +.bi-chat-right::before { content: "\f25d"; } +.bi-chat-square-dots-fill::before { content: "\f25e"; } +.bi-chat-square-dots::before { content: "\f25f"; } +.bi-chat-square-fill::before { content: "\f260"; } +.bi-chat-square-quote-fill::before { content: "\f261"; } +.bi-chat-square-quote::before { content: "\f262"; } +.bi-chat-square-text-fill::before { content: "\f263"; } +.bi-chat-square-text::before { content: "\f264"; } +.bi-chat-square::before { content: "\f265"; } +.bi-chat-text-fill::before { content: "\f266"; } +.bi-chat-text::before { content: "\f267"; } +.bi-chat::before { content: "\f268"; } +.bi-check-all::before { content: "\f269"; } +.bi-check-circle-fill::before { content: "\f26a"; } +.bi-check-circle::before { content: "\f26b"; } +.bi-check-square-fill::before { content: "\f26c"; } +.bi-check-square::before { content: "\f26d"; } +.bi-check::before { content: "\f26e"; } +.bi-check2-all::before { content: "\f26f"; } +.bi-check2-circle::before { content: "\f270"; } +.bi-check2-square::before { content: "\f271"; } +.bi-check2::before { content: "\f272"; } +.bi-chevron-bar-contract::before { content: "\f273"; } +.bi-chevron-bar-down::before { content: "\f274"; } +.bi-chevron-bar-expand::before { content: "\f275"; } +.bi-chevron-bar-left::before { content: "\f276"; } +.bi-chevron-bar-right::before { content: "\f277"; } +.bi-chevron-bar-up::before { content: "\f278"; } +.bi-chevron-compact-down::before { content: "\f279"; } +.bi-chevron-compact-left::before { content: "\f27a"; } +.bi-chevron-compact-right::before { content: "\f27b"; } +.bi-chevron-compact-up::before { content: "\f27c"; } +.bi-chevron-contract::before { content: "\f27d"; } +.bi-chevron-double-down::before { content: "\f27e"; } +.bi-chevron-double-left::before { content: "\f27f"; } +.bi-chevron-double-right::before { content: "\f280"; } +.bi-chevron-double-up::before { content: "\f281"; } +.bi-chevron-down::before { content: "\f282"; } +.bi-chevron-expand::before { content: "\f283"; } +.bi-chevron-left::before { content: "\f284"; } +.bi-chevron-right::before { content: "\f285"; } +.bi-chevron-up::before { content: "\f286"; } +.bi-circle-fill::before { content: "\f287"; } +.bi-circle-half::before { content: "\f288"; } +.bi-circle-square::before { content: "\f289"; } +.bi-circle::before { content: "\f28a"; } +.bi-clipboard-check::before { content: "\f28b"; } +.bi-clipboard-data::before { content: "\f28c"; } +.bi-clipboard-minus::before { content: "\f28d"; } +.bi-clipboard-plus::before { content: "\f28e"; } +.bi-clipboard-x::before { content: "\f28f"; } +.bi-clipboard::before { content: "\f290"; } +.bi-clock-fill::before { content: "\f291"; } +.bi-clock-history::before { content: "\f292"; } +.bi-clock::before { content: "\f293"; } +.bi-cloud-arrow-down-fill::before { content: "\f294"; } +.bi-cloud-arrow-down::before { content: "\f295"; } +.bi-cloud-arrow-up-fill::before { content: "\f296"; } +.bi-cloud-arrow-up::before { content: "\f297"; } +.bi-cloud-check-fill::before { content: "\f298"; } +.bi-cloud-check::before { content: "\f299"; } +.bi-cloud-download-fill::before { content: "\f29a"; } +.bi-cloud-download::before { content: "\f29b"; } +.bi-cloud-drizzle-fill::before { content: "\f29c"; } +.bi-cloud-drizzle::before { content: "\f29d"; } +.bi-cloud-fill::before { content: "\f29e"; } +.bi-cloud-fog-fill::before { content: "\f29f"; } +.bi-cloud-fog::before { content: "\f2a0"; } +.bi-cloud-fog2-fill::before { content: "\f2a1"; } +.bi-cloud-fog2::before { content: "\f2a2"; } +.bi-cloud-hail-fill::before { content: "\f2a3"; } +.bi-cloud-hail::before { content: "\f2a4"; } +.bi-cloud-haze-fill::before { content: "\f2a6"; } +.bi-cloud-haze::before { content: "\f2a7"; } +.bi-cloud-haze2-fill::before { content: "\f2a8"; } +.bi-cloud-lightning-fill::before { content: "\f2a9"; } +.bi-cloud-lightning-rain-fill::before { content: "\f2aa"; } +.bi-cloud-lightning-rain::before { content: "\f2ab"; } +.bi-cloud-lightning::before { content: "\f2ac"; } +.bi-cloud-minus-fill::before { content: "\f2ad"; } +.bi-cloud-minus::before { content: "\f2ae"; } +.bi-cloud-moon-fill::before { content: "\f2af"; } +.bi-cloud-moon::before { content: "\f2b0"; } +.bi-cloud-plus-fill::before { content: "\f2b1"; } +.bi-cloud-plus::before { content: "\f2b2"; } +.bi-cloud-rain-fill::before { content: "\f2b3"; } +.bi-cloud-rain-heavy-fill::before { content: "\f2b4"; } +.bi-cloud-rain-heavy::before { content: "\f2b5"; } +.bi-cloud-rain::before { content: "\f2b6"; } +.bi-cloud-slash-fill::before { content: "\f2b7"; } +.bi-cloud-slash::before { content: "\f2b8"; } +.bi-cloud-sleet-fill::before { content: "\f2b9"; } +.bi-cloud-sleet::before { content: "\f2ba"; } +.bi-cloud-snow-fill::before { content: "\f2bb"; } +.bi-cloud-snow::before { content: "\f2bc"; } +.bi-cloud-sun-fill::before { content: "\f2bd"; } +.bi-cloud-sun::before { content: "\f2be"; } +.bi-cloud-upload-fill::before { content: "\f2bf"; } +.bi-cloud-upload::before { content: "\f2c0"; } +.bi-cloud::before { content: "\f2c1"; } +.bi-clouds-fill::before { content: "\f2c2"; } +.bi-clouds::before { content: "\f2c3"; } +.bi-cloudy-fill::before { content: "\f2c4"; } +.bi-cloudy::before { content: "\f2c5"; } +.bi-code-slash::before { content: "\f2c6"; } +.bi-code-square::before { content: "\f2c7"; } +.bi-code::before { content: "\f2c8"; } +.bi-collection-fill::before { content: "\f2c9"; } +.bi-collection-play-fill::before { content: "\f2ca"; } +.bi-collection-play::before { content: "\f2cb"; } +.bi-collection::before { content: "\f2cc"; } +.bi-columns-gap::before { content: "\f2cd"; } +.bi-columns::before { content: "\f2ce"; } +.bi-command::before { content: "\f2cf"; } +.bi-compass-fill::before { content: "\f2d0"; } +.bi-compass::before { content: "\f2d1"; } +.bi-cone-striped::before { content: "\f2d2"; } +.bi-cone::before { content: "\f2d3"; } +.bi-controller::before { content: "\f2d4"; } +.bi-cpu-fill::before { content: "\f2d5"; } +.bi-cpu::before { content: "\f2d6"; } +.bi-credit-card-2-back-fill::before { content: "\f2d7"; } +.bi-credit-card-2-back::before { content: "\f2d8"; } +.bi-credit-card-2-front-fill::before { content: "\f2d9"; } +.bi-credit-card-2-front::before { content: "\f2da"; } +.bi-credit-card-fill::before { content: "\f2db"; } +.bi-credit-card::before { content: "\f2dc"; } +.bi-crop::before { content: "\f2dd"; } +.bi-cup-fill::before { content: "\f2de"; } +.bi-cup-straw::before { content: "\f2df"; } +.bi-cup::before { content: "\f2e0"; } +.bi-cursor-fill::before { content: "\f2e1"; } +.bi-cursor-text::before { content: "\f2e2"; } +.bi-cursor::before { content: "\f2e3"; } +.bi-dash-circle-dotted::before { content: "\f2e4"; } +.bi-dash-circle-fill::before { content: "\f2e5"; } +.bi-dash-circle::before { content: "\f2e6"; } +.bi-dash-square-dotted::before { content: "\f2e7"; } +.bi-dash-square-fill::before { content: "\f2e8"; } +.bi-dash-square::before { content: "\f2e9"; } +.bi-dash::before { content: "\f2ea"; } +.bi-diagram-2-fill::before { content: "\f2eb"; } +.bi-diagram-2::before { content: "\f2ec"; } +.bi-diagram-3-fill::before { content: "\f2ed"; } +.bi-diagram-3::before { content: "\f2ee"; } +.bi-diamond-fill::before { content: "\f2ef"; } +.bi-diamond-half::before { content: "\f2f0"; } +.bi-diamond::before { content: "\f2f1"; } +.bi-dice-1-fill::before { content: "\f2f2"; } +.bi-dice-1::before { content: "\f2f3"; } +.bi-dice-2-fill::before { content: "\f2f4"; } +.bi-dice-2::before { content: "\f2f5"; } +.bi-dice-3-fill::before { content: "\f2f6"; } +.bi-dice-3::before { content: "\f2f7"; } +.bi-dice-4-fill::before { content: "\f2f8"; } +.bi-dice-4::before { content: "\f2f9"; } +.bi-dice-5-fill::before { content: "\f2fa"; } +.bi-dice-5::before { content: "\f2fb"; } +.bi-dice-6-fill::before { content: "\f2fc"; } +.bi-dice-6::before { content: "\f2fd"; } +.bi-disc-fill::before { content: "\f2fe"; } +.bi-disc::before { content: "\f2ff"; } +.bi-discord::before { content: "\f300"; } +.bi-display-fill::before { content: "\f301"; } +.bi-display::before { content: "\f302"; } +.bi-distribute-horizontal::before { content: "\f303"; } +.bi-distribute-vertical::before { content: "\f304"; } +.bi-door-closed-fill::before { content: "\f305"; } +.bi-door-closed::before { content: "\f306"; } +.bi-door-open-fill::before { content: "\f307"; } +.bi-door-open::before { content: "\f308"; } +.bi-dot::before { content: "\f309"; } +.bi-download::before { content: "\f30a"; } +.bi-droplet-fill::before { content: "\f30b"; } +.bi-droplet-half::before { content: "\f30c"; } +.bi-droplet::before { content: "\f30d"; } +.bi-earbuds::before { content: "\f30e"; } +.bi-easel-fill::before { content: "\f30f"; } +.bi-easel::before { content: "\f310"; } +.bi-egg-fill::before { content: "\f311"; } +.bi-egg-fried::before { content: "\f312"; } +.bi-egg::before { content: "\f313"; } +.bi-eject-fill::before { content: "\f314"; } +.bi-eject::before { content: "\f315"; } +.bi-emoji-angry-fill::before { content: "\f316"; } +.bi-emoji-angry::before { content: "\f317"; } +.bi-emoji-dizzy-fill::before { content: "\f318"; } +.bi-emoji-dizzy::before { content: "\f319"; } +.bi-emoji-expressionless-fill::before { content: "\f31a"; } +.bi-emoji-expressionless::before { content: "\f31b"; } +.bi-emoji-frown-fill::before { content: "\f31c"; } +.bi-emoji-frown::before { content: "\f31d"; } +.bi-emoji-heart-eyes-fill::before { content: "\f31e"; } +.bi-emoji-heart-eyes::before { content: "\f31f"; } +.bi-emoji-laughing-fill::before { content: "\f320"; } +.bi-emoji-laughing::before { content: "\f321"; } +.bi-emoji-neutral-fill::before { content: "\f322"; } +.bi-emoji-neutral::before { content: "\f323"; } +.bi-emoji-smile-fill::before { content: "\f324"; } +.bi-emoji-smile-upside-down-fill::before { content: "\f325"; } +.bi-emoji-smile-upside-down::before { content: "\f326"; } +.bi-emoji-smile::before { content: "\f327"; } +.bi-emoji-sunglasses-fill::before { content: "\f328"; } +.bi-emoji-sunglasses::before { content: "\f329"; } +.bi-emoji-wink-fill::before { content: "\f32a"; } +.bi-emoji-wink::before { content: "\f32b"; } +.bi-envelope-fill::before { content: "\f32c"; } +.bi-envelope-open-fill::before { content: "\f32d"; } +.bi-envelope-open::before { content: "\f32e"; } +.bi-envelope::before { content: "\f32f"; } +.bi-eraser-fill::before { content: "\f330"; } +.bi-eraser::before { content: "\f331"; } +.bi-exclamation-circle-fill::before { content: "\f332"; } +.bi-exclamation-circle::before { content: "\f333"; } +.bi-exclamation-diamond-fill::before { content: "\f334"; } +.bi-exclamation-diamond::before { content: "\f335"; } +.bi-exclamation-octagon-fill::before { content: "\f336"; } +.bi-exclamation-octagon::before { content: "\f337"; } +.bi-exclamation-square-fill::before { content: "\f338"; } +.bi-exclamation-square::before { content: "\f339"; } +.bi-exclamation-triangle-fill::before { content: "\f33a"; } +.bi-exclamation-triangle::before { content: "\f33b"; } +.bi-exclamation::before { content: "\f33c"; } +.bi-exclude::before { content: "\f33d"; } +.bi-eye-fill::before { content: "\f33e"; } +.bi-eye-slash-fill::before { content: "\f33f"; } +.bi-eye-slash::before { content: "\f340"; } +.bi-eye::before { content: "\f341"; } +.bi-eyedropper::before { content: "\f342"; } +.bi-eyeglasses::before { content: "\f343"; } +.bi-facebook::before { content: "\f344"; } +.bi-file-arrow-down-fill::before { content: "\f345"; } +.bi-file-arrow-down::before { content: "\f346"; } +.bi-file-arrow-up-fill::before { content: "\f347"; } +.bi-file-arrow-up::before { content: "\f348"; } +.bi-file-bar-graph-fill::before { content: "\f349"; } +.bi-file-bar-graph::before { content: "\f34a"; } +.bi-file-binary-fill::before { content: "\f34b"; } +.bi-file-binary::before { content: "\f34c"; } +.bi-file-break-fill::before { content: "\f34d"; } +.bi-file-break::before { content: "\f34e"; } +.bi-file-check-fill::before { content: "\f34f"; } +.bi-file-check::before { content: "\f350"; } +.bi-file-code-fill::before { content: "\f351"; } +.bi-file-code::before { content: "\f352"; } +.bi-file-diff-fill::before { content: "\f353"; } +.bi-file-diff::before { content: "\f354"; } +.bi-file-earmark-arrow-down-fill::before { content: "\f355"; } +.bi-file-earmark-arrow-down::before { content: "\f356"; } +.bi-file-earmark-arrow-up-fill::before { content: "\f357"; } +.bi-file-earmark-arrow-up::before { content: "\f358"; } +.bi-file-earmark-bar-graph-fill::before { content: "\f359"; } +.bi-file-earmark-bar-graph::before { content: "\f35a"; } +.bi-file-earmark-binary-fill::before { content: "\f35b"; } +.bi-file-earmark-binary::before { content: "\f35c"; } +.bi-file-earmark-break-fill::before { content: "\f35d"; } +.bi-file-earmark-break::before { content: "\f35e"; } +.bi-file-earmark-check-fill::before { content: "\f35f"; } +.bi-file-earmark-check::before { content: "\f360"; } +.bi-file-earmark-code-fill::before { content: "\f361"; } +.bi-file-earmark-code::before { content: "\f362"; } +.bi-file-earmark-diff-fill::before { content: "\f363"; } +.bi-file-earmark-diff::before { content: "\f364"; } +.bi-file-earmark-easel-fill::before { content: "\f365"; } +.bi-file-earmark-easel::before { content: "\f366"; } +.bi-file-earmark-excel-fill::before { content: "\f367"; } +.bi-file-earmark-excel::before { content: "\f368"; } +.bi-file-earmark-fill::before { content: "\f369"; } +.bi-file-earmark-font-fill::before { content: "\f36a"; } +.bi-file-earmark-font::before { content: "\f36b"; } +.bi-file-earmark-image-fill::before { content: "\f36c"; } +.bi-file-earmark-image::before { content: "\f36d"; } +.bi-file-earmark-lock-fill::before { content: "\f36e"; } +.bi-file-earmark-lock::before { content: "\f36f"; } +.bi-file-earmark-lock2-fill::before { content: "\f370"; } +.bi-file-earmark-lock2::before { content: "\f371"; } +.bi-file-earmark-medical-fill::before { content: "\f372"; } +.bi-file-earmark-medical::before { content: "\f373"; } +.bi-file-earmark-minus-fill::before { content: "\f374"; } +.bi-file-earmark-minus::before { content: "\f375"; } +.bi-file-earmark-music-fill::before { content: "\f376"; } +.bi-file-earmark-music::before { content: "\f377"; } +.bi-file-earmark-person-fill::before { content: "\f378"; } +.bi-file-earmark-person::before { content: "\f379"; } +.bi-file-earmark-play-fill::before { content: "\f37a"; } +.bi-file-earmark-play::before { content: "\f37b"; } +.bi-file-earmark-plus-fill::before { content: "\f37c"; } +.bi-file-earmark-plus::before { content: "\f37d"; } +.bi-file-earmark-post-fill::before { content: "\f37e"; } +.bi-file-earmark-post::before { content: "\f37f"; } +.bi-file-earmark-ppt-fill::before { content: "\f380"; } +.bi-file-earmark-ppt::before { content: "\f381"; } +.bi-file-earmark-richtext-fill::before { content: "\f382"; } +.bi-file-earmark-richtext::before { content: "\f383"; } +.bi-file-earmark-ruled-fill::before { content: "\f384"; } +.bi-file-earmark-ruled::before { content: "\f385"; } +.bi-file-earmark-slides-fill::before { content: "\f386"; } +.bi-file-earmark-slides::before { content: "\f387"; } +.bi-file-earmark-spreadsheet-fill::before { content: "\f388"; } +.bi-file-earmark-spreadsheet::before { content: "\f389"; } +.bi-file-earmark-text-fill::before { content: "\f38a"; } +.bi-file-earmark-text::before { content: "\f38b"; } +.bi-file-earmark-word-fill::before { content: "\f38c"; } +.bi-file-earmark-word::before { content: "\f38d"; } +.bi-file-earmark-x-fill::before { content: "\f38e"; } +.bi-file-earmark-x::before { content: "\f38f"; } +.bi-file-earmark-zip-fill::before { content: "\f390"; } +.bi-file-earmark-zip::before { content: "\f391"; } +.bi-file-earmark::before { content: "\f392"; } +.bi-file-easel-fill::before { content: "\f393"; } +.bi-file-easel::before { content: "\f394"; } +.bi-file-excel-fill::before { content: "\f395"; } +.bi-file-excel::before { content: "\f396"; } +.bi-file-fill::before { content: "\f397"; } +.bi-file-font-fill::before { content: "\f398"; } +.bi-file-font::before { content: "\f399"; } +.bi-file-image-fill::before { content: "\f39a"; } +.bi-file-image::before { content: "\f39b"; } +.bi-file-lock-fill::before { content: "\f39c"; } +.bi-file-lock::before { content: "\f39d"; } +.bi-file-lock2-fill::before { content: "\f39e"; } +.bi-file-lock2::before { content: "\f39f"; } +.bi-file-medical-fill::before { content: "\f3a0"; } +.bi-file-medical::before { content: "\f3a1"; } +.bi-file-minus-fill::before { content: "\f3a2"; } +.bi-file-minus::before { content: "\f3a3"; } +.bi-file-music-fill::before { content: "\f3a4"; } +.bi-file-music::before { content: "\f3a5"; } +.bi-file-person-fill::before { content: "\f3a6"; } +.bi-file-person::before { content: "\f3a7"; } +.bi-file-play-fill::before { content: "\f3a8"; } +.bi-file-play::before { content: "\f3a9"; } +.bi-file-plus-fill::before { content: "\f3aa"; } +.bi-file-plus::before { content: "\f3ab"; } +.bi-file-post-fill::before { content: "\f3ac"; } +.bi-file-post::before { content: "\f3ad"; } +.bi-file-ppt-fill::before { content: "\f3ae"; } +.bi-file-ppt::before { content: "\f3af"; } +.bi-file-richtext-fill::before { content: "\f3b0"; } +.bi-file-richtext::before { content: "\f3b1"; } +.bi-file-ruled-fill::before { content: "\f3b2"; } +.bi-file-ruled::before { content: "\f3b3"; } +.bi-file-slides-fill::before { content: "\f3b4"; } +.bi-file-slides::before { content: "\f3b5"; } +.bi-file-spreadsheet-fill::before { content: "\f3b6"; } +.bi-file-spreadsheet::before { content: "\f3b7"; } +.bi-file-text-fill::before { content: "\f3b8"; } +.bi-file-text::before { content: "\f3b9"; } +.bi-file-word-fill::before { content: "\f3ba"; } +.bi-file-word::before { content: "\f3bb"; } +.bi-file-x-fill::before { content: "\f3bc"; } +.bi-file-x::before { content: "\f3bd"; } +.bi-file-zip-fill::before { content: "\f3be"; } +.bi-file-zip::before { content: "\f3bf"; } +.bi-file::before { content: "\f3c0"; } +.bi-files-alt::before { content: "\f3c1"; } +.bi-files::before { content: "\f3c2"; } +.bi-film::before { content: "\f3c3"; } +.bi-filter-circle-fill::before { content: "\f3c4"; } +.bi-filter-circle::before { content: "\f3c5"; } +.bi-filter-left::before { content: "\f3c6"; } +.bi-filter-right::before { content: "\f3c7"; } +.bi-filter-square-fill::before { content: "\f3c8"; } +.bi-filter-square::before { content: "\f3c9"; } +.bi-filter::before { content: "\f3ca"; } +.bi-flag-fill::before { content: "\f3cb"; } +.bi-flag::before { content: "\f3cc"; } +.bi-flower1::before { content: "\f3cd"; } +.bi-flower2::before { content: "\f3ce"; } +.bi-flower3::before { content: "\f3cf"; } +.bi-folder-check::before { content: "\f3d0"; } +.bi-folder-fill::before { content: "\f3d1"; } +.bi-folder-minus::before { content: "\f3d2"; } +.bi-folder-plus::before { content: "\f3d3"; } +.bi-folder-symlink-fill::before { content: "\f3d4"; } +.bi-folder-symlink::before { content: "\f3d5"; } +.bi-folder-x::before { content: "\f3d6"; } +.bi-folder::before { content: "\f3d7"; } +.bi-folder2-open::before { content: "\f3d8"; } +.bi-folder2::before { content: "\f3d9"; } +.bi-fonts::before { content: "\f3da"; } +.bi-forward-fill::before { content: "\f3db"; } +.bi-forward::before { content: "\f3dc"; } +.bi-front::before { content: "\f3dd"; } +.bi-fullscreen-exit::before { content: "\f3de"; } +.bi-fullscreen::before { content: "\f3df"; } +.bi-funnel-fill::before { content: "\f3e0"; } +.bi-funnel::before { content: "\f3e1"; } +.bi-gear-fill::before { content: "\f3e2"; } +.bi-gear-wide-connected::before { content: "\f3e3"; } +.bi-gear-wide::before { content: "\f3e4"; } +.bi-gear::before { content: "\f3e5"; } +.bi-gem::before { content: "\f3e6"; } +.bi-geo-alt-fill::before { content: "\f3e7"; } +.bi-geo-alt::before { content: "\f3e8"; } +.bi-geo-fill::before { content: "\f3e9"; } +.bi-geo::before { content: "\f3ea"; } +.bi-gift-fill::before { content: "\f3eb"; } +.bi-gift::before { content: "\f3ec"; } +.bi-github::before { content: "\f3ed"; } +.bi-globe::before { content: "\f3ee"; } +.bi-globe2::before { content: "\f3ef"; } +.bi-google::before { content: "\f3f0"; } +.bi-graph-down::before { content: "\f3f1"; } +.bi-graph-up::before { content: "\f3f2"; } +.bi-grid-1x2-fill::before { content: "\f3f3"; } +.bi-grid-1x2::before { content: "\f3f4"; } +.bi-grid-3x2-gap-fill::before { content: "\f3f5"; } +.bi-grid-3x2-gap::before { content: "\f3f6"; } +.bi-grid-3x2::before { content: "\f3f7"; } +.bi-grid-3x3-gap-fill::before { content: "\f3f8"; } +.bi-grid-3x3-gap::before { content: "\f3f9"; } +.bi-grid-3x3::before { content: "\f3fa"; } +.bi-grid-fill::before { content: "\f3fb"; } +.bi-grid::before { content: "\f3fc"; } +.bi-grip-horizontal::before { content: "\f3fd"; } +.bi-grip-vertical::before { content: "\f3fe"; } +.bi-hammer::before { content: "\f3ff"; } +.bi-hand-index-fill::before { content: "\f400"; } +.bi-hand-index-thumb-fill::before { content: "\f401"; } +.bi-hand-index-thumb::before { content: "\f402"; } +.bi-hand-index::before { content: "\f403"; } +.bi-hand-thumbs-down-fill::before { content: "\f404"; } +.bi-hand-thumbs-down::before { content: "\f405"; } +.bi-hand-thumbs-up-fill::before { content: "\f406"; } +.bi-hand-thumbs-up::before { content: "\f407"; } +.bi-handbag-fill::before { content: "\f408"; } +.bi-handbag::before { content: "\f409"; } +.bi-hash::before { content: "\f40a"; } +.bi-hdd-fill::before { content: "\f40b"; } +.bi-hdd-network-fill::before { content: "\f40c"; } +.bi-hdd-network::before { content: "\f40d"; } +.bi-hdd-rack-fill::before { content: "\f40e"; } +.bi-hdd-rack::before { content: "\f40f"; } +.bi-hdd-stack-fill::before { content: "\f410"; } +.bi-hdd-stack::before { content: "\f411"; } +.bi-hdd::before { content: "\f412"; } +.bi-headphones::before { content: "\f413"; } +.bi-headset::before { content: "\f414"; } +.bi-heart-fill::before { content: "\f415"; } +.bi-heart-half::before { content: "\f416"; } +.bi-heart::before { content: "\f417"; } +.bi-heptagon-fill::before { content: "\f418"; } +.bi-heptagon-half::before { content: "\f419"; } +.bi-heptagon::before { content: "\f41a"; } +.bi-hexagon-fill::before { content: "\f41b"; } +.bi-hexagon-half::before { content: "\f41c"; } +.bi-hexagon::before { content: "\f41d"; } +.bi-hourglass-bottom::before { content: "\f41e"; } +.bi-hourglass-split::before { content: "\f41f"; } +.bi-hourglass-top::before { content: "\f420"; } +.bi-hourglass::before { content: "\f421"; } +.bi-house-door-fill::before { content: "\f422"; } +.bi-house-door::before { content: "\f423"; } +.bi-house-fill::before { content: "\f424"; } +.bi-house::before { content: "\f425"; } +.bi-hr::before { content: "\f426"; } +.bi-hurricane::before { content: "\f427"; } +.bi-image-alt::before { content: "\f428"; } +.bi-image-fill::before { content: "\f429"; } +.bi-image::before { content: "\f42a"; } +.bi-images::before { content: "\f42b"; } +.bi-inbox-fill::before { content: "\f42c"; } +.bi-inbox::before { content: "\f42d"; } +.bi-inboxes-fill::before { content: "\f42e"; } +.bi-inboxes::before { content: "\f42f"; } +.bi-info-circle-fill::before { content: "\f430"; } +.bi-info-circle::before { content: "\f431"; } +.bi-info-square-fill::before { content: "\f432"; } +.bi-info-square::before { content: "\f433"; } +.bi-info::before { content: "\f434"; } +.bi-input-cursor-text::before { content: "\f435"; } +.bi-input-cursor::before { content: "\f436"; } +.bi-instagram::before { content: "\f437"; } +.bi-intersect::before { content: "\f438"; } +.bi-journal-album::before { content: "\f439"; } +.bi-journal-arrow-down::before { content: "\f43a"; } +.bi-journal-arrow-up::before { content: "\f43b"; } +.bi-journal-bookmark-fill::before { content: "\f43c"; } +.bi-journal-bookmark::before { content: "\f43d"; } +.bi-journal-check::before { content: "\f43e"; } +.bi-journal-code::before { content: "\f43f"; } +.bi-journal-medical::before { content: "\f440"; } +.bi-journal-minus::before { content: "\f441"; } +.bi-journal-plus::before { content: "\f442"; } +.bi-journal-richtext::before { content: "\f443"; } +.bi-journal-text::before { content: "\f444"; } +.bi-journal-x::before { content: "\f445"; } +.bi-journal::before { content: "\f446"; } +.bi-journals::before { content: "\f447"; } +.bi-joystick::before { content: "\f448"; } +.bi-justify-left::before { content: "\f449"; } +.bi-justify-right::before { content: "\f44a"; } +.bi-justify::before { content: "\f44b"; } +.bi-kanban-fill::before { content: "\f44c"; } +.bi-kanban::before { content: "\f44d"; } +.bi-key-fill::before { content: "\f44e"; } +.bi-key::before { content: "\f44f"; } +.bi-keyboard-fill::before { content: "\f450"; } +.bi-keyboard::before { content: "\f451"; } +.bi-ladder::before { content: "\f452"; } +.bi-lamp-fill::before { content: "\f453"; } +.bi-lamp::before { content: "\f454"; } +.bi-laptop-fill::before { content: "\f455"; } +.bi-laptop::before { content: "\f456"; } +.bi-layer-backward::before { content: "\f457"; } +.bi-layer-forward::before { content: "\f458"; } +.bi-layers-fill::before { content: "\f459"; } +.bi-layers-half::before { content: "\f45a"; } +.bi-layers::before { content: "\f45b"; } +.bi-layout-sidebar-inset-reverse::before { content: "\f45c"; } +.bi-layout-sidebar-inset::before { content: "\f45d"; } +.bi-layout-sidebar-reverse::before { content: "\f45e"; } +.bi-layout-sidebar::before { content: "\f45f"; } +.bi-layout-split::before { content: "\f460"; } +.bi-layout-text-sidebar-reverse::before { content: "\f461"; } +.bi-layout-text-sidebar::before { content: "\f462"; } +.bi-layout-text-window-reverse::before { content: "\f463"; } +.bi-layout-text-window::before { content: "\f464"; } +.bi-layout-three-columns::before { content: "\f465"; } +.bi-layout-wtf::before { content: "\f466"; } +.bi-life-preserver::before { content: "\f467"; } +.bi-lightbulb-fill::before { content: "\f468"; } +.bi-lightbulb-off-fill::before { content: "\f469"; } +.bi-lightbulb-off::before { content: "\f46a"; } +.bi-lightbulb::before { content: "\f46b"; } +.bi-lightning-charge-fill::before { content: "\f46c"; } +.bi-lightning-charge::before { content: "\f46d"; } +.bi-lightning-fill::before { content: "\f46e"; } +.bi-lightning::before { content: "\f46f"; } +.bi-link-45deg::before { content: "\f470"; } +.bi-link::before { content: "\f471"; } +.bi-linkedin::before { content: "\f472"; } +.bi-list-check::before { content: "\f473"; } +.bi-list-nested::before { content: "\f474"; } +.bi-list-ol::before { content: "\f475"; } +.bi-list-stars::before { content: "\f476"; } +.bi-list-task::before { content: "\f477"; } +.bi-list-ul::before { content: "\f478"; } +.bi-list::before { content: "\f479"; } +.bi-lock-fill::before { content: "\f47a"; } +.bi-lock::before { content: "\f47b"; } +.bi-mailbox::before { content: "\f47c"; } +.bi-mailbox2::before { content: "\f47d"; } +.bi-map-fill::before { content: "\f47e"; } +.bi-map::before { content: "\f47f"; } +.bi-markdown-fill::before { content: "\f480"; } +.bi-markdown::before { content: "\f481"; } +.bi-mask::before { content: "\f482"; } +.bi-megaphone-fill::before { content: "\f483"; } +.bi-megaphone::before { content: "\f484"; } +.bi-menu-app-fill::before { content: "\f485"; } +.bi-menu-app::before { content: "\f486"; } +.bi-menu-button-fill::before { content: "\f487"; } +.bi-menu-button-wide-fill::before { content: "\f488"; } +.bi-menu-button-wide::before { content: "\f489"; } +.bi-menu-button::before { content: "\f48a"; } +.bi-menu-down::before { content: "\f48b"; } +.bi-menu-up::before { content: "\f48c"; } +.bi-mic-fill::before { content: "\f48d"; } +.bi-mic-mute-fill::before { content: "\f48e"; } +.bi-mic-mute::before { content: "\f48f"; } +.bi-mic::before { content: "\f490"; } +.bi-minecart-loaded::before { content: "\f491"; } +.bi-minecart::before { content: "\f492"; } +.bi-moisture::before { content: "\f493"; } +.bi-moon-fill::before { content: "\f494"; } +.bi-moon-stars-fill::before { content: "\f495"; } +.bi-moon-stars::before { content: "\f496"; } +.bi-moon::before { content: "\f497"; } +.bi-mouse-fill::before { content: "\f498"; } +.bi-mouse::before { content: "\f499"; } +.bi-mouse2-fill::before { content: "\f49a"; } +.bi-mouse2::before { content: "\f49b"; } +.bi-mouse3-fill::before { content: "\f49c"; } +.bi-mouse3::before { content: "\f49d"; } +.bi-music-note-beamed::before { content: "\f49e"; } +.bi-music-note-list::before { content: "\f49f"; } +.bi-music-note::before { content: "\f4a0"; } +.bi-music-player-fill::before { content: "\f4a1"; } +.bi-music-player::before { content: "\f4a2"; } +.bi-newspaper::before { content: "\f4a3"; } +.bi-node-minus-fill::before { content: "\f4a4"; } +.bi-node-minus::before { content: "\f4a5"; } +.bi-node-plus-fill::before { content: "\f4a6"; } +.bi-node-plus::before { content: "\f4a7"; } +.bi-nut-fill::before { content: "\f4a8"; } +.bi-nut::before { content: "\f4a9"; } +.bi-octagon-fill::before { content: "\f4aa"; } +.bi-octagon-half::before { content: "\f4ab"; } +.bi-octagon::before { content: "\f4ac"; } +.bi-option::before { content: "\f4ad"; } +.bi-outlet::before { content: "\f4ae"; } +.bi-paint-bucket::before { content: "\f4af"; } +.bi-palette-fill::before { content: "\f4b0"; } +.bi-palette::before { content: "\f4b1"; } +.bi-palette2::before { content: "\f4b2"; } +.bi-paperclip::before { content: "\f4b3"; } +.bi-paragraph::before { content: "\f4b4"; } +.bi-patch-check-fill::before { content: "\f4b5"; } +.bi-patch-check::before { content: "\f4b6"; } +.bi-patch-exclamation-fill::before { content: "\f4b7"; } +.bi-patch-exclamation::before { content: "\f4b8"; } +.bi-patch-minus-fill::before { content: "\f4b9"; } +.bi-patch-minus::before { content: "\f4ba"; } +.bi-patch-plus-fill::before { content: "\f4bb"; } +.bi-patch-plus::before { content: "\f4bc"; } +.bi-patch-question-fill::before { content: "\f4bd"; } +.bi-patch-question::before { content: "\f4be"; } +.bi-pause-btn-fill::before { content: "\f4bf"; } +.bi-pause-btn::before { content: "\f4c0"; } +.bi-pause-circle-fill::before { content: "\f4c1"; } +.bi-pause-circle::before { content: "\f4c2"; } +.bi-pause-fill::before { content: "\f4c3"; } +.bi-pause::before { content: "\f4c4"; } +.bi-peace-fill::before { content: "\f4c5"; } +.bi-peace::before { content: "\f4c6"; } +.bi-pen-fill::before { content: "\f4c7"; } +.bi-pen::before { content: "\f4c8"; } +.bi-pencil-fill::before { content: "\f4c9"; } +.bi-pencil-square::before { content: "\f4ca"; } +.bi-pencil::before { content: "\f4cb"; } +.bi-pentagon-fill::before { content: "\f4cc"; } +.bi-pentagon-half::before { content: "\f4cd"; } +.bi-pentagon::before { content: "\f4ce"; } +.bi-people-fill::before { content: "\f4cf"; } +.bi-people::before { content: "\f4d0"; } +.bi-percent::before { content: "\f4d1"; } +.bi-person-badge-fill::before { content: "\f4d2"; } +.bi-person-badge::before { content: "\f4d3"; } +.bi-person-bounding-box::before { content: "\f4d4"; } +.bi-person-check-fill::before { content: "\f4d5"; } +.bi-person-check::before { content: "\f4d6"; } +.bi-person-circle::before { content: "\f4d7"; } +.bi-person-dash-fill::before { content: "\f4d8"; } +.bi-person-dash::before { content: "\f4d9"; } +.bi-person-fill::before { content: "\f4da"; } +.bi-person-lines-fill::before { content: "\f4db"; } +.bi-person-plus-fill::before { content: "\f4dc"; } +.bi-person-plus::before { content: "\f4dd"; } +.bi-person-square::before { content: "\f4de"; } +.bi-person-x-fill::before { content: "\f4df"; } +.bi-person-x::before { content: "\f4e0"; } +.bi-person::before { content: "\f4e1"; } +.bi-phone-fill::before { content: "\f4e2"; } +.bi-phone-landscape-fill::before { content: "\f4e3"; } +.bi-phone-landscape::before { content: "\f4e4"; } +.bi-phone-vibrate-fill::before { content: "\f4e5"; } +.bi-phone-vibrate::before { content: "\f4e6"; } +.bi-phone::before { content: "\f4e7"; } +.bi-pie-chart-fill::before { content: "\f4e8"; } +.bi-pie-chart::before { content: "\f4e9"; } +.bi-pin-angle-fill::before { content: "\f4ea"; } +.bi-pin-angle::before { content: "\f4eb"; } +.bi-pin-fill::before { content: "\f4ec"; } +.bi-pin::before { content: "\f4ed"; } +.bi-pip-fill::before { content: "\f4ee"; } +.bi-pip::before { content: "\f4ef"; } +.bi-play-btn-fill::before { content: "\f4f0"; } +.bi-play-btn::before { content: "\f4f1"; } +.bi-play-circle-fill::before { content: "\f4f2"; } +.bi-play-circle::before { content: "\f4f3"; } +.bi-play-fill::before { content: "\f4f4"; } +.bi-play::before { content: "\f4f5"; } +.bi-plug-fill::before { content: "\f4f6"; } +.bi-plug::before { content: "\f4f7"; } +.bi-plus-circle-dotted::before { content: "\f4f8"; } +.bi-plus-circle-fill::before { content: "\f4f9"; } +.bi-plus-circle::before { content: "\f4fa"; } +.bi-plus-square-dotted::before { content: "\f4fb"; } +.bi-plus-square-fill::before { content: "\f4fc"; } +.bi-plus-square::before { content: "\f4fd"; } +.bi-plus::before { content: "\f4fe"; } +.bi-power::before { content: "\f4ff"; } +.bi-printer-fill::before { content: "\f500"; } +.bi-printer::before { content: "\f501"; } +.bi-puzzle-fill::before { content: "\f502"; } +.bi-puzzle::before { content: "\f503"; } +.bi-question-circle-fill::before { content: "\f504"; } +.bi-question-circle::before { content: "\f505"; } +.bi-question-diamond-fill::before { content: "\f506"; } +.bi-question-diamond::before { content: "\f507"; } +.bi-question-octagon-fill::before { content: "\f508"; } +.bi-question-octagon::before { content: "\f509"; } +.bi-question-square-fill::before { content: "\f50a"; } +.bi-question-square::before { content: "\f50b"; } +.bi-question::before { content: "\f50c"; } +.bi-rainbow::before { content: "\f50d"; } +.bi-receipt-cutoff::before { content: "\f50e"; } +.bi-receipt::before { content: "\f50f"; } +.bi-reception-0::before { content: "\f510"; } +.bi-reception-1::before { content: "\f511"; } +.bi-reception-2::before { content: "\f512"; } +.bi-reception-3::before { content: "\f513"; } +.bi-reception-4::before { content: "\f514"; } +.bi-record-btn-fill::before { content: "\f515"; } +.bi-record-btn::before { content: "\f516"; } +.bi-record-circle-fill::before { content: "\f517"; } +.bi-record-circle::before { content: "\f518"; } +.bi-record-fill::before { content: "\f519"; } +.bi-record::before { content: "\f51a"; } +.bi-record2-fill::before { content: "\f51b"; } +.bi-record2::before { content: "\f51c"; } +.bi-reply-all-fill::before { content: "\f51d"; } +.bi-reply-all::before { content: "\f51e"; } +.bi-reply-fill::before { content: "\f51f"; } +.bi-reply::before { content: "\f520"; } +.bi-rss-fill::before { content: "\f521"; } +.bi-rss::before { content: "\f522"; } +.bi-rulers::before { content: "\f523"; } +.bi-save-fill::before { content: "\f524"; } +.bi-save::before { content: "\f525"; } +.bi-save2-fill::before { content: "\f526"; } +.bi-save2::before { content: "\f527"; } +.bi-scissors::before { content: "\f528"; } +.bi-screwdriver::before { content: "\f529"; } +.bi-search::before { content: "\f52a"; } +.bi-segmented-nav::before { content: "\f52b"; } +.bi-server::before { content: "\f52c"; } +.bi-share-fill::before { content: "\f52d"; } +.bi-share::before { content: "\f52e"; } +.bi-shield-check::before { content: "\f52f"; } +.bi-shield-exclamation::before { content: "\f530"; } +.bi-shield-fill-check::before { content: "\f531"; } +.bi-shield-fill-exclamation::before { content: "\f532"; } +.bi-shield-fill-minus::before { content: "\f533"; } +.bi-shield-fill-plus::before { content: "\f534"; } +.bi-shield-fill-x::before { content: "\f535"; } +.bi-shield-fill::before { content: "\f536"; } +.bi-shield-lock-fill::before { content: "\f537"; } +.bi-shield-lock::before { content: "\f538"; } +.bi-shield-minus::before { content: "\f539"; } +.bi-shield-plus::before { content: "\f53a"; } +.bi-shield-shaded::before { content: "\f53b"; } +.bi-shield-slash-fill::before { content: "\f53c"; } +.bi-shield-slash::before { content: "\f53d"; } +.bi-shield-x::before { content: "\f53e"; } +.bi-shield::before { content: "\f53f"; } +.bi-shift-fill::before { content: "\f540"; } +.bi-shift::before { content: "\f541"; } +.bi-shop-window::before { content: "\f542"; } +.bi-shop::before { content: "\f543"; } +.bi-shuffle::before { content: "\f544"; } +.bi-signpost-2-fill::before { content: "\f545"; } +.bi-signpost-2::before { content: "\f546"; } +.bi-signpost-fill::before { content: "\f547"; } +.bi-signpost-split-fill::before { content: "\f548"; } +.bi-signpost-split::before { content: "\f549"; } +.bi-signpost::before { content: "\f54a"; } +.bi-sim-fill::before { content: "\f54b"; } +.bi-sim::before { content: "\f54c"; } +.bi-skip-backward-btn-fill::before { content: "\f54d"; } +.bi-skip-backward-btn::before { content: "\f54e"; } +.bi-skip-backward-circle-fill::before { content: "\f54f"; } +.bi-skip-backward-circle::before { content: "\f550"; } +.bi-skip-backward-fill::before { content: "\f551"; } +.bi-skip-backward::before { content: "\f552"; } +.bi-skip-end-btn-fill::before { content: "\f553"; } +.bi-skip-end-btn::before { content: "\f554"; } +.bi-skip-end-circle-fill::before { content: "\f555"; } +.bi-skip-end-circle::before { content: "\f556"; } +.bi-skip-end-fill::before { content: "\f557"; } +.bi-skip-end::before { content: "\f558"; } +.bi-skip-forward-btn-fill::before { content: "\f559"; } +.bi-skip-forward-btn::before { content: "\f55a"; } +.bi-skip-forward-circle-fill::before { content: "\f55b"; } +.bi-skip-forward-circle::before { content: "\f55c"; } +.bi-skip-forward-fill::before { content: "\f55d"; } +.bi-skip-forward::before { content: "\f55e"; } +.bi-skip-start-btn-fill::before { content: "\f55f"; } +.bi-skip-start-btn::before { content: "\f560"; } +.bi-skip-start-circle-fill::before { content: "\f561"; } +.bi-skip-start-circle::before { content: "\f562"; } +.bi-skip-start-fill::before { content: "\f563"; } +.bi-skip-start::before { content: "\f564"; } +.bi-slack::before { content: "\f565"; } +.bi-slash-circle-fill::before { content: "\f566"; } +.bi-slash-circle::before { content: "\f567"; } +.bi-slash-square-fill::before { content: "\f568"; } +.bi-slash-square::before { content: "\f569"; } +.bi-slash::before { content: "\f56a"; } +.bi-sliders::before { content: "\f56b"; } +.bi-smartwatch::before { content: "\f56c"; } +.bi-snow::before { content: "\f56d"; } +.bi-snow2::before { content: "\f56e"; } +.bi-snow3::before { content: "\f56f"; } +.bi-sort-alpha-down-alt::before { content: "\f570"; } +.bi-sort-alpha-down::before { content: "\f571"; } +.bi-sort-alpha-up-alt::before { content: "\f572"; } +.bi-sort-alpha-up::before { content: "\f573"; } +.bi-sort-down-alt::before { content: "\f574"; } +.bi-sort-down::before { content: "\f575"; } +.bi-sort-numeric-down-alt::before { content: "\f576"; } +.bi-sort-numeric-down::before { content: "\f577"; } +.bi-sort-numeric-up-alt::before { content: "\f578"; } +.bi-sort-numeric-up::before { content: "\f579"; } +.bi-sort-up-alt::before { content: "\f57a"; } +.bi-sort-up::before { content: "\f57b"; } +.bi-soundwave::before { content: "\f57c"; } +.bi-speaker-fill::before { content: "\f57d"; } +.bi-speaker::before { content: "\f57e"; } +.bi-speedometer::before { content: "\f57f"; } +.bi-speedometer2::before { content: "\f580"; } +.bi-spellcheck::before { content: "\f581"; } +.bi-square-fill::before { content: "\f582"; } +.bi-square-half::before { content: "\f583"; } +.bi-square::before { content: "\f584"; } +.bi-stack::before { content: "\f585"; } +.bi-star-fill::before { content: "\f586"; } +.bi-star-half::before { content: "\f587"; } +.bi-star::before { content: "\f588"; } +.bi-stars::before { content: "\f589"; } +.bi-stickies-fill::before { content: "\f58a"; } +.bi-stickies::before { content: "\f58b"; } +.bi-sticky-fill::before { content: "\f58c"; } +.bi-sticky::before { content: "\f58d"; } +.bi-stop-btn-fill::before { content: "\f58e"; } +.bi-stop-btn::before { content: "\f58f"; } +.bi-stop-circle-fill::before { content: "\f590"; } +.bi-stop-circle::before { content: "\f591"; } +.bi-stop-fill::before { content: "\f592"; } +.bi-stop::before { content: "\f593"; } +.bi-stoplights-fill::before { content: "\f594"; } +.bi-stoplights::before { content: "\f595"; } +.bi-stopwatch-fill::before { content: "\f596"; } +.bi-stopwatch::before { content: "\f597"; } +.bi-subtract::before { content: "\f598"; } +.bi-suit-club-fill::before { content: "\f599"; } +.bi-suit-club::before { content: "\f59a"; } +.bi-suit-diamond-fill::before { content: "\f59b"; } +.bi-suit-diamond::before { content: "\f59c"; } +.bi-suit-heart-fill::before { content: "\f59d"; } +.bi-suit-heart::before { content: "\f59e"; } +.bi-suit-spade-fill::before { content: "\f59f"; } +.bi-suit-spade::before { content: "\f5a0"; } +.bi-sun-fill::before { content: "\f5a1"; } +.bi-sun::before { content: "\f5a2"; } +.bi-sunglasses::before { content: "\f5a3"; } +.bi-sunrise-fill::before { content: "\f5a4"; } +.bi-sunrise::before { content: "\f5a5"; } +.bi-sunset-fill::before { content: "\f5a6"; } +.bi-sunset::before { content: "\f5a7"; } +.bi-symmetry-horizontal::before { content: "\f5a8"; } +.bi-symmetry-vertical::before { content: "\f5a9"; } +.bi-table::before { content: "\f5aa"; } +.bi-tablet-fill::before { content: "\f5ab"; } +.bi-tablet-landscape-fill::before { content: "\f5ac"; } +.bi-tablet-landscape::before { content: "\f5ad"; } +.bi-tablet::before { content: "\f5ae"; } +.bi-tag-fill::before { content: "\f5af"; } +.bi-tag::before { content: "\f5b0"; } +.bi-tags-fill::before { content: "\f5b1"; } +.bi-tags::before { content: "\f5b2"; } +.bi-telegram::before { content: "\f5b3"; } +.bi-telephone-fill::before { content: "\f5b4"; } +.bi-telephone-forward-fill::before { content: "\f5b5"; } +.bi-telephone-forward::before { content: "\f5b6"; } +.bi-telephone-inbound-fill::before { content: "\f5b7"; } +.bi-telephone-inbound::before { content: "\f5b8"; } +.bi-telephone-minus-fill::before { content: "\f5b9"; } +.bi-telephone-minus::before { content: "\f5ba"; } +.bi-telephone-outbound-fill::before { content: "\f5bb"; } +.bi-telephone-outbound::before { content: "\f5bc"; } +.bi-telephone-plus-fill::before { content: "\f5bd"; } +.bi-telephone-plus::before { content: "\f5be"; } +.bi-telephone-x-fill::before { content: "\f5bf"; } +.bi-telephone-x::before { content: "\f5c0"; } +.bi-telephone::before { content: "\f5c1"; } +.bi-terminal-fill::before { content: "\f5c2"; } +.bi-terminal::before { content: "\f5c3"; } +.bi-text-center::before { content: "\f5c4"; } +.bi-text-indent-left::before { content: "\f5c5"; } +.bi-text-indent-right::before { content: "\f5c6"; } +.bi-text-left::before { content: "\f5c7"; } +.bi-text-paragraph::before { content: "\f5c8"; } +.bi-text-right::before { content: "\f5c9"; } +.bi-textarea-resize::before { content: "\f5ca"; } +.bi-textarea-t::before { content: "\f5cb"; } +.bi-textarea::before { content: "\f5cc"; } +.bi-thermometer-half::before { content: "\f5cd"; } +.bi-thermometer-high::before { content: "\f5ce"; } +.bi-thermometer-low::before { content: "\f5cf"; } +.bi-thermometer-snow::before { content: "\f5d0"; } +.bi-thermometer-sun::before { content: "\f5d1"; } +.bi-thermometer::before { content: "\f5d2"; } +.bi-three-dots-vertical::before { content: "\f5d3"; } +.bi-three-dots::before { content: "\f5d4"; } +.bi-toggle-off::before { content: "\f5d5"; } +.bi-toggle-on::before { content: "\f5d6"; } +.bi-toggle2-off::before { content: "\f5d7"; } +.bi-toggle2-on::before { content: "\f5d8"; } +.bi-toggles::before { content: "\f5d9"; } +.bi-toggles2::before { content: "\f5da"; } +.bi-tools::before { content: "\f5db"; } +.bi-tornado::before { content: "\f5dc"; } +.bi-trash-fill::before { content: "\f5dd"; } +.bi-trash::before { content: "\f5de"; } +.bi-trash2-fill::before { content: "\f5df"; } +.bi-trash2::before { content: "\f5e0"; } +.bi-tree-fill::before { content: "\f5e1"; } +.bi-tree::before { content: "\f5e2"; } +.bi-triangle-fill::before { content: "\f5e3"; } +.bi-triangle-half::before { content: "\f5e4"; } +.bi-triangle::before { content: "\f5e5"; } +.bi-trophy-fill::before { content: "\f5e6"; } +.bi-trophy::before { content: "\f5e7"; } +.bi-tropical-storm::before { content: "\f5e8"; } +.bi-truck-flatbed::before { content: "\f5e9"; } +.bi-truck::before { content: "\f5ea"; } +.bi-tsunami::before { content: "\f5eb"; } +.bi-tv-fill::before { content: "\f5ec"; } +.bi-tv::before { content: "\f5ed"; } +.bi-twitch::before { content: "\f5ee"; } +.bi-twitter::before { content: "\f5ef"; } +.bi-type-bold::before { content: "\f5f0"; } +.bi-type-h1::before { content: "\f5f1"; } +.bi-type-h2::before { content: "\f5f2"; } +.bi-type-h3::before { content: "\f5f3"; } +.bi-type-italic::before { content: "\f5f4"; } +.bi-type-strikethrough::before { content: "\f5f5"; } +.bi-type-underline::before { content: "\f5f6"; } +.bi-type::before { content: "\f5f7"; } +.bi-ui-checks-grid::before { content: "\f5f8"; } +.bi-ui-checks::before { content: "\f5f9"; } +.bi-ui-radios-grid::before { content: "\f5fa"; } +.bi-ui-radios::before { content: "\f5fb"; } +.bi-umbrella-fill::before { content: "\f5fc"; } +.bi-umbrella::before { content: "\f5fd"; } +.bi-union::before { content: "\f5fe"; } +.bi-unlock-fill::before { content: "\f5ff"; } +.bi-unlock::before { content: "\f600"; } +.bi-upc-scan::before { content: "\f601"; } +.bi-upc::before { content: "\f602"; } +.bi-upload::before { content: "\f603"; } +.bi-vector-pen::before { content: "\f604"; } +.bi-view-list::before { content: "\f605"; } +.bi-view-stacked::before { content: "\f606"; } +.bi-vinyl-fill::before { content: "\f607"; } +.bi-vinyl::before { content: "\f608"; } +.bi-voicemail::before { content: "\f609"; } +.bi-volume-down-fill::before { content: "\f60a"; } +.bi-volume-down::before { content: "\f60b"; } +.bi-volume-mute-fill::before { content: "\f60c"; } +.bi-volume-mute::before { content: "\f60d"; } +.bi-volume-off-fill::before { content: "\f60e"; } +.bi-volume-off::before { content: "\f60f"; } +.bi-volume-up-fill::before { content: "\f610"; } +.bi-volume-up::before { content: "\f611"; } +.bi-vr::before { content: "\f612"; } +.bi-wallet-fill::before { content: "\f613"; } +.bi-wallet::before { content: "\f614"; } +.bi-wallet2::before { content: "\f615"; } +.bi-watch::before { content: "\f616"; } +.bi-water::before { content: "\f617"; } +.bi-whatsapp::before { content: "\f618"; } +.bi-wifi-1::before { content: "\f619"; } +.bi-wifi-2::before { content: "\f61a"; } +.bi-wifi-off::before { content: "\f61b"; } +.bi-wifi::before { content: "\f61c"; } +.bi-wind::before { content: "\f61d"; } +.bi-window-dock::before { content: "\f61e"; } +.bi-window-sidebar::before { content: "\f61f"; } +.bi-window::before { content: "\f620"; } +.bi-wrench::before { content: "\f621"; } +.bi-x-circle-fill::before { content: "\f622"; } +.bi-x-circle::before { content: "\f623"; } +.bi-x-diamond-fill::before { content: "\f624"; } +.bi-x-diamond::before { content: "\f625"; } +.bi-x-octagon-fill::before { content: "\f626"; } +.bi-x-octagon::before { content: "\f627"; } +.bi-x-square-fill::before { content: "\f628"; } +.bi-x-square::before { content: "\f629"; } +.bi-x::before { content: "\f62a"; } +.bi-youtube::before { content: "\f62b"; } +.bi-zoom-in::before { content: "\f62c"; } +.bi-zoom-out::before { content: "\f62d"; } +.bi-bank::before { content: "\f62e"; } +.bi-bank2::before { content: "\f62f"; } +.bi-bell-slash-fill::before { content: "\f630"; } +.bi-bell-slash::before { content: "\f631"; } +.bi-cash-coin::before { content: "\f632"; } +.bi-check-lg::before { content: "\f633"; } +.bi-coin::before { content: "\f634"; } +.bi-currency-bitcoin::before { content: "\f635"; } +.bi-currency-dollar::before { content: "\f636"; } +.bi-currency-euro::before { content: "\f637"; } +.bi-currency-exchange::before { content: "\f638"; } +.bi-currency-pound::before { content: "\f639"; } +.bi-currency-yen::before { content: "\f63a"; } +.bi-dash-lg::before { content: "\f63b"; } +.bi-exclamation-lg::before { content: "\f63c"; } +.bi-file-earmark-pdf-fill::before { content: "\f63d"; } +.bi-file-earmark-pdf::before { content: "\f63e"; } +.bi-file-pdf-fill::before { content: "\f63f"; } +.bi-file-pdf::before { content: "\f640"; } +.bi-gender-ambiguous::before { content: "\f641"; } +.bi-gender-female::before { content: "\f642"; } +.bi-gender-male::before { content: "\f643"; } +.bi-gender-trans::before { content: "\f644"; } +.bi-headset-vr::before { content: "\f645"; } +.bi-info-lg::before { content: "\f646"; } +.bi-mastodon::before { content: "\f647"; } +.bi-messenger::before { content: "\f648"; } +.bi-piggy-bank-fill::before { content: "\f649"; } +.bi-piggy-bank::before { content: "\f64a"; } +.bi-pin-map-fill::before { content: "\f64b"; } +.bi-pin-map::before { content: "\f64c"; } +.bi-plus-lg::before { content: "\f64d"; } +.bi-question-lg::before { content: "\f64e"; } +.bi-recycle::before { content: "\f64f"; } +.bi-reddit::before { content: "\f650"; } +.bi-safe-fill::before { content: "\f651"; } +.bi-safe2-fill::before { content: "\f652"; } +.bi-safe2::before { content: "\f653"; } +.bi-sd-card-fill::before { content: "\f654"; } +.bi-sd-card::before { content: "\f655"; } +.bi-skype::before { content: "\f656"; } +.bi-slash-lg::before { content: "\f657"; } +.bi-translate::before { content: "\f658"; } +.bi-x-lg::before { content: "\f659"; } +.bi-safe::before { content: "\f65a"; } +.bi-apple::before { content: "\f65b"; } +.bi-microsoft::before { content: "\f65d"; } +.bi-windows::before { content: "\f65e"; } +.bi-behance::before { content: "\f65c"; } +.bi-dribbble::before { content: "\f65f"; } +.bi-line::before { content: "\f660"; } +.bi-medium::before { content: "\f661"; } +.bi-paypal::before { content: "\f662"; } +.bi-pinterest::before { content: "\f663"; } +.bi-signal::before { content: "\f664"; } +.bi-snapchat::before { content: "\f665"; } +.bi-spotify::before { content: "\f666"; } +.bi-stack-overflow::before { content: "\f667"; } +.bi-strava::before { content: "\f668"; } +.bi-wordpress::before { content: "\f669"; } +.bi-vimeo::before { content: "\f66a"; } +.bi-activity::before { content: "\f66b"; } +.bi-easel2-fill::before { content: "\f66c"; } +.bi-easel2::before { content: "\f66d"; } +.bi-easel3-fill::before { content: "\f66e"; } +.bi-easel3::before { content: "\f66f"; } +.bi-fan::before { content: "\f670"; } +.bi-fingerprint::before { content: "\f671"; } +.bi-graph-down-arrow::before { content: "\f672"; } +.bi-graph-up-arrow::before { content: "\f673"; } +.bi-hypnotize::before { content: "\f674"; } +.bi-magic::before { content: "\f675"; } +.bi-person-rolodex::before { content: "\f676"; } +.bi-person-video::before { content: "\f677"; } +.bi-person-video2::before { content: "\f678"; } +.bi-person-video3::before { content: "\f679"; } +.bi-person-workspace::before { content: "\f67a"; } +.bi-radioactive::before { content: "\f67b"; } +.bi-webcam-fill::before { content: "\f67c"; } +.bi-webcam::before { content: "\f67d"; } +.bi-yin-yang::before { content: "\f67e"; } +.bi-bandaid-fill::before { content: "\f680"; } +.bi-bandaid::before { content: "\f681"; } +.bi-bluetooth::before { content: "\f682"; } +.bi-body-text::before { content: "\f683"; } +.bi-boombox::before { content: "\f684"; } +.bi-boxes::before { content: "\f685"; } +.bi-dpad-fill::before { content: "\f686"; } +.bi-dpad::before { content: "\f687"; } +.bi-ear-fill::before { content: "\f688"; } +.bi-ear::before { content: "\f689"; } +.bi-envelope-check-fill::before { content: "\f68b"; } +.bi-envelope-check::before { content: "\f68c"; } +.bi-envelope-dash-fill::before { content: "\f68e"; } +.bi-envelope-dash::before { content: "\f68f"; } +.bi-envelope-exclamation-fill::before { content: "\f691"; } +.bi-envelope-exclamation::before { content: "\f692"; } +.bi-envelope-plus-fill::before { content: "\f693"; } +.bi-envelope-plus::before { content: "\f694"; } +.bi-envelope-slash-fill::before { content: "\f696"; } +.bi-envelope-slash::before { content: "\f697"; } +.bi-envelope-x-fill::before { content: "\f699"; } +.bi-envelope-x::before { content: "\f69a"; } +.bi-explicit-fill::before { content: "\f69b"; } +.bi-explicit::before { content: "\f69c"; } +.bi-git::before { content: "\f69d"; } +.bi-infinity::before { content: "\f69e"; } +.bi-list-columns-reverse::before { content: "\f69f"; } +.bi-list-columns::before { content: "\f6a0"; } +.bi-meta::before { content: "\f6a1"; } +.bi-nintendo-switch::before { content: "\f6a4"; } +.bi-pc-display-horizontal::before { content: "\f6a5"; } +.bi-pc-display::before { content: "\f6a6"; } +.bi-pc-horizontal::before { content: "\f6a7"; } +.bi-pc::before { content: "\f6a8"; } +.bi-playstation::before { content: "\f6a9"; } +.bi-plus-slash-minus::before { content: "\f6aa"; } +.bi-projector-fill::before { content: "\f6ab"; } +.bi-projector::before { content: "\f6ac"; } +.bi-qr-code-scan::before { content: "\f6ad"; } +.bi-qr-code::before { content: "\f6ae"; } +.bi-quora::before { content: "\f6af"; } +.bi-quote::before { content: "\f6b0"; } +.bi-robot::before { content: "\f6b1"; } +.bi-send-check-fill::before { content: "\f6b2"; } +.bi-send-check::before { content: "\f6b3"; } +.bi-send-dash-fill::before { content: "\f6b4"; } +.bi-send-dash::before { content: "\f6b5"; } +.bi-send-exclamation-fill::before { content: "\f6b7"; } +.bi-send-exclamation::before { content: "\f6b8"; } +.bi-send-fill::before { content: "\f6b9"; } +.bi-send-plus-fill::before { content: "\f6ba"; } +.bi-send-plus::before { content: "\f6bb"; } +.bi-send-slash-fill::before { content: "\f6bc"; } +.bi-send-slash::before { content: "\f6bd"; } +.bi-send-x-fill::before { content: "\f6be"; } +.bi-send-x::before { content: "\f6bf"; } +.bi-send::before { content: "\f6c0"; } +.bi-steam::before { content: "\f6c1"; } +.bi-terminal-dash::before { content: "\f6c3"; } +.bi-terminal-plus::before { content: "\f6c4"; } +.bi-terminal-split::before { content: "\f6c5"; } +.bi-ticket-detailed-fill::before { content: "\f6c6"; } +.bi-ticket-detailed::before { content: "\f6c7"; } +.bi-ticket-fill::before { content: "\f6c8"; } +.bi-ticket-perforated-fill::before { content: "\f6c9"; } +.bi-ticket-perforated::before { content: "\f6ca"; } +.bi-ticket::before { content: "\f6cb"; } +.bi-tiktok::before { content: "\f6cc"; } +.bi-window-dash::before { content: "\f6cd"; } +.bi-window-desktop::before { content: "\f6ce"; } +.bi-window-fullscreen::before { content: "\f6cf"; } +.bi-window-plus::before { content: "\f6d0"; } +.bi-window-split::before { content: "\f6d1"; } +.bi-window-stack::before { content: "\f6d2"; } +.bi-window-x::before { content: "\f6d3"; } +.bi-xbox::before { content: "\f6d4"; } +.bi-ethernet::before { content: "\f6d5"; } +.bi-hdmi-fill::before { content: "\f6d6"; } +.bi-hdmi::before { content: "\f6d7"; } +.bi-usb-c-fill::before { content: "\f6d8"; } +.bi-usb-c::before { content: "\f6d9"; } +.bi-usb-fill::before { content: "\f6da"; } +.bi-usb-plug-fill::before { content: "\f6db"; } +.bi-usb-plug::before { content: "\f6dc"; } +.bi-usb-symbol::before { content: "\f6dd"; } +.bi-usb::before { content: "\f6de"; } +.bi-boombox-fill::before { content: "\f6df"; } +.bi-displayport::before { content: "\f6e1"; } +.bi-gpu-card::before { content: "\f6e2"; } +.bi-memory::before { content: "\f6e3"; } +.bi-modem-fill::before { content: "\f6e4"; } +.bi-modem::before { content: "\f6e5"; } +.bi-motherboard-fill::before { content: "\f6e6"; } +.bi-motherboard::before { content: "\f6e7"; } +.bi-optical-audio-fill::before { content: "\f6e8"; } +.bi-optical-audio::before { content: "\f6e9"; } +.bi-pci-card::before { content: "\f6ea"; } +.bi-router-fill::before { content: "\f6eb"; } +.bi-router::before { content: "\f6ec"; } +.bi-thunderbolt-fill::before { content: "\f6ef"; } +.bi-thunderbolt::before { content: "\f6f0"; } +.bi-usb-drive-fill::before { content: "\f6f1"; } +.bi-usb-drive::before { content: "\f6f2"; } +.bi-usb-micro-fill::before { content: "\f6f3"; } +.bi-usb-micro::before { content: "\f6f4"; } +.bi-usb-mini-fill::before { content: "\f6f5"; } +.bi-usb-mini::before { content: "\f6f6"; } +.bi-cloud-haze2::before { content: "\f6f7"; } +.bi-device-hdd-fill::before { content: "\f6f8"; } +.bi-device-hdd::before { content: "\f6f9"; } +.bi-device-ssd-fill::before { content: "\f6fa"; } +.bi-device-ssd::before { content: "\f6fb"; } +.bi-displayport-fill::before { content: "\f6fc"; } +.bi-mortarboard-fill::before { content: "\f6fd"; } +.bi-mortarboard::before { content: "\f6fe"; } +.bi-terminal-x::before { content: "\f6ff"; } +.bi-arrow-through-heart-fill::before { content: "\f700"; } +.bi-arrow-through-heart::before { content: "\f701"; } +.bi-badge-sd-fill::before { content: "\f702"; } +.bi-badge-sd::before { content: "\f703"; } +.bi-bag-heart-fill::before { content: "\f704"; } +.bi-bag-heart::before { content: "\f705"; } +.bi-balloon-fill::before { content: "\f706"; } +.bi-balloon-heart-fill::before { content: "\f707"; } +.bi-balloon-heart::before { content: "\f708"; } +.bi-balloon::before { content: "\f709"; } +.bi-box2-fill::before { content: "\f70a"; } +.bi-box2-heart-fill::before { content: "\f70b"; } +.bi-box2-heart::before { content: "\f70c"; } +.bi-box2::before { content: "\f70d"; } +.bi-braces-asterisk::before { content: "\f70e"; } +.bi-calendar-heart-fill::before { content: "\f70f"; } +.bi-calendar-heart::before { content: "\f710"; } +.bi-calendar2-heart-fill::before { content: "\f711"; } +.bi-calendar2-heart::before { content: "\f712"; } +.bi-chat-heart-fill::before { content: "\f713"; } +.bi-chat-heart::before { content: "\f714"; } +.bi-chat-left-heart-fill::before { content: "\f715"; } +.bi-chat-left-heart::before { content: "\f716"; } +.bi-chat-right-heart-fill::before { content: "\f717"; } +.bi-chat-right-heart::before { content: "\f718"; } +.bi-chat-square-heart-fill::before { content: "\f719"; } +.bi-chat-square-heart::before { content: "\f71a"; } +.bi-clipboard-check-fill::before { content: "\f71b"; } +.bi-clipboard-data-fill::before { content: "\f71c"; } +.bi-clipboard-fill::before { content: "\f71d"; } +.bi-clipboard-heart-fill::before { content: "\f71e"; } +.bi-clipboard-heart::before { content: "\f71f"; } +.bi-clipboard-minus-fill::before { content: "\f720"; } +.bi-clipboard-plus-fill::before { content: "\f721"; } +.bi-clipboard-pulse::before { content: "\f722"; } +.bi-clipboard-x-fill::before { content: "\f723"; } +.bi-clipboard2-check-fill::before { content: "\f724"; } +.bi-clipboard2-check::before { content: "\f725"; } +.bi-clipboard2-data-fill::before { content: "\f726"; } +.bi-clipboard2-data::before { content: "\f727"; } +.bi-clipboard2-fill::before { content: "\f728"; } +.bi-clipboard2-heart-fill::before { content: "\f729"; } +.bi-clipboard2-heart::before { content: "\f72a"; } +.bi-clipboard2-minus-fill::before { content: "\f72b"; } +.bi-clipboard2-minus::before { content: "\f72c"; } +.bi-clipboard2-plus-fill::before { content: "\f72d"; } +.bi-clipboard2-plus::before { content: "\f72e"; } +.bi-clipboard2-pulse-fill::before { content: "\f72f"; } +.bi-clipboard2-pulse::before { content: "\f730"; } +.bi-clipboard2-x-fill::before { content: "\f731"; } +.bi-clipboard2-x::before { content: "\f732"; } +.bi-clipboard2::before { content: "\f733"; } +.bi-emoji-kiss-fill::before { content: "\f734"; } +.bi-emoji-kiss::before { content: "\f735"; } +.bi-envelope-heart-fill::before { content: "\f736"; } +.bi-envelope-heart::before { content: "\f737"; } +.bi-envelope-open-heart-fill::before { content: "\f738"; } +.bi-envelope-open-heart::before { content: "\f739"; } +.bi-envelope-paper-fill::before { content: "\f73a"; } +.bi-envelope-paper-heart-fill::before { content: "\f73b"; } +.bi-envelope-paper-heart::before { content: "\f73c"; } +.bi-envelope-paper::before { content: "\f73d"; } +.bi-filetype-aac::before { content: "\f73e"; } +.bi-filetype-ai::before { content: "\f73f"; } +.bi-filetype-bmp::before { content: "\f740"; } +.bi-filetype-cs::before { content: "\f741"; } +.bi-filetype-css::before { content: "\f742"; } +.bi-filetype-csv::before { content: "\f743"; } +.bi-filetype-doc::before { content: "\f744"; } +.bi-filetype-docx::before { content: "\f745"; } +.bi-filetype-exe::before { content: "\f746"; } +.bi-filetype-gif::before { content: "\f747"; } +.bi-filetype-heic::before { content: "\f748"; } +.bi-filetype-html::before { content: "\f749"; } +.bi-filetype-java::before { content: "\f74a"; } +.bi-filetype-jpg::before { content: "\f74b"; } +.bi-filetype-js::before { content: "\f74c"; } +.bi-filetype-jsx::before { content: "\f74d"; } +.bi-filetype-key::before { content: "\f74e"; } +.bi-filetype-m4p::before { content: "\f74f"; } +.bi-filetype-md::before { content: "\f750"; } +.bi-filetype-mdx::before { content: "\f751"; } +.bi-filetype-mov::before { content: "\f752"; } +.bi-filetype-mp3::before { content: "\f753"; } +.bi-filetype-mp4::before { content: "\f754"; } +.bi-filetype-otf::before { content: "\f755"; } +.bi-filetype-pdf::before { content: "\f756"; } +.bi-filetype-php::before { content: "\f757"; } +.bi-filetype-png::before { content: "\f758"; } +.bi-filetype-ppt::before { content: "\f75a"; } +.bi-filetype-psd::before { content: "\f75b"; } +.bi-filetype-py::before { content: "\f75c"; } +.bi-filetype-raw::before { content: "\f75d"; } +.bi-filetype-rb::before { content: "\f75e"; } +.bi-filetype-sass::before { content: "\f75f"; } +.bi-filetype-scss::before { content: "\f760"; } +.bi-filetype-sh::before { content: "\f761"; } +.bi-filetype-svg::before { content: "\f762"; } +.bi-filetype-tiff::before { content: "\f763"; } +.bi-filetype-tsx::before { content: "\f764"; } +.bi-filetype-ttf::before { content: "\f765"; } +.bi-filetype-txt::before { content: "\f766"; } +.bi-filetype-wav::before { content: "\f767"; } +.bi-filetype-woff::before { content: "\f768"; } +.bi-filetype-xls::before { content: "\f76a"; } +.bi-filetype-xml::before { content: "\f76b"; } +.bi-filetype-yml::before { content: "\f76c"; } +.bi-heart-arrow::before { content: "\f76d"; } +.bi-heart-pulse-fill::before { content: "\f76e"; } +.bi-heart-pulse::before { content: "\f76f"; } +.bi-heartbreak-fill::before { content: "\f770"; } +.bi-heartbreak::before { content: "\f771"; } +.bi-hearts::before { content: "\f772"; } +.bi-hospital-fill::before { content: "\f773"; } +.bi-hospital::before { content: "\f774"; } +.bi-house-heart-fill::before { content: "\f775"; } +.bi-house-heart::before { content: "\f776"; } +.bi-incognito::before { content: "\f777"; } +.bi-magnet-fill::before { content: "\f778"; } +.bi-magnet::before { content: "\f779"; } +.bi-person-heart::before { content: "\f77a"; } +.bi-person-hearts::before { content: "\f77b"; } +.bi-phone-flip::before { content: "\f77c"; } +.bi-plugin::before { content: "\f77d"; } +.bi-postage-fill::before { content: "\f77e"; } +.bi-postage-heart-fill::before { content: "\f77f"; } +.bi-postage-heart::before { content: "\f780"; } +.bi-postage::before { content: "\f781"; } +.bi-postcard-fill::before { content: "\f782"; } +.bi-postcard-heart-fill::before { content: "\f783"; } +.bi-postcard-heart::before { content: "\f784"; } +.bi-postcard::before { content: "\f785"; } +.bi-search-heart-fill::before { content: "\f786"; } +.bi-search-heart::before { content: "\f787"; } +.bi-sliders2-vertical::before { content: "\f788"; } +.bi-sliders2::before { content: "\f789"; } +.bi-trash3-fill::before { content: "\f78a"; } +.bi-trash3::before { content: "\f78b"; } +.bi-valentine::before { content: "\f78c"; } +.bi-valentine2::before { content: "\f78d"; } +.bi-wrench-adjustable-circle-fill::before { content: "\f78e"; } +.bi-wrench-adjustable-circle::before { content: "\f78f"; } +.bi-wrench-adjustable::before { content: "\f790"; } +.bi-filetype-json::before { content: "\f791"; } +.bi-filetype-pptx::before { content: "\f792"; } +.bi-filetype-xlsx::before { content: "\f793"; } +.bi-1-circle-fill::before { content: "\f796"; } +.bi-1-circle::before { content: "\f797"; } +.bi-1-square-fill::before { content: "\f798"; } +.bi-1-square::before { content: "\f799"; } +.bi-2-circle-fill::before { content: "\f79c"; } +.bi-2-circle::before { content: "\f79d"; } +.bi-2-square-fill::before { content: "\f79e"; } +.bi-2-square::before { content: "\f79f"; } +.bi-3-circle-fill::before { content: "\f7a2"; } +.bi-3-circle::before { content: "\f7a3"; } +.bi-3-square-fill::before { content: "\f7a4"; } +.bi-3-square::before { content: "\f7a5"; } +.bi-4-circle-fill::before { content: "\f7a8"; } +.bi-4-circle::before { content: "\f7a9"; } +.bi-4-square-fill::before { content: "\f7aa"; } +.bi-4-square::before { content: "\f7ab"; } +.bi-5-circle-fill::before { content: "\f7ae"; } +.bi-5-circle::before { content: "\f7af"; } +.bi-5-square-fill::before { content: "\f7b0"; } +.bi-5-square::before { content: "\f7b1"; } +.bi-6-circle-fill::before { content: "\f7b4"; } +.bi-6-circle::before { content: "\f7b5"; } +.bi-6-square-fill::before { content: "\f7b6"; } +.bi-6-square::before { content: "\f7b7"; } +.bi-7-circle-fill::before { content: "\f7ba"; } +.bi-7-circle::before { content: "\f7bb"; } +.bi-7-square-fill::before { content: "\f7bc"; } +.bi-7-square::before { content: "\f7bd"; } +.bi-8-circle-fill::before { content: "\f7c0"; } +.bi-8-circle::before { content: "\f7c1"; } +.bi-8-square-fill::before { content: "\f7c2"; } +.bi-8-square::before { content: "\f7c3"; } +.bi-9-circle-fill::before { content: "\f7c6"; } +.bi-9-circle::before { content: "\f7c7"; } +.bi-9-square-fill::before { content: "\f7c8"; } +.bi-9-square::before { content: "\f7c9"; } +.bi-airplane-engines-fill::before { content: "\f7ca"; } +.bi-airplane-engines::before { content: "\f7cb"; } +.bi-airplane-fill::before { content: "\f7cc"; } +.bi-airplane::before { content: "\f7cd"; } +.bi-alexa::before { content: "\f7ce"; } +.bi-alipay::before { content: "\f7cf"; } +.bi-android::before { content: "\f7d0"; } +.bi-android2::before { content: "\f7d1"; } +.bi-box-fill::before { content: "\f7d2"; } +.bi-box-seam-fill::before { content: "\f7d3"; } +.bi-browser-chrome::before { content: "\f7d4"; } +.bi-browser-edge::before { content: "\f7d5"; } +.bi-browser-firefox::before { content: "\f7d6"; } +.bi-browser-safari::before { content: "\f7d7"; } +.bi-c-circle-fill::before { content: "\f7da"; } +.bi-c-circle::before { content: "\f7db"; } +.bi-c-square-fill::before { content: "\f7dc"; } +.bi-c-square::before { content: "\f7dd"; } +.bi-capsule-pill::before { content: "\f7de"; } +.bi-capsule::before { content: "\f7df"; } +.bi-car-front-fill::before { content: "\f7e0"; } +.bi-car-front::before { content: "\f7e1"; } +.bi-cassette-fill::before { content: "\f7e2"; } +.bi-cassette::before { content: "\f7e3"; } +.bi-cc-circle-fill::before { content: "\f7e6"; } +.bi-cc-circle::before { content: "\f7e7"; } +.bi-cc-square-fill::before { content: "\f7e8"; } +.bi-cc-square::before { content: "\f7e9"; } +.bi-cup-hot-fill::before { content: "\f7ea"; } +.bi-cup-hot::before { content: "\f7eb"; } +.bi-currency-rupee::before { content: "\f7ec"; } +.bi-dropbox::before { content: "\f7ed"; } +.bi-escape::before { content: "\f7ee"; } +.bi-fast-forward-btn-fill::before { content: "\f7ef"; } +.bi-fast-forward-btn::before { content: "\f7f0"; } +.bi-fast-forward-circle-fill::before { content: "\f7f1"; } +.bi-fast-forward-circle::before { content: "\f7f2"; } +.bi-fast-forward-fill::before { content: "\f7f3"; } +.bi-fast-forward::before { content: "\f7f4"; } +.bi-filetype-sql::before { content: "\f7f5"; } +.bi-fire::before { content: "\f7f6"; } +.bi-google-play::before { content: "\f7f7"; } +.bi-h-circle-fill::before { content: "\f7fa"; } +.bi-h-circle::before { content: "\f7fb"; } +.bi-h-square-fill::before { content: "\f7fc"; } +.bi-h-square::before { content: "\f7fd"; } +.bi-indent::before { content: "\f7fe"; } +.bi-lungs-fill::before { content: "\f7ff"; } +.bi-lungs::before { content: "\f800"; } +.bi-microsoft-teams::before { content: "\f801"; } +.bi-p-circle-fill::before { content: "\f804"; } +.bi-p-circle::before { content: "\f805"; } +.bi-p-square-fill::before { content: "\f806"; } +.bi-p-square::before { content: "\f807"; } +.bi-pass-fill::before { content: "\f808"; } +.bi-pass::before { content: "\f809"; } +.bi-prescription::before { content: "\f80a"; } +.bi-prescription2::before { content: "\f80b"; } +.bi-r-circle-fill::before { content: "\f80e"; } +.bi-r-circle::before { content: "\f80f"; } +.bi-r-square-fill::before { content: "\f810"; } +.bi-r-square::before { content: "\f811"; } +.bi-repeat-1::before { content: "\f812"; } +.bi-repeat::before { content: "\f813"; } +.bi-rewind-btn-fill::before { content: "\f814"; } +.bi-rewind-btn::before { content: "\f815"; } +.bi-rewind-circle-fill::before { content: "\f816"; } +.bi-rewind-circle::before { content: "\f817"; } +.bi-rewind-fill::before { content: "\f818"; } +.bi-rewind::before { content: "\f819"; } +.bi-train-freight-front-fill::before { content: "\f81a"; } +.bi-train-freight-front::before { content: "\f81b"; } +.bi-train-front-fill::before { content: "\f81c"; } +.bi-train-front::before { content: "\f81d"; } +.bi-train-lightrail-front-fill::before { content: "\f81e"; } +.bi-train-lightrail-front::before { content: "\f81f"; } +.bi-truck-front-fill::before { content: "\f820"; } +.bi-truck-front::before { content: "\f821"; } +.bi-ubuntu::before { content: "\f822"; } +.bi-unindent::before { content: "\f823"; } +.bi-unity::before { content: "\f824"; } +.bi-universal-access-circle::before { content: "\f825"; } +.bi-universal-access::before { content: "\f826"; } +.bi-virus::before { content: "\f827"; } +.bi-virus2::before { content: "\f828"; } +.bi-wechat::before { content: "\f829"; } +.bi-yelp::before { content: "\f82a"; } +.bi-sign-stop-fill::before { content: "\f82b"; } +.bi-sign-stop-lights-fill::before { content: "\f82c"; } +.bi-sign-stop-lights::before { content: "\f82d"; } +.bi-sign-stop::before { content: "\f82e"; } +.bi-sign-turn-left-fill::before { content: "\f82f"; } +.bi-sign-turn-left::before { content: "\f830"; } +.bi-sign-turn-right-fill::before { content: "\f831"; } +.bi-sign-turn-right::before { content: "\f832"; } +.bi-sign-turn-slight-left-fill::before { content: "\f833"; } +.bi-sign-turn-slight-left::before { content: "\f834"; } +.bi-sign-turn-slight-right-fill::before { content: "\f835"; } +.bi-sign-turn-slight-right::before { content: "\f836"; } +.bi-sign-yield-fill::before { content: "\f837"; } +.bi-sign-yield::before { content: "\f838"; } +.bi-ev-station-fill::before { content: "\f839"; } +.bi-ev-station::before { content: "\f83a"; } +.bi-fuel-pump-diesel-fill::before { content: "\f83b"; } +.bi-fuel-pump-diesel::before { content: "\f83c"; } +.bi-fuel-pump-fill::before { content: "\f83d"; } +.bi-fuel-pump::before { content: "\f83e"; } +.bi-0-circle-fill::before { content: "\f83f"; } +.bi-0-circle::before { content: "\f840"; } +.bi-0-square-fill::before { content: "\f841"; } +.bi-0-square::before { content: "\f842"; } +.bi-rocket-fill::before { content: "\f843"; } +.bi-rocket-takeoff-fill::before { content: "\f844"; } +.bi-rocket-takeoff::before { content: "\f845"; } +.bi-rocket::before { content: "\f846"; } +.bi-stripe::before { content: "\f847"; } +.bi-subscript::before { content: "\f848"; } +.bi-superscript::before { content: "\f849"; } +.bi-trello::before { content: "\f84a"; } +.bi-envelope-at-fill::before { content: "\f84b"; } +.bi-envelope-at::before { content: "\f84c"; } +.bi-regex::before { content: "\f84d"; } +.bi-text-wrap::before { content: "\f84e"; } +.bi-sign-dead-end-fill::before { content: "\f84f"; } +.bi-sign-dead-end::before { content: "\f850"; } +.bi-sign-do-not-enter-fill::before { content: "\f851"; } +.bi-sign-do-not-enter::before { content: "\f852"; } +.bi-sign-intersection-fill::before { content: "\f853"; } +.bi-sign-intersection-side-fill::before { content: "\f854"; } +.bi-sign-intersection-side::before { content: "\f855"; } +.bi-sign-intersection-t-fill::before { content: "\f856"; } +.bi-sign-intersection-t::before { content: "\f857"; } +.bi-sign-intersection-y-fill::before { content: "\f858"; } +.bi-sign-intersection-y::before { content: "\f859"; } +.bi-sign-intersection::before { content: "\f85a"; } +.bi-sign-merge-left-fill::before { content: "\f85b"; } +.bi-sign-merge-left::before { content: "\f85c"; } +.bi-sign-merge-right-fill::before { content: "\f85d"; } +.bi-sign-merge-right::before { content: "\f85e"; } +.bi-sign-no-left-turn-fill::before { content: "\f85f"; } +.bi-sign-no-left-turn::before { content: "\f860"; } +.bi-sign-no-parking-fill::before { content: "\f861"; } +.bi-sign-no-parking::before { content: "\f862"; } +.bi-sign-no-right-turn-fill::before { content: "\f863"; } +.bi-sign-no-right-turn::before { content: "\f864"; } +.bi-sign-railroad-fill::before { content: "\f865"; } +.bi-sign-railroad::before { content: "\f866"; } +.bi-building-add::before { content: "\f867"; } +.bi-building-check::before { content: "\f868"; } +.bi-building-dash::before { content: "\f869"; } +.bi-building-down::before { content: "\f86a"; } +.bi-building-exclamation::before { content: "\f86b"; } +.bi-building-fill-add::before { content: "\f86c"; } +.bi-building-fill-check::before { content: "\f86d"; } +.bi-building-fill-dash::before { content: "\f86e"; } +.bi-building-fill-down::before { content: "\f86f"; } +.bi-building-fill-exclamation::before { content: "\f870"; } +.bi-building-fill-gear::before { content: "\f871"; } +.bi-building-fill-lock::before { content: "\f872"; } +.bi-building-fill-slash::before { content: "\f873"; } +.bi-building-fill-up::before { content: "\f874"; } +.bi-building-fill-x::before { content: "\f875"; } +.bi-building-fill::before { content: "\f876"; } +.bi-building-gear::before { content: "\f877"; } +.bi-building-lock::before { content: "\f878"; } +.bi-building-slash::before { content: "\f879"; } +.bi-building-up::before { content: "\f87a"; } +.bi-building-x::before { content: "\f87b"; } +.bi-buildings-fill::before { content: "\f87c"; } +.bi-buildings::before { content: "\f87d"; } +.bi-bus-front-fill::before { content: "\f87e"; } +.bi-bus-front::before { content: "\f87f"; } +.bi-ev-front-fill::before { content: "\f880"; } +.bi-ev-front::before { content: "\f881"; } +.bi-globe-americas::before { content: "\f882"; } +.bi-globe-asia-australia::before { content: "\f883"; } +.bi-globe-central-south-asia::before { content: "\f884"; } +.bi-globe-europe-africa::before { content: "\f885"; } +.bi-house-add-fill::before { content: "\f886"; } +.bi-house-add::before { content: "\f887"; } +.bi-house-check-fill::before { content: "\f888"; } +.bi-house-check::before { content: "\f889"; } +.bi-house-dash-fill::before { content: "\f88a"; } +.bi-house-dash::before { content: "\f88b"; } +.bi-house-down-fill::before { content: "\f88c"; } +.bi-house-down::before { content: "\f88d"; } +.bi-house-exclamation-fill::before { content: "\f88e"; } +.bi-house-exclamation::before { content: "\f88f"; } +.bi-house-gear-fill::before { content: "\f890"; } +.bi-house-gear::before { content: "\f891"; } +.bi-house-lock-fill::before { content: "\f892"; } +.bi-house-lock::before { content: "\f893"; } +.bi-house-slash-fill::before { content: "\f894"; } +.bi-house-slash::before { content: "\f895"; } +.bi-house-up-fill::before { content: "\f896"; } +.bi-house-up::before { content: "\f897"; } +.bi-house-x-fill::before { content: "\f898"; } +.bi-house-x::before { content: "\f899"; } +.bi-person-add::before { content: "\f89a"; } +.bi-person-down::before { content: "\f89b"; } +.bi-person-exclamation::before { content: "\f89c"; } +.bi-person-fill-add::before { content: "\f89d"; } +.bi-person-fill-check::before { content: "\f89e"; } +.bi-person-fill-dash::before { content: "\f89f"; } +.bi-person-fill-down::before { content: "\f8a0"; } +.bi-person-fill-exclamation::before { content: "\f8a1"; } +.bi-person-fill-gear::before { content: "\f8a2"; } +.bi-person-fill-lock::before { content: "\f8a3"; } +.bi-person-fill-slash::before { content: "\f8a4"; } +.bi-person-fill-up::before { content: "\f8a5"; } +.bi-person-fill-x::before { content: "\f8a6"; } +.bi-person-gear::before { content: "\f8a7"; } +.bi-person-lock::before { content: "\f8a8"; } +.bi-person-slash::before { content: "\f8a9"; } +.bi-person-up::before { content: "\f8aa"; } +.bi-scooter::before { content: "\f8ab"; } +.bi-taxi-front-fill::before { content: "\f8ac"; } +.bi-taxi-front::before { content: "\f8ad"; } +.bi-amd::before { content: "\f8ae"; } +.bi-database-add::before { content: "\f8af"; } +.bi-database-check::before { content: "\f8b0"; } +.bi-database-dash::before { content: "\f8b1"; } +.bi-database-down::before { content: "\f8b2"; } +.bi-database-exclamation::before { content: "\f8b3"; } +.bi-database-fill-add::before { content: "\f8b4"; } +.bi-database-fill-check::before { content: "\f8b5"; } +.bi-database-fill-dash::before { content: "\f8b6"; } +.bi-database-fill-down::before { content: "\f8b7"; } +.bi-database-fill-exclamation::before { content: "\f8b8"; } +.bi-database-fill-gear::before { content: "\f8b9"; } +.bi-database-fill-lock::before { content: "\f8ba"; } +.bi-database-fill-slash::before { content: "\f8bb"; } +.bi-database-fill-up::before { content: "\f8bc"; } +.bi-database-fill-x::before { content: "\f8bd"; } +.bi-database-fill::before { content: "\f8be"; } +.bi-database-gear::before { content: "\f8bf"; } +.bi-database-lock::before { content: "\f8c0"; } +.bi-database-slash::before { content: "\f8c1"; } +.bi-database-up::before { content: "\f8c2"; } +.bi-database-x::before { content: "\f8c3"; } +.bi-database::before { content: "\f8c4"; } +.bi-houses-fill::before { content: "\f8c5"; } +.bi-houses::before { content: "\f8c6"; } +.bi-nvidia::before { content: "\f8c7"; } +.bi-person-vcard-fill::before { content: "\f8c8"; } +.bi-person-vcard::before { content: "\f8c9"; } +.bi-sina-weibo::before { content: "\f8ca"; } +.bi-tencent-qq::before { content: "\f8cb"; } +.bi-wikipedia::before { content: "\f8cc"; } +.bi-alphabet-uppercase::before { content: "\f2a5"; } +.bi-alphabet::before { content: "\f68a"; } +.bi-amazon::before { content: "\f68d"; } +.bi-arrows-collapse-vertical::before { content: "\f690"; } +.bi-arrows-expand-vertical::before { content: "\f695"; } +.bi-arrows-vertical::before { content: "\f698"; } +.bi-arrows::before { content: "\f6a2"; } +.bi-ban-fill::before { content: "\f6a3"; } +.bi-ban::before { content: "\f6b6"; } +.bi-bing::before { content: "\f6c2"; } +.bi-cake::before { content: "\f6e0"; } +.bi-cake2::before { content: "\f6ed"; } +.bi-cookie::before { content: "\f6ee"; } +.bi-copy::before { content: "\f759"; } +.bi-crosshair::before { content: "\f769"; } +.bi-crosshair2::before { content: "\f794"; } +.bi-emoji-astonished-fill::before { content: "\f795"; } +.bi-emoji-astonished::before { content: "\f79a"; } +.bi-emoji-grimace-fill::before { content: "\f79b"; } +.bi-emoji-grimace::before { content: "\f7a0"; } +.bi-emoji-grin-fill::before { content: "\f7a1"; } +.bi-emoji-grin::before { content: "\f7a6"; } +.bi-emoji-surprise-fill::before { content: "\f7a7"; } +.bi-emoji-surprise::before { content: "\f7ac"; } +.bi-emoji-tear-fill::before { content: "\f7ad"; } +.bi-emoji-tear::before { content: "\f7b2"; } +.bi-envelope-arrow-down-fill::before { content: "\f7b3"; } +.bi-envelope-arrow-down::before { content: "\f7b8"; } +.bi-envelope-arrow-up-fill::before { content: "\f7b9"; } +.bi-envelope-arrow-up::before { content: "\f7be"; } +.bi-feather::before { content: "\f7bf"; } +.bi-feather2::before { content: "\f7c4"; } +.bi-floppy-fill::before { content: "\f7c5"; } +.bi-floppy::before { content: "\f7d8"; } +.bi-floppy2-fill::before { content: "\f7d9"; } +.bi-floppy2::before { content: "\f7e4"; } +.bi-gitlab::before { content: "\f7e5"; } +.bi-highlighter::before { content: "\f7f8"; } +.bi-marker-tip::before { content: "\f802"; } +.bi-nvme-fill::before { content: "\f803"; } +.bi-nvme::before { content: "\f80c"; } +.bi-opencollective::before { content: "\f80d"; } +.bi-pci-card-network::before { content: "\f8cd"; } +.bi-pci-card-sound::before { content: "\f8ce"; } +.bi-radar::before { content: "\f8cf"; } +.bi-send-arrow-down-fill::before { content: "\f8d0"; } +.bi-send-arrow-down::before { content: "\f8d1"; } +.bi-send-arrow-up-fill::before { content: "\f8d2"; } +.bi-send-arrow-up::before { content: "\f8d3"; } +.bi-sim-slash-fill::before { content: "\f8d4"; } +.bi-sim-slash::before { content: "\f8d5"; } +.bi-sourceforge::before { content: "\f8d6"; } +.bi-substack::before { content: "\f8d7"; } +.bi-threads-fill::before { content: "\f8d8"; } +.bi-threads::before { content: "\f8d9"; } +.bi-transparency::before { content: "\f8da"; } +.bi-twitter-x::before { content: "\f8db"; } +.bi-type-h4::before { content: "\f8dc"; } +.bi-type-h5::before { content: "\f8dd"; } +.bi-type-h6::before { content: "\f8de"; } +.bi-backpack-fill::before { content: "\f8df"; } +.bi-backpack::before { content: "\f8e0"; } +.bi-backpack2-fill::before { content: "\f8e1"; } +.bi-backpack2::before { content: "\f8e2"; } +.bi-backpack3-fill::before { content: "\f8e3"; } +.bi-backpack3::before { content: "\f8e4"; } +.bi-backpack4-fill::before { content: "\f8e5"; } +.bi-backpack4::before { content: "\f8e6"; } +.bi-brilliance::before { content: "\f8e7"; } +.bi-cake-fill::before { content: "\f8e8"; } +.bi-cake2-fill::before { content: "\f8e9"; } +.bi-duffle-fill::before { content: "\f8ea"; } +.bi-duffle::before { content: "\f8eb"; } +.bi-exposure::before { content: "\f8ec"; } +.bi-gender-neuter::before { content: "\f8ed"; } +.bi-highlights::before { content: "\f8ee"; } +.bi-luggage-fill::before { content: "\f8ef"; } +.bi-luggage::before { content: "\f8f0"; } +.bi-mailbox-flag::before { content: "\f8f1"; } +.bi-mailbox2-flag::before { content: "\f8f2"; } +.bi-noise-reduction::before { content: "\f8f3"; } +.bi-passport-fill::before { content: "\f8f4"; } +.bi-passport::before { content: "\f8f5"; } +.bi-person-arms-up::before { content: "\f8f6"; } +.bi-person-raised-hand::before { content: "\f8f7"; } +.bi-person-standing-dress::before { content: "\f8f8"; } +.bi-person-standing::before { content: "\f8f9"; } +.bi-person-walking::before { content: "\f8fa"; } +.bi-person-wheelchair::before { content: "\f8fb"; } +.bi-shadows::before { content: "\f8fc"; } +.bi-suitcase-fill::before { content: "\f8fd"; } +.bi-suitcase-lg-fill::before { content: "\f8fe"; } +.bi-suitcase-lg::before { content: "\f8ff"; } +.bi-suitcase::before { content: "\f900"; } +.bi-suitcase2-fill::before { content: "\f901"; } +.bi-suitcase2::before { content: "\f902"; } +.bi-vignette::before { content: "\f903"; } diff --git a/site_libs/bootstrap/bootstrap-icons.woff b/site_libs/bootstrap/bootstrap-icons.woff new file mode 100644 index 00000000..dbeeb055 Binary files /dev/null and b/site_libs/bootstrap/bootstrap-icons.woff differ diff --git a/site_libs/bootstrap/bootstrap.min.css b/site_libs/bootstrap/bootstrap.min.css new file mode 100644 index 00000000..988da14e --- /dev/null +++ b/site_libs/bootstrap/bootstrap.min.css @@ -0,0 +1,12 @@ +/*! + * Bootstrap v5.3.1 (https://getbootstrap.com/) + * Copyright 2011-2023 The Bootstrap Authors + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */@import"https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@300;400;700&display=swap";:root,[data-bs-theme=light]{--bs-blue: #2780e3;--bs-indigo: #6610f2;--bs-purple: #613d7c;--bs-pink: #e83e8c;--bs-red: #ff0039;--bs-orange: #f0ad4e;--bs-yellow: #ff7518;--bs-green: #3fb618;--bs-teal: #20c997;--bs-cyan: #9954bb;--bs-black: #000;--bs-white: #fff;--bs-gray: #6c757d;--bs-gray-dark: #343a40;--bs-gray-100: #f8f9fa;--bs-gray-200: #e9ecef;--bs-gray-300: #dee2e6;--bs-gray-400: #ced4da;--bs-gray-500: #adb5bd;--bs-gray-600: #6c757d;--bs-gray-700: #495057;--bs-gray-800: #343a40;--bs-gray-900: #212529;--bs-default: #343a40;--bs-primary: #2780e3;--bs-secondary: #343a40;--bs-success: #3fb618;--bs-info: #9954bb;--bs-warning: #ff7518;--bs-danger: #ff0039;--bs-light: #f8f9fa;--bs-dark: #343a40;--bs-default-rgb: 52, 58, 64;--bs-primary-rgb: 39, 128, 227;--bs-secondary-rgb: 52, 58, 64;--bs-success-rgb: 63, 182, 24;--bs-info-rgb: 153, 84, 187;--bs-warning-rgb: 255, 117, 24;--bs-danger-rgb: 255, 0, 57;--bs-light-rgb: 248, 249, 250;--bs-dark-rgb: 52, 58, 64;--bs-primary-text-emphasis: #10335b;--bs-secondary-text-emphasis: #15171a;--bs-success-text-emphasis: #19490a;--bs-info-text-emphasis: #3d224b;--bs-warning-text-emphasis: #662f0a;--bs-danger-text-emphasis: #660017;--bs-light-text-emphasis: #495057;--bs-dark-text-emphasis: #495057;--bs-primary-bg-subtle: #d4e6f9;--bs-secondary-bg-subtle: #d6d8d9;--bs-success-bg-subtle: #d9f0d1;--bs-info-bg-subtle: #ebddf1;--bs-warning-bg-subtle: #ffe3d1;--bs-danger-bg-subtle: #ffccd7;--bs-light-bg-subtle: #fcfcfd;--bs-dark-bg-subtle: #ced4da;--bs-primary-border-subtle: #a9ccf4;--bs-secondary-border-subtle: #aeb0b3;--bs-success-border-subtle: #b2e2a3;--bs-info-border-subtle: #d6bbe4;--bs-warning-border-subtle: #ffc8a3;--bs-danger-border-subtle: #ff99b0;--bs-light-border-subtle: #e9ecef;--bs-dark-border-subtle: #adb5bd;--bs-white-rgb: 255, 255, 255;--bs-black-rgb: 0, 0, 0;--bs-font-sans-serif: "Source Sans Pro", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));--bs-root-font-size: 17px;--bs-body-font-family: "Source Sans Pro", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";--bs-body-font-size:1rem;--bs-body-font-weight: 400;--bs-body-line-height: 1.5;--bs-body-color: #343a40;--bs-body-color-rgb: 52, 58, 64;--bs-body-bg: #fff;--bs-body-bg-rgb: 255, 255, 255;--bs-emphasis-color: #000;--bs-emphasis-color-rgb: 0, 0, 0;--bs-secondary-color: rgba(52, 58, 64, 0.75);--bs-secondary-color-rgb: 52, 58, 64;--bs-secondary-bg: #e9ecef;--bs-secondary-bg-rgb: 233, 236, 239;--bs-tertiary-color: rgba(52, 58, 64, 0.5);--bs-tertiary-color-rgb: 52, 58, 64;--bs-tertiary-bg: #f8f9fa;--bs-tertiary-bg-rgb: 248, 249, 250;--bs-heading-color: inherit;--bs-link-color: #2761e3;--bs-link-color-rgb: 39, 97, 227;--bs-link-decoration: underline;--bs-link-hover-color: #1f4eb6;--bs-link-hover-color-rgb: 31, 78, 182;--bs-code-color: #7d12ba;--bs-highlight-bg: #ffe3d1;--bs-border-width: 1px;--bs-border-style: solid;--bs-border-color: #dee2e6;--bs-border-color-translucent: rgba(0, 0, 0, 0.175);--bs-border-radius: 0.25rem;--bs-border-radius-sm: 0.2em;--bs-border-radius-lg: 0.5rem;--bs-border-radius-xl: 1rem;--bs-border-radius-xxl: 2rem;--bs-border-radius-2xl: var(--bs-border-radius-xxl);--bs-border-radius-pill: 50rem;--bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);--bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);--bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075);--bs-focus-ring-width: 0.25rem;--bs-focus-ring-opacity: 0.25;--bs-focus-ring-color: rgba(39, 128, 227, 0.25);--bs-form-valid-color: #3fb618;--bs-form-valid-border-color: #3fb618;--bs-form-invalid-color: #ff0039;--bs-form-invalid-border-color: #ff0039}[data-bs-theme=dark]{color-scheme:dark;--bs-body-color: #dee2e6;--bs-body-color-rgb: 222, 226, 230;--bs-body-bg: #212529;--bs-body-bg-rgb: 33, 37, 41;--bs-emphasis-color: #fff;--bs-emphasis-color-rgb: 255, 255, 255;--bs-secondary-color: rgba(222, 226, 230, 0.75);--bs-secondary-color-rgb: 222, 226, 230;--bs-secondary-bg: #343a40;--bs-secondary-bg-rgb: 52, 58, 64;--bs-tertiary-color: rgba(222, 226, 230, 0.5);--bs-tertiary-color-rgb: 222, 226, 230;--bs-tertiary-bg: #2b3035;--bs-tertiary-bg-rgb: 43, 48, 53;--bs-primary-text-emphasis: #7db3ee;--bs-secondary-text-emphasis: #85898c;--bs-success-text-emphasis: #8cd374;--bs-info-text-emphasis: #c298d6;--bs-warning-text-emphasis: #ffac74;--bs-danger-text-emphasis: #ff6688;--bs-light-text-emphasis: #f8f9fa;--bs-dark-text-emphasis: #dee2e6;--bs-primary-bg-subtle: #081a2d;--bs-secondary-bg-subtle: #0a0c0d;--bs-success-bg-subtle: #0d2405;--bs-info-bg-subtle: #1f1125;--bs-warning-bg-subtle: #331705;--bs-danger-bg-subtle: #33000b;--bs-light-bg-subtle: #343a40;--bs-dark-bg-subtle: #1a1d20;--bs-primary-border-subtle: #174d88;--bs-secondary-border-subtle: #1f2326;--bs-success-border-subtle: #266d0e;--bs-info-border-subtle: #5c3270;--bs-warning-border-subtle: #99460e;--bs-danger-border-subtle: #990022;--bs-light-border-subtle: #495057;--bs-dark-border-subtle: #343a40;--bs-heading-color: inherit;--bs-link-color: #7db3ee;--bs-link-hover-color: #97c2f1;--bs-link-color-rgb: 125, 179, 238;--bs-link-hover-color-rgb: 151, 194, 241;--bs-code-color: white;--bs-border-color: #495057;--bs-border-color-translucent: rgba(255, 255, 255, 0.15);--bs-form-valid-color: #8cd374;--bs-form-valid-border-color: #8cd374;--bs-form-invalid-color: #ff6688;--bs-form-invalid-border-color: #ff6688}*,*::before,*::after{box-sizing:border-box}:root{font-size:var(--bs-root-font-size)}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:rgba(0,0,0,0)}hr{margin:1rem 0;color:inherit;border:0;border-top:1px solid;opacity:.25}h6,.h6,h5,.h5,h4,.h4,h3,.h3,h2,.h2,h1,.h1{margin-top:0;margin-bottom:.5rem;font-weight:400;line-height:1.2;color:var(--bs-heading-color)}h1,.h1{font-size:calc(1.325rem + 0.9vw)}@media(min-width: 1200px){h1,.h1{font-size:2rem}}h2,.h2{font-size:calc(1.29rem + 0.48vw)}@media(min-width: 1200px){h2,.h2{font-size:1.65rem}}h3,.h3{font-size:calc(1.27rem + 0.24vw)}@media(min-width: 1200px){h3,.h3{font-size:1.45rem}}h4,.h4{font-size:1.25rem}h5,.h5{font-size:1.1rem}h6,.h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[title]{text-decoration:underline dotted;-webkit-text-decoration:underline dotted;-moz-text-decoration:underline dotted;-ms-text-decoration:underline dotted;-o-text-decoration:underline dotted;cursor:help;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}ol,ul,dl{margin-top:0;margin-bottom:1rem}ol ol,ul ul,ol ul,ul ol{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem;padding:.625rem 1.25rem;border-left:.25rem solid #e9ecef}blockquote p:last-child,blockquote ul:last-child,blockquote ol:last-child{margin-bottom:0}b,strong{font-weight:bolder}small,.small{font-size:0.875em}mark,.mark{padding:.1875em;background-color:var(--bs-highlight-bg)}sub,sup{position:relative;font-size:0.75em;line-height:0;vertical-align:baseline}sub{bottom:-0.25em}sup{top:-0.5em}a{color:rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));text-decoration:underline;-webkit-text-decoration:underline;-moz-text-decoration:underline;-ms-text-decoration:underline;-o-text-decoration:underline}a:hover{--bs-link-color-rgb: var(--bs-link-hover-color-rgb)}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}pre,code,kbd,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:0.875em;color:#000;background-color:#f8f9fa;padding:.5rem;border:1px solid var(--bs-border-color, #dee2e6)}pre code{background-color:rgba(0,0,0,0);font-size:inherit;color:inherit;word-break:normal}code{font-size:0.875em;color:var(--bs-code-color);background-color:#f8f9fa;padding:.125rem .25rem;word-wrap:break-word}a>code{color:inherit}kbd{padding:.4rem .4rem;font-size:0.875em;color:#fff;background-color:#343a40}kbd kbd{padding:0;font-size:1em}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:rgba(52,58,64,.75);text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}thead,tbody,tfoot,tr,td,th{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}input,button,select,optgroup,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator{display:none !important}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button}button:not(:disabled),[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + 0.3vw);line-height:inherit}@media(min-width: 1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-text,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::file-selector-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none !important}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:calc(1.625rem + 4.5vw);font-weight:300;line-height:1.2}@media(min-width: 1200px){.display-1{font-size:5rem}}.display-2{font-size:calc(1.575rem + 3.9vw);font-weight:300;line-height:1.2}@media(min-width: 1200px){.display-2{font-size:4.5rem}}.display-3{font-size:calc(1.525rem + 3.3vw);font-weight:300;line-height:1.2}@media(min-width: 1200px){.display-3{font-size:4rem}}.display-4{font-size:calc(1.475rem + 2.7vw);font-weight:300;line-height:1.2}@media(min-width: 1200px){.display-4{font-size:3.5rem}}.display-5{font-size:calc(1.425rem + 2.1vw);font-weight:300;line-height:1.2}@media(min-width: 1200px){.display-5{font-size:3rem}}.display-6{font-size:calc(1.375rem + 1.5vw);font-weight:300;line-height:1.2}@media(min-width: 1200px){.display-6{font-size:2.5rem}}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:0.875em;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote>:last-child{margin-bottom:0}.blockquote-footer{margin-top:-1rem;margin-bottom:1rem;font-size:0.875em;color:#6c757d}.blockquote-footer::before{content:"— "}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#fff;border:1px solid #dee2e6;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:0.875em;color:rgba(52,58,64,.75)}.container,.container-fluid,.container-xxl,.container-xl,.container-lg,.container-md,.container-sm{--bs-gutter-x: 1.5rem;--bs-gutter-y: 0;width:100%;padding-right:calc(var(--bs-gutter-x)*.5);padding-left:calc(var(--bs-gutter-x)*.5);margin-right:auto;margin-left:auto}@media(min-width: 576px){.container-sm,.container{max-width:540px}}@media(min-width: 768px){.container-md,.container-sm,.container{max-width:720px}}@media(min-width: 992px){.container-lg,.container-md,.container-sm,.container{max-width:960px}}@media(min-width: 1200px){.container-xl,.container-lg,.container-md,.container-sm,.container{max-width:1140px}}@media(min-width: 1400px){.container-xxl,.container-xl,.container-lg,.container-md,.container-sm,.container{max-width:1320px}}:root{--bs-breakpoint-xs: 0;--bs-breakpoint-sm: 576px;--bs-breakpoint-md: 768px;--bs-breakpoint-lg: 992px;--bs-breakpoint-xl: 1200px;--bs-breakpoint-xxl: 1400px}.grid{display:grid;grid-template-rows:repeat(var(--bs-rows, 1), 1fr);grid-template-columns:repeat(var(--bs-columns, 12), 1fr);gap:var(--bs-gap, 1.5rem)}.grid .g-col-1{grid-column:auto/span 1}.grid .g-col-2{grid-column:auto/span 2}.grid .g-col-3{grid-column:auto/span 3}.grid .g-col-4{grid-column:auto/span 4}.grid .g-col-5{grid-column:auto/span 5}.grid .g-col-6{grid-column:auto/span 6}.grid .g-col-7{grid-column:auto/span 7}.grid .g-col-8{grid-column:auto/span 8}.grid .g-col-9{grid-column:auto/span 9}.grid .g-col-10{grid-column:auto/span 10}.grid .g-col-11{grid-column:auto/span 11}.grid .g-col-12{grid-column:auto/span 12}.grid .g-start-1{grid-column-start:1}.grid .g-start-2{grid-column-start:2}.grid .g-start-3{grid-column-start:3}.grid .g-start-4{grid-column-start:4}.grid .g-start-5{grid-column-start:5}.grid .g-start-6{grid-column-start:6}.grid .g-start-7{grid-column-start:7}.grid .g-start-8{grid-column-start:8}.grid .g-start-9{grid-column-start:9}.grid .g-start-10{grid-column-start:10}.grid .g-start-11{grid-column-start:11}@media(min-width: 576px){.grid .g-col-sm-1{grid-column:auto/span 1}.grid .g-col-sm-2{grid-column:auto/span 2}.grid .g-col-sm-3{grid-column:auto/span 3}.grid .g-col-sm-4{grid-column:auto/span 4}.grid .g-col-sm-5{grid-column:auto/span 5}.grid .g-col-sm-6{grid-column:auto/span 6}.grid .g-col-sm-7{grid-column:auto/span 7}.grid .g-col-sm-8{grid-column:auto/span 8}.grid .g-col-sm-9{grid-column:auto/span 9}.grid .g-col-sm-10{grid-column:auto/span 10}.grid .g-col-sm-11{grid-column:auto/span 11}.grid .g-col-sm-12{grid-column:auto/span 12}.grid .g-start-sm-1{grid-column-start:1}.grid .g-start-sm-2{grid-column-start:2}.grid .g-start-sm-3{grid-column-start:3}.grid .g-start-sm-4{grid-column-start:4}.grid .g-start-sm-5{grid-column-start:5}.grid .g-start-sm-6{grid-column-start:6}.grid .g-start-sm-7{grid-column-start:7}.grid .g-start-sm-8{grid-column-start:8}.grid .g-start-sm-9{grid-column-start:9}.grid .g-start-sm-10{grid-column-start:10}.grid .g-start-sm-11{grid-column-start:11}}@media(min-width: 768px){.grid .g-col-md-1{grid-column:auto/span 1}.grid .g-col-md-2{grid-column:auto/span 2}.grid .g-col-md-3{grid-column:auto/span 3}.grid .g-col-md-4{grid-column:auto/span 4}.grid .g-col-md-5{grid-column:auto/span 5}.grid .g-col-md-6{grid-column:auto/span 6}.grid .g-col-md-7{grid-column:auto/span 7}.grid .g-col-md-8{grid-column:auto/span 8}.grid .g-col-md-9{grid-column:auto/span 9}.grid .g-col-md-10{grid-column:auto/span 10}.grid .g-col-md-11{grid-column:auto/span 11}.grid .g-col-md-12{grid-column:auto/span 12}.grid .g-start-md-1{grid-column-start:1}.grid .g-start-md-2{grid-column-start:2}.grid .g-start-md-3{grid-column-start:3}.grid .g-start-md-4{grid-column-start:4}.grid .g-start-md-5{grid-column-start:5}.grid .g-start-md-6{grid-column-start:6}.grid .g-start-md-7{grid-column-start:7}.grid .g-start-md-8{grid-column-start:8}.grid .g-start-md-9{grid-column-start:9}.grid .g-start-md-10{grid-column-start:10}.grid .g-start-md-11{grid-column-start:11}}@media(min-width: 992px){.grid .g-col-lg-1{grid-column:auto/span 1}.grid .g-col-lg-2{grid-column:auto/span 2}.grid .g-col-lg-3{grid-column:auto/span 3}.grid .g-col-lg-4{grid-column:auto/span 4}.grid .g-col-lg-5{grid-column:auto/span 5}.grid .g-col-lg-6{grid-column:auto/span 6}.grid .g-col-lg-7{grid-column:auto/span 7}.grid .g-col-lg-8{grid-column:auto/span 8}.grid .g-col-lg-9{grid-column:auto/span 9}.grid .g-col-lg-10{grid-column:auto/span 10}.grid .g-col-lg-11{grid-column:auto/span 11}.grid .g-col-lg-12{grid-column:auto/span 12}.grid .g-start-lg-1{grid-column-start:1}.grid .g-start-lg-2{grid-column-start:2}.grid .g-start-lg-3{grid-column-start:3}.grid .g-start-lg-4{grid-column-start:4}.grid .g-start-lg-5{grid-column-start:5}.grid .g-start-lg-6{grid-column-start:6}.grid .g-start-lg-7{grid-column-start:7}.grid .g-start-lg-8{grid-column-start:8}.grid .g-start-lg-9{grid-column-start:9}.grid .g-start-lg-10{grid-column-start:10}.grid .g-start-lg-11{grid-column-start:11}}@media(min-width: 1200px){.grid .g-col-xl-1{grid-column:auto/span 1}.grid .g-col-xl-2{grid-column:auto/span 2}.grid .g-col-xl-3{grid-column:auto/span 3}.grid .g-col-xl-4{grid-column:auto/span 4}.grid .g-col-xl-5{grid-column:auto/span 5}.grid .g-col-xl-6{grid-column:auto/span 6}.grid .g-col-xl-7{grid-column:auto/span 7}.grid .g-col-xl-8{grid-column:auto/span 8}.grid .g-col-xl-9{grid-column:auto/span 9}.grid .g-col-xl-10{grid-column:auto/span 10}.grid .g-col-xl-11{grid-column:auto/span 11}.grid .g-col-xl-12{grid-column:auto/span 12}.grid .g-start-xl-1{grid-column-start:1}.grid .g-start-xl-2{grid-column-start:2}.grid .g-start-xl-3{grid-column-start:3}.grid .g-start-xl-4{grid-column-start:4}.grid .g-start-xl-5{grid-column-start:5}.grid .g-start-xl-6{grid-column-start:6}.grid .g-start-xl-7{grid-column-start:7}.grid .g-start-xl-8{grid-column-start:8}.grid .g-start-xl-9{grid-column-start:9}.grid .g-start-xl-10{grid-column-start:10}.grid .g-start-xl-11{grid-column-start:11}}@media(min-width: 1400px){.grid .g-col-xxl-1{grid-column:auto/span 1}.grid .g-col-xxl-2{grid-column:auto/span 2}.grid .g-col-xxl-3{grid-column:auto/span 3}.grid .g-col-xxl-4{grid-column:auto/span 4}.grid .g-col-xxl-5{grid-column:auto/span 5}.grid .g-col-xxl-6{grid-column:auto/span 6}.grid .g-col-xxl-7{grid-column:auto/span 7}.grid .g-col-xxl-8{grid-column:auto/span 8}.grid .g-col-xxl-9{grid-column:auto/span 9}.grid .g-col-xxl-10{grid-column:auto/span 10}.grid .g-col-xxl-11{grid-column:auto/span 11}.grid .g-col-xxl-12{grid-column:auto/span 12}.grid .g-start-xxl-1{grid-column-start:1}.grid .g-start-xxl-2{grid-column-start:2}.grid .g-start-xxl-3{grid-column-start:3}.grid .g-start-xxl-4{grid-column-start:4}.grid .g-start-xxl-5{grid-column-start:5}.grid .g-start-xxl-6{grid-column-start:6}.grid .g-start-xxl-7{grid-column-start:7}.grid .g-start-xxl-8{grid-column-start:8}.grid .g-start-xxl-9{grid-column-start:9}.grid .g-start-xxl-10{grid-column-start:10}.grid .g-start-xxl-11{grid-column-start:11}}.table{--bs-table-color-type: initial;--bs-table-bg-type: initial;--bs-table-color-state: initial;--bs-table-bg-state: initial;--bs-table-color: #343a40;--bs-table-bg: #fff;--bs-table-border-color: #dee2e6;--bs-table-accent-bg: transparent;--bs-table-striped-color: #343a40;--bs-table-striped-bg: rgba(0, 0, 0, 0.05);--bs-table-active-color: #343a40;--bs-table-active-bg: rgba(0, 0, 0, 0.1);--bs-table-hover-color: #343a40;--bs-table-hover-bg: rgba(0, 0, 0, 0.075);width:100%;margin-bottom:1rem;vertical-align:top;border-color:var(--bs-table-border-color)}.table>:not(caption)>*>*{padding:.5rem .5rem;color:var(--bs-table-color-state, var(--bs-table-color-type, var(--bs-table-color)));background-color:var(--bs-table-bg);border-bottom-width:1px;box-shadow:inset 0 0 0 9999px var(--bs-table-bg-state, var(--bs-table-bg-type, var(--bs-table-accent-bg)))}.table>tbody{vertical-align:inherit}.table>thead{vertical-align:bottom}.table-group-divider{border-top:calc(1px*2) solid #b2bac1}.caption-top{caption-side:top}.table-sm>:not(caption)>*>*{padding:.25rem .25rem}.table-bordered>:not(caption)>*{border-width:1px 0}.table-bordered>:not(caption)>*>*{border-width:0 1px}.table-borderless>:not(caption)>*>*{border-bottom-width:0}.table-borderless>:not(:first-child){border-top-width:0}.table-striped>tbody>tr:nth-of-type(odd)>*{--bs-table-color-type: var(--bs-table-striped-color);--bs-table-bg-type: var(--bs-table-striped-bg)}.table-striped-columns>:not(caption)>tr>:nth-child(even){--bs-table-color-type: var(--bs-table-striped-color);--bs-table-bg-type: var(--bs-table-striped-bg)}.table-active{--bs-table-color-state: var(--bs-table-active-color);--bs-table-bg-state: var(--bs-table-active-bg)}.table-hover>tbody>tr:hover>*{--bs-table-color-state: var(--bs-table-hover-color);--bs-table-bg-state: var(--bs-table-hover-bg)}.table-primary{--bs-table-color: #000;--bs-table-bg: #d4e6f9;--bs-table-border-color: #bfcfe0;--bs-table-striped-bg: #c9dbed;--bs-table-striped-color: #000;--bs-table-active-bg: #bfcfe0;--bs-table-active-color: #000;--bs-table-hover-bg: #c4d5e6;--bs-table-hover-color: #000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-secondary{--bs-table-color: #000;--bs-table-bg: #d6d8d9;--bs-table-border-color: #c1c2c3;--bs-table-striped-bg: #cbcdce;--bs-table-striped-color: #000;--bs-table-active-bg: #c1c2c3;--bs-table-active-color: #000;--bs-table-hover-bg: #c6c8c9;--bs-table-hover-color: #000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-success{--bs-table-color: #000;--bs-table-bg: #d9f0d1;--bs-table-border-color: #c3d8bc;--bs-table-striped-bg: #cee4c7;--bs-table-striped-color: #000;--bs-table-active-bg: #c3d8bc;--bs-table-active-color: #000;--bs-table-hover-bg: #c9dec1;--bs-table-hover-color: #000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-info{--bs-table-color: #000;--bs-table-bg: #ebddf1;--bs-table-border-color: #d4c7d9;--bs-table-striped-bg: #dfd2e5;--bs-table-striped-color: #000;--bs-table-active-bg: #d4c7d9;--bs-table-active-color: #000;--bs-table-hover-bg: #d9ccdf;--bs-table-hover-color: #000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-warning{--bs-table-color: #000;--bs-table-bg: #ffe3d1;--bs-table-border-color: #e6ccbc;--bs-table-striped-bg: #f2d8c7;--bs-table-striped-color: #000;--bs-table-active-bg: #e6ccbc;--bs-table-active-color: #000;--bs-table-hover-bg: #ecd2c1;--bs-table-hover-color: #000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-danger{--bs-table-color: #000;--bs-table-bg: #ffccd7;--bs-table-border-color: #e6b8c2;--bs-table-striped-bg: #f2c2cc;--bs-table-striped-color: #000;--bs-table-active-bg: #e6b8c2;--bs-table-active-color: #000;--bs-table-hover-bg: #ecbdc7;--bs-table-hover-color: #000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-light{--bs-table-color: #000;--bs-table-bg: #f8f9fa;--bs-table-border-color: #dfe0e1;--bs-table-striped-bg: #ecedee;--bs-table-striped-color: #000;--bs-table-active-bg: #dfe0e1;--bs-table-active-color: #000;--bs-table-hover-bg: #e5e6e7;--bs-table-hover-color: #000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-dark{--bs-table-color: #fff;--bs-table-bg: #343a40;--bs-table-border-color: #484e53;--bs-table-striped-bg: #3e444a;--bs-table-striped-color: #fff;--bs-table-active-bg: #484e53;--bs-table-active-color: #fff;--bs-table-hover-bg: #43494e;--bs-table-hover-color: #fff;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-responsive{overflow-x:auto;-webkit-overflow-scrolling:touch}@media(max-width: 575.98px){.table-responsive-sm{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media(max-width: 767.98px){.table-responsive-md{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media(max-width: 991.98px){.table-responsive-lg{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media(max-width: 1199.98px){.table-responsive-xl{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media(max-width: 1399.98px){.table-responsive-xxl{overflow-x:auto;-webkit-overflow-scrolling:touch}}.form-label,.shiny-input-container .control-label{margin-bottom:.5rem}.col-form-label{padding-top:calc(0.375rem + 1px);padding-bottom:calc(0.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(0.5rem + 1px);padding-bottom:calc(0.5rem + 1px);font-size:1.25rem}.col-form-label-sm{padding-top:calc(0.25rem + 1px);padding-bottom:calc(0.25rem + 1px);font-size:0.875rem}.form-text{margin-top:.25rem;font-size:0.875em;color:rgba(52,58,64,.75)}.form-control{display:block;width:100%;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#343a40;appearance:none;-webkit-appearance:none;-moz-appearance:none;-ms-appearance:none;-o-appearance:none;background-color:#fff;background-clip:padding-box;border:1px solid #dee2e6;border-radius:0;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media(prefers-reduced-motion: reduce){.form-control{transition:none}}.form-control[type=file]{overflow:hidden}.form-control[type=file]:not(:disabled):not([readonly]){cursor:pointer}.form-control:focus{color:#343a40;background-color:#fff;border-color:#93c0f1;outline:0;box-shadow:0 0 0 .25rem rgba(39,128,227,.25)}.form-control::-webkit-date-and-time-value{min-width:85px;height:1.5em;margin:0}.form-control::-webkit-datetime-edit{display:block;padding:0}.form-control::placeholder{color:rgba(52,58,64,.75);opacity:1}.form-control:disabled{background-color:#e9ecef;opacity:1}.form-control::file-selector-button{padding:.375rem .75rem;margin:-0.375rem -0.75rem;margin-inline-end:.75rem;color:#343a40;background-color:#f8f9fa;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media(prefers-reduced-motion: reduce){.form-control::file-selector-button{transition:none}}.form-control:hover:not(:disabled):not([readonly])::file-selector-button{background-color:#e9ecef}.form-control-plaintext{display:block;width:100%;padding:.375rem 0;margin-bottom:0;line-height:1.5;color:#343a40;background-color:rgba(0,0,0,0);border:solid rgba(0,0,0,0);border-width:1px 0}.form-control-plaintext:focus{outline:0}.form-control-plaintext.form-control-sm,.form-control-plaintext.form-control-lg{padding-right:0;padding-left:0}.form-control-sm{min-height:calc(1.5em + 0.5rem + calc(1px * 2));padding:.25rem .5rem;font-size:0.875rem}.form-control-sm::file-selector-button{padding:.25rem .5rem;margin:-0.25rem -0.5rem;margin-inline-end:.5rem}.form-control-lg{min-height:calc(1.5em + 1rem + calc(1px * 2));padding:.5rem 1rem;font-size:1.25rem}.form-control-lg::file-selector-button{padding:.5rem 1rem;margin:-0.5rem -1rem;margin-inline-end:1rem}textarea.form-control{min-height:calc(1.5em + 0.75rem + calc(1px * 2))}textarea.form-control-sm{min-height:calc(1.5em + 0.5rem + calc(1px * 2))}textarea.form-control-lg{min-height:calc(1.5em + 1rem + calc(1px * 2))}.form-control-color{width:3rem;height:calc(1.5em + 0.75rem + calc(1px * 2));padding:.375rem}.form-control-color:not(:disabled):not([readonly]){cursor:pointer}.form-control-color::-moz-color-swatch{border:0 !important}.form-control-color::-webkit-color-swatch{border:0 !important}.form-control-color.form-control-sm{height:calc(1.5em + 0.5rem + calc(1px * 2))}.form-control-color.form-control-lg{height:calc(1.5em + 1rem + calc(1px * 2))}.form-select{--bs-form-select-bg-img: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e");display:block;width:100%;padding:.375rem 2.25rem .375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#343a40;appearance:none;-webkit-appearance:none;-moz-appearance:none;-ms-appearance:none;-o-appearance:none;background-color:#fff;background-image:var(--bs-form-select-bg-img),var(--bs-form-select-bg-icon, none);background-repeat:no-repeat;background-position:right .75rem center;background-size:16px 12px;border:1px solid #dee2e6;border-radius:0;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media(prefers-reduced-motion: reduce){.form-select{transition:none}}.form-select:focus{border-color:#93c0f1;outline:0;box-shadow:0 0 0 .25rem rgba(39,128,227,.25)}.form-select[multiple],.form-select[size]:not([size="1"]){padding-right:.75rem;background-image:none}.form-select:disabled{background-color:#e9ecef}.form-select:-moz-focusring{color:rgba(0,0,0,0);text-shadow:0 0 0 #343a40}.form-select-sm{padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:0.875rem}.form-select-lg{padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem}[data-bs-theme=dark] .form-select{--bs-form-select-bg-img: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23dee2e6' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e")}.form-check,.shiny-input-container .checkbox,.shiny-input-container .radio{display:block;min-height:1.5rem;padding-left:0;margin-bottom:.125rem}.form-check .form-check-input,.form-check .shiny-input-container .checkbox input,.form-check .shiny-input-container .radio input,.shiny-input-container .checkbox .form-check-input,.shiny-input-container .checkbox .shiny-input-container .checkbox input,.shiny-input-container .checkbox .shiny-input-container .radio input,.shiny-input-container .radio .form-check-input,.shiny-input-container .radio .shiny-input-container .checkbox input,.shiny-input-container .radio .shiny-input-container .radio input{float:left;margin-left:0}.form-check-reverse{padding-right:0;padding-left:0;text-align:right}.form-check-reverse .form-check-input{float:right;margin-right:0;margin-left:0}.form-check-input,.shiny-input-container .checkbox input,.shiny-input-container .checkbox-inline input,.shiny-input-container .radio input,.shiny-input-container .radio-inline input{--bs-form-check-bg: #fff;width:1em;height:1em;margin-top:.25em;vertical-align:top;appearance:none;-webkit-appearance:none;-moz-appearance:none;-ms-appearance:none;-o-appearance:none;background-color:var(--bs-form-check-bg);background-image:var(--bs-form-check-bg-image);background-repeat:no-repeat;background-position:center;background-size:contain;border:1px solid #dee2e6;print-color-adjust:exact}.form-check-input[type=radio],.shiny-input-container .checkbox input[type=radio],.shiny-input-container .checkbox-inline input[type=radio],.shiny-input-container .radio input[type=radio],.shiny-input-container .radio-inline input[type=radio]{border-radius:50%}.form-check-input:active,.shiny-input-container .checkbox input:active,.shiny-input-container .checkbox-inline input:active,.shiny-input-container .radio input:active,.shiny-input-container .radio-inline input:active{filter:brightness(90%)}.form-check-input:focus,.shiny-input-container .checkbox input:focus,.shiny-input-container .checkbox-inline input:focus,.shiny-input-container .radio input:focus,.shiny-input-container .radio-inline input:focus{border-color:#93c0f1;outline:0;box-shadow:0 0 0 .25rem rgba(39,128,227,.25)}.form-check-input:checked,.shiny-input-container .checkbox input:checked,.shiny-input-container .checkbox-inline input:checked,.shiny-input-container .radio input:checked,.shiny-input-container .radio-inline input:checked{background-color:#2780e3;border-color:#2780e3}.form-check-input:checked[type=checkbox],.shiny-input-container .checkbox input:checked[type=checkbox],.shiny-input-container .checkbox-inline input:checked[type=checkbox],.shiny-input-container .radio input:checked[type=checkbox],.shiny-input-container .radio-inline input:checked[type=checkbox]{--bs-form-check-bg-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='m6 10 3 3 6-6'/%3e%3c/svg%3e")}.form-check-input:checked[type=radio],.shiny-input-container .checkbox input:checked[type=radio],.shiny-input-container .checkbox-inline input:checked[type=radio],.shiny-input-container .radio input:checked[type=radio],.shiny-input-container .radio-inline input:checked[type=radio]{--bs-form-check-bg-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23fff'/%3e%3c/svg%3e")}.form-check-input[type=checkbox]:indeterminate,.shiny-input-container .checkbox input[type=checkbox]:indeterminate,.shiny-input-container .checkbox-inline input[type=checkbox]:indeterminate,.shiny-input-container .radio input[type=checkbox]:indeterminate,.shiny-input-container .radio-inline input[type=checkbox]:indeterminate{background-color:#2780e3;border-color:#2780e3;--bs-form-check-bg-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e")}.form-check-input:disabled,.shiny-input-container .checkbox input:disabled,.shiny-input-container .checkbox-inline input:disabled,.shiny-input-container .radio input:disabled,.shiny-input-container .radio-inline input:disabled{pointer-events:none;filter:none;opacity:.5}.form-check-input[disabled]~.form-check-label,.form-check-input[disabled]~span,.form-check-input:disabled~.form-check-label,.form-check-input:disabled~span,.shiny-input-container .checkbox input[disabled]~.form-check-label,.shiny-input-container .checkbox input[disabled]~span,.shiny-input-container .checkbox input:disabled~.form-check-label,.shiny-input-container .checkbox input:disabled~span,.shiny-input-container .checkbox-inline input[disabled]~.form-check-label,.shiny-input-container .checkbox-inline input[disabled]~span,.shiny-input-container .checkbox-inline input:disabled~.form-check-label,.shiny-input-container .checkbox-inline input:disabled~span,.shiny-input-container .radio input[disabled]~.form-check-label,.shiny-input-container .radio input[disabled]~span,.shiny-input-container .radio input:disabled~.form-check-label,.shiny-input-container .radio input:disabled~span,.shiny-input-container .radio-inline input[disabled]~.form-check-label,.shiny-input-container .radio-inline input[disabled]~span,.shiny-input-container .radio-inline input:disabled~.form-check-label,.shiny-input-container .radio-inline input:disabled~span{cursor:default;opacity:.5}.form-check-label,.shiny-input-container .checkbox label,.shiny-input-container .checkbox-inline label,.shiny-input-container .radio label,.shiny-input-container .radio-inline label{cursor:pointer}.form-switch{padding-left:2.5em}.form-switch .form-check-input{--bs-form-switch-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%280, 0, 0, 0.25%29'/%3e%3c/svg%3e");width:2em;margin-left:-2.5em;background-image:var(--bs-form-switch-bg);background-position:left center;transition:background-position .15s ease-in-out}@media(prefers-reduced-motion: reduce){.form-switch .form-check-input{transition:none}}.form-switch .form-check-input:focus{--bs-form-switch-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%2393c0f1'/%3e%3c/svg%3e")}.form-switch .form-check-input:checked{background-position:right center;--bs-form-switch-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.form-switch.form-check-reverse{padding-right:2.5em;padding-left:0}.form-switch.form-check-reverse .form-check-input{margin-right:-2.5em;margin-left:0}.form-check-inline{display:inline-block;margin-right:1rem}.btn-check{position:absolute;clip:rect(0, 0, 0, 0);pointer-events:none}.btn-check[disabled]+.btn,.btn-check:disabled+.btn{pointer-events:none;filter:none;opacity:.65}[data-bs-theme=dark] .form-switch .form-check-input:not(:checked):not(:focus){--bs-form-switch-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%28255, 255, 255, 0.25%29'/%3e%3c/svg%3e")}.form-range{width:100%;height:1.5rem;padding:0;appearance:none;-webkit-appearance:none;-moz-appearance:none;-ms-appearance:none;-o-appearance:none;background-color:rgba(0,0,0,0)}.form-range:focus{outline:0}.form-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(39,128,227,.25)}.form-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(39,128,227,.25)}.form-range::-moz-focus-outer{border:0}.form-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-0.25rem;appearance:none;-webkit-appearance:none;-moz-appearance:none;-ms-appearance:none;-o-appearance:none;background-color:#2780e3;border:0;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media(prefers-reduced-motion: reduce){.form-range::-webkit-slider-thumb{transition:none}}.form-range::-webkit-slider-thumb:active{background-color:#bed9f7}.form-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:rgba(0,0,0,0);cursor:pointer;background-color:#f8f9fa;border-color:rgba(0,0,0,0)}.form-range::-moz-range-thumb{width:1rem;height:1rem;appearance:none;-webkit-appearance:none;-moz-appearance:none;-ms-appearance:none;-o-appearance:none;background-color:#2780e3;border:0;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media(prefers-reduced-motion: reduce){.form-range::-moz-range-thumb{transition:none}}.form-range::-moz-range-thumb:active{background-color:#bed9f7}.form-range::-moz-range-track{width:100%;height:.5rem;color:rgba(0,0,0,0);cursor:pointer;background-color:#f8f9fa;border-color:rgba(0,0,0,0)}.form-range:disabled{pointer-events:none}.form-range:disabled::-webkit-slider-thumb{background-color:rgba(52,58,64,.75)}.form-range:disabled::-moz-range-thumb{background-color:rgba(52,58,64,.75)}.form-floating{position:relative}.form-floating>.form-control,.form-floating>.form-control-plaintext,.form-floating>.form-select{height:calc(3.5rem + calc(1px * 2));min-height:calc(3.5rem + calc(1px * 2));line-height:1.25}.form-floating>label{position:absolute;top:0;left:0;z-index:2;height:100%;padding:1rem .75rem;overflow:hidden;text-align:start;text-overflow:ellipsis;white-space:nowrap;pointer-events:none;border:1px solid rgba(0,0,0,0);transform-origin:0 0;transition:opacity .1s ease-in-out,transform .1s ease-in-out}@media(prefers-reduced-motion: reduce){.form-floating>label{transition:none}}.form-floating>.form-control,.form-floating>.form-control-plaintext{padding:1rem .75rem}.form-floating>.form-control::placeholder,.form-floating>.form-control-plaintext::placeholder{color:rgba(0,0,0,0)}.form-floating>.form-control:focus,.form-floating>.form-control:not(:placeholder-shown),.form-floating>.form-control-plaintext:focus,.form-floating>.form-control-plaintext:not(:placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:-webkit-autofill,.form-floating>.form-control-plaintext:-webkit-autofill{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-select{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:focus~label,.form-floating>.form-control:not(:placeholder-shown)~label,.form-floating>.form-control-plaintext~label,.form-floating>.form-select~label{color:rgba(var(--bs-body-color-rgb), 0.65);transform:scale(0.85) translateY(-0.5rem) translateX(0.15rem)}.form-floating>.form-control:focus~label::after,.form-floating>.form-control:not(:placeholder-shown)~label::after,.form-floating>.form-control-plaintext~label::after,.form-floating>.form-select~label::after{position:absolute;inset:1rem .375rem;z-index:-1;height:1.5em;content:"";background-color:#fff}.form-floating>.form-control:-webkit-autofill~label{color:rgba(var(--bs-body-color-rgb), 0.65);transform:scale(0.85) translateY(-0.5rem) translateX(0.15rem)}.form-floating>.form-control-plaintext~label{border-width:1px 0}.form-floating>:disabled~label,.form-floating>.form-control:disabled~label{color:#6c757d}.form-floating>:disabled~label::after,.form-floating>.form-control:disabled~label::after{background-color:#e9ecef}.input-group{position:relative;display:flex;display:-webkit-flex;flex-wrap:wrap;-webkit-flex-wrap:wrap;align-items:stretch;-webkit-align-items:stretch;width:100%}.input-group>.form-control,.input-group>.form-select,.input-group>.form-floating{position:relative;flex:1 1 auto;-webkit-flex:1 1 auto;width:1%;min-width:0}.input-group>.form-control:focus,.input-group>.form-select:focus,.input-group>.form-floating:focus-within{z-index:5}.input-group .btn{position:relative;z-index:2}.input-group .btn:focus{z-index:5}.input-group-text{display:flex;display:-webkit-flex;align-items:center;-webkit-align-items:center;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#343a40;text-align:center;white-space:nowrap;background-color:#f8f9fa;border:1px solid #dee2e6}.input-group-lg>.form-control,.input-group-lg>.form-select,.input-group-lg>.input-group-text,.input-group-lg>.btn{padding:.5rem 1rem;font-size:1.25rem}.input-group-sm>.form-control,.input-group-sm>.form-select,.input-group-sm>.input-group-text,.input-group-sm>.btn{padding:.25rem .5rem;font-size:0.875rem}.input-group-lg>.form-select,.input-group-sm>.form-select{padding-right:3rem}.input-group>:not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback){margin-left:calc(1px*-1)}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:0.875em;color:#3fb618}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:0.875rem;color:#fff;background-color:#3fb618}.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip,.is-valid~.valid-feedback,.is-valid~.valid-tooltip{display:block}.was-validated .form-control:valid,.form-control.is-valid{border-color:#3fb618;padding-right:calc(1.5em + 0.75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%233fb618' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(0.375em + 0.1875rem) center;background-size:calc(0.75em + 0.375rem) calc(0.75em + 0.375rem)}.was-validated .form-control:valid:focus,.form-control.is-valid:focus{border-color:#3fb618;box-shadow:0 0 0 .25rem rgba(63,182,24,.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + 0.75rem);background-position:top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem)}.was-validated .form-select:valid,.form-select.is-valid{border-color:#3fb618}.was-validated .form-select:valid:not([multiple]):not([size]),.was-validated .form-select:valid:not([multiple])[size="1"],.form-select.is-valid:not([multiple]):not([size]),.form-select.is-valid:not([multiple])[size="1"]{--bs-form-select-bg-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%233fb618' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");padding-right:4.125rem;background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(0.75em + 0.375rem) calc(0.75em + 0.375rem)}.was-validated .form-select:valid:focus,.form-select.is-valid:focus{border-color:#3fb618;box-shadow:0 0 0 .25rem rgba(63,182,24,.25)}.was-validated .form-control-color:valid,.form-control-color.is-valid{width:calc(3rem + calc(1.5em + 0.75rem))}.was-validated .form-check-input:valid,.form-check-input.is-valid{border-color:#3fb618}.was-validated .form-check-input:valid:checked,.form-check-input.is-valid:checked{background-color:#3fb618}.was-validated .form-check-input:valid:focus,.form-check-input.is-valid:focus{box-shadow:0 0 0 .25rem rgba(63,182,24,.25)}.was-validated .form-check-input:valid~.form-check-label,.form-check-input.is-valid~.form-check-label{color:#3fb618}.form-check-inline .form-check-input~.valid-feedback{margin-left:.5em}.was-validated .input-group>.form-control:not(:focus):valid,.input-group>.form-control:not(:focus).is-valid,.was-validated .input-group>.form-select:not(:focus):valid,.input-group>.form-select:not(:focus).is-valid,.was-validated .input-group>.form-floating:not(:focus-within):valid,.input-group>.form-floating:not(:focus-within).is-valid{z-index:3}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:0.875em;color:#ff0039}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:0.875rem;color:#fff;background-color:#ff0039}.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip,.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip{display:block}.was-validated .form-control:invalid,.form-control.is-invalid{border-color:#ff0039;padding-right:calc(1.5em + 0.75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23ff0039'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23ff0039' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(0.375em + 0.1875rem) center;background-size:calc(0.75em + 0.375rem) calc(0.75em + 0.375rem)}.was-validated .form-control:invalid:focus,.form-control.is-invalid:focus{border-color:#ff0039;box-shadow:0 0 0 .25rem rgba(255,0,57,.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + 0.75rem);background-position:top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem)}.was-validated .form-select:invalid,.form-select.is-invalid{border-color:#ff0039}.was-validated .form-select:invalid:not([multiple]):not([size]),.was-validated .form-select:invalid:not([multiple])[size="1"],.form-select.is-invalid:not([multiple]):not([size]),.form-select.is-invalid:not([multiple])[size="1"]{--bs-form-select-bg-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23ff0039'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23ff0039' stroke='none'/%3e%3c/svg%3e");padding-right:4.125rem;background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(0.75em + 0.375rem) calc(0.75em + 0.375rem)}.was-validated .form-select:invalid:focus,.form-select.is-invalid:focus{border-color:#ff0039;box-shadow:0 0 0 .25rem rgba(255,0,57,.25)}.was-validated .form-control-color:invalid,.form-control-color.is-invalid{width:calc(3rem + calc(1.5em + 0.75rem))}.was-validated .form-check-input:invalid,.form-check-input.is-invalid{border-color:#ff0039}.was-validated .form-check-input:invalid:checked,.form-check-input.is-invalid:checked{background-color:#ff0039}.was-validated .form-check-input:invalid:focus,.form-check-input.is-invalid:focus{box-shadow:0 0 0 .25rem rgba(255,0,57,.25)}.was-validated .form-check-input:invalid~.form-check-label,.form-check-input.is-invalid~.form-check-label{color:#ff0039}.form-check-inline .form-check-input~.invalid-feedback{margin-left:.5em}.was-validated .input-group>.form-control:not(:focus):invalid,.input-group>.form-control:not(:focus).is-invalid,.was-validated .input-group>.form-select:not(:focus):invalid,.input-group>.form-select:not(:focus).is-invalid,.was-validated .input-group>.form-floating:not(:focus-within):invalid,.input-group>.form-floating:not(:focus-within).is-invalid{z-index:4}.btn{--bs-btn-padding-x: 0.75rem;--bs-btn-padding-y: 0.375rem;--bs-btn-font-family: ;--bs-btn-font-size:1rem;--bs-btn-font-weight: 400;--bs-btn-line-height: 1.5;--bs-btn-color: #343a40;--bs-btn-bg: transparent;--bs-btn-border-width: 1px;--bs-btn-border-color: transparent;--bs-btn-border-radius: 0.25rem;--bs-btn-hover-border-color: transparent;--bs-btn-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 1px rgba(0, 0, 0, 0.075);--bs-btn-disabled-opacity: 0.65;--bs-btn-focus-box-shadow: 0 0 0 0.25rem rgba(var(--bs-btn-focus-shadow-rgb), .5);display:inline-block;padding:var(--bs-btn-padding-y) var(--bs-btn-padding-x);font-family:var(--bs-btn-font-family);font-size:var(--bs-btn-font-size);font-weight:var(--bs-btn-font-weight);line-height:var(--bs-btn-line-height);color:var(--bs-btn-color);text-align:center;text-decoration:none;-webkit-text-decoration:none;-moz-text-decoration:none;-ms-text-decoration:none;-o-text-decoration:none;vertical-align:middle;cursor:pointer;user-select:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;-o-user-select:none;border:var(--bs-btn-border-width) solid var(--bs-btn-border-color);background-color:var(--bs-btn-bg);transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media(prefers-reduced-motion: reduce){.btn{transition:none}}.btn:hover{color:var(--bs-btn-hover-color);background-color:var(--bs-btn-hover-bg);border-color:var(--bs-btn-hover-border-color)}.btn-check+.btn:hover{color:var(--bs-btn-color);background-color:var(--bs-btn-bg);border-color:var(--bs-btn-border-color)}.btn:focus-visible{color:var(--bs-btn-hover-color);background-color:var(--bs-btn-hover-bg);border-color:var(--bs-btn-hover-border-color);outline:0;box-shadow:var(--bs-btn-focus-box-shadow)}.btn-check:focus-visible+.btn{border-color:var(--bs-btn-hover-border-color);outline:0;box-shadow:var(--bs-btn-focus-box-shadow)}.btn-check:checked+.btn,:not(.btn-check)+.btn:active,.btn:first-child:active,.btn.active,.btn.show{color:var(--bs-btn-active-color);background-color:var(--bs-btn-active-bg);border-color:var(--bs-btn-active-border-color)}.btn-check:checked+.btn:focus-visible,:not(.btn-check)+.btn:active:focus-visible,.btn:first-child:active:focus-visible,.btn.active:focus-visible,.btn.show:focus-visible{box-shadow:var(--bs-btn-focus-box-shadow)}.btn:disabled,.btn.disabled,fieldset:disabled .btn{color:var(--bs-btn-disabled-color);pointer-events:none;background-color:var(--bs-btn-disabled-bg);border-color:var(--bs-btn-disabled-border-color);opacity:var(--bs-btn-disabled-opacity)}.btn-default{--bs-btn-color: #fff;--bs-btn-bg: #343a40;--bs-btn-border-color: #343a40;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: #2c3136;--bs-btn-hover-border-color: #2a2e33;--bs-btn-focus-shadow-rgb: 82, 88, 93;--bs-btn-active-color: #fff;--bs-btn-active-bg: #2a2e33;--bs-btn-active-border-color: #272c30;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #fff;--bs-btn-disabled-bg: #343a40;--bs-btn-disabled-border-color: #343a40}.btn-primary{--bs-btn-color: #fff;--bs-btn-bg: #2780e3;--bs-btn-border-color: #2780e3;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: #216dc1;--bs-btn-hover-border-color: #1f66b6;--bs-btn-focus-shadow-rgb: 71, 147, 231;--bs-btn-active-color: #fff;--bs-btn-active-bg: #1f66b6;--bs-btn-active-border-color: #1d60aa;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #fff;--bs-btn-disabled-bg: #2780e3;--bs-btn-disabled-border-color: #2780e3}.btn-secondary{--bs-btn-color: #fff;--bs-btn-bg: #343a40;--bs-btn-border-color: #343a40;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: #2c3136;--bs-btn-hover-border-color: #2a2e33;--bs-btn-focus-shadow-rgb: 82, 88, 93;--bs-btn-active-color: #fff;--bs-btn-active-bg: #2a2e33;--bs-btn-active-border-color: #272c30;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #fff;--bs-btn-disabled-bg: #343a40;--bs-btn-disabled-border-color: #343a40}.btn-success{--bs-btn-color: #fff;--bs-btn-bg: #3fb618;--bs-btn-border-color: #3fb618;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: #369b14;--bs-btn-hover-border-color: #329213;--bs-btn-focus-shadow-rgb: 92, 193, 59;--bs-btn-active-color: #fff;--bs-btn-active-bg: #329213;--bs-btn-active-border-color: #2f8912;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #fff;--bs-btn-disabled-bg: #3fb618;--bs-btn-disabled-border-color: #3fb618}.btn-info{--bs-btn-color: #fff;--bs-btn-bg: #9954bb;--bs-btn-border-color: #9954bb;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: #82479f;--bs-btn-hover-border-color: #7a4396;--bs-btn-focus-shadow-rgb: 168, 110, 197;--bs-btn-active-color: #fff;--bs-btn-active-bg: #7a4396;--bs-btn-active-border-color: #733f8c;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #fff;--bs-btn-disabled-bg: #9954bb;--bs-btn-disabled-border-color: #9954bb}.btn-warning{--bs-btn-color: #fff;--bs-btn-bg: #ff7518;--bs-btn-border-color: #ff7518;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: #d96314;--bs-btn-hover-border-color: #cc5e13;--bs-btn-focus-shadow-rgb: 255, 138, 59;--bs-btn-active-color: #fff;--bs-btn-active-bg: #cc5e13;--bs-btn-active-border-color: #bf5812;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #fff;--bs-btn-disabled-bg: #ff7518;--bs-btn-disabled-border-color: #ff7518}.btn-danger{--bs-btn-color: #fff;--bs-btn-bg: #ff0039;--bs-btn-border-color: #ff0039;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: #d90030;--bs-btn-hover-border-color: #cc002e;--bs-btn-focus-shadow-rgb: 255, 38, 87;--bs-btn-active-color: #fff;--bs-btn-active-bg: #cc002e;--bs-btn-active-border-color: #bf002b;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #fff;--bs-btn-disabled-bg: #ff0039;--bs-btn-disabled-border-color: #ff0039}.btn-light{--bs-btn-color: #000;--bs-btn-bg: #f8f9fa;--bs-btn-border-color: #f8f9fa;--bs-btn-hover-color: #000;--bs-btn-hover-bg: #d3d4d5;--bs-btn-hover-border-color: #c6c7c8;--bs-btn-focus-shadow-rgb: 211, 212, 213;--bs-btn-active-color: #000;--bs-btn-active-bg: #c6c7c8;--bs-btn-active-border-color: #babbbc;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #000;--bs-btn-disabled-bg: #f8f9fa;--bs-btn-disabled-border-color: #f8f9fa}.btn-dark{--bs-btn-color: #fff;--bs-btn-bg: #343a40;--bs-btn-border-color: #343a40;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: #52585d;--bs-btn-hover-border-color: #484e53;--bs-btn-focus-shadow-rgb: 82, 88, 93;--bs-btn-active-color: #fff;--bs-btn-active-bg: #5d6166;--bs-btn-active-border-color: #484e53;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #fff;--bs-btn-disabled-bg: #343a40;--bs-btn-disabled-border-color: #343a40}.btn-outline-default{--bs-btn-color: #343a40;--bs-btn-border-color: #343a40;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: #343a40;--bs-btn-hover-border-color: #343a40;--bs-btn-focus-shadow-rgb: 52, 58, 64;--bs-btn-active-color: #fff;--bs-btn-active-bg: #343a40;--bs-btn-active-border-color: #343a40;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #343a40;--bs-btn-disabled-bg: transparent;--bs-btn-disabled-border-color: #343a40;--bs-btn-bg: transparent;--bs-gradient: none}.btn-outline-primary{--bs-btn-color: #2780e3;--bs-btn-border-color: #2780e3;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: #2780e3;--bs-btn-hover-border-color: #2780e3;--bs-btn-focus-shadow-rgb: 39, 128, 227;--bs-btn-active-color: #fff;--bs-btn-active-bg: #2780e3;--bs-btn-active-border-color: #2780e3;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #2780e3;--bs-btn-disabled-bg: transparent;--bs-btn-disabled-border-color: #2780e3;--bs-btn-bg: transparent;--bs-gradient: none}.btn-outline-secondary{--bs-btn-color: #343a40;--bs-btn-border-color: #343a40;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: #343a40;--bs-btn-hover-border-color: #343a40;--bs-btn-focus-shadow-rgb: 52, 58, 64;--bs-btn-active-color: #fff;--bs-btn-active-bg: #343a40;--bs-btn-active-border-color: #343a40;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #343a40;--bs-btn-disabled-bg: transparent;--bs-btn-disabled-border-color: #343a40;--bs-btn-bg: transparent;--bs-gradient: none}.btn-outline-success{--bs-btn-color: #3fb618;--bs-btn-border-color: #3fb618;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: #3fb618;--bs-btn-hover-border-color: #3fb618;--bs-btn-focus-shadow-rgb: 63, 182, 24;--bs-btn-active-color: #fff;--bs-btn-active-bg: #3fb618;--bs-btn-active-border-color: #3fb618;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #3fb618;--bs-btn-disabled-bg: transparent;--bs-btn-disabled-border-color: #3fb618;--bs-btn-bg: transparent;--bs-gradient: none}.btn-outline-info{--bs-btn-color: #9954bb;--bs-btn-border-color: #9954bb;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: #9954bb;--bs-btn-hover-border-color: #9954bb;--bs-btn-focus-shadow-rgb: 153, 84, 187;--bs-btn-active-color: #fff;--bs-btn-active-bg: #9954bb;--bs-btn-active-border-color: #9954bb;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #9954bb;--bs-btn-disabled-bg: transparent;--bs-btn-disabled-border-color: #9954bb;--bs-btn-bg: transparent;--bs-gradient: none}.btn-outline-warning{--bs-btn-color: #ff7518;--bs-btn-border-color: #ff7518;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: #ff7518;--bs-btn-hover-border-color: #ff7518;--bs-btn-focus-shadow-rgb: 255, 117, 24;--bs-btn-active-color: #fff;--bs-btn-active-bg: #ff7518;--bs-btn-active-border-color: #ff7518;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #ff7518;--bs-btn-disabled-bg: transparent;--bs-btn-disabled-border-color: #ff7518;--bs-btn-bg: transparent;--bs-gradient: none}.btn-outline-danger{--bs-btn-color: #ff0039;--bs-btn-border-color: #ff0039;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: #ff0039;--bs-btn-hover-border-color: #ff0039;--bs-btn-focus-shadow-rgb: 255, 0, 57;--bs-btn-active-color: #fff;--bs-btn-active-bg: #ff0039;--bs-btn-active-border-color: #ff0039;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #ff0039;--bs-btn-disabled-bg: transparent;--bs-btn-disabled-border-color: #ff0039;--bs-btn-bg: transparent;--bs-gradient: none}.btn-outline-light{--bs-btn-color: #f8f9fa;--bs-btn-border-color: #f8f9fa;--bs-btn-hover-color: #000;--bs-btn-hover-bg: #f8f9fa;--bs-btn-hover-border-color: #f8f9fa;--bs-btn-focus-shadow-rgb: 248, 249, 250;--bs-btn-active-color: #000;--bs-btn-active-bg: #f8f9fa;--bs-btn-active-border-color: #f8f9fa;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #f8f9fa;--bs-btn-disabled-bg: transparent;--bs-btn-disabled-border-color: #f8f9fa;--bs-btn-bg: transparent;--bs-gradient: none}.btn-outline-dark{--bs-btn-color: #343a40;--bs-btn-border-color: #343a40;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: #343a40;--bs-btn-hover-border-color: #343a40;--bs-btn-focus-shadow-rgb: 52, 58, 64;--bs-btn-active-color: #fff;--bs-btn-active-bg: #343a40;--bs-btn-active-border-color: #343a40;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #343a40;--bs-btn-disabled-bg: transparent;--bs-btn-disabled-border-color: #343a40;--bs-btn-bg: transparent;--bs-gradient: none}.btn-link{--bs-btn-font-weight: 400;--bs-btn-color: #2761e3;--bs-btn-bg: transparent;--bs-btn-border-color: transparent;--bs-btn-hover-color: #1f4eb6;--bs-btn-hover-border-color: transparent;--bs-btn-active-color: #1f4eb6;--bs-btn-active-border-color: transparent;--bs-btn-disabled-color: #6c757d;--bs-btn-disabled-border-color: transparent;--bs-btn-box-shadow: 0 0 0 #000;--bs-btn-focus-shadow-rgb: 71, 121, 231;text-decoration:underline;-webkit-text-decoration:underline;-moz-text-decoration:underline;-ms-text-decoration:underline;-o-text-decoration:underline}.btn-link:focus-visible{color:var(--bs-btn-color)}.btn-link:hover{color:var(--bs-btn-hover-color)}.btn-lg,.btn-group-lg>.btn{--bs-btn-padding-y: 0.5rem;--bs-btn-padding-x: 1rem;--bs-btn-font-size:1.25rem;--bs-btn-border-radius: 0.5rem}.btn-sm,.btn-group-sm>.btn{--bs-btn-padding-y: 0.25rem;--bs-btn-padding-x: 0.5rem;--bs-btn-font-size:0.875rem;--bs-btn-border-radius: 0.2em}.fade{transition:opacity .15s linear}@media(prefers-reduced-motion: reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{height:0;overflow:hidden;transition:height .2s ease}@media(prefers-reduced-motion: reduce){.collapsing{transition:none}}.collapsing.collapse-horizontal{width:0;height:auto;transition:width .35s ease}@media(prefers-reduced-motion: reduce){.collapsing.collapse-horizontal{transition:none}}.dropup,.dropend,.dropdown,.dropstart,.dropup-center,.dropdown-center{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid rgba(0,0,0,0);border-bottom:0;border-left:.3em solid rgba(0,0,0,0)}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{--bs-dropdown-zindex: 1000;--bs-dropdown-min-width: 10rem;--bs-dropdown-padding-x: 0;--bs-dropdown-padding-y: 0.5rem;--bs-dropdown-spacer: 0.125rem;--bs-dropdown-font-size:1rem;--bs-dropdown-color: #343a40;--bs-dropdown-bg: #fff;--bs-dropdown-border-color: rgba(0, 0, 0, 0.175);--bs-dropdown-border-radius: 0.25rem;--bs-dropdown-border-width: 1px;--bs-dropdown-inner-border-radius: calc(0.25rem - 1px);--bs-dropdown-divider-bg: rgba(0, 0, 0, 0.175);--bs-dropdown-divider-margin-y: 0.5rem;--bs-dropdown-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);--bs-dropdown-link-color: #343a40;--bs-dropdown-link-hover-color: #343a40;--bs-dropdown-link-hover-bg: #f8f9fa;--bs-dropdown-link-active-color: #fff;--bs-dropdown-link-active-bg: #2780e3;--bs-dropdown-link-disabled-color: rgba(52, 58, 64, 0.5);--bs-dropdown-item-padding-x: 1rem;--bs-dropdown-item-padding-y: 0.25rem;--bs-dropdown-header-color: #6c757d;--bs-dropdown-header-padding-x: 1rem;--bs-dropdown-header-padding-y: 0.5rem;position:absolute;z-index:var(--bs-dropdown-zindex);display:none;min-width:var(--bs-dropdown-min-width);padding:var(--bs-dropdown-padding-y) var(--bs-dropdown-padding-x);margin:0;font-size:var(--bs-dropdown-font-size);color:var(--bs-dropdown-color);text-align:left;list-style:none;background-color:var(--bs-dropdown-bg);background-clip:padding-box;border:var(--bs-dropdown-border-width) solid var(--bs-dropdown-border-color)}.dropdown-menu[data-bs-popper]{top:100%;left:0;margin-top:var(--bs-dropdown-spacer)}.dropdown-menu-start{--bs-position: start}.dropdown-menu-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-end{--bs-position: end}.dropdown-menu-end[data-bs-popper]{right:0;left:auto}@media(min-width: 576px){.dropdown-menu-sm-start{--bs-position: start}.dropdown-menu-sm-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-sm-end{--bs-position: end}.dropdown-menu-sm-end[data-bs-popper]{right:0;left:auto}}@media(min-width: 768px){.dropdown-menu-md-start{--bs-position: start}.dropdown-menu-md-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-md-end{--bs-position: end}.dropdown-menu-md-end[data-bs-popper]{right:0;left:auto}}@media(min-width: 992px){.dropdown-menu-lg-start{--bs-position: start}.dropdown-menu-lg-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-lg-end{--bs-position: end}.dropdown-menu-lg-end[data-bs-popper]{right:0;left:auto}}@media(min-width: 1200px){.dropdown-menu-xl-start{--bs-position: start}.dropdown-menu-xl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xl-end{--bs-position: end}.dropdown-menu-xl-end[data-bs-popper]{right:0;left:auto}}@media(min-width: 1400px){.dropdown-menu-xxl-start{--bs-position: start}.dropdown-menu-xxl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xxl-end{--bs-position: end}.dropdown-menu-xxl-end[data-bs-popper]{right:0;left:auto}}.dropup .dropdown-menu[data-bs-popper]{top:auto;bottom:100%;margin-top:0;margin-bottom:var(--bs-dropdown-spacer)}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid rgba(0,0,0,0);border-bottom:.3em solid;border-left:.3em solid rgba(0,0,0,0)}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-menu[data-bs-popper]{top:0;right:auto;left:100%;margin-top:0;margin-left:var(--bs-dropdown-spacer)}.dropend .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid rgba(0,0,0,0);border-right:0;border-bottom:.3em solid rgba(0,0,0,0);border-left:.3em solid}.dropend .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-toggle::after{vertical-align:0}.dropstart .dropdown-menu[data-bs-popper]{top:0;right:100%;left:auto;margin-top:0;margin-right:var(--bs-dropdown-spacer)}.dropstart .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropstart .dropdown-toggle::after{display:none}.dropstart .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid rgba(0,0,0,0);border-right:.3em solid;border-bottom:.3em solid rgba(0,0,0,0)}.dropstart .dropdown-toggle:empty::after{margin-left:0}.dropstart .dropdown-toggle::before{vertical-align:0}.dropdown-divider{height:0;margin:var(--bs-dropdown-divider-margin-y) 0;overflow:hidden;border-top:1px solid var(--bs-dropdown-divider-bg);opacity:1}.dropdown-item{display:block;width:100%;padding:var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x);clear:both;font-weight:400;color:var(--bs-dropdown-link-color);text-align:inherit;text-decoration:none;-webkit-text-decoration:none;-moz-text-decoration:none;-ms-text-decoration:none;-o-text-decoration:none;white-space:nowrap;background-color:rgba(0,0,0,0);border:0}.dropdown-item:hover,.dropdown-item:focus{color:var(--bs-dropdown-link-hover-color);background-color:var(--bs-dropdown-link-hover-bg)}.dropdown-item.active,.dropdown-item:active{color:var(--bs-dropdown-link-active-color);text-decoration:none;background-color:var(--bs-dropdown-link-active-bg)}.dropdown-item.disabled,.dropdown-item:disabled{color:var(--bs-dropdown-link-disabled-color);pointer-events:none;background-color:rgba(0,0,0,0)}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:var(--bs-dropdown-header-padding-y) var(--bs-dropdown-header-padding-x);margin-bottom:0;font-size:0.875rem;color:var(--bs-dropdown-header-color);white-space:nowrap}.dropdown-item-text{display:block;padding:var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x);color:var(--bs-dropdown-link-color)}.dropdown-menu-dark{--bs-dropdown-color: #dee2e6;--bs-dropdown-bg: #343a40;--bs-dropdown-border-color: rgba(0, 0, 0, 0.175);--bs-dropdown-box-shadow: ;--bs-dropdown-link-color: #dee2e6;--bs-dropdown-link-hover-color: #fff;--bs-dropdown-divider-bg: rgba(0, 0, 0, 0.175);--bs-dropdown-link-hover-bg: rgba(255, 255, 255, 0.15);--bs-dropdown-link-active-color: #fff;--bs-dropdown-link-active-bg: #2780e3;--bs-dropdown-link-disabled-color: #adb5bd;--bs-dropdown-header-color: #adb5bd}.btn-group,.btn-group-vertical{position:relative;display:inline-flex;vertical-align:middle}.btn-group>.btn,.btn-group-vertical>.btn{position:relative;flex:1 1 auto;-webkit-flex:1 1 auto}.btn-group>.btn-check:checked+.btn,.btn-group>.btn-check:focus+.btn,.btn-group>.btn:hover,.btn-group>.btn:focus,.btn-group>.btn:active,.btn-group>.btn.active,.btn-group-vertical>.btn-check:checked+.btn,.btn-group-vertical>.btn-check:focus+.btn,.btn-group-vertical>.btn:hover,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn.active{z-index:1}.btn-toolbar{display:flex;display:-webkit-flex;flex-wrap:wrap;-webkit-flex-wrap:wrap;justify-content:flex-start;-webkit-justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>:not(.btn-check:first-child)+.btn,.btn-group>.btn-group:not(:first-child){margin-left:calc(1px*-1)}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after,.dropend .dropdown-toggle-split::after{margin-left:0}.dropstart .dropdown-toggle-split::before{margin-right:0}.btn-sm+.dropdown-toggle-split,.btn-group-sm>.btn+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-lg+.dropdown-toggle-split,.btn-group-lg>.btn+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{flex-direction:column;-webkit-flex-direction:column;align-items:flex-start;-webkit-align-items:flex-start;justify-content:center;-webkit-justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn:not(:first-child),.btn-group-vertical>.btn-group:not(:first-child){margin-top:calc(1px*-1)}.nav{--bs-nav-link-padding-x: 1rem;--bs-nav-link-padding-y: 0.5rem;--bs-nav-link-font-weight: ;--bs-nav-link-color: #2761e3;--bs-nav-link-hover-color: #1f4eb6;--bs-nav-link-disabled-color: rgba(52, 58, 64, 0.75);display:flex;display:-webkit-flex;flex-wrap:wrap;-webkit-flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:var(--bs-nav-link-padding-y) var(--bs-nav-link-padding-x);font-size:var(--bs-nav-link-font-size);font-weight:var(--bs-nav-link-font-weight);color:var(--bs-nav-link-color);text-decoration:none;-webkit-text-decoration:none;-moz-text-decoration:none;-ms-text-decoration:none;-o-text-decoration:none;background:none;border:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out}@media(prefers-reduced-motion: reduce){.nav-link{transition:none}}.nav-link:hover,.nav-link:focus{color:var(--bs-nav-link-hover-color)}.nav-link:focus-visible{outline:0;box-shadow:0 0 0 .25rem rgba(39,128,227,.25)}.nav-link.disabled,.nav-link:disabled{color:var(--bs-nav-link-disabled-color);pointer-events:none;cursor:default}.nav-tabs{--bs-nav-tabs-border-width: 1px;--bs-nav-tabs-border-color: #dee2e6;--bs-nav-tabs-border-radius: 0.25rem;--bs-nav-tabs-link-hover-border-color: #e9ecef #e9ecef #dee2e6;--bs-nav-tabs-link-active-color: #000;--bs-nav-tabs-link-active-bg: #fff;--bs-nav-tabs-link-active-border-color: #dee2e6 #dee2e6 #fff;border-bottom:var(--bs-nav-tabs-border-width) solid var(--bs-nav-tabs-border-color)}.nav-tabs .nav-link{margin-bottom:calc(-1*var(--bs-nav-tabs-border-width));border:var(--bs-nav-tabs-border-width) solid rgba(0,0,0,0)}.nav-tabs .nav-link:hover,.nav-tabs .nav-link:focus{isolation:isolate;border-color:var(--bs-nav-tabs-link-hover-border-color)}.nav-tabs .nav-link.active,.nav-tabs .nav-item.show .nav-link{color:var(--bs-nav-tabs-link-active-color);background-color:var(--bs-nav-tabs-link-active-bg);border-color:var(--bs-nav-tabs-link-active-border-color)}.nav-tabs .dropdown-menu{margin-top:calc(-1*var(--bs-nav-tabs-border-width))}.nav-pills{--bs-nav-pills-border-radius: 0.25rem;--bs-nav-pills-link-active-color: #fff;--bs-nav-pills-link-active-bg: #2780e3}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:var(--bs-nav-pills-link-active-color);background-color:var(--bs-nav-pills-link-active-bg)}.nav-underline{--bs-nav-underline-gap: 1rem;--bs-nav-underline-border-width: 0.125rem;--bs-nav-underline-link-active-color: #000;gap:var(--bs-nav-underline-gap)}.nav-underline .nav-link{padding-right:0;padding-left:0;border-bottom:var(--bs-nav-underline-border-width) solid rgba(0,0,0,0)}.nav-underline .nav-link:hover,.nav-underline .nav-link:focus{border-bottom-color:currentcolor}.nav-underline .nav-link.active,.nav-underline .show>.nav-link{font-weight:700;color:var(--bs-nav-underline-link-active-color);border-bottom-color:currentcolor}.nav-fill>.nav-link,.nav-fill .nav-item{flex:1 1 auto;-webkit-flex:1 1 auto;text-align:center}.nav-justified>.nav-link,.nav-justified .nav-item{flex-basis:0;-webkit-flex-basis:0;flex-grow:1;-webkit-flex-grow:1;text-align:center}.nav-fill .nav-item .nav-link,.nav-justified .nav-item .nav-link{width:100%}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{--bs-navbar-padding-x: 0;--bs-navbar-padding-y: 0.5rem;--bs-navbar-color: #fdfeff;--bs-navbar-hover-color: rgba(253, 253, 255, 0.8);--bs-navbar-disabled-color: rgba(253, 254, 255, 0.75);--bs-navbar-active-color: #fdfdff;--bs-navbar-brand-padding-y: 0.3125rem;--bs-navbar-brand-margin-end: 1rem;--bs-navbar-brand-font-size: 1.25rem;--bs-navbar-brand-color: #fdfeff;--bs-navbar-brand-hover-color: #fdfdff;--bs-navbar-nav-link-padding-x: 0.5rem;--bs-navbar-toggler-padding-y: 0.25;--bs-navbar-toggler-padding-x: 0;--bs-navbar-toggler-font-size: 1.25rem;--bs-navbar-toggler-icon-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='%23fdfeff' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");--bs-navbar-toggler-border-color: rgba(253, 254, 255, 0);--bs-navbar-toggler-border-radius: 0.25rem;--bs-navbar-toggler-focus-width: 0.25rem;--bs-navbar-toggler-transition: box-shadow 0.15s ease-in-out;position:relative;display:flex;display:-webkit-flex;flex-wrap:wrap;-webkit-flex-wrap:wrap;align-items:center;-webkit-align-items:center;justify-content:space-between;-webkit-justify-content:space-between;padding:var(--bs-navbar-padding-y) var(--bs-navbar-padding-x)}.navbar>.container,.navbar>.container-fluid,.navbar>.container-sm,.navbar>.container-md,.navbar>.container-lg,.navbar>.container-xl,.navbar>.container-xxl{display:flex;display:-webkit-flex;flex-wrap:inherit;-webkit-flex-wrap:inherit;align-items:center;-webkit-align-items:center;justify-content:space-between;-webkit-justify-content:space-between}.navbar-brand{padding-top:var(--bs-navbar-brand-padding-y);padding-bottom:var(--bs-navbar-brand-padding-y);margin-right:var(--bs-navbar-brand-margin-end);font-size:var(--bs-navbar-brand-font-size);color:var(--bs-navbar-brand-color);text-decoration:none;-webkit-text-decoration:none;-moz-text-decoration:none;-ms-text-decoration:none;-o-text-decoration:none;white-space:nowrap}.navbar-brand:hover,.navbar-brand:focus{color:var(--bs-navbar-brand-hover-color)}.navbar-nav{--bs-nav-link-padding-x: 0;--bs-nav-link-padding-y: 0.5rem;--bs-nav-link-font-weight: ;--bs-nav-link-color: var(--bs-navbar-color);--bs-nav-link-hover-color: var(--bs-navbar-hover-color);--bs-nav-link-disabled-color: var(--bs-navbar-disabled-color);display:flex;display:-webkit-flex;flex-direction:column;-webkit-flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link.active,.navbar-nav .nav-link.show{color:var(--bs-navbar-active-color)}.navbar-nav .dropdown-menu{position:static}.navbar-text{padding-top:.5rem;padding-bottom:.5rem;color:var(--bs-navbar-color)}.navbar-text a,.navbar-text a:hover,.navbar-text a:focus{color:var(--bs-navbar-active-color)}.navbar-collapse{flex-basis:100%;-webkit-flex-basis:100%;flex-grow:1;-webkit-flex-grow:1;align-items:center;-webkit-align-items:center}.navbar-toggler{padding:var(--bs-navbar-toggler-padding-y) var(--bs-navbar-toggler-padding-x);font-size:var(--bs-navbar-toggler-font-size);line-height:1;color:var(--bs-navbar-color);background-color:rgba(0,0,0,0);border:var(--bs-border-width) solid var(--bs-navbar-toggler-border-color);transition:var(--bs-navbar-toggler-transition)}@media(prefers-reduced-motion: reduce){.navbar-toggler{transition:none}}.navbar-toggler:hover{text-decoration:none}.navbar-toggler:focus{text-decoration:none;outline:0;box-shadow:0 0 0 var(--bs-navbar-toggler-focus-width)}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;background-image:var(--bs-navbar-toggler-icon-bg);background-repeat:no-repeat;background-position:center;background-size:100%}.navbar-nav-scroll{max-height:var(--bs-scroll-height, 75vh);overflow-y:auto}@media(min-width: 576px){.navbar-expand-sm{flex-wrap:nowrap;-webkit-flex-wrap:nowrap;justify-content:flex-start;-webkit-justify-content:flex-start}.navbar-expand-sm .navbar-nav{flex-direction:row;-webkit-flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-sm .navbar-nav-scroll{overflow:visible}.navbar-expand-sm .navbar-collapse{display:flex !important;display:-webkit-flex !important;flex-basis:auto;-webkit-flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}.navbar-expand-sm .offcanvas{position:static;z-index:auto;flex-grow:1;-webkit-flex-grow:1;width:auto !important;height:auto !important;visibility:visible !important;background-color:rgba(0,0,0,0) !important;border:0 !important;transform:none !important;transition:none}.navbar-expand-sm .offcanvas .offcanvas-header{display:none}.navbar-expand-sm .offcanvas .offcanvas-body{display:flex;display:-webkit-flex;flex-grow:0;-webkit-flex-grow:0;padding:0;overflow-y:visible}}@media(min-width: 768px){.navbar-expand-md{flex-wrap:nowrap;-webkit-flex-wrap:nowrap;justify-content:flex-start;-webkit-justify-content:flex-start}.navbar-expand-md .navbar-nav{flex-direction:row;-webkit-flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-md .navbar-nav-scroll{overflow:visible}.navbar-expand-md .navbar-collapse{display:flex !important;display:-webkit-flex !important;flex-basis:auto;-webkit-flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}.navbar-expand-md .offcanvas{position:static;z-index:auto;flex-grow:1;-webkit-flex-grow:1;width:auto !important;height:auto !important;visibility:visible !important;background-color:rgba(0,0,0,0) !important;border:0 !important;transform:none !important;transition:none}.navbar-expand-md .offcanvas .offcanvas-header{display:none}.navbar-expand-md .offcanvas .offcanvas-body{display:flex;display:-webkit-flex;flex-grow:0;-webkit-flex-grow:0;padding:0;overflow-y:visible}}@media(min-width: 992px){.navbar-expand-lg{flex-wrap:nowrap;-webkit-flex-wrap:nowrap;justify-content:flex-start;-webkit-justify-content:flex-start}.navbar-expand-lg .navbar-nav{flex-direction:row;-webkit-flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-lg .navbar-nav-scroll{overflow:visible}.navbar-expand-lg .navbar-collapse{display:flex !important;display:-webkit-flex !important;flex-basis:auto;-webkit-flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}.navbar-expand-lg .offcanvas{position:static;z-index:auto;flex-grow:1;-webkit-flex-grow:1;width:auto !important;height:auto !important;visibility:visible !important;background-color:rgba(0,0,0,0) !important;border:0 !important;transform:none !important;transition:none}.navbar-expand-lg .offcanvas .offcanvas-header{display:none}.navbar-expand-lg .offcanvas .offcanvas-body{display:flex;display:-webkit-flex;flex-grow:0;-webkit-flex-grow:0;padding:0;overflow-y:visible}}@media(min-width: 1200px){.navbar-expand-xl{flex-wrap:nowrap;-webkit-flex-wrap:nowrap;justify-content:flex-start;-webkit-justify-content:flex-start}.navbar-expand-xl .navbar-nav{flex-direction:row;-webkit-flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-xl .navbar-nav-scroll{overflow:visible}.navbar-expand-xl .navbar-collapse{display:flex !important;display:-webkit-flex !important;flex-basis:auto;-webkit-flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}.navbar-expand-xl .offcanvas{position:static;z-index:auto;flex-grow:1;-webkit-flex-grow:1;width:auto !important;height:auto !important;visibility:visible !important;background-color:rgba(0,0,0,0) !important;border:0 !important;transform:none !important;transition:none}.navbar-expand-xl .offcanvas .offcanvas-header{display:none}.navbar-expand-xl .offcanvas .offcanvas-body{display:flex;display:-webkit-flex;flex-grow:0;-webkit-flex-grow:0;padding:0;overflow-y:visible}}@media(min-width: 1400px){.navbar-expand-xxl{flex-wrap:nowrap;-webkit-flex-wrap:nowrap;justify-content:flex-start;-webkit-justify-content:flex-start}.navbar-expand-xxl .navbar-nav{flex-direction:row;-webkit-flex-direction:row}.navbar-expand-xxl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xxl .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-xxl .navbar-nav-scroll{overflow:visible}.navbar-expand-xxl .navbar-collapse{display:flex !important;display:-webkit-flex !important;flex-basis:auto;-webkit-flex-basis:auto}.navbar-expand-xxl .navbar-toggler{display:none}.navbar-expand-xxl .offcanvas{position:static;z-index:auto;flex-grow:1;-webkit-flex-grow:1;width:auto !important;height:auto !important;visibility:visible !important;background-color:rgba(0,0,0,0) !important;border:0 !important;transform:none !important;transition:none}.navbar-expand-xxl .offcanvas .offcanvas-header{display:none}.navbar-expand-xxl .offcanvas .offcanvas-body{display:flex;display:-webkit-flex;flex-grow:0;-webkit-flex-grow:0;padding:0;overflow-y:visible}}.navbar-expand{flex-wrap:nowrap;-webkit-flex-wrap:nowrap;justify-content:flex-start;-webkit-justify-content:flex-start}.navbar-expand .navbar-nav{flex-direction:row;-webkit-flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand .navbar-nav-scroll{overflow:visible}.navbar-expand .navbar-collapse{display:flex !important;display:-webkit-flex !important;flex-basis:auto;-webkit-flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-expand .offcanvas{position:static;z-index:auto;flex-grow:1;-webkit-flex-grow:1;width:auto !important;height:auto !important;visibility:visible !important;background-color:rgba(0,0,0,0) !important;border:0 !important;transform:none !important;transition:none}.navbar-expand .offcanvas .offcanvas-header{display:none}.navbar-expand .offcanvas .offcanvas-body{display:flex;display:-webkit-flex;flex-grow:0;-webkit-flex-grow:0;padding:0;overflow-y:visible}.navbar-dark,.navbar[data-bs-theme=dark]{--bs-navbar-color: #fdfeff;--bs-navbar-hover-color: rgba(253, 253, 255, 0.8);--bs-navbar-disabled-color: rgba(253, 254, 255, 0.75);--bs-navbar-active-color: #fdfdff;--bs-navbar-brand-color: #fdfeff;--bs-navbar-brand-hover-color: #fdfdff;--bs-navbar-toggler-border-color: rgba(253, 254, 255, 0);--bs-navbar-toggler-icon-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='%23fdfeff' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}[data-bs-theme=dark] .navbar-toggler-icon{--bs-navbar-toggler-icon-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='%23fdfeff' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.card{--bs-card-spacer-y: 1rem;--bs-card-spacer-x: 1rem;--bs-card-title-spacer-y: 0.5rem;--bs-card-title-color: ;--bs-card-subtitle-color: ;--bs-card-border-width: 1px;--bs-card-border-color: rgba(0, 0, 0, 0.175);--bs-card-border-radius: 0.25rem;--bs-card-box-shadow: ;--bs-card-inner-border-radius: calc(0.25rem - 1px);--bs-card-cap-padding-y: 0.5rem;--bs-card-cap-padding-x: 1rem;--bs-card-cap-bg: rgba(52, 58, 64, 0.25);--bs-card-cap-color: ;--bs-card-height: ;--bs-card-color: ;--bs-card-bg: #fff;--bs-card-img-overlay-padding: 1rem;--bs-card-group-margin: 0.75rem;position:relative;display:flex;display:-webkit-flex;flex-direction:column;-webkit-flex-direction:column;min-width:0;height:var(--bs-card-height);color:var(--bs-body-color);word-wrap:break-word;background-color:var(--bs-card-bg);background-clip:border-box;border:var(--bs-card-border-width) solid var(--bs-card-border-color)}.card>hr{margin-right:0;margin-left:0}.card>.list-group{border-top:inherit;border-bottom:inherit}.card>.list-group:first-child{border-top-width:0}.card>.list-group:last-child{border-bottom-width:0}.card>.card-header+.list-group,.card>.list-group+.card-footer{border-top:0}.card-body{flex:1 1 auto;-webkit-flex:1 1 auto;padding:var(--bs-card-spacer-y) var(--bs-card-spacer-x);color:var(--bs-card-color)}.card-title{margin-bottom:var(--bs-card-title-spacer-y);color:var(--bs-card-title-color)}.card-subtitle{margin-top:calc(-0.5*var(--bs-card-title-spacer-y));margin-bottom:0;color:var(--bs-card-subtitle-color)}.card-text:last-child{margin-bottom:0}.card-link+.card-link{margin-left:var(--bs-card-spacer-x)}.card-header{padding:var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x);margin-bottom:0;color:var(--bs-card-cap-color);background-color:var(--bs-card-cap-bg);border-bottom:var(--bs-card-border-width) solid var(--bs-card-border-color)}.card-footer{padding:var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x);color:var(--bs-card-cap-color);background-color:var(--bs-card-cap-bg);border-top:var(--bs-card-border-width) solid var(--bs-card-border-color)}.card-header-tabs{margin-right:calc(-0.5*var(--bs-card-cap-padding-x));margin-bottom:calc(-1*var(--bs-card-cap-padding-y));margin-left:calc(-0.5*var(--bs-card-cap-padding-x));border-bottom:0}.card-header-tabs .nav-link.active{background-color:var(--bs-card-bg);border-bottom-color:var(--bs-card-bg)}.card-header-pills{margin-right:calc(-0.5*var(--bs-card-cap-padding-x));margin-left:calc(-0.5*var(--bs-card-cap-padding-x))}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:var(--bs-card-img-overlay-padding)}.card-img,.card-img-top,.card-img-bottom{width:100%}.card-group>.card{margin-bottom:var(--bs-card-group-margin)}@media(min-width: 576px){.card-group{display:flex;display:-webkit-flex;flex-flow:row wrap;-webkit-flex-flow:row wrap}.card-group>.card{flex:1 0 0%;-webkit-flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}}.accordion{--bs-accordion-color: #343a40;--bs-accordion-bg: #fff;--bs-accordion-transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, border-radius 0.15s ease;--bs-accordion-border-color: #dee2e6;--bs-accordion-border-width: 1px;--bs-accordion-border-radius: 0.25rem;--bs-accordion-inner-border-radius: calc(0.25rem - 1px);--bs-accordion-btn-padding-x: 1.25rem;--bs-accordion-btn-padding-y: 1rem;--bs-accordion-btn-color: #343a40;--bs-accordion-btn-bg: #fff;--bs-accordion-btn-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23343a40'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");--bs-accordion-btn-icon-width: 1.25rem;--bs-accordion-btn-icon-transform: rotate(-180deg);--bs-accordion-btn-icon-transition: transform 0.2s ease-in-out;--bs-accordion-btn-active-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%2310335b'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");--bs-accordion-btn-focus-border-color: #93c0f1;--bs-accordion-btn-focus-box-shadow: 0 0 0 0.25rem rgba(39, 128, 227, 0.25);--bs-accordion-body-padding-x: 1.25rem;--bs-accordion-body-padding-y: 1rem;--bs-accordion-active-color: #10335b;--bs-accordion-active-bg: #d4e6f9}.accordion-button{position:relative;display:flex;display:-webkit-flex;align-items:center;-webkit-align-items:center;width:100%;padding:var(--bs-accordion-btn-padding-y) var(--bs-accordion-btn-padding-x);font-size:1rem;color:var(--bs-accordion-btn-color);text-align:left;background-color:var(--bs-accordion-btn-bg);border:0;overflow-anchor:none;transition:var(--bs-accordion-transition)}@media(prefers-reduced-motion: reduce){.accordion-button{transition:none}}.accordion-button:not(.collapsed){color:var(--bs-accordion-active-color);background-color:var(--bs-accordion-active-bg);box-shadow:inset 0 calc(-1*var(--bs-accordion-border-width)) 0 var(--bs-accordion-border-color)}.accordion-button:not(.collapsed)::after{background-image:var(--bs-accordion-btn-active-icon);transform:var(--bs-accordion-btn-icon-transform)}.accordion-button::after{flex-shrink:0;-webkit-flex-shrink:0;width:var(--bs-accordion-btn-icon-width);height:var(--bs-accordion-btn-icon-width);margin-left:auto;content:"";background-image:var(--bs-accordion-btn-icon);background-repeat:no-repeat;background-size:var(--bs-accordion-btn-icon-width);transition:var(--bs-accordion-btn-icon-transition)}@media(prefers-reduced-motion: reduce){.accordion-button::after{transition:none}}.accordion-button:hover{z-index:2}.accordion-button:focus{z-index:3;border-color:var(--bs-accordion-btn-focus-border-color);outline:0;box-shadow:var(--bs-accordion-btn-focus-box-shadow)}.accordion-header{margin-bottom:0}.accordion-item{color:var(--bs-accordion-color);background-color:var(--bs-accordion-bg);border:var(--bs-accordion-border-width) solid var(--bs-accordion-border-color)}.accordion-item:not(:first-of-type){border-top:0}.accordion-body{padding:var(--bs-accordion-body-padding-y) var(--bs-accordion-body-padding-x)}.accordion-flush .accordion-collapse{border-width:0}.accordion-flush .accordion-item{border-right:0;border-left:0}.accordion-flush .accordion-item:first-child{border-top:0}.accordion-flush .accordion-item:last-child{border-bottom:0}[data-bs-theme=dark] .accordion-button::after{--bs-accordion-btn-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%237db3ee'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");--bs-accordion-btn-active-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%237db3ee'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e")}.breadcrumb{--bs-breadcrumb-padding-x: 0;--bs-breadcrumb-padding-y: 0;--bs-breadcrumb-margin-bottom: 1rem;--bs-breadcrumb-bg: ;--bs-breadcrumb-border-radius: ;--bs-breadcrumb-divider-color: rgba(52, 58, 64, 0.75);--bs-breadcrumb-item-padding-x: 0.5rem;--bs-breadcrumb-item-active-color: rgba(52, 58, 64, 0.75);display:flex;display:-webkit-flex;flex-wrap:wrap;-webkit-flex-wrap:wrap;padding:var(--bs-breadcrumb-padding-y) var(--bs-breadcrumb-padding-x);margin-bottom:var(--bs-breadcrumb-margin-bottom);font-size:var(--bs-breadcrumb-font-size);list-style:none;background-color:var(--bs-breadcrumb-bg)}.breadcrumb-item+.breadcrumb-item{padding-left:var(--bs-breadcrumb-item-padding-x)}.breadcrumb-item+.breadcrumb-item::before{float:left;padding-right:var(--bs-breadcrumb-item-padding-x);color:var(--bs-breadcrumb-divider-color);content:var(--bs-breadcrumb-divider, ">") /* rtl: var(--bs-breadcrumb-divider, ">") */}.breadcrumb-item.active{color:var(--bs-breadcrumb-item-active-color)}.pagination{--bs-pagination-padding-x: 0.75rem;--bs-pagination-padding-y: 0.375rem;--bs-pagination-font-size:1rem;--bs-pagination-color: #2761e3;--bs-pagination-bg: #fff;--bs-pagination-border-width: 1px;--bs-pagination-border-color: #dee2e6;--bs-pagination-border-radius: 0.25rem;--bs-pagination-hover-color: #1f4eb6;--bs-pagination-hover-bg: #f8f9fa;--bs-pagination-hover-border-color: #dee2e6;--bs-pagination-focus-color: #1f4eb6;--bs-pagination-focus-bg: #e9ecef;--bs-pagination-focus-box-shadow: 0 0 0 0.25rem rgba(39, 128, 227, 0.25);--bs-pagination-active-color: #fff;--bs-pagination-active-bg: #2780e3;--bs-pagination-active-border-color: #2780e3;--bs-pagination-disabled-color: rgba(52, 58, 64, 0.75);--bs-pagination-disabled-bg: #e9ecef;--bs-pagination-disabled-border-color: #dee2e6;display:flex;display:-webkit-flex;padding-left:0;list-style:none}.page-link{position:relative;display:block;padding:var(--bs-pagination-padding-y) var(--bs-pagination-padding-x);font-size:var(--bs-pagination-font-size);color:var(--bs-pagination-color);text-decoration:none;-webkit-text-decoration:none;-moz-text-decoration:none;-ms-text-decoration:none;-o-text-decoration:none;background-color:var(--bs-pagination-bg);border:var(--bs-pagination-border-width) solid var(--bs-pagination-border-color);transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media(prefers-reduced-motion: reduce){.page-link{transition:none}}.page-link:hover{z-index:2;color:var(--bs-pagination-hover-color);background-color:var(--bs-pagination-hover-bg);border-color:var(--bs-pagination-hover-border-color)}.page-link:focus{z-index:3;color:var(--bs-pagination-focus-color);background-color:var(--bs-pagination-focus-bg);outline:0;box-shadow:var(--bs-pagination-focus-box-shadow)}.page-link.active,.active>.page-link{z-index:3;color:var(--bs-pagination-active-color);background-color:var(--bs-pagination-active-bg);border-color:var(--bs-pagination-active-border-color)}.page-link.disabled,.disabled>.page-link{color:var(--bs-pagination-disabled-color);pointer-events:none;background-color:var(--bs-pagination-disabled-bg);border-color:var(--bs-pagination-disabled-border-color)}.page-item:not(:first-child) .page-link{margin-left:calc(1px*-1)}.pagination-lg{--bs-pagination-padding-x: 1.5rem;--bs-pagination-padding-y: 0.75rem;--bs-pagination-font-size:1.25rem;--bs-pagination-border-radius: 0.5rem}.pagination-sm{--bs-pagination-padding-x: 0.5rem;--bs-pagination-padding-y: 0.25rem;--bs-pagination-font-size:0.875rem;--bs-pagination-border-radius: 0.2em}.badge{--bs-badge-padding-x: 0.65em;--bs-badge-padding-y: 0.35em;--bs-badge-font-size:0.75em;--bs-badge-font-weight: 700;--bs-badge-color: #fff;--bs-badge-border-radius: 0.25rem;display:inline-block;padding:var(--bs-badge-padding-y) var(--bs-badge-padding-x);font-size:var(--bs-badge-font-size);font-weight:var(--bs-badge-font-weight);line-height:1;color:var(--bs-badge-color);text-align:center;white-space:nowrap;vertical-align:baseline}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.alert{--bs-alert-bg: transparent;--bs-alert-padding-x: 1rem;--bs-alert-padding-y: 1rem;--bs-alert-margin-bottom: 1rem;--bs-alert-color: inherit;--bs-alert-border-color: transparent;--bs-alert-border: 0 solid var(--bs-alert-border-color);--bs-alert-border-radius: 0.25rem;--bs-alert-link-color: inherit;position:relative;padding:var(--bs-alert-padding-y) var(--bs-alert-padding-x);margin-bottom:var(--bs-alert-margin-bottom);color:var(--bs-alert-color);background-color:var(--bs-alert-bg);border:var(--bs-alert-border)}.alert-heading{color:inherit}.alert-link{font-weight:700;color:var(--bs-alert-link-color)}.alert-dismissible{padding-right:3rem}.alert-dismissible .btn-close{position:absolute;top:0;right:0;z-index:2;padding:1.25rem 1rem}.alert-default{--bs-alert-color: var(--bs-default-text-emphasis);--bs-alert-bg: var(--bs-default-bg-subtle);--bs-alert-border-color: var(--bs-default-border-subtle);--bs-alert-link-color: var(--bs-default-text-emphasis)}.alert-primary{--bs-alert-color: var(--bs-primary-text-emphasis);--bs-alert-bg: var(--bs-primary-bg-subtle);--bs-alert-border-color: var(--bs-primary-border-subtle);--bs-alert-link-color: var(--bs-primary-text-emphasis)}.alert-secondary{--bs-alert-color: var(--bs-secondary-text-emphasis);--bs-alert-bg: var(--bs-secondary-bg-subtle);--bs-alert-border-color: var(--bs-secondary-border-subtle);--bs-alert-link-color: var(--bs-secondary-text-emphasis)}.alert-success{--bs-alert-color: var(--bs-success-text-emphasis);--bs-alert-bg: var(--bs-success-bg-subtle);--bs-alert-border-color: var(--bs-success-border-subtle);--bs-alert-link-color: var(--bs-success-text-emphasis)}.alert-info{--bs-alert-color: var(--bs-info-text-emphasis);--bs-alert-bg: var(--bs-info-bg-subtle);--bs-alert-border-color: var(--bs-info-border-subtle);--bs-alert-link-color: var(--bs-info-text-emphasis)}.alert-warning{--bs-alert-color: var(--bs-warning-text-emphasis);--bs-alert-bg: var(--bs-warning-bg-subtle);--bs-alert-border-color: var(--bs-warning-border-subtle);--bs-alert-link-color: var(--bs-warning-text-emphasis)}.alert-danger{--bs-alert-color: var(--bs-danger-text-emphasis);--bs-alert-bg: var(--bs-danger-bg-subtle);--bs-alert-border-color: var(--bs-danger-border-subtle);--bs-alert-link-color: var(--bs-danger-text-emphasis)}.alert-light{--bs-alert-color: var(--bs-light-text-emphasis);--bs-alert-bg: var(--bs-light-bg-subtle);--bs-alert-border-color: var(--bs-light-border-subtle);--bs-alert-link-color: var(--bs-light-text-emphasis)}.alert-dark{--bs-alert-color: var(--bs-dark-text-emphasis);--bs-alert-bg: var(--bs-dark-bg-subtle);--bs-alert-border-color: var(--bs-dark-border-subtle);--bs-alert-link-color: var(--bs-dark-text-emphasis)}@keyframes progress-bar-stripes{0%{background-position-x:.5rem}}.progress,.progress-stacked{--bs-progress-height: 0.5rem;--bs-progress-font-size:0.75rem;--bs-progress-bg: #e9ecef;--bs-progress-border-radius: 0.25rem;--bs-progress-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.075);--bs-progress-bar-color: #fff;--bs-progress-bar-bg: #2780e3;--bs-progress-bar-transition: width 0.6s ease;display:flex;display:-webkit-flex;height:var(--bs-progress-height);overflow:hidden;font-size:var(--bs-progress-font-size);background-color:var(--bs-progress-bg)}.progress-bar{display:flex;display:-webkit-flex;flex-direction:column;-webkit-flex-direction:column;justify-content:center;-webkit-justify-content:center;overflow:hidden;color:var(--bs-progress-bar-color);text-align:center;white-space:nowrap;background-color:var(--bs-progress-bar-bg);transition:var(--bs-progress-bar-transition)}@media(prefers-reduced-motion: reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-size:var(--bs-progress-height) var(--bs-progress-height)}.progress-stacked>.progress{overflow:visible}.progress-stacked>.progress>.progress-bar{width:100%}.progress-bar-animated{animation:1s linear infinite progress-bar-stripes}@media(prefers-reduced-motion: reduce){.progress-bar-animated{animation:none}}.list-group{--bs-list-group-color: #343a40;--bs-list-group-bg: #fff;--bs-list-group-border-color: #dee2e6;--bs-list-group-border-width: 1px;--bs-list-group-border-radius: 0.25rem;--bs-list-group-item-padding-x: 1rem;--bs-list-group-item-padding-y: 0.5rem;--bs-list-group-action-color: rgba(52, 58, 64, 0.75);--bs-list-group-action-hover-color: #000;--bs-list-group-action-hover-bg: #f8f9fa;--bs-list-group-action-active-color: #343a40;--bs-list-group-action-active-bg: #e9ecef;--bs-list-group-disabled-color: rgba(52, 58, 64, 0.75);--bs-list-group-disabled-bg: #fff;--bs-list-group-active-color: #fff;--bs-list-group-active-bg: #2780e3;--bs-list-group-active-border-color: #2780e3;display:flex;display:-webkit-flex;flex-direction:column;-webkit-flex-direction:column;padding-left:0;margin-bottom:0}.list-group-numbered{list-style-type:none;counter-reset:section}.list-group-numbered>.list-group-item::before{content:counters(section, ".") ". ";counter-increment:section}.list-group-item-action{width:100%;color:var(--bs-list-group-action-color);text-align:inherit}.list-group-item-action:hover,.list-group-item-action:focus{z-index:1;color:var(--bs-list-group-action-hover-color);text-decoration:none;background-color:var(--bs-list-group-action-hover-bg)}.list-group-item-action:active{color:var(--bs-list-group-action-active-color);background-color:var(--bs-list-group-action-active-bg)}.list-group-item{position:relative;display:block;padding:var(--bs-list-group-item-padding-y) var(--bs-list-group-item-padding-x);color:var(--bs-list-group-color);text-decoration:none;-webkit-text-decoration:none;-moz-text-decoration:none;-ms-text-decoration:none;-o-text-decoration:none;background-color:var(--bs-list-group-bg);border:var(--bs-list-group-border-width) solid var(--bs-list-group-border-color)}.list-group-item.disabled,.list-group-item:disabled{color:var(--bs-list-group-disabled-color);pointer-events:none;background-color:var(--bs-list-group-disabled-bg)}.list-group-item.active{z-index:2;color:var(--bs-list-group-active-color);background-color:var(--bs-list-group-active-bg);border-color:var(--bs-list-group-active-border-color)}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:calc(-1*var(--bs-list-group-border-width));border-top-width:var(--bs-list-group-border-width)}.list-group-horizontal{flex-direction:row;-webkit-flex-direction:row}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:calc(-1*var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}@media(min-width: 576px){.list-group-horizontal-sm{flex-direction:row;-webkit-flex-direction:row}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:calc(-1*var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media(min-width: 768px){.list-group-horizontal-md{flex-direction:row;-webkit-flex-direction:row}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:calc(-1*var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media(min-width: 992px){.list-group-horizontal-lg{flex-direction:row;-webkit-flex-direction:row}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:calc(-1*var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media(min-width: 1200px){.list-group-horizontal-xl{flex-direction:row;-webkit-flex-direction:row}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:calc(-1*var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media(min-width: 1400px){.list-group-horizontal-xxl{flex-direction:row;-webkit-flex-direction:row}.list-group-horizontal-xxl>.list-group-item.active{margin-top:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item.active{margin-left:calc(-1*var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}.list-group-flush>.list-group-item{border-width:0 0 var(--bs-list-group-border-width)}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-default{--bs-list-group-color: var(--bs-default-text-emphasis);--bs-list-group-bg: var(--bs-default-bg-subtle);--bs-list-group-border-color: var(--bs-default-border-subtle);--bs-list-group-action-hover-color: var(--bs-emphasis-color);--bs-list-group-action-hover-bg: var(--bs-default-border-subtle);--bs-list-group-action-active-color: var(--bs-emphasis-color);--bs-list-group-action-active-bg: var(--bs-default-border-subtle);--bs-list-group-active-color: var(--bs-default-bg-subtle);--bs-list-group-active-bg: var(--bs-default-text-emphasis);--bs-list-group-active-border-color: var(--bs-default-text-emphasis)}.list-group-item-primary{--bs-list-group-color: var(--bs-primary-text-emphasis);--bs-list-group-bg: var(--bs-primary-bg-subtle);--bs-list-group-border-color: var(--bs-primary-border-subtle);--bs-list-group-action-hover-color: var(--bs-emphasis-color);--bs-list-group-action-hover-bg: var(--bs-primary-border-subtle);--bs-list-group-action-active-color: var(--bs-emphasis-color);--bs-list-group-action-active-bg: var(--bs-primary-border-subtle);--bs-list-group-active-color: var(--bs-primary-bg-subtle);--bs-list-group-active-bg: var(--bs-primary-text-emphasis);--bs-list-group-active-border-color: var(--bs-primary-text-emphasis)}.list-group-item-secondary{--bs-list-group-color: var(--bs-secondary-text-emphasis);--bs-list-group-bg: var(--bs-secondary-bg-subtle);--bs-list-group-border-color: var(--bs-secondary-border-subtle);--bs-list-group-action-hover-color: var(--bs-emphasis-color);--bs-list-group-action-hover-bg: var(--bs-secondary-border-subtle);--bs-list-group-action-active-color: var(--bs-emphasis-color);--bs-list-group-action-active-bg: var(--bs-secondary-border-subtle);--bs-list-group-active-color: var(--bs-secondary-bg-subtle);--bs-list-group-active-bg: var(--bs-secondary-text-emphasis);--bs-list-group-active-border-color: var(--bs-secondary-text-emphasis)}.list-group-item-success{--bs-list-group-color: var(--bs-success-text-emphasis);--bs-list-group-bg: var(--bs-success-bg-subtle);--bs-list-group-border-color: var(--bs-success-border-subtle);--bs-list-group-action-hover-color: var(--bs-emphasis-color);--bs-list-group-action-hover-bg: var(--bs-success-border-subtle);--bs-list-group-action-active-color: var(--bs-emphasis-color);--bs-list-group-action-active-bg: var(--bs-success-border-subtle);--bs-list-group-active-color: var(--bs-success-bg-subtle);--bs-list-group-active-bg: var(--bs-success-text-emphasis);--bs-list-group-active-border-color: var(--bs-success-text-emphasis)}.list-group-item-info{--bs-list-group-color: var(--bs-info-text-emphasis);--bs-list-group-bg: var(--bs-info-bg-subtle);--bs-list-group-border-color: var(--bs-info-border-subtle);--bs-list-group-action-hover-color: var(--bs-emphasis-color);--bs-list-group-action-hover-bg: var(--bs-info-border-subtle);--bs-list-group-action-active-color: var(--bs-emphasis-color);--bs-list-group-action-active-bg: var(--bs-info-border-subtle);--bs-list-group-active-color: var(--bs-info-bg-subtle);--bs-list-group-active-bg: var(--bs-info-text-emphasis);--bs-list-group-active-border-color: var(--bs-info-text-emphasis)}.list-group-item-warning{--bs-list-group-color: var(--bs-warning-text-emphasis);--bs-list-group-bg: var(--bs-warning-bg-subtle);--bs-list-group-border-color: var(--bs-warning-border-subtle);--bs-list-group-action-hover-color: var(--bs-emphasis-color);--bs-list-group-action-hover-bg: var(--bs-warning-border-subtle);--bs-list-group-action-active-color: var(--bs-emphasis-color);--bs-list-group-action-active-bg: var(--bs-warning-border-subtle);--bs-list-group-active-color: var(--bs-warning-bg-subtle);--bs-list-group-active-bg: var(--bs-warning-text-emphasis);--bs-list-group-active-border-color: var(--bs-warning-text-emphasis)}.list-group-item-danger{--bs-list-group-color: var(--bs-danger-text-emphasis);--bs-list-group-bg: var(--bs-danger-bg-subtle);--bs-list-group-border-color: var(--bs-danger-border-subtle);--bs-list-group-action-hover-color: var(--bs-emphasis-color);--bs-list-group-action-hover-bg: var(--bs-danger-border-subtle);--bs-list-group-action-active-color: var(--bs-emphasis-color);--bs-list-group-action-active-bg: var(--bs-danger-border-subtle);--bs-list-group-active-color: var(--bs-danger-bg-subtle);--bs-list-group-active-bg: var(--bs-danger-text-emphasis);--bs-list-group-active-border-color: var(--bs-danger-text-emphasis)}.list-group-item-light{--bs-list-group-color: var(--bs-light-text-emphasis);--bs-list-group-bg: var(--bs-light-bg-subtle);--bs-list-group-border-color: var(--bs-light-border-subtle);--bs-list-group-action-hover-color: var(--bs-emphasis-color);--bs-list-group-action-hover-bg: var(--bs-light-border-subtle);--bs-list-group-action-active-color: var(--bs-emphasis-color);--bs-list-group-action-active-bg: var(--bs-light-border-subtle);--bs-list-group-active-color: var(--bs-light-bg-subtle);--bs-list-group-active-bg: var(--bs-light-text-emphasis);--bs-list-group-active-border-color: var(--bs-light-text-emphasis)}.list-group-item-dark{--bs-list-group-color: var(--bs-dark-text-emphasis);--bs-list-group-bg: var(--bs-dark-bg-subtle);--bs-list-group-border-color: var(--bs-dark-border-subtle);--bs-list-group-action-hover-color: var(--bs-emphasis-color);--bs-list-group-action-hover-bg: var(--bs-dark-border-subtle);--bs-list-group-action-active-color: var(--bs-emphasis-color);--bs-list-group-action-active-bg: var(--bs-dark-border-subtle);--bs-list-group-active-color: var(--bs-dark-bg-subtle);--bs-list-group-active-bg: var(--bs-dark-text-emphasis);--bs-list-group-active-border-color: var(--bs-dark-text-emphasis)}.btn-close{--bs-btn-close-color: #000;--bs-btn-close-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 0 1 1.414 0L8 6.586 14.293.293a1 1 0 1 1 1.414 1.414L9.414 8l6.293 6.293a1 1 0 0 1-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L6.586 8 .293 1.707a1 1 0 0 1 0-1.414z'/%3e%3c/svg%3e");--bs-btn-close-opacity: 0.5;--bs-btn-close-hover-opacity: 0.75;--bs-btn-close-focus-shadow: 0 0 0 0.25rem rgba(39, 128, 227, 0.25);--bs-btn-close-focus-opacity: 1;--bs-btn-close-disabled-opacity: 0.25;--bs-btn-close-white-filter: invert(1) grayscale(100%) brightness(200%);box-sizing:content-box;width:1em;height:1em;padding:.25em .25em;color:var(--bs-btn-close-color);background:rgba(0,0,0,0) var(--bs-btn-close-bg) center/1em auto no-repeat;border:0;opacity:var(--bs-btn-close-opacity)}.btn-close:hover{color:var(--bs-btn-close-color);text-decoration:none;opacity:var(--bs-btn-close-hover-opacity)}.btn-close:focus{outline:0;box-shadow:var(--bs-btn-close-focus-shadow);opacity:var(--bs-btn-close-focus-opacity)}.btn-close:disabled,.btn-close.disabled{pointer-events:none;user-select:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;-o-user-select:none;opacity:var(--bs-btn-close-disabled-opacity)}.btn-close-white{filter:var(--bs-btn-close-white-filter)}[data-bs-theme=dark] .btn-close{filter:var(--bs-btn-close-white-filter)}.toast{--bs-toast-zindex: 1090;--bs-toast-padding-x: 0.75rem;--bs-toast-padding-y: 0.5rem;--bs-toast-spacing: 1.5rem;--bs-toast-max-width: 350px;--bs-toast-font-size:0.875rem;--bs-toast-color: ;--bs-toast-bg: rgba(255, 255, 255, 0.85);--bs-toast-border-width: 1px;--bs-toast-border-color: rgba(0, 0, 0, 0.175);--bs-toast-border-radius: 0.25rem;--bs-toast-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);--bs-toast-header-color: rgba(52, 58, 64, 0.75);--bs-toast-header-bg: rgba(255, 255, 255, 0.85);--bs-toast-header-border-color: rgba(0, 0, 0, 0.175);width:var(--bs-toast-max-width);max-width:100%;font-size:var(--bs-toast-font-size);color:var(--bs-toast-color);pointer-events:auto;background-color:var(--bs-toast-bg);background-clip:padding-box;border:var(--bs-toast-border-width) solid var(--bs-toast-border-color);box-shadow:var(--bs-toast-box-shadow)}.toast.showing{opacity:0}.toast:not(.show){display:none}.toast-container{--bs-toast-zindex: 1090;position:absolute;z-index:var(--bs-toast-zindex);width:max-content;width:-webkit-max-content;width:-moz-max-content;width:-ms-max-content;width:-o-max-content;max-width:100%;pointer-events:none}.toast-container>:not(:last-child){margin-bottom:var(--bs-toast-spacing)}.toast-header{display:flex;display:-webkit-flex;align-items:center;-webkit-align-items:center;padding:var(--bs-toast-padding-y) var(--bs-toast-padding-x);color:var(--bs-toast-header-color);background-color:var(--bs-toast-header-bg);background-clip:padding-box;border-bottom:var(--bs-toast-border-width) solid var(--bs-toast-header-border-color)}.toast-header .btn-close{margin-right:calc(-0.5*var(--bs-toast-padding-x));margin-left:var(--bs-toast-padding-x)}.toast-body{padding:var(--bs-toast-padding-x);word-wrap:break-word}.modal{--bs-modal-zindex: 1055;--bs-modal-width: 500px;--bs-modal-padding: 1rem;--bs-modal-margin: 0.5rem;--bs-modal-color: ;--bs-modal-bg: #fff;--bs-modal-border-color: rgba(0, 0, 0, 0.175);--bs-modal-border-width: 1px;--bs-modal-border-radius: 0.5rem;--bs-modal-box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);--bs-modal-inner-border-radius: calc(0.5rem - 1px);--bs-modal-header-padding-x: 1rem;--bs-modal-header-padding-y: 1rem;--bs-modal-header-padding: 1rem 1rem;--bs-modal-header-border-color: #dee2e6;--bs-modal-header-border-width: 1px;--bs-modal-title-line-height: 1.5;--bs-modal-footer-gap: 0.5rem;--bs-modal-footer-bg: ;--bs-modal-footer-border-color: #dee2e6;--bs-modal-footer-border-width: 1px;position:fixed;top:0;left:0;z-index:var(--bs-modal-zindex);display:none;width:100%;height:100%;overflow-x:hidden;overflow-y:auto;outline:0}.modal-dialog{position:relative;width:auto;margin:var(--bs-modal-margin);pointer-events:none}.modal.fade .modal-dialog{transition:transform .3s ease-out;transform:translate(0, -50px)}@media(prefers-reduced-motion: reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{transform:none}.modal.modal-static .modal-dialog{transform:scale(1.02)}.modal-dialog-scrollable{height:calc(100% - var(--bs-modal-margin)*2)}.modal-dialog-scrollable .modal-content{max-height:100%;overflow:hidden}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:flex;display:-webkit-flex;align-items:center;-webkit-align-items:center;min-height:calc(100% - var(--bs-modal-margin)*2)}.modal-content{position:relative;display:flex;display:-webkit-flex;flex-direction:column;-webkit-flex-direction:column;width:100%;color:var(--bs-modal-color);pointer-events:auto;background-color:var(--bs-modal-bg);background-clip:padding-box;border:var(--bs-modal-border-width) solid var(--bs-modal-border-color);outline:0}.modal-backdrop{--bs-backdrop-zindex: 1050;--bs-backdrop-bg: #000;--bs-backdrop-opacity: 0.5;position:fixed;top:0;left:0;z-index:var(--bs-backdrop-zindex);width:100vw;height:100vh;background-color:var(--bs-backdrop-bg)}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:var(--bs-backdrop-opacity)}.modal-header{display:flex;display:-webkit-flex;flex-shrink:0;-webkit-flex-shrink:0;align-items:center;-webkit-align-items:center;justify-content:space-between;-webkit-justify-content:space-between;padding:var(--bs-modal-header-padding);border-bottom:var(--bs-modal-header-border-width) solid var(--bs-modal-header-border-color)}.modal-header .btn-close{padding:calc(var(--bs-modal-header-padding-y)*.5) calc(var(--bs-modal-header-padding-x)*.5);margin:calc(-0.5*var(--bs-modal-header-padding-y)) calc(-0.5*var(--bs-modal-header-padding-x)) calc(-0.5*var(--bs-modal-header-padding-y)) auto}.modal-title{margin-bottom:0;line-height:var(--bs-modal-title-line-height)}.modal-body{position:relative;flex:1 1 auto;-webkit-flex:1 1 auto;padding:var(--bs-modal-padding)}.modal-footer{display:flex;display:-webkit-flex;flex-shrink:0;-webkit-flex-shrink:0;flex-wrap:wrap;-webkit-flex-wrap:wrap;align-items:center;-webkit-align-items:center;justify-content:flex-end;-webkit-justify-content:flex-end;padding:calc(var(--bs-modal-padding) - var(--bs-modal-footer-gap)*.5);background-color:var(--bs-modal-footer-bg);border-top:var(--bs-modal-footer-border-width) solid var(--bs-modal-footer-border-color)}.modal-footer>*{margin:calc(var(--bs-modal-footer-gap)*.5)}@media(min-width: 576px){.modal{--bs-modal-margin: 1.75rem;--bs-modal-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15)}.modal-dialog{max-width:var(--bs-modal-width);margin-right:auto;margin-left:auto}.modal-sm{--bs-modal-width: 300px}}@media(min-width: 992px){.modal-lg,.modal-xl{--bs-modal-width: 800px}}@media(min-width: 1200px){.modal-xl{--bs-modal-width: 1140px}}.modal-fullscreen{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen .modal-content{height:100%;border:0}.modal-fullscreen .modal-body{overflow-y:auto}@media(max-width: 575.98px){.modal-fullscreen-sm-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-sm-down .modal-content{height:100%;border:0}.modal-fullscreen-sm-down .modal-body{overflow-y:auto}}@media(max-width: 767.98px){.modal-fullscreen-md-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-md-down .modal-content{height:100%;border:0}.modal-fullscreen-md-down .modal-body{overflow-y:auto}}@media(max-width: 991.98px){.modal-fullscreen-lg-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-lg-down .modal-content{height:100%;border:0}.modal-fullscreen-lg-down .modal-body{overflow-y:auto}}@media(max-width: 1199.98px){.modal-fullscreen-xl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xl-down .modal-content{height:100%;border:0}.modal-fullscreen-xl-down .modal-body{overflow-y:auto}}@media(max-width: 1399.98px){.modal-fullscreen-xxl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xxl-down .modal-content{height:100%;border:0}.modal-fullscreen-xxl-down .modal-body{overflow-y:auto}}.tooltip{--bs-tooltip-zindex: 1080;--bs-tooltip-max-width: 200px;--bs-tooltip-padding-x: 0.5rem;--bs-tooltip-padding-y: 0.25rem;--bs-tooltip-margin: ;--bs-tooltip-font-size:0.875rem;--bs-tooltip-color: #fff;--bs-tooltip-bg: #000;--bs-tooltip-border-radius: 0.25rem;--bs-tooltip-opacity: 0.9;--bs-tooltip-arrow-width: 0.8rem;--bs-tooltip-arrow-height: 0.4rem;z-index:var(--bs-tooltip-zindex);display:block;margin:var(--bs-tooltip-margin);font-family:"Source Sans Pro",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;white-space:normal;word-spacing:normal;line-break:auto;font-size:var(--bs-tooltip-font-size);word-wrap:break-word;opacity:0}.tooltip.show{opacity:var(--bs-tooltip-opacity)}.tooltip .tooltip-arrow{display:block;width:var(--bs-tooltip-arrow-width);height:var(--bs-tooltip-arrow-height)}.tooltip .tooltip-arrow::before{position:absolute;content:"";border-color:rgba(0,0,0,0);border-style:solid}.bs-tooltip-top .tooltip-arrow,.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow{bottom:calc(-1*var(--bs-tooltip-arrow-height))}.bs-tooltip-top .tooltip-arrow::before,.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow::before{top:-1px;border-width:var(--bs-tooltip-arrow-height) calc(var(--bs-tooltip-arrow-width)*.5) 0;border-top-color:var(--bs-tooltip-bg)}.bs-tooltip-end .tooltip-arrow,.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow{left:calc(-1*var(--bs-tooltip-arrow-height));width:var(--bs-tooltip-arrow-height);height:var(--bs-tooltip-arrow-width)}.bs-tooltip-end .tooltip-arrow::before,.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow::before{right:-1px;border-width:calc(var(--bs-tooltip-arrow-width)*.5) var(--bs-tooltip-arrow-height) calc(var(--bs-tooltip-arrow-width)*.5) 0;border-right-color:var(--bs-tooltip-bg)}.bs-tooltip-bottom .tooltip-arrow,.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow{top:calc(-1*var(--bs-tooltip-arrow-height))}.bs-tooltip-bottom .tooltip-arrow::before,.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow::before{bottom:-1px;border-width:0 calc(var(--bs-tooltip-arrow-width)*.5) var(--bs-tooltip-arrow-height);border-bottom-color:var(--bs-tooltip-bg)}.bs-tooltip-start .tooltip-arrow,.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow{right:calc(-1*var(--bs-tooltip-arrow-height));width:var(--bs-tooltip-arrow-height);height:var(--bs-tooltip-arrow-width)}.bs-tooltip-start .tooltip-arrow::before,.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow::before{left:-1px;border-width:calc(var(--bs-tooltip-arrow-width)*.5) 0 calc(var(--bs-tooltip-arrow-width)*.5) var(--bs-tooltip-arrow-height);border-left-color:var(--bs-tooltip-bg)}.tooltip-inner{max-width:var(--bs-tooltip-max-width);padding:var(--bs-tooltip-padding-y) var(--bs-tooltip-padding-x);color:var(--bs-tooltip-color);text-align:center;background-color:var(--bs-tooltip-bg)}.popover{--bs-popover-zindex: 1070;--bs-popover-max-width: 276px;--bs-popover-font-size:0.875rem;--bs-popover-bg: #fff;--bs-popover-border-width: 1px;--bs-popover-border-color: rgba(0, 0, 0, 0.175);--bs-popover-border-radius: 0.5rem;--bs-popover-inner-border-radius: calc(0.5rem - 1px);--bs-popover-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);--bs-popover-header-padding-x: 1rem;--bs-popover-header-padding-y: 0.5rem;--bs-popover-header-font-size:1rem;--bs-popover-header-color: inherit;--bs-popover-header-bg: #e9ecef;--bs-popover-body-padding-x: 1rem;--bs-popover-body-padding-y: 1rem;--bs-popover-body-color: #343a40;--bs-popover-arrow-width: 1rem;--bs-popover-arrow-height: 0.5rem;--bs-popover-arrow-border: var(--bs-popover-border-color);z-index:var(--bs-popover-zindex);display:block;max-width:var(--bs-popover-max-width);font-family:"Source Sans Pro",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;white-space:normal;word-spacing:normal;line-break:auto;font-size:var(--bs-popover-font-size);word-wrap:break-word;background-color:var(--bs-popover-bg);background-clip:padding-box;border:var(--bs-popover-border-width) solid var(--bs-popover-border-color)}.popover .popover-arrow{display:block;width:var(--bs-popover-arrow-width);height:var(--bs-popover-arrow-height)}.popover .popover-arrow::before,.popover .popover-arrow::after{position:absolute;display:block;content:"";border-color:rgba(0,0,0,0);border-style:solid;border-width:0}.bs-popover-top>.popover-arrow,.bs-popover-auto[data-popper-placement^=top]>.popover-arrow{bottom:calc(-1*(var(--bs-popover-arrow-height)) - var(--bs-popover-border-width))}.bs-popover-top>.popover-arrow::before,.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after{border-width:var(--bs-popover-arrow-height) calc(var(--bs-popover-arrow-width)*.5) 0}.bs-popover-top>.popover-arrow::before,.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before{bottom:0;border-top-color:var(--bs-popover-arrow-border)}.bs-popover-top>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after{bottom:var(--bs-popover-border-width);border-top-color:var(--bs-popover-bg)}.bs-popover-end>.popover-arrow,.bs-popover-auto[data-popper-placement^=right]>.popover-arrow{left:calc(-1*(var(--bs-popover-arrow-height)) - var(--bs-popover-border-width));width:var(--bs-popover-arrow-height);height:var(--bs-popover-arrow-width)}.bs-popover-end>.popover-arrow::before,.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after{border-width:calc(var(--bs-popover-arrow-width)*.5) var(--bs-popover-arrow-height) calc(var(--bs-popover-arrow-width)*.5) 0}.bs-popover-end>.popover-arrow::before,.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before{left:0;border-right-color:var(--bs-popover-arrow-border)}.bs-popover-end>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after{left:var(--bs-popover-border-width);border-right-color:var(--bs-popover-bg)}.bs-popover-bottom>.popover-arrow,.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow{top:calc(-1*(var(--bs-popover-arrow-height)) - var(--bs-popover-border-width))}.bs-popover-bottom>.popover-arrow::before,.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after{border-width:0 calc(var(--bs-popover-arrow-width)*.5) var(--bs-popover-arrow-height)}.bs-popover-bottom>.popover-arrow::before,.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before{top:0;border-bottom-color:var(--bs-popover-arrow-border)}.bs-popover-bottom>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after{top:var(--bs-popover-border-width);border-bottom-color:var(--bs-popover-bg)}.bs-popover-bottom .popover-header::before,.bs-popover-auto[data-popper-placement^=bottom] .popover-header::before{position:absolute;top:0;left:50%;display:block;width:var(--bs-popover-arrow-width);margin-left:calc(-0.5*var(--bs-popover-arrow-width));content:"";border-bottom:var(--bs-popover-border-width) solid var(--bs-popover-header-bg)}.bs-popover-start>.popover-arrow,.bs-popover-auto[data-popper-placement^=left]>.popover-arrow{right:calc(-1*(var(--bs-popover-arrow-height)) - var(--bs-popover-border-width));width:var(--bs-popover-arrow-height);height:var(--bs-popover-arrow-width)}.bs-popover-start>.popover-arrow::before,.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after{border-width:calc(var(--bs-popover-arrow-width)*.5) 0 calc(var(--bs-popover-arrow-width)*.5) var(--bs-popover-arrow-height)}.bs-popover-start>.popover-arrow::before,.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before{right:0;border-left-color:var(--bs-popover-arrow-border)}.bs-popover-start>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after{right:var(--bs-popover-border-width);border-left-color:var(--bs-popover-bg)}.popover-header{padding:var(--bs-popover-header-padding-y) var(--bs-popover-header-padding-x);margin-bottom:0;font-size:var(--bs-popover-header-font-size);color:var(--bs-popover-header-color);background-color:var(--bs-popover-header-bg);border-bottom:var(--bs-popover-border-width) solid var(--bs-popover-border-color)}.popover-header:empty{display:none}.popover-body{padding:var(--bs-popover-body-padding-y) var(--bs-popover-body-padding-x);color:var(--bs-popover-body-color)}.carousel{position:relative}.carousel.pointer-event{touch-action:pan-y;-webkit-touch-action:pan-y;-moz-touch-action:pan-y;-ms-touch-action:pan-y;-o-touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;backface-visibility:hidden;-webkit-backface-visibility:hidden;-moz-backface-visibility:hidden;-ms-backface-visibility:hidden;-o-backface-visibility:hidden;transition:transform .6s ease-in-out}@media(prefers-reduced-motion: reduce){.carousel-item{transition:none}}.carousel-item.active,.carousel-item-next,.carousel-item-prev{display:block}.carousel-item-next:not(.carousel-item-start),.active.carousel-item-end{transform:translateX(100%)}.carousel-item-prev:not(.carousel-item-end),.active.carousel-item-start{transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;transform:none}.carousel-fade .carousel-item.active,.carousel-fade .carousel-item-next.carousel-item-start,.carousel-fade .carousel-item-prev.carousel-item-end{z-index:1;opacity:1}.carousel-fade .active.carousel-item-start,.carousel-fade .active.carousel-item-end{z-index:0;opacity:0;transition:opacity 0s .6s}@media(prefers-reduced-motion: reduce){.carousel-fade .active.carousel-item-start,.carousel-fade .active.carousel-item-end{transition:none}}.carousel-control-prev,.carousel-control-next{position:absolute;top:0;bottom:0;z-index:1;display:flex;display:-webkit-flex;align-items:center;-webkit-align-items:center;justify-content:center;-webkit-justify-content:center;width:15%;padding:0;color:#fff;text-align:center;background:none;border:0;opacity:.5;transition:opacity .15s ease}@media(prefers-reduced-motion: reduce){.carousel-control-prev,.carousel-control-next{transition:none}}.carousel-control-prev:hover,.carousel-control-prev:focus,.carousel-control-next:hover,.carousel-control-next:focus{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-prev-icon,.carousel-control-next-icon{display:inline-block;width:2rem;height:2rem;background-repeat:no-repeat;background-position:50%;background-size:100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:2;display:flex;display:-webkit-flex;justify-content:center;-webkit-justify-content:center;padding:0;margin-right:15%;margin-bottom:1rem;margin-left:15%}.carousel-indicators [data-bs-target]{box-sizing:content-box;flex:0 1 auto;-webkit-flex:0 1 auto;width:30px;height:3px;padding:0;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border:0;border-top:10px solid rgba(0,0,0,0);border-bottom:10px solid rgba(0,0,0,0);opacity:.5;transition:opacity .6s ease}@media(prefers-reduced-motion: reduce){.carousel-indicators [data-bs-target]{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:1.25rem;left:15%;padding-top:1.25rem;padding-bottom:1.25rem;color:#fff;text-align:center}.carousel-dark .carousel-control-prev-icon,.carousel-dark .carousel-control-next-icon{filter:invert(1) grayscale(100)}.carousel-dark .carousel-indicators [data-bs-target]{background-color:#000}.carousel-dark .carousel-caption{color:#000}[data-bs-theme=dark] .carousel .carousel-control-prev-icon,[data-bs-theme=dark] .carousel .carousel-control-next-icon,[data-bs-theme=dark].carousel .carousel-control-prev-icon,[data-bs-theme=dark].carousel .carousel-control-next-icon{filter:invert(1) grayscale(100)}[data-bs-theme=dark] .carousel .carousel-indicators [data-bs-target],[data-bs-theme=dark].carousel .carousel-indicators [data-bs-target]{background-color:#000}[data-bs-theme=dark] .carousel .carousel-caption,[data-bs-theme=dark].carousel .carousel-caption{color:#000}.spinner-grow,.spinner-border{display:inline-block;width:var(--bs-spinner-width);height:var(--bs-spinner-height);vertical-align:var(--bs-spinner-vertical-align);border-radius:50%;animation:var(--bs-spinner-animation-speed) linear infinite var(--bs-spinner-animation-name)}@keyframes spinner-border{to{transform:rotate(360deg) /* rtl:ignore */}}.spinner-border{--bs-spinner-width: 2rem;--bs-spinner-height: 2rem;--bs-spinner-vertical-align: -0.125em;--bs-spinner-border-width: 0.25em;--bs-spinner-animation-speed: 0.75s;--bs-spinner-animation-name: spinner-border;border:var(--bs-spinner-border-width) solid currentcolor;border-right-color:rgba(0,0,0,0)}.spinner-border-sm{--bs-spinner-width: 1rem;--bs-spinner-height: 1rem;--bs-spinner-border-width: 0.2em}@keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}.spinner-grow{--bs-spinner-width: 2rem;--bs-spinner-height: 2rem;--bs-spinner-vertical-align: -0.125em;--bs-spinner-animation-speed: 0.75s;--bs-spinner-animation-name: spinner-grow;background-color:currentcolor;opacity:0}.spinner-grow-sm{--bs-spinner-width: 1rem;--bs-spinner-height: 1rem}@media(prefers-reduced-motion: reduce){.spinner-border,.spinner-grow{--bs-spinner-animation-speed: 1.5s}}.offcanvas,.offcanvas-xxl,.offcanvas-xl,.offcanvas-lg,.offcanvas-md,.offcanvas-sm{--bs-offcanvas-zindex: 1045;--bs-offcanvas-width: 400px;--bs-offcanvas-height: 30vh;--bs-offcanvas-padding-x: 1rem;--bs-offcanvas-padding-y: 1rem;--bs-offcanvas-color: #343a40;--bs-offcanvas-bg: #fff;--bs-offcanvas-border-width: 1px;--bs-offcanvas-border-color: rgba(0, 0, 0, 0.175);--bs-offcanvas-box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);--bs-offcanvas-transition: transform 0.3s ease-in-out;--bs-offcanvas-title-line-height: 1.5}@media(max-width: 575.98px){.offcanvas-sm{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;display:-webkit-flex;flex-direction:column;-webkit-flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media(max-width: 575.98px)and (prefers-reduced-motion: reduce){.offcanvas-sm{transition:none}}@media(max-width: 575.98px){.offcanvas-sm.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-sm.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-sm.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-sm.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-sm.showing,.offcanvas-sm.show:not(.hiding){transform:none}.offcanvas-sm.showing,.offcanvas-sm.hiding,.offcanvas-sm.show{visibility:visible}}@media(min-width: 576px){.offcanvas-sm{--bs-offcanvas-height: auto;--bs-offcanvas-border-width: 0;background-color:rgba(0,0,0,0) !important}.offcanvas-sm .offcanvas-header{display:none}.offcanvas-sm .offcanvas-body{display:flex;display:-webkit-flex;flex-grow:0;-webkit-flex-grow:0;padding:0;overflow-y:visible;background-color:rgba(0,0,0,0) !important}}@media(max-width: 767.98px){.offcanvas-md{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;display:-webkit-flex;flex-direction:column;-webkit-flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media(max-width: 767.98px)and (prefers-reduced-motion: reduce){.offcanvas-md{transition:none}}@media(max-width: 767.98px){.offcanvas-md.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-md.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-md.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-md.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-md.showing,.offcanvas-md.show:not(.hiding){transform:none}.offcanvas-md.showing,.offcanvas-md.hiding,.offcanvas-md.show{visibility:visible}}@media(min-width: 768px){.offcanvas-md{--bs-offcanvas-height: auto;--bs-offcanvas-border-width: 0;background-color:rgba(0,0,0,0) !important}.offcanvas-md .offcanvas-header{display:none}.offcanvas-md .offcanvas-body{display:flex;display:-webkit-flex;flex-grow:0;-webkit-flex-grow:0;padding:0;overflow-y:visible;background-color:rgba(0,0,0,0) !important}}@media(max-width: 991.98px){.offcanvas-lg{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;display:-webkit-flex;flex-direction:column;-webkit-flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media(max-width: 991.98px)and (prefers-reduced-motion: reduce){.offcanvas-lg{transition:none}}@media(max-width: 991.98px){.offcanvas-lg.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-lg.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-lg.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-lg.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-lg.showing,.offcanvas-lg.show:not(.hiding){transform:none}.offcanvas-lg.showing,.offcanvas-lg.hiding,.offcanvas-lg.show{visibility:visible}}@media(min-width: 992px){.offcanvas-lg{--bs-offcanvas-height: auto;--bs-offcanvas-border-width: 0;background-color:rgba(0,0,0,0) !important}.offcanvas-lg .offcanvas-header{display:none}.offcanvas-lg .offcanvas-body{display:flex;display:-webkit-flex;flex-grow:0;-webkit-flex-grow:0;padding:0;overflow-y:visible;background-color:rgba(0,0,0,0) !important}}@media(max-width: 1199.98px){.offcanvas-xl{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;display:-webkit-flex;flex-direction:column;-webkit-flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media(max-width: 1199.98px)and (prefers-reduced-motion: reduce){.offcanvas-xl{transition:none}}@media(max-width: 1199.98px){.offcanvas-xl.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-xl.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-xl.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-xl.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-xl.showing,.offcanvas-xl.show:not(.hiding){transform:none}.offcanvas-xl.showing,.offcanvas-xl.hiding,.offcanvas-xl.show{visibility:visible}}@media(min-width: 1200px){.offcanvas-xl{--bs-offcanvas-height: auto;--bs-offcanvas-border-width: 0;background-color:rgba(0,0,0,0) !important}.offcanvas-xl .offcanvas-header{display:none}.offcanvas-xl .offcanvas-body{display:flex;display:-webkit-flex;flex-grow:0;-webkit-flex-grow:0;padding:0;overflow-y:visible;background-color:rgba(0,0,0,0) !important}}@media(max-width: 1399.98px){.offcanvas-xxl{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;display:-webkit-flex;flex-direction:column;-webkit-flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media(max-width: 1399.98px)and (prefers-reduced-motion: reduce){.offcanvas-xxl{transition:none}}@media(max-width: 1399.98px){.offcanvas-xxl.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-xxl.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-xxl.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-xxl.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-xxl.showing,.offcanvas-xxl.show:not(.hiding){transform:none}.offcanvas-xxl.showing,.offcanvas-xxl.hiding,.offcanvas-xxl.show{visibility:visible}}@media(min-width: 1400px){.offcanvas-xxl{--bs-offcanvas-height: auto;--bs-offcanvas-border-width: 0;background-color:rgba(0,0,0,0) !important}.offcanvas-xxl .offcanvas-header{display:none}.offcanvas-xxl .offcanvas-body{display:flex;display:-webkit-flex;flex-grow:0;-webkit-flex-grow:0;padding:0;overflow-y:visible;background-color:rgba(0,0,0,0) !important}}.offcanvas{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;display:-webkit-flex;flex-direction:column;-webkit-flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}@media(prefers-reduced-motion: reduce){.offcanvas{transition:none}}.offcanvas.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas.showing,.offcanvas.show:not(.hiding){transform:none}.offcanvas.showing,.offcanvas.hiding,.offcanvas.show{visibility:visible}.offcanvas-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.offcanvas-backdrop.fade{opacity:0}.offcanvas-backdrop.show{opacity:.5}.offcanvas-header{display:flex;display:-webkit-flex;align-items:center;-webkit-align-items:center;justify-content:space-between;-webkit-justify-content:space-between;padding:var(--bs-offcanvas-padding-y) var(--bs-offcanvas-padding-x)}.offcanvas-header .btn-close{padding:calc(var(--bs-offcanvas-padding-y)*.5) calc(var(--bs-offcanvas-padding-x)*.5);margin-top:calc(-0.5*var(--bs-offcanvas-padding-y));margin-right:calc(-0.5*var(--bs-offcanvas-padding-x));margin-bottom:calc(-0.5*var(--bs-offcanvas-padding-y))}.offcanvas-title{margin-bottom:0;line-height:var(--bs-offcanvas-title-line-height)}.offcanvas-body{flex-grow:1;-webkit-flex-grow:1;padding:var(--bs-offcanvas-padding-y) var(--bs-offcanvas-padding-x);overflow-y:auto}.placeholder{display:inline-block;min-height:1em;vertical-align:middle;cursor:wait;background-color:currentcolor;opacity:.5}.placeholder.btn::before{display:inline-block;content:""}.placeholder-xs{min-height:.6em}.placeholder-sm{min-height:.8em}.placeholder-lg{min-height:1.2em}.placeholder-glow .placeholder{animation:placeholder-glow 2s ease-in-out infinite}@keyframes placeholder-glow{50%{opacity:.2}}.placeholder-wave{mask-image:linear-gradient(130deg, #000 55%, rgba(0, 0, 0, 0.8) 75%, #000 95%);-webkit-mask-image:linear-gradient(130deg, #000 55%, rgba(0, 0, 0, 0.8) 75%, #000 95%);mask-size:200% 100%;-webkit-mask-size:200% 100%;animation:placeholder-wave 2s linear infinite}@keyframes placeholder-wave{100%{mask-position:-200% 0%;-webkit-mask-position:-200% 0%}}.clearfix::after{display:block;clear:both;content:""}.text-bg-default{color:#fff !important;background-color:RGBA(var(--bs-default-rgb), var(--bs-bg-opacity, 1)) !important}.text-bg-primary{color:#fff !important;background-color:RGBA(var(--bs-primary-rgb), var(--bs-bg-opacity, 1)) !important}.text-bg-secondary{color:#fff !important;background-color:RGBA(var(--bs-secondary-rgb), var(--bs-bg-opacity, 1)) !important}.text-bg-success{color:#fff !important;background-color:RGBA(var(--bs-success-rgb), var(--bs-bg-opacity, 1)) !important}.text-bg-info{color:#fff !important;background-color:RGBA(var(--bs-info-rgb), var(--bs-bg-opacity, 1)) !important}.text-bg-warning{color:#fff !important;background-color:RGBA(var(--bs-warning-rgb), var(--bs-bg-opacity, 1)) !important}.text-bg-danger{color:#fff !important;background-color:RGBA(var(--bs-danger-rgb), var(--bs-bg-opacity, 1)) !important}.text-bg-light{color:#000 !important;background-color:RGBA(var(--bs-light-rgb), var(--bs-bg-opacity, 1)) !important}.text-bg-dark{color:#fff !important;background-color:RGBA(var(--bs-dark-rgb), var(--bs-bg-opacity, 1)) !important}.link-default{color:RGBA(var(--bs-default-rgb), var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(var(--bs-default-rgb), var(--bs-link-underline-opacity, 1)) !important}.link-default:hover,.link-default:focus{color:RGBA(42, 46, 51, var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(42, 46, 51, var(--bs-link-underline-opacity, 1)) !important}.link-primary{color:RGBA(var(--bs-primary-rgb), var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(var(--bs-primary-rgb), var(--bs-link-underline-opacity, 1)) !important}.link-primary:hover,.link-primary:focus{color:RGBA(31, 102, 182, var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(31, 102, 182, var(--bs-link-underline-opacity, 1)) !important}.link-secondary{color:RGBA(var(--bs-secondary-rgb), var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(var(--bs-secondary-rgb), var(--bs-link-underline-opacity, 1)) !important}.link-secondary:hover,.link-secondary:focus{color:RGBA(42, 46, 51, var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(42, 46, 51, var(--bs-link-underline-opacity, 1)) !important}.link-success{color:RGBA(var(--bs-success-rgb), var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(var(--bs-success-rgb), var(--bs-link-underline-opacity, 1)) !important}.link-success:hover,.link-success:focus{color:RGBA(50, 146, 19, var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(50, 146, 19, var(--bs-link-underline-opacity, 1)) !important}.link-info{color:RGBA(var(--bs-info-rgb), var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(var(--bs-info-rgb), var(--bs-link-underline-opacity, 1)) !important}.link-info:hover,.link-info:focus{color:RGBA(122, 67, 150, var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(122, 67, 150, var(--bs-link-underline-opacity, 1)) !important}.link-warning{color:RGBA(var(--bs-warning-rgb), var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(var(--bs-warning-rgb), var(--bs-link-underline-opacity, 1)) !important}.link-warning:hover,.link-warning:focus{color:RGBA(204, 94, 19, var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(204, 94, 19, var(--bs-link-underline-opacity, 1)) !important}.link-danger{color:RGBA(var(--bs-danger-rgb), var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(var(--bs-danger-rgb), var(--bs-link-underline-opacity, 1)) !important}.link-danger:hover,.link-danger:focus{color:RGBA(204, 0, 46, var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(204, 0, 46, var(--bs-link-underline-opacity, 1)) !important}.link-light{color:RGBA(var(--bs-light-rgb), var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(var(--bs-light-rgb), var(--bs-link-underline-opacity, 1)) !important}.link-light:hover,.link-light:focus{color:RGBA(249, 250, 251, var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(249, 250, 251, var(--bs-link-underline-opacity, 1)) !important}.link-dark{color:RGBA(var(--bs-dark-rgb), var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(var(--bs-dark-rgb), var(--bs-link-underline-opacity, 1)) !important}.link-dark:hover,.link-dark:focus{color:RGBA(42, 46, 51, var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(42, 46, 51, var(--bs-link-underline-opacity, 1)) !important}.link-body-emphasis{color:RGBA(var(--bs-emphasis-color-rgb), var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(var(--bs-emphasis-color-rgb), var(--bs-link-underline-opacity, 1)) !important}.link-body-emphasis:hover,.link-body-emphasis:focus{color:RGBA(var(--bs-emphasis-color-rgb), var(--bs-link-opacity, 0.75)) !important;text-decoration-color:RGBA(var(--bs-emphasis-color-rgb), var(--bs-link-underline-opacity, 0.75)) !important}.focus-ring:focus{outline:0;box-shadow:var(--bs-focus-ring-x, 0) var(--bs-focus-ring-y, 0) var(--bs-focus-ring-blur, 0) var(--bs-focus-ring-width) var(--bs-focus-ring-color)}.icon-link{display:inline-flex;gap:.375rem;align-items:center;-webkit-align-items:center;text-decoration-color:rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 0.5));text-underline-offset:.25em;backface-visibility:hidden;-webkit-backface-visibility:hidden;-moz-backface-visibility:hidden;-ms-backface-visibility:hidden;-o-backface-visibility:hidden}.icon-link>.bi{flex-shrink:0;-webkit-flex-shrink:0;width:1em;height:1em;fill:currentcolor;transition:.2s ease-in-out transform}@media(prefers-reduced-motion: reduce){.icon-link>.bi{transition:none}}.icon-link-hover:hover>.bi,.icon-link-hover:focus-visible>.bi{transform:var(--bs-icon-link-transform, translate3d(0.25em, 0, 0))}.ratio{position:relative;width:100%}.ratio::before{display:block;padding-top:var(--bs-aspect-ratio);content:""}.ratio>*{position:absolute;top:0;left:0;width:100%;height:100%}.ratio-1x1{--bs-aspect-ratio: 100%}.ratio-4x3{--bs-aspect-ratio: 75%}.ratio-16x9{--bs-aspect-ratio: 56.25%}.ratio-21x9{--bs-aspect-ratio: 42.8571428571%}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}.sticky-top{position:sticky;top:0;z-index:1020}.sticky-bottom{position:sticky;bottom:0;z-index:1020}@media(min-width: 576px){.sticky-sm-top{position:sticky;top:0;z-index:1020}.sticky-sm-bottom{position:sticky;bottom:0;z-index:1020}}@media(min-width: 768px){.sticky-md-top{position:sticky;top:0;z-index:1020}.sticky-md-bottom{position:sticky;bottom:0;z-index:1020}}@media(min-width: 992px){.sticky-lg-top{position:sticky;top:0;z-index:1020}.sticky-lg-bottom{position:sticky;bottom:0;z-index:1020}}@media(min-width: 1200px){.sticky-xl-top{position:sticky;top:0;z-index:1020}.sticky-xl-bottom{position:sticky;bottom:0;z-index:1020}}@media(min-width: 1400px){.sticky-xxl-top{position:sticky;top:0;z-index:1020}.sticky-xxl-bottom{position:sticky;bottom:0;z-index:1020}}.hstack{display:flex;display:-webkit-flex;flex-direction:row;-webkit-flex-direction:row;align-items:center;-webkit-align-items:center;align-self:stretch;-webkit-align-self:stretch}.vstack{display:flex;display:-webkit-flex;flex:1 1 auto;-webkit-flex:1 1 auto;flex-direction:column;-webkit-flex-direction:column;align-self:stretch;-webkit-align-self:stretch}.visually-hidden,.visually-hidden-focusable:not(:focus):not(:focus-within){width:1px !important;height:1px !important;padding:0 !important;margin:-1px !important;overflow:hidden !important;clip:rect(0, 0, 0, 0) !important;white-space:nowrap !important;border:0 !important}.visually-hidden:not(caption),.visually-hidden-focusable:not(:focus):not(:focus-within):not(caption){position:absolute !important}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:""}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.vr{display:inline-block;align-self:stretch;-webkit-align-self:stretch;width:1px;min-height:1em;background-color:currentcolor;opacity:.25}.align-baseline{vertical-align:baseline !important}.align-top{vertical-align:top !important}.align-middle{vertical-align:middle !important}.align-bottom{vertical-align:bottom !important}.align-text-bottom{vertical-align:text-bottom !important}.align-text-top{vertical-align:text-top !important}.float-start{float:left !important}.float-end{float:right !important}.float-none{float:none !important}.object-fit-contain{object-fit:contain !important}.object-fit-cover{object-fit:cover !important}.object-fit-fill{object-fit:fill !important}.object-fit-scale{object-fit:scale-down !important}.object-fit-none{object-fit:none !important}.opacity-0{opacity:0 !important}.opacity-25{opacity:.25 !important}.opacity-50{opacity:.5 !important}.opacity-75{opacity:.75 !important}.opacity-100{opacity:1 !important}.overflow-auto{overflow:auto !important}.overflow-hidden{overflow:hidden !important}.overflow-visible{overflow:visible !important}.overflow-scroll{overflow:scroll !important}.overflow-x-auto{overflow-x:auto !important}.overflow-x-hidden{overflow-x:hidden !important}.overflow-x-visible{overflow-x:visible !important}.overflow-x-scroll{overflow-x:scroll !important}.overflow-y-auto{overflow-y:auto !important}.overflow-y-hidden{overflow-y:hidden !important}.overflow-y-visible{overflow-y:visible !important}.overflow-y-scroll{overflow-y:scroll !important}.d-inline{display:inline !important}.d-inline-block{display:inline-block !important}.d-block{display:block !important}.d-grid{display:grid !important}.d-inline-grid{display:inline-grid !important}.d-table{display:table !important}.d-table-row{display:table-row !important}.d-table-cell{display:table-cell !important}.d-flex{display:flex !important}.d-inline-flex{display:inline-flex !important}.d-none{display:none !important}.shadow{box-shadow:0 .5rem 1rem rgba(0,0,0,.15) !important}.shadow-sm{box-shadow:0 .125rem .25rem rgba(0,0,0,.075) !important}.shadow-lg{box-shadow:0 1rem 3rem rgba(0,0,0,.175) !important}.shadow-none{box-shadow:none !important}.focus-ring-default{--bs-focus-ring-color: rgba(var(--bs-default-rgb), var(--bs-focus-ring-opacity))}.focus-ring-primary{--bs-focus-ring-color: rgba(var(--bs-primary-rgb), var(--bs-focus-ring-opacity))}.focus-ring-secondary{--bs-focus-ring-color: rgba(var(--bs-secondary-rgb), var(--bs-focus-ring-opacity))}.focus-ring-success{--bs-focus-ring-color: rgba(var(--bs-success-rgb), var(--bs-focus-ring-opacity))}.focus-ring-info{--bs-focus-ring-color: rgba(var(--bs-info-rgb), var(--bs-focus-ring-opacity))}.focus-ring-warning{--bs-focus-ring-color: rgba(var(--bs-warning-rgb), var(--bs-focus-ring-opacity))}.focus-ring-danger{--bs-focus-ring-color: rgba(var(--bs-danger-rgb), var(--bs-focus-ring-opacity))}.focus-ring-light{--bs-focus-ring-color: rgba(var(--bs-light-rgb), var(--bs-focus-ring-opacity))}.focus-ring-dark{--bs-focus-ring-color: rgba(var(--bs-dark-rgb), var(--bs-focus-ring-opacity))}.position-static{position:static !important}.position-relative{position:relative !important}.position-absolute{position:absolute !important}.position-fixed{position:fixed !important}.position-sticky{position:sticky !important}.top-0{top:0 !important}.top-50{top:50% !important}.top-100{top:100% !important}.bottom-0{bottom:0 !important}.bottom-50{bottom:50% !important}.bottom-100{bottom:100% !important}.start-0{left:0 !important}.start-50{left:50% !important}.start-100{left:100% !important}.end-0{right:0 !important}.end-50{right:50% !important}.end-100{right:100% !important}.translate-middle{transform:translate(-50%, -50%) !important}.translate-middle-x{transform:translateX(-50%) !important}.translate-middle-y{transform:translateY(-50%) !important}.border{border:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important}.border-0{border:0 !important}.border-top{border-top:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important}.border-top-0{border-top:0 !important}.border-end{border-right:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important}.border-end-0{border-right:0 !important}.border-bottom{border-bottom:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important}.border-bottom-0{border-bottom:0 !important}.border-start{border-left:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important}.border-start-0{border-left:0 !important}.border-default{--bs-border-opacity: 1;border-color:rgba(var(--bs-default-rgb), var(--bs-border-opacity)) !important}.border-primary{--bs-border-opacity: 1;border-color:rgba(var(--bs-primary-rgb), var(--bs-border-opacity)) !important}.border-secondary{--bs-border-opacity: 1;border-color:rgba(var(--bs-secondary-rgb), var(--bs-border-opacity)) !important}.border-success{--bs-border-opacity: 1;border-color:rgba(var(--bs-success-rgb), var(--bs-border-opacity)) !important}.border-info{--bs-border-opacity: 1;border-color:rgba(var(--bs-info-rgb), var(--bs-border-opacity)) !important}.border-warning{--bs-border-opacity: 1;border-color:rgba(var(--bs-warning-rgb), var(--bs-border-opacity)) !important}.border-danger{--bs-border-opacity: 1;border-color:rgba(var(--bs-danger-rgb), var(--bs-border-opacity)) !important}.border-light{--bs-border-opacity: 1;border-color:rgba(var(--bs-light-rgb), var(--bs-border-opacity)) !important}.border-dark{--bs-border-opacity: 1;border-color:rgba(var(--bs-dark-rgb), var(--bs-border-opacity)) !important}.border-black{--bs-border-opacity: 1;border-color:rgba(var(--bs-black-rgb), var(--bs-border-opacity)) !important}.border-white{--bs-border-opacity: 1;border-color:rgba(var(--bs-white-rgb), var(--bs-border-opacity)) !important}.border-primary-subtle{border-color:var(--bs-primary-border-subtle) !important}.border-secondary-subtle{border-color:var(--bs-secondary-border-subtle) !important}.border-success-subtle{border-color:var(--bs-success-border-subtle) !important}.border-info-subtle{border-color:var(--bs-info-border-subtle) !important}.border-warning-subtle{border-color:var(--bs-warning-border-subtle) !important}.border-danger-subtle{border-color:var(--bs-danger-border-subtle) !important}.border-light-subtle{border-color:var(--bs-light-border-subtle) !important}.border-dark-subtle{border-color:var(--bs-dark-border-subtle) !important}.border-1{border-width:1px !important}.border-2{border-width:2px !important}.border-3{border-width:3px !important}.border-4{border-width:4px !important}.border-5{border-width:5px !important}.border-opacity-10{--bs-border-opacity: 0.1}.border-opacity-25{--bs-border-opacity: 0.25}.border-opacity-50{--bs-border-opacity: 0.5}.border-opacity-75{--bs-border-opacity: 0.75}.border-opacity-100{--bs-border-opacity: 1}.w-25{width:25% !important}.w-50{width:50% !important}.w-75{width:75% !important}.w-100{width:100% !important}.w-auto{width:auto !important}.mw-100{max-width:100% !important}.vw-100{width:100vw !important}.min-vw-100{min-width:100vw !important}.h-25{height:25% !important}.h-50{height:50% !important}.h-75{height:75% !important}.h-100{height:100% !important}.h-auto{height:auto !important}.mh-100{max-height:100% !important}.vh-100{height:100vh !important}.min-vh-100{min-height:100vh !important}.flex-fill{flex:1 1 auto !important}.flex-row{flex-direction:row !important}.flex-column{flex-direction:column !important}.flex-row-reverse{flex-direction:row-reverse !important}.flex-column-reverse{flex-direction:column-reverse !important}.flex-grow-0{flex-grow:0 !important}.flex-grow-1{flex-grow:1 !important}.flex-shrink-0{flex-shrink:0 !important}.flex-shrink-1{flex-shrink:1 !important}.flex-wrap{flex-wrap:wrap !important}.flex-nowrap{flex-wrap:nowrap !important}.flex-wrap-reverse{flex-wrap:wrap-reverse !important}.justify-content-start{justify-content:flex-start !important}.justify-content-end{justify-content:flex-end !important}.justify-content-center{justify-content:center !important}.justify-content-between{justify-content:space-between !important}.justify-content-around{justify-content:space-around !important}.justify-content-evenly{justify-content:space-evenly !important}.align-items-start{align-items:flex-start !important}.align-items-end{align-items:flex-end !important}.align-items-center{align-items:center !important}.align-items-baseline{align-items:baseline !important}.align-items-stretch{align-items:stretch !important}.align-content-start{align-content:flex-start !important}.align-content-end{align-content:flex-end !important}.align-content-center{align-content:center !important}.align-content-between{align-content:space-between !important}.align-content-around{align-content:space-around !important}.align-content-stretch{align-content:stretch !important}.align-self-auto{align-self:auto !important}.align-self-start{align-self:flex-start !important}.align-self-end{align-self:flex-end !important}.align-self-center{align-self:center !important}.align-self-baseline{align-self:baseline !important}.align-self-stretch{align-self:stretch !important}.order-first{order:-1 !important}.order-0{order:0 !important}.order-1{order:1 !important}.order-2{order:2 !important}.order-3{order:3 !important}.order-4{order:4 !important}.order-5{order:5 !important}.order-last{order:6 !important}.m-0{margin:0 !important}.m-1{margin:.25rem !important}.m-2{margin:.5rem !important}.m-3{margin:1rem !important}.m-4{margin:1.5rem !important}.m-5{margin:3rem !important}.m-auto{margin:auto !important}.mx-0{margin-right:0 !important;margin-left:0 !important}.mx-1{margin-right:.25rem !important;margin-left:.25rem !important}.mx-2{margin-right:.5rem !important;margin-left:.5rem !important}.mx-3{margin-right:1rem !important;margin-left:1rem !important}.mx-4{margin-right:1.5rem !important;margin-left:1.5rem !important}.mx-5{margin-right:3rem !important;margin-left:3rem !important}.mx-auto{margin-right:auto !important;margin-left:auto !important}.my-0{margin-top:0 !important;margin-bottom:0 !important}.my-1{margin-top:.25rem !important;margin-bottom:.25rem !important}.my-2{margin-top:.5rem !important;margin-bottom:.5rem !important}.my-3{margin-top:1rem !important;margin-bottom:1rem !important}.my-4{margin-top:1.5rem !important;margin-bottom:1.5rem !important}.my-5{margin-top:3rem !important;margin-bottom:3rem !important}.my-auto{margin-top:auto !important;margin-bottom:auto !important}.mt-0{margin-top:0 !important}.mt-1{margin-top:.25rem !important}.mt-2{margin-top:.5rem !important}.mt-3{margin-top:1rem !important}.mt-4{margin-top:1.5rem !important}.mt-5{margin-top:3rem !important}.mt-auto{margin-top:auto !important}.me-0{margin-right:0 !important}.me-1{margin-right:.25rem !important}.me-2{margin-right:.5rem !important}.me-3{margin-right:1rem !important}.me-4{margin-right:1.5rem !important}.me-5{margin-right:3rem !important}.me-auto{margin-right:auto !important}.mb-0{margin-bottom:0 !important}.mb-1{margin-bottom:.25rem !important}.mb-2{margin-bottom:.5rem !important}.mb-3{margin-bottom:1rem !important}.mb-4{margin-bottom:1.5rem !important}.mb-5{margin-bottom:3rem !important}.mb-auto{margin-bottom:auto !important}.ms-0{margin-left:0 !important}.ms-1{margin-left:.25rem !important}.ms-2{margin-left:.5rem !important}.ms-3{margin-left:1rem !important}.ms-4{margin-left:1.5rem !important}.ms-5{margin-left:3rem !important}.ms-auto{margin-left:auto !important}.p-0{padding:0 !important}.p-1{padding:.25rem !important}.p-2{padding:.5rem !important}.p-3{padding:1rem !important}.p-4{padding:1.5rem !important}.p-5{padding:3rem !important}.px-0{padding-right:0 !important;padding-left:0 !important}.px-1{padding-right:.25rem !important;padding-left:.25rem !important}.px-2{padding-right:.5rem !important;padding-left:.5rem !important}.px-3{padding-right:1rem !important;padding-left:1rem !important}.px-4{padding-right:1.5rem !important;padding-left:1.5rem !important}.px-5{padding-right:3rem !important;padding-left:3rem !important}.py-0{padding-top:0 !important;padding-bottom:0 !important}.py-1{padding-top:.25rem !important;padding-bottom:.25rem !important}.py-2{padding-top:.5rem !important;padding-bottom:.5rem !important}.py-3{padding-top:1rem !important;padding-bottom:1rem !important}.py-4{padding-top:1.5rem !important;padding-bottom:1.5rem !important}.py-5{padding-top:3rem !important;padding-bottom:3rem !important}.pt-0{padding-top:0 !important}.pt-1{padding-top:.25rem !important}.pt-2{padding-top:.5rem !important}.pt-3{padding-top:1rem !important}.pt-4{padding-top:1.5rem !important}.pt-5{padding-top:3rem !important}.pe-0{padding-right:0 !important}.pe-1{padding-right:.25rem !important}.pe-2{padding-right:.5rem !important}.pe-3{padding-right:1rem !important}.pe-4{padding-right:1.5rem !important}.pe-5{padding-right:3rem !important}.pb-0{padding-bottom:0 !important}.pb-1{padding-bottom:.25rem !important}.pb-2{padding-bottom:.5rem !important}.pb-3{padding-bottom:1rem !important}.pb-4{padding-bottom:1.5rem !important}.pb-5{padding-bottom:3rem !important}.ps-0{padding-left:0 !important}.ps-1{padding-left:.25rem !important}.ps-2{padding-left:.5rem !important}.ps-3{padding-left:1rem !important}.ps-4{padding-left:1.5rem !important}.ps-5{padding-left:3rem !important}.gap-0{gap:0 !important}.gap-1{gap:.25rem !important}.gap-2{gap:.5rem !important}.gap-3{gap:1rem !important}.gap-4{gap:1.5rem !important}.gap-5{gap:3rem !important}.row-gap-0{row-gap:0 !important}.row-gap-1{row-gap:.25rem !important}.row-gap-2{row-gap:.5rem !important}.row-gap-3{row-gap:1rem !important}.row-gap-4{row-gap:1.5rem !important}.row-gap-5{row-gap:3rem !important}.column-gap-0{column-gap:0 !important}.column-gap-1{column-gap:.25rem !important}.column-gap-2{column-gap:.5rem !important}.column-gap-3{column-gap:1rem !important}.column-gap-4{column-gap:1.5rem !important}.column-gap-5{column-gap:3rem !important}.font-monospace{font-family:var(--bs-font-monospace) !important}.fs-1{font-size:calc(1.325rem + 0.9vw) !important}.fs-2{font-size:calc(1.29rem + 0.48vw) !important}.fs-3{font-size:calc(1.27rem + 0.24vw) !important}.fs-4{font-size:1.25rem !important}.fs-5{font-size:1.1rem !important}.fs-6{font-size:1rem !important}.fst-italic{font-style:italic !important}.fst-normal{font-style:normal !important}.fw-lighter{font-weight:lighter !important}.fw-light{font-weight:300 !important}.fw-normal{font-weight:400 !important}.fw-medium{font-weight:500 !important}.fw-semibold{font-weight:600 !important}.fw-bold{font-weight:700 !important}.fw-bolder{font-weight:bolder !important}.lh-1{line-height:1 !important}.lh-sm{line-height:1.25 !important}.lh-base{line-height:1.5 !important}.lh-lg{line-height:2 !important}.text-start{text-align:left !important}.text-end{text-align:right !important}.text-center{text-align:center !important}.text-decoration-none{text-decoration:none !important}.text-decoration-underline{text-decoration:underline !important}.text-decoration-line-through{text-decoration:line-through !important}.text-lowercase{text-transform:lowercase !important}.text-uppercase{text-transform:uppercase !important}.text-capitalize{text-transform:capitalize !important}.text-wrap{white-space:normal !important}.text-nowrap{white-space:nowrap !important}.text-break{word-wrap:break-word !important;word-break:break-word !important}.text-default{--bs-text-opacity: 1;color:rgba(var(--bs-default-rgb), var(--bs-text-opacity)) !important}.text-primary{--bs-text-opacity: 1;color:rgba(var(--bs-primary-rgb), var(--bs-text-opacity)) !important}.text-secondary{--bs-text-opacity: 1;color:rgba(var(--bs-secondary-rgb), var(--bs-text-opacity)) !important}.text-success{--bs-text-opacity: 1;color:rgba(var(--bs-success-rgb), var(--bs-text-opacity)) !important}.text-info{--bs-text-opacity: 1;color:rgba(var(--bs-info-rgb), var(--bs-text-opacity)) !important}.text-warning{--bs-text-opacity: 1;color:rgba(var(--bs-warning-rgb), var(--bs-text-opacity)) !important}.text-danger{--bs-text-opacity: 1;color:rgba(var(--bs-danger-rgb), var(--bs-text-opacity)) !important}.text-light{--bs-text-opacity: 1;color:rgba(var(--bs-light-rgb), var(--bs-text-opacity)) !important}.text-dark{--bs-text-opacity: 1;color:rgba(var(--bs-dark-rgb), var(--bs-text-opacity)) !important}.text-black{--bs-text-opacity: 1;color:rgba(var(--bs-black-rgb), var(--bs-text-opacity)) !important}.text-white{--bs-text-opacity: 1;color:rgba(var(--bs-white-rgb), var(--bs-text-opacity)) !important}.text-body{--bs-text-opacity: 1;color:rgba(var(--bs-body-color-rgb), var(--bs-text-opacity)) !important}.text-muted{--bs-text-opacity: 1;color:var(--bs-secondary-color) !important}.text-black-50{--bs-text-opacity: 1;color:rgba(0,0,0,.5) !important}.text-white-50{--bs-text-opacity: 1;color:rgba(255,255,255,.5) !important}.text-body-secondary{--bs-text-opacity: 1;color:var(--bs-secondary-color) !important}.text-body-tertiary{--bs-text-opacity: 1;color:var(--bs-tertiary-color) !important}.text-body-emphasis{--bs-text-opacity: 1;color:var(--bs-emphasis-color) !important}.text-reset{--bs-text-opacity: 1;color:inherit !important}.text-opacity-25{--bs-text-opacity: 0.25}.text-opacity-50{--bs-text-opacity: 0.5}.text-opacity-75{--bs-text-opacity: 0.75}.text-opacity-100{--bs-text-opacity: 1}.text-primary-emphasis{color:var(--bs-primary-text-emphasis) !important}.text-secondary-emphasis{color:var(--bs-secondary-text-emphasis) !important}.text-success-emphasis{color:var(--bs-success-text-emphasis) !important}.text-info-emphasis{color:var(--bs-info-text-emphasis) !important}.text-warning-emphasis{color:var(--bs-warning-text-emphasis) !important}.text-danger-emphasis{color:var(--bs-danger-text-emphasis) !important}.text-light-emphasis{color:var(--bs-light-text-emphasis) !important}.text-dark-emphasis{color:var(--bs-dark-text-emphasis) !important}.link-opacity-10{--bs-link-opacity: 0.1}.link-opacity-10-hover:hover{--bs-link-opacity: 0.1}.link-opacity-25{--bs-link-opacity: 0.25}.link-opacity-25-hover:hover{--bs-link-opacity: 0.25}.link-opacity-50{--bs-link-opacity: 0.5}.link-opacity-50-hover:hover{--bs-link-opacity: 0.5}.link-opacity-75{--bs-link-opacity: 0.75}.link-opacity-75-hover:hover{--bs-link-opacity: 0.75}.link-opacity-100{--bs-link-opacity: 1}.link-opacity-100-hover:hover{--bs-link-opacity: 1}.link-offset-1{text-underline-offset:.125em !important}.link-offset-1-hover:hover{text-underline-offset:.125em !important}.link-offset-2{text-underline-offset:.25em !important}.link-offset-2-hover:hover{text-underline-offset:.25em !important}.link-offset-3{text-underline-offset:.375em !important}.link-offset-3-hover:hover{text-underline-offset:.375em !important}.link-underline-default{--bs-link-underline-opacity: 1;text-decoration-color:rgba(var(--bs-default-rgb), var(--bs-link-underline-opacity)) !important}.link-underline-primary{--bs-link-underline-opacity: 1;text-decoration-color:rgba(var(--bs-primary-rgb), var(--bs-link-underline-opacity)) !important}.link-underline-secondary{--bs-link-underline-opacity: 1;text-decoration-color:rgba(var(--bs-secondary-rgb), var(--bs-link-underline-opacity)) !important}.link-underline-success{--bs-link-underline-opacity: 1;text-decoration-color:rgba(var(--bs-success-rgb), var(--bs-link-underline-opacity)) !important}.link-underline-info{--bs-link-underline-opacity: 1;text-decoration-color:rgba(var(--bs-info-rgb), var(--bs-link-underline-opacity)) !important}.link-underline-warning{--bs-link-underline-opacity: 1;text-decoration-color:rgba(var(--bs-warning-rgb), var(--bs-link-underline-opacity)) !important}.link-underline-danger{--bs-link-underline-opacity: 1;text-decoration-color:rgba(var(--bs-danger-rgb), var(--bs-link-underline-opacity)) !important}.link-underline-light{--bs-link-underline-opacity: 1;text-decoration-color:rgba(var(--bs-light-rgb), var(--bs-link-underline-opacity)) !important}.link-underline-dark{--bs-link-underline-opacity: 1;text-decoration-color:rgba(var(--bs-dark-rgb), var(--bs-link-underline-opacity)) !important}.link-underline{--bs-link-underline-opacity: 1;text-decoration-color:rgba(var(--bs-link-color-rgb), var(--bs-link-underline-opacity, 1)) !important}.link-underline-opacity-0{--bs-link-underline-opacity: 0}.link-underline-opacity-0-hover:hover{--bs-link-underline-opacity: 0}.link-underline-opacity-10{--bs-link-underline-opacity: 0.1}.link-underline-opacity-10-hover:hover{--bs-link-underline-opacity: 0.1}.link-underline-opacity-25{--bs-link-underline-opacity: 0.25}.link-underline-opacity-25-hover:hover{--bs-link-underline-opacity: 0.25}.link-underline-opacity-50{--bs-link-underline-opacity: 0.5}.link-underline-opacity-50-hover:hover{--bs-link-underline-opacity: 0.5}.link-underline-opacity-75{--bs-link-underline-opacity: 0.75}.link-underline-opacity-75-hover:hover{--bs-link-underline-opacity: 0.75}.link-underline-opacity-100{--bs-link-underline-opacity: 1}.link-underline-opacity-100-hover:hover{--bs-link-underline-opacity: 1}.bg-default{--bs-bg-opacity: 1;background-color:rgba(var(--bs-default-rgb), var(--bs-bg-opacity)) !important}.bg-primary{--bs-bg-opacity: 1;background-color:rgba(var(--bs-primary-rgb), var(--bs-bg-opacity)) !important}.bg-secondary{--bs-bg-opacity: 1;background-color:rgba(var(--bs-secondary-rgb), var(--bs-bg-opacity)) !important}.bg-success{--bs-bg-opacity: 1;background-color:rgba(var(--bs-success-rgb), var(--bs-bg-opacity)) !important}.bg-info{--bs-bg-opacity: 1;background-color:rgba(var(--bs-info-rgb), var(--bs-bg-opacity)) !important}.bg-warning{--bs-bg-opacity: 1;background-color:rgba(var(--bs-warning-rgb), var(--bs-bg-opacity)) !important}.bg-danger{--bs-bg-opacity: 1;background-color:rgba(var(--bs-danger-rgb), var(--bs-bg-opacity)) !important}.bg-light{--bs-bg-opacity: 1;background-color:rgba(var(--bs-light-rgb), var(--bs-bg-opacity)) !important}.bg-dark{--bs-bg-opacity: 1;background-color:rgba(var(--bs-dark-rgb), var(--bs-bg-opacity)) !important}.bg-black{--bs-bg-opacity: 1;background-color:rgba(var(--bs-black-rgb), var(--bs-bg-opacity)) !important}.bg-white{--bs-bg-opacity: 1;background-color:rgba(var(--bs-white-rgb), var(--bs-bg-opacity)) !important}.bg-body{--bs-bg-opacity: 1;background-color:rgba(var(--bs-body-bg-rgb), var(--bs-bg-opacity)) !important}.bg-transparent{--bs-bg-opacity: 1;background-color:rgba(0,0,0,0) !important}.bg-body-secondary{--bs-bg-opacity: 1;background-color:rgba(var(--bs-secondary-bg-rgb), var(--bs-bg-opacity)) !important}.bg-body-tertiary{--bs-bg-opacity: 1;background-color:rgba(var(--bs-tertiary-bg-rgb), var(--bs-bg-opacity)) !important}.bg-opacity-10{--bs-bg-opacity: 0.1}.bg-opacity-25{--bs-bg-opacity: 0.25}.bg-opacity-50{--bs-bg-opacity: 0.5}.bg-opacity-75{--bs-bg-opacity: 0.75}.bg-opacity-100{--bs-bg-opacity: 1}.bg-primary-subtle{background-color:var(--bs-primary-bg-subtle) !important}.bg-secondary-subtle{background-color:var(--bs-secondary-bg-subtle) !important}.bg-success-subtle{background-color:var(--bs-success-bg-subtle) !important}.bg-info-subtle{background-color:var(--bs-info-bg-subtle) !important}.bg-warning-subtle{background-color:var(--bs-warning-bg-subtle) !important}.bg-danger-subtle{background-color:var(--bs-danger-bg-subtle) !important}.bg-light-subtle{background-color:var(--bs-light-bg-subtle) !important}.bg-dark-subtle{background-color:var(--bs-dark-bg-subtle) !important}.bg-gradient{background-image:var(--bs-gradient) !important}.user-select-all{user-select:all !important}.user-select-auto{user-select:auto !important}.user-select-none{user-select:none !important}.pe-none{pointer-events:none !important}.pe-auto{pointer-events:auto !important}.rounded{border-radius:var(--bs-border-radius) !important}.rounded-0{border-radius:0 !important}.rounded-1{border-radius:var(--bs-border-radius-sm) !important}.rounded-2{border-radius:var(--bs-border-radius) !important}.rounded-3{border-radius:var(--bs-border-radius-lg) !important}.rounded-4{border-radius:var(--bs-border-radius-xl) !important}.rounded-5{border-radius:var(--bs-border-radius-xxl) !important}.rounded-circle{border-radius:50% !important}.rounded-pill{border-radius:var(--bs-border-radius-pill) !important}.rounded-top{border-top-left-radius:var(--bs-border-radius) !important;border-top-right-radius:var(--bs-border-radius) !important}.rounded-top-0{border-top-left-radius:0 !important;border-top-right-radius:0 !important}.rounded-top-1{border-top-left-radius:var(--bs-border-radius-sm) !important;border-top-right-radius:var(--bs-border-radius-sm) !important}.rounded-top-2{border-top-left-radius:var(--bs-border-radius) !important;border-top-right-radius:var(--bs-border-radius) !important}.rounded-top-3{border-top-left-radius:var(--bs-border-radius-lg) !important;border-top-right-radius:var(--bs-border-radius-lg) !important}.rounded-top-4{border-top-left-radius:var(--bs-border-radius-xl) !important;border-top-right-radius:var(--bs-border-radius-xl) !important}.rounded-top-5{border-top-left-radius:var(--bs-border-radius-xxl) !important;border-top-right-radius:var(--bs-border-radius-xxl) !important}.rounded-top-circle{border-top-left-radius:50% !important;border-top-right-radius:50% !important}.rounded-top-pill{border-top-left-radius:var(--bs-border-radius-pill) !important;border-top-right-radius:var(--bs-border-radius-pill) !important}.rounded-end{border-top-right-radius:var(--bs-border-radius) !important;border-bottom-right-radius:var(--bs-border-radius) !important}.rounded-end-0{border-top-right-radius:0 !important;border-bottom-right-radius:0 !important}.rounded-end-1{border-top-right-radius:var(--bs-border-radius-sm) !important;border-bottom-right-radius:var(--bs-border-radius-sm) !important}.rounded-end-2{border-top-right-radius:var(--bs-border-radius) !important;border-bottom-right-radius:var(--bs-border-radius) !important}.rounded-end-3{border-top-right-radius:var(--bs-border-radius-lg) !important;border-bottom-right-radius:var(--bs-border-radius-lg) !important}.rounded-end-4{border-top-right-radius:var(--bs-border-radius-xl) !important;border-bottom-right-radius:var(--bs-border-radius-xl) !important}.rounded-end-5{border-top-right-radius:var(--bs-border-radius-xxl) !important;border-bottom-right-radius:var(--bs-border-radius-xxl) !important}.rounded-end-circle{border-top-right-radius:50% !important;border-bottom-right-radius:50% !important}.rounded-end-pill{border-top-right-radius:var(--bs-border-radius-pill) !important;border-bottom-right-radius:var(--bs-border-radius-pill) !important}.rounded-bottom{border-bottom-right-radius:var(--bs-border-radius) !important;border-bottom-left-radius:var(--bs-border-radius) !important}.rounded-bottom-0{border-bottom-right-radius:0 !important;border-bottom-left-radius:0 !important}.rounded-bottom-1{border-bottom-right-radius:var(--bs-border-radius-sm) !important;border-bottom-left-radius:var(--bs-border-radius-sm) !important}.rounded-bottom-2{border-bottom-right-radius:var(--bs-border-radius) !important;border-bottom-left-radius:var(--bs-border-radius) !important}.rounded-bottom-3{border-bottom-right-radius:var(--bs-border-radius-lg) !important;border-bottom-left-radius:var(--bs-border-radius-lg) !important}.rounded-bottom-4{border-bottom-right-radius:var(--bs-border-radius-xl) !important;border-bottom-left-radius:var(--bs-border-radius-xl) !important}.rounded-bottom-5{border-bottom-right-radius:var(--bs-border-radius-xxl) !important;border-bottom-left-radius:var(--bs-border-radius-xxl) !important}.rounded-bottom-circle{border-bottom-right-radius:50% !important;border-bottom-left-radius:50% !important}.rounded-bottom-pill{border-bottom-right-radius:var(--bs-border-radius-pill) !important;border-bottom-left-radius:var(--bs-border-radius-pill) !important}.rounded-start{border-bottom-left-radius:var(--bs-border-radius) !important;border-top-left-radius:var(--bs-border-radius) !important}.rounded-start-0{border-bottom-left-radius:0 !important;border-top-left-radius:0 !important}.rounded-start-1{border-bottom-left-radius:var(--bs-border-radius-sm) !important;border-top-left-radius:var(--bs-border-radius-sm) !important}.rounded-start-2{border-bottom-left-radius:var(--bs-border-radius) !important;border-top-left-radius:var(--bs-border-radius) !important}.rounded-start-3{border-bottom-left-radius:var(--bs-border-radius-lg) !important;border-top-left-radius:var(--bs-border-radius-lg) !important}.rounded-start-4{border-bottom-left-radius:var(--bs-border-radius-xl) !important;border-top-left-radius:var(--bs-border-radius-xl) !important}.rounded-start-5{border-bottom-left-radius:var(--bs-border-radius-xxl) !important;border-top-left-radius:var(--bs-border-radius-xxl) !important}.rounded-start-circle{border-bottom-left-radius:50% !important;border-top-left-radius:50% !important}.rounded-start-pill{border-bottom-left-radius:var(--bs-border-radius-pill) !important;border-top-left-radius:var(--bs-border-radius-pill) !important}.visible{visibility:visible !important}.invisible{visibility:hidden !important}.z-n1{z-index:-1 !important}.z-0{z-index:0 !important}.z-1{z-index:1 !important}.z-2{z-index:2 !important}.z-3{z-index:3 !important}@media(min-width: 576px){.float-sm-start{float:left !important}.float-sm-end{float:right !important}.float-sm-none{float:none !important}.object-fit-sm-contain{object-fit:contain !important}.object-fit-sm-cover{object-fit:cover !important}.object-fit-sm-fill{object-fit:fill !important}.object-fit-sm-scale{object-fit:scale-down !important}.object-fit-sm-none{object-fit:none !important}.d-sm-inline{display:inline !important}.d-sm-inline-block{display:inline-block !important}.d-sm-block{display:block !important}.d-sm-grid{display:grid !important}.d-sm-inline-grid{display:inline-grid !important}.d-sm-table{display:table !important}.d-sm-table-row{display:table-row !important}.d-sm-table-cell{display:table-cell !important}.d-sm-flex{display:flex !important}.d-sm-inline-flex{display:inline-flex !important}.d-sm-none{display:none !important}.flex-sm-fill{flex:1 1 auto !important}.flex-sm-row{flex-direction:row !important}.flex-sm-column{flex-direction:column !important}.flex-sm-row-reverse{flex-direction:row-reverse !important}.flex-sm-column-reverse{flex-direction:column-reverse !important}.flex-sm-grow-0{flex-grow:0 !important}.flex-sm-grow-1{flex-grow:1 !important}.flex-sm-shrink-0{flex-shrink:0 !important}.flex-sm-shrink-1{flex-shrink:1 !important}.flex-sm-wrap{flex-wrap:wrap !important}.flex-sm-nowrap{flex-wrap:nowrap !important}.flex-sm-wrap-reverse{flex-wrap:wrap-reverse !important}.justify-content-sm-start{justify-content:flex-start !important}.justify-content-sm-end{justify-content:flex-end !important}.justify-content-sm-center{justify-content:center !important}.justify-content-sm-between{justify-content:space-between !important}.justify-content-sm-around{justify-content:space-around !important}.justify-content-sm-evenly{justify-content:space-evenly !important}.align-items-sm-start{align-items:flex-start !important}.align-items-sm-end{align-items:flex-end !important}.align-items-sm-center{align-items:center !important}.align-items-sm-baseline{align-items:baseline !important}.align-items-sm-stretch{align-items:stretch !important}.align-content-sm-start{align-content:flex-start !important}.align-content-sm-end{align-content:flex-end !important}.align-content-sm-center{align-content:center !important}.align-content-sm-between{align-content:space-between !important}.align-content-sm-around{align-content:space-around !important}.align-content-sm-stretch{align-content:stretch !important}.align-self-sm-auto{align-self:auto !important}.align-self-sm-start{align-self:flex-start !important}.align-self-sm-end{align-self:flex-end !important}.align-self-sm-center{align-self:center !important}.align-self-sm-baseline{align-self:baseline !important}.align-self-sm-stretch{align-self:stretch !important}.order-sm-first{order:-1 !important}.order-sm-0{order:0 !important}.order-sm-1{order:1 !important}.order-sm-2{order:2 !important}.order-sm-3{order:3 !important}.order-sm-4{order:4 !important}.order-sm-5{order:5 !important}.order-sm-last{order:6 !important}.m-sm-0{margin:0 !important}.m-sm-1{margin:.25rem !important}.m-sm-2{margin:.5rem !important}.m-sm-3{margin:1rem !important}.m-sm-4{margin:1.5rem !important}.m-sm-5{margin:3rem !important}.m-sm-auto{margin:auto !important}.mx-sm-0{margin-right:0 !important;margin-left:0 !important}.mx-sm-1{margin-right:.25rem !important;margin-left:.25rem !important}.mx-sm-2{margin-right:.5rem !important;margin-left:.5rem !important}.mx-sm-3{margin-right:1rem !important;margin-left:1rem !important}.mx-sm-4{margin-right:1.5rem !important;margin-left:1.5rem !important}.mx-sm-5{margin-right:3rem !important;margin-left:3rem !important}.mx-sm-auto{margin-right:auto !important;margin-left:auto !important}.my-sm-0{margin-top:0 !important;margin-bottom:0 !important}.my-sm-1{margin-top:.25rem !important;margin-bottom:.25rem !important}.my-sm-2{margin-top:.5rem !important;margin-bottom:.5rem !important}.my-sm-3{margin-top:1rem !important;margin-bottom:1rem !important}.my-sm-4{margin-top:1.5rem !important;margin-bottom:1.5rem !important}.my-sm-5{margin-top:3rem !important;margin-bottom:3rem !important}.my-sm-auto{margin-top:auto !important;margin-bottom:auto !important}.mt-sm-0{margin-top:0 !important}.mt-sm-1{margin-top:.25rem !important}.mt-sm-2{margin-top:.5rem !important}.mt-sm-3{margin-top:1rem !important}.mt-sm-4{margin-top:1.5rem !important}.mt-sm-5{margin-top:3rem !important}.mt-sm-auto{margin-top:auto !important}.me-sm-0{margin-right:0 !important}.me-sm-1{margin-right:.25rem !important}.me-sm-2{margin-right:.5rem !important}.me-sm-3{margin-right:1rem !important}.me-sm-4{margin-right:1.5rem !important}.me-sm-5{margin-right:3rem !important}.me-sm-auto{margin-right:auto !important}.mb-sm-0{margin-bottom:0 !important}.mb-sm-1{margin-bottom:.25rem !important}.mb-sm-2{margin-bottom:.5rem !important}.mb-sm-3{margin-bottom:1rem !important}.mb-sm-4{margin-bottom:1.5rem !important}.mb-sm-5{margin-bottom:3rem !important}.mb-sm-auto{margin-bottom:auto !important}.ms-sm-0{margin-left:0 !important}.ms-sm-1{margin-left:.25rem !important}.ms-sm-2{margin-left:.5rem !important}.ms-sm-3{margin-left:1rem !important}.ms-sm-4{margin-left:1.5rem !important}.ms-sm-5{margin-left:3rem !important}.ms-sm-auto{margin-left:auto !important}.p-sm-0{padding:0 !important}.p-sm-1{padding:.25rem !important}.p-sm-2{padding:.5rem !important}.p-sm-3{padding:1rem !important}.p-sm-4{padding:1.5rem !important}.p-sm-5{padding:3rem !important}.px-sm-0{padding-right:0 !important;padding-left:0 !important}.px-sm-1{padding-right:.25rem !important;padding-left:.25rem !important}.px-sm-2{padding-right:.5rem !important;padding-left:.5rem !important}.px-sm-3{padding-right:1rem !important;padding-left:1rem !important}.px-sm-4{padding-right:1.5rem !important;padding-left:1.5rem !important}.px-sm-5{padding-right:3rem !important;padding-left:3rem !important}.py-sm-0{padding-top:0 !important;padding-bottom:0 !important}.py-sm-1{padding-top:.25rem !important;padding-bottom:.25rem !important}.py-sm-2{padding-top:.5rem !important;padding-bottom:.5rem !important}.py-sm-3{padding-top:1rem !important;padding-bottom:1rem !important}.py-sm-4{padding-top:1.5rem !important;padding-bottom:1.5rem !important}.py-sm-5{padding-top:3rem !important;padding-bottom:3rem !important}.pt-sm-0{padding-top:0 !important}.pt-sm-1{padding-top:.25rem !important}.pt-sm-2{padding-top:.5rem !important}.pt-sm-3{padding-top:1rem !important}.pt-sm-4{padding-top:1.5rem !important}.pt-sm-5{padding-top:3rem !important}.pe-sm-0{padding-right:0 !important}.pe-sm-1{padding-right:.25rem !important}.pe-sm-2{padding-right:.5rem !important}.pe-sm-3{padding-right:1rem !important}.pe-sm-4{padding-right:1.5rem !important}.pe-sm-5{padding-right:3rem !important}.pb-sm-0{padding-bottom:0 !important}.pb-sm-1{padding-bottom:.25rem !important}.pb-sm-2{padding-bottom:.5rem !important}.pb-sm-3{padding-bottom:1rem !important}.pb-sm-4{padding-bottom:1.5rem !important}.pb-sm-5{padding-bottom:3rem !important}.ps-sm-0{padding-left:0 !important}.ps-sm-1{padding-left:.25rem !important}.ps-sm-2{padding-left:.5rem !important}.ps-sm-3{padding-left:1rem !important}.ps-sm-4{padding-left:1.5rem !important}.ps-sm-5{padding-left:3rem !important}.gap-sm-0{gap:0 !important}.gap-sm-1{gap:.25rem !important}.gap-sm-2{gap:.5rem !important}.gap-sm-3{gap:1rem !important}.gap-sm-4{gap:1.5rem !important}.gap-sm-5{gap:3rem !important}.row-gap-sm-0{row-gap:0 !important}.row-gap-sm-1{row-gap:.25rem !important}.row-gap-sm-2{row-gap:.5rem !important}.row-gap-sm-3{row-gap:1rem !important}.row-gap-sm-4{row-gap:1.5rem !important}.row-gap-sm-5{row-gap:3rem !important}.column-gap-sm-0{column-gap:0 !important}.column-gap-sm-1{column-gap:.25rem !important}.column-gap-sm-2{column-gap:.5rem !important}.column-gap-sm-3{column-gap:1rem !important}.column-gap-sm-4{column-gap:1.5rem !important}.column-gap-sm-5{column-gap:3rem !important}.text-sm-start{text-align:left !important}.text-sm-end{text-align:right !important}.text-sm-center{text-align:center !important}}@media(min-width: 768px){.float-md-start{float:left !important}.float-md-end{float:right !important}.float-md-none{float:none !important}.object-fit-md-contain{object-fit:contain !important}.object-fit-md-cover{object-fit:cover !important}.object-fit-md-fill{object-fit:fill !important}.object-fit-md-scale{object-fit:scale-down !important}.object-fit-md-none{object-fit:none !important}.d-md-inline{display:inline !important}.d-md-inline-block{display:inline-block !important}.d-md-block{display:block !important}.d-md-grid{display:grid !important}.d-md-inline-grid{display:inline-grid !important}.d-md-table{display:table !important}.d-md-table-row{display:table-row !important}.d-md-table-cell{display:table-cell !important}.d-md-flex{display:flex !important}.d-md-inline-flex{display:inline-flex !important}.d-md-none{display:none !important}.flex-md-fill{flex:1 1 auto !important}.flex-md-row{flex-direction:row !important}.flex-md-column{flex-direction:column !important}.flex-md-row-reverse{flex-direction:row-reverse !important}.flex-md-column-reverse{flex-direction:column-reverse !important}.flex-md-grow-0{flex-grow:0 !important}.flex-md-grow-1{flex-grow:1 !important}.flex-md-shrink-0{flex-shrink:0 !important}.flex-md-shrink-1{flex-shrink:1 !important}.flex-md-wrap{flex-wrap:wrap !important}.flex-md-nowrap{flex-wrap:nowrap !important}.flex-md-wrap-reverse{flex-wrap:wrap-reverse !important}.justify-content-md-start{justify-content:flex-start !important}.justify-content-md-end{justify-content:flex-end !important}.justify-content-md-center{justify-content:center !important}.justify-content-md-between{justify-content:space-between !important}.justify-content-md-around{justify-content:space-around !important}.justify-content-md-evenly{justify-content:space-evenly !important}.align-items-md-start{align-items:flex-start !important}.align-items-md-end{align-items:flex-end !important}.align-items-md-center{align-items:center !important}.align-items-md-baseline{align-items:baseline !important}.align-items-md-stretch{align-items:stretch !important}.align-content-md-start{align-content:flex-start !important}.align-content-md-end{align-content:flex-end !important}.align-content-md-center{align-content:center !important}.align-content-md-between{align-content:space-between !important}.align-content-md-around{align-content:space-around !important}.align-content-md-stretch{align-content:stretch !important}.align-self-md-auto{align-self:auto !important}.align-self-md-start{align-self:flex-start !important}.align-self-md-end{align-self:flex-end !important}.align-self-md-center{align-self:center !important}.align-self-md-baseline{align-self:baseline !important}.align-self-md-stretch{align-self:stretch !important}.order-md-first{order:-1 !important}.order-md-0{order:0 !important}.order-md-1{order:1 !important}.order-md-2{order:2 !important}.order-md-3{order:3 !important}.order-md-4{order:4 !important}.order-md-5{order:5 !important}.order-md-last{order:6 !important}.m-md-0{margin:0 !important}.m-md-1{margin:.25rem !important}.m-md-2{margin:.5rem !important}.m-md-3{margin:1rem !important}.m-md-4{margin:1.5rem !important}.m-md-5{margin:3rem !important}.m-md-auto{margin:auto !important}.mx-md-0{margin-right:0 !important;margin-left:0 !important}.mx-md-1{margin-right:.25rem !important;margin-left:.25rem !important}.mx-md-2{margin-right:.5rem !important;margin-left:.5rem !important}.mx-md-3{margin-right:1rem !important;margin-left:1rem !important}.mx-md-4{margin-right:1.5rem !important;margin-left:1.5rem !important}.mx-md-5{margin-right:3rem !important;margin-left:3rem !important}.mx-md-auto{margin-right:auto !important;margin-left:auto !important}.my-md-0{margin-top:0 !important;margin-bottom:0 !important}.my-md-1{margin-top:.25rem !important;margin-bottom:.25rem !important}.my-md-2{margin-top:.5rem !important;margin-bottom:.5rem !important}.my-md-3{margin-top:1rem !important;margin-bottom:1rem !important}.my-md-4{margin-top:1.5rem !important;margin-bottom:1.5rem !important}.my-md-5{margin-top:3rem !important;margin-bottom:3rem !important}.my-md-auto{margin-top:auto !important;margin-bottom:auto !important}.mt-md-0{margin-top:0 !important}.mt-md-1{margin-top:.25rem !important}.mt-md-2{margin-top:.5rem !important}.mt-md-3{margin-top:1rem !important}.mt-md-4{margin-top:1.5rem !important}.mt-md-5{margin-top:3rem !important}.mt-md-auto{margin-top:auto !important}.me-md-0{margin-right:0 !important}.me-md-1{margin-right:.25rem !important}.me-md-2{margin-right:.5rem !important}.me-md-3{margin-right:1rem !important}.me-md-4{margin-right:1.5rem !important}.me-md-5{margin-right:3rem !important}.me-md-auto{margin-right:auto !important}.mb-md-0{margin-bottom:0 !important}.mb-md-1{margin-bottom:.25rem !important}.mb-md-2{margin-bottom:.5rem !important}.mb-md-3{margin-bottom:1rem !important}.mb-md-4{margin-bottom:1.5rem !important}.mb-md-5{margin-bottom:3rem !important}.mb-md-auto{margin-bottom:auto !important}.ms-md-0{margin-left:0 !important}.ms-md-1{margin-left:.25rem !important}.ms-md-2{margin-left:.5rem !important}.ms-md-3{margin-left:1rem !important}.ms-md-4{margin-left:1.5rem !important}.ms-md-5{margin-left:3rem !important}.ms-md-auto{margin-left:auto !important}.p-md-0{padding:0 !important}.p-md-1{padding:.25rem !important}.p-md-2{padding:.5rem !important}.p-md-3{padding:1rem !important}.p-md-4{padding:1.5rem !important}.p-md-5{padding:3rem !important}.px-md-0{padding-right:0 !important;padding-left:0 !important}.px-md-1{padding-right:.25rem !important;padding-left:.25rem !important}.px-md-2{padding-right:.5rem !important;padding-left:.5rem !important}.px-md-3{padding-right:1rem !important;padding-left:1rem !important}.px-md-4{padding-right:1.5rem !important;padding-left:1.5rem !important}.px-md-5{padding-right:3rem !important;padding-left:3rem !important}.py-md-0{padding-top:0 !important;padding-bottom:0 !important}.py-md-1{padding-top:.25rem !important;padding-bottom:.25rem !important}.py-md-2{padding-top:.5rem !important;padding-bottom:.5rem !important}.py-md-3{padding-top:1rem !important;padding-bottom:1rem !important}.py-md-4{padding-top:1.5rem !important;padding-bottom:1.5rem !important}.py-md-5{padding-top:3rem !important;padding-bottom:3rem !important}.pt-md-0{padding-top:0 !important}.pt-md-1{padding-top:.25rem !important}.pt-md-2{padding-top:.5rem !important}.pt-md-3{padding-top:1rem !important}.pt-md-4{padding-top:1.5rem !important}.pt-md-5{padding-top:3rem !important}.pe-md-0{padding-right:0 !important}.pe-md-1{padding-right:.25rem !important}.pe-md-2{padding-right:.5rem !important}.pe-md-3{padding-right:1rem !important}.pe-md-4{padding-right:1.5rem !important}.pe-md-5{padding-right:3rem !important}.pb-md-0{padding-bottom:0 !important}.pb-md-1{padding-bottom:.25rem !important}.pb-md-2{padding-bottom:.5rem !important}.pb-md-3{padding-bottom:1rem !important}.pb-md-4{padding-bottom:1.5rem !important}.pb-md-5{padding-bottom:3rem !important}.ps-md-0{padding-left:0 !important}.ps-md-1{padding-left:.25rem !important}.ps-md-2{padding-left:.5rem !important}.ps-md-3{padding-left:1rem !important}.ps-md-4{padding-left:1.5rem !important}.ps-md-5{padding-left:3rem !important}.gap-md-0{gap:0 !important}.gap-md-1{gap:.25rem !important}.gap-md-2{gap:.5rem !important}.gap-md-3{gap:1rem !important}.gap-md-4{gap:1.5rem !important}.gap-md-5{gap:3rem !important}.row-gap-md-0{row-gap:0 !important}.row-gap-md-1{row-gap:.25rem !important}.row-gap-md-2{row-gap:.5rem !important}.row-gap-md-3{row-gap:1rem !important}.row-gap-md-4{row-gap:1.5rem !important}.row-gap-md-5{row-gap:3rem !important}.column-gap-md-0{column-gap:0 !important}.column-gap-md-1{column-gap:.25rem !important}.column-gap-md-2{column-gap:.5rem !important}.column-gap-md-3{column-gap:1rem !important}.column-gap-md-4{column-gap:1.5rem !important}.column-gap-md-5{column-gap:3rem !important}.text-md-start{text-align:left !important}.text-md-end{text-align:right !important}.text-md-center{text-align:center !important}}@media(min-width: 992px){.float-lg-start{float:left !important}.float-lg-end{float:right !important}.float-lg-none{float:none !important}.object-fit-lg-contain{object-fit:contain !important}.object-fit-lg-cover{object-fit:cover !important}.object-fit-lg-fill{object-fit:fill !important}.object-fit-lg-scale{object-fit:scale-down !important}.object-fit-lg-none{object-fit:none !important}.d-lg-inline{display:inline !important}.d-lg-inline-block{display:inline-block !important}.d-lg-block{display:block !important}.d-lg-grid{display:grid !important}.d-lg-inline-grid{display:inline-grid !important}.d-lg-table{display:table !important}.d-lg-table-row{display:table-row !important}.d-lg-table-cell{display:table-cell !important}.d-lg-flex{display:flex !important}.d-lg-inline-flex{display:inline-flex !important}.d-lg-none{display:none !important}.flex-lg-fill{flex:1 1 auto !important}.flex-lg-row{flex-direction:row !important}.flex-lg-column{flex-direction:column !important}.flex-lg-row-reverse{flex-direction:row-reverse !important}.flex-lg-column-reverse{flex-direction:column-reverse !important}.flex-lg-grow-0{flex-grow:0 !important}.flex-lg-grow-1{flex-grow:1 !important}.flex-lg-shrink-0{flex-shrink:0 !important}.flex-lg-shrink-1{flex-shrink:1 !important}.flex-lg-wrap{flex-wrap:wrap !important}.flex-lg-nowrap{flex-wrap:nowrap !important}.flex-lg-wrap-reverse{flex-wrap:wrap-reverse !important}.justify-content-lg-start{justify-content:flex-start !important}.justify-content-lg-end{justify-content:flex-end !important}.justify-content-lg-center{justify-content:center !important}.justify-content-lg-between{justify-content:space-between !important}.justify-content-lg-around{justify-content:space-around !important}.justify-content-lg-evenly{justify-content:space-evenly !important}.align-items-lg-start{align-items:flex-start !important}.align-items-lg-end{align-items:flex-end !important}.align-items-lg-center{align-items:center !important}.align-items-lg-baseline{align-items:baseline !important}.align-items-lg-stretch{align-items:stretch !important}.align-content-lg-start{align-content:flex-start !important}.align-content-lg-end{align-content:flex-end !important}.align-content-lg-center{align-content:center !important}.align-content-lg-between{align-content:space-between !important}.align-content-lg-around{align-content:space-around !important}.align-content-lg-stretch{align-content:stretch !important}.align-self-lg-auto{align-self:auto !important}.align-self-lg-start{align-self:flex-start !important}.align-self-lg-end{align-self:flex-end !important}.align-self-lg-center{align-self:center !important}.align-self-lg-baseline{align-self:baseline !important}.align-self-lg-stretch{align-self:stretch !important}.order-lg-first{order:-1 !important}.order-lg-0{order:0 !important}.order-lg-1{order:1 !important}.order-lg-2{order:2 !important}.order-lg-3{order:3 !important}.order-lg-4{order:4 !important}.order-lg-5{order:5 !important}.order-lg-last{order:6 !important}.m-lg-0{margin:0 !important}.m-lg-1{margin:.25rem !important}.m-lg-2{margin:.5rem !important}.m-lg-3{margin:1rem !important}.m-lg-4{margin:1.5rem !important}.m-lg-5{margin:3rem !important}.m-lg-auto{margin:auto !important}.mx-lg-0{margin-right:0 !important;margin-left:0 !important}.mx-lg-1{margin-right:.25rem !important;margin-left:.25rem !important}.mx-lg-2{margin-right:.5rem !important;margin-left:.5rem !important}.mx-lg-3{margin-right:1rem !important;margin-left:1rem !important}.mx-lg-4{margin-right:1.5rem !important;margin-left:1.5rem !important}.mx-lg-5{margin-right:3rem !important;margin-left:3rem !important}.mx-lg-auto{margin-right:auto !important;margin-left:auto !important}.my-lg-0{margin-top:0 !important;margin-bottom:0 !important}.my-lg-1{margin-top:.25rem !important;margin-bottom:.25rem !important}.my-lg-2{margin-top:.5rem !important;margin-bottom:.5rem !important}.my-lg-3{margin-top:1rem !important;margin-bottom:1rem !important}.my-lg-4{margin-top:1.5rem !important;margin-bottom:1.5rem !important}.my-lg-5{margin-top:3rem !important;margin-bottom:3rem !important}.my-lg-auto{margin-top:auto !important;margin-bottom:auto !important}.mt-lg-0{margin-top:0 !important}.mt-lg-1{margin-top:.25rem !important}.mt-lg-2{margin-top:.5rem !important}.mt-lg-3{margin-top:1rem !important}.mt-lg-4{margin-top:1.5rem !important}.mt-lg-5{margin-top:3rem !important}.mt-lg-auto{margin-top:auto !important}.me-lg-0{margin-right:0 !important}.me-lg-1{margin-right:.25rem !important}.me-lg-2{margin-right:.5rem !important}.me-lg-3{margin-right:1rem !important}.me-lg-4{margin-right:1.5rem !important}.me-lg-5{margin-right:3rem !important}.me-lg-auto{margin-right:auto !important}.mb-lg-0{margin-bottom:0 !important}.mb-lg-1{margin-bottom:.25rem !important}.mb-lg-2{margin-bottom:.5rem !important}.mb-lg-3{margin-bottom:1rem !important}.mb-lg-4{margin-bottom:1.5rem !important}.mb-lg-5{margin-bottom:3rem !important}.mb-lg-auto{margin-bottom:auto !important}.ms-lg-0{margin-left:0 !important}.ms-lg-1{margin-left:.25rem !important}.ms-lg-2{margin-left:.5rem !important}.ms-lg-3{margin-left:1rem !important}.ms-lg-4{margin-left:1.5rem !important}.ms-lg-5{margin-left:3rem !important}.ms-lg-auto{margin-left:auto !important}.p-lg-0{padding:0 !important}.p-lg-1{padding:.25rem !important}.p-lg-2{padding:.5rem !important}.p-lg-3{padding:1rem !important}.p-lg-4{padding:1.5rem !important}.p-lg-5{padding:3rem !important}.px-lg-0{padding-right:0 !important;padding-left:0 !important}.px-lg-1{padding-right:.25rem !important;padding-left:.25rem !important}.px-lg-2{padding-right:.5rem !important;padding-left:.5rem !important}.px-lg-3{padding-right:1rem !important;padding-left:1rem !important}.px-lg-4{padding-right:1.5rem !important;padding-left:1.5rem !important}.px-lg-5{padding-right:3rem !important;padding-left:3rem !important}.py-lg-0{padding-top:0 !important;padding-bottom:0 !important}.py-lg-1{padding-top:.25rem !important;padding-bottom:.25rem !important}.py-lg-2{padding-top:.5rem !important;padding-bottom:.5rem !important}.py-lg-3{padding-top:1rem !important;padding-bottom:1rem !important}.py-lg-4{padding-top:1.5rem !important;padding-bottom:1.5rem !important}.py-lg-5{padding-top:3rem !important;padding-bottom:3rem !important}.pt-lg-0{padding-top:0 !important}.pt-lg-1{padding-top:.25rem !important}.pt-lg-2{padding-top:.5rem !important}.pt-lg-3{padding-top:1rem !important}.pt-lg-4{padding-top:1.5rem !important}.pt-lg-5{padding-top:3rem !important}.pe-lg-0{padding-right:0 !important}.pe-lg-1{padding-right:.25rem !important}.pe-lg-2{padding-right:.5rem !important}.pe-lg-3{padding-right:1rem !important}.pe-lg-4{padding-right:1.5rem !important}.pe-lg-5{padding-right:3rem !important}.pb-lg-0{padding-bottom:0 !important}.pb-lg-1{padding-bottom:.25rem !important}.pb-lg-2{padding-bottom:.5rem !important}.pb-lg-3{padding-bottom:1rem !important}.pb-lg-4{padding-bottom:1.5rem !important}.pb-lg-5{padding-bottom:3rem !important}.ps-lg-0{padding-left:0 !important}.ps-lg-1{padding-left:.25rem !important}.ps-lg-2{padding-left:.5rem !important}.ps-lg-3{padding-left:1rem !important}.ps-lg-4{padding-left:1.5rem !important}.ps-lg-5{padding-left:3rem !important}.gap-lg-0{gap:0 !important}.gap-lg-1{gap:.25rem !important}.gap-lg-2{gap:.5rem !important}.gap-lg-3{gap:1rem !important}.gap-lg-4{gap:1.5rem !important}.gap-lg-5{gap:3rem !important}.row-gap-lg-0{row-gap:0 !important}.row-gap-lg-1{row-gap:.25rem !important}.row-gap-lg-2{row-gap:.5rem !important}.row-gap-lg-3{row-gap:1rem !important}.row-gap-lg-4{row-gap:1.5rem !important}.row-gap-lg-5{row-gap:3rem !important}.column-gap-lg-0{column-gap:0 !important}.column-gap-lg-1{column-gap:.25rem !important}.column-gap-lg-2{column-gap:.5rem !important}.column-gap-lg-3{column-gap:1rem !important}.column-gap-lg-4{column-gap:1.5rem !important}.column-gap-lg-5{column-gap:3rem !important}.text-lg-start{text-align:left !important}.text-lg-end{text-align:right !important}.text-lg-center{text-align:center !important}}@media(min-width: 1200px){.float-xl-start{float:left !important}.float-xl-end{float:right !important}.float-xl-none{float:none !important}.object-fit-xl-contain{object-fit:contain !important}.object-fit-xl-cover{object-fit:cover !important}.object-fit-xl-fill{object-fit:fill !important}.object-fit-xl-scale{object-fit:scale-down !important}.object-fit-xl-none{object-fit:none !important}.d-xl-inline{display:inline !important}.d-xl-inline-block{display:inline-block !important}.d-xl-block{display:block !important}.d-xl-grid{display:grid !important}.d-xl-inline-grid{display:inline-grid !important}.d-xl-table{display:table !important}.d-xl-table-row{display:table-row !important}.d-xl-table-cell{display:table-cell !important}.d-xl-flex{display:flex !important}.d-xl-inline-flex{display:inline-flex !important}.d-xl-none{display:none !important}.flex-xl-fill{flex:1 1 auto !important}.flex-xl-row{flex-direction:row !important}.flex-xl-column{flex-direction:column !important}.flex-xl-row-reverse{flex-direction:row-reverse !important}.flex-xl-column-reverse{flex-direction:column-reverse !important}.flex-xl-grow-0{flex-grow:0 !important}.flex-xl-grow-1{flex-grow:1 !important}.flex-xl-shrink-0{flex-shrink:0 !important}.flex-xl-shrink-1{flex-shrink:1 !important}.flex-xl-wrap{flex-wrap:wrap !important}.flex-xl-nowrap{flex-wrap:nowrap !important}.flex-xl-wrap-reverse{flex-wrap:wrap-reverse !important}.justify-content-xl-start{justify-content:flex-start !important}.justify-content-xl-end{justify-content:flex-end !important}.justify-content-xl-center{justify-content:center !important}.justify-content-xl-between{justify-content:space-between !important}.justify-content-xl-around{justify-content:space-around !important}.justify-content-xl-evenly{justify-content:space-evenly !important}.align-items-xl-start{align-items:flex-start !important}.align-items-xl-end{align-items:flex-end !important}.align-items-xl-center{align-items:center !important}.align-items-xl-baseline{align-items:baseline !important}.align-items-xl-stretch{align-items:stretch !important}.align-content-xl-start{align-content:flex-start !important}.align-content-xl-end{align-content:flex-end !important}.align-content-xl-center{align-content:center !important}.align-content-xl-between{align-content:space-between !important}.align-content-xl-around{align-content:space-around !important}.align-content-xl-stretch{align-content:stretch !important}.align-self-xl-auto{align-self:auto !important}.align-self-xl-start{align-self:flex-start !important}.align-self-xl-end{align-self:flex-end !important}.align-self-xl-center{align-self:center !important}.align-self-xl-baseline{align-self:baseline !important}.align-self-xl-stretch{align-self:stretch !important}.order-xl-first{order:-1 !important}.order-xl-0{order:0 !important}.order-xl-1{order:1 !important}.order-xl-2{order:2 !important}.order-xl-3{order:3 !important}.order-xl-4{order:4 !important}.order-xl-5{order:5 !important}.order-xl-last{order:6 !important}.m-xl-0{margin:0 !important}.m-xl-1{margin:.25rem !important}.m-xl-2{margin:.5rem !important}.m-xl-3{margin:1rem !important}.m-xl-4{margin:1.5rem !important}.m-xl-5{margin:3rem !important}.m-xl-auto{margin:auto !important}.mx-xl-0{margin-right:0 !important;margin-left:0 !important}.mx-xl-1{margin-right:.25rem !important;margin-left:.25rem !important}.mx-xl-2{margin-right:.5rem !important;margin-left:.5rem !important}.mx-xl-3{margin-right:1rem !important;margin-left:1rem !important}.mx-xl-4{margin-right:1.5rem !important;margin-left:1.5rem !important}.mx-xl-5{margin-right:3rem !important;margin-left:3rem !important}.mx-xl-auto{margin-right:auto !important;margin-left:auto !important}.my-xl-0{margin-top:0 !important;margin-bottom:0 !important}.my-xl-1{margin-top:.25rem !important;margin-bottom:.25rem !important}.my-xl-2{margin-top:.5rem !important;margin-bottom:.5rem !important}.my-xl-3{margin-top:1rem !important;margin-bottom:1rem !important}.my-xl-4{margin-top:1.5rem !important;margin-bottom:1.5rem !important}.my-xl-5{margin-top:3rem !important;margin-bottom:3rem !important}.my-xl-auto{margin-top:auto !important;margin-bottom:auto !important}.mt-xl-0{margin-top:0 !important}.mt-xl-1{margin-top:.25rem !important}.mt-xl-2{margin-top:.5rem !important}.mt-xl-3{margin-top:1rem !important}.mt-xl-4{margin-top:1.5rem !important}.mt-xl-5{margin-top:3rem !important}.mt-xl-auto{margin-top:auto !important}.me-xl-0{margin-right:0 !important}.me-xl-1{margin-right:.25rem !important}.me-xl-2{margin-right:.5rem !important}.me-xl-3{margin-right:1rem !important}.me-xl-4{margin-right:1.5rem !important}.me-xl-5{margin-right:3rem !important}.me-xl-auto{margin-right:auto !important}.mb-xl-0{margin-bottom:0 !important}.mb-xl-1{margin-bottom:.25rem !important}.mb-xl-2{margin-bottom:.5rem !important}.mb-xl-3{margin-bottom:1rem !important}.mb-xl-4{margin-bottom:1.5rem !important}.mb-xl-5{margin-bottom:3rem !important}.mb-xl-auto{margin-bottom:auto !important}.ms-xl-0{margin-left:0 !important}.ms-xl-1{margin-left:.25rem !important}.ms-xl-2{margin-left:.5rem !important}.ms-xl-3{margin-left:1rem !important}.ms-xl-4{margin-left:1.5rem !important}.ms-xl-5{margin-left:3rem !important}.ms-xl-auto{margin-left:auto !important}.p-xl-0{padding:0 !important}.p-xl-1{padding:.25rem !important}.p-xl-2{padding:.5rem !important}.p-xl-3{padding:1rem !important}.p-xl-4{padding:1.5rem !important}.p-xl-5{padding:3rem !important}.px-xl-0{padding-right:0 !important;padding-left:0 !important}.px-xl-1{padding-right:.25rem !important;padding-left:.25rem !important}.px-xl-2{padding-right:.5rem !important;padding-left:.5rem !important}.px-xl-3{padding-right:1rem !important;padding-left:1rem !important}.px-xl-4{padding-right:1.5rem !important;padding-left:1.5rem !important}.px-xl-5{padding-right:3rem !important;padding-left:3rem !important}.py-xl-0{padding-top:0 !important;padding-bottom:0 !important}.py-xl-1{padding-top:.25rem !important;padding-bottom:.25rem !important}.py-xl-2{padding-top:.5rem !important;padding-bottom:.5rem !important}.py-xl-3{padding-top:1rem !important;padding-bottom:1rem !important}.py-xl-4{padding-top:1.5rem !important;padding-bottom:1.5rem !important}.py-xl-5{padding-top:3rem !important;padding-bottom:3rem !important}.pt-xl-0{padding-top:0 !important}.pt-xl-1{padding-top:.25rem !important}.pt-xl-2{padding-top:.5rem !important}.pt-xl-3{padding-top:1rem !important}.pt-xl-4{padding-top:1.5rem !important}.pt-xl-5{padding-top:3rem !important}.pe-xl-0{padding-right:0 !important}.pe-xl-1{padding-right:.25rem !important}.pe-xl-2{padding-right:.5rem !important}.pe-xl-3{padding-right:1rem !important}.pe-xl-4{padding-right:1.5rem !important}.pe-xl-5{padding-right:3rem !important}.pb-xl-0{padding-bottom:0 !important}.pb-xl-1{padding-bottom:.25rem !important}.pb-xl-2{padding-bottom:.5rem !important}.pb-xl-3{padding-bottom:1rem !important}.pb-xl-4{padding-bottom:1.5rem !important}.pb-xl-5{padding-bottom:3rem !important}.ps-xl-0{padding-left:0 !important}.ps-xl-1{padding-left:.25rem !important}.ps-xl-2{padding-left:.5rem !important}.ps-xl-3{padding-left:1rem !important}.ps-xl-4{padding-left:1.5rem !important}.ps-xl-5{padding-left:3rem !important}.gap-xl-0{gap:0 !important}.gap-xl-1{gap:.25rem !important}.gap-xl-2{gap:.5rem !important}.gap-xl-3{gap:1rem !important}.gap-xl-4{gap:1.5rem !important}.gap-xl-5{gap:3rem !important}.row-gap-xl-0{row-gap:0 !important}.row-gap-xl-1{row-gap:.25rem !important}.row-gap-xl-2{row-gap:.5rem !important}.row-gap-xl-3{row-gap:1rem !important}.row-gap-xl-4{row-gap:1.5rem !important}.row-gap-xl-5{row-gap:3rem !important}.column-gap-xl-0{column-gap:0 !important}.column-gap-xl-1{column-gap:.25rem !important}.column-gap-xl-2{column-gap:.5rem !important}.column-gap-xl-3{column-gap:1rem !important}.column-gap-xl-4{column-gap:1.5rem !important}.column-gap-xl-5{column-gap:3rem !important}.text-xl-start{text-align:left !important}.text-xl-end{text-align:right !important}.text-xl-center{text-align:center !important}}@media(min-width: 1400px){.float-xxl-start{float:left !important}.float-xxl-end{float:right !important}.float-xxl-none{float:none !important}.object-fit-xxl-contain{object-fit:contain !important}.object-fit-xxl-cover{object-fit:cover !important}.object-fit-xxl-fill{object-fit:fill !important}.object-fit-xxl-scale{object-fit:scale-down !important}.object-fit-xxl-none{object-fit:none !important}.d-xxl-inline{display:inline !important}.d-xxl-inline-block{display:inline-block !important}.d-xxl-block{display:block !important}.d-xxl-grid{display:grid !important}.d-xxl-inline-grid{display:inline-grid !important}.d-xxl-table{display:table !important}.d-xxl-table-row{display:table-row !important}.d-xxl-table-cell{display:table-cell !important}.d-xxl-flex{display:flex !important}.d-xxl-inline-flex{display:inline-flex !important}.d-xxl-none{display:none !important}.flex-xxl-fill{flex:1 1 auto !important}.flex-xxl-row{flex-direction:row !important}.flex-xxl-column{flex-direction:column !important}.flex-xxl-row-reverse{flex-direction:row-reverse !important}.flex-xxl-column-reverse{flex-direction:column-reverse !important}.flex-xxl-grow-0{flex-grow:0 !important}.flex-xxl-grow-1{flex-grow:1 !important}.flex-xxl-shrink-0{flex-shrink:0 !important}.flex-xxl-shrink-1{flex-shrink:1 !important}.flex-xxl-wrap{flex-wrap:wrap !important}.flex-xxl-nowrap{flex-wrap:nowrap !important}.flex-xxl-wrap-reverse{flex-wrap:wrap-reverse !important}.justify-content-xxl-start{justify-content:flex-start !important}.justify-content-xxl-end{justify-content:flex-end !important}.justify-content-xxl-center{justify-content:center !important}.justify-content-xxl-between{justify-content:space-between !important}.justify-content-xxl-around{justify-content:space-around !important}.justify-content-xxl-evenly{justify-content:space-evenly !important}.align-items-xxl-start{align-items:flex-start !important}.align-items-xxl-end{align-items:flex-end !important}.align-items-xxl-center{align-items:center !important}.align-items-xxl-baseline{align-items:baseline !important}.align-items-xxl-stretch{align-items:stretch !important}.align-content-xxl-start{align-content:flex-start !important}.align-content-xxl-end{align-content:flex-end !important}.align-content-xxl-center{align-content:center !important}.align-content-xxl-between{align-content:space-between !important}.align-content-xxl-around{align-content:space-around !important}.align-content-xxl-stretch{align-content:stretch !important}.align-self-xxl-auto{align-self:auto !important}.align-self-xxl-start{align-self:flex-start !important}.align-self-xxl-end{align-self:flex-end !important}.align-self-xxl-center{align-self:center !important}.align-self-xxl-baseline{align-self:baseline !important}.align-self-xxl-stretch{align-self:stretch !important}.order-xxl-first{order:-1 !important}.order-xxl-0{order:0 !important}.order-xxl-1{order:1 !important}.order-xxl-2{order:2 !important}.order-xxl-3{order:3 !important}.order-xxl-4{order:4 !important}.order-xxl-5{order:5 !important}.order-xxl-last{order:6 !important}.m-xxl-0{margin:0 !important}.m-xxl-1{margin:.25rem !important}.m-xxl-2{margin:.5rem !important}.m-xxl-3{margin:1rem !important}.m-xxl-4{margin:1.5rem !important}.m-xxl-5{margin:3rem !important}.m-xxl-auto{margin:auto !important}.mx-xxl-0{margin-right:0 !important;margin-left:0 !important}.mx-xxl-1{margin-right:.25rem !important;margin-left:.25rem !important}.mx-xxl-2{margin-right:.5rem !important;margin-left:.5rem !important}.mx-xxl-3{margin-right:1rem !important;margin-left:1rem !important}.mx-xxl-4{margin-right:1.5rem !important;margin-left:1.5rem !important}.mx-xxl-5{margin-right:3rem !important;margin-left:3rem !important}.mx-xxl-auto{margin-right:auto !important;margin-left:auto !important}.my-xxl-0{margin-top:0 !important;margin-bottom:0 !important}.my-xxl-1{margin-top:.25rem !important;margin-bottom:.25rem !important}.my-xxl-2{margin-top:.5rem !important;margin-bottom:.5rem !important}.my-xxl-3{margin-top:1rem !important;margin-bottom:1rem !important}.my-xxl-4{margin-top:1.5rem !important;margin-bottom:1.5rem !important}.my-xxl-5{margin-top:3rem !important;margin-bottom:3rem !important}.my-xxl-auto{margin-top:auto !important;margin-bottom:auto !important}.mt-xxl-0{margin-top:0 !important}.mt-xxl-1{margin-top:.25rem !important}.mt-xxl-2{margin-top:.5rem !important}.mt-xxl-3{margin-top:1rem !important}.mt-xxl-4{margin-top:1.5rem !important}.mt-xxl-5{margin-top:3rem !important}.mt-xxl-auto{margin-top:auto !important}.me-xxl-0{margin-right:0 !important}.me-xxl-1{margin-right:.25rem !important}.me-xxl-2{margin-right:.5rem !important}.me-xxl-3{margin-right:1rem !important}.me-xxl-4{margin-right:1.5rem !important}.me-xxl-5{margin-right:3rem !important}.me-xxl-auto{margin-right:auto !important}.mb-xxl-0{margin-bottom:0 !important}.mb-xxl-1{margin-bottom:.25rem !important}.mb-xxl-2{margin-bottom:.5rem !important}.mb-xxl-3{margin-bottom:1rem !important}.mb-xxl-4{margin-bottom:1.5rem !important}.mb-xxl-5{margin-bottom:3rem !important}.mb-xxl-auto{margin-bottom:auto !important}.ms-xxl-0{margin-left:0 !important}.ms-xxl-1{margin-left:.25rem !important}.ms-xxl-2{margin-left:.5rem !important}.ms-xxl-3{margin-left:1rem !important}.ms-xxl-4{margin-left:1.5rem !important}.ms-xxl-5{margin-left:3rem !important}.ms-xxl-auto{margin-left:auto !important}.p-xxl-0{padding:0 !important}.p-xxl-1{padding:.25rem !important}.p-xxl-2{padding:.5rem !important}.p-xxl-3{padding:1rem !important}.p-xxl-4{padding:1.5rem !important}.p-xxl-5{padding:3rem !important}.px-xxl-0{padding-right:0 !important;padding-left:0 !important}.px-xxl-1{padding-right:.25rem !important;padding-left:.25rem !important}.px-xxl-2{padding-right:.5rem !important;padding-left:.5rem !important}.px-xxl-3{padding-right:1rem !important;padding-left:1rem !important}.px-xxl-4{padding-right:1.5rem !important;padding-left:1.5rem !important}.px-xxl-5{padding-right:3rem !important;padding-left:3rem !important}.py-xxl-0{padding-top:0 !important;padding-bottom:0 !important}.py-xxl-1{padding-top:.25rem !important;padding-bottom:.25rem !important}.py-xxl-2{padding-top:.5rem !important;padding-bottom:.5rem !important}.py-xxl-3{padding-top:1rem !important;padding-bottom:1rem !important}.py-xxl-4{padding-top:1.5rem !important;padding-bottom:1.5rem !important}.py-xxl-5{padding-top:3rem !important;padding-bottom:3rem !important}.pt-xxl-0{padding-top:0 !important}.pt-xxl-1{padding-top:.25rem !important}.pt-xxl-2{padding-top:.5rem !important}.pt-xxl-3{padding-top:1rem !important}.pt-xxl-4{padding-top:1.5rem !important}.pt-xxl-5{padding-top:3rem !important}.pe-xxl-0{padding-right:0 !important}.pe-xxl-1{padding-right:.25rem !important}.pe-xxl-2{padding-right:.5rem !important}.pe-xxl-3{padding-right:1rem !important}.pe-xxl-4{padding-right:1.5rem !important}.pe-xxl-5{padding-right:3rem !important}.pb-xxl-0{padding-bottom:0 !important}.pb-xxl-1{padding-bottom:.25rem !important}.pb-xxl-2{padding-bottom:.5rem !important}.pb-xxl-3{padding-bottom:1rem !important}.pb-xxl-4{padding-bottom:1.5rem !important}.pb-xxl-5{padding-bottom:3rem !important}.ps-xxl-0{padding-left:0 !important}.ps-xxl-1{padding-left:.25rem !important}.ps-xxl-2{padding-left:.5rem !important}.ps-xxl-3{padding-left:1rem !important}.ps-xxl-4{padding-left:1.5rem !important}.ps-xxl-5{padding-left:3rem !important}.gap-xxl-0{gap:0 !important}.gap-xxl-1{gap:.25rem !important}.gap-xxl-2{gap:.5rem !important}.gap-xxl-3{gap:1rem !important}.gap-xxl-4{gap:1.5rem !important}.gap-xxl-5{gap:3rem !important}.row-gap-xxl-0{row-gap:0 !important}.row-gap-xxl-1{row-gap:.25rem !important}.row-gap-xxl-2{row-gap:.5rem !important}.row-gap-xxl-3{row-gap:1rem !important}.row-gap-xxl-4{row-gap:1.5rem !important}.row-gap-xxl-5{row-gap:3rem !important}.column-gap-xxl-0{column-gap:0 !important}.column-gap-xxl-1{column-gap:.25rem !important}.column-gap-xxl-2{column-gap:.5rem !important}.column-gap-xxl-3{column-gap:1rem !important}.column-gap-xxl-4{column-gap:1.5rem !important}.column-gap-xxl-5{column-gap:3rem !important}.text-xxl-start{text-align:left !important}.text-xxl-end{text-align:right !important}.text-xxl-center{text-align:center !important}}.bg-default{color:#fff}.bg-primary{color:#fff}.bg-secondary{color:#fff}.bg-success{color:#fff}.bg-info{color:#fff}.bg-warning{color:#fff}.bg-danger{color:#fff}.bg-light{color:#000}.bg-dark{color:#fff}@media(min-width: 1200px){.fs-1{font-size:2rem !important}.fs-2{font-size:1.65rem !important}.fs-3{font-size:1.45rem !important}}@media print{.d-print-inline{display:inline !important}.d-print-inline-block{display:inline-block !important}.d-print-block{display:block !important}.d-print-grid{display:grid !important}.d-print-inline-grid{display:inline-grid !important}.d-print-table{display:table !important}.d-print-table-row{display:table-row !important}.d-print-table-cell{display:table-cell !important}.d-print-flex{display:flex !important}.d-print-inline-flex{display:inline-flex !important}.d-print-none{display:none !important}}:root{--bslib-spacer: 1rem;--bslib-mb-spacer: var(--bslib-spacer, 1rem)}.bslib-mb-spacing{margin-bottom:var(--bslib-mb-spacer)}.bslib-gap-spacing{gap:var(--bslib-mb-spacer)}.bslib-gap-spacing>.bslib-mb-spacing,.bslib-gap-spacing>.form-group,.bslib-gap-spacing>p,.bslib-gap-spacing>pre{margin-bottom:0}.html-fill-container>.html-fill-item.bslib-mb-spacing{margin-bottom:0}.tab-content>.tab-pane.html-fill-container{display:none}.tab-content>.active.html-fill-container{display:flex}.tab-content.html-fill-container{padding:0}.bg-blue{--bslib-color-bg: #2780e3;--bslib-color-fg: #fff;background-color:var(--bslib-color-bg);color:var(--bslib-color-fg)}.text-blue{--bslib-color-fg: #2780e3;color:var(--bslib-color-fg)}.bg-indigo{--bslib-color-bg: #6610f2;--bslib-color-fg: #fff;background-color:var(--bslib-color-bg);color:var(--bslib-color-fg)}.text-indigo{--bslib-color-fg: #6610f2;color:var(--bslib-color-fg)}.bg-purple{--bslib-color-bg: #613d7c;--bslib-color-fg: #fff;background-color:var(--bslib-color-bg);color:var(--bslib-color-fg)}.text-purple{--bslib-color-fg: #613d7c;color:var(--bslib-color-fg)}.bg-pink{--bslib-color-bg: #e83e8c;--bslib-color-fg: #fff;background-color:var(--bslib-color-bg);color:var(--bslib-color-fg)}.text-pink{--bslib-color-fg: #e83e8c;color:var(--bslib-color-fg)}.bg-red{--bslib-color-bg: #ff0039;--bslib-color-fg: #fff;background-color:var(--bslib-color-bg);color:var(--bslib-color-fg)}.text-red{--bslib-color-fg: #ff0039;color:var(--bslib-color-fg)}.bg-orange{--bslib-color-bg: #f0ad4e;--bslib-color-fg: #000;background-color:var(--bslib-color-bg);color:var(--bslib-color-fg)}.text-orange{--bslib-color-fg: #f0ad4e;color:var(--bslib-color-fg)}.bg-yellow{--bslib-color-bg: #ff7518;--bslib-color-fg: #fff;background-color:var(--bslib-color-bg);color:var(--bslib-color-fg)}.text-yellow{--bslib-color-fg: #ff7518;color:var(--bslib-color-fg)}.bg-green{--bslib-color-bg: #3fb618;--bslib-color-fg: #fff;background-color:var(--bslib-color-bg);color:var(--bslib-color-fg)}.text-green{--bslib-color-fg: #3fb618;color:var(--bslib-color-fg)}.bg-teal{--bslib-color-bg: #20c997;--bslib-color-fg: #000;background-color:var(--bslib-color-bg);color:var(--bslib-color-fg)}.text-teal{--bslib-color-fg: #20c997;color:var(--bslib-color-fg)}.bg-cyan{--bslib-color-bg: #9954bb;--bslib-color-fg: #fff;background-color:var(--bslib-color-bg);color:var(--bslib-color-fg)}.text-cyan{--bslib-color-fg: #9954bb;color:var(--bslib-color-fg)}.text-default{--bslib-color-fg: #343a40}.bg-default{--bslib-color-bg: #343a40;--bslib-color-fg: #fff}.text-primary{--bslib-color-fg: #2780e3}.bg-primary{--bslib-color-bg: #2780e3;--bslib-color-fg: #fff}.text-secondary{--bslib-color-fg: #343a40}.bg-secondary{--bslib-color-bg: #343a40;--bslib-color-fg: #fff}.text-success{--bslib-color-fg: #3fb618}.bg-success{--bslib-color-bg: #3fb618;--bslib-color-fg: #fff}.text-info{--bslib-color-fg: #9954bb}.bg-info{--bslib-color-bg: #9954bb;--bslib-color-fg: #fff}.text-warning{--bslib-color-fg: #ff7518}.bg-warning{--bslib-color-bg: #ff7518;--bslib-color-fg: #fff}.text-danger{--bslib-color-fg: #ff0039}.bg-danger{--bslib-color-bg: #ff0039;--bslib-color-fg: #fff}.text-light{--bslib-color-fg: #f8f9fa}.bg-light{--bslib-color-bg: #f8f9fa;--bslib-color-fg: #000}.text-dark{--bslib-color-fg: #343a40}.bg-dark{--bslib-color-bg: #343a40;--bslib-color-fg: #fff}.bg-gradient-blue-indigo{--bslib-color-fg: #fff;--bslib-color-bg: #4053e9;background:linear-gradient(var(--bg-gradient-deg, 140deg), #2780e3 var(--bg-gradient-start, 36%), #6610f2 var(--bg-gradient-end, 180%)) #4053e9;color:#fff}.bg-gradient-blue-purple{--bslib-color-fg: #fff;--bslib-color-bg: #3e65ba;background:linear-gradient(var(--bg-gradient-deg, 140deg), #2780e3 var(--bg-gradient-start, 36%), #613d7c var(--bg-gradient-end, 180%)) #3e65ba;color:#fff}.bg-gradient-blue-pink{--bslib-color-fg: #fff;--bslib-color-bg: #7466c0;background:linear-gradient(var(--bg-gradient-deg, 140deg), #2780e3 var(--bg-gradient-start, 36%), #e83e8c var(--bg-gradient-end, 180%)) #7466c0;color:#fff}.bg-gradient-blue-red{--bslib-color-fg: #fff;--bslib-color-bg: #7d4d9f;background:linear-gradient(var(--bg-gradient-deg, 140deg), #2780e3 var(--bg-gradient-start, 36%), #ff0039 var(--bg-gradient-end, 180%)) #7d4d9f;color:#fff}.bg-gradient-blue-orange{--bslib-color-fg: #fff;--bslib-color-bg: #7792a7;background:linear-gradient(var(--bg-gradient-deg, 140deg), #2780e3 var(--bg-gradient-start, 36%), #f0ad4e var(--bg-gradient-end, 180%)) #7792a7;color:#fff}.bg-gradient-blue-yellow{--bslib-color-fg: #fff;--bslib-color-bg: #7d7c92;background:linear-gradient(var(--bg-gradient-deg, 140deg), #2780e3 var(--bg-gradient-start, 36%), #ff7518 var(--bg-gradient-end, 180%)) #7d7c92;color:#fff}.bg-gradient-blue-green{--bslib-color-fg: #fff;--bslib-color-bg: #319692;background:linear-gradient(var(--bg-gradient-deg, 140deg), #2780e3 var(--bg-gradient-start, 36%), #3fb618 var(--bg-gradient-end, 180%)) #319692;color:#fff}.bg-gradient-blue-teal{--bslib-color-fg: #fff;--bslib-color-bg: #249dc5;background:linear-gradient(var(--bg-gradient-deg, 140deg), #2780e3 var(--bg-gradient-start, 36%), #20c997 var(--bg-gradient-end, 180%)) #249dc5;color:#fff}.bg-gradient-blue-cyan{--bslib-color-fg: #fff;--bslib-color-bg: #556ed3;background:linear-gradient(var(--bg-gradient-deg, 140deg), #2780e3 var(--bg-gradient-start, 36%), #9954bb var(--bg-gradient-end, 180%)) #556ed3;color:#fff}.bg-gradient-indigo-blue{--bslib-color-fg: #fff;--bslib-color-bg: #4d3dec;background:linear-gradient(var(--bg-gradient-deg, 140deg), #6610f2 var(--bg-gradient-start, 36%), #2780e3 var(--bg-gradient-end, 180%)) #4d3dec;color:#fff}.bg-gradient-indigo-purple{--bslib-color-fg: #fff;--bslib-color-bg: #6422c3;background:linear-gradient(var(--bg-gradient-deg, 140deg), #6610f2 var(--bg-gradient-start, 36%), #613d7c var(--bg-gradient-end, 180%)) #6422c3;color:#fff}.bg-gradient-indigo-pink{--bslib-color-fg: #fff;--bslib-color-bg: #9a22c9;background:linear-gradient(var(--bg-gradient-deg, 140deg), #6610f2 var(--bg-gradient-start, 36%), #e83e8c var(--bg-gradient-end, 180%)) #9a22c9;color:#fff}.bg-gradient-indigo-red{--bslib-color-fg: #fff;--bslib-color-bg: #a30aa8;background:linear-gradient(var(--bg-gradient-deg, 140deg), #6610f2 var(--bg-gradient-start, 36%), #ff0039 var(--bg-gradient-end, 180%)) #a30aa8;color:#fff}.bg-gradient-indigo-orange{--bslib-color-fg: #fff;--bslib-color-bg: #9d4fb0;background:linear-gradient(var(--bg-gradient-deg, 140deg), #6610f2 var(--bg-gradient-start, 36%), #f0ad4e var(--bg-gradient-end, 180%)) #9d4fb0;color:#fff}.bg-gradient-indigo-yellow{--bslib-color-fg: #fff;--bslib-color-bg: #a3389b;background:linear-gradient(var(--bg-gradient-deg, 140deg), #6610f2 var(--bg-gradient-start, 36%), #ff7518 var(--bg-gradient-end, 180%)) #a3389b;color:#fff}.bg-gradient-indigo-green{--bslib-color-fg: #fff;--bslib-color-bg: #56529b;background:linear-gradient(var(--bg-gradient-deg, 140deg), #6610f2 var(--bg-gradient-start, 36%), #3fb618 var(--bg-gradient-end, 180%)) #56529b;color:#fff}.bg-gradient-indigo-teal{--bslib-color-fg: #fff;--bslib-color-bg: #4a5ace;background:linear-gradient(var(--bg-gradient-deg, 140deg), #6610f2 var(--bg-gradient-start, 36%), #20c997 var(--bg-gradient-end, 180%)) #4a5ace;color:#fff}.bg-gradient-indigo-cyan{--bslib-color-fg: #fff;--bslib-color-bg: #7a2bdc;background:linear-gradient(var(--bg-gradient-deg, 140deg), #6610f2 var(--bg-gradient-start, 36%), #9954bb var(--bg-gradient-end, 180%)) #7a2bdc;color:#fff}.bg-gradient-purple-blue{--bslib-color-fg: #fff;--bslib-color-bg: #4a58a5;background:linear-gradient(var(--bg-gradient-deg, 140deg), #613d7c var(--bg-gradient-start, 36%), #2780e3 var(--bg-gradient-end, 180%)) #4a58a5;color:#fff}.bg-gradient-purple-indigo{--bslib-color-fg: #fff;--bslib-color-bg: #632bab;background:linear-gradient(var(--bg-gradient-deg, 140deg), #613d7c var(--bg-gradient-start, 36%), #6610f2 var(--bg-gradient-end, 180%)) #632bab;color:#fff}.bg-gradient-purple-pink{--bslib-color-fg: #fff;--bslib-color-bg: #973d82;background:linear-gradient(var(--bg-gradient-deg, 140deg), #613d7c var(--bg-gradient-start, 36%), #e83e8c var(--bg-gradient-end, 180%)) #973d82;color:#fff}.bg-gradient-purple-red{--bslib-color-fg: #fff;--bslib-color-bg: #a02561;background:linear-gradient(var(--bg-gradient-deg, 140deg), #613d7c var(--bg-gradient-start, 36%), #ff0039 var(--bg-gradient-end, 180%)) #a02561;color:#fff}.bg-gradient-purple-orange{--bslib-color-fg: #fff;--bslib-color-bg: #9a6a6a;background:linear-gradient(var(--bg-gradient-deg, 140deg), #613d7c var(--bg-gradient-start, 36%), #f0ad4e var(--bg-gradient-end, 180%)) #9a6a6a;color:#fff}.bg-gradient-purple-yellow{--bslib-color-fg: #fff;--bslib-color-bg: #a05354;background:linear-gradient(var(--bg-gradient-deg, 140deg), #613d7c var(--bg-gradient-start, 36%), #ff7518 var(--bg-gradient-end, 180%)) #a05354;color:#fff}.bg-gradient-purple-green{--bslib-color-fg: #fff;--bslib-color-bg: #536d54;background:linear-gradient(var(--bg-gradient-deg, 140deg), #613d7c var(--bg-gradient-start, 36%), #3fb618 var(--bg-gradient-end, 180%)) #536d54;color:#fff}.bg-gradient-purple-teal{--bslib-color-fg: #fff;--bslib-color-bg: #477587;background:linear-gradient(var(--bg-gradient-deg, 140deg), #613d7c var(--bg-gradient-start, 36%), #20c997 var(--bg-gradient-end, 180%)) #477587;color:#fff}.bg-gradient-purple-cyan{--bslib-color-fg: #fff;--bslib-color-bg: #774695;background:linear-gradient(var(--bg-gradient-deg, 140deg), #613d7c var(--bg-gradient-start, 36%), #9954bb var(--bg-gradient-end, 180%)) #774695;color:#fff}.bg-gradient-pink-blue{--bslib-color-fg: #fff;--bslib-color-bg: #9b58af;background:linear-gradient(var(--bg-gradient-deg, 140deg), #e83e8c var(--bg-gradient-start, 36%), #2780e3 var(--bg-gradient-end, 180%)) #9b58af;color:#fff}.bg-gradient-pink-indigo{--bslib-color-fg: #fff;--bslib-color-bg: #b42cb5;background:linear-gradient(var(--bg-gradient-deg, 140deg), #e83e8c var(--bg-gradient-start, 36%), #6610f2 var(--bg-gradient-end, 180%)) #b42cb5;color:#fff}.bg-gradient-pink-purple{--bslib-color-fg: #fff;--bslib-color-bg: #b23e86;background:linear-gradient(var(--bg-gradient-deg, 140deg), #e83e8c var(--bg-gradient-start, 36%), #613d7c var(--bg-gradient-end, 180%)) #b23e86;color:#fff}.bg-gradient-pink-red{--bslib-color-fg: #fff;--bslib-color-bg: #f1256b;background:linear-gradient(var(--bg-gradient-deg, 140deg), #e83e8c var(--bg-gradient-start, 36%), #ff0039 var(--bg-gradient-end, 180%)) #f1256b;color:#fff}.bg-gradient-pink-orange{--bslib-color-fg: #fff;--bslib-color-bg: #eb6a73;background:linear-gradient(var(--bg-gradient-deg, 140deg), #e83e8c var(--bg-gradient-start, 36%), #f0ad4e var(--bg-gradient-end, 180%)) #eb6a73;color:#fff}.bg-gradient-pink-yellow{--bslib-color-fg: #fff;--bslib-color-bg: #f1545e;background:linear-gradient(var(--bg-gradient-deg, 140deg), #e83e8c var(--bg-gradient-start, 36%), #ff7518 var(--bg-gradient-end, 180%)) #f1545e;color:#fff}.bg-gradient-pink-green{--bslib-color-fg: #fff;--bslib-color-bg: #a46e5e;background:linear-gradient(var(--bg-gradient-deg, 140deg), #e83e8c var(--bg-gradient-start, 36%), #3fb618 var(--bg-gradient-end, 180%)) #a46e5e;color:#fff}.bg-gradient-pink-teal{--bslib-color-fg: #fff;--bslib-color-bg: #987690;background:linear-gradient(var(--bg-gradient-deg, 140deg), #e83e8c var(--bg-gradient-start, 36%), #20c997 var(--bg-gradient-end, 180%)) #987690;color:#fff}.bg-gradient-pink-cyan{--bslib-color-fg: #fff;--bslib-color-bg: #c8479f;background:linear-gradient(var(--bg-gradient-deg, 140deg), #e83e8c var(--bg-gradient-start, 36%), #9954bb var(--bg-gradient-end, 180%)) #c8479f;color:#fff}.bg-gradient-red-blue{--bslib-color-fg: #fff;--bslib-color-bg: #a9337d;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff0039 var(--bg-gradient-start, 36%), #2780e3 var(--bg-gradient-end, 180%)) #a9337d;color:#fff}.bg-gradient-red-indigo{--bslib-color-fg: #fff;--bslib-color-bg: #c20683;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff0039 var(--bg-gradient-start, 36%), #6610f2 var(--bg-gradient-end, 180%)) #c20683;color:#fff}.bg-gradient-red-purple{--bslib-color-fg: #fff;--bslib-color-bg: #c01854;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff0039 var(--bg-gradient-start, 36%), #613d7c var(--bg-gradient-end, 180%)) #c01854;color:#fff}.bg-gradient-red-pink{--bslib-color-fg: #fff;--bslib-color-bg: #f6195a;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff0039 var(--bg-gradient-start, 36%), #e83e8c var(--bg-gradient-end, 180%)) #f6195a;color:#fff}.bg-gradient-red-orange{--bslib-color-fg: #fff;--bslib-color-bg: #f94541;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff0039 var(--bg-gradient-start, 36%), #f0ad4e var(--bg-gradient-end, 180%)) #f94541;color:#fff}.bg-gradient-red-yellow{--bslib-color-fg: #fff;--bslib-color-bg: #ff2f2c;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff0039 var(--bg-gradient-start, 36%), #ff7518 var(--bg-gradient-end, 180%)) #ff2f2c;color:#fff}.bg-gradient-red-green{--bslib-color-fg: #fff;--bslib-color-bg: #b2492c;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff0039 var(--bg-gradient-start, 36%), #3fb618 var(--bg-gradient-end, 180%)) #b2492c;color:#fff}.bg-gradient-red-teal{--bslib-color-fg: #fff;--bslib-color-bg: #a6505f;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff0039 var(--bg-gradient-start, 36%), #20c997 var(--bg-gradient-end, 180%)) #a6505f;color:#fff}.bg-gradient-red-cyan{--bslib-color-fg: #fff;--bslib-color-bg: #d6226d;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff0039 var(--bg-gradient-start, 36%), #9954bb var(--bg-gradient-end, 180%)) #d6226d;color:#fff}.bg-gradient-orange-blue{--bslib-color-fg: #fff;--bslib-color-bg: #a09b8a;background:linear-gradient(var(--bg-gradient-deg, 140deg), #f0ad4e var(--bg-gradient-start, 36%), #2780e3 var(--bg-gradient-end, 180%)) #a09b8a;color:#fff}.bg-gradient-orange-indigo{--bslib-color-fg: #fff;--bslib-color-bg: #b96e90;background:linear-gradient(var(--bg-gradient-deg, 140deg), #f0ad4e var(--bg-gradient-start, 36%), #6610f2 var(--bg-gradient-end, 180%)) #b96e90;color:#fff}.bg-gradient-orange-purple{--bslib-color-fg: #fff;--bslib-color-bg: #b78060;background:linear-gradient(var(--bg-gradient-deg, 140deg), #f0ad4e var(--bg-gradient-start, 36%), #613d7c var(--bg-gradient-end, 180%)) #b78060;color:#fff}.bg-gradient-orange-pink{--bslib-color-fg: #fff;--bslib-color-bg: #ed8167;background:linear-gradient(var(--bg-gradient-deg, 140deg), #f0ad4e var(--bg-gradient-start, 36%), #e83e8c var(--bg-gradient-end, 180%)) #ed8167;color:#fff}.bg-gradient-orange-red{--bslib-color-fg: #fff;--bslib-color-bg: #f66846;background:linear-gradient(var(--bg-gradient-deg, 140deg), #f0ad4e var(--bg-gradient-start, 36%), #ff0039 var(--bg-gradient-end, 180%)) #f66846;color:#fff}.bg-gradient-orange-yellow{--bslib-color-fg: #000;--bslib-color-bg: #f69738;background:linear-gradient(var(--bg-gradient-deg, 140deg), #f0ad4e var(--bg-gradient-start, 36%), #ff7518 var(--bg-gradient-end, 180%)) #f69738;color:#000}.bg-gradient-orange-green{--bslib-color-fg: #000;--bslib-color-bg: #a9b138;background:linear-gradient(var(--bg-gradient-deg, 140deg), #f0ad4e var(--bg-gradient-start, 36%), #3fb618 var(--bg-gradient-end, 180%)) #a9b138;color:#000}.bg-gradient-orange-teal{--bslib-color-fg: #000;--bslib-color-bg: #9db86b;background:linear-gradient(var(--bg-gradient-deg, 140deg), #f0ad4e var(--bg-gradient-start, 36%), #20c997 var(--bg-gradient-end, 180%)) #9db86b;color:#000}.bg-gradient-orange-cyan{--bslib-color-fg: #fff;--bslib-color-bg: #cd897a;background:linear-gradient(var(--bg-gradient-deg, 140deg), #f0ad4e var(--bg-gradient-start, 36%), #9954bb var(--bg-gradient-end, 180%)) #cd897a;color:#fff}.bg-gradient-yellow-blue{--bslib-color-fg: #fff;--bslib-color-bg: #a97969;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff7518 var(--bg-gradient-start, 36%), #2780e3 var(--bg-gradient-end, 180%)) #a97969;color:#fff}.bg-gradient-yellow-indigo{--bslib-color-fg: #fff;--bslib-color-bg: #c24d6f;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff7518 var(--bg-gradient-start, 36%), #6610f2 var(--bg-gradient-end, 180%)) #c24d6f;color:#fff}.bg-gradient-yellow-purple{--bslib-color-fg: #fff;--bslib-color-bg: #c05f40;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff7518 var(--bg-gradient-start, 36%), #613d7c var(--bg-gradient-end, 180%)) #c05f40;color:#fff}.bg-gradient-yellow-pink{--bslib-color-fg: #fff;--bslib-color-bg: #f65f46;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff7518 var(--bg-gradient-start, 36%), #e83e8c var(--bg-gradient-end, 180%)) #f65f46;color:#fff}.bg-gradient-yellow-red{--bslib-color-fg: #fff;--bslib-color-bg: #ff4625;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff7518 var(--bg-gradient-start, 36%), #ff0039 var(--bg-gradient-end, 180%)) #ff4625;color:#fff}.bg-gradient-yellow-orange{--bslib-color-fg: #000;--bslib-color-bg: #f98b2e;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff7518 var(--bg-gradient-start, 36%), #f0ad4e var(--bg-gradient-end, 180%)) #f98b2e;color:#000}.bg-gradient-yellow-green{--bslib-color-fg: #fff;--bslib-color-bg: #b28f18;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff7518 var(--bg-gradient-start, 36%), #3fb618 var(--bg-gradient-end, 180%)) #b28f18;color:#fff}.bg-gradient-yellow-teal{--bslib-color-fg: #fff;--bslib-color-bg: #a6974b;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff7518 var(--bg-gradient-start, 36%), #20c997 var(--bg-gradient-end, 180%)) #a6974b;color:#fff}.bg-gradient-yellow-cyan{--bslib-color-fg: #fff;--bslib-color-bg: #d66859;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff7518 var(--bg-gradient-start, 36%), #9954bb var(--bg-gradient-end, 180%)) #d66859;color:#fff}.bg-gradient-green-blue{--bslib-color-fg: #fff;--bslib-color-bg: #35a069;background:linear-gradient(var(--bg-gradient-deg, 140deg), #3fb618 var(--bg-gradient-start, 36%), #2780e3 var(--bg-gradient-end, 180%)) #35a069;color:#fff}.bg-gradient-green-indigo{--bslib-color-fg: #fff;--bslib-color-bg: #4f746f;background:linear-gradient(var(--bg-gradient-deg, 140deg), #3fb618 var(--bg-gradient-start, 36%), #6610f2 var(--bg-gradient-end, 180%)) #4f746f;color:#fff}.bg-gradient-green-purple{--bslib-color-fg: #fff;--bslib-color-bg: #4d8640;background:linear-gradient(var(--bg-gradient-deg, 140deg), #3fb618 var(--bg-gradient-start, 36%), #613d7c var(--bg-gradient-end, 180%)) #4d8640;color:#fff}.bg-gradient-green-pink{--bslib-color-fg: #fff;--bslib-color-bg: #838646;background:linear-gradient(var(--bg-gradient-deg, 140deg), #3fb618 var(--bg-gradient-start, 36%), #e83e8c var(--bg-gradient-end, 180%)) #838646;color:#fff}.bg-gradient-green-red{--bslib-color-fg: #fff;--bslib-color-bg: #8c6d25;background:linear-gradient(var(--bg-gradient-deg, 140deg), #3fb618 var(--bg-gradient-start, 36%), #ff0039 var(--bg-gradient-end, 180%)) #8c6d25;color:#fff}.bg-gradient-green-orange{--bslib-color-fg: #000;--bslib-color-bg: #86b22e;background:linear-gradient(var(--bg-gradient-deg, 140deg), #3fb618 var(--bg-gradient-start, 36%), #f0ad4e var(--bg-gradient-end, 180%)) #86b22e;color:#000}.bg-gradient-green-yellow{--bslib-color-fg: #fff;--bslib-color-bg: #8c9c18;background:linear-gradient(var(--bg-gradient-deg, 140deg), #3fb618 var(--bg-gradient-start, 36%), #ff7518 var(--bg-gradient-end, 180%)) #8c9c18;color:#fff}.bg-gradient-green-teal{--bslib-color-fg: #000;--bslib-color-bg: #33be4b;background:linear-gradient(var(--bg-gradient-deg, 140deg), #3fb618 var(--bg-gradient-start, 36%), #20c997 var(--bg-gradient-end, 180%)) #33be4b;color:#000}.bg-gradient-green-cyan{--bslib-color-fg: #fff;--bslib-color-bg: #638f59;background:linear-gradient(var(--bg-gradient-deg, 140deg), #3fb618 var(--bg-gradient-start, 36%), #9954bb var(--bg-gradient-end, 180%)) #638f59;color:#fff}.bg-gradient-teal-blue{--bslib-color-fg: #fff;--bslib-color-bg: #23acb5;background:linear-gradient(var(--bg-gradient-deg, 140deg), #20c997 var(--bg-gradient-start, 36%), #2780e3 var(--bg-gradient-end, 180%)) #23acb5;color:#fff}.bg-gradient-teal-indigo{--bslib-color-fg: #fff;--bslib-color-bg: #3c7fbb;background:linear-gradient(var(--bg-gradient-deg, 140deg), #20c997 var(--bg-gradient-start, 36%), #6610f2 var(--bg-gradient-end, 180%)) #3c7fbb;color:#fff}.bg-gradient-teal-purple{--bslib-color-fg: #fff;--bslib-color-bg: #3a918c;background:linear-gradient(var(--bg-gradient-deg, 140deg), #20c997 var(--bg-gradient-start, 36%), #613d7c var(--bg-gradient-end, 180%)) #3a918c;color:#fff}.bg-gradient-teal-pink{--bslib-color-fg: #fff;--bslib-color-bg: #709193;background:linear-gradient(var(--bg-gradient-deg, 140deg), #20c997 var(--bg-gradient-start, 36%), #e83e8c var(--bg-gradient-end, 180%)) #709193;color:#fff}.bg-gradient-teal-red{--bslib-color-fg: #fff;--bslib-color-bg: #797971;background:linear-gradient(var(--bg-gradient-deg, 140deg), #20c997 var(--bg-gradient-start, 36%), #ff0039 var(--bg-gradient-end, 180%)) #797971;color:#fff}.bg-gradient-teal-orange{--bslib-color-fg: #000;--bslib-color-bg: #73be7a;background:linear-gradient(var(--bg-gradient-deg, 140deg), #20c997 var(--bg-gradient-start, 36%), #f0ad4e var(--bg-gradient-end, 180%)) #73be7a;color:#000}.bg-gradient-teal-yellow{--bslib-color-fg: #fff;--bslib-color-bg: #79a764;background:linear-gradient(var(--bg-gradient-deg, 140deg), #20c997 var(--bg-gradient-start, 36%), #ff7518 var(--bg-gradient-end, 180%)) #79a764;color:#fff}.bg-gradient-teal-green{--bslib-color-fg: #000;--bslib-color-bg: #2cc164;background:linear-gradient(var(--bg-gradient-deg, 140deg), #20c997 var(--bg-gradient-start, 36%), #3fb618 var(--bg-gradient-end, 180%)) #2cc164;color:#000}.bg-gradient-teal-cyan{--bslib-color-fg: #fff;--bslib-color-bg: #509aa5;background:linear-gradient(var(--bg-gradient-deg, 140deg), #20c997 var(--bg-gradient-start, 36%), #9954bb var(--bg-gradient-end, 180%)) #509aa5;color:#fff}.bg-gradient-cyan-blue{--bslib-color-fg: #fff;--bslib-color-bg: #6b66cb;background:linear-gradient(var(--bg-gradient-deg, 140deg), #9954bb var(--bg-gradient-start, 36%), #2780e3 var(--bg-gradient-end, 180%)) #6b66cb;color:#fff}.bg-gradient-cyan-indigo{--bslib-color-fg: #fff;--bslib-color-bg: #8539d1;background:linear-gradient(var(--bg-gradient-deg, 140deg), #9954bb var(--bg-gradient-start, 36%), #6610f2 var(--bg-gradient-end, 180%)) #8539d1;color:#fff}.bg-gradient-cyan-purple{--bslib-color-fg: #fff;--bslib-color-bg: #834ba2;background:linear-gradient(var(--bg-gradient-deg, 140deg), #9954bb var(--bg-gradient-start, 36%), #613d7c var(--bg-gradient-end, 180%)) #834ba2;color:#fff}.bg-gradient-cyan-pink{--bslib-color-fg: #fff;--bslib-color-bg: #b94ba8;background:linear-gradient(var(--bg-gradient-deg, 140deg), #9954bb var(--bg-gradient-start, 36%), #e83e8c var(--bg-gradient-end, 180%)) #b94ba8;color:#fff}.bg-gradient-cyan-red{--bslib-color-fg: #fff;--bslib-color-bg: #c23287;background:linear-gradient(var(--bg-gradient-deg, 140deg), #9954bb var(--bg-gradient-start, 36%), #ff0039 var(--bg-gradient-end, 180%)) #c23287;color:#fff}.bg-gradient-cyan-orange{--bslib-color-fg: #fff;--bslib-color-bg: #bc788f;background:linear-gradient(var(--bg-gradient-deg, 140deg), #9954bb var(--bg-gradient-start, 36%), #f0ad4e var(--bg-gradient-end, 180%)) #bc788f;color:#fff}.bg-gradient-cyan-yellow{--bslib-color-fg: #fff;--bslib-color-bg: #c2617a;background:linear-gradient(var(--bg-gradient-deg, 140deg), #9954bb var(--bg-gradient-start, 36%), #ff7518 var(--bg-gradient-end, 180%)) #c2617a;color:#fff}.bg-gradient-cyan-green{--bslib-color-fg: #fff;--bslib-color-bg: #757b7a;background:linear-gradient(var(--bg-gradient-deg, 140deg), #9954bb var(--bg-gradient-start, 36%), #3fb618 var(--bg-gradient-end, 180%)) #757b7a;color:#fff}.bg-gradient-cyan-teal{--bslib-color-fg: #fff;--bslib-color-bg: #6983ad;background:linear-gradient(var(--bg-gradient-deg, 140deg), #9954bb var(--bg-gradient-start, 36%), #20c997 var(--bg-gradient-end, 180%)) #6983ad;color:#fff}.bg-blue{--bslib-color-bg: #2780e3;--bslib-color-fg: #fff;background-color:var(--bslib-color-bg);color:var(--bslib-color-fg)}.text-blue{--bslib-color-fg: #2780e3;color:var(--bslib-color-fg)}.bg-indigo{--bslib-color-bg: #6610f2;--bslib-color-fg: #fff;background-color:var(--bslib-color-bg);color:var(--bslib-color-fg)}.text-indigo{--bslib-color-fg: #6610f2;color:var(--bslib-color-fg)}.bg-purple{--bslib-color-bg: #613d7c;--bslib-color-fg: #fff;background-color:var(--bslib-color-bg);color:var(--bslib-color-fg)}.text-purple{--bslib-color-fg: #613d7c;color:var(--bslib-color-fg)}.bg-pink{--bslib-color-bg: #e83e8c;--bslib-color-fg: #fff;background-color:var(--bslib-color-bg);color:var(--bslib-color-fg)}.text-pink{--bslib-color-fg: #e83e8c;color:var(--bslib-color-fg)}.bg-red{--bslib-color-bg: #ff0039;--bslib-color-fg: #fff;background-color:var(--bslib-color-bg);color:var(--bslib-color-fg)}.text-red{--bslib-color-fg: #ff0039;color:var(--bslib-color-fg)}.bg-orange{--bslib-color-bg: #f0ad4e;--bslib-color-fg: #000;background-color:var(--bslib-color-bg);color:var(--bslib-color-fg)}.text-orange{--bslib-color-fg: #f0ad4e;color:var(--bslib-color-fg)}.bg-yellow{--bslib-color-bg: #ff7518;--bslib-color-fg: #fff;background-color:var(--bslib-color-bg);color:var(--bslib-color-fg)}.text-yellow{--bslib-color-fg: #ff7518;color:var(--bslib-color-fg)}.bg-green{--bslib-color-bg: #3fb618;--bslib-color-fg: #fff;background-color:var(--bslib-color-bg);color:var(--bslib-color-fg)}.text-green{--bslib-color-fg: #3fb618;color:var(--bslib-color-fg)}.bg-teal{--bslib-color-bg: #20c997;--bslib-color-fg: #000;background-color:var(--bslib-color-bg);color:var(--bslib-color-fg)}.text-teal{--bslib-color-fg: #20c997;color:var(--bslib-color-fg)}.bg-cyan{--bslib-color-bg: #9954bb;--bslib-color-fg: #fff;background-color:var(--bslib-color-bg);color:var(--bslib-color-fg)}.text-cyan{--bslib-color-fg: #9954bb;color:var(--bslib-color-fg)}.text-default{--bslib-color-fg: #343a40}.bg-default{--bslib-color-bg: #343a40;--bslib-color-fg: #fff}.text-primary{--bslib-color-fg: #2780e3}.bg-primary{--bslib-color-bg: #2780e3;--bslib-color-fg: #fff}.text-secondary{--bslib-color-fg: #343a40}.bg-secondary{--bslib-color-bg: #343a40;--bslib-color-fg: #fff}.text-success{--bslib-color-fg: #3fb618}.bg-success{--bslib-color-bg: #3fb618;--bslib-color-fg: #fff}.text-info{--bslib-color-fg: #9954bb}.bg-info{--bslib-color-bg: #9954bb;--bslib-color-fg: #fff}.text-warning{--bslib-color-fg: #ff7518}.bg-warning{--bslib-color-bg: #ff7518;--bslib-color-fg: #fff}.text-danger{--bslib-color-fg: #ff0039}.bg-danger{--bslib-color-bg: #ff0039;--bslib-color-fg: #fff}.text-light{--bslib-color-fg: #f8f9fa}.bg-light{--bslib-color-bg: #f8f9fa;--bslib-color-fg: #000}.text-dark{--bslib-color-fg: #343a40}.bg-dark{--bslib-color-bg: #343a40;--bslib-color-fg: #fff}.bg-gradient-blue-indigo{--bslib-color-fg: #fff;--bslib-color-bg: #4053e9;background:linear-gradient(var(--bg-gradient-deg, 140deg), #2780e3 var(--bg-gradient-start, 36%), #6610f2 var(--bg-gradient-end, 180%)) #4053e9;color:#fff}.bg-gradient-blue-purple{--bslib-color-fg: #fff;--bslib-color-bg: #3e65ba;background:linear-gradient(var(--bg-gradient-deg, 140deg), #2780e3 var(--bg-gradient-start, 36%), #613d7c var(--bg-gradient-end, 180%)) #3e65ba;color:#fff}.bg-gradient-blue-pink{--bslib-color-fg: #fff;--bslib-color-bg: #7466c0;background:linear-gradient(var(--bg-gradient-deg, 140deg), #2780e3 var(--bg-gradient-start, 36%), #e83e8c var(--bg-gradient-end, 180%)) #7466c0;color:#fff}.bg-gradient-blue-red{--bslib-color-fg: #fff;--bslib-color-bg: #7d4d9f;background:linear-gradient(var(--bg-gradient-deg, 140deg), #2780e3 var(--bg-gradient-start, 36%), #ff0039 var(--bg-gradient-end, 180%)) #7d4d9f;color:#fff}.bg-gradient-blue-orange{--bslib-color-fg: #fff;--bslib-color-bg: #7792a7;background:linear-gradient(var(--bg-gradient-deg, 140deg), #2780e3 var(--bg-gradient-start, 36%), #f0ad4e var(--bg-gradient-end, 180%)) #7792a7;color:#fff}.bg-gradient-blue-yellow{--bslib-color-fg: #fff;--bslib-color-bg: #7d7c92;background:linear-gradient(var(--bg-gradient-deg, 140deg), #2780e3 var(--bg-gradient-start, 36%), #ff7518 var(--bg-gradient-end, 180%)) #7d7c92;color:#fff}.bg-gradient-blue-green{--bslib-color-fg: #fff;--bslib-color-bg: #319692;background:linear-gradient(var(--bg-gradient-deg, 140deg), #2780e3 var(--bg-gradient-start, 36%), #3fb618 var(--bg-gradient-end, 180%)) #319692;color:#fff}.bg-gradient-blue-teal{--bslib-color-fg: #fff;--bslib-color-bg: #249dc5;background:linear-gradient(var(--bg-gradient-deg, 140deg), #2780e3 var(--bg-gradient-start, 36%), #20c997 var(--bg-gradient-end, 180%)) #249dc5;color:#fff}.bg-gradient-blue-cyan{--bslib-color-fg: #fff;--bslib-color-bg: #556ed3;background:linear-gradient(var(--bg-gradient-deg, 140deg), #2780e3 var(--bg-gradient-start, 36%), #9954bb var(--bg-gradient-end, 180%)) #556ed3;color:#fff}.bg-gradient-indigo-blue{--bslib-color-fg: #fff;--bslib-color-bg: #4d3dec;background:linear-gradient(var(--bg-gradient-deg, 140deg), #6610f2 var(--bg-gradient-start, 36%), #2780e3 var(--bg-gradient-end, 180%)) #4d3dec;color:#fff}.bg-gradient-indigo-purple{--bslib-color-fg: #fff;--bslib-color-bg: #6422c3;background:linear-gradient(var(--bg-gradient-deg, 140deg), #6610f2 var(--bg-gradient-start, 36%), #613d7c var(--bg-gradient-end, 180%)) #6422c3;color:#fff}.bg-gradient-indigo-pink{--bslib-color-fg: #fff;--bslib-color-bg: #9a22c9;background:linear-gradient(var(--bg-gradient-deg, 140deg), #6610f2 var(--bg-gradient-start, 36%), #e83e8c var(--bg-gradient-end, 180%)) #9a22c9;color:#fff}.bg-gradient-indigo-red{--bslib-color-fg: #fff;--bslib-color-bg: #a30aa8;background:linear-gradient(var(--bg-gradient-deg, 140deg), #6610f2 var(--bg-gradient-start, 36%), #ff0039 var(--bg-gradient-end, 180%)) #a30aa8;color:#fff}.bg-gradient-indigo-orange{--bslib-color-fg: #fff;--bslib-color-bg: #9d4fb0;background:linear-gradient(var(--bg-gradient-deg, 140deg), #6610f2 var(--bg-gradient-start, 36%), #f0ad4e var(--bg-gradient-end, 180%)) #9d4fb0;color:#fff}.bg-gradient-indigo-yellow{--bslib-color-fg: #fff;--bslib-color-bg: #a3389b;background:linear-gradient(var(--bg-gradient-deg, 140deg), #6610f2 var(--bg-gradient-start, 36%), #ff7518 var(--bg-gradient-end, 180%)) #a3389b;color:#fff}.bg-gradient-indigo-green{--bslib-color-fg: #fff;--bslib-color-bg: #56529b;background:linear-gradient(var(--bg-gradient-deg, 140deg), #6610f2 var(--bg-gradient-start, 36%), #3fb618 var(--bg-gradient-end, 180%)) #56529b;color:#fff}.bg-gradient-indigo-teal{--bslib-color-fg: #fff;--bslib-color-bg: #4a5ace;background:linear-gradient(var(--bg-gradient-deg, 140deg), #6610f2 var(--bg-gradient-start, 36%), #20c997 var(--bg-gradient-end, 180%)) #4a5ace;color:#fff}.bg-gradient-indigo-cyan{--bslib-color-fg: #fff;--bslib-color-bg: #7a2bdc;background:linear-gradient(var(--bg-gradient-deg, 140deg), #6610f2 var(--bg-gradient-start, 36%), #9954bb var(--bg-gradient-end, 180%)) #7a2bdc;color:#fff}.bg-gradient-purple-blue{--bslib-color-fg: #fff;--bslib-color-bg: #4a58a5;background:linear-gradient(var(--bg-gradient-deg, 140deg), #613d7c var(--bg-gradient-start, 36%), #2780e3 var(--bg-gradient-end, 180%)) #4a58a5;color:#fff}.bg-gradient-purple-indigo{--bslib-color-fg: #fff;--bslib-color-bg: #632bab;background:linear-gradient(var(--bg-gradient-deg, 140deg), #613d7c var(--bg-gradient-start, 36%), #6610f2 var(--bg-gradient-end, 180%)) #632bab;color:#fff}.bg-gradient-purple-pink{--bslib-color-fg: #fff;--bslib-color-bg: #973d82;background:linear-gradient(var(--bg-gradient-deg, 140deg), #613d7c var(--bg-gradient-start, 36%), #e83e8c var(--bg-gradient-end, 180%)) #973d82;color:#fff}.bg-gradient-purple-red{--bslib-color-fg: #fff;--bslib-color-bg: #a02561;background:linear-gradient(var(--bg-gradient-deg, 140deg), #613d7c var(--bg-gradient-start, 36%), #ff0039 var(--bg-gradient-end, 180%)) #a02561;color:#fff}.bg-gradient-purple-orange{--bslib-color-fg: #fff;--bslib-color-bg: #9a6a6a;background:linear-gradient(var(--bg-gradient-deg, 140deg), #613d7c var(--bg-gradient-start, 36%), #f0ad4e var(--bg-gradient-end, 180%)) #9a6a6a;color:#fff}.bg-gradient-purple-yellow{--bslib-color-fg: #fff;--bslib-color-bg: #a05354;background:linear-gradient(var(--bg-gradient-deg, 140deg), #613d7c var(--bg-gradient-start, 36%), #ff7518 var(--bg-gradient-end, 180%)) #a05354;color:#fff}.bg-gradient-purple-green{--bslib-color-fg: #fff;--bslib-color-bg: #536d54;background:linear-gradient(var(--bg-gradient-deg, 140deg), #613d7c var(--bg-gradient-start, 36%), #3fb618 var(--bg-gradient-end, 180%)) #536d54;color:#fff}.bg-gradient-purple-teal{--bslib-color-fg: #fff;--bslib-color-bg: #477587;background:linear-gradient(var(--bg-gradient-deg, 140deg), #613d7c var(--bg-gradient-start, 36%), #20c997 var(--bg-gradient-end, 180%)) #477587;color:#fff}.bg-gradient-purple-cyan{--bslib-color-fg: #fff;--bslib-color-bg: #774695;background:linear-gradient(var(--bg-gradient-deg, 140deg), #613d7c var(--bg-gradient-start, 36%), #9954bb var(--bg-gradient-end, 180%)) #774695;color:#fff}.bg-gradient-pink-blue{--bslib-color-fg: #fff;--bslib-color-bg: #9b58af;background:linear-gradient(var(--bg-gradient-deg, 140deg), #e83e8c var(--bg-gradient-start, 36%), #2780e3 var(--bg-gradient-end, 180%)) #9b58af;color:#fff}.bg-gradient-pink-indigo{--bslib-color-fg: #fff;--bslib-color-bg: #b42cb5;background:linear-gradient(var(--bg-gradient-deg, 140deg), #e83e8c var(--bg-gradient-start, 36%), #6610f2 var(--bg-gradient-end, 180%)) #b42cb5;color:#fff}.bg-gradient-pink-purple{--bslib-color-fg: #fff;--bslib-color-bg: #b23e86;background:linear-gradient(var(--bg-gradient-deg, 140deg), #e83e8c var(--bg-gradient-start, 36%), #613d7c var(--bg-gradient-end, 180%)) #b23e86;color:#fff}.bg-gradient-pink-red{--bslib-color-fg: #fff;--bslib-color-bg: #f1256b;background:linear-gradient(var(--bg-gradient-deg, 140deg), #e83e8c var(--bg-gradient-start, 36%), #ff0039 var(--bg-gradient-end, 180%)) #f1256b;color:#fff}.bg-gradient-pink-orange{--bslib-color-fg: #fff;--bslib-color-bg: #eb6a73;background:linear-gradient(var(--bg-gradient-deg, 140deg), #e83e8c var(--bg-gradient-start, 36%), #f0ad4e var(--bg-gradient-end, 180%)) #eb6a73;color:#fff}.bg-gradient-pink-yellow{--bslib-color-fg: #fff;--bslib-color-bg: #f1545e;background:linear-gradient(var(--bg-gradient-deg, 140deg), #e83e8c var(--bg-gradient-start, 36%), #ff7518 var(--bg-gradient-end, 180%)) #f1545e;color:#fff}.bg-gradient-pink-green{--bslib-color-fg: #fff;--bslib-color-bg: #a46e5e;background:linear-gradient(var(--bg-gradient-deg, 140deg), #e83e8c var(--bg-gradient-start, 36%), #3fb618 var(--bg-gradient-end, 180%)) #a46e5e;color:#fff}.bg-gradient-pink-teal{--bslib-color-fg: #fff;--bslib-color-bg: #987690;background:linear-gradient(var(--bg-gradient-deg, 140deg), #e83e8c var(--bg-gradient-start, 36%), #20c997 var(--bg-gradient-end, 180%)) #987690;color:#fff}.bg-gradient-pink-cyan{--bslib-color-fg: #fff;--bslib-color-bg: #c8479f;background:linear-gradient(var(--bg-gradient-deg, 140deg), #e83e8c var(--bg-gradient-start, 36%), #9954bb var(--bg-gradient-end, 180%)) #c8479f;color:#fff}.bg-gradient-red-blue{--bslib-color-fg: #fff;--bslib-color-bg: #a9337d;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff0039 var(--bg-gradient-start, 36%), #2780e3 var(--bg-gradient-end, 180%)) #a9337d;color:#fff}.bg-gradient-red-indigo{--bslib-color-fg: #fff;--bslib-color-bg: #c20683;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff0039 var(--bg-gradient-start, 36%), #6610f2 var(--bg-gradient-end, 180%)) #c20683;color:#fff}.bg-gradient-red-purple{--bslib-color-fg: #fff;--bslib-color-bg: #c01854;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff0039 var(--bg-gradient-start, 36%), #613d7c var(--bg-gradient-end, 180%)) #c01854;color:#fff}.bg-gradient-red-pink{--bslib-color-fg: #fff;--bslib-color-bg: #f6195a;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff0039 var(--bg-gradient-start, 36%), #e83e8c var(--bg-gradient-end, 180%)) #f6195a;color:#fff}.bg-gradient-red-orange{--bslib-color-fg: #fff;--bslib-color-bg: #f94541;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff0039 var(--bg-gradient-start, 36%), #f0ad4e var(--bg-gradient-end, 180%)) #f94541;color:#fff}.bg-gradient-red-yellow{--bslib-color-fg: #fff;--bslib-color-bg: #ff2f2c;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff0039 var(--bg-gradient-start, 36%), #ff7518 var(--bg-gradient-end, 180%)) #ff2f2c;color:#fff}.bg-gradient-red-green{--bslib-color-fg: #fff;--bslib-color-bg: #b2492c;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff0039 var(--bg-gradient-start, 36%), #3fb618 var(--bg-gradient-end, 180%)) #b2492c;color:#fff}.bg-gradient-red-teal{--bslib-color-fg: #fff;--bslib-color-bg: #a6505f;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff0039 var(--bg-gradient-start, 36%), #20c997 var(--bg-gradient-end, 180%)) #a6505f;color:#fff}.bg-gradient-red-cyan{--bslib-color-fg: #fff;--bslib-color-bg: #d6226d;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff0039 var(--bg-gradient-start, 36%), #9954bb var(--bg-gradient-end, 180%)) #d6226d;color:#fff}.bg-gradient-orange-blue{--bslib-color-fg: #fff;--bslib-color-bg: #a09b8a;background:linear-gradient(var(--bg-gradient-deg, 140deg), #f0ad4e var(--bg-gradient-start, 36%), #2780e3 var(--bg-gradient-end, 180%)) #a09b8a;color:#fff}.bg-gradient-orange-indigo{--bslib-color-fg: #fff;--bslib-color-bg: #b96e90;background:linear-gradient(var(--bg-gradient-deg, 140deg), #f0ad4e var(--bg-gradient-start, 36%), #6610f2 var(--bg-gradient-end, 180%)) #b96e90;color:#fff}.bg-gradient-orange-purple{--bslib-color-fg: #fff;--bslib-color-bg: #b78060;background:linear-gradient(var(--bg-gradient-deg, 140deg), #f0ad4e var(--bg-gradient-start, 36%), #613d7c var(--bg-gradient-end, 180%)) #b78060;color:#fff}.bg-gradient-orange-pink{--bslib-color-fg: #fff;--bslib-color-bg: #ed8167;background:linear-gradient(var(--bg-gradient-deg, 140deg), #f0ad4e var(--bg-gradient-start, 36%), #e83e8c var(--bg-gradient-end, 180%)) #ed8167;color:#fff}.bg-gradient-orange-red{--bslib-color-fg: #fff;--bslib-color-bg: #f66846;background:linear-gradient(var(--bg-gradient-deg, 140deg), #f0ad4e var(--bg-gradient-start, 36%), #ff0039 var(--bg-gradient-end, 180%)) #f66846;color:#fff}.bg-gradient-orange-yellow{--bslib-color-fg: #000;--bslib-color-bg: #f69738;background:linear-gradient(var(--bg-gradient-deg, 140deg), #f0ad4e var(--bg-gradient-start, 36%), #ff7518 var(--bg-gradient-end, 180%)) #f69738;color:#000}.bg-gradient-orange-green{--bslib-color-fg: #000;--bslib-color-bg: #a9b138;background:linear-gradient(var(--bg-gradient-deg, 140deg), #f0ad4e var(--bg-gradient-start, 36%), #3fb618 var(--bg-gradient-end, 180%)) #a9b138;color:#000}.bg-gradient-orange-teal{--bslib-color-fg: #000;--bslib-color-bg: #9db86b;background:linear-gradient(var(--bg-gradient-deg, 140deg), #f0ad4e var(--bg-gradient-start, 36%), #20c997 var(--bg-gradient-end, 180%)) #9db86b;color:#000}.bg-gradient-orange-cyan{--bslib-color-fg: #fff;--bslib-color-bg: #cd897a;background:linear-gradient(var(--bg-gradient-deg, 140deg), #f0ad4e var(--bg-gradient-start, 36%), #9954bb var(--bg-gradient-end, 180%)) #cd897a;color:#fff}.bg-gradient-yellow-blue{--bslib-color-fg: #fff;--bslib-color-bg: #a97969;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff7518 var(--bg-gradient-start, 36%), #2780e3 var(--bg-gradient-end, 180%)) #a97969;color:#fff}.bg-gradient-yellow-indigo{--bslib-color-fg: #fff;--bslib-color-bg: #c24d6f;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff7518 var(--bg-gradient-start, 36%), #6610f2 var(--bg-gradient-end, 180%)) #c24d6f;color:#fff}.bg-gradient-yellow-purple{--bslib-color-fg: #fff;--bslib-color-bg: #c05f40;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff7518 var(--bg-gradient-start, 36%), #613d7c var(--bg-gradient-end, 180%)) #c05f40;color:#fff}.bg-gradient-yellow-pink{--bslib-color-fg: #fff;--bslib-color-bg: #f65f46;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff7518 var(--bg-gradient-start, 36%), #e83e8c var(--bg-gradient-end, 180%)) #f65f46;color:#fff}.bg-gradient-yellow-red{--bslib-color-fg: #fff;--bslib-color-bg: #ff4625;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff7518 var(--bg-gradient-start, 36%), #ff0039 var(--bg-gradient-end, 180%)) #ff4625;color:#fff}.bg-gradient-yellow-orange{--bslib-color-fg: #000;--bslib-color-bg: #f98b2e;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff7518 var(--bg-gradient-start, 36%), #f0ad4e var(--bg-gradient-end, 180%)) #f98b2e;color:#000}.bg-gradient-yellow-green{--bslib-color-fg: #fff;--bslib-color-bg: #b28f18;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff7518 var(--bg-gradient-start, 36%), #3fb618 var(--bg-gradient-end, 180%)) #b28f18;color:#fff}.bg-gradient-yellow-teal{--bslib-color-fg: #fff;--bslib-color-bg: #a6974b;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff7518 var(--bg-gradient-start, 36%), #20c997 var(--bg-gradient-end, 180%)) #a6974b;color:#fff}.bg-gradient-yellow-cyan{--bslib-color-fg: #fff;--bslib-color-bg: #d66859;background:linear-gradient(var(--bg-gradient-deg, 140deg), #ff7518 var(--bg-gradient-start, 36%), #9954bb var(--bg-gradient-end, 180%)) #d66859;color:#fff}.bg-gradient-green-blue{--bslib-color-fg: #fff;--bslib-color-bg: #35a069;background:linear-gradient(var(--bg-gradient-deg, 140deg), #3fb618 var(--bg-gradient-start, 36%), #2780e3 var(--bg-gradient-end, 180%)) #35a069;color:#fff}.bg-gradient-green-indigo{--bslib-color-fg: #fff;--bslib-color-bg: #4f746f;background:linear-gradient(var(--bg-gradient-deg, 140deg), #3fb618 var(--bg-gradient-start, 36%), #6610f2 var(--bg-gradient-end, 180%)) #4f746f;color:#fff}.bg-gradient-green-purple{--bslib-color-fg: #fff;--bslib-color-bg: #4d8640;background:linear-gradient(var(--bg-gradient-deg, 140deg), #3fb618 var(--bg-gradient-start, 36%), #613d7c var(--bg-gradient-end, 180%)) #4d8640;color:#fff}.bg-gradient-green-pink{--bslib-color-fg: #fff;--bslib-color-bg: #838646;background:linear-gradient(var(--bg-gradient-deg, 140deg), #3fb618 var(--bg-gradient-start, 36%), #e83e8c var(--bg-gradient-end, 180%)) #838646;color:#fff}.bg-gradient-green-red{--bslib-color-fg: #fff;--bslib-color-bg: #8c6d25;background:linear-gradient(var(--bg-gradient-deg, 140deg), #3fb618 var(--bg-gradient-start, 36%), #ff0039 var(--bg-gradient-end, 180%)) #8c6d25;color:#fff}.bg-gradient-green-orange{--bslib-color-fg: #000;--bslib-color-bg: #86b22e;background:linear-gradient(var(--bg-gradient-deg, 140deg), #3fb618 var(--bg-gradient-start, 36%), #f0ad4e var(--bg-gradient-end, 180%)) #86b22e;color:#000}.bg-gradient-green-yellow{--bslib-color-fg: #fff;--bslib-color-bg: #8c9c18;background:linear-gradient(var(--bg-gradient-deg, 140deg), #3fb618 var(--bg-gradient-start, 36%), #ff7518 var(--bg-gradient-end, 180%)) #8c9c18;color:#fff}.bg-gradient-green-teal{--bslib-color-fg: #000;--bslib-color-bg: #33be4b;background:linear-gradient(var(--bg-gradient-deg, 140deg), #3fb618 var(--bg-gradient-start, 36%), #20c997 var(--bg-gradient-end, 180%)) #33be4b;color:#000}.bg-gradient-green-cyan{--bslib-color-fg: #fff;--bslib-color-bg: #638f59;background:linear-gradient(var(--bg-gradient-deg, 140deg), #3fb618 var(--bg-gradient-start, 36%), #9954bb var(--bg-gradient-end, 180%)) #638f59;color:#fff}.bg-gradient-teal-blue{--bslib-color-fg: #fff;--bslib-color-bg: #23acb5;background:linear-gradient(var(--bg-gradient-deg, 140deg), #20c997 var(--bg-gradient-start, 36%), #2780e3 var(--bg-gradient-end, 180%)) #23acb5;color:#fff}.bg-gradient-teal-indigo{--bslib-color-fg: #fff;--bslib-color-bg: #3c7fbb;background:linear-gradient(var(--bg-gradient-deg, 140deg), #20c997 var(--bg-gradient-start, 36%), #6610f2 var(--bg-gradient-end, 180%)) #3c7fbb;color:#fff}.bg-gradient-teal-purple{--bslib-color-fg: #fff;--bslib-color-bg: #3a918c;background:linear-gradient(var(--bg-gradient-deg, 140deg), #20c997 var(--bg-gradient-start, 36%), #613d7c var(--bg-gradient-end, 180%)) #3a918c;color:#fff}.bg-gradient-teal-pink{--bslib-color-fg: #fff;--bslib-color-bg: #709193;background:linear-gradient(var(--bg-gradient-deg, 140deg), #20c997 var(--bg-gradient-start, 36%), #e83e8c var(--bg-gradient-end, 180%)) #709193;color:#fff}.bg-gradient-teal-red{--bslib-color-fg: #fff;--bslib-color-bg: #797971;background:linear-gradient(var(--bg-gradient-deg, 140deg), #20c997 var(--bg-gradient-start, 36%), #ff0039 var(--bg-gradient-end, 180%)) #797971;color:#fff}.bg-gradient-teal-orange{--bslib-color-fg: #000;--bslib-color-bg: #73be7a;background:linear-gradient(var(--bg-gradient-deg, 140deg), #20c997 var(--bg-gradient-start, 36%), #f0ad4e var(--bg-gradient-end, 180%)) #73be7a;color:#000}.bg-gradient-teal-yellow{--bslib-color-fg: #fff;--bslib-color-bg: #79a764;background:linear-gradient(var(--bg-gradient-deg, 140deg), #20c997 var(--bg-gradient-start, 36%), #ff7518 var(--bg-gradient-end, 180%)) #79a764;color:#fff}.bg-gradient-teal-green{--bslib-color-fg: #000;--bslib-color-bg: #2cc164;background:linear-gradient(var(--bg-gradient-deg, 140deg), #20c997 var(--bg-gradient-start, 36%), #3fb618 var(--bg-gradient-end, 180%)) #2cc164;color:#000}.bg-gradient-teal-cyan{--bslib-color-fg: #fff;--bslib-color-bg: #509aa5;background:linear-gradient(var(--bg-gradient-deg, 140deg), #20c997 var(--bg-gradient-start, 36%), #9954bb var(--bg-gradient-end, 180%)) #509aa5;color:#fff}.bg-gradient-cyan-blue{--bslib-color-fg: #fff;--bslib-color-bg: #6b66cb;background:linear-gradient(var(--bg-gradient-deg, 140deg), #9954bb var(--bg-gradient-start, 36%), #2780e3 var(--bg-gradient-end, 180%)) #6b66cb;color:#fff}.bg-gradient-cyan-indigo{--bslib-color-fg: #fff;--bslib-color-bg: #8539d1;background:linear-gradient(var(--bg-gradient-deg, 140deg), #9954bb var(--bg-gradient-start, 36%), #6610f2 var(--bg-gradient-end, 180%)) #8539d1;color:#fff}.bg-gradient-cyan-purple{--bslib-color-fg: #fff;--bslib-color-bg: #834ba2;background:linear-gradient(var(--bg-gradient-deg, 140deg), #9954bb var(--bg-gradient-start, 36%), #613d7c var(--bg-gradient-end, 180%)) #834ba2;color:#fff}.bg-gradient-cyan-pink{--bslib-color-fg: #fff;--bslib-color-bg: #b94ba8;background:linear-gradient(var(--bg-gradient-deg, 140deg), #9954bb var(--bg-gradient-start, 36%), #e83e8c var(--bg-gradient-end, 180%)) #b94ba8;color:#fff}.bg-gradient-cyan-red{--bslib-color-fg: #fff;--bslib-color-bg: #c23287;background:linear-gradient(var(--bg-gradient-deg, 140deg), #9954bb var(--bg-gradient-start, 36%), #ff0039 var(--bg-gradient-end, 180%)) #c23287;color:#fff}.bg-gradient-cyan-orange{--bslib-color-fg: #fff;--bslib-color-bg: #bc788f;background:linear-gradient(var(--bg-gradient-deg, 140deg), #9954bb var(--bg-gradient-start, 36%), #f0ad4e var(--bg-gradient-end, 180%)) #bc788f;color:#fff}.bg-gradient-cyan-yellow{--bslib-color-fg: #fff;--bslib-color-bg: #c2617a;background:linear-gradient(var(--bg-gradient-deg, 140deg), #9954bb var(--bg-gradient-start, 36%), #ff7518 var(--bg-gradient-end, 180%)) #c2617a;color:#fff}.bg-gradient-cyan-green{--bslib-color-fg: #fff;--bslib-color-bg: #757b7a;background:linear-gradient(var(--bg-gradient-deg, 140deg), #9954bb var(--bg-gradient-start, 36%), #3fb618 var(--bg-gradient-end, 180%)) #757b7a;color:#fff}.bg-gradient-cyan-teal{--bslib-color-fg: #fff;--bslib-color-bg: #6983ad;background:linear-gradient(var(--bg-gradient-deg, 140deg), #9954bb var(--bg-gradient-start, 36%), #20c997 var(--bg-gradient-end, 180%)) #6983ad;color:#fff}:root{--bslib-spacer: 1rem;--bslib-mb-spacer: var(--bslib-spacer, 1rem)}.bslib-mb-spacing{margin-bottom:var(--bslib-mb-spacer)}.bslib-gap-spacing{gap:var(--bslib-mb-spacer)}.bslib-gap-spacing>.bslib-mb-spacing,.bslib-gap-spacing>.form-group,.bslib-gap-spacing>p,.bslib-gap-spacing>pre{margin-bottom:0}.html-fill-container>.html-fill-item.bslib-mb-spacing{margin-bottom:0}.tab-content>.tab-pane.html-fill-container{display:none}.tab-content>.active.html-fill-container{display:flex}.tab-content.html-fill-container{padding:0}html{height:100%}.bslib-page-fill{width:100%;height:100%;margin:0;padding:var(--bslib-spacer, 1rem);gap:var(--bslib-spacer, 1rem)}@media(max-width: 575.98px){.bslib-page-fill{height:var(--bslib-page-fill-mobile-height, auto)}}.bslib-grid{display:grid !important;gap:var(--bslib-spacer, 1rem);height:var(--bslib-grid-height)}.bslib-grid.grid{grid-template-columns:repeat(var(--bs-columns, 12), minmax(0, 1fr));grid-template-rows:unset;grid-auto-rows:var(--bslib-grid--row-heights);--bslib-grid--row-heights--xs: unset;--bslib-grid--row-heights--sm: unset;--bslib-grid--row-heights--md: unset;--bslib-grid--row-heights--lg: unset;--bslib-grid--row-heights--xl: unset;--bslib-grid--row-heights--xxl: unset}.bslib-grid.grid.bslib-grid--row-heights--xs{--bslib-grid--row-heights: var(--bslib-grid--row-heights--xs)}@media(min-width: 576px){.bslib-grid.grid.bslib-grid--row-heights--sm{--bslib-grid--row-heights: var(--bslib-grid--row-heights--sm)}}@media(min-width: 768px){.bslib-grid.grid.bslib-grid--row-heights--md{--bslib-grid--row-heights: var(--bslib-grid--row-heights--md)}}@media(min-width: 992px){.bslib-grid.grid.bslib-grid--row-heights--lg{--bslib-grid--row-heights: var(--bslib-grid--row-heights--lg)}}@media(min-width: 1200px){.bslib-grid.grid.bslib-grid--row-heights--xl{--bslib-grid--row-heights: var(--bslib-grid--row-heights--xl)}}@media(min-width: 1400px){.bslib-grid.grid.bslib-grid--row-heights--xxl{--bslib-grid--row-heights: var(--bslib-grid--row-heights--xxl)}}.bslib-grid>*>.shiny-input-container{width:100%}.bslib-grid-item{grid-column:auto/span 1}@media(max-width: 767.98px){.bslib-grid-item{grid-column:1/-1}}@media(max-width: 575.98px){.bslib-grid{grid-template-columns:1fr !important;height:var(--bslib-grid-height-mobile)}.bslib-grid.grid{height:unset !important;grid-auto-rows:var(--bslib-grid--row-heights--xs, auto)}}.navbar+.container-fluid:has(>.tab-content>.tab-pane.active.html-fill-container),.navbar+.container-sm:has(>.tab-content>.tab-pane.active.html-fill-container),.navbar+.container-md:has(>.tab-content>.tab-pane.active.html-fill-container),.navbar+.container-lg:has(>.tab-content>.tab-pane.active.html-fill-container),.navbar+.container-xl:has(>.tab-content>.tab-pane.active.html-fill-container),.navbar+.container-xxl:has(>.tab-content>.tab-pane.active.html-fill-container){padding-left:0;padding-right:0}.navbar+.container-fluid>.tab-content>.tab-pane.active.html-fill-container,.navbar+.container-sm>.tab-content>.tab-pane.active.html-fill-container,.navbar+.container-md>.tab-content>.tab-pane.active.html-fill-container,.navbar+.container-lg>.tab-content>.tab-pane.active.html-fill-container,.navbar+.container-xl>.tab-content>.tab-pane.active.html-fill-container,.navbar+.container-xxl>.tab-content>.tab-pane.active.html-fill-container{padding:var(--bslib-spacer, 1rem);gap:var(--bslib-spacer, 1rem)}.navbar+.container-fluid>.tab-content>.tab-pane.active.html-fill-container:has(>.bslib-sidebar-layout:only-child),.navbar+.container-sm>.tab-content>.tab-pane.active.html-fill-container:has(>.bslib-sidebar-layout:only-child),.navbar+.container-md>.tab-content>.tab-pane.active.html-fill-container:has(>.bslib-sidebar-layout:only-child),.navbar+.container-lg>.tab-content>.tab-pane.active.html-fill-container:has(>.bslib-sidebar-layout:only-child),.navbar+.container-xl>.tab-content>.tab-pane.active.html-fill-container:has(>.bslib-sidebar-layout:only-child),.navbar+.container-xxl>.tab-content>.tab-pane.active.html-fill-container:has(>.bslib-sidebar-layout:only-child){padding:0}.navbar+.container-fluid>.tab-content>.tab-pane.active.html-fill-container>.bslib-sidebar-layout:only-child:not([data-bslib-sidebar-border=true]),.navbar+.container-sm>.tab-content>.tab-pane.active.html-fill-container>.bslib-sidebar-layout:only-child:not([data-bslib-sidebar-border=true]),.navbar+.container-md>.tab-content>.tab-pane.active.html-fill-container>.bslib-sidebar-layout:only-child:not([data-bslib-sidebar-border=true]),.navbar+.container-lg>.tab-content>.tab-pane.active.html-fill-container>.bslib-sidebar-layout:only-child:not([data-bslib-sidebar-border=true]),.navbar+.container-xl>.tab-content>.tab-pane.active.html-fill-container>.bslib-sidebar-layout:only-child:not([data-bslib-sidebar-border=true]),.navbar+.container-xxl>.tab-content>.tab-pane.active.html-fill-container>.bslib-sidebar-layout:only-child:not([data-bslib-sidebar-border=true]){border-left:none;border-right:none;border-bottom:none}.navbar+.container-fluid>.tab-content>.tab-pane.active.html-fill-container>.bslib-sidebar-layout:only-child:not([data-bslib-sidebar-border-radius=true]),.navbar+.container-sm>.tab-content>.tab-pane.active.html-fill-container>.bslib-sidebar-layout:only-child:not([data-bslib-sidebar-border-radius=true]),.navbar+.container-md>.tab-content>.tab-pane.active.html-fill-container>.bslib-sidebar-layout:only-child:not([data-bslib-sidebar-border-radius=true]),.navbar+.container-lg>.tab-content>.tab-pane.active.html-fill-container>.bslib-sidebar-layout:only-child:not([data-bslib-sidebar-border-radius=true]),.navbar+.container-xl>.tab-content>.tab-pane.active.html-fill-container>.bslib-sidebar-layout:only-child:not([data-bslib-sidebar-border-radius=true]),.navbar+.container-xxl>.tab-content>.tab-pane.active.html-fill-container>.bslib-sidebar-layout:only-child:not([data-bslib-sidebar-border-radius=true]){border-radius:0}.navbar+div>.bslib-sidebar-layout{border-top:var(--bslib-sidebar-border)}.bslib-card{overflow:auto}.bslib-card .card-body+.card-body{padding-top:0}.bslib-card .card-body{overflow:auto}.bslib-card .card-body p{margin-top:0}.bslib-card .card-body p:last-child{margin-bottom:0}.bslib-card .card-body{max-height:var(--bslib-card-body-max-height, none)}.bslib-card[data-full-screen=true]>.card-body{max-height:var(--bslib-card-body-max-height-full-screen, none)}.bslib-card .card-header .form-group{margin-bottom:0}.bslib-card .card-header .selectize-control{margin-bottom:0}.bslib-card .card-header .selectize-control .item{margin-right:1.15rem}.bslib-card .card-footer{margin-top:auto}.bslib-card .bslib-navs-card-title{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:center}.bslib-card .bslib-navs-card-title .nav{margin-left:auto}.bslib-card .bslib-sidebar-layout:not([data-bslib-sidebar-border=true]){border:none}.bslib-card .bslib-sidebar-layout:not([data-bslib-sidebar-border-radius=true]){border-top-left-radius:0;border-top-right-radius:0}[data-full-screen=true]{position:fixed;inset:3.5rem 1rem 1rem;height:auto !important;max-height:none !important;width:auto !important;z-index:1070}.bslib-full-screen-enter{display:none;position:absolute;bottom:var(--bslib-full-screen-enter-bottom, 0.2rem);right:var(--bslib-full-screen-enter-right, 0);top:var(--bslib-full-screen-enter-top);left:var(--bslib-full-screen-enter-left);color:var(--bslib-color-fg, var(--bs-card-color));background-color:var(--bslib-color-bg, var(--bs-card-bg, var(--bs-body-bg)));border:var(--bs-card-border-width) solid var(--bslib-color-fg, var(--bs-card-border-color));box-shadow:0 2px 4px rgba(0,0,0,.15);margin:.2rem .4rem;padding:.55rem !important;font-size:.8rem;cursor:pointer;opacity:.7;z-index:1070}.bslib-full-screen-enter:hover{opacity:1}.card[data-full-screen=false]:hover>*>.bslib-full-screen-enter{display:block}.bslib-has-full-screen .card:hover>*>.bslib-full-screen-enter{display:none}@media(max-width: 575.98px){.bslib-full-screen-enter{display:none !important}}.bslib-full-screen-exit{position:relative;top:1.35rem;font-size:.9rem;cursor:pointer;text-decoration:none;display:flex;float:right;margin-right:2.15rem;align-items:center;color:rgba(var(--bs-body-bg-rgb), 0.8)}.bslib-full-screen-exit:hover{color:rgba(var(--bs-body-bg-rgb), 1)}.bslib-full-screen-exit svg{margin-left:.5rem;font-size:1.5rem}#bslib-full-screen-overlay{position:fixed;inset:0;background-color:rgba(var(--bs-body-color-rgb), 0.6);backdrop-filter:blur(2px);-webkit-backdrop-filter:blur(2px);z-index:1069;animation:bslib-full-screen-overlay-enter 400ms cubic-bezier(0.6, 0.02, 0.65, 1) forwards}@keyframes bslib-full-screen-overlay-enter{0%{opacity:0}100%{opacity:1}}.accordion .accordion-header{font-size:calc(1.29rem + 0.48vw);margin-top:0;margin-bottom:.5rem;font-weight:400;line-height:1.2;color:var(--bs-heading-color);margin-bottom:0}@media(min-width: 1200px){.accordion .accordion-header{font-size:1.65rem}}.accordion .accordion-icon:not(:empty){margin-right:.75rem;display:flex}.accordion .accordion-button:not(.collapsed){box-shadow:none}.accordion .accordion-button:not(.collapsed):focus{box-shadow:var(--bs-accordion-btn-focus-box-shadow)}.bslib-sidebar-layout{--bslib-sidebar-transition-duration: 500ms;--bslib-sidebar-transition-easing-x: cubic-bezier(0.8, 0.78, 0.22, 1.07);--bslib-sidebar-border: var(--bs-card-border-width, 1px) solid var(--bs-card-border-color, rgba(0, 0, 0, 0.175));--bslib-sidebar-border-radius: var(--bs-border-radius);--bslib-sidebar-vert-border: var(--bs-card-border-width, 1px) solid var(--bs-card-border-color, rgba(0, 0, 0, 0.175));--bslib-sidebar-bg: rgba(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.05);--bslib-sidebar-fg: var(--bs-emphasis-color, black);--bslib-sidebar-main-fg: var(--bs-card-color, var(--bs-body-color));--bslib-sidebar-main-bg: var(--bs-card-bg, var(--bs-body-bg));--bslib-sidebar-toggle-bg: rgba(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.1);--bslib-sidebar-padding: calc(var(--bslib-spacer) * 1.5);--bslib-sidebar-icon-size: var(--bslib-spacer, 1rem);--bslib-sidebar-icon-button-size: calc(var(--bslib-sidebar-icon-size, 1rem) * 2);--bslib-sidebar-padding-icon: calc(var(--bslib-sidebar-icon-button-size, 2rem) * 1.5);--bslib-collapse-toggle-border-radius: var(--bs-border-radius, 0.25rem);--bslib-collapse-toggle-transform: 0deg;--bslib-sidebar-toggle-transition-easing: cubic-bezier(1, 0, 0, 1);--bslib-collapse-toggle-right-transform: 180deg;--bslib-sidebar-column-main: minmax(0, 1fr);display:grid !important;grid-template-columns:min(100% - var(--bslib-sidebar-icon-size),var(--bslib-sidebar-width, 250px)) var(--bslib-sidebar-column-main);position:relative;transition:grid-template-columns ease-in-out var(--bslib-sidebar-transition-duration);border:var(--bslib-sidebar-border);border-radius:var(--bslib-sidebar-border-radius)}@media(prefers-reduced-motion: reduce){.bslib-sidebar-layout{transition:none}}.bslib-sidebar-layout[data-bslib-sidebar-border=false]{border:none}.bslib-sidebar-layout[data-bslib-sidebar-border-radius=false]{border-radius:initial}.bslib-sidebar-layout>.main,.bslib-sidebar-layout>.sidebar{grid-row:1/2;border-radius:inherit;overflow:auto}.bslib-sidebar-layout>.main{grid-column:2/3;border-top-left-radius:0;border-bottom-left-radius:0;padding:var(--bslib-sidebar-padding);transition:padding var(--bslib-sidebar-transition-easing-x) var(--bslib-sidebar-transition-duration);color:var(--bslib-sidebar-main-fg);background-color:var(--bslib-sidebar-main-bg)}.bslib-sidebar-layout>.sidebar{grid-column:1/2;width:100%;height:100%;border-right:var(--bslib-sidebar-vert-border);border-top-right-radius:0;border-bottom-right-radius:0;color:var(--bslib-sidebar-fg);background-color:var(--bslib-sidebar-bg);backdrop-filter:blur(5px)}.bslib-sidebar-layout>.sidebar>.sidebar-content{display:flex;flex-direction:column;gap:var(--bslib-spacer, 1rem);padding:var(--bslib-sidebar-padding);padding-top:var(--bslib-sidebar-padding-icon)}.bslib-sidebar-layout>.sidebar>.sidebar-content>:last-child:not(.sidebar-title){margin-bottom:0}.bslib-sidebar-layout>.sidebar>.sidebar-content>.accordion{margin-left:calc(-1*var(--bslib-sidebar-padding));margin-right:calc(-1*var(--bslib-sidebar-padding))}.bslib-sidebar-layout>.sidebar>.sidebar-content>.accordion:last-child{margin-bottom:calc(-1*var(--bslib-sidebar-padding))}.bslib-sidebar-layout>.sidebar>.sidebar-content>.accordion:not(:last-child){margin-bottom:1rem}.bslib-sidebar-layout>.sidebar>.sidebar-content>.accordion .accordion-body{display:flex;flex-direction:column}.bslib-sidebar-layout>.sidebar>.sidebar-content>.accordion:not(:first-child) .accordion-item:first-child{border-top:var(--bs-accordion-border-width) solid var(--bs-accordion-border-color)}.bslib-sidebar-layout>.sidebar>.sidebar-content>.accordion:not(:last-child) .accordion-item:last-child{border-bottom:var(--bs-accordion-border-width) solid var(--bs-accordion-border-color)}.bslib-sidebar-layout>.sidebar>.sidebar-content.has-accordion>.sidebar-title{border-bottom:none;padding-bottom:0}.bslib-sidebar-layout>.sidebar .shiny-input-container{width:100%}.bslib-sidebar-layout[data-bslib-sidebar-open=always]>.sidebar>.sidebar-content{padding-top:var(--bslib-sidebar-padding)}.bslib-sidebar-layout>.collapse-toggle{grid-row:1/2;grid-column:1/2;display:inline-flex;align-items:center;position:absolute;right:calc(var(--bslib-sidebar-icon-size));top:calc(var(--bslib-sidebar-icon-size, 1rem)/2);border:none;border-radius:var(--bslib-collapse-toggle-border-radius);height:var(--bslib-sidebar-icon-button-size, 2rem);width:var(--bslib-sidebar-icon-button-size, 2rem);display:flex;align-items:center;justify-content:center;padding:0;color:var(--bslib-sidebar-fg);background-color:unset;transition:color var(--bslib-sidebar-transition-easing-x) var(--bslib-sidebar-transition-duration),top var(--bslib-sidebar-transition-easing-x) var(--bslib-sidebar-transition-duration),right var(--bslib-sidebar-transition-easing-x) var(--bslib-sidebar-transition-duration),left var(--bslib-sidebar-transition-easing-x) var(--bslib-sidebar-transition-duration)}.bslib-sidebar-layout>.collapse-toggle:hover{background-color:var(--bslib-sidebar-toggle-bg)}.bslib-sidebar-layout>.collapse-toggle>.collapse-icon{opacity:.8;width:var(--bslib-sidebar-icon-size);height:var(--bslib-sidebar-icon-size);transform:rotateY(var(--bslib-collapse-toggle-transform));transition:transform var(--bslib-sidebar-toggle-transition-easing) var(--bslib-sidebar-transition-duration)}.bslib-sidebar-layout>.collapse-toggle:hover>.collapse-icon{opacity:1}.bslib-sidebar-layout .sidebar-title{font-size:1.25rem;line-height:1.25;margin-top:0;margin-bottom:1rem;padding-bottom:1rem;border-bottom:var(--bslib-sidebar-border)}.bslib-sidebar-layout.sidebar-right{grid-template-columns:var(--bslib-sidebar-column-main) min(100% - var(--bslib-sidebar-icon-size),var(--bslib-sidebar-width, 250px))}.bslib-sidebar-layout.sidebar-right>.main{grid-column:1/2;border-top-right-radius:0;border-bottom-right-radius:0;border-top-left-radius:inherit;border-bottom-left-radius:inherit}.bslib-sidebar-layout.sidebar-right>.sidebar{grid-column:2/3;border-right:none;border-left:var(--bslib-sidebar-vert-border);border-top-left-radius:0;border-bottom-left-radius:0}.bslib-sidebar-layout.sidebar-right>.collapse-toggle{grid-column:2/3;left:var(--bslib-sidebar-icon-size);right:unset;border:var(--bslib-collapse-toggle-border)}.bslib-sidebar-layout.sidebar-right>.collapse-toggle>.collapse-icon{transform:rotateY(var(--bslib-collapse-toggle-right-transform))}.bslib-sidebar-layout.sidebar-collapsed{--bslib-collapse-toggle-transform: 180deg;--bslib-collapse-toggle-right-transform: 0deg;--bslib-sidebar-vert-border: none;grid-template-columns:0 minmax(0, 1fr)}.bslib-sidebar-layout.sidebar-collapsed.sidebar-right{grid-template-columns:minmax(0, 1fr) 0}.bslib-sidebar-layout.sidebar-collapsed:not(.transitioning)>.sidebar>*{display:none}.bslib-sidebar-layout.sidebar-collapsed>.main{border-radius:inherit}.bslib-sidebar-layout.sidebar-collapsed:not(.sidebar-right)>.main{padding-left:var(--bslib-sidebar-padding-icon)}.bslib-sidebar-layout.sidebar-collapsed.sidebar-right>.main{padding-right:var(--bslib-sidebar-padding-icon)}.bslib-sidebar-layout.sidebar-collapsed>.collapse-toggle{color:var(--bslib-sidebar-main-fg);top:calc(var(--bslib-sidebar-overlap-counter, 0)*(var(--bslib-sidebar-icon-size) + var(--bslib-sidebar-padding)) + var(--bslib-sidebar-icon-size, 1rem)/2);right:calc(-2.5*var(--bslib-sidebar-icon-size) - var(--bs-card-border-width, 1px))}.bslib-sidebar-layout.sidebar-collapsed.sidebar-right>.collapse-toggle{left:calc(-2.5*var(--bslib-sidebar-icon-size) - var(--bs-card-border-width, 1px));right:unset}@media(min-width: 576px){.bslib-sidebar-layout.transitioning>.sidebar>.sidebar-content{display:none}}@media(max-width: 575.98px){.bslib-sidebar-layout[data-bslib-sidebar-open=desktop]{--bslib-sidebar-js-init-collapsed: true}.bslib-sidebar-layout>.sidebar,.bslib-sidebar-layout.sidebar-right>.sidebar{border:none}.bslib-sidebar-layout>.main,.bslib-sidebar-layout.sidebar-right>.main{grid-column:1/3}.bslib-sidebar-layout[data-bslib-sidebar-open=always]{display:block !important}.bslib-sidebar-layout[data-bslib-sidebar-open=always]>.sidebar{max-height:var(--bslib-sidebar-max-height-mobile);overflow-y:auto;border-top:var(--bslib-sidebar-vert-border)}.bslib-sidebar-layout:not([data-bslib-sidebar-open=always]){grid-template-columns:100% 0}.bslib-sidebar-layout:not([data-bslib-sidebar-open=always]):not(.sidebar-collapsed)>.sidebar{z-index:1}.bslib-sidebar-layout:not([data-bslib-sidebar-open=always]):not(.sidebar-collapsed)>.collapse-toggle{z-index:1}.bslib-sidebar-layout:not([data-bslib-sidebar-open=always]).sidebar-right{grid-template-columns:0 100%}.bslib-sidebar-layout:not([data-bslib-sidebar-open=always]).sidebar-collapsed{grid-template-columns:0 100%}.bslib-sidebar-layout:not([data-bslib-sidebar-open=always]).sidebar-collapsed.sidebar-right{grid-template-columns:100% 0}.bslib-sidebar-layout:not([data-bslib-sidebar-open=always]):not(.sidebar-right)>.main{padding-left:var(--bslib-sidebar-padding-icon)}.bslib-sidebar-layout:not([data-bslib-sidebar-open=always]).sidebar-right>.main{padding-right:var(--bslib-sidebar-padding-icon)}.bslib-sidebar-layout:not([data-bslib-sidebar-open=always])>.main{opacity:0;transition:opacity var(--bslib-sidebar-transition-easing-x) var(--bslib-sidebar-transition-duration)}.bslib-sidebar-layout:not([data-bslib-sidebar-open=always]).sidebar-collapsed>.main{opacity:1}}:root{--bslib-value-box-shadow: none;--bslib-value-box-border-width-auto-yes: var(--bslib-value-box-border-width-baseline);--bslib-value-box-border-width-auto-no: 0;--bslib-value-box-border-width-baseline: 1px}.bslib-value-box{border-width:var(--bslib-value-box-border-width-auto-no, var(--bslib-value-box-border-width-baseline));container-name:bslib-value-box;container-type:inline-size}.bslib-value-box.card{box-shadow:var(--bslib-value-box-shadow)}.bslib-value-box.border-auto{border-width:var(--bslib-value-box-border-width-auto-yes, var(--bslib-value-box-border-width-baseline))}.bslib-value-box.default{--bslib-value-box-bg-default: var(--bs-card-bg, #fff);--bslib-value-box-border-color-default: var(--bs-card-border-color, rgba(0, 0, 0, 0.175));color:var(--bslib-value-box-color);background-color:var(--bslib-value-box-bg, var(--bslib-value-box-bg-default));border-color:var(--bslib-value-box-border-color, var(--bslib-value-box-border-color-default))}.bslib-value-box .value-box-grid{display:grid;grid-template-areas:"left right";align-items:center;overflow:hidden}.bslib-value-box .value-box-showcase{height:100%;max-height:var(---bslib-value-box-showcase-max-h, 100%)}.bslib-value-box .value-box-showcase,.bslib-value-box .value-box-showcase>.html-fill-item{width:100%}.bslib-value-box[data-full-screen=true] .value-box-showcase{max-height:var(---bslib-value-box-showcase-max-h-fs, 100%)}@media screen and (min-width: 575.98px){@container bslib-value-box (max-width: 300px){.bslib-value-box:not(.showcase-bottom) .value-box-grid{grid-template-columns:1fr !important;grid-template-rows:auto auto;grid-template-areas:"top" "bottom"}.bslib-value-box:not(.showcase-bottom) .value-box-grid .value-box-showcase{grid-area:top !important}.bslib-value-box:not(.showcase-bottom) .value-box-grid .value-box-area{grid-area:bottom !important;justify-content:end}}}.bslib-value-box .value-box-area{justify-content:center;padding:1.5rem 1rem;font-size:.9rem;font-weight:500}.bslib-value-box .value-box-area *{margin-bottom:0;margin-top:0}.bslib-value-box .value-box-title{font-size:1rem;margin-top:0;margin-bottom:.5rem;font-weight:400;line-height:1.2}.bslib-value-box .value-box-title:empty::after{content:" "}.bslib-value-box .value-box-value{font-size:calc(1.29rem + 0.48vw);margin-top:0;margin-bottom:.5rem;font-weight:400;line-height:1.2}@media(min-width: 1200px){.bslib-value-box .value-box-value{font-size:1.65rem}}.bslib-value-box .value-box-value:empty::after{content:" "}.bslib-value-box .value-box-showcase{align-items:center;justify-content:center;margin-top:auto;margin-bottom:auto;padding:1rem}.bslib-value-box .value-box-showcase .bi,.bslib-value-box .value-box-showcase .fa,.bslib-value-box .value-box-showcase .fab,.bslib-value-box .value-box-showcase .fas,.bslib-value-box .value-box-showcase .far{opacity:.85;min-width:50px;max-width:125%}.bslib-value-box .value-box-showcase .bi,.bslib-value-box .value-box-showcase .fa,.bslib-value-box .value-box-showcase .fab,.bslib-value-box .value-box-showcase .fas,.bslib-value-box .value-box-showcase .far{font-size:4rem}.bslib-value-box.showcase-top-right .value-box-grid{grid-template-columns:1fr var(---bslib-value-box-showcase-w, 50%)}.bslib-value-box.showcase-top-right .value-box-grid .value-box-showcase{grid-area:right;margin-left:auto;align-self:start;align-items:end;padding-left:0;padding-bottom:0}.bslib-value-box.showcase-top-right .value-box-grid .value-box-area{grid-area:left;align-self:end}.bslib-value-box.showcase-top-right[data-full-screen=true] .value-box-grid{grid-template-columns:auto var(---bslib-value-box-showcase-w-fs, 1fr)}.bslib-value-box.showcase-top-right[data-full-screen=true] .value-box-grid>div{align-self:center}.bslib-value-box.showcase-top-right:not([data-full-screen=true]) .value-box-showcase{margin-top:0}@container bslib-value-box (max-width: 300px){.bslib-value-box.showcase-top-right:not([data-full-screen=true]) .value-box-grid .value-box-showcase{padding-left:1rem}}.bslib-value-box.showcase-left-center .value-box-grid{grid-template-columns:var(---bslib-value-box-showcase-w, 30%) auto}.bslib-value-box.showcase-left-center[data-full-screen=true] .value-box-grid{grid-template-columns:var(---bslib-value-box-showcase-w-fs, 1fr) auto}.bslib-value-box.showcase-left-center:not([data-fill-screen=true]) .value-box-grid .value-box-showcase{grid-area:left}.bslib-value-box.showcase-left-center:not([data-fill-screen=true]) .value-box-grid .value-box-area{grid-area:right}.bslib-value-box.showcase-bottom .value-box-grid{grid-template-columns:1fr;grid-template-rows:1fr var(---bslib-value-box-showcase-h, auto);grid-template-areas:"top" "bottom";overflow:hidden}.bslib-value-box.showcase-bottom .value-box-grid .value-box-showcase{grid-area:bottom;padding:0;margin:0}.bslib-value-box.showcase-bottom .value-box-grid .value-box-area{grid-area:top}.bslib-value-box.showcase-bottom[data-full-screen=true] .value-box-grid{grid-template-rows:1fr var(---bslib-value-box-showcase-h-fs, 2fr)}.bslib-value-box.showcase-bottom[data-full-screen=true] .value-box-grid .value-box-showcase{padding:1rem}[data-bs-theme=dark] .bslib-value-box{--bslib-value-box-shadow: 0 0.5rem 1rem rgb(0 0 0 / 50%)}:root{--bslib-page-sidebar-title-bg: #2780e3;--bslib-page-sidebar-title-color: #fff}.bslib-page-title{background-color:var(--bslib-page-sidebar-title-bg);color:var(--bslib-page-sidebar-title-color);font-size:1.25rem;font-weight:300;padding:var(--bslib-spacer, 1rem);padding-left:1.5rem;margin-bottom:0;border-bottom:1px solid #dee2e6}@media(min-width: 576px){.nav:not(.nav-hidden){display:flex !important;display:-webkit-flex !important}.nav:not(.nav-hidden):not(.nav-stacked):not(.flex-column){float:none !important}.nav:not(.nav-hidden):not(.nav-stacked):not(.flex-column)>.bslib-nav-spacer{margin-left:auto !important}.nav:not(.nav-hidden):not(.nav-stacked):not(.flex-column)>.form-inline{margin-top:auto;margin-bottom:auto}.nav:not(.nav-hidden).nav-stacked{flex-direction:column;-webkit-flex-direction:column;height:100%}.nav:not(.nav-hidden).nav-stacked>.bslib-nav-spacer{margin-top:auto !important}}.html-fill-container{display:flex;flex-direction:column;min-height:0;min-width:0}.html-fill-container>.html-fill-item{flex:1 1 auto;min-height:0;min-width:0}.html-fill-container>:not(.html-fill-item){flex:0 0 auto}.quarto-container{min-height:calc(100vh - 132px)}body.hypothesis-enabled #quarto-header{margin-right:16px}footer.footer .nav-footer,#quarto-header>nav{padding-left:1em;padding-right:1em}footer.footer div.nav-footer p:first-child{margin-top:0}footer.footer div.nav-footer p:last-child{margin-bottom:0}#quarto-content>*{padding-top:14px}#quarto-content>#quarto-sidebar-glass{padding-top:0px}@media(max-width: 991.98px){#quarto-content>*{padding-top:0}#quarto-content .subtitle{padding-top:14px}#quarto-content section:first-of-type h2:first-of-type,#quarto-content section:first-of-type .h2:first-of-type{margin-top:1rem}}.headroom-target,header.headroom{will-change:transform;transition:position 200ms linear;transition:all 200ms linear}header.headroom--pinned{transform:translateY(0%)}header.headroom--unpinned{transform:translateY(-100%)}.navbar-container{width:100%}.navbar-brand{overflow:hidden;text-overflow:ellipsis}.navbar-brand-container{max-width:calc(100% - 115px);min-width:0;display:flex;align-items:center}@media(min-width: 992px){.navbar-brand-container{margin-right:1em}}.navbar-brand.navbar-brand-logo{margin-right:4px;display:inline-flex}.navbar-toggler{flex-basis:content;flex-shrink:0}.navbar .navbar-brand-container{order:2}.navbar .navbar-toggler{order:1}.navbar .navbar-container>.navbar-nav{order:20}.navbar .navbar-container>.navbar-brand-container{margin-left:0 !important;margin-right:0 !important}.navbar .navbar-collapse{order:20}.navbar #quarto-search{order:4;margin-left:auto}.navbar .navbar-toggler{margin-right:.5em}.navbar-collapse .quarto-navbar-tools{margin-left:.5em}.navbar-logo{max-height:24px;width:auto;padding-right:4px}nav .nav-item:not(.compact){padding-top:1px}nav .nav-link i,nav .dropdown-item i{padding-right:1px}.navbar-expand-lg .navbar-nav .nav-link{padding-left:.6rem;padding-right:.6rem}nav .nav-item.compact .nav-link{padding-left:.5rem;padding-right:.5rem;font-size:1.1rem}.navbar .quarto-navbar-tools{order:3}.navbar .quarto-navbar-tools div.dropdown{display:inline-block}.navbar .quarto-navbar-tools .quarto-navigation-tool{color:#fdfeff}.navbar .quarto-navbar-tools .quarto-navigation-tool:hover{color:#fdfdff}.navbar-nav .dropdown-menu{min-width:220px;font-size:.9rem}.navbar .navbar-nav .nav-link.dropdown-toggle::after{opacity:.75;vertical-align:.175em}.navbar ul.dropdown-menu{padding-top:0;padding-bottom:0}.navbar .dropdown-header{text-transform:uppercase;font-size:.8rem;padding:0 .5rem}.navbar .dropdown-item{padding:.4rem .5rem}.navbar .dropdown-item>i.bi{margin-left:.1rem;margin-right:.25em}.sidebar #quarto-search{margin-top:-1px}.sidebar #quarto-search svg.aa-SubmitIcon{width:16px;height:16px}.sidebar-navigation a{color:inherit}.sidebar-title{margin-top:.25rem;padding-bottom:.5rem;font-size:1.3rem;line-height:1.6rem;visibility:visible}.sidebar-title>a{font-size:inherit;text-decoration:none}.sidebar-title .sidebar-tools-main{margin-top:-6px}@media(max-width: 991.98px){#quarto-sidebar div.sidebar-header{padding-top:.2em}}.sidebar-header-stacked .sidebar-title{margin-top:.6rem}.sidebar-logo{max-width:90%;padding-bottom:.5rem}.sidebar-logo-link{text-decoration:none}.sidebar-navigation li a{text-decoration:none}.sidebar-navigation .quarto-navigation-tool{opacity:.7;font-size:.875rem}#quarto-sidebar>nav>.sidebar-tools-main{margin-left:14px}.sidebar-tools-main{display:inline-flex;margin-left:0px;order:2}.sidebar-tools-main:not(.tools-wide){vertical-align:middle}.sidebar-navigation .quarto-navigation-tool.dropdown-toggle::after{display:none}.sidebar.sidebar-navigation>*{padding-top:1em}.sidebar-item{margin-bottom:.2em;line-height:1rem;margin-top:.4rem}.sidebar-section{padding-left:.5em;padding-bottom:.2em}.sidebar-item .sidebar-item-container{display:flex;justify-content:space-between;cursor:pointer}.sidebar-item-toggle:hover{cursor:pointer}.sidebar-item .sidebar-item-toggle .bi{font-size:.7rem;text-align:center}.sidebar-item .sidebar-item-toggle .bi-chevron-right::before{transition:transform 200ms ease}.sidebar-item .sidebar-item-toggle[aria-expanded=false] .bi-chevron-right::before{transform:none}.sidebar-item .sidebar-item-toggle[aria-expanded=true] .bi-chevron-right::before{transform:rotate(90deg)}.sidebar-item-text{width:100%}.sidebar-navigation .sidebar-divider{margin-left:0;margin-right:0;margin-top:.5rem;margin-bottom:.5rem}@media(max-width: 991.98px){.quarto-secondary-nav{display:block}.quarto-secondary-nav button.quarto-search-button{padding-right:0em;padding-left:2em}.quarto-secondary-nav button.quarto-btn-toggle{margin-left:-0.75rem;margin-right:.15rem}.quarto-secondary-nav nav.quarto-title-breadcrumbs{display:none}.quarto-secondary-nav nav.quarto-page-breadcrumbs{display:flex;align-items:center;padding-right:1em;margin-left:-0.25em}.quarto-secondary-nav nav.quarto-page-breadcrumbs a{text-decoration:none}.quarto-secondary-nav nav.quarto-page-breadcrumbs ol.breadcrumb{margin-bottom:0}}@media(min-width: 992px){.quarto-secondary-nav{display:none}}.quarto-title-breadcrumbs .breadcrumb{margin-bottom:.5em;font-size:.9rem}.quarto-title-breadcrumbs .breadcrumb li:last-of-type a{color:#6c757d}.quarto-secondary-nav .quarto-btn-toggle{color:#595959}.quarto-secondary-nav[aria-expanded=false] .quarto-btn-toggle .bi-chevron-right::before{transform:none}.quarto-secondary-nav[aria-expanded=true] .quarto-btn-toggle .bi-chevron-right::before{transform:rotate(90deg)}.quarto-secondary-nav .quarto-btn-toggle .bi-chevron-right::before{transition:transform 200ms ease}.quarto-secondary-nav{cursor:pointer}.no-decor{text-decoration:none}.quarto-secondary-nav-title{margin-top:.3em;color:#595959;padding-top:4px}.quarto-secondary-nav nav.quarto-page-breadcrumbs{color:#595959}.quarto-secondary-nav nav.quarto-page-breadcrumbs a{color:#595959}.quarto-secondary-nav nav.quarto-page-breadcrumbs a:hover{color:rgba(33,81,191,.8)}.quarto-secondary-nav nav.quarto-page-breadcrumbs .breadcrumb-item::before{color:#8c8c8c}.breadcrumb-item{line-height:1.2rem}div.sidebar-item-container{color:#595959}div.sidebar-item-container:hover,div.sidebar-item-container:focus{color:rgba(33,81,191,.8)}div.sidebar-item-container.disabled{color:rgba(89,89,89,.75)}div.sidebar-item-container .active,div.sidebar-item-container .show>.nav-link,div.sidebar-item-container .sidebar-link>code{color:#2151bf}div.sidebar.sidebar-navigation.rollup.quarto-sidebar-toggle-contents,nav.sidebar.sidebar-navigation:not(.rollup){background-color:#fff}@media(max-width: 991.98px){.sidebar-navigation .sidebar-item a,.nav-page .nav-page-text,.sidebar-navigation{font-size:1rem}.sidebar-navigation ul.sidebar-section.depth1 .sidebar-section-item{font-size:1.1rem}.sidebar-logo{display:none}.sidebar.sidebar-navigation{position:static;border-bottom:1px solid #dee2e6}.sidebar.sidebar-navigation.collapsing{position:fixed;z-index:1000}.sidebar.sidebar-navigation.show{position:fixed;z-index:1000}.sidebar.sidebar-navigation{min-height:100%}nav.quarto-secondary-nav{background-color:#fff;border-bottom:1px solid #dee2e6}.quarto-banner nav.quarto-secondary-nav{background-color:#2780e3;color:#fdfeff;border-top:1px solid #dee2e6}.sidebar .sidebar-footer{visibility:visible;padding-top:1rem;position:inherit}.sidebar-tools-collapse{display:block}}#quarto-sidebar{transition:width .15s ease-in}#quarto-sidebar>*{padding-right:1em}@media(max-width: 991.98px){#quarto-sidebar .sidebar-menu-container{white-space:nowrap;min-width:225px}#quarto-sidebar.show{transition:width .15s ease-out}}@media(min-width: 992px){#quarto-sidebar{display:flex;flex-direction:column}.nav-page .nav-page-text,.sidebar-navigation .sidebar-section .sidebar-item{font-size:.875rem}.sidebar-navigation .sidebar-item{font-size:.925rem}.sidebar.sidebar-navigation{display:block;position:sticky}.sidebar-search{width:100%}.sidebar .sidebar-footer{visibility:visible}}@media(min-width: 992px){#quarto-sidebar-glass{display:none}}@media(max-width: 991.98px){#quarto-sidebar-glass{position:fixed;top:0;bottom:0;left:0;right:0;background-color:rgba(255,255,255,0);transition:background-color .15s ease-in;z-index:-1}#quarto-sidebar-glass.collapsing{z-index:1000}#quarto-sidebar-glass.show{transition:background-color .15s ease-out;background-color:rgba(102,102,102,.4);z-index:1000}}.sidebar .sidebar-footer{padding:.5rem 1rem;align-self:flex-end;color:#6c757d;width:100%}.quarto-page-breadcrumbs .breadcrumb-item+.breadcrumb-item,.quarto-page-breadcrumbs .breadcrumb-item{padding-right:.33em;padding-left:0}.quarto-page-breadcrumbs .breadcrumb-item::before{padding-right:.33em}.quarto-sidebar-footer{font-size:.875em}.sidebar-section .bi-chevron-right{vertical-align:middle}.sidebar-section .bi-chevron-right::before{font-size:.9em}.notransition{-webkit-transition:none !important;-moz-transition:none !important;-o-transition:none !important;transition:none !important}.btn:focus:not(:focus-visible){box-shadow:none}.page-navigation{display:flex;justify-content:space-between}.nav-page{padding-bottom:.75em}.nav-page .bi{font-size:1.8rem;vertical-align:middle}.nav-page .nav-page-text{padding-left:.25em;padding-right:.25em}.nav-page a{color:#6c757d;text-decoration:none;display:flex;align-items:center}.nav-page a:hover{color:#1f4eb6}.nav-footer .toc-actions{padding-bottom:.5em;padding-top:.5em}.nav-footer .toc-actions a,.nav-footer .toc-actions a:hover{text-decoration:none}.nav-footer .toc-actions ul{display:flex;list-style:none}.nav-footer .toc-actions ul :first-child{margin-left:auto}.nav-footer .toc-actions ul :last-child{margin-right:auto}.nav-footer .toc-actions ul li{padding-right:1.5em}.nav-footer .toc-actions ul li i.bi{padding-right:.4em}.nav-footer .toc-actions ul li:last-of-type{padding-right:0}.nav-footer{display:flex;flex-direction:row;flex-wrap:wrap;justify-content:space-between;align-items:baseline;text-align:center;padding-top:.5rem;padding-bottom:.5rem;background-color:#fff}body.nav-fixed{padding-top:64px}.nav-footer-contents{color:#6c757d;margin-top:.25rem}.nav-footer{min-height:3.5em;color:#757575}.nav-footer a{color:#757575}.nav-footer .nav-footer-left{font-size:.825em}.nav-footer .nav-footer-center{font-size:.825em}.nav-footer .nav-footer-right{font-size:.825em}.nav-footer-left .footer-items,.nav-footer-center .footer-items,.nav-footer-right .footer-items{display:inline-flex;padding-top:.3em;padding-bottom:.3em;margin-bottom:0em}.nav-footer-left .footer-items .nav-link,.nav-footer-center .footer-items .nav-link,.nav-footer-right .footer-items .nav-link{padding-left:.6em;padding-right:.6em}@media(min-width: 768px){.nav-footer-left{flex:1 1 0px;text-align:left}}@media(max-width: 575.98px){.nav-footer-left{margin-bottom:1em;flex:100%}}@media(min-width: 768px){.nav-footer-right{flex:1 1 0px;text-align:right}}@media(max-width: 575.98px){.nav-footer-right{margin-bottom:1em;flex:100%}}.nav-footer-center{text-align:center;min-height:3em}@media(min-width: 768px){.nav-footer-center{flex:1 1 0px}}.nav-footer-center .footer-items{justify-content:center}@media(max-width: 767.98px){.nav-footer-center{margin-bottom:1em;flex:100%}}@media(max-width: 767.98px){.nav-footer-center{margin-top:3em;order:10}}.navbar .quarto-reader-toggle.reader .quarto-reader-toggle-btn{background-color:#fdfeff;border-radius:3px}@media(max-width: 991.98px){.quarto-reader-toggle{display:none}}.quarto-reader-toggle.reader.quarto-navigation-tool .quarto-reader-toggle-btn{background-color:#595959;border-radius:3px}.quarto-reader-toggle .quarto-reader-toggle-btn{display:inline-flex;padding-left:.2em;padding-right:.2em;margin-left:-0.2em;margin-right:-0.2em;text-align:center}.navbar .quarto-reader-toggle:not(.reader) .bi::before{background-image:url('data:image/svg+xml,')}.navbar .quarto-reader-toggle.reader .bi::before{background-image:url('data:image/svg+xml,')}.sidebar-navigation .quarto-reader-toggle:not(.reader) .bi::before{background-image:url('data:image/svg+xml,')}.sidebar-navigation .quarto-reader-toggle.reader .bi::before{background-image:url('data:image/svg+xml,')}#quarto-back-to-top{display:none;position:fixed;bottom:50px;background-color:#fff;border-radius:.25rem;box-shadow:0 .2rem .5rem #6c757d,0 0 .05rem #6c757d;color:#6c757d;text-decoration:none;font-size:.9em;text-align:center;left:50%;padding:.4rem .8rem;transform:translate(-50%, 0)}#quarto-announcement{padding:.5em;display:flex;justify-content:space-between;margin-bottom:0;font-size:.9em}#quarto-announcement .quarto-announcement-content{margin-right:auto}#quarto-announcement .quarto-announcement-content p{margin-bottom:0}#quarto-announcement .quarto-announcement-icon{margin-right:.5em;font-size:1.2em;margin-top:-0.15em}#quarto-announcement .quarto-announcement-action{cursor:pointer}.aa-DetachedSearchButtonQuery{display:none}.aa-DetachedOverlay ul.aa-List,#quarto-search-results ul.aa-List{list-style:none;padding-left:0}.aa-DetachedOverlay .aa-Panel,#quarto-search-results .aa-Panel{background-color:#fff;position:absolute;z-index:2000}#quarto-search-results .aa-Panel{max-width:400px}#quarto-search input{font-size:.925rem}@media(min-width: 992px){.navbar #quarto-search{margin-left:.25rem;order:999}}.navbar.navbar-expand-sm #quarto-search,.navbar.navbar-expand-md #quarto-search{order:999}@media(min-width: 992px){.navbar .quarto-navbar-tools{order:900}}@media(min-width: 992px){.navbar .quarto-navbar-tools.tools-end{margin-left:auto !important}}@media(max-width: 991.98px){#quarto-sidebar .sidebar-search{display:none}}#quarto-sidebar .sidebar-search .aa-Autocomplete{width:100%}.navbar .aa-Autocomplete .aa-Form{width:180px}.navbar #quarto-search.type-overlay .aa-Autocomplete{width:40px}.navbar #quarto-search.type-overlay .aa-Autocomplete .aa-Form{background-color:inherit;border:none}.navbar #quarto-search.type-overlay .aa-Autocomplete .aa-Form:focus-within{box-shadow:none;outline:none}.navbar #quarto-search.type-overlay .aa-Autocomplete .aa-Form .aa-InputWrapper{display:none}.navbar #quarto-search.type-overlay .aa-Autocomplete .aa-Form .aa-InputWrapper:focus-within{display:inherit}.navbar #quarto-search.type-overlay .aa-Autocomplete .aa-Form .aa-Label svg,.navbar #quarto-search.type-overlay .aa-Autocomplete .aa-Form .aa-LoadingIndicator svg{width:26px;height:26px;color:#fdfeff;opacity:1}.navbar #quarto-search.type-overlay .aa-Autocomplete svg.aa-SubmitIcon{width:26px;height:26px;color:#fdfeff;opacity:1}.aa-Autocomplete .aa-Form,.aa-DetachedFormContainer .aa-Form{align-items:center;background-color:#fff;border:1px solid #dee2e6;border-radius:.25rem;color:#343a40;display:flex;line-height:1em;margin:0;position:relative;width:100%}.aa-Autocomplete .aa-Form:focus-within,.aa-DetachedFormContainer .aa-Form:focus-within{box-shadow:rgba(39,128,227,.6) 0 0 0 1px;outline:currentColor none medium}.aa-Autocomplete .aa-Form .aa-InputWrapperPrefix,.aa-DetachedFormContainer .aa-Form .aa-InputWrapperPrefix{align-items:center;display:flex;flex-shrink:0;order:1}.aa-Autocomplete .aa-Form .aa-InputWrapperPrefix .aa-Label,.aa-Autocomplete .aa-Form .aa-InputWrapperPrefix .aa-LoadingIndicator,.aa-DetachedFormContainer .aa-Form .aa-InputWrapperPrefix .aa-Label,.aa-DetachedFormContainer .aa-Form .aa-InputWrapperPrefix .aa-LoadingIndicator{cursor:initial;flex-shrink:0;padding:0;text-align:left}.aa-Autocomplete .aa-Form .aa-InputWrapperPrefix .aa-Label svg,.aa-Autocomplete .aa-Form .aa-InputWrapperPrefix .aa-LoadingIndicator svg,.aa-DetachedFormContainer .aa-Form .aa-InputWrapperPrefix .aa-Label svg,.aa-DetachedFormContainer .aa-Form .aa-InputWrapperPrefix .aa-LoadingIndicator svg{color:#343a40;opacity:.5}.aa-Autocomplete .aa-Form .aa-InputWrapperPrefix .aa-SubmitButton,.aa-DetachedFormContainer .aa-Form .aa-InputWrapperPrefix .aa-SubmitButton{appearance:none;background:none;border:0;margin:0}.aa-Autocomplete .aa-Form .aa-InputWrapperPrefix .aa-LoadingIndicator,.aa-DetachedFormContainer .aa-Form .aa-InputWrapperPrefix .aa-LoadingIndicator{align-items:center;display:flex;justify-content:center}.aa-Autocomplete .aa-Form .aa-InputWrapperPrefix .aa-LoadingIndicator[hidden],.aa-DetachedFormContainer .aa-Form .aa-InputWrapperPrefix .aa-LoadingIndicator[hidden]{display:none}.aa-Autocomplete .aa-Form .aa-InputWrapper,.aa-DetachedFormContainer .aa-Form .aa-InputWrapper{order:3;position:relative;width:100%}.aa-Autocomplete .aa-Form .aa-InputWrapper .aa-Input,.aa-DetachedFormContainer .aa-Form .aa-InputWrapper .aa-Input{appearance:none;background:none;border:0;color:#343a40;font:inherit;height:calc(1.5em + .1rem + 2px);padding:0;width:100%}.aa-Autocomplete .aa-Form .aa-InputWrapper .aa-Input::placeholder,.aa-DetachedFormContainer .aa-Form .aa-InputWrapper .aa-Input::placeholder{color:#343a40;opacity:.8}.aa-Autocomplete .aa-Form .aa-InputWrapper .aa-Input:focus,.aa-DetachedFormContainer .aa-Form .aa-InputWrapper .aa-Input:focus{border-color:none;box-shadow:none;outline:none}.aa-Autocomplete .aa-Form .aa-InputWrapper .aa-Input::-webkit-search-decoration,.aa-Autocomplete .aa-Form .aa-InputWrapper .aa-Input::-webkit-search-cancel-button,.aa-Autocomplete .aa-Form .aa-InputWrapper .aa-Input::-webkit-search-results-button,.aa-Autocomplete .aa-Form .aa-InputWrapper .aa-Input::-webkit-search-results-decoration,.aa-DetachedFormContainer .aa-Form .aa-InputWrapper .aa-Input::-webkit-search-decoration,.aa-DetachedFormContainer .aa-Form .aa-InputWrapper .aa-Input::-webkit-search-cancel-button,.aa-DetachedFormContainer .aa-Form .aa-InputWrapper .aa-Input::-webkit-search-results-button,.aa-DetachedFormContainer .aa-Form .aa-InputWrapper .aa-Input::-webkit-search-results-decoration{display:none}.aa-Autocomplete .aa-Form .aa-InputWrapperSuffix,.aa-DetachedFormContainer .aa-Form .aa-InputWrapperSuffix{align-items:center;display:flex;order:4}.aa-Autocomplete .aa-Form .aa-InputWrapperSuffix .aa-ClearButton,.aa-DetachedFormContainer .aa-Form .aa-InputWrapperSuffix .aa-ClearButton{align-items:center;background:none;border:0;color:#343a40;opacity:.8;cursor:pointer;display:flex;margin:0;width:calc(1.5em + .1rem + 2px)}.aa-Autocomplete .aa-Form .aa-InputWrapperSuffix .aa-ClearButton:hover,.aa-Autocomplete .aa-Form .aa-InputWrapperSuffix .aa-ClearButton:focus,.aa-DetachedFormContainer .aa-Form .aa-InputWrapperSuffix .aa-ClearButton:hover,.aa-DetachedFormContainer .aa-Form .aa-InputWrapperSuffix .aa-ClearButton:focus{color:#343a40;opacity:.8}.aa-Autocomplete .aa-Form .aa-InputWrapperSuffix .aa-ClearButton[hidden],.aa-DetachedFormContainer .aa-Form .aa-InputWrapperSuffix .aa-ClearButton[hidden]{display:none}.aa-Autocomplete .aa-Form .aa-InputWrapperSuffix .aa-ClearButton svg,.aa-DetachedFormContainer .aa-Form .aa-InputWrapperSuffix .aa-ClearButton svg{width:calc(1.5em + 0.75rem + calc(1px * 2))}.aa-Autocomplete .aa-Form .aa-InputWrapperSuffix .aa-CopyButton,.aa-DetachedFormContainer .aa-Form .aa-InputWrapperSuffix .aa-CopyButton{border:none;align-items:center;background:none;color:#343a40;opacity:.4;font-size:.7rem;cursor:pointer;display:none;margin:0;width:calc(1em + .1rem + 2px)}.aa-Autocomplete .aa-Form .aa-InputWrapperSuffix .aa-CopyButton:hover,.aa-Autocomplete .aa-Form .aa-InputWrapperSuffix .aa-CopyButton:focus,.aa-DetachedFormContainer .aa-Form .aa-InputWrapperSuffix .aa-CopyButton:hover,.aa-DetachedFormContainer .aa-Form .aa-InputWrapperSuffix .aa-CopyButton:focus{color:#343a40;opacity:.8}.aa-Autocomplete .aa-Form .aa-InputWrapperSuffix .aa-CopyButton[hidden],.aa-DetachedFormContainer .aa-Form .aa-InputWrapperSuffix .aa-CopyButton[hidden]{display:none}.aa-PanelLayout:empty{display:none}.quarto-search-no-results.no-query{display:none}.aa-Source:has(.no-query){display:none}#quarto-search-results .aa-Panel{border:solid #dee2e6 1px}#quarto-search-results .aa-SourceNoResults{width:398px}.aa-DetachedOverlay .aa-Panel,#quarto-search-results .aa-Panel{max-height:65vh;overflow-y:auto;font-size:.925rem}.aa-DetachedOverlay .aa-SourceNoResults,#quarto-search-results .aa-SourceNoResults{height:60px;display:flex;justify-content:center;align-items:center}.aa-DetachedOverlay .search-error,#quarto-search-results .search-error{padding-top:10px;padding-left:20px;padding-right:20px;cursor:default}.aa-DetachedOverlay .search-error .search-error-title,#quarto-search-results .search-error .search-error-title{font-size:1.1rem;margin-bottom:.5rem}.aa-DetachedOverlay .search-error .search-error-title .search-error-icon,#quarto-search-results .search-error .search-error-title .search-error-icon{margin-right:8px}.aa-DetachedOverlay .search-error .search-error-text,#quarto-search-results .search-error .search-error-text{font-weight:300}.aa-DetachedOverlay .search-result-text,#quarto-search-results .search-result-text{font-weight:300;overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;line-height:1.2rem;max-height:2.4rem}.aa-DetachedOverlay .aa-SourceHeader .search-result-header,#quarto-search-results .aa-SourceHeader .search-result-header{font-size:.875rem;background-color:#f2f2f2;padding-left:14px;padding-bottom:4px;padding-top:4px}.aa-DetachedOverlay .aa-SourceHeader .search-result-header-no-results,#quarto-search-results .aa-SourceHeader .search-result-header-no-results{display:none}.aa-DetachedOverlay .aa-SourceFooter .algolia-search-logo,#quarto-search-results .aa-SourceFooter .algolia-search-logo{width:110px;opacity:.85;margin:8px;float:right}.aa-DetachedOverlay .search-result-section,#quarto-search-results .search-result-section{font-size:.925em}.aa-DetachedOverlay a.search-result-link,#quarto-search-results a.search-result-link{color:inherit;text-decoration:none}.aa-DetachedOverlay li.aa-Item[aria-selected=true] .search-item,#quarto-search-results li.aa-Item[aria-selected=true] .search-item{background-color:#2780e3}.aa-DetachedOverlay li.aa-Item[aria-selected=true] .search-item.search-result-more,.aa-DetachedOverlay li.aa-Item[aria-selected=true] .search-item .search-result-section,.aa-DetachedOverlay li.aa-Item[aria-selected=true] .search-item .search-result-text,.aa-DetachedOverlay li.aa-Item[aria-selected=true] .search-item .search-result-title-container,.aa-DetachedOverlay li.aa-Item[aria-selected=true] .search-item .search-result-text-container,#quarto-search-results li.aa-Item[aria-selected=true] .search-item.search-result-more,#quarto-search-results li.aa-Item[aria-selected=true] .search-item .search-result-section,#quarto-search-results li.aa-Item[aria-selected=true] .search-item .search-result-text,#quarto-search-results li.aa-Item[aria-selected=true] .search-item .search-result-title-container,#quarto-search-results li.aa-Item[aria-selected=true] .search-item .search-result-text-container{color:#fff;background-color:#2780e3}.aa-DetachedOverlay li.aa-Item[aria-selected=true] .search-item mark.search-match,.aa-DetachedOverlay li.aa-Item[aria-selected=true] .search-item .search-match.mark,#quarto-search-results li.aa-Item[aria-selected=true] .search-item mark.search-match,#quarto-search-results li.aa-Item[aria-selected=true] .search-item .search-match.mark{color:#fff;background-color:#4b95e8}.aa-DetachedOverlay li.aa-Item[aria-selected=false] .search-item,#quarto-search-results li.aa-Item[aria-selected=false] .search-item{background-color:#fff}.aa-DetachedOverlay li.aa-Item[aria-selected=false] .search-item.search-result-more,.aa-DetachedOverlay li.aa-Item[aria-selected=false] .search-item .search-result-section,.aa-DetachedOverlay li.aa-Item[aria-selected=false] .search-item .search-result-text,.aa-DetachedOverlay li.aa-Item[aria-selected=false] .search-item .search-result-title-container,.aa-DetachedOverlay li.aa-Item[aria-selected=false] .search-item .search-result-text-container,#quarto-search-results li.aa-Item[aria-selected=false] .search-item.search-result-more,#quarto-search-results li.aa-Item[aria-selected=false] .search-item .search-result-section,#quarto-search-results li.aa-Item[aria-selected=false] .search-item .search-result-text,#quarto-search-results li.aa-Item[aria-selected=false] .search-item .search-result-title-container,#quarto-search-results li.aa-Item[aria-selected=false] .search-item .search-result-text-container{color:#343a40}.aa-DetachedOverlay li.aa-Item[aria-selected=false] .search-item mark.search-match,.aa-DetachedOverlay li.aa-Item[aria-selected=false] .search-item .search-match.mark,#quarto-search-results li.aa-Item[aria-selected=false] .search-item mark.search-match,#quarto-search-results li.aa-Item[aria-selected=false] .search-item .search-match.mark{color:inherit;background-color:#e5effc}.aa-DetachedOverlay .aa-Item .search-result-doc:not(.document-selectable) .search-result-title-container,#quarto-search-results .aa-Item .search-result-doc:not(.document-selectable) .search-result-title-container{background-color:#fff;color:#343a40}.aa-DetachedOverlay .aa-Item .search-result-doc:not(.document-selectable) .search-result-text-container,#quarto-search-results .aa-Item .search-result-doc:not(.document-selectable) .search-result-text-container{padding-top:0px}.aa-DetachedOverlay li.aa-Item .search-result-doc.document-selectable .search-result-text-container,#quarto-search-results li.aa-Item .search-result-doc.document-selectable .search-result-text-container{margin-top:-4px}.aa-DetachedOverlay .aa-Item,#quarto-search-results .aa-Item{cursor:pointer}.aa-DetachedOverlay .aa-Item .search-item,#quarto-search-results .aa-Item .search-item{border-left:none;border-right:none;border-top:none;background-color:#fff;border-color:#dee2e6;color:#343a40}.aa-DetachedOverlay .aa-Item .search-item p,#quarto-search-results .aa-Item .search-item p{margin-top:0;margin-bottom:0}.aa-DetachedOverlay .aa-Item .search-item i.bi,#quarto-search-results .aa-Item .search-item i.bi{padding-left:8px;padding-right:8px;font-size:1.3em}.aa-DetachedOverlay .aa-Item .search-item .search-result-title,#quarto-search-results .aa-Item .search-item .search-result-title{margin-top:.3em;margin-bottom:0em}.aa-DetachedOverlay .aa-Item .search-item .search-result-crumbs,#quarto-search-results .aa-Item .search-item .search-result-crumbs{white-space:nowrap;text-overflow:ellipsis;font-size:.8em;font-weight:300;margin-right:1em}.aa-DetachedOverlay .aa-Item .search-item .search-result-crumbs:not(.search-result-crumbs-wrap),#quarto-search-results .aa-Item .search-item .search-result-crumbs:not(.search-result-crumbs-wrap){max-width:30%;margin-left:auto;margin-top:.5em;margin-bottom:.1rem}.aa-DetachedOverlay .aa-Item .search-item .search-result-crumbs.search-result-crumbs-wrap,#quarto-search-results .aa-Item .search-item .search-result-crumbs.search-result-crumbs-wrap{flex-basis:100%;margin-top:0em;margin-bottom:.2em;margin-left:37px}.aa-DetachedOverlay .aa-Item .search-result-title-container,#quarto-search-results .aa-Item .search-result-title-container{font-size:1em;display:flex;flex-wrap:wrap;padding:6px 4px 6px 4px}.aa-DetachedOverlay .aa-Item .search-result-text-container,#quarto-search-results .aa-Item .search-result-text-container{padding-bottom:8px;padding-right:8px;margin-left:42px}.aa-DetachedOverlay .aa-Item .search-result-doc-section,.aa-DetachedOverlay .aa-Item .search-result-more,#quarto-search-results .aa-Item .search-result-doc-section,#quarto-search-results .aa-Item .search-result-more{padding-top:8px;padding-bottom:8px;padding-left:44px}.aa-DetachedOverlay .aa-Item .search-result-more,#quarto-search-results .aa-Item .search-result-more{font-size:.8em;font-weight:400}.aa-DetachedOverlay .aa-Item .search-result-doc,#quarto-search-results .aa-Item .search-result-doc{border-top:1px solid #dee2e6}.aa-DetachedSearchButton{background:none;border:none}.aa-DetachedSearchButton .aa-DetachedSearchButtonPlaceholder{display:none}.navbar .aa-DetachedSearchButton .aa-DetachedSearchButtonIcon{color:#fdfeff}.sidebar-tools-collapse #quarto-search,.sidebar-tools-main #quarto-search{display:inline}.sidebar-tools-collapse #quarto-search .aa-Autocomplete,.sidebar-tools-main #quarto-search .aa-Autocomplete{display:inline}.sidebar-tools-collapse #quarto-search .aa-DetachedSearchButton,.sidebar-tools-main #quarto-search .aa-DetachedSearchButton{padding-left:4px;padding-right:4px}.sidebar-tools-collapse #quarto-search .aa-DetachedSearchButton .aa-DetachedSearchButtonIcon,.sidebar-tools-main #quarto-search .aa-DetachedSearchButton .aa-DetachedSearchButtonIcon{color:#595959}.sidebar-tools-collapse #quarto-search .aa-DetachedSearchButton .aa-DetachedSearchButtonIcon .aa-SubmitIcon,.sidebar-tools-main #quarto-search .aa-DetachedSearchButton .aa-DetachedSearchButtonIcon .aa-SubmitIcon{margin-top:-3px}.aa-DetachedContainer{background:rgba(255,255,255,.65);width:90%;bottom:0;box-shadow:rgba(222,226,230,.6) 0 0 0 1px;outline:currentColor none medium;display:flex;flex-direction:column;left:0;margin:0;overflow:hidden;padding:0;position:fixed;right:0;top:0;z-index:1101}.aa-DetachedContainer::after{height:32px}.aa-DetachedContainer .aa-SourceHeader{margin:var(--aa-spacing-half) 0 var(--aa-spacing-half) 2px}.aa-DetachedContainer .aa-Panel{background-color:#fff;border-radius:0;box-shadow:none;flex-grow:1;margin:0;padding:0;position:relative}.aa-DetachedContainer .aa-PanelLayout{bottom:0;box-shadow:none;left:0;margin:0;max-height:none;overflow-y:auto;position:absolute;right:0;top:0;width:100%}.aa-DetachedFormContainer{background-color:#fff;border-bottom:1px solid #dee2e6;display:flex;flex-direction:row;justify-content:space-between;margin:0;padding:.5em}.aa-DetachedCancelButton{background:none;font-size:.8em;border:0;border-radius:3px;color:#343a40;cursor:pointer;margin:0 0 0 .5em;padding:0 .5em}.aa-DetachedCancelButton:hover,.aa-DetachedCancelButton:focus{box-shadow:rgba(39,128,227,.6) 0 0 0 1px;outline:currentColor none medium}.aa-DetachedContainer--modal{bottom:inherit;height:auto;margin:0 auto;position:absolute;top:100px;border-radius:6px;max-width:850px}@media(max-width: 575.98px){.aa-DetachedContainer--modal{width:100%;top:0px;border-radius:0px;border:none}}.aa-DetachedContainer--modal .aa-PanelLayout{max-height:var(--aa-detached-modal-max-height);padding-bottom:var(--aa-spacing-half);position:static}.aa-Detached{height:100vh;overflow:hidden}.aa-DetachedOverlay{background-color:rgba(52,58,64,.4);position:fixed;left:0;right:0;top:0;margin:0;padding:0;height:100vh;z-index:1100}.quarto-dashboard.nav-fixed.dashboard-sidebar #quarto-content.quarto-dashboard-content{padding:0em}.quarto-dashboard #quarto-content.quarto-dashboard-content{padding:1em}.quarto-dashboard #quarto-content.quarto-dashboard-content>*{padding-top:0}@media(min-width: 576px){.quarto-dashboard{height:100%}}.quarto-dashboard .card.valuebox.bslib-card.bg-primary{background-color:#5397e9 !important}.quarto-dashboard .card.valuebox.bslib-card.bg-secondary{background-color:#343a40 !important}.quarto-dashboard .card.valuebox.bslib-card.bg-success{background-color:#3aa716 !important}.quarto-dashboard .card.valuebox.bslib-card.bg-info{background-color:rgba(153,84,187,.7019607843) !important}.quarto-dashboard .card.valuebox.bslib-card.bg-warning{background-color:#fa6400 !important}.quarto-dashboard .card.valuebox.bslib-card.bg-danger{background-color:rgba(255,0,57,.7019607843) !important}.quarto-dashboard .card.valuebox.bslib-card.bg-light{background-color:#f8f9fa !important}.quarto-dashboard .card.valuebox.bslib-card.bg-dark{background-color:#343a40 !important}.quarto-dashboard.dashboard-fill{display:flex;flex-direction:column}.quarto-dashboard #quarto-appendix{display:none}.quarto-dashboard #quarto-header #quarto-dashboard-header{border-top:solid 1px #549be9;border-bottom:solid 1px #549be9}.quarto-dashboard #quarto-header #quarto-dashboard-header>nav{padding-left:1em;padding-right:1em}.quarto-dashboard #quarto-header #quarto-dashboard-header>nav .navbar-brand-container{padding-left:0}.quarto-dashboard #quarto-header #quarto-dashboard-header .navbar-toggler{margin-right:0}.quarto-dashboard #quarto-header #quarto-dashboard-header .navbar-toggler-icon{height:1em;width:1em;background-image:url('data:image/svg+xml,')}.quarto-dashboard #quarto-header #quarto-dashboard-header .navbar-brand-container{padding-right:1em}.quarto-dashboard #quarto-header #quarto-dashboard-header .navbar-title{font-size:1.1em}.quarto-dashboard #quarto-header #quarto-dashboard-header .navbar-nav{font-size:.9em}.quarto-dashboard #quarto-dashboard-header .navbar{padding:0}.quarto-dashboard #quarto-dashboard-header .navbar .navbar-container{padding-left:1em}.quarto-dashboard #quarto-dashboard-header .navbar.slim .navbar-brand-container .nav-link,.quarto-dashboard #quarto-dashboard-header .navbar.slim .navbar-nav .nav-link{padding:.7em}.quarto-dashboard #quarto-dashboard-header .navbar .quarto-color-scheme-toggle{order:9}.quarto-dashboard #quarto-dashboard-header .navbar .navbar-toggler{margin-left:.5em;order:10}.quarto-dashboard #quarto-dashboard-header .navbar .navbar-nav .nav-link{padding:.5em;height:100%;display:flex;align-items:center}.quarto-dashboard #quarto-dashboard-header .navbar .navbar-nav .active{background-color:#4b95e8}.quarto-dashboard #quarto-dashboard-header .navbar .navbar-brand-container{padding:.5em .5em .5em 0;display:flex;flex-direction:row;margin-right:2em;align-items:center}@media(max-width: 767.98px){.quarto-dashboard #quarto-dashboard-header .navbar .navbar-brand-container{margin-right:auto}}.quarto-dashboard #quarto-dashboard-header .navbar .navbar-collapse{align-self:stretch}@media(min-width: 768px){.quarto-dashboard #quarto-dashboard-header .navbar .navbar-collapse{order:8}}@media(max-width: 767.98px){.quarto-dashboard #quarto-dashboard-header .navbar .navbar-collapse{order:1000;padding-bottom:.5em}}.quarto-dashboard #quarto-dashboard-header .navbar .navbar-collapse .navbar-nav{align-self:stretch}.quarto-dashboard #quarto-dashboard-header .navbar .navbar-title{font-size:1.25em;line-height:1.1em;display:flex;flex-direction:row;flex-wrap:wrap;align-items:baseline}.quarto-dashboard #quarto-dashboard-header .navbar .navbar-title .navbar-title-text{margin-right:.4em}.quarto-dashboard #quarto-dashboard-header .navbar .navbar-title a{text-decoration:none;color:inherit}.quarto-dashboard #quarto-dashboard-header .navbar .navbar-subtitle,.quarto-dashboard #quarto-dashboard-header .navbar .navbar-author{font-size:.9rem;margin-right:.5em}.quarto-dashboard #quarto-dashboard-header .navbar .navbar-author{margin-left:auto}.quarto-dashboard #quarto-dashboard-header .navbar .navbar-logo{max-height:48px;min-height:30px;object-fit:cover;margin-right:1em}.quarto-dashboard #quarto-dashboard-header .navbar .quarto-dashboard-links{order:9;padding-right:1em}.quarto-dashboard #quarto-dashboard-header .navbar .quarto-dashboard-link-text{margin-left:.25em}.quarto-dashboard #quarto-dashboard-header .navbar .quarto-dashboard-link{padding-right:0em;padding-left:.7em;text-decoration:none;color:#fdfeff}.quarto-dashboard .page-layout-custom .tab-content{padding:0;border:none}.quarto-dashboard-img-contain{height:100%;width:100%;object-fit:contain}@media(max-width: 575.98px){.quarto-dashboard .bslib-grid{grid-template-rows:minmax(1em, max-content) !important}.quarto-dashboard .sidebar-content{height:inherit}.quarto-dashboard .page-layout-custom{min-height:100vh}}.quarto-dashboard.dashboard-toolbar>.page-layout-custom,.quarto-dashboard.dashboard-sidebar>.page-layout-custom{padding:0}.quarto-dashboard .quarto-dashboard-content.quarto-dashboard-pages{padding:0}.quarto-dashboard .callout{margin-bottom:0;margin-top:0}.quarto-dashboard .html-fill-container figure{overflow:hidden}.quarto-dashboard bslib-tooltip .rounded-pill{border:solid #6c757d 1px}.quarto-dashboard bslib-tooltip .rounded-pill .svg{fill:#343a40}.quarto-dashboard .tabset .dashboard-card-no-title .nav-tabs{margin-left:0;margin-right:auto}.quarto-dashboard .tabset .tab-content{border:none}.quarto-dashboard .tabset .card-header .nav-link[role=tab]{margin-top:-6px;padding-top:6px;padding-bottom:6px}.quarto-dashboard .card.valuebox,.quarto-dashboard .card.bslib-value-box{min-height:3rem}.quarto-dashboard .card.valuebox .card-body,.quarto-dashboard .card.bslib-value-box .card-body{padding:0}.quarto-dashboard .bslib-value-box .value-box-value{font-size:clamp(.1em,15cqw,5em)}.quarto-dashboard .bslib-value-box .value-box-showcase .bi{font-size:clamp(.1em,max(18cqw,5.2cqh),5em);text-align:center;height:1em}.quarto-dashboard .bslib-value-box .value-box-showcase .bi::before{vertical-align:1em}.quarto-dashboard .bslib-value-box .value-box-area{margin-top:auto;margin-bottom:auto}.quarto-dashboard .card figure.quarto-float{display:flex;flex-direction:column;align-items:center}.quarto-dashboard .dashboard-scrolling{padding:1em}.quarto-dashboard .full-height{height:100%}.quarto-dashboard .showcase-bottom .value-box-grid{display:grid;grid-template-columns:1fr;grid-template-rows:1fr auto;grid-template-areas:"top" "bottom"}.quarto-dashboard .showcase-bottom .value-box-grid .value-box-showcase{grid-area:bottom;padding:0;margin:0}.quarto-dashboard .showcase-bottom .value-box-grid .value-box-showcase i.bi{font-size:4rem}.quarto-dashboard .showcase-bottom .value-box-grid .value-box-area{grid-area:top}.quarto-dashboard .tab-content{margin-bottom:0}.quarto-dashboard .bslib-card .bslib-navs-card-title{justify-content:stretch;align-items:end}.quarto-dashboard .card-header{display:flex;flex-wrap:wrap;justify-content:space-between}.quarto-dashboard .card-header .card-title{display:flex;flex-direction:column;justify-content:center;margin-bottom:0}.quarto-dashboard .tabset .card-toolbar{margin-bottom:1em}.quarto-dashboard .bslib-grid>.bslib-sidebar-layout{border:none;gap:var(--bslib-spacer, 1rem)}.quarto-dashboard .bslib-grid>.bslib-sidebar-layout>.main{padding:0}.quarto-dashboard .bslib-grid>.bslib-sidebar-layout>.sidebar{border-radius:.25rem;border:1px solid rgba(0,0,0,.175)}.quarto-dashboard .bslib-grid>.bslib-sidebar-layout>.collapse-toggle{display:none}@media(max-width: 767.98px){.quarto-dashboard .bslib-grid>.bslib-sidebar-layout{grid-template-columns:1fr;grid-template-rows:max-content 1fr}.quarto-dashboard .bslib-grid>.bslib-sidebar-layout>.main{grid-column:1;grid-row:2}.quarto-dashboard .bslib-grid>.bslib-sidebar-layout .sidebar{grid-column:1;grid-row:1}}.quarto-dashboard .sidebar-right .sidebar{padding-left:2.5em}.quarto-dashboard .sidebar-right .collapse-toggle{left:2px}.quarto-dashboard .quarto-dashboard .sidebar-right button.collapse-toggle:not(.transitioning){left:unset}.quarto-dashboard aside.sidebar{padding-left:1em;padding-right:1em;background-color:rgba(52,58,64,.25);color:#343a40}.quarto-dashboard .bslib-sidebar-layout>div.main{padding:.7em}.quarto-dashboard .bslib-sidebar-layout button.collapse-toggle{margin-top:.3em}.quarto-dashboard .bslib-sidebar-layout .collapse-toggle{top:0}.quarto-dashboard .bslib-sidebar-layout.sidebar-collapsed:not(.transitioning):not(.sidebar-right) .collapse-toggle{left:2px}.quarto-dashboard .sidebar>section>.h3:first-of-type{margin-top:0em}.quarto-dashboard .sidebar .h3,.quarto-dashboard .sidebar .h4,.quarto-dashboard .sidebar .h5,.quarto-dashboard .sidebar .h6{margin-top:.5em}.quarto-dashboard .sidebar form{flex-direction:column;align-items:start;margin-bottom:1em}.quarto-dashboard .sidebar form div[class*=oi-][class$=-input]{flex-direction:column}.quarto-dashboard .sidebar form[class*=oi-][class$=-toggle]{flex-direction:row-reverse;align-items:center;justify-content:start}.quarto-dashboard .sidebar form input[type=range]{margin-top:.5em;margin-right:.8em;margin-left:1em}.quarto-dashboard .sidebar label{width:fit-content}.quarto-dashboard .sidebar .card-body{margin-bottom:2em}.quarto-dashboard .sidebar .shiny-input-container{margin-bottom:1em}.quarto-dashboard .sidebar .shiny-options-group{margin-top:0}.quarto-dashboard .sidebar .control-label{margin-bottom:.3em}.quarto-dashboard .card .card-body .quarto-layout-row{align-items:stretch}.quarto-dashboard .toolbar{font-size:.9em;display:flex;flex-direction:row;border-top:solid 1px #bcbfc0;padding:1em;flex-wrap:wrap;background-color:rgba(52,58,64,.25)}.quarto-dashboard .toolbar .cell-output-display{display:flex}.quarto-dashboard .toolbar .shiny-input-container{padding-bottom:.5em;margin-bottom:.5em;width:inherit}.quarto-dashboard .toolbar .shiny-input-container>.checkbox:first-child{margin-top:6px}.quarto-dashboard .toolbar>*:last-child{margin-right:0}.quarto-dashboard .toolbar>*>*{margin-right:1em;align-items:baseline}.quarto-dashboard .toolbar>*>*>a{text-decoration:none;margin-top:auto;margin-bottom:auto}.quarto-dashboard .toolbar .shiny-input-container{padding-bottom:0;margin-bottom:0}.quarto-dashboard .toolbar .shiny-input-container>*{flex-shrink:0;flex-grow:0}.quarto-dashboard .toolbar .form-group.shiny-input-container:not([role=group])>label{margin-bottom:0}.quarto-dashboard .toolbar .shiny-input-container.no-baseline{align-items:start;padding-top:6px}.quarto-dashboard .toolbar .shiny-input-container{display:flex;align-items:baseline}.quarto-dashboard .toolbar .shiny-input-container label{padding-right:.4em}.quarto-dashboard .toolbar .shiny-input-container .bslib-input-switch{margin-top:6px}.quarto-dashboard .toolbar input[type=text]{line-height:1;width:inherit}.quarto-dashboard .toolbar .input-daterange{width:inherit}.quarto-dashboard .toolbar .input-daterange input[type=text]{height:2.4em;width:10em}.quarto-dashboard .toolbar .input-daterange .input-group-addon{height:auto;padding:0;margin-left:-5px !important;margin-right:-5px}.quarto-dashboard .toolbar .input-daterange .input-group-addon .input-group-text{padding-top:0;padding-bottom:0;height:100%}.quarto-dashboard .toolbar span.irs.irs--shiny{width:10em}.quarto-dashboard .toolbar span.irs.irs--shiny .irs-line{top:9px}.quarto-dashboard .toolbar span.irs.irs--shiny .irs-min,.quarto-dashboard .toolbar span.irs.irs--shiny .irs-max,.quarto-dashboard .toolbar span.irs.irs--shiny .irs-from,.quarto-dashboard .toolbar span.irs.irs--shiny .irs-to,.quarto-dashboard .toolbar span.irs.irs--shiny .irs-single{top:20px}.quarto-dashboard .toolbar span.irs.irs--shiny .irs-bar{top:8px}.quarto-dashboard .toolbar span.irs.irs--shiny .irs-handle{top:0px}.quarto-dashboard .toolbar .shiny-input-checkboxgroup>label{margin-top:6px}.quarto-dashboard .toolbar .shiny-input-checkboxgroup>.shiny-options-group{margin-top:0;align-items:baseline}.quarto-dashboard .toolbar .shiny-input-radiogroup>label{margin-top:6px}.quarto-dashboard .toolbar .shiny-input-radiogroup>.shiny-options-group{align-items:baseline;margin-top:0}.quarto-dashboard .toolbar .shiny-input-radiogroup>.shiny-options-group>.radio{margin-right:.3em}.quarto-dashboard .toolbar .form-select{padding-top:.2em;padding-bottom:.2em}.quarto-dashboard .toolbar .shiny-input-select{min-width:6em}.quarto-dashboard .toolbar div.checkbox{margin-bottom:0px}.quarto-dashboard .toolbar>.checkbox:first-child{margin-top:6px}.quarto-dashboard .toolbar form{width:fit-content}.quarto-dashboard .toolbar form label{padding-top:.2em;padding-bottom:.2em;width:fit-content}.quarto-dashboard .toolbar form input[type=date]{width:fit-content}.quarto-dashboard .toolbar form input[type=color]{width:3em}.quarto-dashboard .toolbar form button{padding:.4em}.quarto-dashboard .toolbar form select{width:fit-content}.quarto-dashboard .toolbar>*{font-size:.9em;flex-grow:0}.quarto-dashboard .toolbar .shiny-input-container label{margin-bottom:1px}.quarto-dashboard .toolbar-bottom{margin-top:1em;margin-bottom:0 !important;order:2}.quarto-dashboard .quarto-dashboard-content>.dashboard-toolbar-container>.toolbar-content>.tab-content>.tab-pane>*:not(.bslib-sidebar-layout){padding:1em}.quarto-dashboard .quarto-dashboard-content>.dashboard-toolbar-container>.toolbar-content>*:not(.tab-content){padding:1em}.quarto-dashboard .quarto-dashboard-content>.tab-content>.dashboard-page>.dashboard-toolbar-container>.toolbar-content,.quarto-dashboard .quarto-dashboard-content>.tab-content>.dashboard-page:not(.dashboard-sidebar-container)>*:not(.dashboard-toolbar-container){padding:1em}.quarto-dashboard .toolbar-content{padding:0}.quarto-dashboard .quarto-dashboard-content.quarto-dashboard-pages .tab-pane>.dashboard-toolbar-container .toolbar{border-radius:0;margin-bottom:0}.quarto-dashboard .dashboard-toolbar-container.toolbar-toplevel .toolbar{border-bottom:1px solid rgba(0,0,0,.175)}.quarto-dashboard .dashboard-toolbar-container.toolbar-toplevel .toolbar-bottom{margin-top:0}.quarto-dashboard .dashboard-toolbar-container:not(.toolbar-toplevel) .toolbar{margin-bottom:1em;border-top:none;border-radius:.25rem;border:1px solid rgba(0,0,0,.175)}.quarto-dashboard .vega-embed.has-actions details{width:1.7em;height:2em;position:absolute !important;top:0;right:0}.quarto-dashboard .dashboard-toolbar-container{padding:0}.quarto-dashboard .card .card-header p:last-child,.quarto-dashboard .card .card-footer p:last-child{margin-bottom:0}.quarto-dashboard .card .card-body>.h4:first-child{margin-top:0}.quarto-dashboard .card .card-body{z-index:4}@media(max-width: 767.98px){.quarto-dashboard .card .card-body .itables div.dataTables_wrapper div.dataTables_length,.quarto-dashboard .card .card-body .itables div.dataTables_wrapper div.dataTables_info,.quarto-dashboard .card .card-body .itables div.dataTables_wrapper div.dataTables_paginate{text-align:initial}.quarto-dashboard .card .card-body .itables div.dataTables_wrapper div.dataTables_filter{text-align:right}.quarto-dashboard .card .card-body .itables div.dataTables_wrapper div.dataTables_paginate ul.pagination{justify-content:initial}}.quarto-dashboard .card .card-body .itables .dataTables_wrapper{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:center;padding-top:0}.quarto-dashboard .card .card-body .itables .dataTables_wrapper table{flex-shrink:0}.quarto-dashboard .card .card-body .itables .dataTables_wrapper .dt-buttons{margin-bottom:.5em;margin-left:auto;width:fit-content;float:right}.quarto-dashboard .card .card-body .itables .dataTables_wrapper .dt-buttons.btn-group{background:#fff;border:none}.quarto-dashboard .card .card-body .itables .dataTables_wrapper .dt-buttons .btn-secondary{background-color:#fff;background-image:none;border:solid #dee2e6 1px;padding:.2em .7em}.quarto-dashboard .card .card-body .itables .dataTables_wrapper .dt-buttons .btn span{font-size:.8em;color:#343a40}.quarto-dashboard .card .card-body .itables .dataTables_wrapper .dataTables_info{margin-left:.5em;margin-bottom:.5em;padding-top:0}@media(min-width: 768px){.quarto-dashboard .card .card-body .itables .dataTables_wrapper .dataTables_info{font-size:.875em}}@media(max-width: 767.98px){.quarto-dashboard .card .card-body .itables .dataTables_wrapper .dataTables_info{font-size:.8em}}.quarto-dashboard .card .card-body .itables .dataTables_wrapper .dataTables_filter{margin-bottom:.5em;font-size:.875em}.quarto-dashboard .card .card-body .itables .dataTables_wrapper .dataTables_filter input[type=search]{padding:1px 5px 1px 5px;font-size:.875em}.quarto-dashboard .card .card-body .itables .dataTables_wrapper .dataTables_length{flex-basis:1 1 50%;margin-bottom:.5em;font-size:.875em}.quarto-dashboard .card .card-body .itables .dataTables_wrapper .dataTables_length select{padding:.4em 3em .4em .5em;font-size:.875em;margin-left:.2em;margin-right:.2em}.quarto-dashboard .card .card-body .itables .dataTables_wrapper .dataTables_paginate{flex-shrink:0}@media(min-width: 768px){.quarto-dashboard .card .card-body .itables .dataTables_wrapper .dataTables_paginate{margin-left:auto}}.quarto-dashboard .card .card-body .itables .dataTables_wrapper .dataTables_paginate ul.pagination .paginate_button .page-link{font-size:.8em}.quarto-dashboard .card .card-footer{font-size:.9em}.quarto-dashboard .card .card-toolbar{display:flex;flex-grow:1;flex-direction:row;width:100%;flex-wrap:wrap}.quarto-dashboard .card .card-toolbar>*{font-size:.8em;flex-grow:0}.quarto-dashboard .card .card-toolbar>.card-title{font-size:1em;flex-grow:1;align-self:flex-start;margin-top:.1em}.quarto-dashboard .card .card-toolbar .cell-output-display{display:flex}.quarto-dashboard .card .card-toolbar .shiny-input-container{padding-bottom:.5em;margin-bottom:.5em;width:inherit}.quarto-dashboard .card .card-toolbar .shiny-input-container>.checkbox:first-child{margin-top:6px}.quarto-dashboard .card .card-toolbar>*:last-child{margin-right:0}.quarto-dashboard .card .card-toolbar>*>*{margin-right:1em;align-items:baseline}.quarto-dashboard .card .card-toolbar>*>*>a{text-decoration:none;margin-top:auto;margin-bottom:auto}.quarto-dashboard .card .card-toolbar form{width:fit-content}.quarto-dashboard .card .card-toolbar form label{padding-top:.2em;padding-bottom:.2em;width:fit-content}.quarto-dashboard .card .card-toolbar form input[type=date]{width:fit-content}.quarto-dashboard .card .card-toolbar form input[type=color]{width:3em}.quarto-dashboard .card .card-toolbar form button{padding:.4em}.quarto-dashboard .card .card-toolbar form select{width:fit-content}.quarto-dashboard .card .card-toolbar .cell-output-display{display:flex}.quarto-dashboard .card .card-toolbar .shiny-input-container{padding-bottom:.5em;margin-bottom:.5em;width:inherit}.quarto-dashboard .card .card-toolbar .shiny-input-container>.checkbox:first-child{margin-top:6px}.quarto-dashboard .card .card-toolbar>*:last-child{margin-right:0}.quarto-dashboard .card .card-toolbar>*>*{margin-right:1em;align-items:baseline}.quarto-dashboard .card .card-toolbar>*>*>a{text-decoration:none;margin-top:auto;margin-bottom:auto}.quarto-dashboard .card .card-toolbar .shiny-input-container{padding-bottom:0;margin-bottom:0}.quarto-dashboard .card .card-toolbar .shiny-input-container>*{flex-shrink:0;flex-grow:0}.quarto-dashboard .card .card-toolbar .form-group.shiny-input-container:not([role=group])>label{margin-bottom:0}.quarto-dashboard .card .card-toolbar .shiny-input-container.no-baseline{align-items:start;padding-top:6px}.quarto-dashboard .card .card-toolbar .shiny-input-container{display:flex;align-items:baseline}.quarto-dashboard .card .card-toolbar .shiny-input-container label{padding-right:.4em}.quarto-dashboard .card .card-toolbar .shiny-input-container .bslib-input-switch{margin-top:6px}.quarto-dashboard .card .card-toolbar input[type=text]{line-height:1;width:inherit}.quarto-dashboard .card .card-toolbar .input-daterange{width:inherit}.quarto-dashboard .card .card-toolbar .input-daterange input[type=text]{height:2.4em;width:10em}.quarto-dashboard .card .card-toolbar .input-daterange .input-group-addon{height:auto;padding:0;margin-left:-5px !important;margin-right:-5px}.quarto-dashboard .card .card-toolbar .input-daterange .input-group-addon .input-group-text{padding-top:0;padding-bottom:0;height:100%}.quarto-dashboard .card .card-toolbar span.irs.irs--shiny{width:10em}.quarto-dashboard .card .card-toolbar span.irs.irs--shiny .irs-line{top:9px}.quarto-dashboard .card .card-toolbar span.irs.irs--shiny .irs-min,.quarto-dashboard .card .card-toolbar span.irs.irs--shiny .irs-max,.quarto-dashboard .card .card-toolbar span.irs.irs--shiny .irs-from,.quarto-dashboard .card .card-toolbar span.irs.irs--shiny .irs-to,.quarto-dashboard .card .card-toolbar span.irs.irs--shiny .irs-single{top:20px}.quarto-dashboard .card .card-toolbar span.irs.irs--shiny .irs-bar{top:8px}.quarto-dashboard .card .card-toolbar span.irs.irs--shiny .irs-handle{top:0px}.quarto-dashboard .card .card-toolbar .shiny-input-checkboxgroup>label{margin-top:6px}.quarto-dashboard .card .card-toolbar .shiny-input-checkboxgroup>.shiny-options-group{margin-top:0;align-items:baseline}.quarto-dashboard .card .card-toolbar .shiny-input-radiogroup>label{margin-top:6px}.quarto-dashboard .card .card-toolbar .shiny-input-radiogroup>.shiny-options-group{align-items:baseline;margin-top:0}.quarto-dashboard .card .card-toolbar .shiny-input-radiogroup>.shiny-options-group>.radio{margin-right:.3em}.quarto-dashboard .card .card-toolbar .form-select{padding-top:.2em;padding-bottom:.2em}.quarto-dashboard .card .card-toolbar .shiny-input-select{min-width:6em}.quarto-dashboard .card .card-toolbar div.checkbox{margin-bottom:0px}.quarto-dashboard .card .card-toolbar>.checkbox:first-child{margin-top:6px}.quarto-dashboard .card-body>table>thead{border-top:none}.quarto-dashboard .card-body>.table>:not(caption)>*>*{background-color:#fff}.tableFloatingHeaderOriginal{background-color:#fff;position:sticky !important;top:0 !important}.dashboard-data-table{margin-top:-1px}div.value-box-area span.observablehq--number{font-size:calc(clamp(.1em,15cqw,5em)*1.25);line-height:1.2;color:inherit;font-family:var(--bs-body-font-family)}.quarto-listing{padding-bottom:1em}.listing-pagination{padding-top:.5em}ul.pagination{float:right;padding-left:8px;padding-top:.5em}ul.pagination li{padding-right:.75em}ul.pagination li.disabled a,ul.pagination li.active a{color:#fff;text-decoration:none}ul.pagination li:last-of-type{padding-right:0}.listing-actions-group{display:flex}.quarto-listing-filter{margin-bottom:1em;width:200px;margin-left:auto}.quarto-listing-sort{margin-bottom:1em;margin-right:auto;width:auto}.quarto-listing-sort .input-group-text{font-size:.8em}.input-group-text{border-right:none}.quarto-listing-sort select.form-select{font-size:.8em}.listing-no-matching{text-align:center;padding-top:2em;padding-bottom:3em;font-size:1em}#quarto-margin-sidebar .quarto-listing-category{padding-top:0;font-size:1rem}#quarto-margin-sidebar .quarto-listing-category-title{cursor:pointer;font-weight:600;font-size:1rem}.quarto-listing-category .category{cursor:pointer}.quarto-listing-category .category.active{font-weight:600}.quarto-listing-category.category-cloud{display:flex;flex-wrap:wrap;align-items:baseline}.quarto-listing-category.category-cloud .category{padding-right:5px}.quarto-listing-category.category-cloud .category-cloud-1{font-size:.75em}.quarto-listing-category.category-cloud .category-cloud-2{font-size:.95em}.quarto-listing-category.category-cloud .category-cloud-3{font-size:1.15em}.quarto-listing-category.category-cloud .category-cloud-4{font-size:1.35em}.quarto-listing-category.category-cloud .category-cloud-5{font-size:1.55em}.quarto-listing-category.category-cloud .category-cloud-6{font-size:1.75em}.quarto-listing-category.category-cloud .category-cloud-7{font-size:1.95em}.quarto-listing-category.category-cloud .category-cloud-8{font-size:2.15em}.quarto-listing-category.category-cloud .category-cloud-9{font-size:2.35em}.quarto-listing-category.category-cloud .category-cloud-10{font-size:2.55em}.quarto-listing-cols-1{grid-template-columns:repeat(1, minmax(0, 1fr));gap:1.5em}@media(max-width: 767.98px){.quarto-listing-cols-1{grid-template-columns:repeat(1, minmax(0, 1fr));gap:1.5em}}@media(max-width: 575.98px){.quarto-listing-cols-1{grid-template-columns:minmax(0, 1fr);gap:1.5em}}.quarto-listing-cols-2{grid-template-columns:repeat(2, minmax(0, 1fr));gap:1.5em}@media(max-width: 767.98px){.quarto-listing-cols-2{grid-template-columns:repeat(2, minmax(0, 1fr));gap:1.5em}}@media(max-width: 575.98px){.quarto-listing-cols-2{grid-template-columns:minmax(0, 1fr);gap:1.5em}}.quarto-listing-cols-3{grid-template-columns:repeat(3, minmax(0, 1fr));gap:1.5em}@media(max-width: 767.98px){.quarto-listing-cols-3{grid-template-columns:repeat(2, minmax(0, 1fr));gap:1.5em}}@media(max-width: 575.98px){.quarto-listing-cols-3{grid-template-columns:minmax(0, 1fr);gap:1.5em}}.quarto-listing-cols-4{grid-template-columns:repeat(4, minmax(0, 1fr));gap:1.5em}@media(max-width: 767.98px){.quarto-listing-cols-4{grid-template-columns:repeat(2, minmax(0, 1fr));gap:1.5em}}@media(max-width: 575.98px){.quarto-listing-cols-4{grid-template-columns:minmax(0, 1fr);gap:1.5em}}.quarto-listing-cols-5{grid-template-columns:repeat(5, minmax(0, 1fr));gap:1.5em}@media(max-width: 767.98px){.quarto-listing-cols-5{grid-template-columns:repeat(2, minmax(0, 1fr));gap:1.5em}}@media(max-width: 575.98px){.quarto-listing-cols-5{grid-template-columns:minmax(0, 1fr);gap:1.5em}}.quarto-listing-cols-6{grid-template-columns:repeat(6, minmax(0, 1fr));gap:1.5em}@media(max-width: 767.98px){.quarto-listing-cols-6{grid-template-columns:repeat(2, minmax(0, 1fr));gap:1.5em}}@media(max-width: 575.98px){.quarto-listing-cols-6{grid-template-columns:minmax(0, 1fr);gap:1.5em}}.quarto-listing-cols-7{grid-template-columns:repeat(7, minmax(0, 1fr));gap:1.5em}@media(max-width: 767.98px){.quarto-listing-cols-7{grid-template-columns:repeat(2, minmax(0, 1fr));gap:1.5em}}@media(max-width: 575.98px){.quarto-listing-cols-7{grid-template-columns:minmax(0, 1fr);gap:1.5em}}.quarto-listing-cols-8{grid-template-columns:repeat(8, minmax(0, 1fr));gap:1.5em}@media(max-width: 767.98px){.quarto-listing-cols-8{grid-template-columns:repeat(2, minmax(0, 1fr));gap:1.5em}}@media(max-width: 575.98px){.quarto-listing-cols-8{grid-template-columns:minmax(0, 1fr);gap:1.5em}}.quarto-listing-cols-9{grid-template-columns:repeat(9, minmax(0, 1fr));gap:1.5em}@media(max-width: 767.98px){.quarto-listing-cols-9{grid-template-columns:repeat(2, minmax(0, 1fr));gap:1.5em}}@media(max-width: 575.98px){.quarto-listing-cols-9{grid-template-columns:minmax(0, 1fr);gap:1.5em}}.quarto-listing-cols-10{grid-template-columns:repeat(10, minmax(0, 1fr));gap:1.5em}@media(max-width: 767.98px){.quarto-listing-cols-10{grid-template-columns:repeat(2, minmax(0, 1fr));gap:1.5em}}@media(max-width: 575.98px){.quarto-listing-cols-10{grid-template-columns:minmax(0, 1fr);gap:1.5em}}.quarto-listing-cols-11{grid-template-columns:repeat(11, minmax(0, 1fr));gap:1.5em}@media(max-width: 767.98px){.quarto-listing-cols-11{grid-template-columns:repeat(2, minmax(0, 1fr));gap:1.5em}}@media(max-width: 575.98px){.quarto-listing-cols-11{grid-template-columns:minmax(0, 1fr);gap:1.5em}}.quarto-listing-cols-12{grid-template-columns:repeat(12, minmax(0, 1fr));gap:1.5em}@media(max-width: 767.98px){.quarto-listing-cols-12{grid-template-columns:repeat(2, minmax(0, 1fr));gap:1.5em}}@media(max-width: 575.98px){.quarto-listing-cols-12{grid-template-columns:minmax(0, 1fr);gap:1.5em}}.quarto-listing-grid{gap:1.5em}.quarto-grid-item.borderless{border:none}.quarto-grid-item.borderless .listing-categories .listing-category:last-of-type,.quarto-grid-item.borderless .listing-categories .listing-category:first-of-type{padding-left:0}.quarto-grid-item.borderless .listing-categories .listing-category{border:0}.quarto-grid-link{text-decoration:none;color:inherit}.quarto-grid-link:hover{text-decoration:none;color:inherit}.quarto-grid-item h5.title,.quarto-grid-item .title.h5{margin-top:0;margin-bottom:0}.quarto-grid-item .card-footer{display:flex;justify-content:space-between;font-size:.8em}.quarto-grid-item .card-footer p{margin-bottom:0}.quarto-grid-item p.card-img-top{margin-bottom:0}.quarto-grid-item p.card-img-top>img{object-fit:cover}.quarto-grid-item .card-other-values{margin-top:.5em;font-size:.8em}.quarto-grid-item .card-other-values tr{margin-bottom:.5em}.quarto-grid-item .card-other-values tr>td:first-of-type{font-weight:600;padding-right:1em;padding-left:1em;vertical-align:top}.quarto-grid-item div.post-contents{display:flex;flex-direction:column;text-decoration:none;height:100%}.quarto-grid-item .listing-item-img-placeholder{background-color:rgba(52,58,64,.25);flex-shrink:0}.quarto-grid-item .card-attribution{padding-top:1em;display:flex;gap:1em;text-transform:uppercase;color:#6c757d;font-weight:500;flex-grow:10;align-items:flex-end}.quarto-grid-item .description{padding-bottom:1em}.quarto-grid-item .card-attribution .date{align-self:flex-end}.quarto-grid-item .card-attribution.justify{justify-content:space-between}.quarto-grid-item .card-attribution.start{justify-content:flex-start}.quarto-grid-item .card-attribution.end{justify-content:flex-end}.quarto-grid-item .card-title{margin-bottom:.1em}.quarto-grid-item .card-subtitle{padding-top:.25em}.quarto-grid-item .card-text{font-size:.9em}.quarto-grid-item .listing-reading-time{padding-bottom:.25em}.quarto-grid-item .card-text-small{font-size:.8em}.quarto-grid-item .card-subtitle.subtitle{font-size:.9em;font-weight:600;padding-bottom:.5em}.quarto-grid-item .listing-categories{display:flex;flex-wrap:wrap;padding-bottom:5px}.quarto-grid-item .listing-categories .listing-category{color:#6c757d;border:solid 1px #dee2e6;border-radius:.25rem;text-transform:uppercase;font-size:.65em;padding-left:.5em;padding-right:.5em;padding-top:.15em;padding-bottom:.15em;cursor:pointer;margin-right:4px;margin-bottom:4px}.quarto-grid-item.card-right{text-align:right}.quarto-grid-item.card-right .listing-categories{justify-content:flex-end}.quarto-grid-item.card-left{text-align:left}.quarto-grid-item.card-center{text-align:center}.quarto-grid-item.card-center .listing-description{text-align:justify}.quarto-grid-item.card-center .listing-categories{justify-content:center}table.quarto-listing-table td.image{padding:0px}table.quarto-listing-table td.image img{width:100%;max-width:50px;object-fit:contain}table.quarto-listing-table a{text-decoration:none;word-break:keep-all}table.quarto-listing-table th a{color:inherit}table.quarto-listing-table th a.asc:after{margin-bottom:-2px;margin-left:5px;display:inline-block;height:1rem;width:1rem;background-repeat:no-repeat;background-size:1rem 1rem;background-image:url('data:image/svg+xml,');content:""}table.quarto-listing-table th a.desc:after{margin-bottom:-2px;margin-left:5px;display:inline-block;height:1rem;width:1rem;background-repeat:no-repeat;background-size:1rem 1rem;background-image:url('data:image/svg+xml,');content:""}table.quarto-listing-table.table-hover td{cursor:pointer}.quarto-post.image-left{flex-direction:row}.quarto-post.image-right{flex-direction:row-reverse}@media(max-width: 767.98px){.quarto-post.image-right,.quarto-post.image-left{gap:0em;flex-direction:column}.quarto-post .metadata{padding-bottom:1em;order:2}.quarto-post .body{order:1}.quarto-post .thumbnail{order:3}}.list.quarto-listing-default div:last-of-type{border-bottom:none}@media(min-width: 992px){.quarto-listing-container-default{margin-right:2em}}div.quarto-post{display:flex;gap:2em;margin-bottom:1.5em;border-bottom:1px solid #dee2e6}@media(max-width: 767.98px){div.quarto-post{padding-bottom:1em}}div.quarto-post .metadata{flex-basis:20%;flex-grow:0;margin-top:.2em;flex-shrink:10}div.quarto-post .thumbnail{flex-basis:30%;flex-grow:0;flex-shrink:0}div.quarto-post .thumbnail img{margin-top:.4em;width:100%;object-fit:cover}div.quarto-post .body{flex-basis:45%;flex-grow:1;flex-shrink:0}div.quarto-post .body h3.listing-title,div.quarto-post .body .listing-title.h3{margin-top:0px;margin-bottom:0px;border-bottom:none}div.quarto-post .body .listing-subtitle{font-size:.875em;margin-bottom:.5em;margin-top:.2em}div.quarto-post .body .description{font-size:.9em}div.quarto-post .body pre code{white-space:pre-wrap}div.quarto-post a{color:#343a40;text-decoration:none}div.quarto-post .metadata{display:flex;flex-direction:column;font-size:.8em;font-family:"Source Sans Pro",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";flex-basis:33%}div.quarto-post .listing-categories{display:flex;flex-wrap:wrap;padding-bottom:5px}div.quarto-post .listing-categories .listing-category{color:#6c757d;border:solid 1px #dee2e6;border-radius:.25rem;text-transform:uppercase;font-size:.65em;padding-left:.5em;padding-right:.5em;padding-top:.15em;padding-bottom:.15em;cursor:pointer;margin-right:4px;margin-bottom:4px}div.quarto-post .listing-description{margin-bottom:.5em}div.quarto-about-jolla{display:flex !important;flex-direction:column;align-items:center;margin-top:10%;padding-bottom:1em}div.quarto-about-jolla .about-image{object-fit:cover;margin-left:auto;margin-right:auto;margin-bottom:1.5em}div.quarto-about-jolla img.round{border-radius:50%}div.quarto-about-jolla img.rounded{border-radius:10px}div.quarto-about-jolla .quarto-title h1.title,div.quarto-about-jolla .quarto-title .title.h1{text-align:center}div.quarto-about-jolla .quarto-title .description{text-align:center}div.quarto-about-jolla h2,div.quarto-about-jolla .h2{border-bottom:none}div.quarto-about-jolla .about-sep{width:60%}div.quarto-about-jolla main{text-align:center}div.quarto-about-jolla .about-links{display:flex}@media(min-width: 992px){div.quarto-about-jolla .about-links{flex-direction:row;column-gap:.8em;row-gap:15px;flex-wrap:wrap}}@media(max-width: 991.98px){div.quarto-about-jolla .about-links{flex-direction:column;row-gap:1em;width:100%;padding-bottom:1.5em}}div.quarto-about-jolla .about-link{color:#626d78;text-decoration:none;border:solid 1px}@media(min-width: 992px){div.quarto-about-jolla .about-link{font-size:.8em;padding:.25em .5em;border-radius:4px}}@media(max-width: 991.98px){div.quarto-about-jolla .about-link{font-size:1.1em;padding:.5em .5em;text-align:center;border-radius:6px}}div.quarto-about-jolla .about-link:hover{color:#2761e3}div.quarto-about-jolla .about-link i.bi{margin-right:.15em}div.quarto-about-solana{display:flex !important;flex-direction:column;padding-top:3em !important;padding-bottom:1em}div.quarto-about-solana .about-entity{display:flex !important;align-items:start;justify-content:space-between}@media(min-width: 992px){div.quarto-about-solana .about-entity{flex-direction:row}}@media(max-width: 991.98px){div.quarto-about-solana .about-entity{flex-direction:column-reverse;align-items:center;text-align:center}}div.quarto-about-solana .about-entity .entity-contents{display:flex;flex-direction:column}@media(max-width: 767.98px){div.quarto-about-solana .about-entity .entity-contents{width:100%}}div.quarto-about-solana .about-entity .about-image{object-fit:cover}@media(max-width: 991.98px){div.quarto-about-solana .about-entity .about-image{margin-bottom:1.5em}}div.quarto-about-solana .about-entity img.round{border-radius:50%}div.quarto-about-solana .about-entity img.rounded{border-radius:10px}div.quarto-about-solana .about-entity .about-links{display:flex;justify-content:left;padding-bottom:1.2em}@media(min-width: 992px){div.quarto-about-solana .about-entity .about-links{flex-direction:row;column-gap:.8em;row-gap:15px;flex-wrap:wrap}}@media(max-width: 991.98px){div.quarto-about-solana .about-entity .about-links{flex-direction:column;row-gap:1em;width:100%;padding-bottom:1.5em}}div.quarto-about-solana .about-entity .about-link{color:#626d78;text-decoration:none;border:solid 1px}@media(min-width: 992px){div.quarto-about-solana .about-entity .about-link{font-size:.8em;padding:.25em .5em;border-radius:4px}}@media(max-width: 991.98px){div.quarto-about-solana .about-entity .about-link{font-size:1.1em;padding:.5em .5em;text-align:center;border-radius:6px}}div.quarto-about-solana .about-entity .about-link:hover{color:#2761e3}div.quarto-about-solana .about-entity .about-link i.bi{margin-right:.15em}div.quarto-about-solana .about-contents{padding-right:1.5em;flex-basis:0;flex-grow:1}div.quarto-about-solana .about-contents main.content{margin-top:0}div.quarto-about-solana .about-contents h2,div.quarto-about-solana .about-contents .h2{border-bottom:none}div.quarto-about-trestles{display:flex !important;flex-direction:row;padding-top:3em !important;padding-bottom:1em}@media(max-width: 991.98px){div.quarto-about-trestles{flex-direction:column;padding-top:0em !important}}div.quarto-about-trestles .about-entity{display:flex !important;flex-direction:column;align-items:center;text-align:center;padding-right:1em}@media(min-width: 992px){div.quarto-about-trestles .about-entity{flex:0 0 42%}}div.quarto-about-trestles .about-entity .about-image{object-fit:cover;margin-bottom:1.5em}div.quarto-about-trestles .about-entity img.round{border-radius:50%}div.quarto-about-trestles .about-entity img.rounded{border-radius:10px}div.quarto-about-trestles .about-entity .about-links{display:flex;justify-content:center}@media(min-width: 992px){div.quarto-about-trestles .about-entity .about-links{flex-direction:row;column-gap:.8em;row-gap:15px;flex-wrap:wrap}}@media(max-width: 991.98px){div.quarto-about-trestles .about-entity .about-links{flex-direction:column;row-gap:1em;width:100%;padding-bottom:1.5em}}div.quarto-about-trestles .about-entity .about-link{color:#626d78;text-decoration:none;border:solid 1px}@media(min-width: 992px){div.quarto-about-trestles .about-entity .about-link{font-size:.8em;padding:.25em .5em;border-radius:4px}}@media(max-width: 991.98px){div.quarto-about-trestles .about-entity .about-link{font-size:1.1em;padding:.5em .5em;text-align:center;border-radius:6px}}div.quarto-about-trestles .about-entity .about-link:hover{color:#2761e3}div.quarto-about-trestles .about-entity .about-link i.bi{margin-right:.15em}div.quarto-about-trestles .about-contents{flex-basis:0;flex-grow:1}div.quarto-about-trestles .about-contents h2,div.quarto-about-trestles .about-contents .h2{border-bottom:none}@media(min-width: 992px){div.quarto-about-trestles .about-contents{border-left:solid 1px #dee2e6;padding-left:1.5em}}div.quarto-about-trestles .about-contents main.content{margin-top:0}div.quarto-about-marquee{padding-bottom:1em}div.quarto-about-marquee .about-contents{display:flex;flex-direction:column}div.quarto-about-marquee .about-image{max-height:550px;margin-bottom:1.5em;object-fit:cover}div.quarto-about-marquee img.round{border-radius:50%}div.quarto-about-marquee img.rounded{border-radius:10px}div.quarto-about-marquee h2,div.quarto-about-marquee .h2{border-bottom:none}div.quarto-about-marquee .about-links{display:flex;justify-content:center;padding-top:1.5em}@media(min-width: 992px){div.quarto-about-marquee .about-links{flex-direction:row;column-gap:.8em;row-gap:15px;flex-wrap:wrap}}@media(max-width: 991.98px){div.quarto-about-marquee .about-links{flex-direction:column;row-gap:1em;width:100%;padding-bottom:1.5em}}div.quarto-about-marquee .about-link{color:#626d78;text-decoration:none;border:solid 1px}@media(min-width: 992px){div.quarto-about-marquee .about-link{font-size:.8em;padding:.25em .5em;border-radius:4px}}@media(max-width: 991.98px){div.quarto-about-marquee .about-link{font-size:1.1em;padding:.5em .5em;text-align:center;border-radius:6px}}div.quarto-about-marquee .about-link:hover{color:#2761e3}div.quarto-about-marquee .about-link i.bi{margin-right:.15em}@media(min-width: 992px){div.quarto-about-marquee .about-link{border:none}}div.quarto-about-broadside{display:flex;flex-direction:column;padding-bottom:1em}div.quarto-about-broadside .about-main{display:flex !important;padding-top:0 !important}@media(min-width: 992px){div.quarto-about-broadside .about-main{flex-direction:row;align-items:flex-start}}@media(max-width: 991.98px){div.quarto-about-broadside .about-main{flex-direction:column}}@media(max-width: 991.98px){div.quarto-about-broadside .about-main .about-entity{flex-shrink:0;width:100%;height:450px;margin-bottom:1.5em;background-size:cover;background-repeat:no-repeat}}@media(min-width: 992px){div.quarto-about-broadside .about-main .about-entity{flex:0 10 50%;margin-right:1.5em;width:100%;height:100%;background-size:100%;background-repeat:no-repeat}}div.quarto-about-broadside .about-main .about-contents{padding-top:14px;flex:0 0 50%}div.quarto-about-broadside h2,div.quarto-about-broadside .h2{border-bottom:none}div.quarto-about-broadside .about-sep{margin-top:1.5em;width:60%;align-self:center}div.quarto-about-broadside .about-links{display:flex;justify-content:center;column-gap:20px;padding-top:1.5em}@media(min-width: 992px){div.quarto-about-broadside .about-links{flex-direction:row;column-gap:.8em;row-gap:15px;flex-wrap:wrap}}@media(max-width: 991.98px){div.quarto-about-broadside .about-links{flex-direction:column;row-gap:1em;width:100%;padding-bottom:1.5em}}div.quarto-about-broadside .about-link{color:#626d78;text-decoration:none;border:solid 1px}@media(min-width: 992px){div.quarto-about-broadside .about-link{font-size:.8em;padding:.25em .5em;border-radius:4px}}@media(max-width: 991.98px){div.quarto-about-broadside .about-link{font-size:1.1em;padding:.5em .5em;text-align:center;border-radius:6px}}div.quarto-about-broadside .about-link:hover{color:#2761e3}div.quarto-about-broadside .about-link i.bi{margin-right:.15em}@media(min-width: 992px){div.quarto-about-broadside .about-link{border:none}}.tippy-box[data-theme~=quarto]{background-color:#fff;border:solid 1px #dee2e6;border-radius:.25rem;color:#343a40;font-size:.875rem}.tippy-box[data-theme~=quarto]>.tippy-backdrop{background-color:#fff}.tippy-box[data-theme~=quarto]>.tippy-arrow:after,.tippy-box[data-theme~=quarto]>.tippy-svg-arrow:after{content:"";position:absolute;z-index:-1}.tippy-box[data-theme~=quarto]>.tippy-arrow:after{border-color:rgba(0,0,0,0);border-style:solid}.tippy-box[data-placement^=top]>.tippy-arrow:before{bottom:-6px}.tippy-box[data-placement^=bottom]>.tippy-arrow:before{top:-6px}.tippy-box[data-placement^=right]>.tippy-arrow:before{left:-6px}.tippy-box[data-placement^=left]>.tippy-arrow:before{right:-6px}.tippy-box[data-theme~=quarto][data-placement^=top]>.tippy-arrow:before{border-top-color:#fff}.tippy-box[data-theme~=quarto][data-placement^=top]>.tippy-arrow:after{border-top-color:#dee2e6;border-width:7px 7px 0;top:17px;left:1px}.tippy-box[data-theme~=quarto][data-placement^=top]>.tippy-svg-arrow>svg{top:16px}.tippy-box[data-theme~=quarto][data-placement^=top]>.tippy-svg-arrow:after{top:17px}.tippy-box[data-theme~=quarto][data-placement^=bottom]>.tippy-arrow:before{border-bottom-color:#fff;bottom:16px}.tippy-box[data-theme~=quarto][data-placement^=bottom]>.tippy-arrow:after{border-bottom-color:#dee2e6;border-width:0 7px 7px;bottom:17px;left:1px}.tippy-box[data-theme~=quarto][data-placement^=bottom]>.tippy-svg-arrow>svg{bottom:15px}.tippy-box[data-theme~=quarto][data-placement^=bottom]>.tippy-svg-arrow:after{bottom:17px}.tippy-box[data-theme~=quarto][data-placement^=left]>.tippy-arrow:before{border-left-color:#fff}.tippy-box[data-theme~=quarto][data-placement^=left]>.tippy-arrow:after{border-left-color:#dee2e6;border-width:7px 0 7px 7px;left:17px;top:1px}.tippy-box[data-theme~=quarto][data-placement^=left]>.tippy-svg-arrow>svg{left:11px}.tippy-box[data-theme~=quarto][data-placement^=left]>.tippy-svg-arrow:after{left:12px}.tippy-box[data-theme~=quarto][data-placement^=right]>.tippy-arrow:before{border-right-color:#fff;right:16px}.tippy-box[data-theme~=quarto][data-placement^=right]>.tippy-arrow:after{border-width:7px 7px 7px 0;right:17px;top:1px;border-right-color:#dee2e6}.tippy-box[data-theme~=quarto][data-placement^=right]>.tippy-svg-arrow>svg{right:11px}.tippy-box[data-theme~=quarto][data-placement^=right]>.tippy-svg-arrow:after{right:12px}.tippy-box[data-theme~=quarto]>.tippy-svg-arrow{fill:#343a40}.tippy-box[data-theme~=quarto]>.tippy-svg-arrow:after{background-image:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iNiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNMCA2czEuNzk2LS4wMTMgNC42Ny0zLjYxNUM1Ljg1MS45IDYuOTMuMDA2IDggMGMxLjA3LS4wMDYgMi4xNDguODg3IDMuMzQzIDIuMzg1QzE0LjIzMyA2LjAwNSAxNiA2IDE2IDZIMHoiIGZpbGw9InJnYmEoMCwgOCwgMTYsIDAuMikiLz48L3N2Zz4=);background-size:16px 6px;width:16px;height:6px}.top-right{position:absolute;top:1em;right:1em}.visually-hidden{border:0;clip:rect(0 0 0 0);height:auto;margin:0;overflow:hidden;padding:0;position:absolute;width:1px;white-space:nowrap}.hidden{display:none !important}.zindex-bottom{z-index:-1 !important}figure.figure{display:block}.quarto-layout-panel{margin-bottom:1em}.quarto-layout-panel>figure{width:100%}.quarto-layout-panel>figure>figcaption,.quarto-layout-panel>.panel-caption{margin-top:10pt}.quarto-layout-panel>.table-caption{margin-top:0px}.table-caption p{margin-bottom:.5em}.quarto-layout-row{display:flex;flex-direction:row;align-items:flex-start}.quarto-layout-valign-top{align-items:flex-start}.quarto-layout-valign-bottom{align-items:flex-end}.quarto-layout-valign-center{align-items:center}.quarto-layout-cell{position:relative;margin-right:20px}.quarto-layout-cell:last-child{margin-right:0}.quarto-layout-cell figure,.quarto-layout-cell>p{margin:.2em}.quarto-layout-cell img{max-width:100%}.quarto-layout-cell .html-widget{width:100% !important}.quarto-layout-cell div figure p{margin:0}.quarto-layout-cell figure{display:block;margin-inline-start:0;margin-inline-end:0}.quarto-layout-cell table{display:inline-table}.quarto-layout-cell-subref figcaption,figure .quarto-layout-row figure figcaption{text-align:center;font-style:italic}.quarto-figure{position:relative;margin-bottom:1em}.quarto-figure>figure{width:100%;margin-bottom:0}.quarto-figure-left>figure>p,.quarto-figure-left>figure>div{text-align:left}.quarto-figure-center>figure>p,.quarto-figure-center>figure>div{text-align:center}.quarto-figure-right>figure>p,.quarto-figure-right>figure>div{text-align:right}.quarto-figure>figure>div.cell-annotation,.quarto-figure>figure>div code{text-align:left}figure>p:empty{display:none}figure>p:first-child{margin-top:0;margin-bottom:0}figure>figcaption.quarto-float-caption-bottom{margin-bottom:.5em}figure>figcaption.quarto-float-caption-top{margin-top:.5em}div[id^=tbl-]{position:relative}.quarto-figure>.anchorjs-link{position:absolute;top:.6em;right:.5em}div[id^=tbl-]>.anchorjs-link{position:absolute;top:.7em;right:.3em}.quarto-figure:hover>.anchorjs-link,div[id^=tbl-]:hover>.anchorjs-link,h2:hover>.anchorjs-link,.h2:hover>.anchorjs-link,h3:hover>.anchorjs-link,.h3:hover>.anchorjs-link,h4:hover>.anchorjs-link,.h4:hover>.anchorjs-link,h5:hover>.anchorjs-link,.h5:hover>.anchorjs-link,h6:hover>.anchorjs-link,.h6:hover>.anchorjs-link,.reveal-anchorjs-link>.anchorjs-link{opacity:1}#title-block-header{margin-block-end:1rem;position:relative;margin-top:-1px}#title-block-header .abstract{margin-block-start:1rem}#title-block-header .abstract .abstract-title{font-weight:600}#title-block-header a{text-decoration:none}#title-block-header .author,#title-block-header .date,#title-block-header .doi{margin-block-end:.2rem}#title-block-header .quarto-title-block>div{display:flex}#title-block-header .quarto-title-block>div>h1,#title-block-header .quarto-title-block>div>.h1{flex-grow:1}#title-block-header .quarto-title-block>div>button{flex-shrink:0;height:2.25rem;margin-top:0}@media(min-width: 992px){#title-block-header .quarto-title-block>div>button{margin-top:5px}}tr.header>th>p:last-of-type{margin-bottom:0px}table,table.table{margin-top:.5rem;margin-bottom:.5rem}caption,.table-caption{padding-top:.5rem;padding-bottom:.5rem;text-align:center}figure.quarto-float-tbl figcaption.quarto-float-caption-top{margin-top:.5rem;margin-bottom:.25rem;text-align:center}figure.quarto-float-tbl figcaption.quarto-float-caption-bottom{padding-top:.25rem;margin-bottom:.5rem;text-align:center}.utterances{max-width:none;margin-left:-8px}iframe{margin-bottom:1em}details{margin-bottom:1em}details[show]{margin-bottom:0}details>summary{color:#6c757d}details>summary>p:only-child{display:inline}pre.sourceCode,code.sourceCode{position:relative}dd code:not(.sourceCode),p code:not(.sourceCode){white-space:pre-wrap}code{white-space:pre}@media print{code{white-space:pre-wrap}}pre>code{display:block}pre>code.sourceCode{white-space:pre}pre>code.sourceCode>span>a:first-child::before{text-decoration:none}pre.code-overflow-wrap>code.sourceCode{white-space:pre-wrap}pre.code-overflow-scroll>code.sourceCode{white-space:pre}code a:any-link{color:inherit;text-decoration:none}code a:hover{color:inherit;text-decoration:underline}ul.task-list{padding-left:1em}[data-tippy-root]{display:inline-block}.tippy-content .footnote-back{display:none}.footnote-back{margin-left:.2em}.tippy-content{overflow-x:auto}.quarto-embedded-source-code{display:none}.quarto-unresolved-ref{font-weight:600}.quarto-cover-image{max-width:35%;float:right;margin-left:30px}.cell-output-display .widget-subarea{margin-bottom:1em}.cell-output-display:not(.no-overflow-x),.knitsql-table:not(.no-overflow-x){overflow-x:auto}.panel-input{margin-bottom:1em}.panel-input>div,.panel-input>div>div{display:inline-block;vertical-align:top;padding-right:12px}.panel-input>p:last-child{margin-bottom:0}.layout-sidebar{margin-bottom:1em}.layout-sidebar .tab-content{border:none}.tab-content>.page-columns.active{display:grid}div.sourceCode>iframe{width:100%;height:300px;margin-bottom:-0.5em}a{text-underline-offset:3px}div.ansi-escaped-output{font-family:monospace;display:block}/*! +* +* ansi colors from IPython notebook's +* +* we also add `bright-[color]-` synonyms for the `-[color]-intense` classes since +* that seems to be what ansi_up emits +* +*/.ansi-black-fg{color:#3e424d}.ansi-black-bg{background-color:#3e424d}.ansi-black-intense-black,.ansi-bright-black-fg{color:#282c36}.ansi-black-intense-black,.ansi-bright-black-bg{background-color:#282c36}.ansi-red-fg{color:#e75c58}.ansi-red-bg{background-color:#e75c58}.ansi-red-intense-red,.ansi-bright-red-fg{color:#b22b31}.ansi-red-intense-red,.ansi-bright-red-bg{background-color:#b22b31}.ansi-green-fg{color:#00a250}.ansi-green-bg{background-color:#00a250}.ansi-green-intense-green,.ansi-bright-green-fg{color:#007427}.ansi-green-intense-green,.ansi-bright-green-bg{background-color:#007427}.ansi-yellow-fg{color:#ddb62b}.ansi-yellow-bg{background-color:#ddb62b}.ansi-yellow-intense-yellow,.ansi-bright-yellow-fg{color:#b27d12}.ansi-yellow-intense-yellow,.ansi-bright-yellow-bg{background-color:#b27d12}.ansi-blue-fg{color:#208ffb}.ansi-blue-bg{background-color:#208ffb}.ansi-blue-intense-blue,.ansi-bright-blue-fg{color:#0065ca}.ansi-blue-intense-blue,.ansi-bright-blue-bg{background-color:#0065ca}.ansi-magenta-fg{color:#d160c4}.ansi-magenta-bg{background-color:#d160c4}.ansi-magenta-intense-magenta,.ansi-bright-magenta-fg{color:#a03196}.ansi-magenta-intense-magenta,.ansi-bright-magenta-bg{background-color:#a03196}.ansi-cyan-fg{color:#60c6c8}.ansi-cyan-bg{background-color:#60c6c8}.ansi-cyan-intense-cyan,.ansi-bright-cyan-fg{color:#258f8f}.ansi-cyan-intense-cyan,.ansi-bright-cyan-bg{background-color:#258f8f}.ansi-white-fg{color:#c5c1b4}.ansi-white-bg{background-color:#c5c1b4}.ansi-white-intense-white,.ansi-bright-white-fg{color:#a1a6b2}.ansi-white-intense-white,.ansi-bright-white-bg{background-color:#a1a6b2}.ansi-default-inverse-fg{color:#fff}.ansi-default-inverse-bg{background-color:#000}.ansi-bold{font-weight:bold}.ansi-underline{text-decoration:underline}:root{--quarto-body-bg: #fff;--quarto-body-color: #343a40;--quarto-text-muted: #6c757d;--quarto-border-color: #dee2e6;--quarto-border-width: 1px;--quarto-border-radius: 0.25rem}table.gt_table{color:var(--quarto-body-color);font-size:1em;width:100%;background-color:rgba(0,0,0,0);border-top-width:inherit;border-bottom-width:inherit;border-color:var(--quarto-border-color)}table.gt_table th.gt_column_spanner_outer{color:var(--quarto-body-color);background-color:rgba(0,0,0,0);border-top-width:inherit;border-bottom-width:inherit;border-color:var(--quarto-border-color)}table.gt_table th.gt_col_heading{color:var(--quarto-body-color);font-weight:bold;background-color:rgba(0,0,0,0)}table.gt_table thead.gt_col_headings{border-bottom:1px solid currentColor;border-top-width:inherit;border-top-color:var(--quarto-border-color)}table.gt_table thead.gt_col_headings:not(:first-child){border-top-width:1px;border-top-color:var(--quarto-border-color)}table.gt_table td.gt_row{border-bottom-width:1px;border-bottom-color:var(--quarto-border-color);border-top-width:0px}table.gt_table tbody.gt_table_body{border-top-width:1px;border-bottom-width:1px;border-bottom-color:var(--quarto-border-color);border-top-color:currentColor}div.columns{display:initial;gap:initial}div.column{display:inline-block;overflow-x:initial;vertical-align:top;width:50%}.code-annotation-tip-content{word-wrap:break-word}.code-annotation-container-hidden{display:none !important}dl.code-annotation-container-grid{display:grid;grid-template-columns:min-content auto}dl.code-annotation-container-grid dt{grid-column:1}dl.code-annotation-container-grid dd{grid-column:2}pre.sourceCode.code-annotation-code{padding-right:0}code.sourceCode .code-annotation-anchor{z-index:100;position:relative;float:right;background-color:rgba(0,0,0,0)}input[type=checkbox]{margin-right:.5ch}:root{--mermaid-bg-color: #fff;--mermaid-edge-color: #343a40;--mermaid-node-fg-color: #343a40;--mermaid-fg-color: #343a40;--mermaid-fg-color--lighter: #4b545c;--mermaid-fg-color--lightest: #626d78;--mermaid-font-family: Source Sans Pro, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol;--mermaid-label-bg-color: #fff;--mermaid-label-fg-color: #2780e3;--mermaid-node-bg-color: rgba(39, 128, 227, 0.1);--mermaid-node-fg-color: #343a40}@media print{:root{font-size:11pt}#quarto-sidebar,#TOC,.nav-page{display:none}.page-columns .content{grid-column-start:page-start}.fixed-top{position:relative}.panel-caption,.figure-caption,figcaption{color:#666}}.code-copy-button{position:absolute;top:0;right:0;border:0;margin-top:5px;margin-right:5px;background-color:rgba(0,0,0,0);z-index:3}.code-copy-button:focus{outline:none}.code-copy-button-tooltip{font-size:.75em}pre.sourceCode:hover>.code-copy-button>.bi::before{display:inline-block;height:1rem;width:1rem;content:"";vertical-align:-0.125em;background-image:url('data:image/svg+xml,');background-repeat:no-repeat;background-size:1rem 1rem}pre.sourceCode:hover>.code-copy-button-checked>.bi::before{background-image:url('data:image/svg+xml,')}pre.sourceCode:hover>.code-copy-button:hover>.bi::before{background-image:url('data:image/svg+xml,')}pre.sourceCode:hover>.code-copy-button-checked:hover>.bi::before{background-image:url('data:image/svg+xml,')}main ol ol,main ul ul,main ol ul,main ul ol{margin-bottom:1em}ul>li:not(:has(>p))>ul,ol>li:not(:has(>p))>ul,ul>li:not(:has(>p))>ol,ol>li:not(:has(>p))>ol{margin-bottom:0}ul>li:not(:has(>p))>ul>li:has(>p),ol>li:not(:has(>p))>ul>li:has(>p),ul>li:not(:has(>p))>ol>li:has(>p),ol>li:not(:has(>p))>ol>li:has(>p){margin-top:1rem}body{margin:0}main.page-columns>header>h1.title,main.page-columns>header>.title.h1{margin-bottom:0}@media(min-width: 992px){body .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset] 5fr [page-start page-start-inset] 35px [body-start-outset] 35px [body-start] 1.5em [body-content-start] minmax(500px, calc(850px - 3em)) [body-content-end] 1.5em [body-end] 35px [body-end-outset] minmax(75px, 145px) [page-end-inset] 35px [page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.fullcontent:not(.floating):not(.docked) .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset] 5fr [page-start page-start-inset] 35px [body-start-outset] 35px [body-start] 1.5em [body-content-start] minmax(500px, calc(850px - 3em)) [body-content-end] 1.5em [body-end] 35px [body-end-outset] 35px [page-end-inset page-end] 5fr [screen-end-inset] 1.5em}body.slimcontent:not(.floating):not(.docked) .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset] 5fr [page-start page-start-inset] 35px [body-start-outset] 35px [body-start] 1.5em [body-content-start] minmax(500px, calc(850px - 3em)) [body-content-end] 1.5em [body-end] 50px [body-end-outset] minmax(0px, 200px) [page-end-inset] 35px [page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.listing:not(.floating):not(.docked) .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset page-start] minmax(50px, 100px) [page-start-inset] 50px [body-start-outset] 50px [body-start] 1.5em [body-content-start] minmax(500px, calc(850px - 3em)) [body-content-end] 3em [body-end] 50px [body-end-outset] minmax(0px, 250px) [page-end-inset] minmax(50px, 100px) [page-end] 1fr [screen-end-inset] 1.5em [screen-end]}body:not(.floating):not(.docked) .page-columns.toc-left{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset] 5fr [page-start] 35px [page-start-inset] minmax(0px, 175px) [body-start-outset] 35px [body-start] 1.5em [body-content-start] minmax(450px, calc(800px - 3em)) [body-content-end] 1.5em [body-end] 50px [body-end-outset] minmax(0px, 200px) [page-end-inset] 50px [page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body:not(.floating):not(.docked) .page-columns.toc-left .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset] 5fr [page-start] 35px [page-start-inset] minmax(0px, 175px) [body-start-outset] 35px [body-start] 1.5em [body-content-start] minmax(450px, calc(800px - 3em)) [body-content-end] 1.5em [body-end] 50px [body-end-outset] minmax(0px, 200px) [page-end-inset] 50px [page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.floating .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset] 5fr [page-start] minmax(25px, 50px) [page-start-inset] minmax(50px, 150px) [body-start-outset] minmax(25px, 50px) [body-start] 1.5em [body-content-start] minmax(500px, calc(800px - 3em)) [body-content-end] 1.5em [body-end] minmax(25px, 50px) [body-end-outset] minmax(50px, 150px) [page-end-inset] minmax(25px, 50px) [page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.docked .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset page-start] minmax(50px, 100px) [page-start-inset] 50px [body-start-outset] 50px [body-start] 1.5em [body-content-start] minmax(500px, calc(1000px - 3em)) [body-content-end] 1.5em [body-end] 50px [body-end-outset] minmax(50px, 100px) [page-end-inset] 50px [page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.docked.fullcontent .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset page-start] minmax(50px, 100px) [page-start-inset] 50px [body-start-outset] 50px [body-start] 1.5em [body-content-start] minmax(500px, calc(1000px - 3em)) [body-content-end] 1.5em [body-end body-end-outset page-end-inset page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.floating.fullcontent .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset] 5fr [page-start] 50px [page-start-inset] minmax(50px, 150px) [body-start-outset] 50px [body-start] 1.5em [body-content-start] minmax(500px, calc(800px - 3em)) [body-content-end] 1.5em [body-end body-end-outset page-end-inset page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.docked.slimcontent .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset page-start] minmax(50px, 100px) [page-start-inset] 50px [body-start-outset] 50px [body-start] 1.5em [body-content-start] minmax(450px, calc(750px - 3em)) [body-content-end] 1.5em [body-end] 50px [body-end-outset] minmax(0px, 200px) [page-end-inset] 50px [page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.docked.listing .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset page-start] minmax(50px, 100px) [page-start-inset] 50px [body-start-outset] 50px [body-start] 1.5em [body-content-start] minmax(500px, calc(1000px - 3em)) [body-content-end] 1.5em [body-end] 50px [body-end-outset] minmax(0px, 200px) [page-end-inset] 50px [page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.floating.slimcontent .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset] 5fr [page-start] 50px [page-start-inset] minmax(50px, 150px) [body-start-outset] 50px [body-start] 1.5em [body-content-start] minmax(450px, calc(750px - 3em)) [body-content-end] 1.5em [body-end] 50px [body-end-outset] minmax(50px, 150px) [page-end-inset] 50px [page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.floating.listing .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset] 5fr [page-start] minmax(25px, 50px) [page-start-inset] minmax(50px, 150px) [body-start-outset] minmax(25px, 50px) [body-start] 1.5em [body-content-start] minmax(500px, calc(800px - 3em)) [body-content-end] 1.5em [body-end] minmax(25px, 50px) [body-end-outset] minmax(50px, 150px) [page-end-inset] minmax(25px, 50px) [page-end] 5fr [screen-end-inset] 1.5em [screen-end]}}@media(max-width: 991.98px){body .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset page-start page-start-inset body-start-outset] 5fr [body-start] 1.5em [body-content-start] minmax(500px, calc(800px - 3em)) [body-content-end] 1.5em [body-end] 35px [body-end-outset] minmax(75px, 145px) [page-end-inset] 35px [page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.fullcontent:not(.floating):not(.docked) .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset page-start page-start-inset body-start-outset] 5fr [body-start] 1.5em [body-content-start] minmax(500px, calc(800px - 3em)) [body-content-end] 1.5em [body-end body-end-outset page-end-inset page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.slimcontent:not(.floating):not(.docked) .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset page-start page-start-inset body-start-outset] 5fr [body-start] 1.5em [body-content-start] minmax(500px, calc(800px - 3em)) [body-content-end] 1.5em [body-end] 35px [body-end-outset] minmax(75px, 145px) [page-end-inset] 35px [page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.listing:not(.floating):not(.docked) .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset page-start page-start-inset body-start-outset] 5fr [body-start] 1.5em [body-content-start] minmax(500px, calc(1250px - 3em)) [body-content-end body-end body-end-outset page-end-inset page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body:not(.floating):not(.docked) .page-columns.toc-left{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset] 5fr [page-start] 35px [page-start-inset] minmax(0px, 145px) [body-start-outset] 35px [body-start] 1.5em [body-content-start] minmax(450px, calc(800px - 3em)) [body-content-end] 1.5em [body-end body-end-outset page-end-inset page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body:not(.floating):not(.docked) .page-columns.toc-left .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset] 5fr [page-start] 35px [page-start-inset] minmax(0px, 145px) [body-start-outset] 35px [body-start] 1.5em [body-content-start] minmax(450px, calc(800px - 3em)) [body-content-end] 1.5em [body-end body-end-outset page-end-inset page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.floating .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset] 5fr [page-start page-start-inset body-start-outset body-start] 1.5em [body-content-start] minmax(500px, calc(750px - 3em)) [body-content-end] 1.5em [body-end] 50px [body-end-outset] minmax(75px, 150px) [page-end-inset] 25px [page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.docked .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset page-start page-start-inset body-start-outset body-start body-content-start] minmax(500px, calc(750px - 3em)) [body-content-end] 1.5em [body-end] 50px [body-end-outset] minmax(25px, 50px) [page-end-inset] 50px [page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.docked.fullcontent .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset page-start page-start-inset body-start-outset body-start body-content-start] minmax(500px, calc(1000px - 3em)) [body-content-end] 1.5em [body-end body-end-outset page-end-inset page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.floating.fullcontent .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset] 5fr [page-start page-start-inset body-start-outset body-start] 1em [body-content-start] minmax(500px, calc(800px - 3em)) [body-content-end] 1.5em [body-end body-end-outset page-end-inset page-end] 4fr [screen-end-inset] 1.5em [screen-end]}body.docked.slimcontent .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset page-start page-start-inset body-start-outset body-start body-content-start] minmax(500px, calc(750px - 3em)) [body-content-end] 1.5em [body-end] 50px [body-end-outset] minmax(25px, 50px) [page-end-inset] 50px [page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.docked.listing .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset page-start page-start-inset body-start-outset body-start body-content-start] minmax(500px, calc(750px - 3em)) [body-content-end] 1.5em [body-end] 50px [body-end-outset] minmax(25px, 50px) [page-end-inset] 50px [page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.floating.slimcontent .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset] 5fr [page-start page-start-inset body-start-outset body-start] 1em [body-content-start] minmax(500px, calc(750px - 3em)) [body-content-end] 1.5em [body-end] 35px [body-end-outset] minmax(75px, 145px) [page-end-inset] 35px [page-end] 4fr [screen-end-inset] 1.5em [screen-end]}body.floating.listing .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset] 5fr [page-start page-start-inset body-start-outset body-start] 1em [body-content-start] minmax(500px, calc(750px - 3em)) [body-content-end] 1.5em [body-end] 50px [body-end-outset] minmax(75px, 150px) [page-end-inset] 25px [page-end] 4fr [screen-end-inset] 1.5em [screen-end]}}@media(max-width: 767.98px){body .page-columns,body.fullcontent:not(.floating):not(.docked) .page-columns,body.slimcontent:not(.floating):not(.docked) .page-columns,body.docked .page-columns,body.docked.slimcontent .page-columns,body.docked.fullcontent .page-columns,body.floating .page-columns,body.floating.slimcontent .page-columns,body.floating.fullcontent .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset page-start page-start-inset body-start-outset body-start body-content-start] minmax(0px, 1fr) [body-content-end body-end body-end-outset page-end-inset page-end screen-end-inset] 1.5em [screen-end]}body:not(.floating):not(.docked) .page-columns.toc-left{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset page-start page-start-inset body-start-outset body-start body-content-start] minmax(0px, 1fr) [body-content-end body-end body-end-outset page-end-inset page-end screen-end-inset] 1.5em [screen-end]}body:not(.floating):not(.docked) .page-columns.toc-left .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset page-start page-start-inset body-start-outset body-start body-content-start] minmax(0px, 1fr) [body-content-end body-end body-end-outset page-end-inset page-end screen-end-inset] 1.5em [screen-end]}nav[role=doc-toc]{display:none}}body,.page-row-navigation{grid-template-rows:[page-top] max-content [contents-top] max-content [contents-bottom] max-content [page-bottom]}.page-rows-contents{grid-template-rows:[content-top] minmax(max-content, 1fr) [content-bottom] minmax(60px, max-content) [page-bottom]}.page-full{grid-column:screen-start/screen-end !important}.page-columns>*{grid-column:body-content-start/body-content-end}.page-columns.column-page>*{grid-column:page-start/page-end}.page-columns.column-page-left .page-columns.page-full>*,.page-columns.column-page-left>*{grid-column:page-start/body-content-end}.page-columns.column-page-right .page-columns.page-full>*,.page-columns.column-page-right>*{grid-column:body-content-start/page-end}.page-rows{grid-auto-rows:auto}.header{grid-column:screen-start/screen-end;grid-row:page-top/contents-top}#quarto-content{padding:0;grid-column:screen-start/screen-end;grid-row:contents-top/contents-bottom}body.floating .sidebar.sidebar-navigation{grid-column:page-start/body-start;grid-row:content-top/page-bottom}body.docked .sidebar.sidebar-navigation{grid-column:screen-start/body-start;grid-row:content-top/page-bottom}.sidebar.toc-left{grid-column:page-start/body-start;grid-row:content-top/page-bottom}.sidebar.margin-sidebar{grid-column:body-end/page-end;grid-row:content-top/page-bottom}.page-columns .content{grid-column:body-content-start/body-content-end;grid-row:content-top/content-bottom;align-content:flex-start}.page-columns .page-navigation{grid-column:body-content-start/body-content-end;grid-row:content-bottom/page-bottom}.page-columns .footer{grid-column:screen-start/screen-end;grid-row:contents-bottom/page-bottom}.page-columns .column-body{grid-column:body-content-start/body-content-end}.page-columns .column-body-fullbleed{grid-column:body-start/body-end}.page-columns .column-body-outset{grid-column:body-start-outset/body-end-outset;z-index:998;opacity:.999}.page-columns .column-body-outset table{background:#fff}.page-columns .column-body-outset-left{grid-column:body-start-outset/body-content-end;z-index:998;opacity:.999}.page-columns .column-body-outset-left table{background:#fff}.page-columns .column-body-outset-right{grid-column:body-content-start/body-end-outset;z-index:998;opacity:.999}.page-columns .column-body-outset-right table{background:#fff}.page-columns .column-page{grid-column:page-start/page-end;z-index:998;opacity:.999}.page-columns .column-page table{background:#fff}.page-columns .column-page-inset{grid-column:page-start-inset/page-end-inset;z-index:998;opacity:.999}.page-columns .column-page-inset table{background:#fff}.page-columns .column-page-inset-left{grid-column:page-start-inset/body-content-end;z-index:998;opacity:.999}.page-columns .column-page-inset-left table{background:#fff}.page-columns .column-page-inset-right{grid-column:body-content-start/page-end-inset;z-index:998;opacity:.999}.page-columns .column-page-inset-right figcaption table{background:#fff}.page-columns .column-page-left{grid-column:page-start/body-content-end;z-index:998;opacity:.999}.page-columns .column-page-left table{background:#fff}.page-columns .column-page-right{grid-column:body-content-start/page-end;z-index:998;opacity:.999}.page-columns .column-page-right figcaption table{background:#fff}#quarto-content.page-columns #quarto-margin-sidebar,#quarto-content.page-columns #quarto-sidebar{z-index:1}@media(max-width: 991.98px){#quarto-content.page-columns #quarto-margin-sidebar.collapse,#quarto-content.page-columns #quarto-sidebar.collapse,#quarto-content.page-columns #quarto-margin-sidebar.collapsing,#quarto-content.page-columns #quarto-sidebar.collapsing{z-index:1055}}#quarto-content.page-columns main.column-page,#quarto-content.page-columns main.column-page-right,#quarto-content.page-columns main.column-page-left{z-index:0}.page-columns .column-screen-inset{grid-column:screen-start-inset/screen-end-inset;z-index:998;opacity:.999}.page-columns .column-screen-inset table{background:#fff}.page-columns .column-screen-inset-left{grid-column:screen-start-inset/body-content-end;z-index:998;opacity:.999}.page-columns .column-screen-inset-left table{background:#fff}.page-columns .column-screen-inset-right{grid-column:body-content-start/screen-end-inset;z-index:998;opacity:.999}.page-columns .column-screen-inset-right table{background:#fff}.page-columns .column-screen{grid-column:screen-start/screen-end;z-index:998;opacity:.999}.page-columns .column-screen table{background:#fff}.page-columns .column-screen-left{grid-column:screen-start/body-content-end;z-index:998;opacity:.999}.page-columns .column-screen-left table{background:#fff}.page-columns .column-screen-right{grid-column:body-content-start/screen-end;z-index:998;opacity:.999}.page-columns .column-screen-right table{background:#fff}.page-columns .column-screen-inset-shaded{grid-column:screen-start/screen-end;padding:1em;background:#f8f9fa;z-index:998;opacity:.999;margin-bottom:1em}.zindex-content{z-index:998;opacity:.999}.zindex-modal{z-index:1055;opacity:.999}.zindex-over-content{z-index:999;opacity:.999}img.img-fluid.column-screen,img.img-fluid.column-screen-inset-shaded,img.img-fluid.column-screen-inset,img.img-fluid.column-screen-inset-left,img.img-fluid.column-screen-inset-right,img.img-fluid.column-screen-left,img.img-fluid.column-screen-right{width:100%}@media(min-width: 992px){.margin-caption,div.aside,aside:not(.footnotes):not(.sidebar),.column-margin{grid-column:body-end/page-end !important;z-index:998}.column-sidebar{grid-column:page-start/body-start !important;z-index:998}.column-leftmargin{grid-column:screen-start-inset/body-start !important;z-index:998}.no-row-height{height:1em;overflow:visible}}@media(max-width: 991.98px){.margin-caption,div.aside,aside:not(.footnotes):not(.sidebar),.column-margin{grid-column:body-end/page-end !important;z-index:998}.no-row-height{height:1em;overflow:visible}.page-columns.page-full{overflow:visible}.page-columns.toc-left .margin-caption,.page-columns.toc-left div.aside,.page-columns.toc-left aside:not(.footnotes):not(.sidebar),.page-columns.toc-left .column-margin{grid-column:body-content-start/body-content-end !important;z-index:998;opacity:.999}.page-columns.toc-left .no-row-height{height:initial;overflow:initial}}@media(max-width: 767.98px){.margin-caption,div.aside,aside:not(.footnotes):not(.sidebar),.column-margin{grid-column:body-content-start/body-content-end !important;z-index:998;opacity:.999}.no-row-height{height:initial;overflow:initial}#quarto-margin-sidebar{display:none}#quarto-sidebar-toc-left{display:none}.hidden-sm{display:none}}.panel-grid{display:grid;grid-template-rows:repeat(1, 1fr);grid-template-columns:repeat(24, 1fr);gap:1em}.panel-grid .g-col-1{grid-column:auto/span 1}.panel-grid .g-col-2{grid-column:auto/span 2}.panel-grid .g-col-3{grid-column:auto/span 3}.panel-grid .g-col-4{grid-column:auto/span 4}.panel-grid .g-col-5{grid-column:auto/span 5}.panel-grid .g-col-6{grid-column:auto/span 6}.panel-grid .g-col-7{grid-column:auto/span 7}.panel-grid .g-col-8{grid-column:auto/span 8}.panel-grid .g-col-9{grid-column:auto/span 9}.panel-grid .g-col-10{grid-column:auto/span 10}.panel-grid .g-col-11{grid-column:auto/span 11}.panel-grid .g-col-12{grid-column:auto/span 12}.panel-grid .g-col-13{grid-column:auto/span 13}.panel-grid .g-col-14{grid-column:auto/span 14}.panel-grid .g-col-15{grid-column:auto/span 15}.panel-grid .g-col-16{grid-column:auto/span 16}.panel-grid .g-col-17{grid-column:auto/span 17}.panel-grid .g-col-18{grid-column:auto/span 18}.panel-grid .g-col-19{grid-column:auto/span 19}.panel-grid .g-col-20{grid-column:auto/span 20}.panel-grid .g-col-21{grid-column:auto/span 21}.panel-grid .g-col-22{grid-column:auto/span 22}.panel-grid .g-col-23{grid-column:auto/span 23}.panel-grid .g-col-24{grid-column:auto/span 24}.panel-grid .g-start-1{grid-column-start:1}.panel-grid .g-start-2{grid-column-start:2}.panel-grid .g-start-3{grid-column-start:3}.panel-grid .g-start-4{grid-column-start:4}.panel-grid .g-start-5{grid-column-start:5}.panel-grid .g-start-6{grid-column-start:6}.panel-grid .g-start-7{grid-column-start:7}.panel-grid .g-start-8{grid-column-start:8}.panel-grid .g-start-9{grid-column-start:9}.panel-grid .g-start-10{grid-column-start:10}.panel-grid .g-start-11{grid-column-start:11}.panel-grid .g-start-12{grid-column-start:12}.panel-grid .g-start-13{grid-column-start:13}.panel-grid .g-start-14{grid-column-start:14}.panel-grid .g-start-15{grid-column-start:15}.panel-grid .g-start-16{grid-column-start:16}.panel-grid .g-start-17{grid-column-start:17}.panel-grid .g-start-18{grid-column-start:18}.panel-grid .g-start-19{grid-column-start:19}.panel-grid .g-start-20{grid-column-start:20}.panel-grid .g-start-21{grid-column-start:21}.panel-grid .g-start-22{grid-column-start:22}.panel-grid .g-start-23{grid-column-start:23}@media(min-width: 576px){.panel-grid .g-col-sm-1{grid-column:auto/span 1}.panel-grid .g-col-sm-2{grid-column:auto/span 2}.panel-grid .g-col-sm-3{grid-column:auto/span 3}.panel-grid .g-col-sm-4{grid-column:auto/span 4}.panel-grid .g-col-sm-5{grid-column:auto/span 5}.panel-grid .g-col-sm-6{grid-column:auto/span 6}.panel-grid .g-col-sm-7{grid-column:auto/span 7}.panel-grid .g-col-sm-8{grid-column:auto/span 8}.panel-grid .g-col-sm-9{grid-column:auto/span 9}.panel-grid .g-col-sm-10{grid-column:auto/span 10}.panel-grid .g-col-sm-11{grid-column:auto/span 11}.panel-grid .g-col-sm-12{grid-column:auto/span 12}.panel-grid .g-col-sm-13{grid-column:auto/span 13}.panel-grid .g-col-sm-14{grid-column:auto/span 14}.panel-grid .g-col-sm-15{grid-column:auto/span 15}.panel-grid .g-col-sm-16{grid-column:auto/span 16}.panel-grid .g-col-sm-17{grid-column:auto/span 17}.panel-grid .g-col-sm-18{grid-column:auto/span 18}.panel-grid .g-col-sm-19{grid-column:auto/span 19}.panel-grid .g-col-sm-20{grid-column:auto/span 20}.panel-grid .g-col-sm-21{grid-column:auto/span 21}.panel-grid .g-col-sm-22{grid-column:auto/span 22}.panel-grid .g-col-sm-23{grid-column:auto/span 23}.panel-grid .g-col-sm-24{grid-column:auto/span 24}.panel-grid .g-start-sm-1{grid-column-start:1}.panel-grid .g-start-sm-2{grid-column-start:2}.panel-grid .g-start-sm-3{grid-column-start:3}.panel-grid .g-start-sm-4{grid-column-start:4}.panel-grid .g-start-sm-5{grid-column-start:5}.panel-grid .g-start-sm-6{grid-column-start:6}.panel-grid .g-start-sm-7{grid-column-start:7}.panel-grid .g-start-sm-8{grid-column-start:8}.panel-grid .g-start-sm-9{grid-column-start:9}.panel-grid .g-start-sm-10{grid-column-start:10}.panel-grid .g-start-sm-11{grid-column-start:11}.panel-grid .g-start-sm-12{grid-column-start:12}.panel-grid .g-start-sm-13{grid-column-start:13}.panel-grid .g-start-sm-14{grid-column-start:14}.panel-grid .g-start-sm-15{grid-column-start:15}.panel-grid .g-start-sm-16{grid-column-start:16}.panel-grid .g-start-sm-17{grid-column-start:17}.panel-grid .g-start-sm-18{grid-column-start:18}.panel-grid .g-start-sm-19{grid-column-start:19}.panel-grid .g-start-sm-20{grid-column-start:20}.panel-grid .g-start-sm-21{grid-column-start:21}.panel-grid .g-start-sm-22{grid-column-start:22}.panel-grid .g-start-sm-23{grid-column-start:23}}@media(min-width: 768px){.panel-grid .g-col-md-1{grid-column:auto/span 1}.panel-grid .g-col-md-2{grid-column:auto/span 2}.panel-grid .g-col-md-3{grid-column:auto/span 3}.panel-grid .g-col-md-4{grid-column:auto/span 4}.panel-grid .g-col-md-5{grid-column:auto/span 5}.panel-grid .g-col-md-6{grid-column:auto/span 6}.panel-grid .g-col-md-7{grid-column:auto/span 7}.panel-grid .g-col-md-8{grid-column:auto/span 8}.panel-grid .g-col-md-9{grid-column:auto/span 9}.panel-grid .g-col-md-10{grid-column:auto/span 10}.panel-grid .g-col-md-11{grid-column:auto/span 11}.panel-grid .g-col-md-12{grid-column:auto/span 12}.panel-grid .g-col-md-13{grid-column:auto/span 13}.panel-grid .g-col-md-14{grid-column:auto/span 14}.panel-grid .g-col-md-15{grid-column:auto/span 15}.panel-grid .g-col-md-16{grid-column:auto/span 16}.panel-grid .g-col-md-17{grid-column:auto/span 17}.panel-grid .g-col-md-18{grid-column:auto/span 18}.panel-grid .g-col-md-19{grid-column:auto/span 19}.panel-grid .g-col-md-20{grid-column:auto/span 20}.panel-grid .g-col-md-21{grid-column:auto/span 21}.panel-grid .g-col-md-22{grid-column:auto/span 22}.panel-grid .g-col-md-23{grid-column:auto/span 23}.panel-grid .g-col-md-24{grid-column:auto/span 24}.panel-grid .g-start-md-1{grid-column-start:1}.panel-grid .g-start-md-2{grid-column-start:2}.panel-grid .g-start-md-3{grid-column-start:3}.panel-grid .g-start-md-4{grid-column-start:4}.panel-grid .g-start-md-5{grid-column-start:5}.panel-grid .g-start-md-6{grid-column-start:6}.panel-grid .g-start-md-7{grid-column-start:7}.panel-grid .g-start-md-8{grid-column-start:8}.panel-grid .g-start-md-9{grid-column-start:9}.panel-grid .g-start-md-10{grid-column-start:10}.panel-grid .g-start-md-11{grid-column-start:11}.panel-grid .g-start-md-12{grid-column-start:12}.panel-grid .g-start-md-13{grid-column-start:13}.panel-grid .g-start-md-14{grid-column-start:14}.panel-grid .g-start-md-15{grid-column-start:15}.panel-grid .g-start-md-16{grid-column-start:16}.panel-grid .g-start-md-17{grid-column-start:17}.panel-grid .g-start-md-18{grid-column-start:18}.panel-grid .g-start-md-19{grid-column-start:19}.panel-grid .g-start-md-20{grid-column-start:20}.panel-grid .g-start-md-21{grid-column-start:21}.panel-grid .g-start-md-22{grid-column-start:22}.panel-grid .g-start-md-23{grid-column-start:23}}@media(min-width: 992px){.panel-grid .g-col-lg-1{grid-column:auto/span 1}.panel-grid .g-col-lg-2{grid-column:auto/span 2}.panel-grid .g-col-lg-3{grid-column:auto/span 3}.panel-grid .g-col-lg-4{grid-column:auto/span 4}.panel-grid .g-col-lg-5{grid-column:auto/span 5}.panel-grid .g-col-lg-6{grid-column:auto/span 6}.panel-grid .g-col-lg-7{grid-column:auto/span 7}.panel-grid .g-col-lg-8{grid-column:auto/span 8}.panel-grid .g-col-lg-9{grid-column:auto/span 9}.panel-grid .g-col-lg-10{grid-column:auto/span 10}.panel-grid .g-col-lg-11{grid-column:auto/span 11}.panel-grid .g-col-lg-12{grid-column:auto/span 12}.panel-grid .g-col-lg-13{grid-column:auto/span 13}.panel-grid .g-col-lg-14{grid-column:auto/span 14}.panel-grid .g-col-lg-15{grid-column:auto/span 15}.panel-grid .g-col-lg-16{grid-column:auto/span 16}.panel-grid .g-col-lg-17{grid-column:auto/span 17}.panel-grid .g-col-lg-18{grid-column:auto/span 18}.panel-grid .g-col-lg-19{grid-column:auto/span 19}.panel-grid .g-col-lg-20{grid-column:auto/span 20}.panel-grid .g-col-lg-21{grid-column:auto/span 21}.panel-grid .g-col-lg-22{grid-column:auto/span 22}.panel-grid .g-col-lg-23{grid-column:auto/span 23}.panel-grid .g-col-lg-24{grid-column:auto/span 24}.panel-grid .g-start-lg-1{grid-column-start:1}.panel-grid .g-start-lg-2{grid-column-start:2}.panel-grid .g-start-lg-3{grid-column-start:3}.panel-grid .g-start-lg-4{grid-column-start:4}.panel-grid .g-start-lg-5{grid-column-start:5}.panel-grid .g-start-lg-6{grid-column-start:6}.panel-grid .g-start-lg-7{grid-column-start:7}.panel-grid .g-start-lg-8{grid-column-start:8}.panel-grid .g-start-lg-9{grid-column-start:9}.panel-grid .g-start-lg-10{grid-column-start:10}.panel-grid .g-start-lg-11{grid-column-start:11}.panel-grid .g-start-lg-12{grid-column-start:12}.panel-grid .g-start-lg-13{grid-column-start:13}.panel-grid .g-start-lg-14{grid-column-start:14}.panel-grid .g-start-lg-15{grid-column-start:15}.panel-grid .g-start-lg-16{grid-column-start:16}.panel-grid .g-start-lg-17{grid-column-start:17}.panel-grid .g-start-lg-18{grid-column-start:18}.panel-grid .g-start-lg-19{grid-column-start:19}.panel-grid .g-start-lg-20{grid-column-start:20}.panel-grid .g-start-lg-21{grid-column-start:21}.panel-grid .g-start-lg-22{grid-column-start:22}.panel-grid .g-start-lg-23{grid-column-start:23}}@media(min-width: 1200px){.panel-grid .g-col-xl-1{grid-column:auto/span 1}.panel-grid .g-col-xl-2{grid-column:auto/span 2}.panel-grid .g-col-xl-3{grid-column:auto/span 3}.panel-grid .g-col-xl-4{grid-column:auto/span 4}.panel-grid .g-col-xl-5{grid-column:auto/span 5}.panel-grid .g-col-xl-6{grid-column:auto/span 6}.panel-grid .g-col-xl-7{grid-column:auto/span 7}.panel-grid .g-col-xl-8{grid-column:auto/span 8}.panel-grid .g-col-xl-9{grid-column:auto/span 9}.panel-grid .g-col-xl-10{grid-column:auto/span 10}.panel-grid .g-col-xl-11{grid-column:auto/span 11}.panel-grid .g-col-xl-12{grid-column:auto/span 12}.panel-grid .g-col-xl-13{grid-column:auto/span 13}.panel-grid .g-col-xl-14{grid-column:auto/span 14}.panel-grid .g-col-xl-15{grid-column:auto/span 15}.panel-grid .g-col-xl-16{grid-column:auto/span 16}.panel-grid .g-col-xl-17{grid-column:auto/span 17}.panel-grid .g-col-xl-18{grid-column:auto/span 18}.panel-grid .g-col-xl-19{grid-column:auto/span 19}.panel-grid .g-col-xl-20{grid-column:auto/span 20}.panel-grid .g-col-xl-21{grid-column:auto/span 21}.panel-grid .g-col-xl-22{grid-column:auto/span 22}.panel-grid .g-col-xl-23{grid-column:auto/span 23}.panel-grid .g-col-xl-24{grid-column:auto/span 24}.panel-grid .g-start-xl-1{grid-column-start:1}.panel-grid .g-start-xl-2{grid-column-start:2}.panel-grid .g-start-xl-3{grid-column-start:3}.panel-grid .g-start-xl-4{grid-column-start:4}.panel-grid .g-start-xl-5{grid-column-start:5}.panel-grid .g-start-xl-6{grid-column-start:6}.panel-grid .g-start-xl-7{grid-column-start:7}.panel-grid .g-start-xl-8{grid-column-start:8}.panel-grid .g-start-xl-9{grid-column-start:9}.panel-grid .g-start-xl-10{grid-column-start:10}.panel-grid .g-start-xl-11{grid-column-start:11}.panel-grid .g-start-xl-12{grid-column-start:12}.panel-grid .g-start-xl-13{grid-column-start:13}.panel-grid .g-start-xl-14{grid-column-start:14}.panel-grid .g-start-xl-15{grid-column-start:15}.panel-grid .g-start-xl-16{grid-column-start:16}.panel-grid .g-start-xl-17{grid-column-start:17}.panel-grid .g-start-xl-18{grid-column-start:18}.panel-grid .g-start-xl-19{grid-column-start:19}.panel-grid .g-start-xl-20{grid-column-start:20}.panel-grid .g-start-xl-21{grid-column-start:21}.panel-grid .g-start-xl-22{grid-column-start:22}.panel-grid .g-start-xl-23{grid-column-start:23}}@media(min-width: 1400px){.panel-grid .g-col-xxl-1{grid-column:auto/span 1}.panel-grid .g-col-xxl-2{grid-column:auto/span 2}.panel-grid .g-col-xxl-3{grid-column:auto/span 3}.panel-grid .g-col-xxl-4{grid-column:auto/span 4}.panel-grid .g-col-xxl-5{grid-column:auto/span 5}.panel-grid .g-col-xxl-6{grid-column:auto/span 6}.panel-grid .g-col-xxl-7{grid-column:auto/span 7}.panel-grid .g-col-xxl-8{grid-column:auto/span 8}.panel-grid .g-col-xxl-9{grid-column:auto/span 9}.panel-grid .g-col-xxl-10{grid-column:auto/span 10}.panel-grid .g-col-xxl-11{grid-column:auto/span 11}.panel-grid .g-col-xxl-12{grid-column:auto/span 12}.panel-grid .g-col-xxl-13{grid-column:auto/span 13}.panel-grid .g-col-xxl-14{grid-column:auto/span 14}.panel-grid .g-col-xxl-15{grid-column:auto/span 15}.panel-grid .g-col-xxl-16{grid-column:auto/span 16}.panel-grid .g-col-xxl-17{grid-column:auto/span 17}.panel-grid .g-col-xxl-18{grid-column:auto/span 18}.panel-grid .g-col-xxl-19{grid-column:auto/span 19}.panel-grid .g-col-xxl-20{grid-column:auto/span 20}.panel-grid .g-col-xxl-21{grid-column:auto/span 21}.panel-grid .g-col-xxl-22{grid-column:auto/span 22}.panel-grid .g-col-xxl-23{grid-column:auto/span 23}.panel-grid .g-col-xxl-24{grid-column:auto/span 24}.panel-grid .g-start-xxl-1{grid-column-start:1}.panel-grid .g-start-xxl-2{grid-column-start:2}.panel-grid .g-start-xxl-3{grid-column-start:3}.panel-grid .g-start-xxl-4{grid-column-start:4}.panel-grid .g-start-xxl-5{grid-column-start:5}.panel-grid .g-start-xxl-6{grid-column-start:6}.panel-grid .g-start-xxl-7{grid-column-start:7}.panel-grid .g-start-xxl-8{grid-column-start:8}.panel-grid .g-start-xxl-9{grid-column-start:9}.panel-grid .g-start-xxl-10{grid-column-start:10}.panel-grid .g-start-xxl-11{grid-column-start:11}.panel-grid .g-start-xxl-12{grid-column-start:12}.panel-grid .g-start-xxl-13{grid-column-start:13}.panel-grid .g-start-xxl-14{grid-column-start:14}.panel-grid .g-start-xxl-15{grid-column-start:15}.panel-grid .g-start-xxl-16{grid-column-start:16}.panel-grid .g-start-xxl-17{grid-column-start:17}.panel-grid .g-start-xxl-18{grid-column-start:18}.panel-grid .g-start-xxl-19{grid-column-start:19}.panel-grid .g-start-xxl-20{grid-column-start:20}.panel-grid .g-start-xxl-21{grid-column-start:21}.panel-grid .g-start-xxl-22{grid-column-start:22}.panel-grid .g-start-xxl-23{grid-column-start:23}}main{margin-top:1em;margin-bottom:1em}h1,.h1,h2,.h2{color:inherit;margin-top:2rem;margin-bottom:1rem;font-weight:600}h1.title,.title.h1{margin-top:0}main.content>section:first-of-type>h2:first-child,main.content>section:first-of-type>.h2:first-child{margin-top:0}h2,.h2{border-bottom:1px solid #dee2e6;padding-bottom:.5rem}h3,.h3{font-weight:600}h3,.h3,h4,.h4{opacity:.9;margin-top:1.5rem}h5,.h5,h6,.h6{opacity:.9}.header-section-number{color:#6d7a86}.nav-link.active .header-section-number{color:inherit}mark,.mark{padding:0em}.panel-caption,.figure-caption,.subfigure-caption,.table-caption,figcaption,caption{font-size:.9rem;color:#6d7a86}.quarto-layout-cell[data-ref-parent] caption{color:#6d7a86}.column-margin figcaption,.margin-caption,div.aside,aside,.column-margin{color:#6d7a86;font-size:.825rem}.panel-caption.margin-caption{text-align:inherit}.column-margin.column-container p{margin-bottom:0}.column-margin.column-container>*:not(.collapse):first-child{padding-bottom:.5em;display:block}.column-margin.column-container>*:not(.collapse):not(:first-child){padding-top:.5em;padding-bottom:.5em;display:block}.column-margin.column-container>*.collapse:not(.show){display:none}@media(min-width: 768px){.column-margin.column-container .callout-margin-content:first-child{margin-top:4.5em}.column-margin.column-container .callout-margin-content-simple:first-child{margin-top:3.5em}}.margin-caption>*{padding-top:.5em;padding-bottom:.5em}@media(max-width: 767.98px){.quarto-layout-row{flex-direction:column}}.nav-tabs .nav-item{margin-top:1px;cursor:pointer}.tab-content{margin-top:0px;border-left:#dee2e6 1px solid;border-right:#dee2e6 1px solid;border-bottom:#dee2e6 1px solid;margin-left:0;padding:1em;margin-bottom:1em}@media(max-width: 767.98px){.layout-sidebar{margin-left:0;margin-right:0}}.panel-sidebar,.panel-sidebar .form-control,.panel-input,.panel-input .form-control,.selectize-dropdown{font-size:.9rem}.panel-sidebar .form-control,.panel-input .form-control{padding-top:.1rem}.tab-pane div.sourceCode{margin-top:0px}.tab-pane>p{padding-top:0}.tab-pane>p:nth-child(1){padding-top:0}.tab-pane>p:last-child{margin-bottom:0}.tab-pane>pre:last-child{margin-bottom:0}.tab-content>.tab-pane:not(.active){display:none !important}div.sourceCode{background-color:rgba(233,236,239,.65);border:1px solid rgba(233,236,239,.65);border-radius:.25rem}pre.sourceCode{background-color:rgba(0,0,0,0)}pre.sourceCode{border:none;font-size:.875em;overflow:visible !important;padding:.4em}.callout pre.sourceCode{padding-left:0}div.sourceCode{overflow-y:hidden}.callout div.sourceCode{margin-left:initial}.blockquote{font-size:inherit;padding-left:1rem;padding-right:1.5rem;color:#6d7a86}.blockquote h1:first-child,.blockquote .h1:first-child,.blockquote h2:first-child,.blockquote .h2:first-child,.blockquote h3:first-child,.blockquote .h3:first-child,.blockquote h4:first-child,.blockquote .h4:first-child,.blockquote h5:first-child,.blockquote .h5:first-child{margin-top:0}pre{background-color:initial;padding:initial;border:initial}p pre code:not(.sourceCode),li pre code:not(.sourceCode),pre code:not(.sourceCode){background-color:initial}p code:not(.sourceCode),li code:not(.sourceCode),td code:not(.sourceCode){background-color:#f8f9fa;padding:.2em}nav p code:not(.sourceCode),nav li code:not(.sourceCode),nav td code:not(.sourceCode){background-color:rgba(0,0,0,0);padding:0}td code:not(.sourceCode){white-space:pre-wrap}#quarto-embedded-source-code-modal>.modal-dialog{max-width:1000px;padding-left:1.75rem;padding-right:1.75rem}#quarto-embedded-source-code-modal>.modal-dialog>.modal-content>.modal-body{padding:0}#quarto-embedded-source-code-modal>.modal-dialog>.modal-content>.modal-body div.sourceCode{margin:0;padding:.2rem .2rem;border-radius:0px;border:none}#quarto-embedded-source-code-modal>.modal-dialog>.modal-content>.modal-header{padding:.7rem}.code-tools-button{font-size:1rem;padding:.15rem .15rem;margin-left:5px;color:#6c757d;background-color:rgba(0,0,0,0);transition:initial;cursor:pointer}.code-tools-button>.bi::before{display:inline-block;height:1rem;width:1rem;content:"";vertical-align:-0.125em;background-image:url('data:image/svg+xml,');background-repeat:no-repeat;background-size:1rem 1rem}.code-tools-button:hover>.bi::before{background-image:url('data:image/svg+xml,')}#quarto-embedded-source-code-modal .code-copy-button>.bi::before{background-image:url('data:image/svg+xml,')}#quarto-embedded-source-code-modal .code-copy-button-checked>.bi::before{background-image:url('data:image/svg+xml,')}.sidebar{will-change:top;transition:top 200ms linear;position:sticky;overflow-y:auto;padding-top:1.2em;max-height:100vh}.sidebar.toc-left,.sidebar.margin-sidebar{top:0px;padding-top:1em}.sidebar.quarto-banner-title-block-sidebar>*{padding-top:1.65em}figure .quarto-notebook-link{margin-top:.5em}.quarto-notebook-link{font-size:.75em;color:#6c757d;margin-bottom:1em;text-decoration:none;display:block}.quarto-notebook-link:hover{text-decoration:underline;color:#2761e3}.quarto-notebook-link::before{display:inline-block;height:.75rem;width:.75rem;margin-bottom:0em;margin-right:.25em;content:"";vertical-align:-0.125em;background-image:url('data:image/svg+xml,');background-repeat:no-repeat;background-size:.75rem .75rem}.toc-actions i.bi,.quarto-code-links i.bi,.quarto-other-links i.bi,.quarto-alternate-notebooks i.bi,.quarto-alternate-formats i.bi{margin-right:.4em;font-size:.8rem}.quarto-other-links-text-target .quarto-code-links i.bi,.quarto-other-links-text-target .quarto-other-links i.bi{margin-right:.2em}.quarto-other-formats-text-target .quarto-alternate-formats i.bi{margin-right:.1em}.toc-actions i.bi.empty,.quarto-code-links i.bi.empty,.quarto-other-links i.bi.empty,.quarto-alternate-notebooks i.bi.empty,.quarto-alternate-formats i.bi.empty{padding-left:1em}.quarto-notebook h2,.quarto-notebook .h2{border-bottom:none}.quarto-notebook .cell-container{display:flex}.quarto-notebook .cell-container .cell{flex-grow:4}.quarto-notebook .cell-container .cell-decorator{padding-top:1.5em;padding-right:1em;text-align:right}.quarto-notebook .cell-container.code-fold .cell-decorator{padding-top:3em}.quarto-notebook .cell-code code{white-space:pre-wrap}.quarto-notebook .cell .cell-output-stderr pre code,.quarto-notebook .cell .cell-output-stdout pre code{white-space:pre-wrap;overflow-wrap:anywhere}.toc-actions,.quarto-alternate-formats,.quarto-other-links,.quarto-code-links,.quarto-alternate-notebooks{padding-left:0em}.sidebar .toc-actions a,.sidebar .quarto-alternate-formats a,.sidebar .quarto-other-links a,.sidebar .quarto-code-links a,.sidebar .quarto-alternate-notebooks a,.sidebar nav[role=doc-toc] a{text-decoration:none}.sidebar .toc-actions a:hover,.sidebar .quarto-other-links a:hover,.sidebar .quarto-code-links a:hover,.sidebar .quarto-alternate-formats a:hover,.sidebar .quarto-alternate-notebooks a:hover{color:#2761e3}.sidebar .toc-actions h2,.sidebar .toc-actions .h2,.sidebar .quarto-code-links h2,.sidebar .quarto-code-links .h2,.sidebar .quarto-other-links h2,.sidebar .quarto-other-links .h2,.sidebar .quarto-alternate-notebooks h2,.sidebar .quarto-alternate-notebooks .h2,.sidebar .quarto-alternate-formats h2,.sidebar .quarto-alternate-formats .h2,.sidebar nav[role=doc-toc]>h2,.sidebar nav[role=doc-toc]>.h2{font-weight:500;margin-bottom:.2rem;margin-top:.3rem;font-family:inherit;border-bottom:0;padding-bottom:0;padding-top:0px}.sidebar .toc-actions>h2,.sidebar .toc-actions>.h2,.sidebar .quarto-code-links>h2,.sidebar .quarto-code-links>.h2,.sidebar .quarto-other-links>h2,.sidebar .quarto-other-links>.h2,.sidebar .quarto-alternate-notebooks>h2,.sidebar .quarto-alternate-notebooks>.h2,.sidebar .quarto-alternate-formats>h2,.sidebar .quarto-alternate-formats>.h2{font-size:.8rem}.sidebar nav[role=doc-toc]>h2,.sidebar nav[role=doc-toc]>.h2{font-size:.875rem}.sidebar nav[role=doc-toc]>ul a{border-left:1px solid #e9ecef;padding-left:.6rem}.sidebar .toc-actions h2>ul a,.sidebar .toc-actions .h2>ul a,.sidebar .quarto-code-links h2>ul a,.sidebar .quarto-code-links .h2>ul a,.sidebar .quarto-other-links h2>ul a,.sidebar .quarto-other-links .h2>ul a,.sidebar .quarto-alternate-notebooks h2>ul a,.sidebar .quarto-alternate-notebooks .h2>ul a,.sidebar .quarto-alternate-formats h2>ul a,.sidebar .quarto-alternate-formats .h2>ul a{border-left:none;padding-left:.6rem}.sidebar .toc-actions ul a:empty,.sidebar .quarto-code-links ul a:empty,.sidebar .quarto-other-links ul a:empty,.sidebar .quarto-alternate-notebooks ul a:empty,.sidebar .quarto-alternate-formats ul a:empty,.sidebar nav[role=doc-toc]>ul a:empty{display:none}.sidebar .toc-actions ul,.sidebar .quarto-code-links ul,.sidebar .quarto-other-links ul,.sidebar .quarto-alternate-notebooks ul,.sidebar .quarto-alternate-formats ul{padding-left:0;list-style:none}.sidebar nav[role=doc-toc] ul{list-style:none;padding-left:0;list-style:none}.sidebar nav[role=doc-toc]>ul{margin-left:.45em}.quarto-margin-sidebar nav[role=doc-toc]{padding-left:.5em}.sidebar .toc-actions>ul,.sidebar .quarto-code-links>ul,.sidebar .quarto-other-links>ul,.sidebar .quarto-alternate-notebooks>ul,.sidebar .quarto-alternate-formats>ul{font-size:.8rem}.sidebar nav[role=doc-toc]>ul{font-size:.875rem}.sidebar .toc-actions ul li a,.sidebar .quarto-code-links ul li a,.sidebar .quarto-other-links ul li a,.sidebar .quarto-alternate-notebooks ul li a,.sidebar .quarto-alternate-formats ul li a,.sidebar nav[role=doc-toc]>ul li a{line-height:1.1rem;padding-bottom:.2rem;padding-top:.2rem;color:inherit}.sidebar nav[role=doc-toc] ul>li>ul>li>a{padding-left:1.2em}.sidebar nav[role=doc-toc] ul>li>ul>li>ul>li>a{padding-left:2.4em}.sidebar nav[role=doc-toc] ul>li>ul>li>ul>li>ul>li>a{padding-left:3.6em}.sidebar nav[role=doc-toc] ul>li>ul>li>ul>li>ul>li>ul>li>a{padding-left:4.8em}.sidebar nav[role=doc-toc] ul>li>ul>li>ul>li>ul>li>ul>li>ul>li>a{padding-left:6em}.sidebar nav[role=doc-toc] ul>li>a.active,.sidebar nav[role=doc-toc] ul>li>ul>li>a.active{border-left:1px solid #2761e3;color:#2761e3 !important}.sidebar nav[role=doc-toc] ul>li>a:hover,.sidebar nav[role=doc-toc] ul>li>ul>li>a:hover{color:#2761e3 !important}kbd,.kbd{color:#343a40;background-color:#f8f9fa;border:1px solid;border-radius:5px;border-color:#dee2e6}.quarto-appendix-contents div.hanging-indent{margin-left:0em}.quarto-appendix-contents div.hanging-indent div.csl-entry{margin-left:1em;text-indent:-1em}.citation a,.footnote-ref{text-decoration:none}.footnotes ol{padding-left:1em}.tippy-content>*{margin-bottom:.7em}.tippy-content>*:last-child{margin-bottom:0}.callout{margin-top:1.25rem;margin-bottom:1.25rem;border-radius:.25rem;overflow-wrap:break-word}.callout .callout-title-container{overflow-wrap:anywhere}.callout.callout-style-simple{padding:.4em .7em;border-left:5px solid;border-right:1px solid #dee2e6;border-top:1px solid #dee2e6;border-bottom:1px solid #dee2e6}.callout.callout-style-default{border-left:5px solid;border-right:1px solid #dee2e6;border-top:1px solid #dee2e6;border-bottom:1px solid #dee2e6}.callout .callout-body-container{flex-grow:1}.callout.callout-style-simple .callout-body{font-size:.9rem;font-weight:400}.callout.callout-style-default .callout-body{font-size:.9rem;font-weight:400}.callout:not(.no-icon).callout-titled.callout-style-simple .callout-body{padding-left:1.6em}.callout.callout-titled>.callout-header{padding-top:.2em;margin-bottom:-0.2em}.callout.callout-style-simple>div.callout-header{border-bottom:none;font-size:.9rem;font-weight:600;opacity:75%}.callout.callout-style-default>div.callout-header{border-bottom:none;font-weight:600;opacity:85%;font-size:.9rem;padding-left:.5em;padding-right:.5em}.callout.callout-style-default .callout-body{padding-left:.5em;padding-right:.5em}.callout.callout-style-default .callout-body>:first-child{padding-top:.5rem;margin-top:0}.callout>div.callout-header[data-bs-toggle=collapse]{cursor:pointer}.callout.callout-style-default .callout-header[aria-expanded=false],.callout.callout-style-default .callout-header[aria-expanded=true]{padding-top:0px;margin-bottom:0px;align-items:center}.callout.callout-titled .callout-body>:last-child:not(.sourceCode),.callout.callout-titled .callout-body>div>:last-child:not(.sourceCode){padding-bottom:.5rem;margin-bottom:0}.callout:not(.callout-titled) .callout-body>:first-child,.callout:not(.callout-titled) .callout-body>div>:first-child{margin-top:.25rem}.callout:not(.callout-titled) .callout-body>:last-child,.callout:not(.callout-titled) .callout-body>div>:last-child{margin-bottom:.2rem}.callout.callout-style-simple .callout-icon::before,.callout.callout-style-simple .callout-toggle::before{height:1rem;width:1rem;display:inline-block;content:"";background-repeat:no-repeat;background-size:1rem 1rem}.callout.callout-style-default .callout-icon::before,.callout.callout-style-default .callout-toggle::before{height:.9rem;width:.9rem;display:inline-block;content:"";background-repeat:no-repeat;background-size:.9rem .9rem}.callout.callout-style-default .callout-toggle::before{margin-top:5px}.callout .callout-btn-toggle .callout-toggle::before{transition:transform .2s linear}.callout .callout-header[aria-expanded=false] .callout-toggle::before{transform:rotate(-90deg)}.callout .callout-header[aria-expanded=true] .callout-toggle::before{transform:none}.callout.callout-style-simple:not(.no-icon) div.callout-icon-container{padding-top:.2em;padding-right:.55em}.callout.callout-style-default:not(.no-icon) div.callout-icon-container{padding-top:.1em;padding-right:.35em}.callout.callout-style-default:not(.no-icon) div.callout-title-container{margin-top:-1px}.callout.callout-style-default.callout-caution:not(.no-icon) div.callout-icon-container{padding-top:.3em;padding-right:.35em}.callout>.callout-body>.callout-icon-container>.no-icon,.callout>.callout-header>.callout-icon-container>.no-icon{display:none}div.callout.callout{border-left-color:#6c757d}div.callout.callout-style-default>.callout-header{background-color:#6c757d}div.callout-note.callout{border-left-color:#2780e3}div.callout-note.callout-style-default>.callout-header{background-color:#e9f2fc}div.callout-note:not(.callout-titled) .callout-icon::before{background-image:url('data:image/svg+xml,');}div.callout-note.callout-titled .callout-icon::before{background-image:url('data:image/svg+xml,');}div.callout-note .callout-toggle::before{background-image:url('data:image/svg+xml,')}div.callout-tip.callout{border-left-color:#3fb618}div.callout-tip.callout-style-default>.callout-header{background-color:#ecf8e8}div.callout-tip:not(.callout-titled) .callout-icon::before{background-image:url('data:image/svg+xml,');}div.callout-tip.callout-titled .callout-icon::before{background-image:url('data:image/svg+xml,');}div.callout-tip .callout-toggle::before{background-image:url('data:image/svg+xml,')}div.callout-warning.callout{border-left-color:#ff7518}div.callout-warning.callout-style-default>.callout-header{background-color:#fff1e8}div.callout-warning:not(.callout-titled) .callout-icon::before{background-image:url('data:image/svg+xml,');}div.callout-warning.callout-titled .callout-icon::before{background-image:url('data:image/svg+xml,');}div.callout-warning .callout-toggle::before{background-image:url('data:image/svg+xml,')}div.callout-caution.callout{border-left-color:#f0ad4e}div.callout-caution.callout-style-default>.callout-header{background-color:#fef7ed}div.callout-caution:not(.callout-titled) .callout-icon::before{background-image:url('data:image/svg+xml,');}div.callout-caution.callout-titled .callout-icon::before{background-image:url('data:image/svg+xml,');}div.callout-caution .callout-toggle::before{background-image:url('data:image/svg+xml,')}div.callout-important.callout{border-left-color:#ff0039}div.callout-important.callout-style-default>.callout-header{background-color:#ffe6eb}div.callout-important:not(.callout-titled) .callout-icon::before{background-image:url('data:image/svg+xml,');}div.callout-important.callout-titled .callout-icon::before{background-image:url('data:image/svg+xml,');}div.callout-important .callout-toggle::before{background-image:url('data:image/svg+xml,')}.quarto-toggle-container{display:flex;align-items:center}.quarto-reader-toggle .bi::before,.quarto-color-scheme-toggle .bi::before{display:inline-block;height:1rem;width:1rem;content:"";background-repeat:no-repeat;background-size:1rem 1rem}.sidebar-navigation{padding-left:20px}.navbar{background-color:#2780e3;color:#fdfeff}.navbar .quarto-color-scheme-toggle:not(.alternate) .bi::before{background-image:url('data:image/svg+xml,')}.navbar .quarto-color-scheme-toggle.alternate .bi::before{background-image:url('data:image/svg+xml,')}.sidebar-navigation .quarto-color-scheme-toggle:not(.alternate) .bi::before{background-image:url('data:image/svg+xml,')}.sidebar-navigation .quarto-color-scheme-toggle.alternate .bi::before{background-image:url('data:image/svg+xml,')}.quarto-sidebar-toggle{border-color:#dee2e6;border-bottom-left-radius:.25rem;border-bottom-right-radius:.25rem;border-style:solid;border-width:1px;overflow:hidden;border-top-width:0px;padding-top:0px !important}.quarto-sidebar-toggle-title{cursor:pointer;padding-bottom:2px;margin-left:.25em;text-align:center;font-weight:400;font-size:.775em}#quarto-content .quarto-sidebar-toggle{background:#fafafa}#quarto-content .quarto-sidebar-toggle-title{color:#343a40}.quarto-sidebar-toggle-icon{color:#dee2e6;margin-right:.5em;float:right;transition:transform .2s ease}.quarto-sidebar-toggle-icon::before{padding-top:5px}.quarto-sidebar-toggle.expanded .quarto-sidebar-toggle-icon{transform:rotate(-180deg)}.quarto-sidebar-toggle.expanded .quarto-sidebar-toggle-title{border-bottom:solid #dee2e6 1px}.quarto-sidebar-toggle-contents{background-color:#fff;padding-right:10px;padding-left:10px;margin-top:0px !important;transition:max-height .5s ease}.quarto-sidebar-toggle.expanded .quarto-sidebar-toggle-contents{padding-top:1em;padding-bottom:10px}@media(max-width: 767.98px){.sidebar-menu-container{padding-bottom:5em}}.quarto-sidebar-toggle:not(.expanded) .quarto-sidebar-toggle-contents{padding-top:0px !important;padding-bottom:0px}nav[role=doc-toc]{z-index:1020}#quarto-sidebar>*,nav[role=doc-toc]>*{transition:opacity .1s ease,border .1s ease}#quarto-sidebar.slow>*,nav[role=doc-toc].slow>*{transition:opacity .4s ease,border .4s ease}.quarto-color-scheme-toggle:not(.alternate).top-right .bi::before{background-image:url('data:image/svg+xml,')}.quarto-color-scheme-toggle.alternate.top-right .bi::before{background-image:url('data:image/svg+xml,')}#quarto-appendix.default{border-top:1px solid #dee2e6}#quarto-appendix.default{background-color:#fff;padding-top:1.5em;margin-top:2em;z-index:998}#quarto-appendix.default .quarto-appendix-heading{margin-top:0;line-height:1.4em;font-weight:600;opacity:.9;border-bottom:none;margin-bottom:0}#quarto-appendix.default .footnotes ol,#quarto-appendix.default .footnotes ol li>p:last-of-type,#quarto-appendix.default .quarto-appendix-contents>p:last-of-type{margin-bottom:0}#quarto-appendix.default .footnotes ol{margin-left:.5em}#quarto-appendix.default .quarto-appendix-secondary-label{margin-bottom:.4em}#quarto-appendix.default .quarto-appendix-bibtex{font-size:.7em;padding:1em;border:solid 1px #dee2e6;margin-bottom:1em}#quarto-appendix.default .quarto-appendix-bibtex code.sourceCode{white-space:pre-wrap}#quarto-appendix.default .quarto-appendix-citeas{font-size:.9em;padding:1em;border:solid 1px #dee2e6;margin-bottom:1em}#quarto-appendix.default .quarto-appendix-heading{font-size:1em !important}#quarto-appendix.default *[role=doc-endnotes]>ol,#quarto-appendix.default .quarto-appendix-contents>*:not(h2):not(.h2){font-size:.9em}#quarto-appendix.default section{padding-bottom:1.5em}#quarto-appendix.default section *[role=doc-endnotes],#quarto-appendix.default section>*:not(a){opacity:.9;word-wrap:break-word}.btn.btn-quarto,div.cell-output-display .btn-quarto{--bs-btn-color: #cacccd;--bs-btn-bg: #343a40;--bs-btn-border-color: #343a40;--bs-btn-hover-color: #cacccd;--bs-btn-hover-bg: #52585d;--bs-btn-hover-border-color: #484e53;--bs-btn-focus-shadow-rgb: 75, 80, 85;--bs-btn-active-color: #fff;--bs-btn-active-bg: #5d6166;--bs-btn-active-border-color: #484e53;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #fff;--bs-btn-disabled-bg: #343a40;--bs-btn-disabled-border-color: #343a40}nav.quarto-secondary-nav.color-navbar{background-color:#2780e3;color:#fdfeff}nav.quarto-secondary-nav.color-navbar h1,nav.quarto-secondary-nav.color-navbar .h1,nav.quarto-secondary-nav.color-navbar .quarto-btn-toggle{color:#fdfeff}@media(max-width: 991.98px){body.nav-sidebar .quarto-title-banner{margin-bottom:0;padding-bottom:1em}body.nav-sidebar #title-block-header{margin-block-end:0}}p.subtitle{margin-top:.25em;margin-bottom:.5em}code a:any-link{color:inherit;text-decoration-color:#6c757d}/*! light */div.observablehq table thead tr th{background-color:var(--bs-body-bg)}input,button,select,optgroup,textarea{background-color:var(--bs-body-bg)}.code-annotated .code-copy-button{margin-right:1.25em;margin-top:0;padding-bottom:0;padding-top:3px}.code-annotation-gutter-bg{background-color:#fff}.code-annotation-gutter{background-color:rgba(233,236,239,.65)}.code-annotation-gutter,.code-annotation-gutter-bg{height:100%;width:calc(20px + .5em);position:absolute;top:0;right:0}dl.code-annotation-container-grid dt{margin-right:1em;margin-top:.25rem}dl.code-annotation-container-grid dt{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;color:#4b545c;border:solid #4b545c 1px;border-radius:50%;height:22px;width:22px;line-height:22px;font-size:11px;text-align:center;vertical-align:middle;text-decoration:none}dl.code-annotation-container-grid dt[data-target-cell]{cursor:pointer}dl.code-annotation-container-grid dt[data-target-cell].code-annotation-active{color:#fff;border:solid #aaa 1px;background-color:#aaa}pre.code-annotation-code{padding-top:0;padding-bottom:0}pre.code-annotation-code code{z-index:3}#code-annotation-line-highlight-gutter{width:100%;border-top:solid rgba(170,170,170,.2666666667) 1px;border-bottom:solid rgba(170,170,170,.2666666667) 1px;z-index:2;background-color:rgba(170,170,170,.1333333333)}#code-annotation-line-highlight{margin-left:-4em;width:calc(100% + 4em);border-top:solid rgba(170,170,170,.2666666667) 1px;border-bottom:solid rgba(170,170,170,.2666666667) 1px;z-index:2;background-color:rgba(170,170,170,.1333333333)}code.sourceCode .code-annotation-anchor.code-annotation-active{background-color:var(--quarto-hl-normal-color, #aaaaaa);border:solid var(--quarto-hl-normal-color, #aaaaaa) 1px;color:#e9ecef;font-weight:bolder}code.sourceCode .code-annotation-anchor{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;color:var(--quarto-hl-co-color);border:solid var(--quarto-hl-co-color) 1px;border-radius:50%;height:18px;width:18px;font-size:9px;margin-top:2px}code.sourceCode button.code-annotation-anchor{padding:2px;user-select:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;-o-user-select:none}code.sourceCode a.code-annotation-anchor{line-height:18px;text-align:center;vertical-align:middle;cursor:default;text-decoration:none}@media print{.page-columns .column-screen-inset{grid-column:page-start-inset/page-end-inset;z-index:998;opacity:.999}.page-columns .column-screen-inset table{background:#fff}.page-columns .column-screen-inset-left{grid-column:page-start-inset/body-content-end;z-index:998;opacity:.999}.page-columns .column-screen-inset-left table{background:#fff}.page-columns .column-screen-inset-right{grid-column:body-content-start/page-end-inset;z-index:998;opacity:.999}.page-columns .column-screen-inset-right table{background:#fff}.page-columns .column-screen{grid-column:page-start/page-end;z-index:998;opacity:.999}.page-columns .column-screen table{background:#fff}.page-columns .column-screen-left{grid-column:page-start/body-content-end;z-index:998;opacity:.999}.page-columns .column-screen-left table{background:#fff}.page-columns .column-screen-right{grid-column:body-content-start/page-end;z-index:998;opacity:.999}.page-columns .column-screen-right table{background:#fff}.page-columns .column-screen-inset-shaded{grid-column:page-start-inset/page-end-inset;padding:1em;background:#f8f9fa;z-index:998;opacity:.999;margin-bottom:1em}}.quarto-video{margin-bottom:1em}.table{border-top:1px solid #ebedee;border-bottom:1px solid #ebedee}.table>thead{border-top-width:0;border-bottom:1px solid #b2bac1}.table a{word-break:break-word}.table>:not(caption)>*>*{background-color:unset;color:unset}#quarto-document-content .crosstalk-input .checkbox input[type=checkbox],#quarto-document-content .crosstalk-input .checkbox-inline input[type=checkbox]{position:unset;margin-top:unset;margin-left:unset}#quarto-document-content .row{margin-left:unset;margin-right:unset}.quarto-xref{white-space:nowrap}#quarto-draft-alert{margin-top:0px;margin-bottom:0px;padding:.3em;text-align:center;font-size:.9em}#quarto-draft-alert i{margin-right:.3em}a.external:after{content:"";background-image:url('data:image/svg+xml,');background-size:contain;background-repeat:no-repeat;background-position:center center;margin-left:.2em;padding-right:.75em}div.sourceCode code a.external:after{content:none}a.external:after:hover{cursor:pointer}.quarto-ext-icon{display:inline-block;font-size:.75em;padding-left:.3em}.code-with-filename .code-with-filename-file{margin-bottom:0;padding-bottom:2px;padding-top:2px;padding-left:.7em;border:var(--quarto-border-width) solid var(--quarto-border-color);border-radius:var(--quarto-border-radius);border-bottom:0;border-bottom-left-radius:0%;border-bottom-right-radius:0%}.code-with-filename div.sourceCode,.reveal .code-with-filename div.sourceCode{margin-top:0;border-top-left-radius:0%;border-top-right-radius:0%}.code-with-filename .code-with-filename-file pre{margin-bottom:0}.code-with-filename .code-with-filename-file{background-color:rgba(219,219,219,.8)}.quarto-dark .code-with-filename .code-with-filename-file{background-color:#555}.code-with-filename .code-with-filename-file strong{font-weight:400}.quarto-title-banner{margin-bottom:1em;color:#fdfeff;background:#2780e3}.quarto-title-banner a{color:#fdfeff}.quarto-title-banner h1,.quarto-title-banner .h1,.quarto-title-banner h2,.quarto-title-banner .h2{color:#fdfeff}.quarto-title-banner .code-tools-button{color:#97cbff}.quarto-title-banner .code-tools-button:hover{color:#fdfeff}.quarto-title-banner .code-tools-button>.bi::before{background-image:url('data:image/svg+xml,')}.quarto-title-banner .code-tools-button:hover>.bi::before{background-image:url('data:image/svg+xml,')}.quarto-title-banner .quarto-title .title{font-weight:600}.quarto-title-banner .quarto-categories{margin-top:.75em}@media(min-width: 992px){.quarto-title-banner{padding-top:2.5em;padding-bottom:2.5em}}@media(max-width: 991.98px){.quarto-title-banner{padding-top:1em;padding-bottom:1em}}@media(max-width: 767.98px){body.hypothesis-enabled #title-block-header>*{padding-right:20px}}main.quarto-banner-title-block>section:first-child>h2,main.quarto-banner-title-block>section:first-child>.h2,main.quarto-banner-title-block>section:first-child>h3,main.quarto-banner-title-block>section:first-child>.h3,main.quarto-banner-title-block>section:first-child>h4,main.quarto-banner-title-block>section:first-child>.h4{margin-top:0}.quarto-title .quarto-categories{display:flex;flex-wrap:wrap;row-gap:.5em;column-gap:.4em;padding-bottom:.5em;margin-top:.75em}.quarto-title .quarto-categories .quarto-category{padding:.25em .75em;font-size:.65em;text-transform:uppercase;border:solid 1px;border-radius:.25rem;opacity:.6}.quarto-title .quarto-categories .quarto-category a{color:inherit}.quarto-title-meta-container{display:grid;grid-template-columns:1fr auto}.quarto-title-meta-column-end{display:flex;flex-direction:column;padding-left:1em}.quarto-title-meta-column-end a .bi{margin-right:.3em}#title-block-header.quarto-title-block.default .quarto-title-meta{display:grid;grid-template-columns:repeat(2, 1fr);grid-column-gap:1em}#title-block-header.quarto-title-block.default .quarto-title .title{margin-bottom:0}#title-block-header.quarto-title-block.default .quarto-title-author-orcid img{margin-top:-0.2em;height:.8em;width:.8em}#title-block-header.quarto-title-block.default .quarto-title-author-email{opacity:.7}#title-block-header.quarto-title-block.default .quarto-description p:last-of-type{margin-bottom:0}#title-block-header.quarto-title-block.default .quarto-title-meta-contents p,#title-block-header.quarto-title-block.default .quarto-title-authors p,#title-block-header.quarto-title-block.default .quarto-title-affiliations p{margin-bottom:.1em}#title-block-header.quarto-title-block.default .quarto-title-meta-heading{text-transform:uppercase;margin-top:1em;font-size:.8em;opacity:.8;font-weight:400}#title-block-header.quarto-title-block.default .quarto-title-meta-contents{font-size:.9em}#title-block-header.quarto-title-block.default .quarto-title-meta-contents p.affiliation:last-of-type{margin-bottom:.1em}#title-block-header.quarto-title-block.default p.affiliation{margin-bottom:.1em}#title-block-header.quarto-title-block.default .keywords,#title-block-header.quarto-title-block.default .description,#title-block-header.quarto-title-block.default .abstract{margin-top:0}#title-block-header.quarto-title-block.default .keywords>p,#title-block-header.quarto-title-block.default .description>p,#title-block-header.quarto-title-block.default .abstract>p{font-size:.9em}#title-block-header.quarto-title-block.default .keywords>p:last-of-type,#title-block-header.quarto-title-block.default .description>p:last-of-type,#title-block-header.quarto-title-block.default .abstract>p:last-of-type{margin-bottom:0}#title-block-header.quarto-title-block.default .keywords .block-title,#title-block-header.quarto-title-block.default .description .block-title,#title-block-header.quarto-title-block.default .abstract .block-title{margin-top:1em;text-transform:uppercase;font-size:.8em;opacity:.8;font-weight:400}#title-block-header.quarto-title-block.default .quarto-title-meta-author{display:grid;grid-template-columns:minmax(max-content, 1fr) 1fr;grid-column-gap:1em}.quarto-title-tools-only{display:flex;justify-content:right}body{-webkit-font-smoothing:antialiased}.badge.bg-light{color:#343a40}.progress .progress-bar{font-size:8px;line-height:8px} diff --git a/site_libs/bootstrap/bootstrap.min.js b/site_libs/bootstrap/bootstrap.min.js new file mode 100644 index 00000000..e8f21f70 --- /dev/null +++ b/site_libs/bootstrap/bootstrap.min.js @@ -0,0 +1,7 @@ +/*! + * Bootstrap v5.3.1 (https://getbootstrap.com/) + * Copyright 2011-2023 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).bootstrap=e()}(this,(function(){"use strict";const t=new Map,e={set(e,i,n){t.has(e)||t.set(e,new Map);const s=t.get(e);s.has(i)||0===s.size?s.set(i,n):console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(s.keys())[0]}.`)},get:(e,i)=>t.has(e)&&t.get(e).get(i)||null,remove(e,i){if(!t.has(e))return;const n=t.get(e);n.delete(i),0===n.size&&t.delete(e)}},i="transitionend",n=t=>(t&&window.CSS&&window.CSS.escape&&(t=t.replace(/#([^\s"#']+)/g,((t,e)=>`#${CSS.escape(e)}`))),t),s=t=>{t.dispatchEvent(new Event(i))},o=t=>!(!t||"object"!=typeof t)&&(void 0!==t.jquery&&(t=t[0]),void 0!==t.nodeType),r=t=>o(t)?t.jquery?t[0]:t:"string"==typeof t&&t.length>0?document.querySelector(n(t)):null,a=t=>{if(!o(t)||0===t.getClientRects().length)return!1;const e="visible"===getComputedStyle(t).getPropertyValue("visibility"),i=t.closest("details:not([open])");if(!i)return e;if(i!==t){const e=t.closest("summary");if(e&&e.parentNode!==i)return!1;if(null===e)return!1}return e},l=t=>!t||t.nodeType!==Node.ELEMENT_NODE||!!t.classList.contains("disabled")||(void 0!==t.disabled?t.disabled:t.hasAttribute("disabled")&&"false"!==t.getAttribute("disabled")),c=t=>{if(!document.documentElement.attachShadow)return null;if("function"==typeof t.getRootNode){const e=t.getRootNode();return e instanceof ShadowRoot?e:null}return t instanceof ShadowRoot?t:t.parentNode?c(t.parentNode):null},h=()=>{},d=t=>{t.offsetHeight},u=()=>window.jQuery&&!document.body.hasAttribute("data-bs-no-jquery")?window.jQuery:null,f=[],p=()=>"rtl"===document.documentElement.dir,m=t=>{var e;e=()=>{const e=u();if(e){const i=t.NAME,n=e.fn[i];e.fn[i]=t.jQueryInterface,e.fn[i].Constructor=t,e.fn[i].noConflict=()=>(e.fn[i]=n,t.jQueryInterface)}},"loading"===document.readyState?(f.length||document.addEventListener("DOMContentLoaded",(()=>{for(const t of f)t()})),f.push(e)):e()},g=(t,e=[],i=t)=>"function"==typeof t?t(...e):i,_=(t,e,n=!0)=>{if(!n)return void g(t);const o=(t=>{if(!t)return 0;let{transitionDuration:e,transitionDelay:i}=window.getComputedStyle(t);const n=Number.parseFloat(e),s=Number.parseFloat(i);return n||s?(e=e.split(",")[0],i=i.split(",")[0],1e3*(Number.parseFloat(e)+Number.parseFloat(i))):0})(e)+5;let r=!1;const a=({target:n})=>{n===e&&(r=!0,e.removeEventListener(i,a),g(t))};e.addEventListener(i,a),setTimeout((()=>{r||s(e)}),o)},b=(t,e,i,n)=>{const s=t.length;let o=t.indexOf(e);return-1===o?!i&&n?t[s-1]:t[0]:(o+=i?1:-1,n&&(o=(o+s)%s),t[Math.max(0,Math.min(o,s-1))])},v=/[^.]*(?=\..*)\.|.*/,y=/\..*/,w=/::\d+$/,A={};let E=1;const T={mouseenter:"mouseover",mouseleave:"mouseout"},C=new Set(["click","dblclick","mouseup","mousedown","contextmenu","mousewheel","DOMMouseScroll","mouseover","mouseout","mousemove","selectstart","selectend","keydown","keypress","keyup","orientationchange","touchstart","touchmove","touchend","touchcancel","pointerdown","pointermove","pointerup","pointerleave","pointercancel","gesturestart","gesturechange","gestureend","focus","blur","change","reset","select","submit","focusin","focusout","load","unload","beforeunload","resize","move","DOMContentLoaded","readystatechange","error","abort","scroll"]);function O(t,e){return e&&`${e}::${E++}`||t.uidEvent||E++}function x(t){const e=O(t);return t.uidEvent=e,A[e]=A[e]||{},A[e]}function k(t,e,i=null){return Object.values(t).find((t=>t.callable===e&&t.delegationSelector===i))}function L(t,e,i){const n="string"==typeof e,s=n?i:e||i;let o=I(t);return C.has(o)||(o=t),[n,s,o]}function S(t,e,i,n,s){if("string"!=typeof e||!t)return;let[o,r,a]=L(e,i,n);if(e in T){const t=t=>function(e){if(!e.relatedTarget||e.relatedTarget!==e.delegateTarget&&!e.delegateTarget.contains(e.relatedTarget))return t.call(this,e)};r=t(r)}const l=x(t),c=l[a]||(l[a]={}),h=k(c,r,o?i:null);if(h)return void(h.oneOff=h.oneOff&&s);const d=O(r,e.replace(v,"")),u=o?function(t,e,i){return function n(s){const o=t.querySelectorAll(e);for(let{target:r}=s;r&&r!==this;r=r.parentNode)for(const a of o)if(a===r)return P(s,{delegateTarget:r}),n.oneOff&&N.off(t,s.type,e,i),i.apply(r,[s])}}(t,i,r):function(t,e){return function i(n){return P(n,{delegateTarget:t}),i.oneOff&&N.off(t,n.type,e),e.apply(t,[n])}}(t,r);u.delegationSelector=o?i:null,u.callable=r,u.oneOff=s,u.uidEvent=d,c[d]=u,t.addEventListener(a,u,o)}function D(t,e,i,n,s){const o=k(e[i],n,s);o&&(t.removeEventListener(i,o,Boolean(s)),delete e[i][o.uidEvent])}function $(t,e,i,n){const s=e[i]||{};for(const[o,r]of Object.entries(s))o.includes(n)&&D(t,e,i,r.callable,r.delegationSelector)}function I(t){return t=t.replace(y,""),T[t]||t}const N={on(t,e,i,n){S(t,e,i,n,!1)},one(t,e,i,n){S(t,e,i,n,!0)},off(t,e,i,n){if("string"!=typeof e||!t)return;const[s,o,r]=L(e,i,n),a=r!==e,l=x(t),c=l[r]||{},h=e.startsWith(".");if(void 0===o){if(h)for(const i of Object.keys(l))$(t,l,i,e.slice(1));for(const[i,n]of Object.entries(c)){const s=i.replace(w,"");a&&!e.includes(s)||D(t,l,r,n.callable,n.delegationSelector)}}else{if(!Object.keys(c).length)return;D(t,l,r,o,s?i:null)}},trigger(t,e,i){if("string"!=typeof e||!t)return null;const n=u();let s=null,o=!0,r=!0,a=!1;e!==I(e)&&n&&(s=n.Event(e,i),n(t).trigger(s),o=!s.isPropagationStopped(),r=!s.isImmediatePropagationStopped(),a=s.isDefaultPrevented());const l=P(new Event(e,{bubbles:o,cancelable:!0}),i);return a&&l.preventDefault(),r&&t.dispatchEvent(l),l.defaultPrevented&&s&&s.preventDefault(),l}};function P(t,e={}){for(const[i,n]of Object.entries(e))try{t[i]=n}catch(e){Object.defineProperty(t,i,{configurable:!0,get:()=>n})}return t}function M(t){if("true"===t)return!0;if("false"===t)return!1;if(t===Number(t).toString())return Number(t);if(""===t||"null"===t)return null;if("string"!=typeof t)return t;try{return JSON.parse(decodeURIComponent(t))}catch(e){return t}}function j(t){return t.replace(/[A-Z]/g,(t=>`-${t.toLowerCase()}`))}const F={setDataAttribute(t,e,i){t.setAttribute(`data-bs-${j(e)}`,i)},removeDataAttribute(t,e){t.removeAttribute(`data-bs-${j(e)}`)},getDataAttributes(t){if(!t)return{};const e={},i=Object.keys(t.dataset).filter((t=>t.startsWith("bs")&&!t.startsWith("bsConfig")));for(const n of i){let i=n.replace(/^bs/,"");i=i.charAt(0).toLowerCase()+i.slice(1,i.length),e[i]=M(t.dataset[n])}return e},getDataAttribute:(t,e)=>M(t.getAttribute(`data-bs-${j(e)}`))};class H{static get Default(){return{}}static get DefaultType(){return{}}static get NAME(){throw new Error('You have to implement the static method "NAME", for each component!')}_getConfig(t){return t=this._mergeConfigObj(t),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}_configAfterMerge(t){return t}_mergeConfigObj(t,e){const i=o(e)?F.getDataAttribute(e,"config"):{};return{...this.constructor.Default,..."object"==typeof i?i:{},...o(e)?F.getDataAttributes(e):{},..."object"==typeof t?t:{}}}_typeCheckConfig(t,e=this.constructor.DefaultType){for(const[n,s]of Object.entries(e)){const e=t[n],r=o(e)?"element":null==(i=e)?`${i}`:Object.prototype.toString.call(i).match(/\s([a-z]+)/i)[1].toLowerCase();if(!new RegExp(s).test(r))throw new TypeError(`${this.constructor.NAME.toUpperCase()}: Option "${n}" provided type "${r}" but expected type "${s}".`)}var i}}class W extends H{constructor(t,i){super(),(t=r(t))&&(this._element=t,this._config=this._getConfig(i),e.set(this._element,this.constructor.DATA_KEY,this))}dispose(){e.remove(this._element,this.constructor.DATA_KEY),N.off(this._element,this.constructor.EVENT_KEY);for(const t of Object.getOwnPropertyNames(this))this[t]=null}_queueCallback(t,e,i=!0){_(t,e,i)}_getConfig(t){return t=this._mergeConfigObj(t,this._element),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}static getInstance(t){return e.get(r(t),this.DATA_KEY)}static getOrCreateInstance(t,e={}){return this.getInstance(t)||new this(t,"object"==typeof e?e:null)}static get VERSION(){return"5.3.1"}static get DATA_KEY(){return`bs.${this.NAME}`}static get EVENT_KEY(){return`.${this.DATA_KEY}`}static eventName(t){return`${t}${this.EVENT_KEY}`}}const B=t=>{let e=t.getAttribute("data-bs-target");if(!e||"#"===e){let i=t.getAttribute("href");if(!i||!i.includes("#")&&!i.startsWith("."))return null;i.includes("#")&&!i.startsWith("#")&&(i=`#${i.split("#")[1]}`),e=i&&"#"!==i?i.trim():null}return n(e)},z={find:(t,e=document.documentElement)=>[].concat(...Element.prototype.querySelectorAll.call(e,t)),findOne:(t,e=document.documentElement)=>Element.prototype.querySelector.call(e,t),children:(t,e)=>[].concat(...t.children).filter((t=>t.matches(e))),parents(t,e){const i=[];let n=t.parentNode.closest(e);for(;n;)i.push(n),n=n.parentNode.closest(e);return i},prev(t,e){let i=t.previousElementSibling;for(;i;){if(i.matches(e))return[i];i=i.previousElementSibling}return[]},next(t,e){let i=t.nextElementSibling;for(;i;){if(i.matches(e))return[i];i=i.nextElementSibling}return[]},focusableChildren(t){const e=["a","button","input","textarea","select","details","[tabindex]",'[contenteditable="true"]'].map((t=>`${t}:not([tabindex^="-"])`)).join(",");return this.find(e,t).filter((t=>!l(t)&&a(t)))},getSelectorFromElement(t){const e=B(t);return e&&z.findOne(e)?e:null},getElementFromSelector(t){const e=B(t);return e?z.findOne(e):null},getMultipleElementsFromSelector(t){const e=B(t);return e?z.find(e):[]}},R=(t,e="hide")=>{const i=`click.dismiss${t.EVENT_KEY}`,n=t.NAME;N.on(document,i,`[data-bs-dismiss="${n}"]`,(function(i){if(["A","AREA"].includes(this.tagName)&&i.preventDefault(),l(this))return;const s=z.getElementFromSelector(this)||this.closest(`.${n}`);t.getOrCreateInstance(s)[e]()}))},q=".bs.alert",V=`close${q}`,K=`closed${q}`;class Q extends W{static get NAME(){return"alert"}close(){if(N.trigger(this._element,V).defaultPrevented)return;this._element.classList.remove("show");const t=this._element.classList.contains("fade");this._queueCallback((()=>this._destroyElement()),this._element,t)}_destroyElement(){this._element.remove(),N.trigger(this._element,K),this.dispose()}static jQueryInterface(t){return this.each((function(){const e=Q.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}R(Q,"close"),m(Q);const X='[data-bs-toggle="button"]';class Y extends W{static get NAME(){return"button"}toggle(){this._element.setAttribute("aria-pressed",this._element.classList.toggle("active"))}static jQueryInterface(t){return this.each((function(){const e=Y.getOrCreateInstance(this);"toggle"===t&&e[t]()}))}}N.on(document,"click.bs.button.data-api",X,(t=>{t.preventDefault();const e=t.target.closest(X);Y.getOrCreateInstance(e).toggle()})),m(Y);const U=".bs.swipe",G=`touchstart${U}`,J=`touchmove${U}`,Z=`touchend${U}`,tt=`pointerdown${U}`,et=`pointerup${U}`,it={endCallback:null,leftCallback:null,rightCallback:null},nt={endCallback:"(function|null)",leftCallback:"(function|null)",rightCallback:"(function|null)"};class st extends H{constructor(t,e){super(),this._element=t,t&&st.isSupported()&&(this._config=this._getConfig(e),this._deltaX=0,this._supportPointerEvents=Boolean(window.PointerEvent),this._initEvents())}static get Default(){return it}static get DefaultType(){return nt}static get NAME(){return"swipe"}dispose(){N.off(this._element,U)}_start(t){this._supportPointerEvents?this._eventIsPointerPenTouch(t)&&(this._deltaX=t.clientX):this._deltaX=t.touches[0].clientX}_end(t){this._eventIsPointerPenTouch(t)&&(this._deltaX=t.clientX-this._deltaX),this._handleSwipe(),g(this._config.endCallback)}_move(t){this._deltaX=t.touches&&t.touches.length>1?0:t.touches[0].clientX-this._deltaX}_handleSwipe(){const t=Math.abs(this._deltaX);if(t<=40)return;const e=t/this._deltaX;this._deltaX=0,e&&g(e>0?this._config.rightCallback:this._config.leftCallback)}_initEvents(){this._supportPointerEvents?(N.on(this._element,tt,(t=>this._start(t))),N.on(this._element,et,(t=>this._end(t))),this._element.classList.add("pointer-event")):(N.on(this._element,G,(t=>this._start(t))),N.on(this._element,J,(t=>this._move(t))),N.on(this._element,Z,(t=>this._end(t))))}_eventIsPointerPenTouch(t){return this._supportPointerEvents&&("pen"===t.pointerType||"touch"===t.pointerType)}static isSupported(){return"ontouchstart"in document.documentElement||navigator.maxTouchPoints>0}}const ot=".bs.carousel",rt=".data-api",at="next",lt="prev",ct="left",ht="right",dt=`slide${ot}`,ut=`slid${ot}`,ft=`keydown${ot}`,pt=`mouseenter${ot}`,mt=`mouseleave${ot}`,gt=`dragstart${ot}`,_t=`load${ot}${rt}`,bt=`click${ot}${rt}`,vt="carousel",yt="active",wt=".active",At=".carousel-item",Et=wt+At,Tt={ArrowLeft:ht,ArrowRight:ct},Ct={interval:5e3,keyboard:!0,pause:"hover",ride:!1,touch:!0,wrap:!0},Ot={interval:"(number|boolean)",keyboard:"boolean",pause:"(string|boolean)",ride:"(boolean|string)",touch:"boolean",wrap:"boolean"};class xt extends W{constructor(t,e){super(t,e),this._interval=null,this._activeElement=null,this._isSliding=!1,this.touchTimeout=null,this._swipeHelper=null,this._indicatorsElement=z.findOne(".carousel-indicators",this._element),this._addEventListeners(),this._config.ride===vt&&this.cycle()}static get Default(){return Ct}static get DefaultType(){return Ot}static get NAME(){return"carousel"}next(){this._slide(at)}nextWhenVisible(){!document.hidden&&a(this._element)&&this.next()}prev(){this._slide(lt)}pause(){this._isSliding&&s(this._element),this._clearInterval()}cycle(){this._clearInterval(),this._updateInterval(),this._interval=setInterval((()=>this.nextWhenVisible()),this._config.interval)}_maybeEnableCycle(){this._config.ride&&(this._isSliding?N.one(this._element,ut,(()=>this.cycle())):this.cycle())}to(t){const e=this._getItems();if(t>e.length-1||t<0)return;if(this._isSliding)return void N.one(this._element,ut,(()=>this.to(t)));const i=this._getItemIndex(this._getActive());if(i===t)return;const n=t>i?at:lt;this._slide(n,e[t])}dispose(){this._swipeHelper&&this._swipeHelper.dispose(),super.dispose()}_configAfterMerge(t){return t.defaultInterval=t.interval,t}_addEventListeners(){this._config.keyboard&&N.on(this._element,ft,(t=>this._keydown(t))),"hover"===this._config.pause&&(N.on(this._element,pt,(()=>this.pause())),N.on(this._element,mt,(()=>this._maybeEnableCycle()))),this._config.touch&&st.isSupported()&&this._addTouchEventListeners()}_addTouchEventListeners(){for(const t of z.find(".carousel-item img",this._element))N.on(t,gt,(t=>t.preventDefault()));const t={leftCallback:()=>this._slide(this._directionToOrder(ct)),rightCallback:()=>this._slide(this._directionToOrder(ht)),endCallback:()=>{"hover"===this._config.pause&&(this.pause(),this.touchTimeout&&clearTimeout(this.touchTimeout),this.touchTimeout=setTimeout((()=>this._maybeEnableCycle()),500+this._config.interval))}};this._swipeHelper=new st(this._element,t)}_keydown(t){if(/input|textarea/i.test(t.target.tagName))return;const e=Tt[t.key];e&&(t.preventDefault(),this._slide(this._directionToOrder(e)))}_getItemIndex(t){return this._getItems().indexOf(t)}_setActiveIndicatorElement(t){if(!this._indicatorsElement)return;const e=z.findOne(wt,this._indicatorsElement);e.classList.remove(yt),e.removeAttribute("aria-current");const i=z.findOne(`[data-bs-slide-to="${t}"]`,this._indicatorsElement);i&&(i.classList.add(yt),i.setAttribute("aria-current","true"))}_updateInterval(){const t=this._activeElement||this._getActive();if(!t)return;const e=Number.parseInt(t.getAttribute("data-bs-interval"),10);this._config.interval=e||this._config.defaultInterval}_slide(t,e=null){if(this._isSliding)return;const i=this._getActive(),n=t===at,s=e||b(this._getItems(),i,n,this._config.wrap);if(s===i)return;const o=this._getItemIndex(s),r=e=>N.trigger(this._element,e,{relatedTarget:s,direction:this._orderToDirection(t),from:this._getItemIndex(i),to:o});if(r(dt).defaultPrevented)return;if(!i||!s)return;const a=Boolean(this._interval);this.pause(),this._isSliding=!0,this._setActiveIndicatorElement(o),this._activeElement=s;const l=n?"carousel-item-start":"carousel-item-end",c=n?"carousel-item-next":"carousel-item-prev";s.classList.add(c),d(s),i.classList.add(l),s.classList.add(l),this._queueCallback((()=>{s.classList.remove(l,c),s.classList.add(yt),i.classList.remove(yt,c,l),this._isSliding=!1,r(ut)}),i,this._isAnimated()),a&&this.cycle()}_isAnimated(){return this._element.classList.contains("slide")}_getActive(){return z.findOne(Et,this._element)}_getItems(){return z.find(At,this._element)}_clearInterval(){this._interval&&(clearInterval(this._interval),this._interval=null)}_directionToOrder(t){return p()?t===ct?lt:at:t===ct?at:lt}_orderToDirection(t){return p()?t===lt?ct:ht:t===lt?ht:ct}static jQueryInterface(t){return this.each((function(){const e=xt.getOrCreateInstance(this,t);if("number"!=typeof t){if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}else e.to(t)}))}}N.on(document,bt,"[data-bs-slide], [data-bs-slide-to]",(function(t){const e=z.getElementFromSelector(this);if(!e||!e.classList.contains(vt))return;t.preventDefault();const i=xt.getOrCreateInstance(e),n=this.getAttribute("data-bs-slide-to");return n?(i.to(n),void i._maybeEnableCycle()):"next"===F.getDataAttribute(this,"slide")?(i.next(),void i._maybeEnableCycle()):(i.prev(),void i._maybeEnableCycle())})),N.on(window,_t,(()=>{const t=z.find('[data-bs-ride="carousel"]');for(const e of t)xt.getOrCreateInstance(e)})),m(xt);const kt=".bs.collapse",Lt=`show${kt}`,St=`shown${kt}`,Dt=`hide${kt}`,$t=`hidden${kt}`,It=`click${kt}.data-api`,Nt="show",Pt="collapse",Mt="collapsing",jt=`:scope .${Pt} .${Pt}`,Ft='[data-bs-toggle="collapse"]',Ht={parent:null,toggle:!0},Wt={parent:"(null|element)",toggle:"boolean"};class Bt extends W{constructor(t,e){super(t,e),this._isTransitioning=!1,this._triggerArray=[];const i=z.find(Ft);for(const t of i){const e=z.getSelectorFromElement(t),i=z.find(e).filter((t=>t===this._element));null!==e&&i.length&&this._triggerArray.push(t)}this._initializeChildren(),this._config.parent||this._addAriaAndCollapsedClass(this._triggerArray,this._isShown()),this._config.toggle&&this.toggle()}static get Default(){return Ht}static get DefaultType(){return Wt}static get NAME(){return"collapse"}toggle(){this._isShown()?this.hide():this.show()}show(){if(this._isTransitioning||this._isShown())return;let t=[];if(this._config.parent&&(t=this._getFirstLevelChildren(".collapse.show, .collapse.collapsing").filter((t=>t!==this._element)).map((t=>Bt.getOrCreateInstance(t,{toggle:!1})))),t.length&&t[0]._isTransitioning)return;if(N.trigger(this._element,Lt).defaultPrevented)return;for(const e of t)e.hide();const e=this._getDimension();this._element.classList.remove(Pt),this._element.classList.add(Mt),this._element.style[e]=0,this._addAriaAndCollapsedClass(this._triggerArray,!0),this._isTransitioning=!0;const i=`scroll${e[0].toUpperCase()+e.slice(1)}`;this._queueCallback((()=>{this._isTransitioning=!1,this._element.classList.remove(Mt),this._element.classList.add(Pt,Nt),this._element.style[e]="",N.trigger(this._element,St)}),this._element,!0),this._element.style[e]=`${this._element[i]}px`}hide(){if(this._isTransitioning||!this._isShown())return;if(N.trigger(this._element,Dt).defaultPrevented)return;const t=this._getDimension();this._element.style[t]=`${this._element.getBoundingClientRect()[t]}px`,d(this._element),this._element.classList.add(Mt),this._element.classList.remove(Pt,Nt);for(const t of this._triggerArray){const e=z.getElementFromSelector(t);e&&!this._isShown(e)&&this._addAriaAndCollapsedClass([t],!1)}this._isTransitioning=!0,this._element.style[t]="",this._queueCallback((()=>{this._isTransitioning=!1,this._element.classList.remove(Mt),this._element.classList.add(Pt),N.trigger(this._element,$t)}),this._element,!0)}_isShown(t=this._element){return t.classList.contains(Nt)}_configAfterMerge(t){return t.toggle=Boolean(t.toggle),t.parent=r(t.parent),t}_getDimension(){return this._element.classList.contains("collapse-horizontal")?"width":"height"}_initializeChildren(){if(!this._config.parent)return;const t=this._getFirstLevelChildren(Ft);for(const e of t){const t=z.getElementFromSelector(e);t&&this._addAriaAndCollapsedClass([e],this._isShown(t))}}_getFirstLevelChildren(t){const e=z.find(jt,this._config.parent);return z.find(t,this._config.parent).filter((t=>!e.includes(t)))}_addAriaAndCollapsedClass(t,e){if(t.length)for(const i of t)i.classList.toggle("collapsed",!e),i.setAttribute("aria-expanded",e)}static jQueryInterface(t){const e={};return"string"==typeof t&&/show|hide/.test(t)&&(e.toggle=!1),this.each((function(){const i=Bt.getOrCreateInstance(this,e);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t]()}}))}}N.on(document,It,Ft,(function(t){("A"===t.target.tagName||t.delegateTarget&&"A"===t.delegateTarget.tagName)&&t.preventDefault();for(const t of z.getMultipleElementsFromSelector(this))Bt.getOrCreateInstance(t,{toggle:!1}).toggle()})),m(Bt);var zt="top",Rt="bottom",qt="right",Vt="left",Kt="auto",Qt=[zt,Rt,qt,Vt],Xt="start",Yt="end",Ut="clippingParents",Gt="viewport",Jt="popper",Zt="reference",te=Qt.reduce((function(t,e){return t.concat([e+"-"+Xt,e+"-"+Yt])}),[]),ee=[].concat(Qt,[Kt]).reduce((function(t,e){return t.concat([e,e+"-"+Xt,e+"-"+Yt])}),[]),ie="beforeRead",ne="read",se="afterRead",oe="beforeMain",re="main",ae="afterMain",le="beforeWrite",ce="write",he="afterWrite",de=[ie,ne,se,oe,re,ae,le,ce,he];function ue(t){return t?(t.nodeName||"").toLowerCase():null}function fe(t){if(null==t)return window;if("[object Window]"!==t.toString()){var e=t.ownerDocument;return e&&e.defaultView||window}return t}function pe(t){return t instanceof fe(t).Element||t instanceof Element}function me(t){return t instanceof fe(t).HTMLElement||t instanceof HTMLElement}function ge(t){return"undefined"!=typeof ShadowRoot&&(t instanceof fe(t).ShadowRoot||t instanceof ShadowRoot)}const _e={name:"applyStyles",enabled:!0,phase:"write",fn:function(t){var e=t.state;Object.keys(e.elements).forEach((function(t){var i=e.styles[t]||{},n=e.attributes[t]||{},s=e.elements[t];me(s)&&ue(s)&&(Object.assign(s.style,i),Object.keys(n).forEach((function(t){var e=n[t];!1===e?s.removeAttribute(t):s.setAttribute(t,!0===e?"":e)})))}))},effect:function(t){var e=t.state,i={popper:{position:e.options.strategy,left:"0",top:"0",margin:"0"},arrow:{position:"absolute"},reference:{}};return Object.assign(e.elements.popper.style,i.popper),e.styles=i,e.elements.arrow&&Object.assign(e.elements.arrow.style,i.arrow),function(){Object.keys(e.elements).forEach((function(t){var n=e.elements[t],s=e.attributes[t]||{},o=Object.keys(e.styles.hasOwnProperty(t)?e.styles[t]:i[t]).reduce((function(t,e){return t[e]="",t}),{});me(n)&&ue(n)&&(Object.assign(n.style,o),Object.keys(s).forEach((function(t){n.removeAttribute(t)})))}))}},requires:["computeStyles"]};function be(t){return t.split("-")[0]}var ve=Math.max,ye=Math.min,we=Math.round;function Ae(){var t=navigator.userAgentData;return null!=t&&t.brands&&Array.isArray(t.brands)?t.brands.map((function(t){return t.brand+"/"+t.version})).join(" "):navigator.userAgent}function Ee(){return!/^((?!chrome|android).)*safari/i.test(Ae())}function Te(t,e,i){void 0===e&&(e=!1),void 0===i&&(i=!1);var n=t.getBoundingClientRect(),s=1,o=1;e&&me(t)&&(s=t.offsetWidth>0&&we(n.width)/t.offsetWidth||1,o=t.offsetHeight>0&&we(n.height)/t.offsetHeight||1);var r=(pe(t)?fe(t):window).visualViewport,a=!Ee()&&i,l=(n.left+(a&&r?r.offsetLeft:0))/s,c=(n.top+(a&&r?r.offsetTop:0))/o,h=n.width/s,d=n.height/o;return{width:h,height:d,top:c,right:l+h,bottom:c+d,left:l,x:l,y:c}}function Ce(t){var e=Te(t),i=t.offsetWidth,n=t.offsetHeight;return Math.abs(e.width-i)<=1&&(i=e.width),Math.abs(e.height-n)<=1&&(n=e.height),{x:t.offsetLeft,y:t.offsetTop,width:i,height:n}}function Oe(t,e){var i=e.getRootNode&&e.getRootNode();if(t.contains(e))return!0;if(i&&ge(i)){var n=e;do{if(n&&t.isSameNode(n))return!0;n=n.parentNode||n.host}while(n)}return!1}function xe(t){return fe(t).getComputedStyle(t)}function ke(t){return["table","td","th"].indexOf(ue(t))>=0}function Le(t){return((pe(t)?t.ownerDocument:t.document)||window.document).documentElement}function Se(t){return"html"===ue(t)?t:t.assignedSlot||t.parentNode||(ge(t)?t.host:null)||Le(t)}function De(t){return me(t)&&"fixed"!==xe(t).position?t.offsetParent:null}function $e(t){for(var e=fe(t),i=De(t);i&&ke(i)&&"static"===xe(i).position;)i=De(i);return i&&("html"===ue(i)||"body"===ue(i)&&"static"===xe(i).position)?e:i||function(t){var e=/firefox/i.test(Ae());if(/Trident/i.test(Ae())&&me(t)&&"fixed"===xe(t).position)return null;var i=Se(t);for(ge(i)&&(i=i.host);me(i)&&["html","body"].indexOf(ue(i))<0;){var n=xe(i);if("none"!==n.transform||"none"!==n.perspective||"paint"===n.contain||-1!==["transform","perspective"].indexOf(n.willChange)||e&&"filter"===n.willChange||e&&n.filter&&"none"!==n.filter)return i;i=i.parentNode}return null}(t)||e}function Ie(t){return["top","bottom"].indexOf(t)>=0?"x":"y"}function Ne(t,e,i){return ve(t,ye(e,i))}function Pe(t){return Object.assign({},{top:0,right:0,bottom:0,left:0},t)}function Me(t,e){return e.reduce((function(e,i){return e[i]=t,e}),{})}const je={name:"arrow",enabled:!0,phase:"main",fn:function(t){var e,i=t.state,n=t.name,s=t.options,o=i.elements.arrow,r=i.modifiersData.popperOffsets,a=be(i.placement),l=Ie(a),c=[Vt,qt].indexOf(a)>=0?"height":"width";if(o&&r){var h=function(t,e){return Pe("number"!=typeof(t="function"==typeof t?t(Object.assign({},e.rects,{placement:e.placement})):t)?t:Me(t,Qt))}(s.padding,i),d=Ce(o),u="y"===l?zt:Vt,f="y"===l?Rt:qt,p=i.rects.reference[c]+i.rects.reference[l]-r[l]-i.rects.popper[c],m=r[l]-i.rects.reference[l],g=$e(o),_=g?"y"===l?g.clientHeight||0:g.clientWidth||0:0,b=p/2-m/2,v=h[u],y=_-d[c]-h[f],w=_/2-d[c]/2+b,A=Ne(v,w,y),E=l;i.modifiersData[n]=((e={})[E]=A,e.centerOffset=A-w,e)}},effect:function(t){var e=t.state,i=t.options.element,n=void 0===i?"[data-popper-arrow]":i;null!=n&&("string"!=typeof n||(n=e.elements.popper.querySelector(n)))&&Oe(e.elements.popper,n)&&(e.elements.arrow=n)},requires:["popperOffsets"],requiresIfExists:["preventOverflow"]};function Fe(t){return t.split("-")[1]}var He={top:"auto",right:"auto",bottom:"auto",left:"auto"};function We(t){var e,i=t.popper,n=t.popperRect,s=t.placement,o=t.variation,r=t.offsets,a=t.position,l=t.gpuAcceleration,c=t.adaptive,h=t.roundOffsets,d=t.isFixed,u=r.x,f=void 0===u?0:u,p=r.y,m=void 0===p?0:p,g="function"==typeof h?h({x:f,y:m}):{x:f,y:m};f=g.x,m=g.y;var _=r.hasOwnProperty("x"),b=r.hasOwnProperty("y"),v=Vt,y=zt,w=window;if(c){var A=$e(i),E="clientHeight",T="clientWidth";A===fe(i)&&"static"!==xe(A=Le(i)).position&&"absolute"===a&&(E="scrollHeight",T="scrollWidth"),(s===zt||(s===Vt||s===qt)&&o===Yt)&&(y=Rt,m-=(d&&A===w&&w.visualViewport?w.visualViewport.height:A[E])-n.height,m*=l?1:-1),s!==Vt&&(s!==zt&&s!==Rt||o!==Yt)||(v=qt,f-=(d&&A===w&&w.visualViewport?w.visualViewport.width:A[T])-n.width,f*=l?1:-1)}var C,O=Object.assign({position:a},c&&He),x=!0===h?function(t,e){var i=t.x,n=t.y,s=e.devicePixelRatio||1;return{x:we(i*s)/s||0,y:we(n*s)/s||0}}({x:f,y:m},fe(i)):{x:f,y:m};return f=x.x,m=x.y,l?Object.assign({},O,((C={})[y]=b?"0":"",C[v]=_?"0":"",C.transform=(w.devicePixelRatio||1)<=1?"translate("+f+"px, "+m+"px)":"translate3d("+f+"px, "+m+"px, 0)",C)):Object.assign({},O,((e={})[y]=b?m+"px":"",e[v]=_?f+"px":"",e.transform="",e))}const Be={name:"computeStyles",enabled:!0,phase:"beforeWrite",fn:function(t){var e=t.state,i=t.options,n=i.gpuAcceleration,s=void 0===n||n,o=i.adaptive,r=void 0===o||o,a=i.roundOffsets,l=void 0===a||a,c={placement:be(e.placement),variation:Fe(e.placement),popper:e.elements.popper,popperRect:e.rects.popper,gpuAcceleration:s,isFixed:"fixed"===e.options.strategy};null!=e.modifiersData.popperOffsets&&(e.styles.popper=Object.assign({},e.styles.popper,We(Object.assign({},c,{offsets:e.modifiersData.popperOffsets,position:e.options.strategy,adaptive:r,roundOffsets:l})))),null!=e.modifiersData.arrow&&(e.styles.arrow=Object.assign({},e.styles.arrow,We(Object.assign({},c,{offsets:e.modifiersData.arrow,position:"absolute",adaptive:!1,roundOffsets:l})))),e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-placement":e.placement})},data:{}};var ze={passive:!0};const Re={name:"eventListeners",enabled:!0,phase:"write",fn:function(){},effect:function(t){var e=t.state,i=t.instance,n=t.options,s=n.scroll,o=void 0===s||s,r=n.resize,a=void 0===r||r,l=fe(e.elements.popper),c=[].concat(e.scrollParents.reference,e.scrollParents.popper);return o&&c.forEach((function(t){t.addEventListener("scroll",i.update,ze)})),a&&l.addEventListener("resize",i.update,ze),function(){o&&c.forEach((function(t){t.removeEventListener("scroll",i.update,ze)})),a&&l.removeEventListener("resize",i.update,ze)}},data:{}};var qe={left:"right",right:"left",bottom:"top",top:"bottom"};function Ve(t){return t.replace(/left|right|bottom|top/g,(function(t){return qe[t]}))}var Ke={start:"end",end:"start"};function Qe(t){return t.replace(/start|end/g,(function(t){return Ke[t]}))}function Xe(t){var e=fe(t);return{scrollLeft:e.pageXOffset,scrollTop:e.pageYOffset}}function Ye(t){return Te(Le(t)).left+Xe(t).scrollLeft}function Ue(t){var e=xe(t),i=e.overflow,n=e.overflowX,s=e.overflowY;return/auto|scroll|overlay|hidden/.test(i+s+n)}function Ge(t){return["html","body","#document"].indexOf(ue(t))>=0?t.ownerDocument.body:me(t)&&Ue(t)?t:Ge(Se(t))}function Je(t,e){var i;void 0===e&&(e=[]);var n=Ge(t),s=n===(null==(i=t.ownerDocument)?void 0:i.body),o=fe(n),r=s?[o].concat(o.visualViewport||[],Ue(n)?n:[]):n,a=e.concat(r);return s?a:a.concat(Je(Se(r)))}function Ze(t){return Object.assign({},t,{left:t.x,top:t.y,right:t.x+t.width,bottom:t.y+t.height})}function ti(t,e,i){return e===Gt?Ze(function(t,e){var i=fe(t),n=Le(t),s=i.visualViewport,o=n.clientWidth,r=n.clientHeight,a=0,l=0;if(s){o=s.width,r=s.height;var c=Ee();(c||!c&&"fixed"===e)&&(a=s.offsetLeft,l=s.offsetTop)}return{width:o,height:r,x:a+Ye(t),y:l}}(t,i)):pe(e)?function(t,e){var i=Te(t,!1,"fixed"===e);return i.top=i.top+t.clientTop,i.left=i.left+t.clientLeft,i.bottom=i.top+t.clientHeight,i.right=i.left+t.clientWidth,i.width=t.clientWidth,i.height=t.clientHeight,i.x=i.left,i.y=i.top,i}(e,i):Ze(function(t){var e,i=Le(t),n=Xe(t),s=null==(e=t.ownerDocument)?void 0:e.body,o=ve(i.scrollWidth,i.clientWidth,s?s.scrollWidth:0,s?s.clientWidth:0),r=ve(i.scrollHeight,i.clientHeight,s?s.scrollHeight:0,s?s.clientHeight:0),a=-n.scrollLeft+Ye(t),l=-n.scrollTop;return"rtl"===xe(s||i).direction&&(a+=ve(i.clientWidth,s?s.clientWidth:0)-o),{width:o,height:r,x:a,y:l}}(Le(t)))}function ei(t){var e,i=t.reference,n=t.element,s=t.placement,o=s?be(s):null,r=s?Fe(s):null,a=i.x+i.width/2-n.width/2,l=i.y+i.height/2-n.height/2;switch(o){case zt:e={x:a,y:i.y-n.height};break;case Rt:e={x:a,y:i.y+i.height};break;case qt:e={x:i.x+i.width,y:l};break;case Vt:e={x:i.x-n.width,y:l};break;default:e={x:i.x,y:i.y}}var c=o?Ie(o):null;if(null!=c){var h="y"===c?"height":"width";switch(r){case Xt:e[c]=e[c]-(i[h]/2-n[h]/2);break;case Yt:e[c]=e[c]+(i[h]/2-n[h]/2)}}return e}function ii(t,e){void 0===e&&(e={});var i=e,n=i.placement,s=void 0===n?t.placement:n,o=i.strategy,r=void 0===o?t.strategy:o,a=i.boundary,l=void 0===a?Ut:a,c=i.rootBoundary,h=void 0===c?Gt:c,d=i.elementContext,u=void 0===d?Jt:d,f=i.altBoundary,p=void 0!==f&&f,m=i.padding,g=void 0===m?0:m,_=Pe("number"!=typeof g?g:Me(g,Qt)),b=u===Jt?Zt:Jt,v=t.rects.popper,y=t.elements[p?b:u],w=function(t,e,i,n){var s="clippingParents"===e?function(t){var e=Je(Se(t)),i=["absolute","fixed"].indexOf(xe(t).position)>=0&&me(t)?$e(t):t;return pe(i)?e.filter((function(t){return pe(t)&&Oe(t,i)&&"body"!==ue(t)})):[]}(t):[].concat(e),o=[].concat(s,[i]),r=o[0],a=o.reduce((function(e,i){var s=ti(t,i,n);return e.top=ve(s.top,e.top),e.right=ye(s.right,e.right),e.bottom=ye(s.bottom,e.bottom),e.left=ve(s.left,e.left),e}),ti(t,r,n));return a.width=a.right-a.left,a.height=a.bottom-a.top,a.x=a.left,a.y=a.top,a}(pe(y)?y:y.contextElement||Le(t.elements.popper),l,h,r),A=Te(t.elements.reference),E=ei({reference:A,element:v,strategy:"absolute",placement:s}),T=Ze(Object.assign({},v,E)),C=u===Jt?T:A,O={top:w.top-C.top+_.top,bottom:C.bottom-w.bottom+_.bottom,left:w.left-C.left+_.left,right:C.right-w.right+_.right},x=t.modifiersData.offset;if(u===Jt&&x){var k=x[s];Object.keys(O).forEach((function(t){var e=[qt,Rt].indexOf(t)>=0?1:-1,i=[zt,Rt].indexOf(t)>=0?"y":"x";O[t]+=k[i]*e}))}return O}function ni(t,e){void 0===e&&(e={});var i=e,n=i.placement,s=i.boundary,o=i.rootBoundary,r=i.padding,a=i.flipVariations,l=i.allowedAutoPlacements,c=void 0===l?ee:l,h=Fe(n),d=h?a?te:te.filter((function(t){return Fe(t)===h})):Qt,u=d.filter((function(t){return c.indexOf(t)>=0}));0===u.length&&(u=d);var f=u.reduce((function(e,i){return e[i]=ii(t,{placement:i,boundary:s,rootBoundary:o,padding:r})[be(i)],e}),{});return Object.keys(f).sort((function(t,e){return f[t]-f[e]}))}const si={name:"flip",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,n=t.name;if(!e.modifiersData[n]._skip){for(var s=i.mainAxis,o=void 0===s||s,r=i.altAxis,a=void 0===r||r,l=i.fallbackPlacements,c=i.padding,h=i.boundary,d=i.rootBoundary,u=i.altBoundary,f=i.flipVariations,p=void 0===f||f,m=i.allowedAutoPlacements,g=e.options.placement,_=be(g),b=l||(_!==g&&p?function(t){if(be(t)===Kt)return[];var e=Ve(t);return[Qe(t),e,Qe(e)]}(g):[Ve(g)]),v=[g].concat(b).reduce((function(t,i){return t.concat(be(i)===Kt?ni(e,{placement:i,boundary:h,rootBoundary:d,padding:c,flipVariations:p,allowedAutoPlacements:m}):i)}),[]),y=e.rects.reference,w=e.rects.popper,A=new Map,E=!0,T=v[0],C=0;C=0,S=L?"width":"height",D=ii(e,{placement:O,boundary:h,rootBoundary:d,altBoundary:u,padding:c}),$=L?k?qt:Vt:k?Rt:zt;y[S]>w[S]&&($=Ve($));var I=Ve($),N=[];if(o&&N.push(D[x]<=0),a&&N.push(D[$]<=0,D[I]<=0),N.every((function(t){return t}))){T=O,E=!1;break}A.set(O,N)}if(E)for(var P=function(t){var e=v.find((function(e){var i=A.get(e);if(i)return i.slice(0,t).every((function(t){return t}))}));if(e)return T=e,"break"},M=p?3:1;M>0&&"break"!==P(M);M--);e.placement!==T&&(e.modifiersData[n]._skip=!0,e.placement=T,e.reset=!0)}},requiresIfExists:["offset"],data:{_skip:!1}};function oi(t,e,i){return void 0===i&&(i={x:0,y:0}),{top:t.top-e.height-i.y,right:t.right-e.width+i.x,bottom:t.bottom-e.height+i.y,left:t.left-e.width-i.x}}function ri(t){return[zt,qt,Rt,Vt].some((function(e){return t[e]>=0}))}const ai={name:"hide",enabled:!0,phase:"main",requiresIfExists:["preventOverflow"],fn:function(t){var e=t.state,i=t.name,n=e.rects.reference,s=e.rects.popper,o=e.modifiersData.preventOverflow,r=ii(e,{elementContext:"reference"}),a=ii(e,{altBoundary:!0}),l=oi(r,n),c=oi(a,s,o),h=ri(l),d=ri(c);e.modifiersData[i]={referenceClippingOffsets:l,popperEscapeOffsets:c,isReferenceHidden:h,hasPopperEscaped:d},e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-reference-hidden":h,"data-popper-escaped":d})}},li={name:"offset",enabled:!0,phase:"main",requires:["popperOffsets"],fn:function(t){var e=t.state,i=t.options,n=t.name,s=i.offset,o=void 0===s?[0,0]:s,r=ee.reduce((function(t,i){return t[i]=function(t,e,i){var n=be(t),s=[Vt,zt].indexOf(n)>=0?-1:1,o="function"==typeof i?i(Object.assign({},e,{placement:t})):i,r=o[0],a=o[1];return r=r||0,a=(a||0)*s,[Vt,qt].indexOf(n)>=0?{x:a,y:r}:{x:r,y:a}}(i,e.rects,o),t}),{}),a=r[e.placement],l=a.x,c=a.y;null!=e.modifiersData.popperOffsets&&(e.modifiersData.popperOffsets.x+=l,e.modifiersData.popperOffsets.y+=c),e.modifiersData[n]=r}},ci={name:"popperOffsets",enabled:!0,phase:"read",fn:function(t){var e=t.state,i=t.name;e.modifiersData[i]=ei({reference:e.rects.reference,element:e.rects.popper,strategy:"absolute",placement:e.placement})},data:{}},hi={name:"preventOverflow",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,n=t.name,s=i.mainAxis,o=void 0===s||s,r=i.altAxis,a=void 0!==r&&r,l=i.boundary,c=i.rootBoundary,h=i.altBoundary,d=i.padding,u=i.tether,f=void 0===u||u,p=i.tetherOffset,m=void 0===p?0:p,g=ii(e,{boundary:l,rootBoundary:c,padding:d,altBoundary:h}),_=be(e.placement),b=Fe(e.placement),v=!b,y=Ie(_),w="x"===y?"y":"x",A=e.modifiersData.popperOffsets,E=e.rects.reference,T=e.rects.popper,C="function"==typeof m?m(Object.assign({},e.rects,{placement:e.placement})):m,O="number"==typeof C?{mainAxis:C,altAxis:C}:Object.assign({mainAxis:0,altAxis:0},C),x=e.modifiersData.offset?e.modifiersData.offset[e.placement]:null,k={x:0,y:0};if(A){if(o){var L,S="y"===y?zt:Vt,D="y"===y?Rt:qt,$="y"===y?"height":"width",I=A[y],N=I+g[S],P=I-g[D],M=f?-T[$]/2:0,j=b===Xt?E[$]:T[$],F=b===Xt?-T[$]:-E[$],H=e.elements.arrow,W=f&&H?Ce(H):{width:0,height:0},B=e.modifiersData["arrow#persistent"]?e.modifiersData["arrow#persistent"].padding:{top:0,right:0,bottom:0,left:0},z=B[S],R=B[D],q=Ne(0,E[$],W[$]),V=v?E[$]/2-M-q-z-O.mainAxis:j-q-z-O.mainAxis,K=v?-E[$]/2+M+q+R+O.mainAxis:F+q+R+O.mainAxis,Q=e.elements.arrow&&$e(e.elements.arrow),X=Q?"y"===y?Q.clientTop||0:Q.clientLeft||0:0,Y=null!=(L=null==x?void 0:x[y])?L:0,U=I+K-Y,G=Ne(f?ye(N,I+V-Y-X):N,I,f?ve(P,U):P);A[y]=G,k[y]=G-I}if(a){var J,Z="x"===y?zt:Vt,tt="x"===y?Rt:qt,et=A[w],it="y"===w?"height":"width",nt=et+g[Z],st=et-g[tt],ot=-1!==[zt,Vt].indexOf(_),rt=null!=(J=null==x?void 0:x[w])?J:0,at=ot?nt:et-E[it]-T[it]-rt+O.altAxis,lt=ot?et+E[it]+T[it]-rt-O.altAxis:st,ct=f&&ot?function(t,e,i){var n=Ne(t,e,i);return n>i?i:n}(at,et,lt):Ne(f?at:nt,et,f?lt:st);A[w]=ct,k[w]=ct-et}e.modifiersData[n]=k}},requiresIfExists:["offset"]};function di(t,e,i){void 0===i&&(i=!1);var n,s,o=me(e),r=me(e)&&function(t){var e=t.getBoundingClientRect(),i=we(e.width)/t.offsetWidth||1,n=we(e.height)/t.offsetHeight||1;return 1!==i||1!==n}(e),a=Le(e),l=Te(t,r,i),c={scrollLeft:0,scrollTop:0},h={x:0,y:0};return(o||!o&&!i)&&(("body"!==ue(e)||Ue(a))&&(c=(n=e)!==fe(n)&&me(n)?{scrollLeft:(s=n).scrollLeft,scrollTop:s.scrollTop}:Xe(n)),me(e)?((h=Te(e,!0)).x+=e.clientLeft,h.y+=e.clientTop):a&&(h.x=Ye(a))),{x:l.left+c.scrollLeft-h.x,y:l.top+c.scrollTop-h.y,width:l.width,height:l.height}}function ui(t){var e=new Map,i=new Set,n=[];function s(t){i.add(t.name),[].concat(t.requires||[],t.requiresIfExists||[]).forEach((function(t){if(!i.has(t)){var n=e.get(t);n&&s(n)}})),n.push(t)}return t.forEach((function(t){e.set(t.name,t)})),t.forEach((function(t){i.has(t.name)||s(t)})),n}var fi={placement:"bottom",modifiers:[],strategy:"absolute"};function pi(){for(var t=arguments.length,e=new Array(t),i=0;iNumber.parseInt(t,10))):"function"==typeof t?e=>t(e,this._element):t}_getPopperConfig(){const t={placement:this._getPlacement(),modifiers:[{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"offset",options:{offset:this._getOffset()}}]};return(this._inNavbar||"static"===this._config.display)&&(F.setDataAttribute(this._menu,"popper","static"),t.modifiers=[{name:"applyStyles",enabled:!1}]),{...t,...g(this._config.popperConfig,[t])}}_selectMenuItem({key:t,target:e}){const i=z.find(".dropdown-menu .dropdown-item:not(.disabled):not(:disabled)",this._menu).filter((t=>a(t)));i.length&&b(i,e,t===Ti,!i.includes(e)).focus()}static jQueryInterface(t){return this.each((function(){const e=qi.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}static clearMenus(t){if(2===t.button||"keyup"===t.type&&"Tab"!==t.key)return;const e=z.find(Ni);for(const i of e){const e=qi.getInstance(i);if(!e||!1===e._config.autoClose)continue;const n=t.composedPath(),s=n.includes(e._menu);if(n.includes(e._element)||"inside"===e._config.autoClose&&!s||"outside"===e._config.autoClose&&s)continue;if(e._menu.contains(t.target)&&("keyup"===t.type&&"Tab"===t.key||/input|select|option|textarea|form/i.test(t.target.tagName)))continue;const o={relatedTarget:e._element};"click"===t.type&&(o.clickEvent=t),e._completeHide(o)}}static dataApiKeydownHandler(t){const e=/input|textarea/i.test(t.target.tagName),i="Escape"===t.key,n=[Ei,Ti].includes(t.key);if(!n&&!i)return;if(e&&!i)return;t.preventDefault();const s=this.matches(Ii)?this:z.prev(this,Ii)[0]||z.next(this,Ii)[0]||z.findOne(Ii,t.delegateTarget.parentNode),o=qi.getOrCreateInstance(s);if(n)return t.stopPropagation(),o.show(),void o._selectMenuItem(t);o._isShown()&&(t.stopPropagation(),o.hide(),s.focus())}}N.on(document,Si,Ii,qi.dataApiKeydownHandler),N.on(document,Si,Pi,qi.dataApiKeydownHandler),N.on(document,Li,qi.clearMenus),N.on(document,Di,qi.clearMenus),N.on(document,Li,Ii,(function(t){t.preventDefault(),qi.getOrCreateInstance(this).toggle()})),m(qi);const Vi="backdrop",Ki="show",Qi=`mousedown.bs.${Vi}`,Xi={className:"modal-backdrop",clickCallback:null,isAnimated:!1,isVisible:!0,rootElement:"body"},Yi={className:"string",clickCallback:"(function|null)",isAnimated:"boolean",isVisible:"boolean",rootElement:"(element|string)"};class Ui extends H{constructor(t){super(),this._config=this._getConfig(t),this._isAppended=!1,this._element=null}static get Default(){return Xi}static get DefaultType(){return Yi}static get NAME(){return Vi}show(t){if(!this._config.isVisible)return void g(t);this._append();const e=this._getElement();this._config.isAnimated&&d(e),e.classList.add(Ki),this._emulateAnimation((()=>{g(t)}))}hide(t){this._config.isVisible?(this._getElement().classList.remove(Ki),this._emulateAnimation((()=>{this.dispose(),g(t)}))):g(t)}dispose(){this._isAppended&&(N.off(this._element,Qi),this._element.remove(),this._isAppended=!1)}_getElement(){if(!this._element){const t=document.createElement("div");t.className=this._config.className,this._config.isAnimated&&t.classList.add("fade"),this._element=t}return this._element}_configAfterMerge(t){return t.rootElement=r(t.rootElement),t}_append(){if(this._isAppended)return;const t=this._getElement();this._config.rootElement.append(t),N.on(t,Qi,(()=>{g(this._config.clickCallback)})),this._isAppended=!0}_emulateAnimation(t){_(t,this._getElement(),this._config.isAnimated)}}const Gi=".bs.focustrap",Ji=`focusin${Gi}`,Zi=`keydown.tab${Gi}`,tn="backward",en={autofocus:!0,trapElement:null},nn={autofocus:"boolean",trapElement:"element"};class sn extends H{constructor(t){super(),this._config=this._getConfig(t),this._isActive=!1,this._lastTabNavDirection=null}static get Default(){return en}static get DefaultType(){return nn}static get NAME(){return"focustrap"}activate(){this._isActive||(this._config.autofocus&&this._config.trapElement.focus(),N.off(document,Gi),N.on(document,Ji,(t=>this._handleFocusin(t))),N.on(document,Zi,(t=>this._handleKeydown(t))),this._isActive=!0)}deactivate(){this._isActive&&(this._isActive=!1,N.off(document,Gi))}_handleFocusin(t){const{trapElement:e}=this._config;if(t.target===document||t.target===e||e.contains(t.target))return;const i=z.focusableChildren(e);0===i.length?e.focus():this._lastTabNavDirection===tn?i[i.length-1].focus():i[0].focus()}_handleKeydown(t){"Tab"===t.key&&(this._lastTabNavDirection=t.shiftKey?tn:"forward")}}const on=".fixed-top, .fixed-bottom, .is-fixed, .sticky-top",rn=".sticky-top",an="padding-right",ln="margin-right";class cn{constructor(){this._element=document.body}getWidth(){const t=document.documentElement.clientWidth;return Math.abs(window.innerWidth-t)}hide(){const t=this.getWidth();this._disableOverFlow(),this._setElementAttributes(this._element,an,(e=>e+t)),this._setElementAttributes(on,an,(e=>e+t)),this._setElementAttributes(rn,ln,(e=>e-t))}reset(){this._resetElementAttributes(this._element,"overflow"),this._resetElementAttributes(this._element,an),this._resetElementAttributes(on,an),this._resetElementAttributes(rn,ln)}isOverflowing(){return this.getWidth()>0}_disableOverFlow(){this._saveInitialAttribute(this._element,"overflow"),this._element.style.overflow="hidden"}_setElementAttributes(t,e,i){const n=this.getWidth();this._applyManipulationCallback(t,(t=>{if(t!==this._element&&window.innerWidth>t.clientWidth+n)return;this._saveInitialAttribute(t,e);const s=window.getComputedStyle(t).getPropertyValue(e);t.style.setProperty(e,`${i(Number.parseFloat(s))}px`)}))}_saveInitialAttribute(t,e){const i=t.style.getPropertyValue(e);i&&F.setDataAttribute(t,e,i)}_resetElementAttributes(t,e){this._applyManipulationCallback(t,(t=>{const i=F.getDataAttribute(t,e);null!==i?(F.removeDataAttribute(t,e),t.style.setProperty(e,i)):t.style.removeProperty(e)}))}_applyManipulationCallback(t,e){if(o(t))e(t);else for(const i of z.find(t,this._element))e(i)}}const hn=".bs.modal",dn=`hide${hn}`,un=`hidePrevented${hn}`,fn=`hidden${hn}`,pn=`show${hn}`,mn=`shown${hn}`,gn=`resize${hn}`,_n=`click.dismiss${hn}`,bn=`mousedown.dismiss${hn}`,vn=`keydown.dismiss${hn}`,yn=`click${hn}.data-api`,wn="modal-open",An="show",En="modal-static",Tn={backdrop:!0,focus:!0,keyboard:!0},Cn={backdrop:"(boolean|string)",focus:"boolean",keyboard:"boolean"};class On extends W{constructor(t,e){super(t,e),this._dialog=z.findOne(".modal-dialog",this._element),this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._isShown=!1,this._isTransitioning=!1,this._scrollBar=new cn,this._addEventListeners()}static get Default(){return Tn}static get DefaultType(){return Cn}static get NAME(){return"modal"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||this._isTransitioning||N.trigger(this._element,pn,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._isTransitioning=!0,this._scrollBar.hide(),document.body.classList.add(wn),this._adjustDialog(),this._backdrop.show((()=>this._showElement(t))))}hide(){this._isShown&&!this._isTransitioning&&(N.trigger(this._element,dn).defaultPrevented||(this._isShown=!1,this._isTransitioning=!0,this._focustrap.deactivate(),this._element.classList.remove(An),this._queueCallback((()=>this._hideModal()),this._element,this._isAnimated())))}dispose(){N.off(window,hn),N.off(this._dialog,hn),this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}handleUpdate(){this._adjustDialog()}_initializeBackDrop(){return new Ui({isVisible:Boolean(this._config.backdrop),isAnimated:this._isAnimated()})}_initializeFocusTrap(){return new sn({trapElement:this._element})}_showElement(t){document.body.contains(this._element)||document.body.append(this._element),this._element.style.display="block",this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.scrollTop=0;const e=z.findOne(".modal-body",this._dialog);e&&(e.scrollTop=0),d(this._element),this._element.classList.add(An),this._queueCallback((()=>{this._config.focus&&this._focustrap.activate(),this._isTransitioning=!1,N.trigger(this._element,mn,{relatedTarget:t})}),this._dialog,this._isAnimated())}_addEventListeners(){N.on(this._element,vn,(t=>{"Escape"===t.key&&(this._config.keyboard?this.hide():this._triggerBackdropTransition())})),N.on(window,gn,(()=>{this._isShown&&!this._isTransitioning&&this._adjustDialog()})),N.on(this._element,bn,(t=>{N.one(this._element,_n,(e=>{this._element===t.target&&this._element===e.target&&("static"!==this._config.backdrop?this._config.backdrop&&this.hide():this._triggerBackdropTransition())}))}))}_hideModal(){this._element.style.display="none",this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._isTransitioning=!1,this._backdrop.hide((()=>{document.body.classList.remove(wn),this._resetAdjustments(),this._scrollBar.reset(),N.trigger(this._element,fn)}))}_isAnimated(){return this._element.classList.contains("fade")}_triggerBackdropTransition(){if(N.trigger(this._element,un).defaultPrevented)return;const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._element.style.overflowY;"hidden"===e||this._element.classList.contains(En)||(t||(this._element.style.overflowY="hidden"),this._element.classList.add(En),this._queueCallback((()=>{this._element.classList.remove(En),this._queueCallback((()=>{this._element.style.overflowY=e}),this._dialog)}),this._dialog),this._element.focus())}_adjustDialog(){const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._scrollBar.getWidth(),i=e>0;if(i&&!t){const t=p()?"paddingLeft":"paddingRight";this._element.style[t]=`${e}px`}if(!i&&t){const t=p()?"paddingRight":"paddingLeft";this._element.style[t]=`${e}px`}}_resetAdjustments(){this._element.style.paddingLeft="",this._element.style.paddingRight=""}static jQueryInterface(t,e){return this.each((function(){const i=On.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t](e)}}))}}N.on(document,yn,'[data-bs-toggle="modal"]',(function(t){const e=z.getElementFromSelector(this);["A","AREA"].includes(this.tagName)&&t.preventDefault(),N.one(e,pn,(t=>{t.defaultPrevented||N.one(e,fn,(()=>{a(this)&&this.focus()}))}));const i=z.findOne(".modal.show");i&&On.getInstance(i).hide(),On.getOrCreateInstance(e).toggle(this)})),R(On),m(On);const xn=".bs.offcanvas",kn=".data-api",Ln=`load${xn}${kn}`,Sn="show",Dn="showing",$n="hiding",In=".offcanvas.show",Nn=`show${xn}`,Pn=`shown${xn}`,Mn=`hide${xn}`,jn=`hidePrevented${xn}`,Fn=`hidden${xn}`,Hn=`resize${xn}`,Wn=`click${xn}${kn}`,Bn=`keydown.dismiss${xn}`,zn={backdrop:!0,keyboard:!0,scroll:!1},Rn={backdrop:"(boolean|string)",keyboard:"boolean",scroll:"boolean"};class qn extends W{constructor(t,e){super(t,e),this._isShown=!1,this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._addEventListeners()}static get Default(){return zn}static get DefaultType(){return Rn}static get NAME(){return"offcanvas"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||N.trigger(this._element,Nn,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._backdrop.show(),this._config.scroll||(new cn).hide(),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.classList.add(Dn),this._queueCallback((()=>{this._config.scroll&&!this._config.backdrop||this._focustrap.activate(),this._element.classList.add(Sn),this._element.classList.remove(Dn),N.trigger(this._element,Pn,{relatedTarget:t})}),this._element,!0))}hide(){this._isShown&&(N.trigger(this._element,Mn).defaultPrevented||(this._focustrap.deactivate(),this._element.blur(),this._isShown=!1,this._element.classList.add($n),this._backdrop.hide(),this._queueCallback((()=>{this._element.classList.remove(Sn,$n),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._config.scroll||(new cn).reset(),N.trigger(this._element,Fn)}),this._element,!0)))}dispose(){this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}_initializeBackDrop(){const t=Boolean(this._config.backdrop);return new Ui({className:"offcanvas-backdrop",isVisible:t,isAnimated:!0,rootElement:this._element.parentNode,clickCallback:t?()=>{"static"!==this._config.backdrop?this.hide():N.trigger(this._element,jn)}:null})}_initializeFocusTrap(){return new sn({trapElement:this._element})}_addEventListeners(){N.on(this._element,Bn,(t=>{"Escape"===t.key&&(this._config.keyboard?this.hide():N.trigger(this._element,jn))}))}static jQueryInterface(t){return this.each((function(){const e=qn.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}N.on(document,Wn,'[data-bs-toggle="offcanvas"]',(function(t){const e=z.getElementFromSelector(this);if(["A","AREA"].includes(this.tagName)&&t.preventDefault(),l(this))return;N.one(e,Fn,(()=>{a(this)&&this.focus()}));const i=z.findOne(In);i&&i!==e&&qn.getInstance(i).hide(),qn.getOrCreateInstance(e).toggle(this)})),N.on(window,Ln,(()=>{for(const t of z.find(In))qn.getOrCreateInstance(t).show()})),N.on(window,Hn,(()=>{for(const t of z.find("[aria-modal][class*=show][class*=offcanvas-]"))"fixed"!==getComputedStyle(t).position&&qn.getOrCreateInstance(t).hide()})),R(qn),m(qn);const Vn={"*":["class","dir","id","lang","role",/^aria-[\w-]*$/i],a:["target","href","title","rel"],area:[],b:[],br:[],col:[],code:[],div:[],em:[],hr:[],h1:[],h2:[],h3:[],h4:[],h5:[],h6:[],i:[],img:["src","srcset","alt","title","width","height"],li:[],ol:[],p:[],pre:[],s:[],small:[],span:[],sub:[],sup:[],strong:[],u:[],ul:[]},Kn=new Set(["background","cite","href","itemtype","longdesc","poster","src","xlink:href"]),Qn=/^(?!javascript:)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i,Xn=(t,e)=>{const i=t.nodeName.toLowerCase();return e.includes(i)?!Kn.has(i)||Boolean(Qn.test(t.nodeValue)):e.filter((t=>t instanceof RegExp)).some((t=>t.test(i)))},Yn={allowList:Vn,content:{},extraClass:"",html:!1,sanitize:!0,sanitizeFn:null,template:"
"},Un={allowList:"object",content:"object",extraClass:"(string|function)",html:"boolean",sanitize:"boolean",sanitizeFn:"(null|function)",template:"string"},Gn={entry:"(string|element|function|null)",selector:"(string|element)"};class Jn extends H{constructor(t){super(),this._config=this._getConfig(t)}static get Default(){return Yn}static get DefaultType(){return Un}static get NAME(){return"TemplateFactory"}getContent(){return Object.values(this._config.content).map((t=>this._resolvePossibleFunction(t))).filter(Boolean)}hasContent(){return this.getContent().length>0}changeContent(t){return this._checkContent(t),this._config.content={...this._config.content,...t},this}toHtml(){const t=document.createElement("div");t.innerHTML=this._maybeSanitize(this._config.template);for(const[e,i]of Object.entries(this._config.content))this._setContent(t,i,e);const e=t.children[0],i=this._resolvePossibleFunction(this._config.extraClass);return i&&e.classList.add(...i.split(" ")),e}_typeCheckConfig(t){super._typeCheckConfig(t),this._checkContent(t.content)}_checkContent(t){for(const[e,i]of Object.entries(t))super._typeCheckConfig({selector:e,entry:i},Gn)}_setContent(t,e,i){const n=z.findOne(i,t);n&&((e=this._resolvePossibleFunction(e))?o(e)?this._putElementInTemplate(r(e),n):this._config.html?n.innerHTML=this._maybeSanitize(e):n.textContent=e:n.remove())}_maybeSanitize(t){return this._config.sanitize?function(t,e,i){if(!t.length)return t;if(i&&"function"==typeof i)return i(t);const n=(new window.DOMParser).parseFromString(t,"text/html"),s=[].concat(...n.body.querySelectorAll("*"));for(const t of s){const i=t.nodeName.toLowerCase();if(!Object.keys(e).includes(i)){t.remove();continue}const n=[].concat(...t.attributes),s=[].concat(e["*"]||[],e[i]||[]);for(const e of n)Xn(e,s)||t.removeAttribute(e.nodeName)}return n.body.innerHTML}(t,this._config.allowList,this._config.sanitizeFn):t}_resolvePossibleFunction(t){return g(t,[this])}_putElementInTemplate(t,e){if(this._config.html)return e.innerHTML="",void e.append(t);e.textContent=t.textContent}}const Zn=new Set(["sanitize","allowList","sanitizeFn"]),ts="fade",es="show",is=".modal",ns="hide.bs.modal",ss="hover",os="focus",rs={AUTO:"auto",TOP:"top",RIGHT:p()?"left":"right",BOTTOM:"bottom",LEFT:p()?"right":"left"},as={allowList:Vn,animation:!0,boundary:"clippingParents",container:!1,customClass:"",delay:0,fallbackPlacements:["top","right","bottom","left"],html:!1,offset:[0,6],placement:"top",popperConfig:null,sanitize:!0,sanitizeFn:null,selector:!1,template:'',title:"",trigger:"hover focus"},ls={allowList:"object",animation:"boolean",boundary:"(string|element)",container:"(string|element|boolean)",customClass:"(string|function)",delay:"(number|object)",fallbackPlacements:"array",html:"boolean",offset:"(array|string|function)",placement:"(string|function)",popperConfig:"(null|object|function)",sanitize:"boolean",sanitizeFn:"(null|function)",selector:"(string|boolean)",template:"string",title:"(string|element|function)",trigger:"string"};class cs extends W{constructor(t,e){if(void 0===vi)throw new TypeError("Bootstrap's tooltips require Popper (https://popper.js.org)");super(t,e),this._isEnabled=!0,this._timeout=0,this._isHovered=null,this._activeTrigger={},this._popper=null,this._templateFactory=null,this._newContent=null,this.tip=null,this._setListeners(),this._config.selector||this._fixTitle()}static get Default(){return as}static get DefaultType(){return ls}static get NAME(){return"tooltip"}enable(){this._isEnabled=!0}disable(){this._isEnabled=!1}toggleEnabled(){this._isEnabled=!this._isEnabled}toggle(){this._isEnabled&&(this._activeTrigger.click=!this._activeTrigger.click,this._isShown()?this._leave():this._enter())}dispose(){clearTimeout(this._timeout),N.off(this._element.closest(is),ns,this._hideModalHandler),this._element.getAttribute("data-bs-original-title")&&this._element.setAttribute("title",this._element.getAttribute("data-bs-original-title")),this._disposePopper(),super.dispose()}show(){if("none"===this._element.style.display)throw new Error("Please use show on visible elements");if(!this._isWithContent()||!this._isEnabled)return;const t=N.trigger(this._element,this.constructor.eventName("show")),e=(c(this._element)||this._element.ownerDocument.documentElement).contains(this._element);if(t.defaultPrevented||!e)return;this._disposePopper();const i=this._getTipElement();this._element.setAttribute("aria-describedby",i.getAttribute("id"));const{container:n}=this._config;if(this._element.ownerDocument.documentElement.contains(this.tip)||(n.append(i),N.trigger(this._element,this.constructor.eventName("inserted"))),this._popper=this._createPopper(i),i.classList.add(es),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))N.on(t,"mouseover",h);this._queueCallback((()=>{N.trigger(this._element,this.constructor.eventName("shown")),!1===this._isHovered&&this._leave(),this._isHovered=!1}),this.tip,this._isAnimated())}hide(){if(this._isShown()&&!N.trigger(this._element,this.constructor.eventName("hide")).defaultPrevented){if(this._getTipElement().classList.remove(es),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))N.off(t,"mouseover",h);this._activeTrigger.click=!1,this._activeTrigger[os]=!1,this._activeTrigger[ss]=!1,this._isHovered=null,this._queueCallback((()=>{this._isWithActiveTrigger()||(this._isHovered||this._disposePopper(),this._element.removeAttribute("aria-describedby"),N.trigger(this._element,this.constructor.eventName("hidden")))}),this.tip,this._isAnimated())}}update(){this._popper&&this._popper.update()}_isWithContent(){return Boolean(this._getTitle())}_getTipElement(){return this.tip||(this.tip=this._createTipElement(this._newContent||this._getContentForTemplate())),this.tip}_createTipElement(t){const e=this._getTemplateFactory(t).toHtml();if(!e)return null;e.classList.remove(ts,es),e.classList.add(`bs-${this.constructor.NAME}-auto`);const i=(t=>{do{t+=Math.floor(1e6*Math.random())}while(document.getElementById(t));return t})(this.constructor.NAME).toString();return e.setAttribute("id",i),this._isAnimated()&&e.classList.add(ts),e}setContent(t){this._newContent=t,this._isShown()&&(this._disposePopper(),this.show())}_getTemplateFactory(t){return this._templateFactory?this._templateFactory.changeContent(t):this._templateFactory=new Jn({...this._config,content:t,extraClass:this._resolvePossibleFunction(this._config.customClass)}),this._templateFactory}_getContentForTemplate(){return{".tooltip-inner":this._getTitle()}}_getTitle(){return this._resolvePossibleFunction(this._config.title)||this._element.getAttribute("data-bs-original-title")}_initializeOnDelegatedTarget(t){return this.constructor.getOrCreateInstance(t.delegateTarget,this._getDelegateConfig())}_isAnimated(){return this._config.animation||this.tip&&this.tip.classList.contains(ts)}_isShown(){return this.tip&&this.tip.classList.contains(es)}_createPopper(t){const e=g(this._config.placement,[this,t,this._element]),i=rs[e.toUpperCase()];return bi(this._element,t,this._getPopperConfig(i))}_getOffset(){const{offset:t}=this._config;return"string"==typeof t?t.split(",").map((t=>Number.parseInt(t,10))):"function"==typeof t?e=>t(e,this._element):t}_resolvePossibleFunction(t){return g(t,[this._element])}_getPopperConfig(t){const e={placement:t,modifiers:[{name:"flip",options:{fallbackPlacements:this._config.fallbackPlacements}},{name:"offset",options:{offset:this._getOffset()}},{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"arrow",options:{element:`.${this.constructor.NAME}-arrow`}},{name:"preSetPlacement",enabled:!0,phase:"beforeMain",fn:t=>{this._getTipElement().setAttribute("data-popper-placement",t.state.placement)}}]};return{...e,...g(this._config.popperConfig,[e])}}_setListeners(){const t=this._config.trigger.split(" ");for(const e of t)if("click"===e)N.on(this._element,this.constructor.eventName("click"),this._config.selector,(t=>{this._initializeOnDelegatedTarget(t).toggle()}));else if("manual"!==e){const t=e===ss?this.constructor.eventName("mouseenter"):this.constructor.eventName("focusin"),i=e===ss?this.constructor.eventName("mouseleave"):this.constructor.eventName("focusout");N.on(this._element,t,this._config.selector,(t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger["focusin"===t.type?os:ss]=!0,e._enter()})),N.on(this._element,i,this._config.selector,(t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger["focusout"===t.type?os:ss]=e._element.contains(t.relatedTarget),e._leave()}))}this._hideModalHandler=()=>{this._element&&this.hide()},N.on(this._element.closest(is),ns,this._hideModalHandler)}_fixTitle(){const t=this._element.getAttribute("title");t&&(this._element.getAttribute("aria-label")||this._element.textContent.trim()||this._element.setAttribute("aria-label",t),this._element.setAttribute("data-bs-original-title",t),this._element.removeAttribute("title"))}_enter(){this._isShown()||this._isHovered?this._isHovered=!0:(this._isHovered=!0,this._setTimeout((()=>{this._isHovered&&this.show()}),this._config.delay.show))}_leave(){this._isWithActiveTrigger()||(this._isHovered=!1,this._setTimeout((()=>{this._isHovered||this.hide()}),this._config.delay.hide))}_setTimeout(t,e){clearTimeout(this._timeout),this._timeout=setTimeout(t,e)}_isWithActiveTrigger(){return Object.values(this._activeTrigger).includes(!0)}_getConfig(t){const e=F.getDataAttributes(this._element);for(const t of Object.keys(e))Zn.has(t)&&delete e[t];return t={...e,..."object"==typeof t&&t?t:{}},t=this._mergeConfigObj(t),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}_configAfterMerge(t){return t.container=!1===t.container?document.body:r(t.container),"number"==typeof t.delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),t}_getDelegateConfig(){const t={};for(const[e,i]of Object.entries(this._config))this.constructor.Default[e]!==i&&(t[e]=i);return t.selector=!1,t.trigger="manual",t}_disposePopper(){this._popper&&(this._popper.destroy(),this._popper=null),this.tip&&(this.tip.remove(),this.tip=null)}static jQueryInterface(t){return this.each((function(){const e=cs.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}m(cs);const hs={...cs.Default,content:"",offset:[0,8],placement:"right",template:'',trigger:"click"},ds={...cs.DefaultType,content:"(null|string|element|function)"};class us extends cs{static get Default(){return hs}static get DefaultType(){return ds}static get NAME(){return"popover"}_isWithContent(){return this._getTitle()||this._getContent()}_getContentForTemplate(){return{".popover-header":this._getTitle(),".popover-body":this._getContent()}}_getContent(){return this._resolvePossibleFunction(this._config.content)}static jQueryInterface(t){return this.each((function(){const e=us.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}m(us);const fs=".bs.scrollspy",ps=`activate${fs}`,ms=`click${fs}`,gs=`load${fs}.data-api`,_s="active",bs="[href]",vs=".nav-link",ys=`${vs}, .nav-item > ${vs}, .list-group-item`,ws={offset:null,rootMargin:"0px 0px -25%",smoothScroll:!1,target:null,threshold:[.1,.5,1]},As={offset:"(number|null)",rootMargin:"string",smoothScroll:"boolean",target:"element",threshold:"array"};class Es extends W{constructor(t,e){super(t,e),this._targetLinks=new Map,this._observableSections=new Map,this._rootElement="visible"===getComputedStyle(this._element).overflowY?null:this._element,this._activeTarget=null,this._observer=null,this._previousScrollData={visibleEntryTop:0,parentScrollTop:0},this.refresh()}static get Default(){return ws}static get DefaultType(){return As}static get NAME(){return"scrollspy"}refresh(){this._initializeTargetsAndObservables(),this._maybeEnableSmoothScroll(),this._observer?this._observer.disconnect():this._observer=this._getNewObserver();for(const t of this._observableSections.values())this._observer.observe(t)}dispose(){this._observer.disconnect(),super.dispose()}_configAfterMerge(t){return t.target=r(t.target)||document.body,t.rootMargin=t.offset?`${t.offset}px 0px -30%`:t.rootMargin,"string"==typeof t.threshold&&(t.threshold=t.threshold.split(",").map((t=>Number.parseFloat(t)))),t}_maybeEnableSmoothScroll(){this._config.smoothScroll&&(N.off(this._config.target,ms),N.on(this._config.target,ms,bs,(t=>{const e=this._observableSections.get(t.target.hash);if(e){t.preventDefault();const i=this._rootElement||window,n=e.offsetTop-this._element.offsetTop;if(i.scrollTo)return void i.scrollTo({top:n,behavior:"smooth"});i.scrollTop=n}})))}_getNewObserver(){const t={root:this._rootElement,threshold:this._config.threshold,rootMargin:this._config.rootMargin};return new IntersectionObserver((t=>this._observerCallback(t)),t)}_observerCallback(t){const e=t=>this._targetLinks.get(`#${t.target.id}`),i=t=>{this._previousScrollData.visibleEntryTop=t.target.offsetTop,this._process(e(t))},n=(this._rootElement||document.documentElement).scrollTop,s=n>=this._previousScrollData.parentScrollTop;this._previousScrollData.parentScrollTop=n;for(const o of t){if(!o.isIntersecting){this._activeTarget=null,this._clearActiveClass(e(o));continue}const t=o.target.offsetTop>=this._previousScrollData.visibleEntryTop;if(s&&t){if(i(o),!n)return}else s||t||i(o)}}_initializeTargetsAndObservables(){this._targetLinks=new Map,this._observableSections=new Map;const t=z.find(bs,this._config.target);for(const e of t){if(!e.hash||l(e))continue;const t=z.findOne(decodeURI(e.hash),this._element);a(t)&&(this._targetLinks.set(decodeURI(e.hash),e),this._observableSections.set(e.hash,t))}}_process(t){this._activeTarget!==t&&(this._clearActiveClass(this._config.target),this._activeTarget=t,t.classList.add(_s),this._activateParents(t),N.trigger(this._element,ps,{relatedTarget:t}))}_activateParents(t){if(t.classList.contains("dropdown-item"))z.findOne(".dropdown-toggle",t.closest(".dropdown")).classList.add(_s);else for(const e of z.parents(t,".nav, .list-group"))for(const t of z.prev(e,ys))t.classList.add(_s)}_clearActiveClass(t){t.classList.remove(_s);const e=z.find(`${bs}.${_s}`,t);for(const t of e)t.classList.remove(_s)}static jQueryInterface(t){return this.each((function(){const e=Es.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}))}}N.on(window,gs,(()=>{for(const t of z.find('[data-bs-spy="scroll"]'))Es.getOrCreateInstance(t)})),m(Es);const Ts=".bs.tab",Cs=`hide${Ts}`,Os=`hidden${Ts}`,xs=`show${Ts}`,ks=`shown${Ts}`,Ls=`click${Ts}`,Ss=`keydown${Ts}`,Ds=`load${Ts}`,$s="ArrowLeft",Is="ArrowRight",Ns="ArrowUp",Ps="ArrowDown",Ms="Home",js="End",Fs="active",Hs="fade",Ws="show",Bs=":not(.dropdown-toggle)",zs='[data-bs-toggle="tab"], [data-bs-toggle="pill"], [data-bs-toggle="list"]',Rs=`.nav-link${Bs}, .list-group-item${Bs}, [role="tab"]${Bs}, ${zs}`,qs=`.${Fs}[data-bs-toggle="tab"], .${Fs}[data-bs-toggle="pill"], .${Fs}[data-bs-toggle="list"]`;class Vs extends W{constructor(t){super(t),this._parent=this._element.closest('.list-group, .nav, [role="tablist"]'),this._parent&&(this._setInitialAttributes(this._parent,this._getChildren()),N.on(this._element,Ss,(t=>this._keydown(t))))}static get NAME(){return"tab"}show(){const t=this._element;if(this._elemIsActive(t))return;const e=this._getActiveElem(),i=e?N.trigger(e,Cs,{relatedTarget:t}):null;N.trigger(t,xs,{relatedTarget:e}).defaultPrevented||i&&i.defaultPrevented||(this._deactivate(e,t),this._activate(t,e))}_activate(t,e){t&&(t.classList.add(Fs),this._activate(z.getElementFromSelector(t)),this._queueCallback((()=>{"tab"===t.getAttribute("role")?(t.removeAttribute("tabindex"),t.setAttribute("aria-selected",!0),this._toggleDropDown(t,!0),N.trigger(t,ks,{relatedTarget:e})):t.classList.add(Ws)}),t,t.classList.contains(Hs)))}_deactivate(t,e){t&&(t.classList.remove(Fs),t.blur(),this._deactivate(z.getElementFromSelector(t)),this._queueCallback((()=>{"tab"===t.getAttribute("role")?(t.setAttribute("aria-selected",!1),t.setAttribute("tabindex","-1"),this._toggleDropDown(t,!1),N.trigger(t,Os,{relatedTarget:e})):t.classList.remove(Ws)}),t,t.classList.contains(Hs)))}_keydown(t){if(![$s,Is,Ns,Ps,Ms,js].includes(t.key))return;t.stopPropagation(),t.preventDefault();const e=this._getChildren().filter((t=>!l(t)));let i;if([Ms,js].includes(t.key))i=e[t.key===Ms?0:e.length-1];else{const n=[Is,Ps].includes(t.key);i=b(e,t.target,n,!0)}i&&(i.focus({preventScroll:!0}),Vs.getOrCreateInstance(i).show())}_getChildren(){return z.find(Rs,this._parent)}_getActiveElem(){return this._getChildren().find((t=>this._elemIsActive(t)))||null}_setInitialAttributes(t,e){this._setAttributeIfNotExists(t,"role","tablist");for(const t of e)this._setInitialAttributesOnChild(t)}_setInitialAttributesOnChild(t){t=this._getInnerElement(t);const e=this._elemIsActive(t),i=this._getOuterElement(t);t.setAttribute("aria-selected",e),i!==t&&this._setAttributeIfNotExists(i,"role","presentation"),e||t.setAttribute("tabindex","-1"),this._setAttributeIfNotExists(t,"role","tab"),this._setInitialAttributesOnTargetPanel(t)}_setInitialAttributesOnTargetPanel(t){const e=z.getElementFromSelector(t);e&&(this._setAttributeIfNotExists(e,"role","tabpanel"),t.id&&this._setAttributeIfNotExists(e,"aria-labelledby",`${t.id}`))}_toggleDropDown(t,e){const i=this._getOuterElement(t);if(!i.classList.contains("dropdown"))return;const n=(t,n)=>{const s=z.findOne(t,i);s&&s.classList.toggle(n,e)};n(".dropdown-toggle",Fs),n(".dropdown-menu",Ws),i.setAttribute("aria-expanded",e)}_setAttributeIfNotExists(t,e,i){t.hasAttribute(e)||t.setAttribute(e,i)}_elemIsActive(t){return t.classList.contains(Fs)}_getInnerElement(t){return t.matches(Rs)?t:z.findOne(Rs,t)}_getOuterElement(t){return t.closest(".nav-item, .list-group-item")||t}static jQueryInterface(t){return this.each((function(){const e=Vs.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}))}}N.on(document,Ls,zs,(function(t){["A","AREA"].includes(this.tagName)&&t.preventDefault(),l(this)||Vs.getOrCreateInstance(this).show()})),N.on(window,Ds,(()=>{for(const t of z.find(qs))Vs.getOrCreateInstance(t)})),m(Vs);const Ks=".bs.toast",Qs=`mouseover${Ks}`,Xs=`mouseout${Ks}`,Ys=`focusin${Ks}`,Us=`focusout${Ks}`,Gs=`hide${Ks}`,Js=`hidden${Ks}`,Zs=`show${Ks}`,to=`shown${Ks}`,eo="hide",io="show",no="showing",so={animation:"boolean",autohide:"boolean",delay:"number"},oo={animation:!0,autohide:!0,delay:5e3};class ro extends W{constructor(t,e){super(t,e),this._timeout=null,this._hasMouseInteraction=!1,this._hasKeyboardInteraction=!1,this._setListeners()}static get Default(){return oo}static get DefaultType(){return so}static get NAME(){return"toast"}show(){N.trigger(this._element,Zs).defaultPrevented||(this._clearTimeout(),this._config.animation&&this._element.classList.add("fade"),this._element.classList.remove(eo),d(this._element),this._element.classList.add(io,no),this._queueCallback((()=>{this._element.classList.remove(no),N.trigger(this._element,to),this._maybeScheduleHide()}),this._element,this._config.animation))}hide(){this.isShown()&&(N.trigger(this._element,Gs).defaultPrevented||(this._element.classList.add(no),this._queueCallback((()=>{this._element.classList.add(eo),this._element.classList.remove(no,io),N.trigger(this._element,Js)}),this._element,this._config.animation)))}dispose(){this._clearTimeout(),this.isShown()&&this._element.classList.remove(io),super.dispose()}isShown(){return this._element.classList.contains(io)}_maybeScheduleHide(){this._config.autohide&&(this._hasMouseInteraction||this._hasKeyboardInteraction||(this._timeout=setTimeout((()=>{this.hide()}),this._config.delay)))}_onInteraction(t,e){switch(t.type){case"mouseover":case"mouseout":this._hasMouseInteraction=e;break;case"focusin":case"focusout":this._hasKeyboardInteraction=e}if(e)return void this._clearTimeout();const i=t.relatedTarget;this._element===i||this._element.contains(i)||this._maybeScheduleHide()}_setListeners(){N.on(this._element,Qs,(t=>this._onInteraction(t,!0))),N.on(this._element,Xs,(t=>this._onInteraction(t,!1))),N.on(this._element,Ys,(t=>this._onInteraction(t,!0))),N.on(this._element,Us,(t=>this._onInteraction(t,!1)))}_clearTimeout(){clearTimeout(this._timeout),this._timeout=null}static jQueryInterface(t){return this.each((function(){const e=ro.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}return R(ro),m(ro),{Alert:Q,Button:Y,Carousel:xt,Collapse:Bt,Dropdown:qi,Modal:On,Offcanvas:qn,Popover:us,ScrollSpy:Es,Tab:Vs,Toast:ro,Tooltip:cs}})); +//# sourceMappingURL=bootstrap.bundle.min.js.map \ No newline at end of file diff --git a/site_libs/clipboard/clipboard.min.js b/site_libs/clipboard/clipboard.min.js new file mode 100644 index 00000000..1103f811 --- /dev/null +++ b/site_libs/clipboard/clipboard.min.js @@ -0,0 +1,7 @@ +/*! + * clipboard.js v2.0.11 + * https://clipboardjs.com/ + * + * Licensed MIT © Zeno Rocha + */ +!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.ClipboardJS=e():t.ClipboardJS=e()}(this,function(){return n={686:function(t,e,n){"use strict";n.d(e,{default:function(){return b}});var e=n(279),i=n.n(e),e=n(370),u=n.n(e),e=n(817),r=n.n(e);function c(t){try{return document.execCommand(t)}catch(t){return}}var a=function(t){t=r()(t);return c("cut"),t};function o(t,e){var n,o,t=(n=t,o="rtl"===document.documentElement.getAttribute("dir"),(t=document.createElement("textarea")).style.fontSize="12pt",t.style.border="0",t.style.padding="0",t.style.margin="0",t.style.position="absolute",t.style[o?"right":"left"]="-9999px",o=window.pageYOffset||document.documentElement.scrollTop,t.style.top="".concat(o,"px"),t.setAttribute("readonly",""),t.value=n,t);return e.container.appendChild(t),e=r()(t),c("copy"),t.remove(),e}var f=function(t){var e=1.anchorjs-link,.anchorjs-link:focus{opacity:1}",A.sheet.cssRules.length),A.sheet.insertRule("[data-anchorjs-icon]::after{content:attr(data-anchorjs-icon)}",A.sheet.cssRules.length),A.sheet.insertRule('@font-face{font-family:anchorjs-icons;src:url(data:n/a;base64,AAEAAAALAIAAAwAwT1MvMg8yG2cAAAE4AAAAYGNtYXDp3gC3AAABpAAAAExnYXNwAAAAEAAAA9wAAAAIZ2x5ZlQCcfwAAAH4AAABCGhlYWQHFvHyAAAAvAAAADZoaGVhBnACFwAAAPQAAAAkaG10eASAADEAAAGYAAAADGxvY2EACACEAAAB8AAAAAhtYXhwAAYAVwAAARgAAAAgbmFtZQGOH9cAAAMAAAAAunBvc3QAAwAAAAADvAAAACAAAQAAAAEAAHzE2p9fDzz1AAkEAAAAAADRecUWAAAAANQA6R8AAAAAAoACwAAAAAgAAgAAAAAAAAABAAADwP/AAAACgAAA/9MCrQABAAAAAAAAAAAAAAAAAAAAAwABAAAAAwBVAAIAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAMCQAGQAAUAAAKZAswAAACPApkCzAAAAesAMwEJAAAAAAAAAAAAAAAAAAAAARAAAAAAAAAAAAAAAAAAAAAAQAAg//0DwP/AAEADwABAAAAAAQAAAAAAAAAAAAAAIAAAAAAAAAIAAAACgAAxAAAAAwAAAAMAAAAcAAEAAwAAABwAAwABAAAAHAAEADAAAAAIAAgAAgAAACDpy//9//8AAAAg6cv//f///+EWNwADAAEAAAAAAAAAAAAAAAAACACEAAEAAAAAAAAAAAAAAAAxAAACAAQARAKAAsAAKwBUAAABIiYnJjQ3NzY2MzIWFxYUBwcGIicmNDc3NjQnJiYjIgYHBwYUFxYUBwYGIwciJicmNDc3NjIXFhQHBwYUFxYWMzI2Nzc2NCcmNDc2MhcWFAcHBgYjARQGDAUtLXoWOR8fORYtLTgKGwoKCjgaGg0gEhIgDXoaGgkJBQwHdR85Fi0tOAobCgoKOBoaDSASEiANehoaCQkKGwotLXoWOR8BMwUFLYEuehYXFxYugC44CQkKGwo4GkoaDQ0NDXoaShoKGwoFBe8XFi6ALjgJCQobCjgaShoNDQ0NehpKGgobCgoKLYEuehYXAAAADACWAAEAAAAAAAEACAAAAAEAAAAAAAIAAwAIAAEAAAAAAAMACAAAAAEAAAAAAAQACAAAAAEAAAAAAAUAAQALAAEAAAAAAAYACAAAAAMAAQQJAAEAEAAMAAMAAQQJAAIABgAcAAMAAQQJAAMAEAAMAAMAAQQJAAQAEAAMAAMAAQQJAAUAAgAiAAMAAQQJAAYAEAAMYW5jaG9yanM0MDBAAGEAbgBjAGgAbwByAGoAcwA0ADAAMABAAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAH//wAP) format("truetype")}',A.sheet.cssRules.length)),h=document.querySelectorAll("[id]"),t=[].map.call(h,function(A){return A.id}),i=0;i\]./()*\\\n\t\b\v\u00A0]/g,"-").replace(/-{2,}/g,"-").substring(0,this.options.truncate).replace(/^-+|-+$/gm,"").toLowerCase()},this.hasAnchorJSLink=function(A){var e=A.firstChild&&-1<(" "+A.firstChild.className+" ").indexOf(" anchorjs-link "),A=A.lastChild&&-1<(" "+A.lastChild.className+" ").indexOf(" anchorjs-link ");return e||A||!1}}}); +// @license-end \ No newline at end of file diff --git a/site_libs/quarto-html/popper.min.js b/site_libs/quarto-html/popper.min.js new file mode 100644 index 00000000..e3726d72 --- /dev/null +++ b/site_libs/quarto-html/popper.min.js @@ -0,0 +1,6 @@ +/** + * @popperjs/core v2.11.7 - MIT License + */ + +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).Popper={})}(this,(function(e){"use strict";function t(e){if(null==e)return window;if("[object Window]"!==e.toString()){var t=e.ownerDocument;return t&&t.defaultView||window}return e}function n(e){return e instanceof t(e).Element||e instanceof Element}function r(e){return e instanceof t(e).HTMLElement||e instanceof HTMLElement}function o(e){return"undefined"!=typeof ShadowRoot&&(e instanceof t(e).ShadowRoot||e instanceof ShadowRoot)}var i=Math.max,a=Math.min,s=Math.round;function f(){var e=navigator.userAgentData;return null!=e&&e.brands&&Array.isArray(e.brands)?e.brands.map((function(e){return e.brand+"/"+e.version})).join(" "):navigator.userAgent}function c(){return!/^((?!chrome|android).)*safari/i.test(f())}function p(e,o,i){void 0===o&&(o=!1),void 0===i&&(i=!1);var a=e.getBoundingClientRect(),f=1,p=1;o&&r(e)&&(f=e.offsetWidth>0&&s(a.width)/e.offsetWidth||1,p=e.offsetHeight>0&&s(a.height)/e.offsetHeight||1);var u=(n(e)?t(e):window).visualViewport,l=!c()&&i,d=(a.left+(l&&u?u.offsetLeft:0))/f,h=(a.top+(l&&u?u.offsetTop:0))/p,m=a.width/f,v=a.height/p;return{width:m,height:v,top:h,right:d+m,bottom:h+v,left:d,x:d,y:h}}function u(e){var n=t(e);return{scrollLeft:n.pageXOffset,scrollTop:n.pageYOffset}}function l(e){return e?(e.nodeName||"").toLowerCase():null}function d(e){return((n(e)?e.ownerDocument:e.document)||window.document).documentElement}function h(e){return p(d(e)).left+u(e).scrollLeft}function m(e){return t(e).getComputedStyle(e)}function v(e){var t=m(e),n=t.overflow,r=t.overflowX,o=t.overflowY;return/auto|scroll|overlay|hidden/.test(n+o+r)}function y(e,n,o){void 0===o&&(o=!1);var i,a,f=r(n),c=r(n)&&function(e){var t=e.getBoundingClientRect(),n=s(t.width)/e.offsetWidth||1,r=s(t.height)/e.offsetHeight||1;return 1!==n||1!==r}(n),m=d(n),y=p(e,c,o),g={scrollLeft:0,scrollTop:0},b={x:0,y:0};return(f||!f&&!o)&&(("body"!==l(n)||v(m))&&(g=(i=n)!==t(i)&&r(i)?{scrollLeft:(a=i).scrollLeft,scrollTop:a.scrollTop}:u(i)),r(n)?((b=p(n,!0)).x+=n.clientLeft,b.y+=n.clientTop):m&&(b.x=h(m))),{x:y.left+g.scrollLeft-b.x,y:y.top+g.scrollTop-b.y,width:y.width,height:y.height}}function g(e){var t=p(e),n=e.offsetWidth,r=e.offsetHeight;return Math.abs(t.width-n)<=1&&(n=t.width),Math.abs(t.height-r)<=1&&(r=t.height),{x:e.offsetLeft,y:e.offsetTop,width:n,height:r}}function b(e){return"html"===l(e)?e:e.assignedSlot||e.parentNode||(o(e)?e.host:null)||d(e)}function x(e){return["html","body","#document"].indexOf(l(e))>=0?e.ownerDocument.body:r(e)&&v(e)?e:x(b(e))}function w(e,n){var r;void 0===n&&(n=[]);var o=x(e),i=o===(null==(r=e.ownerDocument)?void 0:r.body),a=t(o),s=i?[a].concat(a.visualViewport||[],v(o)?o:[]):o,f=n.concat(s);return i?f:f.concat(w(b(s)))}function O(e){return["table","td","th"].indexOf(l(e))>=0}function j(e){return r(e)&&"fixed"!==m(e).position?e.offsetParent:null}function E(e){for(var n=t(e),i=j(e);i&&O(i)&&"static"===m(i).position;)i=j(i);return i&&("html"===l(i)||"body"===l(i)&&"static"===m(i).position)?n:i||function(e){var t=/firefox/i.test(f());if(/Trident/i.test(f())&&r(e)&&"fixed"===m(e).position)return null;var n=b(e);for(o(n)&&(n=n.host);r(n)&&["html","body"].indexOf(l(n))<0;){var i=m(n);if("none"!==i.transform||"none"!==i.perspective||"paint"===i.contain||-1!==["transform","perspective"].indexOf(i.willChange)||t&&"filter"===i.willChange||t&&i.filter&&"none"!==i.filter)return n;n=n.parentNode}return null}(e)||n}var D="top",A="bottom",L="right",P="left",M="auto",k=[D,A,L,P],W="start",B="end",H="viewport",T="popper",R=k.reduce((function(e,t){return e.concat([t+"-"+W,t+"-"+B])}),[]),S=[].concat(k,[M]).reduce((function(e,t){return e.concat([t,t+"-"+W,t+"-"+B])}),[]),V=["beforeRead","read","afterRead","beforeMain","main","afterMain","beforeWrite","write","afterWrite"];function q(e){var t=new Map,n=new Set,r=[];function o(e){n.add(e.name),[].concat(e.requires||[],e.requiresIfExists||[]).forEach((function(e){if(!n.has(e)){var r=t.get(e);r&&o(r)}})),r.push(e)}return e.forEach((function(e){t.set(e.name,e)})),e.forEach((function(e){n.has(e.name)||o(e)})),r}function C(e){return e.split("-")[0]}function N(e,t){var n=t.getRootNode&&t.getRootNode();if(e.contains(t))return!0;if(n&&o(n)){var r=t;do{if(r&&e.isSameNode(r))return!0;r=r.parentNode||r.host}while(r)}return!1}function I(e){return Object.assign({},e,{left:e.x,top:e.y,right:e.x+e.width,bottom:e.y+e.height})}function _(e,r,o){return r===H?I(function(e,n){var r=t(e),o=d(e),i=r.visualViewport,a=o.clientWidth,s=o.clientHeight,f=0,p=0;if(i){a=i.width,s=i.height;var u=c();(u||!u&&"fixed"===n)&&(f=i.offsetLeft,p=i.offsetTop)}return{width:a,height:s,x:f+h(e),y:p}}(e,o)):n(r)?function(e,t){var n=p(e,!1,"fixed"===t);return n.top=n.top+e.clientTop,n.left=n.left+e.clientLeft,n.bottom=n.top+e.clientHeight,n.right=n.left+e.clientWidth,n.width=e.clientWidth,n.height=e.clientHeight,n.x=n.left,n.y=n.top,n}(r,o):I(function(e){var t,n=d(e),r=u(e),o=null==(t=e.ownerDocument)?void 0:t.body,a=i(n.scrollWidth,n.clientWidth,o?o.scrollWidth:0,o?o.clientWidth:0),s=i(n.scrollHeight,n.clientHeight,o?o.scrollHeight:0,o?o.clientHeight:0),f=-r.scrollLeft+h(e),c=-r.scrollTop;return"rtl"===m(o||n).direction&&(f+=i(n.clientWidth,o?o.clientWidth:0)-a),{width:a,height:s,x:f,y:c}}(d(e)))}function F(e,t,o,s){var f="clippingParents"===t?function(e){var t=w(b(e)),o=["absolute","fixed"].indexOf(m(e).position)>=0&&r(e)?E(e):e;return n(o)?t.filter((function(e){return n(e)&&N(e,o)&&"body"!==l(e)})):[]}(e):[].concat(t),c=[].concat(f,[o]),p=c[0],u=c.reduce((function(t,n){var r=_(e,n,s);return t.top=i(r.top,t.top),t.right=a(r.right,t.right),t.bottom=a(r.bottom,t.bottom),t.left=i(r.left,t.left),t}),_(e,p,s));return u.width=u.right-u.left,u.height=u.bottom-u.top,u.x=u.left,u.y=u.top,u}function U(e){return e.split("-")[1]}function z(e){return["top","bottom"].indexOf(e)>=0?"x":"y"}function X(e){var t,n=e.reference,r=e.element,o=e.placement,i=o?C(o):null,a=o?U(o):null,s=n.x+n.width/2-r.width/2,f=n.y+n.height/2-r.height/2;switch(i){case D:t={x:s,y:n.y-r.height};break;case A:t={x:s,y:n.y+n.height};break;case L:t={x:n.x+n.width,y:f};break;case P:t={x:n.x-r.width,y:f};break;default:t={x:n.x,y:n.y}}var c=i?z(i):null;if(null!=c){var p="y"===c?"height":"width";switch(a){case W:t[c]=t[c]-(n[p]/2-r[p]/2);break;case B:t[c]=t[c]+(n[p]/2-r[p]/2)}}return t}function Y(e){return Object.assign({},{top:0,right:0,bottom:0,left:0},e)}function G(e,t){return t.reduce((function(t,n){return t[n]=e,t}),{})}function J(e,t){void 0===t&&(t={});var r=t,o=r.placement,i=void 0===o?e.placement:o,a=r.strategy,s=void 0===a?e.strategy:a,f=r.boundary,c=void 0===f?"clippingParents":f,u=r.rootBoundary,l=void 0===u?H:u,h=r.elementContext,m=void 0===h?T:h,v=r.altBoundary,y=void 0!==v&&v,g=r.padding,b=void 0===g?0:g,x=Y("number"!=typeof b?b:G(b,k)),w=m===T?"reference":T,O=e.rects.popper,j=e.elements[y?w:m],E=F(n(j)?j:j.contextElement||d(e.elements.popper),c,l,s),P=p(e.elements.reference),M=X({reference:P,element:O,strategy:"absolute",placement:i}),W=I(Object.assign({},O,M)),B=m===T?W:P,R={top:E.top-B.top+x.top,bottom:B.bottom-E.bottom+x.bottom,left:E.left-B.left+x.left,right:B.right-E.right+x.right},S=e.modifiersData.offset;if(m===T&&S){var V=S[i];Object.keys(R).forEach((function(e){var t=[L,A].indexOf(e)>=0?1:-1,n=[D,A].indexOf(e)>=0?"y":"x";R[e]+=V[n]*t}))}return R}var K={placement:"bottom",modifiers:[],strategy:"absolute"};function Q(){for(var e=arguments.length,t=new Array(e),n=0;n=0?-1:1,i="function"==typeof n?n(Object.assign({},t,{placement:e})):n,a=i[0],s=i[1];return a=a||0,s=(s||0)*o,[P,L].indexOf(r)>=0?{x:s,y:a}:{x:a,y:s}}(n,t.rects,i),e}),{}),s=a[t.placement],f=s.x,c=s.y;null!=t.modifiersData.popperOffsets&&(t.modifiersData.popperOffsets.x+=f,t.modifiersData.popperOffsets.y+=c),t.modifiersData[r]=a}},se={left:"right",right:"left",bottom:"top",top:"bottom"};function fe(e){return e.replace(/left|right|bottom|top/g,(function(e){return se[e]}))}var ce={start:"end",end:"start"};function pe(e){return e.replace(/start|end/g,(function(e){return ce[e]}))}function ue(e,t){void 0===t&&(t={});var n=t,r=n.placement,o=n.boundary,i=n.rootBoundary,a=n.padding,s=n.flipVariations,f=n.allowedAutoPlacements,c=void 0===f?S:f,p=U(r),u=p?s?R:R.filter((function(e){return U(e)===p})):k,l=u.filter((function(e){return c.indexOf(e)>=0}));0===l.length&&(l=u);var d=l.reduce((function(t,n){return t[n]=J(e,{placement:n,boundary:o,rootBoundary:i,padding:a})[C(n)],t}),{});return Object.keys(d).sort((function(e,t){return d[e]-d[t]}))}var le={name:"flip",enabled:!0,phase:"main",fn:function(e){var t=e.state,n=e.options,r=e.name;if(!t.modifiersData[r]._skip){for(var o=n.mainAxis,i=void 0===o||o,a=n.altAxis,s=void 0===a||a,f=n.fallbackPlacements,c=n.padding,p=n.boundary,u=n.rootBoundary,l=n.altBoundary,d=n.flipVariations,h=void 0===d||d,m=n.allowedAutoPlacements,v=t.options.placement,y=C(v),g=f||(y===v||!h?[fe(v)]:function(e){if(C(e)===M)return[];var t=fe(e);return[pe(e),t,pe(t)]}(v)),b=[v].concat(g).reduce((function(e,n){return e.concat(C(n)===M?ue(t,{placement:n,boundary:p,rootBoundary:u,padding:c,flipVariations:h,allowedAutoPlacements:m}):n)}),[]),x=t.rects.reference,w=t.rects.popper,O=new Map,j=!0,E=b[0],k=0;k=0,S=R?"width":"height",V=J(t,{placement:B,boundary:p,rootBoundary:u,altBoundary:l,padding:c}),q=R?T?L:P:T?A:D;x[S]>w[S]&&(q=fe(q));var N=fe(q),I=[];if(i&&I.push(V[H]<=0),s&&I.push(V[q]<=0,V[N]<=0),I.every((function(e){return e}))){E=B,j=!1;break}O.set(B,I)}if(j)for(var _=function(e){var t=b.find((function(t){var n=O.get(t);if(n)return n.slice(0,e).every((function(e){return e}))}));if(t)return E=t,"break"},F=h?3:1;F>0;F--){if("break"===_(F))break}t.placement!==E&&(t.modifiersData[r]._skip=!0,t.placement=E,t.reset=!0)}},requiresIfExists:["offset"],data:{_skip:!1}};function de(e,t,n){return i(e,a(t,n))}var he={name:"preventOverflow",enabled:!0,phase:"main",fn:function(e){var t=e.state,n=e.options,r=e.name,o=n.mainAxis,s=void 0===o||o,f=n.altAxis,c=void 0!==f&&f,p=n.boundary,u=n.rootBoundary,l=n.altBoundary,d=n.padding,h=n.tether,m=void 0===h||h,v=n.tetherOffset,y=void 0===v?0:v,b=J(t,{boundary:p,rootBoundary:u,padding:d,altBoundary:l}),x=C(t.placement),w=U(t.placement),O=!w,j=z(x),M="x"===j?"y":"x",k=t.modifiersData.popperOffsets,B=t.rects.reference,H=t.rects.popper,T="function"==typeof y?y(Object.assign({},t.rects,{placement:t.placement})):y,R="number"==typeof T?{mainAxis:T,altAxis:T}:Object.assign({mainAxis:0,altAxis:0},T),S=t.modifiersData.offset?t.modifiersData.offset[t.placement]:null,V={x:0,y:0};if(k){if(s){var q,N="y"===j?D:P,I="y"===j?A:L,_="y"===j?"height":"width",F=k[j],X=F+b[N],Y=F-b[I],G=m?-H[_]/2:0,K=w===W?B[_]:H[_],Q=w===W?-H[_]:-B[_],Z=t.elements.arrow,$=m&&Z?g(Z):{width:0,height:0},ee=t.modifiersData["arrow#persistent"]?t.modifiersData["arrow#persistent"].padding:{top:0,right:0,bottom:0,left:0},te=ee[N],ne=ee[I],re=de(0,B[_],$[_]),oe=O?B[_]/2-G-re-te-R.mainAxis:K-re-te-R.mainAxis,ie=O?-B[_]/2+G+re+ne+R.mainAxis:Q+re+ne+R.mainAxis,ae=t.elements.arrow&&E(t.elements.arrow),se=ae?"y"===j?ae.clientTop||0:ae.clientLeft||0:0,fe=null!=(q=null==S?void 0:S[j])?q:0,ce=F+ie-fe,pe=de(m?a(X,F+oe-fe-se):X,F,m?i(Y,ce):Y);k[j]=pe,V[j]=pe-F}if(c){var ue,le="x"===j?D:P,he="x"===j?A:L,me=k[M],ve="y"===M?"height":"width",ye=me+b[le],ge=me-b[he],be=-1!==[D,P].indexOf(x),xe=null!=(ue=null==S?void 0:S[M])?ue:0,we=be?ye:me-B[ve]-H[ve]-xe+R.altAxis,Oe=be?me+B[ve]+H[ve]-xe-R.altAxis:ge,je=m&&be?function(e,t,n){var r=de(e,t,n);return r>n?n:r}(we,me,Oe):de(m?we:ye,me,m?Oe:ge);k[M]=je,V[M]=je-me}t.modifiersData[r]=V}},requiresIfExists:["offset"]};var me={name:"arrow",enabled:!0,phase:"main",fn:function(e){var t,n=e.state,r=e.name,o=e.options,i=n.elements.arrow,a=n.modifiersData.popperOffsets,s=C(n.placement),f=z(s),c=[P,L].indexOf(s)>=0?"height":"width";if(i&&a){var p=function(e,t){return Y("number"!=typeof(e="function"==typeof e?e(Object.assign({},t.rects,{placement:t.placement})):e)?e:G(e,k))}(o.padding,n),u=g(i),l="y"===f?D:P,d="y"===f?A:L,h=n.rects.reference[c]+n.rects.reference[f]-a[f]-n.rects.popper[c],m=a[f]-n.rects.reference[f],v=E(i),y=v?"y"===f?v.clientHeight||0:v.clientWidth||0:0,b=h/2-m/2,x=p[l],w=y-u[c]-p[d],O=y/2-u[c]/2+b,j=de(x,O,w),M=f;n.modifiersData[r]=((t={})[M]=j,t.centerOffset=j-O,t)}},effect:function(e){var t=e.state,n=e.options.element,r=void 0===n?"[data-popper-arrow]":n;null!=r&&("string"!=typeof r||(r=t.elements.popper.querySelector(r)))&&N(t.elements.popper,r)&&(t.elements.arrow=r)},requires:["popperOffsets"],requiresIfExists:["preventOverflow"]};function ve(e,t,n){return void 0===n&&(n={x:0,y:0}),{top:e.top-t.height-n.y,right:e.right-t.width+n.x,bottom:e.bottom-t.height+n.y,left:e.left-t.width-n.x}}function ye(e){return[D,L,A,P].some((function(t){return e[t]>=0}))}var ge={name:"hide",enabled:!0,phase:"main",requiresIfExists:["preventOverflow"],fn:function(e){var t=e.state,n=e.name,r=t.rects.reference,o=t.rects.popper,i=t.modifiersData.preventOverflow,a=J(t,{elementContext:"reference"}),s=J(t,{altBoundary:!0}),f=ve(a,r),c=ve(s,o,i),p=ye(f),u=ye(c);t.modifiersData[n]={referenceClippingOffsets:f,popperEscapeOffsets:c,isReferenceHidden:p,hasPopperEscaped:u},t.attributes.popper=Object.assign({},t.attributes.popper,{"data-popper-reference-hidden":p,"data-popper-escaped":u})}},be=Z({defaultModifiers:[ee,te,oe,ie]}),xe=[ee,te,oe,ie,ae,le,he,me,ge],we=Z({defaultModifiers:xe});e.applyStyles=ie,e.arrow=me,e.computeStyles=oe,e.createPopper=we,e.createPopperLite=be,e.defaultModifiers=xe,e.detectOverflow=J,e.eventListeners=ee,e.flip=le,e.hide=ge,e.offset=ae,e.popperGenerator=Z,e.popperOffsets=te,e.preventOverflow=he,Object.defineProperty(e,"__esModule",{value:!0})})); + diff --git a/site_libs/quarto-html/quarto-syntax-highlighting.css b/site_libs/quarto-html/quarto-syntax-highlighting.css new file mode 100644 index 00000000..b30ce576 --- /dev/null +++ b/site_libs/quarto-html/quarto-syntax-highlighting.css @@ -0,0 +1,205 @@ +/* quarto syntax highlight colors */ +:root { + --quarto-hl-ot-color: #003B4F; + --quarto-hl-at-color: #657422; + --quarto-hl-ss-color: #20794D; + --quarto-hl-an-color: #5E5E5E; + --quarto-hl-fu-color: #4758AB; + --quarto-hl-st-color: #20794D; + --quarto-hl-cf-color: #003B4F; + --quarto-hl-op-color: #5E5E5E; + --quarto-hl-er-color: #AD0000; + --quarto-hl-bn-color: #AD0000; + --quarto-hl-al-color: #AD0000; + --quarto-hl-va-color: #111111; + --quarto-hl-bu-color: inherit; + --quarto-hl-ex-color: inherit; + --quarto-hl-pp-color: #AD0000; + --quarto-hl-in-color: #5E5E5E; + --quarto-hl-vs-color: #20794D; + --quarto-hl-wa-color: #5E5E5E; + --quarto-hl-do-color: #5E5E5E; + --quarto-hl-im-color: #00769E; + --quarto-hl-ch-color: #20794D; + --quarto-hl-dt-color: #AD0000; + --quarto-hl-fl-color: #AD0000; + --quarto-hl-co-color: #5E5E5E; + --quarto-hl-cv-color: #5E5E5E; + --quarto-hl-cn-color: #8f5902; + --quarto-hl-sc-color: #5E5E5E; + --quarto-hl-dv-color: #AD0000; + --quarto-hl-kw-color: #003B4F; +} + +/* other quarto variables */ +:root { + --quarto-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; +} + +pre > code.sourceCode > span { + color: #003B4F; +} + +code span { + color: #003B4F; +} + +code.sourceCode > span { + color: #003B4F; +} + +div.sourceCode, +div.sourceCode pre.sourceCode { + color: #003B4F; +} + +code span.ot { + color: #003B4F; + font-style: inherit; +} + +code span.at { + color: #657422; + font-style: inherit; +} + +code span.ss { + color: #20794D; + font-style: inherit; +} + +code span.an { + color: #5E5E5E; + font-style: inherit; +} + +code span.fu { + color: #4758AB; + font-style: inherit; +} + +code span.st { + color: #20794D; + font-style: inherit; +} + +code span.cf { + color: #003B4F; + font-weight: bold; + font-style: inherit; +} + +code span.op { + color: #5E5E5E; + font-style: inherit; +} + +code span.er { + color: #AD0000; + font-style: inherit; +} + +code span.bn { + color: #AD0000; + font-style: inherit; +} + +code span.al { + color: #AD0000; + font-style: inherit; +} + +code span.va { + color: #111111; + font-style: inherit; +} + +code span.bu { + font-style: inherit; +} + +code span.ex { + font-style: inherit; +} + +code span.pp { + color: #AD0000; + font-style: inherit; +} + +code span.in { + color: #5E5E5E; + font-style: inherit; +} + +code span.vs { + color: #20794D; + font-style: inherit; +} + +code span.wa { + color: #5E5E5E; + font-style: italic; +} + +code span.do { + color: #5E5E5E; + font-style: italic; +} + +code span.im { + color: #00769E; + font-style: inherit; +} + +code span.ch { + color: #20794D; + font-style: inherit; +} + +code span.dt { + color: #AD0000; + font-style: inherit; +} + +code span.fl { + color: #AD0000; + font-style: inherit; +} + +code span.co { + color: #5E5E5E; + font-style: inherit; +} + +code span.cv { + color: #5E5E5E; + font-style: italic; +} + +code span.cn { + color: #8f5902; + font-style: inherit; +} + +code span.sc { + color: #5E5E5E; + font-style: inherit; +} + +code span.dv { + color: #AD0000; + font-style: inherit; +} + +code span.kw { + color: #003B4F; + font-weight: bold; + font-style: inherit; +} + +.prevent-inlining { + content: " { + // Find any conflicting margin elements and add margins to the + // top to prevent overlap + const marginChildren = window.document.querySelectorAll( + ".column-margin.column-container > *, .margin-caption, .aside" + ); + + let lastBottom = 0; + for (const marginChild of marginChildren) { + if (marginChild.offsetParent !== null) { + // clear the top margin so we recompute it + marginChild.style.marginTop = null; + const top = marginChild.getBoundingClientRect().top + window.scrollY; + if (top < lastBottom) { + const marginChildStyle = window.getComputedStyle(marginChild); + const marginBottom = parseFloat(marginChildStyle["marginBottom"]); + const margin = lastBottom - top + marginBottom; + marginChild.style.marginTop = `${margin}px`; + } + const styles = window.getComputedStyle(marginChild); + const marginTop = parseFloat(styles["marginTop"]); + lastBottom = top + marginChild.getBoundingClientRect().height + marginTop; + } + } +}; + +window.document.addEventListener("DOMContentLoaded", function (_event) { + // Recompute the position of margin elements anytime the body size changes + if (window.ResizeObserver) { + const resizeObserver = new window.ResizeObserver( + throttle(() => { + layoutMarginEls(); + if ( + window.document.body.getBoundingClientRect().width < 990 && + isReaderMode() + ) { + quartoToggleReader(); + } + }, 50) + ); + resizeObserver.observe(window.document.body); + } + + const tocEl = window.document.querySelector('nav.toc-active[role="doc-toc"]'); + const sidebarEl = window.document.getElementById("quarto-sidebar"); + const leftTocEl = window.document.getElementById("quarto-sidebar-toc-left"); + const marginSidebarEl = window.document.getElementById( + "quarto-margin-sidebar" + ); + // function to determine whether the element has a previous sibling that is active + const prevSiblingIsActiveLink = (el) => { + const sibling = el.previousElementSibling; + if (sibling && sibling.tagName === "A") { + return sibling.classList.contains("active"); + } else { + return false; + } + }; + + // fire slideEnter for bootstrap tab activations (for htmlwidget resize behavior) + function fireSlideEnter(e) { + const event = window.document.createEvent("Event"); + event.initEvent("slideenter", true, true); + window.document.dispatchEvent(event); + } + const tabs = window.document.querySelectorAll('a[data-bs-toggle="tab"]'); + tabs.forEach((tab) => { + tab.addEventListener("shown.bs.tab", fireSlideEnter); + }); + + // fire slideEnter for tabby tab activations (for htmlwidget resize behavior) + document.addEventListener("tabby", fireSlideEnter, false); + + // Track scrolling and mark TOC links as active + // get table of contents and sidebar (bail if we don't have at least one) + const tocLinks = tocEl + ? [...tocEl.querySelectorAll("a[data-scroll-target]")] + : []; + const makeActive = (link) => tocLinks[link].classList.add("active"); + const removeActive = (link) => tocLinks[link].classList.remove("active"); + const removeAllActive = () => + [...Array(tocLinks.length).keys()].forEach((link) => removeActive(link)); + + // activate the anchor for a section associated with this TOC entry + tocLinks.forEach((link) => { + link.addEventListener("click", () => { + if (link.href.indexOf("#") !== -1) { + const anchor = link.href.split("#")[1]; + const heading = window.document.querySelector( + `[data-anchor-id="${anchor}"]` + ); + if (heading) { + // Add the class + heading.classList.add("reveal-anchorjs-link"); + + // function to show the anchor + const handleMouseout = () => { + heading.classList.remove("reveal-anchorjs-link"); + heading.removeEventListener("mouseout", handleMouseout); + }; + + // add a function to clear the anchor when the user mouses out of it + heading.addEventListener("mouseout", handleMouseout); + } + } + }); + }); + + const sections = tocLinks.map((link) => { + const target = link.getAttribute("data-scroll-target"); + if (target.startsWith("#")) { + return window.document.getElementById(decodeURI(`${target.slice(1)}`)); + } else { + return window.document.querySelector(decodeURI(`${target}`)); + } + }); + + const sectionMargin = 200; + let currentActive = 0; + // track whether we've initialized state the first time + let init = false; + + const updateActiveLink = () => { + // The index from bottom to top (e.g. reversed list) + let sectionIndex = -1; + if ( + window.innerHeight + window.pageYOffset >= + window.document.body.offsetHeight + ) { + // This is the no-scroll case where last section should be the active one + sectionIndex = 0; + } else { + // This finds the last section visible on screen that should be made active + sectionIndex = [...sections].reverse().findIndex((section) => { + if (section) { + return window.pageYOffset >= section.offsetTop - sectionMargin; + } else { + return false; + } + }); + } + if (sectionIndex > -1) { + const current = sections.length - sectionIndex - 1; + if (current !== currentActive) { + removeAllActive(); + currentActive = current; + makeActive(current); + if (init) { + window.dispatchEvent(sectionChanged); + } + init = true; + } + } + }; + + const inHiddenRegion = (top, bottom, hiddenRegions) => { + for (const region of hiddenRegions) { + if (top <= region.bottom && bottom >= region.top) { + return true; + } + } + return false; + }; + + const categorySelector = "header.quarto-title-block .quarto-category"; + const activateCategories = (href) => { + // Find any categories + // Surround them with a link pointing back to: + // #category=Authoring + try { + const categoryEls = window.document.querySelectorAll(categorySelector); + for (const categoryEl of categoryEls) { + const categoryText = categoryEl.textContent; + if (categoryText) { + const link = `${href}#category=${encodeURIComponent(categoryText)}`; + const linkEl = window.document.createElement("a"); + linkEl.setAttribute("href", link); + for (const child of categoryEl.childNodes) { + linkEl.append(child); + } + categoryEl.appendChild(linkEl); + } + } + } catch { + // Ignore errors + } + }; + function hasTitleCategories() { + return window.document.querySelector(categorySelector) !== null; + } + + function offsetRelativeUrl(url) { + const offset = getMeta("quarto:offset"); + return offset ? offset + url : url; + } + + function offsetAbsoluteUrl(url) { + const offset = getMeta("quarto:offset"); + const baseUrl = new URL(offset, window.location); + + const projRelativeUrl = url.replace(baseUrl, ""); + if (projRelativeUrl.startsWith("/")) { + return projRelativeUrl; + } else { + return "/" + projRelativeUrl; + } + } + + // read a meta tag value + function getMeta(metaName) { + const metas = window.document.getElementsByTagName("meta"); + for (let i = 0; i < metas.length; i++) { + if (metas[i].getAttribute("name") === metaName) { + return metas[i].getAttribute("content"); + } + } + return ""; + } + + async function findAndActivateCategories() { + const currentPagePath = offsetAbsoluteUrl(window.location.href); + const response = await fetch(offsetRelativeUrl("listings.json")); + if (response.status == 200) { + return response.json().then(function (listingPaths) { + const listingHrefs = []; + for (const listingPath of listingPaths) { + const pathWithoutLeadingSlash = listingPath.listing.substring(1); + for (const item of listingPath.items) { + if ( + item === currentPagePath || + item === currentPagePath + "index.html" + ) { + // Resolve this path against the offset to be sure + // we already are using the correct path to the listing + // (this adjusts the listing urls to be rooted against + // whatever root the page is actually running against) + const relative = offsetRelativeUrl(pathWithoutLeadingSlash); + const baseUrl = window.location; + const resolvedPath = new URL(relative, baseUrl); + listingHrefs.push(resolvedPath.pathname); + break; + } + } + } + + // Look up the tree for a nearby linting and use that if we find one + const nearestListing = findNearestParentListing( + offsetAbsoluteUrl(window.location.pathname), + listingHrefs + ); + if (nearestListing) { + activateCategories(nearestListing); + } else { + // See if the referrer is a listing page for this item + const referredRelativePath = offsetAbsoluteUrl(document.referrer); + const referrerListing = listingHrefs.find((listingHref) => { + const isListingReferrer = + listingHref === referredRelativePath || + listingHref === referredRelativePath + "index.html"; + return isListingReferrer; + }); + + if (referrerListing) { + // Try to use the referrer if possible + activateCategories(referrerListing); + } else if (listingHrefs.length > 0) { + // Otherwise, just fall back to the first listing + activateCategories(listingHrefs[0]); + } + } + }); + } + } + if (hasTitleCategories()) { + findAndActivateCategories(); + } + + const findNearestParentListing = (href, listingHrefs) => { + if (!href || !listingHrefs) { + return undefined; + } + // Look up the tree for a nearby linting and use that if we find one + const relativeParts = href.substring(1).split("/"); + while (relativeParts.length > 0) { + const path = relativeParts.join("/"); + for (const listingHref of listingHrefs) { + if (listingHref.startsWith(path)) { + return listingHref; + } + } + relativeParts.pop(); + } + + return undefined; + }; + + const manageSidebarVisiblity = (el, placeholderDescriptor) => { + let isVisible = true; + let elRect; + + return (hiddenRegions) => { + if (el === null) { + return; + } + + // Find the last element of the TOC + const lastChildEl = el.lastElementChild; + + if (lastChildEl) { + // Converts the sidebar to a menu + const convertToMenu = () => { + for (const child of el.children) { + child.style.opacity = 0; + child.style.overflow = "hidden"; + child.style.pointerEvents = "none"; + } + + nexttick(() => { + const toggleContainer = window.document.createElement("div"); + toggleContainer.style.width = "100%"; + toggleContainer.classList.add("zindex-over-content"); + toggleContainer.classList.add("quarto-sidebar-toggle"); + toggleContainer.classList.add("headroom-target"); // Marks this to be managed by headeroom + toggleContainer.id = placeholderDescriptor.id; + toggleContainer.style.position = "fixed"; + + const toggleIcon = window.document.createElement("i"); + toggleIcon.classList.add("quarto-sidebar-toggle-icon"); + toggleIcon.classList.add("bi"); + toggleIcon.classList.add("bi-caret-down-fill"); + + const toggleTitle = window.document.createElement("div"); + const titleEl = window.document.body.querySelector( + placeholderDescriptor.titleSelector + ); + if (titleEl) { + toggleTitle.append( + titleEl.textContent || titleEl.innerText, + toggleIcon + ); + } + toggleTitle.classList.add("zindex-over-content"); + toggleTitle.classList.add("quarto-sidebar-toggle-title"); + toggleContainer.append(toggleTitle); + + const toggleContents = window.document.createElement("div"); + toggleContents.classList = el.classList; + toggleContents.classList.add("zindex-over-content"); + toggleContents.classList.add("quarto-sidebar-toggle-contents"); + for (const child of el.children) { + if (child.id === "toc-title") { + continue; + } + + const clone = child.cloneNode(true); + clone.style.opacity = 1; + clone.style.pointerEvents = null; + clone.style.display = null; + toggleContents.append(clone); + } + toggleContents.style.height = "0px"; + const positionToggle = () => { + // position the element (top left of parent, same width as parent) + if (!elRect) { + elRect = el.getBoundingClientRect(); + } + toggleContainer.style.left = `${elRect.left}px`; + toggleContainer.style.top = `${elRect.top}px`; + toggleContainer.style.width = `${elRect.width}px`; + }; + positionToggle(); + + toggleContainer.append(toggleContents); + el.parentElement.prepend(toggleContainer); + + // Process clicks + let tocShowing = false; + // Allow the caller to control whether this is dismissed + // when it is clicked (e.g. sidebar navigation supports + // opening and closing the nav tree, so don't dismiss on click) + const clickEl = placeholderDescriptor.dismissOnClick + ? toggleContainer + : toggleTitle; + + const closeToggle = () => { + if (tocShowing) { + toggleContainer.classList.remove("expanded"); + toggleContents.style.height = "0px"; + tocShowing = false; + } + }; + + // Get rid of any expanded toggle if the user scrolls + window.document.addEventListener( + "scroll", + throttle(() => { + closeToggle(); + }, 50) + ); + + // Handle positioning of the toggle + window.addEventListener( + "resize", + throttle(() => { + elRect = undefined; + positionToggle(); + }, 50) + ); + + window.addEventListener("quarto-hrChanged", () => { + elRect = undefined; + }); + + // Process the click + clickEl.onclick = () => { + if (!tocShowing) { + toggleContainer.classList.add("expanded"); + toggleContents.style.height = null; + tocShowing = true; + } else { + closeToggle(); + } + }; + }); + }; + + // Converts a sidebar from a menu back to a sidebar + const convertToSidebar = () => { + for (const child of el.children) { + child.style.opacity = 1; + child.style.overflow = null; + child.style.pointerEvents = null; + } + + const placeholderEl = window.document.getElementById( + placeholderDescriptor.id + ); + if (placeholderEl) { + placeholderEl.remove(); + } + + el.classList.remove("rollup"); + }; + + if (isReaderMode()) { + convertToMenu(); + isVisible = false; + } else { + // Find the top and bottom o the element that is being managed + const elTop = el.offsetTop; + const elBottom = + elTop + lastChildEl.offsetTop + lastChildEl.offsetHeight; + + if (!isVisible) { + // If the element is current not visible reveal if there are + // no conflicts with overlay regions + if (!inHiddenRegion(elTop, elBottom, hiddenRegions)) { + convertToSidebar(); + isVisible = true; + } + } else { + // If the element is visible, hide it if it conflicts with overlay regions + // and insert a placeholder toggle (or if we're in reader mode) + if (inHiddenRegion(elTop, elBottom, hiddenRegions)) { + convertToMenu(); + isVisible = false; + } + } + } + } + }; + }; + + const tabEls = document.querySelectorAll('a[data-bs-toggle="tab"]'); + for (const tabEl of tabEls) { + const id = tabEl.getAttribute("data-bs-target"); + if (id) { + const columnEl = document.querySelector( + `${id} .column-margin, .tabset-margin-content` + ); + if (columnEl) + tabEl.addEventListener("shown.bs.tab", function (event) { + const el = event.srcElement; + if (el) { + const visibleCls = `${el.id}-margin-content`; + // walk up until we find a parent tabset + let panelTabsetEl = el.parentElement; + while (panelTabsetEl) { + if (panelTabsetEl.classList.contains("panel-tabset")) { + break; + } + panelTabsetEl = panelTabsetEl.parentElement; + } + + if (panelTabsetEl) { + const prevSib = panelTabsetEl.previousElementSibling; + if ( + prevSib && + prevSib.classList.contains("tabset-margin-container") + ) { + const childNodes = prevSib.querySelectorAll( + ".tabset-margin-content" + ); + for (const childEl of childNodes) { + if (childEl.classList.contains(visibleCls)) { + childEl.classList.remove("collapse"); + } else { + childEl.classList.add("collapse"); + } + } + } + } + } + + layoutMarginEls(); + }); + } + } + + // Manage the visibility of the toc and the sidebar + const marginScrollVisibility = manageSidebarVisiblity(marginSidebarEl, { + id: "quarto-toc-toggle", + titleSelector: "#toc-title", + dismissOnClick: true, + }); + const sidebarScrollVisiblity = manageSidebarVisiblity(sidebarEl, { + id: "quarto-sidebarnav-toggle", + titleSelector: ".title", + dismissOnClick: false, + }); + let tocLeftScrollVisibility; + if (leftTocEl) { + tocLeftScrollVisibility = manageSidebarVisiblity(leftTocEl, { + id: "quarto-lefttoc-toggle", + titleSelector: "#toc-title", + dismissOnClick: true, + }); + } + + // Find the first element that uses formatting in special columns + const conflictingEls = window.document.body.querySelectorAll( + '[class^="column-"], [class*=" column-"], aside, [class*="margin-caption"], [class*=" margin-caption"], [class*="margin-ref"], [class*=" margin-ref"]' + ); + + // Filter all the possibly conflicting elements into ones + // the do conflict on the left or ride side + const arrConflictingEls = Array.from(conflictingEls); + const leftSideConflictEls = arrConflictingEls.filter((el) => { + if (el.tagName === "ASIDE") { + return false; + } + return Array.from(el.classList).find((className) => { + return ( + className !== "column-body" && + className.startsWith("column-") && + !className.endsWith("right") && + !className.endsWith("container") && + className !== "column-margin" + ); + }); + }); + const rightSideConflictEls = arrConflictingEls.filter((el) => { + if (el.tagName === "ASIDE") { + return true; + } + + const hasMarginCaption = Array.from(el.classList).find((className) => { + return className == "margin-caption"; + }); + if (hasMarginCaption) { + return true; + } + + return Array.from(el.classList).find((className) => { + return ( + className !== "column-body" && + !className.endsWith("container") && + className.startsWith("column-") && + !className.endsWith("left") + ); + }); + }); + + const kOverlapPaddingSize = 10; + function toRegions(els) { + return els.map((el) => { + const boundRect = el.getBoundingClientRect(); + const top = + boundRect.top + + document.documentElement.scrollTop - + kOverlapPaddingSize; + return { + top, + bottom: top + el.scrollHeight + 2 * kOverlapPaddingSize, + }; + }); + } + + let hasObserved = false; + const visibleItemObserver = (els) => { + let visibleElements = [...els]; + const intersectionObserver = new IntersectionObserver( + (entries, _observer) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + if (visibleElements.indexOf(entry.target) === -1) { + visibleElements.push(entry.target); + } + } else { + visibleElements = visibleElements.filter((visibleEntry) => { + return visibleEntry !== entry; + }); + } + }); + + if (!hasObserved) { + hideOverlappedSidebars(); + } + hasObserved = true; + }, + {} + ); + els.forEach((el) => { + intersectionObserver.observe(el); + }); + + return { + getVisibleEntries: () => { + return visibleElements; + }, + }; + }; + + const rightElementObserver = visibleItemObserver(rightSideConflictEls); + const leftElementObserver = visibleItemObserver(leftSideConflictEls); + + const hideOverlappedSidebars = () => { + marginScrollVisibility(toRegions(rightElementObserver.getVisibleEntries())); + sidebarScrollVisiblity(toRegions(leftElementObserver.getVisibleEntries())); + if (tocLeftScrollVisibility) { + tocLeftScrollVisibility( + toRegions(leftElementObserver.getVisibleEntries()) + ); + } + }; + + window.quartoToggleReader = () => { + // Applies a slow class (or removes it) + // to update the transition speed + const slowTransition = (slow) => { + const manageTransition = (id, slow) => { + const el = document.getElementById(id); + if (el) { + if (slow) { + el.classList.add("slow"); + } else { + el.classList.remove("slow"); + } + } + }; + + manageTransition("TOC", slow); + manageTransition("quarto-sidebar", slow); + }; + const readerMode = !isReaderMode(); + setReaderModeValue(readerMode); + + // If we're entering reader mode, slow the transition + if (readerMode) { + slowTransition(readerMode); + } + highlightReaderToggle(readerMode); + hideOverlappedSidebars(); + + // If we're exiting reader mode, restore the non-slow transition + if (!readerMode) { + slowTransition(!readerMode); + } + }; + + const highlightReaderToggle = (readerMode) => { + const els = document.querySelectorAll(".quarto-reader-toggle"); + if (els) { + els.forEach((el) => { + if (readerMode) { + el.classList.add("reader"); + } else { + el.classList.remove("reader"); + } + }); + } + }; + + const setReaderModeValue = (val) => { + if (window.location.protocol !== "file:") { + window.localStorage.setItem("quarto-reader-mode", val); + } else { + localReaderMode = val; + } + }; + + const isReaderMode = () => { + if (window.location.protocol !== "file:") { + return window.localStorage.getItem("quarto-reader-mode") === "true"; + } else { + return localReaderMode; + } + }; + let localReaderMode = null; + + const tocOpenDepthStr = tocEl?.getAttribute("data-toc-expanded"); + const tocOpenDepth = tocOpenDepthStr ? Number(tocOpenDepthStr) : 1; + + // Walk the TOC and collapse/expand nodes + // Nodes are expanded if: + // - they are top level + // - they have children that are 'active' links + // - they are directly below an link that is 'active' + const walk = (el, depth) => { + // Tick depth when we enter a UL + if (el.tagName === "UL") { + depth = depth + 1; + } + + // It this is active link + let isActiveNode = false; + if (el.tagName === "A" && el.classList.contains("active")) { + isActiveNode = true; + } + + // See if there is an active child to this element + let hasActiveChild = false; + for (child of el.children) { + hasActiveChild = walk(child, depth) || hasActiveChild; + } + + // Process the collapse state if this is an UL + if (el.tagName === "UL") { + if (tocOpenDepth === -1 && depth > 1) { + // toc-expand: false + el.classList.add("collapse"); + } else if ( + depth <= tocOpenDepth || + hasActiveChild || + prevSiblingIsActiveLink(el) + ) { + el.classList.remove("collapse"); + } else { + el.classList.add("collapse"); + } + + // untick depth when we leave a UL + depth = depth - 1; + } + return hasActiveChild || isActiveNode; + }; + + // walk the TOC and expand / collapse any items that should be shown + if (tocEl) { + updateActiveLink(); + walk(tocEl, 0); + } + + // Throttle the scroll event and walk peridiocally + window.document.addEventListener( + "scroll", + throttle(() => { + if (tocEl) { + updateActiveLink(); + walk(tocEl, 0); + } + if (!isReaderMode()) { + hideOverlappedSidebars(); + } + }, 5) + ); + window.addEventListener( + "resize", + throttle(() => { + if (tocEl) { + updateActiveLink(); + walk(tocEl, 0); + } + if (!isReaderMode()) { + hideOverlappedSidebars(); + } + }, 10) + ); + hideOverlappedSidebars(); + highlightReaderToggle(isReaderMode()); +}); + +// grouped tabsets +window.addEventListener("pageshow", (_event) => { + function getTabSettings() { + const data = localStorage.getItem("quarto-persistent-tabsets-data"); + if (!data) { + localStorage.setItem("quarto-persistent-tabsets-data", "{}"); + return {}; + } + if (data) { + return JSON.parse(data); + } + } + + function setTabSettings(data) { + localStorage.setItem( + "quarto-persistent-tabsets-data", + JSON.stringify(data) + ); + } + + function setTabState(groupName, groupValue) { + const data = getTabSettings(); + data[groupName] = groupValue; + setTabSettings(data); + } + + function toggleTab(tab, active) { + const tabPanelId = tab.getAttribute("aria-controls"); + const tabPanel = document.getElementById(tabPanelId); + if (active) { + tab.classList.add("active"); + tabPanel.classList.add("active"); + } else { + tab.classList.remove("active"); + tabPanel.classList.remove("active"); + } + } + + function toggleAll(selectedGroup, selectorsToSync) { + for (const [thisGroup, tabs] of Object.entries(selectorsToSync)) { + const active = selectedGroup === thisGroup; + for (const tab of tabs) { + toggleTab(tab, active); + } + } + } + + function findSelectorsToSyncByLanguage() { + const result = {}; + const tabs = Array.from( + document.querySelectorAll(`div[data-group] a[id^='tabset-']`) + ); + for (const item of tabs) { + const div = item.parentElement.parentElement.parentElement; + const group = div.getAttribute("data-group"); + if (!result[group]) { + result[group] = {}; + } + const selectorsToSync = result[group]; + const value = item.innerHTML; + if (!selectorsToSync[value]) { + selectorsToSync[value] = []; + } + selectorsToSync[value].push(item); + } + return result; + } + + function setupSelectorSync() { + const selectorsToSync = findSelectorsToSyncByLanguage(); + Object.entries(selectorsToSync).forEach(([group, tabSetsByValue]) => { + Object.entries(tabSetsByValue).forEach(([value, items]) => { + items.forEach((item) => { + item.addEventListener("click", (_event) => { + setTabState(group, value); + toggleAll(value, selectorsToSync[group]); + }); + }); + }); + }); + return selectorsToSync; + } + + const selectorsToSync = setupSelectorSync(); + for (const [group, selectedName] of Object.entries(getTabSettings())) { + const selectors = selectorsToSync[group]; + // it's possible that stale state gives us empty selections, so we explicitly check here. + if (selectors) { + toggleAll(selectedName, selectors); + } + } +}); + +function throttle(func, wait) { + let waiting = false; + return function () { + if (!waiting) { + func.apply(this, arguments); + waiting = true; + setTimeout(function () { + waiting = false; + }, wait); + } + }; +} + +function nexttick(func) { + return setTimeout(func, 0); +} diff --git a/site_libs/quarto-html/tippy.css b/site_libs/quarto-html/tippy.css new file mode 100644 index 00000000..e6ae635c --- /dev/null +++ b/site_libs/quarto-html/tippy.css @@ -0,0 +1 @@ +.tippy-box[data-animation=fade][data-state=hidden]{opacity:0}[data-tippy-root]{max-width:calc(100vw - 10px)}.tippy-box{position:relative;background-color:#333;color:#fff;border-radius:4px;font-size:14px;line-height:1.4;white-space:normal;outline:0;transition-property:transform,visibility,opacity}.tippy-box[data-placement^=top]>.tippy-arrow{bottom:0}.tippy-box[data-placement^=top]>.tippy-arrow:before{bottom:-7px;left:0;border-width:8px 8px 0;border-top-color:initial;transform-origin:center top}.tippy-box[data-placement^=bottom]>.tippy-arrow{top:0}.tippy-box[data-placement^=bottom]>.tippy-arrow:before{top:-7px;left:0;border-width:0 8px 8px;border-bottom-color:initial;transform-origin:center bottom}.tippy-box[data-placement^=left]>.tippy-arrow{right:0}.tippy-box[data-placement^=left]>.tippy-arrow:before{border-width:8px 0 8px 8px;border-left-color:initial;right:-7px;transform-origin:center left}.tippy-box[data-placement^=right]>.tippy-arrow{left:0}.tippy-box[data-placement^=right]>.tippy-arrow:before{left:-7px;border-width:8px 8px 8px 0;border-right-color:initial;transform-origin:center right}.tippy-box[data-inertia][data-state=visible]{transition-timing-function:cubic-bezier(.54,1.5,.38,1.11)}.tippy-arrow{width:16px;height:16px;color:#333}.tippy-arrow:before{content:"";position:absolute;border-color:transparent;border-style:solid}.tippy-content{position:relative;padding:5px 9px;z-index:1} \ No newline at end of file diff --git a/site_libs/quarto-html/tippy.umd.min.js b/site_libs/quarto-html/tippy.umd.min.js new file mode 100644 index 00000000..ca292be3 --- /dev/null +++ b/site_libs/quarto-html/tippy.umd.min.js @@ -0,0 +1,2 @@ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t(require("@popperjs/core")):"function"==typeof define&&define.amd?define(["@popperjs/core"],t):(e=e||self).tippy=t(e.Popper)}(this,(function(e){"use strict";var t={passive:!0,capture:!0},n=function(){return document.body};function r(e,t,n){if(Array.isArray(e)){var r=e[t];return null==r?Array.isArray(n)?n[t]:n:r}return e}function o(e,t){var n={}.toString.call(e);return 0===n.indexOf("[object")&&n.indexOf(t+"]")>-1}function i(e,t){return"function"==typeof e?e.apply(void 0,t):e}function a(e,t){return 0===t?e:function(r){clearTimeout(n),n=setTimeout((function(){e(r)}),t)};var n}function s(e,t){var n=Object.assign({},e);return t.forEach((function(e){delete n[e]})),n}function u(e){return[].concat(e)}function c(e,t){-1===e.indexOf(t)&&e.push(t)}function p(e){return e.split("-")[0]}function f(e){return[].slice.call(e)}function l(e){return Object.keys(e).reduce((function(t,n){return void 0!==e[n]&&(t[n]=e[n]),t}),{})}function d(){return document.createElement("div")}function v(e){return["Element","Fragment"].some((function(t){return o(e,t)}))}function m(e){return o(e,"MouseEvent")}function g(e){return!(!e||!e._tippy||e._tippy.reference!==e)}function h(e){return v(e)?[e]:function(e){return o(e,"NodeList")}(e)?f(e):Array.isArray(e)?e:f(document.querySelectorAll(e))}function b(e,t){e.forEach((function(e){e&&(e.style.transitionDuration=t+"ms")}))}function y(e,t){e.forEach((function(e){e&&e.setAttribute("data-state",t)}))}function w(e){var t,n=u(e)[0];return null!=n&&null!=(t=n.ownerDocument)&&t.body?n.ownerDocument:document}function E(e,t,n){var r=t+"EventListener";["transitionend","webkitTransitionEnd"].forEach((function(t){e[r](t,n)}))}function O(e,t){for(var n=t;n;){var r;if(e.contains(n))return!0;n=null==n.getRootNode||null==(r=n.getRootNode())?void 0:r.host}return!1}var x={isTouch:!1},C=0;function T(){x.isTouch||(x.isTouch=!0,window.performance&&document.addEventListener("mousemove",A))}function A(){var e=performance.now();e-C<20&&(x.isTouch=!1,document.removeEventListener("mousemove",A)),C=e}function L(){var e=document.activeElement;if(g(e)){var t=e._tippy;e.blur&&!t.state.isVisible&&e.blur()}}var D=!!("undefined"!=typeof window&&"undefined"!=typeof document)&&!!window.msCrypto,R=Object.assign({appendTo:n,aria:{content:"auto",expanded:"auto"},delay:0,duration:[300,250],getReferenceClientRect:null,hideOnClick:!0,ignoreAttributes:!1,interactive:!1,interactiveBorder:2,interactiveDebounce:0,moveTransition:"",offset:[0,10],onAfterUpdate:function(){},onBeforeUpdate:function(){},onCreate:function(){},onDestroy:function(){},onHidden:function(){},onHide:function(){},onMount:function(){},onShow:function(){},onShown:function(){},onTrigger:function(){},onUntrigger:function(){},onClickOutside:function(){},placement:"top",plugins:[],popperOptions:{},render:null,showOnCreate:!1,touch:!0,trigger:"mouseenter focus",triggerTarget:null},{animateFill:!1,followCursor:!1,inlinePositioning:!1,sticky:!1},{allowHTML:!1,animation:"fade",arrow:!0,content:"",inertia:!1,maxWidth:350,role:"tooltip",theme:"",zIndex:9999}),k=Object.keys(R);function P(e){var t=(e.plugins||[]).reduce((function(t,n){var r,o=n.name,i=n.defaultValue;o&&(t[o]=void 0!==e[o]?e[o]:null!=(r=R[o])?r:i);return t}),{});return Object.assign({},e,t)}function j(e,t){var n=Object.assign({},t,{content:i(t.content,[e])},t.ignoreAttributes?{}:function(e,t){return(t?Object.keys(P(Object.assign({},R,{plugins:t}))):k).reduce((function(t,n){var r=(e.getAttribute("data-tippy-"+n)||"").trim();if(!r)return t;if("content"===n)t[n]=r;else try{t[n]=JSON.parse(r)}catch(e){t[n]=r}return t}),{})}(e,t.plugins));return n.aria=Object.assign({},R.aria,n.aria),n.aria={expanded:"auto"===n.aria.expanded?t.interactive:n.aria.expanded,content:"auto"===n.aria.content?t.interactive?null:"describedby":n.aria.content},n}function M(e,t){e.innerHTML=t}function V(e){var t=d();return!0===e?t.className="tippy-arrow":(t.className="tippy-svg-arrow",v(e)?t.appendChild(e):M(t,e)),t}function I(e,t){v(t.content)?(M(e,""),e.appendChild(t.content)):"function"!=typeof t.content&&(t.allowHTML?M(e,t.content):e.textContent=t.content)}function S(e){var t=e.firstElementChild,n=f(t.children);return{box:t,content:n.find((function(e){return e.classList.contains("tippy-content")})),arrow:n.find((function(e){return e.classList.contains("tippy-arrow")||e.classList.contains("tippy-svg-arrow")})),backdrop:n.find((function(e){return e.classList.contains("tippy-backdrop")}))}}function N(e){var t=d(),n=d();n.className="tippy-box",n.setAttribute("data-state","hidden"),n.setAttribute("tabindex","-1");var r=d();function o(n,r){var o=S(t),i=o.box,a=o.content,s=o.arrow;r.theme?i.setAttribute("data-theme",r.theme):i.removeAttribute("data-theme"),"string"==typeof r.animation?i.setAttribute("data-animation",r.animation):i.removeAttribute("data-animation"),r.inertia?i.setAttribute("data-inertia",""):i.removeAttribute("data-inertia"),i.style.maxWidth="number"==typeof r.maxWidth?r.maxWidth+"px":r.maxWidth,r.role?i.setAttribute("role",r.role):i.removeAttribute("role"),n.content===r.content&&n.allowHTML===r.allowHTML||I(a,e.props),r.arrow?s?n.arrow!==r.arrow&&(i.removeChild(s),i.appendChild(V(r.arrow))):i.appendChild(V(r.arrow)):s&&i.removeChild(s)}return r.className="tippy-content",r.setAttribute("data-state","hidden"),I(r,e.props),t.appendChild(n),n.appendChild(r),o(e.props,e.props),{popper:t,onUpdate:o}}N.$$tippy=!0;var B=1,H=[],U=[];function _(o,s){var v,g,h,C,T,A,L,k,M=j(o,Object.assign({},R,P(l(s)))),V=!1,I=!1,N=!1,_=!1,F=[],W=a(we,M.interactiveDebounce),X=B++,Y=(k=M.plugins).filter((function(e,t){return k.indexOf(e)===t})),$={id:X,reference:o,popper:d(),popperInstance:null,props:M,state:{isEnabled:!0,isVisible:!1,isDestroyed:!1,isMounted:!1,isShown:!1},plugins:Y,clearDelayTimeouts:function(){clearTimeout(v),clearTimeout(g),cancelAnimationFrame(h)},setProps:function(e){if($.state.isDestroyed)return;ae("onBeforeUpdate",[$,e]),be();var t=$.props,n=j(o,Object.assign({},t,l(e),{ignoreAttributes:!0}));$.props=n,he(),t.interactiveDebounce!==n.interactiveDebounce&&(ce(),W=a(we,n.interactiveDebounce));t.triggerTarget&&!n.triggerTarget?u(t.triggerTarget).forEach((function(e){e.removeAttribute("aria-expanded")})):n.triggerTarget&&o.removeAttribute("aria-expanded");ue(),ie(),J&&J(t,n);$.popperInstance&&(Ce(),Ae().forEach((function(e){requestAnimationFrame(e._tippy.popperInstance.forceUpdate)})));ae("onAfterUpdate",[$,e])},setContent:function(e){$.setProps({content:e})},show:function(){var e=$.state.isVisible,t=$.state.isDestroyed,o=!$.state.isEnabled,a=x.isTouch&&!$.props.touch,s=r($.props.duration,0,R.duration);if(e||t||o||a)return;if(te().hasAttribute("disabled"))return;if(ae("onShow",[$],!1),!1===$.props.onShow($))return;$.state.isVisible=!0,ee()&&(z.style.visibility="visible");ie(),de(),$.state.isMounted||(z.style.transition="none");if(ee()){var u=re(),p=u.box,f=u.content;b([p,f],0)}A=function(){var e;if($.state.isVisible&&!_){if(_=!0,z.offsetHeight,z.style.transition=$.props.moveTransition,ee()&&$.props.animation){var t=re(),n=t.box,r=t.content;b([n,r],s),y([n,r],"visible")}se(),ue(),c(U,$),null==(e=$.popperInstance)||e.forceUpdate(),ae("onMount",[$]),$.props.animation&&ee()&&function(e,t){me(e,t)}(s,(function(){$.state.isShown=!0,ae("onShown",[$])}))}},function(){var e,t=$.props.appendTo,r=te();e=$.props.interactive&&t===n||"parent"===t?r.parentNode:i(t,[r]);e.contains(z)||e.appendChild(z);$.state.isMounted=!0,Ce()}()},hide:function(){var e=!$.state.isVisible,t=$.state.isDestroyed,n=!$.state.isEnabled,o=r($.props.duration,1,R.duration);if(e||t||n)return;if(ae("onHide",[$],!1),!1===$.props.onHide($))return;$.state.isVisible=!1,$.state.isShown=!1,_=!1,V=!1,ee()&&(z.style.visibility="hidden");if(ce(),ve(),ie(!0),ee()){var i=re(),a=i.box,s=i.content;$.props.animation&&(b([a,s],o),y([a,s],"hidden"))}se(),ue(),$.props.animation?ee()&&function(e,t){me(e,(function(){!$.state.isVisible&&z.parentNode&&z.parentNode.contains(z)&&t()}))}(o,$.unmount):$.unmount()},hideWithInteractivity:function(e){ne().addEventListener("mousemove",W),c(H,W),W(e)},enable:function(){$.state.isEnabled=!0},disable:function(){$.hide(),$.state.isEnabled=!1},unmount:function(){$.state.isVisible&&$.hide();if(!$.state.isMounted)return;Te(),Ae().forEach((function(e){e._tippy.unmount()})),z.parentNode&&z.parentNode.removeChild(z);U=U.filter((function(e){return e!==$})),$.state.isMounted=!1,ae("onHidden",[$])},destroy:function(){if($.state.isDestroyed)return;$.clearDelayTimeouts(),$.unmount(),be(),delete o._tippy,$.state.isDestroyed=!0,ae("onDestroy",[$])}};if(!M.render)return $;var q=M.render($),z=q.popper,J=q.onUpdate;z.setAttribute("data-tippy-root",""),z.id="tippy-"+$.id,$.popper=z,o._tippy=$,z._tippy=$;var G=Y.map((function(e){return e.fn($)})),K=o.hasAttribute("aria-expanded");return he(),ue(),ie(),ae("onCreate",[$]),M.showOnCreate&&Le(),z.addEventListener("mouseenter",(function(){$.props.interactive&&$.state.isVisible&&$.clearDelayTimeouts()})),z.addEventListener("mouseleave",(function(){$.props.interactive&&$.props.trigger.indexOf("mouseenter")>=0&&ne().addEventListener("mousemove",W)})),$;function Q(){var e=$.props.touch;return Array.isArray(e)?e:[e,0]}function Z(){return"hold"===Q()[0]}function ee(){var e;return!(null==(e=$.props.render)||!e.$$tippy)}function te(){return L||o}function ne(){var e=te().parentNode;return e?w(e):document}function re(){return S(z)}function oe(e){return $.state.isMounted&&!$.state.isVisible||x.isTouch||C&&"focus"===C.type?0:r($.props.delay,e?0:1,R.delay)}function ie(e){void 0===e&&(e=!1),z.style.pointerEvents=$.props.interactive&&!e?"":"none",z.style.zIndex=""+$.props.zIndex}function ae(e,t,n){var r;(void 0===n&&(n=!0),G.forEach((function(n){n[e]&&n[e].apply(n,t)})),n)&&(r=$.props)[e].apply(r,t)}function se(){var e=$.props.aria;if(e.content){var t="aria-"+e.content,n=z.id;u($.props.triggerTarget||o).forEach((function(e){var r=e.getAttribute(t);if($.state.isVisible)e.setAttribute(t,r?r+" "+n:n);else{var o=r&&r.replace(n,"").trim();o?e.setAttribute(t,o):e.removeAttribute(t)}}))}}function ue(){!K&&$.props.aria.expanded&&u($.props.triggerTarget||o).forEach((function(e){$.props.interactive?e.setAttribute("aria-expanded",$.state.isVisible&&e===te()?"true":"false"):e.removeAttribute("aria-expanded")}))}function ce(){ne().removeEventListener("mousemove",W),H=H.filter((function(e){return e!==W}))}function pe(e){if(!x.isTouch||!N&&"mousedown"!==e.type){var t=e.composedPath&&e.composedPath()[0]||e.target;if(!$.props.interactive||!O(z,t)){if(u($.props.triggerTarget||o).some((function(e){return O(e,t)}))){if(x.isTouch)return;if($.state.isVisible&&$.props.trigger.indexOf("click")>=0)return}else ae("onClickOutside",[$,e]);!0===$.props.hideOnClick&&($.clearDelayTimeouts(),$.hide(),I=!0,setTimeout((function(){I=!1})),$.state.isMounted||ve())}}}function fe(){N=!0}function le(){N=!1}function de(){var e=ne();e.addEventListener("mousedown",pe,!0),e.addEventListener("touchend",pe,t),e.addEventListener("touchstart",le,t),e.addEventListener("touchmove",fe,t)}function ve(){var e=ne();e.removeEventListener("mousedown",pe,!0),e.removeEventListener("touchend",pe,t),e.removeEventListener("touchstart",le,t),e.removeEventListener("touchmove",fe,t)}function me(e,t){var n=re().box;function r(e){e.target===n&&(E(n,"remove",r),t())}if(0===e)return t();E(n,"remove",T),E(n,"add",r),T=r}function ge(e,t,n){void 0===n&&(n=!1),u($.props.triggerTarget||o).forEach((function(r){r.addEventListener(e,t,n),F.push({node:r,eventType:e,handler:t,options:n})}))}function he(){var e;Z()&&(ge("touchstart",ye,{passive:!0}),ge("touchend",Ee,{passive:!0})),(e=$.props.trigger,e.split(/\s+/).filter(Boolean)).forEach((function(e){if("manual"!==e)switch(ge(e,ye),e){case"mouseenter":ge("mouseleave",Ee);break;case"focus":ge(D?"focusout":"blur",Oe);break;case"focusin":ge("focusout",Oe)}}))}function be(){F.forEach((function(e){var t=e.node,n=e.eventType,r=e.handler,o=e.options;t.removeEventListener(n,r,o)})),F=[]}function ye(e){var t,n=!1;if($.state.isEnabled&&!xe(e)&&!I){var r="focus"===(null==(t=C)?void 0:t.type);C=e,L=e.currentTarget,ue(),!$.state.isVisible&&m(e)&&H.forEach((function(t){return t(e)})),"click"===e.type&&($.props.trigger.indexOf("mouseenter")<0||V)&&!1!==$.props.hideOnClick&&$.state.isVisible?n=!0:Le(e),"click"===e.type&&(V=!n),n&&!r&&De(e)}}function we(e){var t=e.target,n=te().contains(t)||z.contains(t);"mousemove"===e.type&&n||function(e,t){var n=t.clientX,r=t.clientY;return e.every((function(e){var t=e.popperRect,o=e.popperState,i=e.props.interactiveBorder,a=p(o.placement),s=o.modifiersData.offset;if(!s)return!0;var u="bottom"===a?s.top.y:0,c="top"===a?s.bottom.y:0,f="right"===a?s.left.x:0,l="left"===a?s.right.x:0,d=t.top-r+u>i,v=r-t.bottom-c>i,m=t.left-n+f>i,g=n-t.right-l>i;return d||v||m||g}))}(Ae().concat(z).map((function(e){var t,n=null==(t=e._tippy.popperInstance)?void 0:t.state;return n?{popperRect:e.getBoundingClientRect(),popperState:n,props:M}:null})).filter(Boolean),e)&&(ce(),De(e))}function Ee(e){xe(e)||$.props.trigger.indexOf("click")>=0&&V||($.props.interactive?$.hideWithInteractivity(e):De(e))}function Oe(e){$.props.trigger.indexOf("focusin")<0&&e.target!==te()||$.props.interactive&&e.relatedTarget&&z.contains(e.relatedTarget)||De(e)}function xe(e){return!!x.isTouch&&Z()!==e.type.indexOf("touch")>=0}function Ce(){Te();var t=$.props,n=t.popperOptions,r=t.placement,i=t.offset,a=t.getReferenceClientRect,s=t.moveTransition,u=ee()?S(z).arrow:null,c=a?{getBoundingClientRect:a,contextElement:a.contextElement||te()}:o,p=[{name:"offset",options:{offset:i}},{name:"preventOverflow",options:{padding:{top:2,bottom:2,left:5,right:5}}},{name:"flip",options:{padding:5}},{name:"computeStyles",options:{adaptive:!s}},{name:"$$tippy",enabled:!0,phase:"beforeWrite",requires:["computeStyles"],fn:function(e){var t=e.state;if(ee()){var n=re().box;["placement","reference-hidden","escaped"].forEach((function(e){"placement"===e?n.setAttribute("data-placement",t.placement):t.attributes.popper["data-popper-"+e]?n.setAttribute("data-"+e,""):n.removeAttribute("data-"+e)})),t.attributes.popper={}}}}];ee()&&u&&p.push({name:"arrow",options:{element:u,padding:3}}),p.push.apply(p,(null==n?void 0:n.modifiers)||[]),$.popperInstance=e.createPopper(c,z,Object.assign({},n,{placement:r,onFirstUpdate:A,modifiers:p}))}function Te(){$.popperInstance&&($.popperInstance.destroy(),$.popperInstance=null)}function Ae(){return f(z.querySelectorAll("[data-tippy-root]"))}function Le(e){$.clearDelayTimeouts(),e&&ae("onTrigger",[$,e]),de();var t=oe(!0),n=Q(),r=n[0],o=n[1];x.isTouch&&"hold"===r&&o&&(t=o),t?v=setTimeout((function(){$.show()}),t):$.show()}function De(e){if($.clearDelayTimeouts(),ae("onUntrigger",[$,e]),$.state.isVisible){if(!($.props.trigger.indexOf("mouseenter")>=0&&$.props.trigger.indexOf("click")>=0&&["mouseleave","mousemove"].indexOf(e.type)>=0&&V)){var t=oe(!1);t?g=setTimeout((function(){$.state.isVisible&&$.hide()}),t):h=requestAnimationFrame((function(){$.hide()}))}}else ve()}}function F(e,n){void 0===n&&(n={});var r=R.plugins.concat(n.plugins||[]);document.addEventListener("touchstart",T,t),window.addEventListener("blur",L);var o=Object.assign({},n,{plugins:r}),i=h(e).reduce((function(e,t){var n=t&&_(t,o);return n&&e.push(n),e}),[]);return v(e)?i[0]:i}F.defaultProps=R,F.setDefaultProps=function(e){Object.keys(e).forEach((function(t){R[t]=e[t]}))},F.currentInput=x;var W=Object.assign({},e.applyStyles,{effect:function(e){var t=e.state,n={popper:{position:t.options.strategy,left:"0",top:"0",margin:"0"},arrow:{position:"absolute"},reference:{}};Object.assign(t.elements.popper.style,n.popper),t.styles=n,t.elements.arrow&&Object.assign(t.elements.arrow.style,n.arrow)}}),X={mouseover:"mouseenter",focusin:"focus",click:"click"};var Y={name:"animateFill",defaultValue:!1,fn:function(e){var t;if(null==(t=e.props.render)||!t.$$tippy)return{};var n=S(e.popper),r=n.box,o=n.content,i=e.props.animateFill?function(){var e=d();return e.className="tippy-backdrop",y([e],"hidden"),e}():null;return{onCreate:function(){i&&(r.insertBefore(i,r.firstElementChild),r.setAttribute("data-animatefill",""),r.style.overflow="hidden",e.setProps({arrow:!1,animation:"shift-away"}))},onMount:function(){if(i){var e=r.style.transitionDuration,t=Number(e.replace("ms",""));o.style.transitionDelay=Math.round(t/10)+"ms",i.style.transitionDuration=e,y([i],"visible")}},onShow:function(){i&&(i.style.transitionDuration="0ms")},onHide:function(){i&&y([i],"hidden")}}}};var $={clientX:0,clientY:0},q=[];function z(e){var t=e.clientX,n=e.clientY;$={clientX:t,clientY:n}}var J={name:"followCursor",defaultValue:!1,fn:function(e){var t=e.reference,n=w(e.props.triggerTarget||t),r=!1,o=!1,i=!0,a=e.props;function s(){return"initial"===e.props.followCursor&&e.state.isVisible}function u(){n.addEventListener("mousemove",f)}function c(){n.removeEventListener("mousemove",f)}function p(){r=!0,e.setProps({getReferenceClientRect:null}),r=!1}function f(n){var r=!n.target||t.contains(n.target),o=e.props.followCursor,i=n.clientX,a=n.clientY,s=t.getBoundingClientRect(),u=i-s.left,c=a-s.top;!r&&e.props.interactive||e.setProps({getReferenceClientRect:function(){var e=t.getBoundingClientRect(),n=i,r=a;"initial"===o&&(n=e.left+u,r=e.top+c);var s="horizontal"===o?e.top:r,p="vertical"===o?e.right:n,f="horizontal"===o?e.bottom:r,l="vertical"===o?e.left:n;return{width:p-l,height:f-s,top:s,right:p,bottom:f,left:l}}})}function l(){e.props.followCursor&&(q.push({instance:e,doc:n}),function(e){e.addEventListener("mousemove",z)}(n))}function d(){0===(q=q.filter((function(t){return t.instance!==e}))).filter((function(e){return e.doc===n})).length&&function(e){e.removeEventListener("mousemove",z)}(n)}return{onCreate:l,onDestroy:d,onBeforeUpdate:function(){a=e.props},onAfterUpdate:function(t,n){var i=n.followCursor;r||void 0!==i&&a.followCursor!==i&&(d(),i?(l(),!e.state.isMounted||o||s()||u()):(c(),p()))},onMount:function(){e.props.followCursor&&!o&&(i&&(f($),i=!1),s()||u())},onTrigger:function(e,t){m(t)&&($={clientX:t.clientX,clientY:t.clientY}),o="focus"===t.type},onHidden:function(){e.props.followCursor&&(p(),c(),i=!0)}}}};var G={name:"inlinePositioning",defaultValue:!1,fn:function(e){var t,n=e.reference;var r=-1,o=!1,i=[],a={name:"tippyInlinePositioning",enabled:!0,phase:"afterWrite",fn:function(o){var a=o.state;e.props.inlinePositioning&&(-1!==i.indexOf(a.placement)&&(i=[]),t!==a.placement&&-1===i.indexOf(a.placement)&&(i.push(a.placement),e.setProps({getReferenceClientRect:function(){return function(e){return function(e,t,n,r){if(n.length<2||null===e)return t;if(2===n.length&&r>=0&&n[0].left>n[1].right)return n[r]||t;switch(e){case"top":case"bottom":var o=n[0],i=n[n.length-1],a="top"===e,s=o.top,u=i.bottom,c=a?o.left:i.left,p=a?o.right:i.right;return{top:s,bottom:u,left:c,right:p,width:p-c,height:u-s};case"left":case"right":var f=Math.min.apply(Math,n.map((function(e){return e.left}))),l=Math.max.apply(Math,n.map((function(e){return e.right}))),d=n.filter((function(t){return"left"===e?t.left===f:t.right===l})),v=d[0].top,m=d[d.length-1].bottom;return{top:v,bottom:m,left:f,right:l,width:l-f,height:m-v};default:return t}}(p(e),n.getBoundingClientRect(),f(n.getClientRects()),r)}(a.placement)}})),t=a.placement)}};function s(){var t;o||(t=function(e,t){var n;return{popperOptions:Object.assign({},e.popperOptions,{modifiers:[].concat(((null==(n=e.popperOptions)?void 0:n.modifiers)||[]).filter((function(e){return e.name!==t.name})),[t])})}}(e.props,a),o=!0,e.setProps(t),o=!1)}return{onCreate:s,onAfterUpdate:s,onTrigger:function(t,n){if(m(n)){var o=f(e.reference.getClientRects()),i=o.find((function(e){return e.left-2<=n.clientX&&e.right+2>=n.clientX&&e.top-2<=n.clientY&&e.bottom+2>=n.clientY})),a=o.indexOf(i);r=a>-1?a:r}},onHidden:function(){r=-1}}}};var K={name:"sticky",defaultValue:!1,fn:function(e){var t=e.reference,n=e.popper;function r(t){return!0===e.props.sticky||e.props.sticky===t}var o=null,i=null;function a(){var s=r("reference")?(e.popperInstance?e.popperInstance.state.elements.reference:t).getBoundingClientRect():null,u=r("popper")?n.getBoundingClientRect():null;(s&&Q(o,s)||u&&Q(i,u))&&e.popperInstance&&e.popperInstance.update(),o=s,i=u,e.state.isMounted&&requestAnimationFrame(a)}return{onMount:function(){e.props.sticky&&a()}}}};function Q(e,t){return!e||!t||(e.top!==t.top||e.right!==t.right||e.bottom!==t.bottom||e.left!==t.left)}return F.setDefaultProps({plugins:[Y,J,G,K],render:N}),F.createSingleton=function(e,t){var n;void 0===t&&(t={});var r,o=e,i=[],a=[],c=t.overrides,p=[],f=!1;function l(){a=o.map((function(e){return u(e.props.triggerTarget||e.reference)})).reduce((function(e,t){return e.concat(t)}),[])}function v(){i=o.map((function(e){return e.reference}))}function m(e){o.forEach((function(t){e?t.enable():t.disable()}))}function g(e){return o.map((function(t){var n=t.setProps;return t.setProps=function(o){n(o),t.reference===r&&e.setProps(o)},function(){t.setProps=n}}))}function h(e,t){var n=a.indexOf(t);if(t!==r){r=t;var s=(c||[]).concat("content").reduce((function(e,t){return e[t]=o[n].props[t],e}),{});e.setProps(Object.assign({},s,{getReferenceClientRect:"function"==typeof s.getReferenceClientRect?s.getReferenceClientRect:function(){var e;return null==(e=i[n])?void 0:e.getBoundingClientRect()}}))}}m(!1),v(),l();var b={fn:function(){return{onDestroy:function(){m(!0)},onHidden:function(){r=null},onClickOutside:function(e){e.props.showOnCreate&&!f&&(f=!0,r=null)},onShow:function(e){e.props.showOnCreate&&!f&&(f=!0,h(e,i[0]))},onTrigger:function(e,t){h(e,t.currentTarget)}}}},y=F(d(),Object.assign({},s(t,["overrides"]),{plugins:[b].concat(t.plugins||[]),triggerTarget:a,popperOptions:Object.assign({},t.popperOptions,{modifiers:[].concat((null==(n=t.popperOptions)?void 0:n.modifiers)||[],[W])})})),w=y.show;y.show=function(e){if(w(),!r&&null==e)return h(y,i[0]);if(!r||null!=e){if("number"==typeof e)return i[e]&&h(y,i[e]);if(o.indexOf(e)>=0){var t=e.reference;return h(y,t)}return i.indexOf(e)>=0?h(y,e):void 0}},y.showNext=function(){var e=i[0];if(!r)return y.show(0);var t=i.indexOf(r);y.show(i[t+1]||e)},y.showPrevious=function(){var e=i[i.length-1];if(!r)return y.show(e);var t=i.indexOf(r),n=i[t-1]||e;y.show(n)};var E=y.setProps;return y.setProps=function(e){c=e.overrides||c,E(e)},y.setInstances=function(e){m(!0),p.forEach((function(e){return e()})),o=e,m(!1),v(),l(),p=g(y),y.setProps({triggerTarget:a})},p=g(y),y},F.delegate=function(e,n){var r=[],o=[],i=!1,a=n.target,c=s(n,["target"]),p=Object.assign({},c,{trigger:"manual",touch:!1}),f=Object.assign({touch:R.touch},c,{showOnCreate:!0}),l=F(e,p);function d(e){if(e.target&&!i){var t=e.target.closest(a);if(t){var r=t.getAttribute("data-tippy-trigger")||n.trigger||R.trigger;if(!t._tippy&&!("touchstart"===e.type&&"boolean"==typeof f.touch||"touchstart"!==e.type&&r.indexOf(X[e.type])<0)){var s=F(t,f);s&&(o=o.concat(s))}}}}function v(e,t,n,o){void 0===o&&(o=!1),e.addEventListener(t,n,o),r.push({node:e,eventType:t,handler:n,options:o})}return u(l).forEach((function(e){var n=e.destroy,a=e.enable,s=e.disable;e.destroy=function(e){void 0===e&&(e=!0),e&&o.forEach((function(e){e.destroy()})),o=[],r.forEach((function(e){var t=e.node,n=e.eventType,r=e.handler,o=e.options;t.removeEventListener(n,r,o)})),r=[],n()},e.enable=function(){a(),o.forEach((function(e){return e.enable()})),i=!1},e.disable=function(){s(),o.forEach((function(e){return e.disable()})),i=!0},function(e){var n=e.reference;v(n,"touchstart",d,t),v(n,"mouseover",d),v(n,"focusin",d),v(n,"click",d)}(e)})),l},F.hideAll=function(e){var t=void 0===e?{}:e,n=t.exclude,r=t.duration;U.forEach((function(e){var t=!1;if(n&&(t=g(n)?e.reference===n:e.popper===n.popper),!t){var o=e.props.duration;e.setProps({duration:r}),e.hide(),e.state.isDestroyed||e.setProps({duration:o})}}))},F.roundArrow='',F})); + diff --git a/site_libs/quarto-nav/headroom.min.js b/site_libs/quarto-nav/headroom.min.js new file mode 100644 index 00000000..b08f1dff --- /dev/null +++ b/site_libs/quarto-nav/headroom.min.js @@ -0,0 +1,7 @@ +/*! + * headroom.js v0.12.0 - Give your page some headroom. Hide your header until you need it + * Copyright (c) 2020 Nick Williams - http://wicky.nillia.ms/headroom.js + * License: MIT + */ + +!function(t,n){"object"==typeof exports&&"undefined"!=typeof module?module.exports=n():"function"==typeof define&&define.amd?define(n):(t=t||self).Headroom=n()}(this,function(){"use strict";function t(){return"undefined"!=typeof window}function d(t){return function(t){return t&&t.document&&function(t){return 9===t.nodeType}(t.document)}(t)?function(t){var n=t.document,o=n.body,s=n.documentElement;return{scrollHeight:function(){return Math.max(o.scrollHeight,s.scrollHeight,o.offsetHeight,s.offsetHeight,o.clientHeight,s.clientHeight)},height:function(){return t.innerHeight||s.clientHeight||o.clientHeight},scrollY:function(){return void 0!==t.pageYOffset?t.pageYOffset:(s||o.parentNode||o).scrollTop}}}(t):function(t){return{scrollHeight:function(){return Math.max(t.scrollHeight,t.offsetHeight,t.clientHeight)},height:function(){return Math.max(t.offsetHeight,t.clientHeight)},scrollY:function(){return t.scrollTop}}}(t)}function n(t,s,e){var n,o=function(){var n=!1;try{var t={get passive(){n=!0}};window.addEventListener("test",t,t),window.removeEventListener("test",t,t)}catch(t){n=!1}return n}(),i=!1,r=d(t),l=r.scrollY(),a={};function c(){var t=Math.round(r.scrollY()),n=r.height(),o=r.scrollHeight();a.scrollY=t,a.lastScrollY=l,a.direction=ls.tolerance[a.direction],e(a),l=t,i=!1}function h(){i||(i=!0,n=requestAnimationFrame(c))}var u=!!o&&{passive:!0,capture:!1};return t.addEventListener("scroll",h,u),c(),{destroy:function(){cancelAnimationFrame(n),t.removeEventListener("scroll",h,u)}}}function o(t){return t===Object(t)?t:{down:t,up:t}}function s(t,n){n=n||{},Object.assign(this,s.options,n),this.classes=Object.assign({},s.options.classes,n.classes),this.elem=t,this.tolerance=o(this.tolerance),this.offset=o(this.offset),this.initialised=!1,this.frozen=!1}return s.prototype={constructor:s,init:function(){return s.cutsTheMustard&&!this.initialised&&(this.addClass("initial"),this.initialised=!0,setTimeout(function(t){t.scrollTracker=n(t.scroller,{offset:t.offset,tolerance:t.tolerance},t.update.bind(t))},100,this)),this},destroy:function(){this.initialised=!1,Object.keys(this.classes).forEach(this.removeClass,this),this.scrollTracker.destroy()},unpin:function(){!this.hasClass("pinned")&&this.hasClass("unpinned")||(this.addClass("unpinned"),this.removeClass("pinned"),this.onUnpin&&this.onUnpin.call(this))},pin:function(){this.hasClass("unpinned")&&(this.addClass("pinned"),this.removeClass("unpinned"),this.onPin&&this.onPin.call(this))},freeze:function(){this.frozen=!0,this.addClass("frozen")},unfreeze:function(){this.frozen=!1,this.removeClass("frozen")},top:function(){this.hasClass("top")||(this.addClass("top"),this.removeClass("notTop"),this.onTop&&this.onTop.call(this))},notTop:function(){this.hasClass("notTop")||(this.addClass("notTop"),this.removeClass("top"),this.onNotTop&&this.onNotTop.call(this))},bottom:function(){this.hasClass("bottom")||(this.addClass("bottom"),this.removeClass("notBottom"),this.onBottom&&this.onBottom.call(this))},notBottom:function(){this.hasClass("notBottom")||(this.addClass("notBottom"),this.removeClass("bottom"),this.onNotBottom&&this.onNotBottom.call(this))},shouldUnpin:function(t){return"down"===t.direction&&!t.top&&t.toleranceExceeded},shouldPin:function(t){return"up"===t.direction&&t.toleranceExceeded||t.top},addClass:function(t){this.elem.classList.add.apply(this.elem.classList,this.classes[t].split(" "))},removeClass:function(t){this.elem.classList.remove.apply(this.elem.classList,this.classes[t].split(" "))},hasClass:function(t){return this.classes[t].split(" ").every(function(t){return this.classList.contains(t)},this.elem)},update:function(t){t.isOutOfBounds||!0!==this.frozen&&(t.top?this.top():this.notTop(),t.bottom?this.bottom():this.notBottom(),this.shouldUnpin(t)?this.unpin():this.shouldPin(t)&&this.pin())}},s.options={tolerance:{up:0,down:0},offset:0,scroller:t()?window:null,classes:{frozen:"headroom--frozen",pinned:"headroom--pinned",unpinned:"headroom--unpinned",top:"headroom--top",notTop:"headroom--not-top",bottom:"headroom--bottom",notBottom:"headroom--not-bottom",initial:"headroom"}},s.cutsTheMustard=!!(t()&&function(){}.bind&&"classList"in document.documentElement&&Object.assign&&Object.keys&&requestAnimationFrame),s}); diff --git a/site_libs/quarto-nav/quarto-nav.js b/site_libs/quarto-nav/quarto-nav.js new file mode 100644 index 00000000..38cc4305 --- /dev/null +++ b/site_libs/quarto-nav/quarto-nav.js @@ -0,0 +1,325 @@ +const headroomChanged = new CustomEvent("quarto-hrChanged", { + detail: {}, + bubbles: true, + cancelable: false, + composed: false, +}); + +const announceDismiss = () => { + const annEl = window.document.getElementById("quarto-announcement"); + if (annEl) { + annEl.remove(); + + const annId = annEl.getAttribute("data-announcement-id"); + window.localStorage.setItem(`quarto-announce-${annId}`, "true"); + } +}; + +const announceRegister = () => { + const annEl = window.document.getElementById("quarto-announcement"); + if (annEl) { + const annId = annEl.getAttribute("data-announcement-id"); + const isDismissed = + window.localStorage.getItem(`quarto-announce-${annId}`) || false; + if (isDismissed) { + announceDismiss(); + return; + } else { + annEl.classList.remove("hidden"); + } + + const actionEl = annEl.querySelector(".quarto-announcement-action"); + if (actionEl) { + actionEl.addEventListener("click", function (e) { + e.preventDefault(); + // Hide the bar immediately + announceDismiss(); + }); + } + } +}; + +window.document.addEventListener("DOMContentLoaded", function () { + let init = false; + + announceRegister(); + + // Manage the back to top button, if one is present. + let lastScrollTop = window.pageYOffset || document.documentElement.scrollTop; + const scrollDownBuffer = 5; + const scrollUpBuffer = 35; + const btn = document.getElementById("quarto-back-to-top"); + const hideBackToTop = () => { + btn.style.display = "none"; + }; + const showBackToTop = () => { + btn.style.display = "inline-block"; + }; + if (btn) { + window.document.addEventListener( + "scroll", + function () { + const currentScrollTop = + window.pageYOffset || document.documentElement.scrollTop; + + // Shows and hides the button 'intelligently' as the user scrolls + if (currentScrollTop - scrollDownBuffer > lastScrollTop) { + hideBackToTop(); + lastScrollTop = currentScrollTop <= 0 ? 0 : currentScrollTop; + } else if (currentScrollTop < lastScrollTop - scrollUpBuffer) { + showBackToTop(); + lastScrollTop = currentScrollTop <= 0 ? 0 : currentScrollTop; + } + + // Show the button at the bottom, hides it at the top + if (currentScrollTop <= 0) { + hideBackToTop(); + } else if ( + window.innerHeight + currentScrollTop >= + document.body.offsetHeight + ) { + showBackToTop(); + } + }, + false + ); + } + + function throttle(func, wait) { + var timeout; + return function () { + const context = this; + const args = arguments; + const later = function () { + clearTimeout(timeout); + timeout = null; + func.apply(context, args); + }; + + if (!timeout) { + timeout = setTimeout(later, wait); + } + }; + } + + function headerOffset() { + // Set an offset if there is are fixed top navbar + const headerEl = window.document.querySelector("header.fixed-top"); + if (headerEl) { + return headerEl.clientHeight; + } else { + return 0; + } + } + + function footerOffset() { + const footerEl = window.document.querySelector("footer.footer"); + if (footerEl) { + return footerEl.clientHeight; + } else { + return 0; + } + } + + function dashboardOffset() { + const dashboardNavEl = window.document.getElementById( + "quarto-dashboard-header" + ); + if (dashboardNavEl !== null) { + return dashboardNavEl.clientHeight; + } else { + return 0; + } + } + + function updateDocumentOffsetWithoutAnimation() { + updateDocumentOffset(false); + } + + function updateDocumentOffset(animated) { + // set body offset + const topOffset = headerOffset(); + const bodyOffset = topOffset + footerOffset() + dashboardOffset(); + const bodyEl = window.document.body; + bodyEl.setAttribute("data-bs-offset", topOffset); + bodyEl.style.paddingTop = topOffset + "px"; + + // deal with sidebar offsets + const sidebars = window.document.querySelectorAll( + ".sidebar, .headroom-target" + ); + sidebars.forEach((sidebar) => { + if (!animated) { + sidebar.classList.add("notransition"); + // Remove the no transition class after the animation has time to complete + setTimeout(function () { + sidebar.classList.remove("notransition"); + }, 201); + } + + if (window.Headroom && sidebar.classList.contains("sidebar-unpinned")) { + sidebar.style.top = "0"; + sidebar.style.maxHeight = "100vh"; + } else { + sidebar.style.top = topOffset + "px"; + sidebar.style.maxHeight = "calc(100vh - " + topOffset + "px)"; + } + }); + + // allow space for footer + const mainContainer = window.document.querySelector(".quarto-container"); + if (mainContainer) { + mainContainer.style.minHeight = "calc(100vh - " + bodyOffset + "px)"; + } + + // link offset + let linkStyle = window.document.querySelector("#quarto-target-style"); + if (!linkStyle) { + linkStyle = window.document.createElement("style"); + linkStyle.setAttribute("id", "quarto-target-style"); + window.document.head.appendChild(linkStyle); + } + while (linkStyle.firstChild) { + linkStyle.removeChild(linkStyle.firstChild); + } + if (topOffset > 0) { + linkStyle.appendChild( + window.document.createTextNode(` + section:target::before { + content: ""; + display: block; + height: ${topOffset}px; + margin: -${topOffset}px 0 0; + }`) + ); + } + if (init) { + window.dispatchEvent(headroomChanged); + } + init = true; + } + + // initialize headroom + var header = window.document.querySelector("#quarto-header"); + if (header && window.Headroom) { + const headroom = new window.Headroom(header, { + tolerance: 5, + onPin: function () { + const sidebars = window.document.querySelectorAll( + ".sidebar, .headroom-target" + ); + sidebars.forEach((sidebar) => { + sidebar.classList.remove("sidebar-unpinned"); + }); + updateDocumentOffset(); + }, + onUnpin: function () { + const sidebars = window.document.querySelectorAll( + ".sidebar, .headroom-target" + ); + sidebars.forEach((sidebar) => { + sidebar.classList.add("sidebar-unpinned"); + }); + updateDocumentOffset(); + }, + }); + headroom.init(); + + let frozen = false; + window.quartoToggleHeadroom = function () { + if (frozen) { + headroom.unfreeze(); + frozen = false; + } else { + headroom.freeze(); + frozen = true; + } + }; + } + + window.addEventListener( + "hashchange", + function (e) { + if ( + getComputedStyle(document.documentElement).scrollBehavior !== "smooth" + ) { + window.scrollTo(0, window.pageYOffset - headerOffset()); + } + }, + false + ); + + // Observe size changed for the header + const headerEl = window.document.querySelector("header.fixed-top"); + if (headerEl && window.ResizeObserver) { + const observer = new window.ResizeObserver(() => { + setTimeout(updateDocumentOffsetWithoutAnimation, 0); + }); + observer.observe(headerEl, { + attributes: true, + childList: true, + characterData: true, + }); + } else { + window.addEventListener( + "resize", + throttle(updateDocumentOffsetWithoutAnimation, 50) + ); + } + setTimeout(updateDocumentOffsetWithoutAnimation, 250); + + // fixup index.html links if we aren't on the filesystem + if (window.location.protocol !== "file:") { + const links = window.document.querySelectorAll("a"); + for (let i = 0; i < links.length; i++) { + if (links[i].href) { + links[i].dataset.originalHref = links[i].href; + links[i].href = links[i].href.replace(/\/index\.html/, "/"); + } + } + + // Fixup any sharing links that require urls + // Append url to any sharing urls + const sharingLinks = window.document.querySelectorAll( + "a.sidebar-tools-main-item, a.quarto-navigation-tool, a.quarto-navbar-tools, a.quarto-navbar-tools-item" + ); + for (let i = 0; i < sharingLinks.length; i++) { + const sharingLink = sharingLinks[i]; + const href = sharingLink.getAttribute("href"); + if (href) { + sharingLink.setAttribute( + "href", + href.replace("|url|", window.location.href) + ); + } + } + + // Scroll the active navigation item into view, if necessary + const navSidebar = window.document.querySelector("nav#quarto-sidebar"); + if (navSidebar) { + // Find the active item + const activeItem = navSidebar.querySelector("li.sidebar-item a.active"); + if (activeItem) { + // Wait for the scroll height and height to resolve by observing size changes on the + // nav element that is scrollable + const resizeObserver = new ResizeObserver((_entries) => { + // The bottom of the element + const elBottom = activeItem.offsetTop; + const viewBottom = navSidebar.scrollTop + navSidebar.clientHeight; + + // The element height and scroll height are the same, then we are still loading + if (viewBottom !== navSidebar.scrollHeight) { + // Determine if the item isn't visible and scroll to it + if (elBottom >= viewBottom) { + navSidebar.scrollTop = elBottom; + } + + // stop observing now since we've completed the scroll + resizeObserver.unobserve(navSidebar); + } + }); + resizeObserver.observe(navSidebar); + } + } + } +}); diff --git a/site_libs/quarto-search/autocomplete.umd.js b/site_libs/quarto-search/autocomplete.umd.js new file mode 100644 index 00000000..ae0063aa --- /dev/null +++ b/site_libs/quarto-search/autocomplete.umd.js @@ -0,0 +1,3 @@ +/*! @algolia/autocomplete-js 1.11.1 | MIT License | © Algolia, Inc. and contributors | https://github.com/algolia/autocomplete */ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self)["@algolia/autocomplete-js"]={})}(this,(function(e){"use strict";function t(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function n(e){for(var n=1;n=0||(o[n]=e[n]);return o}(e,t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(o[n]=e[n])}return o}function a(e,t){return function(e){if(Array.isArray(e))return e}(e)||function(e,t){var n=null==e?null:"undefined"!=typeof Symbol&&e[Symbol.iterator]||e["@@iterator"];if(null!=n){var r,o,i,u,a=[],l=!0,c=!1;try{if(i=(n=n.call(e)).next,0===t){if(Object(n)!==n)return;l=!1}else for(;!(l=(r=i.call(n)).done)&&(a.push(r.value),a.length!==t);l=!0);}catch(e){c=!0,o=e}finally{try{if(!l&&null!=n.return&&(u=n.return(),Object(u)!==u))return}finally{if(c)throw o}}return a}}(e,t)||c(e,t)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function l(e){return function(e){if(Array.isArray(e))return s(e)}(e)||function(e){if("undefined"!=typeof Symbol&&null!=e[Symbol.iterator]||null!=e["@@iterator"])return Array.from(e)}(e)||c(e)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function c(e,t){if(e){if("string"==typeof e)return s(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);return"Object"===n&&e.constructor&&(n=e.constructor.name),"Map"===n||"Set"===n?Array.from(e):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?s(e,t):void 0}}function s(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);ne.length)&&(t=e.length);for(var n=0,r=new Array(t);ne.length)&&(t=e.length);for(var n=0,r=new Array(t);n=0||(o[n]=e[n]);return o}(e,t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(o[n]=e[n])}return o}function x(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function N(e){for(var t=1;t1&&void 0!==arguments[1]?arguments[1]:20,n=[],r=0;r=3||2===n&&r>=4||1===n&&r>=10);function i(t,n,r){if(o&&void 0!==r){var i=r[0].__autocomplete_algoliaCredentials,u={"X-Algolia-Application-Id":i.appId,"X-Algolia-API-Key":i.apiKey};e.apply(void 0,[t].concat(D(n),[{headers:u}]))}else e.apply(void 0,[t].concat(D(n)))}return{init:function(t,n){e("init",{appId:t,apiKey:n})},setUserToken:function(t){e("setUserToken",t)},clickedObjectIDsAfterSearch:function(){for(var e=arguments.length,t=new Array(e),n=0;n0&&i("clickedObjectIDsAfterSearch",B(t),t[0].items)},clickedObjectIDs:function(){for(var e=arguments.length,t=new Array(e),n=0;n0&&i("clickedObjectIDs",B(t),t[0].items)},clickedFilters:function(){for(var t=arguments.length,n=new Array(t),r=0;r0&&e.apply(void 0,["clickedFilters"].concat(n))},convertedObjectIDsAfterSearch:function(){for(var e=arguments.length,t=new Array(e),n=0;n0&&i("convertedObjectIDsAfterSearch",B(t),t[0].items)},convertedObjectIDs:function(){for(var e=arguments.length,t=new Array(e),n=0;n0&&i("convertedObjectIDs",B(t),t[0].items)},convertedFilters:function(){for(var t=arguments.length,n=new Array(t),r=0;r0&&e.apply(void 0,["convertedFilters"].concat(n))},viewedObjectIDs:function(){for(var e=arguments.length,t=new Array(e),n=0;n0&&t.reduce((function(e,t){var n=t.items,r=k(t,A);return[].concat(D(e),D(q(N(N({},r),{},{objectIDs:(null==n?void 0:n.map((function(e){return e.objectID})))||r.objectIDs})).map((function(e){return{items:n,payload:e}}))))}),[]).forEach((function(e){var t=e.items;return i("viewedObjectIDs",[e.payload],t)}))},viewedFilters:function(){for(var t=arguments.length,n=new Array(t),r=0;r0&&e.apply(void 0,["viewedFilters"].concat(n))}}}function F(e){var t=e.items.reduce((function(e,t){var n;return e[t.__autocomplete_indexName]=(null!==(n=e[t.__autocomplete_indexName])&&void 0!==n?n:[]).concat(t),e}),{});return Object.keys(t).map((function(e){return{index:e,items:t[e],algoliaSource:["autocomplete"]}}))}function L(e){return e.objectID&&e.__autocomplete_indexName&&e.__autocomplete_queryID}function U(e){return U="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},U(e)}function M(e){return function(e){if(Array.isArray(e))return H(e)}(e)||function(e){if("undefined"!=typeof Symbol&&null!=e[Symbol.iterator]||null!=e["@@iterator"])return Array.from(e)}(e)||function(e,t){if(!e)return;if("string"==typeof e)return H(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);"Object"===n&&e.constructor&&(n=e.constructor.name);if("Map"===n||"Set"===n)return Array.from(e);if("Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n))return H(e,t)}(e)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function H(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n0&&z({onItemsChange:r,items:n,insights:a,state:t}))}}),0);return{name:"aa.algoliaInsightsPlugin",subscribe:function(e){var t=e.setContext,n=e.onSelect,r=e.onActive;function l(e){t({algoliaInsightsPlugin:{__algoliaSearchParameters:W({clickAnalytics:!0},e?{userToken:e}:{}),insights:a}})}u("addAlgoliaAgent","insights-plugin"),l(),u("onUserTokenChange",l),u("getUserToken",null,(function(e,t){l(t)})),n((function(e){var t=e.item,n=e.state,r=e.event,i=e.source;L(t)&&o({state:n,event:r,insights:a,item:t,insightsEvents:[W({eventName:"Item Selected"},j({item:t,items:i.getItems().filter(L)}))]})})),r((function(e){var t=e.item,n=e.source,r=e.state,o=e.event;L(t)&&i({state:r,event:o,insights:a,item:t,insightsEvents:[W({eventName:"Item Active"},j({item:t,items:n.getItems().filter(L)}))]})}))},onStateChange:function(e){var t=e.state;c({state:t})},__autocomplete_pluginOptions:e}}function J(e,t){var n=t;return{then:function(t,r){return J(e.then(Y(t,n,e),Y(r,n,e)),n)},catch:function(t){return J(e.catch(Y(t,n,e)),n)},finally:function(t){return t&&n.onCancelList.push(t),J(e.finally(Y(t&&function(){return n.onCancelList=[],t()},n,e)),n)},cancel:function(){n.isCanceled=!0;var e=n.onCancelList;n.onCancelList=[],e.forEach((function(e){e()}))},isCanceled:function(){return!0===n.isCanceled}}}function X(e){return J(e,{isCanceled:!1,onCancelList:[]})}function Y(e,t,n){return e?function(n){return t.isCanceled?n:e(n)}:n}function Z(e,t,n,r){if(!n)return null;if(e<0&&(null===t||null!==r&&0===t))return n+e;var o=(null===t?-1:t)+e;return o<=-1||o>=n?null===r?null:0:o}function ee(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function te(e){for(var t=1;te.length)&&(t=e.length);for(var n=0,r=new Array(t);n0},reshape:function(e){return e.sources}},e),{},{id:null!==(n=e.id)&&void 0!==n?n:d(),plugins:o,initialState:he({activeItemId:null,query:"",completion:null,collections:[],isOpen:!1,status:"idle",context:{}},e.initialState),onStateChange:function(t){var n;null===(n=e.onStateChange)||void 0===n||n.call(e,t),o.forEach((function(e){var n;return null===(n=e.onStateChange)||void 0===n?void 0:n.call(e,t)}))},onSubmit:function(t){var n;null===(n=e.onSubmit)||void 0===n||n.call(e,t),o.forEach((function(e){var n;return null===(n=e.onSubmit)||void 0===n?void 0:n.call(e,t)}))},onReset:function(t){var n;null===(n=e.onReset)||void 0===n||n.call(e,t),o.forEach((function(e){var n;return null===(n=e.onReset)||void 0===n?void 0:n.call(e,t)}))},getSources:function(n){return Promise.all([].concat(ye(o.map((function(e){return e.getSources}))),[e.getSources]).filter(Boolean).map((function(e){return function(e,t){var n=[];return Promise.resolve(e(t)).then((function(e){return Promise.all(e.filter((function(e){return Boolean(e)})).map((function(e){if(e.sourceId,n.includes(e.sourceId))throw new Error("[Autocomplete] The `sourceId` ".concat(JSON.stringify(e.sourceId)," is not unique."));n.push(e.sourceId);var t={getItemInputValue:function(e){return e.state.query},getItemUrl:function(){},onSelect:function(e){(0,e.setIsOpen)(!1)},onActive:O,onResolve:O};Object.keys(t).forEach((function(e){t[e].__default=!0}));var r=te(te({},t),e);return Promise.resolve(r)})))}))}(e,n)}))).then((function(e){return m(e)})).then((function(e){return e.map((function(e){return he(he({},e),{},{onSelect:function(n){e.onSelect(n),t.forEach((function(e){var t;return null===(t=e.onSelect)||void 0===t?void 0:t.call(e,n)}))},onActive:function(n){e.onActive(n),t.forEach((function(e){var t;return null===(t=e.onActive)||void 0===t?void 0:t.call(e,n)}))},onResolve:function(n){e.onResolve(n),t.forEach((function(e){var t;return null===(t=e.onResolve)||void 0===t?void 0:t.call(e,n)}))}})}))}))},navigator:he({navigate:function(e){var t=e.itemUrl;r.location.assign(t)},navigateNewTab:function(e){var t=e.itemUrl,n=r.open(t,"_blank","noopener");null==n||n.focus()},navigateNewWindow:function(e){var t=e.itemUrl;r.open(t,"_blank","noopener")}},e.navigator)})}function Se(e){return Se="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},Se(e)}function je(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function Pe(e){for(var t=1;te.length)&&(t=e.length);for(var n=0,r=new Array(t);n=0||(o[n]=e[n]);return o}(e,t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(o[n]=e[n])}return o}var He,Ve,We,Ke=null,Qe=(He=-1,Ve=-1,We=void 0,function(e){var t=++He;return Promise.resolve(e).then((function(e){return We&&t=0||(o[n]=e[n]);return o}(e,t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(o[n]=e[n])}return o}function et(e){return et="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},et(e)}var tt=["props","refresh","store"],nt=["inputElement","formElement","panelElement"],rt=["inputElement"],ot=["inputElement","maxLength"],it=["source"],ut=["item","source"];function at(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function lt(e){for(var t=1;t=0||(o[n]=e[n]);return o}(e,t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(o[n]=e[n])}return o}function ft(e){var t=e.props,n=e.refresh,r=e.store,o=st(e,tt);return{getEnvironmentProps:function(e){var n=e.inputElement,o=e.formElement,i=e.panelElement;function u(e){!r.getState().isOpen&&r.pendingRequests.isEmpty()||e.target===n||!1===[o,i].some((function(t){return n=t,r=e.target,n===r||n.contains(r);var n,r}))&&(r.dispatch("blur",null),t.debug||r.pendingRequests.cancelAll())}return lt({onTouchStart:u,onMouseDown:u,onTouchMove:function(e){!1!==r.getState().isOpen&&n===t.environment.document.activeElement&&e.target!==n&&n.blur()}},st(e,nt))},getRootProps:function(e){return lt({role:"combobox","aria-expanded":r.getState().isOpen,"aria-haspopup":"listbox","aria-owns":r.getState().isOpen?r.getState().collections.map((function(e){var n=e.source;return ie(t.id,"list",n)})).join(" "):void 0,"aria-labelledby":ie(t.id,"label")},e)},getFormProps:function(e){return e.inputElement,lt({action:"",noValidate:!0,role:"search",onSubmit:function(i){var u;i.preventDefault(),t.onSubmit(lt({event:i,refresh:n,state:r.getState()},o)),r.dispatch("submit",null),null===(u=e.inputElement)||void 0===u||u.blur()},onReset:function(i){var u;i.preventDefault(),t.onReset(lt({event:i,refresh:n,state:r.getState()},o)),r.dispatch("reset",null),null===(u=e.inputElement)||void 0===u||u.focus()}},st(e,rt))},getLabelProps:function(e){return lt({htmlFor:ie(t.id,"input"),id:ie(t.id,"label")},e)},getInputProps:function(e){var i;function u(e){(t.openOnFocus||Boolean(r.getState().query))&&$e(lt({event:e,props:t,query:r.getState().completion||r.getState().query,refresh:n,store:r},o)),r.dispatch("focus",null)}var a=e||{};a.inputElement;var l=a.maxLength,c=void 0===l?512:l,s=st(a,ot),f=oe(r.getState()),p=function(e){return Boolean(e&&e.match(ue))}((null===(i=t.environment.navigator)||void 0===i?void 0:i.userAgent)||""),m=t.enterKeyHint||(null!=f&&f.itemUrl&&!p?"go":"search");return lt({"aria-autocomplete":"both","aria-activedescendant":r.getState().isOpen&&null!==r.getState().activeItemId?ie(t.id,"item-".concat(r.getState().activeItemId),null==f?void 0:f.source):void 0,"aria-controls":r.getState().isOpen?r.getState().collections.map((function(e){var n=e.source;return ie(t.id,"list",n)})).join(" "):void 0,"aria-labelledby":ie(t.id,"label"),value:r.getState().completion||r.getState().query,id:ie(t.id,"input"),autoComplete:"off",autoCorrect:"off",autoCapitalize:"off",enterKeyHint:m,spellCheck:"false",autoFocus:t.autoFocus,placeholder:t.placeholder,maxLength:c,type:"search",onChange:function(e){$e(lt({event:e,props:t,query:e.currentTarget.value.slice(0,c),refresh:n,store:r},o))},onKeyDown:function(e){!function(e){var t=e.event,n=e.props,r=e.refresh,o=e.store,i=Ze(e,Ge);if("ArrowUp"===t.key||"ArrowDown"===t.key){var u=function(){var e=oe(o.getState()),t=n.environment.document.getElementById(ie(n.id,"item-".concat(o.getState().activeItemId),null==e?void 0:e.source));t&&(t.scrollIntoViewIfNeeded?t.scrollIntoViewIfNeeded(!1):t.scrollIntoView(!1))},a=function(){var e=oe(o.getState());if(null!==o.getState().activeItemId&&e){var n=e.item,u=e.itemInputValue,a=e.itemUrl,l=e.source;l.onActive(Xe({event:t,item:n,itemInputValue:u,itemUrl:a,refresh:r,source:l,state:o.getState()},i))}};t.preventDefault(),!1===o.getState().isOpen&&(n.openOnFocus||Boolean(o.getState().query))?$e(Xe({event:t,props:n,query:o.getState().query,refresh:r,store:o},i)).then((function(){o.dispatch(t.key,{nextActiveItemId:n.defaultActiveItemId}),a(),setTimeout(u,0)})):(o.dispatch(t.key,{}),a(),u())}else if("Escape"===t.key)t.preventDefault(),o.dispatch(t.key,null),o.pendingRequests.cancelAll();else if("Tab"===t.key)o.dispatch("blur",null),o.pendingRequests.cancelAll();else if("Enter"===t.key){if(null===o.getState().activeItemId||o.getState().collections.every((function(e){return 0===e.items.length})))return void(n.debug||o.pendingRequests.cancelAll());t.preventDefault();var l=oe(o.getState()),c=l.item,s=l.itemInputValue,f=l.itemUrl,p=l.source;if(t.metaKey||t.ctrlKey)void 0!==f&&(p.onSelect(Xe({event:t,item:c,itemInputValue:s,itemUrl:f,refresh:r,source:p,state:o.getState()},i)),n.navigator.navigateNewTab({itemUrl:f,item:c,state:o.getState()}));else if(t.shiftKey)void 0!==f&&(p.onSelect(Xe({event:t,item:c,itemInputValue:s,itemUrl:f,refresh:r,source:p,state:o.getState()},i)),n.navigator.navigateNewWindow({itemUrl:f,item:c,state:o.getState()}));else if(t.altKey);else{if(void 0!==f)return p.onSelect(Xe({event:t,item:c,itemInputValue:s,itemUrl:f,refresh:r,source:p,state:o.getState()},i)),void n.navigator.navigate({itemUrl:f,item:c,state:o.getState()});$e(Xe({event:t,nextState:{isOpen:!1},props:n,query:s,refresh:r,store:o},i)).then((function(){p.onSelect(Xe({event:t,item:c,itemInputValue:s,itemUrl:f,refresh:r,source:p,state:o.getState()},i))}))}}}(lt({event:e,props:t,refresh:n,store:r},o))},onFocus:u,onBlur:O,onClick:function(n){e.inputElement!==t.environment.document.activeElement||r.getState().isOpen||u(n)}},s)},getPanelProps:function(e){return lt({onMouseDown:function(e){e.preventDefault()},onMouseLeave:function(){r.dispatch("mouseleave",null)}},e)},getListProps:function(e){var n=e||{},r=n.source,o=st(n,it);return lt({role:"listbox","aria-labelledby":ie(t.id,"label"),id:ie(t.id,"list",r)},o)},getItemProps:function(e){var i=e.item,u=e.source,a=st(e,ut);return lt({id:ie(t.id,"item-".concat(i.__autocomplete_id),u),role:"option","aria-selected":r.getState().activeItemId===i.__autocomplete_id,onMouseMove:function(e){if(i.__autocomplete_id!==r.getState().activeItemId){r.dispatch("mousemove",i.__autocomplete_id);var t=oe(r.getState());if(null!==r.getState().activeItemId&&t){var u=t.item,a=t.itemInputValue,l=t.itemUrl,c=t.source;c.onActive(lt({event:e,item:u,itemInputValue:a,itemUrl:l,refresh:n,source:c,state:r.getState()},o))}}},onMouseDown:function(e){e.preventDefault()},onClick:function(e){var a=u.getItemInputValue({item:i,state:r.getState()}),l=u.getItemUrl({item:i,state:r.getState()});(l?Promise.resolve():$e(lt({event:e,nextState:{isOpen:!1},props:t,query:a,refresh:n,store:r},o))).then((function(){u.onSelect(lt({event:e,item:i,itemInputValue:a,itemUrl:l,refresh:n,source:u,state:r.getState()},o))}))}},a)}}}function pt(e){return pt="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},pt(e)}function mt(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function vt(e){for(var t=1;t=5&&((o||!e&&5===r)&&(u.push(r,0,o,n),r=6),e&&(u.push(r,e,0,n),r=6)),o=""},l=0;l"===t?(r=1,o=""):o=t+o[0]:i?t===i?i="":o+=t:'"'===t||"'"===t?i=t:">"===t?(a(),r=1):r&&("="===t?(r=5,n=o,o=""):"/"===t&&(r<5||">"===e[l][c+1])?(a(),3===r&&(u=u[0]),r=u,(u=u[0]).push(2,0,r),r=0):" "===t||"\t"===t||"\n"===t||"\r"===t?(a(),r=2):o+=t),3===r&&"!--"===o&&(r=4,u=u[0])}return a(),u}(e)),t),arguments,[])).length>1?t:t[0]}var kt=function(e){var t=e.environment,n=t.document.createElementNS("http://www.w3.org/2000/svg","svg");n.setAttribute("class","aa-ClearIcon"),n.setAttribute("viewBox","0 0 24 24"),n.setAttribute("width","18"),n.setAttribute("height","18"),n.setAttribute("fill","currentColor");var r=t.document.createElementNS("http://www.w3.org/2000/svg","path");return r.setAttribute("d","M5.293 6.707l5.293 5.293-5.293 5.293c-0.391 0.391-0.391 1.024 0 1.414s1.024 0.391 1.414 0l5.293-5.293 5.293 5.293c0.391 0.391 1.024 0.391 1.414 0s0.391-1.024 0-1.414l-5.293-5.293 5.293-5.293c0.391-0.391 0.391-1.024 0-1.414s-1.024-0.391-1.414 0l-5.293 5.293-5.293-5.293c-0.391-0.391-1.024-0.391-1.414 0s-0.391 1.024 0 1.414z"),n.appendChild(r),n};function xt(e,t){if("string"==typeof t){var n=e.document.querySelector(t);return"The element ".concat(JSON.stringify(t)," is not in the document."),n}return t}function Nt(){for(var e=arguments.length,t=new Array(e),n=0;n2&&(u.children=arguments.length>3?Jt.call(arguments,2):n),"function"==typeof e&&null!=e.defaultProps)for(i in e.defaultProps)void 0===u[i]&&(u[i]=e.defaultProps[i]);return sn(e,u,r,o,null)}function sn(e,t,n,r,o){var i={type:e,props:t,key:n,ref:r,__k:null,__:null,__b:0,__e:null,__d:void 0,__c:null,__h:null,constructor:void 0,__v:null==o?++Yt:o};return null==o&&null!=Xt.vnode&&Xt.vnode(i),i}function fn(e){return e.children}function pn(e,t){this.props=e,this.context=t}function mn(e,t){if(null==t)return e.__?mn(e.__,e.__.__k.indexOf(e)+1):null;for(var n;tt&&Zt.sort(nn));yn.__r=0}function bn(e,t,n,r,o,i,u,a,l,c){var s,f,p,m,v,d,y,b=r&&r.__k||on,g=b.length;for(n.__k=[],s=0;s0?sn(m.type,m.props,m.key,m.ref?m.ref:null,m.__v):m)){if(m.__=n,m.__b=n.__b+1,null===(p=b[s])||p&&m.key==p.key&&m.type===p.type)b[s]=void 0;else for(f=0;f=0;t--)if((n=e.__k[t])&&(r=On(n)))return r;return null}function _n(e,t,n){"-"===t[0]?e.setProperty(t,null==n?"":n):e[t]=null==n?"":"number"!=typeof n||un.test(t)?n:n+"px"}function Sn(e,t,n,r,o){var i;e:if("style"===t)if("string"==typeof n)e.style.cssText=n;else{if("string"==typeof r&&(e.style.cssText=r=""),r)for(t in r)n&&t in n||_n(e.style,t,"");if(n)for(t in n)r&&n[t]===r[t]||_n(e.style,t,n[t])}else if("o"===t[0]&&"n"===t[1])i=t!==(t=t.replace(/Capture$/,"")),t=t.toLowerCase()in e?t.toLowerCase().slice(2):t.slice(2),e.l||(e.l={}),e.l[t+i]=n,n?r||e.addEventListener(t,i?Pn:jn,i):e.removeEventListener(t,i?Pn:jn,i);else if("dangerouslySetInnerHTML"!==t){if(o)t=t.replace(/xlink(H|:h)/,"h").replace(/sName$/,"s");else if("width"!==t&&"height"!==t&&"href"!==t&&"list"!==t&&"form"!==t&&"tabIndex"!==t&&"download"!==t&&t in e)try{e[t]=null==n?"":n;break e}catch(e){}"function"==typeof n||(null==n||!1===n&&"-"!==t[4]?e.removeAttribute(t):e.setAttribute(t,n))}}function jn(e){return this.l[e.type+!1](Xt.event?Xt.event(e):e)}function Pn(e){return this.l[e.type+!0](Xt.event?Xt.event(e):e)}function wn(e,t,n,r,o,i,u,a,l){var c,s,f,p,m,v,d,y,b,g,h,O,_,S,j,P=t.type;if(void 0!==t.constructor)return null;null!=n.__h&&(l=n.__h,a=t.__e=n.__e,t.__h=null,i=[a]),(c=Xt.__b)&&c(t);try{e:if("function"==typeof P){if(y=t.props,b=(c=P.contextType)&&r[c.__c],g=c?b?b.props.value:c.__:r,n.__c?d=(s=t.__c=n.__c).__=s.__E:("prototype"in P&&P.prototype.render?t.__c=s=new P(y,g):(t.__c=s=new pn(y,g),s.constructor=P,s.render=Cn),b&&b.sub(s),s.props=y,s.state||(s.state={}),s.context=g,s.__n=r,f=s.__d=!0,s.__h=[],s._sb=[]),null==s.__s&&(s.__s=s.state),null!=P.getDerivedStateFromProps&&(s.__s==s.state&&(s.__s=an({},s.__s)),an(s.__s,P.getDerivedStateFromProps(y,s.__s))),p=s.props,m=s.state,s.__v=t,f)null==P.getDerivedStateFromProps&&null!=s.componentWillMount&&s.componentWillMount(),null!=s.componentDidMount&&s.__h.push(s.componentDidMount);else{if(null==P.getDerivedStateFromProps&&y!==p&&null!=s.componentWillReceiveProps&&s.componentWillReceiveProps(y,g),!s.__e&&null!=s.shouldComponentUpdate&&!1===s.shouldComponentUpdate(y,s.__s,g)||t.__v===n.__v){for(t.__v!==n.__v&&(s.props=y,s.state=s.__s,s.__d=!1),s.__e=!1,t.__e=n.__e,t.__k=n.__k,t.__k.forEach((function(e){e&&(e.__=t)})),h=0;h0&&void 0!==arguments[0]?arguments[0]:[];return{get:function(){return e},add:function(t){var n=e[e.length-1];(null==n?void 0:n.isHighlighted)===t.isHighlighted?e[e.length-1]={value:n.value+t.value,isHighlighted:n.isHighlighted}:e.push(t)}}}(n?[{value:n,isHighlighted:!1}]:[]);return t.forEach((function(e){var t=e.split(xn);r.add({value:t[0],isHighlighted:!0}),""!==t[1]&&r.add({value:t[1],isHighlighted:!1})})),r.get()}function Tn(e){return function(e){if(Array.isArray(e))return qn(e)}(e)||function(e){if("undefined"!=typeof Symbol&&null!=e[Symbol.iterator]||null!=e["@@iterator"])return Array.from(e)}(e)||function(e,t){if(!e)return;if("string"==typeof e)return qn(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);"Object"===n&&e.constructor&&(n=e.constructor.name);if("Map"===n||"Set"===n)return Array.from(e);if("Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n))return qn(e,t)}(e)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function qn(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n",""":'"',"'":"'"},Fn=new RegExp(/\w/i),Ln=/&(amp|quot|lt|gt|#39);/g,Un=RegExp(Ln.source);function Mn(e,t){var n,r,o,i=e[t],u=(null===(n=e[t+1])||void 0===n?void 0:n.isHighlighted)||!0,a=(null===(r=e[t-1])||void 0===r?void 0:r.isHighlighted)||!0;return Fn.test((o=i.value)&&Un.test(o)?o.replace(Ln,(function(e){return Rn[e]})):o)||a!==u?i.isHighlighted:a}function Hn(e){return Hn="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},Hn(e)}function Vn(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function Wn(e){for(var t=1;te.length)&&(t=e.length);for(var n=0,r=new Array(t);n=0||(o[n]=e[n]);return o}(e,t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(o[n]=e[n])}return o}function ur(e){return function(e){if(Array.isArray(e))return ar(e)}(e)||function(e){if("undefined"!=typeof Symbol&&null!=e[Symbol.iterator]||null!=e["@@iterator"])return Array.from(e)}(e)||function(e,t){if(!e)return;if("string"==typeof e)return ar(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);"Object"===n&&e.constructor&&(n=e.constructor.name);if("Map"===n||"Set"===n)return Array.from(e);if("Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n))return ar(e,t)}(e)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function ar(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n0;if(!O.value.core.openOnFocus&&!t.query)return n;var r=Boolean(y.current||O.value.renderer.renderNoResults);return!n&&r||n},__autocomplete_metadata:{userAgents:br,options:e}}))})),j=f(n({collections:[],completion:null,context:{},isOpen:!1,query:"",activeItemId:null,status:"idle"},O.value.core.initialState)),P={getEnvironmentProps:O.value.renderer.getEnvironmentProps,getFormProps:O.value.renderer.getFormProps,getInputProps:O.value.renderer.getInputProps,getItemProps:O.value.renderer.getItemProps,getLabelProps:O.value.renderer.getLabelProps,getListProps:O.value.renderer.getListProps,getPanelProps:O.value.renderer.getPanelProps,getRootProps:O.value.renderer.getRootProps},w={setActiveItemId:S.value.setActiveItemId,setQuery:S.value.setQuery,setCollections:S.value.setCollections,setIsOpen:S.value.setIsOpen,setStatus:S.value.setStatus,setContext:S.value.setContext,refresh:S.value.refresh,navigator:S.value.navigator},I=m((function(){return Ct.bind(O.value.renderer.renderer.createElement)})),A=m((function(){return Gt({autocomplete:S.value,autocompleteScopeApi:w,classNames:O.value.renderer.classNames,environment:O.value.core.environment,isDetached:_.value,placeholder:O.value.core.placeholder,propGetters:P,setIsModalOpen:k,state:j.current,translations:O.value.renderer.translations})}));function E(){Ht(A.value.panel,{style:_.value?{}:yr({panelPlacement:O.value.renderer.panelPlacement,container:A.value.root,form:A.value.form,environment:O.value.core.environment})})}function D(e){j.current=e;var t={autocomplete:S.value,autocompleteScopeApi:w,classNames:O.value.renderer.classNames,components:O.value.renderer.components,container:O.value.renderer.container,html:I.value,dom:A.value,panelContainer:_.value?A.value.detachedContainer:O.value.renderer.panelContainer,propGetters:P,state:j.current,renderer:O.value.renderer.renderer},r=!b(e)&&!y.current&&O.value.renderer.renderNoResults||O.value.renderer.render;!function(e){var t=e.autocomplete,r=e.autocompleteScopeApi,o=e.dom,i=e.propGetters,u=e.state;Vt(o.root,i.getRootProps(n({state:u,props:t.getRootProps({})},r))),Vt(o.input,i.getInputProps(n({state:u,props:t.getInputProps({inputElement:o.input}),inputElement:o.input},r))),Ht(o.label,{hidden:"stalled"===u.status}),Ht(o.loadingIndicator,{hidden:"stalled"!==u.status}),Ht(o.clearButton,{hidden:!u.query}),Ht(o.detachedSearchButtonQuery,{textContent:u.query}),Ht(o.detachedSearchButtonPlaceholder,{hidden:Boolean(u.query)})}(t),function(e,t){var r=t.autocomplete,o=t.autocompleteScopeApi,u=t.classNames,a=t.html,l=t.dom,c=t.panelContainer,s=t.propGetters,f=t.state,p=t.components,m=t.renderer;if(f.isOpen){c.contains(l.panel)||"loading"===f.status||c.appendChild(l.panel),l.panel.classList.toggle("aa-Panel--stalled","stalled"===f.status);var v=f.collections.filter((function(e){var t=e.source,n=e.items;return t.templates.noResults||n.length>0})).map((function(e,t){var l=e.source,c=e.items;return m.createElement("section",{key:t,className:u.source,"data-autocomplete-source-id":l.sourceId},l.templates.header&&m.createElement("div",{className:u.sourceHeader},l.templates.header({components:p,createElement:m.createElement,Fragment:m.Fragment,items:c,source:l,state:f,html:a})),l.templates.noResults&&0===c.length?m.createElement("div",{className:u.sourceNoResults},l.templates.noResults({components:p,createElement:m.createElement,Fragment:m.Fragment,source:l,state:f,html:a})):m.createElement("ul",i({className:u.list},s.getListProps(n({state:f,props:r.getListProps({source:l})},o))),c.map((function(e){var t=r.getItemProps({item:e,source:l});return m.createElement("li",i({key:t.id,className:u.item},s.getItemProps(n({state:f,props:t},o))),l.templates.item({components:p,createElement:m.createElement,Fragment:m.Fragment,item:e,state:f,html:a}))}))),l.templates.footer&&m.createElement("div",{className:u.sourceFooter},l.templates.footer({components:p,createElement:m.createElement,Fragment:m.Fragment,items:c,source:l,state:f,html:a})))})),d=m.createElement(m.Fragment,null,m.createElement("div",{className:u.panelLayout},v),m.createElement("div",{className:"aa-GradientBottom"})),y=v.reduce((function(e,t){return e[t.props["data-autocomplete-source-id"]]=t,e}),{});e(n(n({children:d,state:f,sections:v,elements:y},m),{},{components:p,html:a},o),l.panel)}else c.contains(l.panel)&&c.removeChild(l.panel)}(r,t)}function C(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};l();var t=O.value.renderer,n=t.components,r=u(t,gr);g.current=qt(r,O.value.core,{components:Bt(n,(function(e){return!e.value.hasOwnProperty("__autocomplete_componentName")})),initialState:j.current},e),v(),c(),S.value.refresh().then((function(){D(j.current)}))}function k(e){requestAnimationFrame((function(){var t=O.value.core.environment.document.body.contains(A.value.detachedOverlay);e!==t&&(e?(O.value.core.environment.document.body.appendChild(A.value.detachedOverlay),O.value.core.environment.document.body.classList.add("aa-Detached"),A.value.input.focus()):(O.value.core.environment.document.body.removeChild(A.value.detachedOverlay),O.value.core.environment.document.body.classList.remove("aa-Detached")))}))}return a((function(){var e=S.value.getEnvironmentProps({formElement:A.value.form,panelElement:A.value.panel,inputElement:A.value.input});return Ht(O.value.core.environment,e),function(){Ht(O.value.core.environment,Object.keys(e).reduce((function(e,t){return n(n({},e),{},o({},t,void 0))}),{}))}})),a((function(){var e=_.value?O.value.core.environment.document.body:O.value.renderer.panelContainer,t=_.value?A.value.detachedOverlay:A.value.panel;return _.value&&j.current.isOpen&&k(!0),D(j.current),function(){e.contains(t)&&e.removeChild(t)}})),a((function(){var e=O.value.renderer.container;return e.appendChild(A.value.root),function(){e.removeChild(A.value.root)}})),a((function(){var e=p((function(e){D(e.state)}),0);return h.current=function(t){var n=t.state,r=t.prevState;(_.value&&r.isOpen!==n.isOpen&&k(n.isOpen),_.value||!n.isOpen||r.isOpen||E(),n.query!==r.query)&&O.value.core.environment.document.querySelectorAll(".aa-Panel--scrollable").forEach((function(e){0!==e.scrollTop&&(e.scrollTop=0)}));e({state:n})},function(){h.current=void 0}})),a((function(){var e=p((function(){var e=_.value;_.value=O.value.core.environment.matchMedia(O.value.renderer.detachedMediaQuery).matches,e!==_.value?C({}):requestAnimationFrame(E)}),20);return O.value.core.environment.addEventListener("resize",e),function(){O.value.core.environment.removeEventListener("resize",e)}})),a((function(){if(!_.value)return function(){};function e(e){A.value.detachedContainer.classList.toggle("aa-DetachedContainer--modal",e)}function t(t){e(t.matches)}var n=O.value.core.environment.matchMedia(getComputedStyle(O.value.core.environment.document.documentElement).getPropertyValue("--aa-detached-modal-media-query"));e(n.matches);var r=Boolean(n.addEventListener);return r?n.addEventListener("change",t):n.addListener(t),function(){r?n.removeEventListener("change",t):n.removeListener(t)}})),a((function(){return requestAnimationFrame(E),function(){}})),n(n({},w),{},{update:C,destroy:function(){l()}})},e.getAlgoliaFacets=function(e){var t=hr({transformResponse:function(e){return e.facetHits}}),r=e.queries.map((function(e){return n(n({},e),{},{type:"facet"})}));return t(n(n({},e),{},{queries:r}))},e.getAlgoliaResults=Or,Object.defineProperty(e,"__esModule",{value:!0})})); + diff --git a/site_libs/quarto-search/fuse.min.js b/site_libs/quarto-search/fuse.min.js new file mode 100644 index 00000000..adc28356 --- /dev/null +++ b/site_libs/quarto-search/fuse.min.js @@ -0,0 +1,9 @@ +/** + * Fuse.js v6.6.2 - Lightweight fuzzy-search (http://fusejs.io) + * + * Copyright (c) 2022 Kiro Risk (http://kiro.me) + * All Rights Reserved. Apache Software License 2.0 + * + * http://www.apache.org/licenses/LICENSE-2.0 + */ +var e,t;e=this,t=function(){"use strict";function e(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function t(t){for(var n=1;ne.length)&&(t=e.length);for(var n=0,r=new Array(t);n0&&void 0!==arguments[0]?arguments[0]:1,t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:3,n=new Map,r=Math.pow(10,t);return{get:function(t){var i=t.match(C).length;if(n.has(i))return n.get(i);var o=1/Math.pow(i,.5*e),c=parseFloat(Math.round(o*r)/r);return n.set(i,c),c},clear:function(){n.clear()}}}var $=function(){function e(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},n=t.getFn,i=void 0===n?I.getFn:n,o=t.fieldNormWeight,c=void 0===o?I.fieldNormWeight:o;r(this,e),this.norm=E(c,3),this.getFn=i,this.isCreated=!1,this.setIndexRecords()}return o(e,[{key:"setSources",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[];this.docs=e}},{key:"setIndexRecords",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[];this.records=e}},{key:"setKeys",value:function(){var e=this,t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[];this.keys=t,this._keysMap={},t.forEach((function(t,n){e._keysMap[t.id]=n}))}},{key:"create",value:function(){var e=this;!this.isCreated&&this.docs.length&&(this.isCreated=!0,g(this.docs[0])?this.docs.forEach((function(t,n){e._addString(t,n)})):this.docs.forEach((function(t,n){e._addObject(t,n)})),this.norm.clear())}},{key:"add",value:function(e){var t=this.size();g(e)?this._addString(e,t):this._addObject(e,t)}},{key:"removeAt",value:function(e){this.records.splice(e,1);for(var t=e,n=this.size();t2&&void 0!==arguments[2]?arguments[2]:{},r=n.getFn,i=void 0===r?I.getFn:r,o=n.fieldNormWeight,c=void 0===o?I.fieldNormWeight:o,a=new $({getFn:i,fieldNormWeight:c});return a.setKeys(e.map(_)),a.setSources(t),a.create(),a}function R(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=t.errors,r=void 0===n?0:n,i=t.currentLocation,o=void 0===i?0:i,c=t.expectedLocation,a=void 0===c?0:c,s=t.distance,u=void 0===s?I.distance:s,h=t.ignoreLocation,l=void 0===h?I.ignoreLocation:h,f=r/e.length;if(l)return f;var d=Math.abs(a-o);return u?f+d/u:d?1:f}function N(){for(var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[],t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:I.minMatchCharLength,n=[],r=-1,i=-1,o=0,c=e.length;o=t&&n.push([r,i]),r=-1)}return e[o-1]&&o-r>=t&&n.push([r,o-1]),n}var P=32;function W(e){for(var t={},n=0,r=e.length;n1&&void 0!==arguments[1]?arguments[1]:{},o=i.location,c=void 0===o?I.location:o,a=i.threshold,s=void 0===a?I.threshold:a,u=i.distance,h=void 0===u?I.distance:u,l=i.includeMatches,f=void 0===l?I.includeMatches:l,d=i.findAllMatches,v=void 0===d?I.findAllMatches:d,g=i.minMatchCharLength,y=void 0===g?I.minMatchCharLength:g,p=i.isCaseSensitive,m=void 0===p?I.isCaseSensitive:p,k=i.ignoreLocation,M=void 0===k?I.ignoreLocation:k;if(r(this,e),this.options={location:c,threshold:s,distance:h,includeMatches:f,findAllMatches:v,minMatchCharLength:y,isCaseSensitive:m,ignoreLocation:M},this.pattern=m?t:t.toLowerCase(),this.chunks=[],this.pattern.length){var b=function(e,t){n.chunks.push({pattern:e,alphabet:W(e),startIndex:t})},x=this.pattern.length;if(x>P){for(var w=0,L=x%P,S=x-L;w3&&void 0!==arguments[3]?arguments[3]:{},i=r.location,o=void 0===i?I.location:i,c=r.distance,a=void 0===c?I.distance:c,s=r.threshold,u=void 0===s?I.threshold:s,h=r.findAllMatches,l=void 0===h?I.findAllMatches:h,f=r.minMatchCharLength,d=void 0===f?I.minMatchCharLength:f,v=r.includeMatches,g=void 0===v?I.includeMatches:v,y=r.ignoreLocation,p=void 0===y?I.ignoreLocation:y;if(t.length>P)throw new Error(w(P));for(var m,k=t.length,M=e.length,b=Math.max(0,Math.min(o,M)),x=u,L=b,S=d>1||g,_=S?Array(M):[];(m=e.indexOf(t,L))>-1;){var O=R(t,{currentLocation:m,expectedLocation:b,distance:a,ignoreLocation:p});if(x=Math.min(O,x),L=m+k,S)for(var j=0;j=z;q-=1){var B=q-1,J=n[e.charAt(B)];if(S&&(_[B]=+!!J),K[q]=(K[q+1]<<1|1)&J,F&&(K[q]|=(A[q+1]|A[q])<<1|1|A[q+1]),K[q]&$&&(C=R(t,{errors:F,currentLocation:B,expectedLocation:b,distance:a,ignoreLocation:p}))<=x){if(x=C,(L=B)<=b)break;z=Math.max(1,2*b-L)}}if(R(t,{errors:F+1,currentLocation:b,expectedLocation:b,distance:a,ignoreLocation:p})>x)break;A=K}var U={isMatch:L>=0,score:Math.max(.001,C)};if(S){var V=N(_,d);V.length?g&&(U.indices=V):U.isMatch=!1}return U}(e,n,i,{location:c+o,distance:a,threshold:s,findAllMatches:u,minMatchCharLength:h,includeMatches:r,ignoreLocation:l}),p=y.isMatch,m=y.score,k=y.indices;p&&(g=!0),v+=m,p&&k&&(d=[].concat(f(d),f(k)))}));var y={isMatch:g,score:g?v/this.chunks.length:1};return g&&r&&(y.indices=d),y}}]),e}(),z=function(){function e(t){r(this,e),this.pattern=t}return o(e,[{key:"search",value:function(){}}],[{key:"isMultiMatch",value:function(e){return D(e,this.multiRegex)}},{key:"isSingleMatch",value:function(e){return D(e,this.singleRegex)}}]),e}();function D(e,t){var n=e.match(t);return n?n[1]:null}var K=function(e){a(n,e);var t=l(n);function n(e){return r(this,n),t.call(this,e)}return o(n,[{key:"search",value:function(e){var t=e===this.pattern;return{isMatch:t,score:t?0:1,indices:[0,this.pattern.length-1]}}}],[{key:"type",get:function(){return"exact"}},{key:"multiRegex",get:function(){return/^="(.*)"$/}},{key:"singleRegex",get:function(){return/^=(.*)$/}}]),n}(z),q=function(e){a(n,e);var t=l(n);function n(e){return r(this,n),t.call(this,e)}return o(n,[{key:"search",value:function(e){var t=-1===e.indexOf(this.pattern);return{isMatch:t,score:t?0:1,indices:[0,e.length-1]}}}],[{key:"type",get:function(){return"inverse-exact"}},{key:"multiRegex",get:function(){return/^!"(.*)"$/}},{key:"singleRegex",get:function(){return/^!(.*)$/}}]),n}(z),B=function(e){a(n,e);var t=l(n);function n(e){return r(this,n),t.call(this,e)}return o(n,[{key:"search",value:function(e){var t=e.startsWith(this.pattern);return{isMatch:t,score:t?0:1,indices:[0,this.pattern.length-1]}}}],[{key:"type",get:function(){return"prefix-exact"}},{key:"multiRegex",get:function(){return/^\^"(.*)"$/}},{key:"singleRegex",get:function(){return/^\^(.*)$/}}]),n}(z),J=function(e){a(n,e);var t=l(n);function n(e){return r(this,n),t.call(this,e)}return o(n,[{key:"search",value:function(e){var t=!e.startsWith(this.pattern);return{isMatch:t,score:t?0:1,indices:[0,e.length-1]}}}],[{key:"type",get:function(){return"inverse-prefix-exact"}},{key:"multiRegex",get:function(){return/^!\^"(.*)"$/}},{key:"singleRegex",get:function(){return/^!\^(.*)$/}}]),n}(z),U=function(e){a(n,e);var t=l(n);function n(e){return r(this,n),t.call(this,e)}return o(n,[{key:"search",value:function(e){var t=e.endsWith(this.pattern);return{isMatch:t,score:t?0:1,indices:[e.length-this.pattern.length,e.length-1]}}}],[{key:"type",get:function(){return"suffix-exact"}},{key:"multiRegex",get:function(){return/^"(.*)"\$$/}},{key:"singleRegex",get:function(){return/^(.*)\$$/}}]),n}(z),V=function(e){a(n,e);var t=l(n);function n(e){return r(this,n),t.call(this,e)}return o(n,[{key:"search",value:function(e){var t=!e.endsWith(this.pattern);return{isMatch:t,score:t?0:1,indices:[0,e.length-1]}}}],[{key:"type",get:function(){return"inverse-suffix-exact"}},{key:"multiRegex",get:function(){return/^!"(.*)"\$$/}},{key:"singleRegex",get:function(){return/^!(.*)\$$/}}]),n}(z),G=function(e){a(n,e);var t=l(n);function n(e){var i,o=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},c=o.location,a=void 0===c?I.location:c,s=o.threshold,u=void 0===s?I.threshold:s,h=o.distance,l=void 0===h?I.distance:h,f=o.includeMatches,d=void 0===f?I.includeMatches:f,v=o.findAllMatches,g=void 0===v?I.findAllMatches:v,y=o.minMatchCharLength,p=void 0===y?I.minMatchCharLength:y,m=o.isCaseSensitive,k=void 0===m?I.isCaseSensitive:m,M=o.ignoreLocation,b=void 0===M?I.ignoreLocation:M;return r(this,n),(i=t.call(this,e))._bitapSearch=new T(e,{location:a,threshold:u,distance:l,includeMatches:d,findAllMatches:g,minMatchCharLength:p,isCaseSensitive:k,ignoreLocation:b}),i}return o(n,[{key:"search",value:function(e){return this._bitapSearch.searchIn(e)}}],[{key:"type",get:function(){return"fuzzy"}},{key:"multiRegex",get:function(){return/^"(.*)"$/}},{key:"singleRegex",get:function(){return/^(.*)$/}}]),n}(z),H=function(e){a(n,e);var t=l(n);function n(e){return r(this,n),t.call(this,e)}return o(n,[{key:"search",value:function(e){for(var t,n=0,r=[],i=this.pattern.length;(t=e.indexOf(this.pattern,n))>-1;)n=t+i,r.push([t,n-1]);var o=!!r.length;return{isMatch:o,score:o?0:1,indices:r}}}],[{key:"type",get:function(){return"include"}},{key:"multiRegex",get:function(){return/^'"(.*)"$/}},{key:"singleRegex",get:function(){return/^'(.*)$/}}]),n}(z),Q=[K,H,B,J,V,U,q,G],X=Q.length,Y=/ +(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)/;function Z(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return e.split("|").map((function(e){for(var n=e.trim().split(Y).filter((function(e){return e&&!!e.trim()})),r=[],i=0,o=n.length;i1&&void 0!==arguments[1]?arguments[1]:{},i=n.isCaseSensitive,o=void 0===i?I.isCaseSensitive:i,c=n.includeMatches,a=void 0===c?I.includeMatches:c,s=n.minMatchCharLength,u=void 0===s?I.minMatchCharLength:s,h=n.ignoreLocation,l=void 0===h?I.ignoreLocation:h,f=n.findAllMatches,d=void 0===f?I.findAllMatches:f,v=n.location,g=void 0===v?I.location:v,y=n.threshold,p=void 0===y?I.threshold:y,m=n.distance,k=void 0===m?I.distance:m;r(this,e),this.query=null,this.options={isCaseSensitive:o,includeMatches:a,minMatchCharLength:u,findAllMatches:d,ignoreLocation:l,location:g,threshold:p,distance:k},this.pattern=o?t:t.toLowerCase(),this.query=Z(this.pattern,this.options)}return o(e,[{key:"searchIn",value:function(e){var t=this.query;if(!t)return{isMatch:!1,score:1};var n=this.options,r=n.includeMatches;e=n.isCaseSensitive?e:e.toLowerCase();for(var i=0,o=[],c=0,a=0,s=t.length;a-1&&(n.refIndex=e.idx),t.matches.push(n)}}))}function ve(e,t){t.score=e.score}function ge(e,t){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},r=n.includeMatches,i=void 0===r?I.includeMatches:r,o=n.includeScore,c=void 0===o?I.includeScore:o,a=[];return i&&a.push(de),c&&a.push(ve),e.map((function(e){var n=e.idx,r={item:t[n],refIndex:n};return a.length&&a.forEach((function(t){t(e,r)})),r}))}var ye=function(){function e(n){var i=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},o=arguments.length>2?arguments[2]:void 0;r(this,e),this.options=t(t({},I),i),this.options.useExtendedSearch,this._keyStore=new S(this.options.keys),this.setCollection(n,o)}return o(e,[{key:"setCollection",value:function(e,t){if(this._docs=e,t&&!(t instanceof $))throw new Error("Incorrect 'index' type");this._myIndex=t||F(this.options.keys,this._docs,{getFn:this.options.getFn,fieldNormWeight:this.options.fieldNormWeight})}},{key:"add",value:function(e){k(e)&&(this._docs.push(e),this._myIndex.add(e))}},{key:"remove",value:function(){for(var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:function(){return!1},t=[],n=0,r=this._docs.length;n1&&void 0!==arguments[1]?arguments[1]:{},n=t.limit,r=void 0===n?-1:n,i=this.options,o=i.includeMatches,c=i.includeScore,a=i.shouldSort,s=i.sortFn,u=i.ignoreFieldNorm,h=g(e)?g(this._docs[0])?this._searchStringList(e):this._searchObjectList(e):this._searchLogical(e);return fe(h,{ignoreFieldNorm:u}),a&&h.sort(s),y(r)&&r>-1&&(h=h.slice(0,r)),ge(h,this._docs,{includeMatches:o,includeScore:c})}},{key:"_searchStringList",value:function(e){var t=re(e,this.options),n=this._myIndex.records,r=[];return n.forEach((function(e){var n=e.v,i=e.i,o=e.n;if(k(n)){var c=t.searchIn(n),a=c.isMatch,s=c.score,u=c.indices;a&&r.push({item:n,idx:i,matches:[{score:s,value:n,norm:o,indices:u}]})}})),r}},{key:"_searchLogical",value:function(e){var t=this,n=function(e,t){var n=(arguments.length>2&&void 0!==arguments[2]?arguments[2]:{}).auto,r=void 0===n||n,i=function e(n){var i=Object.keys(n),o=ue(n);if(!o&&i.length>1&&!se(n))return e(le(n));if(he(n)){var c=o?n[ce]:i[0],a=o?n[ae]:n[c];if(!g(a))throw new Error(x(c));var s={keyId:j(c),pattern:a};return r&&(s.searcher=re(a,t)),s}var u={children:[],operator:i[0]};return i.forEach((function(t){var r=n[t];v(r)&&r.forEach((function(t){u.children.push(e(t))}))})),u};return se(e)||(e=le(e)),i(e)}(e,this.options),r=function e(n,r,i){if(!n.children){var o=n.keyId,c=n.searcher,a=t._findMatches({key:t._keyStore.get(o),value:t._myIndex.getValueForItemAtKeyId(r,o),searcher:c});return a&&a.length?[{idx:i,item:r,matches:a}]:[]}for(var s=[],u=0,h=n.children.length;u1&&void 0!==arguments[1]?arguments[1]:{},n=t.getFn,r=void 0===n?I.getFn:n,i=t.fieldNormWeight,o=void 0===i?I.fieldNormWeight:i,c=e.keys,a=e.records,s=new $({getFn:r,fieldNormWeight:o});return s.setKeys(c),s.setIndexRecords(a),s},ye.config=I,function(){ne.push.apply(ne,arguments)}(te),ye},"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).Fuse=t(); \ No newline at end of file diff --git a/site_libs/quarto-search/quarto-search.js b/site_libs/quarto-search/quarto-search.js new file mode 100644 index 00000000..d788a958 --- /dev/null +++ b/site_libs/quarto-search/quarto-search.js @@ -0,0 +1,1290 @@ +const kQueryArg = "q"; +const kResultsArg = "show-results"; + +// If items don't provide a URL, then both the navigator and the onSelect +// function aren't called (and therefore, the default implementation is used) +// +// We're using this sentinel URL to signal to those handlers that this +// item is a more item (along with the type) and can be handled appropriately +const kItemTypeMoreHref = "0767FDFD-0422-4E5A-BC8A-3BE11E5BBA05"; + +window.document.addEventListener("DOMContentLoaded", function (_event) { + // Ensure that search is available on this page. If it isn't, + // should return early and not do anything + var searchEl = window.document.getElementById("quarto-search"); + if (!searchEl) return; + + const { autocomplete } = window["@algolia/autocomplete-js"]; + + let quartoSearchOptions = {}; + let language = {}; + const searchOptionEl = window.document.getElementById( + "quarto-search-options" + ); + if (searchOptionEl) { + const jsonStr = searchOptionEl.textContent; + quartoSearchOptions = JSON.parse(jsonStr); + language = quartoSearchOptions.language; + } + + // note the search mode + if (quartoSearchOptions.type === "overlay") { + searchEl.classList.add("type-overlay"); + } else { + searchEl.classList.add("type-textbox"); + } + + // Used to determine highlighting behavior for this page + // A `q` query param is expected when the user follows a search + // to this page + const currentUrl = new URL(window.location); + const query = currentUrl.searchParams.get(kQueryArg); + const showSearchResults = currentUrl.searchParams.get(kResultsArg); + const mainEl = window.document.querySelector("main"); + + // highlight matches on the page + if (query && mainEl) { + // perform any highlighting + highlight(escapeRegExp(query), mainEl); + + // fix up the URL to remove the q query param + const replacementUrl = new URL(window.location); + replacementUrl.searchParams.delete(kQueryArg); + window.history.replaceState({}, "", replacementUrl); + } + + // function to clear highlighting on the page when the search query changes + // (e.g. if the user edits the query or clears it) + let highlighting = true; + const resetHighlighting = (searchTerm) => { + if (mainEl && highlighting && query && searchTerm !== query) { + clearHighlight(query, mainEl); + highlighting = false; + } + }; + + // Clear search highlighting when the user scrolls sufficiently + const resetFn = () => { + resetHighlighting(""); + window.removeEventListener("quarto-hrChanged", resetFn); + window.removeEventListener("quarto-sectionChanged", resetFn); + }; + + // Register this event after the initial scrolling and settling of events + // on the page + window.addEventListener("quarto-hrChanged", resetFn); + window.addEventListener("quarto-sectionChanged", resetFn); + + // Responsively switch to overlay mode if the search is present on the navbar + // Note that switching the sidebar to overlay mode requires more coordinate (not just + // the media query since we generate different HTML for sidebar overlays than we do + // for sidebar input UI) + const detachedMediaQuery = + quartoSearchOptions.type === "overlay" ? "all" : "(max-width: 991px)"; + + // If configured, include the analytics client to send insights + const plugins = configurePlugins(quartoSearchOptions); + + let lastState = null; + const { setIsOpen, setQuery, setCollections } = autocomplete({ + container: searchEl, + detachedMediaQuery: detachedMediaQuery, + defaultActiveItemId: 0, + panelContainer: "#quarto-search-results", + panelPlacement: quartoSearchOptions["panel-placement"], + debug: false, + openOnFocus: true, + plugins, + classNames: { + form: "d-flex", + }, + placeholder: language["search-text-placeholder"], + translations: { + clearButtonTitle: language["search-clear-button-title"], + detachedCancelButtonText: language["search-detached-cancel-button-title"], + submitButtonTitle: language["search-submit-button-title"], + }, + initialState: { + query, + }, + getItemUrl({ item }) { + return item.href; + }, + onStateChange({ state }) { + // If this is a file URL, note that + + // Perhaps reset highlighting + resetHighlighting(state.query); + + // If the panel just opened, ensure the panel is positioned properly + if (state.isOpen) { + if (lastState && !lastState.isOpen) { + setTimeout(() => { + positionPanel(quartoSearchOptions["panel-placement"]); + }, 150); + } + } + + // Perhaps show the copy link + showCopyLink(state.query, quartoSearchOptions); + + lastState = state; + }, + reshape({ sources, state }) { + return sources.map((source) => { + try { + const items = source.getItems(); + + // Validate the items + validateItems(items); + + // group the items by document + const groupedItems = new Map(); + items.forEach((item) => { + const hrefParts = item.href.split("#"); + const baseHref = hrefParts[0]; + const isDocumentItem = hrefParts.length === 1; + + const items = groupedItems.get(baseHref); + if (!items) { + groupedItems.set(baseHref, [item]); + } else { + // If the href for this item matches the document + // exactly, place this item first as it is the item that represents + // the document itself + if (isDocumentItem) { + items.unshift(item); + } else { + items.push(item); + } + groupedItems.set(baseHref, items); + } + }); + + const reshapedItems = []; + let count = 1; + for (const [_key, value] of groupedItems) { + const firstItem = value[0]; + reshapedItems.push({ + ...firstItem, + type: kItemTypeDoc, + }); + + const collapseMatches = quartoSearchOptions["collapse-after"]; + const collapseCount = + typeof collapseMatches === "number" ? collapseMatches : 1; + + if (value.length > 1) { + const target = `search-more-${count}`; + const isExpanded = + state.context.expanded && + state.context.expanded.includes(target); + + const remainingCount = value.length - collapseCount; + + for (let i = 1; i < value.length; i++) { + if (collapseMatches && i === collapseCount) { + reshapedItems.push({ + target, + title: isExpanded + ? language["search-hide-matches-text"] + : remainingCount === 1 + ? `${remainingCount} ${language["search-more-match-text"]}` + : `${remainingCount} ${language["search-more-matches-text"]}`, + type: kItemTypeMore, + href: kItemTypeMoreHref, + }); + } + + if (isExpanded || !collapseMatches || i < collapseCount) { + reshapedItems.push({ + ...value[i], + type: kItemTypeItem, + target, + }); + } + } + } + count += 1; + } + + return { + ...source, + getItems() { + return reshapedItems; + }, + }; + } catch (error) { + // Some form of error occurred + return { + ...source, + getItems() { + return [ + { + title: error.name || "An Error Occurred While Searching", + text: + error.message || + "An unknown error occurred while attempting to perform the requested search.", + type: kItemTypeError, + }, + ]; + }, + }; + } + }); + }, + navigator: { + navigate({ itemUrl }) { + if (itemUrl !== offsetURL(kItemTypeMoreHref)) { + window.location.assign(itemUrl); + } + }, + navigateNewTab({ itemUrl }) { + if (itemUrl !== offsetURL(kItemTypeMoreHref)) { + const windowReference = window.open(itemUrl, "_blank", "noopener"); + if (windowReference) { + windowReference.focus(); + } + } + }, + navigateNewWindow({ itemUrl }) { + if (itemUrl !== offsetURL(kItemTypeMoreHref)) { + window.open(itemUrl, "_blank", "noopener"); + } + }, + }, + getSources({ state, setContext, setActiveItemId, refresh }) { + return [ + { + sourceId: "documents", + getItemUrl({ item }) { + if (item.href) { + return offsetURL(item.href); + } else { + return undefined; + } + }, + onSelect({ + item, + state, + setContext, + setIsOpen, + setActiveItemId, + refresh, + }) { + if (item.type === kItemTypeMore) { + toggleExpanded(item, state, setContext, setActiveItemId, refresh); + + // Toggle more + setIsOpen(true); + } + }, + getItems({ query }) { + if (query === null || query === "") { + return []; + } + + const limit = quartoSearchOptions.limit; + if (quartoSearchOptions.algolia) { + return algoliaSearch(query, limit, quartoSearchOptions.algolia); + } else { + // Fuse search options + const fuseSearchOptions = { + isCaseSensitive: false, + shouldSort: true, + minMatchCharLength: 2, + limit: limit, + }; + + return readSearchData().then(function (fuse) { + return fuseSearch(query, fuse, fuseSearchOptions); + }); + } + }, + templates: { + noResults({ createElement }) { + const hasQuery = lastState.query; + + return createElement( + "div", + { + class: `quarto-search-no-results${ + hasQuery ? "" : " no-query" + }`, + }, + language["search-no-results-text"] + ); + }, + header({ items, createElement }) { + // count the documents + const count = items.filter((item) => { + return item.type === kItemTypeDoc; + }).length; + + if (count > 0) { + return createElement( + "div", + { class: "search-result-header" }, + `${count} ${language["search-matching-documents-text"]}` + ); + } else { + return createElement( + "div", + { class: "search-result-header-no-results" }, + `` + ); + } + }, + footer({ _items, createElement }) { + if ( + quartoSearchOptions.algolia && + quartoSearchOptions.algolia["show-logo"] + ) { + const libDir = quartoSearchOptions.algolia["libDir"]; + const logo = createElement("img", { + src: offsetURL( + `${libDir}/quarto-search/search-by-algolia.svg` + ), + class: "algolia-search-logo", + }); + return createElement( + "a", + { href: "http://www.algolia.com/" }, + logo + ); + } + }, + + item({ item, createElement }) { + return renderItem( + item, + createElement, + state, + setActiveItemId, + setContext, + refresh, + quartoSearchOptions + ); + }, + }, + }, + ]; + }, + }); + + window.quartoOpenSearch = () => { + setIsOpen(false); + setIsOpen(true); + focusSearchInput(); + }; + + document.addEventListener("keyup", (event) => { + const { key } = event; + const kbds = quartoSearchOptions["keyboard-shortcut"]; + const focusedEl = document.activeElement; + + const isFormElFocused = [ + "input", + "select", + "textarea", + "button", + "option", + ].find((tag) => { + return focusedEl.tagName.toLowerCase() === tag; + }); + + if ( + kbds && + kbds.includes(key) && + !isFormElFocused && + !document.activeElement.isContentEditable + ) { + event.preventDefault(); + window.quartoOpenSearch(); + } + }); + + // Remove the labeleledby attribute since it is pointing + // to a non-existent label + if (quartoSearchOptions.type === "overlay") { + const inputEl = window.document.querySelector( + "#quarto-search .aa-Autocomplete" + ); + if (inputEl) { + inputEl.removeAttribute("aria-labelledby"); + } + } + + function throttle(func, wait) { + let waiting = false; + return function () { + if (!waiting) { + func.apply(this, arguments); + waiting = true; + setTimeout(function () { + waiting = false; + }, wait); + } + }; + } + + // If the main document scrolls dismiss the search results + // (otherwise, since they're floating in the document they can scroll with the document) + window.document.body.onscroll = throttle(() => { + // Only do this if we're not detached + // Bug #7117 + // This will happen when the keyboard is shown on ios (resulting in a scroll) + // which then closed the search UI + if (!window.matchMedia(detachedMediaQuery).matches) { + setIsOpen(false); + } + }, 50); + + if (showSearchResults) { + setIsOpen(true); + focusSearchInput(); + } +}); + +function configurePlugins(quartoSearchOptions) { + const autocompletePlugins = []; + const algoliaOptions = quartoSearchOptions.algolia; + if ( + algoliaOptions && + algoliaOptions["analytics-events"] && + algoliaOptions["search-only-api-key"] && + algoliaOptions["application-id"] + ) { + const apiKey = algoliaOptions["search-only-api-key"]; + const appId = algoliaOptions["application-id"]; + + // Aloglia insights may not be loaded because they require cookie consent + // Use deferred loading so events will start being recorded when/if consent + // is granted. + const algoliaInsightsDeferredPlugin = deferredLoadPlugin(() => { + if ( + window.aa && + window["@algolia/autocomplete-plugin-algolia-insights"] + ) { + window.aa("init", { + appId, + apiKey, + useCookie: true, + }); + + const { createAlgoliaInsightsPlugin } = + window["@algolia/autocomplete-plugin-algolia-insights"]; + // Register the insights client + const algoliaInsightsPlugin = createAlgoliaInsightsPlugin({ + insightsClient: window.aa, + onItemsChange({ insights, insightsEvents }) { + const events = insightsEvents.flatMap((event) => { + // This API limits the number of items per event to 20 + const chunkSize = 20; + const itemChunks = []; + const eventItems = event.items; + for (let i = 0; i < eventItems.length; i += chunkSize) { + itemChunks.push(eventItems.slice(i, i + chunkSize)); + } + // Split the items into multiple events that can be sent + const events = itemChunks.map((items) => { + return { + ...event, + items, + }; + }); + return events; + }); + + for (const event of events) { + insights.viewedObjectIDs(event); + } + }, + }); + return algoliaInsightsPlugin; + } + }); + + // Add the plugin + autocompletePlugins.push(algoliaInsightsDeferredPlugin); + return autocompletePlugins; + } +} + +// For plugins that may not load immediately, create a wrapper +// plugin and forward events and plugin data once the plugin +// is initialized. This is useful for cases like cookie consent +// which may prevent the analytics insights event plugin from initializing +// immediately. +function deferredLoadPlugin(createPlugin) { + let plugin = undefined; + let subscribeObj = undefined; + const wrappedPlugin = () => { + if (!plugin && subscribeObj) { + plugin = createPlugin(); + if (plugin && plugin.subscribe) { + plugin.subscribe(subscribeObj); + } + } + return plugin; + }; + + return { + subscribe: (obj) => { + subscribeObj = obj; + }, + onStateChange: (obj) => { + const plugin = wrappedPlugin(); + if (plugin && plugin.onStateChange) { + plugin.onStateChange(obj); + } + }, + onSubmit: (obj) => { + const plugin = wrappedPlugin(); + if (plugin && plugin.onSubmit) { + plugin.onSubmit(obj); + } + }, + onReset: (obj) => { + const plugin = wrappedPlugin(); + if (plugin && plugin.onReset) { + plugin.onReset(obj); + } + }, + getSources: (obj) => { + const plugin = wrappedPlugin(); + if (plugin && plugin.getSources) { + return plugin.getSources(obj); + } else { + return Promise.resolve([]); + } + }, + data: (obj) => { + const plugin = wrappedPlugin(); + if (plugin && plugin.data) { + plugin.data(obj); + } + }, + }; +} + +function validateItems(items) { + // Validate the first item + if (items.length > 0) { + const item = items[0]; + const missingFields = []; + if (item.href == undefined) { + missingFields.push("href"); + } + if (!item.title == undefined) { + missingFields.push("title"); + } + if (!item.text == undefined) { + missingFields.push("text"); + } + + if (missingFields.length === 1) { + throw { + name: `Error: Search index is missing the ${missingFields[0]} field.`, + message: `The items being returned for this search do not include all the required fields. Please ensure that your index items include the ${missingFields[0]} field or use index-fields in your _quarto.yml file to specify the field names.`, + }; + } else if (missingFields.length > 1) { + const missingFieldList = missingFields + .map((field) => { + return `${field}`; + }) + .join(", "); + + throw { + name: `Error: Search index is missing the following fields: ${missingFieldList}.`, + message: `The items being returned for this search do not include all the required fields. Please ensure that your index items includes the following fields: ${missingFieldList}, or use index-fields in your _quarto.yml file to specify the field names.`, + }; + } + } +} + +let lastQuery = null; +function showCopyLink(query, options) { + const language = options.language; + lastQuery = query; + // Insert share icon + const inputSuffixEl = window.document.body.querySelector( + ".aa-Form .aa-InputWrapperSuffix" + ); + + if (inputSuffixEl) { + let copyButtonEl = window.document.body.querySelector( + ".aa-Form .aa-InputWrapperSuffix .aa-CopyButton" + ); + + if (copyButtonEl === null) { + copyButtonEl = window.document.createElement("button"); + copyButtonEl.setAttribute("class", "aa-CopyButton"); + copyButtonEl.setAttribute("type", "button"); + copyButtonEl.setAttribute("title", language["search-copy-link-title"]); + copyButtonEl.onmousedown = (e) => { + e.preventDefault(); + e.stopPropagation(); + }; + + const linkIcon = "bi-clipboard"; + const checkIcon = "bi-check2"; + + const shareIconEl = window.document.createElement("i"); + shareIconEl.setAttribute("class", `bi ${linkIcon}`); + copyButtonEl.appendChild(shareIconEl); + inputSuffixEl.prepend(copyButtonEl); + + const clipboard = new window.ClipboardJS(".aa-CopyButton", { + text: function (_trigger) { + const copyUrl = new URL(window.location); + copyUrl.searchParams.set(kQueryArg, lastQuery); + copyUrl.searchParams.set(kResultsArg, "1"); + return copyUrl.toString(); + }, + }); + clipboard.on("success", function (e) { + // Focus the input + + // button target + const button = e.trigger; + const icon = button.querySelector("i.bi"); + + // flash "checked" + icon.classList.add(checkIcon); + icon.classList.remove(linkIcon); + setTimeout(function () { + icon.classList.remove(checkIcon); + icon.classList.add(linkIcon); + }, 1000); + }); + } + + // If there is a query, show the link icon + if (copyButtonEl) { + if (lastQuery && options["copy-button"]) { + copyButtonEl.style.display = "flex"; + } else { + copyButtonEl.style.display = "none"; + } + } + } +} + +/* Search Index Handling */ +// create the index +var fuseIndex = undefined; +var shownWarning = false; + +// fuse index options +const kFuseIndexOptions = { + keys: [ + { name: "title", weight: 20 }, + { name: "section", weight: 20 }, + { name: "text", weight: 10 }, + ], + ignoreLocation: true, + threshold: 0.1, +}; + +async function readSearchData() { + // Initialize the search index on demand + if (fuseIndex === undefined) { + if (window.location.protocol === "file:" && !shownWarning) { + window.alert( + "Search requires JavaScript features disabled when running in file://... URLs. In order to use search, please run this document in a web server." + ); + shownWarning = true; + return; + } + const fuse = new window.Fuse([], kFuseIndexOptions); + + // fetch the main search.json + const response = await fetch(offsetURL("search.json")); + if (response.status == 200) { + return response.json().then(function (searchDocs) { + searchDocs.forEach(function (searchDoc) { + fuse.add(searchDoc); + }); + fuseIndex = fuse; + return fuseIndex; + }); + } else { + return Promise.reject( + new Error( + "Unexpected status from search index request: " + response.status + ) + ); + } + } + + return fuseIndex; +} + +function inputElement() { + return window.document.body.querySelector(".aa-Form .aa-Input"); +} + +function focusSearchInput() { + setTimeout(() => { + const inputEl = inputElement(); + if (inputEl) { + inputEl.focus(); + } + }, 50); +} + +/* Panels */ +const kItemTypeDoc = "document"; +const kItemTypeMore = "document-more"; +const kItemTypeItem = "document-item"; +const kItemTypeError = "error"; + +function renderItem( + item, + createElement, + state, + setActiveItemId, + setContext, + refresh, + quartoSearchOptions +) { + switch (item.type) { + case kItemTypeDoc: + return createDocumentCard( + createElement, + "file-richtext", + item.title, + item.section, + item.text, + item.href, + item.crumbs, + quartoSearchOptions + ); + case kItemTypeMore: + return createMoreCard( + createElement, + item, + state, + setActiveItemId, + setContext, + refresh + ); + case kItemTypeItem: + return createSectionCard( + createElement, + item.section, + item.text, + item.href + ); + case kItemTypeError: + return createErrorCard(createElement, item.title, item.text); + default: + return undefined; + } +} + +function createDocumentCard( + createElement, + icon, + title, + section, + text, + href, + crumbs, + quartoSearchOptions +) { + const iconEl = createElement("i", { + class: `bi bi-${icon} search-result-icon`, + }); + const titleEl = createElement("p", { class: "search-result-title" }, title); + const titleContents = [iconEl, titleEl]; + const showParent = quartoSearchOptions["show-item-context"]; + if (crumbs && showParent) { + let crumbsOut = undefined; + const crumbClz = ["search-result-crumbs"]; + if (showParent === "root") { + crumbsOut = crumbs.length > 1 ? crumbs[0] : undefined; + } else if (showParent === "parent") { + crumbsOut = crumbs.length > 1 ? crumbs[crumbs.length - 2] : undefined; + } else { + crumbsOut = crumbs.length > 1 ? crumbs.join(" > ") : undefined; + crumbClz.push("search-result-crumbs-wrap"); + } + + const crumbEl = createElement( + "p", + { class: crumbClz.join(" ") }, + crumbsOut + ); + titleContents.push(crumbEl); + } + + const titleContainerEl = createElement( + "div", + { class: "search-result-title-container" }, + titleContents + ); + + const textEls = []; + if (section) { + const sectionEl = createElement( + "p", + { class: "search-result-section" }, + section + ); + textEls.push(sectionEl); + } + const descEl = createElement("p", { + class: "search-result-text", + dangerouslySetInnerHTML: { + __html: text, + }, + }); + textEls.push(descEl); + + const textContainerEl = createElement( + "div", + { class: "search-result-text-container" }, + textEls + ); + + const containerEl = createElement( + "div", + { + class: "search-result-container", + }, + [titleContainerEl, textContainerEl] + ); + + const linkEl = createElement( + "a", + { + href: offsetURL(href), + class: "search-result-link", + }, + containerEl + ); + + const classes = ["search-result-doc", "search-item"]; + if (!section) { + classes.push("document-selectable"); + } + + return createElement( + "div", + { + class: classes.join(" "), + }, + linkEl + ); +} + +function createMoreCard( + createElement, + item, + state, + setActiveItemId, + setContext, + refresh +) { + const moreCardEl = createElement( + "div", + { + class: "search-result-more search-item", + onClick: (e) => { + // Handle expanding the sections by adding the expanded + // section to the list of expanded sections + toggleExpanded(item, state, setContext, setActiveItemId, refresh); + e.stopPropagation(); + }, + }, + item.title + ); + + return moreCardEl; +} + +function toggleExpanded(item, state, setContext, setActiveItemId, refresh) { + const expanded = state.context.expanded || []; + if (expanded.includes(item.target)) { + setContext({ + expanded: expanded.filter((target) => target !== item.target), + }); + } else { + setContext({ expanded: [...expanded, item.target] }); + } + + refresh(); + setActiveItemId(item.__autocomplete_id); +} + +function createSectionCard(createElement, section, text, href) { + const sectionEl = createSection(createElement, section, text, href); + return createElement( + "div", + { + class: "search-result-doc-section search-item", + }, + sectionEl + ); +} + +function createSection(createElement, title, text, href) { + const descEl = createElement("p", { + class: "search-result-text", + dangerouslySetInnerHTML: { + __html: text, + }, + }); + + const titleEl = createElement("p", { class: "search-result-section" }, title); + const linkEl = createElement( + "a", + { + href: offsetURL(href), + class: "search-result-link", + }, + [titleEl, descEl] + ); + return linkEl; +} + +function createErrorCard(createElement, title, text) { + const descEl = createElement("p", { + class: "search-error-text", + dangerouslySetInnerHTML: { + __html: text, + }, + }); + + const titleEl = createElement("p", { + class: "search-error-title", + dangerouslySetInnerHTML: { + __html: ` ${title}`, + }, + }); + const errorEl = createElement("div", { class: "search-error" }, [ + titleEl, + descEl, + ]); + return errorEl; +} + +function positionPanel(pos) { + const panelEl = window.document.querySelector( + "#quarto-search-results .aa-Panel" + ); + const inputEl = window.document.querySelector( + "#quarto-search .aa-Autocomplete" + ); + + if (panelEl && inputEl) { + panelEl.style.top = `${Math.round(panelEl.offsetTop)}px`; + if (pos === "start") { + panelEl.style.left = `${Math.round(inputEl.left)}px`; + } else { + panelEl.style.right = `${Math.round(inputEl.offsetRight)}px`; + } + } +} + +/* Highlighting */ +// highlighting functions +function highlightMatch(query, text) { + if (text) { + const start = text.toLowerCase().indexOf(query.toLowerCase()); + if (start !== -1) { + const startMark = ""; + const endMark = ""; + + const end = start + query.length; + text = + text.slice(0, start) + + startMark + + text.slice(start, end) + + endMark + + text.slice(end); + const startInfo = clipStart(text, start); + const endInfo = clipEnd( + text, + startInfo.position + startMark.length + endMark.length + ); + text = + startInfo.prefix + + text.slice(startInfo.position, endInfo.position) + + endInfo.suffix; + + return text; + } else { + return text; + } + } else { + return text; + } +} + +function clipStart(text, pos) { + const clipStart = pos - 50; + if (clipStart < 0) { + // This will just return the start of the string + return { + position: 0, + prefix: "", + }; + } else { + // We're clipping before the start of the string, walk backwards to the first space. + const spacePos = findSpace(text, pos, -1); + return { + position: spacePos.position, + prefix: "", + }; + } +} + +function clipEnd(text, pos) { + const clipEnd = pos + 200; + if (clipEnd > text.length) { + return { + position: text.length, + suffix: "", + }; + } else { + const spacePos = findSpace(text, clipEnd, 1); + return { + position: spacePos.position, + suffix: spacePos.clipped ? "…" : "", + }; + } +} + +function findSpace(text, start, step) { + let stepPos = start; + while (stepPos > -1 && stepPos < text.length) { + const char = text[stepPos]; + if (char === " " || char === "," || char === ":") { + return { + position: step === 1 ? stepPos : stepPos - step, + clipped: stepPos > 1 && stepPos < text.length, + }; + } + stepPos = stepPos + step; + } + + return { + position: stepPos - step, + clipped: false, + }; +} + +// removes highlighting as implemented by the mark tag +function clearHighlight(searchterm, el) { + const childNodes = el.childNodes; + for (let i = childNodes.length - 1; i >= 0; i--) { + const node = childNodes[i]; + if (node.nodeType === Node.ELEMENT_NODE) { + if ( + node.tagName === "MARK" && + node.innerText.toLowerCase() === searchterm.toLowerCase() + ) { + el.replaceChild(document.createTextNode(node.innerText), node); + } else { + clearHighlight(searchterm, node); + } + } + } +} + +function escapeRegExp(string) { + return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string +} + +// highlight matches +function highlight(term, el) { + const termRegex = new RegExp(term, "ig"); + const childNodes = el.childNodes; + + // walk back to front avoid mutating elements in front of us + for (let i = childNodes.length - 1; i >= 0; i--) { + const node = childNodes[i]; + + if (node.nodeType === Node.TEXT_NODE) { + // Search text nodes for text to highlight + const text = node.nodeValue; + + let startIndex = 0; + let matchIndex = text.search(termRegex); + if (matchIndex > -1) { + const markFragment = document.createDocumentFragment(); + while (matchIndex > -1) { + const prefix = text.slice(startIndex, matchIndex); + markFragment.appendChild(document.createTextNode(prefix)); + + const mark = document.createElement("mark"); + mark.appendChild( + document.createTextNode( + text.slice(matchIndex, matchIndex + term.length) + ) + ); + markFragment.appendChild(mark); + + startIndex = matchIndex + term.length; + matchIndex = text.slice(startIndex).search(new RegExp(term, "ig")); + if (matchIndex > -1) { + matchIndex = startIndex + matchIndex; + } + } + if (startIndex < text.length) { + markFragment.appendChild( + document.createTextNode(text.slice(startIndex, text.length)) + ); + } + + el.replaceChild(markFragment, node); + } + } else if (node.nodeType === Node.ELEMENT_NODE) { + // recurse through elements + highlight(term, node); + } + } +} + +/* Link Handling */ +// get the offset from this page for a given site root relative url +function offsetURL(url) { + var offset = getMeta("quarto:offset"); + return offset ? offset + url : url; +} + +// read a meta tag value +function getMeta(metaName) { + var metas = window.document.getElementsByTagName("meta"); + for (let i = 0; i < metas.length; i++) { + if (metas[i].getAttribute("name") === metaName) { + return metas[i].getAttribute("content"); + } + } + return ""; +} + +function algoliaSearch(query, limit, algoliaOptions) { + const { getAlgoliaResults } = window["@algolia/autocomplete-preset-algolia"]; + + const applicationId = algoliaOptions["application-id"]; + const searchOnlyApiKey = algoliaOptions["search-only-api-key"]; + const indexName = algoliaOptions["index-name"]; + const indexFields = algoliaOptions["index-fields"]; + const searchClient = window.algoliasearch(applicationId, searchOnlyApiKey); + const searchParams = algoliaOptions["params"]; + const searchAnalytics = !!algoliaOptions["analytics-events"]; + + return getAlgoliaResults({ + searchClient, + queries: [ + { + indexName: indexName, + query, + params: { + hitsPerPage: limit, + clickAnalytics: searchAnalytics, + ...searchParams, + }, + }, + ], + transformResponse: (response) => { + if (!indexFields) { + return response.hits.map((hit) => { + return hit.map((item) => { + return { + ...item, + text: highlightMatch(query, item.text), + }; + }); + }); + } else { + const remappedHits = response.hits.map((hit) => { + return hit.map((item) => { + const newItem = { ...item }; + ["href", "section", "title", "text", "crumbs"].forEach( + (keyName) => { + const mappedName = indexFields[keyName]; + if ( + mappedName && + item[mappedName] !== undefined && + mappedName !== keyName + ) { + newItem[keyName] = item[mappedName]; + delete newItem[mappedName]; + } + } + ); + newItem.text = highlightMatch(query, newItem.text); + return newItem; + }); + }); + return remappedHits; + } + }, + }); +} + +let subSearchTerm = undefined; +let subSearchFuse = undefined; +const kFuseMaxWait = 125; + +async function fuseSearch(query, fuse, fuseOptions) { + let index = fuse; + // Fuse.js using the Bitap algorithm for text matching which runs in + // O(nm) time (no matter the structure of the text). In our case this + // means that long search terms mixed with large index gets very slow + // + // This injects a subIndex that will be used once the terms get long enough + // Usually making this subindex is cheap since there will typically be + // a subset of results matching the existing query + if (subSearchFuse !== undefined && query.startsWith(subSearchTerm)) { + // Use the existing subSearchFuse + index = subSearchFuse; + } else if (subSearchFuse !== undefined) { + // The term changed, discard the existing fuse + subSearchFuse = undefined; + subSearchTerm = undefined; + } + + // Search using the active fuse + const then = performance.now(); + const resultsRaw = await index.search(query, fuseOptions); + const now = performance.now(); + + const results = resultsRaw.map((result) => { + const addParam = (url, name, value) => { + const anchorParts = url.split("#"); + const baseUrl = anchorParts[0]; + const sep = baseUrl.search("\\?") > 0 ? "&" : "?"; + anchorParts[0] = baseUrl + sep + name + "=" + value; + return anchorParts.join("#"); + }; + + return { + title: result.item.title, + section: result.item.section, + href: addParam(result.item.href, kQueryArg, query), + text: highlightMatch(query, result.item.text), + crumbs: result.item.crumbs, + }; + }); + + // If we don't have a subfuse and the query is long enough, go ahead + // and create a subfuse to use for subsequent queries + if ( + now - then > kFuseMaxWait && + subSearchFuse === undefined && + resultsRaw.length < fuseOptions.limit + ) { + subSearchTerm = query; + subSearchFuse = new window.Fuse([], kFuseIndexOptions); + resultsRaw.forEach((rr) => { + subSearchFuse.add(rr.item); + }); + } + return results; +} diff --git a/sitemap.xml b/sitemap.xml new file mode 100644 index 00000000..50af827c --- /dev/null +++ b/sitemap.xml @@ -0,0 +1,71 @@ + + + + https://fastcore.fast.ai/style.html + 2024-10-18T02:55:17.629Z + + + https://fastcore.fast.ai/xtras.html + 2024-10-18T02:55:17.777Z + + + https://fastcore.fast.ai/foundation.html + 2024-10-18T02:55:17.733Z + + + https://fastcore.fast.ai/tour.html + 2024-10-18T02:55:17.105Z + + + https://fastcore.fast.ai/net.html + 2024-10-18T02:55:17.053Z + + + https://fastcore.fast.ai/index.html + 2024-10-18T02:55:16.841Z + + + https://fastcore.fast.ai/basics.html + 2024-10-18T02:55:17.425Z + + + https://fastcore.fast.ai/dispatch.html + 2024-10-18T02:55:16.845Z + + + https://fastcore.fast.ai/meta.html + 2024-10-18T02:55:16.677Z + + + https://fastcore.fast.ai/xml.html + 2024-10-18T02:55:15.733Z + + + https://fastcore.fast.ai/transform.html + 2024-10-18T02:55:17.277Z + + + https://fastcore.fast.ai/parallel.html + 2024-10-18T02:55:16.805Z + + + https://fastcore.fast.ai/py2pyi.html + 2024-10-18T02:55:16.953Z + + + https://fastcore.fast.ai/test.html + 2024-10-18T02:55:18.821Z + + + https://fastcore.fast.ai/script.html + 2024-10-18T02:55:17.213Z + + + https://fastcore.fast.ai/xdg.html + 2024-10-18T02:55:17.353Z + + + https://fastcore.fast.ai/docments.html + 2024-10-18T02:55:17.557Z + + diff --git a/style.html b/style.html new file mode 100644 index 00000000..18b1761f --- /dev/null +++ b/style.html @@ -0,0 +1,893 @@ + + + + + + + + + + +Style – fastcore + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ +
+ + +
+ + + +
+ +
+
+

Style

+
+ +
+
+ Fast styling for friendly CLIs. +
+
+ + +
+ + + + +
+ + + +
+ + + +
+
+
+ +
+
+Note +
+
+
+

Styled outputs don’t show in Quarto documentation. Please use a notebook editor to correctly view this page.

+
+
+
+

source

+
+

StyleCode

+
+
 StyleCode (name, code, typ)
+
+

An escape sequence for styling terminal text.

+

The primary building block of the S API.

+
+
print(str(StyleCode('blue', 34, 'fg')) + 'hello' + str(StyleCode('default', 39, 'fg')) + ' world')
+
+
hello world
+
+
+
+

source

+
+
+

Style

+
+
 Style (codes=None)
+
+

A minimal terminal text styler.

+

The main way to use it is via the exported S object.

+
+
+Exported source +
S = Style()
+
+
+

We start with an empty style:

+
+
S
+
+
<Style: none>
+
+
+

Define a new style by chaining attributes:

+
+
s = S.blue.bold.underline
+s
+
+
<Style: blue bold underline>
+
+
+

You can see a full list of available styles with auto-complete by typing S . Tab.

+

Apply a style by calling it with a string:

+
+
s('hello world')
+
+
'\x1b[34m\x1b[1m\x1b[4mhello world\x1b[22m\x1b[24m\x1b[39m'
+
+
+

That’s a raw string with the underlying escape sequences that tell the terminal how to format text. To see the styled version we have to print it:

+
+
print(s('hello world'))
+
+
hello world
+
+
+

You can also nest styles:

+
+
print(S.bold(S.blue('key') + ' = value ') + S.light_gray(' ' + S.underline('# With a comment')) + ' and unstyled text')
+
+
key = value  # With a comment and unstyled text
+
+
+
+
print(S.blue('this '+S.bold('is')+' a test'))
+
+
this is a test
+
+
+
+

source

+
+
+

demo

+
+
 demo ()
+
+

Demonstrate all available styles and their codes.

+
+
demo()
+
+
 30    black           
+ 31    red             
+ 32    green           
+ 33    yellow          
+ 34    blue            
+ 35    magenta         
+ 36    cyan            
+ 37    light_gray      
+ 39    default         
+ 90    dark_gray       
+ 91    light_red       
+ 92    light_green     
+ 93    light_yellow    
+ 94    light_blue      
+ 95    light_magenta   
+ 96    light_cyan      
+ 97    white           
+ 40    black_bg        
+ 41    red_bg          
+ 42    green_bg        
+ 43    yellow_bg       
+ 44    blue_bg         
+ 45    magenta_bg      
+ 46    cyan_bg         
+ 47    light_gray_bg   
+ 49    default_bg      
+100    dark_gray_bg    
+101    light_red_bg    
+102    light_green_bg  
+103    light_yellow_bg 
+104    light_blue_bg   
+105    light_magenta_bg
+106    light_cyan_bg   
+107    white_bg        
+  1    bold            
+  2    dim             
+  3    italic          
+  4    underline       
+  5    blink           
+  7    invert          
+  8    hidden          
+  9    strikethrough   
+ 22    reset_bold      
+ 22    reset_dim       
+ 23    reset_italic    
+ 24    reset_underline 
+ 25    reset_blink     
+ 27    reset_invert    
+ 28    reset_hidden    
+ 29    reset_strikethrough
+  0    reset           
+
+
+ + +
+ +
+ +
+ + + + + \ No newline at end of file diff --git a/style.html.md b/style.html.md new file mode 100644 index 00000000..861e494d --- /dev/null +++ b/style.html.md @@ -0,0 +1,176 @@ +# Style + + + + +
+ +> **Note** +> +> Styled outputs don’t show in Quarto documentation. Please use a +> notebook editor to correctly view this page. + +
+ +------------------------------------------------------------------------ + +source + +### StyleCode + +> StyleCode (name, code, typ) + +*An escape sequence for styling terminal text.* + +The primary building block of the `S` API. + +``` python +print(str(StyleCode('blue', 34, 'fg')) + 'hello' + str(StyleCode('default', 39, 'fg')) + ' world') +``` + + hello world + +------------------------------------------------------------------------ + +source + +### Style + +> Style (codes=None) + +*A minimal terminal text styler.* + +The main way to use it is via the exported `S` object. + +
+Exported source + +``` python +S = Style() +``` + +
+ +We start with an empty style: + +``` python +S +``` + + + +Define a new style by chaining attributes: + +``` python +s = S.blue.bold.underline +s +``` + + + +You can see a full list of available styles with auto-complete by typing +S . Tab. + +Apply a style by calling it with a string: + +``` python +s('hello world') +``` + + '\x1b[34m\x1b[1m\x1b[4mhello world\x1b[22m\x1b[24m\x1b[39m' + +That’s a raw string with the underlying escape sequences that tell the +terminal how to format text. To see the styled version we have to print +it: + +``` python +print(s('hello world')) +``` + + hello world + +You can also nest styles: + +``` python +print(S.bold(S.blue('key') + ' = value ') + S.light_gray(' ' + S.underline('# With a comment')) + ' and unstyled text') +``` + + key = value # With a comment and unstyled text + +``` python +print(S.blue('this '+S.bold('is')+' a test')) +``` + + this is a test + +------------------------------------------------------------------------ + +source + +### demo + +> demo () + +*Demonstrate all available styles and their codes.* + +``` python +demo() +``` + + 30 black + 31 red + 32 green + 33 yellow + 34 blue + 35 magenta + 36 cyan + 37 light_gray + 39 default + 90 dark_gray + 91 light_red + 92 light_green + 93 light_yellow + 94 light_blue + 95 light_magenta + 96 light_cyan + 97 white + 40 black_bg + 41 red_bg + 42 green_bg + 43 yellow_bg + 44 blue_bg + 45 magenta_bg + 46 cyan_bg + 47 light_gray_bg + 49 default_bg + 100 dark_gray_bg + 101 light_red_bg + 102 light_green_bg + 103 light_yellow_bg + 104 light_blue_bg + 105 light_magenta_bg + 106 light_cyan_bg + 107 white_bg + 1 bold + 2 dim + 3 italic + 4 underline + 5 blink + 7 invert + 8 hidden + 9 strikethrough + 22 reset_bold + 22 reset_dim + 23 reset_italic + 24 reset_underline + 25 reset_blink + 27 reset_invert + 28 reset_hidden + 29 reset_strikethrough + 0 reset diff --git a/styles.css b/styles.css new file mode 100644 index 00000000..b46c2093 --- /dev/null +++ b/styles.css @@ -0,0 +1,18 @@ +.cell-output pre { + margin-left: 0.8rem; + margin-top: 0; + background: none; + border-left: 2px solid lightsalmon; + border-top-left-radius: 0; + border-top-right-radius: 0; + } + + .cell-output .sourceCode { + background: none; + margin-top: 0; + } + + .cell > .sourceCode { + margin-bottom: 0; + } + \ No newline at end of file diff --git a/test.html b/test.html new file mode 100644 index 00000000..450c88dc --- /dev/null +++ b/test.html @@ -0,0 +1,1079 @@ + + + + + + + + + + +Test – fastcore + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ +
+ + +
+ + + +
+ +
+
+

Test

+
+ +
+
+ Helper functions to quickly write tests in notebooks +
+
+ + +
+ + + + +
+ + + +
+ + + +
+

Simple test functions

+

We can check that code raises an exception when that’s expected (test_fail).

+

To test for equality or inequality (with different types of things) we define a simple function test that compares two objects with a given cmp operator.

+
+

source

+
+

test_fail

+
+
 test_fail (f, msg='', contains='', args=None, kwargs=None)
+
+

Fails with msg unless f() raises an exception and (optionally) has contains in e.args

+
+
def _fail(): raise Exception("foobar")
+test_fail(_fail, contains="foo")
+
+def _fail(): raise Exception()
+test_fail(_fail)
+
+

We can also pass args and kwargs to function to check if it fails with special inputs.

+
+
def _fail_args(a):
+    if a == 5:
+        raise ValueError
+test_fail(_fail_args, args=(5,))
+test_fail(_fail_args, kwargs=dict(a=5))
+
+
+

source

+
+
+

test

+
+
 test (a, b, cmp, cname=None)
+
+

assert that cmp(a,b); display inputs and cname or cmp.__name__ if it fails

+
+
test([1,2],[1,2], operator.eq)
+test_fail(lambda: test([1,2],[1], operator.eq))
+test([1,2],[1],   operator.ne)
+test_fail(lambda: test([1,2],[1,2], operator.ne))
+
+
+
+
+

all_equal

+
+
 all_equal (a, b)
+
+

Compares whether a and b are the same length and have the same contents

+
+
test(['abc'], ['abc'], all_equal)
+test_fail(lambda: test(['abc'],['cab'], all_equal))
+
+
+
+
+

equals

+
+
 equals (a, b)
+
+

Compares a and b for equality; supports sublists, tensors and arrays too

+
+
test([['abc'],['a']], [['abc'],['a']],  equals)
+test([['abc'],['a'],'b', [['x']]], [['abc'],['a'],'b', [['x']]],  equals) # supports any depth and nested structure
+
+
+

source

+
+
+

nequals

+
+
 nequals (a, b)
+
+

Compares a and b for not equals

+
+
test(['abc'], ['ab' ], nequals)
+
+
+
+
+

test_eq test_ne, etc…

+

Just use test_eq/test_ne to test for ==/!=. test_eq_type checks things are equal and of the same type. We define them using test:

+
+

source

+
+

test_eq

+
+
 test_eq (a, b)
+
+

test that a==b

+
+
test_eq([1,2],[1,2])
+test_eq([1,2],map(int,[1,2]))
+test_eq(array([1,2]),array([1,2]))
+test_eq(array([1,2]),array([1,2]))
+test_eq([array([1,2]),3],[array([1,2]),3])
+test_eq(dict(a=1,b=2), dict(b=2,a=1))
+test_fail(lambda: test_eq([1,2], 1), contains="==")
+test_fail(lambda: test_eq(None, np.array([1,2])), contains="==")
+test_eq({'a', 'b', 'c'}, {'c', 'a', 'b'})
+
+
+
df1 = pd.DataFrame(dict(a=[1,2],b=['a','b']))
+df2 = pd.DataFrame(dict(a=[1,2],b=['a','b']))
+df3 = pd.DataFrame(dict(a=[1,2],b=['a','c']))
+
+test_eq(df1,df2)
+test_eq(df1.a,df2.a)
+test_fail(lambda: test_eq(df1,df3), contains='==')
+class T(pd.Series): pass
+test_eq(df1.iloc[0], T(df2.iloc[0])) # works with subclasses
+
+
+
test_eq(torch.zeros(10), torch.zeros(10, dtype=torch.float64))
+test_eq(torch.zeros(10), torch.ones(10)-1)
+test_fail(lambda:test_eq(torch.zeros(10), torch.ones(1, 10)), contains='==')
+test_eq(torch.zeros(3), [0,0,0])
+
+
+

source

+
+
+

test_eq_type

+
+
 test_eq_type (a, b)
+
+

test that a==b and are same type

+
+
test_eq_type(1,1)
+test_fail(lambda: test_eq_type(1,1.))
+test_eq_type([1,1],[1,1])
+test_fail(lambda: test_eq_type([1,1],(1,1)))
+test_fail(lambda: test_eq_type([1,1],[1,1.]))
+
+
+

source

+
+
+

test_ne

+
+
 test_ne (a, b)
+
+

test that a!=b

+
+
test_ne([1,2],[1])
+test_ne([1,2],[1,3])
+test_ne(array([1,2]),array([1,1]))
+test_ne(array([1,2]),array([1,1]))
+test_ne([array([1,2]),3],[array([1,2])])
+test_ne([3,4],array([3]))
+test_ne([3,4],array([3,5]))
+test_ne(dict(a=1,b=2), ['a', 'b'])
+test_ne(['a', 'b'], dict(a=1,b=2))
+
+
+

source

+
+
+

is_close

+
+
 is_close (a, b, eps=1e-05)
+
+

Is a within eps of b

+
+

source

+
+
+

test_close

+
+
 test_close (a, b, eps=1e-05)
+
+

test that a is within eps of b

+
+
test_close(1,1.001,eps=1e-2)
+test_fail(lambda: test_close(1,1.001))
+test_close([-0.001,1.001], [0.,1.], eps=1e-2)
+test_close(np.array([-0.001,1.001]), np.array([0.,1.]), eps=1e-2)
+test_close(array([-0.001,1.001]), array([0.,1.]), eps=1e-2)
+
+
+

source

+
+
+

test_is

+
+
 test_is (a, b)
+
+

test that a is b

+
+
test_fail(lambda: test_is([1], [1]))
+a = [1]
+test_is(a, a)
+b = [2]; test_fail(lambda: test_is(a, b))
+
+
+

source

+
+
+

test_shuffled

+
+
 test_shuffled (a, b)
+
+

test that a and b are shuffled versions of the same sequence of items

+
+
a = list(range(50))
+b = copy(a)
+random.shuffle(b)
+test_shuffled(a,b)
+test_fail(lambda:test_shuffled(a,a))
+
+
+
a = 'abc'
+b = 'abcabc'
+test_fail(lambda:test_shuffled(a,b))
+
+
+
a = ['a', 42, True] 
+b = [42, True, 'a']
+test_shuffled(a,b)
+
+
+

source

+
+
+

test_stdout

+
+
 test_stdout (f, exp, regex=False)
+
+

Test that f prints exp to stdout, optionally checking as regex

+
+
test_stdout(lambda: print('hi'), 'hi')
+test_fail(lambda: test_stdout(lambda: print('hi'), 'ho'))
+test_stdout(lambda: 1+1, '')
+test_stdout(lambda: print('hi there!'), r'^hi.*!$', regex=True)
+
+
+

source

+
+
+

test_warns

+
+
 test_warns (f, show=False)
+
+
+
test_warns(lambda: warnings.warn("Oh no!"))
+test_fail(lambda: test_warns(lambda: 2+2), contains='No warnings raised')
+
+
+
test_warns(lambda: warnings.warn("Oh no!"), show=True)
+
+
<class 'UserWarning'>: Oh no!
+
+
+
+
im = Image.open(TEST_IMAGE).resize((128,128)); im
+
+
+
+

+
+
+
+
+
+
im = Image.open(TEST_IMAGE_BW).resize((128,128)); im
+
+
+
+

+
+
+
+
+
+

source

+
+
+

test_fig_exists

+
+
 test_fig_exists (ax)
+
+

Test there is a figure displayed in ax

+
+
fig,ax = plt.subplots()
+ax.imshow(array(im));
+
+
+
+

+
+
+
+
+
+
test_fig_exists(ax)
+
+
+

source

+
+
+

ExceptionExpected

+
+
 ExceptionExpected (ex=<class 'Exception'>, regex='')
+
+

Context manager that tests if an exception is raised

+
+
def _tst_1(): assert False, "This is a test"
+def _tst_2(): raise SyntaxError
+
+with ExceptionExpected(): _tst_1()
+with ExceptionExpected(ex=AssertionError, regex="This is a test"): _tst_1()
+with ExceptionExpected(ex=SyntaxError): _tst_2()
+
+

exception is an abbreviation for ExceptionExpected().

+
+
with exception: _tst_1()
+
+ + +
+
+ +
+ +
+ + + + + \ No newline at end of file diff --git a/test.html.md b/test.html.md new file mode 100644 index 00000000..eb2898ea --- /dev/null +++ b/test.html.md @@ -0,0 +1,394 @@ +# Test + + + + +## Simple test functions + +We can check that code raises an exception when that’s expected +([`test_fail`](https://fastcore.fast.ai/test.html#test_fail)). + +To test for equality or inequality (with different types of things) we +define a simple function +[`test`](https://fastcore.fast.ai/test.html#test) that compares two +objects with a given `cmp` operator. + +------------------------------------------------------------------------ + +source + +### test_fail + +> test_fail (f, msg='', contains='', args=None, kwargs=None) + +*Fails with `msg` unless `f()` raises an exception and (optionally) has +`contains` in `e.args`* + +``` python +def _fail(): raise Exception("foobar") +test_fail(_fail, contains="foo") + +def _fail(): raise Exception() +test_fail(_fail) +``` + +We can also pass `args` and `kwargs` to function to check if it fails +with special inputs. + +``` python +def _fail_args(a): + if a == 5: + raise ValueError +test_fail(_fail_args, args=(5,)) +test_fail(_fail_args, kwargs=dict(a=5)) +``` + +------------------------------------------------------------------------ + +source + +### test + +> test (a, b, cmp, cname=None) + +*`assert` that `cmp(a,b)`; display inputs and `cname or cmp.__name__` if +it fails* + +``` python +test([1,2],[1,2], operator.eq) +test_fail(lambda: test([1,2],[1], operator.eq)) +test([1,2],[1], operator.ne) +test_fail(lambda: test([1,2],[1,2], operator.ne)) +``` + +------------------------------------------------------------------------ + +### all_equal + +> all_equal (a, b) + +*Compares whether `a` and `b` are the same length and have the same +contents* + +``` python +test(['abc'], ['abc'], all_equal) +test_fail(lambda: test(['abc'],['cab'], all_equal)) +``` + +------------------------------------------------------------------------ + +### equals + +> equals (a, b) + +*Compares `a` and `b` for equality; supports sublists, tensors and +arrays too* + +``` python +test([['abc'],['a']], [['abc'],['a']], equals) +test([['abc'],['a'],'b', [['x']]], [['abc'],['a'],'b', [['x']]], equals) # supports any depth and nested structure +``` + +------------------------------------------------------------------------ + +source + +### nequals + +> nequals (a, b) + +*Compares `a` and `b` for `not equals`* + +``` python +test(['abc'], ['ab' ], nequals) +``` + +## test_eq test_ne, etc… + +Just use +[`test_eq`](https://fastcore.fast.ai/test.html#test_eq)/[`test_ne`](https://fastcore.fast.ai/test.html#test_ne) +to test for `==`/`!=`. +[`test_eq_type`](https://fastcore.fast.ai/test.html#test_eq_type) checks +things are equal and of the same type. We define them using +[`test`](https://fastcore.fast.ai/test.html#test): + +------------------------------------------------------------------------ + +source + +### test_eq + +> test_eq (a, b) + +*[`test`](https://fastcore.fast.ai/test.html#test) that `a==b`* + +``` python +test_eq([1,2],[1,2]) +test_eq([1,2],map(int,[1,2])) +test_eq(array([1,2]),array([1,2])) +test_eq(array([1,2]),array([1,2])) +test_eq([array([1,2]),3],[array([1,2]),3]) +test_eq(dict(a=1,b=2), dict(b=2,a=1)) +test_fail(lambda: test_eq([1,2], 1), contains="==") +test_fail(lambda: test_eq(None, np.array([1,2])), contains="==") +test_eq({'a', 'b', 'c'}, {'c', 'a', 'b'}) +``` + +``` python +df1 = pd.DataFrame(dict(a=[1,2],b=['a','b'])) +df2 = pd.DataFrame(dict(a=[1,2],b=['a','b'])) +df3 = pd.DataFrame(dict(a=[1,2],b=['a','c'])) + +test_eq(df1,df2) +test_eq(df1.a,df2.a) +test_fail(lambda: test_eq(df1,df3), contains='==') +class T(pd.Series): pass +test_eq(df1.iloc[0], T(df2.iloc[0])) # works with subclasses +``` + +``` python +test_eq(torch.zeros(10), torch.zeros(10, dtype=torch.float64)) +test_eq(torch.zeros(10), torch.ones(10)-1) +test_fail(lambda:test_eq(torch.zeros(10), torch.ones(1, 10)), contains='==') +test_eq(torch.zeros(3), [0,0,0]) +``` + +------------------------------------------------------------------------ + +source + +### test_eq_type + +> test_eq_type (a, b) + +*[`test`](https://fastcore.fast.ai/test.html#test) that `a==b` and are +same type* + +``` python +test_eq_type(1,1) +test_fail(lambda: test_eq_type(1,1.)) +test_eq_type([1,1],[1,1]) +test_fail(lambda: test_eq_type([1,1],(1,1))) +test_fail(lambda: test_eq_type([1,1],[1,1.])) +``` + +------------------------------------------------------------------------ + +source + +### test_ne + +> test_ne (a, b) + +*[`test`](https://fastcore.fast.ai/test.html#test) that `a!=b`* + +``` python +test_ne([1,2],[1]) +test_ne([1,2],[1,3]) +test_ne(array([1,2]),array([1,1])) +test_ne(array([1,2]),array([1,1])) +test_ne([array([1,2]),3],[array([1,2])]) +test_ne([3,4],array([3])) +test_ne([3,4],array([3,5])) +test_ne(dict(a=1,b=2), ['a', 'b']) +test_ne(['a', 'b'], dict(a=1,b=2)) +``` + +------------------------------------------------------------------------ + +source + +### is_close + +> is_close (a, b, eps=1e-05) + +*Is `a` within `eps` of `b`* + +------------------------------------------------------------------------ + +source + +### test_close + +> test_close (a, b, eps=1e-05) + +*[`test`](https://fastcore.fast.ai/test.html#test) that `a` is within +`eps` of `b`* + +``` python +test_close(1,1.001,eps=1e-2) +test_fail(lambda: test_close(1,1.001)) +test_close([-0.001,1.001], [0.,1.], eps=1e-2) +test_close(np.array([-0.001,1.001]), np.array([0.,1.]), eps=1e-2) +test_close(array([-0.001,1.001]), array([0.,1.]), eps=1e-2) +``` + +------------------------------------------------------------------------ + +source + +### test_is + +> test_is (a, b) + +*[`test`](https://fastcore.fast.ai/test.html#test) that `a is b`* + +``` python +test_fail(lambda: test_is([1], [1])) +a = [1] +test_is(a, a) +b = [2]; test_fail(lambda: test_is(a, b)) +``` + +------------------------------------------------------------------------ + +source + +### test_shuffled + +> test_shuffled (a, b) + +*[`test`](https://fastcore.fast.ai/test.html#test) that `a` and `b` are +shuffled versions of the same sequence of items* + +``` python +a = list(range(50)) +b = copy(a) +random.shuffle(b) +test_shuffled(a,b) +test_fail(lambda:test_shuffled(a,a)) +``` + +``` python +a = 'abc' +b = 'abcabc' +test_fail(lambda:test_shuffled(a,b)) +``` + +``` python +a = ['a', 42, True] +b = [42, True, 'a'] +test_shuffled(a,b) +``` + +------------------------------------------------------------------------ + +source + +### test_stdout + +> test_stdout (f, exp, regex=False) + +*Test that `f` prints `exp` to stdout, optionally checking as `regex`* + +``` python +test_stdout(lambda: print('hi'), 'hi') +test_fail(lambda: test_stdout(lambda: print('hi'), 'ho')) +test_stdout(lambda: 1+1, '') +test_stdout(lambda: print('hi there!'), r'^hi.*!$', regex=True) +``` + +------------------------------------------------------------------------ + +source + +### test_warns + +> test_warns (f, show=False) + +``` python +test_warns(lambda: warnings.warn("Oh no!")) +test_fail(lambda: test_warns(lambda: 2+2), contains='No warnings raised') +``` + +``` python +test_warns(lambda: warnings.warn("Oh no!"), show=True) +``` + + : Oh no! + +``` python +im = Image.open(TEST_IMAGE).resize((128,128)); im +``` + +![](00_test_files/figure-commonmark/cell-35-output-1.png) + +``` python +im = Image.open(TEST_IMAGE_BW).resize((128,128)); im +``` + +![](00_test_files/figure-commonmark/cell-36-output-1.png) + +------------------------------------------------------------------------ + +source + +### test_fig_exists + +> test_fig_exists (ax) + +*Test there is a figure displayed in `ax`* + +``` python +fig,ax = plt.subplots() +ax.imshow(array(im)); +``` + +![](00_test_files/figure-commonmark/cell-38-output-1.png) + +``` python +test_fig_exists(ax) +``` + +------------------------------------------------------------------------ + +source + +### ExceptionExpected + +> ExceptionExpected (ex=, regex='') + +*Context manager that tests if an exception is raised* + +``` python +def _tst_1(): assert False, "This is a test" +def _tst_2(): raise SyntaxError + +with ExceptionExpected(): _tst_1() +with ExceptionExpected(ex=AssertionError, regex="This is a test"): _tst_1() +with ExceptionExpected(ex=SyntaxError): _tst_2() +``` + +`exception` is an abbreviation for `ExceptionExpected()`. + +``` python +with exception: _tst_1() +``` diff --git a/tour.html b/tour.html new file mode 100644 index 00000000..624aa54c --- /dev/null +++ b/tour.html @@ -0,0 +1,953 @@ + + + + + + + + + +A tour of fastcore – fastcore + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ +
+ + +
+ + + +
+ +
+
+

A tour of fastcore

+
+ + + +
+ + + + +
+ + + +
+ + + +

Here’s a (somewhat) quick tour of a few higlights from fastcore.

+
+

Documentation

+

All fast.ai projects, including this one, are built with nbdev, which is a full literate programming environment built on Jupyter Notebooks. That means that every piece of documentation, including the page you’re reading now, can be accessed as interactive Jupyter notebooks. In fact, you can even grab a link directly to a notebook running interactively on Google Colab - if you want to follow along with this tour, click the link below:

+
+
colab_link('index')
+ +
+

The full docs are available at fastcore.fast.ai. The code in the examples and in all fast.ai libraries follow the fast.ai style guide. In order to support interactive programming, all fast.ai libraries are designed to allow for import * to be used safely, particular by ensuring that __all__ is defined in all packages. In order to see where a function is from, just type it:

+
+
coll_repr
+
+
<function fastcore.foundation.coll_repr(c, max_n=10)>
+
+
+

For more details, including a link to the full documentation and source code, use doc, which pops up a window with this information:

+
doc(coll_repr)
+

+

The documentation also contains links to any related functions or classes, which appear like this: coll_repr (in the notebook itself you will just see a word with back-ticks around it; the links are auto-generated in the documentation site). The documentation will generally show one or more examples of use, along with any background context necessary to understand them. As you’ll see, the examples for each function and method are shown as tests, rather than example outputs, so let’s start by explaining that.

+
+
+

Testing

+

fastcore’s testing module is designed to work well with nbdev, which is a full literate programming environment built on Jupyter Notebooks. That means that your tests, docs, and code all live together in the same notebook. fastcore and nbdev’s approach to testing starts with the premise that all your tests should pass. If one fails, no more tests in a notebook are run.

+

Tests look like this:

+
+
test_eq(coll_repr(range(1000), 5), '(#1000) [0,1,2,3,4...]')
+
+

That’s an example from the docs for coll_repr. As you see, it’s not showing you the output directly. Here’s what that would look like:

+
+
coll_repr(range(1000), 5)
+
+
'(#1000) [0,1,2,3,4...]'
+
+
+

So, the test is actually showing you what the output looks like, because if the function call didn’t return '(#1000) [0,1,2,3,4...]', then the test would have failed.

+

So every test shown in the docs is also showing you the behavior of the library — and vice versa!

+

Test functions always start with test_, and then follow with the operation being tested. So test_eq tests for equality (as you saw in the example above). This includes tests for equality of arrays and tensors, lists and generators, and many more:

+
+
test_eq([0,1,2,3], np.arange(4))
+
+

When a test fails, it prints out information about what was expected:

+
test_eq([0,1,2,3], np.arange(3))
+
----
+  AssertionError: ==:
+  [0, 1, 2, 3]
+  [0 1 2]
+

If you want to check that objects are the same type, rather than the just contain the same collection, use test_eq_type.

+

You can test with any comparison function using test, e.g test whether an object is less than:

+
+
test(2, 3, operator.lt)
+
+

You can even test that exceptions are raised:

+
+
def divide_zero(): return 1/0
+test_fail(divide_zero)
+
+

…and test that things are printed to stdout:

+
+
test_stdout(lambda: print('hi'), 'hi')
+
+
+
+

Foundations

+

fast.ai is unusual in that we often use mixins in our code. Mixins are widely used in many programming languages, such as Ruby, but not so much in Python. We use mixins to attach new behavior to existing libraries, or to allow modules to add new behavior to our own classes, such as in extension modules. One useful example of a mixin we define is Path.ls, which lists a directory and returns an L (an extended list class which we’ll discuss shortly):

+
+
p = Path('images')
+p.ls()
+
+
(#6) [Path('images/mnist3.png'),Path('images/att_00000.png'),Path('images/puppy.jpg'),Path('images/att_00005.png'),Path('images/att_00007.png'),Path('images/att_00006.png')]
+
+
+

You can easily add you own mixins with the patch decorator, which takes advantage of Python 3 function annotations to say what class to patch:

+
+
@patch
+def num_items(self:Path): return len(self.ls())
+
+p.num_items()
+
+
6
+
+
+

We also use **kwargs frequently. In python **kwargs in a parameter like means “put any additional keyword arguments into a dict called kwargs”. Normally, using kwargs makes an API quite difficult to work with, because it breaks things like tab-completion and popup lists of signatures. utils provides use_kwargs and delegates to avoid this problem. See our detailed article on delegation on this topic.

+

GetAttr solves a similar problem (and is also discussed in the article linked above): it’s allows you to use Python’s exceptionally useful __getattr__ magic method, but avoids the problem that normally in Python tab-completion and docs break when using this. For instance, you can see here that Python’s dir function, which is used to find the attributes of a python object, finds everything inside the self.default attribute here:

+
+
class Author:
+    def __init__(self, name): self.name = name
+
+class ProductPage(GetAttr):
+    _default = 'author'
+    def __init__(self,author,price,cost): self.author,self.price,self.cost = author,price,cost
+
+p = ProductPage(Author("Jeremy"), 1.50, 0.50)
+[o for o in dir(p) if not o.startswith('_')]
+
+
['author', 'cost', 'name', 'price']
+
+
+

Looking at that ProductPage example, it’s rather verbose and duplicates a lot of attribute names, which can lead to bugs later if you change them only in one place. fastcore provides store_attr to simplify this common pattern. It also provides basic_repr to give simple objects a useful repr:

+
+
class ProductPage:
+    def __init__(self,author,price,cost): store_attr()
+    __repr__ = basic_repr('author,price,cost')
+
+ProductPage("Jeremy", 1.50, 0.50)
+
+
__main__.ProductPage(author='Jeremy', price=1.5, cost=0.5)
+
+
+

One of the most interesting fastcore functions is the funcs_kwargs decorator. This allows class behavior to be modified without sub-classing. This can allow folks that aren’t familiar with object-oriented programming to customize your class more easily. Here’s an example of a class that uses funcs_kwargs:

+
+
@funcs_kwargs
+class T:
+    _methods=['some_method']
+    def __init__(self, **kwargs): assert not kwargs, f'Passed unknown args: {kwargs}'
+
+p = T(some_method = print)
+p.some_method("hello")
+
+
hello
+
+
+

The assert not kwargs above is used to ensure that the user doesn’t pass an unknown parameter (i.e one that’s not in _methods). fastai uses funcs_kwargs in many places, for instance, you can customize any part of a DataLoader by passing your own methods.

+

fastcore also provides many utility functions that make a Python programmer’s life easier, in fastcore.utils. We won’t look at many here, since you can easily look at the docs yourself. To get you started, have a look at the docs for chunked (remember, if you’re in a notebook, type doc(chunked)), which is a handy function for creating lazily generated batches from a collection.

+

Python’s ProcessPoolExecutor is extended to allow max_workers to be set to 0, to easily turn off parallel processing. This makes it easy to debug your code in serial, then run it in parallel. It also allows you to pass arguments to your parallel function, and to ensure there’s a pause between calls, in case the process you are running has race conditions. parallel makes parallel processing even easier to use, and even adds an optional progress bar.

+
+
+

L

+

Like most languages, Python allows for very concise syntax for some very common types, such as list, which can be constructed with [1,2,3]. Perl’s designer Larry Wall explained the reasoning for this kind of syntax:

+
+

In metaphorical honor of Huffman’s compression code that assigns smaller numbers of bits to more common bytes. In terms of syntax, it simply means that commonly used things should be shorter, but you shouldn’t waste short sequences on less common constructs.

+
+

On this basis, fastcore has just one type that has a single letter name: L. The reason for this is that it is designed to be a replacement for list, so we want it to be just as easy to use as [1,2,3]. Here’s how to create that as an L:

+
+
L(1,2,3)
+
+
(#3) [1,2,3]
+
+
+

The first thing to notice is that an L object includes in its representation its number of elements; that’s the (#3) in the output above. If there’s more than 10 elements, it will automatically truncate the list:

+
+
p = L.range(20).shuffle()
+p
+
+
(#20) [5,1,9,10,18,13,6,17,3,16...]
+
+
+

L contains many of the same indexing ideas that NumPy’s array does, including indexing with a list of indexes, or a boolean mask list:

+
+
p[2,4,6]
+
+
(#3) [9,18,6]
+
+
+

It also contains other methods used in array, such as L.argwhere:

+
+
p.argwhere(ge(15))
+
+
(#5) [4,7,9,18,19]
+
+
+

As you can see from this example, fastcore also includes a number of features that make a functional style of programming easier, such as a full range of boolean functions (e.g ge, gt, etc) which give the same answer as the functions from Python’s operator module if given two parameters, but return a curried function if given one parameter.

+

There’s too much functionality to show it all here, so be sure to check the docs. Many little things are added that we thought should have been in list in the first place, such as making this do what you’d expect (which is an error with list, but works fine with L):

+
+
1 + L(2,3,4)
+
+
(#4) [1,2,3,4]
+
+
+
+
+

Transforms

+

A Transform is the main building block of the fastai data pipelines. In the most general terms a transform can be any function you want to apply to your data, however the Transform class provides several mechanisms that make the process of building them easy and flexible (see the docs for information about each of these):

+
    +
  • Type dispatch
  • +
  • Dispatch over tuples
  • +
  • Reversability
  • +
  • Type propagation
  • +
  • Preprocessing
  • +
  • Filtering based on the dataset type
  • +
  • Ordering
  • +
  • Appending new behavior with decorators
  • +
+

Transform looks for three special methods, encodes, decodes, and setups, which provide the implementation for __call__, decode, and setup respectively. For instance:

+
+
class A(Transform):
+    def encodes(self, x): return x+1
+
+A()(1)
+
+
2
+
+
+

For simple transforms like this, you can also use Transform as a decorator:

+
+
@Transform
+def f(x): return x+1
+
+f(1)
+
+
2
+
+
+

Transforms can be composed into a Pipeline:

+
+
@Transform
+def g(x): return x/2
+
+pipe = Pipeline([f,g])
+pipe(3)
+
+
2.0
+
+
+

The power of Transform and Pipeline is best understood by seeing how they’re used to create a complete data processing pipeline. This is explained in chapter 11 of the fastai book, which is available for free in Jupyter Notebook format.

+ + +
+ +
+ +
+ + + + + \ No newline at end of file diff --git a/tour.html.md b/tour.html.md new file mode 100644 index 00000000..4b04a689 --- /dev/null +++ b/tour.html.md @@ -0,0 +1,417 @@ +# A tour of fastcore + + + + +Here’s a (somewhat) quick tour of a few higlights from fastcore. + +### Documentation + +All fast.ai projects, including this one, are built with +[nbdev](https://nbdev.fast.ai), which is a full literate programming +environment built on Jupyter Notebooks. That means that every piece of +documentation, including the page you’re reading now, can be accessed as +interactive Jupyter notebooks. In fact, you can even grab a link +directly to a notebook running interactively on Google Colab - if you +want to follow along with this tour, click the link below: + +``` python +colab_link('index') +``` + +[Open `index` in +Colab](https://colab.research.google.com/github/fastai/fastcore/blob/master/nbs/index.ipynb) + +The full docs are available at +[fastcore.fast.ai](https://fastcore.fast.ai). The code in the examples +and in all fast.ai libraries follow the [fast.ai style +guide](https://docs.fast.ai/dev/style.html). In order to support +interactive programming, all fast.ai libraries are designed to allow for +`import *` to be used safely, particular by ensuring that +[`__all__`](https://riptutorial.com/python/example/2894/the---all---special-variable) +is defined in all packages. In order to see where a function is from, +just type it: + +``` python +coll_repr +``` + + + +For more details, including a link to the full documentation and source +code, use `doc`, which pops up a window with this information: + +``` python +doc(coll_repr) +``` + + + +The documentation also contains links to any related functions or +classes, which appear like this: +[`coll_repr`](https://fastcore.fast.ai/foundation.html#coll_repr) (in +the notebook itself you will just see a word with back-ticks around it; +the links are auto-generated in the documentation site). The +documentation will generally show one or more examples of use, along +with any background context necessary to understand them. As you’ll see, +the examples for each function and method are shown as tests, rather +than example outputs, so let’s start by explaining that. + +### Testing + +fastcore’s testing module is designed to work well with +[nbdev](https://nbdev.fast.ai), which is a full literate programming +environment built on Jupyter Notebooks. That means that your tests, +docs, and code all live together in the same notebook. fastcore and +nbdev’s approach to testing starts with the premise that all your tests +should pass. If one fails, no more tests in a notebook are run. + +Tests look like this: + +``` python +test_eq(coll_repr(range(1000), 5), '(#1000) [0,1,2,3,4...]') +``` + +That’s an example from the docs for +[`coll_repr`](https://fastcore.fast.ai/foundation.html#coll_repr). As +you see, it’s not showing you the output directly. Here’s what that +would look like: + +``` python +coll_repr(range(1000), 5) +``` + + '(#1000) [0,1,2,3,4...]' + +So, the test is actually showing you what the output looks like, because +if the function call didn’t return `'(#1000) [0,1,2,3,4...]'`, then the +test would have failed. + +So every test shown in the docs is also showing you the behavior of the +library — and vice versa! + +Test functions always start with `test_`, and then follow with the +operation being tested. So +[`test_eq`](https://fastcore.fast.ai/test.html#test_eq) tests for +equality (as you saw in the example above). This includes tests for +equality of arrays and tensors, lists and generators, and many more: + +``` python +test_eq([0,1,2,3], np.arange(4)) +``` + +When a test fails, it prints out information about what was expected: + +``` python +test_eq([0,1,2,3], np.arange(3)) +``` + + ---- + AssertionError: ==: + [0, 1, 2, 3] + [0 1 2] + +If you want to check that objects are the same type, rather than the +just contain the same collection, use +[`test_eq_type`](https://fastcore.fast.ai/test.html#test_eq_type). + +You can test with any comparison function using +[`test`](https://fastcore.fast.ai/test.html#test), e.g test whether an +object is less than: + +``` python +test(2, 3, operator.lt) +``` + +You can even test that exceptions are raised: + +``` python +def divide_zero(): return 1/0 +test_fail(divide_zero) +``` + +…and test that things are printed to stdout: + +``` python +test_stdout(lambda: print('hi'), 'hi') +``` + +### Foundations + +fast.ai is unusual in that we often use +[mixins](https://en.wikipedia.org/wiki/Mixin) in our code. Mixins are +widely used in many programming languages, such as Ruby, but not so much +in Python. We use mixins to attach new behavior to existing libraries, +or to allow modules to add new behavior to our own classes, such as in +extension modules. One useful example of a mixin we define is +[`Path.ls`](https://fastcore.fast.ai/xtras.html#path.ls), which lists a +directory and returns an +[`L`](https://fastcore.fast.ai/foundation.html#l) (an extended list +class which we’ll discuss shortly): + +``` python +p = Path('images') +p.ls() +``` + + (#6) [Path('images/mnist3.png'),Path('images/att_00000.png'),Path('images/puppy.jpg'),Path('images/att_00005.png'),Path('images/att_00007.png'),Path('images/att_00006.png')] + +You can easily add you own mixins with the +[`patch`](https://fastcore.fast.ai/basics.html#patch) +[decorator](https://realpython.com/primer-on-python-decorators/), which +takes advantage of Python 3 [function +annotations](https://www.python.org/dev/peps/pep-3107/#parameters) to +say what class to patch: + +``` python +@patch +def num_items(self:Path): return len(self.ls()) + +p.num_items() +``` + + 6 + +We also use `**kwargs` frequently. In python `**kwargs` in a parameter +like means “*put any additional keyword arguments into a dict called +`kwargs`*”. Normally, using `kwargs` makes an API quite difficult to +work with, because it breaks things like tab-completion and popup lists +of signatures. `utils` provides +[`use_kwargs`](https://fastcore.fast.ai/meta.html#use_kwargs) and +[`delegates`](https://fastcore.fast.ai/meta.html#delegates) to avoid +this problem. See our [detailed article on +delegation](https://www.fast.ai/2019/08/06/delegation/) on this topic. + +[`GetAttr`](https://fastcore.fast.ai/basics.html#getattr) solves a +similar problem (and is also discussed in the article linked above): +it’s allows you to use Python’s exceptionally useful +[`__getattr__`](https://fastcore.fast.ai/xml.html#__getattr__) magic +method, but avoids the problem that normally in Python tab-completion +and docs break when using this. For instance, you can see here that +Python’s `dir` function, which is used to find the attributes of a +python object, finds everything inside the `self.default` attribute +here: + +``` python +class Author: + def __init__(self, name): self.name = name + +class ProductPage(GetAttr): + _default = 'author' + def __init__(self,author,price,cost): self.author,self.price,self.cost = author,price,cost + +p = ProductPage(Author("Jeremy"), 1.50, 0.50) +[o for o in dir(p) if not o.startswith('_')] +``` + + ['author', 'cost', 'name', 'price'] + +Looking at that `ProductPage` example, it’s rather verbose and +duplicates a lot of attribute names, which can lead to bugs later if you +change them only in one place. `fastcore` provides +[`store_attr`](https://fastcore.fast.ai/basics.html#store_attr) to +simplify this common pattern. It also provides +[`basic_repr`](https://fastcore.fast.ai/basics.html#basic_repr) to give +simple objects a useful `repr`: + +``` python +class ProductPage: + def __init__(self,author,price,cost): store_attr() + __repr__ = basic_repr('author,price,cost') + +ProductPage("Jeremy", 1.50, 0.50) +``` + + __main__.ProductPage(author='Jeremy', price=1.5, cost=0.5) + +One of the most interesting `fastcore` functions is the +[`funcs_kwargs`](https://fastcore.fast.ai/meta.html#funcs_kwargs) +decorator. This allows class behavior to be modified without +sub-classing. This can allow folks that aren’t familiar with +object-oriented programming to customize your class more easily. Here’s +an example of a class that uses +[`funcs_kwargs`](https://fastcore.fast.ai/meta.html#funcs_kwargs): + +``` python +@funcs_kwargs +class T: + _methods=['some_method'] + def __init__(self, **kwargs): assert not kwargs, f'Passed unknown args: {kwargs}' + +p = T(some_method = print) +p.some_method("hello") +``` + + hello + +The `assert not kwargs` above is used to ensure that the user doesn’t +pass an unknown parameter (i.e one that’s not in `_methods`). `fastai` +uses [`funcs_kwargs`](https://fastcore.fast.ai/meta.html#funcs_kwargs) +in many places, for instance, you can customize any part of a +`DataLoader` by passing your own methods. + +`fastcore` also provides many utility functions that make a Python +programmer’s life easier, in `fastcore.utils`. We won’t look at many +here, since you can easily look at the docs yourself. To get you +started, have a look at the docs for +[`chunked`](https://fastcore.fast.ai/basics.html#chunked) (remember, if +you’re in a notebook, type `doc(chunked)`), which is a handy function +for creating lazily generated batches from a collection. + +Python’s +[`ProcessPoolExecutor`](https://fastcore.fast.ai/parallel.html#processpoolexecutor) +is extended to allow `max_workers` to be set to `0`, to easily turn off +parallel processing. This makes it easy to debug your code in serial, +then run it in parallel. It also allows you to pass arguments to your +parallel function, and to ensure there’s a pause between calls, in case +the process you are running has race conditions. +[`parallel`](https://fastcore.fast.ai/parallel.html#parallel) makes +parallel processing even easier to use, and even adds an optional +progress bar. + +### L + +Like most languages, Python allows for very concise syntax for some very +common types, such as `list`, which can be constructed with `[1,2,3]`. +Perl’s designer Larry Wall explained the reasoning for this kind of +syntax: + +> In metaphorical honor of Huffman’s compression code that assigns +> smaller numbers of bits to more common bytes. In terms of syntax, it +> simply means that commonly used things should be shorter, but you +> shouldn’t waste short sequences on less common constructs. + +On this basis, `fastcore` has just one type that has a single letter +name: [`L`](https://fastcore.fast.ai/foundation.html#l). The reason for +this is that it is designed to be a replacement for `list`, so we want +it to be just as easy to use as `[1,2,3]`. Here’s how to create that as +an [`L`](https://fastcore.fast.ai/foundation.html#l): + +``` python +L(1,2,3) +``` + + (#3) [1,2,3] + +The first thing to notice is that an +[`L`](https://fastcore.fast.ai/foundation.html#l) object includes in its +representation its number of elements; that’s the `(#3)` in the output +above. If there’s more than 10 elements, it will automatically truncate +the list: + +``` python +p = L.range(20).shuffle() +p +``` + + (#20) [5,1,9,10,18,13,6,17,3,16...] + +[`L`](https://fastcore.fast.ai/foundation.html#l) contains many of the +same indexing ideas that NumPy’s `array` does, including indexing with a +list of indexes, or a boolean mask list: + +``` python +p[2,4,6] +``` + + (#3) [9,18,6] + +It also contains other methods used in `array`, such as +[`L.argwhere`](https://fastcore.fast.ai/foundation.html#l.argwhere): + +``` python +p.argwhere(ge(15)) +``` + + (#5) [4,7,9,18,19] + +As you can see from this example, `fastcore` also includes a number of +features that make a functional style of programming easier, such as a +full range of boolean functions (e.g `ge`, `gt`, etc) which give the +same answer as the functions from Python’s `operator` module if given +two parameters, but return a [curried +function](https://en.wikipedia.org/wiki/Currying) if given one +parameter. + +There’s too much functionality to show it all here, so be sure to check +the docs. Many little things are added that we thought should have been +in `list` in the first place, such as making this do what you’d expect +(which is an error with `list`, but works fine with +[`L`](https://fastcore.fast.ai/foundation.html#l)): + +``` python +1 + L(2,3,4) +``` + + (#4) [1,2,3,4] + +### Transforms + +A [`Transform`](https://fastcore.fast.ai/transform.html#transform) is +the main building block of the fastai data pipelines. In the most +general terms a transform can be any function you want to apply to your +data, however the +[`Transform`](https://fastcore.fast.ai/transform.html#transform) class +provides several mechanisms that make the process of building them easy +and flexible (see the docs for information about each of these): + +- Type dispatch +- Dispatch over tuples +- Reversability +- Type propagation +- Preprocessing +- Filtering based on the dataset type +- Ordering +- Appending new behavior with decorators + +[`Transform`](https://fastcore.fast.ai/transform.html#transform) looks +for three special methods, encodes, decodes, +and setups, which provide the implementation for +[`__call__`](https://www.python-course.eu/python3_magic_methods.php), +`decode`, and `setup` respectively. For instance: + +``` python +class A(Transform): + def encodes(self, x): return x+1 + +A()(1) +``` + + 2 + +For simple transforms like this, you can also use +[`Transform`](https://fastcore.fast.ai/transform.html#transform) as a +decorator: + +``` python +@Transform +def f(x): return x+1 + +f(1) +``` + + 2 + +Transforms can be composed into a +[`Pipeline`](https://fastcore.fast.ai/transform.html#pipeline): + +``` python +@Transform +def g(x): return x/2 + +pipe = Pipeline([f,g]) +pipe(3) +``` + + 2.0 + +The power of +[`Transform`](https://fastcore.fast.ai/transform.html#transform) and +[`Pipeline`](https://fastcore.fast.ai/transform.html#pipeline) is best +understood by seeing how they’re used to create a complete data +processing pipeline. This is explained in [chapter +11](https://github.com/fastai/fastbook/blob/master/11_midlevel_data.ipynb) +of the [fastai +book](https://www.amazon.com/Deep-Learning-Coders-fastai-PyTorch/dp/1492045527), +which is [available for free](https://github.com/fastai/fastbook) in +Jupyter Notebook format. diff --git a/transform.html b/transform.html new file mode 100644 index 00000000..fe9eae9a --- /dev/null +++ b/transform.html @@ -0,0 +1,1450 @@ + + + + + + + + + + +Transforms – fastcore + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ +
+ + +
+ + + +
+ +
+
+

Transforms

+
+ +
+
+ Definition of Transform and Pipeline +
+
+ + +
+ + + + +
+ + + +
+ + + +
+
from __future__ import annotations
+from nbdev.showdoc import *
+from fastcore.test import *
+from fastcore.nb_imports import *
+
+

The classes here provide functionality for creating a composition of partially reversible functions. By “partially reversible” we mean that a transform can be decoded, creating a form suitable for display. This is not necessarily identical to the original form (e.g. a transform that changes a byte tensor to a float tensor does not recreate a byte tensor when decoded, since that may lose precision, and a float tensor can be displayed already).

+

Classes are also provided and for composing transforms, and mapping them over collections. Pipeline is a transform which composes several Transform, knowing how to decode them or show an encoded item.

+
+

source

+
+

Transform

+
+
 Transform (enc=None, dec=None, split_idx=None, order=None)
+
+

Delegates (__call__,decode,setup) to (encodes,decodes,setups) if split_idx matches

+

A Transform is the main building block of the fastai data pipelines. In the most general terms a transform can be any function you want to apply to your data, however the Transform class provides several mechanisms that make the process of building them easy and flexible.

+
+
+

The main Transform features:

+
    +
  • Type dispatch - Type annotations are used to determine if a transform should be applied to the given argument. It also gives an option to provide several implementations and it choses the one to run based on the type. This is useful for example when running both independent and dependent variables through the pipeline where some transforms only make sense for one and not the other. Another usecase is designing a transform that handles different data formats. Note that if a transform takes multiple arguments only the type of the first one is used for dispatch.
  • +
  • Handling of tuples - When a tuple (or a subclass of tuple) of data is passed to a transform it will get applied to each element separately. You can opt out of this behavior by passing a list or an L, as only tuples gets this specific behavior. An alternative is to use ItemTransform defined below, which will always take the input as a whole.
  • +
  • Reversability - A transform can be made reversible by implementing the decodes method. This is mainly used to turn something like a category which is encoded as a number back into a label understandable by humans for showing purposes. Like the regular call method, the decode method that is used to decode will be applied over each element of a tuple separately.
  • +
  • Type propagation - Whenever possible a transform tries to return data of the same type it received. Mainly used to maintain semantics of things like ArrayImage which is a thin wrapper of pytorch’s Tensor. You can opt out of this behavior by adding ->None return type annotation.
  • +
  • Preprocessing - The setup method can be used to perform any one-time calculations to be later used by the transform, for example generating a vocabulary to encode categorical data.
  • +
  • Filtering based on the dataset type - By setting the split_idx flag you can make the transform be used only in a specific DataSource subset like in training, but not validation.
  • +
  • Ordering - You can set the order attribute which the Pipeline uses when it needs to merge two lists of transforms.
  • +
  • Appending new behavior with decorators - You can easily extend an existing Transform by creating encodes or decodes methods for new data types. You can put those new methods outside the original transform definition and decorate them with the class you wish them patched into. This can be used by the fastai library users to add their own behavior, or multiple modules contributing to the same transform.
  • +
+
+
+

Defining a Transform

+

There are a few ways to create a transform with different ratios of simplicity to flexibility. - Extending the Transform class - Use inheritence to implement the methods you want. - Passing methods to the constructor - Instantiate the Transform class and pass your functions as enc and dec arguments. - @Transform decorator - Turn any function into a Transform by just adding a decorator - very straightforward if all you need is a single encodes implementation. - Passing a function to fastai APIs - Same as above, but when passing a function to other transform aware classes like Pipeline or TfmdDS you don’t even need a decorator. Your function will get converted to a Transform automatically.

+

A simple way to create a Transform is to pass a function to the constructor. In the below example, we pass an anonymous function that does integer division by 2:

+
+
f = Transform(lambda o:o//2)
+
+

If you call this transform, it will apply the transformation:

+
+
test_eq_type(f(2), 1)
+
+

Another way to define a Transform is to extend the Transform class:

+
+
class A(Transform): pass
+
+

However, to enable your transform to do something, you have to define an encodes method. Note that we can use the class name as a decorator to add this method to the original class.

+
+
@A
+def encodes(self, x): return x+1
+
+f1 = A()
+test_eq(f1(1), 2) # f1(1) is the same as f1.encode(1)
+
+

In addition to adding an encodes method, we can also add a decodes method. This enables you to call the decode method (without an s). For more information about the purpose of decodes, see the discussion about Reversibility in the above section.

+

Just like with encodes, you can add a decodes method to the original class by using the class name as a decorator:

+
+
class B(A): pass
+
+@B
+def decodes(self, x): return x-1
+
+f2 = B()
+test_eq(f2.decode(2), 1)
+
+test_eq(f2(1), 2) # uses A's encode method from the parent class
+
+

If you do not define an encodes or decodes method the original value will be returned:

+
+
class _Tst(Transform): pass 
+
+f3 = _Tst() # no encodes or decodes method have been defined
+test_eq_type(f3.decode(2.0), 2.0)
+test_eq_type(f3(2), 2)
+
+

Transforms can be created from class methods too:

+
+
class A:
+    @classmethod
+    def create(cls, x:int): return x+1
+test_eq(Transform(A.create)(1), 2)
+
+
+

Defining Transforms With A Decorator

+

Transform can be used as a decorator to turn a function into a Transform.

+
+
@Transform
+def f(x): return x//2
+test_eq_type(f(2), 1)
+test_eq_type(f.decode(2.0), 2.0)
+
+@Transform
+def f(x): return x*2
+test_eq_type(f(2), 4)
+test_eq_type(f.decode(2.0), 2.0)
+
+
+
+

Typed Dispatch and Transforms

+

We can also apply different transformations depending on the type of the input passed by using TypedDispatch. TypedDispatch automatically works with Transform when using type hints:

+
+
class A(Transform): pass
+
+@A
+def encodes(self, x:int): return x//2
+
+@A
+def encodes(self, x:float): return x+1
+
+

When we pass in an int, this calls the first encodes method:

+
+
f = A()
+test_eq_type(f(3), 1)
+
+

When we pass in a float, this calls the second encodes method:

+
+
test_eq_type(f(2.), 3.)
+
+

When we pass in a type that is not specified in encodes, the original value is returned:

+
+
test_eq(f('a'), 'a')
+
+

If the type annotation is a tuple, then any type in the tuple will match:

+
+
class MyClass(int): pass
+
+class A(Transform):
+    def encodes(self, x:MyClass|float): return x/2
+    def encodes(self, x:str|list): return str(x)+'_1'
+
+f = A()
+
+

The below two examples match the first encodes, with a type of MyClass and float, respectively:

+
+
test_eq(f(MyClass(2)), 1.) # input is of type MyClass 
+test_eq(f(6.0), 3.0) # input is of type float
+
+

The next two examples match the second encodes method, with a type of str and list, respectively:

+
+
test_eq(f('a'), 'a_1') # input is of type str
+test_eq(f(['a','b','c']), "['a', 'b', 'c']_1") # input is of type list
+
+
+
+

Casting Types With Transform

+

Without any intervention it is easy for operations to change types in Python. For example, FloatSubclass (defined below) becomes a float after performing multiplication:

+
+
class FloatSubclass(float): pass
+test_eq_type(FloatSubclass(3.0) * 2, 6.0)
+
+

This behavior is often not desirable when performing transformations on data. Therefore, Transform will attempt to cast the output to be of the same type as the input by default. In the below example, the output will be cast to a FloatSubclass type to match the type of the input:

+
+
@Transform
+def f(x): return x*2
+
+test_eq_type(f(FloatSubclass(3.0)), FloatSubclass(6.0))
+
+

We can optionally turn off casting by annotating the transform function with a return type of None:

+
+
@Transform
+def f(x)-> None: return x*2 # Same transform as above, but with a -> None annotation
+
+test_eq_type(f(FloatSubclass(3.0)), 6.0)  # Casting is turned off because of -> None annotation
+
+

However, Transform will only cast output back to the input type when the input is a subclass of the output. In the below example, the input is of type FloatSubclass which is not a subclass of the output which is of type str. Therefore, the output doesn’t get cast back to FloatSubclass and stays as type str:

+
+
@Transform
+def f(x): return str(x)
+    
+test_eq_type(f(Float(2.)), '2.0')
+
+

Just like encodes, the decodes method will cast outputs to match the input type in the same way. In the below example, the output of decodes remains of type MySubclass:

+
+
class MySubclass(int): pass
+
+def enc(x): return MySubclass(x+1)
+def dec(x): return x-1
+
+
+f = Transform(enc,dec)
+t = f(1) # t is of type MySubclass
+test_eq_type(f.decode(t), MySubclass(1)) # the output of decode is cast to MySubclass to match the input type.
+
+
+
+

Apply Transforms On Subsets With split_idx

+

You can apply transformations to subsets of data by specifying a split_idx property. If a transform has a split_idx then it’s only applied if the split_idx param matches. In the below example, we set split_idx equal to 1:

+
+
def enc(x): return x+1
+def dec(x): return x-1
+f = Transform(enc,dec)
+f.split_idx = 1
+
+

The transformations are applied when a matching split_idx parameter is passed:

+
+
test_eq(f(1, split_idx=1),2)
+test_eq(f.decode(2, split_idx=1),1)
+
+

On the other hand, transformations are ignored when the split_idx parameter does not match:

+
+
test_eq(f(1, split_idx=0), 1)
+test_eq(f.decode(2, split_idx=0), 2)
+
+
+
+

Transforms on Lists

+

Transform operates on lists as a whole, not element-wise:

+
+
class A(Transform):
+    def encodes(self, x): return dict(x)
+    def decodes(self, x): return list(x.items())
+    
+f = A()
+_inp = [(1,2), (3,4)]
+t = f(_inp)
+
+test_eq(t, dict(_inp))
+test_eq(f.decodes(t), _inp)
+
+

If you want a transform to operate on a list elementwise, you must implement this appropriately in the encodes and decodes methods:

+
+
class AL(Transform): pass
+
+@AL
+def encodes(self, x): return [x_+1 for x_ in x]
+
+@AL
+def decodes(self, x): return [x_-1 for x_ in x]
+
+f = AL()
+t = f([1,2])
+
+test_eq(t, [2,3])
+test_eq(f.decode(t), [1,2])
+
+
+
+

Transforms on Tuples

+

Unlike lists, Transform operates on tuples element-wise.

+
+
def neg_int(x): return -x
+f = Transform(neg_int)
+
+test_eq(f((1,2,3)), (-1,-2,-3))
+
+

Transforms will also apply TypedDispatch element-wise on tuples when an input type annotation is specified. In the below example, the values 1.0 and 3.0 are ignored because they are of type float, not int:

+
+
def neg_int(x:int): return -x
+f = Transform(neg_int)
+
+test_eq(f((1.0, 2, 3.0)), (1.0, -2, 3.0))
+
+

Another example of how Transform can use TypedDispatch with tuples is shown below:

+
+
class B(Transform): pass
+
+@B
+def encodes(self, x:int): return x+1
+
+@B
+def encodes(self, x:str): return x+'hello'
+
+@B
+def encodes(self, x): return str(x)+'!'
+
+

If the input is not an int or str, the third encodes method will apply:

+
+
b = B()
+test_eq(b([1]), '[1]!') 
+test_eq(b([1.0]), '[1.0]!')
+
+

However, if the input is a tuple, then the appropriate method will apply according to the type of each element in the tuple:

+
+
test_eq(b(('1',)), ('1hello',))
+test_eq(b((1,2)), (2,3))
+test_eq(b(('a',1.0)), ('ahello','1.0!'))
+
+

Dispatching over tuples works recursively, by the way:

+
+
class B(Transform):
+    def encodes(self, x:int): return x+1
+    def encodes(self, x:str): return x+'_hello'
+    def decodes(self, x:int): return x-1
+    def decodes(self, x:str): return x.replace('_hello', '')
+
+f = B()
+start = (1.,(2,'3'))
+t = f(start)
+test_eq_type(t, (1.,(3,'3_hello')))
+test_eq(f.decode(t), start)
+
+

Dispatching also works with typing module type classes, like numbers.integral:

+
+
@Transform
+def f(x:numbers.Integral): return x+1
+
+t = f((1,'1',1))
+test_eq(t, (2, '1', 2))
+
+
+

source

+
+
+
+

InplaceTransform

+
+
 InplaceTransform (enc=None, dec=None, split_idx=None, order=None)
+
+

A Transform that modifies in-place and just returns whatever it’s passed

+
+
class A(InplaceTransform): pass
+
+@A
+def encodes(self, x:pd.Series): x.fillna(10, inplace=True)
+    
+f = A()
+
+test_eq_type(f(pd.Series([1,2,None])),pd.Series([1,2,10],dtype=np.float64)) #fillna fills with floats.
+
+
+

source

+
+
+

DisplayedTransform

+
+
 DisplayedTransform (enc=None, dec=None, split_idx=None, order=None)
+
+

A transform with a __repr__ that shows its attrs

+

Transforms normally are represented by just their class name and a list of encodes and decodes implementations:

+
+
class A(Transform): encodes,decodes = noop,noop
+f = A()
+f
+
+
A:
+encodes: (object,object) -> noop
+decodes: (object,object) -> noop
+
+
+

A DisplayedTransform will in addition show the contents of all attributes listed in the comma-delimited string self.store_attrs:

+
+
class A(DisplayedTransform):
+    encodes = noop
+    def __init__(self, a, b=2):
+        super().__init__()
+        store_attr()
+    
+A(a=1,b=2)
+
+
A -- {'a': 1, 'b': 2}:
+encodes: (object,object) -> noop
+decodes: 
+
+
+
+

source

+
+
+

ItemTransform

+
+
 ItemTransform (enc=None, dec=None, split_idx=None, order=None)
+
+

A transform that always take tuples as items

+

ItemTransform is the class to use to opt out of the default behavior of Transform.

+
+
class AIT(ItemTransform): 
+    def encodes(self, xy): x,y=xy; return (x+y,y)
+    def decodes(self, xy): x,y=xy; return (x-y,y)
+    
+f = AIT()
+test_eq(f((1,2)), (3,2))
+test_eq(f.decode((3,2)), (1,2))
+
+

If you pass a special tuple subclass, the usual retain type behavior of Transform will keep it:

+
+
class _T(tuple): pass
+x = _T((1,2))
+test_eq_type(f(x), _T((3,2)))
+
+
+

source

+
+
+

get_func

+
+
 get_func (t, name, *args, **kwargs)
+
+

Get the t.name (potentially partial-ized with args and kwargs) or noop if not defined

+

This works for any kind of t supporting getattr, so a class or a module.

+
+
test_eq(get_func(operator, 'neg', 2)(), -2)
+test_eq(get_func(operator.neg, '__call__')(2), -2)
+test_eq(get_func(list, 'foobar')([2]), [2])
+a = [2,1]
+get_func(list, 'sort')(a)
+test_eq(a, [1,2])
+
+

Transforms are built with multiple-dispatch: a given function can have several methods depending on the type of the object received. This is done directly with the TypeDispatch module and type-annotation in Transform, but you can also use the following class.

+
+

source

+
+
+

Func

+
+
 Func (name, *args, **kwargs)
+
+

Basic wrapper around a name with args and kwargs to call on a given type

+

You can call the Func object on any module name or type, even a list of types. It will return the corresponding function (with a default to noop if nothing is found) or list of functions.

+
+
test_eq(Func('sqrt')(math), math.sqrt)
+
+
+
+
+

Sig

+
+
 Sig (*args, **kwargs)
+
+

Sig is just sugar-syntax to create a Func object more easily with the syntax Sig.name(*args, **kwargs).

+
+
f = Sig.sqrt()
+test_eq(f(math), math.sqrt)
+
+
+

source

+
+
+

compose_tfms

+
+
 compose_tfms (x, tfms, is_enc=True, reverse=False, **kwargs)
+
+

Apply all func_nm attribute of tfms on x, maybe in reverse order

+
+
def to_int  (x):   return Int(x)
+def to_float(x):   return Float(x)
+def double  (x):   return x*2
+def half(x)->None: return x/2
+
+
+
def test_compose(a, b, *fs): test_eq_type(compose_tfms(a, tfms=map(Transform,fs)), b)
+
+test_compose(1,   Int(1),   to_int)
+test_compose(1,   Float(1), to_int,to_float)
+test_compose(1,   Float(2), to_int,to_float,double)
+test_compose(2.0, 2.0,      to_int,double,half)
+
+
+
class A(Transform):
+    def encodes(self, x:float):  return Float(x+1)
+    def decodes(self, x): return x-1
+    
+tfms = [A(), Transform(math.sqrt)]
+t = compose_tfms(3., tfms=tfms)
+test_eq_type(t, Float(2.))
+test_eq(compose_tfms(t, tfms=tfms, is_enc=False), 1.)
+test_eq(compose_tfms(4., tfms=tfms, reverse=True), 3.)
+
+
+
tfms = [A(), Transform(math.sqrt)]
+test_eq(compose_tfms((9,3.), tfms=tfms), (3,2.))
+
+
+

source

+
+
+

mk_transform

+
+
 mk_transform (f)
+
+

Convert function f to Transform if it isn’t already one

+
+

source

+
+
+

gather_attrs

+
+
 gather_attrs (o, k, nm)
+
+

Used in getattr to collect all attrs k from self.{nm}

+
+

source

+
+
+

gather_attr_names

+
+
 gather_attr_names (o, nm)
+
+

Used in dir to collect all attrs k from self.{nm}

+
+

source

+
+
+

Pipeline

+
+
 Pipeline (funcs=None, split_idx=None)
+
+

A pipeline of composed (for encode/decode) transforms, setup with types

+
+
add_docs(Pipeline,
+         __call__="Compose `__call__` of all `fs` on `o`",
+         decode="Compose `decode` of all `fs` on `o`",
+         show="Show `o`, a single item from a tuple, decoding as needed",
+         add="Add transforms `ts`",
+         setup="Call each tfm's `setup` in order")
+
+

Pipeline is a wrapper for compose_tfms. You can pass instances of Transform or regular functions in funcs, the Pipeline will wrap them all in Transform (and instantiate them if needed) during the initialization. It handles the transform setup by adding them one at a time and calling setup on each, goes through them in order in __call__ or decode and can show an object by applying decoding the transforms up until the point it gets an object that knows how to show itself.

+
+
# Empty pipeline is noop
+pipe = Pipeline()
+test_eq(pipe(1), 1)
+test_eq(pipe((1,)), (1,))
+# Check pickle works
+assert pickle.loads(pickle.dumps(pipe))
+
+
+
class IntFloatTfm(Transform):
+    def encodes(self, x):  return Int(x)
+    def decodes(self, x):  return Float(x)
+    foo=1
+
+int_tfm=IntFloatTfm()
+
+def neg(x): return -x
+neg_tfm = Transform(neg, neg)
+
+
+
pipe = Pipeline([neg_tfm, int_tfm])
+
+start = 2.0
+t = pipe(start)
+test_eq_type(t, Int(-2))
+test_eq_type(pipe.decode(t), Float(start))
+test_stdout(lambda:pipe.show(t), '-2')
+
+
+
pipe = Pipeline([neg_tfm, int_tfm])
+t = pipe(start)
+test_stdout(lambda:pipe.show(pipe((1.,2.))), '-1\n-2')
+test_eq(pipe.foo, 1)
+assert 'foo' in dir(pipe)
+assert 'int_float_tfm' in dir(pipe)
+
+

You can add a single transform or multiple transforms ts using Pipeline.add. Transforms will be ordered by Transform.order.

+
+
pipe = Pipeline([neg_tfm, int_tfm])
+class SqrtTfm(Transform):
+    order=-1
+    def encodes(self, x): 
+        return x**(.5)
+    def decodes(self, x): return x**2
+pipe.add(SqrtTfm())
+test_eq(pipe(4),-2)
+test_eq(pipe.decode(-2),4)
+pipe.add([SqrtTfm(),SqrtTfm()])
+test_eq(pipe(256),-2)
+test_eq(pipe.decode(-2),256)
+
+

Transforms are available as attributes named with the snake_case version of the names of their types. Attributes in transforms can be directly accessed as attributes of the pipeline.

+
+
test_eq(pipe.int_float_tfm, int_tfm)
+test_eq(pipe.foo, 1)
+
+pipe = Pipeline([int_tfm, int_tfm])
+pipe.int_float_tfm
+test_eq(pipe.int_float_tfm[0], int_tfm)
+test_eq(pipe.foo, [1,1])
+
+
+
# Check opposite order
+pipe = Pipeline([int_tfm,neg_tfm])
+t = pipe(start)
+test_eq(t, -2)
+test_stdout(lambda:pipe.show(t), '-2')
+
+
+
class A(Transform):
+    def encodes(self, x):  return int(x)
+    def decodes(self, x):  return Float(x)
+
+pipe = Pipeline([neg_tfm, A])
+t = pipe(start)
+test_eq_type(t, -2)
+test_eq_type(pipe.decode(t), Float(start))
+test_stdout(lambda:pipe.show(t), '-2.0')
+
+
+
s2 = (1,2)
+pipe = Pipeline([neg_tfm, A])
+t = pipe(s2)
+test_eq_type(t, (-1,-2))
+test_eq_type(pipe.decode(t), (Float(1.),Float(2.)))
+test_stdout(lambda:pipe.show(t), '-1.0\n-2.0')
+
+
+
from PIL import Image
+
+
+
class ArrayImage(ndarray):
+    _show_args = {'cmap':'viridis'}
+    def __new__(cls, x, *args, **kwargs):
+        if isinstance(x,tuple): super().__new__(cls, x, *args, **kwargs)
+        if args or kwargs: raise RuntimeError('Unknown array init args')
+        if not isinstance(x,ndarray): x = array(x)
+        return x.view(cls)
+    
+    def show(self, ctx=None, figsize=None, **kwargs):
+        if ctx is None: _,ctx = plt.subplots(figsize=figsize)
+        ctx.imshow(im, **{**self._show_args, **kwargs})
+        ctx.axis('off')
+        return ctx
+    
+im = Image.open(TEST_IMAGE)
+im_t = ArrayImage(im)
+
+
+
def f1(x:ArrayImage): return -x
+def f2(x): return Image.open(x).resize((128,128))
+def f3(x:Image.Image): return(ArrayImage(array(x)))
+
+
+
pipe = Pipeline([f2,f3,f1])
+t = pipe(TEST_IMAGE)
+test_eq(type(t), ArrayImage)
+test_eq(t, -array(f3(f2(TEST_IMAGE))))
+
+
+
pipe = Pipeline([f2,f3])
+t = pipe(TEST_IMAGE)
+ax = pipe.show(t)
+
+
+
+

+
+
+
+
+
+
#test_fig_exists(ax)
+
+
+
#Check filtering is properly applied
+add1 = B()
+add1.split_idx = 1
+pipe = Pipeline([neg_tfm, A(), add1])
+test_eq(pipe(start), -2)
+pipe.split_idx=1
+test_eq(pipe(start), -1)
+pipe.split_idx=0
+test_eq(pipe(start), -2)
+for t in [None, 0, 1]:
+    pipe.split_idx=t
+    test_eq(pipe.decode(pipe(start)), start)
+    test_stdout(lambda: pipe.show(pipe(start)), "-2.0")
+
+
+
def neg(x): return -x
+test_eq(type(mk_transform(neg)), Transform)
+test_eq(type(mk_transform(math.sqrt)), Transform)
+test_eq(type(mk_transform(lambda a:a*2)), Transform)
+test_eq(type(mk_transform(Pipeline([neg]))), Pipeline)
+
+
+
+

Methods

+
+
#TODO: method examples
+
+
+

source

+
+
+

Pipeline.__call__

+
+
 Pipeline.__call__ (o)
+
+

Call self as a function.

+
+

source

+
+
+

Pipeline.decode

+
+
 Pipeline.decode (o, full=True)
+
+
+

source

+
+
+

Pipeline.setup

+
+
 Pipeline.setup (items=None, train_setup=False)
+
+

During the setup, the Pipeline starts with no transform and adds them one at a time, so that during its setup, each transform gets the items processed up to its point and not after.

+ + +
+ +
+ +
+ + + + + \ No newline at end of file diff --git a/transform.html.md b/transform.html.md new file mode 100644 index 00000000..be2df566 --- /dev/null +++ b/transform.html.md @@ -0,0 +1,1009 @@ +# Transforms + + + + +``` python +from __future__ import annotations +from nbdev.showdoc import * +from fastcore.test import * +from fastcore.nb_imports import * +``` + +The classes here provide functionality for creating a composition of +*partially reversible functions*. By “partially reversible” we mean that +a transform can be `decode`d, creating a form suitable for display. This +is not necessarily identical to the original form (e.g. a transform that +changes a byte tensor to a float tensor does not recreate a byte tensor +when decoded, since that may lose precision, and a float tensor can be +displayed already). + +Classes are also provided and for composing transforms, and mapping them +over collections. +[`Pipeline`](https://fastcore.fast.ai/transform.html#pipeline) is a +transform which composes several +[`Transform`](https://fastcore.fast.ai/transform.html#transform), +knowing how to decode them or show an encoded item. + +------------------------------------------------------------------------ + +source + +### Transform + +> Transform (enc=None, dec=None, split_idx=None, order=None) + +*Delegates (`__call__`,`decode`,`setup`) to +(encodes,decodes,setups) if +`split_idx` matches* + +A [`Transform`](https://fastcore.fast.ai/transform.html#transform) is +the main building block of the fastai data pipelines. In the most +general terms a transform can be any function you want to apply to your +data, however the +[`Transform`](https://fastcore.fast.ai/transform.html#transform) class +provides several mechanisms that make the process of building them easy +and flexible. + +### The main [`Transform`](https://fastcore.fast.ai/transform.html#transform) features: + +- **Type dispatch** - Type annotations are used to determine if a + transform should be applied to the given argument. It also gives an + option to provide several implementations and it choses the one to run + based on the type. This is useful for example when running both + independent and dependent variables through the pipeline where some + transforms only make sense for one and not the other. Another usecase + is designing a transform that handles different data formats. Note + that if a transform takes multiple arguments only the type of the + first one is used for dispatch. +- **Handling of tuples** - When a tuple (or a subclass of tuple) of data + is passed to a transform it will get applied to each element + separately. You can opt out of this behavior by passing a list or an + [`L`](https://fastcore.fast.ai/foundation.html#l), as only tuples gets + this specific behavior. An alternative is to use + [`ItemTransform`](https://fastcore.fast.ai/transform.html#itemtransform) + defined below, which will always take the input as a whole. +- **Reversability** - A transform can be made reversible by implementing + the decodes method. This is mainly used to turn something + like a category which is encoded as a number back into a label + understandable by humans for showing purposes. Like the regular call + method, the `decode` method that is used to decode will be applied + over each element of a tuple separately. +- **Type propagation** - Whenever possible a transform tries to return + data of the same type it received. Mainly used to maintain semantics + of things like `ArrayImage` which is a thin wrapper of pytorch’s + `Tensor`. You can opt out of this behavior by adding `->None` return + type annotation. +- **Preprocessing** - The `setup` method can be used to perform any + one-time calculations to be later used by the transform, for example + generating a vocabulary to encode categorical data. +- **Filtering based on the dataset type** - By setting the `split_idx` + flag you can make the transform be used only in a specific + `DataSource` subset like in training, but not validation. +- **Ordering** - You can set the `order` attribute which the + [`Pipeline`](https://fastcore.fast.ai/transform.html#pipeline) uses + when it needs to merge two lists of transforms. +- **Appending new behavior with decorators** - You can easily extend an + existing + [`Transform`](https://fastcore.fast.ai/transform.html#transform) by + creating encodes or decodes methods for new + data types. You can put those new methods outside the original + transform definition and decorate them with the class you wish them + patched into. This can be used by the fastai library users to add + their own behavior, or multiple modules contributing to the same + transform. + +### Defining a [`Transform`](https://fastcore.fast.ai/transform.html#transform) + +There are a few ways to create a transform with different ratios of +simplicity to flexibility. - **Extending the +[`Transform`](https://fastcore.fast.ai/transform.html#transform) +class** - Use inheritence to implement the methods you want. - **Passing +methods to the constructor** - Instantiate the +[`Transform`](https://fastcore.fast.ai/transform.html#transform) class +and pass your functions as `enc` and `dec` arguments. - **@Transform +decorator** - Turn any function into a +[`Transform`](https://fastcore.fast.ai/transform.html#transform) by just +adding a decorator - very straightforward if all you need is a single +encodes implementation. - **Passing a function to fastai +APIs** - Same as above, but when passing a function to other transform +aware classes like +[`Pipeline`](https://fastcore.fast.ai/transform.html#pipeline) or +`TfmdDS` you don’t even need a decorator. Your function will get +converted to a +[`Transform`](https://fastcore.fast.ai/transform.html#transform) +automatically. + +A simple way to create a +[`Transform`](https://fastcore.fast.ai/transform.html#transform) is to +pass a function to the constructor. In the below example, we pass an +anonymous function that does integer division by 2: + +``` python +f = Transform(lambda o:o//2) +``` + +If you call this transform, it will apply the transformation: + +``` python +test_eq_type(f(2), 1) +``` + +Another way to define a Transform is to extend the +[`Transform`](https://fastcore.fast.ai/transform.html#transform) class: + +``` python +class A(Transform): pass +``` + +However, to enable your transform to do something, you have to define an +encodes method. Note that we can use the class name as a +decorator to add this method to the original class. + +``` python +@A +def encodes(self, x): return x+1 + +f1 = A() +test_eq(f1(1), 2) # f1(1) is the same as f1.encode(1) +``` + +In addition to adding an encodes method, we can also add a +decodes method. This enables you to call the `decode` +method (without an s). For more information about the purpose of +decodes, see the discussion about Reversibility in [the +above section](#The-main-Transform-features). + +Just like with encodes, you can add a decodes method to the +original class by using the class name as a decorator: + +``` python +class B(A): pass + +@B +def decodes(self, x): return x-1 + +f2 = B() +test_eq(f2.decode(2), 1) + +test_eq(f2(1), 2) # uses A's encode method from the parent class +``` + +If you do not define an encodes or decodes +method the original value will be returned: + +``` python +class _Tst(Transform): pass + +f3 = _Tst() # no encodes or decodes method have been defined +test_eq_type(f3.decode(2.0), 2.0) +test_eq_type(f3(2), 2) +``` + +Transforms can be created from class methods too: + +``` python +class A: + @classmethod + def create(cls, x:int): return x+1 +test_eq(Transform(A.create)(1), 2) +``` + +#### Defining Transforms With A Decorator + +[`Transform`](https://fastcore.fast.ai/transform.html#transform) can be +used as a decorator to turn a function into a +[`Transform`](https://fastcore.fast.ai/transform.html#transform). + +``` python +@Transform +def f(x): return x//2 +test_eq_type(f(2), 1) +test_eq_type(f.decode(2.0), 2.0) + +@Transform +def f(x): return x*2 +test_eq_type(f(2), 4) +test_eq_type(f.decode(2.0), 2.0) +``` + +#### Typed Dispatch and Transforms + +We can also apply different transformations depending on the type of the +input passed by using `TypedDispatch`. `TypedDispatch` automatically +works with +[`Transform`](https://fastcore.fast.ai/transform.html#transform) when +using type hints: + +``` python +class A(Transform): pass + +@A +def encodes(self, x:int): return x//2 + +@A +def encodes(self, x:float): return x+1 +``` + +When we pass in an `int`, this calls the first encodes method: + +``` python +f = A() +test_eq_type(f(3), 1) +``` + +When we pass in a `float`, this calls the second encodes method: + +``` python +test_eq_type(f(2.), 3.) +``` + +When we pass in a type that is not specified in encodes, +the original value is returned: + +``` python +test_eq(f('a'), 'a') +``` + +If the type annotation is a tuple, then any type in the tuple will +match: + +``` python +class MyClass(int): pass + +class A(Transform): + def encodes(self, x:MyClass|float): return x/2 + def encodes(self, x:str|list): return str(x)+'_1' + +f = A() +``` + +The below two examples match the first encodes, with a type of `MyClass` +and `float`, respectively: + +``` python +test_eq(f(MyClass(2)), 1.) # input is of type MyClass +test_eq(f(6.0), 3.0) # input is of type float +``` + +The next two examples match the second `encodes` method, with a type of +`str` and `list`, respectively: + +``` python +test_eq(f('a'), 'a_1') # input is of type str +test_eq(f(['a','b','c']), "['a', 'b', 'c']_1") # input is of type list +``` + +#### Casting Types With Transform + +Without any intervention it is easy for operations to change types in +Python. For example, `FloatSubclass` (defined below) becomes a `float` +after performing multiplication: + +``` python +class FloatSubclass(float): pass +test_eq_type(FloatSubclass(3.0) * 2, 6.0) +``` + +This behavior is often not desirable when performing transformations on +data. Therefore, +[`Transform`](https://fastcore.fast.ai/transform.html#transform) will +attempt to cast the output to be of the same type as the input by +default. In the below example, the output will be cast to a +`FloatSubclass` type to match the type of the input: + +``` python +@Transform +def f(x): return x*2 + +test_eq_type(f(FloatSubclass(3.0)), FloatSubclass(6.0)) +``` + +We can optionally turn off casting by annotating the transform function +with a return type of `None`: + +``` python +@Transform +def f(x)-> None: return x*2 # Same transform as above, but with a -> None annotation + +test_eq_type(f(FloatSubclass(3.0)), 6.0) # Casting is turned off because of -> None annotation +``` + +However, +[`Transform`](https://fastcore.fast.ai/transform.html#transform) will +only cast output back to the input type when the input is a subclass of +the output. In the below example, the input is of type `FloatSubclass` +which is not a subclass of the output which is of type `str`. Therefore, +the output doesn’t get cast back to `FloatSubclass` and stays as type +`str`: + +``` python +@Transform +def f(x): return str(x) + +test_eq_type(f(Float(2.)), '2.0') +``` + +Just like encodes, the decodes method will +cast outputs to match the input type in the same way. In the below +example, the output of decodes remains of type +`MySubclass`: + +``` python +class MySubclass(int): pass + +def enc(x): return MySubclass(x+1) +def dec(x): return x-1 + + +f = Transform(enc,dec) +t = f(1) # t is of type MySubclass +test_eq_type(f.decode(t), MySubclass(1)) # the output of decode is cast to MySubclass to match the input type. +``` + +#### Apply Transforms On Subsets With `split_idx` + +You can apply transformations to subsets of data by specifying a +`split_idx` property. If a transform has a `split_idx` then it’s only +applied if the `split_idx` param matches. In the below example, we set +`split_idx` equal to `1`: + +``` python +def enc(x): return x+1 +def dec(x): return x-1 +f = Transform(enc,dec) +f.split_idx = 1 +``` + +The transformations are applied when a matching `split_idx` parameter is +passed: + +``` python +test_eq(f(1, split_idx=1),2) +test_eq(f.decode(2, split_idx=1),1) +``` + +On the other hand, transformations are ignored when the `split_idx` +parameter does not match: + +``` python +test_eq(f(1, split_idx=0), 1) +test_eq(f.decode(2, split_idx=0), 2) +``` + +#### Transforms on Lists + +Transform operates on lists as a whole, **not element-wise**: + +``` python +class A(Transform): + def encodes(self, x): return dict(x) + def decodes(self, x): return list(x.items()) + +f = A() +_inp = [(1,2), (3,4)] +t = f(_inp) + +test_eq(t, dict(_inp)) +test_eq(f.decodes(t), _inp) +``` + +If you want a transform to operate on a list elementwise, you must +implement this appropriately in the encodes and +decodes methods: + +``` python +class AL(Transform): pass + +@AL +def encodes(self, x): return [x_+1 for x_ in x] + +@AL +def decodes(self, x): return [x_-1 for x_ in x] + +f = AL() +t = f([1,2]) + +test_eq(t, [2,3]) +test_eq(f.decode(t), [1,2]) +``` + +#### Transforms on Tuples + +Unlike lists, +[`Transform`](https://fastcore.fast.ai/transform.html#transform) +operates on tuples element-wise. + +``` python +def neg_int(x): return -x +f = Transform(neg_int) + +test_eq(f((1,2,3)), (-1,-2,-3)) +``` + +Transforms will also apply `TypedDispatch` element-wise on tuples when +an input type annotation is specified. In the below example, the values +`1.0` and `3.0` are ignored because they are of type `float`, not `int`: + +``` python +def neg_int(x:int): return -x +f = Transform(neg_int) + +test_eq(f((1.0, 2, 3.0)), (1.0, -2, 3.0)) +``` + +Another example of how +[`Transform`](https://fastcore.fast.ai/transform.html#transform) can use +`TypedDispatch` with tuples is shown below: + +``` python +class B(Transform): pass + +@B +def encodes(self, x:int): return x+1 + +@B +def encodes(self, x:str): return x+'hello' + +@B +def encodes(self, x): return str(x)+'!' +``` + +If the input is not an `int` or `str`, the third `encodes` method will +apply: + +``` python +b = B() +test_eq(b([1]), '[1]!') +test_eq(b([1.0]), '[1.0]!') +``` + +However, if the input is a tuple, then the appropriate method will apply +according to the type of each element in the tuple: + +``` python +test_eq(b(('1',)), ('1hello',)) +test_eq(b((1,2)), (2,3)) +test_eq(b(('a',1.0)), ('ahello','1.0!')) +``` + +Dispatching over tuples works recursively, by the way: + +``` python +class B(Transform): + def encodes(self, x:int): return x+1 + def encodes(self, x:str): return x+'_hello' + def decodes(self, x:int): return x-1 + def decodes(self, x:str): return x.replace('_hello', '') + +f = B() +start = (1.,(2,'3')) +t = f(start) +test_eq_type(t, (1.,(3,'3_hello'))) +test_eq(f.decode(t), start) +``` + +Dispatching also works with `typing` module type classes, like +`numbers.integral`: + +``` python +@Transform +def f(x:numbers.Integral): return x+1 + +t = f((1,'1',1)) +test_eq(t, (2, '1', 2)) +``` + +------------------------------------------------------------------------ + +source + +### InplaceTransform + +> InplaceTransform (enc=None, dec=None, split_idx=None, order=None) + +*A [`Transform`](https://fastcore.fast.ai/transform.html#transform) that +modifies in-place and just returns whatever it’s passed* + +``` python +class A(InplaceTransform): pass + +@A +def encodes(self, x:pd.Series): x.fillna(10, inplace=True) + +f = A() + +test_eq_type(f(pd.Series([1,2,None])),pd.Series([1,2,10],dtype=np.float64)) #fillna fills with floats. +``` + +------------------------------------------------------------------------ + +source + +### DisplayedTransform + +> DisplayedTransform (enc=None, dec=None, split_idx=None, order=None) + +*A transform with a `__repr__` that shows its attrs* + +Transforms normally are represented by just their class name and a list +of encodes and decodes implementations: + +``` python +class A(Transform): encodes,decodes = noop,noop +f = A() +f +``` + + A: + encodes: (object,object) -> noop + decodes: (object,object) -> noop + +A +[`DisplayedTransform`](https://fastcore.fast.ai/transform.html#displayedtransform) +will in addition show the contents of all attributes listed in the +comma-delimited string `self.store_attrs`: + +``` python +class A(DisplayedTransform): + encodes = noop + def __init__(self, a, b=2): + super().__init__() + store_attr() + +A(a=1,b=2) +``` + + A -- {'a': 1, 'b': 2}: + encodes: (object,object) -> noop + decodes: + +------------------------------------------------------------------------ + +source + +### ItemTransform + +> ItemTransform (enc=None, dec=None, split_idx=None, order=None) + +*A transform that always take tuples as items* + +[`ItemTransform`](https://fastcore.fast.ai/transform.html#itemtransform) +is the class to use to opt out of the default behavior of +[`Transform`](https://fastcore.fast.ai/transform.html#transform). + +``` python +class AIT(ItemTransform): + def encodes(self, xy): x,y=xy; return (x+y,y) + def decodes(self, xy): x,y=xy; return (x-y,y) + +f = AIT() +test_eq(f((1,2)), (3,2)) +test_eq(f.decode((3,2)), (1,2)) +``` + +If you pass a special tuple subclass, the usual retain type behavior of +[`Transform`](https://fastcore.fast.ai/transform.html#transform) will +keep it: + +``` python +class _T(tuple): pass +x = _T((1,2)) +test_eq_type(f(x), _T((3,2))) +``` + +------------------------------------------------------------------------ + +source + +### get_func + +> get_func (t, name, *args, **kwargs) + +*Get the `t.name` (potentially partial-ized with `args` and `kwargs`) or +`noop` if not defined* + +This works for any kind of `t` supporting `getattr`, so a class or a +module. + +``` python +test_eq(get_func(operator, 'neg', 2)(), -2) +test_eq(get_func(operator.neg, '__call__')(2), -2) +test_eq(get_func(list, 'foobar')([2]), [2]) +a = [2,1] +get_func(list, 'sort')(a) +test_eq(a, [1,2]) +``` + +Transforms are built with multiple-dispatch: a given function can have +several methods depending on the type of the object received. This is +done directly with the +[`TypeDispatch`](https://fastcore.fast.ai/dispatch.html#typedispatch) +module and type-annotation in +[`Transform`](https://fastcore.fast.ai/transform.html#transform), but +you can also use the following class. + +------------------------------------------------------------------------ + +source + +### Func + +> Func (name, *args, **kwargs) + +*Basic wrapper around a `name` with `args` and `kwargs` to call on a +given type* + +You can call the [`Func`](https://fastcore.fast.ai/transform.html#func) +object on any module name or type, even a list of types. It will return +the corresponding function (with a default to `noop` if nothing is +found) or list of functions. + +``` python +test_eq(Func('sqrt')(math), math.sqrt) +``` + +------------------------------------------------------------------------ + +### Sig + +> Sig (*args, **kwargs) + +`Sig` is just sugar-syntax to create a +[`Func`](https://fastcore.fast.ai/transform.html#func) object more +easily with the syntax `Sig.name(*args, **kwargs)`. + +``` python +f = Sig.sqrt() +test_eq(f(math), math.sqrt) +``` + +------------------------------------------------------------------------ + +source + +### compose_tfms + +> compose_tfms (x, tfms, is_enc=True, reverse=False, **kwargs) + +*Apply all `func_nm` attribute of `tfms` on `x`, maybe in `reverse` +order* + +``` python +def to_int (x): return Int(x) +def to_float(x): return Float(x) +def double (x): return x*2 +def half(x)->None: return x/2 +``` + +``` python +def test_compose(a, b, *fs): test_eq_type(compose_tfms(a, tfms=map(Transform,fs)), b) + +test_compose(1, Int(1), to_int) +test_compose(1, Float(1), to_int,to_float) +test_compose(1, Float(2), to_int,to_float,double) +test_compose(2.0, 2.0, to_int,double,half) +``` + +``` python +class A(Transform): + def encodes(self, x:float): return Float(x+1) + def decodes(self, x): return x-1 + +tfms = [A(), Transform(math.sqrt)] +t = compose_tfms(3., tfms=tfms) +test_eq_type(t, Float(2.)) +test_eq(compose_tfms(t, tfms=tfms, is_enc=False), 1.) +test_eq(compose_tfms(4., tfms=tfms, reverse=True), 3.) +``` + +``` python +tfms = [A(), Transform(math.sqrt)] +test_eq(compose_tfms((9,3.), tfms=tfms), (3,2.)) +``` + +------------------------------------------------------------------------ + +source + +### mk_transform + +> mk_transform (f) + +*Convert function `f` to +[`Transform`](https://fastcore.fast.ai/transform.html#transform) if it +isn’t already one* + +------------------------------------------------------------------------ + +source + +### gather_attrs + +> gather_attrs (o, k, nm) + +*Used in **getattr** to collect all attrs `k` from `self.{nm}`* + +------------------------------------------------------------------------ + +source + +### gather_attr_names + +> gather_attr_names (o, nm) + +*Used in **dir** to collect all attrs `k` from `self.{nm}`* + +------------------------------------------------------------------------ + +source + +### Pipeline + +> Pipeline (funcs=None, split_idx=None) + +*A pipeline of composed (for encode/decode) transforms, setup with +types* + +``` python +add_docs(Pipeline, + __call__="Compose `__call__` of all `fs` on `o`", + decode="Compose `decode` of all `fs` on `o`", + show="Show `o`, a single item from a tuple, decoding as needed", + add="Add transforms `ts`", + setup="Call each tfm's `setup` in order") +``` + +[`Pipeline`](https://fastcore.fast.ai/transform.html#pipeline) is a +wrapper for +[`compose_tfms`](https://fastcore.fast.ai/transform.html#compose_tfms). +You can pass instances of +[`Transform`](https://fastcore.fast.ai/transform.html#transform) or +regular functions in `funcs`, the +[`Pipeline`](https://fastcore.fast.ai/transform.html#pipeline) will wrap +them all in +[`Transform`](https://fastcore.fast.ai/transform.html#transform) (and +instantiate them if needed) during the initialization. It handles the +transform `setup` by adding them one at a time and calling setup on +each, goes through them in order in `__call__` or `decode` and can +`show` an object by applying decoding the transforms up until the point +it gets an object that knows how to show itself. + +``` python +# Empty pipeline is noop +pipe = Pipeline() +test_eq(pipe(1), 1) +test_eq(pipe((1,)), (1,)) +# Check pickle works +assert pickle.loads(pickle.dumps(pipe)) +``` + +``` python +class IntFloatTfm(Transform): + def encodes(self, x): return Int(x) + def decodes(self, x): return Float(x) + foo=1 + +int_tfm=IntFloatTfm() + +def neg(x): return -x +neg_tfm = Transform(neg, neg) +``` + +``` python +pipe = Pipeline([neg_tfm, int_tfm]) + +start = 2.0 +t = pipe(start) +test_eq_type(t, Int(-2)) +test_eq_type(pipe.decode(t), Float(start)) +test_stdout(lambda:pipe.show(t), '-2') +``` + +``` python +pipe = Pipeline([neg_tfm, int_tfm]) +t = pipe(start) +test_stdout(lambda:pipe.show(pipe((1.,2.))), '-1\n-2') +test_eq(pipe.foo, 1) +assert 'foo' in dir(pipe) +assert 'int_float_tfm' in dir(pipe) +``` + +You can add a single transform or multiple transforms `ts` using +[`Pipeline.add`](https://fastcore.fast.ai/transform.html#pipeline.add). +Transforms will be ordered by `Transform.order`. + +``` python +pipe = Pipeline([neg_tfm, int_tfm]) +class SqrtTfm(Transform): + order=-1 + def encodes(self, x): + return x**(.5) + def decodes(self, x): return x**2 +pipe.add(SqrtTfm()) +test_eq(pipe(4),-2) +test_eq(pipe.decode(-2),4) +pipe.add([SqrtTfm(),SqrtTfm()]) +test_eq(pipe(256),-2) +test_eq(pipe.decode(-2),256) +``` + +Transforms are available as attributes named with the snake_case version +of the names of their types. Attributes in transforms can be directly +accessed as attributes of the pipeline. + +``` python +test_eq(pipe.int_float_tfm, int_tfm) +test_eq(pipe.foo, 1) + +pipe = Pipeline([int_tfm, int_tfm]) +pipe.int_float_tfm +test_eq(pipe.int_float_tfm[0], int_tfm) +test_eq(pipe.foo, [1,1]) +``` + +``` python +# Check opposite order +pipe = Pipeline([int_tfm,neg_tfm]) +t = pipe(start) +test_eq(t, -2) +test_stdout(lambda:pipe.show(t), '-2') +``` + +``` python +class A(Transform): + def encodes(self, x): return int(x) + def decodes(self, x): return Float(x) + +pipe = Pipeline([neg_tfm, A]) +t = pipe(start) +test_eq_type(t, -2) +test_eq_type(pipe.decode(t), Float(start)) +test_stdout(lambda:pipe.show(t), '-2.0') +``` + +``` python +s2 = (1,2) +pipe = Pipeline([neg_tfm, A]) +t = pipe(s2) +test_eq_type(t, (-1,-2)) +test_eq_type(pipe.decode(t), (Float(1.),Float(2.))) +test_stdout(lambda:pipe.show(t), '-1.0\n-2.0') +``` + +``` python +from PIL import Image +``` + +``` python +class ArrayImage(ndarray): + _show_args = {'cmap':'viridis'} + def __new__(cls, x, *args, **kwargs): + if isinstance(x,tuple): super().__new__(cls, x, *args, **kwargs) + if args or kwargs: raise RuntimeError('Unknown array init args') + if not isinstance(x,ndarray): x = array(x) + return x.view(cls) + + def show(self, ctx=None, figsize=None, **kwargs): + if ctx is None: _,ctx = plt.subplots(figsize=figsize) + ctx.imshow(im, **{**self._show_args, **kwargs}) + ctx.axis('off') + return ctx + +im = Image.open(TEST_IMAGE) +im_t = ArrayImage(im) +``` + +``` python +def f1(x:ArrayImage): return -x +def f2(x): return Image.open(x).resize((128,128)) +def f3(x:Image.Image): return(ArrayImage(array(x))) +``` + +``` python +pipe = Pipeline([f2,f3,f1]) +t = pipe(TEST_IMAGE) +test_eq(type(t), ArrayImage) +test_eq(t, -array(f3(f2(TEST_IMAGE)))) +``` + +``` python +pipe = Pipeline([f2,f3]) +t = pipe(TEST_IMAGE) +ax = pipe.show(t) +``` + +![](05_transform_files/figure-commonmark/cell-73-output-1.png) + +``` python +#test_fig_exists(ax) +``` + +``` python +#Check filtering is properly applied +add1 = B() +add1.split_idx = 1 +pipe = Pipeline([neg_tfm, A(), add1]) +test_eq(pipe(start), -2) +pipe.split_idx=1 +test_eq(pipe(start), -1) +pipe.split_idx=0 +test_eq(pipe(start), -2) +for t in [None, 0, 1]: + pipe.split_idx=t + test_eq(pipe.decode(pipe(start)), start) + test_stdout(lambda: pipe.show(pipe(start)), "-2.0") +``` + +``` python +def neg(x): return -x +test_eq(type(mk_transform(neg)), Transform) +test_eq(type(mk_transform(math.sqrt)), Transform) +test_eq(type(mk_transform(lambda a:a*2)), Transform) +test_eq(type(mk_transform(Pipeline([neg]))), Pipeline) +``` + +### Methods + +``` python +#TODO: method examples +``` + +------------------------------------------------------------------------ + +source + +### Pipeline.\_\_call\_\_ + +> Pipeline.__call__ (o) + +*Call self as a function.* + +------------------------------------------------------------------------ + +source + +### Pipeline.decode + +> Pipeline.decode (o, full=True) + +------------------------------------------------------------------------ + +source + +### Pipeline.setup + +> Pipeline.setup (items=None, train_setup=False) + +During the setup, the +[`Pipeline`](https://fastcore.fast.ai/transform.html#pipeline) starts +with no transform and adds them one at a time, so that during its setup, +each transform gets the items processed up to its point and not after. diff --git a/xdg.html b/xdg.html new file mode 100644 index 00000000..347c7f0a --- /dev/null +++ b/xdg.html @@ -0,0 +1,867 @@ + + + + + + + + + + +XDG – fastcore + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ +
+ + +
+ + + +
+ +
+
+

XDG

+
+ +
+
+ XDG Base Directory Specification helpers. +
+
+ + +
+ + + + +
+ + + +
+ + + +

See the XDG Base Directory Specification for more information.

+
+

Overview

+

xdg_cache_home, xdg_config_home, xdg_data_home, and xdg_state_home return pathlib.Path objects containing the value of the environment variable named XDG_CACHE_HOME, XDG_CONFIG_HOME, XDG_DATA_HOME, and XDG_STATE_HOME respectively, or the default defined in the specification if the environment variable is unset, empty, or contains a relative path rather than absolute path.

+

xdg_config_dirs and xdg_data_dirs return a list of pathlib.Path objects containing the value, split on colons, of the environment variable named XDG_CONFIG_DIRS and XDG_DATA_DIRS respectively, or the default defined in the specification if the environment variable is unset or empty. Relative paths are ignored, as per the specification.

+

xdg_runtime_dir returns a pathlib.Path object containing the value of the XDG_RUNTIME_DIR environment variable, or None if the environment variable is not set, or contains a relative path rather than absolute path.

+
+
+

Helpers

+

We’ll start by defining a context manager that temporarily sets an environment variable to demonstrate the behaviour of each helper function:

+
+
from contextlib import contextmanager
+
+
+
@contextmanager
+def env(variable, value):
+    old = os.environ.get(variable, None)
+    try:
+        os.environ[variable] = value
+        yield
+    finally:
+        if old is None: del os.environ[variable]
+        else: os.environ[variable] = old
+
+
+

source

+
+

xdg_cache_home

+
+
 xdg_cache_home ()
+
+

Path corresponding to XDG_CACHE_HOME

+
+
from fastcore.test import *
+
+
+
test_eq(xdg_cache_home(), Path.home()/'.cache')
+with env('XDG_CACHE_HOME', '/home/fastai/.cache'):
+    test_eq(xdg_cache_home(), Path('/home/fastai/.cache'))
+
+
+

source

+
+
+

xdg_config_dirs

+
+
 xdg_config_dirs ()
+
+

Paths corresponding to XDG_CONFIG_DIRS

+
+
test_eq(xdg_config_dirs(), [Path('/etc/xdg')])
+with env('XDG_CONFIG_DIRS', '/home/fastai/.xdg:/home/fastai/.config'):
+    test_eq(xdg_config_dirs(), [Path('/home/fastai/.xdg'), Path('/home/fastai/.config')])
+
+
+

source

+
+
+

xdg_config_home

+
+
 xdg_config_home ()
+
+

Path corresponding to XDG_CONFIG_HOME

+
+
test_eq(xdg_config_home(), Path.home()/'.config')
+with env('XDG_CONFIG_HOME', '/home/fastai/.config'):
+    test_eq(xdg_config_home(), Path('/home/fastai/.config'))
+
+
+

source

+
+
+

xdg_data_dirs

+
+
 xdg_data_dirs ()
+
+

Paths corresponding to XDG_DATA_DIRS`

+
+

source

+
+
+

xdg_data_home

+
+
 xdg_data_home ()
+
+

Path corresponding to XDG_DATA_HOME

+
+
test_eq(xdg_data_home(), Path.home()/'.local/share')
+with env('XDG_DATA_HOME', '/home/fastai/.data'):
+    test_eq(xdg_data_home(), Path('/home/fastai/.data'))
+
+
+

source

+
+
+

xdg_runtime_dir

+
+
 xdg_runtime_dir ()
+
+

Path corresponding to XDG_RUNTIME_DIR

+
+

source

+
+
+

xdg_state_home

+
+
 xdg_state_home ()
+
+

Path corresponding to XDG_STATE_HOME

+
+
test_eq(xdg_state_home(), Path.home()/'.local/state')
+with env('XDG_STATE_HOME', '/home/fastai/.state'):
+    test_eq(xdg_state_home(), Path('/home/fastai/.state'))
+
+
+

Copyright © 2016-2021 Scott Stevenson

+

Modifications copyright © 2022 onwards Jeremy Howard

+ + +
+
+ +
+ +
+ + + + + \ No newline at end of file diff --git a/xdg.html.md b/xdg.html.md new file mode 100644 index 00000000..d21d25c3 --- /dev/null +++ b/xdg.html.md @@ -0,0 +1,180 @@ +# XDG + + + + +See the [XDG Base Directory +Specification](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html) +for more information. + +## Overview + +[`xdg_cache_home`](https://fastcore.fast.ai/xdg.html#xdg_cache_home), +[`xdg_config_home`](https://fastcore.fast.ai/xdg.html#xdg_config_home), +[`xdg_data_home`](https://fastcore.fast.ai/xdg.html#xdg_data_home), and +[`xdg_state_home`](https://fastcore.fast.ai/xdg.html#xdg_state_home) +return `pathlib.Path` objects containing the value of the environment +variable named `XDG_CACHE_HOME`, `XDG_CONFIG_HOME`, `XDG_DATA_HOME`, and +`XDG_STATE_HOME` respectively, or the default defined in the +specification if the environment variable is unset, empty, or contains a +relative path rather than absolute path. + +[`xdg_config_dirs`](https://fastcore.fast.ai/xdg.html#xdg_config_dirs) +and [`xdg_data_dirs`](https://fastcore.fast.ai/xdg.html#xdg_data_dirs) +return a list of `pathlib.Path` objects containing the value, split on +colons, of the environment variable named `XDG_CONFIG_DIRS` and +`XDG_DATA_DIRS` respectively, or the default defined in the +specification if the environment variable is unset or empty. Relative +paths are ignored, as per the specification. + +[`xdg_runtime_dir`](https://fastcore.fast.ai/xdg.html#xdg_runtime_dir) +returns a `pathlib.Path` object containing the value of the +`XDG_RUNTIME_DIR` environment variable, or `None` if the environment +variable is not set, or contains a relative path rather than absolute +path. + +## Helpers + +We’ll start by defining a context manager that temporarily sets an +environment variable to demonstrate the behaviour of each helper +function: + +``` python +from contextlib import contextmanager +``` + +``` python +@contextmanager +def env(variable, value): + old = os.environ.get(variable, None) + try: + os.environ[variable] = value + yield + finally: + if old is None: del os.environ[variable] + else: os.environ[variable] = old +``` + +------------------------------------------------------------------------ + +source + +### xdg_cache_home + +> xdg_cache_home () + +*Path corresponding to `XDG_CACHE_HOME`* + +``` python +from fastcore.test import * +``` + +``` python +test_eq(xdg_cache_home(), Path.home()/'.cache') +with env('XDG_CACHE_HOME', '/home/fastai/.cache'): + test_eq(xdg_cache_home(), Path('/home/fastai/.cache')) +``` + +------------------------------------------------------------------------ + +source + +### xdg_config_dirs + +> xdg_config_dirs () + +*Paths corresponding to `XDG_CONFIG_DIRS`* + +``` python +test_eq(xdg_config_dirs(), [Path('/etc/xdg')]) +with env('XDG_CONFIG_DIRS', '/home/fastai/.xdg:/home/fastai/.config'): + test_eq(xdg_config_dirs(), [Path('/home/fastai/.xdg'), Path('/home/fastai/.config')]) +``` + +------------------------------------------------------------------------ + +source + +### xdg_config_home + +> xdg_config_home () + +*Path corresponding to `XDG_CONFIG_HOME`* + +``` python +test_eq(xdg_config_home(), Path.home()/'.config') +with env('XDG_CONFIG_HOME', '/home/fastai/.config'): + test_eq(xdg_config_home(), Path('/home/fastai/.config')) +``` + +------------------------------------------------------------------------ + +source + +### xdg_data_dirs + +> xdg_data_dirs () + +*Paths corresponding to XDG_DATA_DIRS\`* + +------------------------------------------------------------------------ + +source + +### xdg_data_home + +> xdg_data_home () + +*Path corresponding to `XDG_DATA_HOME`* + +``` python +test_eq(xdg_data_home(), Path.home()/'.local/share') +with env('XDG_DATA_HOME', '/home/fastai/.data'): + test_eq(xdg_data_home(), Path('/home/fastai/.data')) +``` + +------------------------------------------------------------------------ + +source + +### xdg_runtime_dir + +> xdg_runtime_dir () + +*Path corresponding to `XDG_RUNTIME_DIR`* + +------------------------------------------------------------------------ + +source + +### xdg_state_home + +> xdg_state_home () + +*Path corresponding to `XDG_STATE_HOME`* + +``` python +test_eq(xdg_state_home(), Path.home()/'.local/state') +with env('XDG_STATE_HOME', '/home/fastai/.state'): + test_eq(xdg_state_home(), Path('/home/fastai/.state')) +``` + +------------------------------------------------------------------------ + +Copyright © 2016-2021 Scott Stevenson + +Modifications copyright © 2022 onwards Jeremy Howard diff --git a/xml.html b/xml.html new file mode 100644 index 00000000..f076c0dd --- /dev/null +++ b/xml.html @@ -0,0 +1,986 @@ + + + + + + + + + + +XML – fastcore + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ +
+ + +
+ + + +
+ +
+
+

XML

+
+ +
+
+ Concise generation of XML. +
+
+ + +
+ + + + +
+ + + +
+ + + +
+
from IPython.display import Markdown
+from pprint import pprint
+
+from fastcore.test import test_eq
+
+
+

FT functions

+
+

source

+
+

attrmap

+
+
 attrmap (o)
+
+
+

source

+
+
+

valmap

+
+
 valmap (o)
+
+
+

source

+
+
+

FT

+
+
 FT (tag:str, cs:tuple, attrs:dict=None, void_=False, **kwargs)
+
+

A ‘Fast Tag’ structure, containing tag,children,and attrs

+
+

source

+
+
+

ft

+
+
 ft (tag:str, *c, void_:bool=False, attrmap:<built-
+     infunctioncallable>=<function attrmap>, valmap:<built-
+     infunctioncallable>=<function valmap>, ft_cls=<class '__main__.FT'>,
+     **kw)
+
+

Create an FT structure for to_xml()

+

The main HTML tags are exported as ft partials.

+

Attributes are passed as keywords. Use ‘klass’ and ‘fr’ instead of ‘class’ and ‘for’, to avoid Python reserved word clashes.

+
+

source

+
+
+

Html

+
+
 Html (*c, doctype=True, **kwargs)
+
+

An HTML tag, optionally preceeded by !DOCTYPE HTML

+
+
samp = Html(
+    Head(Title('Some page')),
+    Body(Div('Some text\nanother line', (Input(name="jph's"), Img(src="filename", data=1)),
+             cls=['myclass', 'another'],
+             style={'padding':1, 'margin':2}))
+)
+pprint(samp)
+
+
(!doctype((),{'html': True}),
+ html((head((title(('Some page',),{}),),{}), body((div(('Some text\nanother line', input((),{'name': "jph's"}), img((),{'src': 'filename', 'data': 1})),{'class': 'myclass another', 'style': 'padding:1; margin:2'}),),{})),{}))
+
+
+
+
elem = P('Some text', id="myid")
+print(elem.tag)
+print(elem.children)
+print(elem.attrs)
+
+
p
+('Some text',)
+{'id': 'myid'}
+
+
+

You can get and set attrs directly:

+
+
elem.id = 'newid'
+print(elem.id, elem.get('id'), elem.get('foo', 'missing'))
+elem
+
+
newid newid missing
+
+
+
p(('Some text',),{'id': 'newid'})
+
+
+
+

source

+
+
+

Safe

+

*str(object=’’) -> str str(bytes_or_buffer[, encoding[, errors]]) -> str

+

Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.__str__() (if defined) or repr(object). encoding defaults to sys.getdefaultencoding(). errors defaults to ‘strict’.*

+
+
+
+

Conversion to XML/HTML

+
+

source

+
+

to_xml

+
+
 to_xml (elm, lvl=0, indent=True, do_escape=True)
+
+

Convert ft element tree into an XML string

+
+
h = to_xml(samp, do_escape=False)
+print(h)
+
+
<!doctype html>
+<html>
+  <head>
+    <title>Some page</title>
+  </head>
+  <body>
+    <div class="myclass another" style="padding:1; margin:2">
+Some text
+another line      <input name="jph's">
+<img src="filename" data="1">    </div>
+  </body>
+</html>
+
+
+
+
+
class PageTitle:
+    def __ft__(self): return H1("Hello")
+
+class HomePage:
+    def __ft__(self): return Div(PageTitle(), Div('hello'))
+
+h = to_xml(Div(HomePage()))
+expected_output = """<div>
+  <div>
+    <h1>Hello</h1>
+    <div>hello</div>
+  </div>
+</div>
+"""
+assert h == expected_output
+
+
+
print(h)
+
+
<div>
+  <div>
+    <h1>Hello</h1>
+    <div>hello</div>
+  </div>
+</div>
+
+
+
+
+
h = to_xml(samp, indent=False)
+print(h)
+
+
<!doctype html><html><head><title>Some page</title></head><body><div class="myclass another" style="padding:1; margin:2">Some text
+another line<input name="jph's"><img src="filename" data="1"></div></body></html>
+
+
+

Interoperability both directions with Django and Jinja using the html() protocol:

+
+
def _esc(s): return s.__html__() if hasattr(s, '__html__') else Safe(escape(s))
+
+r = Safe('<b>Hello from Django</b>')
+print(to_xml(Div(r)))
+print(_esc(Div(P('Hello from fastcore <3'))))
+
+
<div><b>Hello from Django</b></div>
+
+<div>
+  <p>Hello from fastcore &lt;3</p>
+</div>
+
+
+
+
+
+
+

Display

+
+

source

+
+

highlight

+
+
 highlight (s, lang='html')
+
+

Markdown to syntax-highlight s in language lang

+
+

source

+
+
+

showtags

+
+
 showtags (s)
+
+

You can also reorder the children to come after the attrs, if you use this alternative syntax for FT where the children are in a second pair of () (behind the scenes this is because FT implements __call__ to add children).

+
+
Body(klass='myclass')(
+    Div(style='padding:3px')(
+        'Some text 1<2',
+        I(spurious=True)('in italics'),
+        Input(name='me'),
+        Img(src="filename", data=1)
+    )
+)
+
+
<body class="myclass">
+  <div style="padding:3px">
+Some text 1&lt;2<i spurious>in italics</i>    <input name="me">
+<img src="filename" data="1">  </div>
+</body>
+
+
+
+

source

+
+
+

getattr

+
+
 __getattr__ (tag)
+
+ + +
+
+ +
+ +
+ + + + + \ No newline at end of file diff --git a/xml.html.md b/xml.html.md new file mode 100644 index 00000000..31dfdb18 --- /dev/null +++ b/xml.html.md @@ -0,0 +1,280 @@ +# XML + + + + +``` python +from IPython.display import Markdown +from pprint import pprint + +from fastcore.test import test_eq +``` + +## FT functions + +------------------------------------------------------------------------ + +source + +### attrmap + +> attrmap (o) + +------------------------------------------------------------------------ + +source + +### valmap + +> valmap (o) + +------------------------------------------------------------------------ + +source + +### FT + +> FT (tag:str, cs:tuple, attrs:dict=None, void_=False, **kwargs) + +*A ‘Fast Tag’ structure, containing `tag`,`children`,and `attrs`* + +------------------------------------------------------------------------ + +source + +### ft + +> ft (tag:str, *c, void_:bool=False, attrmap: infunctioncallable>=, valmap: infunctioncallable>=, ft_cls=, +> **kw) + +*Create an [`FT`](https://fastcore.fast.ai/xml.html#ft) structure for +`to_xml()`* + +The main HTML tags are exported as +[`ft`](https://fastcore.fast.ai/xml.html#ft) partials. + +Attributes are passed as keywords. Use ‘klass’ and ‘fr’ instead of +‘class’ and ‘for’, to avoid Python reserved word clashes. + +------------------------------------------------------------------------ + +source + +### Html + +> Html (*c, doctype=True, **kwargs) + +*An HTML tag, optionally preceeded by `!DOCTYPE HTML`* + +``` python +samp = Html( + Head(Title('Some page')), + Body(Div('Some text\nanother line', (Input(name="jph's"), Img(src="filename", data=1)), + cls=['myclass', 'another'], + style={'padding':1, 'margin':2})) +) +pprint(samp) +``` + + (!doctype((),{'html': True}), + html((head((title(('Some page',),{}),),{}), body((div(('Some text\nanother line', input((),{'name': "jph's"}), img((),{'src': 'filename', 'data': 1})),{'class': 'myclass another', 'style': 'padding:1; margin:2'}),),{})),{})) + +``` python +elem = P('Some text', id="myid") +print(elem.tag) +print(elem.children) +print(elem.attrs) +``` + + p + ('Some text',) + {'id': 'myid'} + +You can get and set attrs directly: + +``` python +elem.id = 'newid' +print(elem.id, elem.get('id'), elem.get('foo', 'missing')) +elem +``` + + newid newid missing + + p(('Some text',),{'id': 'newid'}) + +------------------------------------------------------------------------ + +source + +### Safe + +\*str(object=’’) -\> str str(bytes_or_buffer\[, encoding\[, errors\]\]) +-\> str + +Create a new string object from the given object. If encoding or errors +is specified, then the object must expose a data buffer that will be +decoded using the given encoding and error handler. Otherwise, returns +the result of object.\_\_str\_\_() (if defined) or repr(object). +encoding defaults to sys.getdefaultencoding(). errors defaults to +‘strict’.\* + +## Conversion to XML/HTML + +------------------------------------------------------------------------ + +source + +### to_xml + +> to_xml (elm, lvl=0, indent=True, do_escape=True) + +*Convert [`ft`](https://fastcore.fast.ai/xml.html#ft) element tree into +an XML string* + +``` python +h = to_xml(samp, do_escape=False) +print(h) +``` + + + + + Some page + + +
+ Some text + another line +
+ + + +``` python +class PageTitle: + def __ft__(self): return H1("Hello") + +class HomePage: + def __ft__(self): return Div(PageTitle(), Div('hello')) + +h = to_xml(Div(HomePage())) +expected_output = """
+
+

Hello

+
hello
+
+
+""" +assert h == expected_output +``` + +``` python +print(h) +``` + +
+
+

Hello

+
hello
+
+
+ +``` python +h = to_xml(samp, indent=False) +print(h) +``` + + Some page
Some text + another line
+ +Interoperability both directions with Django and Jinja using the +[**html**() +protocol](https://jinja.palletsprojects.com/en/3.1.x/templates/#jinja-filters.escape): + +``` python +def _esc(s): return s.__html__() if hasattr(s, '__html__') else Safe(escape(s)) + +r = Safe('Hello from Django') +print(to_xml(Div(r))) +print(_esc(Div(P('Hello from fastcore <3')))) +``` + +
Hello from Django
+ +
+

Hello from fastcore <3

+
+ +## Display + +------------------------------------------------------------------------ + +source + +### highlight + +> highlight (s, lang='html') + +*Markdown to syntax-highlight `s` in language `lang`* + +------------------------------------------------------------------------ + +source + +### showtags + +> showtags (s) + +You can also reorder the children to come *after* the attrs, if you use +this alternative syntax for [`FT`](https://fastcore.fast.ai/xml.html#ft) +where the children are in a second pair of `()` (behind the scenes this +is because [`FT`](https://fastcore.fast.ai/xml.html#ft) implements +`__call__` to add children). + +``` python +Body(klass='myclass')( + Div(style='padding:3px')( + 'Some text 1<2', + I(spurious=True)('in italics'), + Input(name='me'), + Img(src="filename", data=1) + ) +) +``` + +``` html + +
+Some text 1<2in italics +
+ +``` + +------------------------------------------------------------------------ + +source + +### **getattr** + +> __getattr__ (tag) diff --git a/xtras.html b/xtras.html new file mode 100644 index 00000000..750dff30 --- /dev/null +++ b/xtras.html @@ -0,0 +1,2293 @@ + + + + + + + + + + +Utility functions – fastcore + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ +
+ + +
+ + + +
+ +
+
+

Utility functions

+
+ +
+
+ Utility functions used in the fastai library +
+
+ + +
+ + + + +
+ + + +
+ + + +
+

File Functions

+

Utilities (other than extensions to Pathlib.Path) for dealing with IO.

+
+

source

+
+

walk

+
+
 walk (path:pathlib.Path|str, symlinks:bool=True, keep_file:<built-
+       infunctioncallable>=<function ret_true>, keep_folder:<built-
+       infunctioncallable>=<function ret_true>, skip_folder:<built-
+       infunctioncallable>=<function ret_false>, func:<built-
+       infunctioncallable>=<function join>, ret_folders:bool=False)
+
+

Generator version of os.walk, using functions to filter files and folders

+ ++++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TypeDefaultDetails
pathpathlib.Path | strpath to start searching
symlinksboolTruefollow symlinks?
keep_filecallableret_truefunction that returns True for wanted files
keep_foldercallableret_truefunction that returns True for folders to enter
skip_foldercallableret_falsefunction that returns True for folders to skip
funccallablejoinfunction to apply to each matched file
ret_foldersboolFalsereturn folders, not just files
+
+

source

+
+
+

globtastic

+
+
 globtastic (path:pathlib.Path|str, recursive:bool=True,
+             symlinks:bool=True, file_glob:str=None, file_re:str=None,
+             folder_re:str=None, skip_file_glob:str=None,
+             skip_file_re:str=None, skip_folder_re:str=None, func:<built-
+             infunctioncallable>=<function join>, ret_folders:bool=False)
+
+

A more powerful glob, including regex matches, symlink handling, and skip parameters

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TypeDefaultDetails
pathpathlib.Path | strpath to start searching
recursiveboolTruesearch subfolders
symlinksboolTruefollow symlinks?
file_globstrNoneOnly include files matching glob
file_restrNoneOnly include files matching regex
folder_restrNoneOnly enter folders matching regex
skip_file_globstrNoneSkip files matching glob
skip_file_restrNoneSkip files matching regex
skip_folder_restrNoneSkip folders matching regex,
funccallablejoinfunction to apply to each matched file
ret_foldersboolFalsereturn folders, not just files
ReturnsLPaths to matched files
+
+
globtastic('.', skip_folder_re='^[_.]', folder_re='core', file_glob='*.*py*', file_re='c')
+
+
(#5) ['./fastcore/docments.py','./fastcore/dispatch.py','./fastcore/basics.py','./fastcore/docscrape.py','./fastcore/script.py']
+
+
+
+

source

+
+
+

maybe_open

+
+
 maybe_open (f, mode='r', **kwargs)
+
+

Context manager: open f if it is a path (and close on exit)

+

This is useful for functions where you want to accept a path or file. maybe_open will not close your file handle if you pass one in.

+
+
def _f(fn):
+    with maybe_open(fn) as f: return f.encoding
+
+fname = '00_test.ipynb'
+sys_encoding = 'cp1252' if sys.platform == 'win32' else 'UTF-8'
+test_eq(_f(fname), sys_encoding)
+with open(fname) as fh: test_eq(_f(fh), sys_encoding)
+
+

For example, we can use this to reimplement imghdr.what from the Python standard library, which is written in Python 3.9 as:

+
+
from fastcore import imghdr
+
+
+
def what(file, h=None):
+    f = None
+    try:
+        if h is None:
+            if isinstance(file, (str,os.PathLike)):
+                f = open(file, 'rb')
+                h = f.read(32)
+            else:
+                location = file.tell()
+                h = file.read(32)
+                file.seek(location)
+        for tf in imghdr.tests:
+            res = tf(h, f)
+            if res: return res
+    finally:
+        if f: f.close()
+    return None
+
+

Here’s an example of the use of this function:

+
+
fname = 'images/puppy.jpg'
+what(fname)
+
+
'jpeg'
+
+
+

With maybe_open, Self, and L.map_first, we can rewrite this in a much more concise and (in our opinion) clear way:

+
+
def what(file, h=None):
+    if h is None:
+        with maybe_open(file, 'rb') as f: h = f.peek(32)
+    return L(imghdr.tests).map_first(Self(h,file))
+
+

…and we can check that it still works:

+
+
test_eq(what(fname), 'jpeg')
+
+

…along with the version passing a file handle:

+
+
with open(fname,'rb') as f: test_eq(what(f), 'jpeg')
+
+

…along with the h parameter version:

+
+
with open(fname,'rb') as f: test_eq(what(None, h=f.read(32)), 'jpeg')
+
+
+

source

+
+
+

mkdir

+
+
 mkdir (path, exist_ok=False, parents=False, overwrite=False, **kwargs)
+
+

Creates and returns a directory defined by path, optionally removing previous existing directory if overwrite is True

+
+
with tempfile.TemporaryDirectory() as d:
+    path = Path(os.path.join(d, 'new_dir'))
+    new_dir = mkdir(path)
+    assert new_dir.exists()
+    test_eq(new_dir, path)
+        
+    # test overwrite
+    with open(new_dir/'test.txt', 'w') as f: f.writelines('test')
+    test_eq(len(list(walk(new_dir))), 1) # assert file is present
+    new_dir = mkdir(new_dir, overwrite=True)
+    test_eq(len(list(walk(new_dir))), 0) # assert file was deleted
+
+
+

source

+
+
+

image_size

+
+
 image_size (fn)
+
+

Tuple of (w,h) for png, gif, or jpg; None otherwise

+
+
test_eq(image_size(fname), (1200,803))
+
+
+

source

+
+
+

bunzip

+
+
 bunzip (fn)
+
+

bunzip fn, raising exception if output already exists

+
+
f = Path('files/test.txt')
+if f.exists(): f.unlink()
+bunzip('files/test.txt.bz2')
+t = f.open().readlines()
+test_eq(len(t),1)
+test_eq(t[0], 'test\n')
+f.unlink()
+
+
+

source

+
+
+

loads

+
+
 loads (s, **kw)
+
+

Same as json.loads, but handles None

+
+

source

+
+
+

loads_multi

+
+
 loads_multi (s:str)
+
+

Generator of >=0 decoded json dicts, possibly with non-json ignored text at start and end

+
+
tst = """
+# ignored
+{ "a":1 }
+hello
+{
+"b":2
+}
+"""
+
+test_eq(list(loads_multi(tst)), [{'a': 1}, {'b': 2}])
+
+
+

source

+
+
+

dumps

+
+
 dumps (obj, **kw)
+
+

Same as json.dumps, but uses ujson if available

+
+

source

+
+
+

untar_dir

+
+
 untar_dir (fname, dest, rename=False, overwrite=False)
+
+

untar file into dest, creating a directory if the root contains more than one item

+
+
def test_untar(foldername, rename=False, **kwargs):
+    with tempfile.TemporaryDirectory() as d:
+        nm = os.path.join(d, 'a')
+        shutil.make_archive(nm, 'gztar', **kwargs)
+        with tempfile.TemporaryDirectory() as d2:
+            d2 = Path(d2)
+            untar_dir(nm+'.tar.gz', d2, rename=rename)
+            test_eq(d2.ls(), [d2/foldername])
+
+

If the contents of fname contain just one file or directory, it is placed directly in dest:

+
+
# using `base_dir` in `make_archive` results in `images` directory included in file names
+test_untar('images', base_dir='images')
+
+

If rename then the directory created is named based on the archive, without extension:

+
+
test_untar('a', base_dir='images', rename=True)
+
+

If the contents of fname contain multiple files and directories, a new folder in dest is created with the same name as fname (but without extension):

+
+
# using `root_dir` in `make_archive` results in `images` directory *not* included in file names
+test_untar('a', root_dir='images')
+
+
+

source

+
+
+

repo_details

+
+
 repo_details (url)
+
+

Tuple of owner,name from ssh or https git repo url

+
+
test_eq(repo_details('https://github.com/fastai/fastai.git'), ['fastai', 'fastai'])
+test_eq(repo_details('git@github.com:fastai/nbdev.git\n'), ['fastai', 'nbdev'])
+
+
+

source

+
+
+

run

+
+
 run (cmd, *rest, same_in_win=False, ignore_ex=False, as_bytes=False,
+      stderr=False)
+
+

Pass cmd (splitting with shlex if string) to subprocess.run; return stdout; raise IOError if fails

+

You can pass a string (which will be split based on standard shell rules), a list, or pass args directly:

+
+
run('echo', same_in_win=True)
+run('pip', '--version', same_in_win=True)
+run(['pip', '--version'], same_in_win=True)
+
+
'pip 23.3.1 from /Users/jhoward/miniconda3/lib/python3.11/site-packages/pip (python 3.11)'
+
+
+
+
if sys.platform == 'win32':
+    assert 'ipynb' in run('cmd /c dir /p')
+    assert 'ipynb' in run(['cmd', '/c', 'dir', '/p'])
+    assert 'ipynb' in run('cmd', '/c', 'dir',  '/p')
+else:
+    assert 'ipynb' in run('ls -ls')
+    assert 'ipynb' in run(['ls', '-l'])
+    assert 'ipynb' in run('ls', '-l')
+
+

Some commands fail in non-error situations, like grep. Use ignore_ex in those cases, which will return a tuple of stdout and returncode:

+
+
if sys.platform == 'win32':
+    test_eq(run('cmd /c findstr asdfds 00_test.ipynb', ignore_ex=True)[0], 1)
+else:
+    test_eq(run('grep asdfds 00_test.ipynb', ignore_ex=True)[0], 1)
+
+

run automatically decodes returned bytes to a str. Use as_bytes to skip that:

+
+
if sys.platform == 'win32':
+    test_eq(run('cmd /c echo hi'), 'hi')
+else:
+    test_eq(run('echo hi', as_bytes=True), b'hi\n')
+
+
+

source

+
+
+

open_file

+
+
 open_file (fn, mode='r', **kwargs)
+
+

Open a file, with optional compression if gz or bz2 suffix

+
+

source

+
+
+

save_pickle

+
+
 save_pickle (fn, o)
+
+

Save a pickle file, to a file name or opened file

+
+

source

+
+
+

load_pickle

+
+
 load_pickle (fn)
+
+

Load a pickle file from a file name or opened file

+
+
for suf in '.pkl','.bz2','.gz':
+    # delete=False is added for Windows
+    # https://stackoverflow.com/questions/23212435/permission-denied-to-write-to-my-temporary-file
+    with tempfile.NamedTemporaryFile(suffix=suf, delete=False) as f:
+        fn = Path(f.name)
+        save_pickle(fn, 't')
+        t = load_pickle(fn)
+    f.close()
+    test_eq(t,'t')
+
+
+

source

+
+
+

parse_env

+
+
 parse_env (s:str=None, fn:Union[str,pathlib.Path]=None)
+
+

Parse a shell-style environment string or file

+
+
testf = """# comment
+   # another comment
+ export FOO="bar#baz"
+BAR=thing # comment "ok"
+  baz='thong'
+QUX=quux
+export ZAP = "zip" # more comments
+   FOOBAR = 42   # trailing space and comment"""
+
+exp = dict(FOO='bar#baz', BAR='thing', baz='thong', QUX='quux', ZAP='zip', FOOBAR='42')
+
+test_eq(parse_env(testf),  exp)
+
+
+

source

+
+
+

expand_wildcards

+
+
 expand_wildcards (code)
+
+

Expand all wildcard imports in the given code string.

+
+
inp = """from math import *
+from os import *
+from random import *
+def func(): return sin(pi) + path.join('a', 'b') + randint(1, 10)"""
+
+exp = """from math import pi, sin
+from os import path
+from random import randint
+def func(): return sin(pi) + path.join('a', 'b') + randint(1, 10)"""
+
+test_eq(expand_wildcards(inp), exp)
+
+inp = """from itertools import *
+def func(): pass"""
+test_eq(expand_wildcards(inp), inp)
+
+inp = """def outer():
+    from math import *
+    def inner():
+        from os import *
+        return sin(pi) + path.join('a', 'b')"""
+
+exp = """def outer():
+    from math import pi, sin
+    def inner():
+        from os import path
+        return sin(pi) + path.join('a', 'b')"""
+
+test_eq(expand_wildcards(inp), exp)
+
+
+
+
+

Collections

+
+

source

+
+

dict2obj

+
+
 dict2obj (d, list_func=<class 'fastcore.foundation.L'>, dict_func=<class
+           'fastcore.basics.AttrDict'>)
+
+

Convert (possibly nested) dicts (or lists of dicts) to AttrDict

+

This is a convenience to give you “dotted” access to (possibly nested) dictionaries, e.g:

+
+
d1 = dict(a=1, b=dict(c=2,d=3))
+d2 = dict2obj(d1)
+test_eq(d2.b.c, 2)
+test_eq(d2.b['c'], 2)
+
+

It can also be used on lists of dicts.

+
+
_list_of_dicts = [d1, d1]
+ds = dict2obj(_list_of_dicts)
+test_eq(ds[0].b.c, 2)
+
+
+

source

+
+
+

obj2dict

+
+
 obj2dict (d)
+
+

Convert (possibly nested) AttrDicts (or lists of AttrDicts) to dict

+

obj2dict can be used to reverse what is done by dict2obj:

+
+
test_eq(obj2dict(d2), d1)
+test_eq(obj2dict(ds), _list_of_dicts)
+
+
+

source

+
+
+

repr_dict

+
+
 repr_dict (d)
+
+

Print nested dicts and lists, such as returned by dict2obj

+
+
print(repr_dict(d2))
+
+
- a: 1
+- b: 
+  - c: 2
+  - d: 3
+
+
+
+

source

+
+
+

is_listy

+
+
 is_listy (x)
+
+

isinstance(x, (tuple,list,L,slice,Generator))

+
+
assert is_listy((1,))
+assert is_listy([1])
+assert is_listy(L([1]))
+assert is_listy(slice(2))
+assert not is_listy(array([1]))
+
+
+

source

+
+
+

mapped

+
+
 mapped (f, it)
+
+

map f over it, unless it’s not listy, in which case return f(it)

+
+
def _f(x,a=1): return x-a
+
+test_eq(mapped(_f,1),0)
+test_eq(mapped(_f,[1,2]),[0,1])
+test_eq(mapped(_f,(1,)),(0,))
+
+
+
+
+

Extensions to Pathlib.Path

+

The following methods are added to the standard python libary Pathlib.Path.

+
+

source

+
+

Path.readlines

+
+
 Path.readlines (hint=-1, encoding='utf8')
+
+

Read the content of self

+
+

source

+
+
+

Path.read_json

+
+
 Path.read_json (encoding=None, errors=None)
+
+

Same as read_text followed by loads

+
+

source

+
+
+

Path.mk_write

+
+
 Path.mk_write (data, encoding=None, errors=None, mode=511)
+
+

Make all parent dirs of self, and write data

+
+

source

+
+
+

Path.relpath

+
+
 Path.relpath (start=None)
+
+

Same as os.path.relpath, but returns a Path, and resolves symlinks

+
+
p = Path('../fastcore/').resolve()
+p
+
+
Path('/Users/daniel.roy.greenfeld/fh/fastcore/fastcore')
+
+
+
+
p.relpath(Path.cwd())
+
+
Path('../fastcore')
+
+
+
+

source

+
+
+

Path.ls

+
+
 Path.ls (n_max=None, file_type=None, file_exts=None)
+
+

Contents of path as a list

+

We add an ls() method to pathlib.Path which is simply defined as list(Path.iterdir()), mainly for convenience in REPL environments such as notebooks.

+
+
path = Path()
+t = path.ls()
+assert len(t)>0
+t1 = path.ls(10)
+test_eq(len(t1), 10)
+t2 = path.ls(file_exts='.ipynb')
+assert len(t)>len(t2)
+t[0]
+
+
Path('000_tour.ipynb')
+
+
+

You can also pass an optional file_type MIME prefix and/or a list of file extensions.

+
+
lib_path = (path/'../fastcore')
+txt_files=lib_path.ls(file_type='text')
+assert len(txt_files) > 0 and txt_files[0].suffix=='.py'
+ipy_files=path.ls(file_exts=['.ipynb'])
+assert len(ipy_files) > 0 and ipy_files[0].suffix=='.ipynb'
+txt_files[0],ipy_files[0]
+
+
(Path('../fastcore/shutil.py'), Path('000_tour.ipynb'))
+
+
+
+

source

+
+
+

Path.__repr__

+
+
 Path.__repr__ ()
+
+

Return repr(self).

+

fastai also updates the repr of Path such that, if Path.BASE_PATH is defined, all paths are printed relative to that path (as long as they are contained in Path.BASE_PATH:

+
+
t = ipy_files[0].absolute()
+try:
+    Path.BASE_PATH = t.parent.parent
+    test_eq(repr(t), f"Path('nbs/{t.name}')")
+finally: Path.BASE_PATH = None
+
+
+

source

+
+
+

Path.delete

+
+
 Path.delete ()
+
+

Delete a file, symlink, or directory tree

+
+
+
+

Reindexing Collections

+
+

source

+
+

ReindexCollection

+
+
 ReindexCollection (coll, idxs=None, cache=None, tfm=<function noop>)
+
+

Reindexes collection coll with indices idxs and optional LRU cache of size cache

+

This is useful when constructing batches or organizing data in a particular manner (i.e. for deep learning). This class is primarly used in organizing data for language models in fastai.

+

You can supply a custom index upon instantiation with the idxs argument, or you can call the reindex method to supply a new index for your collection.

+

Here is how you can reindex a list such that the elements are reversed:

+
+
rc=ReindexCollection(['a', 'b', 'c', 'd', 'e'], idxs=[4,3,2,1,0])
+list(rc)
+
+
['e', 'd', 'c', 'b', 'a']
+
+
+

Alternatively, you can use the reindex method:

+
+

source

+
+
ReindexCollection.reindex
+
+
 ReindexCollection.reindex (idxs)
+
+

Replace self.idxs with idxs

+
+
rc=ReindexCollection(['a', 'b', 'c', 'd', 'e'])
+rc.reindex([4,3,2,1,0])
+list(rc)
+
+
['e', 'd', 'c', 'b', 'a']
+
+
+

You can optionally specify a LRU cache, which uses functools.lru_cache upon instantiation:

+
+
sz = 50
+t = ReindexCollection(L.range(sz), cache=2)
+
+#trigger a cache hit by indexing into the same element multiple times
+t[0], t[0]
+t._get.cache_info()
+
+
CacheInfo(hits=1, misses=1, maxsize=2, currsize=1)
+
+
+

You can optionally clear the LRU cache by calling the cache_clear method:

+
+

source

+
+
+
ReindexCollection.cache_clear
+
+
 ReindexCollection.cache_clear ()
+
+

Clear LRU cache

+
+
sz = 50
+t = ReindexCollection(L.range(sz), cache=2)
+
+#trigger a cache hit by indexing into the same element multiple times
+t[0], t[0]
+t.cache_clear()
+t._get.cache_info()
+
+
CacheInfo(hits=0, misses=0, maxsize=2, currsize=0)
+
+
+
+

source

+
+
+
ReindexCollection.shuffle
+
+
 ReindexCollection.shuffle ()
+
+

Randomly shuffle indices

+

Note that an ordered index is automatically constructed for the data structure even if one is not supplied.

+
+
rc=ReindexCollection(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'])
+rc.shuffle()
+list(rc)
+
+
['a', 'd', 'h', 'c', 'e', 'b', 'f', 'g']
+
+
+
+
sz = 50
+t = ReindexCollection(L.range(sz), cache=2)
+test_eq(list(t), range(sz))
+test_eq(t[sz-1], sz-1)
+test_eq(t._get.cache_info().hits, 1)
+t.shuffle()
+test_eq(t._get.cache_info().hits, 1)
+test_ne(list(t), range(sz))
+test_eq(set(t), set(range(sz)))
+t.cache_clear()
+test_eq(t._get.cache_info().hits, 0)
+test_eq(t.count(0), 1)
+
+
+
+
+
+

Other Helpers

+
+

source

+ +
+

truncstr

+
+
 truncstr (s:str, maxlen:int, suf:str='…', space='')
+
+

Truncate s to length maxlen, adding suffix suf if truncated

+
+
w = 'abacadabra'
+test_eq(truncstr(w, 10), w)
+test_eq(truncstr(w, 5), 'abac…')
+test_eq(truncstr(w, 5, suf=''), 'abaca')
+test_eq(truncstr(w, 11, space='_'), w+"_")
+test_eq(truncstr(w, 10, space='_'), w[:-1]+'…')
+test_eq(truncstr(w, 5, suf='!!'), 'aba!!')
+
+
+

source

+
+
+

sparkline

+
+
 sparkline (data, mn=None, mx=None, empty_zero=False)
+
+

Sparkline for data, with Nones (and zero, if empty_zero) shown as empty column

+
+
data = [9,6,None,1,4,0,8,15,10]
+print(f'without "empty_zero": {sparkline(data, empty_zero=False)}')
+print(f'   with "empty_zero": {sparkline(data, empty_zero=True )}')
+
+
without "empty_zero": ▅▂ ▁▂▁▃▇▅
+   with "empty_zero": ▅▂ ▁▂ ▃▇▅
+
+
+

You can set a maximum and minimum for the y-axis of the sparkline with the arguments mn and mx respectively:

+
+
sparkline([1,2,3,400], mn=0, mx=3)
+
+
'▂▅▇▇'
+
+
+
+

source

+
+
+

modify_exception

+
+
 modify_exception (e:Exception, msg:str=None, replace:bool=False)
+
+

Modifies e with a custom message attached

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TypeDefaultDetails
eExceptionAn exception
msgstrNoneA custom message
replaceboolFalseWhether to replace e.args with [msg]
ReturnsException
+
+
msg = "This is my custom message!"
+
+test_fail(lambda: (_ for _ in ()).throw(modify_exception(Exception(), None)), contains='')
+test_fail(lambda: (_ for _ in ()).throw(modify_exception(Exception(), msg)), contains=msg)
+test_fail(lambda: (_ for _ in ()).throw(modify_exception(Exception("The first message"), msg)), contains="The first message This is my custom message!")
+test_fail(lambda: (_ for _ in ()).throw(modify_exception(Exception("The first message"), msg, True)), contains="This is my custom message!")
+
+
+

source

+
+
+

round_multiple

+
+
 round_multiple (x, mult, round_down=False)
+
+

Round x to nearest multiple of mult

+
+
test_eq(round_multiple(63,32), 64)
+test_eq(round_multiple(50,32), 64)
+test_eq(round_multiple(40,32), 32)
+test_eq(round_multiple( 0,32),  0)
+test_eq(round_multiple(63,32, round_down=True), 32)
+test_eq(round_multiple((63,40),32), (64,32))
+
+
+

source

+
+
+

set_num_threads

+
+
 set_num_threads (nt)
+
+

Get numpy (and others) to use nt threads

+

This sets the number of threads consistently for many tools, by:

+
    +
  1. Set the following environment variables equal to nt: OPENBLAS_NUM_THREADS,NUMEXPR_NUM_THREADS,OMP_NUM_THREADS,MKL_NUM_THREADS
  2. +
  3. Sets nt threads for numpy and pytorch.
  4. +
+
+

source

+
+
+

join_path_file

+
+
 join_path_file (file, path, ext='')
+
+

Return path/file if file is a string or a Path, file otherwise

+
+
path = Path.cwd()/'_tmp'/'tst'
+f = join_path_file('tst.txt', path)
+assert path.exists()
+test_eq(f, path/'tst.txt')
+with open(f, 'w') as f_: assert join_path_file(f_, path) == f_
+shutil.rmtree(Path.cwd()/'_tmp')
+
+
+

source

+
+
+

autostart

+
+
 autostart (g)
+
+

Decorator that automatically starts a generator

+
+

source

+
+

EventTimer

+
+
 EventTimer (store=5, span=60)
+
+

An event timer with history of store items of time span

+

Add events with add, and get number of events and their frequency (freq).

+
+
# Random wait function for testing
+def _randwait(): yield from (sleep(random.random()/200) for _ in range(100))
+
+c = EventTimer(store=5, span=0.03)
+for o in _randwait(): c.add(1)
+print(f'Num Events: {c.events}, Freq/sec: {c.freq:.01f}')
+print('Most recent: ', sparkline(c.hist), *L(c.hist).map('{:.01f}'))
+
+
Num Events: 3, Freq/sec: 205.6
+Most recent:  ▁▁▃▁▇ 254.1 263.2 284.5 259.9 315.7
+
+
+
+

source

+
+
+
+

stringfmt_names

+
+
 stringfmt_names (s:str)
+
+

Unique brace-delimited names in s

+
+
s = '/pulls/{pull_number}/reviews/{review_id}'
+test_eq(stringfmt_names(s), ['pull_number','review_id'])
+
+
+

source

+
+

PartialFormatter

+
+
 PartialFormatter ()
+
+

A string.Formatter that doesn’t error on missing fields, and tracks missing fields and unused args

+
+

source

+
+
+
+

partial_format

+
+
 partial_format (s:str, **kwargs)
+
+

string format s, ignoring missing field errors, returning missing and extra fields

+

The result is a tuple of (formatted_string,missing_fields,extra_fields), e.g:

+
+
res,missing,xtra = partial_format(s, pull_number=1, foo=2)
+test_eq(res, '/pulls/1/reviews/{review_id}')
+test_eq(missing, ['review_id'])
+test_eq(xtra, {'foo':2})
+
+
+

source

+
+
+

utc2local

+
+
 utc2local (dt:datetime.datetime)
+
+

Convert dt from UTC to local time

+
+
dt = datetime(2000,1,1,12)
+print(f'{dt} UTC is {utc2local(dt)} local time')
+
+
2000-01-01 12:00:00 UTC is 2000-01-01 22:00:00+10:00 local time
+
+
+
+

source

+
+
+

local2utc

+
+
 local2utc (dt:datetime.datetime)
+
+

Convert dt from local to UTC time

+
+
print(f'{dt} local is {local2utc(dt)} UTC time')
+
+
2000-01-01 12:00:00 local is 2000-01-01 02:00:00+00:00 UTC time
+
+
+
+

source

+
+
+

trace

+
+
 trace (f)
+
+

Add set_trace to an existing function f

+

You can add a breakpoint to an existing function, e.g:

+
Path.cwd = trace(Path.cwd)
+Path.cwd()
+

Now, when the function is called it will drop you into the debugger. Note, you must issue the s command when you begin to step into the function that is being traced.

+
+

source

+
+
+

modified_env

+
+
 modified_env (*delete, **replace)
+
+

Context manager temporarily modifying os.environ by deleting delete and replacing replace

+
+
# USER isn't in Cloud Linux Environments
+env_test = 'USERNAME' if sys.platform == "win32" else 'SHELL'
+oldusr = os.environ[env_test]
+
+replace_param = {env_test: 'a'}
+with modified_env('PATH', **replace_param):
+    test_eq(os.environ[env_test], 'a')
+    assert 'PATH' not in os.environ
+
+assert 'PATH' in os.environ
+test_eq(os.environ[env_test], oldusr)
+
+
+

source

+
+

ContextManagers

+
+
 ContextManagers (mgrs)
+
+

Wrapper for contextlib.ExitStack which enters a collection of context managers

+
+

source

+
+
+
+

shufflish

+
+
 shufflish (x, pct=0.04)
+
+

Randomly relocate items of x up to pct of len(x) from their starting location

+
+

source

+
+
+

console_help

+
+
 console_help (libname:str)
+
+

Show help for all console scripts from libname

+ + + + + + + + + + + + + + + +
TypeDetails
libnamestrname of library for console script listing
+
+

source

+
+
+

hl_md

+
+
 hl_md (s, lang='xml', show=True)
+
+

Syntax highlight s using lang.

+

When we display code in a notebook, it’s nice to highlight it, so we create a function to simplify that:

+
+
hl_md('<test><xml foo="bar">a child</xml></test>')
+
+
<test><xml foo="bar">a child</xml></test>
+
+
+
+

source

+
+
+

type2str

+
+
 type2str (typ:type)
+
+

Stringify typ

+
+
test_eq(type2str(Optional[float]), 'Union[float, None]')
+
+
+

source

+
+
+

dataclass_src

+
+
 dataclass_src (cls)
+
+
+
DC = make_dataclass('DC', [('x', int), ('y', Optional[float], None), ('z', float, None)])
+print(dataclass_src(DC))
+
+
@dataclass
+class DC:
+    x: int
+    y: Union[float, None] = None
+    z: float = None
+
+
+
+
+

source

+
+
+

Unset

+
+
 Unset (value, names=None, module=None, qualname=None, type=None, start=1)
+
+

An enumeration.

+
+

source

+
+
+

nullable_dc

+
+
 nullable_dc (cls)
+
+

Like dataclass, but default of UNSET added to fields without defaults

+
+
@nullable_dc
+class Person: name: str; age: int; city: str = "Unknown"
+Person(name="Bob")
+
+
Person(name='Bob', age=UNSET, city='Unknown')
+
+
+
+

source

+
+
+

make_nullable

+
+
 make_nullable (clas)
+
+
+
@dataclass
+class Person: name: str; age: int; city: str = "Unknown"
+
+make_nullable(Person)
+Person("Bob", city='NY')
+
+
Person(name='Bob', age=UNSET, city='NY')
+
+
+
+
Person(name="Bob")
+
+
Person(name='Bob', age=UNSET, city='Unknown')
+
+
+
+
Person("Bob", 34)
+
+
Person(name='Bob', age=34, city='Unknown')
+
+
+
+

source

+
+
+

flexiclass

+
+
 flexiclass (cls)
+
+

Convert cls into a dataclass like make_nullable. Converts in place and also returns the result.

+ + + + + + + + + + + + + + + + + + + + +
TypeDetails
clsThe class to convert
Returnsdataclass
+

This can be used as a decorator…

+
+
@flexiclass
+class Person: name: str; age: int; city: str = "Unknown"
+
+bob = Person(name="Bob")
+bob
+
+
Person(name='Bob', age=UNSET, city='Unknown')
+
+
+

…or can update the behavior of an existing class (or dataclass):

+
+
class Person: name: str; age: int; city: str = "Unknown"
+
+flexiclass(Person)
+bob = Person(name="Bob")
+bob
+
+
Person(name='Bob', age=UNSET, city='Unknown')
+
+
+

Action occurs in-place:

+
+
class Person: name: str; age: int; city: str = "Unknown"
+
+flexiclass(Person)
+is_dataclass(Person)
+
+
True
+
+
+
+

source

+
+
+

asdict

+
+
 asdict (o)
+
+

Convert o to a dict, supporting dataclasses, namedtuples, iterables, and __dict__ attrs.

+

Any UNSET values are not included.

+
+
asdict(bob)
+
+
{'name': 'Bob', 'city': 'Unknown'}
+
+
+

To customise dict conversion behavior for a class, implement the _asdict method (this is used in the Python stdlib for named tuples).

+
+

source

+
+
+

is_typeddict

+
+
 is_typeddict (cls:type)
+
+

Check if cls is a TypedDict

+
+
class MyDict(TypedDict): name:str
+
+assert is_typeddict(MyDict)
+assert not is_typeddict({'a':1})
+
+
+

source

+
+
+

is_namedtuple

+
+
 is_namedtuple (cls)
+
+

True if cls is a namedtuple type

+
+
assert is_namedtuple(namedtuple('tst', ['a']))
+assert not is_namedtuple(tuple)
+
+
+

source

+
+
+

flexicache

+
+
 flexicache (*funcs, maxsize=128)
+
+

Like lru_cache, but customisable with policy funcs

+

This is a flexible lru cache function that you can pass a list of functions to. Those functions define the cache eviction policy. For instance, time_policy is provided for time-based cache eviction, and mtime_policy evicts based on a file’s modified-time changing. The policy functions are passed the last value that function returned was (initially None), and return a new value to indicate the cache has expired. When the cache expires, all functions are called with None to force getting new values.

+
+

source

+
+
+

time_policy

+
+
 time_policy (seconds)
+
+

A flexicache policy that expires cached items after seconds have passed

+
+

source

+
+
+

mtime_policy

+
+
 mtime_policy (filepath)
+
+

A flexicache policy that expires cached items after filepath modified-time changes

+
+
@flexicache(time_policy(10), mtime_policy('000_tour.ipynb'))
+def cached_func(x, y): return x+y
+
+cached_func(1,2)
+
+
3
+
+
+
+
@flexicache(time_policy(10), mtime_policy('000_tour.ipynb'))
+async def cached_func(x, y): return x+y
+
+await cached_func(1,2)
+await cached_func(1,2)
+
+
3
+
+
+
+

source

+
+
+

timed_cache

+
+
 timed_cache (seconds=60, maxsize=128)
+
+

Like lru_cache, but also with time-based eviction

+

This function is a small convenience wrapper for using flexicache with time_policy.

+
+
@timed_cache(seconds=0.05, maxsize=2)
+def cached_func(x): return x * 2, time()
+
+# basic caching
+result1, time1 = cached_func(2)
+test_eq(result1, 4)
+sleep(0.001)
+result2, time2 = cached_func(2)
+test_eq(result2, 4)
+test_eq(time1, time2)
+
+# caching different values
+result3, _ = cached_func(3)
+test_eq(result3, 6)
+
+# maxsize
+_, time4 = cached_func(4)
+_, time2_new = cached_func(2)
+test_close(time2, time2_new, eps=0.1)
+_, time3_new = cached_func(3)
+test_ne(time3_new, time())
+
+# time expiration
+sleep(0.05)
+_, time4_new = cached_func(4)
+test_ne(time4_new, time())
+
+ + +
+
+ +
+ +
+ + + + + \ No newline at end of file diff --git a/xtras.html.md b/xtras.html.md new file mode 100644 index 00000000..15c27e43 --- /dev/null +++ b/xtras.html.md @@ -0,0 +1,1854 @@ +# Utility functions + + + + +## File Functions + +Utilities (other than extensions to Pathlib.Path) for dealing with IO. + +------------------------------------------------------------------------ + +source + +### walk + +> walk (path:pathlib.Path|str, symlinks:bool=True, keep_file: infunctioncallable>=, keep_folder: infunctioncallable>=, skip_folder: infunctioncallable>=, func: infunctioncallable>=, ret_folders:bool=False) + +*Generator version of `os.walk`, using functions to filter files and +folders* + + ++++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TypeDefaultDetails
pathpathlib.Path | strpath to start searching
symlinksboolTruefollow symlinks?
keep_filecallableret_truefunction that returns True for wanted files
keep_foldercallableret_truefunction that returns True for folders to enter
skip_foldercallableret_falsefunction that returns True for folders to skip
funccallablejoinfunction to apply to each matched file
ret_foldersboolFalsereturn folders, not just files
+ +------------------------------------------------------------------------ + +source + +### globtastic + +> globtastic (path:pathlib.Path|str, recursive:bool=True, +> symlinks:bool=True, file_glob:str=None, file_re:str=None, +> folder_re:str=None, skip_file_glob:str=None, +> skip_file_re:str=None, skip_folder_re:str=None, func: infunctioncallable>=, ret_folders:bool=False) + +*A more powerful `glob`, including regex matches, symlink handling, and +skip parameters* + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TypeDefaultDetails
pathpathlib.Path | strpath to start searching
recursiveboolTruesearch subfolders
symlinksboolTruefollow symlinks?
file_globstrNoneOnly include files matching glob
file_restrNoneOnly include files matching regex
folder_restrNoneOnly enter folders matching regex
skip_file_globstrNoneSkip files matching glob
skip_file_restrNoneSkip files matching regex
skip_folder_restrNoneSkip folders matching regex,
funccallablejoinfunction to apply to each matched file
ret_foldersboolFalsereturn folders, not just files
ReturnsLPaths to matched files
+ +``` python +globtastic('.', skip_folder_re='^[_.]', folder_re='core', file_glob='*.*py*', file_re='c') +``` + + (#5) ['./fastcore/docments.py','./fastcore/dispatch.py','./fastcore/basics.py','./fastcore/docscrape.py','./fastcore/script.py'] + +------------------------------------------------------------------------ + +source + +### maybe_open + +> maybe_open (f, mode='r', **kwargs) + +*Context manager: open `f` if it is a path (and close on exit)* + +This is useful for functions where you want to accept a path *or* file. +[`maybe_open`](https://fastcore.fast.ai/xtras.html#maybe_open) will not +close your file handle if you pass one in. + +``` python +def _f(fn): + with maybe_open(fn) as f: return f.encoding + +fname = '00_test.ipynb' +sys_encoding = 'cp1252' if sys.platform == 'win32' else 'UTF-8' +test_eq(_f(fname), sys_encoding) +with open(fname) as fh: test_eq(_f(fh), sys_encoding) +``` + +For example, we can use this to reimplement +[`imghdr.what`](https://docs.python.org/3/library/imghdr.html#imghdr.what) +from the Python standard library, which is [written in Python +3.9](https://github.com/python/cpython/blob/3.9/Lib/imghdr.py#L11) as: + +``` python +from fastcore import imghdr +``` + +``` python +def what(file, h=None): + f = None + try: + if h is None: + if isinstance(file, (str,os.PathLike)): + f = open(file, 'rb') + h = f.read(32) + else: + location = file.tell() + h = file.read(32) + file.seek(location) + for tf in imghdr.tests: + res = tf(h, f) + if res: return res + finally: + if f: f.close() + return None +``` + +Here’s an example of the use of this function: + +``` python +fname = 'images/puppy.jpg' +what(fname) +``` + + 'jpeg' + +With [`maybe_open`](https://fastcore.fast.ai/xtras.html#maybe_open), +`Self`, and +[`L.map_first`](https://fastcore.fast.ai/foundation.html#l.map_first), +we can rewrite this in a much more concise and (in our opinion) clear +way: + +``` python +def what(file, h=None): + if h is None: + with maybe_open(file, 'rb') as f: h = f.peek(32) + return L(imghdr.tests).map_first(Self(h,file)) +``` + +…and we can check that it still works: + +``` python +test_eq(what(fname), 'jpeg') +``` + +…along with the version passing a file handle: + +``` python +with open(fname,'rb') as f: test_eq(what(f), 'jpeg') +``` + +…along with the `h` parameter version: + +``` python +with open(fname,'rb') as f: test_eq(what(None, h=f.read(32)), 'jpeg') +``` + +------------------------------------------------------------------------ + +source + +### mkdir + +> mkdir (path, exist_ok=False, parents=False, overwrite=False, **kwargs) + +*Creates and returns a directory defined by `path`, optionally removing +previous existing directory if `overwrite` is `True`* + +``` python +with tempfile.TemporaryDirectory() as d: + path = Path(os.path.join(d, 'new_dir')) + new_dir = mkdir(path) + assert new_dir.exists() + test_eq(new_dir, path) + + # test overwrite + with open(new_dir/'test.txt', 'w') as f: f.writelines('test') + test_eq(len(list(walk(new_dir))), 1) # assert file is present + new_dir = mkdir(new_dir, overwrite=True) + test_eq(len(list(walk(new_dir))), 0) # assert file was deleted +``` + +------------------------------------------------------------------------ + +source + +### image_size + +> image_size (fn) + +*Tuple of (w,h) for png, gif, or jpg; `None` otherwise* + +``` python +test_eq(image_size(fname), (1200,803)) +``` + +------------------------------------------------------------------------ + +source + +### bunzip + +> bunzip (fn) + +*bunzip `fn`, raising exception if output already exists* + +``` python +f = Path('files/test.txt') +if f.exists(): f.unlink() +bunzip('files/test.txt.bz2') +t = f.open().readlines() +test_eq(len(t),1) +test_eq(t[0], 'test\n') +f.unlink() +``` + +------------------------------------------------------------------------ + +source + +### loads + +> loads (s, **kw) + +*Same as `json.loads`, but handles `None`* + +------------------------------------------------------------------------ + +source + +### loads_multi + +> loads_multi (s:str) + +*Generator of \>=0 decoded json dicts, possibly with non-json ignored +text at start and end* + +``` python +tst = """ +# ignored +{ "a":1 } +hello +{ +"b":2 +} +""" + +test_eq(list(loads_multi(tst)), [{'a': 1}, {'b': 2}]) +``` + +------------------------------------------------------------------------ + +source + +### dumps + +> dumps (obj, **kw) + +*Same as `json.dumps`, but uses `ujson` if available* + +------------------------------------------------------------------------ + +source + +### untar_dir + +> untar_dir (fname, dest, rename=False, overwrite=False) + +*untar `file` into `dest`, creating a directory if the root contains +more than one item* + +``` python +def test_untar(foldername, rename=False, **kwargs): + with tempfile.TemporaryDirectory() as d: + nm = os.path.join(d, 'a') + shutil.make_archive(nm, 'gztar', **kwargs) + with tempfile.TemporaryDirectory() as d2: + d2 = Path(d2) + untar_dir(nm+'.tar.gz', d2, rename=rename) + test_eq(d2.ls(), [d2/foldername]) +``` + +If the contents of `fname` contain just one file or directory, it is +placed directly in `dest`: + +``` python +# using `base_dir` in `make_archive` results in `images` directory included in file names +test_untar('images', base_dir='images') +``` + +If `rename` then the directory created is named based on the archive, +without extension: + +``` python +test_untar('a', base_dir='images', rename=True) +``` + +If the contents of `fname` contain multiple files and directories, a new +folder in `dest` is created with the same name as `fname` (but without +extension): + +``` python +# using `root_dir` in `make_archive` results in `images` directory *not* included in file names +test_untar('a', root_dir='images') +``` + +------------------------------------------------------------------------ + +source + +### repo_details + +> repo_details (url) + +*Tuple of `owner,name` from ssh or https git repo `url`* + +``` python +test_eq(repo_details('https://github.com/fastai/fastai.git'), ['fastai', 'fastai']) +test_eq(repo_details('git@github.com:fastai/nbdev.git\n'), ['fastai', 'nbdev']) +``` + +------------------------------------------------------------------------ + +source + +### run + +> run (cmd, *rest, same_in_win=False, ignore_ex=False, as_bytes=False, +> stderr=False) + +*Pass `cmd` (splitting with `shlex` if string) to `subprocess.run`; +return `stdout`; raise `IOError` if fails* + +You can pass a string (which will be split based on standard shell +rules), a list, or pass args directly: + +``` python +run('echo', same_in_win=True) +run('pip', '--version', same_in_win=True) +run(['pip', '--version'], same_in_win=True) +``` + + 'pip 23.3.1 from /Users/jhoward/miniconda3/lib/python3.11/site-packages/pip (python 3.11)' + +``` python +if sys.platform == 'win32': + assert 'ipynb' in run('cmd /c dir /p') + assert 'ipynb' in run(['cmd', '/c', 'dir', '/p']) + assert 'ipynb' in run('cmd', '/c', 'dir', '/p') +else: + assert 'ipynb' in run('ls -ls') + assert 'ipynb' in run(['ls', '-l']) + assert 'ipynb' in run('ls', '-l') +``` + +Some commands fail in non-error situations, like `grep`. Use `ignore_ex` +in those cases, which will return a tuple of stdout and returncode: + +``` python +if sys.platform == 'win32': + test_eq(run('cmd /c findstr asdfds 00_test.ipynb', ignore_ex=True)[0], 1) +else: + test_eq(run('grep asdfds 00_test.ipynb', ignore_ex=True)[0], 1) +``` + +[`run`](https://fastcore.fast.ai/xtras.html#run) automatically decodes +returned bytes to a `str`. Use `as_bytes` to skip that: + +``` python +if sys.platform == 'win32': + test_eq(run('cmd /c echo hi'), 'hi') +else: + test_eq(run('echo hi', as_bytes=True), b'hi\n') +``` + +------------------------------------------------------------------------ + +source + +### open_file + +> open_file (fn, mode='r', **kwargs) + +*Open a file, with optional compression if gz or bz2 suffix* + +------------------------------------------------------------------------ + +source + +### save_pickle + +> save_pickle (fn, o) + +*Save a pickle file, to a file name or opened file* + +------------------------------------------------------------------------ + +source + +### load_pickle + +> load_pickle (fn) + +*Load a pickle file from a file name or opened file* + +``` python +for suf in '.pkl','.bz2','.gz': + # delete=False is added for Windows + # https://stackoverflow.com/questions/23212435/permission-denied-to-write-to-my-temporary-file + with tempfile.NamedTemporaryFile(suffix=suf, delete=False) as f: + fn = Path(f.name) + save_pickle(fn, 't') + t = load_pickle(fn) + f.close() + test_eq(t,'t') +``` + +------------------------------------------------------------------------ + +source + +### parse_env + +> parse_env (s:str=None, fn:Union[str,pathlib.Path]=None) + +*Parse a shell-style environment string or file* + +``` python +testf = """# comment + # another comment + export FOO="bar#baz" +BAR=thing # comment "ok" + baz='thong' +QUX=quux +export ZAP = "zip" # more comments + FOOBAR = 42 # trailing space and comment""" + +exp = dict(FOO='bar#baz', BAR='thing', baz='thong', QUX='quux', ZAP='zip', FOOBAR='42') + +test_eq(parse_env(testf), exp) +``` + +------------------------------------------------------------------------ + +source + +### expand_wildcards + +> expand_wildcards (code) + +*Expand all wildcard imports in the given code string.* + +``` python +inp = """from math import * +from os import * +from random import * +def func(): return sin(pi) + path.join('a', 'b') + randint(1, 10)""" + +exp = """from math import pi, sin +from os import path +from random import randint +def func(): return sin(pi) + path.join('a', 'b') + randint(1, 10)""" + +test_eq(expand_wildcards(inp), exp) + +inp = """from itertools import * +def func(): pass""" +test_eq(expand_wildcards(inp), inp) + +inp = """def outer(): + from math import * + def inner(): + from os import * + return sin(pi) + path.join('a', 'b')""" + +exp = """def outer(): + from math import pi, sin + def inner(): + from os import path + return sin(pi) + path.join('a', 'b')""" + +test_eq(expand_wildcards(inp), exp) +``` + +## Collections + +------------------------------------------------------------------------ + +source + +### dict2obj + +> dict2obj (d, list_func=, dict_func= 'fastcore.basics.AttrDict'>) + +*Convert (possibly nested) dicts (or lists of dicts) to +[`AttrDict`](https://fastcore.fast.ai/basics.html#attrdict)* + +This is a convenience to give you “dotted” access to (possibly nested) +dictionaries, e.g: + +``` python +d1 = dict(a=1, b=dict(c=2,d=3)) +d2 = dict2obj(d1) +test_eq(d2.b.c, 2) +test_eq(d2.b['c'], 2) +``` + +It can also be used on lists of dicts. + +``` python +_list_of_dicts = [d1, d1] +ds = dict2obj(_list_of_dicts) +test_eq(ds[0].b.c, 2) +``` + +------------------------------------------------------------------------ + +source + +### obj2dict + +> obj2dict (d) + +*Convert (possibly nested) AttrDicts (or lists of AttrDicts) to `dict`* + +[`obj2dict`](https://fastcore.fast.ai/xtras.html#obj2dict) can be used +to reverse what is done by +[`dict2obj`](https://fastcore.fast.ai/xtras.html#dict2obj): + +``` python +test_eq(obj2dict(d2), d1) +test_eq(obj2dict(ds), _list_of_dicts) +``` + +------------------------------------------------------------------------ + +source + +### repr_dict + +> repr_dict (d) + +*Print nested dicts and lists, such as returned by +[`dict2obj`](https://fastcore.fast.ai/xtras.html#dict2obj)* + +``` python +print(repr_dict(d2)) +``` + + - a: 1 + - b: + - c: 2 + - d: 3 + +------------------------------------------------------------------------ + +source + +### is_listy + +> is_listy (x) + +*`isinstance(x, (tuple,list,L,slice,Generator))`* + +``` python +assert is_listy((1,)) +assert is_listy([1]) +assert is_listy(L([1])) +assert is_listy(slice(2)) +assert not is_listy(array([1])) +``` + +------------------------------------------------------------------------ + +source + +### mapped + +> mapped (f, it) + +*map `f` over `it`, unless it’s not listy, in which case return `f(it)`* + +``` python +def _f(x,a=1): return x-a + +test_eq(mapped(_f,1),0) +test_eq(mapped(_f,[1,2]),[0,1]) +test_eq(mapped(_f,(1,)),(0,)) +``` + +## Extensions to Pathlib.Path + +The following methods are added to the standard python libary +[Pathlib.Path](https://docs.python.org/3/library/pathlib.html#basic-use). + +------------------------------------------------------------------------ + +source + +### Path.readlines + +> Path.readlines (hint=-1, encoding='utf8') + +*Read the content of `self`* + +------------------------------------------------------------------------ + +source + +### Path.read_json + +> Path.read_json (encoding=None, errors=None) + +*Same as `read_text` followed by +[`loads`](https://fastcore.fast.ai/xtras.html#loads)* + +------------------------------------------------------------------------ + +source + +### Path.mk_write + +> Path.mk_write (data, encoding=None, errors=None, mode=511) + +*Make all parent dirs of `self`, and write `data`* + +------------------------------------------------------------------------ + +source + +### Path.relpath + +> Path.relpath (start=None) + +*Same as `os.path.relpath`, but returns a `Path`, and resolves symlinks* + +``` python +p = Path('../fastcore/').resolve() +p +``` + + Path('/Users/daniel.roy.greenfeld/fh/fastcore/fastcore') + +``` python +p.relpath(Path.cwd()) +``` + + Path('../fastcore') + +------------------------------------------------------------------------ + +source + +### Path.ls + +> Path.ls (n_max=None, file_type=None, file_exts=None) + +*Contents of path as a list* + +We add an `ls()` method to `pathlib.Path` which is simply defined as +`list(Path.iterdir())`, mainly for convenience in REPL environments such +as notebooks. + +``` python +path = Path() +t = path.ls() +assert len(t)>0 +t1 = path.ls(10) +test_eq(len(t1), 10) +t2 = path.ls(file_exts='.ipynb') +assert len(t)>len(t2) +t[0] +``` + + Path('000_tour.ipynb') + +You can also pass an optional `file_type` MIME prefix and/or a list of +file extensions. + +``` python +lib_path = (path/'../fastcore') +txt_files=lib_path.ls(file_type='text') +assert len(txt_files) > 0 and txt_files[0].suffix=='.py' +ipy_files=path.ls(file_exts=['.ipynb']) +assert len(ipy_files) > 0 and ipy_files[0].suffix=='.ipynb' +txt_files[0],ipy_files[0] +``` + + (Path('../fastcore/shutil.py'), Path('000_tour.ipynb')) + +------------------------------------------------------------------------ + +source + +### Path.\_\_repr\_\_ + +> Path.__repr__ () + +*Return repr(self).* + +fastai also updates the `repr` of `Path` such that, if `Path.BASE_PATH` +is defined, all paths are printed relative to that path (as long as they +are contained in `Path.BASE_PATH`: + +``` python +t = ipy_files[0].absolute() +try: + Path.BASE_PATH = t.parent.parent + test_eq(repr(t), f"Path('nbs/{t.name}')") +finally: Path.BASE_PATH = None +``` + +------------------------------------------------------------------------ + +source + +### Path.delete + +> Path.delete () + +*Delete a file, symlink, or directory tree* + +## Reindexing Collections + +------------------------------------------------------------------------ + +source + +#### ReindexCollection + +> ReindexCollection (coll, idxs=None, cache=None, tfm=) + +*Reindexes collection `coll` with indices `idxs` and optional LRU cache +of size `cache`* + +This is useful when constructing batches or organizing data in a +particular manner (i.e. for deep learning). This class is primarly used +in organizing data for language models in fastai. + +You can supply a custom index upon instantiation with the `idxs` +argument, or you can call the `reindex` method to supply a new index for +your collection. + +Here is how you can reindex a list such that the elements are reversed: + +``` python +rc=ReindexCollection(['a', 'b', 'c', 'd', 'e'], idxs=[4,3,2,1,0]) +list(rc) +``` + + ['e', 'd', 'c', 'b', 'a'] + +Alternatively, you can use the `reindex` method: + +------------------------------------------------------------------------ + +source + +###### ReindexCollection.reindex + +> ReindexCollection.reindex (idxs) + +*Replace `self.idxs` with idxs* + +``` python +rc=ReindexCollection(['a', 'b', 'c', 'd', 'e']) +rc.reindex([4,3,2,1,0]) +list(rc) +``` + + ['e', 'd', 'c', 'b', 'a'] + +You can optionally specify a LRU cache, which uses +[functools.lru_cache](https://docs.python.org/3/library/functools.html#functools.lru_cache) +upon instantiation: + +``` python +sz = 50 +t = ReindexCollection(L.range(sz), cache=2) + +#trigger a cache hit by indexing into the same element multiple times +t[0], t[0] +t._get.cache_info() +``` + + CacheInfo(hits=1, misses=1, maxsize=2, currsize=1) + +You can optionally clear the LRU cache by calling the `cache_clear` +method: + +------------------------------------------------------------------------ + +source + +##### ReindexCollection.cache_clear + +> ReindexCollection.cache_clear () + +*Clear LRU cache* + +``` python +sz = 50 +t = ReindexCollection(L.range(sz), cache=2) + +#trigger a cache hit by indexing into the same element multiple times +t[0], t[0] +t.cache_clear() +t._get.cache_info() +``` + + CacheInfo(hits=0, misses=0, maxsize=2, currsize=0) + +------------------------------------------------------------------------ + +source + +##### ReindexCollection.shuffle + +> ReindexCollection.shuffle () + +*Randomly shuffle indices* + +Note that an ordered index is automatically constructed for the data +structure even if one is not supplied. + +``` python +rc=ReindexCollection(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']) +rc.shuffle() +list(rc) +``` + + ['a', 'd', 'h', 'c', 'e', 'b', 'f', 'g'] + +``` python +sz = 50 +t = ReindexCollection(L.range(sz), cache=2) +test_eq(list(t), range(sz)) +test_eq(t[sz-1], sz-1) +test_eq(t._get.cache_info().hits, 1) +t.shuffle() +test_eq(t._get.cache_info().hits, 1) +test_ne(list(t), range(sz)) +test_eq(set(t), set(range(sz))) +t.cache_clear() +test_eq(t._get.cache_info().hits, 0) +test_eq(t.count(0), 1) +``` + +## Other Helpers + +------------------------------------------------------------------------ + +source + +### get_source_link + +> get_source_link (func) + +*Return link to `func` in source code* + +[`get_source_link`](https://fastcore.fast.ai/xtras.html#get_source_link) +allows you get a link to source code related to an object. For +[nbdev](https://github.com/fastai/nbdev) related projects such as +fastcore, we can get the full link to a GitHub repo. For `nbdev` +projects, be sure to properly set the `git_url` in `settings.ini` +(derived from `lib_name` and `branch` on top of the prefix you will need +to adapt) so that those links are correct. + +For example, below we get the link to +[`fastcore.test.test_eq`](https://fastcore.fast.ai/test.html#test_eq): + +``` python +from fastcore.test import test_eq +``` + +``` python +assert 'fastcore/test.py' in get_source_link(test_eq) +assert get_source_link(test_eq).startswith('https://github.com/fastai/fastcore') +get_source_link(test_eq) +``` + + 'https://github.com/fastai/fastcore/tree/master/fastcore/test.py#L35' + +------------------------------------------------------------------------ + +source + +### truncstr + +> truncstr (s:str, maxlen:int, suf:str='…', space='') + +*Truncate `s` to length `maxlen`, adding suffix `suf` if truncated* + +``` python +w = 'abacadabra' +test_eq(truncstr(w, 10), w) +test_eq(truncstr(w, 5), 'abac…') +test_eq(truncstr(w, 5, suf=''), 'abaca') +test_eq(truncstr(w, 11, space='_'), w+"_") +test_eq(truncstr(w, 10, space='_'), w[:-1]+'…') +test_eq(truncstr(w, 5, suf='!!'), 'aba!!') +``` + +------------------------------------------------------------------------ + +source + +### sparkline + +> sparkline (data, mn=None, mx=None, empty_zero=False) + +*Sparkline for `data`, with `None`s (and zero, if `empty_zero`) shown as +empty column* + +``` python +data = [9,6,None,1,4,0,8,15,10] +print(f'without "empty_zero": {sparkline(data, empty_zero=False)}') +print(f' with "empty_zero": {sparkline(data, empty_zero=True )}') +``` + + without "empty_zero": ▅▂ ▁▂▁▃▇▅ + with "empty_zero": ▅▂ ▁▂ ▃▇▅ + +You can set a maximum and minimum for the y-axis of the sparkline with +the arguments `mn` and `mx` respectively: + +``` python +sparkline([1,2,3,400], mn=0, mx=3) +``` + + '▂▅▇▇' + +------------------------------------------------------------------------ + +source + +### modify_exception + +> modify_exception (e:Exception, msg:str=None, replace:bool=False) + +*Modifies `e` with a custom message attached* + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TypeDefaultDetails
eExceptionAn exception
msgstrNoneA custom message
replaceboolFalseWhether to replace e.args with [msg]
ReturnsException
+ +``` python +msg = "This is my custom message!" + +test_fail(lambda: (_ for _ in ()).throw(modify_exception(Exception(), None)), contains='') +test_fail(lambda: (_ for _ in ()).throw(modify_exception(Exception(), msg)), contains=msg) +test_fail(lambda: (_ for _ in ()).throw(modify_exception(Exception("The first message"), msg)), contains="The first message This is my custom message!") +test_fail(lambda: (_ for _ in ()).throw(modify_exception(Exception("The first message"), msg, True)), contains="This is my custom message!") +``` + +------------------------------------------------------------------------ + +source + +### round_multiple + +> round_multiple (x, mult, round_down=False) + +*Round `x` to nearest multiple of `mult`* + +``` python +test_eq(round_multiple(63,32), 64) +test_eq(round_multiple(50,32), 64) +test_eq(round_multiple(40,32), 32) +test_eq(round_multiple( 0,32), 0) +test_eq(round_multiple(63,32, round_down=True), 32) +test_eq(round_multiple((63,40),32), (64,32)) +``` + +------------------------------------------------------------------------ + +source + +### set_num_threads + +> set_num_threads (nt) + +*Get numpy (and others) to use `nt` threads* + +This sets the number of threads consistently for many tools, by: + +1. Set the following environment variables equal to `nt`: + `OPENBLAS_NUM_THREADS`,`NUMEXPR_NUM_THREADS`,`OMP_NUM_THREADS`,`MKL_NUM_THREADS` +2. Sets `nt` threads for numpy and pytorch. + +------------------------------------------------------------------------ + +source + +### join_path_file + +> join_path_file (file, path, ext='') + +*Return `path/file` if file is a string or a `Path`, file otherwise* + +``` python +path = Path.cwd()/'_tmp'/'tst' +f = join_path_file('tst.txt', path) +assert path.exists() +test_eq(f, path/'tst.txt') +with open(f, 'w') as f_: assert join_path_file(f_, path) == f_ +shutil.rmtree(Path.cwd()/'_tmp') +``` + +------------------------------------------------------------------------ + +source + +### autostart + +> autostart (g) + +*Decorator that automatically starts a generator* + +------------------------------------------------------------------------ + +source + +#### EventTimer + +> EventTimer (store=5, span=60) + +*An event timer with history of `store` items of time `span`* + +Add events with `add`, and get number of `events` and their frequency +(`freq`). + +``` python +# Random wait function for testing +def _randwait(): yield from (sleep(random.random()/200) for _ in range(100)) + +c = EventTimer(store=5, span=0.03) +for o in _randwait(): c.add(1) +print(f'Num Events: {c.events}, Freq/sec: {c.freq:.01f}') +print('Most recent: ', sparkline(c.hist), *L(c.hist).map('{:.01f}')) +``` + + Num Events: 3, Freq/sec: 205.6 + Most recent: ▁▁▃▁▇ 254.1 263.2 284.5 259.9 315.7 + +------------------------------------------------------------------------ + +source + +### stringfmt_names + +> stringfmt_names (s:str) + +*Unique brace-delimited names in `s`* + +``` python +s = '/pulls/{pull_number}/reviews/{review_id}' +test_eq(stringfmt_names(s), ['pull_number','review_id']) +``` + +------------------------------------------------------------------------ + +source + +#### PartialFormatter + +> PartialFormatter () + +*A `string.Formatter` that doesn’t error on missing fields, and tracks +missing fields and unused args* + +------------------------------------------------------------------------ + +source + +### partial_format + +> partial_format (s:str, **kwargs) + +*string format `s`, ignoring missing field errors, returning missing and +extra fields* + +The result is a tuple of +`(formatted_string,missing_fields,extra_fields)`, e.g: + +``` python +res,missing,xtra = partial_format(s, pull_number=1, foo=2) +test_eq(res, '/pulls/1/reviews/{review_id}') +test_eq(missing, ['review_id']) +test_eq(xtra, {'foo':2}) +``` + +------------------------------------------------------------------------ + +source + +### utc2local + +> utc2local (dt:datetime.datetime) + +*Convert `dt` from UTC to local time* + +``` python +dt = datetime(2000,1,1,12) +print(f'{dt} UTC is {utc2local(dt)} local time') +``` + + 2000-01-01 12:00:00 UTC is 2000-01-01 22:00:00+10:00 local time + +------------------------------------------------------------------------ + +source + +### local2utc + +> local2utc (dt:datetime.datetime) + +*Convert `dt` from local to UTC time* + +``` python +print(f'{dt} local is {local2utc(dt)} UTC time') +``` + + 2000-01-01 12:00:00 local is 2000-01-01 02:00:00+00:00 UTC time + +------------------------------------------------------------------------ + +source + +### trace + +> trace (f) + +*Add `set_trace` to an existing function `f`* + +You can add a breakpoint to an existing function, e.g: + +``` python +Path.cwd = trace(Path.cwd) +Path.cwd() +``` + +Now, when the function is called it will drop you into the debugger. +Note, you must issue the `s` command when you begin to step into the +function that is being traced. + +------------------------------------------------------------------------ + +source + +### modified_env + +> modified_env (*delete, **replace) + +*Context manager temporarily modifying `os.environ` by deleting `delete` +and replacing `replace`* + +``` python +# USER isn't in Cloud Linux Environments +env_test = 'USERNAME' if sys.platform == "win32" else 'SHELL' +oldusr = os.environ[env_test] + +replace_param = {env_test: 'a'} +with modified_env('PATH', **replace_param): + test_eq(os.environ[env_test], 'a') + assert 'PATH' not in os.environ + +assert 'PATH' in os.environ +test_eq(os.environ[env_test], oldusr) +``` + +------------------------------------------------------------------------ + +source + +#### ContextManagers + +> ContextManagers (mgrs) + +*Wrapper for `contextlib.ExitStack` which enters a collection of context +managers* + +------------------------------------------------------------------------ + +source + +### shufflish + +> shufflish (x, pct=0.04) + +*Randomly relocate items of `x` up to `pct` of `len(x)` from their +starting location* + +------------------------------------------------------------------------ + +source + +### console_help + +> console_help (libname:str) + +*Show help for all console scripts from `libname`* + + + + + + + + + + + + + + + + +
TypeDetails
libnamestrname of library for console script listing
+ +------------------------------------------------------------------------ + +source + +### hl_md + +> hl_md (s, lang='xml', show=True) + +*Syntax highlight `s` using `lang`.* + +When we display code in a notebook, it’s nice to highlight it, so we +create a function to simplify that: + +``` python +hl_md('a child') +``` + +``` xml +a child +``` + +------------------------------------------------------------------------ + +source + +### type2str + +> type2str (typ:type) + +*Stringify `typ`* + +``` python +test_eq(type2str(Optional[float]), 'Union[float, None]') +``` + +------------------------------------------------------------------------ + +source + +### dataclass_src + +> dataclass_src (cls) + +``` python +DC = make_dataclass('DC', [('x', int), ('y', Optional[float], None), ('z', float, None)]) +print(dataclass_src(DC)) +``` + + @dataclass + class DC: + x: int + y: Union[float, None] = None + z: float = None + +------------------------------------------------------------------------ + +source + +### Unset + +> Unset (value, names=None, module=None, qualname=None, type=None, start=1) + +*An enumeration.* + +------------------------------------------------------------------------ + +source + +### nullable_dc + +> nullable_dc (cls) + +*Like `dataclass`, but default of `UNSET` added to fields without +defaults* + +``` python +@nullable_dc +class Person: name: str; age: int; city: str = "Unknown" +Person(name="Bob") +``` + + Person(name='Bob', age=UNSET, city='Unknown') + +------------------------------------------------------------------------ + +source + +### make_nullable + +> make_nullable (clas) + +``` python +@dataclass +class Person: name: str; age: int; city: str = "Unknown" + +make_nullable(Person) +Person("Bob", city='NY') +``` + + Person(name='Bob', age=UNSET, city='NY') + +``` python +Person(name="Bob") +``` + + Person(name='Bob', age=UNSET, city='Unknown') + +``` python +Person("Bob", 34) +``` + + Person(name='Bob', age=34, city='Unknown') + +------------------------------------------------------------------------ + +source + +### flexiclass + +> flexiclass (cls) + +*Convert `cls` into a `dataclass` like +[`make_nullable`](https://fastcore.fast.ai/xtras.html#make_nullable). +Converts in place and also returns the result.* + + + + + + + + + + + + + + + + + + + + + +
TypeDetails
clsThe class to convert
Returnsdataclass
+ +This can be used as a decorator… + +``` python +@flexiclass +class Person: name: str; age: int; city: str = "Unknown" + +bob = Person(name="Bob") +bob +``` + + Person(name='Bob', age=UNSET, city='Unknown') + +…or can update the behavior of an existing class (or dataclass): + +``` python +class Person: name: str; age: int; city: str = "Unknown" + +flexiclass(Person) +bob = Person(name="Bob") +bob +``` + + Person(name='Bob', age=UNSET, city='Unknown') + +Action occurs in-place: + +``` python +class Person: name: str; age: int; city: str = "Unknown" + +flexiclass(Person) +is_dataclass(Person) +``` + + True + +------------------------------------------------------------------------ + +source + +### asdict + +> asdict (o) + +*Convert `o` to a `dict`, supporting dataclasses, namedtuples, +iterables, and `__dict__` attrs.* + +Any `UNSET` values are not included. + +``` python +asdict(bob) +``` + + {'name': 'Bob', 'city': 'Unknown'} + +To customise dict conversion behavior for a class, implement the +`_asdict` method (this is used in the Python stdlib for named tuples). + +------------------------------------------------------------------------ + +source + +### is_typeddict + +> is_typeddict (cls:type) + +*Check if `cls` is a `TypedDict`* + +``` python +class MyDict(TypedDict): name:str + +assert is_typeddict(MyDict) +assert not is_typeddict({'a':1}) +``` + +------------------------------------------------------------------------ + +source + +### is_namedtuple + +> is_namedtuple (cls) + +*`True` if `cls` is a namedtuple type* + +``` python +assert is_namedtuple(namedtuple('tst', ['a'])) +assert not is_namedtuple(tuple) +``` + +------------------------------------------------------------------------ + +source + +### flexicache + +> flexicache (*funcs, maxsize=128) + +*Like `lru_cache`, but customisable with policy `funcs`* + +This is a flexible lru cache function that you can pass a list of +functions to. Those functions define the cache eviction policy. For +instance, +[`time_policy`](https://fastcore.fast.ai/xtras.html#time_policy) is +provided for time-based cache eviction, and +[`mtime_policy`](https://fastcore.fast.ai/xtras.html#mtime_policy) +evicts based on a file’s modified-time changing. The policy functions +are passed the last value that function returned was (initially `None`), +and return a new value to indicate the cache has expired. When the cache +expires, all functions are called with `None` to force getting new +values. + +------------------------------------------------------------------------ + +source + +### time_policy + +> time_policy (seconds) + +*A [`flexicache`](https://fastcore.fast.ai/xtras.html#flexicache) policy +that expires cached items after `seconds` have passed* + +------------------------------------------------------------------------ + +source + +### mtime_policy + +> mtime_policy (filepath) + +*A [`flexicache`](https://fastcore.fast.ai/xtras.html#flexicache) policy +that expires cached items after `filepath` modified-time changes* + +``` python +@flexicache(time_policy(10), mtime_policy('000_tour.ipynb')) +def cached_func(x, y): return x+y + +cached_func(1,2) +``` + + 3 + +``` python +@flexicache(time_policy(10), mtime_policy('000_tour.ipynb')) +async def cached_func(x, y): return x+y + +await cached_func(1,2) +await cached_func(1,2) +``` + + 3 + +------------------------------------------------------------------------ + +source + +### timed_cache + +> timed_cache (seconds=60, maxsize=128) + +*Like `lru_cache`, but also with time-based eviction* + +This function is a small convenience wrapper for using +[`flexicache`](https://fastcore.fast.ai/xtras.html#flexicache) with +[`time_policy`](https://fastcore.fast.ai/xtras.html#time_policy). + +``` python +@timed_cache(seconds=0.05, maxsize=2) +def cached_func(x): return x * 2, time() + +# basic caching +result1, time1 = cached_func(2) +test_eq(result1, 4) +sleep(0.001) +result2, time2 = cached_func(2) +test_eq(result2, 4) +test_eq(time1, time2) + +# caching different values +result3, _ = cached_func(3) +test_eq(result3, 6) + +# maxsize +_, time4 = cached_func(4) +_, time2_new = cached_func(2) +test_close(time2, time2_new, eps=0.1) +_, time3_new = cached_func(3) +test_ne(time3_new, time()) + +# time expiration +sleep(0.05) +_, time4_new = cached_func(4) +test_ne(time4_new, time()) +```