models.py 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
  1. """Immutable value objects used by external data-source services."""
  2. import hashlib
  3. import json
  4. from dataclasses import dataclass, field, replace
  5. from types import MappingProxyType
  6. from typing import Mapping, Optional
  7. @dataclass(frozen=True)
  8. class DataSourceCredential:
  9. username: str
  10. password: str = field(repr=False)
  11. options: Mapping[str, str] = field(default_factory=dict)
  12. def __post_init__(self):
  13. object.__setattr__(
  14. self,
  15. "options",
  16. MappingProxyType(dict(self.options)),
  17. )
  18. @dataclass(frozen=True)
  19. class SealedCredential:
  20. id: str
  21. data_source_uid: str
  22. credential_version: int
  23. encrypted_payload: bytes = field(repr=False)
  24. nonce: bytes = field(repr=False)
  25. key_version: str
  26. status: str = "active"
  27. @dataclass(frozen=True)
  28. class DataSourceDefinition:
  29. uid: Optional[str]
  30. name_en: str
  31. database_type: str
  32. host: str
  33. port: int
  34. database: str
  35. name_zh: Optional[str] = None
  36. schema: Optional[str] = None
  37. credential_ref: Optional[str] = None
  38. credential_version: Optional[int] = None
  39. pool_size: Optional[int] = None
  40. max_overflow: Optional[int] = None
  41. tls_options: Mapping[str, str] = field(default_factory=dict)
  42. status: bool = True
  43. description: Optional[str] = None
  44. extra_properties: Mapping[str, object] = field(default_factory=dict)
  45. def __post_init__(self):
  46. object.__setattr__(
  47. self,
  48. "database_type",
  49. str(self.database_type).strip().lower(),
  50. )
  51. object.__setattr__(self, "port", int(self.port))
  52. object.__setattr__(
  53. self,
  54. "tls_options",
  55. MappingProxyType(dict(self.tls_options)),
  56. )
  57. object.__setattr__(
  58. self,
  59. "extra_properties",
  60. MappingProxyType(dict(self.extra_properties)),
  61. )
  62. def with_uid(self, uid: str):
  63. return replace(self, uid=uid)
  64. def pool_overrides(self) -> dict:
  65. return {
  66. key: value
  67. for key, value in {
  68. "pool_size": self.pool_size,
  69. "max_overflow": self.max_overflow,
  70. }.items()
  71. if value is not None
  72. }
  73. def connection_fingerprint(self) -> str:
  74. payload = {
  75. "database_type": self.database_type,
  76. "host": self.host,
  77. "port": self.port,
  78. "database": self.database,
  79. "schema": self.schema,
  80. "pool_overrides": self.pool_overrides(),
  81. "tls_options": dict(self.tls_options),
  82. }
  83. canonical = json.dumps(
  84. payload,
  85. sort_keys=True,
  86. separators=(",", ":"),
  87. )
  88. return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
  89. @dataclass(frozen=True)
  90. class PoolKey:
  91. data_source_uid: str
  92. credential_version: int
  93. config_fingerprint: str
  94. @dataclass(frozen=True)
  95. class PoolStatus:
  96. data_source_uid: str
  97. credential_version: int
  98. config_fingerprint: str
  99. pool_state: str
  100. leases: int
  101. draining: bool
  102. created_at: float
  103. last_used_at: float
  104. pool_size: int = 0
  105. checked_out: int = 0
  106. checked_out_peak: int = 0
  107. checked_in: int = 0
  108. overflow: int = 0
  109. checkout_wait_ms: float = 0.0
  110. connection_created_total: int = 0
  111. connection_failed_total: int = 0
  112. pool_timeout_total: int = 0
  113. invalidated_total: int = 0
  114. query_total: int = 0
  115. last_query_duration_ms: float = 0.0
  116. consecutive_failures: int = 0
  117. circuit_open_until: float = 0.0