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