# pyquickbase API reference: Policies This describes version 1.0.0rc13. Other pages: [Quickbase](https://matthew-williams-solutions.github.io/pyquickbase/reference/quickbase.txt), [App](https://matthew-williams-solutions.github.io/pyquickbase/reference/app.txt), [Table](https://matthew-williams-solutions.github.io/pyquickbase/reference/table.txt), [Report](https://matthew-williams-solutions.github.io/pyquickbase/reference/report.txt), [Operation](https://matthew-williams-solutions.github.io/pyquickbase/reference/operation.txt), [SchemaOperation](https://matthew-williams-solutions.github.io/pyquickbase/reference/schema-operation.txt), [Queries](https://matthew-williams-solutions.github.io/pyquickbase/reference/queries.txt), [Records and results](https://matthew-williams-solutions.github.io/pyquickbase/reference/records.txt), [Metadata](https://matthew-williams-solutions.github.io/pyquickbase/reference/metadata.txt), [Schema changes](https://matthew-williams-solutions.github.io/pyquickbase/reference/schema-changes.txt), [Rules](https://matthew-williams-solutions.github.io/pyquickbase/reference/rules.txt), [Retries](https://matthew-williams-solutions.github.io/pyquickbase/reference/retries.txt), [Tools for models](https://matthew-williams-solutions.github.io/pyquickbase/reference/tools.txt), [Errors](https://matthew-williams-solutions.github.io/pyquickbase/reference/errors.txt). A [`Policy`](https://matthew-williams-solutions.github.io/pyquickbase/reference/policies/#Policy) says what a client may read and change. Bind it to an app, which pins every table and field by ID, and pass the [`BoundPolicy`](https://matthew-williams-solutions.github.io/pyquickbase/reference/policies/#BoundPolicy) to [`Quickbase`](https://matthew-williams-solutions.github.io/pyquickbase/reference/quickbase/) as `policy`. It protects against bugs in your code and mistakes by a model, so give your token access to as little as possible as well, as Quickbase's own permissions are still the real boundary. Each example makes its own client from the environment, with `bqtasks123` standing for the ID of the `Job Tasks` table. A live test runs every example with both clients and checks its output. ## Write and bind a policy For the full picture, see [Policies](https://matthew-williams-solutions.github.io/pyquickbase/policies/). ### `Policy` What a client may do, as rules over tables and fields. Bind it to an app, then give the [`BoundPolicy`](https://matthew-williams-solutions.github.io/pyquickbase/reference/policies/#BoundPolicy) to a client. ```python pay = ["Technicians.Hourly Rate"] policy = Policy( classes={"pay": pay}, rules=[ allow.read(tables.all()), allow.update( "Job Tasks", ["Status", "Notes"] ), deny.all(fields.classed("pay")), ], ) app = qb.app("bq1234567") bound = await policy.bind(app) ``` Anything no rule allows is refused, and a deny always wins. A formula, lookup, or summary is in every class of the fields it reads, through any chain of them, so it's denied wherever they are. ```python Policy( *, app: str | None = None, classes: Mapping[ str, Sequence[str | tuple[str, str | int] | FieldSelector], ] | None = None, rules: Sequence[PolicyRule | Rule] = (), new_fields: Literal["read", "hidden"] | str = "read", ) ``` **Parameters** - **`app`** `str | None`, optional The app's ID, which `bind()` checks, or None for any. - **`classes`** `Mapping[str, Sequence[str | tuple[str, str | int] | FieldSelector]] | None`, optional Names for groups of fields, as `{name: [fields]}`, for `fields.classed()`. Each field is `"Table.Field"`, `("Table", "Field")`, or a [`fields`](https://matthew-williams-solutions.github.io/pyquickbase/reference/rules/#fields) selector, such as `fields.labeled_like("_*")`. - **`rules`** `Sequence[PolicyRule | Rule]`, default `()` What's allowed and refused, from [`allow`](https://matthew-williams-solutions.github.io/pyquickbase/reference/rules/#allow), [`deny`](https://matthew-williams-solutions.github.io/pyquickbase/reference/rules/#deny), and [`limit`](https://matthew-williams-solutions.github.io/pyquickbase/reference/rules/#limit), and any [`Rule`](https://matthew-williams-solutions.github.io/pyquickbase/reference/rules/#Rule). - **`new_fields`** `Literal['read', 'hidden'] | str`, default `'read'` What a field added after binding gets: `read`, to be read, filtered, and sorted by where a rule allows it, `hidden`, or a class's name, which also leaves it read-only. It's never writable until the policy is bound again. **Attributes** - **`needs_app`** `bool` Whether anything in it names a table or field, so it needs `bind()` before use. **Raises** - [`ConfigurationError`](https://matthew-williams-solutions.github.io/pyquickbase/reference/errors/#ConfigurationError): A rule or class can't be used. #### `Policy.read_only()` Read anything the token can, with no limit on records, and change nothing. It needs no binding, so a client takes it as it is. ```python read_only() -> Policy ``` **Returns** [`Policy`](https://matthew-williams-solutions.github.io/pyquickbase/reference/policies/#Policy) #### `Policy.bind()` Resolve every table, field, and class against an app, once. Every problem is reported at once. With an [`AsyncApp`](https://matthew-williams-solutions.github.io/pyquickbase/reference/app/), this returns an awaitable. It reads the app's tables, each table's fields and relationships, and the reports of any table `allow.report()` lists by name, so use a client that can see them all. Quickbase only shows relationships to a role that can edit the app's structure. With any other role this still binds, but if the policy hides a field or limits rows, every lookup and summary field is hidden too, because what they read can't be traced. ```python bind( app: AsyncApp, ) -> Coroutine[Any, Any, BoundPolicy] bind(app: App) -> BoundPolicy ``` **Parameters** - **`app`** `App | AsyncApp`, required The app, from a client without a policy. **Returns** A [`BoundPolicy`](https://matthew-williams-solutions.github.io/pyquickbase/reference/policies/#BoundPolicy), which is frozen and keyed by ID. **Raises** - [`ConfigurationError`](https://matthew-williams-solutions.github.io/pyquickbase/reference/errors/#ConfigurationError): A table, field, report, or class can't be found, or is ambiguous. The message lists every one. #### `Policy.bind_offline()` Bind a policy that names no table or field, without reading an app. Every field then counts as existing, so `new_fields` doesn't apply. ```python bind_offline() -> BoundPolicy ``` **Returns** [`BoundPolicy`](https://matthew-williams-solutions.github.io/pyquickbase/reference/policies/#BoundPolicy) **Raises** - [`ConfigurationError`](https://matthew-williams-solutions.github.io/pyquickbase/reference/errors/#ConfigurationError): It names a table, field, class, report, or row limit. Sync example: ```python from pyquickbase import ( Quickbase, Policy, allow, deny, fields, tables, ) policy = Policy( classes={"internal": [("Job Tasks", "Code")]}, rules=[ allow.read(tables.all(), max_paging_depth=50), allow.update("Job Tasks", ["Status"]), deny.all(fields.classed("internal")), ], ) with Quickbase.from_env() as setup: bound = policy.bind(setup.app("bq1234567")) with Quickbase.from_env(policy=bound) as qb: tasks = qb.app("bq1234567").table("Job Tasks") print( [ item.label for item in tasks.fields() if item.id > 5 ] ) ``` Async example: ```python from pyquickbase import ( AsyncQuickbase, Policy, allow, deny, fields, tables, ) policy = Policy( classes={"internal": [("Job Tasks", "Code")]}, rules=[ allow.read(tables.all(), max_paging_depth=50), allow.update("Job Tasks", ["Status"]), deny.all(fields.classed("internal")), ], ) async with AsyncQuickbase.from_env() as setup: bound = await policy.bind(setup.app("bq1234567")) async with AsyncQuickbase.from_env(policy=bound) as qb: tasks = qb.app("bq1234567").table("Job Tasks") print( [ item.label for item in await tasks.fields() if item.id > 5 ] ) ``` Output: ```text ['Title', 'Status', 'Hours', 'Due Date', 'Attachment'] ``` ### `BoundPolicy` A [`Policy`](https://matthew-williams-solutions.github.io/pyquickbase/reference/policies/#Policy) resolved against one app, keyed by ID, which decides every request a client sends. It's frozen: a client that creates a table gets a new one, as `client.policy`. Quickbase never reuses a field's ID within a table, so an ID pinned here can't come to mean another field. A field added after binding gets the policy's `new_fields`, and a renamed one keeps its rules, until the policy is bound again. **Attributes** - **`created`** `tuple[str, ...]` The IDs of the tables this client created after binding. - **`hash`** `str` A fingerprint of everything the policy decides with, which confirmations hold. #### `BoundPolicy.classes_of()` The classes a field is in, including those it reads through formulas, lookups, and summaries. ```python classes_of( table_id: str, field_id: int ) -> frozenset[str] ``` **Parameters** - **`table_id`** `str`, required The table's ID. - **`field_id`** `int`, required The field's ID. **Returns** `frozenset[str]` The class names, which are empty for a field in none. #### `BoundPolicy.decide()` Decide whether a request may be sent. This is the only code that decides. ```python decide( request: ReadRequest | WriteRequest, ) -> Decision ``` **Parameters** - **`request`** `ReadRequest | WriteRequest`, required From `describe_read()` or `describe_write()`. **Returns** [`Decision`](https://matthew-williams-solutions.github.io/pyquickbase/reference/policies/#Decision) A [`Decision`](https://matthew-williams-solutions.github.io/pyquickbase/reference/policies/#Decision). A refusal names the first part refused, and the rule. #### `BoundPolicy.allows()` Whether the policy allows a verb on a table, or on some of its fields. ```python bound.allows( "update", "Job Tasks", field="Labor Cost", ) bound.allows( "lookup", "Job Tasks", source=("Technicians", "Hourly Rate"), ) ``` ```python allows( verb: Verb, table: str, *, field: str | int | None = None, fields: Sequence[str | int] = (), source: tuple[str, str | int] | None = None, ) -> Decision ``` **Parameters** - **`verb`** `Verb`, required Such as `read`, `update`, or `lookup`. - **`table`** `str`, required The table, by name or ID. - **`field`** `str | int | None`, optional One field, by label or ID. - **`fields`** `Sequence[str | int]`, default `()` Several fields. - **`source`** `tuple[str, str | int] | None`, optional For `lookup` and `summary`, the other table's field the new one would show, which needs `read`. **Returns** [`Decision`](https://matthew-williams-solutions.github.io/pyquickbase/reference/policies/#Decision) A [`Decision`](https://matthew-williams-solutions.github.io/pyquickbase/reference/policies/#Decision). **Raises** - [`ConfigurationError`](https://matthew-williams-solutions.github.io/pyquickbase/reference/errors/#ConfigurationError): The policy doesn't know the table. - [`UnknownFieldError`](https://matthew-williams-solutions.github.io/pyquickbase/reference/errors/#UnknownFieldError): The policy doesn't know a field. #### `BoundPolicy.explain()` What `allows()` decides, in words, with every rule that covers the table, and the classes of the fields involved. ```python explain( verb: Verb, table: str, *, field: str | int | None = None, fields: Sequence[str | int] = (), source: tuple[str, str | int] | None = None, ) -> str ``` **Parameters** - **`verb`** `Verb`, required Such as `read`, `update`, or `lookup`. - **`table`** `str`, required The table, by name or ID. - **`field`** `str | int | None`, optional One field, by label or ID. - **`fields`** `Sequence[str | int]`, default `()` Several fields. - **`source`** `tuple[str, str | int] | None`, optional For `lookup` and `summary`, the other table's field the new one would show. **Returns** `str` Several lines of text. #### `BoundPolicy.max_records()` The most records one query may reach in a table, or None for no limit. ```python max_records(table_id: str) -> int | None ``` **Parameters** - **`table_id`** `str`, required The table's ID. **Returns** `int | None` #### `BoundPolicy.row_limit()` The condition every query and delete of a table is ANDed with, by field ID, or None. ```python row_limit(table_id: str) -> str | None ``` **Parameters** - **`table_id`** `str`, required The table's ID. **Returns** `str | None` #### `BoundPolicy.offers()` What the policy allows on a table, by field, as an operation's schema offers it. ```python offers(table: str) -> Offers[Named] ``` **Parameters** - **`table`** `str`, required The table, by name or ID. **Returns** [`Offers`](https://matthew-williams-solutions.github.io/pyquickbase/reference/policies/#Offers) `[` [`Named`](https://matthew-williams-solutions.github.io/pyquickbase/reference/policies/#Named) `]` An [`Offers`](https://matthew-williams-solutions.github.io/pyquickbase/reference/policies/#Offers), whose lists hold each field's [`Named`](https://matthew-williams-solutions.github.io/pyquickbase/reference/policies/#Named). **Raises** - [`ConfigurationError`](https://matthew-williams-solutions.github.io/pyquickbase/reference/errors/#ConfigurationError): The policy doesn't know the table, or was bound without an app. #### `BoundPolicy.to_json()` Save the bound policy, to use in another process without binding again. Custom [`Rule`](https://matthew-williams-solutions.github.io/pyquickbase/reference/rules/#Rule)s can't be saved, so pass them again to `from_json()`. ```python to_json() -> str ``` **Returns** `str` #### `BoundPolicy.from_json()` Load a bound policy that `to_json()` saved. ```python from_json( text: str, *, rules: Sequence[Rule] = () ) -> BoundPolicy ``` **Parameters** - **`text`** `str`, required What `to_json()` returned. - **`rules`** `Sequence[Rule]`, default `()` The custom [`Rule`](https://matthew-williams-solutions.github.io/pyquickbase/reference/rules/#Rule)s the policy had, which can't be saved. **Returns** [`BoundPolicy`](https://matthew-williams-solutions.github.io/pyquickbase/reference/policies/#BoundPolicy) **Raises** - [`ConfigurationError`](https://matthew-williams-solutions.github.io/pyquickbase/reference/errors/#ConfigurationError): The text isn't a saved policy, or the custom rules don't match. Sync example: ```python from pyquickbase import ( Quickbase, BoundPolicy, Policy, allow, ) policy = Policy( rules=[allow.read("Job Tasks", ["Title", "Status"])] ) with Quickbase.from_env() as setup: bound = policy.bind(setup.app("bq1234567")) print(bound.explain("read", "Job Tasks", field="Hours")) saved = bound.to_json() print(BoundPolicy.from_json(saved) == bound) ``` Async example: ```python from pyquickbase import ( AsyncQuickbase, BoundPolicy, Policy, allow, ) policy = Policy( rules=[allow.read("Job Tasks", ["Title", "Status"])] ) async with AsyncQuickbase.from_env() as setup: bound = await policy.bind(setup.app("bq1234567")) print(bound.explain("read", "Job Tasks", field="Hours")) saved = bound.to_json() print(BoundPolicy.from_json(saved) == bound) ``` Output: ```text Refused: reading Hours (8) in Job Tasks (bqtasks123). Layer: policy. Rule: none. Reason: No rule allows it. To allow it: add "Hours" to an allow.read("Job Tasks", ...) rule. Rules that cover it: allow.read('Job Tasks', ['Title', 'Status']). True ``` ## Decisions For the full picture, see [Debugging refusals](https://matthew-williams-solutions.github.io/pyquickbase/policies/debugging/). ### `Decision` What a [`BoundPolicy`](https://matthew-williams-solutions.github.io/pyquickbase/reference/policies/#BoundPolicy) decided about a request, and why. It holds no values from the request, so it can be logged. A [`Rule`](https://matthew-williams-solutions.github.io/pyquickbase/reference/rules/#Rule) of your own returns one to refuse a request. ```python Decision( *, allowed: bool, verb: Verb, layer: Layer = "policy", rule: str | None = None, reason: str = "", hint: str | None = None, table: Named | None = None, fields: tuple[Named, ...] = (), ) ``` **Parameters** - **`allowed`** `bool`, required Whether the request may be sent. - **`verb`** `Verb`, required What was asked, such as `update`, for the first part refused, or the last allowed. - **`layer`** `Layer`, default `'policy'` Which check decided: `policy` for the rules, `rows` for a row limit, and `derived` for what a formula, lookup, or summary reads. A preview's confirmation isn't part of the policy, and raises [`ConfirmationError`](https://matthew-williams-solutions.github.io/pyquickbase/reference/errors/#ConfirmationError). - **`rule`** `str | None`, optional The rule that decided, as written, such as `deny.all(fields.classed("pay"))`, or None when nothing allowed it. - **`reason`** `str`, default `''` Why, in a sentence. - **`hint`** `str | None`, optional What would allow it, or None when nothing should, such as for a deny. - **`table`** `Named | None`, optional The table it's about, if any. - **`fields`** `tuple[Named, ...]`, default `()` The fields it's about, if any. **Attributes** - **`what`** `str` What was asked, by name, such as `updating Labor Cost (6) in Job Tasks (bqtasks)`. #### `Decision.message()` The refusal's message, in the form [`PolicyRefusedError`](https://matthew-williams-solutions.github.io/pyquickbase/reference/errors/#PolicyRefusedError) uses. ```python message() -> str ``` **Returns** `str` Sync example: ```python from pyquickbase import Quickbase, Policy, allow policy = Policy(rules=[allow.update("Job Tasks", ["Status"])]) with Quickbase.from_env() as setup: bound = policy.bind(setup.app("bq1234567")) decision = bound.allows("update", "Job Tasks", field="Status") print(decision.allowed, decision.rule) print( bound.allows( "update", "Job Tasks", field="Hours" ).message() ) ``` Async example: ```python from pyquickbase import AsyncQuickbase, Policy, allow policy = Policy(rules=[allow.update("Job Tasks", ["Status"])]) async with AsyncQuickbase.from_env() as setup: bound = await policy.bind(setup.app("bq1234567")) decision = bound.allows("update", "Job Tasks", field="Status") print(decision.allowed, decision.rule) print( bound.allows( "update", "Job Tasks", field="Hours" ).message() ) ``` Output: ```text True allow.update('Job Tasks', ['Status']) Refused by policy: updating Hours (8) in Job Tasks (bqtasks123). No rule allows it. (rule: none) Nothing was sent. To allow it: add "Hours" to an allow.update("Job Tasks", ...) rule. ``` ### `Named` A table or field, by its ID and its name when the policy was bound. **Attributes** - **`id`** `str | int` The table's ID, or the field's. - **`name`** `str` The table's name, or the field's label. Its ID, as text, if the policy doesn't know it. Sync example: ```python from pyquickbase import Named print(Named("bqtasks123", "Job Tasks")) ``` Async example: ```python from pyquickbase import Named print(Named("bqtasks123", "Job Tasks")) ``` Output: ```text Job Tasks (bqtasks123) ``` ### `Offers` What a [`BoundPolicy`](https://matthew-williams-solutions.github.io/pyquickbase/reference/policies/#BoundPolicy) allows on one table, for building an operation's schema. Each list comes from asking `decide()` about one field and one verb at a time. **Attributes** - **`table`** `Named` The table. - **`readable`** `tuple[T, ...]` Fields it may return. - **`filterable`** `tuple[T, ...]` Fields it may filter on. - **`sortable`** `tuple[T, ...]` Fields it may sort by. - **`insertable`** `tuple[T, ...]` Fields a new record may set. - **`updatable`** `tuple[T, ...]` Fields an update may change. - **`downloadable`** `tuple[T, ...]` File fields it may download from. - **`file_deletable`** `tuple[T, ...]` File fields it may delete versions from. - **`can_delete`** `bool` Whether it may delete records. - **`reports`** `tuple[Any, ...]` Reports it may run. - **`schema`** `frozenset[str]` The schema verbs allowed on the table, such as `add_field`. - **`field_types`** `frozenset[str] | None` The field types `add_field` may create, or None for any. - **`max_paging_depth`** `int | None` The most records one query may reach, or None for no limit. - **`max_records_scanned`** `int | None` The most records one summary may read, or None for no limit. - **`max_records_per_delete`** `int | None` The most records one delete may remove, or None for no limit. - **`user_emails`** `bool` Whether user values keep email addresses. - **`row_limit`** `str | None` The condition every query is limited to, or None. Sync example: ```python from pyquickbase import Quickbase, Policy, allow policy = Policy( rules=[ allow.read("Job Tasks", ["Title", "Status"]), allow.update("Job Tasks", ["Status"]), ] ) with Quickbase.from_env() as setup: bound = policy.bind(setup.app("bq1234567")) offers = bound.offers("Job Tasks") print([item.name for item in offers.readable]) print([item.name for item in offers.updatable]) print(offers.can_delete, offers.max_paging_depth) ``` Async example: ```python from pyquickbase import AsyncQuickbase, Policy, allow policy = Policy( rules=[ allow.read("Job Tasks", ["Title", "Status"]), allow.update("Job Tasks", ["Status"]), ] ) async with AsyncQuickbase.from_env() as setup: bound = await policy.bind(setup.app("bq1234567")) offers = bound.offers("Job Tasks") print([item.name for item in offers.readable]) print([item.name for item in offers.updatable]) print(offers.can_delete, offers.max_paging_depth) ``` Output: ```text ['Record ID#', 'Title', 'Status'] ['Status'] False 100 ```