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
appstr | None, optional
The app's ID, whichbind()checks, or None for any.classesMapping[str, Sequence[str | tuple[str, str | int] | FieldSelector]] | None, optional
Names for groups of fields, as{name: [fields]}, forfields.classed(). Each field is"Table.Field",("Table", "Field"), or afieldsselector, such asfields.labeled_like("_*").rulesSequence[PolicyRule | Rule], default()
What's allowed and refused, fromallow,deny, andlimit, and anyRule.new_fieldsLiteral['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_appbool
Whether anything in it names a table or field, so it needsbind()before use.
Raises
ConfigurationError: A rule or class can't be used.
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
appApp | 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
ConfigurationError: It names a table, field, class, report, or row limit.
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
createdtuple[str, ...]
The IDs of the tables this client created after binding.hashstr
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_idstr, required
The table's ID.field_idint, 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
requestReadRequest | WriteRequest, required
Fromdescribe_read()ordescribe_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
verbVerb, required
Such asread,update, orlookup.tablestr, required
The table, by name or ID.fieldstr | int | None, optional
One field, by label or ID.
2 more: fields, source
fieldsSequence[str | int], default()
Several fields.sourcetuple[str, str | int] | None, optional
Forlookupandsummary, the other table's field the new one would show, which needsread.
Returns Decision
A Decision.
Raises
ConfigurationError: The policy doesn't know the table.UnknownFieldError: The policy doesn't know a field.
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
verbVerb, required
Such asread,update, orlookup.tablestr, required
The table, by name or ID.fieldstr | int | None, optional
One field, by label or ID.
2 more: fields, source
fieldsSequence[str | int], default()
Several fields.sourcetuple[str, str | int] | None, optional
Forlookupandsummary, 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_idstr, 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_idstr, 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
tablestr, required
The table, by name or ID.
An Offers, whose lists hold each field's Named.
Raises
ConfigurationError: The policy doesn't know the table, or was bound without an app.
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
textstr, required
Whatto_json()returned.rulesSequence[Rule], default()
The customRules the policy had, which can't be saved.
Returns BoundPolicy
Raises
ConfigurationError: The text isn't a saved policy, or the custom rules don't match.
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
allowedbool, required
Whether the request may be sent.verbVerb, required
What was asked, such asupdate, for the first part refused, or the last allowed.layerLayer, default'policy'
Which check decided:policyfor the rules,rowsfor a row limit, andderivedfor what a formula, lookup, or summary reads. A preview's confirmation isn't part of the policy, and raisesConfirmationError.
5 more: rule, reason, hint, table, fields
rulestr | None, optional
The rule that decided, as written, such asdeny.all(fields.classed("pay")), or None when nothing allowed it.reasonstr, default''
Why, in a sentence.hintstr | None, optional
What would allow it, or None when nothing should, such as for a deny.tableNamed | None, optional
The table it's about, if any.fieldstuple[Named, ...], default()
The fields it's about, if any.
Attributes
whatstr
What was asked, by name, such asupdating 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.
message() -> str
Named¶
A table or field, by its ID and its name when the policy was bound.
Attributes
idstr | int
The table's ID, or the field's.namestr
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
tableNamed
The table.readabletuple[T, ...]
Fields it may return.filterabletuple[T, ...]
Fields it may filter on.sortabletuple[T, ...]
Fields it may sort by.insertabletuple[T, ...]
Fields a new record may set.updatabletuple[T, ...]
Fields an update may change.downloadabletuple[T, ...]
File fields it may download from.file_deletabletuple[T, ...]
File fields it may delete versions from.can_deletebool
Whether it may delete records.reportstuple[Any, ...]
Reports it may run.schemafrozenset[str]
The schema verbs allowed on the table, such asadd_field.field_typesfrozenset[str] | None
The field typesadd_fieldmay create, or None for any.max_paging_depthint | None
The most records one query may reach, or None for no limit.max_records_scannedint | None
The most records one summary may read, or None for no limit.max_records_per_deleteint | None
The most records one delete may remove, or None for no limit.user_emailsbool
Whether user values keep email addresses.row_limitstr | 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