(function () {
const spinner = document.getElementById("loadingSpinner");
const getBgImage = (pseudo) => getComputedStyle(spinner, pseudo).backgroundImage;
for (const pseudo of [undefined, "::after", "::before"]) {
if (getBgImage(pseudo) !== "none") {
const svg = spinner.querySelector("svg")
svg && svg.style.setProperty("display", "none");
return;
}
}
}());
window.anvilCDNOrigin = "https://anvil.works/runtime-new";
window.anvilAppOrigin = "https://jumbo-outdoor-game.anvil.app";
window.anvilRuntimeCommonUrl = "https://anvil.works/apps"
window.anvilEnvironmentOrigin = "https://jumbo-outdoor-game.anvil.app";
window.anvilSessionToken = "QDSUFTDUSTHU6V2C4T5JXIAFCO72LLSW=HHB9FwP1XpllqxQHf0hZ68YVf18h";
window.anvilVersion = "$(git rev-parse HEAD)";
window.anvilAppInfo = {"id":"ZG2QFGYIJH2EUQNX","branch":"master","environment":{"description":"Published","tags":["default_published"]}};
window.anvilOAuthInfo = "Guqff+EL6Mu5Oo++TMVX24tEYtWlOsG53krfAejOMJckepVuCm+TuUiNrS7JpIPQKiu6+1CKy5dAsFb9J2OWwtewtBohriSyuX4VIA2qwWoBAGyUvkZBabF7xh/GuccGLZjDazEdi1VaBpdFK2+3bVxF3DJZezQl8qB5LmTNUMKRDAvqThCqSwb1SJwKWe1z0+ZEIaFgu2TpuRN7EfUzDRoTJUfeSu3Edw/1EmNSqu5GA3O8Ssfzow7Uryw63GIWEhRXUr7dCUV5oR+myPah5AuOVW+uSJWjQtBZ9Ct2eWF8pcIcZzUVUmA0Hloks4BtuNubJyuBqXyH8z00N6hG5hA+UunV/0mB6O+5KxcNQpjWA5vCPO3I+xI/bfpb6E63Jg==";
window.anvilGoogleApiKey = "AIzaSyCn8yc8dmMNcmAn-e_K5HT7NX19csXUGUA";
{
// remove _anvil_session to prevent referrer links from including the session
const url = new URL(window.location.href);
url.searchParams.delete("_anvil_session");
window.history.replaceState(window.history.state || {}, "Anvil App", url);
}
//
$(function() {Sk.builtinFiles.files["anvil-services\/tables\/__init__.py"] = "from anvil.tables import *\nfrom anvil.tables import _page_size\n";Sk.builtinFiles.files["anvil-services\/anvil\/tables\/__init__.py"] = "import time\n\nimport anvil.server\n\nfrom . import _config\nfrom ._base_classes import Row, SearchIterator, Table\nfrom ._errors import (\n NoSuchColumnError,\n QuotaExceededError,\n RowDeleted,\n TableError,\n TransactionConflict,\n)\nfrom ._helpers import _hash_wrapper\n\n\n# Use old app tables by default\nclass AppTables(object):\n cache = None\n\n def __getattr__(self, name):\n if AppTables.cache is None:\n AppTables.cache = anvil.server.call(\"anvil.private.tables.get_app_tables\")\n\n tbl = AppTables.cache.get(name)\n if tbl is not None:\n return tbl\n\n raise AttributeError(\"No such app table: '%s'\" % name)\n\n def __setattr__(self, name, val):\n raise Exception(\"app_tables is read-only\")\n\n def __iter__(self):\n return AppTableIterator()\n\n\nclass AppTableIterator:\n def __init__(self):\n self._it = None\n\n def __iter__(self):\n return self\n\n def __next__(self):\n # because __iter__ can't suspend\n if AppTables.cache is None:\n AppTables.cache = anvil.server.call(\"anvil.private.tables.get_app_tables\")\n if self._it is None:\n self._it = AppTables.cache.keys().__iter__()\n return next(self._it)\n\n next = __next__\n\n\n_set_class = object.__dict__[\"__class__\"].__set__\n\n\ndef _lazy_replace_class(self):\n if _config.get_client_config().get(\"enable_v2\"):\n from . import v2\n\n v2._app_tables._clear_cache()\n _set_class(self, type(v2.app_tables))\n else:\n AppTables.cache = None\n _set_class(self, AppTables)\n\n\ndef _wrap_dunder(method):\n def wrapped(self, *args, **kws):\n _lazy_replace_class(self)\n return getattr(self, method)(*args, **kws)\n\n return wrapped\n\n\nclass _LazyAppTables(object):\n def __getattribute__(self, name):\n if name == \"__module__\":\n return \"anvil.tables\"\n\n _lazy_replace_class(self)\n return getattr(self, name)\n\n __setattr__ = _wrap_dunder(\"__setattr__\")\n __getitem__ = _wrap_dunder(\"__getitem__\")\n __dir__ = _wrap_dunder(\"__dir__\")\n __iter__ = AppTables.__iter__\n\n\n_ThreadLocal = object\n\nif anvil.is_server_side():\n try:\n from anvil._threaded_server import ThreadLocal as _ThreadLocal\n except ImportError:\n pass\n\n\nclass _LazyContext(_ThreadLocal):\n def __enter__(self):\n batchers = {\"batch_update\": batch_update, \"batch_delete\": batch_delete, \"batch\": batch}\n if not _config.get_client_config().get(\"enable_v2\"):\n for obj in batchers.values():\n obj.__class__ = type(None)\n return self.__enter__()\n\n from .v2 import _batcher as _b\n\n for orig, obj in batchers.items():\n obj.__class__ = type(getattr(_b, orig))\n obj.__init__()\n setattr(_b, orig, obj)\n return self.__enter__()\n\n def __exit__(self, *args):\n assert not isinstance(self, _LazyContext)\n return self.__exit__(*args)\n\n\ndef _clear_cache():\n _config.reset_config()\n _set_class(app_tables, _LazyAppTables)\n\n\nanvil.server._on_invalidate_client_objects(_clear_cache)\n\n\n#!defModuleAttr(anvil.tables)!1:\n# {\n# \tname: \"app_tables\",\n# \ttype: \"any\",\n# \tanvil$helpLink: \"\/docs\/data-tables\/data-tables-in-code\",\n# \t$doc: \"Access Table objects from the datatables services. You can access a Table object with dot notation e.g. `app_tables.my_table`. To access a table with strings use `getattr(app_tables, 'my_table')`. If no table is present an AttributeError will be thrown.\"\n# }\n#\napp_tables = _LazyAppTables()\nbatch_update = _LazyContext()\nbatch_delete = _LazyContext()\nbatch = _LazyContext()\n# Not very nice but these references exist in uplink code\n# before we have a chance to know if we're using the v1\/v2 config option\n# we can't call anvil.server until the uplink has made a connetion\n\n#!defFunction(anvil.tables,anvil.tables.Table,table_id)!2:\n# {\n# \t$doc: \"Get a table by id. Can pass a row id in, and the table the row belongs to will be returned.\",\n# anvil$helpLink: \"\/docs\/data-tables\/accelerated-tables\"\n# } [\"get_table_by_id\"]\ndef get_table_by_id(table_id):\n if _config.get_client_config().get(\"enable_v2\"):\n from .v2 import get_table_by_id\n\n return get_table_by_id(table_id)\n raise TableError(\"get_table_by_id is only available in Accelerated Tables beta\")\n\n\n#!defModuleAttr(anvil.tables)!1:\n# {\n# \tname: \"app_tables\",\n# \ttype: \"any\",\n# \tanvil$helpLink: \"\/docs\/data-tables\/data-tables-in-code\",\n# \t$doc: \"Access Table objects from the datatables services. You can access a Table object with dot notation e.g. `app_tables.my_table`. To access a table with strings use `getattr(app_tables, 'my_table')`. If no table is present an AttributeError will be thrown.\"\n# }\n#\n\n\nclass Transaction:\n def __init__(self, relaxed=False):\n self._aborting = False\n self._isolation = \"relaxed\" if relaxed else None\n\n #!defMethod(anvil.tables.Transaction instance)!2: \"Begin the transaction\" [\"__enter__\"]\n def __enter__(self):\n anvil.server.call(\n \"anvil.private.tables.open_transaction\", isolation=self._isolation\n )\n return self\n\n #!defMethod(_)!2: \"End the transaction\" [\"__exit__\"]\n def __exit__(self, e_type, e_val, tb):\n anvil.server.call(\n \"anvil.private.tables.close_transaction\",\n self._aborting or e_val is not None,\n )\n\n #!defMethod(_)!2: \"Abort this transaction. When it ends, all write operations performed during it will be cancelled\" [\"abort\"]\n def abort(self):\n self._aborting = True\n\n\n#!defClass(anvil.tables,%Transaction)!:\n\n\n#!defFunction(anvil.tables,%,function,server_function)!2:\n# {\n# \t$doc: \"When applied to a function (as a decorator), the whole function will run in a data tables transaction. If it conflicts with another transaction, it will retry up to five times.\",\n# anvil$helpLink: \"\/docs\/data-tables\/transactions\"\n# } [\"in_transaction\"]\ndef in_transaction(maybe_f=None, relaxed=None):\n # we don't want to import this on the client unnecessarily\n import functools\n\n def wrap(f):\n @functools.wraps(f)\n def new_f(*args, **kwargs):\n n = 0\n while True:\n try:\n with Transaction(relaxed=relaxed):\n return f(*args, **kwargs)\n except TransactionConflict:\n # lazy load random incase we make random.js a slow path on the client\n import random\n\n n += 1\n if n == 18:\n raise\n # print(f\"RETRYING TXN {n}\")\n # Max total sleep time is a little under 150 seconds (avg 75), so server calls will timeout before this finishes usually.\n sleep_amt = random.random() * (1.5**n) * 0.05\n try:\n time.sleep(sleep_amt)\n except:\n anvil.server.call(\"anvil.private._sleep\", sleep_amt)\n\n try:\n reregister = f._anvil_reregister\n except AttributeError:\n pass\n else:\n reregister(new_f)\n\n return new_f\n\n if maybe_f is None:\n return wrap\n else:\n return wrap(maybe_f)\n\n\n#!defFunction(anvil.tables,_,column_name,ascending=)!2: \"Sort the results of this table search by a particular column. Default to ascending order.\" [\"order_by\"]\n@anvil.server.portable_class\nclass order_by(object):\n def __init__(self, column_name, ascending=True):\n self.column_name = column_name\n self.ascending = ascending\n\n __hash__, __eq__ = _hash_wrapper(\"column_name\", \"ascending\")\n\n\n# backward compatability\nfrom .query import fetch_only\nfrom .query import page_size as _page_size\n\n\n#!defFunction(anvil.tables,%,[via_host=],[via_port=])!2: \"Get a Postgres connection string for accessing this app's Data Tables via SQL.\\n\\nThe returned string includes temporary login credentials and sets the search path to a schema representing this app's Data Table environment.\\n\\nYou can override the host and port for the database connection to connect via a secure tunnel.\\n\\n(Available on the Dedicated Plan only.)\" [\"get_connection_string\"]\ndef get_connection_string(via_host=None, via_port=None):\n return anvil.server.call(\n \"anvil.private.get_direct_postgres_connection_string\",\n via_host=via_host,\n via_port=via_port,\n )\n\n\n#!defMethod(table row, **column_values)!2: \"Add a row to the data table. Use keyword arguments to specify column values.\" [\"add_row\"]\n#!defMethod(client readable view)!2: \"Return a view on the table that can be read by client code. Use keyword arguments to specify view restrictions\" [\"client_readable\"]\n#!defMethod(client writable view)!2: \"Return a view on the table that can be written by client code. Use keyword arguments to specify view restrictions. This does not give the client write access to other tables referred to by the table.\" [\"client_writable\"]\n#!defMethod(client writable view)!2: \"Return a view on this table that can be written by client code. Use keyword arguments to specify view restrictions.\" [\"client_writable_cascade\"]\n#!defMethod(_)!2: \"Delete all the rows from the data table\" [\"delete_all_rows\"]\n#!defMethod(_)!2: \"Get a single matching row from the data table whose columns match the keyword arguments. Returns None if no matching row exists, and raises an exception if more than one row matches.\\n\\nEg: app_tables.table_1.get(name='John Smith')\" [\"get\"]\n#!defMethod(row,id)!2: \"Get the matching row from this data table, by its unique ID\" [\"get_by_id\"]\n#!defMethod(bool,row)!2: \"Returns true if the table (or view) contains the provided row.\" [\"has_row\"]\n#!defMethod(list of dicts)!2: \"Get the spec for the table as a list of dicts. Each dict contains the name and type of a column.\" [\"list_columns\"]\n#!defMethod(Row or None)!2: \"Get rows from a data table. If you specify keyword arguments, you will retrieve only rows whose columns match those values.\\n\\nEg: app_tables.table_1.search(name='John Smith')\" [\"search\"]\n#!defMethod(Media object, [escape_for_excel=False])!2: \"Get the table in CSV format, optionally escaped for use in Excel. Returns a downloadable Media object; use its url property.\" [\"to_csv\"]\n#!defClassNoConstructor(anvil.tables,#Table)!1: \"A table returned from app_tables\"\n\n#!defMethod(Media object, [escape_for_excel=False])!2: \"Get the results of the SearchIterator in CSV format, optionally escaped for use in Excel. Returns a downloadable Media object; use its url property.\" [\"to_csv\"]\n#!defClassNoConstructor(anvil.tables,#SearchIterator)!1: \"An iterator of table rows returned from a search()\";\n\n\n#!defMethod(_)!2: \"Delete the row from its data table\" [\"delete\"]\n#!defMethod(id)!2: \"Get the unique ID of the table row\" [\"get_id\"]\n#!defMethod(_,**column_values)!2: \"update the data for multiple columns\" [\"update\"]\n#!defClassNoConstructor(anvil.tables,#Row)!1: \"A table row\";\n";Sk.builtinFiles.files["anvil-services\/anvil\/tables\/query.py"] = "from anvil.server import portable_class\n\nfrom ._helpers import _hash_wrapper\n\n# Don't load v2 code unless v2 is imported. v2._load_hacks will inject this for us.\n# from .v2._refs import make_refs as _make_refs\n_make_refs = lambda x: x\n\n\n\n\nclass _pattern_query(object):\n def __init__(self, pattern):\n self.pattern = pattern\n\n __hash__, __eq__ = _hash_wrapper(\"pattern\")\n\n\nclass _value_query(object):\n def __init__(self, value):\n self.value = value\n\n __hash__, __eq__ = _hash_wrapper(\"value\")\n\n\nclass _of_query(object):\n def __init__(self, *args, **kwargs):\n self.args = _make_refs(args)\n self.kwargs = _make_refs(kwargs)\n\n def __hash__(self):\n return hash(self.args + tuple(sorted(self.kwargs.items())))\n\n def __eq__(self, other):\n if type(other) is not type(self):\n return NotImplemented\n return self.args == other.args and self.kwargs == other.kwargs\n\n\n#!defFunction(anvil.tables.query,_,pattern)!2: \"Match values using a case-sensitive LIKE query, using the % wildcard character.\" [\"like\"]\n@portable_class\nclass like(_pattern_query):\n pass\n\n\n#!defFunction(anvil.tables.query,_,pattern)!2: \"Match values using a case-insensitive ILIKE query, using the % wildcard character.\" [\"ilike\"]\n@portable_class\nclass ilike(_pattern_query):\n pass\n\n\n#!defFunction(anvil.tables.query,_,value)!2: \"Match values greater than the provided value.\" [\"greater_than\"]\n@portable_class\nclass greater_than(_value_query):\n pass\n\n\n#!defFunction(anvil.tables.query,_,value)!2: \"Match values less than the provided value.\" [\"less_than\"]\n@portable_class\nclass less_than(_value_query):\n pass\n\n\n#!defFunction(anvil.tables.query,_,value)!2: \"Match values greater than or equal to the provided value.\" [\"greater_than_or_equal_to\"]\n@portable_class\nclass greater_than_or_equal_to(_value_query):\n pass\n\n\n#!defFunction(anvil.tables.query,_,value)!2: \"Match values less than or equal to the provided value.\" [\"less_than_or_equal_to\"]\n@portable_class\nclass less_than_or_equal_to(_value_query):\n pass\n\n\n#!defFunction(anvil.tables.query,_,min,max,[min_inclusive=True],[max_inclusive=False])!2: \"Match values between the provided min and max, optionally inclusive.\" [\"between\"]\ndef between(min, max, min_inclusive=True, max_inclusive=False):\n return all_of(\n greater_than_or_equal_to(min) if min_inclusive else greater_than(min),\n less_than_or_equal_to(max) if max_inclusive else less_than(max),\n )\n\n\n#!defFunction(anvil.tables.query,_,query,[raw=False])!2: \"Match values that match the provided full-text search query.\" [\"full_text_match\"]\n@portable_class\nclass full_text_match(object):\n def __init__(self, query, raw=False):\n self.query = query\n self.raw = raw\n\n __hash__, __eq__ = _hash_wrapper(\"query\", \"raw\")\n\n\n#!defFunction(anvil.tables.query,_,*query_expressions)!2: \"Match all query parameters given as arguments and keyword arguments\" [\"all_of\"]\n@portable_class\nclass all_of(_of_query):\n pass\n\n\n#!defFunction(anvil.tables.query,_,*query_expressions)!2: \"Match any query parameters given as arguments and keyword arguments\" [\"any_of\"]\n@portable_class\nclass any_of(_of_query):\n pass\n\n\n#!defFunction(anvil.tables.query,_,*query_expressions)!2: \"Match none of the query parameters given as arguments and keyword arguments\" [\"none_of\"]\n@portable_class\nclass none_of(_of_query):\n pass\n\n\n#!defFunction(anvil.tables.query,_,*query_expressions)!2: \"Match none of the query parameters given as arguments and keyword arguments\" [\"not_\"]\nnot_ = none_of\n\n#!defFunction(anvil.tables.query,_,rows)!2: \"Define the number of rows that are fetched per round trip to the server.\" [\"page_size\"]\n@portable_class\nclass page_size(object):\n def __init__(self, rows):\n self.rows = rows\n\n __hash__, __eq__ = _hash_wrapper(\"rows\")\n\n\n#!defFunction(anvil.tables.query,_,*only_cols,**linked_cols)!2:\n# {\n# $doc: \"Control which columns are loaded from a table search to speed up queries by only fetching the data you need.\",\n# anvil$helpLink: \"\/docs\/data-tables\/accelerated-tables#explicit-cache-control\",\n# anvil$args: {\n# only_cols: \"Column names to fetch from the table. Example: fetch_only('email', group=q.fetch_only('name'))\",\n# linked_cols: \"Linked columns to fetch, specified as keyword arguments. Each value must be another fetch_only() object.\"\n# }\n# }[\"fetch_only\"]\n@portable_class(\"anvil.tables.fetch_only\")\nclass fetch_only(object):\n def __init__(self, *only_cols, **linked_cols):\n spec = {}\n for col in only_cols:\n if not isinstance(col, str):\n raise TypeError(\"columns must be strings\")\n spec[col] = True\n for col, only in linked_cols.items():\n if not isinstance(only, fetch_only):\n raise TypeError(\"keyword arguments must use q.fetch_only()\")\n spec[col] = only.spec\n self.spec = spec\n\n def _hashable(self, val):\n if val is True:\n return val\n return self._as_tuple(val)\n\n def _as_tuple(self, spec):\n return tuple((col_name, self._hashable(val)) for col_name, val in sorted(spec.items()))\n\n def __hash__(self):\n return hash(self._as_tuple(self.spec))\n\n def __eq__(self, other):\n if type(other) is not type(self):\n return NotImplemented\n return other.spec == self.spec\n\n\n#!defFunction(anvil.tables.query,_,*cols)!2:\n# {\n# $doc: \"Control which columns are accessible from a view, restricting access to specific columns.\",\n# anvil$helpLink: \"\/docs\/data-tables\/accelerated-tables#column-restricted-views\",\n# anvil$args: {\n# cols: \"Column names to make accessible in the view. Example: only_cols('email', 'enabled')\"\n# }\n# }[\"only_cols\"]\n@portable_class\nclass only_cols(object):\n def __init__(self, *cols):\n self.cols = tuple(sorted(cols))\n\n __hash__, __eq__ = _hash_wrapper(\"cols\")\n";Sk.builtinFiles.files["anvil-services\/anvil\/tables\/_helpers.py"] = "def _hash_wrapper(*params):\n # this makes query objects cachable as keys of dictionaries\n def _mk_tuple(self):\n return tuple(getattr(self, param) for param in params)\n\n def __hash__(self):\n return hash(_mk_tuple(self))\n\n def __eq__(self, other):\n if type(other) is not type(self):\n return NotImplemented\n return _mk_tuple(self) == _mk_tuple(other)\n\n return __hash__, __eq__\n";Sk.builtinFiles.files["anvil-services\/anvil\/tables\/_errors.py"] = "import anvil.server\n\n\n#!defMethod()!2: \"Superclass of all table exceptions\" [\"__init__\"]\n#!defClass(anvil.tables,TableError,__builtins__..Exception)!:\nclass TableError(anvil.server.AnvilWrappedError):\n pass\n\n\n#!defMethod()!2: \"Raised when attempting to accessing a table row that has been deleted - for example, accessing a row after calling its delete() method, or following a link to a deleted row.\" [\"__init__\"]\n#!defClass(anvil.tables,RowDeleted,anvil.tables.TableError)!:\nclass RowDeleted(TableError):\n pass\n\n\n#!defMethod()!2: \"Raised when attempting to access a column that does not exist in this table.\" [\"__init__\"]\n#!defClass(anvil.tables,NoSuchColumnError,anvil.tables.TableError)!:\nclass NoSuchColumnError(TableError):\n pass\n\n\n#!defMethod()!2: \"Raised when a transaction conflicts and has been aborted.\" [\"__init__\"]\n#!defClass(anvil.tables,TransactionConflict,anvil.tables.TableError)!:\nclass TransactionConflict(TableError):\n pass\n\n\n#!defMethod()!2: \"Raised when an app has exceeded its quota.\" [\"__init__\"]\n#!defClass(anvil.tables,QuotaExceededError,anvil.tables.TableError)!:\nclass QuotaExceededError(TableError):\n pass\n\n\nanvil.server._register_exception_type(\"anvil.tables.TransactionConflict\", TransactionConflict)\nanvil.server._register_exception_type(\"anvil.tables.TableError\", TableError)\nanvil.server._register_exception_type(\"anvil.tables.RowDeleted\", RowDeleted)\nanvil.server._register_exception_type(\"anvil.tables.NoSuchColumnError\", NoSuchColumnError)\nanvil.server._register_exception_type(\"anvil.tables.QuotaExceededError\", QuotaExceededError)\n";Sk.builtinFiles.files["anvil-services\/anvil\/tables\/_config.py"] = "import anvil\n\n_config = None\n\n\ndef get_client_config():\n global _config\n if _config is not None:\n return _config\n _config = anvil._get_service_client_config(\"\/runtime\/services\/tables.yml\") or {}\n return _config\n\ndef reset_config():\n global _config\n _config = None";Sk.builtinFiles.files["anvil-services\/anvil\/tables\/_base_classes.py"] = "class AppTables(object):\n def __repr__(self):\n return \"\".format(type(self).__name__)\n\n\nclass AbstractTableClass(object):\n __slots__ = ()\n _instead = None\n\n def __new__(cls, *args, **kwargs):\n raise TypeError(\n \"Can't create a {} object. Use {} instead.\".format(\n cls.__name__, cls._instead\n )\n )\n\n def __repr__(self):\n return \"\".format(type(self).__name__)\n\n def __dir__(self):\n # TODO should we keep this?\n # remove private attributes and methods from the dir\n return [\n key\n for key in object.__dir__(self)\n if (not key.startswith(\"_\")) or key.startswith(\"__\")\n ]\n\n\nclass Table(AbstractTableClass):\n _instead = \"app_tables.my_table\"\n\n\nclass SearchIterator(AbstractTableClass):\n _instead = \"app_tables.my_table.search()\"\n\n\nclass Row(AbstractTableClass):\n __slots__ = ()\n _instead = \"app_tables.my_table.add_row()\"\n";Sk.builtinFiles.files["anvil-services\/anvil\/tables\/v2\/__init__.py"] = "from .._base_classes import Row, SearchIterator, Table # noqa: F401\nfrom . import _load_hacks # noqa: F401\nfrom ._app_tables import app_tables, get_table_by_id\n\n# from ._batcher import batch_delete, batch_update\n\n__all__ = [\"app_tables\", \"get_table_by_id\"]\n";Sk.builtinFiles.files["anvil-services\/anvil\/tables\/v2\/_app_tables.py"] = "import anvil.server\n\nfrom .._base_classes import AppTables as BaseAppTables\nfrom ._constants import SERVER_PREFIX\nfrom ._table import Table\n\n_table_cache = None\n\n\ndef _fill_cache():\n global _table_cache\n if _table_cache is None:\n _table_cache = anvil.server.call(SERVER_PREFIX + \"get_app_tables\")\n return _table_cache\n\n\ndef _clear_cache():\n global _table_cache\n _table_cache = None\n\n\nclass AppTableIterator:\n def __init__(self):\n self._it = None\n\n def __iter__(self):\n return self\n \n def __next__(self):\n if self._it is None:\n self._it = _fill_cache().__iter__()\n return next(self._it)\n \n next = __next__\n\n\nclass AppTables(BaseAppTables):\n def __getattribute__(self, name):\n # use __getattribute__ so that we prioritise the table name\n try:\n return self[name]\n except KeyError:\n return object.__getattribute__(self, name)\n\n def __getitem__(self, name):\n cache = _fill_cache()\n table_args = cache[name]\n return Table._create(*table_args)\n\n def __setattr__(self, name, val):\n raise AttributeError(\"app_tables is read-only\")\n\n def __dir__(self):\n return list(_fill_cache().keys()) + object.__dir__(self)\n \n def __iter__(self):\n return AppTableIterator()\n\n\n\ndef get_table_by_id(table_id):\n table_args = anvil.server.call(SERVER_PREFIX + \"get_table_by_id\", table_id)\n return table_args and Table._create(*table_args)\n\n\napp_tables = AppTables()\n";Sk.builtinFiles.files["anvil-services\/anvil\/tables\/v2\/_batcher.py"] = "import anvil\nimport anvil.server\n\nfrom ._constants import NOT_FOUND, SERVER_PREFIX\nfrom ._utils import ThreadLocal\n\nPREFIX = SERVER_PREFIX + \"row.\"\n_make_refs = None # Circular import\n\n\nclass _Batcher(ThreadLocal):\n _name = \"\"\n _instance = None\n\n def __new__(cls):\n if cls._instance is None:\n cls._instance = ThreadLocal.__new__(cls)\n return cls._instance\n\n def __init__(self):\n self._active = 0\n self._args = []\n self._buffer = {}\n self._func = PREFIX + self._name\n\n @property\n def active(self):\n return self._active > 0\n\n def push(self, *args):\n self._args.append(args)\n\n def flush(self):\n if not self.active:\n return\n args = self._args\n if not args:\n return\n try:\n rv = anvil.server.call(self._func, args)\n self.post_flush(args, rv)\n finally:\n self.reset()\n\n def reset(self):\n self._args.clear()\n self._buffer.clear()\n\n def post_flush(self, args, result):\n raise NotImplementedError\n\n def __enter__(self):\n self._active += 1\n\n def __exit__(self, exc_type, exc_value, traceback):\n is_final_context = self._active == 1\n try:\n if exc_value is None and is_final_context:\n self.flush()\n finally:\n self._active -= 1\n if is_final_context:\n self.reset()\n\n\n\nclass BatchUpdate(_Batcher):\n _name = \"batch_update\"\n\n def push(self, cap, update, on_behalf_of_client):\n global _make_refs\n if _make_refs is None:\n from ._refs import make_refs # circular import\n\n _make_refs = make_refs\n\n self._args.append((cap, _make_refs(update), on_behalf_of_client))\n self._buffer.setdefault(cap, {}).update(update)\n\n def get_updates(self, cap):\n return self._buffer.get(cap, {})\n\n def read(self, cap, key):\n return self.get_updates(cap).get(key, NOT_FOUND)\n\n def post_flush(self, args, result):\n from ._row import _send_cap_update\n\n _seen = set()\n for (cap, _, _), spec in zip(args, result):\n if cap in _seen:\n continue\n\n _seen.add(cap)\n update = self._buffer.get(cap, {})\n _send_cap_update(cap, {\"S\": spec, \"U\": update})\n\n\nclass BatchDelete(_Batcher):\n _name = \"batch_delete_2\"\n\n def post_flush(self, args, result):\n from ._row import _send_cap_update\n\n for cap, _ in args:\n _send_cap_update(cap, {\"D\": True})\n\n\nbatch_update = BatchUpdate()\nbatch_delete = BatchDelete()\n\n\ndef flush():\n batch_update.flush()\n batch_delete.flush()\n\n\ndef flush_and_call(fn, *args, **kws):\n flush()\n return anvil.server.call(fn, *args, **kws)\n\n\nclass CombinedBatch(ThreadLocal):\n def __init__(self):\n self._batchers = [batch_delete, batch_update]\n\n def __enter__(self):\n for batcher in self._batchers:\n batcher.__enter__()\n return self\n\n def __exit__(self, exc_type, exc_value, traceback):\n raise_exc = False\n for batcher in reversed(self._batchers):\n try:\n batcher.__exit__(exc_type, exc_value, traceback)\n except Exception as e:\n exc_type = type(e)\n exc_value = e\n raise_exc = True\n if raise_exc:\n raise exc_value\n\n\nbatch = CombinedBatch()\n";Sk.builtinFiles.files["anvil-services\/anvil\/tables\/v2\/_constants.py"] = "import anvil.server\n\n# USED as an argument to the \"create_view\" private method\nREAD = \"r\"\nWRITE = \"rw\"\nCASCADE = \"rwc\"\nKNOWN_PERMS = (READ, WRITE, CASCADE)\n\nNOT_FOUND = object()\nCAP_KEY = \"c\"\n\nSINGLE = \"link_single\"\nMULTIPLE = \"link_multiple\"\nDATETIME = \"datetime\"\nMEDIA = \"media\"\n\nSHARED_DATA_KEY = \"anvil.tables\"\n\nSERVER_PREFIX = \"anvil.private.tables.v2.\"\n\n\n@anvil.server.portable_class(\"anvil.tables.v2.UNCACHED\")\nclass _UncachedType(object):\n _instance = None\n\n def __new__(cls):\n self = cls._instance\n if self is None:\n cls._instance = self = object.__new__(cls)\n return self\n\n def __repr__(self):\n return \"UNCACHED\"\n\n @classmethod\n def __new_deserialized__(cls, data, info):\n return UNCACHED\n\n def __serialize__(self, info):\n return None\n\n\nUNCACHED = _UncachedType()\n";Sk.builtinFiles.files["anvil-services\/anvil\/tables\/v2\/_load_hacks.py"] = "# For the sake of a soft roll-out, we don't want to load v2 code implicitly\n# from anvil.tables.query, but that module needs access to `make_refs` if we're\n# using v2. So we inject it (only) when v2 loads.\n\nfrom .. import query\nfrom . import _refs\n\nquery._make_refs = _refs.make_refs\n";Sk.builtinFiles.files["anvil-services\/anvil\/tables\/v2\/_model.py"] = "import anvil\nfrom anvil.server import AnvilWrappedError, portable_class\n\nfrom .._errors import NoSuchColumnError\nfrom ._constants import NOT_FOUND\nfrom ._utils import maybe_handle_descriptors\n\nrow_cls_by_id = {}\n\nglobal _Row\n\n\ndef getattr_impl(self, attr):\n try:\n return self[attr]\n except (AnvilWrappedError, NoSuchColumnError):\n raise AttributeError(attr)\n\n\ndef setattr_impl(self, attr, val):\n if not maybe_handle_descriptors(self, attr, val):\n self[attr] = val\n\n\ndef _clear_cache():\n # private method for clearing the cache\n row_cls_by_id.clear()\n\n\nCOMMON_SPELLING_ERRORS = {\n \"client_writeable\": \"client_writable\",\n \"client_updateable\": \"client_updatable\",\n \"client_createable\": \"client_creatable\",\n \"client_deleteable\": \"client_deletable\",\n}\n\n\ndef is_server_class_method(method):\n if not isinstance(method, anvil.server.server_method):\n return False\n return getattr(method, \"_is_class_method\", False)\n\n\ndef override_server_class_methods(cls, most_base):\n \"\"\"\n a hack\n if we have a client model and a server model that inherits from the client model\n then if the client model calls a server_method classmethod directly on the server\n then it should actually call the server model's server_method classmethod instead\n \"\"\"\n my_dict = cls.__dict__\n for base in cls.__bases__:\n if base is most_base:\n return\n for attr, method in base.__dict__.items():\n my_method = my_dict.get(attr)\n if my_method is None:\n continue\n if not is_server_class_method(method):\n continue\n if not is_server_class_method(my_method):\n continue\n\n setattr(base, attr, my_method)\n\n\ndef get_base_model_cls(table_id):\n cls = row_cls_by_id.get(table_id)\n if cls is not None:\n return cls\n\n global _Row\n from ._app_tables import _table_cache\n from ._row import Row as _Row\n\n _table_cache = _table_cache or {}\n\n tb_name = next(\n (name for name, args in (_table_cache).items() if str(args[-1]) == table_id),\n None,\n )\n\n class Row(_Row):\n __slots__ = ()\n _Row_model_ = None\n _Row_permissions_ = {\"update\": False, \"create\": False, \"delete\": False}\n _Row_buffered_ = False\n\n def __new__(cls, **buffer):\n cls = get_model_cls(table_id)\n self = object.__new__(cls)\n self._anvil_setup(None, table_id, None, buffer=buffer)\n return self\n\n @classmethod\n def _do_create(cls, values, from_client, **kws):\n trusted_values = kws.pop(\"trusted_values\", None)\n if kws:\n raise TypeError(\"Unexpected keyword arguments: {}\".format(kws))\n\n from . import get_table_by_id\n\n from ._row import _make_request_overrides\n\n use_client_config = cls._anvil_use_client_config(\"create\", from_client)\n table = get_table_by_id(table_id)\n\n return table._do_add_row(\n values,\n _make_request_overrides(use_client_config, from_client),\n trusted_values,\n )\n\n def __init_subclass__(\n cls,\n attrs=False,\n buffered=False,\n client_writable=False,\n client_updatable=NOT_FOUND,\n client_creatable=NOT_FOUND,\n client_deletable=NOT_FOUND,\n **kws,\n ):\n cls_dict = cls.__dict__\n for attr in (\n \"__deserialize__\",\n \"__serialize__\",\n \"__init__\",\n \"__new__\",\n \"__new_deserialized__\",\n ):\n if cls_dict.get(attr):\n msg = \"It is not possible to customize the method {!r} for {}.{}\".format(\n attr, cls.__module__, cls.__name__\n )\n raise TypeError(msg)\n\n cls._Row_prefix_ = \"{}.{}\".format(cls.__module__, cls.__name__)\n if attrs and not hasattr(cls, \"__getattr__\"):\n cls.__getattr__ = getattr_impl\n cls.__setattr__ = setattr_impl\n\n if buffered:\n cls._Row_buffered_ = True\n\n row_permissions = {}\n\n for perm_type, value in [\n (\"update\", client_updatable),\n (\"create\", client_creatable),\n (\"delete\", client_deletable),\n ]:\n if value is not NOT_FOUND:\n row_permissions[perm_type] = value\n continue\n\n if client_writable:\n row_permissions[perm_type] = True\n continue\n\n # use the inherited permission\n super_permission = cls._Row_permissions_[perm_type]\n row_permissions[perm_type] = super_permission\n\n cls._Row_permissions_ = row_permissions\n\n if kws:\n # check if something is misspelt\n for a, b in COMMON_SPELLING_ERRORS.items():\n if a in kws:\n msg = \"Parameter '{}' is misspelled. Did you mean '{}'?\".format(\n a, b\n )\n raise TypeError(msg)\n raise TypeError(\"Unexpected keyword arguments: {}\".format(kws))\n\n override_server_class_methods(cls, Row)\n\n # models are portable by default\n # the first subclass gets registered\n # subsequent models use the same name\n # A model can override this in advanced use cases e.g. uplink implementations\n if Row._Row_model_ is Row:\n portable_class(cls)\n else:\n name = Row._Row_model_.SERIALIZATION_INFO[0]\n portable_class(name)(cls)\n\n Row._Row_model_ = cls\n\n # This allows us to pretend that the model class is the Row class for serialization\n Row.__name__ = _Row.__name__\n Row.__module__ = _Row.__module__\n Row.__qualname__ = _Row.__qualname__\n\n Row._Row_model_ = Row\n if tb_name:\n Row._Row_prefix_ = \"app_tables.{}.Row\".format(tb_name)\n row_cls_by_id[table_id] = Row\n\n # we'll be overriding the base Row class in the portable classes registry\n # But that's fine since Row._anvil_create checks the correct Row subclass to __new__\n return portable_class(Row)\n\n\ndef get_model_cls(table_id):\n return get_base_model_cls(table_id)._Row_model_\n\n\ndef serialize_model(table_id, force=False):\n model = get_model_cls(table_id)\n if model is None or model is row_cls_by_id.get(table_id) and not force:\n return None\n return model\n";Sk.builtinFiles.files["anvil-services\/anvil\/tables\/v2\/_refs.py"] = "import anvil.server\nfrom anvil.server import portable_class\n\nfrom ._row import Row, _is_draft\n\n# Helpful classes for table methods that include Rows\/SearchIterators\n# But sending the Row across the wire is unnecessary\n# We shouldn't be deserializing these objects but we include __deserialize__ for completeness\n\n\nclass _Ref(object):\n def __init__(self, cap):\n self.cap = cap\n\n def __hash__(self):\n return hash(self.cap)\n\n def __serialize__(self, info):\n return self.cap\n\n def __deserialize__(self, cap, info):\n self.cap = cap\n\n def __eq__(self, other):\n if type(self) is not type(other):\n return NotImplemented\n return self.cap == other.cap\n\n\n@portable_class(\"anvil.tables.v2._RowRef\")\nclass RowRef(_Ref):\n pass\n\n\n@portable_class\nclass SearchIteratorRef(_Ref):\n pass\n\n\ndef to_ref(obj):\n ob_type = type(obj)\n if ob_type in (list, tuple):\n return tuple(to_ref(item) for item in obj)\n elif isinstance(obj, Row):\n if _is_draft(obj):\n raise ValueError(\n \"It looks like you're trying to write a draft as a linked row. This is not allowed.\"\n \"Either use row.buffer_changes(True), or save the draft first.\"\n \" (Found {!r})\".format(obj)\n )\n elif obj._anvil.cap is None:\n raise RuntimeError(\"Row has no capability\")\n return RowRef(obj._anvil.cap)\n return obj\n\n\ndef make_refs(args_or_kws):\n if type(args_or_kws) is dict:\n return {key: to_ref(val) for key, val in args_or_kws.items()}\n else:\n return tuple(to_ref(val) for val in args_or_kws)\n";Sk.builtinFiles.files["anvil-services\/anvil\/tables\/v2\/_row.py"] = "import anvil\nimport anvil.server\nfrom anvil.server import Capability\n\nfrom .._base_classes import Row as BaseRow\nfrom .._errors import NoSuchColumnError, RowDeleted, TableError\nfrom . import _batcher\nfrom ._constants import (\n CAP_KEY,\n DATETIME,\n MEDIA,\n MULTIPLE,\n NOT_FOUND,\n SERVER_PREFIX,\n SHARED_DATA_KEY,\n SINGLE,\n UNCACHED,\n)\nfrom ._model import get_model_cls\nfrom ._utils import (\n InternalDict,\n check_serialized,\n clean_cache_table_spec,\n clean_local_datetime,\n init_spec_rows,\n init_view_data,\n maybe_handle_descriptors,\n merge_row_data,\n validate_cap,\n)\n\nPREFIX = SERVER_PREFIX + \"row.\"\n_make_refs = None # for circular imports\n_auto_create_is_enabled = NOT_FOUND\n\n\nclass _MODE:\n NORMAL = 0\n BUFFERED = 1\n DRAFT = 2\n # Buffered draft is when we explicitly call\n # buffer_changes(True) on a draft\n BUFFERED_DRAFT = 3\n\n\ndef _is_draft(row):\n return row._anvil.mode is _MODE.DRAFT or row._anvil.mode is _MODE.BUFFERED_DRAFT\n\n\ndef _is_buffered(row):\n return row._anvil.mode is not _MODE.NORMAL\n\n\ndef _copy(so):\n if isinstance(so, list):\n return [_copy(o) for o in so]\n if isinstance(so, dict):\n return {k: _copy(v) for k, v in so.items()}\n return so\n\n\ndef _new_save_plan():\n \"\"\"Return the shared save\/reset plan structure.\n\n `_walk_buffered_changes()` builds this plan from one or more starting rows.\n `save_all()` executes it. `reset_all()` only uses it to discover every row\n whose local buffer should be cleared.\n\n Shape:\n - rows \/ buffers:\n Realized rows with buffered updates that should be sent via `_do_update()`.\n `rows[i]` is updated with `buffers[i]`.\n - draft_info:\n Draft rows that should be created via `_do_create()`.\n Each entry stores the draft's current buffer plus its table id.\n - single \/ multi:\n Patch instructions for linked draft references that could not be sent\n inline while the plan was being built. Each path is\n `[\"rows\" | \"drafts\", index, key]` and points at the buffer entry that\n should receive either a realized row (`single`) or a list containing\n realized rows and\/or draft indices (`multi`).\n \"\"\"\n return {\n \"rows\": [],\n \"buffers\": [],\n \"draft_info\": [],\n \"single\": [],\n \"multi\": [],\n }\n\n\n# Protocol version that supports new cap update format {\"D\": True} \/ {\"U\": ..., \"S\": ...}\nNEW_CAP_UPDATE_PROTOCOL_VERSION = 8\n# Version identifier for new cap update format - column names unlikely to start with $\nVERSION_KEY = \"$_V\"\nVERSION_IDENTIFIER = {VERSION_KEY: 0}\n\n\ndef _any_caller_needs_old_format():\n \"\"\"Check if ANY caller in the stack needs old cap update format.\n\n We check uplinks and browsers because:\n - Uplinks (user-controlled Python) might be old\n - Browsers without a protocol version predate the new format\n - Downlinks are deployed with the server and always support both formats\n\n Returns True if we need old format, False if we can use new format.\n \"\"\"\n call_context = anvil.server.context\n\n # Check the full call stack\n call_stack = getattr(call_context, 'call_stack', None)\n if not call_stack:\n return False # No stack = internal call, use new format\n\n for frame in call_stack:\n # Check uplinks and browsers for version compatibility\n if frame.type in ('uplink', 'client-uplink', 'browser'):\n if frame.protocol_version is None:\n # Unknown version - be conservative, assume old\n return True\n if frame.protocol_version < NEW_CAP_UPDATE_PROTOCOL_VERSION:\n return True\n\n return False\n\n\ndef _make_request_overrides(use_client_config, filter_client_hidden=False):\n \"\"\"Create request overrides dict, or None if all false (backwards compat).\n\n Old Clojure evaluates truthiness of this parameter. Sending {} would be\n truthy but have no flags set, causing incorrect behavior. Send None instead.\n \"\"\"\n if use_client_config or filter_client_hidden:\n return {\n \"use_client_config\": use_client_config,\n \"filter_client_hidden\": filter_client_hidden,\n }\n return None\n\n\ndef _normalize_cap_update(cap_update):\n \"\"\"Normalize cap_update to new format for backwards compatibility.\n\n Old format: False (deletion) or flat dict {col: value} (updates)\n New format: {\"$_V\": 0, \"D\": True} or {\"$_V\": 0, \"U\": dict, \"S\": spec}\n\n We use \"$_V\" as a version marker since column names can't start with $.\n \"\"\"\n if cap_update is False:\n return {\"D\": True}\n if cap_update is None:\n return None\n if isinstance(cap_update, dict):\n if VERSION_KEY not in cap_update:\n # Old format: flat dict of updates\n return {\"U\": cap_update}\n return cap_update\n\n\ndef _send_cap_update(cap, payload):\n \"\"\"Send a capability update in the appropriate format based on stack versions.\n\n Args:\n cap: The capability to send the update through\n payload: The new-format payload ({\"D\": True} or {\"U\": updates, \"S\": spec})\n The \"$_V\" marker will be added automatically for new format.\n \"\"\"\n if _any_caller_needs_old_format():\n # Convert to old format for old uplinks\/browsers\n if payload.get(\"D\"):\n cap.send_update(False)\n else:\n cap.send_update(payload.get(\"U\", {}))\n else:\n # Add version marker so receiver knows this is new format\n cap.send_update({**VERSION_IDENTIFIER, **payload})\n\n\nclass _BufferedContext(object):\n def __init__(self, row):\n self._row = row\n\n def __enter__(self):\n mode = self._row._anvil.mode\n if mode is _MODE.NORMAL:\n self._row._anvil.mode = _MODE.BUFFERED\n elif mode is _MODE.DRAFT:\n self._row._anvil.mode = _MODE.BUFFERED_DRAFT\n return self\n\n def __exit__(self, exc_type, exc_val, exc_tb):\n self._row._anvil.buffer.clear()\n if self._row._anvil.mode is _MODE.BUFFERED_DRAFT:\n # revert back to a state where if we call save()\n # then we will return to the default buffered mode\n self._row._anvil.mode = _MODE.DRAFT\n else:\n self._row._anvil.mode = _MODE.NORMAL\n\n\n@anvil.server.portable_class\nclass Row(BaseRow):\n __slots__ = (\"_anvil\",)\n _Row_prefix_ = \"anvil.tables.Row\"\n _Row_buffered_ = False\n _Row_permissions_ = {\"update\": False, \"create\": False, \"delete\": False}\n\n @classmethod\n def _anvil_create(cls, view_key, table_id, row_id, spec=None, cap=None):\n cls = get_model_cls(table_id)\n buffer = {} if cls._Row_buffered_ else None\n row = object.__new__(cls)\n row._anvil_setup(view_key, table_id, row_id, spec, cap, buffer=buffer)\n return row\n\n def _anvil_setup(\n self, view_key, table_id, row_id, spec=None, cap=None, buffer=None\n ):\n object.__setattr__(self, \"_anvil\", InternalDict())\n self._anvil.view_key = view_key\n self._anvil.table_id = table_id\n self._anvil.id = row_id\n self._anvil.cap = cap\n self._anvil.queued_cap_updates = {}\n self._anvil.cache = {}\n self._anvil.spec = (\n spec # None when we are deserialized without access to table_data\n )\n self._anvil.cache_spec = spec[\"cache\"] if spec is not None else []\n self._anvil.has_uncached = True\n self._anvil.exists = True\n self._anvil.dirty_spec = False # used for serialization\n if view_key is None:\n self._anvil.mode = _MODE.DRAFT\n elif buffer is not None:\n self._anvil.mode = _MODE.BUFFERED\n else:\n self._anvil.mode = _MODE.NORMAL\n self._anvil.buffer = buffer or {}\n\n if cap is not None:\n cap.set_update_handler(\n self._anvil_cap_update_handler,\n get_update=self._anvil_get_cap_update,\n )\n\n return self\n\n @classmethod\n def _anvil_create_from_untrusted(cls, view_key, table_id, row_id, cap, local_data):\n # check that we can trust the data that was sent!\n row = local_data.get(cap)\n if row is None:\n row = local_data[cap] = cls._anvil_create(\n view_key, table_id, row_id, None, cap\n )\n return row\n\n @classmethod\n def _anvil_create_from_trusted(cls, view_key, table_id, row_id, table_data):\n table_id, row_id = str(table_id), str(row_id)\n view_data = table_data[view_key]\n rows = view_data[\"rows\"]\n row_data = rows[row_id]\n if isinstance(row_data, Row):\n # prevent circular and use the created row from view_data\n return row_data\n spec = view_data[\"spec\"]\n row = rows[row_id] = cls._anvil_create(view_key, table_id, row_id, spec)\n # Replace the compact row_data with ourself\n # This prevents circular references and has the benefit that\n # we create the same rows and linked rows when creating Row objects from the same data\n row._anvil_unpack(table_data, row_data)\n if view_data.get(\"dirty_spec\"):\n # a serialized row marked its spec as dirty after an update\n row._anvil_clear_cache()\n return row\n\n @classmethod\n def _anvil_create_from_local_values(\n cls, view_key, table_id, row_id, spec, cap, local_items\n ):\n # the basic idea here is that we need to clean datetime objects and UNCACHE any linked rows\n # where the view_key doesn't match what we expect from the col_spec\n table_id, row_id = str(table_id), str(row_id)\n row = cls._anvil_create(view_key, table_id, row_id, spec, cap)\n clean_items = row._anvil_walk_local_items(local_items, missing=None)\n row._anvil.cache.update(clean_items)\n row._anvil_check_has_cached()\n return row\n\n # DESERIALIZE\n @classmethod\n def __new_deserialized__(cls, data, info):\n table_data, local_data = info.shared_data(SHARED_DATA_KEY)\n view_key, table_id, row_id, cap = data\n if not info.remote_is_trusted:\n validate_cap(cap, table_id, row_id)\n table_data = None # just incase\n if not table_data:\n # table_data None is not enough because we may be sending rows back and forward\n # i.e. passing from client to server to client goes untrusted -> trusted -> client\n return cls._anvil_create_from_untrusted(\n view_key, table_id, row_id, cap, local_data\n )\n return cls._anvil_create_from_trusted(view_key, table_id, row_id, table_data)\n\n def _anvil_unpack(self, table_data, row_data):\n assert type(row_data) in (\n list,\n dict,\n ), \"Unable to create Row object, bad row_data\"\n spec = table_data[self._anvil.view_key][\"spec\"]\n if self._anvil.spec is None:\n self._anvil.spec = spec\n cols = spec[\"cols\"] if spec is not None else []\n initial_load = not bool(self._anvil.cache)\n row_data_type = type(row_data)\n # if the spec is None we must have a dict data type with a single cap key\n # this potentially happens in (and is enforced by) serialization\n if row_data_type is list:\n unpacked_cache, cap = self._anvil_unpack_compact(\n table_data, spec, cols, row_data, initial_load\n )\n elif row_data_type is dict:\n unpacked_cache, cap = self._anvil_unpack_dict(\n table_data, cols, row_data, initial_load\n )\n else:\n raise TableError(\"the row data is invalid\")\n\n assert type(cap) is Capability, \"invalid row_data\"\n if self._anvil.cap is None:\n self._anvil.cap = cap\n cap.set_update_handler(\n self._anvil_cap_update_handler, get_update=self._anvil_get_cap_update\n )\n self._anvil.cache.update(unpacked_cache)\n self._anvil_check_has_cached()\n\n def _anvil_unpack_compact(self, table_data, spec, cols, row_data, initial_load):\n # spec[\"cache\"] 1s matches the len(row_data) (+cap)\n iter_row_data = iter(row_data)\n unpacked_cache = {}\n for col, is_cached in zip(cols, spec[\"cache\"]):\n if is_cached:\n val = self._anvil_maybe_unpack_linked(\n next(iter_row_data), col, table_data\n )\n elif initial_load:\n val = UNCACHED # there's nothing there yet so fill it\n else:\n continue\n unpacked_cache[col[\"name\"]] = val\n return unpacked_cache, next(iter_row_data)\n\n def _anvil_unpack_dict(self, table_data, cols, row_data, initial_load):\n unpacked_cache = {}\n for i, col in enumerate(cols):\n val = row_data.pop(str(i), UNCACHED)\n if val is UNCACHED and not initial_load:\n # does this ever happen?\n continue\n unpacked_cache[col[\"name\"]] = self._anvil_maybe_unpack_linked(\n val, col, table_data\n )\n cap = row_data.pop(CAP_KEY, None)\n assert len(row_data) == 0, \"Invalid row data\"\n return unpacked_cache, cap\n\n def _anvil_maybe_unpack_linked(self, val, col, table_data):\n table_id = col.get(\"table_id\")\n did_return_value = 0\n if table_id is None:\n did_return_value += 1\n # not a linked row\n return val\n elif val is UNCACHED:\n did_return_value += 2\n # UNCACHED linked row\n return val\n elif val is None:\n did_return_value += 3\n # linked row is None\n return val\n else:\n try:\n return self._anvil_unpack_linked(table_id, val, col, table_data)\n except KeyError:\n # This line is failing for some users - wrap in a try except\n # it has since been reported on the forum and it seemed to be a client-side issue\n # where the early return was not being triggered\n import json\n\n msg = (\n 'Failed to get \"view_key\" or \"type\" from col={!r}, '\n \"found table_id={!r}, \"\n \"table_id is None={!r},\"\n \"val={!r}, val is UNCACHED={!r}\"\n \"row_id={!r}, \"\n \"did_return_value={!r}, \"\n \"server_side={!r}\".format(\n col,\n table_id,\n table_id is None,\n val,\n val is UNCACHED,\n self._anvil.id,\n did_return_value,\n anvil.is_server_side(),\n )\n )\n try:\n _data = json.dumps(\n table_data, indent=2, default=lambda o: str(type(o))\n )\n msg += \"\\n\\nTable data:\\n{}\".format(_data)\n except Exception:\n pass\n\n raise KeyError(msg)\n\n def _anvil_unpack_linked(self, table_id, val, col, table_data):\n col_type, view_key = col[\"type\"], col[\"view_key\"]\n\n if col_type == SINGLE:\n row_id = val\n return Row._anvil_create_from_trusted(\n view_key, table_id, row_id, table_data\n )\n elif col_type == MULTIPLE:\n row_ids = val\n return [\n Row._anvil_create_from_trusted(view_key, table_id, row_id, table_data)\n for row_id in row_ids\n ]\n\n raise AssertionError(\"bad col type with table_id\")\n\n # SERIALIZATION\n def __serialize__(self, info):\n self._anvil_check_can_serialize()\n table_data, local_data = info.shared_data(SHARED_DATA_KEY)\n if table_data is not None and info.local_is_trusted:\n self._anvil_merge_and_reduce(table_data, local_data, info.remote_is_trusted)\n else:\n # We want to ensure we're not trying to send a linked draft or row that has buffered changes\n # TODO - we could be a bit more efficient about this since we don't actually need the data!\n self._anvil_merge_and_reduce({}, local_data, info.remote_is_trusted)\n return [\n self._anvil.view_key,\n self._anvil.table_id,\n self._anvil.id,\n self._anvil.cap,\n ]\n\n def _anvil_check_can_serialize(self, linked=False):\n error = None\n pre = \"Linked \" if linked else \"\"\n if _is_draft(self):\n error = \"Draft Rows cannot be serialized. Call save() first. (Found {!r})\"\n elif self._anvil.buffer:\n error = \"Rows with buffered changes cannot be serialized. Call save() or reset() first, (Found {!r})\"\n if error:\n raise anvil.server.SerializationError(pre + error.format(self))\n\n def _anvil_merge_linked(\n self, val, col, g_table_data, local_data, remote_is_trusted\n ):\n type = col[\"type\"]\n if val is UNCACHED or val is None:\n # maybe we were serialized and converted linked row(s) to UNCACHED\n # or actually the linked row is None\n pass\n elif type == SINGLE:\n row = val\n val = row._anvil_merge_and_reduce(\n g_table_data, local_data, remote_is_trusted\n )\n elif type == MULTIPLE:\n val = [\n row._anvil_merge_and_reduce(g_table_data, local_data, remote_is_trusted)\n for row in val\n ]\n return val\n\n def _anvil_make_row_data(\n self, g_table_data, local_data, table_cols, cache_spec, remote_is_trusted\n ):\n self._anvil_check_can_serialize(linked=True)\n cache = self._anvil.cache\n # we can't rely on the order of cache in python 2\n cached_data = []\n for i, (col, is_cached) in enumerate(zip(table_cols, cache_spec)):\n if not is_cached:\n continue\n name = col[\"name\"]\n val = self._anvil_merge_linked(\n cache[name], col, g_table_data, local_data, remote_is_trusted\n )\n cached_data.append((i, val))\n cached_data.append((CAP_KEY, self._anvil.cap))\n return cached_data\n\n def _anvil_merge_and_reduce(self, g_table_data, local_data, remote_is_trusted):\n if check_serialized(self, local_data):\n return int(self._anvil.id)\n g_view_data = init_view_data(self._anvil.view_key, g_table_data)\n row_id = self._anvil.id\n\n cache_spec, table_spec = clean_cache_table_spec(\n self._anvil.cache_spec, self._anvil.spec, remote_is_trusted\n )\n\n # We assert that there is no way for rows from the same view_key to have different col_specs\n # This includes the order\n # the only thing they may differ on is cache_specs\n g_table_spec, g_table_rows = init_spec_rows(g_view_data, table_spec, cache_spec)\n g_cache_spec = g_table_spec[\"cache\"] if g_table_spec is not None else None\n\n if table_spec is not None and g_cache_spec is not None:\n is_dirty = self._anvil.dirty_spec or len(cache_spec) != len(g_cache_spec)\n else:\n is_dirty = self._anvil.dirty_spec\n\n if is_dirty:\n g_view_data[\"dirty_spec\"] = True\n cache_spec = []\n\n table_cols = table_spec[\"cols\"] if table_spec is not None else []\n cached_data = self._anvil_make_row_data(\n g_table_data, local_data, table_cols, cache_spec, remote_is_trusted\n )\n existing = g_table_rows.get(row_id, [])\n\n if not is_dirty and cache_spec == g_cache_spec and type(existing) is list:\n row_data = [val for _, val in cached_data]\n else:\n row_data = {str(key): val for key, val in cached_data}\n\n merge_row_data(row_id, row_data, g_table_rows, g_table_spec, cache_spec)\n return int(row_id)\n\n def _anvil_queue_cap_update(self, cap_update):\n if cap_update is None:\n return\n\n if not anvil.is_server_side():\n return\n\n D = cap_update.get(\"D\")\n S = cap_update.get(\"S\")\n U = cap_update.get(\"U\")\n\n queued = self._anvil.queued_cap_updates\n\n if D:\n queued[\"D\"] = True\n\n if S is not None:\n queued[\"S\"] = S\n\n if U is not None:\n queued[\"U\"] = queued.get(\"U\", {})\n queued[\"U\"].update(U)\n\n # PRIVATE METHODS\n def _anvil_cap_update_handler(self, cap_update):\n # Normalize old format to new format for backwards compatibility\n cap_update = _normalize_cap_update(cap_update)\n if cap_update is None:\n return\n\n self._anvil_queue_cap_update(cap_update)\n\n # queue the updates\n D = cap_update.get(\"D\")\n S = cap_update.get(\"S\")\n U = cap_update.get(\"U\")\n\n # Deleted\n if D:\n # We've been deleted clear_cache so that\n # server calls are required for data access\n self._anvil_clear_cache()\n self._anvil.mode = _MODE.NORMAL\n self._anvil.buffer.clear()\n self._anvil.exists = False\n return\n\n # Spec\n if S is not None:\n self._anvil.spec = S\n for col in S[\"cols\"]:\n self._anvil.cache.setdefault(col[\"name\"], UNCACHED)\n\n # Update\n if U and self._anvil.spec is not None:\n clean_items = self._anvil_walk_local_items(U)\n self._anvil.cache.update(clean_items)\n for key in clean_items:\n self._anvil.buffer.pop(key, None)\n\n self._anvil_check_has_cached()\n\n def _anvil_check_has_cached(self):\n if self._anvil.spec is None:\n return\n self._anvil.cache_spec = [\n int(self._anvil.cache.get(col[\"name\"], UNCACHED) is not UNCACHED)\n for col in self._anvil.spec[\"cols\"]\n ]\n self._anvil.has_uncached = any(\n val is UNCACHED for val in self._anvil.cache.values()\n )\n\n def _anvil_clear_cache(self):\n # clearing the cache also clears the spec - this forces a call to the server to update a spec\n self._anvil.spec = None\n self._anvil.cache.clear()\n self._anvil.cache_spec = []\n self._anvil.has_uncached = True\n\n def _anvil_fill_cache(self, fetch=None):\n if fetch is not None:\n uncached_keys = None if fetch is True else fetch\n elif self._anvil.spec is None:\n uncached_keys = None\n elif self._anvil.has_uncached:\n uncached_keys = [\n key for key, val in self._anvil.cache.items() if val is UNCACHED\n ]\n else:\n return # no uncached values\n\n table_data = _batcher.flush_and_call(\n PREFIX + \"fetch\", self._anvil.cap, uncached_keys\n )\n rows = table_data[self._anvil.view_key][\"rows\"]\n row_data = rows[self._anvil.id]\n # Replace the compact row data with this Row instance\n # so circular references don't clobber the data while we're unpacking.\n rows[self._anvil.id] = self\n self._anvil_unpack(table_data, row_data)\n\n def _anvil_walk_local_items(self, items, missing=NOT_FOUND):\n # We are about to put local items in the cache\n # so check linked rows have valid view keys datetimes have tz.offset applied\n items = items.copy()\n rv = {}\n cols = self._anvil.spec[\"cols\"]\n for col in cols:\n name, type = col[\"name\"], col[\"type\"]\n val = items.pop(name, missing)\n if val is NOT_FOUND:\n continue\n else:\n rv[name] = _copy(val)\n if val is UNCACHED or val is None:\n continue\n elif type == DATETIME:\n rv[name] = clean_local_datetime(val)\n continue\n elif type == MEDIA:\n rv[name] = UNCACHED # we need to fetch a lazy media with a valid url\n continue\n elif type == SINGLE:\n val = [val]\n elif type != MULTIPLE:\n continue\n rows = val\n expected_view_key = col[\"view_key\"]\n if any(row._anvil.view_key != expected_view_key for row in rows):\n rv[name] = UNCACHED\n if len(items):\n # more items than we should have - our col spec is no good anymore\n self._anvil.dirty_spec = True\n rv.update(items)\n return rv\n\n def _anvil_check_exists(self):\n # only call this if we're not doing a server call\n if not self._anvil.exists:\n raise RowDeleted(\"This row has been deleted\")\n\n # DUNDER METHODS\n def __setattr__(self, attr, val):\n if not maybe_handle_descriptors(self, attr, val):\n raise AttributeError(\n f\"Rows cannot have local state, trying to set {attr!r} attribute on {self!r}\"\n )\n\n def __iter__(self):\n # call to __iter__ can't suspend\n # so only do suspension stuff in __next__\n # note that this will not get called for dict(row)\n # keys() and __getitem__ wins for a call to dict\n return RowIterator(self)\n\n def __contains__(self, key):\n return key in self.keys()\n\n def __getitem__(self, key):\n if not isinstance(key, str):\n raise TypeError(\n \"Row columns are always strings, not {}\".format(type(key).__name__)\n )\n if _is_buffered(self):\n rv = self._anvil.buffer.get(key, NOT_FOUND)\n if rv is not NOT_FOUND:\n return _copy(rv)\n if _is_draft(self):\n return None\n\n if _batcher.batch_update.active:\n rv = _batcher.batch_update.read(self._anvil.cap, key)\n if rv is not NOT_FOUND:\n return _copy(rv)\n if self._anvil.spec is None:\n self._anvil_fill_cache()\n hit = self._anvil.cache.get(key, NOT_FOUND)\n if hit is UNCACHED:\n # we have a spec now so we'll fetch the remaining columns\n self._anvil_fill_cache()\n elif hit is NOT_FOUND:\n global _auto_create_is_enabled\n if _auto_create_is_enabled is NOT_FOUND:\n _auto_create_is_enabled = anvil.server.call(PREFIX + \"can_auto_create\")\n if _auto_create_is_enabled:\n # try to force fetch this key - incase we have a bad spec - i.e auto-columns\n self._anvil_fill_cache([key])\n else:\n return _copy(hit)\n try:\n return _copy(self._anvil.cache[key])\n except KeyError:\n raise NoSuchColumnError(\"No such column '\" + key + \"'\")\n\n def __setitem__(self, key, value):\n return self.update(**{key: value})\n\n def __eq__(self, other):\n if not isinstance(other, Row):\n return NotImplemented\n if self is other:\n return True\n return (\n self._anvil.id is not None\n and other._anvil.id == self._anvil.id\n and other._anvil.table_id == self._anvil.table_id\n )\n\n def __hash__(self):\n if _is_draft(self):\n raise ValueError(\"draft rows are unhashable\")\n self._anvil_check_exists()\n return hash((self._anvil.table_id, self._anvil.id))\n\n def __repr__(self):\n cls = type(self)\n prefix = cls._Row_prefix_\n if _is_draft(self):\n return \"<{} (draft) object>\".format(prefix)\n\n if self._anvil.spec is None:\n return \"<{} object>\".format(prefix)\n\n # custom reprs depending on type\n def trunc_str(s):\n return repr(s) if len(s) < 20 else repr(s[:17] + \"...\")\n\n def dt_repr(d):\n return \"datetime(\" + str(d) + \")\"\n\n def d_repr(d):\n return \"date(\" + str(d) + \")\"\n\n printable_types = {\n \"string\": trunc_str,\n \"bool\": repr,\n \"date\": d_repr,\n \"datetime\": dt_repr,\n \"number\": repr,\n }\n\n # Find cols that are both cached and easily printed\n cache, cols = self._anvil.cache, self._anvil.spec[\"cols\"]\n cached_printable_cols = [\n (c[\"name\"], printable_types[c[\"type\"]], cache[c[\"name\"]])\n for c in cols\n if c[\"type\"] in printable_types and cache[c[\"name\"]] is not UNCACHED\n ]\n # Only keep the first 5\n cached_printable_cols = cached_printable_cols[:5]\n # Find all the remaining columns\n num_remaning = len(cols) - len(cached_printable_cols)\n\n vals = \", \".join(\n \"{}={}\".format(name, None if val is None else meth(val))\n for name, meth, val in cached_printable_cols\n )\n\n if not num_remaning:\n and_more = \"\"\n elif cached_printable_cols:\n and_more = \", plus {} more column{}\".format(\n num_remaning, \"s\" if num_remaning != 1 else \"\"\n )\n else:\n and_more = \"{} column{}\".format(\n num_remaning, \"s\" if num_remaning != 1 else \"\"\n )\n\n return \"<{}: {}{}>\".format(prefix, vals, and_more)\n\n # PUBLIC API\n def buffer_changes(self, buffered=None):\n if buffered:\n if _is_draft(self):\n # we are explicitly setting the mode to buffered\n # this effects the save behaviour\n # this would be an unusual thing to do - but it does mean you can re-use logic between drafts and rows\n self._anvil.mode = _MODE.BUFFERED_DRAFT\n else:\n self._anvil.mode = _MODE.BUFFERED\n return _BufferedContext(self)\n elif buffered is None:\n # we don't start buffering until we enter the context manager\n return _BufferedContext(self)\n elif _is_draft(self):\n # alternatively we could go to `DRAFT` mode from `BUFFERED_DRAFT` mode\n raise ValueError(\n \"Changes in a draft row must always be buffered\"\n \" - call save() to convert to a realized row or reset to clear the buffer\"\n )\n else:\n self._anvil.mode = _MODE.NORMAL\n self._anvil.buffer.clear()\n\n @property\n def buffered_changes(self):\n if _is_buffered(self):\n return _copy(self._anvil.buffer)\n else:\n return None\n\n def save(self):\n # TODO - add a cascade argument and decide on the correct behaviour\n # e.g. what happens if cascade is false but you have draft linked rows?\n save_all(self)\n\n def reset(self):\n reset_all(self)\n\n # deprecated\n def get_id(self):\n # For compatibility with LiveObjects\n self._anvil_check_exists()\n if _is_draft(self):\n return None\n return \"[{},{}]\".format(self._anvil.table_id, self._anvil.id)\n\n # TODO reinclude this api\n # @property\n # def id(self):\n # return self._anvil.id\n\n # TODO reinclude this api\n # @property\n # def table_id(self):\n # return self._anvil.table_id\n\n def get(self, key, default=None):\n if key in self.keys():\n return self[key]\n return default\n\n def keys(self):\n if _is_draft(self):\n return self._anvil.buffer.keys()\n if self._anvil.spec is None:\n # if we don't have a _spec we don't have any keys\n # but we don't need to blindly call _fill_uncached: UNCACHED values are fine\n self._anvil_fill_cache([])\n return self._anvil.cache.keys()\n\n def _anvil_get_view(self):\n fetch = None\n if _is_buffered(self):\n fetch = [k for k in self.keys() if k not in self._anvil.buffer]\n self._anvil_fill_cache(fetch)\n\n view = _copy(self._anvil.cache)\n\n if _batcher.batch_update.active:\n batched = _batcher.batch_update.get_updates(self._anvil.cap)\n view.update(_copy(batched))\n\n if _is_buffered(self):\n view.update(_copy(self._anvil.buffer))\n\n return view\n\n def items(self):\n return self._anvil_get_view().items()\n\n def values(self):\n return self._anvil_get_view().values()\n\n def update(*args, **new_items):\n # avoid name conflicts with columns, could use (self, other, \/, **kws)\n # but positioin only args not available in py2\/Skulpt\n if not args:\n raise TypeError(\"method 'update' of 'Row' object needs an argument\")\n elif len(args) > 2:\n raise TypeError(\"expected at most 1 argument, got %d\" % (len(args) - 1))\n elif len(args) == 2:\n new_items = dict(args[1], **new_items)\n self = args[0]\n if not new_items:\n # backwards compatability hack\n self._anvil_clear_cache()\n return\n\n if _is_buffered(self):\n self._anvil.buffer.update(new_items)\n elif not anvil.is_server_side() and type(self)._Row_permissions_[\"update\"]:\n # If we are on the client and we are a client-updatable model, we should send updates via the\n # server.\n _batcher.flush_and_call(\n \"anvil.tables.v2._update_row_on_server\", self, new_items, [type(self)]\n )\n else:\n self._do_update(new_items, not anvil.is_server_side())\n\n def delete(self):\n if not anvil.is_server_side() and type(self)._Row_permissions_[\"delete\"]:\n # If we are a client-deletable model, we should send delete requests via the server\n _batcher.flush_and_call(\n \"anvil.tables.v2._delete_row_on_server\", self, type(self)\n )\n else:\n self._do_delete(not anvil.is_server_side())\n\n def _do_delete(self, from_client):\n use_client_config = self._anvil_use_client_config(\"delete\", from_client)\n request_overrides = _make_request_overrides(use_client_config)\n\n if _batcher.batch_delete.active:\n return _batcher.batch_delete.push(self._anvil.cap, request_overrides)\n\n _batcher.flush_and_call(PREFIX + \"delete\", self._anvil.cap, request_overrides)\n _send_cap_update(self._anvil.cap, {\"D\": True})\n\n def refresh(self, fetch=None):\n self._anvil_clear_cache()\n if fetch is None:\n self._anvil_fill_cache()\n else:\n self.fetch(fetch)\n\n def fetch(self, fetch):\n from ..query import fetch_only\n\n if not isinstance(fetch, fetch_only):\n nm = type(fetch).__name__\n raise TypeError(\"expected a q.fetch_only() object, got {!r}\".format(nm))\n\n self._anvil_fill_cache(fetch.spec)\n\n @classmethod\n def _anvil_use_client_config(cls, permission, from_client):\n # We're on the server, this request originated on the client, and the model hasn't overridden the permission\n return (\n anvil.is_server_side()\n and from_client\n and not cls._Row_permissions_[permission]\n )\n\n def _anvil_sanitize_update_for_client(self, update):\n if self._anvil.spec is None:\n # this shouldn't happen - we should have our spec by now from the cap updates\n return {}\n\n rv = {}\n\n for col in self._anvil.spec[\"cols\"]:\n name = col[\"name\"]\n client_hidden = col.get(\"client_hidden\")\n if client_hidden:\n continue\n\n val = update.get(name, NOT_FOUND)\n if val is NOT_FOUND:\n continue\n rv[name] = val\n\n return rv\n\n def _anvil_get_cap_update(self):\n if not anvil.is_server_side():\n # this shouldn't happen - we never need to send a cap update on the client to anywhere\n return None\n\n queued_cap_updates = self._anvil.queued_cap_updates\n if not queued_cap_updates:\n return None\n\n # Check if the receiver supports the new cap update format\n use_new_format = not _any_caller_needs_old_format()\n\n if \"D\" in queued_cap_updates:\n if use_new_format:\n return {**VERSION_IDENTIFIER, \"D\": True}\n else:\n return False\n\n rv = {}\n\n update = queued_cap_updates.get(\"U\")\n\n if update is not None:\n if anvil.server.context.remote_caller.is_trusted:\n rv[\"U\"] = update\n else:\n rv[\"U\"] = self._anvil_sanitize_update_for_client(update)\n\n spec = queued_cap_updates.get(\"S\")\n if spec is not None and anvil.server.context.remote_caller.is_trusted:\n # don't send our spec onto the client\n # if we want to then we'll need to filter out client hidden columns from the spec\n rv[\"S\"] = spec\n\n if use_new_format:\n return {**VERSION_IDENTIFIER, **rv}\n else:\n # Old format: return flat dict of updates (no spec for old uplinks)\n return rv.get(\"U\", {})\n\n def _do_update(self, updates, from_client, **kws):\n trusted_updates = kws.pop(\"trusted_updates\", None)\n if kws:\n raise TypeError(\"Unexpected keyword arguments: {}\".format(kws))\n\n use_client_config = self._anvil_use_client_config(\"update\", from_client)\n request_overrides = _make_request_overrides(use_client_config, from_client)\n trusted_overrides = _make_request_overrides(use_client_config)\n\n if _batcher.batch_update.active:\n # a batch update might be on_behalf_of_client, if we are called during a save\n # and the save was from the client\n # and one of the updates does not have client_updatable permissions set\n _batcher.batch_update.push(self._anvil.cap, updates, request_overrides)\n if trusted_updates:\n _batcher.batch_update.push(\n self._anvil.cap, trusted_updates, trusted_overrides\n )\n return\n\n global _make_refs\n if _make_refs is None:\n from ._refs import make_refs # circular import\n\n _make_refs = make_refs\n\n spec = None\n if not trusted_updates:\n spec = _batcher.flush_and_call(\n PREFIX + \"update\",\n self._anvil.cap,\n _make_refs(updates),\n request_overrides,\n )\n else:\n specs = _batcher.flush_and_call(\n PREFIX + \"batch_update\",\n [\n (self._anvil.cap, _make_refs(updates), request_overrides),\n (self._anvil.cap, _make_refs(trusted_updates), trusted_overrides),\n ],\n )\n spec = specs[0] if specs else None\n\n # ok so now we have to clean the updates\n _send_cap_update(self._anvil.cap, {\"S\": spec, \"U\": updates})\n\n @classmethod\n def _do_create(cls, buffer, from_client):\n raise NotImplementedError(\"Must be implemented by a subclass\")\n\n\nclass RowIterator:\n def __init__(self, row):\n self._row = row\n self._fill_required = row._anvil.spec is None and not _is_draft(row)\n if _is_draft(row):\n self._iter = iter(row._anvil.buffer.items())\n else:\n self._iter = iter(row._anvil.cache.items())\n\n def __iter__(self):\n return self\n\n def __next__(self):\n if self._fill_required:\n self._row._anvil_fill_cache()\n self.__init__(self._row)\n\n key, value = next(self._iter)\n\n if _batcher.batch_update.active:\n batched = _batcher.batch_update.read(self._row._anvil.cap, key)\n if batched is not NOT_FOUND:\n value = batched\n\n if not _is_draft(self._row) and key in self._row._anvil.buffer:\n value = self._row._anvil.buffer[key]\n\n if value is UNCACHED:\n # fill the rest of the cache\n # since we probably want all the items!\n # we rely here on the _cache keys not changing during iteration\n # which works since we've filled it with UNCACHED values that match our expected keys\n self._row._anvil_fill_cache()\n value = self._row._anvil.cache[key]\n\n return (key, _copy(value))\n\n next = __next__\n\n\nif anvil.is_server_side():\n import anvil.tables\n\n @anvil.tables.in_transaction(relaxed=True)\n def _save_on_server(changes, from_client):\n from . import get_table_by_id\n\n drafts = []\n\n # First create draft rows from the buffers collected in the save plan.\n # Linked draft references were stripped out while planning and will be\n # stitched back in below, once every referenced draft has a realized row.\n for draft_info in changes[\"draft_info\"]:\n table_id = draft_info[\"table_id\"]\n table = get_table_by_id(table_id)\n from ._model import get_model_cls\n\n model = get_model_cls(table_id)\n rv = model._do_create(draft_info[\"buffer\"], from_client)\n if not isinstance(rv, table.Row):\n raise Exception(\"Row._do_create() must return a Row\")\n drafts.append(rv)\n\n # Each created draft may still need follow-up link updates for values that\n # pointed at other drafts. Keep those separate so `_do_create()` never sees\n # unresolved draft objects.\n draft_buffers = [{} for _ in changes[\"draft_info\"]]\n\n for single in changes[\"single\"]:\n # `single` patches replace a placeholder hole in either a realized row\n # buffer or a draft follow-up buffer with the newly created row.\n path = single[\"path\"]\n row_index = single[\"row\"]\n row = drafts[row_index]\n if path[0] == \"rows\":\n buffer = changes[\"buffers\"][path[1]]\n elif path[0] == \"drafts\":\n buffer = draft_buffers[path[1]]\n key = path[2]\n buffer[key] = row\n\n for multi in changes[\"multi\"]:\n # `multi` works the same way as `single`, but patches list elements\n # that were temporarily encoded as draft indices.\n path = multi[\"path\"]\n rows = multi[\"rows\"]\n for i, row in enumerate(rows):\n if isinstance(row, int):\n rows[i] = drafts[row]\n if path[0] == \"rows\":\n buffer = changes[\"buffers\"][path[1]]\n elif path[0] == \"drafts\":\n buffer = draft_buffers[path[1]]\n key = path[2]\n buffer[key] = rows\n\n with _batcher.batch_update:\n # Apply updates to already-realized rows first, then to drafts that\n # needed a second pass for linked draft values.\n for row, buffer in zip(changes[\"rows\"], changes[\"buffers\"]):\n buffer = buffer or {}\n table = get_table_by_id(row._anvil.table_id)\n if not isinstance(row, Row):\n raise TypeError(\"changes['rows'] must consist of Row objects\")\n row._do_update(buffer, from_client)\n\n for row, buffer in zip(drafts, draft_buffers):\n if not buffer:\n continue\n\n table = get_table_by_id(row._anvil.table_id)\n row._do_update(buffer, from_client)\n\n return drafts\n\n if anvil.server.context.type != \"uplink\":\n\n @anvil.server.callable(\"anvil.tables.v2._save_on_server\")\n def _wrap_save_on_server(changes, models=None):\n return _save_on_server(changes, True)\n\n @anvil.server.callable(\"anvil.tables.v2._update_row_on_server\")\n def _wrap_update_on_server(row, changes, models=None):\n if not isinstance(row, Row):\n raise TypeError(\"Must pass a table row\")\n row._do_update(changes, True)\n\n @anvil.server.callable(\"anvil.tables.v2._delete_row_on_server\")\n def _wrap_delete_on_server(row, models=None):\n if not isinstance(row, Row):\n raise TypeError(\"Must pass a table row\")\n row._do_delete(True)\n\n\ndef _walk_buffered_changes(row, changes, drafts, buffered, seen):\n # Walk the reachable row graph once and populate the shared save plan.\n #\n # For buffered realized rows we record `(row, buffer)` in `changes[\"rows\"]`\n # and `changes[\"buffers\"]`.\n #\n # For drafts we record their current buffer in `changes[\"draft_info\"]`.\n #\n # If any buffered value points at another draft, we cannot serialize that\n # draft object into the plan yet. Instead we:\n # - replace the slot in the copied buffer with `None` (single link) or a\n # draft index (multi link), and\n # - append a patch instruction to `changes[\"single\"]` \/ `changes[\"multi\"]`\n # saying where the realized row should be re-inserted later.\n\n # we can't hash a draft, use the id, because to equal rows might have different buffered changes\n row_key = id(row)\n if row_key in seen:\n return changes\n\n seen[row_key] = {}\n buffer = {}\n path_start = \"rows\"\n index = None\n\n if _is_draft(row):\n buffer = _copy(row._anvil.buffer)\n index = len(drafts)\n seen[row_key][\"index\"] = index\n drafts.append(row)\n changes[\"draft_info\"].append(\n {\"buffer\": buffer, \"table_id\": row._anvil.table_id}\n )\n path_start = \"drafts\"\n\n elif _is_buffered(row):\n buffered.append(row)\n buffer = _copy(row._anvil.buffer)\n if buffer:\n index = len(changes[\"rows\"])\n changes[\"rows\"].append(row)\n changes[\"buffers\"].append(buffer)\n\n else:\n assert not buffer, \"buffer should be empty\"\n\n # Inspect buffered values first: these are the values that matter for save\n # and reset semantics. Cache traversal below only exists so we can discover\n # further reachable rows and clear all affected local buffers.\n for key, val in buffer.items():\n if isinstance(val, Row):\n _walk_buffered_changes(\n val,\n changes=changes,\n seen=seen,\n buffered=buffered,\n drafts=drafts,\n )\n\n if _is_draft(val):\n buffer[key] = None\n val_index = seen[id(val)][\"index\"]\n path = [path_start, index, key]\n changes[\"single\"].append({\"path\": path, \"row\": val_index})\n\n elif isinstance(val, list):\n has_draft = False\n for i, v in enumerate(val):\n if isinstance(v, Row):\n _walk_buffered_changes(\n v,\n changes=changes,\n seen=seen,\n buffered=buffered,\n drafts=drafts,\n )\n\n if _is_draft(v):\n has_draft = True\n val[i] = seen[id(v)][\"index\"]\n\n if has_draft:\n changes[\"multi\"].append({\"path\": [path_start, index, key], \"rows\": val})\n # `_do_create()` \/ `_do_update()` should not see a partially\n # materialized multi-link. Keep the real list in `changes[\"multi\"]`\n # and leave a temporary hole here until the referenced drafts\n # have been realized.\n buffer[key] = None\n\n if row._anvil.spec is None:\n # we don't have anything in our cache that needs changing\n return\n\n # we don't need to worry about drafts in the cache\n for val in row._anvil.cache.values():\n if isinstance(val, Row):\n _walk_buffered_changes(\n val,\n changes=changes,\n seen=seen,\n buffered=buffered,\n drafts=drafts,\n )\n elif isinstance(val, list):\n for v in val:\n if isinstance(v, Row):\n _walk_buffered_changes(\n v,\n changes=changes,\n seen=seen,\n buffered=buffered,\n drafts=drafts,\n )\n\n\ndef _initialize_drafts(server_drafts, drafts):\n # `drafts` contains the original local draft objects. `server_drafts`\n # contains the realized rows returned from `_save_on_server()`, in matching\n # order. Replace each draft's internal state with the realized state while\n # preserving the caller-visible Python object identity.\n assert len(server_drafts) == len(drafts), \"Draft count doesn't match response count\"\n for server_draft, draft in zip(server_drafts, drafts):\n draft_internal = draft._anvil\n assert draft_internal.table_id == server_draft._anvil.table_id, (\n \"Table ids don't match\"\n )\n object.__setattr__(draft, \"_anvil\", _copy(server_draft._anvil))\n draft._anvil.buffer = draft_internal.buffer\n draft._anvil.mode = draft_internal.mode\n\n # mode switches to the default mode - unless we have explicitly set buffer_changes(True)\n if draft._anvil.mode is _MODE.BUFFERED_DRAFT:\n # explicitly set to buffered mode\n draft._anvil.mode = _MODE.BUFFERED\n elif type(draft)._Row_buffered_:\n draft._anvil.mode = _MODE.BUFFERED\n else:\n draft._anvil.mode = _MODE.NORMAL\n\n\ndef _reset_changes(buffered, drafts):\n # `buffered` and `drafts` are kept separate by `_walk_buffered_changes()`\n # because drafts need extra handling during save, but reset semantics are\n # identical: local pending changes are discarded by clearing each buffer.\n for row in buffered:\n row._anvil.buffer.clear()\n\n for row in drafts:\n row._anvil.buffer.clear()\n\n\ndef save_all(*rows):\n # TODO - only check this on the client\n # but wait for auto batching to be implemented\n if not anvil.is_server_side() and _batcher.batch_update.active:\n raise RuntimeError(\n \"Cannot call save() inside a batch_update block on the client\"\n )\n\n changes = _new_save_plan()\n drafts = []\n buffered = []\n seen = {}\n\n for row in rows:\n _walk_buffered_changes(\n row,\n changes=changes,\n drafts=drafts,\n buffered=buffered,\n seen=seen,\n )\n\n # Saving temporarily clears realized-row buffers so any cap updates emitted\n # by `_do_update()` are merged into cache rather than treated as still-local\n # pending changes. If the save fails we restore exactly what the caller had.\n temp_buffers = [{**row._anvil.buffer} for row in buffered]\n _reset_changes(buffered, [])\n server_drafts = []\n\n try:\n if changes[\"draft_info\"] or changes[\"rows\"]:\n if anvil.is_server_side():\n server_drafts = _save_on_server(changes, False)\n else:\n from ._model import serialize_model\n\n table_ids = {d[\"table_id\"] for d in changes[\"draft_info\"]}\n models = [serialize_model(id, True) for id in table_ids]\n server_drafts = _batcher.flush_and_call(\n \"anvil.tables.v2._save_on_server\", changes, models\n )\n except:\n for row, buffer in zip(buffered, temp_buffers):\n row._anvil.buffer.update(buffer)\n raise\n\n # Draft objects are mutated in place to become realized rows, then every\n # local buffer involved in the save is cleared.\n _initialize_drafts(server_drafts, drafts)\n _reset_changes(buffered, drafts)\n\n\ndef reset_all(*rows):\n changes = _new_save_plan()\n drafts = []\n seen = {}\n buffered = []\n\n for row in rows:\n _walk_buffered_changes(\n row,\n changes=changes,\n drafts=drafts,\n buffered=buffered,\n seen=seen,\n )\n\n _reset_changes(buffered, drafts)\n";Sk.builtinFiles.files["anvil-services\/anvil\/tables\/v2\/_search.py"] = "import anvil.server\nfrom anvil.server import Capability\n\nfrom .._base_classes import SearchIterator as BaseSearchIterator\nfrom . import _batcher\nfrom ._constants import CAP_KEY, SERVER_PREFIX, SHARED_DATA_KEY\nfrom ._row import Row\nfrom ._utils import (\n check_serialized,\n clean_row_data,\n clean_table_spec,\n init_spec_rows,\n init_view_data,\n merge_row_data,\n validate_cap,\n)\n\nPREFIX = SERVER_PREFIX + \"search.\"\n\n\nclass PartialSearchIter(object):\n def __init__(self, s, slice_):\n self._view_key = s._view_key\n self._table_id = s._table_id\n self._cap = s._cap\n self._idx = slice_.start or 0\n self._step = slice_.step or 1\n self._stop = slice_.stop\n row_ids, cap_next = s._row_ids, s._cap_next\n if row_ids is None:\n # this can happen in deserialization from untrusted\/None transmited data\n row_ids, cap_next = [], s._cap\n assert cap_next is None or type(cap_next) is Capability\n self._reset(row_ids, cap_next, s._table_data)\n\n def _reset(self, row_ids, cap_next, table_data):\n if self._stop is not None and len(row_ids) > self._stop:\n row_ids, cap_next = row_ids[: self._stop], None\n self._row_ids = row_ids\n self._cap_next = cap_next\n self._table_data = table_data\n\n def _iter_next_page(self):\n if self._cap_next is None:\n raise StopIteration\n\n num_row_ids = len(self._row_ids)\n self._idx -= num_row_ids\n if self._stop is not None:\n self._stop -= num_row_ids\n\n row_ids, cap_next, table_data = _batcher.flush_and_call(\n PREFIX + \"next_page\", self._cap_next\n )\n\n self._reset(row_ids, cap_next, table_data)\n return self.__next__()\n\n def __iter__(self):\n return self\n\n def __next__(self):\n try:\n row_id = self._row_ids[self._idx]\n except IndexError:\n return self._iter_next_page()\n self._idx += self._step\n return Row._anvil_create_from_trusted(\n self._view_key, self._table_id, row_id, self._table_data\n )\n\n next = __next__\n\n\n@anvil.server.portable_class\nclass SearchIterator(BaseSearchIterator):\n @classmethod\n def _create(cls, view_key, table_id, row_ids, cap, cap_next, table_data):\n self = object.__new__(cls)\n assert cap_next is None or type(cap_next) is Capability\n self._view_key = view_key\n self._table_id = table_id\n self._row_ids = row_ids\n self._cap = cap\n self._cap_next = cap_next\n self._table_data = table_data\n self._from_serialize = False\n return self\n\n @classmethod\n def __new_deserialized__(cls, data, info):\n view_key, table_id, row_ids, cap, cap_next = data\n table_data, _ = info.shared_data(SHARED_DATA_KEY)\n if not info.remote_is_trusted:\n validate_cap(cap, table_id)\n table_data = None\n if not table_data:\n row_ids = cap_next = None\n # when we deserialize ourselves we may have more data than we need\n self = cls._create(view_key, table_id, row_ids, cap, cap_next, table_data)\n self._from_serialize = True\n return self\n\n def _fill_data(self):\n self._row_ids, self._cap_next, self._table_data = _batcher.flush_and_call(\n PREFIX + \"next_page\", self._cap\n )\n\n def _clear_cache(self):\n self._row_ids = self._table_data = self._cap_next = None\n\n # SERIALIZATION\n def _make_row_data(self, row_data, table_spec, compact=True):\n if type(row_data) is dict or compact:\n # this row didn't match our cache_spec so just send it\n # or we are list and we're compact because our cache_specs already match\n return row_data\n\n cache_spec = table_spec[\"cache\"]\n # we are currently compact and we need to be a dict\n new_data = {CAP_KEY: row_data[-1]}\n iter_row_data = iter(row_data)\n\n new_data = {\n str(i): next(iter_row_data)\n for i, is_cached in enumerate(cache_spec)\n if is_cached\n }\n cap = next(iter_row_data)\n assert type(cap is Capability)\n new_data[CAP_KEY] = cap\n return new_data\n\n def _get_table_view_iter(self):\n if not self._from_serialize:\n # Fast Path - we were created from my_table.search() so the table_data is already minimal\n # i.e. we don't need to clean it based on table_specs\n return self._table_data.keys()\n\n # Slow Path - we're reserializing ourselves from a previous serialization\n # so we may have too much data if we were serialized with merged table_data\n table_view_keys = set()\n # walk the table_specs and insert the view_keys and table_ids we need\n _populate_table_views_ids(self._view_key, self._table_data, table_view_keys)\n return table_view_keys\n\n def _merge(self, g_table_data, local_data, remote_is_trusted):\n if check_serialized(self, local_data):\n return\n\n table_view_keys = self._get_table_view_iter()\n\n for view_key in table_view_keys:\n g_view_data = init_view_data(view_key, g_table_data)\n l_view_data = self._table_data[view_key]\n\n l_table_spec = clean_table_spec(l_view_data[\"spec\"], remote_is_trusted)\n l_table_rows = l_view_data.get(\"rows\", {})\n g_table_spec, g_table_rows = init_spec_rows(g_view_data, l_table_spec)\n\n g_cache_spec = g_table_spec[\"cache\"]\n l_cache_spec = l_table_spec[\"cache\"]\n cache_match = g_table_spec is l_table_spec or g_cache_spec == l_cache_spec\n\n for row_id, row_data in l_table_rows.items():\n if isinstance(row_data, Row):\n # Ok we've already been created\n # this is rare - we've consumed the search iterator and now we're serializing\n # or we created this row from shared serialization data and we're now reserializing\n row = row_data\n row._anvil_merge_and_reduce(\n g_table_data, local_data, remote_is_trusted\n )\n continue\n\n \n row_data = clean_row_data(row_data, l_view_data[\"spec\"], remote_is_trusted)\n\n g_row_data = g_table_rows.get(row_id, [])\n g_is_compact = cache_match and type(g_row_data) is list\n row_data = self._make_row_data(\n row_data, l_view_data[\"spec\"], compact=g_is_compact\n )\n merge_row_data(\n row_id, row_data, g_table_rows, g_table_spec, l_cache_spec\n )\n\n def __serialize__(self, info):\n table_data, local_data = info.shared_data(SHARED_DATA_KEY)\n row_ids = self._row_ids\n if table_data is None:\n row_ids = self._cap_next = None\n elif info.local_is_trusted and self._table_data is not None:\n self._merge(table_data, local_data, info.remote_is_trusted)\n return [self._view_key, self._table_id, row_ids, self._cap, self._cap_next]\n\n def _make_partial_iterator(self, slice_=slice(None)):\n return PartialSearchIter(self, slice_)\n\n def __iter__(self):\n return self._make_partial_iterator()\n\n def __len__(self):\n if self._cap_next is None and self._row_ids is not None:\n return len(self._row_ids)\n return _batcher.flush_and_call(PREFIX + \"get_length\", self._cap)\n\n def __hash__(self):\n return hash((self._table_id, self._cap))\n\n def __eq__(self, other):\n if not isinstance(other, SearchIterator):\n return NotImplemented\n return self._cap == other._cap\n\n def __bool__(self):\n # because we have a __len__ and we can't suspend\n return True\n\n __nonzero__ = __bool__\n\n def refresh(self):\n self._clear_cache()\n\n def to_csv(self, escape_for_excel=False):\n return _batcher.flush_and_call(\n PREFIX + \"to_csv\", self._cap, escape_for_excel=escape_for_excel\n )\n\n def delete_all_rows(self):\n result = _batcher.flush_and_call(PREFIX + \"delete_all\", self._cap)\n self._clear_cache()\n return result\n\n def __getitem__(self, idx):\n if self._row_ids is None:\n self._fill_data()\n\n if isinstance(idx, slice):\n slice_ = slice(\n as_slice_idx(idx.start), as_slice_idx(idx.stop), as_slice_idx(idx.step)\n )\n return self._make_partial_iterator(slice_)\n else:\n slice_ = slice(as_idx(idx), None)\n try:\n return next(self._make_partial_iterator(slice_))\n except StopIteration:\n raise IndexError(\"search index out of range\")\n\n\ndef as_idx(i, msg=\"search indices must be non-negative integers\", can_be_none=False):\n if i is None and can_be_none:\n return None\n elif type(i) is int:\n pass\n elif hasattr(i, \"__index__\"):\n i = i.__index__()\n else:\n raise TypeError(msg)\n if i < 0:\n raise ValueError(msg)\n return i\n\n\ndef as_slice_idx(i):\n msg = \"search slice indices must non-negative itegers (or None)\"\n return as_idx(i, msg, True)\n\n\ndef _populate_table_views_ids(view_key, table_data, seen):\n # We might hold too much data if our table_data was from another serialization\n # If we're reserializing ourselves then this method prevents sending unnecessary data across the wire\n if view_key in seen:\n # prevent circular references\n return\n\n try:\n table_spec = table_data[view_key][\"spec\"]\n except KeyError:\n # Then these linked rows were not included in the data - probably uncached from the cache spec\n # don't try include this view_key when serializing the data\n return\n\n seen.add(view_key)\n cols = table_spec[\"cols\"]\n\n for col in cols:\n view_key = col.get(\"view_key\")\n if view_key is None:\n continue\n _populate_table_views_ids(view_key, table_data, seen)\n";Sk.builtinFiles.files["anvil-services\/anvil\/tables\/v2\/_table.py"] = "import anvil.server\nfrom anvil.server import Capability\n\nfrom .._base_classes import Table as BaseTable\nfrom ._constants import CASCADE, KNOWN_PERMS, READ, SERVER_PREFIX, WRITE\nfrom ._model import get_base_model_cls\nfrom ._refs import make_refs\nfrom ._row import Row\nfrom ._search import SearchIterator\nfrom ._utils import validate_cap\nfrom . import _batcher\n\nPREFIX = SERVER_PREFIX + \"table.\"\n\n\n@anvil.server.portable_class\nclass Table(BaseTable):\n @classmethod\n def _create(cls, cap, view_key, table_id):\n assert cap is None or type(cap) is Capability, \"expected a table capability\"\n self = object.__new__(cls)\n self._cap = cap\n self._view_key = view_key\n table_id = str(table_id)\n self._id = table_id # N.B. Customers depend on this because there's no public API. Yell loudly before changing.\n self.Row = get_base_model_cls(table_id)\n return self\n\n @classmethod\n def __new_deserialized__(cls, data, info):\n cap, view_key, table_id = data\n if not info.remote_is_trusted:\n validate_cap(cap, table_id)\n return cls._create(cap, view_key, table_id)\n\n def __serialize__(self, _info):\n return [self._cap, self._view_key, self._id]\n\n def __iter__(self):\n raise TypeError(\n \"You can't iterate on a table. Call search() on this table to get an iterator of rows instead.\"\n )\n\n def __eq__(self, other):\n if not isinstance(other, Table):\n return NotImplemented\n return other._id == self._id\n\n def __hash__(self):\n return hash(self._id)\n\n def __contains__(self, row):\n return self.has_row(row)\n\n def _get_view(self, perm, args, kws):\n assert perm in KNOWN_PERMS, \"bad permission\"\n new_cap, view_key = _batcher.flush_and_call(\n PREFIX + \"get_view\", self._cap, perm, None, make_refs(args), make_refs(kws)\n )\n return Table._create(new_cap, view_key, self._id)\n\n # PUBLIC API\n def restrict_columns(self, col_spec):\n new_cap, view_key = _batcher.flush_and_call(\n \"get_restricted_columns\", self._cap, col_spec\n )\n return Table._create(new_cap, view_key, self._id)\n\n def client_readable(self, *args, **kws):\n return self._get_view(READ, args, kws)\n\n def client_writable(self, *args, **kws):\n return self._get_view(WRITE, args, kws)\n\n def client_writable_cascade(self, *args, **kws):\n return self._get_view(CASCADE, args, kws)\n\n def delete_all_rows(self):\n return _batcher.flush_and_call(PREFIX + \"delete_all_rows\", self._cap)\n\n def add_rows(self, rows):\n # rows can be an iterable of dicts\n row_dicts = []\n refs = []\n for row in rows:\n row = dict(row)\n refs.append(make_refs(row))\n row_dicts.append(row)\n row_id_caps, spec = anvil.server.call(PREFIX + \"add_rows\", self._cap, refs)\n return [\n self.Row._anvil_create_from_local_values(\n self._view_key, self._id, row_id, spec, cap, row_items\n )\n for (row_id, cap), row_items in zip(row_id_caps, row_dicts)\n ]\n\n def add_row(self, **data):\n return self._do_add_row(data)\n\n def _do_add_row(self, data, client_request_overrides=None, trusted_values=None):\n row_id, cap, spec = anvil.server.call(\n PREFIX + \"add_row\",\n self._cap,\n make_refs(data),\n client_request_overrides,\n make_refs(trusted_values) if trusted_values else None,\n )\n return self.Row._anvil_create_from_local_values(\n self._view_key, self._id, row_id, spec, cap, data\n )\n\n def get(self, *args, **kws):\n row_id_table_data = _batcher.flush_and_call(\n PREFIX + \"get_row\", self._cap, make_refs(args), make_refs(kws)\n )\n return row_id_table_data and self.Row._anvil_create_from_trusted(\n self._view_key, self._id, *row_id_table_data\n )\n\n def get_by_id(self, row_id, fetch=None):\n row_id_table_data = _batcher.flush_and_call(\n PREFIX + \"get_row_by_id\", self._cap, row_id, fetch=fetch\n )\n return row_id_table_data and self.Row._anvil_create_from_trusted(\n self._view_key, self._id, *row_id_table_data\n )\n\n def has_row(self, row):\n if not isinstance(row, Row):\n # backwards compatability return False\n return False\n elif row._anvil.table_id != self._id:\n return False\n return _batcher.flush_and_call(PREFIX + \"has_row\", self._cap, row._anvil.id)\n\n def list_columns(self):\n return _batcher.flush_and_call(PREFIX + \"list_columns\", self._cap)\n\n def search(self, *args, **kws):\n kws = make_refs(kws)\n row_ids, cap, cap_next, table_data = _batcher.flush_and_call(\n PREFIX + \"search\", self._cap, args, kws\n )\n return SearchIterator._create(\n self._view_key, self._id, row_ids, cap, cap_next, table_data\n )\n\n def to_csv(self, escape_for_excel=False):\n return _batcher.flush_and_call(\n PREFIX + \"to_csv\", self._cap, escape_for_excel=escape_for_excel\n )\n\n # TODO reinclude this API\n # @property\n # def id(self):\n # return self._id\n";Sk.builtinFiles.files["anvil-services\/anvil\/tables\/v2\/_utils.py"] = "import anvil\nimport anvil.tz\nfrom anvil.server import Capability, unwrap_capability\n\nfrom ._constants import CAP_KEY, NOT_FOUND, UNCACHED\n\n\nclass InternalDict:\n pass\n\n\nThreadLocal = object\n\nif anvil.is_server_side():\n try:\n from anvil._threaded_server import ThreadLocal\n except ImportError:\n pass\n\n\nSPECIAL_ATTRS = (\"__dict__\", \"__class__\", \"__module__\")\n\n\ndef maybe_handle_descriptors(self, attr, val):\n if attr in SPECIAL_ATTRS:\n object.__setattr__(self, attr, val)\n return True\n\n maybe_descriptor = getattr(type(self), attr, None)\n if hasattr(maybe_descriptor, \"__set__\"):\n object.__setattr__(self, attr, val)\n return True\n \n return False\n\n\ndef validate_cap(cap, table_id, row_id=NOT_FOUND):\n # this function ensures that the cap is the right shape and references the right table\/row\n # full validation happens in clojure\n _, _, view_dict, narrowed, _ = unwrap_capability(\n cap, [\"_\", \"t\", Capability.ANY, Capability.ANY, Capability.ANY]\n )\n assert str(view_dict[\"id\"]) == table_id\n if row_id is not NOT_FOUND:\n assert row_id == str(narrowed[\"r\"])\n\n\ndef clean_local_datetime(d):\n if d.tzinfo is not None:\n offset = d.utcoffset().total_seconds()\n else:\n offset = anvil.tz.tzlocal().utcoffset(d).total_seconds()\n return d.replace(tzinfo=anvil.tz.tzoffset(seconds=offset))\n\n\n# Serialization helpers\ndef check_serialized(self, local_data):\n self_id = id(self)\n serialized = local_data.get(self_id, False)\n local_data[self_id] = True\n return serialized\n\n\ndef init_view_data(view_key, g_table_data):\n return g_table_data.setdefault(view_key, {})\n\n\ndef clean_row_data_dict(row_data, table_spec, remote_is_trusted):\n if remote_is_trusted:\n return row_data\n\n if table_spec is None:\n return row_data\n\n rv = {}\n\n orig_i = 0\n new_i = 0\n\n for col, is_cached in zip(table_spec[\"cols\"], table_spec[\"cache\"]):\n if not is_cached:\n orig_i += 1\n new_i += 1\n continue\n\n if col.get(\"client_hidden\", False):\n orig_i += 1\n continue\n\n val = row_data.get(str(orig_i), NOT_FOUND)\n if val is not NOT_FOUND:\n rv[str(new_i)] = val\n new_i += 1\n orig_i += 1\n\n rv[CAP_KEY] = row_data[CAP_KEY]\n return rv\n\n\ndef clean_row_data_list(row_data, table_spec, remote_is_trusted):\n if remote_is_trusted:\n return row_data\n\n if table_spec is None:\n return row_data\n\n rv = []\n\n # We should have cached data for all cached columns (plus the Capability at the end)\n assert len(table_spec[\"cache\"]) == len(table_spec[\"cols\"])\n assert (len(row_data) - 1) == sum(table_spec[\"cache\"])\n\n row_data_iter = iter(row_data[:-1])\n for (col, is_cached) in zip(table_spec[\"cols\"], table_spec[\"cache\"]):\n if not is_cached:\n continue\n\n data = next(row_data_iter)\n if not col.get(\"client_hidden\", False):\n rv.append(data)\n \n rv.append(row_data[-1])\n\n return rv\n\n\ndef clean_row_data(row_data, table_spec, remote_is_trusted):\n if isinstance(row_data, dict):\n return clean_row_data_dict(row_data, table_spec, remote_is_trusted)\n\n if isinstance(row_data, list):\n return clean_row_data_list(row_data, table_spec, remote_is_trusted)\n\n raise TypeError(\"Invalid row data\")\n\n\ndef clean_table_spec(table_spec, remote_is_trusted):\n if remote_is_trusted:\n return table_spec\n\n if table_spec is None:\n return table_spec\n\n rv_table_spec = {\"cols\": [], \"cache\": []}\n\n for col, init_cache in zip(table_spec[\"cols\"], table_spec[\"cache\"]):\n if not col.get(\"client_hidden\", False):\n rv_table_spec[\"cols\"].append(col)\n rv_table_spec[\"cache\"].append(init_cache)\n\n return rv_table_spec\n\n\ndef clean_cache_table_spec(cache_spec, table_spec, remote_is_trusted):\n if remote_is_trusted:\n return cache_spec, table_spec\n\n if table_spec is None:\n return cache_spec, table_spec\n\n rv_cache_spec = []\n rv_table_spec = {\"cols\": [], \"cache\": []}\n\n assert len(cache_spec) == len(table_spec[\"cache\"]) == len(table_spec[\"cols\"]), (\n \"cache spec length mismatch\"\n )\n\n for col, init_cache, is_cached in zip(\n table_spec[\"cols\"], table_spec[\"cache\"], cache_spec\n ):\n if not col.get(\"client_hidden\", False):\n rv_cache_spec.append(is_cached)\n rv_table_spec[\"cols\"].append(col)\n rv_table_spec[\"cache\"].append(init_cache)\n\n return rv_cache_spec, rv_table_spec\n\n\ndef init_spec_rows(g_view_data, table_spec, cache_spec=None):\n g_table_spec = g_view_data.get(\"spec\")\n if g_table_spec is not None:\n pass\n elif table_spec is None or cache_spec is None:\n g_table_spec = g_view_data[\"spec\"] = table_spec\n else:\n g_table_spec = g_view_data[\"spec\"] = {\n \"cols\": table_spec[\"cols\"],\n \"cache\": cache_spec,\n }\n g_table_rows = g_view_data.setdefault(\"rows\", {})\n return g_table_spec, g_table_rows\n\n\ndef merge_row_data(row_id, row_data, g_table_rows, g_table_spec, row_cache_spec):\n # we've already cleaned the row_data\n # - it will only be a compact list if the caches match\n # - and g_row_data is either None or also a compact list\n # otherwise row_data will be a dict\n g_row_data = g_table_rows.get(row_id)\n\n # FAST - common case - nothing in row_data\n if g_row_data is None:\n g_table_rows[row_id] = row_data\n return\n\n g_row_type = type(g_row_data)\n row_type = type(row_data)\n\n # handle all UNCACHED - i.e. the partially cached writer wins\n if g_row_type is list and len(g_row_data) == 1:\n # the row serialized before us has an all 0 cache_spec and is compact\n # we are either a dict or a list of the same length\n g_table_rows[row_id] = row_data\n return\n if not any(row_cache_spec):\n # the row to merge has an all 0 cache_spec\n return\n\n # SLOW PATH - uncommon cases\n # Another reference to this row (not the exact same row) was already serialized before us\n if row_type is list:\n # g_row_data must also be a compact list if row_data is a list\n # they must have the same length at this stage since we know the cache specs match\n if g_row_type is list:\n # fail safe sanity check\n merge_compact(row_data, g_row_data)\n\n elif g_row_type is dict:\n # then the previously serialized reference to this row\n # didn't match the g_cache_spec\n # so just take the itersect of the dictionaries\n g_table_rows[row_id] = merge_dicts(row_data, g_row_data)\n return\n else:\n # finally the g_row_type is a compact list and we are a dict - make it a dict\n g_cache_spec = g_table_spec[\"cache\"]\n merge_dict_with_compact(row_data, g_row_data, row_cache_spec, g_cache_spec)\n g_table_rows[row_id] = row_data\n\n\ndef merge_compact(row_data, g_row_data):\n # any conflicts just replace with UNCACHED sentinel\n # use len - 1 so we skip the Capability\n for i in range(len(row_data) - 1):\n gbl, loc = g_row_data[i], row_data[i]\n if gbl != loc:\n g_row_data[i] = UNCACHED\n\n\ndef merge_dicts(row_data, g_row_data):\n # walk the smallest\n merged = {}\n a, b = (\n (row_data, g_row_data)\n if len(row_data) < len(g_row_data)\n else (g_row_data, row_data)\n )\n cap = a.pop(CAP_KEY)\n for key, a_val in a.items():\n b_val = b.get(key, NOT_FOUND)\n if a_val == b_val:\n merged[key] = a_val\n merged[CAP_KEY] = a[CAP_KEY] = cap\n return a\n\n\ndef merge_dict_with_compact(row_data, g_row_data, row_cache_spec, g_cache_spec):\n iter_g_row_data = iter(g_row_data)\n for i, (is_cached, g_is_cached) in enumerate(zip(row_cache_spec, g_cache_spec)):\n i = str(i)\n if not g_is_cached:\n # we could use the incoming caller wins here\n if is_cached:\n row_data.pop(i, None)\n continue\n\n g_val = next(iter_g_row_data)\n if not is_cached:\n continue\n\n if i in row_data and row_data[i] != g_val:\n row_data.pop(i)\n\n return row_data\n";Sk.builtinFiles.files["anvil-services\/anvil\/email.py"] = "import anvil.server\n\n#!defModule(anvil.email)!1: \"The `anvil.email` module contains functions for sending and receiving email in your Anvil app.\"\n\n#!suggestAttr(anvil.email,send)!0:\n\n#!defClass(anvil.email,SendFailure)!:\nclass SendFailure(anvil.server.AnvilWrappedError):\n pass\n\nanvil.server._register_exception_type(\"anvil.email.SendFailure\", SendFailure)\n\nclass DeliveryFailure(Exception):\n #!defMethod(_,message=None,smtp_code=554)!2: \n # {anvil$helpLink: \"\/docs\/email\/sending_and_receiving#rejecting-email\", $doc: \"While handling an error, you can raise a DeliveryFailure exception to reject email delivery. Optionally, you may specify a message and SMTP error code with the rejection.\"} [\"__init__\"]\n def __init__(self, message=None, smtp_code=None):\n if message is None:\n super(DeliveryFailure, self).__init__()\n elif smtp_code is not None:\n message = \"{}: {}\".format(smtp_code, message)\n super(DeliveryFailure, self).__init__(message)\n #!defClass(anvil.email,DeliveryFailure)!:\n\n\n\n#!defFunction(anvil.email,anvil.email.SendReport instance,[to=],[cc=],[bcc=],[from_address=\"no-reply\"],[from_name=],[subject=],[text=],[html=],[attachments=],[inline_attachments=])!2:\n# {\n# $doc: \"Send an email\",\n# anvil$helpLink: \"\/docs\/email\",\n# anvil$args: {\n# to: \"The email recipient[s] in the 'To' field. Can be a string or list of strings.\\n\\nEach string can be a bare address (eg 'joe@example.com') or include a display name (eg 'Joe Bloggs ').\",\n# cc: \"The email recipient[s] in the 'Cc' field. Can be a string or list of strings.\\n\\nEach string can be a bare address (eg 'joe@example.com') or include a display name (eg 'Joe Bloggs ').\",\n# bcc: \"The email recipient[s] in the 'Bcc' field. Can be a string or list of strings.\\n\\nEach string can be a bare address (eg 'joe@example.com') or include a display name (eg 'Joe Bloggs ').\",\n# from_address: \"The From: address from this email. Can be a bare address (eg 'joe@example.com') or include a display name (eg 'Joe Bloggs ').\\n\\nIf no domain is specified, or the specified domain is not a legal sending domain for this app, the address will be replaced with a valid domain. So if you specify 'noreply', the email will come from 'noreply@your-app-domain.anvil.app'.\",\n# from_name: \"The name associated with the From: address for this email. (Only valid if the from_address is a bare email address.)\",\n# subject: \"The subject line for this email.\",\n# text: \"The plain-text (no HTML) content for this email. You must specify at least one of 'text' and 'html'.\",\n# html: \"The HTML content for this email. You must specify at least one of 'text' and 'html'.\",\n# attachments: \"A list of Media objects to send as attachments with this email.\",\n# inline_attachments: \"Inline that can be used in this email's HTML, for example in tags. Must be a dictionary whose keys are IDs and values are Media objects. IDs can then be used in a message's HTML with 'cid:xxx' URIs.\",\n# }\n# } [\"send\"]\ndef send(**kw):\n return anvil.server.call(\"anvil.private.email.send.v2\", **kw)\n\n# NB no defFunction() here; this one is defined in the autocompleter\ndef handle_message(fn=None, require_dkim=False):\n def wrapper(fn):\n import functools # don't try to import this on the client\n @functools.wraps(fn)\n def handler(msg_dict):\n msg = Message(msg_dict)\n if require_dkim and not msg.dkim.valid_from_sender:\n raise DeliveryFailure(\"No valid DKIM signature for %s\" % msg.envelope.from_address)\n fn(msg)\n return anvil.server.callable(\"email:handle_message\")(handler)\n return wrapper(fn) if fn is not None else wrapper\n\n\n@anvil.server.portable_class\nclass Address(object):\n\n #!defAttr()!1: {name:\"address\",type:\"string\",description:\"The email address this object represents.\"}\n #!defAttr()!1: {name:\"name\",type:\"string\",description:\"The name associated with the address this object represents.\"}\n #!defAttr()!1: {name:\"raw_value\",type:\"string\",description:\"The full string value of this address.\"}\n def __init__(self, address):\n self.address = address['address']\n self.name = address['name']\n self.raw_value = address['raw']\n\n #!defClass(anvil.email,#Address)!:\n\n\n@anvil.server.portable_class\nclass Message(object):\n #!defAttr()!1: {name:\"from_address\",type:\"string\",description:\"The email address from which this message was sent, according to the SMTP envelope.\"}\n #!defAttr()!1: {name:\"recipient\",type:\"string\",description:\"The email address that received this message.\\n\\nNote that this email address may not appear in any of the headers (eg if the email has been BCCed or blind forwarded).\"}\n @anvil.server.portable_class\n class Envelope(object):\n def __init__(self, envelope):\n self.from_address = envelope['from']\n self.recipient = envelope['recipient']\n #!defClass(anvil.email.Message,#Envelope)!:\n\n #!defAttr()!1: {name:\"valid_from_sender\",type:\"boolean\",description:\"Was this message signed by the domain in its envelope \\\"from\\\" address?\"}\n #!defAttr()!1: {name:\"domains\",type:\"list(string)\",description:\"A list of the DKIM domains that signed this message.\"}\n @anvil.server.portable_class\n class DKIM(object):\n def __init__(self, dkim):\n self.valid_from_sender = dkim['valid_from_sender']\n self.domains = dkim['domains']\n #!defClass(anvil.email.Message,#DKIM)!:\n\n\n #!defAttr()!1: {name:\"to_addresses\",pyType:\"list(anvil.email.Address instance)\",description:\"The addresses this message was sent to.\"}\n #!defAttr()!1: {name:\"from_address\",pyType:\"anvil.email.Address instance\",description:\"The address this message was sent from.\"}\n #!defAttr()!1: {name:\"cc_addresses\",pyType:\"list(anvil.email.Address instance)\",description:\"The addresses this message was copied to.\"}\n @anvil.server.portable_class\n class Addressees(object):\n def __init__(self, addressees):\n self.to_addresses = [Address(a) for a in addressees.get('to',[])]\n self.from_address = Address(addressees['from'][0]) if 'from' in addressees else None\n self.cc_addresses = [Address(a) for a in addressees.get('cc',[])]\n #!defClass(anvil.email.Message,#Addressees)!:\n\n\n #!defAttr()!1: {name:\"envelope\",pyType:\"anvil.email.Message.Envelope instance\",description:\"The sender and receipient of this email, according to the SMTP envelope.\"}\n #!defAttr()!1: {name:\"dkim\",pyType:\"anvil.email.Message.DKIM instance\",description:\"Object describing whether this message was signed by the sending domain\"}\n #!defAttr()!1: {name:\"addressees\",pyType:\"anvil.email.Message.Addressees instance\",description:\"The addresses this email was sent from and to, according to the headers.\"}\n #!defAttr()!1: {name:\"headers\",type:\"list\",description:\"All the headers in this email, as a list of (name,value) pairs.\"}\n #!defAttr()!1: {name:\"text\",type:\"string\",description:\"The plain-text content of this email, or None if there is no plain-text part.\"}\n #!defAttr()!1: {name:\"subject\",type:\"string\",description:\"The subject of this email, or None if there is no subject.\"}\n #!defAttr()!1: {name:\"html\",type:\"string\",description:\"The HTML content of this email, or None if there is no HTML part.\"}\n #!defAttr()!1: {name:\"attachments\",pyType:\"list(anvil.Media instance)\",description:\"A list of this email's attachments.\"}\n #!defAttr()!1: {name:\"inline_attachments\",pyType:\"dict(string,anvil.Media instance)\",description:\"A dictionary of this email's inline attachments. Keys are ContentID headers, values are the attachments as Media Objects.\"}\n\n def __init__(self, msg_dict):\n self.envelope = Message.Envelope(msg_dict['envelope'])\n self.dkim = Message.DKIM(msg_dict['dkim'])\n self.addressees = Message.Addressees(msg_dict['addressees'])\n self.headers = msg_dict['headers']\n self.subject = msg_dict['subject']\n self.text = msg_dict['text']\n self.html = msg_dict['html']\n self.attachments = msg_dict['attachments']\n self.inline_attachments = msg_dict['inline_attachments']\n\n #!defMethod(_,header_name,[default=None])!2: \"Return the value of the specified header, or default value if it is not present.\\n\\nCase-insensitive. If the header is specified multiple times, returns the first value.\" [\"get_header\"]\n def get_header(self, header_name, default=None):\n header_name = header_name.lower()\n for name,value in self.headers:\n if name.lower() == header_name:\n return value\n return default\n\n #!defMethod(_,header_name)!2: \"Return a list containing every value of the specified header. Case-insensitive.\" [\"list_header\"]\n def list_header(self, header_name):\n header_name = header_name.lower()\n return [value for name,value in self.headers\n if name.lower() == header_name]\n\n #!defMethod(_,[cc=],[bcc=],[from_address=],[from_name=],[text=],[html=],[attachments=])!2: \"Reply to this email.\" [\"reply\"]\n def reply(self,**kw):\n kw['to'] = kw.get('to', self.get_header(\"Reply-To\", None))\n if kw['to'] is None:\n if self.addressees.from_address is not None:\n kw['to'] = self.addressees.from_address.raw_value\n else:\n kw['to'] = self.envelope.from_address\n if kw['to'] is None:\n raise Exception(\"Cannot reply to a message with no Reply-To header, From address, or Envelope From address.\")\n\n kw['subject'] = kw.get('subject', self.subject)\n kw['in_reply_to'] = self.get_header(\"Message-ID\")\n if kw['in_reply_to']:\n kw['references'] = self.get_header(\"References\", \"\") + \" \" + kw['in_reply_to']\n kw['from_address'] = kw.get('from_address', self.envelope.recipient)\n send(**kw)\n\n def __str__(self):\n\n truncated_text = \"\"\n if self.text:\n truncated_text = self.text.replace(\"\\n\", \" \\\\ \")\n (truncated_text[:70] + '...') if len(truncated_text) > 70 else truncated_text,\n\n return \"\"\"anvil.email.Message:\n from: %s\n to: %s\n subject: %s\n text: %s\n attachments: %s\"\"\" % (\n self.addressees.from_address and self.addressees.from_address.raw_value,\n len(self.addressees.to_addresses) > 0 and self.addressees.to_addresses[0].raw_value,\n self.subject,\n truncated_text,\n \", \".join([\"%s (%s bytes)\" % (a.name, len(a.get_bytes())) for a in self.attachments]) if len(self.attachments) > 0 else None\n )\n\n #!defClass(anvil.email,#Message)!:\n\n\n@anvil.server.portable_class\nclass SendReport(object):\n\n #!defAttr()!1: {name: \"message_id\", type: \"string\", description: \"The Message-ID header given to this outgoing message.\"}\n\n def __init__(self):\n raise Exception(\"Cannot construct a SendReport manually\")\n\n #!defClass(anvil.email,#SendReport)!:\n";const loadApp = window.loadApp({"app":{"allow_embedding":false,"dependency_code":{},"package_name":"Simple_Website_Template","startup":{"type":"form","module":"Main"},"config":{"client":{}},"modules":[],"name":"Simple Website Template","dependency_ids":{},"startup_form":null,"dependency_order":[],"theme":{"html":{"standard-page.html":"\n\n