models.py 60 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613
  1. from __future__ import annotations
  2. import typing as t
  3. from dataclasses import asdict, dataclass, field
  4. from enum import Enum
  5. from rdflib import Literal, URIRef
  6. from rdflib.util import from_n3
  7. @dataclass(frozen=True)
  8. class TopologyStatus:
  9. state: str
  10. primaryTags: dict[str, t.Any] # noqa: N815
  11. def __post_init__(self) -> None:
  12. invalid: list[tuple[str, t.Any, type]] = []
  13. state = t.cast(t.Any, self.state)
  14. primary_tags = t.cast(t.Any, self.primaryTags)
  15. if not isinstance(state, str):
  16. invalid.append(("state", state, type(state)))
  17. if not isinstance(primary_tags, dict):
  18. invalid.append(("primaryTags", primary_tags, type(primary_tags)))
  19. else:
  20. for key, value in primary_tags.items():
  21. if not isinstance(key, str):
  22. invalid.append((f"primaryTags key '{key}'", key, type(key)))
  23. if invalid:
  24. raise ValueError("Invalid TopologyStatus values: ", invalid)
  25. @dataclass(frozen=True)
  26. class RecoveryOperation:
  27. name: str
  28. message: str
  29. def __post_init__(self) -> None:
  30. invalid: list[tuple[str, t.Any, type]] = []
  31. name = t.cast(t.Any, self.name)
  32. message = t.cast(t.Any, self.message)
  33. if not isinstance(name, str):
  34. invalid.append(("name", name, type(name)))
  35. if not isinstance(message, str):
  36. invalid.append(("message", message, type(message)))
  37. if invalid:
  38. raise ValueError("Invalid RecoveryOperation values: ", invalid)
  39. @dataclass(frozen=True)
  40. class RecoveryStatus:
  41. state: RecoveryOperation | None = None
  42. message: str | None = None
  43. affectedNodes: list[str] = field(default_factory=list) # noqa: N815
  44. def __post_init__(self) -> None:
  45. invalid: list[tuple[str, t.Any, type]] = []
  46. state = t.cast(t.Any, self.state)
  47. message = t.cast(t.Any, self.message)
  48. affected_nodes = t.cast(t.Any, self.affectedNodes)
  49. if state is not None and not isinstance(state, RecoveryOperation):
  50. invalid.append(("state", state, type(state)))
  51. if message is not None and not isinstance(message, str):
  52. invalid.append(("message", message, type(message)))
  53. if not isinstance(affected_nodes, list):
  54. invalid.append(("affectedNodes", affected_nodes, type(affected_nodes)))
  55. else:
  56. for index, value in enumerate(affected_nodes):
  57. if not isinstance(value, str):
  58. invalid.append((f"affectedNodes[{index}]", value, type(value)))
  59. if invalid:
  60. raise ValueError("Invalid RecoveryStatus values: ", invalid)
  61. @classmethod
  62. def from_dict(cls, data: dict) -> RecoveryStatus:
  63. """Create a RecoveryStatus instance from a dict.
  64. This is useful for converting JSON response data into the dataclass structure.
  65. Handles empty dict {} by returning a RecoveryStatus with all None/default values.
  66. The nested 'state' dict (if present) is automatically converted to
  67. a RecoveryOperation instance.
  68. Parameters:
  69. data: A dict containing the recovery status data, typically
  70. parsed from a JSON response. Can be an empty dict.
  71. Returns:
  72. A RecoveryStatus instance with nested dataclass objects.
  73. Raises:
  74. KeyError: If required keys are missing from the input dict.
  75. TypeError: If nested 'state' cannot be unpacked into RecoveryOperation.
  76. ValueError: If field validation fails in RecoveryOperation or
  77. RecoveryStatus (e.g., invalid types).
  78. """
  79. # Handle empty dict case
  80. if not data:
  81. return cls()
  82. state = None
  83. if "state" in data and data["state"] is not None:
  84. state = RecoveryOperation(**data["state"])
  85. return cls(
  86. state=state,
  87. message=data.get("message"),
  88. affectedNodes=data.get("affectedNodes", []),
  89. )
  90. @dataclass(frozen=True)
  91. class NodeStatus:
  92. address: str
  93. nodeState: str # noqa: N815
  94. term: int
  95. syncStatus: dict[str, str] # noqa: N815
  96. lastLogTerm: int # noqa: N815
  97. lastLogIndex: int # noqa: N815
  98. endpoint: str
  99. recoveryStatus: RecoveryStatus | None # noqa: N815
  100. topologyStatus: TopologyStatus | None # noqa: N815
  101. clusterEnabled: bool | None # noqa: N815
  102. def __post_init__(self) -> None:
  103. invalid: list[tuple[str, t.Any, type]] = []
  104. address = t.cast(t.Any, self.address)
  105. node_state = t.cast(t.Any, self.nodeState)
  106. term = t.cast(t.Any, self.term)
  107. sync_status = t.cast(t.Any, self.syncStatus)
  108. last_log_term = t.cast(t.Any, self.lastLogTerm)
  109. last_log_index = t.cast(t.Any, self.lastLogIndex)
  110. endpoint = t.cast(t.Any, self.endpoint)
  111. recovery_status = t.cast(t.Any, self.recoveryStatus)
  112. topology_status = t.cast(t.Any, self.topologyStatus)
  113. cluster_enabled = t.cast(t.Any, self.clusterEnabled)
  114. if not isinstance(address, str):
  115. invalid.append(("address", address, type(address)))
  116. if not isinstance(node_state, str):
  117. invalid.append(("nodeState", node_state, type(node_state)))
  118. if type(term) is not int:
  119. invalid.append(("term", term, type(term)))
  120. if not isinstance(sync_status, dict):
  121. invalid.append(("syncStatus", sync_status, type(sync_status)))
  122. else:
  123. for key, value in sync_status.items():
  124. if not isinstance(key, str):
  125. invalid.append((f"syncStatus key '{key}'", key, type(key)))
  126. if not isinstance(value, str):
  127. invalid.append((f"syncStatus['{key}']", value, type(value)))
  128. if type(last_log_term) is not int:
  129. invalid.append(("lastLogTerm", last_log_term, type(last_log_term)))
  130. if type(last_log_index) is not int:
  131. invalid.append(("lastLogIndex", last_log_index, type(last_log_index)))
  132. if not isinstance(endpoint, str):
  133. invalid.append(("endpoint", endpoint, type(endpoint)))
  134. if recovery_status is not None and not isinstance(
  135. recovery_status, RecoveryStatus
  136. ):
  137. invalid.append(("recoveryStatus", recovery_status, type(recovery_status)))
  138. if topology_status is not None and not isinstance(
  139. topology_status, TopologyStatus
  140. ):
  141. invalid.append(("topologyStatus", topology_status, type(topology_status)))
  142. if cluster_enabled is not None and type(cluster_enabled) is not bool:
  143. invalid.append(("clusterEnabled", cluster_enabled, type(cluster_enabled)))
  144. if invalid:
  145. raise ValueError("Invalid NodeStatus values: ", invalid)
  146. @classmethod
  147. def from_dict(cls, data: dict) -> NodeStatus:
  148. """Create a NodeStatus instance from a dict.
  149. This is useful for converting JSON response data into the dataclass structure.
  150. The nested 'recoveryStatus' and 'topologyStatus' dicts are automatically
  151. converted to their respective dataclass instances.
  152. Parameters:
  153. data: A dict containing the node status data, typically
  154. parsed from a JSON response.
  155. Returns:
  156. A NodeStatus instance with nested dataclass objects.
  157. Raises:
  158. KeyError: If required keys are missing from the input dict.
  159. TypeError: If nested dicts cannot be unpacked into their respective
  160. dataclass instances.
  161. ValueError: If field validation fails in TopologyStatus, RecoveryOperation,
  162. RecoveryStatus, or NodeStatus (e.g., invalid types).
  163. """
  164. # Handle recoveryStatus - can be empty dict or actual data
  165. recovery_status = None
  166. if "recoveryStatus" in data and data["recoveryStatus"]:
  167. recovery_status = RecoveryStatus.from_dict(data["recoveryStatus"])
  168. elif "recoveryStatus" in data:
  169. # Empty dict case
  170. recovery_status = RecoveryStatus.from_dict({})
  171. # Handle topologyStatus - optional field
  172. topology_status = None
  173. if "topologyStatus" in data and data["topologyStatus"]:
  174. topology_status = TopologyStatus(**data["topologyStatus"])
  175. return cls(
  176. address=data["address"],
  177. nodeState=data["nodeState"],
  178. term=data["term"],
  179. syncStatus=data["syncStatus"],
  180. lastLogTerm=data["lastLogTerm"],
  181. lastLogIndex=data["lastLogIndex"],
  182. endpoint=data["endpoint"],
  183. recoveryStatus=recovery_status,
  184. topologyStatus=topology_status,
  185. clusterEnabled=data.get("clusterEnabled"),
  186. )
  187. @dataclass(frozen=True)
  188. class ClusterRequest:
  189. electionMinTimeout: int # noqa: N815
  190. electionRangeTimeout: int # noqa: N815
  191. heartbeatInterval: int # noqa: N815
  192. messageSizeKB: int # noqa: N815
  193. verificationTimeout: int # noqa: N815
  194. transactionLogMaximumSizeGB: int # noqa: N815
  195. batchUpdateInterval: int # noqa: N815
  196. nodes: list[str]
  197. def __post_init__(self) -> None:
  198. invalid: list[tuple[str, t.Any, type]] = []
  199. election_min_timeout = t.cast(t.Any, self.electionMinTimeout)
  200. election_range_timeout = t.cast(t.Any, self.electionRangeTimeout)
  201. heartbeat_interval = t.cast(t.Any, self.heartbeatInterval)
  202. message_size_kb = t.cast(t.Any, self.messageSizeKB)
  203. verification_timeout = t.cast(t.Any, self.verificationTimeout)
  204. transaction_log_maximum_size_gb = t.cast(
  205. t.Any, self.transactionLogMaximumSizeGB
  206. )
  207. batch_update_interval = t.cast(t.Any, self.batchUpdateInterval)
  208. nodes = t.cast(t.Any, self.nodes)
  209. if type(election_min_timeout) is not int:
  210. invalid.append(
  211. ("electionMinTimeout", election_min_timeout, type(election_min_timeout))
  212. )
  213. if type(election_range_timeout) is not int:
  214. invalid.append(
  215. (
  216. "electionRangeTimeout",
  217. election_range_timeout,
  218. type(election_range_timeout),
  219. )
  220. )
  221. if type(heartbeat_interval) is not int:
  222. invalid.append(
  223. ("heartbeatInterval", heartbeat_interval, type(heartbeat_interval))
  224. )
  225. if type(message_size_kb) is not int:
  226. invalid.append(("messageSizeKB", message_size_kb, type(message_size_kb)))
  227. if type(verification_timeout) is not int:
  228. invalid.append(
  229. (
  230. "verificationTimeout",
  231. verification_timeout,
  232. type(verification_timeout),
  233. )
  234. )
  235. if type(transaction_log_maximum_size_gb) is not int:
  236. invalid.append(
  237. (
  238. "transactionLogMaximumSizeGB",
  239. transaction_log_maximum_size_gb,
  240. type(transaction_log_maximum_size_gb),
  241. )
  242. )
  243. if type(batch_update_interval) is not int:
  244. invalid.append(
  245. (
  246. "batchUpdateInterval",
  247. batch_update_interval,
  248. type(batch_update_interval),
  249. )
  250. )
  251. if not isinstance(nodes, list):
  252. invalid.append(("nodes", nodes, type(nodes)))
  253. else:
  254. for index, value in enumerate(nodes):
  255. if not isinstance(value, str):
  256. invalid.append((f"nodes[{index}]", value, type(value)))
  257. if invalid:
  258. raise ValueError("Invalid ClusterRequest values: ", invalid)
  259. @classmethod
  260. def from_dict(cls, data: dict) -> ClusterRequest:
  261. """Create a ClusterRequest instance from a dict.
  262. This is useful for converting JSON response data into the dataclass structure.
  263. Parameters:
  264. data: A dict containing the cluster request data, typically
  265. parsed from a JSON response.
  266. Returns:
  267. A ClusterRequest instance.
  268. Raises:
  269. KeyError: If required keys are missing from the input dict.
  270. TypeError: If the dict cannot be unpacked into ClusterRequest.
  271. ValueError: If field validation fails in ClusterRequest
  272. (e.g., invalid types for integer fields or nodes).
  273. """
  274. return cls(
  275. electionMinTimeout=data["electionMinTimeout"],
  276. electionRangeTimeout=data["electionRangeTimeout"],
  277. heartbeatInterval=data["heartbeatInterval"],
  278. messageSizeKB=data["messageSizeKB"],
  279. verificationTimeout=data["verificationTimeout"],
  280. transactionLogMaximumSizeGB=data["transactionLogMaximumSizeGB"],
  281. batchUpdateInterval=data["batchUpdateInterval"],
  282. nodes=data["nodes"],
  283. )
  284. @dataclass(frozen=True)
  285. class SnapshotOptionsBean:
  286. withRepositoryData: bool # noqa: N815
  287. withSystemData: bool # noqa: N815
  288. cleanDataDir: bool # noqa: N815
  289. repositories: list[str] | None = None
  290. def __post_init__(self) -> None:
  291. invalid: list[tuple[str, t.Any, type]] = []
  292. with_repository_data = t.cast(t.Any, self.withRepositoryData)
  293. with_system_data = t.cast(t.Any, self.withSystemData)
  294. clean_data_dir = t.cast(t.Any, self.cleanDataDir)
  295. repositories = t.cast(t.Any, self.repositories)
  296. if type(with_repository_data) is not bool:
  297. invalid.append(
  298. ("withRepositoryData", with_repository_data, type(with_repository_data))
  299. )
  300. if type(with_system_data) is not bool:
  301. invalid.append(("withSystemData", with_system_data, type(with_system_data)))
  302. if type(clean_data_dir) is not bool:
  303. invalid.append(("cleanDataDir", clean_data_dir, type(clean_data_dir)))
  304. if repositories is not None:
  305. if not isinstance(repositories, list):
  306. invalid.append(("repositories", repositories, type(repositories)))
  307. else:
  308. for index, value in enumerate(repositories):
  309. if not isinstance(value, str):
  310. invalid.append((f"repositories[{index}]", value, type(value)))
  311. if invalid:
  312. raise ValueError("Invalid SnapshotOptionsBean values: ", invalid)
  313. @dataclass(frozen=True)
  314. class BackupOperationBean:
  315. id: str
  316. username: str
  317. operation: t.Literal[
  318. "CREATE_BACKUP_IN_PROGRESS",
  319. "RESTORE_BACKUP_IN_PROGRESS",
  320. "CREATE_CLOUD_BACKUP_IN_PROGRESS",
  321. "RESTORE_CLOUD_BACKUP_IN_PROGRESS",
  322. ]
  323. affectedRepositories: list[str] # noqa: N815
  324. msSinceCreated: int # noqa: N815
  325. snapshotOptions: SnapshotOptionsBean # noqa: N815
  326. nodePerformingClusterBackup: str | None = None # noqa: N815
  327. def __post_init__(self) -> None:
  328. _allowed_operations = {
  329. "CREATE_BACKUP_IN_PROGRESS",
  330. "RESTORE_BACKUP_IN_PROGRESS",
  331. "CREATE_CLOUD_BACKUP_IN_PROGRESS",
  332. "RESTORE_CLOUD_BACKUP_IN_PROGRESS",
  333. }
  334. invalid: list[tuple[str, t.Any, type]] = []
  335. id_ = t.cast(t.Any, self.id)
  336. username = t.cast(t.Any, self.username)
  337. operation = t.cast(t.Any, self.operation)
  338. affected_repositories = t.cast(t.Any, self.affectedRepositories)
  339. ms_since_created = t.cast(t.Any, self.msSinceCreated)
  340. snapshot_options = t.cast(t.Any, self.snapshotOptions)
  341. node_performing_cluster_backup = t.cast(t.Any, self.nodePerformingClusterBackup)
  342. if not isinstance(id_, str):
  343. invalid.append(("id", id_, type(id_)))
  344. if not isinstance(username, str):
  345. invalid.append(("username", username, type(username)))
  346. if not isinstance(operation, str) or operation not in _allowed_operations:
  347. invalid.append(("operation", operation, type(operation)))
  348. if not isinstance(affected_repositories, list):
  349. invalid.append(
  350. (
  351. "affectedRepositories",
  352. affected_repositories,
  353. type(affected_repositories),
  354. )
  355. )
  356. else:
  357. for index, value in enumerate(affected_repositories):
  358. if not isinstance(value, str):
  359. invalid.append(
  360. (f"affectedRepositories[{index}]", value, type(value))
  361. )
  362. if type(ms_since_created) is not int:
  363. invalid.append(("msSinceCreated", ms_since_created, type(ms_since_created)))
  364. if not isinstance(snapshot_options, SnapshotOptionsBean):
  365. invalid.append(
  366. ("snapshotOptions", snapshot_options, type(snapshot_options))
  367. )
  368. if node_performing_cluster_backup is not None and not isinstance(
  369. node_performing_cluster_backup, str
  370. ):
  371. invalid.append(
  372. (
  373. "nodePerformingClusterBackup",
  374. node_performing_cluster_backup,
  375. type(node_performing_cluster_backup),
  376. )
  377. )
  378. if invalid:
  379. raise ValueError("Invalid BackupOperationBean values: ", invalid)
  380. @classmethod
  381. def from_dict(cls, data: dict) -> BackupOperationBean:
  382. """Create a BackupOperationBean instance from a dict.
  383. This is useful for converting JSON response data into the dataclass structure.
  384. The nested 'snapshotOptions' dict is automatically converted to
  385. a SnapshotOptionsBean instance.
  386. Parameters:
  387. data: A dict containing the backup operation data, typically
  388. parsed from a JSON response.
  389. Returns:
  390. A BackupOperationBean instance with nested dataclass objects.
  391. Raises:
  392. KeyError: If required keys are missing from the input dict.
  393. TypeError: If nested 'snapshotOptions' cannot be unpacked into
  394. SnapshotOptionsBean.
  395. ValueError: If field validation fails in SnapshotOptionsBean or
  396. BackupOperationBean (e.g., invalid types or operation values).
  397. """
  398. snapshot_options = SnapshotOptionsBean(**data["snapshotOptions"])
  399. return cls(
  400. id=data["id"],
  401. username=data["username"],
  402. operation=data["operation"],
  403. affectedRepositories=data["affectedRepositories"],
  404. msSinceCreated=data["msSinceCreated"],
  405. snapshotOptions=snapshot_options,
  406. nodePerformingClusterBackup=data.get("nodePerformingClusterBackup"),
  407. )
  408. @dataclass(frozen=True)
  409. class StructuresStatistics:
  410. cacheHit: int # noqa: N815
  411. cacheMiss: int # noqa: N815
  412. def __post_init__(self) -> None:
  413. invalid: list[tuple[str, t.Any, type]] = []
  414. cache_hit = t.cast(t.Any, self.cacheHit)
  415. cache_miss = t.cast(t.Any, self.cacheMiss)
  416. if type(cache_hit) is not int:
  417. invalid.append(("cacheHit", cache_hit, type(cache_hit)))
  418. if type(cache_miss) is not int:
  419. invalid.append(("cacheMiss", cache_miss, type(cache_miss)))
  420. if invalid:
  421. raise ValueError("Invalid StructuresStatistics values: ", invalid)
  422. @dataclass(frozen=True)
  423. class RepositoryStatisticsQueries:
  424. slow: int
  425. suboptimal: int
  426. def __post_init__(self) -> None:
  427. invalid: list[tuple[str, t.Any, type]] = []
  428. slow = t.cast(t.Any, self.slow)
  429. suboptimal = t.cast(t.Any, self.suboptimal)
  430. if type(slow) is not int:
  431. invalid.append(("slow", slow, type(slow)))
  432. if type(suboptimal) is not int:
  433. invalid.append(("suboptimal", suboptimal, type(suboptimal)))
  434. if invalid:
  435. raise ValueError("Invalid RepositoryStatisticsQueries values: ", invalid)
  436. @dataclass(frozen=True)
  437. class RepositoryStatisticsEntityPool:
  438. epoolReads: int # noqa: N815
  439. epoolWrites: int # noqa: N815
  440. epoolSize: int # noqa: N815
  441. def __post_init__(self) -> None:
  442. invalid: list[tuple[str, t.Any, type]] = []
  443. epool_reads = t.cast(t.Any, self.epoolReads)
  444. epool_writes = t.cast(t.Any, self.epoolWrites)
  445. epool_size = t.cast(t.Any, self.epoolSize)
  446. if type(epool_reads) is not int:
  447. invalid.append(("epoolReads", epool_reads, type(epool_reads)))
  448. if type(epool_writes) is not int:
  449. invalid.append(("epoolWrites", epool_writes, type(epool_writes)))
  450. if type(epool_size) is not int:
  451. invalid.append(("epoolSize", epool_size, type(epool_size)))
  452. if invalid:
  453. raise ValueError("Invalid RepositoryStatisticsEntityPool values: ", invalid)
  454. @dataclass(frozen=True)
  455. class RepositoryStatistics:
  456. queries: RepositoryStatisticsQueries
  457. entityPool: RepositoryStatisticsEntityPool # noqa: N815
  458. activeTransactions: int # noqa: N815
  459. openConnections: int # noqa: N815
  460. def __post_init__(self) -> None:
  461. invalid: list[tuple[str, t.Any, type]] = []
  462. queries = t.cast(t.Any, self.queries)
  463. entity_pool = t.cast(t.Any, self.entityPool)
  464. active_transactions = t.cast(t.Any, self.activeTransactions)
  465. open_connections = t.cast(t.Any, self.openConnections)
  466. if not isinstance(queries, RepositoryStatisticsQueries):
  467. invalid.append(("queries", queries, type(queries)))
  468. if not isinstance(entity_pool, RepositoryStatisticsEntityPool):
  469. invalid.append(("entityPool", entity_pool, type(entity_pool)))
  470. if type(active_transactions) is not int:
  471. invalid.append(
  472. ("activeTransactions", active_transactions, type(active_transactions))
  473. )
  474. if type(open_connections) is not int:
  475. invalid.append(
  476. ("openConnections", open_connections, type(open_connections))
  477. )
  478. if invalid:
  479. raise ValueError("Invalid RepositoryStatistics values: ", invalid)
  480. @classmethod
  481. def from_dict(cls, data: dict) -> RepositoryStatistics:
  482. """Create a RepositoryStatistics instance from a dict.
  483. This is useful for converting JSON response data into the dataclass structure.
  484. The nested 'queries' and 'entityPool' dicts are automatically converted to
  485. their respective dataclass instances.
  486. Parameters:
  487. data: A dict containing the repository statistics data, typically
  488. parsed from a JSON response.
  489. Returns:
  490. A RepositoryStatistics instance with nested dataclass objects.
  491. Raises:
  492. KeyError: If required keys are missing from the input dict.
  493. TypeError: If nested dicts cannot be unpacked into their respective
  494. dataclass instances.
  495. ValueError: If field validation fails in RepositoryStatisticsQueries,
  496. RepositoryStatisticsEntityPool, or RepositoryStatistics
  497. (e.g., invalid types).
  498. """
  499. queries = RepositoryStatisticsQueries(**data["queries"])
  500. entity_pool = RepositoryStatisticsEntityPool(**data["entityPool"])
  501. return cls(
  502. queries=queries,
  503. entityPool=entity_pool,
  504. activeTransactions=data["activeTransactions"],
  505. openConnections=data["openConnections"],
  506. )
  507. @dataclass(frozen=True)
  508. class InfrastructureMemoryUsage:
  509. max: int
  510. committed: int
  511. init: int
  512. used: int
  513. def __post_init__(self) -> None:
  514. invalid: list[tuple[str, t.Any, type]] = []
  515. max_val = t.cast(t.Any, self.max)
  516. committed = t.cast(t.Any, self.committed)
  517. init = t.cast(t.Any, self.init)
  518. used = t.cast(t.Any, self.used)
  519. if type(max_val) is not int:
  520. invalid.append(("max", max_val, type(max_val)))
  521. if type(committed) is not int:
  522. invalid.append(("committed", committed, type(committed)))
  523. if type(init) is not int:
  524. invalid.append(("init", init, type(init)))
  525. if type(used) is not int:
  526. invalid.append(("used", used, type(used)))
  527. if invalid:
  528. raise ValueError("Invalid InfrastructureMemoryUsage values: ", invalid)
  529. @dataclass(frozen=True)
  530. class InfrastructureStorageMemory:
  531. dataDirUsed: int # noqa: N815
  532. workDirUsed: int # noqa: N815
  533. logsDirUsed: int # noqa: N815
  534. dataDirFree: int # noqa: N815
  535. workDirFree: int # noqa: N815
  536. logsDirFree: int # noqa: N815
  537. def __post_init__(self) -> None:
  538. invalid: list[tuple[str, t.Any, type]] = []
  539. data_dir_used = t.cast(t.Any, self.dataDirUsed)
  540. work_dir_used = t.cast(t.Any, self.workDirUsed)
  541. logs_dir_used = t.cast(t.Any, self.logsDirUsed)
  542. data_dir_free = t.cast(t.Any, self.dataDirFree)
  543. work_dir_free = t.cast(t.Any, self.workDirFree)
  544. logs_dir_free = t.cast(t.Any, self.logsDirFree)
  545. if type(data_dir_used) is not int:
  546. invalid.append(("dataDirUsed", data_dir_used, type(data_dir_used)))
  547. if type(work_dir_used) is not int:
  548. invalid.append(("workDirUsed", work_dir_used, type(work_dir_used)))
  549. if type(logs_dir_used) is not int:
  550. invalid.append(("logsDirUsed", logs_dir_used, type(logs_dir_used)))
  551. if type(data_dir_free) is not int:
  552. invalid.append(("dataDirFree", data_dir_free, type(data_dir_free)))
  553. if type(work_dir_free) is not int:
  554. invalid.append(("workDirFree", work_dir_free, type(work_dir_free)))
  555. if type(logs_dir_free) is not int:
  556. invalid.append(("logsDirFree", logs_dir_free, type(logs_dir_free)))
  557. if invalid:
  558. raise ValueError("Invalid InfrastructureStorageMemory values: ", invalid)
  559. @dataclass(frozen=True)
  560. class InfrastructureStatistics:
  561. heapMemoryUsage: InfrastructureMemoryUsage # noqa: N815
  562. nonHeapMemoryUsage: InfrastructureMemoryUsage # noqa: N815
  563. storageMemory: InfrastructureStorageMemory # noqa: N815
  564. threadCount: int # noqa: N815
  565. cpuLoad: float # noqa: N815
  566. classCount: int # noqa: N815
  567. gcCount: int # noqa: N815
  568. openFileDescriptors: int # noqa: N815
  569. maxFileDescriptors: int # noqa: N815
  570. def __post_init__(self) -> None:
  571. invalid: list[tuple[str, t.Any, type]] = []
  572. heap_memory_usage = t.cast(t.Any, self.heapMemoryUsage)
  573. non_heap_memory_usage = t.cast(t.Any, self.nonHeapMemoryUsage)
  574. storage_memory = t.cast(t.Any, self.storageMemory)
  575. thread_count = t.cast(t.Any, self.threadCount)
  576. cpu_load = t.cast(t.Any, self.cpuLoad)
  577. class_count = t.cast(t.Any, self.classCount)
  578. gc_count = t.cast(t.Any, self.gcCount)
  579. open_file_descriptors = t.cast(t.Any, self.openFileDescriptors)
  580. max_file_descriptors = t.cast(t.Any, self.maxFileDescriptors)
  581. if not isinstance(heap_memory_usage, InfrastructureMemoryUsage):
  582. invalid.append(
  583. ("heapMemoryUsage", heap_memory_usage, type(heap_memory_usage))
  584. )
  585. if not isinstance(non_heap_memory_usage, InfrastructureMemoryUsage):
  586. invalid.append(
  587. (
  588. "nonHeapMemoryUsage",
  589. non_heap_memory_usage,
  590. type(non_heap_memory_usage),
  591. )
  592. )
  593. if not isinstance(storage_memory, InfrastructureStorageMemory):
  594. invalid.append(("storageMemory", storage_memory, type(storage_memory)))
  595. if type(thread_count) is not int:
  596. invalid.append(("threadCount", thread_count, type(thread_count)))
  597. if type(cpu_load) is not float and type(cpu_load) is not int:
  598. invalid.append(("cpuLoad", cpu_load, type(cpu_load)))
  599. if type(class_count) is not int:
  600. invalid.append(("classCount", class_count, type(class_count)))
  601. if type(gc_count) is not int:
  602. invalid.append(("gcCount", gc_count, type(gc_count)))
  603. if type(open_file_descriptors) is not int:
  604. invalid.append(
  605. (
  606. "openFileDescriptors",
  607. open_file_descriptors,
  608. type(open_file_descriptors),
  609. )
  610. )
  611. if type(max_file_descriptors) is not int:
  612. invalid.append(
  613. ("maxFileDescriptors", max_file_descriptors, type(max_file_descriptors))
  614. )
  615. if invalid:
  616. raise ValueError("Invalid InfrastructureStatistics values: ", invalid)
  617. @classmethod
  618. def from_dict(cls, data: dict) -> InfrastructureStatistics:
  619. """Create an InfrastructureStatistics instance from a dict.
  620. This is useful for converting JSON response data into the dataclass structure.
  621. The nested memory and storage dicts are automatically converted to their
  622. respective dataclass instances.
  623. Parameters:
  624. data: A dict containing the infrastructure statistics data, typically
  625. parsed from a JSON response.
  626. Returns:
  627. An InfrastructureStatistics instance with nested dataclass objects.
  628. Raises:
  629. KeyError: If required keys are missing from the input dict.
  630. TypeError: If nested dicts cannot be unpacked into their respective
  631. dataclass instances.
  632. ValueError: If field validation fails in InfrastructureMemoryUsage,
  633. InfrastructureStorageMemory, or InfrastructureStatistics
  634. (e.g., invalid types).
  635. """
  636. heap_memory_usage = InfrastructureMemoryUsage(**data["heapMemoryUsage"])
  637. non_heap_memory_usage = InfrastructureMemoryUsage(**data["nonHeapMemoryUsage"])
  638. storage_memory = InfrastructureStorageMemory(**data["storageMemory"])
  639. return cls(
  640. heapMemoryUsage=heap_memory_usage,
  641. nonHeapMemoryUsage=non_heap_memory_usage,
  642. storageMemory=storage_memory,
  643. threadCount=data["threadCount"],
  644. cpuLoad=float(data["cpuLoad"]),
  645. classCount=data["classCount"],
  646. gcCount=data["gcCount"],
  647. openFileDescriptors=data["openFileDescriptors"],
  648. maxFileDescriptors=data["maxFileDescriptors"],
  649. )
  650. @dataclass(frozen=True)
  651. class ParserSettings:
  652. preserveBNodeIds: bool = False # noqa: N815
  653. failOnUnknownDataTypes: bool = False # noqa: N815
  654. verifyDataTypeValues: bool = False # noqa: N815
  655. normalizeDataTypeValues: bool = False # noqa: N815
  656. failOnUnknownLanguageTags: bool = False # noqa: N815
  657. verifyLanguageTags: bool = True # noqa: N815
  658. normalizeLanguageTags: bool = False # noqa: N815
  659. stopOnError: bool = True # noqa: N815
  660. contextLink: t.Any | None = None # noqa: N815
  661. def __post_init__(self) -> None:
  662. invalid: list[tuple[str, t.Any, type]] = []
  663. preserve_bnode_ids = t.cast(t.Any, self.preserveBNodeIds)
  664. fail_on_unknown_data_types = t.cast(t.Any, self.failOnUnknownDataTypes)
  665. verify_data_type_values = t.cast(t.Any, self.verifyDataTypeValues)
  666. normalize_data_type_values = t.cast(t.Any, self.normalizeDataTypeValues)
  667. fail_on_unknown_language_tags = t.cast(t.Any, self.failOnUnknownLanguageTags)
  668. verify_language_tags = t.cast(t.Any, self.verifyLanguageTags)
  669. normalize_language_tags = t.cast(t.Any, self.normalizeLanguageTags)
  670. stop_on_error = t.cast(t.Any, self.stopOnError)
  671. if type(preserve_bnode_ids) is not bool:
  672. invalid.append(
  673. ("preserveBNodeIds", preserve_bnode_ids, type(preserve_bnode_ids))
  674. )
  675. if type(fail_on_unknown_data_types) is not bool:
  676. invalid.append(
  677. (
  678. "failOnUnknownDataTypes",
  679. fail_on_unknown_data_types,
  680. type(fail_on_unknown_data_types),
  681. )
  682. )
  683. if type(verify_data_type_values) is not bool:
  684. invalid.append(
  685. (
  686. "verifyDataTypeValues",
  687. verify_data_type_values,
  688. type(verify_data_type_values),
  689. )
  690. )
  691. if type(normalize_data_type_values) is not bool:
  692. invalid.append(
  693. (
  694. "normalizeDataTypeValues",
  695. normalize_data_type_values,
  696. type(normalize_data_type_values),
  697. )
  698. )
  699. if type(fail_on_unknown_language_tags) is not bool:
  700. invalid.append(
  701. (
  702. "failOnUnknownLanguageTags",
  703. fail_on_unknown_language_tags,
  704. type(fail_on_unknown_language_tags),
  705. )
  706. )
  707. if type(verify_language_tags) is not bool:
  708. invalid.append(
  709. ("verifyLanguageTags", verify_language_tags, type(verify_language_tags))
  710. )
  711. if type(normalize_language_tags) is not bool:
  712. invalid.append(
  713. (
  714. "normalizeLanguageTags",
  715. normalize_language_tags,
  716. type(normalize_language_tags),
  717. )
  718. )
  719. if type(stop_on_error) is not bool:
  720. invalid.append(("stopOnError", stop_on_error, type(stop_on_error)))
  721. # Note: don't check contextLink here since it's of type t.Any.
  722. if invalid:
  723. raise ValueError("Invalid ParserSettings values: ", invalid)
  724. def as_dict(self) -> dict[str, t.Any]:
  725. return asdict(self)
  726. @dataclass(frozen=True)
  727. class ImportSettings:
  728. name: str
  729. status: t.Literal["PENDING", "IMPORTING", "DONE", "ERROR", "NONE", "INTERRUPTING"]
  730. size: str
  731. lastModified: int # noqa: N815
  732. imported: int
  733. addedStatements: int # noqa: N815
  734. removedStatements: int # noqa: N815
  735. numReplacedGraphs: int # noqa: N815
  736. message: str = ""
  737. context: t.Any | None = None
  738. replaceGraphs: t.List = field(default_factory=list) # noqa: N815
  739. baseURI: t.Any | None = None # noqa: N815
  740. forceSerial: bool = False # noqa: N815
  741. type: str = "file"
  742. format: t.Any | None = None
  743. data: t.Any | None = None
  744. parserSettings: ParserSettings = field(default_factory=ParserSettings) # noqa: N815
  745. def __post_init__(self) -> None:
  746. _allowed_status = {
  747. "PENDING",
  748. "IMPORTING",
  749. "DONE",
  750. "ERROR",
  751. "NONE",
  752. "INTERRUPTING",
  753. }
  754. invalid: list[tuple[str, t.Any, type]] = []
  755. name = t.cast(t.Any, self.name)
  756. status = t.cast(t.Any, self.status)
  757. message = t.cast(t.Any, self.message)
  758. replace_graphs = t.cast(t.Any, self.replaceGraphs)
  759. force_serial = t.cast(t.Any, self.forceSerial)
  760. type_ = t.cast(t.Any, self.type)
  761. parser_settings = t.cast(t.Any, self.parserSettings)
  762. size = t.cast(t.Any, self.size)
  763. last_modified = t.cast(t.Any, self.lastModified)
  764. imported = t.cast(t.Any, self.imported)
  765. added_statements = t.cast(t.Any, self.addedStatements)
  766. removed_statements = t.cast(t.Any, self.removedStatements)
  767. num_replaced_graphs = t.cast(t.Any, self.numReplacedGraphs)
  768. if not isinstance(name, str):
  769. invalid.append(("name", name, type(name)))
  770. if status not in _allowed_status:
  771. invalid.append(("status", status, type(status)))
  772. if not isinstance(message, str):
  773. invalid.append(("message", message, type(message)))
  774. if not isinstance(replace_graphs, list):
  775. invalid.append(("replaceGraphs", replace_graphs, type(replace_graphs)))
  776. if type(force_serial) is not bool:
  777. invalid.append(("forceSerial", force_serial, type(force_serial)))
  778. if not isinstance(type_, str):
  779. invalid.append(("type", type_, type(type_)))
  780. if not isinstance(parser_settings, ParserSettings):
  781. invalid.append(("parserSettings", parser_settings, type(parser_settings)))
  782. if not isinstance(size, str):
  783. invalid.append(("size", size, type(size)))
  784. if type(last_modified) is not int:
  785. invalid.append(("lastModified", last_modified, type(last_modified)))
  786. if type(imported) is not int:
  787. invalid.append(("imported", imported, type(imported)))
  788. if type(added_statements) is not int:
  789. invalid.append(
  790. ("addedStatements", added_statements, type(added_statements))
  791. )
  792. if type(removed_statements) is not int:
  793. invalid.append(
  794. ("removedStatements", removed_statements, type(removed_statements))
  795. )
  796. if type(num_replaced_graphs) is not int:
  797. invalid.append(
  798. ("numReplacedGraphs", num_replaced_graphs, type(num_replaced_graphs))
  799. )
  800. # Note: don't check context, baseURI, format, or data here since they are of type t.Any.
  801. if invalid:
  802. raise ValueError("Invalid ImportSettings values: ", invalid)
  803. def as_dict(self) -> dict[str, t.Any]:
  804. return asdict(self)
  805. @dataclass(frozen=True)
  806. class ServerImportBody:
  807. fileNames: list[str] # noqa: N815
  808. importSettings: ImportSettings | None = None # noqa: N815
  809. def __post_init__(self) -> None:
  810. invalid: list[tuple[str, t.Any, type]] = []
  811. import_settings = t.cast(t.Any, self.importSettings)
  812. file_names = t.cast(t.Any, self.fileNames)
  813. if import_settings is not None and not isinstance(
  814. import_settings, ImportSettings
  815. ):
  816. invalid.append(("importSettings", import_settings, type(import_settings)))
  817. if not isinstance(file_names, list):
  818. invalid.append(("fileNames", file_names, type(file_names)))
  819. else:
  820. for index, value in enumerate(file_names):
  821. if not isinstance(value, str):
  822. invalid.append((f"fileNames[{index}]", value, type(value)))
  823. if invalid:
  824. raise ValueError("Invalid ServerImportBody values: ", invalid)
  825. def as_dict(self) -> dict[str, t.Any]:
  826. result = asdict(self)
  827. if self.importSettings is None:
  828. result.pop("importSettings", None)
  829. return result
  830. @dataclass(frozen=True)
  831. class UserUpdate:
  832. password: str = field(default="")
  833. appSettings: dict[str, t.Any] = field(default_factory=dict) # noqa: N815
  834. gptThreads: list[t.Any] = field(default_factory=list) # noqa: N815
  835. def as_dict(self) -> dict[str, t.Any]:
  836. return asdict(self)
  837. @dataclass(frozen=True)
  838. class UserCreate:
  839. """Dataclass for creating a new user in GraphDB.
  840. Unlike `User`, this class does not include `dateCreated` since
  841. GraphDB automatically assigns this value when the user is created.
  842. """
  843. username: str
  844. password: str
  845. grantedAuthorities: list[str] = field(default_factory=list) # noqa: N815
  846. appSettings: dict[str, t.Any] = field(default_factory=dict) # noqa: N815
  847. gptThreads: list[t.Any] = field(default_factory=list) # noqa: N815
  848. def __post_init__(self) -> None:
  849. invalid: list[tuple[str, t.Any, type]] = []
  850. username = t.cast(t.Any, self.username)
  851. password = t.cast(t.Any, self.password)
  852. granted_authorities = t.cast(t.Any, self.grantedAuthorities)
  853. app_settings = t.cast(t.Any, self.appSettings)
  854. gpt_threads = t.cast(t.Any, self.gptThreads)
  855. if not isinstance(username, str):
  856. invalid.append(("username", username, type(username)))
  857. if not isinstance(password, str):
  858. invalid.append(("password", password, type(password)))
  859. if not isinstance(granted_authorities, list):
  860. invalid.append(
  861. ("grantedAuthorities", granted_authorities, type(granted_authorities))
  862. )
  863. else:
  864. for index, value in enumerate(granted_authorities):
  865. if not isinstance(value, str):
  866. invalid.append((f"grantedAuthorities[{index}]", value, type(value)))
  867. if not isinstance(app_settings, dict):
  868. invalid.append(("appSettings", app_settings, type(app_settings)))
  869. else:
  870. for key in app_settings.keys():
  871. if not isinstance(key, str):
  872. invalid.append(("appSettings key", key, type(key)))
  873. break
  874. if not isinstance(gpt_threads, list):
  875. invalid.append(("gptThreads", gpt_threads, type(gpt_threads)))
  876. if invalid:
  877. raise ValueError("Invalid UserCreate values: ", invalid)
  878. def as_dict(self) -> dict[str, t.Any]:
  879. return asdict(self)
  880. @dataclass(frozen=True)
  881. class User:
  882. username: str
  883. password: str
  884. dateCreated: int # noqa: N815
  885. grantedAuthorities: list[str] = field(default_factory=list) # noqa: N815
  886. appSettings: dict[str, t.Any] = field(default_factory=dict) # noqa: N815
  887. gptThreads: list[t.Any] = field(default_factory=list) # noqa: N815
  888. def __post_init__(self) -> None:
  889. # Normalize None values from API responses to empty collections
  890. if self.grantedAuthorities is None:
  891. object.__setattr__(self, "grantedAuthorities", []) # type: ignore[unreachable]
  892. if self.appSettings is None:
  893. object.__setattr__(self, "appSettings", {}) # type: ignore[unreachable]
  894. if self.gptThreads is None:
  895. object.__setattr__(self, "gptThreads", []) # type: ignore[unreachable]
  896. invalid: list[tuple[str, t.Any, type]] = []
  897. username = t.cast(t.Any, self.username)
  898. password = t.cast(t.Any, self.password)
  899. date_created = t.cast(t.Any, self.dateCreated)
  900. granted_authorities = t.cast(t.Any, self.grantedAuthorities)
  901. app_settings = t.cast(t.Any, self.appSettings)
  902. gpt_threads = t.cast(t.Any, self.gptThreads)
  903. if not isinstance(username, str):
  904. invalid.append(("username", username, type(username)))
  905. if not isinstance(password, str):
  906. invalid.append(("password", password, type(password)))
  907. if not isinstance(date_created, int):
  908. invalid.append(("dateCreated", date_created, type(date_created)))
  909. if not isinstance(granted_authorities, list):
  910. invalid.append(
  911. ("grantedAuthorities", granted_authorities, type(granted_authorities))
  912. )
  913. else:
  914. for index, value in enumerate(granted_authorities):
  915. if not isinstance(value, str):
  916. invalid.append((f"grantedAuthorities[{index}]", value, type(value)))
  917. if not isinstance(app_settings, dict):
  918. invalid.append(("appSettings", app_settings, type(app_settings)))
  919. else:
  920. for key in app_settings.keys():
  921. if not isinstance(key, str):
  922. invalid.append(("appSettings key", key, type(key)))
  923. break
  924. if not isinstance(gpt_threads, list):
  925. invalid.append(("gptThreads", gpt_threads, type(gpt_threads)))
  926. if invalid:
  927. raise ValueError("Invalid User values: ", invalid)
  928. def as_dict(self):
  929. return asdict(self)
  930. @dataclass(frozen=True)
  931. class AuthenticatedUser:
  932. """Represents an authenticated user returned from POST /rest/login.
  933. Attributes:
  934. username: The username of the authenticated user.
  935. authorities: List of granted authorities/roles (e.g., ["ROLE_USER", "ROLE_ADMIN"]).
  936. appSettings: Application settings for the user.
  937. external: Whether the user is external (e.g., from LDAP/OAuth).
  938. token: The full Authorization header value (e.g., "GDB <token>").
  939. Can be passed directly to GraphDBClient's auth parameter.
  940. """
  941. username: str
  942. authorities: list[str] = field(default_factory=list)
  943. appSettings: dict[str, t.Any] = field(default_factory=dict) # noqa: N815
  944. external: bool = False
  945. token: str = ""
  946. def __post_init__(self) -> None:
  947. # Normalize None values from API responses to empty collections
  948. if self.authorities is None:
  949. object.__setattr__(self, "authorities", []) # type: ignore[unreachable]
  950. if self.appSettings is None:
  951. object.__setattr__(self, "appSettings", {}) # type: ignore[unreachable]
  952. invalid: list[tuple[str, t.Any, type]] = []
  953. username = t.cast(t.Any, self.username)
  954. authorities = t.cast(t.Any, self.authorities)
  955. app_settings = t.cast(t.Any, self.appSettings)
  956. external = t.cast(t.Any, self.external)
  957. token = t.cast(t.Any, self.token)
  958. if not isinstance(username, str):
  959. invalid.append(("username", username, type(username)))
  960. if not isinstance(authorities, list):
  961. invalid.append(("authorities", authorities, type(authorities)))
  962. else:
  963. for index, value in enumerate(authorities):
  964. if not isinstance(value, str):
  965. invalid.append((f"authorities[{index}]", value, type(value)))
  966. if not isinstance(app_settings, dict):
  967. invalid.append(("appSettings", app_settings, type(app_settings)))
  968. else:
  969. for key in app_settings.keys():
  970. if not isinstance(key, str):
  971. invalid.append(("appSettings key", key, type(key)))
  972. break
  973. if type(external) is not bool:
  974. invalid.append(("external", external, type(external)))
  975. if not isinstance(token, str):
  976. invalid.append(("token", token, type(token)))
  977. if invalid:
  978. raise ValueError("Invalid AuthenticatedUser values: ", invalid)
  979. @classmethod
  980. def from_response(cls, data: dict[str, t.Any], token: str) -> AuthenticatedUser:
  981. """Create an AuthenticatedUser from API response data and token.
  982. Parameters:
  983. data: The JSON response body from POST /rest/login.
  984. token: The GDB token extracted from the Authorization header.
  985. Returns:
  986. An AuthenticatedUser instance.
  987. Raises:
  988. ValueError: If required fields are missing or invalid.
  989. TypeError: If data is not a dict.
  990. """
  991. if not isinstance(data, dict):
  992. raise TypeError("Response data must be a dict")
  993. if "username" not in data:
  994. raise ValueError("Response data must contain 'username'")
  995. return cls(
  996. username=data["username"],
  997. authorities=data.get("authorities", []),
  998. appSettings=data.get("appSettings", {}),
  999. external=data.get("external", False),
  1000. token=token,
  1001. )
  1002. @dataclass(frozen=True)
  1003. class FreeAccessSettings:
  1004. enabled: bool
  1005. authorities: list[str] = field(default_factory=list)
  1006. appSettings: dict[str, t.Any] = field(default_factory=dict) # noqa: N815
  1007. def __post_init__(self) -> None:
  1008. invalid: list[tuple[str, t.Any, type]] = []
  1009. enabled = t.cast(t.Any, self.enabled)
  1010. authorities = t.cast(t.Any, self.authorities)
  1011. app_settings = t.cast(t.Any, self.appSettings)
  1012. if type(enabled) is not bool:
  1013. invalid.append(("enabled", enabled, type(enabled)))
  1014. if not isinstance(authorities, list):
  1015. invalid.append(("authorities", authorities, type(authorities)))
  1016. else:
  1017. for index, value in enumerate(authorities):
  1018. if not isinstance(value, str):
  1019. invalid.append((f"authorities[{index}]", value, type(value)))
  1020. if not isinstance(app_settings, dict):
  1021. invalid.append(("appSettings", app_settings, type(app_settings)))
  1022. else:
  1023. for key in app_settings.keys():
  1024. if not isinstance(key, str):
  1025. invalid.append(("appSettings key", key, type(key)))
  1026. break
  1027. if invalid:
  1028. raise ValueError("Invalid FreeAccessSettings values: ", invalid)
  1029. def as_dict(self):
  1030. return asdict(self)
  1031. @dataclass(frozen=True)
  1032. class RepositorySizeInfo:
  1033. inferred: int
  1034. total: int
  1035. explicit: int
  1036. def __post_init__(self):
  1037. invalid = []
  1038. if type(self.inferred) is not int:
  1039. invalid.append("inferred")
  1040. if type(self.total) is not int:
  1041. invalid.append("total")
  1042. if type(self.explicit) is not int:
  1043. invalid.append("explicit")
  1044. if invalid:
  1045. raise ValueError(
  1046. "Invalid RepositorySizeInfo values: ",
  1047. [(x, self.__dict__[x], type(self.__dict__[x])) for x in invalid],
  1048. )
  1049. @dataclass(frozen=True)
  1050. class OWLimParameter:
  1051. name: str
  1052. label: str
  1053. value: str
  1054. @dataclass(frozen=True)
  1055. class RepositoryConfigBean:
  1056. id: str
  1057. title: str
  1058. type: str
  1059. sesameType: str # noqa: N815
  1060. location: str
  1061. params: dict[str, OWLimParameter] = field(default_factory=dict)
  1062. @dataclass(frozen=True)
  1063. class RepositoryConfigBeanCreate:
  1064. id: str
  1065. title: str
  1066. type: str
  1067. sesameType: str # noqa: N815
  1068. location: str
  1069. params: dict[str, OWLimParameter] = field(default_factory=dict)
  1070. missingDefaults: dict[str, OWLimParameter] = field( # noqa: N815
  1071. default_factory=dict
  1072. )
  1073. def as_dict(self) -> dict:
  1074. """Serialize the dataclass to a Python dict.
  1075. Returns:
  1076. dict: A dictionary representation of the dataclass suitable for use
  1077. with httpx POST requests (e.g., via the `json` parameter).
  1078. Examples:
  1079. >>> config = RepositoryConfigBeanCreate(
  1080. ... id="test-repo",
  1081. ... title="Test Repository",
  1082. ... type="graphdb:FreeSailRepository",
  1083. ... sesameType="graphdb:FreeSailRepository",
  1084. ... location="",
  1085. ... )
  1086. >>> config_dict = config.as_dict()
  1087. >>> isinstance(config_dict, dict)
  1088. True
  1089. """
  1090. return asdict(self)
  1091. class RepositoryState(str, Enum):
  1092. """Enumeration for repository state values."""
  1093. INACTIVE = "INACTIVE"
  1094. STARTING = "STARTING"
  1095. RUNNING = "RUNNING"
  1096. RESTARTING = "RESTARTING"
  1097. STOPPING = "STOPPING"
  1098. @dataclass
  1099. class GraphDBRepository:
  1100. id: t.Optional[str] = None
  1101. title: t.Optional[str] = None
  1102. uri: t.Optional[str] = None
  1103. externalUrl: t.Optional[str] = None # noqa: N815
  1104. local: t.Optional[bool] = None
  1105. type: t.Optional[str] = None
  1106. sesameType: t.Optional[str] = None # noqa: N815
  1107. location: t.Optional[str] = None
  1108. readable: t.Optional[bool] = None
  1109. writable: t.Optional[bool] = None
  1110. unsupported: t.Optional[bool] = None
  1111. state: t.Optional[RepositoryState] = None
  1112. @classmethod
  1113. def from_dict(cls, data: dict) -> GraphDBRepository:
  1114. """Create a GraphDBRepository instance from a dict.
  1115. Parameters:
  1116. data: A dict containing the repository data, typically
  1117. parsed from a JSON response.
  1118. Returns:
  1119. A GraphDBRepository instance.
  1120. Raises:
  1121. TypeError: If the dict contains keys that do not match
  1122. GraphDBRepository fields.
  1123. ValueError: If the 'state' value is not a valid RepositoryState.
  1124. """
  1125. data_copy = dict(data)
  1126. if "state" in data_copy and data_copy["state"] is not None:
  1127. data_copy["state"] = RepositoryState(data_copy["state"])
  1128. return cls(**data_copy)
  1129. @dataclass
  1130. class AccessControlEntry:
  1131. scope: t.Literal["statement", "clear_graph", "plugin", "system"]
  1132. policy: t.Literal["allow", "deny", "abstain"]
  1133. role: str
  1134. def as_dict(self) -> dict[str, t.Any]:
  1135. raise NotImplementedError("Use a concrete AccessControlEntry subclass.")
  1136. @classmethod
  1137. def from_dict(
  1138. cls, data: dict
  1139. ) -> (
  1140. SystemAccessControlEntry
  1141. | StatementAccessControlEntry
  1142. | PluginAccessControlEntry
  1143. | ClearGraphAccessControlEntry
  1144. ):
  1145. """Create an AccessControlEntry subclass instance from raw GraphDB data.
  1146. Note: we perform parse validation essentially twice (here and in the
  1147. subclass's __post_init__) to ensure mypy is satisfied with the value's type.
  1148. Parameters:
  1149. data: A dict containing the access control entry data, typically
  1150. parsed from a JSON response.
  1151. Returns:
  1152. An AccessControlEntry subclass instance (SystemAccessControlEntry,
  1153. StatementAccessControlEntry, PluginAccessControlEntry, or
  1154. ClearGraphAccessControlEntry) depending on the 'scope' field.
  1155. Raises:
  1156. TypeError: If data is not a dict.
  1157. ValueError: If 'scope' is not a supported value ('system', 'statement',
  1158. 'plugin', 'clear_graph'), or if any field fails validation
  1159. (e.g., invalid policy, operation, role, plugin, subject,
  1160. predicate, object, or graph values).
  1161. """
  1162. if not isinstance(data, dict):
  1163. raise TypeError("ACL entry must be a mapping.")
  1164. scope = data.get("scope")
  1165. if scope == "system":
  1166. policy = _parse_policy(data.get("policy"))
  1167. role = _parse_role(data.get("role"))
  1168. operation = _parse_operation(data.get("operation"))
  1169. return SystemAccessControlEntry(
  1170. scope="system",
  1171. policy=policy,
  1172. role=role,
  1173. operation=operation,
  1174. )
  1175. if scope == "statement":
  1176. policy = _parse_policy(data.get("policy"))
  1177. role = _parse_role(data.get("role"))
  1178. operation = _parse_operation(data.get("operation"))
  1179. subject = _parse_subject(data.get("subject"))
  1180. predicate = _parse_predicate(data.get("predicate"))
  1181. obj = _parse_object(data.get("object"))
  1182. graph = _parse_graph(data.get("context"))
  1183. return StatementAccessControlEntry(
  1184. scope="statement",
  1185. policy=policy,
  1186. role=role,
  1187. operation=operation,
  1188. subject=subject,
  1189. predicate=predicate,
  1190. object=obj,
  1191. graph=graph,
  1192. )
  1193. if scope == "plugin":
  1194. policy = _parse_policy(data.get("policy"))
  1195. role = _parse_role(data.get("role"))
  1196. operation = _parse_operation(data.get("operation"))
  1197. plugin = _parse_plugin(data.get("plugin"))
  1198. return PluginAccessControlEntry(
  1199. scope="plugin",
  1200. policy=policy,
  1201. role=role,
  1202. operation=operation,
  1203. plugin=plugin,
  1204. )
  1205. if scope == "clear_graph":
  1206. policy = _parse_policy(data.get("policy"))
  1207. role = _parse_role(data.get("role"))
  1208. graph = _parse_graph(data.get("context"))
  1209. return ClearGraphAccessControlEntry(
  1210. scope="clear_graph",
  1211. policy=policy,
  1212. role=role,
  1213. graph=graph,
  1214. )
  1215. raise ValueError(f"Unsupported FGAC scope: {scope!r}")
  1216. @dataclass
  1217. class SystemAccessControlEntry(AccessControlEntry):
  1218. scope: t.Literal["system"]
  1219. policy: t.Literal["allow", "deny", "abstain"]
  1220. role: str
  1221. operation: t.Literal["read", "write", "*"]
  1222. def __post_init__(self) -> None:
  1223. self.policy = _parse_policy(self.policy)
  1224. self.role = _parse_role(self.role)
  1225. self.operation = _parse_operation(self.operation)
  1226. def as_dict(self) -> dict[str, t.Any]:
  1227. return {
  1228. "scope": self.scope,
  1229. "policy": self.policy,
  1230. "role": self.role,
  1231. "operation": self.operation,
  1232. }
  1233. @dataclass
  1234. class StatementAccessControlEntry(AccessControlEntry):
  1235. scope: t.Literal["statement"]
  1236. policy: t.Literal["allow", "deny", "abstain"]
  1237. role: str
  1238. operation: t.Literal["read", "write", "*"]
  1239. subject: t.Literal["*"] | URIRef
  1240. predicate: t.Literal["*"] | URIRef
  1241. object: t.Literal["*"] | URIRef | Literal
  1242. graph: t.Literal["*", "named", "default"] | URIRef
  1243. def __post_init__(self) -> None:
  1244. self.policy = _parse_policy(self.policy)
  1245. self.role = _parse_role(self.role)
  1246. self.operation = _parse_operation(self.operation)
  1247. self.subject = _parse_subject(self.subject)
  1248. self.predicate = _parse_predicate(self.predicate)
  1249. self.object = _parse_object(self.object)
  1250. self.graph = _parse_graph(self.graph)
  1251. def as_dict(self) -> dict[str, t.Any]:
  1252. return {
  1253. "scope": self.scope,
  1254. "policy": self.policy,
  1255. "role": self.role,
  1256. "operation": self.operation,
  1257. "subject": _format_term(self.subject),
  1258. "predicate": _format_term(self.predicate),
  1259. "object": _format_term(self.object),
  1260. "context": _format_term(self.graph),
  1261. }
  1262. @dataclass
  1263. class PluginAccessControlEntry(AccessControlEntry):
  1264. scope: t.Literal["plugin"]
  1265. policy: t.Literal["allow", "deny", "abstain"]
  1266. role: str
  1267. operation: t.Literal["read", "write", "*"]
  1268. plugin: str
  1269. def __post_init__(self) -> None:
  1270. self.policy = _parse_policy(self.policy)
  1271. self.role = _parse_role(self.role)
  1272. self.operation = _parse_operation(self.operation)
  1273. self.plugin = _parse_plugin(self.plugin)
  1274. def as_dict(self) -> dict[str, t.Any]:
  1275. return {
  1276. "scope": self.scope,
  1277. "policy": self.policy,
  1278. "role": self.role,
  1279. "operation": self.operation,
  1280. "plugin": self.plugin,
  1281. }
  1282. @dataclass
  1283. class ClearGraphAccessControlEntry(AccessControlEntry):
  1284. scope: t.Literal["clear_graph"]
  1285. policy: t.Literal["allow", "deny", "abstain"]
  1286. role: str
  1287. graph: t.Literal["*", "named", "default"] | URIRef
  1288. def __post_init__(self) -> None:
  1289. self.policy = _parse_policy(self.policy)
  1290. self.role = _parse_role(self.role)
  1291. self.graph = _parse_graph(self.graph)
  1292. def as_dict(self) -> dict[str, t.Any]:
  1293. return {
  1294. "scope": self.scope,
  1295. "policy": self.policy,
  1296. "role": self.role,
  1297. "context": _format_term(self.graph),
  1298. }
  1299. _ALLOWED_POLICIES = {"allow", "deny", "abstain"}
  1300. _ALLOWED_OPERATIONS = {"read", "write", "*"}
  1301. _ALLOWED_GRAPHS = {"*", "named", "default"}
  1302. def _parse_policy(policy: t.Any) -> t.Literal["allow", "deny", "abstain"]:
  1303. if policy not in _ALLOWED_POLICIES:
  1304. raise ValueError(f"Invalid FGAC policy: {policy!r}")
  1305. return t.cast(t.Literal["allow", "deny", "abstain"], policy)
  1306. def _parse_operation(operation: t.Any) -> t.Literal["read", "write", "*"]:
  1307. if operation not in _ALLOWED_OPERATIONS:
  1308. raise ValueError(f"Invalid FGAC operation: {operation!r}")
  1309. return t.cast(t.Literal["read", "write", "*"], operation)
  1310. def _parse_role(role: t.Any) -> str:
  1311. if not isinstance(role, str):
  1312. raise ValueError(f"Invalid FGAC role: {role!r}")
  1313. return role
  1314. def _parse_plugin(plugin: t.Any) -> str:
  1315. if not isinstance(plugin, str):
  1316. raise ValueError(f"Invalid FGAC plugin: {plugin!r}")
  1317. return plugin
  1318. def _parse_subject(subject: t.Any) -> t.Literal["*"] | URIRef:
  1319. if subject == "*":
  1320. return "*"
  1321. if isinstance(subject, URIRef):
  1322. return subject
  1323. if isinstance(subject, str):
  1324. parsed = _parse_with_n3(subject)
  1325. if isinstance(parsed, URIRef):
  1326. return parsed
  1327. raise ValueError(f"Invalid FGAC subject: {subject!r}")
  1328. def _parse_predicate(predicate: t.Any) -> t.Literal["*"] | URIRef:
  1329. if predicate == "*":
  1330. return "*"
  1331. if isinstance(predicate, URIRef):
  1332. return predicate
  1333. if isinstance(predicate, str):
  1334. parsed = _parse_with_n3(predicate)
  1335. if isinstance(parsed, URIRef):
  1336. return parsed
  1337. raise ValueError(f"Invalid FGAC predicate: {predicate!r}")
  1338. def _parse_object(obj: t.Any) -> t.Literal["*"] | URIRef | Literal:
  1339. if obj == "*":
  1340. return "*"
  1341. if isinstance(obj, (URIRef, Literal)):
  1342. return obj
  1343. if isinstance(obj, str):
  1344. parsed = _parse_with_n3(obj)
  1345. if isinstance(parsed, (URIRef, Literal)):
  1346. return parsed
  1347. raise ValueError(f"Invalid FGAC object: {obj!r}")
  1348. def _parse_graph(graph: t.Any) -> t.Literal["*", "named", "default"] | URIRef:
  1349. if graph in _ALLOWED_GRAPHS:
  1350. return t.cast(t.Literal["*", "named", "default"], graph)
  1351. if isinstance(graph, URIRef):
  1352. return graph
  1353. if isinstance(graph, str):
  1354. parsed = _parse_with_n3(graph)
  1355. if isinstance(parsed, URIRef):
  1356. return parsed
  1357. raise ValueError(f"Invalid FGAC graph: {graph!r}")
  1358. def _parse_with_n3(value: str) -> URIRef | Literal:
  1359. try:
  1360. parsed = from_n3(value)
  1361. except Exception:
  1362. parsed = None
  1363. if isinstance(parsed, (URIRef, Literal)):
  1364. return parsed
  1365. try:
  1366. return URIRef(value)
  1367. except Exception as err: # pragma: no cover - defensive
  1368. raise ValueError(f"Unable to parse value {value!r} into an RDF term.") from err
  1369. def _format_term(value: t.Any) -> str:
  1370. if isinstance(value, (URIRef, Literal)):
  1371. return value.n3()
  1372. return t.cast(str, value)