summary.py 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905
  1. # Copyright (c) "Neo4j"
  2. # Neo4j Sweden AB [https://neo4j.com]
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License");
  5. # you may not use this file except in compliance with the License.
  6. # You may obtain a copy of the License at
  7. #
  8. # https://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. from __future__ import annotations
  16. import itertools
  17. import typing as t
  18. from copy import deepcopy
  19. from dataclasses import dataclass
  20. from .._api import (
  21. NotificationCategory,
  22. NotificationClassification,
  23. NotificationSeverity,
  24. )
  25. from .._exceptions import BoltProtocolError
  26. from .._meta import preview
  27. if t.TYPE_CHECKING:
  28. import typing_extensions as te
  29. from ..addressing import Address
  30. from ..api import ServerInfo
  31. _T = te.TypeVar("_T")
  32. class ResultSummary:
  33. """A summary of execution returned with a :class:`.Result` object."""
  34. #: A :class:`neo4j.ServerInfo` instance. Provides some basic information of
  35. #: the server where the result is obtained from.
  36. server: ServerInfo
  37. #: The database name where this summary is obtained from.
  38. database: str | None
  39. #: The query that was executed to produce this result.
  40. query: str | None
  41. #: Dictionary of parameters passed with the statement.
  42. parameters: dict[str, t.Any] | None
  43. #: A string that describes the type of query
  44. # ``'r'`` = read-only, ``'rw'`` = read/write, ``'w'`` = write-only,
  45. # ``'s'`` = schema.
  46. query_type: te.Literal["r", "rw", "w", "s"] | None
  47. #: A :class:`neo4j.SummaryCounters` instance. Counters for operations the
  48. #: query triggered.
  49. counters: SummaryCounters
  50. #: Dictionary that describes how the database will execute the query.
  51. plan: dict | None
  52. #: Dictionary that describes how the database executed the query.
  53. profile: dict | None
  54. #: The time it took for the server to have the result available.
  55. #: (milliseconds)
  56. result_available_after: int | None
  57. #: The time it took for the server to consume the result. (milliseconds)
  58. result_consumed_after: int | None
  59. #: A list of Dictionaries containing notification information.
  60. #: Notifications provide extra information for a user executing a
  61. #: statement.
  62. #: They can be warnings about problematic queries or other valuable
  63. #: information that can be
  64. #: presented in a client.
  65. #: Unlike failures or errors, notifications do not affect the execution of
  66. #: a statement.
  67. #:
  68. #: .. seealso:: :attr:`.summary_notifications`
  69. notifications: list[dict] | None
  70. # cache for notifications
  71. _notifications_set: bool = False
  72. # cache for property `summary_notifications`
  73. _summary_notifications: list[SummaryNotification]
  74. # cache for property `summary_notifications`
  75. _gql_status_objects: tuple[GqlStatusObject, ...]
  76. _had_key: bool
  77. _had_record: bool
  78. def __init__(
  79. self,
  80. address: Address,
  81. had_key: bool,
  82. had_record: bool,
  83. metadata: dict[str, t.Any],
  84. ) -> None:
  85. self._had_key = had_key
  86. self._had_record = had_record
  87. self.metadata = metadata
  88. self.server = metadata["server"]
  89. self.database = metadata.get("db")
  90. self.query = metadata.get("query")
  91. self.parameters = metadata.get("parameters")
  92. if "type" in metadata:
  93. self.query_type = metadata["type"]
  94. if self.query_type not in {"r", "w", "rw", "s"}:
  95. raise BoltProtocolError(
  96. f"Unexpected query type {self.query_type!r} received from "
  97. "server. Consider updating the driver.",
  98. address,
  99. )
  100. self.query_type = metadata.get("type")
  101. self.plan = metadata.get("plan")
  102. self.profile = metadata.get("profile")
  103. self.counters = SummaryCounters(metadata.get("stats", {}))
  104. if self.server.protocol_version[0] < 3:
  105. self.result_available_after = metadata.get(
  106. "result_available_after"
  107. )
  108. self.result_consumed_after = metadata.get("result_consumed_after")
  109. else:
  110. self.result_available_after = metadata.get("t_first")
  111. self.result_consumed_after = metadata.get("t_last")
  112. def __dir__(self):
  113. return {*super().__dir__(), "notifications"}
  114. def __getattr__(self, key):
  115. if key == "notifications":
  116. self._set_notifications()
  117. return self.notifications
  118. raise AttributeError(
  119. f"'{self.__class__.__name__}' object has no attribute '{key}'"
  120. )
  121. @staticmethod
  122. def _notification_from_status(status: dict) -> dict:
  123. notification = {}
  124. for notification_key, status_key in (
  125. ("title", "title"),
  126. ("code", "neo4j_code"),
  127. ("description", "description"),
  128. ):
  129. if status_key in status:
  130. notification[notification_key] = status[status_key]
  131. if "diagnostic_record" in status:
  132. diagnostic_record = status["diagnostic_record"]
  133. if not isinstance(diagnostic_record, dict):
  134. diagnostic_record = {}
  135. for notification_key, diag_record_key in (
  136. ("severity", "_severity"),
  137. ("category", "_classification"),
  138. ("position", "_position"),
  139. ):
  140. if diag_record_key in diagnostic_record:
  141. notification[notification_key] = diagnostic_record[
  142. diag_record_key
  143. ]
  144. return notification
  145. def _set_notifications(self) -> None:
  146. if "notifications" in self.metadata:
  147. notifications = self.metadata["notifications"]
  148. if not isinstance(notifications, list):
  149. self.notifications = None
  150. return
  151. self.notifications = notifications
  152. return
  153. # polyfill notifications from GqlStatusObjects
  154. if "statuses" in self.metadata:
  155. statuses = self.metadata["statuses"]
  156. if not isinstance(statuses, list):
  157. self.notifications = None
  158. return
  159. notifications = []
  160. for status in statuses:
  161. if not (isinstance(status, dict) and "neo4j_code" in status):
  162. # not a notification status
  163. continue
  164. notification = self._notification_from_status(status)
  165. notifications.append(notification)
  166. self.notifications = notifications or None
  167. return
  168. self.notifications = None
  169. # TODO: 6.0 - return a tuple for immutability (annotate with Sequence)
  170. @property
  171. def summary_notifications(self) -> list[SummaryNotification]:
  172. """
  173. The same as ``notifications`` but in a parsed, structured form.
  174. Further, if connected to a gql-aware server, this property will be
  175. polyfilled from :attr:`gql_status_objects`.
  176. .. seealso:: :attr:`.notifications`, :class:`.SummaryNotification`
  177. .. versionadded:: 5.7
  178. """
  179. if getattr(self, "_summary_notifications", None) is not None:
  180. return self._summary_notifications
  181. raw_notifications = self.notifications
  182. if not isinstance(raw_notifications, list):
  183. self._summary_notifications = []
  184. return self._summary_notifications
  185. self._summary_notifications = [
  186. SummaryNotification._from_metadata(n) for n in raw_notifications
  187. ]
  188. return self._summary_notifications
  189. @property
  190. @preview("GQLSTATUS support is a preview feature.")
  191. def gql_status_objects(self) -> t.Sequence[GqlStatusObject]:
  192. """
  193. Get GqlStatusObjects that arose when executing the query.
  194. The sequence always contains at least 1 status representing the
  195. Success, No Data or Omitted Result.
  196. All other status are notifications like warnings about problematic
  197. queries or other valuable information that can be presented in a
  198. client.
  199. The GqlStatusObjects will be presented in the following order:
  200. * A "no data" (``02xxx``) has precedence over a warning.
  201. * A "warning" (``01xxx``) has precedence over a success.
  202. * A "success" (``00xxx``) has precedence over anything informational
  203. (``03xxx``).
  204. **This is a preview** (see :ref:`filter-warnings-ref`).
  205. It might be changed without following the deprecation policy.
  206. See also
  207. https://github.com/neo4j/neo4j-python-driver/wiki/preview-features
  208. .. versionadded:: 5.22
  209. """
  210. raw_status_objects = self.metadata.get("statuses")
  211. if isinstance(raw_status_objects, list):
  212. self._gql_status_objects = tuple(
  213. GqlStatusObject._from_status_metadata(s)
  214. for s in raw_status_objects
  215. )
  216. return self._gql_status_objects
  217. raw_notifications = self.notifications
  218. notification_status_objects: t.Iterable[GqlStatusObject]
  219. if isinstance(raw_notifications, list):
  220. notification_status_objects = [
  221. GqlStatusObject._from_notification_metadata(n)
  222. for n in raw_notifications
  223. ]
  224. else:
  225. notification_status_objects = ()
  226. if self._had_record:
  227. # polyfill with a Success status
  228. result_status = GqlStatusObject._success()
  229. elif self._had_key:
  230. # polyfill with an Omitted Result status
  231. result_status = GqlStatusObject._no_data()
  232. else:
  233. # polyfill with a No Data status
  234. result_status = GqlStatusObject._omitted_result()
  235. notification_status_objects = itertools.chain(
  236. notification_status_objects, (result_status,)
  237. )
  238. def status_precedence(status: GqlStatusObject) -> int:
  239. if status.gql_status.startswith("02"):
  240. # no data
  241. return 3
  242. if status.gql_status.startswith("01"):
  243. # warning
  244. return 2
  245. if status.gql_status.startswith("00"):
  246. # success
  247. return 1
  248. if status.gql_status.startswith("03"):
  249. # informational
  250. return 0
  251. return -1
  252. notification_status_objects = sorted(
  253. notification_status_objects,
  254. key=status_precedence,
  255. reverse=True,
  256. )
  257. self._gql_status_objects = tuple(notification_status_objects)
  258. return self._gql_status_objects
  259. class SummaryCounters:
  260. """Contains counters for various operations that a query triggered."""
  261. #:
  262. nodes_created: int = 0
  263. #:
  264. nodes_deleted: int = 0
  265. #:
  266. relationships_created: int = 0
  267. #:
  268. relationships_deleted: int = 0
  269. #:
  270. properties_set: int = 0
  271. #:
  272. labels_added: int = 0
  273. #:
  274. labels_removed: int = 0
  275. #:
  276. indexes_added: int = 0
  277. #:
  278. indexes_removed: int = 0
  279. #:
  280. constraints_added: int = 0
  281. #:
  282. constraints_removed: int = 0
  283. #:
  284. system_updates: int = 0
  285. _contains_updates = None
  286. _contains_system_updates = None
  287. def __init__(self, statistics) -> None:
  288. key_to_attr_name = {
  289. "nodes-created": "nodes_created",
  290. "nodes-deleted": "nodes_deleted",
  291. "relationships-created": "relationships_created",
  292. "relationships-deleted": "relationships_deleted",
  293. "properties-set": "properties_set",
  294. "labels-added": "labels_added",
  295. "labels-removed": "labels_removed",
  296. "indexes-added": "indexes_added",
  297. "indexes-removed": "indexes_removed",
  298. "constraints-added": "constraints_added",
  299. "constraints-removed": "constraints_removed",
  300. "system-updates": "system_updates",
  301. "contains-updates": "_contains_updates",
  302. "contains-system-updates": "_contains_system_updates",
  303. }
  304. for key, value in dict(statistics).items():
  305. attr_name = key_to_attr_name.get(key)
  306. if attr_name:
  307. setattr(self, attr_name, value)
  308. def __repr__(self) -> str:
  309. return repr(vars(self))
  310. @property
  311. def contains_updates(self) -> bool:
  312. """
  313. Check if any counters tracking graph updates are greater than 0.
  314. True if any of the counters except for system_updates, are greater
  315. than 0. Otherwise, False.
  316. """
  317. if self._contains_updates is not None:
  318. return self._contains_updates
  319. return bool(
  320. self.nodes_created
  321. or self.nodes_deleted
  322. or self.relationships_created
  323. or self.relationships_deleted
  324. or self.properties_set
  325. or self.labels_added
  326. or self.labels_removed
  327. or self.indexes_added
  328. or self.indexes_removed
  329. or self.constraints_added
  330. or self.constraints_removed
  331. )
  332. @property
  333. def contains_system_updates(self) -> bool:
  334. """True if the system database was updated, otherwise False."""
  335. if self._contains_system_updates is not None:
  336. return self._contains_system_updates
  337. return self.system_updates > 0
  338. @dataclass
  339. class SummaryInputPosition:
  340. """
  341. Structured form of a gql status/notification position.
  342. .. seealso::
  343. :attr:`.GqlStatusObject.position`,
  344. :attr:`.SummaryNotification.position`,
  345. :data:`.SummaryNotificationPosition`
  346. .. versionadded:: 5.22
  347. """
  348. #: The line number of the notification. Line numbers start at 1.
  349. line: int
  350. #: The column number of the notification. Column numbers start at 1.
  351. column: int
  352. #: The character offset of the notification. Offsets start at 0.
  353. offset: int
  354. @classmethod
  355. def _from_metadata(cls, metadata: object) -> te.Self | None:
  356. if not isinstance(metadata, dict):
  357. return None
  358. line = metadata.get("line")
  359. if not isinstance(line, int) or isinstance(line, bool):
  360. return None
  361. column = metadata.get("column")
  362. if not isinstance(column, int) or isinstance(column, bool):
  363. return None
  364. offset = metadata.get("offset")
  365. if not isinstance(offset, int) or isinstance(offset, bool):
  366. return None
  367. return cls(line=line, column=column, offset=offset)
  368. def __str__(self) -> str:
  369. return (
  370. f"line: {self.line}, column: {self.column}, offset: {self.offset}"
  371. )
  372. # Deprecated alias for :class:`.SummaryInputPosition`.
  373. #
  374. # .. versionadded:: 5.7
  375. #
  376. # .. versionchanged:: 5.22
  377. # Deprecated in favor of :class:`.SummaryInputPosition`.
  378. SummaryNotificationPosition: te.TypeAlias = SummaryInputPosition
  379. _SEVERITY_LOOKUP: dict[t.Any, NotificationSeverity] = {
  380. "WARNING": NotificationSeverity.WARNING,
  381. "INFORMATION": NotificationSeverity.INFORMATION,
  382. }
  383. _CATEGORY_LOOKUP: dict[t.Any, NotificationCategory] = {
  384. "HINT": NotificationCategory.HINT,
  385. "UNRECOGNIZED": NotificationCategory.UNRECOGNIZED,
  386. "UNSUPPORTED": NotificationCategory.UNSUPPORTED,
  387. "PERFORMANCE": NotificationCategory.PERFORMANCE,
  388. "DEPRECATION": NotificationCategory.DEPRECATION,
  389. "GENERIC": NotificationCategory.GENERIC,
  390. "SECURITY": NotificationCategory.SECURITY,
  391. "TOPOLOGY": NotificationCategory.TOPOLOGY,
  392. "SCHEMA": NotificationCategory.SCHEMA,
  393. }
  394. _CLASSIFICATION_LOOKUP: dict[t.Any, NotificationClassification] = {
  395. k: NotificationClassification(v) for k, v in _CATEGORY_LOOKUP.items()
  396. }
  397. if t.TYPE_CHECKING:
  398. class _SummaryNotificationKwargs(te.TypedDict, total=False):
  399. title: str
  400. code: str
  401. description: str
  402. severity_level: NotificationSeverity
  403. category: NotificationCategory
  404. raw_severity_level: str
  405. raw_category: str
  406. position: SummaryInputPosition | None
  407. @dataclass
  408. class SummaryNotification:
  409. """
  410. Structured form of a notification received from the server.
  411. .. seealso:: :attr:`.ResultSummary.summary_notifications`
  412. .. versionadded:: 5.7
  413. """
  414. title: str = ""
  415. code: str = ""
  416. description: str = ""
  417. severity_level: NotificationSeverity = NotificationSeverity.UNKNOWN
  418. category: NotificationCategory = NotificationCategory.UNKNOWN
  419. raw_severity_level: str = ""
  420. raw_category: str = ""
  421. position: SummaryNotificationPosition | None = None
  422. @classmethod
  423. def _from_metadata(cls, metadata: object) -> te.Self:
  424. if not isinstance(metadata, dict):
  425. return cls()
  426. kwargs: _SummaryNotificationKwargs = {
  427. "position": SummaryInputPosition._from_metadata(
  428. metadata.get("position")
  429. ),
  430. }
  431. str_keys: tuple[te.Literal["title", "code", "description"], ...] = (
  432. "title",
  433. "code",
  434. "description",
  435. )
  436. for key in str_keys:
  437. value = metadata.get(key)
  438. if isinstance(value, str):
  439. kwargs[key] = value
  440. severity = metadata.get("severity")
  441. if isinstance(severity, str):
  442. kwargs["raw_severity_level"] = severity
  443. kwargs["severity_level"] = _SEVERITY_LOOKUP.get(
  444. severity, NotificationSeverity.UNKNOWN
  445. )
  446. category = metadata.get("category")
  447. if isinstance(category, str):
  448. kwargs["raw_category"] = category
  449. kwargs["category"] = _CATEGORY_LOOKUP.get(
  450. category, NotificationCategory.UNKNOWN
  451. )
  452. return cls(**kwargs)
  453. def __str__(self) -> str:
  454. return (
  455. f"{{severity: {self.raw_severity_level}}} {{code: {self.code}}} "
  456. f"{{category: {self.raw_category}}} {{title: {self.title}}} "
  457. f"{{description: {self.description}}} "
  458. f"{{position: {self.position}}}"
  459. )
  460. POLYFILL_DIAGNOSTIC_RECORD = (
  461. ("OPERATION", ""),
  462. ("OPERATION_CODE", "0"),
  463. ("CURRENT_SCHEMA", "/"),
  464. )
  465. _SUCCESS_STATUS_METADATA = {
  466. "gql_status": "00000",
  467. "status_description": "note: successful completion",
  468. "diagnostic_record": dict(POLYFILL_DIAGNOSTIC_RECORD),
  469. }
  470. _OMITTED_RESULT_STATUS_METADATA = {
  471. "gql_status": "00001",
  472. "status_description": "note: successful completion - omitted result",
  473. "diagnostic_record": dict(POLYFILL_DIAGNOSTIC_RECORD),
  474. }
  475. _NO_DATA_STATUS_METADATA = {
  476. "gql_status": "02000",
  477. "status_description": "note: no data",
  478. "diagnostic_record": dict(POLYFILL_DIAGNOSTIC_RECORD),
  479. }
  480. class GqlStatusObject:
  481. """
  482. Representation for GqlStatusObject found when executing a query.
  483. GqlStatusObjects are a superset of notifications, i.e., some but not all
  484. GqlStatusObjects are notifications.
  485. Notifications can be filtered server-side with
  486. driver config
  487. :ref:`driver-notifications-disabled-classifications-ref` and
  488. :ref:`driver-notifications-min-severity-ref` as well as
  489. session config
  490. :ref:`session-notifications-disabled-classifications-ref` and
  491. :ref:`session-notifications-min-severity-ref`.
  492. .. seealso:: :attr:`.ResultSummary.gql_status_objects`
  493. .. versionadded:: 5.22
  494. """
  495. # internal dictionaries, never handed to assure immutability
  496. _status_metadata: dict[str, t.Any]
  497. _status_diagnostic_record: dict[str, t.Any] | None = None
  498. _is_notification: bool
  499. _gql_status: str
  500. _status_description: str
  501. _position: SummaryInputPosition | None
  502. _raw_classification: str | None
  503. _classification: NotificationClassification
  504. _raw_severity: str | None
  505. _severity: NotificationSeverity
  506. _diagnostic_record: dict[str, t.Any]
  507. @classmethod
  508. def _success(cls) -> te.Self:
  509. obj = cls()
  510. obj._status_metadata = _SUCCESS_STATUS_METADATA
  511. return obj
  512. @classmethod
  513. def _omitted_result(cls) -> te.Self:
  514. obj = cls()
  515. obj._status_metadata = _OMITTED_RESULT_STATUS_METADATA
  516. return obj
  517. @classmethod
  518. def _no_data(cls) -> te.Self:
  519. obj = cls()
  520. obj._status_metadata = _NO_DATA_STATUS_METADATA
  521. return obj
  522. @classmethod
  523. def _from_status_metadata(cls, metadata: object) -> te.Self:
  524. obj = cls()
  525. if isinstance(metadata, dict):
  526. obj._status_metadata = metadata
  527. else:
  528. obj._status_metadata = {}
  529. return obj
  530. @classmethod
  531. def _from_notification_metadata(cls, metadata: object) -> te.Self:
  532. obj = cls()
  533. if not isinstance(metadata, dict):
  534. metadata = {}
  535. description = metadata.get("description")
  536. neo4j_code = metadata.get("neo4j_code")
  537. if not isinstance(neo4j_code, str):
  538. neo4j_code = ""
  539. title = metadata.get("title")
  540. if not isinstance(title, str):
  541. title = ""
  542. position = SummaryInputPosition._from_metadata(
  543. metadata.get("position")
  544. )
  545. classification = metadata.get("category")
  546. if not isinstance(classification, str):
  547. classification = None
  548. severity = metadata.get("severity")
  549. if not isinstance(severity, str):
  550. severity = None
  551. if severity == "WARNING":
  552. gql_status = "01N42"
  553. if not isinstance(description, str) or not description:
  554. description = "warn: unknown warning"
  555. else:
  556. # for "INFORMATION" or if severity is missing
  557. gql_status = "03N42"
  558. if not isinstance(description, str) or not description:
  559. description = "info: unknown notification"
  560. diagnostic_record = dict(POLYFILL_DIAGNOSTIC_RECORD)
  561. if "category" in metadata:
  562. diagnostic_record["_classification"] = metadata["category"]
  563. if "severity" in metadata:
  564. diagnostic_record["_severity"] = metadata["severity"]
  565. if "position" in metadata:
  566. diagnostic_record["_position"] = metadata["position"]
  567. obj._status_metadata = {
  568. "gql_status": gql_status,
  569. "status_description": description,
  570. "neo4j_code": neo4j_code,
  571. "title": title,
  572. "diagnostic_record": diagnostic_record,
  573. }
  574. obj._gql_status = gql_status
  575. obj._status_description = description
  576. obj._position = position
  577. obj._raw_classification = classification
  578. obj._raw_severity = severity
  579. obj._is_notification = True
  580. return obj
  581. def __str__(self) -> str:
  582. return self.status_description
  583. def __repr__(self) -> str:
  584. return (
  585. "GqlStatusObject("
  586. f"gql_status={self.gql_status!r}, "
  587. f"status_description={self.status_description!r}, "
  588. f"position={self.position!r}, "
  589. f"raw_classification={self.raw_classification!r}, "
  590. f"classification={self.classification!r}, "
  591. f"raw_severity={self.raw_severity!r}, "
  592. f"severity={self.severity!r}, "
  593. f"diagnostic_record={self.diagnostic_record!r}"
  594. ")"
  595. )
  596. @property
  597. def is_notification(self) -> bool:
  598. """
  599. Whether this GqlStatusObject is a notification.
  600. Only some GqlStatusObjects are notifications.
  601. The definition of notification is vendor-specific.
  602. Notifications are those GqlStatusObjects that provide additional
  603. information and can be filtered out via
  604. :ref:`driver-notifications-disabled-classifications-ref` and
  605. :ref:`driver-notifications-min-severity-ref` as well as.
  606. The fields :attr:`.position`,
  607. :attr:`.raw_classification`, :attr:`.classification`,
  608. :attr:`.raw_severity`, and :attr:`.severity` are only meaningful
  609. for notifications.
  610. """
  611. if hasattr(self, "_is_notification"):
  612. return self._is_notification
  613. neo4j_code = self._status_metadata.get("neo4j_code")
  614. self._is_notification = bool(
  615. isinstance(neo4j_code, str) and neo4j_code
  616. )
  617. return self._is_notification
  618. @classmethod
  619. def _extract_str_field(
  620. cls,
  621. data: dict[str, t.Any],
  622. key: str,
  623. default: _T = "", # type: ignore[assignment]
  624. ) -> str | _T:
  625. value = data.get(key)
  626. if isinstance(value, str):
  627. return value
  628. else:
  629. return default
  630. @property
  631. def gql_status(self) -> str:
  632. """
  633. The GQLSTATUS.
  634. The following GQLSTATUS codes denote codes that the driver will use
  635. for polyfilling (when connected to an old, non-GQL-aware server).
  636. Further, they may be used by servers during the transition-phase to
  637. GQLSTATUS-awareness.
  638. * ``01N42`` (warning - unknown warning)
  639. * ``02N42`` (no data - unknown subcondition)
  640. * ``03N42`` (informational - unknown notification)
  641. * ``05N42`` (general processing exception - unknown error)
  642. .. note::
  643. This means these codes are not guaranteed to be stable and may
  644. change in future versions of the driver or the server.
  645. """
  646. if hasattr(self, "_gql_status"):
  647. return self._gql_status
  648. self._gql_status = self._extract_str_field(
  649. self._status_metadata, "gql_status"
  650. )
  651. return self._gql_status
  652. @property
  653. def status_description(self) -> str:
  654. """A description of the status."""
  655. if hasattr(self, "_status_description"):
  656. return self._status_description
  657. self._status_description = self._extract_str_field(
  658. self._status_metadata, "status_description"
  659. )
  660. return self._status_description
  661. def _get_status_diagnostic_record(self) -> dict[str, t.Any]:
  662. if self._status_diagnostic_record is not None:
  663. return self._status_diagnostic_record
  664. self._status_diagnostic_record = self._status_metadata.get(
  665. "diagnostic_record", {}
  666. )
  667. if not isinstance(self._status_diagnostic_record, dict):
  668. self._status_diagnostic_record = {}
  669. return self._status_diagnostic_record
  670. @property
  671. def position(self) -> SummaryInputPosition | None:
  672. """
  673. The position of the input that caused the status (if applicable).
  674. This is vendor-specific information.
  675. Only notifications (see :attr:`.is_notification`) have a meaningful
  676. position.
  677. The value is :data:`None` if the server's data was missing or could not
  678. be interpreted.
  679. """
  680. if hasattr(self, "_position"):
  681. return self._position
  682. diag_record = self._get_status_diagnostic_record()
  683. self._position = SummaryInputPosition._from_metadata(
  684. diag_record.get("_position")
  685. )
  686. return self._position
  687. @property
  688. def raw_classification(self) -> str | None:
  689. """
  690. The raw (``str``) classification of the status.
  691. This is a vendor-specific classification that can be used to filter
  692. notifications.
  693. Only notifications (see :attr:`.is_notification`) have a meaningful
  694. classification.
  695. """
  696. if hasattr(self, "_raw_classification"):
  697. return self._raw_classification
  698. diag_record = self._get_status_diagnostic_record()
  699. self._raw_classification = self._extract_str_field(
  700. diag_record, "_classification", None
  701. )
  702. return self._raw_classification
  703. @property
  704. def classification(self) -> NotificationClassification:
  705. """
  706. Parsed version of :attr:`.raw_classification`.
  707. Only notifications (see :attr:`.is_notification`) have a meaningful
  708. classification.
  709. """
  710. if hasattr(self, "_classification"):
  711. return self._classification
  712. self._classification = _CLASSIFICATION_LOOKUP.get(
  713. self.raw_classification, NotificationClassification.UNKNOWN
  714. )
  715. return self._classification
  716. @property
  717. def raw_severity(self) -> str | None:
  718. """
  719. The raw (``str``) severity of the status.
  720. This is a vendor-specific severity that can be used to filter
  721. notifications.
  722. Only notifications (see :attr:`.is_notification`) have a meaningful
  723. severity.
  724. """
  725. if hasattr(self, "_raw_severity"):
  726. return self._raw_severity
  727. diag_record = self._get_status_diagnostic_record()
  728. self._raw_severity = self._extract_str_field(
  729. diag_record, "_severity", None
  730. )
  731. return self._raw_severity
  732. @property
  733. def severity(self) -> NotificationSeverity:
  734. """
  735. Parsed version of :attr:`.raw_severity`.
  736. Only notifications (see :attr:`.is_notification`) have a meaningful
  737. severity.
  738. """
  739. if hasattr(self, "_severity"):
  740. return self._severity
  741. self._severity = _SEVERITY_LOOKUP.get(
  742. self.raw_severity, NotificationSeverity.UNKNOWN
  743. )
  744. return self._severity
  745. @property
  746. def diagnostic_record(self) -> dict[str, t.Any]:
  747. """Further information about the GQLSTATUS for diagnostic purposes."""
  748. if hasattr(self, "_diagnostic_record"):
  749. return self._diagnostic_record
  750. self._diagnostic_record = deepcopy(
  751. self._get_status_diagnostic_record()
  752. )
  753. return self._diagnostic_record