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.
Files changed (331) hide show
  1. htmlgraph/.htmlgraph/.session-warning-state.json +6 -0
  2. htmlgraph/.htmlgraph/agents.json +72 -0
  3. htmlgraph/.htmlgraph/htmlgraph.db +0 -0
  4. htmlgraph/__init__.py +173 -17
  5. htmlgraph/__init__.pyi +123 -0
  6. htmlgraph/agent_detection.py +127 -0
  7. htmlgraph/agent_registry.py +45 -30
  8. htmlgraph/agents.py +160 -107
  9. htmlgraph/analytics/__init__.py +9 -2
  10. htmlgraph/analytics/cli.py +190 -51
  11. htmlgraph/analytics/cost_analyzer.py +391 -0
  12. htmlgraph/analytics/cost_monitor.py +664 -0
  13. htmlgraph/analytics/cost_reporter.py +675 -0
  14. htmlgraph/analytics/cross_session.py +617 -0
  15. htmlgraph/analytics/dependency.py +192 -100
  16. htmlgraph/analytics/pattern_learning.py +771 -0
  17. htmlgraph/analytics/session_graph.py +707 -0
  18. htmlgraph/analytics/strategic/__init__.py +80 -0
  19. htmlgraph/analytics/strategic/cost_optimizer.py +611 -0
  20. htmlgraph/analytics/strategic/pattern_detector.py +876 -0
  21. htmlgraph/analytics/strategic/preference_manager.py +709 -0
  22. htmlgraph/analytics/strategic/suggestion_engine.py +747 -0
  23. htmlgraph/analytics/work_type.py +190 -14
  24. htmlgraph/analytics_index.py +135 -51
  25. htmlgraph/api/__init__.py +3 -0
  26. htmlgraph/api/cost_alerts_websocket.py +416 -0
  27. htmlgraph/api/main.py +2498 -0
  28. htmlgraph/api/static/htmx.min.js +1 -0
  29. htmlgraph/api/static/style-redesign.css +1344 -0
  30. htmlgraph/api/static/style.css +1079 -0
  31. htmlgraph/api/templates/dashboard-redesign.html +1366 -0
  32. htmlgraph/api/templates/dashboard.html +794 -0
  33. htmlgraph/api/templates/partials/activity-feed-hierarchical.html +326 -0
  34. htmlgraph/api/templates/partials/activity-feed.html +1100 -0
  35. htmlgraph/api/templates/partials/agents-redesign.html +317 -0
  36. htmlgraph/api/templates/partials/agents.html +317 -0
  37. htmlgraph/api/templates/partials/event-traces.html +373 -0
  38. htmlgraph/api/templates/partials/features-kanban-redesign.html +509 -0
  39. htmlgraph/api/templates/partials/features.html +578 -0
  40. htmlgraph/api/templates/partials/metrics-redesign.html +346 -0
  41. htmlgraph/api/templates/partials/metrics.html +346 -0
  42. htmlgraph/api/templates/partials/orchestration-redesign.html +443 -0
  43. htmlgraph/api/templates/partials/orchestration.html +198 -0
  44. htmlgraph/api/templates/partials/spawners.html +375 -0
  45. htmlgraph/api/templates/partials/work-items.html +613 -0
  46. htmlgraph/api/websocket.py +538 -0
  47. htmlgraph/archive/__init__.py +24 -0
  48. htmlgraph/archive/bloom.py +234 -0
  49. htmlgraph/archive/fts.py +297 -0
  50. htmlgraph/archive/manager.py +583 -0
  51. htmlgraph/archive/search.py +244 -0
  52. htmlgraph/atomic_ops.py +560 -0
  53. htmlgraph/attribute_index.py +208 -0
  54. htmlgraph/bounded_paths.py +539 -0
  55. htmlgraph/builders/__init__.py +14 -0
  56. htmlgraph/builders/base.py +118 -29
  57. htmlgraph/builders/bug.py +150 -0
  58. htmlgraph/builders/chore.py +119 -0
  59. htmlgraph/builders/epic.py +150 -0
  60. htmlgraph/builders/feature.py +31 -6
  61. htmlgraph/builders/insight.py +195 -0
  62. htmlgraph/builders/metric.py +217 -0
  63. htmlgraph/builders/pattern.py +202 -0
  64. htmlgraph/builders/phase.py +162 -0
  65. htmlgraph/builders/spike.py +52 -19
  66. htmlgraph/builders/track.py +148 -72
  67. htmlgraph/cigs/__init__.py +81 -0
  68. htmlgraph/cigs/autonomy.py +385 -0
  69. htmlgraph/cigs/cost.py +475 -0
  70. htmlgraph/cigs/messages_basic.py +472 -0
  71. htmlgraph/cigs/messaging.py +365 -0
  72. htmlgraph/cigs/models.py +771 -0
  73. htmlgraph/cigs/pattern_storage.py +427 -0
  74. htmlgraph/cigs/patterns.py +503 -0
  75. htmlgraph/cigs/posttool_analyzer.py +234 -0
  76. htmlgraph/cigs/reporter.py +818 -0
  77. htmlgraph/cigs/tracker.py +317 -0
  78. htmlgraph/cli/.htmlgraph/.session-warning-state.json +6 -0
  79. htmlgraph/cli/.htmlgraph/agents.json +72 -0
  80. htmlgraph/cli/.htmlgraph/htmlgraph.db +0 -0
  81. htmlgraph/cli/__init__.py +42 -0
  82. htmlgraph/cli/__main__.py +6 -0
  83. htmlgraph/cli/analytics.py +1424 -0
  84. htmlgraph/cli/base.py +685 -0
  85. htmlgraph/cli/constants.py +206 -0
  86. htmlgraph/cli/core.py +954 -0
  87. htmlgraph/cli/main.py +147 -0
  88. htmlgraph/cli/models.py +475 -0
  89. htmlgraph/cli/templates/__init__.py +1 -0
  90. htmlgraph/cli/templates/cost_dashboard.py +399 -0
  91. htmlgraph/cli/work/__init__.py +239 -0
  92. htmlgraph/cli/work/browse.py +115 -0
  93. htmlgraph/cli/work/features.py +568 -0
  94. htmlgraph/cli/work/orchestration.py +676 -0
  95. htmlgraph/cli/work/report.py +728 -0
  96. htmlgraph/cli/work/sessions.py +466 -0
  97. htmlgraph/cli/work/snapshot.py +559 -0
  98. htmlgraph/cli/work/tracks.py +486 -0
  99. htmlgraph/cli_commands/__init__.py +1 -0
  100. htmlgraph/cli_commands/feature.py +195 -0
  101. htmlgraph/cli_framework.py +115 -0
  102. htmlgraph/collections/__init__.py +18 -0
  103. htmlgraph/collections/base.py +415 -98
  104. htmlgraph/collections/bug.py +53 -0
  105. htmlgraph/collections/chore.py +53 -0
  106. htmlgraph/collections/epic.py +53 -0
  107. htmlgraph/collections/feature.py +12 -26
  108. htmlgraph/collections/insight.py +100 -0
  109. htmlgraph/collections/metric.py +92 -0
  110. htmlgraph/collections/pattern.py +97 -0
  111. htmlgraph/collections/phase.py +53 -0
  112. htmlgraph/collections/session.py +194 -0
  113. htmlgraph/collections/spike.py +56 -16
  114. htmlgraph/collections/task_delegation.py +241 -0
  115. htmlgraph/collections/todo.py +511 -0
  116. htmlgraph/collections/traces.py +487 -0
  117. htmlgraph/config/cost_models.json +56 -0
  118. htmlgraph/config.py +190 -0
  119. htmlgraph/context_analytics.py +344 -0
  120. htmlgraph/converter.py +216 -28
  121. htmlgraph/cost_analysis/__init__.py +5 -0
  122. htmlgraph/cost_analysis/analyzer.py +438 -0
  123. htmlgraph/dashboard.html +2406 -307
  124. htmlgraph/dashboard.html.backup +6592 -0
  125. htmlgraph/dashboard.html.bak +7181 -0
  126. htmlgraph/dashboard.html.bak2 +7231 -0
  127. htmlgraph/dashboard.html.bak3 +7232 -0
  128. htmlgraph/db/__init__.py +38 -0
  129. htmlgraph/db/queries.py +790 -0
  130. htmlgraph/db/schema.py +1788 -0
  131. htmlgraph/decorators.py +317 -0
  132. htmlgraph/dependency_models.py +19 -2
  133. htmlgraph/deploy.py +142 -125
  134. htmlgraph/deployment_models.py +474 -0
  135. htmlgraph/docs/API_REFERENCE.md +841 -0
  136. htmlgraph/docs/HTTP_API.md +750 -0
  137. htmlgraph/docs/INTEGRATION_GUIDE.md +752 -0
  138. htmlgraph/docs/ORCHESTRATION_PATTERNS.md +717 -0
  139. htmlgraph/docs/README.md +532 -0
  140. htmlgraph/docs/__init__.py +77 -0
  141. htmlgraph/docs/docs_version.py +55 -0
  142. htmlgraph/docs/metadata.py +93 -0
  143. htmlgraph/docs/migrations.py +232 -0
  144. htmlgraph/docs/template_engine.py +143 -0
  145. htmlgraph/docs/templates/_sections/cli_reference.md.j2 +52 -0
  146. htmlgraph/docs/templates/_sections/core_concepts.md.j2 +29 -0
  147. htmlgraph/docs/templates/_sections/sdk_basics.md.j2 +69 -0
  148. htmlgraph/docs/templates/base_agents.md.j2 +78 -0
  149. htmlgraph/docs/templates/example_user_override.md.j2 +47 -0
  150. htmlgraph/docs/version_check.py +163 -0
  151. htmlgraph/edge_index.py +182 -27
  152. htmlgraph/error_handler.py +544 -0
  153. htmlgraph/event_log.py +100 -52
  154. htmlgraph/event_migration.py +13 -4
  155. htmlgraph/exceptions.py +49 -0
  156. htmlgraph/file_watcher.py +101 -28
  157. htmlgraph/find_api.py +75 -63
  158. htmlgraph/git_events.py +145 -63
  159. htmlgraph/graph.py +1122 -106
  160. htmlgraph/hooks/.htmlgraph/.session-warning-state.json +6 -0
  161. htmlgraph/hooks/.htmlgraph/agents.json +72 -0
  162. htmlgraph/hooks/.htmlgraph/index.sqlite +0 -0
  163. htmlgraph/hooks/__init__.py +45 -0
  164. htmlgraph/hooks/bootstrap.py +169 -0
  165. htmlgraph/hooks/cigs_pretool_enforcer.py +354 -0
  166. htmlgraph/hooks/concurrent_sessions.py +208 -0
  167. htmlgraph/hooks/context.py +350 -0
  168. htmlgraph/hooks/drift_handler.py +525 -0
  169. htmlgraph/hooks/event_tracker.py +1314 -0
  170. htmlgraph/hooks/git_commands.py +175 -0
  171. htmlgraph/hooks/hooks-config.example.json +12 -0
  172. htmlgraph/hooks/installer.py +343 -0
  173. htmlgraph/hooks/orchestrator.py +674 -0
  174. htmlgraph/hooks/orchestrator_reflector.py +223 -0
  175. htmlgraph/hooks/post-checkout.sh +28 -0
  176. htmlgraph/hooks/post-commit.sh +24 -0
  177. htmlgraph/hooks/post-merge.sh +26 -0
  178. htmlgraph/hooks/post_tool_use_failure.py +273 -0
  179. htmlgraph/hooks/post_tool_use_handler.py +257 -0
  180. htmlgraph/hooks/posttooluse.py +408 -0
  181. htmlgraph/hooks/pre-commit.sh +94 -0
  182. htmlgraph/hooks/pre-push.sh +28 -0
  183. htmlgraph/hooks/pretooluse.py +819 -0
  184. htmlgraph/hooks/prompt_analyzer.py +637 -0
  185. htmlgraph/hooks/session_handler.py +668 -0
  186. htmlgraph/hooks/session_summary.py +395 -0
  187. htmlgraph/hooks/state_manager.py +504 -0
  188. htmlgraph/hooks/subagent_detection.py +202 -0
  189. htmlgraph/hooks/subagent_stop.py +369 -0
  190. htmlgraph/hooks/task_enforcer.py +255 -0
  191. htmlgraph/hooks/task_validator.py +177 -0
  192. htmlgraph/hooks/validator.py +628 -0
  193. htmlgraph/ids.py +41 -27
  194. htmlgraph/index.d.ts +286 -0
  195. htmlgraph/learning.py +767 -0
  196. htmlgraph/mcp_server.py +69 -23
  197. htmlgraph/models.py +1586 -87
  198. htmlgraph/operations/README.md +62 -0
  199. htmlgraph/operations/__init__.py +79 -0
  200. htmlgraph/operations/analytics.py +339 -0
  201. htmlgraph/operations/bootstrap.py +289 -0
  202. htmlgraph/operations/events.py +244 -0
  203. htmlgraph/operations/fastapi_server.py +231 -0
  204. htmlgraph/operations/hooks.py +350 -0
  205. htmlgraph/operations/initialization.py +597 -0
  206. htmlgraph/operations/initialization.py.backup +228 -0
  207. htmlgraph/operations/server.py +303 -0
  208. htmlgraph/orchestration/__init__.py +58 -0
  209. htmlgraph/orchestration/claude_launcher.py +179 -0
  210. htmlgraph/orchestration/command_builder.py +72 -0
  211. htmlgraph/orchestration/headless_spawner.py +281 -0
  212. htmlgraph/orchestration/live_events.py +377 -0
  213. htmlgraph/orchestration/model_selection.py +327 -0
  214. htmlgraph/orchestration/plugin_manager.py +140 -0
  215. htmlgraph/orchestration/prompts.py +137 -0
  216. htmlgraph/orchestration/spawner_event_tracker.py +383 -0
  217. htmlgraph/orchestration/spawners/__init__.py +16 -0
  218. htmlgraph/orchestration/spawners/base.py +194 -0
  219. htmlgraph/orchestration/spawners/claude.py +173 -0
  220. htmlgraph/orchestration/spawners/codex.py +435 -0
  221. htmlgraph/orchestration/spawners/copilot.py +294 -0
  222. htmlgraph/orchestration/spawners/gemini.py +471 -0
  223. htmlgraph/orchestration/subprocess_runner.py +36 -0
  224. htmlgraph/orchestration/task_coordination.py +343 -0
  225. htmlgraph/orchestration.md +563 -0
  226. htmlgraph/orchestrator-system-prompt-optimized.txt +863 -0
  227. htmlgraph/orchestrator.py +669 -0
  228. htmlgraph/orchestrator_config.py +357 -0
  229. htmlgraph/orchestrator_mode.py +328 -0
  230. htmlgraph/orchestrator_validator.py +133 -0
  231. htmlgraph/parallel.py +646 -0
  232. htmlgraph/parser.py +160 -35
  233. htmlgraph/path_query.py +608 -0
  234. htmlgraph/pattern_matcher.py +636 -0
  235. htmlgraph/planning.py +147 -52
  236. htmlgraph/pydantic_models.py +476 -0
  237. htmlgraph/quality_gates.py +350 -0
  238. htmlgraph/query_builder.py +109 -72
  239. htmlgraph/query_composer.py +509 -0
  240. htmlgraph/reflection.py +443 -0
  241. htmlgraph/refs.py +344 -0
  242. htmlgraph/repo_hash.py +512 -0
  243. htmlgraph/repositories/__init__.py +292 -0
  244. htmlgraph/repositories/analytics_repository.py +455 -0
  245. htmlgraph/repositories/analytics_repository_standard.py +628 -0
  246. htmlgraph/repositories/feature_repository.py +581 -0
  247. htmlgraph/repositories/feature_repository_htmlfile.py +668 -0
  248. htmlgraph/repositories/feature_repository_memory.py +607 -0
  249. htmlgraph/repositories/feature_repository_sqlite.py +858 -0
  250. htmlgraph/repositories/filter_service.py +620 -0
  251. htmlgraph/repositories/filter_service_standard.py +445 -0
  252. htmlgraph/repositories/shared_cache.py +621 -0
  253. htmlgraph/repositories/shared_cache_memory.py +395 -0
  254. htmlgraph/repositories/track_repository.py +552 -0
  255. htmlgraph/repositories/track_repository_htmlfile.py +619 -0
  256. htmlgraph/repositories/track_repository_memory.py +508 -0
  257. htmlgraph/repositories/track_repository_sqlite.py +711 -0
  258. htmlgraph/routing.py +8 -19
  259. htmlgraph/scripts/deploy.py +1 -2
  260. htmlgraph/sdk/__init__.py +398 -0
  261. htmlgraph/sdk/__init__.pyi +14 -0
  262. htmlgraph/sdk/analytics/__init__.py +19 -0
  263. htmlgraph/sdk/analytics/engine.py +155 -0
  264. htmlgraph/sdk/analytics/helpers.py +178 -0
  265. htmlgraph/sdk/analytics/registry.py +109 -0
  266. htmlgraph/sdk/base.py +484 -0
  267. htmlgraph/sdk/constants.py +216 -0
  268. htmlgraph/sdk/core.pyi +308 -0
  269. htmlgraph/sdk/discovery.py +120 -0
  270. htmlgraph/sdk/help/__init__.py +12 -0
  271. htmlgraph/sdk/help/mixin.py +699 -0
  272. htmlgraph/sdk/mixins/__init__.py +15 -0
  273. htmlgraph/sdk/mixins/attribution.py +113 -0
  274. htmlgraph/sdk/mixins/mixin.py +410 -0
  275. htmlgraph/sdk/operations/__init__.py +12 -0
  276. htmlgraph/sdk/operations/mixin.py +427 -0
  277. htmlgraph/sdk/orchestration/__init__.py +17 -0
  278. htmlgraph/sdk/orchestration/coordinator.py +203 -0
  279. htmlgraph/sdk/orchestration/spawner.py +204 -0
  280. htmlgraph/sdk/planning/__init__.py +19 -0
  281. htmlgraph/sdk/planning/bottlenecks.py +93 -0
  282. htmlgraph/sdk/planning/mixin.py +211 -0
  283. htmlgraph/sdk/planning/parallel.py +186 -0
  284. htmlgraph/sdk/planning/queue.py +210 -0
  285. htmlgraph/sdk/planning/recommendations.py +87 -0
  286. htmlgraph/sdk/planning/smart_planning.py +319 -0
  287. htmlgraph/sdk/session/__init__.py +19 -0
  288. htmlgraph/sdk/session/continuity.py +57 -0
  289. htmlgraph/sdk/session/handoff.py +110 -0
  290. htmlgraph/sdk/session/info.py +309 -0
  291. htmlgraph/sdk/session/manager.py +103 -0
  292. htmlgraph/sdk/strategic/__init__.py +26 -0
  293. htmlgraph/sdk/strategic/mixin.py +563 -0
  294. htmlgraph/server.py +685 -180
  295. htmlgraph/services/__init__.py +10 -0
  296. htmlgraph/services/claiming.py +199 -0
  297. htmlgraph/session_hooks.py +300 -0
  298. htmlgraph/session_manager.py +1392 -175
  299. htmlgraph/session_registry.py +587 -0
  300. htmlgraph/session_state.py +436 -0
  301. htmlgraph/session_warning.py +201 -0
  302. htmlgraph/sessions/__init__.py +23 -0
  303. htmlgraph/sessions/handoff.py +756 -0
  304. htmlgraph/setup.py +34 -17
  305. htmlgraph/spike_index.py +143 -0
  306. htmlgraph/sync_docs.py +12 -15
  307. htmlgraph/system_prompts.py +450 -0
  308. htmlgraph/templates/AGENTS.md.template +366 -0
  309. htmlgraph/templates/CLAUDE.md.template +97 -0
  310. htmlgraph/templates/GEMINI.md.template +87 -0
  311. htmlgraph/templates/orchestration-view.html +350 -0
  312. htmlgraph/track_builder.py +146 -15
  313. htmlgraph/track_manager.py +69 -21
  314. htmlgraph/transcript.py +890 -0
  315. htmlgraph/transcript_analytics.py +699 -0
  316. htmlgraph/types.py +323 -0
  317. htmlgraph/validation.py +115 -0
  318. htmlgraph/watch.py +8 -5
  319. htmlgraph/work_type_utils.py +3 -2
  320. {htmlgraph-0.9.3.data → htmlgraph-0.27.5.data}/data/htmlgraph/dashboard.html +2406 -307
  321. htmlgraph-0.27.5.data/data/htmlgraph/templates/AGENTS.md.template +366 -0
  322. htmlgraph-0.27.5.data/data/htmlgraph/templates/CLAUDE.md.template +97 -0
  323. htmlgraph-0.27.5.data/data/htmlgraph/templates/GEMINI.md.template +87 -0
  324. {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/METADATA +97 -64
  325. htmlgraph-0.27.5.dist-info/RECORD +337 -0
  326. {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/entry_points.txt +1 -1
  327. htmlgraph/cli.py +0 -2688
  328. htmlgraph/sdk.py +0 -709
  329. htmlgraph-0.9.3.dist-info/RECORD +0 -61
  330. {htmlgraph-0.9.3.data → htmlgraph-0.27.5.data}/data/htmlgraph/styles.css +0 -0
  331. {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
+ }