vibey-bootstrap 4.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. vibey_bootstrap/__init__.py +271 -0
  2. vibey_bootstrap/aks/__init__.py +84 -0
  3. vibey_bootstrap/aks/leader_election.py +82 -0
  4. vibey_bootstrap/alerts/__init__.py +36 -0
  5. vibey_bootstrap/alerts/dispatcher.py +358 -0
  6. vibey_bootstrap/alerts/escalation.py +25 -0
  7. vibey_bootstrap/alerts/render.py +97 -0
  8. vibey_bootstrap/audit/__init__.py +411 -0
  9. vibey_bootstrap/auth/__init__.py +19 -0
  10. vibey_bootstrap/auth/api_key.py +18 -0
  11. vibey_bootstrap/auth/hmac.py +40 -0
  12. vibey_bootstrap/auth/webhook.py +202 -0
  13. vibey_bootstrap/bootstrap/__init__.py +107 -0
  14. vibey_bootstrap/config_refresh/__init__.py +92 -0
  15. vibey_bootstrap/contrib/__init__.py +1 -0
  16. vibey_bootstrap/contrib/scaffold.py +107 -0
  17. vibey_bootstrap/contrib/templates/bicep/app/main.bicep.template +2 -0
  18. vibey_bootstrap/contrib/templates/cicd/github/test.yml.template +12 -0
  19. vibey_bootstrap/contrib/templates/gitops/base/kustomization.yaml.template +4 -0
  20. vibey_bootstrap/contrib/templates/helm/worker/Chart.yaml.template +4 -0
  21. vibey_bootstrap/contrib/templates/policy/conftest/kubernetes.rego.template +7 -0
  22. vibey_bootstrap/contrib/templates/terraform/aks/main.tf.template +4 -0
  23. vibey_bootstrap/counters/__init__.py +43 -0
  24. vibey_bootstrap/db/__init__.py +109 -0
  25. vibey_bootstrap/db/migrations/__init__.py +86 -0
  26. vibey_bootstrap/db/outbox.py +185 -0
  27. vibey_bootstrap/documentdb/__init__.py +38 -0
  28. vibey_bootstrap/email/__init__.py +69 -0
  29. vibey_bootstrap/exceptions/__init__.py +108 -0
  30. vibey_bootstrap/failclose/__init__.py +52 -0
  31. vibey_bootstrap/fastapi_middleware/__init__.py +144 -0
  32. vibey_bootstrap/governance/__init__.py +112 -0
  33. vibey_bootstrap/health/__init__.py +117 -0
  34. vibey_bootstrap/heartbeat/__init__.py +260 -0
  35. vibey_bootstrap/http/__init__.py +82 -0
  36. vibey_bootstrap/http/_common.py +37 -0
  37. vibey_bootstrap/http/async_client.py +40 -0
  38. vibey_bootstrap/identity/__init__.py +385 -0
  39. vibey_bootstrap/ingress/__init__.py +41 -0
  40. vibey_bootstrap/ingress/classifier.py +119 -0
  41. vibey_bootstrap/ingress/extensions.py +42 -0
  42. vibey_bootstrap/ingress/magic_bytes.py +63 -0
  43. vibey_bootstrap/ingress/mime.py +38 -0
  44. vibey_bootstrap/ingress/size.py +32 -0
  45. vibey_bootstrap/ingress/zip_safety.py +49 -0
  46. vibey_bootstrap/logging/__init__.py +69 -0
  47. vibey_bootstrap/logging/config.py +102 -0
  48. vibey_bootstrap/logging/correlation.py +97 -0
  49. vibey_bootstrap/logging/formatter.py +78 -0
  50. vibey_bootstrap/logging/jsonformatter.py +87 -0
  51. vibey_bootstrap/logging/masking.py +170 -0
  52. vibey_bootstrap/logging/noise.py +53 -0
  53. vibey_bootstrap/metrics/__init__.py +42 -0
  54. vibey_bootstrap/models/__init__.py +13 -0
  55. vibey_bootstrap/models/exceptions.py +38 -0
  56. vibey_bootstrap/notify/__init__.py +25 -0
  57. vibey_bootstrap/notify/templates.py +281 -0
  58. vibey_bootstrap/notify/throttle.py +68 -0
  59. vibey_bootstrap/openai/__init__.py +462 -0
  60. vibey_bootstrap/path_safety/__init__.py +79 -0
  61. vibey_bootstrap/pdf_safety/__init__.py +104 -0
  62. vibey_bootstrap/phases/__init__.py +112 -0
  63. vibey_bootstrap/py.typed +2 -0
  64. vibey_bootstrap/ratelimit/__init__.py +137 -0
  65. vibey_bootstrap/repositories/__init__.py +13 -0
  66. vibey_bootstrap/repositories/enhanced_config_repository.py +580 -0
  67. vibey_bootstrap/repositories/interfaces/__init__.py +17 -0
  68. vibey_bootstrap/repositories/interfaces/enhanced_config_repository_interface.py +140 -0
  69. vibey_bootstrap/repositories/interfaces/secrets_repository_interface.py +96 -0
  70. vibey_bootstrap/repositories/secrets_repository.py +243 -0
  71. vibey_bootstrap/retry/__init__.py +170 -0
  72. vibey_bootstrap/sb_lock/__init__.py +128 -0
  73. vibey_bootstrap/scheduler/__init__.py +76 -0
  74. vibey_bootstrap/security/__init__.py +65 -0
  75. vibey_bootstrap/servicebus/__init__.py +51 -0
  76. vibey_bootstrap/servicebus/async_ext.py +99 -0
  77. vibey_bootstrap/servicebus/consumer.py +20 -0
  78. vibey_bootstrap/servicebus/consumer_wrapper.py +235 -0
  79. vibey_bootstrap/servicebus/dlq_alarm.py +85 -0
  80. vibey_bootstrap/servicebus/dlq_digest.py +162 -0
  81. vibey_bootstrap/services/__init__.py +29 -0
  82. vibey_bootstrap/services/application_bootstrap.py +592 -0
  83. vibey_bootstrap/services/bootstrap_logging.py +199 -0
  84. vibey_bootstrap/services/interfaces/__init__.py +21 -0
  85. vibey_bootstrap/services/interfaces/application_bootstrap_interface.py +58 -0
  86. vibey_bootstrap/services/interfaces/bootstrap_logger_interface.py +57 -0
  87. vibey_bootstrap/services/interfaces/telemetry_manager_interface.py +149 -0
  88. vibey_bootstrap/services/telemetry.py +277 -0
  89. vibey_bootstrap/softfail/__init__.py +146 -0
  90. vibey_bootstrap/subscription/__init__.py +154 -0
  91. vibey_bootstrap/tokens/__init__.py +95 -0
  92. vibey_bootstrap/tracing/__init__.py +28 -0
  93. vibey_bootstrap/tracing/decorators.py +235 -0
  94. vibey_bootstrap/tracing/latency.py +95 -0
  95. vibey_bootstrap/tracing/log_exception_context.py +24 -0
  96. vibey_bootstrap/tracing/slow_thresholds.py +47 -0
  97. vibey_bootstrap/tracing/timed_operation.py +29 -0
  98. vibey_bootstrap/transports/__init__.py +251 -0
  99. vibey_bootstrap/transports/_base.py +309 -0
  100. vibey_bootstrap/transports/adx.py +125 -0
  101. vibey_bootstrap/transports/blob.py +208 -0
  102. vibey_bootstrap/transports/builtins.py +93 -0
  103. vibey_bootstrap/transports/event_hubs.py +122 -0
  104. vibey_bootstrap/transports/file.py +251 -0
  105. vibey_bootstrap/transports/nosql.py +235 -0
  106. vibey_bootstrap/transports/panther.py +261 -0
  107. vibey_bootstrap/transports/sql.py +293 -0
  108. vibey_bootstrap/transports/sumologic.py +180 -0
  109. vibey_bootstrap/validation/__init__.py +151 -0
  110. vibey_bootstrap-4.0.0.dist-info/METADATA +354 -0
  111. vibey_bootstrap-4.0.0.dist-info/RECORD +116 -0
  112. vibey_bootstrap-4.0.0.dist-info/WHEEL +5 -0
  113. vibey_bootstrap-4.0.0.dist-info/entry_points.txt +3 -0
  114. vibey_bootstrap-4.0.0.dist-info/licenses/LICENSE +22 -0
  115. vibey_bootstrap-4.0.0.dist-info/licenses/NOTICE.md +8 -0
  116. vibey_bootstrap-4.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,271 @@
