htmlgraph 0.20.1__py3-none-any.whl → 0.27.5__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 (304) hide show
  1. htmlgraph/.htmlgraph/.session-warning-state.json +6 -0
  2. htmlgraph/.htmlgraph/agents.json +72 -0
  3. htmlgraph/.htmlgraph/htmlgraph.db +0 -0
  4. htmlgraph/__init__.py +51 -1
  5. htmlgraph/__init__.pyi +123 -0
  6. htmlgraph/agent_detection.py +26 -10
  7. htmlgraph/agent_registry.py +2 -1
  8. htmlgraph/analytics/__init__.py +8 -1
  9. htmlgraph/analytics/cli.py +86 -20
  10. htmlgraph/analytics/cost_analyzer.py +391 -0
  11. htmlgraph/analytics/cost_monitor.py +664 -0
  12. htmlgraph/analytics/cost_reporter.py +675 -0
  13. htmlgraph/analytics/cross_session.py +617 -0
  14. htmlgraph/analytics/dependency.py +10 -6
  15. htmlgraph/analytics/pattern_learning.py +771 -0
  16. htmlgraph/analytics/session_graph.py +707 -0
  17. htmlgraph/analytics/strategic/__init__.py +80 -0
  18. htmlgraph/analytics/strategic/cost_optimizer.py +611 -0
  19. htmlgraph/analytics/strategic/pattern_detector.py +876 -0
  20. htmlgraph/analytics/strategic/preference_manager.py +709 -0
  21. htmlgraph/analytics/strategic/suggestion_engine.py +747 -0
  22. htmlgraph/analytics/work_type.py +67 -27
  23. htmlgraph/analytics_index.py +53 -20
  24. htmlgraph/api/__init__.py +3 -0
  25. htmlgraph/api/cost_alerts_websocket.py +416 -0
  26. htmlgraph/api/main.py +2498 -0
  27. htmlgraph/api/static/htmx.min.js +1 -0
  28. htmlgraph/api/static/style-redesign.css +1344 -0
  29. htmlgraph/api/static/style.css +1079 -0
  30. htmlgraph/api/templates/dashboard-redesign.html +1366 -0
  31. htmlgraph/api/templates/dashboard.html +794 -0
  32. htmlgraph/api/templates/partials/activity-feed-hierarchical.html +326 -0
  33. htmlgraph/api/templates/partials/activity-feed.html +1100 -0
  34. htmlgraph/api/templates/partials/agents-redesign.html +317 -0
  35. htmlgraph/api/templates/partials/agents.html +317 -0
  36. htmlgraph/api/templates/partials/event-traces.html +373 -0
  37. htmlgraph/api/templates/partials/features-kanban-redesign.html +509 -0
  38. htmlgraph/api/templates/partials/features.html +578 -0
  39. htmlgraph/api/templates/partials/metrics-redesign.html +346 -0
  40. htmlgraph/api/templates/partials/metrics.html +346 -0
  41. htmlgraph/api/templates/partials/orchestration-redesign.html +443 -0
  42. htmlgraph/api/templates/partials/orchestration.html +198 -0
  43. htmlgraph/api/templates/partials/spawners.html +375 -0
  44. htmlgraph/api/templates/partials/work-items.html +613 -0
  45. htmlgraph/api/websocket.py +538 -0
  46. htmlgraph/archive/__init__.py +24 -0
  47. htmlgraph/archive/bloom.py +234 -0
  48. htmlgraph/archive/fts.py +297 -0
  49. htmlgraph/archive/manager.py +583 -0
  50. htmlgraph/archive/search.py +244 -0
  51. htmlgraph/atomic_ops.py +560 -0
  52. htmlgraph/attribute_index.py +2 -1
  53. htmlgraph/bounded_paths.py +539 -0
  54. htmlgraph/builders/base.py +57 -2
  55. htmlgraph/builders/bug.py +19 -3
  56. htmlgraph/builders/chore.py +19 -3
  57. htmlgraph/builders/epic.py +19 -3
  58. htmlgraph/builders/feature.py +27 -3
  59. htmlgraph/builders/insight.py +2 -1
  60. htmlgraph/builders/metric.py +2 -1
  61. htmlgraph/builders/pattern.py +2 -1
  62. htmlgraph/builders/phase.py +19 -3
  63. htmlgraph/builders/spike.py +29 -3
  64. htmlgraph/builders/track.py +42 -1
  65. htmlgraph/cigs/__init__.py +81 -0
  66. htmlgraph/cigs/autonomy.py +385 -0
  67. htmlgraph/cigs/cost.py +475 -0
  68. htmlgraph/cigs/messages_basic.py +472 -0
  69. htmlgraph/cigs/messaging.py +365 -0
  70. htmlgraph/cigs/models.py +771 -0
  71. htmlgraph/cigs/pattern_storage.py +427 -0
  72. htmlgraph/cigs/patterns.py +503 -0
  73. htmlgraph/cigs/posttool_analyzer.py +234 -0
  74. htmlgraph/cigs/reporter.py +818 -0
  75. htmlgraph/cigs/tracker.py +317 -0
  76. htmlgraph/cli/.htmlgraph/.session-warning-state.json +6 -0
  77. htmlgraph/cli/.htmlgraph/agents.json +72 -0
  78. htmlgraph/cli/.htmlgraph/htmlgraph.db +0 -0
  79. htmlgraph/cli/__init__.py +42 -0
  80. htmlgraph/cli/__main__.py +6 -0
  81. htmlgraph/cli/analytics.py +1424 -0
  82. htmlgraph/cli/base.py +685 -0
  83. htmlgraph/cli/constants.py +206 -0
  84. htmlgraph/cli/core.py +954 -0
  85. htmlgraph/cli/main.py +147 -0
  86. htmlgraph/cli/models.py +475 -0
  87. htmlgraph/cli/templates/__init__.py +1 -0
  88. htmlgraph/cli/templates/cost_dashboard.py +399 -0
  89. htmlgraph/cli/work/__init__.py +239 -0
  90. htmlgraph/cli/work/browse.py +115 -0
  91. htmlgraph/cli/work/features.py +568 -0
  92. htmlgraph/cli/work/orchestration.py +676 -0
  93. htmlgraph/cli/work/report.py +728 -0
  94. htmlgraph/cli/work/sessions.py +466 -0
  95. htmlgraph/cli/work/snapshot.py +559 -0
  96. htmlgraph/cli/work/tracks.py +486 -0
  97. htmlgraph/cli_commands/__init__.py +1 -0
  98. htmlgraph/cli_commands/feature.py +195 -0
  99. htmlgraph/cli_framework.py +115 -0
  100. htmlgraph/collections/__init__.py +2 -0
  101. htmlgraph/collections/base.py +197 -14
  102. htmlgraph/collections/bug.py +2 -1
  103. htmlgraph/collections/chore.py +2 -1
  104. htmlgraph/collections/epic.py +2 -1
  105. htmlgraph/collections/feature.py +2 -1
  106. htmlgraph/collections/insight.py +2 -1
  107. htmlgraph/collections/metric.py +2 -1
  108. htmlgraph/collections/pattern.py +2 -1
  109. htmlgraph/collections/phase.py +2 -1
  110. htmlgraph/collections/session.py +194 -0
  111. htmlgraph/collections/spike.py +13 -2
  112. htmlgraph/collections/task_delegation.py +241 -0
  113. htmlgraph/collections/todo.py +14 -1
  114. htmlgraph/collections/traces.py +487 -0
  115. htmlgraph/config/cost_models.json +56 -0
  116. htmlgraph/config.py +190 -0
  117. htmlgraph/context_analytics.py +2 -1
  118. htmlgraph/converter.py +116 -7
  119. htmlgraph/cost_analysis/__init__.py +5 -0
  120. htmlgraph/cost_analysis/analyzer.py +438 -0
  121. htmlgraph/dashboard.html +2246 -248
  122. htmlgraph/dashboard.html.backup +6592 -0
  123. htmlgraph/dashboard.html.bak +7181 -0
  124. htmlgraph/dashboard.html.bak2 +7231 -0
  125. htmlgraph/dashboard.html.bak3 +7232 -0
  126. htmlgraph/db/__init__.py +38 -0
  127. htmlgraph/db/queries.py +790 -0
  128. htmlgraph/db/schema.py +1788 -0
  129. htmlgraph/decorators.py +317 -0
  130. htmlgraph/dependency_models.py +2 -1
  131. htmlgraph/deploy.py +26 -27
  132. htmlgraph/docs/API_REFERENCE.md +841 -0
  133. htmlgraph/docs/HTTP_API.md +750 -0
  134. htmlgraph/docs/INTEGRATION_GUIDE.md +752 -0
  135. htmlgraph/docs/ORCHESTRATION_PATTERNS.md +717 -0
  136. htmlgraph/docs/README.md +532 -0
  137. htmlgraph/docs/__init__.py +77 -0
  138. htmlgraph/docs/docs_version.py +55 -0
  139. htmlgraph/docs/metadata.py +93 -0
  140. htmlgraph/docs/migrations.py +232 -0
  141. htmlgraph/docs/template_engine.py +143 -0
  142. htmlgraph/docs/templates/_sections/cli_reference.md.j2 +52 -0
  143. htmlgraph/docs/templates/_sections/core_concepts.md.j2 +29 -0
  144. htmlgraph/docs/templates/_sections/sdk_basics.md.j2 +69 -0
  145. htmlgraph/docs/templates/base_agents.md.j2 +78 -0
  146. htmlgraph/docs/templates/example_user_override.md.j2 +47 -0
  147. htmlgraph/docs/version_check.py +163 -0
  148. htmlgraph/edge_index.py +2 -1
  149. htmlgraph/error_handler.py +544 -0
  150. htmlgraph/event_log.py +86 -37
  151. htmlgraph/event_migration.py +2 -1
  152. htmlgraph/file_watcher.py +12 -8
  153. htmlgraph/find_api.py +2 -1
  154. htmlgraph/git_events.py +67 -9
  155. htmlgraph/hooks/.htmlgraph/.session-warning-state.json +6 -0
  156. htmlgraph/hooks/.htmlgraph/agents.json +72 -0
  157. htmlgraph/hooks/.htmlgraph/index.sqlite +0 -0
  158. htmlgraph/hooks/__init__.py +8 -0
  159. htmlgraph/hooks/bootstrap.py +169 -0
  160. htmlgraph/hooks/cigs_pretool_enforcer.py +354 -0
  161. htmlgraph/hooks/concurrent_sessions.py +208 -0
  162. htmlgraph/hooks/context.py +350 -0
  163. htmlgraph/hooks/drift_handler.py +525 -0
  164. htmlgraph/hooks/event_tracker.py +790 -99
  165. htmlgraph/hooks/git_commands.py +175 -0
  166. htmlgraph/hooks/installer.py +5 -1
  167. htmlgraph/hooks/orchestrator.py +327 -76
  168. htmlgraph/hooks/orchestrator_reflector.py +31 -4
  169. htmlgraph/hooks/post_tool_use_failure.py +32 -7
  170. htmlgraph/hooks/post_tool_use_handler.py +257 -0
  171. htmlgraph/hooks/posttooluse.py +92 -19
  172. htmlgraph/hooks/pretooluse.py +527 -7
  173. htmlgraph/hooks/prompt_analyzer.py +637 -0
  174. htmlgraph/hooks/session_handler.py +668 -0
  175. htmlgraph/hooks/session_summary.py +395 -0
  176. htmlgraph/hooks/state_manager.py +504 -0
  177. htmlgraph/hooks/subagent_detection.py +202 -0
  178. htmlgraph/hooks/subagent_stop.py +369 -0
  179. htmlgraph/hooks/task_enforcer.py +99 -4
  180. htmlgraph/hooks/validator.py +212 -91
  181. htmlgraph/ids.py +2 -1
  182. htmlgraph/learning.py +125 -100
  183. htmlgraph/mcp_server.py +2 -1
  184. htmlgraph/models.py +217 -18
  185. htmlgraph/operations/README.md +62 -0
  186. htmlgraph/operations/__init__.py +79 -0
  187. htmlgraph/operations/analytics.py +339 -0
  188. htmlgraph/operations/bootstrap.py +289 -0
  189. htmlgraph/operations/events.py +244 -0
  190. htmlgraph/operations/fastapi_server.py +231 -0
  191. htmlgraph/operations/hooks.py +350 -0
  192. htmlgraph/operations/initialization.py +597 -0
  193. htmlgraph/operations/initialization.py.backup +228 -0
  194. htmlgraph/operations/server.py +303 -0
  195. htmlgraph/orchestration/__init__.py +58 -0
  196. htmlgraph/orchestration/claude_launcher.py +179 -0
  197. htmlgraph/orchestration/command_builder.py +72 -0
  198. htmlgraph/orchestration/headless_spawner.py +281 -0
  199. htmlgraph/orchestration/live_events.py +377 -0
  200. htmlgraph/orchestration/model_selection.py +327 -0
  201. htmlgraph/orchestration/plugin_manager.py +140 -0
  202. htmlgraph/orchestration/prompts.py +137 -0
  203. htmlgraph/orchestration/spawner_event_tracker.py +383 -0
  204. htmlgraph/orchestration/spawners/__init__.py +16 -0
  205. htmlgraph/orchestration/spawners/base.py +194 -0
  206. htmlgraph/orchestration/spawners/claude.py +173 -0
  207. htmlgraph/orchestration/spawners/codex.py +435 -0
  208. htmlgraph/orchestration/spawners/copilot.py +294 -0
  209. htmlgraph/orchestration/spawners/gemini.py +471 -0
  210. htmlgraph/orchestration/subprocess_runner.py +36 -0
  211. htmlgraph/{orchestration.py → orchestration/task_coordination.py} +16 -8
  212. htmlgraph/orchestration.md +563 -0
  213. htmlgraph/orchestrator-system-prompt-optimized.txt +863 -0
  214. htmlgraph/orchestrator.py +2 -1
  215. htmlgraph/orchestrator_config.py +357 -0
  216. htmlgraph/orchestrator_mode.py +115 -4
  217. htmlgraph/parallel.py +2 -1
  218. htmlgraph/parser.py +86 -6
  219. htmlgraph/path_query.py +608 -0
  220. htmlgraph/pattern_matcher.py +636 -0
  221. htmlgraph/pydantic_models.py +476 -0
  222. htmlgraph/quality_gates.py +350 -0
  223. htmlgraph/query_builder.py +2 -1
  224. htmlgraph/query_composer.py +509 -0
  225. htmlgraph/reflection.py +443 -0
  226. htmlgraph/refs.py +344 -0
  227. htmlgraph/repo_hash.py +512 -0
  228. htmlgraph/repositories/__init__.py +292 -0
  229. htmlgraph/repositories/analytics_repository.py +455 -0
  230. htmlgraph/repositories/analytics_repository_standard.py +628 -0
  231. htmlgraph/repositories/feature_repository.py +581 -0
  232. htmlgraph/repositories/feature_repository_htmlfile.py +668 -0
  233. htmlgraph/repositories/feature_repository_memory.py +607 -0
  234. htmlgraph/repositories/feature_repository_sqlite.py +858 -0
  235. htmlgraph/repositories/filter_service.py +620 -0
  236. htmlgraph/repositories/filter_service_standard.py +445 -0
  237. htmlgraph/repositories/shared_cache.py +621 -0
  238. htmlgraph/repositories/shared_cache_memory.py +395 -0
  239. htmlgraph/repositories/track_repository.py +552 -0
  240. htmlgraph/repositories/track_repository_htmlfile.py +619 -0
  241. htmlgraph/repositories/track_repository_memory.py +508 -0
  242. htmlgraph/repositories/track_repository_sqlite.py +711 -0
  243. htmlgraph/sdk/__init__.py +398 -0
  244. htmlgraph/sdk/__init__.pyi +14 -0
  245. htmlgraph/sdk/analytics/__init__.py +19 -0
  246. htmlgraph/sdk/analytics/engine.py +155 -0
  247. htmlgraph/sdk/analytics/helpers.py +178 -0
  248. htmlgraph/sdk/analytics/registry.py +109 -0
  249. htmlgraph/sdk/base.py +484 -0
  250. htmlgraph/sdk/constants.py +216 -0
  251. htmlgraph/sdk/core.pyi +308 -0
  252. htmlgraph/sdk/discovery.py +120 -0
  253. htmlgraph/sdk/help/__init__.py +12 -0
  254. htmlgraph/sdk/help/mixin.py +699 -0
  255. htmlgraph/sdk/mixins/__init__.py +15 -0
  256. htmlgraph/sdk/mixins/attribution.py +113 -0
  257. htmlgraph/sdk/mixins/mixin.py +410 -0
  258. htmlgraph/sdk/operations/__init__.py +12 -0
  259. htmlgraph/sdk/operations/mixin.py +427 -0
  260. htmlgraph/sdk/orchestration/__init__.py +17 -0
  261. htmlgraph/sdk/orchestration/coordinator.py +203 -0
  262. htmlgraph/sdk/orchestration/spawner.py +204 -0
  263. htmlgraph/sdk/planning/__init__.py +19 -0
  264. htmlgraph/sdk/planning/bottlenecks.py +93 -0
  265. htmlgraph/sdk/planning/mixin.py +211 -0
  266. htmlgraph/sdk/planning/parallel.py +186 -0
  267. htmlgraph/sdk/planning/queue.py +210 -0
  268. htmlgraph/sdk/planning/recommendations.py +87 -0
  269. htmlgraph/sdk/planning/smart_planning.py +319 -0
  270. htmlgraph/sdk/session/__init__.py +19 -0
  271. htmlgraph/sdk/session/continuity.py +57 -0
  272. htmlgraph/sdk/session/handoff.py +110 -0
  273. htmlgraph/sdk/session/info.py +309 -0
  274. htmlgraph/sdk/session/manager.py +103 -0
  275. htmlgraph/sdk/strategic/__init__.py +26 -0
  276. htmlgraph/sdk/strategic/mixin.py +563 -0
  277. htmlgraph/server.py +295 -107
  278. htmlgraph/session_hooks.py +300 -0
  279. htmlgraph/session_manager.py +285 -3
  280. htmlgraph/session_registry.py +587 -0
  281. htmlgraph/session_state.py +436 -0
  282. htmlgraph/session_warning.py +2 -1
  283. htmlgraph/sessions/__init__.py +23 -0
  284. htmlgraph/sessions/handoff.py +756 -0
  285. htmlgraph/system_prompts.py +450 -0
  286. htmlgraph/templates/orchestration-view.html +350 -0
  287. htmlgraph/track_builder.py +33 -1
  288. htmlgraph/track_manager.py +38 -0
  289. htmlgraph/transcript.py +18 -5
  290. htmlgraph/validation.py +115 -0
  291. htmlgraph/watch.py +2 -1
  292. htmlgraph/work_type_utils.py +2 -1
  293. {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/dashboard.html +2246 -248
  294. {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/METADATA +95 -64
  295. htmlgraph-0.27.5.dist-info/RECORD +337 -0
  296. {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/entry_points.txt +1 -1
  297. htmlgraph/cli.py +0 -4839
  298. htmlgraph/sdk.py +0 -2359
  299. htmlgraph-0.20.1.dist-info/RECORD +0 -118
  300. {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/styles.css +0 -0
  301. {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
  302. {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
  303. {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
  304. {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/WHEEL +0 -0
@@ -0,0 +1,317 @@
1
+ """Decorators for function enhancement and cross-cutting concerns.
2
+
3
+ This module provides decorators for common patterns like retry logic with
4
+ exponential backoff, caching, timing, and error handling.
5
+ """
6
+
7
+ import functools
8
+ import logging
9
+ import random
10
+ import time
11
+ from collections.abc import Callable
12
+ from typing import Any, TypeVar
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ T = TypeVar("T")
17
+
18
+
19
+ class RetryError(Exception):
20
+ """Raised when a function exhausts all retry attempts."""
21
+
22
+ def __init__(
23
+ self,
24
+ function_name: str,
25
+ attempts: int,
26
+ last_exception: Exception,
27
+ ):
28
+ self.function_name = function_name
29
+ self.attempts = attempts
30
+ self.last_exception = last_exception
31
+ super().__init__(
32
+ f"Function '{function_name}' failed after {attempts} attempts. "
33
+ f"Last error: {last_exception}"
34
+ )
35
+
36
+
37
+ def retry(
38
+ max_attempts: int = 3,
39
+ initial_delay: float = 1.0,
40
+ max_delay: float = 60.0,
41
+ exponential_base: float = 2.0,
42
+ jitter: bool = True,
43
+ exceptions: tuple[type[Exception], ...] = (Exception,),
44
+ on_retry: Callable[[int, Exception, float], None] | None = None,
45
+ ) -> Callable[[Callable[..., T]], Callable[..., T]]:
46
+ """Decorator adding retry logic with exponential backoff to any function.
47
+
48
+ Implements exponential backoff with optional jitter to gracefully handle
49
+ transient failures. Useful for I/O operations, API calls, and distributed
50
+ system interactions.
51
+
52
+ Args:
53
+ max_attempts: Maximum number of attempts (default: 3). Must be >= 1.
54
+ initial_delay: Initial delay in seconds before first retry (default: 1.0).
55
+ Must be >= 0.
56
+ max_delay: Maximum delay in seconds between retries (default: 60.0).
57
+ Caps the exponential backoff. Must be >= initial_delay.
58
+ exponential_base: Base for exponential backoff calculation (default: 2.0).
59
+ delay = min(initial_delay * (base ** attempt_number), max_delay)
60
+ jitter: Whether to add random jitter to delays (default: True).
61
+ Helps prevent thundering herd problem in distributed systems.
62
+ exceptions: Tuple of exception types to catch and retry on
63
+ (default: (Exception,)). Other exceptions propagate immediately.
64
+ on_retry: Optional callback invoked on each retry with signature:
65
+ on_retry(attempt_number, exception, delay_seconds).
66
+ Useful for logging, metrics, or custom backoff strategies.
67
+
68
+ Returns:
69
+ Decorated function that retries on specified exceptions.
70
+
71
+ Raises:
72
+ RetryError: If all retry attempts are exhausted.
73
+ Other exceptions: If exception type is not in the retry list.
74
+
75
+ Examples:
76
+ Basic retry with default parameters:
77
+ >>> @retry()
78
+ ... def unstable_api_call():
79
+ ... response = requests.get('https://api.example.com/data')
80
+ ... response.raise_for_status()
81
+ ... return response.json()
82
+
83
+ Retry with custom parameters:
84
+ >>> @retry(
85
+ ... max_attempts=5,
86
+ ... initial_delay=0.5,
87
+ ... max_delay=30.0,
88
+ ... exponential_base=1.5,
89
+ ... exceptions=(ConnectionError, TimeoutError),
90
+ ... )
91
+ ... def fetch_with_timeout():
92
+ ... return expensive_io_operation()
93
+
94
+ With custom retry callback for logging:
95
+ >>> def log_retry(attempt, exc, delay):
96
+ ... logger.warning(
97
+ ... f"Retry attempt {attempt} after {delay}s: {exc}"
98
+ ... )
99
+ >>> @retry(
100
+ ... max_attempts=3,
101
+ ... on_retry=log_retry,
102
+ ... exceptions=(IOError,),
103
+ ... )
104
+ ... def read_file(path):
105
+ ... with open(path) as f:
106
+ ... return f.read()
107
+
108
+ Retry only specific exceptions (fail fast for others):
109
+ >>> @retry(
110
+ ... max_attempts=3,
111
+ ... exceptions=(ConnectionError, TimeoutError),
112
+ ... )
113
+ ... def resilient_request(url):
114
+ ... # Will retry on connection errors but fail immediately on 404
115
+ ... return requests.get(url, timeout=5).json()
116
+
117
+ Using with async functions:
118
+ >>> import asyncio
119
+ >>> @retry(max_attempts=3, initial_delay=0.1)
120
+ ... async def async_api_call():
121
+ ... async with aiohttp.ClientSession() as session:
122
+ ... async with session.get('https://api.example.com') as resp:
123
+ ... return await resp.json()
124
+ >>> asyncio.run(async_api_call())
125
+
126
+ Backoff Calculation:
127
+ The delay before retry N is calculated as:
128
+ - exponential: initial_delay * (exponential_base ** (attempt - 1))
129
+ - capped: min(exponential, max_delay)
130
+ - jittered: delay * (0.5 + random(0.0, 1.0)) if jitter=True
131
+
132
+ Example with exponential_base=2.0, initial_delay=1.0, max_delay=60.0:
133
+ - Attempt 1 fails, retry after: 1s
134
+ - Attempt 2 fails, retry after: 2s
135
+ - Attempt 3 fails, retry after: 4s
136
+ - Attempt 4 fails, retry after: 8s
137
+ - Attempt 5 fails, retry after: 16s
138
+ - Attempt 6 fails, retry after: 32s
139
+ - Attempt 7 fails, raise RetryError (max_attempts=3 means 3 total attempts)
140
+
141
+ Notes:
142
+ - If max_attempts=1, no retries occur (function runs once)
143
+ - Jitter is uniformly distributed in range [0.5 * delay, 1.5 * delay]
144
+ - Callbacks (on_retry) are invoked BEFORE sleeping, not after
145
+ - Thread-safe but not async-safe without adaptation
146
+ """
147
+ if max_attempts < 1:
148
+ raise ValueError("max_attempts must be >= 1")
149
+ if initial_delay < 0:
150
+ raise ValueError("initial_delay must be >= 0")
151
+ if max_delay < initial_delay:
152
+ raise ValueError("max_delay must be >= initial_delay")
153
+ if exponential_base <= 0:
154
+ raise ValueError("exponential_base must be > 0")
155
+
156
+ def decorator(func: Callable[..., T]) -> Callable[..., T]:
157
+ @functools.wraps(func)
158
+ def wrapper(*args: Any, **kwargs: Any) -> T:
159
+ last_exception: Exception | None = None
160
+
161
+ for attempt in range(1, max_attempts + 1):
162
+ try:
163
+ return func(*args, **kwargs)
164
+ except exceptions as e:
165
+ last_exception = e
166
+
167
+ if attempt == max_attempts:
168
+ # Last attempt failed, raise RetryError
169
+ raise RetryError(
170
+ function_name=func.__name__,
171
+ attempts=max_attempts,
172
+ last_exception=e,
173
+ ) from e
174
+
175
+ # Calculate backoff with exponential growth and jitter
176
+ exponential_delay = initial_delay * (
177
+ exponential_base ** (attempt - 1)
178
+ )
179
+ delay = min(exponential_delay, max_delay)
180
+
181
+ if jitter:
182
+ # Add jitter: multiply by random value in [0.5, 1.5]
183
+ delay *= 0.5 + random.random()
184
+
185
+ # Invoke callback before sleeping
186
+ if on_retry is not None:
187
+ on_retry(attempt, e, delay)
188
+ else:
189
+ logger.debug(
190
+ f"Retry attempt {attempt}/{max_attempts} for "
191
+ f"{func.__name__} after {delay:.2f}s: {e}"
192
+ )
193
+
194
+ time.sleep(delay)
195
+
196
+ # This should never be reached, but satisfy type checker
197
+ assert last_exception is not None
198
+ raise RetryError(
199
+ function_name=func.__name__,
200
+ attempts=max_attempts,
201
+ last_exception=last_exception,
202
+ )
203
+
204
+ return wrapper
205
+
206
+ return decorator
207
+
208
+
209
+ def retry_async(
210
+ max_attempts: int = 3,
211
+ initial_delay: float = 1.0,
212
+ max_delay: float = 60.0,
213
+ exponential_base: float = 2.0,
214
+ jitter: bool = True,
215
+ exceptions: tuple[type[Exception], ...] = (Exception,),
216
+ on_retry: Callable[[int, Exception, float], None] | None = None,
217
+ ) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
218
+ """Async version of retry decorator with exponential backoff.
219
+
220
+ Identical to retry() but uses asyncio.sleep instead of time.sleep,
221
+ allowing it to be used with async/await functions without blocking.
222
+
223
+ Args:
224
+ max_attempts: Maximum number of attempts (default: 3). Must be >= 1.
225
+ initial_delay: Initial delay in seconds before first retry (default: 1.0).
226
+ max_delay: Maximum delay in seconds between retries (default: 60.0).
227
+ exponential_base: Base for exponential backoff (default: 2.0).
228
+ jitter: Whether to add random jitter to delays (default: True).
229
+ exceptions: Tuple of exception types to catch and retry on.
230
+ on_retry: Optional callback invoked on each retry.
231
+
232
+ Returns:
233
+ Decorated async function that retries on specified exceptions.
234
+
235
+ Raises:
236
+ RetryError: If all retry attempts are exhausted.
237
+
238
+ Examples:
239
+ >>> import asyncio
240
+ >>> @retry_async(max_attempts=3)
241
+ ... async def fetch_data():
242
+ ... async with aiohttp.ClientSession() as session:
243
+ ... async with session.get('https://api.example.com') as resp:
244
+ ... return await resp.json()
245
+
246
+ >>> @retry_async(
247
+ ... max_attempts=5,
248
+ ... initial_delay=0.1,
249
+ ... exceptions=(asyncio.TimeoutError, ConnectionError),
250
+ ... )
251
+ ... async def resilient_query():
252
+ ... return await db.query("SELECT * FROM users")
253
+ """
254
+ if max_attempts < 1:
255
+ raise ValueError("max_attempts must be >= 1")
256
+ if initial_delay < 0:
257
+ raise ValueError("initial_delay must be >= 0")
258
+ if max_delay < initial_delay:
259
+ raise ValueError("max_delay must be >= initial_delay")
260
+ if exponential_base <= 0:
261
+ raise ValueError("exponential_base must be > 0")
262
+
263
+ def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
264
+ @functools.wraps(func)
265
+ async def wrapper(*args: Any, **kwargs: Any) -> Any:
266
+ import asyncio
267
+
268
+ last_exception: Exception | None = None
269
+
270
+ for attempt in range(1, max_attempts + 1):
271
+ try:
272
+ return await func(*args, **kwargs)
273
+ except exceptions as e:
274
+ last_exception = e
275
+
276
+ if attempt == max_attempts:
277
+ raise RetryError(
278
+ function_name=func.__name__,
279
+ attempts=max_attempts,
280
+ last_exception=e,
281
+ ) from e
282
+
283
+ exponential_delay = initial_delay * (
284
+ exponential_base ** (attempt - 1)
285
+ )
286
+ delay = min(exponential_delay, max_delay)
287
+
288
+ if jitter:
289
+ delay *= 0.5 + random.random()
290
+
291
+ if on_retry is not None:
292
+ on_retry(attempt, e, delay)
293
+ else:
294
+ logger.debug(
295
+ f"Retry attempt {attempt}/{max_attempts} for "
296
+ f"{func.__name__} after {delay:.2f}s: {e}"
297
+ )
298
+
299
+ await asyncio.sleep(delay)
300
+
301
+ assert last_exception is not None
302
+ raise RetryError(
303
+ function_name=func.__name__,
304
+ attempts=max_attempts,
305
+ last_exception=last_exception,
306
+ )
307
+
308
+ return wrapper
309
+
310
+ return decorator
311
+
312
+
313
+ __all__ = [
314
+ "retry",
315
+ "retry_async",
316
+ "RetryError",
317
+ ]
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  """
2
4
  Data models for dependency analytics.
3
5
 
@@ -9,7 +11,6 @@ Provides Pydantic models for dependency-aware analytics results including:
9
11
  - Work prioritization
10
12
  """
11
13
 
12
- from __future__ import annotations
13
14
 
14
15
  from typing import Literal
15
16
 
htmlgraph/deploy.py CHANGED
@@ -16,14 +16,11 @@ from dataclasses import dataclass, field
16
16
  from pathlib import Path
17
17
  from typing import cast
18
18
 
19
+ from rich.console import Console
20
+ from rich.prompt import Confirm
19
21
 
20
- # ANSI color codes
21
- class Colors:
22
- RED = "\033[0;31m"
23
- GREEN = "\033[0;32m"
24
- YELLOW = "\033[1;33m"
25
- BLUE = "\033[0;34m"
26
- NC = "\033[0m" # No Color
22
+ # Global Rich Console for beautiful CLI output
23
+ console = Console()
27
24
 
28
25
 
29
26
  @dataclass
@@ -157,27 +154,25 @@ class Deployer:
157
154
 
158
155
  def log_section(self, message: str) -> None:
159
156
  """Log a section header."""
160
- print()
161
- print(f"{Colors.BLUE}{'=' * 60}{Colors.NC}")
162
- print(f"{Colors.BLUE}{message}{Colors.NC}")
163
- print(f"{Colors.BLUE}{'=' * 60}{Colors.NC}")
164
- print()
157
+ console.print()
158
+ console.print(f"[bold blue]{message}[/bold blue]")
159
+ console.print()
165
160
 
166
161
  def log_success(self, message: str) -> None:
167
162
  """Log a success message."""
168
- print(f"{Colors.GREEN}✅ {message}{Colors.NC}")
163
+ console.print(f"[green]✅ {message}[/green]")
169
164
 
170
165
  def log_error(self, message: str) -> None:
171
166
  """Log an error message."""
172
- print(f"{Colors.RED}❌ {message}{Colors.NC}")
167
+ console.print(f"[red]❌ {message}[/red]")
173
168
 
174
169
  def log_warning(self, message: str) -> None:
175
170
  """Log a warning message."""
176
- print(f"{Colors.YELLOW}⚠️ {message}{Colors.NC}")
171
+ console.print(f"[yellow]⚠️ {message}[/yellow]")
177
172
 
178
173
  def log_info(self, message: str) -> None:
179
174
  """Log an info message."""
180
- print(f"ℹ️ {message}")
175
+ console.print(f"[cyan]ℹ️ {message}[/cyan]")
181
176
 
182
177
  def run_command(
183
178
  self,
@@ -205,7 +200,8 @@ class Deployer:
205
200
  except subprocess.CalledProcessError as e:
206
201
  self.log_error(f"Command failed: {' '.join(cmd)}")
207
202
  if e.stderr:
208
- print(e.stderr, file=sys.stderr)
203
+ # Print error to stderr using console (Rich supports stderr output)
204
+ console.print(e.stderr, style="red")
209
205
  raise
210
206
 
211
207
  def run_hook(self, hook_commands: list[str], hook_name: str) -> None:
@@ -242,8 +238,7 @@ class Deployer:
242
238
  if result.returncode != 0:
243
239
  self.log_warning("You have uncommitted changes")
244
240
  if not self.dry_run:
245
- response = input("Continue anyway? (y/n) ")
246
- if response.lower() != "y":
241
+ if not Confirm.ask("Continue anyway?", default=False):
247
242
  sys.exit(1)
248
243
 
249
244
  # Push to remote
@@ -286,7 +281,7 @@ class Deployer:
286
281
  if dist_dir.exists():
287
282
  self.log_info("Build artifacts:")
288
283
  for file in dist_dir.iterdir():
289
- print(f" - {file.name}")
284
+ console.print(f"[dim] - {file.name}[/dim]")
290
285
 
291
286
  def _step_pypi_publish(self) -> None:
292
287
  """Publish to PyPI."""
@@ -324,8 +319,7 @@ class Deployer:
324
319
  f"PyPI token not found (looking for {token_var} or UV_PUBLISH_TOKEN)"
325
320
  )
326
321
  if not self.dry_run:
327
- response = input("Continue anyway? (y/n) ")
328
- if response.lower() != "y":
322
+ if not Confirm.ask("Continue anyway?", default=False):
329
323
  sys.exit(1)
330
324
 
331
325
  # Publish
@@ -464,7 +458,7 @@ class Deployer:
464
458
  # Summary
465
459
  self.log_section("Deployment Complete! 🎉")
466
460
  self.log_success("All deployment steps completed successfully!")
467
- print()
461
+ console.print()
468
462
 
469
463
 
470
464
  def create_deployment_config_template(output_path: Path) -> None:
@@ -526,7 +520,12 @@ post_publish = []
526
520
  """
527
521
 
528
522
  output_path.write_text(template)
529
- print(f"✅ Created deployment config template: {output_path}")
530
- print("\nNext steps:")
531
- print("1. Edit htmlgraph-deploy.toml to customize your deployment")
532
- print("2. Run: htmlgraph deploy run")
523
+ console.print(
524
+ f"[green]✅ Created deployment config template: {output_path}[/green]"
525
+ )
526
+ console.print()
527
+ console.print("[bold cyan]Next steps:[/bold cyan]")
528
+ console.print(
529
+ "[dim]1. Edit htmlgraph-deploy.toml to customize your deployment[/dim]"
530
+ )
531
+ console.print("[dim]2. Run: htmlgraph deploy run[/dim]")