Skip to content

Markdown

Policies

A 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 to 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.

Policy

What a client may do, as rules over tables and fields. Bind it to an app, then give the BoundPolicy to a client.

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.

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 selector, such as fields.labeled_like("_*").
  • rules Sequence[PolicyRule | Rule], default ()
    What's allowed and refused, from allow, deny, and limit, and any 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

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",
)

Example

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
        ]
    )
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

['Title', 'Status', 'Hours', 'Due Date', 'Attachment']

Make a read-only policy

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.

Returns Policy

read_only() -> Policy

Bind to an app

Resolve every table, field, and class against an app, once.

Every problem is reported at once. With an AsyncApp, 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.

Parameters

  • app App | AsyncApp, required
    The app, from a client without a policy.

Returns

A BoundPolicy, which is frozen and keyed by ID.

Raises

  • ConfigurationError: A table, field, report, or class can't be found, or is ambiguous. The message lists every one.
bind(
    app: AsyncApp,
) -> Coroutine[Any, Any, BoundPolicy]
bind(app: App) -> BoundPolicy

Bind without an app

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.

Returns BoundPolicy

Raises

bind_offline() -> BoundPolicy

BoundPolicy

A 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.

Example

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)
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

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

Get a field's classes

The classes a field is in, including those it reads through formulas, lookups, and summaries.

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.

classes_of(
    table_id: str, field_id: int
) -> frozenset[str]

Decide a request

Decide whether a request may be sent. This is the only code that decides.

Parameters

  • request ReadRequest | WriteRequest, required
    From describe_read() or describe_write().

Returns Decision

A Decision. A refusal names the first part refused, and the rule.

decide(
    request: ReadRequest | WriteRequest,
) -> Decision

Check what's allowed

Whether the policy allows a verb on a table, or on some of its fields.

bound.allows(
    "update",
    "Job Tasks",
    field="Labor Cost",
)
bound.allows(
    "lookup",
    "Job Tasks",
    source=("Technicians", "Hourly Rate"),
)

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.
2 more: fields, source
  • 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

A Decision.

Raises

allows(
    verb: Verb,
    table: str,
    *,
    field: str | int | None = None,
    fields: Sequence[str | int] = (),
    source: tuple[str, str | int] | None = None,
) -> Decision

Explain what's allowed

What allows() decides, in words, with every rule that covers the table, and the classes of the fields involved.

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.
2 more: fields, source
  • 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.

explain(
    verb: Verb,
    table: str,
    *,
    field: str | int | None = None,
    fields: Sequence[str | int] = (),
    source: tuple[str, str | int] | None = None,
) -> str

Get a table's record limit

The most records one query may reach in a table, or None for no limit.

Parameters

  • table_id str, required
    The table's ID.

Returns int | None

max_records(table_id: str) -> int | None

Get a table's row limit

The condition every query and delete of a table is ANDed with, by field ID, or None.

Parameters

  • table_id str, required
    The table's ID.

Returns str | None

row_limit(table_id: str) -> str | None

Get what an operation may offer

What the policy allows on a table, by field, as an operation's schema offers it.

Parameters

  • table str, required
    The table, by name or ID.

Returns Offers [ Named ]

An Offers, whose lists hold each field's Named.

Raises

offers(table: str) -> Offers[Named]

Save a bound policy

Save the bound policy, to use in another process without binding again.

Custom Rules can't be saved, so pass them again to from_json().

Returns str

to_json() -> str

Load a bound policy

Load a bound policy that to_json() saved.

Parameters

  • text str, required
    What to_json() returned.
  • rules Sequence[Rule], default ()
    The custom Rules the policy had, which can't be saved.

Returns BoundPolicy

Raises

from_json(
    text: str, *, rules: Sequence[Rule] = ()
) -> BoundPolicy

Decisions

For the full picture, see Debugging refusals.

Decision

What a BoundPolicy decided about a request, and why. It holds no values from the request, so it can be logged. A Rule of your own returns one to refuse a request.

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.
5 more: rule, reason, hint, table, fields
  • 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(
    *,
    allowed: bool,
    verb: Verb,
    layer: Layer = "policy",
    rule: str | None = None,
    reason: str = "",
    hint: str | None = None,
    table: Named | None = None,
    fields: tuple[Named, ...] = (),
)

Example

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()
)
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

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.

Get the refusal's message

The refusal's message, in the form PolicyRefusedError uses.

Returns str

message() -> str

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.

Example

from pyquickbase import Named

print(Named("bqtasks123", "Job Tasks"))
from pyquickbase import Named

print(Named("bqtasks123", "Job Tasks"))

Output

Job Tasks (bqtasks123)

Offers

What a 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.

Example

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)
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

['Record ID#', 'Title', 'Status']
['Status']
False 100