Operating¶
Use these methods to delete, suppress or withdraw what a committed database serves, and to reach the server it runs under. Because nothing serves a database before its first commit, remove, suppress, unsuppress, leave, drop_layer, drop_view, compact and status are refused until then.
Database¶
remove
¶
remove(items: Any, strict: bool = False) -> ChangeReport
Delete items from the database, and return a report of what was deleted.
The items stop being served as soon as the call returns, and their rows are removed from
disc at the next compaction, which compact() asks for.
items: the items to delete, as onemosaica_id, a list of them or a table. A table is a pandas or polars data frame, a pyarrow table or a dict of columns, and each of its rows names one item by itsmosaica_idcolumn or by its values of the unique attributes, which it carries in the columns an insert reads them from.strict:Truerefuses the whole request when a row names no item, or names two, and the call then raisesRefusal.
Without strict, a row that names no item or two is listed in the report as refused and the
other rows are applied. A None in the list, or a row whose cells are all null, names no
item. A table's columns that name no item are not sent, and the report lists them in
ignored_columns, but a table with no column that names items is refused before anything is
sent. A mosaica_id may also name an annotation, while a layer's own is refused, because
drop_layer is how a layer is removed.
A call in which the server refused every request raises Refusal, with the report as its
report.
db.remove([mosaica_id])
db.remove({"paper": ["paper-17", "paper-23"]})
suppress
¶
suppress(items: Any, strict: bool = False) -> ChangeReport
Hide items from every reader until unsuppress shows them again, and return a report.
A suppressed item stays stored and is left out of every answer to a request that starts after the call returns.
items,strict: as forremove, except that a layer'smosaica_idhides the layer and its annotations.
unsuppress
¶
unsuppress(items: Any, strict: bool = False) -> ChangeReport
Lift the suppression of items, so that their readers see them again, and return a report.
items,strict: as forremove.
addresses
¶
addresses(items: Any) -> list[dict]
The rows that name the items given in a change request, as a list of dictionaries.
Each mosaica_id in a list becomes {"mosaica_id": ...}, and each row of a table becomes
its mosaica_id value and its values of the unique attributes, under the attributes' names.
A None or a null cell stays None, and every other value is written as text: an integer
in decimal digits, a timestamp as the decimal digits of its microseconds since the epoch,
and a string as itself.
leave
¶
leave(
layer: str,
key: str,
items: Any,
rank: int = 0,
level: int = 0,
view: str | None = None,
strict: bool = False,
) -> ChangeReport
Take items out of the set of items a label's text was written from, and return a report.
A label declared with content_requires="all" is shown only to a reader who may see every
item its text was written from, so once an item is taken out of that set, a reader no
longer needs to see it to be shown the label.
layer: the label set.key: the label's key.items,strict: as forremove.rank: which of the label's texts to change, by its position among them, which is 0 by default. A rank keeps its number when a text before it is withdrawn.level: the level the label is at, which is 0 by default.view: the view the label belongs to, on a layer scoped to a view group.
Taking out every item withdraws the text, and its rank is never used again, so inserting
the label with the same texts adds each withdrawn one as a new text at the next free rank.
A call the server refuses raises Refusal, with the report as its report.
drop_layer
¶
drop_layer(name: str, wait: bool = False) -> dict
Remove an annotation layer or a label set, and return the server's answer.
The layer and its uncommitted inserts also leave this database's declaration, so that the
name can be declared again, as a new layer that none of the dropped layer's mosaica_ids
names.
name: the name of the layer or label set.wait:Truereturns only once readers no longer see the layer, with the time limit thatControl.flushdescribes.
The answer's publication names the publication from which readers no longer see the layer.
drop_view
¶
drop_view(group: str, key: str, wait: bool = False) -> dict
Remove one view of a view group, and return the server's answer.
Items that the drop leaves in no view are deleted, which cannot be undone, and the
answer's deleted says how many there were. Rows sent for the view that readers cannot
see yet are dropped with it.
group,key: the group and the key of the view"<group>:<key>".wait: as fordrop_layer.
compact
¶
compact(
wait: bool = False, progress: bool | Callable[[dict], Any] = True
) -> dict
Ask the server to remove the rows of deleted items from disc now.
The server compacts each shard that has something to reclaim, and a request made while a
compaction runs waits until that compaction has finished. When no compaction can start
until the server is restarted, the call raises Refusal with the reason and requests
nothing.
wait:False, the default, returns{"request": n}once the server has accepted the request, before the work is done, andcontrol.compaction_request(n)says where the request is.Truewaits asControl.await_compactiondoes and returns{"request": n}with what it returned merged in. Where itsokis false, it raisesRefusalwith the server'sdetail, from the answer or from the reading that ended the wait, that same dictionary as the refusal'sreport, and the HTTP status of that reading as itsstatus.-
progress: how the wait shows the compaction's record, ascommittakes it. The default isTrue.db.compact(wait=True)
status
¶
status() -> dict
The server's report on the state of this database, as a dictionary.
The report says how far writes have got, how deep the server's queues are and how large a
page each write request accepts. In it, overlay counts the items deleted or suppressed
that no compaction has removed yet, and layer_spaces counts the layers in count, the
deleted or suppressed layers and annotations in depth and the deleted ones alone in
retirable. shards.records holds one record per shard.
serve
¶
serve() -> Listening
Start the database's server if it is not running, print the address it serves at, and return the addresses it listens on.
Reads and later commits call this themselves. The server listens on this machine only, on
the port stated at create() or open(), or else on the one its mosaica.toml names.
To see the map from another machine, forward that port to the same port there, as
ssh -L 9141:127.0.0.1:9141 host does for 9141.
control
property
¶
control: Control
A Control client for this database's control plane.
Use it for what Database has no method for. The client sends the operator credential,
and reading this property starts the server if it is not running.
ChangeReport
dataclass
¶
What a call to remove, suppress, unsuppress or leave did.
op: the change, as"delete","suppress","unsuppress"or"leave <layer>/<key> rank <rank>".requested,accepted: how many rows the call was given, and how many were applied.refused: each row refused because of how it named its item, as{"row", "item", "reason"}, whererowis its position in what was given,itemis the row as given, such as{"mosaica_id": "17"}, andreasonisnames_no_item,names_two_itemsorunknown_mosaica_id. The other rows were applied, andrefused_by_reasoncounts these.ignored_columns: the columns given that were not sent, or that the server ignored, because they are neithermosaica_idnor a unique attribute's column.refusals: each request the server refused, with its status and answer.
ok is True when every row given was applied.
summary
¶
summary() -> list[str]
The lines print() shows: the rows given and applied, up to fifty refused rows with
their reasons, the columns ignored, then the refused requests. The first line ends ok
only when every row was applied.
Listening
dataclass
¶
The addresses a database's server listens on, each as host:port, which
Database.serve() returns. The three are one address unless the database's mosaica.toml
gives a plane an address of its own.
viewer: the viewer plane, which readers read from with a token.session: the session plane, where the operator credential or an API key holdingauthorise-asmakes and revokes tokens.control: the control plane, where the operator declares and writes.
ServeRefused
¶
Bases: Refusal
The Refusal raised when the server a database starts does not report the addresses it
listens on.
Database.serve() raises it, as does any call that starts the server, such as the first
commit(). A server that prints no line naming its viewer, session and control addresses
within the wait is stopped, and the message holds the last lines it wrote to its output and
error streams. A server whose line leaves an address out is stopped too, and the message
quotes the line. The wait is 30 seconds unless the MOSAICA_SERVE_TIMEOUT environment
variable names another, so raise it on a machine where the server takes longer to start.
Control
¶
Control(base: str, credential: str | None = None, timeout: float = 300.0)
A client for the control plane of a served database, where the operator declares, writes, deletes and manages who may read.
Database.control returns one for a database you made, and Control(base, credential) makes
one for a database someone else runs. Each route method sends one HTTP request and returns its
Answer without raising, so a refusal is read from the answer's status and detail. The
exceptions are await_compaction, which reads compaction_request until the request is
answered and returns how that ended, and compact(wait=True), which calls it. Both raise
Refusal for a progress they do not take, and pass on what a progress function raises.
When a busy server answers 429, the method waits as long as the server asks, up to 30
seconds, and sends the request again. It returns the 429 once the waits would pass five
minutes in all.
base: the control plane's address, such ashttp://127.0.0.1:41234.credential: the operator credential, an API key or an OIDC access token, orNonefor a server started withmosaica serve --open, which needs none. A method that writes, flushes or compacts needs thewritepermission, one that declares or drops needswrite-allas well, andstatusand the catalogue methods needadmin. A credential that holdswritewithoutwrite-allcan name only what its terms let it see.timeout: how long to wait for one answer, in seconds. The default is 300.
The declaring methods take a block of a declaration in JSON, as mosaica check --payloads
prints it. Declaring a thing again exactly as it stands answers 200 and changes nothing, so a
declaration is safe to send twice. A different declaration under a name in use is refused,
except where a method says otherwise.
The catalogue methods manage who may read and write. A catalogue change ends every session whose
access it could alter, and its answer counts them in sessions_ended; create_key answers with
the new key instead.
status
¶
status() -> dict
Return the server's status report as a dictionary (GET /control/status).
overlay counts the deletions and suppressions of items that no compaction has yet removed,
and compaction reports the requests compact makes. When the server refuses the request,
the dictionary is its error body, {"error": ..., "detail": ...}, and it is {} only when
no server answered. Call status_answer() to read a refusal's status.
status_answer
¶
status_answer() -> Answer
Return the server's status report as an Answer (GET /control/status).
Use it to read the status and detail of a refusal, which status() does not give.
limits
¶
limits() -> dict
Return the limits on what one request to each write route may carry, so that a client can size its requests.
The limits are the limits block of status(), read once and kept for the life of this
Control. A request over a limit is refused with 422.
ingest
¶
ingest(
body: bytes, batch: str, view: str | None = None, strict: bool = False
) -> Answer
Create items, edit them or add them to a view, from a batch of rows
(POST /control/ingest).
body: the rows, as the bytes of an Arrow IPC stream.batch: an id for this batch, made fresh for each new one. The server answers the same id with the same bytes as a replay that writes nothing twice, which makes a retry safe, and refuses the same id with other bytes. When no server answers, the method sends the batch once more under its id.view: the view the rows are for, which may be left out when the database has one view.strict:Truerefuses the whole batch with409at its first refused row, so that nothing is written.
Each row names its item by its mosaica_id and its values of the unique columns. A row
that names no item creates one at the row's position, while a row that names an item edits
it, so that the item keeps its mosaica_id and joins this view if it had no row
there. An edit changes only the columns the row carries, and a null clears a stored value.
A row is refused when it names two items, when its mosaica_id names no item, when it names
no item and carries no coordinates, or when it names an item or sets a unique value that an
earlier row of the batch did. The answer counts the rows applied as created, edited,
added and unchanged, lists the refused rows in refused by position and reason, and
gives each row's mosaica_id in mosaica_ids, with None for a refused row. A row whose
only change places its item in an annotation is counted unchanged, and the membership it
adds is counted in joined.
declare_layer
¶
declare_layer(payload: dict) -> Answer
Declare an annotation layer, a named set of annotations over items, such as clusters with
their labels (PUT /control/layers).
Declaring a new layer, whose name may be that of a dropped one, answers 201 with the
layer's mosaica_id. When every layer space is in use, or the layer declares more levels than one
space holds, the declaration is refused with 409.
declare_view_group
¶
declare_view_group(name: str, body: dict) -> Answer
Declare a view group, a set of views that share every setting and differ by a key
(PUT /control/view_groups/{name}).
The group starts with no views, and create_view adds them.
declare_attribute
¶
declare_attribute(body: dict) -> Answer
Declare an attribute, a column of values that items carry (PUT /control/attributes).
body names the column, and a new column holds no value for any item until a write gives it
one. unique is the one field a declared column may change. Declaring it True builds the
column's index over every stored value, and the answer waits for the build. When two items
hold the same value, the declaration is refused with 409.
declare_shards
¶
declare_shards(body: dict) -> Answer
Declare the shard size, the number of row ids a shard hands out to new items before it is
sealed (PUT /control/shards).
body is {"size": n}. A new size replaces the old one, and a size no larger than the
number of row ids the open shard has handed out seals that shard at once.
declare_vocabulary
¶
declare_vocabulary(name: str, body: dict) -> Answer
Declare a vocabulary, the set of values a category column takes
(PUT /control/vocabularies/{name}).
Declaring a vocabulary again as it stands adds the values it sends, as vocabulary_values
does, and changes nothing else.
vocabulary_values
¶
vocabulary_values(name: str, body: dict) -> Answer
Add a page of values to a vocabulary, or change their titles
(PATCH /control/vocabularies/{name}/values).
body holds the page as rows of {key, title?}. A key the vocabulary holds keeps its code
and takes the title sent, and a page removes nothing.
declare_view
¶
declare_view(name: str, body: dict) -> Answer
Declare a view that belongs to no view group (PUT /control/views/{name}).
A view is a coordinate system that items are placed in. This one starts with no rows and takes them at its first flush, and it cannot be dropped.
create_view
¶
create_view(group: str, key: str, body: dict) -> Answer
Add a view to a view group (PUT /control/views/{group}/{key}).
group,key: the group's name and the new view's key, which together name the view<group>:<key>.body: the view's roster record, which holds its metadata and, where it has one, itsvisibility.
Because the record cannot change once the view exists, sending the record a live view holds
answers 200 with that view, and another record for its key is refused with 409. A
dropped key may be used again.
publish
¶
publish(layer: str, body: bytes, strict: bool = False) -> Answer
Publish annotations into one level of a layer (PUT /control/layers/{layer}/artifacts).
body: the request's JSON, as bytes, in which each annotation names its members in a member table.strict:Truerefuses the whole request at its first refused member, with404or409.
A member that names no item, or two, is left out and listed in the answer's refused, and
the rest of the request is applied as one change. An annotation whose key the level already
holds is not created again, and the members sent join it. The answer gives each annotation's
mosaica_id in the order sent. A request that would create more annotations than the
level's layer space has ids left for is refused with 409.
grow
¶
grow(layer: str, body: bytes, strict: bool = False) -> Answer
Change the annotations a level already holds (PATCH /control/layers/{layer}/artifacts).
body is the request's JSON, as bytes. It can add members, fill parts an annotation lacks,
and take items out of the set a label's text was written from. Members are refused as for
publish, and strict works as it does there. A part that differs from the one the
annotation holds is refused with 409, and a key no annotation holds is refused with 422,
because this method creates nothing.
changes
¶
changes(items: list[dict], strict: bool = False) -> Answer
Delete, suppress or unsuppress items, annotations and layers (POST /control/changes).
items: one record per change,{"op": ..., "match": {column: value, ...}}.opis"delete","suppress"or"unsuppress", andmatchnames an item by itsmosaica_idand its values of unique columns, asDatabase.addressesreturns them, or an annotation or a layer by itsmosaica_id.strict:Truerefuses the whole request at its first change that names no item, with404, or names two, with409.
A change that names no item, or two, is listed in the answer's refused, and the others are
applied. A deletion or suppression applies to every request that starts after the server
accepts it. The server never answers this route 429, so a busy server does not hold a
deletion back. A request that deletes a layer is refused with 422, because drop_layer
removes a layer.
drop_layer
¶
drop_layer(name: str, wait: bool = False) -> Answer
Remove an annotation layer with all its annotations (DELETE /control/layers/{name}).
Readers stop being served the layer at once. Its name may be declared again afterwards, as a
new layer that none of the dropped layer's mosaica_ids names. wait holds the answer
until the publication carrying the drop is visible, as on flush.
drop_view
¶
drop_view(group: str, key: str, wait: bool = False) -> Answer
Remove one view of a view group, with the annotations scoped to it
(DELETE /control/views/{group}/{key}).
The items the drop leaves in no view are deleted, and the answer's deleted counts them.
wait is as for flush.
compact
¶
compact(
wait: bool = False, progress: bool | Callable[[dict], Any] = True
) -> Answer
Ask the server for a compaction, which removes the rows of deleted items from disc
(POST /control/compact), and wait for its outcome where wait is True.
The server answers 202 with the request's number in body["request"], or 503 where no
compaction can start until it restarts, and then compacts every shard with something to
reclaim. compaction_request reads where the request is.
wait:Truepasses the number toawait_compactionand returns the202as the server sent it, withwaitedset to whatawait_compactionreturned.progress: whatawait_compactiontakes. It is checked before the request is sent.
compaction_request
¶
compaction_request(request: int) -> Answer
Return where one compaction request is, pending or answered
(GET /control/compact/{request}).
request is the number compact answered. The route's description in
docs/openapi/mosaica.yaml says what the body holds and when it is 404 or 410.
await_compaction
¶
await_compaction(
request: int, progress: bool | Callable[[dict], Any] = True
) -> dict
Wait until the server has answered compaction request number request, reading
compaction_request(request) about once a second, and return how the wait ended.
progress: how the wait shows the compaction serving the request, the route'sjob, asDatabase.commit'sprogresstakes it. The default isTrue. Once the request is answered, the last record shown is that of the compaction that answered it, where one ran. A compaction shorter than the interval between two readings is shown only by that record.
A reading that no server answered is read again a second later, so a server that
restarts while the call waits ends the wait as lost once it is back. The wait has no
bound of its own: to stop it, interrupt it, or call compaction_request in a loop of
your own instead.
The dictionary returned has ok, which is True only where the request succeeded, and
ended, which says how the wait ended:
answered:answerandjobare the route's, as served, andokis the answer's.lost: the route answered404, because the server restarted after the request was made.readis thatAnswer. Ask again.gone: the route answered410, because the answer is older than those the server keeps.readis thatAnswer.unread: the route answered another status outside the 200s, such as503once the server's write executor has stopped, andreadis thatAnswer.
flush
¶
flush(wait: bool = False) -> Answer
Make the writes sent so far visible to readers sooner, by bringing the next publication
forward (POST /control/flush).
Writes become visible to readers at a publication, and the answer's publication names the
first one certain to include every request sent before this one.
wait:Trueholds the answer until readers see that publication, or until the server'sserve.visible_wait_max_secspasses, in which case the answer'svisibleisfalse.
list_principals
¶
list_principals() -> Answer
List every local principal (GET /control/principals).
The answer's principals holds one record per principal, with its name, kind,
disabled, has_password, and the terms, permissions and groups granted to it
directly. The answer holds no password or key.
show_principal
¶
show_principal(name: str) -> Answer
Return one principal's record, as list_principals describes it
(GET /control/principals/{name}).
The answer's status is 404 when no principal has the name.
create_principal
¶
create_principal(name: str, kind: str) -> Answer
Create a local principal, a named person or service that can be granted access
(POST /control/principals).
name: the principal's name. A name already in use is refused with409.kind:"person"or"service".
The principal starts with no password, key, term or permission.
change_principal
¶
change_principal(name: str, disabled: bool) -> Answer
Disable or enable a principal (PATCH /control/principals/{name}).
disabled:Truedisables the principal, andFalseenables it again.
Disabling a principal ends its sessions and the tokens it made for others with authorise,
and the server accepts none of its credentials until it is enabled.
delete_principal
¶
delete_principal(name: str) -> Answer
Delete a principal with its password, API keys, grants and group memberships
(DELETE /control/principals/{name}).
set_password
¶
set_password(name: str, password: str) -> Answer
Set a principal's password, replacing any it had
(PUT /control/principals/{name}/password).
A password shorter than the server's catalogue.min_password_length is refused with 422.
clear_password
¶
clear_password(name: str) -> Answer
Remove a principal's password (DELETE /control/principals/{name}/password).
No password logs the principal in afterwards.
list_keys
¶
list_keys(principal: str) -> Answer
List a principal's API keys (GET /control/principals/{name}/keys).
The answer holds them under keys, without their secrets.
create_key
¶
create_key(
principal: str,
expires_at: int | None = None,
permissions: Sequence[str] | None = None,
) -> Answer
Issue an API key for a principal (POST /control/principals/{name}/keys).
principal: the principal the key authenticates.expires_at: when the key expires, in seconds since 1970, which also ends every session made with it. Left out, the key does not expire.permissions: the permissions the key is limited to, which never exceed its principal's. Left out, the key holds its principal's.
The answer's key is the whole key, which no later answer shows again, so keep it when you
make it. prefix names the key to list_keys and revoke_key.
revoke_key
¶
revoke_key(prefix: str) -> Answer
Revoke an API key by its prefix (DELETE /control/keys/{prefix}).
Revoking a key ends every session made with it, including the tokens authorise made with
it.
list_groups
¶
list_groups() -> Answer
List every local group, with its terms, permissions and members, under groups in the
answer (GET /control/groups).
show_group
¶
show_group(name: str) -> Answer
Return one group's record (GET /control/groups/{name}).
The answer's status is 404 when no group has the name.
create_group
¶
create_group(name: str) -> Answer
Create a local group, whose members hold every term and permission granted to it
(POST /control/groups).
A name already in use is refused with 409.
delete_group
¶
delete_group(name: str) -> Answer
Delete a group with its grants and memberships (DELETE /control/groups/{name}).
add_member
¶
add_member(group: str, principal: str) -> Answer
Add a principal to a group (PUT /control/groups/{name}/members/{principal}).
remove_member
¶
remove_member(group: str, principal: str) -> Answer
Remove a principal from a group (DELETE /control/groups/{name}/members/{principal}).
grant
¶
grant(
principal: str | None = None,
group: str | None = None,
term: str | None = None,
terms: Sequence[str] | None = None,
permission: str | None = None,
) -> Answer
Grant access terms or a permission to a principal or a group (POST /control/grants).
principal,group: the grantee.term,terms: an access term, which says what the grantee may see, or several, which are granted as one change and refused together if one is refused.permission: what the grantee may do, one of"read","write","authorise-as","admin","read-all"and"write-all".
Name exactly one grantee and exactly one of term, terms and permission. The server
refuses any other combination with 422. A grant already held changes nothing.
revoke_grant
¶
revoke_grant(
principal: str | None = None,
group: str | None = None,
term: str | None = None,
terms: Sequence[str] | None = None,
permission: str | None = None,
) -> Answer
Take back terms or a permission that grant gave, with the same arguments
(POST /control/grants/revoke).
A grant that is not held changes nothing.
list_providers
¶
list_providers() -> Answer
List every OIDC provider, whether declared through this API or in mosaica.toml, under
providers in the answer (GET /control/providers).
show_provider
¶
show_provider(name: str) -> Answer
Return one OIDC provider's record (GET /control/providers/{name}).
The answer's status is 404 when no provider has the name.
put_provider
¶
put_provider(
name: str,
issuer: str,
audience: str,
jwks_url: str,
claim_rules: Sequence[dict] | None = None,
role_mappings: Sequence[dict] | None = None,
) -> Answer
Declare an OIDC provider whose access tokens the database accepts, or replace one whole
(PUT /control/providers/{name}).
issuer,audience: theissthe provider's tokens carry and theaudthey must hold.jwks_url: where the provider publishes its signing keys, overhttps, or overhttpto a loopback address.claim_rules: rules that turn a claim's values into access terms, each{"claim": ..., "template": ...}, such as{"claim": "groups[*]", "template": "{value}"}.role_mappings: rules that give an identity a local group's terms and permissions when one of its claims holds a value exactly, each{"claim": ..., "value": ..., "group": ...}.
Replacing a provider ends every session authorised through it. A provider declared in
mosaica.toml is refused with 409, because it is changed there.
drop_provider
¶
drop_provider(name: str) -> Answer
Remove an OIDC provider, which ends every session authorised through it
(DELETE /control/providers/{name}).
A provider declared in mosaica.toml is refused with 409, because it is removed there.
list_sessions
¶
list_sessions(
principal: str | None = None, provider: str | None = None
) -> Answer
List the live sessions under sessions in the answer (GET /control/sessions).
principal: lists only a local principal's sessions, including thoseauthorisemade for it.provider: lists only the sessions authorised through an OIDC provider.
Name at most one of them, and leave both out to list every session.
end_sessions
¶
end_sessions(
token_id: int | None = None,
principal: str | None = None,
provider: str | None = None,
) -> Answer
End one session, or every session of a local principal or an OIDC provider
(POST /control/sessions/end).
Name exactly one of token_id, principal and provider. A session's token_id is in
list_sessions and on the Token that authorise made, and an ended session's token stops
working.
Answer
dataclass
¶
The answer to one request a Control sent, whether the server accepted it, refused it or
never answered.
status: the HTTP status, or 0 when no server answered.body: the answer's JSON object. It is{}when the answer was not JSON, and a JSON value that is not an object is under"value".detail: the answer's text, or why no answer came.attempts: how many times the request was sent, which is more than 1 when the server asked the client to wait with a429, or when an ingest that got no answer was sent again.seconds: how long the request took, from its first attempt to its answer.waited: forControl.compact(wait=True)where the server accepted the request, how the wait for its outcome ended, ascompactdescribes, andNonefor every other answer.
ok
property
¶
ok: bool
True when the status is in the 200s, which means the server accepted the request.