mirror of
https://github.com/hypervortex/VH-Bombsquad-Modded-Server-Files
synced 2025-11-07 17:36:08 +00:00
Delete dist directory
This commit is contained in:
parent
b2bfb6afd8
commit
9e59089810
1776 changed files with 0 additions and 548278 deletions
602
dist/ba_data/python/efro/entity/_field.py
vendored
602
dist/ba_data/python/efro/entity/_field.py
vendored
|
|
@ -1,602 +0,0 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Field types for the entity system."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import logging
|
||||
from enum import Enum
|
||||
from typing import TYPE_CHECKING, Generic, TypeVar, overload
|
||||
|
||||
# from efro.util import enum_by_value
|
||||
from efro.entity._base import BaseField, dict_key_to_raw, dict_key_from_raw
|
||||
from efro.entity._support import (BoundCompoundValue, BoundListField,
|
||||
BoundDictField, BoundCompoundListField,
|
||||
BoundCompoundDictField)
|
||||
from efro.entity.util import have_matching_fields
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Dict, Type, List, Any
|
||||
from efro.entity._value import TypedValue, CompoundValue
|
||||
|
||||
T = TypeVar('T')
|
||||
TK = TypeVar('TK')
|
||||
TC = TypeVar('TC', bound='CompoundValue')
|
||||
|
||||
|
||||
class Field(BaseField, Generic[T]):
|
||||
"""Field consisting of a single value."""
|
||||
|
||||
def __init__(self,
|
||||
d_key: str,
|
||||
value: TypedValue[T],
|
||||
store_default: bool = True) -> None:
|
||||
super().__init__(d_key)
|
||||
self.d_value = value
|
||||
self._store_default = store_default
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f'<Field "{self.d_key}" with {self.d_value}>'
|
||||
|
||||
def get_default_data(self) -> Any:
|
||||
return self.d_value.get_default_data()
|
||||
|
||||
def filter_input(self, data: Any, error: bool) -> Any:
|
||||
return self.d_value.filter_input(data, error)
|
||||
|
||||
def filter_output(self, data: Any) -> Any:
|
||||
return self.d_value.filter_output(data)
|
||||
|
||||
def prune_data(self, data: Any) -> bool:
|
||||
return self.d_value.prune_data(data)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# Use default runtime get/set but let type-checker know our types.
|
||||
# Note: we actually return a bound-field when accessed on
|
||||
# a type instead of an instance, but we don't reflect that here yet
|
||||
# (would need to write a mypy plugin so sub-field access works first)
|
||||
|
||||
@overload
|
||||
def __get__(self, obj: None, cls: Any = None) -> Field[T]:
|
||||
...
|
||||
|
||||
@overload
|
||||
def __get__(self, obj: Any, cls: Any = None) -> T:
|
||||
...
|
||||
|
||||
def __get__(self, obj: Any, cls: Any = None) -> Any:
|
||||
...
|
||||
|
||||
def __set__(self, obj: Any, value: T) -> None:
|
||||
...
|
||||
|
||||
|
||||
class CompoundField(BaseField, Generic[TC]):
|
||||
"""Field consisting of a single compound value."""
|
||||
|
||||
def __init__(self,
|
||||
d_key: str,
|
||||
value: TC,
|
||||
store_default: bool = True) -> None:
|
||||
super().__init__(d_key)
|
||||
if __debug__:
|
||||
from efro.entity._value import CompoundValue
|
||||
assert isinstance(value, CompoundValue)
|
||||
assert not hasattr(value, 'd_data')
|
||||
self.d_value = value
|
||||
self._store_default = store_default
|
||||
|
||||
def get_default_data(self) -> dict:
|
||||
return self.d_value.get_default_data()
|
||||
|
||||
def filter_input(self, data: Any, error: bool) -> dict:
|
||||
return self.d_value.filter_input(data, error)
|
||||
|
||||
def prune_data(self, data: Any) -> bool:
|
||||
return self.d_value.prune_data(data)
|
||||
|
||||
# Note:
|
||||
# Currently, to the type-checker we just return a simple instance
|
||||
# of our CompoundValue so it can properly type-check access to its
|
||||
# attrs. However at runtime we return a FieldInspector or
|
||||
# BoundCompoundField which both use magic to provide the same attrs
|
||||
# dynamically (but which the type-checker doesn't understand).
|
||||
# Perhaps at some point we can write a mypy plugin to correct this.
|
||||
if TYPE_CHECKING:
|
||||
|
||||
def __get__(self, obj: Any, cls: Any = None) -> TC:
|
||||
...
|
||||
|
||||
# Theoretically this type-checking may be too tight;
|
||||
# we can support assigning a parent class to a child class if
|
||||
# their fields match. Not sure if that'll ever come up though;
|
||||
# gonna leave this for now as I prefer to have *some* checking.
|
||||
# Also once we get BoundCompoundValues working with mypy we'll
|
||||
# need to accept those too.
|
||||
def __set__(self: CompoundField[TC], obj: Any, value: TC) -> None:
|
||||
...
|
||||
|
||||
def get_with_data(self, data: Any) -> Any:
|
||||
assert self.d_key in data
|
||||
return BoundCompoundValue(self.d_value, data[self.d_key])
|
||||
|
||||
def set_with_data(self, data: Any, value: Any, error: bool) -> Any:
|
||||
from efro.entity._value import CompoundValue
|
||||
|
||||
# Ok here's the deal: our type checking above allows any subtype
|
||||
# of our CompoundValue in here, but we want to be more picky than
|
||||
# that. Let's check fields for equality. This way we'll allow
|
||||
# assigning something like a Carentity to a Car field
|
||||
# (where the data is the same), but won't allow assigning a Car
|
||||
# to a Vehicle field (as Car probably adds more fields).
|
||||
value1: CompoundValue
|
||||
if isinstance(value, BoundCompoundValue):
|
||||
value1 = value.d_value
|
||||
elif isinstance(value, CompoundValue):
|
||||
value1 = value
|
||||
else:
|
||||
raise ValueError(f"Can't assign from object type {type(value)}")
|
||||
dataval = getattr(value, 'd_data', None)
|
||||
if dataval is None:
|
||||
raise ValueError(f"Can't assign from unbound object {value}")
|
||||
if self.d_value.get_fields() != value1.get_fields():
|
||||
raise ValueError(f"Can't assign to {self.d_value} from"
|
||||
f' incompatible type {value.d_value}; '
|
||||
f'sub-fields do not match.')
|
||||
|
||||
# If we're allowing this to go through, we can simply copy the
|
||||
# data from the passed in value. The fields match so it should
|
||||
# be in a valid state already.
|
||||
data[self.d_key] = copy.deepcopy(dataval)
|
||||
|
||||
|
||||
class ListField(BaseField, Generic[T]):
|
||||
"""Field consisting of repeated values."""
|
||||
|
||||
def __init__(self,
|
||||
d_key: str,
|
||||
value: TypedValue[T],
|
||||
store_default: bool = True) -> None:
|
||||
super().__init__(d_key)
|
||||
self.d_value = value
|
||||
self._store_default = store_default
|
||||
|
||||
def get_default_data(self) -> list:
|
||||
return []
|
||||
|
||||
def filter_input(self, data: Any, error: bool) -> Any:
|
||||
|
||||
# If we were passed a BoundListField, operate on its raw values
|
||||
if isinstance(data, BoundListField):
|
||||
data = data.d_data
|
||||
|
||||
if not isinstance(data, list):
|
||||
if error:
|
||||
raise TypeError(f'list value expected; got {type(data)}')
|
||||
logging.error('Ignoring non-list data for %s: %s', self, data)
|
||||
data = []
|
||||
for i, entry in enumerate(data):
|
||||
data[i] = self.d_value.filter_input(entry, error=error)
|
||||
return data
|
||||
|
||||
def prune_data(self, data: Any) -> bool:
|
||||
# We never prune individual values since that would fundamentally
|
||||
# change the list, but we can prune completely if empty (and allowed).
|
||||
return not data and not self._store_default
|
||||
|
||||
# When accessed on a FieldInspector we return a sub-field FieldInspector.
|
||||
# When accessed on an instance we return a BoundListField.
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
# Access via type gives our field; via an instance gives a bound field.
|
||||
@overload
|
||||
def __get__(self, obj: None, cls: Any = None) -> ListField[T]:
|
||||
...
|
||||
|
||||
@overload
|
||||
def __get__(self, obj: Any, cls: Any = None) -> BoundListField[T]:
|
||||
...
|
||||
|
||||
def __get__(self, obj: Any, cls: Any = None) -> Any:
|
||||
...
|
||||
|
||||
# Allow setting via a raw value list or a bound list field
|
||||
@overload
|
||||
def __set__(self, obj: Any, value: List[T]) -> None:
|
||||
...
|
||||
|
||||
@overload
|
||||
def __set__(self, obj: Any, value: BoundListField[T]) -> None:
|
||||
...
|
||||
|
||||
def __set__(self, obj: Any, value: Any) -> None:
|
||||
...
|
||||
|
||||
def get_with_data(self, data: Any) -> Any:
|
||||
return BoundListField(self, data[self.d_key])
|
||||
|
||||
|
||||
class DictField(BaseField, Generic[TK, T]):
|
||||
"""A field of values in a dict with a specified index type."""
|
||||
|
||||
def __init__(self,
|
||||
d_key: str,
|
||||
keytype: Type[TK],
|
||||
field: TypedValue[T],
|
||||
store_default: bool = True) -> None:
|
||||
super().__init__(d_key)
|
||||
self.d_value = field
|
||||
self._store_default = store_default
|
||||
self._keytype = keytype
|
||||
|
||||
def get_default_data(self) -> dict:
|
||||
return {}
|
||||
|
||||
def filter_input(self, data: Any, error: bool) -> Any:
|
||||
|
||||
# If we were passed a BoundDictField, operate on its raw values
|
||||
if isinstance(data, BoundDictField):
|
||||
data = data.d_data
|
||||
|
||||
if not isinstance(data, dict):
|
||||
if error:
|
||||
raise TypeError('dict value expected')
|
||||
logging.error('Ignoring non-dict data for %s: %s', self, data)
|
||||
data = {}
|
||||
data_out = {}
|
||||
for key, val in data.items():
|
||||
|
||||
# For enum keys, make sure its a valid enum.
|
||||
if issubclass(self._keytype, Enum):
|
||||
# Our input data can either be an enum or the underlying type.
|
||||
if isinstance(key, self._keytype):
|
||||
key = dict_key_to_raw(key, self._keytype)
|
||||
# key = key.value
|
||||
else:
|
||||
try:
|
||||
_enumval = dict_key_from_raw(key, self._keytype)
|
||||
# _enumval = enum_by_value(self._keytype, key)
|
||||
except Exception as exc:
|
||||
if error:
|
||||
raise ValueError(
|
||||
f'No enum of type {self._keytype}'
|
||||
f' exists with value {key}') from exc
|
||||
logging.error('Ignoring invalid key type for %s: %s',
|
||||
self, data)
|
||||
continue
|
||||
|
||||
# For all other keys we can check for exact types.
|
||||
elif not isinstance(key, self._keytype):
|
||||
if error:
|
||||
raise TypeError(
|
||||
f'Invalid key type; expected {self._keytype},'
|
||||
f' got {type(key)}.')
|
||||
logging.error('Ignoring invalid key type for %s: %s', self,
|
||||
data)
|
||||
continue
|
||||
|
||||
data_out[key] = self.d_value.filter_input(val, error=error)
|
||||
return data_out
|
||||
|
||||
def prune_data(self, data: Any) -> bool:
|
||||
# We never prune individual values since that would fundamentally
|
||||
# change the dict, but we can prune completely if empty (and allowed)
|
||||
return not data and not self._store_default
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
# Return our field if accessed via type and bound-dict-field
|
||||
# if via instance.
|
||||
@overload
|
||||
def __get__(self, obj: None, cls: Any = None) -> DictField[TK, T]:
|
||||
...
|
||||
|
||||
@overload
|
||||
def __get__(self, obj: Any, cls: Any = None) -> BoundDictField[TK, T]:
|
||||
...
|
||||
|
||||
def __get__(self, obj: Any, cls: Any = None) -> Any:
|
||||
...
|
||||
|
||||
# Allow setting via matching dict values or BoundDictFields
|
||||
@overload
|
||||
def __set__(self, obj: Any, value: Dict[TK, T]) -> None:
|
||||
...
|
||||
|
||||
@overload
|
||||
def __set__(self, obj: Any, value: BoundDictField[TK, T]) -> None:
|
||||
...
|
||||
|
||||
def __set__(self, obj: Any, value: Any) -> None:
|
||||
...
|
||||
|
||||
def get_with_data(self, data: Any) -> Any:
|
||||
return BoundDictField(self._keytype, self, data[self.d_key])
|
||||
|
||||
|
||||
class CompoundListField(BaseField, Generic[TC]):
|
||||
"""A field consisting of repeated instances of a compound-value.
|
||||
|
||||
Element access returns the sub-field, allowing nested field access.
|
||||
ie: mylist[10].fieldattr = 'foo'
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
d_key: str,
|
||||
valuetype: TC,
|
||||
store_default: bool = True) -> None:
|
||||
super().__init__(d_key)
|
||||
self.d_value = valuetype
|
||||
|
||||
# This doesnt actually exist for us, but want the type-checker
|
||||
# to think it does (see TYPE_CHECKING note below).
|
||||
self.d_data: Any
|
||||
self._store_default = store_default
|
||||
|
||||
def filter_input(self, data: Any, error: bool) -> list:
|
||||
|
||||
if not isinstance(data, list):
|
||||
if error:
|
||||
raise TypeError('list value expected')
|
||||
logging.error('Ignoring non-list data for %s: %s', self, data)
|
||||
data = []
|
||||
assert isinstance(data, list)
|
||||
|
||||
# Ok we've got a list; now run everything in it through validation.
|
||||
for i, subdata in enumerate(data):
|
||||
data[i] = self.d_value.filter_input(subdata, error=error)
|
||||
return data
|
||||
|
||||
def get_default_data(self) -> list:
|
||||
return []
|
||||
|
||||
def prune_data(self, data: Any) -> bool:
|
||||
# Run pruning on all individual entries' data through out child field.
|
||||
# However we don't *completely* prune values from the list since that
|
||||
# would change it.
|
||||
for subdata in data:
|
||||
self.d_value.prune_fields_data(subdata)
|
||||
|
||||
# We can also optionally prune the whole list if empty and allowed.
|
||||
return not data and not self._store_default
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
@overload
|
||||
def __get__(self, obj: None, cls: Any = None) -> CompoundListField[TC]:
|
||||
...
|
||||
|
||||
@overload
|
||||
def __get__(self,
|
||||
obj: Any,
|
||||
cls: Any = None) -> BoundCompoundListField[TC]:
|
||||
...
|
||||
|
||||
def __get__(self, obj: Any, cls: Any = None) -> Any:
|
||||
...
|
||||
|
||||
# Note:
|
||||
# When setting the list, we tell the type-checker that we also accept
|
||||
# a raw list of CompoundValue objects, but at runtime we actually
|
||||
# always deal with BoundCompoundValue objects (see note in
|
||||
# BoundCompoundListField for why we accept CompoundValue objs)
|
||||
@overload
|
||||
def __set__(self, obj: Any, value: List[TC]) -> None:
|
||||
...
|
||||
|
||||
@overload
|
||||
def __set__(self, obj: Any, value: BoundCompoundListField[TC]) -> None:
|
||||
...
|
||||
|
||||
def __set__(self, obj: Any, value: Any) -> None:
|
||||
...
|
||||
|
||||
def get_with_data(self, data: Any) -> Any:
|
||||
assert self.d_key in data
|
||||
return BoundCompoundListField(self, data[self.d_key])
|
||||
|
||||
def set_with_data(self, data: Any, value: Any, error: bool) -> Any:
|
||||
|
||||
# If we were passed a BoundCompoundListField,
|
||||
# simply convert it to a flat list of BoundCompoundValue objects which
|
||||
# is what we work with natively here.
|
||||
if isinstance(value, BoundCompoundListField):
|
||||
value = list(value)
|
||||
|
||||
if not isinstance(value, list):
|
||||
raise TypeError(f'CompoundListField expected list value on set;'
|
||||
f' got {type(value)}.')
|
||||
|
||||
# Allow assigning only from a sequence of our existing children.
|
||||
# (could look into expanding this to other children if we can
|
||||
# be sure the underlying data will line up; for example two
|
||||
# CompoundListFields with different child_field values should not
|
||||
# be inter-assignable.
|
||||
if not all(isinstance(i, BoundCompoundValue) for i in value):
|
||||
raise ValueError('CompoundListField assignment must be a '
|
||||
'list containing only BoundCompoundValue objs.')
|
||||
|
||||
# Make sure the data all has the same CompoundValue type and
|
||||
# compare that type against ours once to make sure its fields match.
|
||||
# (this will not allow passing CompoundValues from multiple sources
|
||||
# but I don't know if that would ever come up..)
|
||||
for i, val in enumerate(value):
|
||||
if i == 0:
|
||||
# Do the full field comparison on the first value only..
|
||||
if not have_matching_fields(val.d_value, self.d_value):
|
||||
raise ValueError(
|
||||
'CompoundListField assignment must be a '
|
||||
'list containing matching CompoundValues.')
|
||||
else:
|
||||
# For all remaining values, just ensure they match the first.
|
||||
if val.d_value is not value[0].d_value:
|
||||
raise ValueError(
|
||||
'CompoundListField assignment cannot contain '
|
||||
'multiple CompoundValue types as sources.')
|
||||
|
||||
data[self.d_key] = self.filter_input([i.d_data for i in value],
|
||||
error=error)
|
||||
|
||||
|
||||
class CompoundDictField(BaseField, Generic[TK, TC]):
|
||||
"""A field consisting of key-indexed instances of a compound-value.
|
||||
|
||||
Element access returns the sub-field, allowing nested field access.
|
||||
ie: mylist[10].fieldattr = 'foo'
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
d_key: str,
|
||||
keytype: Type[TK],
|
||||
valuetype: TC,
|
||||
store_default: bool = True) -> None:
|
||||
super().__init__(d_key)
|
||||
self.d_value = valuetype
|
||||
|
||||
# This doesnt actually exist for us, but want the type-checker
|
||||
# to think it does (see TYPE_CHECKING note below).
|
||||
self.d_data: Any
|
||||
|
||||
self.d_keytype = keytype
|
||||
self._store_default = store_default
|
||||
|
||||
def filter_input(self, data: Any, error: bool) -> dict:
|
||||
if not isinstance(data, dict):
|
||||
if error:
|
||||
raise TypeError('dict value expected')
|
||||
logging.error('Ignoring non-dict data for %s: %s', self, data)
|
||||
data = {}
|
||||
data_out = {}
|
||||
for key, val in data.items():
|
||||
|
||||
# For enum keys, make sure its a valid enum.
|
||||
if issubclass(self.d_keytype, Enum):
|
||||
# Our input data can either be an enum or the underlying type.
|
||||
if isinstance(key, self.d_keytype):
|
||||
key = dict_key_to_raw(key, self.d_keytype)
|
||||
# key = key.value
|
||||
else:
|
||||
try:
|
||||
_enumval = dict_key_from_raw(key, self.d_keytype)
|
||||
# _enumval = enum_by_value(self.d_keytype, key)
|
||||
except Exception as exc:
|
||||
if error:
|
||||
raise ValueError(
|
||||
f'No enum of type {self.d_keytype}'
|
||||
f' exists with value {key}') from exc
|
||||
logging.error('Ignoring invalid key type for %s: %s',
|
||||
self, data)
|
||||
continue
|
||||
|
||||
# For all other keys we can check for exact types.
|
||||
elif not isinstance(key, self.d_keytype):
|
||||
if error:
|
||||
raise TypeError(
|
||||
f'Invalid key type; expected {self.d_keytype},'
|
||||
f' got {type(key)}.')
|
||||
logging.error('Ignoring invalid key type for %s: %s', self,
|
||||
data)
|
||||
continue
|
||||
|
||||
data_out[key] = self.d_value.filter_input(val, error=error)
|
||||
return data_out
|
||||
|
||||
def get_default_data(self) -> dict:
|
||||
return {}
|
||||
|
||||
def prune_data(self, data: Any) -> bool:
|
||||
# Run pruning on all individual entries' data through our child field.
|
||||
# However we don't *completely* prune values from the list since that
|
||||
# would change it.
|
||||
for subdata in data.values():
|
||||
self.d_value.prune_fields_data(subdata)
|
||||
|
||||
# We can also optionally prune the whole list if empty and allowed.
|
||||
return not data and not self._store_default
|
||||
|
||||
# ONLY overriding these in type-checker land to clarify types.
|
||||
# (see note in BaseField)
|
||||
if TYPE_CHECKING:
|
||||
|
||||
@overload
|
||||
def __get__(self,
|
||||
obj: None,
|
||||
cls: Any = None) -> CompoundDictField[TK, TC]:
|
||||
...
|
||||
|
||||
@overload
|
||||
def __get__(self,
|
||||
obj: Any,
|
||||
cls: Any = None) -> BoundCompoundDictField[TK, TC]:
|
||||
...
|
||||
|
||||
def __get__(self, obj: Any, cls: Any = None) -> Any:
|
||||
...
|
||||
|
||||
# Note:
|
||||
# When setting the dict, we tell the type-checker that we also accept
|
||||
# a raw dict of CompoundValue objects, but at runtime we actually
|
||||
# always deal with BoundCompoundValue objects (see note in
|
||||
# BoundCompoundDictField for why we accept CompoundValue objs)
|
||||
@overload
|
||||
def __set__(self, obj: Any, value: Dict[TK, TC]) -> None:
|
||||
...
|
||||
|
||||
@overload
|
||||
def __set__(self, obj: Any, value: BoundCompoundDictField[TK,
|
||||
TC]) -> None:
|
||||
...
|
||||
|
||||
def __set__(self, obj: Any, value: Any) -> None:
|
||||
...
|
||||
|
||||
def get_with_data(self, data: Any) -> Any:
|
||||
assert self.d_key in data
|
||||
return BoundCompoundDictField(self, data[self.d_key])
|
||||
|
||||
def set_with_data(self, data: Any, value: Any, error: bool) -> Any:
|
||||
|
||||
# If we were passed a BoundCompoundDictField,
|
||||
# simply convert it to a flat dict of BoundCompoundValue objects which
|
||||
# is what we work with natively here.
|
||||
if isinstance(value, BoundCompoundDictField):
|
||||
value = dict(value.items())
|
||||
|
||||
if not isinstance(value, dict):
|
||||
raise TypeError('CompoundDictField expected dict value on set.')
|
||||
|
||||
# Allow assigning only from a sequence of our existing children.
|
||||
# (could look into expanding this to other children if we can
|
||||
# be sure the underlying data will line up; for example two
|
||||
# CompoundListFields with different child_field values should not
|
||||
# be inter-assignable.
|
||||
if (not all(isinstance(i, BoundCompoundValue)
|
||||
for i in value.values())):
|
||||
raise ValueError('CompoundDictField assignment must be a '
|
||||
'dict containing only BoundCompoundValues.')
|
||||
|
||||
# Make sure the data all has the same CompoundValue type and
|
||||
# compare that type against ours once to make sure its fields match.
|
||||
# (this will not allow passing CompoundValues from multiple sources
|
||||
# but I don't know if that would ever come up..)
|
||||
first_value: Any = None
|
||||
for i, val in enumerate(value.values()):
|
||||
if i == 0:
|
||||
first_value = val.d_value
|
||||
# Do the full field comparison on the first value only..
|
||||
if not have_matching_fields(val.d_value, self.d_value):
|
||||
raise ValueError(
|
||||
'CompoundListField assignment must be a '
|
||||
'list containing matching CompoundValues.')
|
||||
else:
|
||||
# For all remaining values, just ensure they match the first.
|
||||
if val.d_value is not first_value:
|
||||
raise ValueError(
|
||||
'CompoundListField assignment cannot contain '
|
||||
'multiple CompoundValue types as sources.')
|
||||
|
||||
data[self.d_key] = self.filter_input(
|
||||
{key: val.d_data
|
||||
for key, val in value.items()}, error=error)
|
||||
Loading…
Add table
Add a link
Reference in a new issue