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
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
"""
|
|
2
|
+
StandardFilterService - Unified filtering with pre-compilation and optimization.
|
|
3
|
+
|
|
4
|
+
Stateless service providing filter creation, composition, and application.
|
|
5
|
+
Thread-safe with compiled filter caching for performance.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from collections.abc import Callable
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from .filter_service import (
|
|
13
|
+
Filter,
|
|
14
|
+
FilterLogic,
|
|
15
|
+
FilterOperator,
|
|
16
|
+
FilterService,
|
|
17
|
+
InvalidFilterError,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
# Priority mapping for comparison
|
|
21
|
+
PRIORITY_MAP = {
|
|
22
|
+
"low": 1,
|
|
23
|
+
"medium": 2,
|
|
24
|
+
"high": 3,
|
|
25
|
+
"critical": 4,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class StandardFilterService(FilterService):
|
|
30
|
+
"""
|
|
31
|
+
Standard implementation of FilterService.
|
|
32
|
+
|
|
33
|
+
Features:
|
|
34
|
+
- Stateless operation (thread-safe)
|
|
35
|
+
- Pre-compilation of filters for performance
|
|
36
|
+
- Support for all standard operators
|
|
37
|
+
- Boolean combination (AND/OR/NOT)
|
|
38
|
+
- Custom predicate support
|
|
39
|
+
|
|
40
|
+
Performance:
|
|
41
|
+
- create_filter(): O(1)
|
|
42
|
+
- compile(): O(1) with caching
|
|
43
|
+
- apply(): O(n) where n = items
|
|
44
|
+
- apply_compiled(): O(n) with minimal overhead
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(self) -> None:
|
|
48
|
+
"""Initialize filter service."""
|
|
49
|
+
self._compiled_cache: dict[str, Callable[[Any], bool]] = {}
|
|
50
|
+
|
|
51
|
+
# ===== ATOMIC FILTER CREATION =====
|
|
52
|
+
|
|
53
|
+
def create_filter(
|
|
54
|
+
self, field: str, operator: FilterOperator | str, value: Any
|
|
55
|
+
) -> Filter:
|
|
56
|
+
"""Create atomic filter for single field."""
|
|
57
|
+
# Convert string operator to FilterOperator
|
|
58
|
+
if isinstance(operator, str):
|
|
59
|
+
try:
|
|
60
|
+
operator = FilterOperator(operator)
|
|
61
|
+
except ValueError:
|
|
62
|
+
raise InvalidFilterError(f"Invalid operator: {operator}")
|
|
63
|
+
|
|
64
|
+
# Validate operator
|
|
65
|
+
if not isinstance(operator, FilterOperator):
|
|
66
|
+
raise InvalidFilterError("Operator must be FilterOperator or string")
|
|
67
|
+
|
|
68
|
+
return Filter(
|
|
69
|
+
field=field,
|
|
70
|
+
operator=operator,
|
|
71
|
+
value=value,
|
|
72
|
+
predicate=None,
|
|
73
|
+
logic=None,
|
|
74
|
+
sub_filters=None,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
def custom(self, predicate: Callable[[Any], bool]) -> Filter:
|
|
78
|
+
"""Create custom filter with arbitrary predicate."""
|
|
79
|
+
return Filter(
|
|
80
|
+
field="",
|
|
81
|
+
operator=FilterOperator.EQUALS,
|
|
82
|
+
value=None,
|
|
83
|
+
predicate=predicate,
|
|
84
|
+
logic=None,
|
|
85
|
+
sub_filters=None,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# ===== STANDARD FILTERS (COMMON PATTERNS) =====
|
|
89
|
+
|
|
90
|
+
def status_is(self, status: str) -> Filter:
|
|
91
|
+
"""Filter by exact status match."""
|
|
92
|
+
return self.create_filter("status", FilterOperator.EQUALS, status)
|
|
93
|
+
|
|
94
|
+
def priority_gte(self, priority: str) -> Filter:
|
|
95
|
+
"""Filter by priority >= threshold."""
|
|
96
|
+
priority_value = PRIORITY_MAP.get(priority.lower(), 0)
|
|
97
|
+
return Filter(
|
|
98
|
+
field="priority",
|
|
99
|
+
operator=FilterOperator.GREATER_EQUAL,
|
|
100
|
+
value=priority_value,
|
|
101
|
+
predicate=lambda item: PRIORITY_MAP.get(
|
|
102
|
+
getattr(item, "priority", "low").lower(), 0
|
|
103
|
+
)
|
|
104
|
+
>= priority_value,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
def priority_lte(self, priority: str) -> Filter:
|
|
108
|
+
"""Filter by priority <= threshold."""
|
|
109
|
+
priority_value = PRIORITY_MAP.get(priority.lower(), 0)
|
|
110
|
+
return Filter(
|
|
111
|
+
field="priority",
|
|
112
|
+
operator=FilterOperator.LESS_EQUAL,
|
|
113
|
+
value=priority_value,
|
|
114
|
+
predicate=lambda item: PRIORITY_MAP.get(
|
|
115
|
+
getattr(item, "priority", "low").lower(), 0
|
|
116
|
+
)
|
|
117
|
+
<= priority_value,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
def assigned_to(self, agent: str) -> Filter:
|
|
121
|
+
"""Filter by assignment to specific agent."""
|
|
122
|
+
return self.create_filter("assigned_to", FilterOperator.EQUALS, agent)
|
|
123
|
+
|
|
124
|
+
def created_after(self, date: datetime) -> Filter:
|
|
125
|
+
"""Filter by creation date after threshold."""
|
|
126
|
+
return self.create_filter("created_at", FilterOperator.GREATER_THAN, date)
|
|
127
|
+
|
|
128
|
+
def created_before(self, date: datetime) -> Filter:
|
|
129
|
+
"""Filter by creation date before threshold."""
|
|
130
|
+
return self.create_filter("created_at", FilterOperator.LESS_THAN, date)
|
|
131
|
+
|
|
132
|
+
def updated_after(self, date: datetime) -> Filter:
|
|
133
|
+
"""Filter by last update after threshold."""
|
|
134
|
+
return self.create_filter("updated_at", FilterOperator.GREATER_THAN, date)
|
|
135
|
+
|
|
136
|
+
def updated_before(self, date: datetime) -> Filter:
|
|
137
|
+
"""Filter by last update before threshold."""
|
|
138
|
+
return self.create_filter("updated_at", FilterOperator.LESS_THAN, date)
|
|
139
|
+
|
|
140
|
+
def any_of(self, field: str, values: list[Any]) -> Filter:
|
|
141
|
+
"""Filter where field value is in set (IN operator)."""
|
|
142
|
+
return self.create_filter(field, FilterOperator.IN, values)
|
|
143
|
+
|
|
144
|
+
def none_of(self, field: str, values: list[Any]) -> Filter:
|
|
145
|
+
"""Filter where field value is NOT in set (NOT IN operator)."""
|
|
146
|
+
return self.create_filter(field, FilterOperator.NOT_IN, values)
|
|
147
|
+
|
|
148
|
+
def text_contains(self, field: str, text: str) -> Filter:
|
|
149
|
+
"""Filter where text field contains substring."""
|
|
150
|
+
return self.create_filter(field, FilterOperator.CONTAINS, text)
|
|
151
|
+
|
|
152
|
+
def text_starts_with(self, field: str, prefix: str) -> Filter:
|
|
153
|
+
"""Filter where text field starts with prefix."""
|
|
154
|
+
return self.create_filter(field, FilterOperator.STARTS_WITH, prefix)
|
|
155
|
+
|
|
156
|
+
def range(
|
|
157
|
+
self, field: str, min_value: Any | None = None, max_value: Any | None = None
|
|
158
|
+
) -> Filter:
|
|
159
|
+
"""Filter where numeric field is in range."""
|
|
160
|
+
filters = []
|
|
161
|
+
|
|
162
|
+
if min_value is not None:
|
|
163
|
+
filters.append(
|
|
164
|
+
self.create_filter(field, FilterOperator.GREATER_EQUAL, min_value)
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
if max_value is not None:
|
|
168
|
+
filters.append(
|
|
169
|
+
self.create_filter(field, FilterOperator.LESS_EQUAL, max_value)
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
if not filters:
|
|
173
|
+
raise InvalidFilterError("range() requires at least min_value or max_value")
|
|
174
|
+
|
|
175
|
+
if len(filters) == 1:
|
|
176
|
+
return filters[0]
|
|
177
|
+
|
|
178
|
+
return self.combine(filters, FilterLogic.AND)
|
|
179
|
+
|
|
180
|
+
# ===== FILTER COMPOSITION =====
|
|
181
|
+
|
|
182
|
+
def combine(
|
|
183
|
+
self, filters: list[Filter], logic: FilterLogic | str = FilterLogic.AND
|
|
184
|
+
) -> Filter:
|
|
185
|
+
"""Combine multiple filters with boolean logic."""
|
|
186
|
+
if not filters:
|
|
187
|
+
raise InvalidFilterError("combine() requires at least one filter")
|
|
188
|
+
|
|
189
|
+
# Convert string logic to FilterLogic
|
|
190
|
+
if isinstance(logic, str):
|
|
191
|
+
try:
|
|
192
|
+
logic = FilterLogic(logic.lower())
|
|
193
|
+
except ValueError:
|
|
194
|
+
raise InvalidFilterError(f"Invalid logic: {logic}")
|
|
195
|
+
|
|
196
|
+
if len(filters) == 1:
|
|
197
|
+
return filters[0]
|
|
198
|
+
|
|
199
|
+
return Filter(
|
|
200
|
+
field="",
|
|
201
|
+
operator=FilterOperator.EQUALS,
|
|
202
|
+
value=None,
|
|
203
|
+
predicate=None,
|
|
204
|
+
logic=logic,
|
|
205
|
+
sub_filters=filters,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
def all_of(self, *filters: Filter) -> Filter:
|
|
209
|
+
"""Shorthand for combine(filters, AND)."""
|
|
210
|
+
return self.combine(list(filters), FilterLogic.AND)
|
|
211
|
+
|
|
212
|
+
def any(self, *filters: Filter) -> Filter:
|
|
213
|
+
"""Shorthand for combine(filters, OR)."""
|
|
214
|
+
return self.combine(list(filters), FilterLogic.OR)
|
|
215
|
+
|
|
216
|
+
def not_filter(self, filter: Filter) -> Filter:
|
|
217
|
+
"""Negate a filter (logical NOT)."""
|
|
218
|
+
return Filter(
|
|
219
|
+
field="",
|
|
220
|
+
operator=FilterOperator.EQUALS,
|
|
221
|
+
value=None,
|
|
222
|
+
predicate=None,
|
|
223
|
+
logic=FilterLogic.NOT,
|
|
224
|
+
sub_filters=[filter],
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
# ===== FILTER VALIDATION & COMPILATION =====
|
|
228
|
+
|
|
229
|
+
def validate(self, filter: Filter) -> bool:
|
|
230
|
+
"""Validate filter is well-formed and applicable."""
|
|
231
|
+
try:
|
|
232
|
+
# Custom predicate filters always valid if predicate exists
|
|
233
|
+
if filter.is_custom:
|
|
234
|
+
return filter.predicate is not None
|
|
235
|
+
|
|
236
|
+
# Compound filters
|
|
237
|
+
if filter.is_compound:
|
|
238
|
+
if filter.logic is None or filter.sub_filters is None:
|
|
239
|
+
return False
|
|
240
|
+
# Recursively validate sub-filters
|
|
241
|
+
return all(self.validate(f) for f in filter.sub_filters)
|
|
242
|
+
|
|
243
|
+
# Atomic filters
|
|
244
|
+
if not filter.field:
|
|
245
|
+
return False
|
|
246
|
+
|
|
247
|
+
if not isinstance(filter.operator, FilterOperator):
|
|
248
|
+
return False
|
|
249
|
+
|
|
250
|
+
return True
|
|
251
|
+
|
|
252
|
+
except Exception:
|
|
253
|
+
return False
|
|
254
|
+
|
|
255
|
+
def compile(self, filter: Filter) -> Callable[[Any], bool]:
|
|
256
|
+
"""Pre-compile filter to fast callable."""
|
|
257
|
+
if not self.validate(filter):
|
|
258
|
+
raise InvalidFilterError("Invalid filter cannot be compiled")
|
|
259
|
+
|
|
260
|
+
# Generate cache key
|
|
261
|
+
cache_key = self._filter_cache_key(filter)
|
|
262
|
+
|
|
263
|
+
# Check cache
|
|
264
|
+
if cache_key in self._compiled_cache:
|
|
265
|
+
return self._compiled_cache[cache_key]
|
|
266
|
+
|
|
267
|
+
# Compile filter
|
|
268
|
+
compiled = self._compile_filter(filter)
|
|
269
|
+
|
|
270
|
+
# Cache result
|
|
271
|
+
self._compiled_cache[cache_key] = compiled
|
|
272
|
+
|
|
273
|
+
return compiled
|
|
274
|
+
|
|
275
|
+
def _filter_cache_key(self, filter: Filter) -> str:
|
|
276
|
+
"""Generate cache key for filter."""
|
|
277
|
+
if filter.is_custom:
|
|
278
|
+
return f"custom:{id(filter.predicate)}"
|
|
279
|
+
|
|
280
|
+
if (
|
|
281
|
+
filter.is_compound
|
|
282
|
+
and filter.sub_filters is not None
|
|
283
|
+
and filter.logic is not None
|
|
284
|
+
):
|
|
285
|
+
sub_keys = [self._filter_cache_key(f) for f in filter.sub_filters]
|
|
286
|
+
return f"{filter.logic.value}:({','.join(sub_keys)})"
|
|
287
|
+
|
|
288
|
+
op_value = (
|
|
289
|
+
filter.operator.value
|
|
290
|
+
if isinstance(filter.operator, FilterOperator)
|
|
291
|
+
else filter.operator
|
|
292
|
+
)
|
|
293
|
+
return f"{filter.field}:{op_value}:{filter.value}"
|
|
294
|
+
|
|
295
|
+
def _compile_filter(self, filter: Filter) -> Callable[[Any], bool]:
|
|
296
|
+
"""Internal: compile filter to callable."""
|
|
297
|
+
# Custom predicate
|
|
298
|
+
if filter.is_custom and filter.predicate is not None:
|
|
299
|
+
return filter.predicate
|
|
300
|
+
|
|
301
|
+
# Compound filters
|
|
302
|
+
if filter.is_compound and filter.sub_filters is not None:
|
|
303
|
+
compiled_subs = [self.compile(f) for f in filter.sub_filters]
|
|
304
|
+
|
|
305
|
+
if filter.logic == FilterLogic.AND:
|
|
306
|
+
return lambda item: all(f(item) for f in compiled_subs)
|
|
307
|
+
elif filter.logic == FilterLogic.OR:
|
|
308
|
+
return lambda item: any(f(item) for f in compiled_subs)
|
|
309
|
+
elif filter.logic == FilterLogic.NOT:
|
|
310
|
+
return lambda item: not compiled_subs[0](item)
|
|
311
|
+
|
|
312
|
+
# Atomic filters
|
|
313
|
+
field = filter.field
|
|
314
|
+
operator = filter.operator
|
|
315
|
+
value = filter.value
|
|
316
|
+
|
|
317
|
+
def apply_filter(item: Any) -> bool:
|
|
318
|
+
try:
|
|
319
|
+
item_value = getattr(item, field, None)
|
|
320
|
+
|
|
321
|
+
if operator == FilterOperator.EQUALS:
|
|
322
|
+
return bool(item_value == value)
|
|
323
|
+
elif operator == FilterOperator.NOT_EQUALS:
|
|
324
|
+
return bool(item_value != value)
|
|
325
|
+
elif operator == FilterOperator.GREATER_THAN:
|
|
326
|
+
return bool(item_value > value)
|
|
327
|
+
elif operator == FilterOperator.GREATER_EQUAL:
|
|
328
|
+
return bool(item_value >= value)
|
|
329
|
+
elif operator == FilterOperator.LESS_THAN:
|
|
330
|
+
return bool(item_value < value)
|
|
331
|
+
elif operator == FilterOperator.LESS_EQUAL:
|
|
332
|
+
return bool(item_value <= value)
|
|
333
|
+
elif operator == FilterOperator.IN:
|
|
334
|
+
return bool(item_value in value)
|
|
335
|
+
elif operator == FilterOperator.NOT_IN:
|
|
336
|
+
return bool(item_value not in value)
|
|
337
|
+
elif operator == FilterOperator.CONTAINS:
|
|
338
|
+
return bool(value in str(item_value))
|
|
339
|
+
elif operator == FilterOperator.STARTS_WITH:
|
|
340
|
+
return bool(str(item_value).startswith(value))
|
|
341
|
+
elif operator == FilterOperator.ENDS_WITH:
|
|
342
|
+
return bool(str(item_value).endswith(value))
|
|
343
|
+
|
|
344
|
+
return False
|
|
345
|
+
|
|
346
|
+
except (AttributeError, TypeError, ValueError):
|
|
347
|
+
return False
|
|
348
|
+
|
|
349
|
+
return apply_filter
|
|
350
|
+
|
|
351
|
+
# ===== FILTER APPLICATION =====
|
|
352
|
+
|
|
353
|
+
def apply(
|
|
354
|
+
self, items: list[Any], filter: Filter, limit: int | None = None
|
|
355
|
+
) -> list[Any]:
|
|
356
|
+
"""Apply filter to item list."""
|
|
357
|
+
compiled = self.compile(filter)
|
|
358
|
+
return self.apply_compiled(items, compiled, limit)
|
|
359
|
+
|
|
360
|
+
def apply_compiled(
|
|
361
|
+
self,
|
|
362
|
+
items: list[Any],
|
|
363
|
+
compiled_filter: Callable[[Any], bool],
|
|
364
|
+
limit: int | None = None,
|
|
365
|
+
) -> list[Any]:
|
|
366
|
+
"""Apply pre-compiled filter to items."""
|
|
367
|
+
result = []
|
|
368
|
+
|
|
369
|
+
for item in items:
|
|
370
|
+
if compiled_filter(item):
|
|
371
|
+
result.append(item)
|
|
372
|
+
|
|
373
|
+
# Early termination if limit reached
|
|
374
|
+
if limit is not None and len(result) >= limit:
|
|
375
|
+
break
|
|
376
|
+
|
|
377
|
+
return result
|
|
378
|
+
|
|
379
|
+
def filter_count(self, items: list[Any], filter: Filter) -> int:
|
|
380
|
+
"""Count items matching filter without materializing list."""
|
|
381
|
+
compiled = self.compile(filter)
|
|
382
|
+
count = 0
|
|
383
|
+
|
|
384
|
+
for item in items:
|
|
385
|
+
if compiled(item):
|
|
386
|
+
count += 1
|
|
387
|
+
|
|
388
|
+
return count
|
|
389
|
+
|
|
390
|
+
# ===== UTILITY METHODS =====
|
|
391
|
+
|
|
392
|
+
def describe(self, filter: Filter) -> str:
|
|
393
|
+
"""Get human-readable description of filter."""
|
|
394
|
+
if filter.is_custom:
|
|
395
|
+
return "custom predicate"
|
|
396
|
+
|
|
397
|
+
if filter.is_compound and filter.sub_filters is not None:
|
|
398
|
+
sub_descriptions = [self.describe(f) for f in filter.sub_filters]
|
|
399
|
+
|
|
400
|
+
if filter.logic == FilterLogic.AND:
|
|
401
|
+
return f"({' AND '.join(sub_descriptions)})"
|
|
402
|
+
elif filter.logic == FilterLogic.OR:
|
|
403
|
+
return f"({' OR '.join(sub_descriptions)})"
|
|
404
|
+
elif filter.logic == FilterLogic.NOT:
|
|
405
|
+
return f"NOT ({sub_descriptions[0]})"
|
|
406
|
+
|
|
407
|
+
# Atomic filter
|
|
408
|
+
op_map: dict[FilterOperator, str] = {
|
|
409
|
+
FilterOperator.EQUALS: "is",
|
|
410
|
+
FilterOperator.NOT_EQUALS: "is not",
|
|
411
|
+
FilterOperator.GREATER_THAN: ">",
|
|
412
|
+
FilterOperator.GREATER_EQUAL: ">=",
|
|
413
|
+
FilterOperator.LESS_THAN: "<",
|
|
414
|
+
FilterOperator.LESS_EQUAL: "<=",
|
|
415
|
+
FilterOperator.IN: "in",
|
|
416
|
+
FilterOperator.NOT_IN: "not in",
|
|
417
|
+
FilterOperator.CONTAINS: "contains",
|
|
418
|
+
FilterOperator.STARTS_WITH: "starts with",
|
|
419
|
+
FilterOperator.ENDS_WITH: "ends with",
|
|
420
|
+
}
|
|
421
|
+
op_str = (
|
|
422
|
+
op_map.get(filter.operator, str(filter.operator))
|
|
423
|
+
if isinstance(filter.operator, FilterOperator)
|
|
424
|
+
else str(filter.operator)
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
return f"{filter.field} {op_str} '{filter.value}'"
|
|
428
|
+
|
|
429
|
+
def get_standard_filters(self) -> dict[str, Callable]:
|
|
430
|
+
"""Get all available standard filters."""
|
|
431
|
+
return {
|
|
432
|
+
"status_is": self.status_is,
|
|
433
|
+
"priority_gte": self.priority_gte,
|
|
434
|
+
"priority_lte": self.priority_lte,
|
|
435
|
+
"assigned_to": self.assigned_to,
|
|
436
|
+
"created_after": self.created_after,
|
|
437
|
+
"created_before": self.created_before,
|
|
438
|
+
"updated_after": self.updated_after,
|
|
439
|
+
"updated_before": self.updated_before,
|
|
440
|
+
"any_of": self.any_of,
|
|
441
|
+
"none_of": self.none_of,
|
|
442
|
+
"text_contains": self.text_contains,
|
|
443
|
+
"text_starts_with": self.text_starts_with,
|
|
444
|
+
"range": self.range,
|
|
445
|
+
}
|