1
+ """
2
+ vibey-bootstrap (formerly azure-bootstrap)
3
+
4
+ A production-ready Azure bootstrap library that handles application initialization
5
+ for Azure Functions, including App Configuration, Key Vault, and App Insights integration.
6
+
7
+ This library solves the circular dependency between logging and configuration by:
8
+ 1. Starting with basic console logging
9
+ 2. Loading configuration from Azure App Configuration + Key Vault
10
+ 3. Upgrading to App Insights telemetry when available
11
+ 4. Loading all configs to os.environ for transparent access
12
+
13
+ Quick Start:
14
+ from vibey_bootstrap import initialize_application, get_bootstrap_logger
15
+
16
+ # Get logger that works immediately
17
+ logger = get_bootstrap_logger(__name__)
18
+
19
+ # Bootstrap the application (App Config + Key Vault + App Insights)
20
+ config_repo = initialize_application()
21
+
22
+ # Now all configs are in os.environ
23
+ db_host = os.getenv("DATABASE_HOST")
24
+
25
+ For detailed usage, see: https://github.com/adammatthewsteinberger/vibey-bootstrap
26
+ """
27
+
28
+ __version__ = "4.0.0"
29
+ __author__ = "Adam Matthew Steinberger"
30
+ __license__ = "MIT"
31
+
32
+ # ──────────────────────────────────────────────────────────────────────────
33
+ # v2 additions (additive only — never alters the v1 surface above)
34
+ # ──────────────────────────────────────────────────────────────────────────
35
+ import logging as _stdlib_logging
36
+ import os as _os
37
+
38
+ # v3.0.0 additions (opt-in subpackages re-exported for convenience)
39
+ from vibey_bootstrap.aks import build_info as build_info
40
+ from vibey_bootstrap.audit import verify_chain as verify_chain
41
+ from vibey_bootstrap.auth import verify_hmac_signature as verify_hmac_signature
42
+ from vibey_bootstrap.bootstrap import bootstrap_initialized as bootstrap_initialized
43
+ from vibey_bootstrap.bootstrap import ensure_bootstrap as ensure_bootstrap
44
+ from vibey_bootstrap.bootstrap import load_local_settings as load_local_settings
45
+ from vibey_bootstrap.counters import bump_counter as bump_counter
46
+ from vibey_bootstrap.counters import counter_snapshot as counter_snapshot
47
+ from vibey_bootstrap.db.outbox import drain_outbox as drain_outbox
48
+ from vibey_bootstrap.email import AcsEmailSender as AcsEmailSender
49
+ from vibey_bootstrap.exceptions import InvalidMessageError as InvalidMessageError
50
+ from vibey_bootstrap.exceptions import NetworkError as NetworkError
51
+ from vibey_bootstrap.exceptions import PipelineError as PipelineError
52
+ from vibey_bootstrap.exceptions import RateLimitError as RateLimitError
53
+ from vibey_bootstrap.exceptions import TransientError as TransientError
54
+ from vibey_bootstrap.exceptions import UnrecoverableError as UnrecoverableError
55
+ from vibey_bootstrap.exceptions import is_unrecoverable as is_unrecoverable
56
+ from vibey_bootstrap.governance import budget_guard as budget_guard
57
+ from vibey_bootstrap.governance import track_usage as track_usage
58
+ from vibey_bootstrap.http import build_session as build_session
59
+ from vibey_bootstrap.http import request_with_retry as request_with_retry
60
+ from vibey_bootstrap.identity import build_tenant_credential as build_tenant_credential
61
+ from vibey_bootstrap.identity import (
62
+ build_tenant_credential_cached as build_tenant_credential_cached,
63
+ )
64
+ from vibey_bootstrap.logging import JsonLogFormatter as JsonLogFormatter
65
+ from vibey_bootstrap.logging import configure_logging as configure_logging
66
+ from vibey_bootstrap.logging import correlation_scope as correlation_scope
67
+ from vibey_bootstrap.logging import get_correlation_id as get_correlation_id
68
+ from vibey_bootstrap.logging import mask_api_key as mask_api_key
69
+ from vibey_bootstrap.logging import mask_bearer_token as mask_bearer_token
70
+ from vibey_bootstrap.logging import mask_email_address as mask_email_address
71
+ from vibey_bootstrap.logging import mask_secrets_in_dict as mask_secrets_in_dict
72
+ from vibey_bootstrap.logging import safe_json_dumps as safe_json_dumps
73
+ from vibey_bootstrap.logging import sanitize_for_log as sanitize_for_log
74
+ from vibey_bootstrap.logging import set_correlation_id as set_correlation_id
75
+
76
+ # Exceptions
77
+ from vibey_bootstrap.models.exceptions import ConfigurationError as ConfigurationError
78
+ from vibey_bootstrap.models.exceptions import KeyVaultError as KeyVaultError
79
+ from vibey_bootstrap.models.exceptions import RepositoryError as RepositoryError
80
+ from vibey_bootstrap.path_safety import confine_to_root as confine_to_root
81
+ from vibey_bootstrap.path_safety import sanitize_path_segment as sanitize_path_segment
82
+ from vibey_bootstrap.phases import PhaseResult as PhaseResult
83
+ from vibey_bootstrap.phases import run_phase as run_phase
84
+ from vibey_bootstrap.phases import run_phases as run_phases
85
+
86
+ # Repository implementations
87
+ from vibey_bootstrap.repositories.enhanced_config_repository import (
88
+ EnhancedConfigRepository as EnhancedConfigRepository,
89
+ )
90
+ from vibey_bootstrap.repositories.enhanced_config_repository import (
91
+ create_enhanced_config_repository as create_enhanced_config_repository,
92
+ )
93
+ from vibey_bootstrap.repositories.interfaces.enhanced_config_repository_interface import (
94
+ EnhancedConfigRepositoryInterface as EnhancedConfigRepositoryInterface,
95
+ )
96
+ from vibey_bootstrap.repositories.interfaces.secrets_repository_interface import (
97
+ SecretsRepositoryInterface as SecretsRepositoryInterface,
98
+ )
99
+ from vibey_bootstrap.repositories.secrets_repository import SecretsRepository as SecretsRepository
100
+ from vibey_bootstrap.security import compare_secrets as compare_secrets
101
+
102
+ # Core classes for advanced usage
103
+ # Main bootstrap function - most users only need this
104
+ from vibey_bootstrap.services.application_bootstrap import (
105
+ ApplicationBootstrap as ApplicationBootstrap,
106
+ )
107
+ from vibey_bootstrap.services.application_bootstrap import (
108
+ initialize_application as initialize_application,
109
+ )
110
+
111
+ # Bootstrap logging - works before configuration loaded
112
+ from vibey_bootstrap.services.bootstrap_logging import BootstrapLogger as BootstrapLogger
113
+ from vibey_bootstrap.services.bootstrap_logging import ExtraFieldsFormatter as ExtraFieldsFormatter
114
+ from vibey_bootstrap.services.bootstrap_logging import (
115
+ ensure_bootstrap_logging as ensure_bootstrap_logging,
116
+ )
117
+ from vibey_bootstrap.services.bootstrap_logging import get_bootstrap_logger as get_bootstrap_logger
118
+
119
+ # Interfaces for type hinting and custom implementations
120
+ from vibey_bootstrap.services.interfaces.application_bootstrap_interface import (
121
+ ApplicationBootstrapInterface as ApplicationBootstrapInterface,
122
+ )
123
+ from vibey_bootstrap.services.interfaces.bootstrap_logger_interface import (
124
+ BootstrapLoggerInterface as BootstrapLoggerInterface,
125
+ )
126
+ from vibey_bootstrap.services.interfaces.telemetry_manager_interface import (
127
+ TelemetryManagerInterface as TelemetryManagerInterface,
128
+ )
129
+ from vibey_bootstrap.services.telemetry import TelemetryManager as TelemetryManager
130
+ from vibey_bootstrap.services.telemetry import telemetry_manager as telemetry_manager
131
+ from vibey_bootstrap.softfail import SoftFailResult as SoftFailResult
132
+ from vibey_bootstrap.softfail import soft_fail as soft_fail
133
+ from vibey_bootstrap.softfail import soft_fail_with as soft_fail_with
134
+ from vibey_bootstrap.tracing import latency_snapshot as latency_snapshot
135
+ from vibey_bootstrap.tracing import traced as traced
136
+ from vibey_bootstrap.transports import configure_transports as configure_transports
137
+ from vibey_bootstrap.transports import disable_transport as disable_transport
138
+ from vibey_bootstrap.transports import enable_transport as enable_transport
139
+ from vibey_bootstrap.transports import list_transports as list_transports
140
+ from vibey_bootstrap.transports import register_transport as register_transport
141
+ from vibey_bootstrap.validation import MessageSchema as MessageSchema
142
+ from vibey_bootstrap.validation import queue_message_schema as queue_message_schema
143
+ from vibey_bootstrap.validation import validate_message as validate_message
144
+
145
+
146
+ def refresh_setting(*names: str) -> None:
147
+ """Re-read named settings from the cached App Configuration repo and
148
+ write their values into ``os.environ``.
149
+
150
+ Net-new in v2. Designed to be called from a recurring job (see
151
+ ``vibey_bootstrap.config_refresh.refresh_log_flags``) so ops can flip a
152
+ setting in App Configuration and see it take effect within seconds
153
+ without redeploying.
154
+
155
+ No-ops with a DEBUG log when ``initialize_application()`` has not yet
156
+ run. Best-effort — never raises.
157
+ """
158
+ if not names:
159
+ return
160
+ logger = _stdlib_logging.getLogger(__name__)
161
+ try:
162
+ from vibey_bootstrap.services.application_bootstrap import (
163
+ get_last_initialized_repo,
164
+ )
165
+ except Exception:
166
+ logger.debug("refresh_setting: bootstrap module unavailable")
167
+ return
168
+ repo = get_last_initialized_repo()
169
+ if repo is None:
170
+ logger.debug("refresh_setting: no cached repo (initialize_application not called)")
171
+ return
172
+ for name in names:
173
+ if not isinstance(name, str) or not name:
174
+ continue
175
+ try:
176
+ value = repo.get_value(name)
177
+ except Exception as exc:
178
+ logger.warning("refresh_setting: failed to read %s: %s", name, exc)
179
+ continue
180
+ if value is None:
181
+ continue
182
+ _os.environ[name] = str(value)
183
+
184
+
185
+ # Public API
186
+ __all__ = [
187
+ # Version
188
+ "__version__",
189
+ # Main bootstrap functions (most common usage)
190
+ "initialize_application",
191
+ "get_bootstrap_logger",
192
+ "create_enhanced_config_repository",
193
+ "ensure_bootstrap_logging",
194
+ # Singleton instance
195
+ "telemetry_manager",
196
+ # Core classes
197
+ "ApplicationBootstrap",
198
+ "BootstrapLogger",
199
+ "ExtraFieldsFormatter",
200
+ "TelemetryManager",
201
+ "EnhancedConfigRepository",
202
+ "SecretsRepository",
203
+ # Interfaces
204
+ "ApplicationBootstrapInterface",
205
+ "BootstrapLoggerInterface",
206
+ "TelemetryManagerInterface",
207
+ "EnhancedConfigRepositoryInterface",
208
+ "SecretsRepositoryInterface",
209
+ # Exceptions
210
+ "RepositoryError",
211
+ "ConfigurationError",
212
+ "KeyVaultError",
213
+ # v2 additions — Tier 1 always-on primitives
214
+ "bootstrap_initialized",
215
+ "bump_counter",
216
+ "configure_logging",
217
+ "correlation_scope",
218
+ "counter_snapshot",
219
+ "ensure_bootstrap",
220
+ "get_correlation_id",
221
+ "latency_snapshot",
222
+ "load_local_settings",
223
+ "mask_api_key",
224
+ "mask_bearer_token",
225
+ "mask_email_address",
226
+ "mask_secrets_in_dict",
227
+ "refresh_setting",
228
+ "safe_json_dumps",
229
+ "sanitize_for_log",
230
+ "set_correlation_id",
231
+ "traced",
232
+ # v2 Parts 2+3 — exceptions, soft-fail, phases, validation, path safety, security
233
+ "InvalidMessageError",
234
+ "MessageSchema",
235
+ "NetworkError",
236
+ "PhaseResult",
237
+ "PipelineError",
238
+ "RateLimitError",
239
+ "SoftFailResult",
240
+ "TransientError",
241
+ "UnrecoverableError",
242
+ "compare_secrets",
243
+ "confine_to_root",
244
+ "is_unrecoverable",
245
+ "queue_message_schema",
246
+ "run_phase",
247
+ "run_phases",
248
+ "sanitize_path_segment",
249
+ "soft_fail",
250
+ "soft_fail_with",
251
+ "validate_message",
252
+ # v2.1 additions — logging transport layer
253
+ "JsonLogFormatter",
254
+ "configure_transports",
255
+ "disable_transport",
256
+ "enable_transport",
257
+ "list_transports",
258
+ "register_transport",
259
+ # v3.0.0 — db, outbox, email, http, documentdb, aks, governance, identity, audit
260
+ "build_tenant_credential",
261
+ "build_tenant_credential_cached",
262
+ "verify_chain",
263
+ "budget_guard",
264
+ "track_usage",
265
+ "build_session",
266
+ "request_with_retry",
267
+ "build_info",
268
+ "drain_outbox",
269
+ "AcsEmailSender",
270
+ "verify_hmac_signature",
271
+ ]
@@ -0,0 +1,84 @@
1
+ """AKS runtime helpers — graceful shutdown, build info, probes, pod context."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import logging
7
+ import os
8
+ import signal
9
+ import threading
10
+ from collections.abc import Callable
11
+ from typing import Any
12
+
13
+ from vibey_bootstrap.counters import counter_snapshot
14
+
15
+ _logger = logging.getLogger(__name__)
16
+
17
+
18
+ def install_sigterm_handler(stop_event: threading.Event) -> None:
19
+ """Register SIGTERM handler that sets *stop_event* for graceful drain."""
20
+
21
+ def _handler(signum: int, frame: Any) -> None:
22
+ _logger.info("SIGTERM received — setting stop event")
23
+ stop_event.set()
24
+
25
+ signal.signal(signal.SIGTERM, _handler)
26
+
27
+
28
+ def setup_async_sigterm_handler() -> asyncio.Event:
29
+ """Return an asyncio.Event set on SIGTERM."""
30
+ stop = asyncio.Event()
31
+ loop = asyncio.get_running_loop()
32
+
33
+ def _handler() -> None:
34
+ _logger.info("SIGTERM received — setting async stop event")
35
+ stop.set()
36
+
37
+ loop.add_signal_handler(signal.SIGTERM, _handler)
38
+ return stop
39
+
40
+
41
+ def build_info() -> dict[str, str | None]:
42
+ """Return version/build metadata from downward-API or CI env vars."""
43
+ return {
44
+ "version": os.environ.get("BUILD_VERSION") or os.environ.get("APP_VERSION"),
45
+ "git_sha": os.environ.get("GIT_SHA"),
46
+ "build_time": os.environ.get("BUILD_TIME"),
47
+ "image_tag": os.environ.get("IMAGE_TAG"),
48
+ "pod_name": os.environ.get("POD_NAME"),
49
+ "pod_namespace": os.environ.get("POD_NAMESPACE"),
50
+ "node_name": os.environ.get("NODE_NAME"),
51
+ }
52
+
53
+
54
+ def keda_metric_value(name: str = "queue_depth") -> float:
55
+ """Scalar metric for KEDA metrics-api scaler (from counters snapshot)."""
56
+ snap = counter_snapshot()
57
+ return float(snap.get(name, 0))
58
+
59
+
60
+ def mount_build_info_route(app: Any, path: str = "/api/version") -> None:
61
+ """Mount a FastAPI route returning :func:`build_info`."""
62
+
63
+ @app.get(path)
64
+ def _version() -> dict[str, str | None]:
65
+ return build_info()
66
+
67
+
68
+ def pod_context_extra() -> dict[str, str | None]:
69
+ """Correlation/log extras from Kubernetes downward API env vars."""
70
+ return {
71
+ "pod_name": os.environ.get("POD_NAME"),
72
+ "pod_namespace": os.environ.get("POD_NAMESPACE"),
73
+ "node_name": os.environ.get("NODE_NAME"),
74
+ }
75
+
76
+
77
+ __all__ = [
78
+ "build_info",
79
+ "install_sigterm_handler",
80
+ "keda_metric_value",
81
+ "mount_build_info_route",
82
+ "pod_context_extra",
83
+ "setup_async_sigterm_handler",
84
+ ]
@@ -0,0 +1,82 @@
1
+ """Generic leader election — K8s ConfigMap lock or soft no-op."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import os
7
+ import threading
8
+ from typing import Any
9
+
10
+ _logger = logging.getLogger(__name__)
11
+
12
+
13
+ class LeaderElection:
14
+ """Simple in-process leader election using a ConfigMap name env var.
15
+
16
+ When ``LEADER_ELECTION_CONFIGMAP`` is unset, :meth:`is_leader` always
17
+ returns ``True`` (soft no-op for single-replica dev).
18
+ """
19
+
20
+ def __init__(
21
+ self,
22
+ *,
23
+ configmap_name: str | None = None,
24
+ namespace: str | None = None,
25
+ identity: str | None = None,
26
+ lease_seconds: int = 30,
27
+ ) -> None:
28
+ self._configmap = configmap_name or os.environ.get("LEADER_ELECTION_CONFIGMAP")
29
+ self._namespace = namespace or os.environ.get("POD_NAMESPACE", "default")
30
+ self._identity = identity or os.environ.get("POD_NAME", "local")
31
+ self._lease_seconds = lease_seconds
32
+ self._is_leader = False
33
+ self._lock = threading.Lock()
34
+ self._stop = threading.Event()
35
+ self._thread: threading.Thread | None = None
36
+
37
+ @property
38
+ def is_leader(self) -> bool:
39
+ if not self._configmap:
40
+ return True
41
+ with self._lock:
42
+ return self._is_leader
43
+
44
+ def start(self) -> None:
45
+ if not self._configmap:
46
+ self._is_leader = True
47
+ return
48
+ self._thread = threading.Thread(target=self._run, name="leader-election", daemon=True)
49
+ self._thread.start()
50
+
51
+ def stop(self) -> None:
52
+ self._stop.set()
53
+ if self._thread and self._thread.is_alive():
54
+ self._thread.join(timeout=5.0)
55
+
56
+ def _run(self) -> None:
57
+ while not self._stop.is_set():
58
+ try:
59
+ acquired = self._try_acquire()
60
+ with self._lock:
61
+ self._is_leader = acquired
62
+ except Exception:
63
+ _logger.debug("leader election cycle failed", exc_info=True)
64
+ self._stop.wait(self._lease_seconds / 2)
65
+
66
+ def _try_acquire(self) -> bool:
67
+ # Production apps use the K8s API; this stub uses env-only soft lock.
68
+ holder = os.environ.get("LEADER_HOLDER")
69
+ if holder is None or holder == self._identity:
70
+ os.environ["LEADER_HOLDER"] = self._identity
71
+ return True
72
+ return holder == self._identity
73
+
74
+
75
+ def leader_election(**kwargs: Any) -> LeaderElection:
76
+ """Factory for :class:`LeaderElection`."""
77
+ le = LeaderElection(**kwargs)
78
+ le.start()
79
+ return le
80
+
81
+
82
+ __all__ = ["LeaderElection", "leader_election"]
@@ -0,0 +1,36 @@
1
+ """Tier 2 tiered-alert dispatcher.
2
+
3
+ Apps register a sender (any callable matching ``AlertSender``) at startup,
4
+ then code anywhere in the app can fire ``alert_dev_team(...)`` — the
5
+ dispatcher handles dedup, rate-limit, escalation, and HTML rendering.
6
+ """
7
+
8
+ from vibey_bootstrap.alerts.dispatcher import (
9
+ AlertRecord,
10
+ AlertSender,
11
+ AlertSeverity,
12
+ alert_dev_team,
13
+ drain_pending_alerts,
14
+ install_global_exception_hooks,
15
+ register_dispatcher,
16
+ reset_state,
17
+ )
18
+ from vibey_bootstrap.alerts.render import (
19
+ _render_alert_html,
20
+ render_pending_alerts_html,
21
+ )
22
+ from vibey_bootstrap.counters import bump_counter, counter_snapshot
23
+
24
+ __all__ = [
25
+ "AlertRecord",
26
+ "AlertSender",
27
+ "AlertSeverity",
28
+ "alert_dev_team",
29
+ "bump_counter",
30
+ "counter_snapshot",
31
+ "drain_pending_alerts",
32
+ "install_global_exception_hooks",
33
+ "register_dispatcher",
34
+ "render_pending_alerts_html",
35
+ "reset_state",
36
+ ]