Working with Records¶
pyfsr offers two ways to work with FortiSOAR data: a generic
RecordSet that works for any module, and typed,
module-specific APIs like client.alerts.
See also
Runnable examples:
examples/list_alerts.py
(a minimal read) and
examples/upload_attachment_record.py
(file upload + linking an attachment record).
Generic CRUD¶
client.records("<module>") returns a RecordSet bound to that
module, so you never hand-build /api/3/<module> URLs or unwrap Hydra
envelopes:
incidents = client.records("incidents")
inc = incidents.get("0d2c...") # fetch by uuid
created = incidents.create(name="Breach", severity=...)
incidents.update("0d2c...", status=...)
incidents.delete("0d2c...")
A record reference can be a bare uuid, the module:uuid shorthand, or a full
/api/3/<module>/<uuid> IRI — all resolve to the same record.
Return shapes¶
get returns the bound model (here Alert); picklist fields come back as their
IRI string. Pass raw=True for the plain decoded dict, where a picklist keeps
its full itemValue block:
>>> client = demo_client()
>>> alerts = client.records("alerts")
>>> alert = alerts.get("9f0eb603-ac1e-41c3-b47b-444589beed39")
>>> type(alert).__name__, alert.name
('Alert', 'Response Capture Test Alert')
>>> alert.severity # typed: the picklist IRI string
'/api/3/picklists/58d0753f-f7e4-403b-953c-b0f521eab759'
>>> raw = alerts.get("9f0eb603-ac1e-41c3-b47b-444589beed39", raw=True)
>>> raw["severity"]["itemValue"], raw["status"]["itemValue"] # raw: friendly values
('Low', 'Open')
create and update return the created/updated record the same way;
delete returns None:
>>> created = alerts.create({"name": "New Alert"}, resolve_picklists=False)
>>> type(created).__name__, created.name, created.uuid[:8]
('Alert', 'Response Capture Test Alert', '9f0eb603')
>>> updated = alerts.update(
... "9f0eb603-ac1e-41c3-b47b-444589beed39", {"name": "Renamed"},
... resolve_picklists=False)
>>> updated.name
'Response Capture Test Alert'
>>> alerts.delete("9f0eb603-ac1e-41c3-b47b-444589beed39") # returns None
list and query return a HydraPage — iterate it,
index members, or read total / has_next:
>>> page = alerts.list()
>>> type(page).__name__, len(page), page.total, page.has_next
('HydraPage', 1, 1, False)
>>> [a.name for a in page]
['Response Capture Test Alert']
>>> qpage = alerts.query(Query().eq("status.itemValue", "Open"))
>>> qpage.members[0].name
'Response Capture Test Alert'
iterate() streams across pages (here one page, one record) and first /
count / exists are the one-liner conveniences:
>>> [a.name for a in alerts.iterate(Query())]
['Response Capture Test Alert']
>>> alerts.first(Query()).name, alerts.count(Query()), alerts.exists(Query())
('Response Capture Test Alert', 1, True)
Querying & iterating¶
Pass a Query to fetch a page, or iterate() to stream across
pages transparently:
from pyfsr import Query
page = incidents.query(Query().eq("status.itemValue", "Open").limit(50))
for rec in incidents.iterate(Query().gt("createDate", ts)):
print(rec.name)
See Querying for the full DSL.
Typed models¶
client.records("<module>") parses each record into the module’s Pydantic
model (here Alert) — attribute access, validation, and picklist-IRI
flattening for free:
>>> alert = client.records("alerts").get("9f0eb603-ac1e-41c3-b47b-444589beed39")
>>> type(alert).__name__, alert.name
('Alert', 'Response Capture Test Alert')
The legacy client.alerts accessor (and the other package-level module APIs)
return the raw decoded dict instead — handy when you want the wire shape
untouched, but without the typed niceties:
>>> raw = client.alerts.get("9f0eb603-ac1e-41c3-b47b-444589beed39")
>>> type(raw).__name__, raw["name"]
('dict', 'Response Capture Test Alert')
Available typed models include Alert, Incident,
Task, Comment, Workflow, and
more. Look up the model class for any module with model_for().
Reads always come back typed; pass raw=True on an individual read (e.g.
client.records("alerts").get(uuid, raw=True)) when you want a plain dict.
Picklist resolution¶
Picklist fields are stored as IRIs, not friendly strings — but create,
update, and upsert resolve friendly values for you automatically, so you
can pass "High" / "Open" directly:
alert = client.records("alerts").create({
"name": "Test Alert",
"severity": "High", # → resolved to the severity IRI
"status": "Open", # → resolved to the status IRI
})
Resolution only touches fields the module flags as picklist-backed, passes
already-resolved IRIs through untouched, and is cached per client. Pass
resolve_picklists=False to skip it when every value is already an IRI:
client.records("alerts").create(data, resolve_picklists=False)
Need to resolve a value yourself? client.picklists exposes the lower-level
resolve() and
resolve_record_fields helpers (including a strict=True mode that raises with
the valid options on a bad value).