exceptions.py 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188
  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. # ruff: noqa: N818
  16. # Not going to rename all Error classes that don't end on Error,
  17. # which would break pretty much all users just to please the linter.
  18. """
  19. Module containing the core driver exceptions.
  20. Driver API Errors
  21. =================
  22. + Neo4jError
  23. + ClientError
  24. + CypherSyntaxError
  25. + CypherTypeError
  26. + ConstraintError
  27. + AuthError
  28. + TokenExpired
  29. + Forbidden
  30. + DatabaseError
  31. + TransientError
  32. + DatabaseUnavailable
  33. + NotALeader
  34. + ForbiddenOnReadOnlyDatabase
  35. + DriverError
  36. + SessionError
  37. + TransactionError
  38. + TransactionNestingError
  39. + ResultError
  40. + ResultFailedError
  41. + ResultConsumedError
  42. + ResultNotSingleError
  43. + BrokenRecordError
  44. + SessionExpired
  45. + ServiceUnavailable
  46. + RoutingServiceUnavailable
  47. + WriteServiceUnavailable
  48. + ReadServiceUnavailable
  49. + IncompleteCommit
  50. + ConfigurationError
  51. + AuthConfigurationError
  52. + CertificateConfigurationError
  53. """
  54. from __future__ import annotations
  55. import typing as t
  56. from copy import deepcopy as _deepcopy
  57. from enum import Enum as _Enum
  58. from ._meta import (
  59. deprecated,
  60. preview as _preview,
  61. )
  62. __all__ = [
  63. "AuthConfigurationError",
  64. "AuthError",
  65. "BrokenRecordError",
  66. "CertificateConfigurationError",
  67. "ClientError",
  68. "ConfigurationError",
  69. "ConstraintError",
  70. "CypherSyntaxError",
  71. "CypherTypeError",
  72. "DatabaseError",
  73. "DatabaseUnavailable",
  74. "DriverError",
  75. "Forbidden",
  76. "ForbiddenOnReadOnlyDatabase",
  77. "GqlError",
  78. "GqlErrorClassification",
  79. "IncompleteCommit",
  80. "Neo4jError",
  81. "NotALeader",
  82. "ReadServiceUnavailable",
  83. "ResultConsumedError",
  84. "ResultError",
  85. "ResultFailedError",
  86. "ResultNotSingleError",
  87. "RoutingServiceUnavailable",
  88. "ServiceUnavailable",
  89. "SessionError",
  90. "SessionExpired",
  91. "TokenExpired",
  92. "TransactionError",
  93. "TransactionNestingError",
  94. "TransientError",
  95. "UnsupportedServerProduct",
  96. "WriteServiceUnavailable",
  97. ]
  98. if t.TYPE_CHECKING:
  99. from collections.abc import Mapping
  100. import typing_extensions as te
  101. from ._async.work import (
  102. AsyncManagedTransaction,
  103. AsyncResult,
  104. AsyncSession,
  105. AsyncTransaction,
  106. )
  107. from ._sync.work import (
  108. ManagedTransaction,
  109. Result,
  110. Session,
  111. Transaction,
  112. )
  113. _TTransaction = t.Union[
  114. AsyncManagedTransaction,
  115. AsyncTransaction,
  116. ManagedTransaction,
  117. Transaction,
  118. ]
  119. _TResult = t.Union[AsyncResult, Result]
  120. _TSession = t.Union[AsyncSession, Session]
  121. _T = t.TypeVar("_T")
  122. else:
  123. _TTransaction = t.Union[
  124. "AsyncManagedTransaction",
  125. "AsyncTransaction",
  126. "ManagedTransaction",
  127. "Transaction",
  128. ]
  129. _TResult = t.Union["AsyncResult", "Result"]
  130. _TSession = t.Union["AsyncSession", "Session"]
  131. __all__ = [
  132. "CLASSIFICATION_CLIENT", # TODO: 6.0 - make constant private
  133. "CLASSIFICATION_DATABASE", # TODO: 6.0 - make constant private
  134. "CLASSIFICATION_TRANSIENT", # TODO: 6.0 - make constant private
  135. "ERROR_REWRITE_MAP", # TODO: 6.0 - make constant private
  136. "AuthConfigurationError",
  137. "AuthError",
  138. "BrokenRecordError",
  139. "CertificateConfigurationError",
  140. "ClientError",
  141. "ConfigurationError",
  142. "ConstraintError",
  143. "CypherSyntaxError",
  144. "CypherTypeError",
  145. "DatabaseError",
  146. "DatabaseUnavailable",
  147. "DriverError",
  148. "Forbidden",
  149. "ForbiddenOnReadOnlyDatabase",
  150. "IncompleteCommit",
  151. "Neo4jError",
  152. "NotALeader",
  153. "ReadServiceUnavailable",
  154. "ResultConsumedError",
  155. "ResultError",
  156. "ResultFailedError",
  157. "ResultNotSingleError",
  158. "RoutingServiceUnavailable",
  159. "ServiceUnavailable",
  160. "SessionError",
  161. "SessionExpired",
  162. "TokenExpired",
  163. "TransactionError",
  164. "TransactionNestingError",
  165. "TransientError",
  166. "UnsupportedServerProduct",
  167. "WriteServiceUnavailable",
  168. ]
  169. CLASSIFICATION_CLIENT: te.Final[str] = "ClientError"
  170. CLASSIFICATION_TRANSIENT: te.Final[str] = "TransientError"
  171. CLASSIFICATION_DATABASE: te.Final[str] = "DatabaseError"
  172. ERROR_REWRITE_MAP: dict[str, tuple[str, str | None]] = {
  173. # This error can be retried ed. The driver just needs to re-authenticate
  174. # with the same credentials.
  175. "Neo.ClientError.Security.AuthorizationExpired": (
  176. CLASSIFICATION_TRANSIENT,
  177. None,
  178. ),
  179. # In 5.0, this error has been re-classified as ClientError.
  180. # For backwards compatibility with Neo4j 4.4 and earlier, we re-map it in
  181. # the driver, too.
  182. "Neo.TransientError.Transaction.Terminated": (
  183. CLASSIFICATION_CLIENT,
  184. "Neo.ClientError.Transaction.Terminated",
  185. ),
  186. # In 5.0, this error has been re-classified as ClientError.
  187. # For backwards compatibility with Neo4j 4.4 and earlier, we re-map it in
  188. # the driver, too.
  189. "Neo.TransientError.Transaction.LockClientStopped": (
  190. CLASSIFICATION_CLIENT,
  191. "Neo.ClientError.Transaction.LockClientStopped",
  192. ),
  193. }
  194. _UNKNOWN_NEO4J_CODE: te.Final[str] = "Neo.DatabaseError.General.UnknownError"
  195. # TODO: 6.0 - Make _UNKNOWN_GQL_MESSAGE the default message
  196. _UNKNOWN_MESSAGE: te.Final[str] = "An unknown error occurred"
  197. _UNKNOWN_GQL_STATUS: te.Final[str] = "50N42"
  198. _UNKNOWN_GQL_DESCRIPTION: te.Final[str] = (
  199. "error: general processing exception - unexpected error"
  200. )
  201. _UNKNOWN_GQL_MESSAGE: te.Final[str] = (
  202. f"{_UNKNOWN_GQL_STATUS}: "
  203. "Unexpected error has occurred. See debug log for details."
  204. )
  205. _UNKNOWN_GQL_DIAGNOSTIC_RECORD: te.Final[tuple[tuple[str, t.Any], ...]] = (
  206. ("OPERATION", ""),
  207. ("OPERATION_CODE", "0"),
  208. ("CURRENT_SCHEMA", "/"),
  209. )
  210. class GqlErrorClassification(str, _Enum):
  211. """
  212. Server-side GQL error category.
  213. Inherits from :class:`str` and :class:`enum.Enum`.
  214. Hence, can also be compared to its string value::
  215. >>> GqlErrorClassification.CLIENT_ERROR == "CLIENT_ERROR"
  216. True
  217. >>> GqlErrorClassification.DATABASE_ERROR == "DATABASE_ERROR"
  218. True
  219. >>> GqlErrorClassification.TRANSIENT_ERROR == "TRANSIENT_ERROR"
  220. True
  221. **This is a preview**.
  222. It might be changed without following the deprecation policy.
  223. See also
  224. https://github.com/neo4j/neo4j-python-driver/wiki/preview-features
  225. .. seealso:: :attr:`.GqlError.gql_classification`
  226. .. versionadded:: 5.26
  227. """
  228. CLIENT_ERROR = "CLIENT_ERROR"
  229. DATABASE_ERROR = "DATABASE_ERROR"
  230. TRANSIENT_ERROR = "TRANSIENT_ERROR"
  231. #: Used when the server provides a Classification which the driver is
  232. #: unaware of.
  233. #: This can happen when connecting to a server newer than the driver or
  234. #: before GQL errors were introduced.
  235. UNKNOWN = "UNKNOWN"
  236. class GqlError(Exception):
  237. """
  238. The GQL compliant data of an error.
  239. This error isn't raised by the driver as is.
  240. Instead, only subclasses are raised.
  241. Further, it is used as the :attr:`__cause__` of GqlError subclasses.
  242. **This is a preview**.
  243. It might be changed without following the deprecation policy.
  244. See also
  245. https://github.com/neo4j/neo4j-python-driver/wiki/preview-features
  246. .. versionadded: 5.26
  247. """
  248. _gql_status: str
  249. # TODO: 6.0 - make message always str
  250. _message: str | None
  251. _gql_status_description: str
  252. _gql_raw_classification: str | None
  253. _gql_classification: GqlErrorClassification
  254. _status_diagnostic_record: dict[str, t.Any] # original, internal only
  255. _diagnostic_record: dict[str, t.Any] # copy to be used externally
  256. _gql_cause: GqlError | None
  257. @staticmethod
  258. def _hydrate_cause(**metadata: t.Any) -> GqlError:
  259. meta_extractor = _MetaExtractor(metadata)
  260. gql_status = meta_extractor.str_value("gql_status")
  261. description = meta_extractor.str_value("description")
  262. message = meta_extractor.str_value("message")
  263. diagnostic_record = meta_extractor.map_value("diagnostic_record")
  264. cause_map = meta_extractor.map_value("cause")
  265. if cause_map is not None:
  266. cause = GqlError._hydrate_cause(**cause_map)
  267. else:
  268. cause = None
  269. inst = GqlError()
  270. inst._init_gql(
  271. gql_status=gql_status,
  272. message=message,
  273. description=description,
  274. diagnostic_record=diagnostic_record,
  275. cause=cause,
  276. )
  277. return inst
  278. def _init_gql(
  279. self,
  280. *,
  281. gql_status: str | None = None,
  282. message: str | None = None,
  283. description: str | None = None,
  284. diagnostic_record: dict[str, t.Any] | None = None,
  285. cause: GqlError | None = None,
  286. ) -> None:
  287. if gql_status is None or message is None or description is None:
  288. self._gql_status = _UNKNOWN_GQL_STATUS
  289. self._message = _UNKNOWN_GQL_MESSAGE
  290. self._gql_status_description = _UNKNOWN_GQL_DESCRIPTION
  291. else:
  292. self._gql_status = gql_status
  293. self._message = message
  294. self._gql_status_description = description
  295. if diagnostic_record is not None:
  296. self._status_diagnostic_record = diagnostic_record
  297. self._gql_cause = cause
  298. def _set_unknown_gql(self):
  299. self._gql_status = _UNKNOWN_GQL_STATUS
  300. self._message = _UNKNOWN_GQL_MESSAGE
  301. self._gql_status_description = _UNKNOWN_GQL_DESCRIPTION
  302. def __getattribute__(self, item):
  303. if item != "__cause__":
  304. return super().__getattribute__(item)
  305. gql_cause = self._get_attr_or_none("_gql_cause")
  306. if gql_cause is None:
  307. # No GQL cause, no magic needed
  308. return super().__getattribute__(item)
  309. local_cause = self._get_attr_or_none("__cause__")
  310. if local_cause is None:
  311. # We have a GQL cause but no local cause
  312. # => set the GQL cause as the local cause
  313. self.__cause__ = gql_cause
  314. self.__suppress_context__ = True
  315. self._gql_cause = None
  316. return super().__getattribute__(item)
  317. # We have both a GQL cause and a local cause
  318. # => traverse the cause chain and append the local cause.
  319. root = gql_cause
  320. seen_errors = {id(self), id(root)}
  321. while True:
  322. cause = getattr(root, "__cause__", None)
  323. if cause is None:
  324. root.__cause__ = local_cause
  325. root.__suppress_context__ = True
  326. self.__cause__ = gql_cause
  327. self.__suppress_context__ = True
  328. self._gql_cause = None
  329. return gql_cause
  330. root = cause
  331. if id(root) in seen_errors:
  332. # Circular cause chain -> we have no choice but to either
  333. # overwrite the cause or ignore the new one.
  334. return local_cause
  335. seen_errors.add(id(root))
  336. def _get_attr_or_none(self, item):
  337. try:
  338. return super().__getattribute__(item)
  339. except AttributeError:
  340. return None
  341. @property
  342. def _gql_status_no_preview(self) -> str:
  343. if hasattr(self, "_gql_status"):
  344. return self._gql_status
  345. self._set_unknown_gql()
  346. return self._gql_status
  347. @property
  348. @_preview("GQLSTATUS support is a preview feature.")
  349. def gql_status(self) -> str:
  350. """
  351. The GQLSTATUS returned from the server.
  352. The status code ``50N42`` (unknown error) is a special code that the
  353. driver will use for polyfilling (when connected to an old,
  354. non-GQL-aware server).
  355. Further, it may be used by servers during the transition-phase to
  356. GQLSTATUS-awareness.
  357. .. note::
  358. This means that the code ``50N42`` is not guaranteed to be stable
  359. and may change in future versions of the driver or the server.
  360. """
  361. return self._gql_status_no_preview
  362. @property
  363. def _message_no_preview(self) -> str | None:
  364. if hasattr(self, "_message"):
  365. return self._message
  366. self._set_unknown_gql()
  367. return self._message
  368. @property
  369. @_preview("GQLSTATUS support is a preview feature.")
  370. def message(self) -> str | None:
  371. """
  372. The error message returned by the server.
  373. It is a string representation of the error that occurred.
  374. This message is meant for human consumption and debugging purposes.
  375. Don't rely on it in a programmatic way.
  376. This value is never :data:`None` unless the subclass in question
  377. states otherwise.
  378. """
  379. return self._message_no_preview
  380. @property
  381. def _gql_status_description_no_preview(self) -> str:
  382. if hasattr(self, "_gql_status_description"):
  383. return self._gql_status_description
  384. self._set_unknown_gql()
  385. return self._gql_status_description
  386. @property
  387. @_preview("GQLSTATUS support is a preview feature.")
  388. def gql_status_description(self) -> str:
  389. """
  390. A description of the GQLSTATUS returned from the server.
  391. It describes the error that occurred in detail.
  392. This description is meant for human consumption and debugging purposes.
  393. Don't rely on it in a programmatic way.
  394. """
  395. return self._gql_status_description_no_preview
  396. @property
  397. def _gql_raw_classification_no_preview(self) -> str | None:
  398. if hasattr(self, "_gql_raw_classification"):
  399. return self._gql_raw_classification
  400. diag_record = self._get_status_diagnostic_record()
  401. classification = diag_record.get("_classification")
  402. if not isinstance(classification, str):
  403. self._gql_raw_classification = None
  404. else:
  405. self._gql_raw_classification = classification
  406. return self._gql_raw_classification
  407. @property
  408. @_preview("GQLSTATUS support is a preview feature.")
  409. def gql_raw_classification(self) -> str | None:
  410. """
  411. Vendor specific classification of the error.
  412. This is a convenience accessor for ``_classification`` in the
  413. diagnostic record.
  414. :data:`None` is returned if the classification is not available
  415. or not a string.
  416. """
  417. return self._gql_raw_classification_no_preview
  418. @property
  419. def _gql_classification_no_preview(self) -> GqlErrorClassification:
  420. if hasattr(self, "_gql_classification"):
  421. return self._gql_classification
  422. classification = self._gql_raw_classification_no_preview
  423. if not (
  424. isinstance(classification, str)
  425. and classification
  426. in t.cast(t.Iterable[str], iter(GqlErrorClassification))
  427. ):
  428. self._gql_classification = GqlErrorClassification.UNKNOWN
  429. else:
  430. self._gql_classification = GqlErrorClassification(classification)
  431. return self._gql_classification
  432. @property
  433. @_preview("GQLSTATUS support is a preview feature.")
  434. def gql_classification(self) -> GqlErrorClassification:
  435. return self._gql_classification_no_preview
  436. def _get_status_diagnostic_record(self) -> dict[str, t.Any]:
  437. if hasattr(self, "_status_diagnostic_record"):
  438. return self._status_diagnostic_record
  439. self._status_diagnostic_record = dict(_UNKNOWN_GQL_DIAGNOSTIC_RECORD)
  440. return self._status_diagnostic_record
  441. @property
  442. def _diagnostic_record_no_preview(self) -> Mapping[str, t.Any]:
  443. if hasattr(self, "_diagnostic_record"):
  444. return self._diagnostic_record
  445. self._diagnostic_record = _deepcopy(
  446. self._get_status_diagnostic_record()
  447. )
  448. return self._diagnostic_record
  449. @property
  450. @_preview("GQLSTATUS support is a preview feature.")
  451. def diagnostic_record(self) -> Mapping[str, t.Any]:
  452. return self._diagnostic_record_no_preview
  453. def __str__(self):
  454. return (
  455. f"{{gql_status: {self._gql_status_no_preview}}} "
  456. f"{{gql_status_description: "
  457. f"{self._gql_status_description_no_preview}}} "
  458. f"{{message: {self._message_no_preview}}} "
  459. f"{{diagnostic_record: {self._diagnostic_record_no_preview}}} "
  460. f"{{raw_classification: "
  461. f"{self._gql_raw_classification_no_preview}}}"
  462. )
  463. # Neo4jError
  464. class Neo4jError(GqlError):
  465. """Raised when the Cypher engine returns an error to the client."""
  466. _neo4j_code: str | None
  467. _classification: str | None
  468. _category: str | None
  469. _title: str | None
  470. #: (dict) Any additional information returned by the server.
  471. _metadata: dict[str, t.Any] | None
  472. _retryable = False
  473. def __init__(self, *args) -> None:
  474. Exception.__init__(self, *args)
  475. self._neo4j_code = None
  476. self._classification = None
  477. self._category = None
  478. self._title = None
  479. self._metadata = None
  480. self._message = None
  481. # TODO: 6.0 - do this instead to get rid of all optional attributes
  482. # self._neo4j_code = _UNKNOWN_NEO4J_CODE
  483. # _, self._classification, self._category, self._title = (
  484. # self._neo4j_code.split(".")
  485. # )
  486. # self._metadata = {}
  487. # self._init_gql()
  488. # TODO: 6.0 - Remove this alias
  489. @classmethod
  490. @deprecated(
  491. "Neo4jError.hydrate is deprecated and will be removed in a future "
  492. "version. It is an internal method and not meant for external use."
  493. )
  494. def hydrate(
  495. cls,
  496. code: str | None = None,
  497. message: str | None = None,
  498. **metadata: t.Any,
  499. ) -> Neo4jError:
  500. # backward compatibility: make falsy values None
  501. code = code or None
  502. message = message or None
  503. return cls._hydrate_neo4j(code=code, message=message, **metadata)
  504. @classmethod
  505. def _hydrate_neo4j(cls, **metadata: t.Any) -> Neo4jError:
  506. meta_extractor = _MetaExtractor(metadata)
  507. code = meta_extractor.str_value("code") or _UNKNOWN_NEO4J_CODE
  508. message = meta_extractor.str_value("message") or _UNKNOWN_MESSAGE
  509. inst = cls._basic_hydrate(
  510. neo4j_code=code,
  511. message=message,
  512. )
  513. inst._init_gql(
  514. gql_status=_UNKNOWN_GQL_STATUS,
  515. message=message,
  516. description=f"{_UNKNOWN_GQL_DESCRIPTION}. {message}",
  517. )
  518. inst._metadata = meta_extractor.rest()
  519. return inst
  520. @classmethod
  521. def _hydrate_gql(cls, **metadata: t.Any) -> Neo4jError:
  522. meta_extractor = _MetaExtractor(metadata)
  523. gql_status = meta_extractor.str_value("gql_status")
  524. status_description = meta_extractor.str_value("description")
  525. message = meta_extractor.str_value("message")
  526. if gql_status is None or status_description is None or message is None:
  527. gql_status = _UNKNOWN_GQL_STATUS
  528. # TODO: 6.0 - Make this fall back to _UNKNOWN_GQL_MESSAGE
  529. message = _UNKNOWN_MESSAGE
  530. status_description = _UNKNOWN_GQL_DESCRIPTION
  531. neo4j_code = meta_extractor.str_value(
  532. "neo4j_code",
  533. _UNKNOWN_NEO4J_CODE,
  534. )
  535. diagnostic_record = meta_extractor.map_value("diagnostic_record")
  536. cause_map = meta_extractor.map_value("cause")
  537. if cause_map is not None:
  538. cause = cls._hydrate_cause(**cause_map)
  539. else:
  540. cause = None
  541. inst = cls._basic_hydrate(
  542. neo4j_code=neo4j_code,
  543. message=message,
  544. )
  545. inst._init_gql(
  546. gql_status=gql_status,
  547. message=message,
  548. description=status_description,
  549. diagnostic_record=diagnostic_record,
  550. cause=cause,
  551. )
  552. inst._metadata = meta_extractor.rest()
  553. return inst
  554. @classmethod
  555. def _basic_hydrate(cls, *, neo4j_code: str, message: str) -> Neo4jError:
  556. try:
  557. _, classification, category, title = neo4j_code.split(".")
  558. except ValueError:
  559. classification = CLASSIFICATION_DATABASE
  560. category = "General"
  561. title = "UnknownError"
  562. else:
  563. classification_override, code_override = ERROR_REWRITE_MAP.get(
  564. neo4j_code, (None, None)
  565. )
  566. if classification_override is not None:
  567. classification = classification_override
  568. if code_override is not None:
  569. neo4j_code = code_override
  570. error_class: type[Neo4jError] = cls._extract_error_class(
  571. classification, neo4j_code
  572. )
  573. inst = error_class(message)
  574. inst._neo4j_code = neo4j_code
  575. inst._classification = classification
  576. inst._category = category
  577. inst._title = title
  578. inst._message = message
  579. return inst
  580. @classmethod
  581. def _extract_error_class(cls, classification, code) -> type[Neo4jError]:
  582. if classification == CLASSIFICATION_CLIENT:
  583. try:
  584. return client_errors[code]
  585. except KeyError:
  586. return ClientError
  587. elif classification == CLASSIFICATION_TRANSIENT:
  588. try:
  589. return transient_errors[code]
  590. except KeyError:
  591. return TransientError
  592. elif classification == CLASSIFICATION_DATABASE:
  593. return DatabaseError
  594. else:
  595. return cls
  596. @property
  597. def message(self) -> str | None:
  598. """
  599. The error message returned by the server.
  600. This value is only :data:`None` for locally created errors.
  601. """
  602. return self._message
  603. @message.setter
  604. @deprecated("Altering the message of a Neo4jError is deprecated.")
  605. def message(self, value: str) -> None:
  606. self._message = value
  607. @property
  608. def code(self) -> str | None:
  609. """
  610. The neo4j error code returned by the server.
  611. For example, "Neo.ClientError.Security.AuthorizationExpired".
  612. This value is only :data:`None` for locally created errors.
  613. """
  614. return self._neo4j_code
  615. # TODO: 6.0 - Remove this and all other deprecated setters
  616. @code.setter
  617. @deprecated("Altering the code of a Neo4jError is deprecated.")
  618. def code(self, value: str) -> None:
  619. self._neo4j_code = value
  620. @property
  621. def classification(self) -> str | None:
  622. # Undocumented, will likely be removed with support for neo4j codes
  623. return self._classification
  624. @classification.setter
  625. @deprecated("Altering the classification of Neo4jError is deprecated.")
  626. def classification(self, value: str) -> None:
  627. self._classification = value
  628. @property
  629. def category(self) -> str | None:
  630. # Undocumented, will likely be removed with support for neo4j codes
  631. return self._category
  632. @category.setter
  633. @deprecated("Altering the category of Neo4jError is deprecated.")
  634. def category(self, value: str) -> None:
  635. self._category = value
  636. @property
  637. def title(self) -> str | None:
  638. # Undocumented, will likely be removed with support for neo4j codes
  639. return self._title
  640. @title.setter
  641. @deprecated("Altering the title of Neo4jError is deprecated.")
  642. def title(self, value: str) -> None:
  643. self._title = value
  644. @property
  645. def metadata(self) -> dict[str, t.Any] | None:
  646. # Undocumented, might be useful for debugging
  647. return self._metadata
  648. @metadata.setter
  649. @deprecated("Altering the metadata of Neo4jError is deprecated.")
  650. def metadata(self, value: dict[str, t.Any]) -> None:
  651. self._metadata = value
  652. # TODO: 6.0 - Remove this alias
  653. @deprecated(
  654. "Neo4jError.is_retriable is deprecated and will be removed in a "
  655. "future version. Please use Neo4jError.is_retryable instead."
  656. )
  657. def is_retriable(self) -> bool:
  658. """
  659. Whether the error is retryable.
  660. See :meth:`.is_retryable`.
  661. :returns: :data:`True` if the error is retryable,
  662. :data:`False` otherwise.
  663. .. deprecated:: 5.0
  664. This method will be removed in a future version.
  665. Please use :meth:`.is_retryable` instead.
  666. """
  667. return self.is_retryable()
  668. def is_retryable(self) -> bool:
  669. """
  670. Whether the error is retryable.
  671. Indicates whether a transaction that yielded this error makes sense to
  672. retry. This method makes mostly sense when implementing a custom
  673. retry policy in conjunction with :ref:`explicit-transactions-ref`.
  674. :returns: :data:`True` if the error is retryable,
  675. :data:`False` otherwise.
  676. .. versionadded:: 5.0
  677. """
  678. return self._retryable
  679. def _unauthenticates_all_connections(self) -> bool:
  680. return (
  681. self._neo4j_code == "Neo.ClientError.Security.AuthorizationExpired"
  682. )
  683. # TODO: 6.0 - Remove this alias
  684. invalidates_all_connections = deprecated(
  685. "Neo4jError.invalidates_all_connections is deprecated and will be "
  686. "removed in a future version. It is an internal method and not meant "
  687. "for external use."
  688. )(_unauthenticates_all_connections)
  689. def _is_fatal_during_discovery(self) -> bool:
  690. # checks if the code is an error that is caused by the client. In this
  691. # case the driver should fail fast during discovery.
  692. code = self._neo4j_code
  693. if not isinstance(code, str):
  694. return False
  695. if code in {
  696. "Neo.ClientError.Database.DatabaseNotFound",
  697. "Neo.ClientError.Transaction.InvalidBookmark",
  698. "Neo.ClientError.Transaction.InvalidBookmarkMixture",
  699. "Neo.ClientError.Statement.TypeError",
  700. "Neo.ClientError.Statement.ArgumentError",
  701. "Neo.ClientError.Request.Invalid",
  702. }:
  703. return True
  704. return (
  705. code.startswith("Neo.ClientError.Security.")
  706. and code != "Neo.ClientError.Security.AuthorizationExpired"
  707. )
  708. def _has_security_code(self) -> bool:
  709. if self._neo4j_code is None:
  710. return False
  711. return self._neo4j_code.startswith("Neo.ClientError.Security.")
  712. # TODO: 6.0 - Remove this alias
  713. is_fatal_during_discovery = deprecated(
  714. "Neo4jError.is_fatal_during_discovery is deprecated and will be "
  715. "removed in a future version. It is an internal method and not meant "
  716. "for external use."
  717. )(_is_fatal_during_discovery)
  718. def __str__(self):
  719. code = self._neo4j_code
  720. message = self._message
  721. if code or message:
  722. return f"{{code: {code}}} {{message: {message}}}"
  723. # TODO: 6.0 - Use gql status and status_description instead
  724. # something like:
  725. # return (
  726. # f"{{gql_status: {self.gql_status}}} "
  727. # f"{{neo4j_code: {self.neo4j_code}}} "
  728. # f"{{gql_status_description: {self.gql_status_description}}} "
  729. # f"{{diagnostic_record: {self.diagnostic_record}}}"
  730. # )
  731. return Exception.__str__(self)
  732. class _MetaExtractor:
  733. def __init__(self, metadata: dict[str, t.Any]):
  734. self._metadata = metadata
  735. def rest(self) -> dict[str, t.Any]:
  736. return self._metadata
  737. @t.overload
  738. def str_value(self, key: str) -> str | None: ...
  739. @t.overload
  740. def str_value(self, key: str, default: _T) -> str | _T: ...
  741. def str_value(
  742. self, key: str, default: _T | None = None
  743. ) -> str | _T | None:
  744. res = self._metadata.pop(key, default)
  745. if not isinstance(res, str):
  746. res = default
  747. return res
  748. @t.overload
  749. def map_value(self, key: str) -> dict[str, t.Any] | None: ...
  750. @t.overload
  751. def map_value(self, key: str, default: _T) -> dict[str, t.Any] | _T: ...
  752. def map_value(
  753. self, key: str, default: _T | None = None
  754. ) -> dict[str, t.Any] | _T | None:
  755. res = self._metadata.pop(key, default)
  756. if not (
  757. isinstance(res, dict) and all(isinstance(k, str) for k in res)
  758. ):
  759. res = default
  760. return res
  761. # Neo4jError > ClientError
  762. class ClientError(Neo4jError):
  763. """
  764. Bad client request.
  765. The Client sent a bad request - changing the request might yield a
  766. successful outcome.
  767. """
  768. # Neo4jError > ClientError > CypherSyntaxError
  769. class CypherSyntaxError(ClientError):
  770. pass
  771. # Neo4jError > ClientError > CypherTypeError
  772. class CypherTypeError(ClientError):
  773. pass
  774. # Neo4jError > ClientError > ConstraintError
  775. class ConstraintError(ClientError):
  776. pass
  777. # Neo4jError > ClientError > AuthError
  778. class AuthError(ClientError):
  779. """Raised when authentication failure occurs."""
  780. # Neo4jError > ClientError > AuthError > TokenExpired
  781. class TokenExpired(AuthError):
  782. """Raised when the authentication token has expired."""
  783. # Neo4jError > ClientError > Forbidden
  784. class Forbidden(ClientError):
  785. pass
  786. # Neo4jError > DatabaseError
  787. class DatabaseError(Neo4jError):
  788. """The database failed to service the request."""
  789. # Neo4jError > TransientError
  790. class TransientError(Neo4jError):
  791. """
  792. Transient Error.
  793. The database cannot service the request right now, retrying later might
  794. yield a successful outcome.
  795. """
  796. _retryable = True
  797. # Neo4jError > TransientError > DatabaseUnavailable
  798. class DatabaseUnavailable(TransientError):
  799. pass
  800. # Neo4jError > TransientError > NotALeader
  801. class NotALeader(TransientError):
  802. pass
  803. # Neo4jError > TransientError > ForbiddenOnReadOnlyDatabase
  804. class ForbiddenOnReadOnlyDatabase(TransientError):
  805. pass
  806. # TODO: 6.0 - Make map private
  807. client_errors: dict[str, type[Neo4jError]] = {
  808. # ConstraintError
  809. "Neo.ClientError.Schema.ConstraintValidationFailed": ConstraintError,
  810. "Neo.ClientError.Schema.ConstraintViolation": ConstraintError,
  811. "Neo.ClientError.Statement.ConstraintVerificationFailed": ConstraintError,
  812. "Neo.ClientError.Statement.ConstraintViolation": ConstraintError,
  813. # CypherSyntaxError
  814. "Neo.ClientError.Statement.InvalidSyntax": CypherSyntaxError,
  815. "Neo.ClientError.Statement.SyntaxError": CypherSyntaxError,
  816. # CypherTypeError
  817. "Neo.ClientError.Procedure.TypeError": CypherTypeError,
  818. "Neo.ClientError.Statement.InvalidType": CypherTypeError,
  819. "Neo.ClientError.Statement.TypeError": CypherTypeError,
  820. # Forbidden
  821. "Neo.ClientError.General.ForbiddenOnReadOnlyDatabase": ForbiddenOnReadOnlyDatabase, # noqa: E501
  822. "Neo.ClientError.General.ReadOnly": Forbidden,
  823. "Neo.ClientError.Schema.ForbiddenOnConstraintIndex": Forbidden,
  824. "Neo.ClientError.Schema.IndexBelongsToConstraint": Forbidden,
  825. "Neo.ClientError.Security.Forbidden": Forbidden,
  826. "Neo.ClientError.Transaction.ForbiddenDueToTransactionType": Forbidden,
  827. # AuthError
  828. "Neo.ClientError.Security.AuthorizationFailed": AuthError,
  829. "Neo.ClientError.Security.Unauthorized": AuthError,
  830. # TokenExpired
  831. "Neo.ClientError.Security.TokenExpired": TokenExpired,
  832. # NotALeader
  833. "Neo.ClientError.Cluster.NotALeader": NotALeader,
  834. }
  835. # TODO: 6.0 - Make map private
  836. transient_errors: dict[str, type[Neo4jError]] = {
  837. # DatabaseUnavailableError
  838. "Neo.TransientError.General.DatabaseUnavailable": DatabaseUnavailable
  839. }
  840. # DriverError
  841. class DriverError(GqlError):
  842. """Raised when the Driver raises an error."""
  843. def is_retryable(self) -> bool:
  844. """
  845. Whether the error is retryable.
  846. Indicates whether a transaction that yielded this error makes sense to
  847. retry. This method makes mostly sense when implementing a custom
  848. retry policy in conjunction with :ref:`explicit-transactions-ref`.
  849. :returns: :data:`True` if the error is retryable,
  850. :data:`False` otherwise.
  851. .. versionadded:: 5.0
  852. """
  853. return False
  854. def __str__(self):
  855. return Exception.__str__(self)
  856. # DriverError > SessionError
  857. class SessionError(DriverError):
  858. """Raised when an error occurs while using a session."""
  859. session: _TSession
  860. def __init__(self, session_, *args, **kwargs):
  861. super().__init__(*args, **kwargs)
  862. self.session = session_
  863. # DriverError > TransactionError
  864. class TransactionError(DriverError):
  865. """Raised when an error occurs while using a transaction."""
  866. transaction: _TTransaction
  867. def __init__(self, transaction_, *args, **kwargs):
  868. super().__init__(*args, **kwargs)
  869. self.transaction = transaction_
  870. # DriverError > TransactionError > TransactionNestingError
  871. class TransactionNestingError(TransactionError):
  872. """Raised when transactions are nested incorrectly."""
  873. # DriverError > ResultError
  874. class ResultError(DriverError):
  875. """Raised when an error occurs while using a result object."""
  876. result: _TResult
  877. def __init__(self, result_, *args, **kwargs):
  878. super().__init__(*args, **kwargs)
  879. self.result = result_
  880. # DriverError > ResultError > ResultFailedError
  881. class ResultFailedError(ResultError):
  882. """
  883. Raised when trying to access records of a failed result.
  884. A :class:`.Result` will be considered failed if
  885. * itself encountered an error while fetching records
  886. * another result within the same transaction encountered an error while
  887. fetching records
  888. """
  889. # DriverError > ResultError > ResultConsumedError
  890. class ResultConsumedError(ResultError):
  891. """Raised when trying to access records of a consumed result."""
  892. # DriverError > ResultError > ResultNotSingleError
  893. class ResultNotSingleError(ResultError):
  894. """Raised when a result should have exactly one record but does not."""
  895. # DriverError > BrokenRecordError
  896. class BrokenRecordError(DriverError):
  897. """
  898. Raised when accessing a Record's field that couldn't be decoded.
  899. This can for instance happen when the server sends a zoned datetime with a
  900. zone id unknown to the client.
  901. """
  902. # DriverError > SessionExpired
  903. class SessionExpired(DriverError):
  904. """
  905. The session has expired.
  906. Raised when a session is no longer able to fulfil the purpose described by
  907. its original parameters.
  908. """
  909. def __init__(self, *args):
  910. super().__init__(*args)
  911. self._init_gql(
  912. gql_status="08000",
  913. description="error: connection exception",
  914. )
  915. def is_retryable(self) -> bool:
  916. return True
  917. # DriverError > ServiceUnavailable
  918. class ServiceUnavailable(DriverError):
  919. """
  920. Raised when no database service is available.
  921. This may be due to incorrect configuration or could indicate a runtime
  922. failure of a database service that the driver is unable to route around.
  923. """
  924. def __init__(self, *args):
  925. super().__init__(*args)
  926. self._init_gql(
  927. gql_status="08000",
  928. description="error: connection exception",
  929. )
  930. def is_retryable(self) -> bool:
  931. return True
  932. # DriverError > ServiceUnavailable > RoutingServiceUnavailable
  933. class RoutingServiceUnavailable(ServiceUnavailable):
  934. """Raised when no routing service is available."""
  935. # DriverError > ServiceUnavailable > WriteServiceUnavailable
  936. class WriteServiceUnavailable(ServiceUnavailable):
  937. """Raised when no write service is available."""
  938. # DriverError > ServiceUnavailable > ReadServiceUnavailable
  939. class ReadServiceUnavailable(ServiceUnavailable):
  940. """Raised when no read service is available."""
  941. # DriverError > ServiceUnavailable > IncompleteCommit
  942. class IncompleteCommit(ServiceUnavailable):
  943. """
  944. Raised when the client looses connection while committing a transaction.
  945. Raised when a disconnection occurs while still waiting for a commit
  946. response. For non-idempotent write transactions, this leaves the data
  947. in an unknown state with regard to whether the transaction completed
  948. successfully or not.
  949. """
  950. def __init__(self, *args):
  951. super().__init__(*args)
  952. self._init_gql(
  953. gql_status="08007",
  954. description=(
  955. "error: connection exception - "
  956. "transaction resolution unknown"
  957. ),
  958. )
  959. def is_retryable(self) -> bool:
  960. return False
  961. # DriverError > ConfigurationError
  962. class ConfigurationError(DriverError):
  963. """Raised when there is an error concerning a configuration."""
  964. # DriverError > ConfigurationError > AuthConfigurationError
  965. class AuthConfigurationError(ConfigurationError):
  966. """Raised when there is an error with the authentication configuration."""
  967. # DriverError > ConfigurationError > CertificateConfigurationError
  968. class CertificateConfigurationError(ConfigurationError):
  969. """Raised when there is an error with the certificate configuration."""
  970. class UnsupportedServerProduct(Exception):
  971. """Raised when an unsupported server product is detected."""