htmlgraph 0.9.3__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.
- htmlgraph/.htmlgraph/.session-warning-state.json +6 -0
- htmlgraph/.htmlgraph/agents.json +72 -0
- htmlgraph/.htmlgraph/htmlgraph.db +0 -0
- htmlgraph/__init__.py +173 -17
- htmlgraph/__init__.pyi +123 -0
- htmlgraph/agent_detection.py +127 -0
- htmlgraph/agent_registry.py +45 -30
- htmlgraph/agents.py +160 -107
- htmlgraph/analytics/__init__.py +9 -2
- htmlgraph/analytics/cli.py +190 -51
- htmlgraph/analytics/cost_analyzer.py +391 -0
- htmlgraph/analytics/cost_monitor.py +664 -0
- htmlgraph/analytics/cost_reporter.py +675 -0
- htmlgraph/analytics/cross_session.py +617 -0
- htmlgraph/analytics/dependency.py +192 -100
- htmlgraph/analytics/pattern_learning.py +771 -0
- htmlgraph/analytics/session_graph.py +707 -0
- htmlgraph/analytics/strategic/__init__.py +80 -0
- htmlgraph/analytics/strategic/cost_optimizer.py +611 -0
- htmlgraph/analytics/strategic/pattern_detector.py +876 -0
- htmlgraph/analytics/strategic/preference_manager.py +709 -0
- htmlgraph/analytics/strategic/suggestion_engine.py +747 -0
- htmlgraph/analytics/work_type.py +190 -14
- htmlgraph/analytics_index.py +135 -51
- htmlgraph/api/__init__.py +3 -0
- htmlgraph/api/cost_alerts_websocket.py +416 -0
- htmlgraph/api/main.py +2498 -0
- htmlgraph/api/static/htmx.min.js +1 -0
- htmlgraph/api/static/style-redesign.css +1344 -0
- htmlgraph/api/static/style.css +1079 -0
- htmlgraph/api/templates/dashboard-redesign.html +1366 -0
- htmlgraph/api/templates/dashboard.html +794 -0
- htmlgraph/api/templates/partials/activity-feed-hierarchical.html +326 -0
- htmlgraph/api/templates/partials/activity-feed.html +1100 -0
- htmlgraph/api/templates/partials/agents-redesign.html +317 -0
- htmlgraph/api/templates/partials/agents.html +317 -0
- htmlgraph/api/templates/partials/event-traces.html +373 -0
- htmlgraph/api/templates/partials/features-kanban-redesign.html +509 -0
- htmlgraph/api/templates/partials/features.html +578 -0
- htmlgraph/api/templates/partials/metrics-redesign.html +346 -0
- htmlgraph/api/templates/partials/metrics.html +346 -0
- htmlgraph/api/templates/partials/orchestration-redesign.html +443 -0
- htmlgraph/api/templates/partials/orchestration.html +198 -0
- htmlgraph/api/templates/partials/spawners.html +375 -0
- htmlgraph/api/templates/partials/work-items.html +613 -0
- htmlgraph/api/websocket.py +538 -0
- htmlgraph/archive/__init__.py +24 -0
- htmlgraph/archive/bloom.py +234 -0
- htmlgraph/archive/fts.py +297 -0
- htmlgraph/archive/manager.py +583 -0
- htmlgraph/archive/search.py +244 -0
- htmlgraph/atomic_ops.py +560 -0
- htmlgraph/attribute_index.py +208 -0
- htmlgraph/bounded_paths.py +539 -0
- htmlgraph/builders/__init__.py +14 -0
- htmlgraph/builders/base.py +118 -29
- htmlgraph/builders/bug.py +150 -0
- htmlgraph/builders/chore.py +119 -0
- htmlgraph/builders/epic.py +150 -0
- htmlgraph/builders/feature.py +31 -6
- htmlgraph/builders/insight.py +195 -0
- htmlgraph/builders/metric.py +217 -0
- htmlgraph/builders/pattern.py +202 -0
- htmlgraph/builders/phase.py +162 -0
- htmlgraph/builders/spike.py +52 -19
- htmlgraph/builders/track.py +148 -72
- htmlgraph/cigs/__init__.py +81 -0
- htmlgraph/cigs/autonomy.py +385 -0
- htmlgraph/cigs/cost.py +475 -0
- htmlgraph/cigs/messages_basic.py +472 -0
- htmlgraph/cigs/messaging.py +365 -0
- htmlgraph/cigs/models.py +771 -0
- htmlgraph/cigs/pattern_storage.py +427 -0
- htmlgraph/cigs/patterns.py +503 -0
- htmlgraph/cigs/posttool_analyzer.py +234 -0
- htmlgraph/cigs/reporter.py +818 -0
- htmlgraph/cigs/tracker.py +317 -0
- htmlgraph/cli/.htmlgraph/.session-warning-state.json +6 -0
- htmlgraph/cli/.htmlgraph/agents.json +72 -0
- htmlgraph/cli/.htmlgraph/htmlgraph.db +0 -0
- htmlgraph/cli/__init__.py +42 -0
- htmlgraph/cli/__main__.py +6 -0
- htmlgraph/cli/analytics.py +1424 -0
- htmlgraph/cli/base.py +685 -0
- htmlgraph/cli/constants.py +206 -0
- htmlgraph/cli/core.py +954 -0
- htmlgraph/cli/main.py +147 -0
- htmlgraph/cli/models.py +475 -0
- htmlgraph/cli/templates/__init__.py +1 -0
- htmlgraph/cli/templates/cost_dashboard.py +399 -0
- htmlgraph/cli/work/__init__.py +239 -0
- htmlgraph/cli/work/browse.py +115 -0
- htmlgraph/cli/work/features.py +568 -0
- htmlgraph/cli/work/orchestration.py +676 -0
- htmlgraph/cli/work/report.py +728 -0
- htmlgraph/cli/work/sessions.py +466 -0
- htmlgraph/cli/work/snapshot.py +559 -0
- htmlgraph/cli/work/tracks.py +486 -0
- htmlgraph/cli_commands/__init__.py +1 -0
- htmlgraph/cli_commands/feature.py +195 -0
- htmlgraph/cli_framework.py +115 -0
- htmlgraph/collections/__init__.py +18 -0
- htmlgraph/collections/base.py +415 -98
- htmlgraph/collections/bug.py +53 -0
- htmlgraph/collections/chore.py +53 -0
- htmlgraph/collections/epic.py +53 -0
- htmlgraph/collections/feature.py +12 -26
- htmlgraph/collections/insight.py +100 -0
- htmlgraph/collections/metric.py +92 -0
- htmlgraph/collections/pattern.py +97 -0
- htmlgraph/collections/phase.py +53 -0
- htmlgraph/collections/session.py +194 -0
- htmlgraph/collections/spike.py +56 -16
- htmlgraph/collections/task_delegation.py +241 -0
- htmlgraph/collections/todo.py +511 -0
- htmlgraph/collections/traces.py +487 -0
- htmlgraph/config/cost_models.json +56 -0
- htmlgraph/config.py +190 -0
- htmlgraph/context_analytics.py +344 -0
- htmlgraph/converter.py +216 -28
- htmlgraph/cost_analysis/__init__.py +5 -0
- htmlgraph/cost_analysis/analyzer.py +438 -0
- htmlgraph/dashboard.html +2406 -307
- htmlgraph/dashboard.html.backup +6592 -0
- htmlgraph/dashboard.html.bak +7181 -0
- htmlgraph/dashboard.html.bak2 +7231 -0
- htmlgraph/dashboard.html.bak3 +7232 -0
- htmlgraph/db/__init__.py +38 -0
- htmlgraph/db/queries.py +790 -0
- htmlgraph/db/schema.py +1788 -0
- htmlgraph/decorators.py +317 -0
- htmlgraph/dependency_models.py +19 -2
- htmlgraph/deploy.py +142 -125
- htmlgraph/deployment_models.py +474 -0
- htmlgraph/docs/API_REFERENCE.md +841 -0
- htmlgraph/docs/HTTP_API.md +750 -0
- htmlgraph/docs/INTEGRATION_GUIDE.md +752 -0
- htmlgraph/docs/ORCHESTRATION_PATTERNS.md +717 -0
- htmlgraph/docs/README.md +532 -0
- htmlgraph/docs/__init__.py +77 -0
- htmlgraph/docs/docs_version.py +55 -0
- htmlgraph/docs/metadata.py +93 -0
- htmlgraph/docs/migrations.py +232 -0
- htmlgraph/docs/template_engine.py +143 -0
- htmlgraph/docs/templates/_sections/cli_reference.md.j2 +52 -0
- htmlgraph/docs/templates/_sections/core_concepts.md.j2 +29 -0
- htmlgraph/docs/templates/_sections/sdk_basics.md.j2 +69 -0
- htmlgraph/docs/templates/base_agents.md.j2 +78 -0
- htmlgraph/docs/templates/example_user_override.md.j2 +47 -0
- htmlgraph/docs/version_check.py +163 -0
- htmlgraph/edge_index.py +182 -27
- htmlgraph/error_handler.py +544 -0
- htmlgraph/event_log.py +100 -52
- htmlgraph/event_migration.py +13 -4
- htmlgraph/exceptions.py +49 -0
- htmlgraph/file_watcher.py +101 -28
- htmlgraph/find_api.py +75 -63
- htmlgraph/git_events.py +145 -63
- htmlgraph/graph.py +1122 -106
- htmlgraph/hooks/.htmlgraph/.session-warning-state.json +6 -0
- htmlgraph/hooks/.htmlgraph/agents.json +72 -0
- htmlgraph/hooks/.htmlgraph/index.sqlite +0 -0
- htmlgraph/hooks/__init__.py +45 -0
- htmlgraph/hooks/bootstrap.py +169 -0
- htmlgraph/hooks/cigs_pretool_enforcer.py +354 -0
- htmlgraph/hooks/concurrent_sessions.py +208 -0
- htmlgraph/hooks/context.py +350 -0
- htmlgraph/hooks/drift_handler.py +525 -0
- htmlgraph/hooks/event_tracker.py +1314 -0
- htmlgraph/hooks/git_commands.py +175 -0
- htmlgraph/hooks/hooks-config.example.json +12 -0
- htmlgraph/hooks/installer.py +343 -0
- htmlgraph/hooks/orchestrator.py +674 -0
- htmlgraph/hooks/orchestrator_reflector.py +223 -0
- htmlgraph/hooks/post-checkout.sh +28 -0
- htmlgraph/hooks/post-commit.sh +24 -0
- htmlgraph/hooks/post-merge.sh +26 -0
- htmlgraph/hooks/post_tool_use_failure.py +273 -0
- htmlgraph/hooks/post_tool_use_handler.py +257 -0
- htmlgraph/hooks/posttooluse.py +408 -0
- htmlgraph/hooks/pre-commit.sh +94 -0
- htmlgraph/hooks/pre-push.sh +28 -0
- htmlgraph/hooks/pretooluse.py +819 -0
- htmlgraph/hooks/prompt_analyzer.py +637 -0
- htmlgraph/hooks/session_handler.py +668 -0
- htmlgraph/hooks/session_summary.py +395 -0
- htmlgraph/hooks/state_manager.py +504 -0
- htmlgraph/hooks/subagent_detection.py +202 -0
- htmlgraph/hooks/subagent_stop.py +369 -0
- htmlgraph/hooks/task_enforcer.py +255 -0
- htmlgraph/hooks/task_validator.py +177 -0
- htmlgraph/hooks/validator.py +628 -0
- htmlgraph/ids.py +41 -27
- htmlgraph/index.d.ts +286 -0
- htmlgraph/learning.py +767 -0
- htmlgraph/mcp_server.py +69 -23
- htmlgraph/models.py +1586 -87
- htmlgraph/operations/README.md +62 -0
- htmlgraph/operations/__init__.py +79 -0
- htmlgraph/operations/analytics.py +339 -0
- htmlgraph/operations/bootstrap.py +289 -0
- htmlgraph/operations/events.py +244 -0
- htmlgraph/operations/fastapi_server.py +231 -0
- htmlgraph/operations/hooks.py +350 -0
- htmlgraph/operations/initialization.py +597 -0
- htmlgraph/operations/initialization.py.backup +228 -0
- htmlgraph/operations/server.py +303 -0
- htmlgraph/orchestration/__init__.py +58 -0
- htmlgraph/orchestration/claude_launcher.py +179 -0
- htmlgraph/orchestration/command_builder.py +72 -0
- htmlgraph/orchestration/headless_spawner.py +281 -0
- htmlgraph/orchestration/live_events.py +377 -0
- htmlgraph/orchestration/model_selection.py +327 -0
- htmlgraph/orchestration/plugin_manager.py +140 -0
- htmlgraph/orchestration/prompts.py +137 -0
- htmlgraph/orchestration/spawner_event_tracker.py +383 -0
- htmlgraph/orchestration/spawners/__init__.py +16 -0
- htmlgraph/orchestration/spawners/base.py +194 -0
- htmlgraph/orchestration/spawners/claude.py +173 -0
- htmlgraph/orchestration/spawners/codex.py +435 -0
- htmlgraph/orchestration/spawners/copilot.py +294 -0
- htmlgraph/orchestration/spawners/gemini.py +471 -0
- htmlgraph/orchestration/subprocess_runner.py +36 -0
- htmlgraph/orchestration/task_coordination.py +343 -0
- htmlgraph/orchestration.md +563 -0
- htmlgraph/orchestrator-system-prompt-optimized.txt +863 -0
- htmlgraph/orchestrator.py +669 -0
- htmlgraph/orchestrator_config.py +357 -0
- htmlgraph/orchestrator_mode.py +328 -0
- htmlgraph/orchestrator_validator.py +133 -0
- htmlgraph/parallel.py +646 -0
- htmlgraph/parser.py +160 -35
- htmlgraph/path_query.py +608 -0
- htmlgraph/pattern_matcher.py +636 -0
- htmlgraph/planning.py +147 -52
- htmlgraph/pydantic_models.py +476 -0
- htmlgraph/quality_gates.py +350 -0
- htmlgraph/query_builder.py +109 -72
- htmlgraph/query_composer.py +509 -0
- htmlgraph/reflection.py +443 -0
- htmlgraph/refs.py +344 -0
- htmlgraph/repo_hash.py +512 -0
- htmlgraph/repositories/__init__.py +292 -0
- htmlgraph/repositories/analytics_repository.py +455 -0
- htmlgraph/repositories/analytics_repository_standard.py +628 -0
- htmlgraph/repositories/feature_repository.py +581 -0
- htmlgraph/repositories/feature_repository_htmlfile.py +668 -0
- htmlgraph/repositories/feature_repository_memory.py +607 -0
- htmlgraph/repositories/feature_repository_sqlite.py +858 -0
- htmlgraph/repositories/filter_service.py +620 -0
- htmlgraph/repositories/filter_service_standard.py +445 -0
- htmlgraph/repositories/shared_cache.py +621 -0
- htmlgraph/repositories/shared_cache_memory.py +395 -0
- htmlgraph/repositories/track_repository.py +552 -0
- htmlgraph/repositories/track_repository_htmlfile.py +619 -0
- htmlgraph/repositories/track_repository_memory.py +508 -0
- htmlgraph/repositories/track_repository_sqlite.py +711 -0
- htmlgraph/routing.py +8 -19
- htmlgraph/scripts/deploy.py +1 -2
- htmlgraph/sdk/__init__.py +398 -0
- htmlgraph/sdk/__init__.pyi +14 -0
- htmlgraph/sdk/analytics/__init__.py +19 -0
- htmlgraph/sdk/analytics/engine.py +155 -0
- htmlgraph/sdk/analytics/helpers.py +178 -0
- htmlgraph/sdk/analytics/registry.py +109 -0
- htmlgraph/sdk/base.py +484 -0
- htmlgraph/sdk/constants.py +216 -0
- htmlgraph/sdk/core.pyi +308 -0
- htmlgraph/sdk/discovery.py +120 -0
- htmlgraph/sdk/help/__init__.py +12 -0
- htmlgraph/sdk/help/mixin.py +699 -0
- htmlgraph/sdk/mixins/__init__.py +15 -0
- htmlgraph/sdk/mixins/attribution.py +113 -0
- htmlgraph/sdk/mixins/mixin.py +410 -0
- htmlgraph/sdk/operations/__init__.py +12 -0
- htmlgraph/sdk/operations/mixin.py +427 -0
- htmlgraph/sdk/orchestration/__init__.py +17 -0
- htmlgraph/sdk/orchestration/coordinator.py +203 -0
- htmlgraph/sdk/orchestration/spawner.py +204 -0
- htmlgraph/sdk/planning/__init__.py +19 -0
- htmlgraph/sdk/planning/bottlenecks.py +93 -0
- htmlgraph/sdk/planning/mixin.py +211 -0
- htmlgraph/sdk/planning/parallel.py +186 -0
- htmlgraph/sdk/planning/queue.py +210 -0
- htmlgraph/sdk/planning/recommendations.py +87 -0
- htmlgraph/sdk/planning/smart_planning.py +319 -0
- htmlgraph/sdk/session/__init__.py +19 -0
- htmlgraph/sdk/session/continuity.py +57 -0
- htmlgraph/sdk/session/handoff.py +110 -0
- htmlgraph/sdk/session/info.py +309 -0
- htmlgraph/sdk/session/manager.py +103 -0
- htmlgraph/sdk/strategic/__init__.py +26 -0
- htmlgraph/sdk/strategic/mixin.py +563 -0
- htmlgraph/server.py +685 -180
- htmlgraph/services/__init__.py +10 -0
- htmlgraph/services/claiming.py +199 -0
- htmlgraph/session_hooks.py +300 -0
- htmlgraph/session_manager.py +1392 -175
- htmlgraph/session_registry.py +587 -0
- htmlgraph/session_state.py +436 -0
- htmlgraph/session_warning.py +201 -0
- htmlgraph/sessions/__init__.py +23 -0
- htmlgraph/sessions/handoff.py +756 -0
- htmlgraph/setup.py +34 -17
- htmlgraph/spike_index.py +143 -0
- htmlgraph/sync_docs.py +12 -15
- htmlgraph/system_prompts.py +450 -0
- htmlgraph/templates/AGENTS.md.template +366 -0
- htmlgraph/templates/CLAUDE.md.template +97 -0
- htmlgraph/templates/GEMINI.md.template +87 -0
- htmlgraph/templates/orchestration-view.html +350 -0
- htmlgraph/track_builder.py +146 -15
- htmlgraph/track_manager.py +69 -21
- htmlgraph/transcript.py +890 -0
- htmlgraph/transcript_analytics.py +699 -0
- htmlgraph/types.py +323 -0
- htmlgraph/validation.py +115 -0
- htmlgraph/watch.py +8 -5
- htmlgraph/work_type_utils.py +3 -2
- {htmlgraph-0.9.3.data → htmlgraph-0.27.5.data}/data/htmlgraph/dashboard.html +2406 -307
- htmlgraph-0.27.5.data/data/htmlgraph/templates/AGENTS.md.template +366 -0
- htmlgraph-0.27.5.data/data/htmlgraph/templates/CLAUDE.md.template +97 -0
- htmlgraph-0.27.5.data/data/htmlgraph/templates/GEMINI.md.template +87 -0
- {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/METADATA +97 -64
- htmlgraph-0.27.5.dist-info/RECORD +337 -0
- {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/entry_points.txt +1 -1
- htmlgraph/cli.py +0 -2688
- htmlgraph/sdk.py +0 -709
- htmlgraph-0.9.3.dist-info/RECORD +0 -61
- {htmlgraph-0.9.3.data → htmlgraph-0.27.5.data}/data/htmlgraph/styles.css +0 -0
- {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/WHEEL +0 -0
htmlgraph/decorators.py
ADDED
|
@@ -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
|
+
]
|
htmlgraph/dependency_models.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
"""
|
|
2
4
|
Data models for dependency analytics.
|
|
3
5
|
|
|
@@ -9,13 +11,15 @@ Provides Pydantic models for dependency-aware analytics results including:
|
|
|
9
11
|
- Work prioritization
|
|
10
12
|
"""
|
|
11
13
|
|
|
12
|
-
|
|
13
|
-
from pydantic import BaseModel, Field
|
|
14
|
+
|
|
14
15
|
from typing import Literal
|
|
15
16
|
|
|
17
|
+
from pydantic import BaseModel, Field
|
|
18
|
+
|
|
16
19
|
|
|
17
20
|
class CriticalPathNode(BaseModel):
|
|
18
21
|
"""Node on critical path with timing information."""
|
|
22
|
+
|
|
19
23
|
id: str
|
|
20
24
|
title: str
|
|
21
25
|
est: int = 0 # Earliest start time
|
|
@@ -27,6 +31,7 @@ class CriticalPathNode(BaseModel):
|
|
|
27
31
|
|
|
28
32
|
class CriticalPathResult(BaseModel):
|
|
29
33
|
"""Result of critical path analysis."""
|
|
34
|
+
|
|
30
35
|
path: list[str] = Field(default_factory=list) # Node IDs on critical path
|
|
31
36
|
length: int = 0 # Number of nodes
|
|
32
37
|
total_effort: float = 0.0 # Sum of effort estimates
|
|
@@ -36,6 +41,7 @@ class CriticalPathResult(BaseModel):
|
|
|
36
41
|
|
|
37
42
|
class BottleneckNode(BaseModel):
|
|
38
43
|
"""Node identified as a bottleneck."""
|
|
44
|
+
|
|
39
45
|
id: str
|
|
40
46
|
title: str
|
|
41
47
|
status: str
|
|
@@ -49,6 +55,7 @@ class BottleneckNode(BaseModel):
|
|
|
49
55
|
|
|
50
56
|
class ParallelLevel(BaseModel):
|
|
51
57
|
"""Group of nodes at same dependency level."""
|
|
58
|
+
|
|
52
59
|
level: int
|
|
53
60
|
nodes: list[str] = Field(default_factory=list)
|
|
54
61
|
max_parallel: int = 0
|
|
@@ -57,6 +64,7 @@ class ParallelLevel(BaseModel):
|
|
|
57
64
|
|
|
58
65
|
class ParallelizationReport(BaseModel):
|
|
59
66
|
"""Analysis of parallelization opportunities."""
|
|
67
|
+
|
|
60
68
|
max_parallelism: int = 0
|
|
61
69
|
dependency_levels: list[ParallelLevel] = Field(default_factory=list)
|
|
62
70
|
suggested_assignments: list[tuple[str, list[str]]] = Field(default_factory=list)
|
|
@@ -64,6 +72,7 @@ class ParallelizationReport(BaseModel):
|
|
|
64
72
|
|
|
65
73
|
class RiskFactor(BaseModel):
|
|
66
74
|
"""Individual risk factor for a node."""
|
|
75
|
+
|
|
67
76
|
type: Literal["spof", "deep_chain", "circular", "orphan"]
|
|
68
77
|
severity: Literal["low", "medium", "high", "critical"]
|
|
69
78
|
description: str
|
|
@@ -72,6 +81,7 @@ class RiskFactor(BaseModel):
|
|
|
72
81
|
|
|
73
82
|
class RiskNode(BaseModel):
|
|
74
83
|
"""Node with identified risks."""
|
|
84
|
+
|
|
75
85
|
id: str
|
|
76
86
|
title: str
|
|
77
87
|
risk_score: float = 0.0 # 0.0-1.0
|
|
@@ -80,6 +90,7 @@ class RiskNode(BaseModel):
|
|
|
80
90
|
|
|
81
91
|
class RiskAssessment(BaseModel):
|
|
82
92
|
"""Overall risk assessment."""
|
|
93
|
+
|
|
83
94
|
high_risk: list[RiskNode] = Field(default_factory=list)
|
|
84
95
|
circular_dependencies: list[list[str]] = Field(default_factory=list)
|
|
85
96
|
orphaned_nodes: list[str] = Field(default_factory=list)
|
|
@@ -88,6 +99,7 @@ class RiskAssessment(BaseModel):
|
|
|
88
99
|
|
|
89
100
|
class HealthIndicator(BaseModel):
|
|
90
101
|
"""Individual health metric indicator."""
|
|
102
|
+
|
|
91
103
|
metric: str
|
|
92
104
|
value: float
|
|
93
105
|
status: Literal["healthy", "warning", "critical"]
|
|
@@ -97,6 +109,7 @@ class HealthIndicator(BaseModel):
|
|
|
97
109
|
|
|
98
110
|
class HealthReport(BaseModel):
|
|
99
111
|
"""Overall dependency health."""
|
|
112
|
+
|
|
100
113
|
score: float = 0.0 # 0.0-1.0
|
|
101
114
|
metrics: dict[str, float] = Field(default_factory=dict)
|
|
102
115
|
health_indicators: list[HealthIndicator] = Field(default_factory=list)
|
|
@@ -104,6 +117,7 @@ class HealthReport(BaseModel):
|
|
|
104
117
|
|
|
105
118
|
class TaskRecommendation(BaseModel):
|
|
106
119
|
"""Recommended task to work on."""
|
|
120
|
+
|
|
107
121
|
id: str
|
|
108
122
|
title: str
|
|
109
123
|
priority: str
|
|
@@ -115,12 +129,14 @@ class TaskRecommendation(BaseModel):
|
|
|
115
129
|
|
|
116
130
|
class TaskRecommendations(BaseModel):
|
|
117
131
|
"""Set of task recommendations."""
|
|
132
|
+
|
|
118
133
|
recommendations: list[TaskRecommendation] = Field(default_factory=list)
|
|
119
134
|
parallel_suggestions: list[list[str]] = Field(default_factory=list)
|
|
120
135
|
|
|
121
136
|
|
|
122
137
|
class ImpactAnalysis(BaseModel):
|
|
123
138
|
"""Downstream impact analysis for a node."""
|
|
139
|
+
|
|
124
140
|
node_id: str
|
|
125
141
|
direct_dependents: int = 0
|
|
126
142
|
transitive_dependents: int = 0
|
|
@@ -130,6 +146,7 @@ class ImpactAnalysis(BaseModel):
|
|
|
130
146
|
|
|
131
147
|
class WhatIfResult(BaseModel):
|
|
132
148
|
"""Result of what-if completion simulation."""
|
|
149
|
+
|
|
133
150
|
completed_nodes: list[str] = Field(default_factory=list)
|
|
134
151
|
newly_available: list[str] = Field(default_factory=list)
|
|
135
152
|
total_unlocked: int = 0
|