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/query_builder.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
"""
|
|
2
4
|
Fluent Query Builder for HtmlGraph.
|
|
3
5
|
|
|
@@ -24,11 +26,12 @@ Example:
|
|
|
24
26
|
.execute()
|
|
25
27
|
"""
|
|
26
28
|
|
|
27
|
-
|
|
29
|
+
|
|
28
30
|
import re
|
|
31
|
+
from collections.abc import Callable, Iterator
|
|
29
32
|
from dataclasses import dataclass, field
|
|
30
33
|
from enum import Enum
|
|
31
|
-
from typing import TYPE_CHECKING, Any
|
|
34
|
+
from typing import TYPE_CHECKING, Any
|
|
32
35
|
|
|
33
36
|
if TYPE_CHECKING:
|
|
34
37
|
from htmlgraph.graph import HtmlGraph
|
|
@@ -37,25 +40,27 @@ if TYPE_CHECKING:
|
|
|
37
40
|
|
|
38
41
|
class Operator(Enum):
|
|
39
42
|
"""Query operators for comparisons."""
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
43
|
+
|
|
44
|
+
EQ = "eq" # Equal
|
|
45
|
+
NE = "ne" # Not equal
|
|
46
|
+
GT = "gt" # Greater than
|
|
47
|
+
GTE = "gte" # Greater than or equal
|
|
48
|
+
LT = "lt" # Less than
|
|
49
|
+
LTE = "lte" # Less than or equal
|
|
50
|
+
IN = "in" # In list
|
|
51
|
+
NOT_IN = "not_in" # Not in list
|
|
52
|
+
BETWEEN = "between" # Between two values (inclusive)
|
|
53
|
+
CONTAINS = "contains" # String contains
|
|
50
54
|
STARTS_WITH = "starts_with"
|
|
51
55
|
ENDS_WITH = "ends_with"
|
|
52
|
-
MATCHES = "matches"
|
|
56
|
+
MATCHES = "matches" # Regex match
|
|
53
57
|
IS_NULL = "is_null"
|
|
54
58
|
IS_NOT_NULL = "is_not_null"
|
|
55
59
|
|
|
56
60
|
|
|
57
61
|
class LogicalOp(Enum):
|
|
58
62
|
"""Logical operators for combining conditions."""
|
|
63
|
+
|
|
59
64
|
AND = "and"
|
|
60
65
|
OR = "or"
|
|
61
66
|
NOT = "not"
|
|
@@ -64,12 +69,13 @@ class LogicalOp(Enum):
|
|
|
64
69
|
@dataclass
|
|
65
70
|
class Condition:
|
|
66
71
|
"""A single query condition."""
|
|
72
|
+
|
|
67
73
|
attribute: str
|
|
68
74
|
operator: Operator
|
|
69
75
|
value: Any = None
|
|
70
76
|
logical_op: LogicalOp = LogicalOp.AND
|
|
71
77
|
|
|
72
|
-
def evaluate(self, node:
|
|
78
|
+
def evaluate(self, node: Node) -> bool:
|
|
73
79
|
"""Evaluate this condition against a node."""
|
|
74
80
|
# Get attribute value with nested access support
|
|
75
81
|
actual = _get_nested_attr(node, self.attribute)
|
|
@@ -86,9 +92,11 @@ class Condition:
|
|
|
86
92
|
|
|
87
93
|
# Evaluate based on operator
|
|
88
94
|
if self.operator == Operator.EQ:
|
|
89
|
-
|
|
95
|
+
result: bool = actual == self.value
|
|
96
|
+
return result
|
|
90
97
|
elif self.operator == Operator.NE:
|
|
91
|
-
|
|
98
|
+
result2: bool = actual != self.value
|
|
99
|
+
return result2
|
|
92
100
|
elif self.operator == Operator.GT:
|
|
93
101
|
return _compare_numeric(actual, self.value, lambda a, b: a > b)
|
|
94
102
|
elif self.operator == Operator.GTE:
|
|
@@ -103,8 +111,9 @@ class Condition:
|
|
|
103
111
|
return actual not in self.value
|
|
104
112
|
elif self.operator == Operator.BETWEEN:
|
|
105
113
|
low, high = self.value
|
|
106
|
-
return _compare_numeric(
|
|
107
|
-
|
|
114
|
+
return _compare_numeric(
|
|
115
|
+
actual, low, lambda a, b: a >= b
|
|
116
|
+
) and _compare_numeric(actual, high, lambda a, b: a <= b)
|
|
108
117
|
elif self.operator == Operator.CONTAINS:
|
|
109
118
|
return self.value.lower() in str(actual).lower()
|
|
110
119
|
elif self.operator == Operator.STARTS_WITH:
|
|
@@ -112,7 +121,11 @@ class Condition:
|
|
|
112
121
|
elif self.operator == Operator.ENDS_WITH:
|
|
113
122
|
return str(actual).lower().endswith(self.value.lower())
|
|
114
123
|
elif self.operator == Operator.MATCHES:
|
|
115
|
-
pattern =
|
|
124
|
+
pattern = (
|
|
125
|
+
self.value
|
|
126
|
+
if isinstance(self.value, re.Pattern)
|
|
127
|
+
else re.compile(self.value)
|
|
128
|
+
)
|
|
116
129
|
return bool(pattern.search(str(actual)))
|
|
117
130
|
|
|
118
131
|
return False
|
|
@@ -168,11 +181,15 @@ def _compare_numeric(actual: Any, expected: Any, comparator: Callable) -> bool:
|
|
|
168
181
|
try:
|
|
169
182
|
# Convert to float for numeric comparison
|
|
170
183
|
actual_num = float(actual) if not isinstance(actual, (int, float)) else actual
|
|
171
|
-
expected_num =
|
|
172
|
-
|
|
184
|
+
expected_num = (
|
|
185
|
+
float(expected) if not isinstance(expected, (int, float)) else expected
|
|
186
|
+
)
|
|
187
|
+
result: bool = comparator(actual_num, expected_num)
|
|
188
|
+
return result
|
|
173
189
|
except (ValueError, TypeError):
|
|
174
190
|
# Fall back to string comparison
|
|
175
|
-
|
|
191
|
+
result2: bool = comparator(str(actual), str(expected))
|
|
192
|
+
return result2
|
|
176
193
|
|
|
177
194
|
|
|
178
195
|
@dataclass
|
|
@@ -187,77 +204,77 @@ class ConditionBuilder:
|
|
|
187
204
|
query.where("completion").gt(50)
|
|
188
205
|
"""
|
|
189
206
|
|
|
190
|
-
_query_builder:
|
|
207
|
+
_query_builder: QueryBuilder
|
|
191
208
|
_attribute: str
|
|
192
209
|
_logical_op: LogicalOp = LogicalOp.AND
|
|
193
210
|
|
|
194
|
-
def eq(self, value: Any) ->
|
|
211
|
+
def eq(self, value: Any) -> QueryBuilder:
|
|
195
212
|
"""Equal to value."""
|
|
196
213
|
return self._add_condition(Operator.EQ, value)
|
|
197
214
|
|
|
198
|
-
def ne(self, value: Any) ->
|
|
215
|
+
def ne(self, value: Any) -> QueryBuilder:
|
|
199
216
|
"""Not equal to value."""
|
|
200
217
|
return self._add_condition(Operator.NE, value)
|
|
201
218
|
|
|
202
|
-
def gt(self, value: Any) ->
|
|
219
|
+
def gt(self, value: Any) -> QueryBuilder:
|
|
203
220
|
"""Greater than value."""
|
|
204
221
|
return self._add_condition(Operator.GT, value)
|
|
205
222
|
|
|
206
|
-
def gte(self, value: Any) ->
|
|
223
|
+
def gte(self, value: Any) -> QueryBuilder:
|
|
207
224
|
"""Greater than or equal to value."""
|
|
208
225
|
return self._add_condition(Operator.GTE, value)
|
|
209
226
|
|
|
210
|
-
def lt(self, value: Any) ->
|
|
227
|
+
def lt(self, value: Any) -> QueryBuilder:
|
|
211
228
|
"""Less than value."""
|
|
212
229
|
return self._add_condition(Operator.LT, value)
|
|
213
230
|
|
|
214
|
-
def lte(self, value: Any) ->
|
|
231
|
+
def lte(self, value: Any) -> QueryBuilder:
|
|
215
232
|
"""Less than or equal to value."""
|
|
216
233
|
return self._add_condition(Operator.LTE, value)
|
|
217
234
|
|
|
218
|
-
def in_(self, values: list) ->
|
|
235
|
+
def in_(self, values: list) -> QueryBuilder:
|
|
219
236
|
"""Value is in list."""
|
|
220
237
|
return self._add_condition(Operator.IN, values)
|
|
221
238
|
|
|
222
|
-
def not_in(self, values: list) ->
|
|
239
|
+
def not_in(self, values: list) -> QueryBuilder:
|
|
223
240
|
"""Value is not in list."""
|
|
224
241
|
return self._add_condition(Operator.NOT_IN, values)
|
|
225
242
|
|
|
226
|
-
def between(self, low: Any, high: Any) ->
|
|
243
|
+
def between(self, low: Any, high: Any) -> QueryBuilder:
|
|
227
244
|
"""Value is between low and high (inclusive)."""
|
|
228
245
|
return self._add_condition(Operator.BETWEEN, (low, high))
|
|
229
246
|
|
|
230
|
-
def contains(self, substring: str) ->
|
|
247
|
+
def contains(self, substring: str) -> QueryBuilder:
|
|
231
248
|
"""String contains substring (case-insensitive)."""
|
|
232
249
|
return self._add_condition(Operator.CONTAINS, substring)
|
|
233
250
|
|
|
234
|
-
def starts_with(self, prefix: str) ->
|
|
251
|
+
def starts_with(self, prefix: str) -> QueryBuilder:
|
|
235
252
|
"""String starts with prefix (case-insensitive)."""
|
|
236
253
|
return self._add_condition(Operator.STARTS_WITH, prefix)
|
|
237
254
|
|
|
238
|
-
def ends_with(self, suffix: str) ->
|
|
255
|
+
def ends_with(self, suffix: str) -> QueryBuilder:
|
|
239
256
|
"""String ends with suffix (case-insensitive)."""
|
|
240
257
|
return self._add_condition(Operator.ENDS_WITH, suffix)
|
|
241
258
|
|
|
242
|
-
def matches(self, pattern: str | re.Pattern) ->
|
|
259
|
+
def matches(self, pattern: str | re.Pattern) -> QueryBuilder:
|
|
243
260
|
"""String matches regex pattern."""
|
|
244
261
|
return self._add_condition(Operator.MATCHES, pattern)
|
|
245
262
|
|
|
246
|
-
def is_null(self) ->
|
|
263
|
+
def is_null(self) -> QueryBuilder:
|
|
247
264
|
"""Attribute is None/null."""
|
|
248
265
|
return self._add_condition(Operator.IS_NULL, None)
|
|
249
266
|
|
|
250
|
-
def is_not_null(self) ->
|
|
267
|
+
def is_not_null(self) -> QueryBuilder:
|
|
251
268
|
"""Attribute is not None/null."""
|
|
252
269
|
return self._add_condition(Operator.IS_NOT_NULL, None)
|
|
253
270
|
|
|
254
|
-
def _add_condition(self, operator: Operator, value: Any) ->
|
|
271
|
+
def _add_condition(self, operator: Operator, value: Any) -> QueryBuilder:
|
|
255
272
|
"""Add condition and return query builder for chaining."""
|
|
256
273
|
condition = Condition(
|
|
257
274
|
attribute=self._attribute,
|
|
258
275
|
operator=operator,
|
|
259
276
|
value=value,
|
|
260
|
-
logical_op=self._logical_op
|
|
277
|
+
logical_op=self._logical_op,
|
|
261
278
|
)
|
|
262
279
|
self._query_builder._conditions.append(condition)
|
|
263
280
|
return self._query_builder
|
|
@@ -282,13 +299,15 @@ class QueryBuilder:
|
|
|
282
299
|
.execute()
|
|
283
300
|
"""
|
|
284
301
|
|
|
285
|
-
_graph:
|
|
302
|
+
_graph: HtmlGraph
|
|
286
303
|
_conditions: list[Condition] = field(default_factory=list)
|
|
287
304
|
_type_filter: str | None = None
|
|
288
305
|
_limit: int | None = None
|
|
289
306
|
_offset: int = 0
|
|
290
307
|
|
|
291
|
-
def where(
|
|
308
|
+
def where(
|
|
309
|
+
self, attribute: str, value: Any = None
|
|
310
|
+
) -> QueryBuilder | ConditionBuilder:
|
|
292
311
|
"""
|
|
293
312
|
Start a query condition.
|
|
294
313
|
|
|
@@ -309,19 +328,19 @@ class QueryBuilder:
|
|
|
309
328
|
attribute=attribute,
|
|
310
329
|
operator=Operator.EQ,
|
|
311
330
|
value=value,
|
|
312
|
-
logical_op=LogicalOp.AND
|
|
331
|
+
logical_op=LogicalOp.AND,
|
|
313
332
|
)
|
|
314
333
|
self._conditions.append(condition)
|
|
315
334
|
return self
|
|
316
335
|
else:
|
|
317
336
|
# Return condition builder for fluent operator
|
|
318
337
|
return ConditionBuilder(
|
|
319
|
-
_query_builder=self,
|
|
320
|
-
_attribute=attribute,
|
|
321
|
-
_logical_op=LogicalOp.AND
|
|
338
|
+
_query_builder=self, _attribute=attribute, _logical_op=LogicalOp.AND
|
|
322
339
|
)
|
|
323
340
|
|
|
324
|
-
def and_(
|
|
341
|
+
def and_(
|
|
342
|
+
self, attribute: str, value: Any = None
|
|
343
|
+
) -> QueryBuilder | ConditionBuilder:
|
|
325
344
|
"""
|
|
326
345
|
Add an AND condition.
|
|
327
346
|
|
|
@@ -337,18 +356,16 @@ class QueryBuilder:
|
|
|
337
356
|
attribute=attribute,
|
|
338
357
|
operator=Operator.EQ,
|
|
339
358
|
value=value,
|
|
340
|
-
logical_op=LogicalOp.AND
|
|
359
|
+
logical_op=LogicalOp.AND,
|
|
341
360
|
)
|
|
342
361
|
self._conditions.append(condition)
|
|
343
362
|
return self
|
|
344
363
|
else:
|
|
345
364
|
return ConditionBuilder(
|
|
346
|
-
_query_builder=self,
|
|
347
|
-
_attribute=attribute,
|
|
348
|
-
_logical_op=LogicalOp.AND
|
|
365
|
+
_query_builder=self, _attribute=attribute, _logical_op=LogicalOp.AND
|
|
349
366
|
)
|
|
350
367
|
|
|
351
|
-
def or_(self, attribute: str, value: Any = None) ->
|
|
368
|
+
def or_(self, attribute: str, value: Any = None) -> QueryBuilder | ConditionBuilder:
|
|
352
369
|
"""
|
|
353
370
|
Add an OR condition.
|
|
354
371
|
|
|
@@ -364,18 +381,16 @@ class QueryBuilder:
|
|
|
364
381
|
attribute=attribute,
|
|
365
382
|
operator=Operator.EQ,
|
|
366
383
|
value=value,
|
|
367
|
-
logical_op=LogicalOp.OR
|
|
384
|
+
logical_op=LogicalOp.OR,
|
|
368
385
|
)
|
|
369
386
|
self._conditions.append(condition)
|
|
370
387
|
return self
|
|
371
388
|
else:
|
|
372
389
|
return ConditionBuilder(
|
|
373
|
-
_query_builder=self,
|
|
374
|
-
_attribute=attribute,
|
|
375
|
-
_logical_op=LogicalOp.OR
|
|
390
|
+
_query_builder=self, _attribute=attribute, _logical_op=LogicalOp.OR
|
|
376
391
|
)
|
|
377
392
|
|
|
378
|
-
def not_(self, attribute: str) ->
|
|
393
|
+
def not_(self, attribute: str) -> ConditionBuilder:
|
|
379
394
|
"""
|
|
380
395
|
Add a NOT condition.
|
|
381
396
|
|
|
@@ -389,12 +404,10 @@ class QueryBuilder:
|
|
|
389
404
|
ConditionBuilder for specifying the condition
|
|
390
405
|
"""
|
|
391
406
|
return ConditionBuilder(
|
|
392
|
-
_query_builder=self,
|
|
393
|
-
_attribute=attribute,
|
|
394
|
-
_logical_op=LogicalOp.NOT
|
|
407
|
+
_query_builder=self, _attribute=attribute, _logical_op=LogicalOp.NOT
|
|
395
408
|
)
|
|
396
409
|
|
|
397
|
-
def of_type(self, node_type: str) ->
|
|
410
|
+
def of_type(self, node_type: str) -> QueryBuilder:
|
|
398
411
|
"""
|
|
399
412
|
Filter by node type.
|
|
400
413
|
|
|
@@ -407,7 +420,7 @@ class QueryBuilder:
|
|
|
407
420
|
self._type_filter = node_type
|
|
408
421
|
return self
|
|
409
422
|
|
|
410
|
-
def limit(self, count: int) ->
|
|
423
|
+
def limit(self, count: int) -> QueryBuilder:
|
|
411
424
|
"""
|
|
412
425
|
Limit number of results.
|
|
413
426
|
|
|
@@ -420,7 +433,7 @@ class QueryBuilder:
|
|
|
420
433
|
self._limit = count
|
|
421
434
|
return self
|
|
422
435
|
|
|
423
|
-
def offset(self, skip: int) ->
|
|
436
|
+
def offset(self, skip: int) -> QueryBuilder:
|
|
424
437
|
"""
|
|
425
438
|
Skip first N results.
|
|
426
439
|
|
|
@@ -433,7 +446,7 @@ class QueryBuilder:
|
|
|
433
446
|
self._offset = skip
|
|
434
447
|
return self
|
|
435
448
|
|
|
436
|
-
def execute(self) -> list[
|
|
449
|
+
def execute(self) -> list[Node]:
|
|
437
450
|
"""
|
|
438
451
|
Execute the query and return matching nodes.
|
|
439
452
|
|
|
@@ -453,13 +466,13 @@ class QueryBuilder:
|
|
|
453
466
|
|
|
454
467
|
# Apply offset and limit
|
|
455
468
|
if self._offset:
|
|
456
|
-
results = results[self._offset:]
|
|
469
|
+
results = results[self._offset :]
|
|
457
470
|
if self._limit:
|
|
458
|
-
results = results[:self._limit]
|
|
471
|
+
results = results[: self._limit]
|
|
459
472
|
|
|
460
473
|
return results
|
|
461
474
|
|
|
462
|
-
def first(self) ->
|
|
475
|
+
def first(self) -> Node | None:
|
|
463
476
|
"""
|
|
464
477
|
Execute query and return first matching node.
|
|
465
478
|
|
|
@@ -493,7 +506,7 @@ class QueryBuilder:
|
|
|
493
506
|
"""
|
|
494
507
|
return self.first() is not None
|
|
495
508
|
|
|
496
|
-
def _evaluate_conditions(self, node:
|
|
509
|
+
def _evaluate_conditions(self, node: Node) -> bool:
|
|
497
510
|
"""
|
|
498
511
|
Evaluate all conditions against a node.
|
|
499
512
|
|
|
@@ -533,11 +546,34 @@ class QueryBuilder:
|
|
|
533
546
|
|
|
534
547
|
return result if result is not None else True
|
|
535
548
|
|
|
536
|
-
def __iter__(self) -> Iterator[
|
|
537
|
-
"""
|
|
549
|
+
def __iter__(self) -> Iterator[Node]:
|
|
550
|
+
"""
|
|
551
|
+
Iterate over query results.
|
|
552
|
+
|
|
553
|
+
Enables using QueryBuilder directly in for loops without calling execute().
|
|
554
|
+
This provides a more Pythonic interface similar to Django ORM or SQLAlchemy.
|
|
555
|
+
|
|
556
|
+
Yields:
|
|
557
|
+
Node: Each node matching the query
|
|
558
|
+
|
|
559
|
+
Example:
|
|
560
|
+
>>> # Instead of: for node in graph.query_builder().execute()
|
|
561
|
+
>>> # You can do:
|
|
562
|
+
>>> query = graph.query_builder().where("status", "todo").and_("priority", "high")
|
|
563
|
+
>>> for node in query:
|
|
564
|
+
... print(f"{node.id}: {node.title}")
|
|
565
|
+
feat-001: User Authentication
|
|
566
|
+
feat-002: Database Migration
|
|
567
|
+
|
|
568
|
+
>>> # Works with comprehensions
|
|
569
|
+
>>> titles = [n.title for n in query]
|
|
570
|
+
>>>
|
|
571
|
+
>>> # Works with any iterable operation
|
|
572
|
+
>>> first = next(iter(query), None)
|
|
573
|
+
"""
|
|
538
574
|
return iter(self.execute())
|
|
539
575
|
|
|
540
|
-
def to_predicate(self) -> Callable[[
|
|
576
|
+
def to_predicate(self) -> Callable[[Node], bool]:
|
|
541
577
|
"""
|
|
542
578
|
Convert query to a predicate function.
|
|
543
579
|
|
|
@@ -546,7 +582,8 @@ class QueryBuilder:
|
|
|
546
582
|
Returns:
|
|
547
583
|
Function that takes a Node and returns bool
|
|
548
584
|
"""
|
|
549
|
-
|
|
585
|
+
|
|
586
|
+
def predicate(node: Node) -> bool:
|
|
550
587
|
if self._type_filter and node.type != self._type_filter:
|
|
551
588
|
return False
|
|
552
589
|
return self._evaluate_conditions(node)
|