diff --git a/.husky/pre-commit b/.husky/pre-commit index 9e0f455..2a02a31 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,5 +1,23 @@ npx lint-staged +# Run ruff and mypy on Python files in flowquery-py +if git diff --cached --name-only | grep -q "^flowquery-py/.*\.py$"; then + echo "Running ruff on Python files..." + cd flowquery-py + conda run -n flowquery ruff check src --select=F401,E,W,I + if [ $? -ne 0 ]; then + echo "Ruff check failed. Please fix the issues before committing." + exit 1 + fi + echo "Running mypy on Python files..." + conda run -n flowquery mypy src --no-error-summary + if [ $? -ne 0 ]; then + echo "Mypy check failed. Please fix the type errors before committing." + exit 1 + fi + cd .. +fi + # Strip outputs from notebook files in flowquery-py for notebook in $(git ls-files 'flowquery-py/**/*.ipynb'); do if [ -f "$notebook" ]; then diff --git a/flowquery-py/.gitignore b/flowquery-py/.gitignore index 479bdd5..76b9799 100644 --- a/flowquery-py/.gitignore +++ b/flowquery-py/.gitignore @@ -73,6 +73,9 @@ venv.bak/ .dmypy.json dmypy.json +# ruff +.ruff_cache/ + # Pyre type checker .pyre/ diff --git a/flowquery-py/pyproject.toml b/flowquery-py/pyproject.toml index 79b64eb..40d8f3a 100644 --- a/flowquery-py/pyproject.toml +++ b/flowquery-py/pyproject.toml @@ -41,6 +41,8 @@ dev = [ "jupyter>=1.0.0", "ipykernel>=6.0.0", "nbstripout>=0.6.0", + "mypy>=1.0.0", + "ruff>=0.1.0", ] [build-system] @@ -75,4 +77,45 @@ python_functions = ["test_*"] addopts = "-v --tb=short" [tool.pytest-asyncio] -mode = "auto" \ No newline at end of file +mode = "auto" + +[tool.mypy] +python_version = "3.10" +strict = true +ignore_missing_imports = true +exclude = [ + "tests/", + "__pycache__", + ".git", + "build", + "dist", +] + +[[tool.mypy.overrides]] +module = "src.parsing.parser" +warn_return_any = false +disable_error_code = ["union-attr", "arg-type"] + +[tool.ruff] +target-version = "py310" +line-length = 120 +exclude = [ + ".git", + "__pycache__", + "build", + "dist", +] + +[tool.ruff.lint] +select = [ + "F", # Pyflakes (includes F401 unused imports) + "E", # pycodestyle errors + "W", # pycodestyle warnings + "I", # isort +] +ignore = [ + "E501", # line too long (handled by formatter) +] + +[tool.ruff.lint.isort] +known-first-party = ["flowquery", "src"] \ No newline at end of file diff --git a/flowquery-py/src/__init__.py b/flowquery-py/src/__init__.py index 1d940a3..fc28357 100644 --- a/flowquery-py/src/__init__.py +++ b/flowquery-py/src/__init__.py @@ -8,17 +8,17 @@ from .compute.runner import Runner from .io.command_line import CommandLine -from .parsing.parser import Parser -from .parsing.functions.function import Function from .parsing.functions.aggregate_function import AggregateFunction from .parsing.functions.async_function import AsyncFunction -from .parsing.functions.predicate_function import PredicateFunction -from .parsing.functions.reducer_element import ReducerElement +from .parsing.functions.function import Function from .parsing.functions.function_metadata import ( + FunctionCategory, FunctionDef, FunctionMetadata, - FunctionCategory, ) +from .parsing.functions.predicate_function import PredicateFunction +from .parsing.functions.reducer_element import ReducerElement +from .parsing.parser import Parser __all__ = [ "Runner", diff --git a/flowquery-py/src/compute/runner.py b/flowquery-py/src/compute/runner.py index 5a03eba..c07ed68 100644 --- a/flowquery-py/src/compute/runner.py +++ b/flowquery-py/src/compute/runner.py @@ -9,10 +9,10 @@ class Runner: """Executes a FlowQuery statement and retrieves the results. - + The Runner class parses a FlowQuery statement into an AST and executes it, managing the execution flow from the first operation to the final return statement. - + Example: runner = Runner("WITH 1 as x RETURN x") await runner.run() @@ -25,24 +25,28 @@ def __init__( ast: Optional[ASTNode] = None ): """Creates a new Runner instance and parses the FlowQuery statement. - + Args: statement: The FlowQuery statement to execute ast: An already-parsed AST (optional) - + Raises: ValueError: If neither statement nor AST is provided """ if (statement is None or statement == "") and ast is None: raise ValueError("Either statement or AST must be provided") - - _ast = ast if ast is not None else Parser().parse(statement) - self._first: Operation = _ast.first_child() - self._last: Operation = _ast.last_child() + + _ast = ast if ast is not None else Parser().parse(statement or "") + first = _ast.first_child() + last = _ast.last_child() + if not isinstance(first, Operation) or not isinstance(last, Operation): + raise ValueError("AST must contain Operations") + self._first: Operation = first + self._last: Operation = last async def run(self) -> None: """Executes the parsed FlowQuery statement. - + Raises: Exception: If an error occurs during execution """ @@ -53,7 +57,7 @@ async def run(self) -> None: @property def results(self) -> List[Dict[str, Any]]: """Gets the results from the executed statement. - + Returns: The results from the last operation (typically a RETURN statement) """ diff --git a/flowquery-py/src/extensibility.py b/flowquery-py/src/extensibility.py index aa8225c..ab66306 100644 --- a/flowquery-py/src/extensibility.py +++ b/flowquery-py/src/extensibility.py @@ -4,7 +4,7 @@ Example: from flowquery.extensibility import Function, FunctionDef - + @FunctionDef({ 'description': "Converts a string to uppercase", 'category': "string", @@ -15,27 +15,27 @@ class UpperCase(Function): def __init__(self): super().__init__("uppercase") self._expected_parameter_count = 1 - + def value(self) -> str: return str(self.get_children()[0].value()).upper() """ # Base function classes for creating custom functions -from .parsing.functions.function import Function from .parsing.functions.aggregate_function import AggregateFunction from .parsing.functions.async_function import AsyncFunction -from .parsing.functions.predicate_function import PredicateFunction -from .parsing.functions.reducer_element import ReducerElement +from .parsing.functions.function import Function # Decorator and metadata types for function registration from .parsing.functions.function_metadata import ( + FunctionCategory, FunctionDef, - FunctionMetadata, FunctionDefOptions, - ParameterSchema, + FunctionMetadata, OutputSchema, - FunctionCategory, + ParameterSchema, ) +from .parsing.functions.predicate_function import PredicateFunction +from .parsing.functions.reducer_element import ReducerElement __all__ = [ "Function", diff --git a/flowquery-py/src/graph/__init__.py b/flowquery-py/src/graph/__init__.py index cb3a637..a6de5a7 100644 --- a/flowquery-py/src/graph/__init__.py +++ b/flowquery-py/src/graph/__init__.py @@ -1,18 +1,18 @@ """Graph module for FlowQuery.""" -from .node import Node -from .relationship import Relationship -from .pattern import Pattern -from .patterns import Patterns -from .pattern_expression import PatternExpression from .database import Database from .hops import Hops +from .node import Node from .node_data import NodeData from .node_reference import NodeReference -from .relationship_data import RelationshipData -from .relationship_reference import RelationshipReference +from .pattern import Pattern +from .pattern_expression import PatternExpression +from .patterns import Patterns from .physical_node import PhysicalNode from .physical_relationship import PhysicalRelationship +from .relationship import Relationship +from .relationship_data import RelationshipData +from .relationship_reference import RelationshipReference __all__ = [ "Node", diff --git a/flowquery-py/src/graph/database.py b/flowquery-py/src/graph/database.py index d0f35c2..815115a 100644 --- a/flowquery-py/src/graph/database.py +++ b/flowquery-py/src/graph/database.py @@ -1,14 +1,16 @@ """Graph database for FlowQuery.""" -from typing import Any, Dict, Optional, Union, TYPE_CHECKING +from __future__ import annotations -from ..parsing.ast_node import ASTNode +from typing import Dict, Optional, Union -if TYPE_CHECKING: - from .node import Node - from .relationship import Relationship - from .node_data import NodeData - from .relationship_data import RelationshipData +from ..parsing.ast_node import ASTNode +from .node import Node +from .node_data import NodeData +from .physical_node import PhysicalNode +from .physical_relationship import PhysicalRelationship +from .relationship import Relationship +from .relationship_data import RelationshipData class Database: @@ -18,7 +20,7 @@ class Database: _nodes: Dict[str, 'PhysicalNode'] = {} _relationships: Dict[str, 'PhysicalRelationship'] = {} - def __init__(self): + def __init__(self) -> None: pass @classmethod @@ -29,7 +31,6 @@ def get_instance(cls) -> 'Database': def add_node(self, node: 'Node', statement: ASTNode) -> None: """Adds a node to the database.""" - from .physical_node import PhysicalNode if node.label is None: raise ValueError("Node label is null") physical = PhysicalNode(None, node.label) @@ -42,7 +43,6 @@ def get_node(self, node: 'Node') -> Optional['PhysicalNode']: def add_relationship(self, relationship: 'Relationship', statement: ASTNode) -> None: """Adds a relationship to the database.""" - from .physical_relationship import PhysicalRelationship if relationship.type is None: raise ValueError("Relationship type is null") physical = PhysicalRelationship() @@ -56,11 +56,6 @@ def get_relationship(self, relationship: 'Relationship') -> Optional['PhysicalRe async def get_data(self, element: Union['Node', 'Relationship']) -> Union['NodeData', 'RelationshipData']: """Gets data for a node or relationship.""" - from .node import Node - from .relationship import Relationship - from .node_data import NodeData - from .relationship_data import RelationshipData - if isinstance(element, Node): node = self.get_node(element) if node is None: @@ -75,8 +70,3 @@ async def get_data(self, element: Union['Node', 'Relationship']) -> Union['NodeD return RelationshipData(data) else: raise ValueError("Element is neither Node nor Relationship") - - -# Import for type hints -from .physical_node import PhysicalNode -from .physical_relationship import PhysicalRelationship diff --git a/flowquery-py/src/graph/node.py b/flowquery-py/src/graph/node.py index e7fe34c..33e652c 100644 --- a/flowquery-py/src/graph/node.py +++ b/flowquery-py/src/graph/node.py @@ -1,13 +1,15 @@ """Graph node representation for FlowQuery.""" -from typing import Any, Callable, Dict, Optional, TYPE_CHECKING +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict, Optional, Union from ..parsing.ast_node import ASTNode from ..parsing.expressions.expression import Expression +from .node_data import NodeData, NodeRecord if TYPE_CHECKING: from .relationship import Relationship - from .node_data import NodeData, NodeRecord class Node(ASTNode): @@ -26,7 +28,7 @@ def __init__( self._incoming: Optional['Relationship'] = None self._outgoing: Optional['Relationship'] = None self._data: Optional['NodeData'] = None - self._todo_next: Optional[Callable[[], None]] = None + self._todo_next: Optional[Callable[[], Union[None, Awaitable[None]]]] = None @property def identifier(self) -> Optional[str]: @@ -41,7 +43,7 @@ def label(self) -> Optional[str]: return self._label @label.setter - def label(self, value: str) -> None: + def label(self, value: Optional[str]) -> None: self._label = value @property @@ -54,8 +56,8 @@ def set_property(self, key: str, value: Expression) -> None: def get_property(self, key: str) -> Optional[Expression]: return self._properties.get(key) - def set_value(self, value: 'NodeRecord') -> None: - self._value = value + def set_value(self, value: Dict[str, Any]) -> None: + self._value = value # type: ignore[assignment] def value(self) -> Optional['NodeRecord']: return self._value @@ -83,30 +85,36 @@ async def next(self) -> None: if self._data: self._data.reset() while self._data.next(): - self.set_value(self._data.current()) - if self._outgoing: - await self._outgoing.find(self._value['id']) - await self.run_todo_next() + current = self._data.current() + if current is not None: + self.set_value(current) + if self._outgoing and self._value: + await self._outgoing.find(self._value['id']) + await self.run_todo_next() async def find(self, id_: str, hop: int = 0) -> None: if self._data: self._data.reset() while self._data.find(id_, hop): - self.set_value(self._data.current(hop)) - if self._incoming: - self._incoming.set_end_node(self) - if self._outgoing: - await self._outgoing.find(self._value['id'], hop) - await self.run_todo_next() + current = self._data.current(hop) + if current is not None: + self.set_value(current) + if self._incoming: + self._incoming.set_end_node(self) + if self._outgoing and self._value: + await self._outgoing.find(self._value['id'], hop) + await self.run_todo_next() @property - def todo_next(self) -> Optional[Callable[[], None]]: + def todo_next(self) -> Optional[Callable[[], Union[None, Awaitable[None]]]]: return self._todo_next @todo_next.setter - def todo_next(self, func: Optional[Callable[[], None]]) -> None: + def todo_next(self, func: Optional[Callable[[], Union[None, Awaitable[None]]]]) -> None: self._todo_next = func async def run_todo_next(self) -> None: if self._todo_next: - await self._todo_next() + result = self._todo_next() + if result is not None: + await result diff --git a/flowquery-py/src/graph/node_reference.py b/flowquery-py/src/graph/node_reference.py index cce4694..909a11e 100644 --- a/flowquery-py/src/graph/node_reference.py +++ b/flowquery-py/src/graph/node_reference.py @@ -1,17 +1,14 @@ -"""Node reference for FlowQuery.""" +from __future__ import annotations -from typing import Optional, TYPE_CHECKING +from typing import Any, Optional from .node import Node -if TYPE_CHECKING: - from ..parsing.ast_node import ASTNode - class NodeReference(Node): """Represents a reference to an existing node variable.""" - def __init__(self, base: Node, reference: Node): + def __init__(self, base: Node, reference: Node) -> None: super().__init__(base.identifier, base.label) self._reference: Node = reference # Copy properties from base @@ -28,14 +25,16 @@ def reference(self) -> Node: def referred(self) -> Node: return self._reference - def value(self): + def value(self) -> Optional[Any]: return self._reference.value() if self._reference else None async def next(self) -> None: """Process next using the referenced node's value.""" - self.set_value(self._reference.value()) - if self._outgoing and self._value: - await self._outgoing.find(self._value['id']) + ref_value = self._reference.value() + if ref_value is not None: + self.set_value(dict(ref_value)) + if self._outgoing and self._value: + await self._outgoing.find(self._value['id']) await self.run_todo_next() async def find(self, id_: str, hop: int = 0) -> None: @@ -43,7 +42,7 @@ async def find(self, id_: str, hop: int = 0) -> None: referenced = self._reference.value() if referenced is None or id_ != referenced.get('id'): return - self.set_value(referenced) + self.set_value(dict(referenced)) if self._outgoing and self._value: await self._outgoing.find(self._value['id'], hop) await self.run_todo_next() diff --git a/flowquery-py/src/graph/pattern.py b/flowquery-py/src/graph/pattern.py index f654c8d..ae471c9 100644 --- a/flowquery-py/src/graph/pattern.py +++ b/flowquery-py/src/graph/pattern.py @@ -1,18 +1,21 @@ """Graph pattern representation for FlowQuery.""" -from typing import Any, Generator, List, Optional, TYPE_CHECKING, Union +from __future__ import annotations -from ..parsing.ast_node import ASTNode +from typing import Any, Generator, List, Optional, Sequence, Union -if TYPE_CHECKING: - from .node import Node - from .relationship import Relationship +from ..parsing.ast_node import ASTNode +from .database import Database +from .node import Node +from .node_data import NodeData +from .relationship import Relationship +from .relationship_data import RelationshipData class Pattern(ASTNode): """Represents a graph pattern for matching.""" - def __init__(self): + def __init__(self) -> None: super().__init__() self._identifier: Optional[str] = None self._chain: List[Union['Node', 'Relationship']] = [] @@ -30,17 +33,14 @@ def chain(self) -> List[Union['Node', 'Relationship']]: return self._chain @property - def elements(self) -> List[ASTNode]: + def elements(self) -> Sequence[ASTNode]: return self._chain def add_element(self, element: Union['Node', 'Relationship']) -> None: - from .node import Node - from .relationship import Relationship - - if (len(self._chain) > 0 and - type(self._chain[-1]) == type(element)): + if (len(self._chain) > 0 and + type(self._chain[-1]) is type(element)): raise ValueError("Cannot add two consecutive elements of the same type to the graph pattern") - + if len(self._chain) > 0: last = self._chain[-1] if isinstance(last, Node) and isinstance(element, Relationship): @@ -49,13 +49,12 @@ def add_element(self, element: Union['Node', 'Relationship']) -> None: if isinstance(last, Relationship) and isinstance(element, Node): last.target = element element.incoming = last - + self._chain.append(element) self.add_child(element) @property def start_node(self) -> 'Node': - from .node import Node if len(self._chain) == 0: raise ValueError("Pattern is empty") first = self._chain[0] @@ -65,7 +64,6 @@ def start_node(self) -> 'Node': @property def end_node(self) -> 'Node': - from .node import Node if len(self._chain) == 0: raise ValueError("Pattern is empty") last = self._chain[-1] @@ -73,7 +71,7 @@ def end_node(self) -> 'Node': return last raise ValueError("Pattern does not end with a node") - def first_node(self) -> Optional['Node']: + def first_node(self) -> Optional[Union['Node', 'Relationship']]: if len(self._chain) > 0: return self._chain[0] return None @@ -82,41 +80,30 @@ def value(self) -> List[Any]: return list(self.values()) def values(self) -> Generator[Any, None, None]: - from .node import Node - from .relationship import Relationship - for i, element in enumerate(self._chain): if isinstance(element, Node): # Skip node if previous element was a zero-hop relationship (no matches) - if i > 0 and isinstance(self._chain[i-1], Relationship) and len(self._chain[i-1].matches) == 0: + prev = self._chain[i-1] if i > 0 else None + if isinstance(prev, Relationship) and len(prev.matches) == 0: continue yield element.value() elif isinstance(element, Relationship): - j = 0 - for match in element.matches: + for j, match in enumerate(element.matches): yield match if j < len(element.matches) - 1: yield match["endNode"] - j += 1 async def fetch_data(self) -> None: """Loads data from the database for all elements.""" - from .database import Database - from .node import Node - from .relationship import Relationship - from .node_reference import NodeReference - from .relationship_reference import RelationshipReference - from .node_data import NodeData - from .relationship_data import RelationshipData - db = Database.get_instance() for element in self._chain: - if isinstance(element, (NodeReference, RelationshipReference)): + # Use type name comparison to avoid issues with module double-loading + if type(element).__name__ in ('NodeReference', 'RelationshipReference'): continue data = await db.get_data(element) - if isinstance(element, Node): + if isinstance(element, Node) and isinstance(data, NodeData): element.set_data(data) - elif isinstance(element, Relationship): + elif isinstance(element, Relationship) and isinstance(data, RelationshipData): element.set_data(data) async def initialize(self) -> None: @@ -124,5 +111,5 @@ async def initialize(self) -> None: async def traverse(self) -> None: first = self.first_node() - if first: + if first and isinstance(first, Node): await first.next() diff --git a/flowquery-py/src/graph/pattern_expression.py b/flowquery-py/src/graph/pattern_expression.py index 00fed7d..e6d04b4 100644 --- a/flowquery-py/src/graph/pattern_expression.py +++ b/flowquery-py/src/graph/pattern_expression.py @@ -1,25 +1,27 @@ """Pattern expression for FlowQuery.""" -from typing import Any +from typing import Any, Union from ..parsing.ast_node import ASTNode +from .node import Node from .node_reference import NodeReference from .pattern import Pattern +from .relationship import Relationship class PatternExpression(Pattern): """Represents a pattern expression that can be evaluated. - + PatternExpression is used in WHERE clauses to test whether a graph pattern exists. It evaluates to True if the pattern is matched, False otherwise. """ - def __init__(self): + def __init__(self) -> None: super().__init__() self._fetched: bool = False self._evaluation: bool = False - def add_element(self, element) -> None: + def add_element(self, element: Union[Node, Relationship]) -> None: super().add_element(element) def verify(self) -> None: @@ -29,11 +31,11 @@ def verify(self) -> None: raise ValueError("PatternExpression must contain at least one NodeReference") @property - def identifier(self): + def identifier(self) -> None: return None @identifier.setter - def identifier(self, value): + def identifier(self, value: str) -> None: raise ValueError("Cannot set identifier on PatternExpression") async def fetch_data(self) -> None: @@ -45,18 +47,18 @@ async def fetch_data(self) -> None: async def evaluate(self) -> None: """Evaluates the pattern expression by traversing the graph. - + Sets _evaluation to True if the pattern is matched, False otherwise. """ self._evaluation = False - - async def set_evaluation_true(): + + async def set_evaluation_true() -> None: self._evaluation = True - + self.end_node.todo_next = set_evaluation_true await self.start_node.next() - def value(self) -> bool: + def value(self) -> Any: """Returns the result of the pattern evaluation.""" return self._evaluation diff --git a/flowquery-py/src/graph/patterns.py b/flowquery-py/src/graph/patterns.py index 29c10b8..0df3ca8 100644 --- a/flowquery-py/src/graph/patterns.py +++ b/flowquery-py/src/graph/patterns.py @@ -8,7 +8,7 @@ class Patterns: """Manages a collection of graph patterns.""" - def __init__(self, patterns: Optional[List[Pattern]] = None): + def __init__(self, patterns: Optional[List[Pattern]] = None) -> None: self._patterns = patterns or [] self._to_do_next: Optional[Callable[[], Awaitable[None]]] = None @@ -32,7 +32,7 @@ async def initialize(self) -> None: await pattern.fetch_data() # Ensure data is loaded if previous is not None: # Chain the patterns together - async def next_pattern_start(p=pattern): + async def next_pattern_start(p: Pattern = pattern) -> None: await p.start_node.next() previous.end_node.todo_next = next_pattern_start previous = pattern diff --git a/flowquery-py/src/graph/physical_node.py b/flowquery-py/src/graph/physical_node.py index de1952a..6ad8777 100644 --- a/flowquery-py/src/graph/physical_node.py +++ b/flowquery-py/src/graph/physical_node.py @@ -1,10 +1,10 @@ """Physical node representation for FlowQuery.""" -from typing import Any, Dict, List, Optional, TYPE_CHECKING +from __future__ import annotations -if TYPE_CHECKING: - from ..parsing.ast_node import ASTNode +from typing import Any, Dict, List, Optional +from ..parsing.ast_node import ASTNode from .node import Node @@ -34,6 +34,7 @@ def statement(self, value: Optional["ASTNode"]) -> None: async def data(self) -> List[Dict[str, Any]]: if self._statement is None: raise ValueError("Statement is null") + # Import at runtime to avoid circular dependency from ..compute.runner import Runner runner = Runner(ast=self._statement) await runner.run() diff --git a/flowquery-py/src/graph/physical_relationship.py b/flowquery-py/src/graph/physical_relationship.py index e36da42..943ad11 100644 --- a/flowquery-py/src/graph/physical_relationship.py +++ b/flowquery-py/src/graph/physical_relationship.py @@ -1,18 +1,17 @@ """Physical relationship representation for FlowQuery.""" from __future__ import annotations -from typing import Any, Dict, List, Optional, TYPE_CHECKING -from .relationship import Relationship +from typing import Any, Dict, List, Optional -if TYPE_CHECKING: - from ..parsing.ast_node import ASTNode +from ..parsing.ast_node import ASTNode +from .relationship import Relationship class PhysicalRelationship(Relationship): """Represents a physical relationship in the graph database.""" - def __init__(self): + def __init__(self) -> None: super().__init__() self._statement: Optional[ASTNode] = None @@ -30,6 +29,7 @@ async def data(self) -> List[Dict[str, Any]]: """Execute the statement and return results.""" if self._statement is None: raise ValueError("Statement is null") + # Import at runtime to avoid circular dependency from ..compute.runner import Runner runner = Runner(None, self._statement) await runner.run() diff --git a/flowquery-py/src/graph/relationship.py b/flowquery-py/src/graph/relationship.py index c80e88b..b87b023 100644 --- a/flowquery-py/src/graph/relationship.py +++ b/flowquery-py/src/graph/relationship.py @@ -1,20 +1,22 @@ """Graph relationship representation for FlowQuery.""" -from typing import Any, Dict, List, Optional, TYPE_CHECKING, Union +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union from ..parsing.ast_node import ASTNode from .hops import Hops +from .relationship_data import RelationshipData from .relationship_match_collector import RelationshipMatchCollector, RelationshipMatchRecord if TYPE_CHECKING: from .node import Node - from .relationship_data import RelationshipData, RelationshipRecord class Relationship(ASTNode): """Represents a relationship in a graph pattern.""" - def __init__(self): + def __init__(self) -> None: super().__init__() self._identifier: Optional[str] = None self._type: Optional[str] = None @@ -118,14 +120,14 @@ async def find(self, left_id: str, hop: int = 0) -> None: self._source = self._target if hop == 0: self._data.reset() if self._data else None - + # Handle zero-hop case: when min is 0 on a variable-length relationship, # match source node as target (no traversal) if self._hops and self._hops.multi() and self._hops.min == 0 and self._target: # For zero-hop, target finds the same node as source (left_id) # No relationship match is pushed since no edge is traversed await self._target.find(left_id, hop) - + while self._data and self._data.find(left_id, hop): data = self._data.current(hop) if data and self._hops and hop >= self._hops.min: @@ -137,6 +139,6 @@ async def find(self, left_id: str, hop: int = 0) -> None: if self._hops and hop + 1 < self._hops.max: await self.find(data['right_id'], hop + 1) self._matches.pop() - + # Restore original source node self._source = original diff --git a/flowquery-py/src/graph/relationship_match_collector.py b/flowquery-py/src/graph/relationship_match_collector.py index 9c94c66..0c63912 100644 --- a/flowquery-py/src/graph/relationship_match_collector.py +++ b/flowquery-py/src/graph/relationship_match_collector.py @@ -1,43 +1,45 @@ """Collector for relationship match records.""" -from typing import Any, Dict, List, Optional, TYPE_CHECKING, TypedDict, Union +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Dict, List, Optional, TypedDict, Union if TYPE_CHECKING: - from .relationship import Relationship from .node import Node + from .relationship import Relationship class RelationshipMatchRecord(TypedDict, total=False): """Represents a matched relationship record.""" type: str - startNode: Dict[str, Any] - endNode: Optional[Dict[str, Any]] + startNode: Any + endNode: Any properties: Dict[str, Any] class RelationshipMatchCollector: """Collects relationship matches during graph traversal.""" - def __init__(self): + def __init__(self) -> None: self._matches: List[RelationshipMatchRecord] = [] self._node_ids: List[str] = [] def push(self, relationship: 'Relationship') -> RelationshipMatchRecord: """Push a new match onto the collector.""" + start_node_value = relationship.source.value() if relationship.source else None match: RelationshipMatchRecord = { "type": relationship.type or "", - "startNode": relationship.source.value() if relationship.source else {}, + "startNode": start_node_value or {}, "endNode": None, "properties": relationship.properties, } self._matches.append(match) - start_node_value = match.get("startNode", {}) if isinstance(start_node_value, dict): self._node_ids.append(start_node_value.get("id", "")) return match @property - def end_node(self) -> Optional[Dict[str, Any]]: + def end_node(self) -> Any: """Get the end node of the last match.""" if self._matches: return self._matches[-1].get("endNode") @@ -47,7 +49,8 @@ def end_node(self) -> Optional[Dict[str, Any]]: def end_node(self, node: 'Node') -> None: """Set the end node of the last match.""" if self._matches: - self._matches[-1]["endNode"] = node.value() + node_value = node.value() + self._matches[-1]["endNode"] = node_value if node_value else None def pop(self) -> Optional[RelationshipMatchRecord]: """Pop the last match from the collector.""" diff --git a/flowquery-py/src/graph/relationship_reference.py b/flowquery-py/src/graph/relationship_reference.py index 9419c4f..6a84ed3 100644 --- a/flowquery-py/src/graph/relationship_reference.py +++ b/flowquery-py/src/graph/relationship_reference.py @@ -1,13 +1,13 @@ -"""Relationship reference for FlowQuery.""" +from typing import Any, Optional -from .relationship import Relationship from ..parsing.ast_node import ASTNode +from .relationship import Relationship class RelationshipReference(Relationship): """Represents a reference to an existing relationship variable.""" - def __init__(self, relationship: Relationship, referred: ASTNode): + def __init__(self, relationship: Relationship, referred: ASTNode) -> None: super().__init__() self._referred = referred if relationship.type: @@ -17,5 +17,5 @@ def __init__(self, relationship: Relationship, referred: ASTNode): def referred(self) -> ASTNode: return self._referred - def value(self): + def value(self) -> Optional[Any]: return self._referred.value() if self._referred else None diff --git a/flowquery-py/src/io/command_line.py b/flowquery-py/src/io/command_line.py index 059f989..d1bd689 100644 --- a/flowquery-py/src/io/command_line.py +++ b/flowquery-py/src/io/command_line.py @@ -2,34 +2,33 @@ import argparse import asyncio -from typing import Optional from ..compute.runner import Runner class CommandLine: """Interactive command-line interface for FlowQuery. - + Provides a REPL (Read-Eval-Print Loop) for executing FlowQuery statements and displaying results. - + Example: cli = CommandLine() cli.loop() # Starts interactive mode - + # Or execute a single query: cli.execute("load json from 'https://example.com/data' as d return d") """ def execute(self, query: str) -> None: """Execute a single FlowQuery statement and print results. - + Args: query: The FlowQuery statement to execute. """ # Remove the termination semicolon if present query = query.strip().rstrip(";") - + try: runner = Runner(query) asyncio.run(self._execute(runner)) @@ -38,13 +37,13 @@ def execute(self, query: str) -> None: def loop(self) -> None: """Starts the interactive command loop. - + Prompts the user for FlowQuery statements, executes them, and displays results. Type "exit" to quit the loop. End multi-line queries with ";". """ print('Welcome to FlowQuery! Type "exit" to quit.') print('End queries with ";" to execute. Multi-line input supported.') - + while True: try: lines = [] @@ -61,13 +60,13 @@ def loop(self) -> None: prompt = "... " except EOFError: break - + if user_input.strip() == "": continue - + # Remove the termination semicolon before sending to the engine user_input = user_input.strip().rstrip(";") - + try: runner = Runner(user_input) asyncio.run(self._execute(runner)) @@ -83,7 +82,7 @@ async def _execute(self, runner: Runner) -> None: def main() -> None: """Entry point for the flowquery CLI command. - + Usage: flowquery # Start interactive mode flowquery -c "query" # Execute a single query @@ -99,10 +98,10 @@ def main() -> None: metavar="QUERY", help="Execute a FlowQuery statement and exit" ) - + args = parser.parse_args() cli = CommandLine() - + if args.command: cli.execute(args.command) else: diff --git a/flowquery-py/src/parsing/__init__.py b/flowquery-py/src/parsing/__init__.py index 8e2be71..b9828b2 100644 --- a/flowquery-py/src/parsing/__init__.py +++ b/flowquery-py/src/parsing/__init__.py @@ -1,10 +1,10 @@ """Parsing module for FlowQuery.""" -from .ast_node import ASTNode -from .context import Context from .alias import Alias from .alias_option import AliasOption +from .ast_node import ASTNode from .base_parser import BaseParser +from .context import Context from .parser import Parser __all__ = [ diff --git a/flowquery-py/src/parsing/alias_option.py b/flowquery-py/src/parsing/alias_option.py index b4b7e95..10967e2 100644 --- a/flowquery-py/src/parsing/alias_option.py +++ b/flowquery-py/src/parsing/alias_option.py @@ -5,7 +5,7 @@ class AliasOption(Enum): """Enumeration of alias options for parsing.""" - + NOT_ALLOWED = 0 OPTIONAL = 1 REQUIRED = 2 diff --git a/flowquery-py/src/parsing/ast_node.py b/flowquery-py/src/parsing/ast_node.py index c431ee2..937a10b 100644 --- a/flowquery-py/src/parsing/ast_node.py +++ b/flowquery-py/src/parsing/ast_node.py @@ -1,28 +1,29 @@ """Represents a node in the Abstract Syntax Tree (AST).""" from __future__ import annotations -from typing import List, Any, Generator, Optional + +from typing import Any, Generator, List, Optional class ASTNode: """Represents a node in the Abstract Syntax Tree (AST). - + The AST is a tree representation of the parsed FlowQuery statement structure. Each node can have children and maintains a reference to its parent. - + Example: root = ASTNode() child = ASTNode() root.add_child(child) """ - def __init__(self): + def __init__(self) -> None: self._parent: Optional[ASTNode] = None self.children: List[ASTNode] = [] def add_child(self, child: ASTNode) -> None: """Adds a child node to this node and sets the child's parent reference. - + Args: child: The child node to add """ @@ -31,10 +32,10 @@ def add_child(self, child: ASTNode) -> None: def first_child(self) -> ASTNode: """Returns the first child node. - + Returns: The first child node - + Raises: ValueError: If the node has no children """ @@ -44,10 +45,10 @@ def first_child(self) -> ASTNode: def last_child(self) -> ASTNode: """Returns the last child node. - + Returns: The last child node - + Raises: ValueError: If the node has no children """ @@ -57,7 +58,7 @@ def last_child(self) -> ASTNode: def get_children(self) -> List[ASTNode]: """Returns all child nodes. - + Returns: Array of child nodes """ @@ -65,7 +66,7 @@ def get_children(self) -> List[ASTNode]: def child_count(self) -> int: """Returns the number of child nodes. - + Returns: The count of children """ @@ -73,7 +74,7 @@ def child_count(self) -> int: def value(self) -> Any: """Returns the value of this node. Override in subclasses to provide specific values. - + Returns: The node's value, or None if not applicable """ @@ -81,7 +82,7 @@ def value(self) -> Any: def is_operator(self) -> bool: """Checks if this node represents an operator. - + Returns: True if this is an operator node, False otherwise """ @@ -89,7 +90,7 @@ def is_operator(self) -> bool: def is_operand(self) -> bool: """Checks if this node represents an operand (the opposite of an operator). - + Returns: True if this is an operand node, False otherwise """ @@ -98,7 +99,7 @@ def is_operand(self) -> bool: @property def precedence(self) -> int: """Gets the operator precedence for this node. Higher values indicate higher precedence. - + Returns: The precedence value (0 for non-operators) """ @@ -107,7 +108,7 @@ def precedence(self) -> int: @property def left_associative(self) -> bool: """Indicates whether this operator is left-associative. - + Returns: True if left-associative, False otherwise """ @@ -115,7 +116,7 @@ def left_associative(self) -> bool: def print(self) -> str: """Prints a string representation of the AST tree starting from this node. - + Returns: A formatted string showing the tree structure """ @@ -123,10 +124,10 @@ def print(self) -> str: def _print(self, indent: int) -> Generator[str, None, None]: """Generator function for recursively printing the tree structure. - + Args: indent: The current indentation level - + Yields: Lines representing each node in the tree """ @@ -139,7 +140,7 @@ def _print(self, indent: int) -> Generator[str, None, None]: def __str__(self) -> str: """Returns a string representation of this node. Override in subclasses for custom formatting. - + Returns: The string representation """ diff --git a/flowquery-py/src/parsing/base_parser.py b/flowquery-py/src/parsing/base_parser.py index a5a9d4b..a4f394e 100644 --- a/flowquery-py/src/parsing/base_parser.py +++ b/flowquery-py/src/parsing/base_parser.py @@ -8,7 +8,7 @@ class BaseParser: """Base class for parsers providing common token manipulation functionality. - + This class handles tokenization and provides utility methods for navigating through tokens, peeking ahead, and checking token sequences. """ @@ -19,7 +19,7 @@ def __init__(self, tokens: Optional[List[Token]] = None): def tokenize(self, statement: str) -> None: """Tokenizes a statement and initializes the token array. - + Args: statement: The input statement to tokenize """ @@ -32,7 +32,7 @@ def set_next_token(self) -> None: def peek(self) -> Optional[Token]: """Peeks at the next token without advancing the current position. - + Returns: The next token, or None if at the end of the token stream """ @@ -42,11 +42,11 @@ def peek(self) -> Optional[Token]: def ahead(self, tokens: List[Token], skip_whitespace_and_comments: bool = True) -> bool: """Checks if a sequence of tokens appears ahead in the token stream. - + Args: tokens: The sequence of tokens to look for skip_whitespace_and_comments: Whether to skip whitespace and comments when matching - + Returns: True if the token sequence is found ahead, False otherwise """ @@ -64,7 +64,7 @@ def ahead(self, tokens: List[Token], skip_whitespace_and_comments: bool = True) @property def token(self) -> Token: """Gets the current token. - + Returns: The current token, or EOF if at the end """ @@ -75,7 +75,7 @@ def token(self) -> Token: @property def previous_token(self) -> Token: """Gets the previous token. - + Returns: The previous token, or EOF if at the beginning """ diff --git a/flowquery-py/src/parsing/components/__init__.py b/flowquery-py/src/parsing/components/__init__.py index d9a4396..2f634a2 100644 --- a/flowquery-py/src/parsing/components/__init__.py +++ b/flowquery-py/src/parsing/components/__init__.py @@ -1,12 +1,12 @@ """Components module for FlowQuery parsing.""" from .csv import CSV -from .json import JSON -from .text import Text from .from_ import From from .headers import Headers -from .post import Post +from .json import JSON from .null import Null +from .post import Post +from .text import Text __all__ = [ "CSV", diff --git a/flowquery-py/src/parsing/components/from_.py b/flowquery-py/src/parsing/components/from_.py index e6c956c..3249b5b 100644 --- a/flowquery-py/src/parsing/components/from_.py +++ b/flowquery-py/src/parsing/components/from_.py @@ -1,10 +1,12 @@ """From component node.""" +from typing import Any + from ..ast_node import ASTNode class From(ASTNode): """Represents a FROM clause in LOAD operations.""" - def value(self) -> str: + def value(self) -> Any: return self.children[0].value() diff --git a/flowquery-py/src/parsing/components/headers.py b/flowquery-py/src/parsing/components/headers.py index 98f0a23..110b292 100644 --- a/flowquery-py/src/parsing/components/headers.py +++ b/flowquery-py/src/parsing/components/headers.py @@ -1,6 +1,6 @@ """Headers component node.""" -from typing import Dict +from typing import Any, Dict from ..ast_node import ASTNode @@ -8,5 +8,5 @@ class Headers(ASTNode): """Represents a HEADERS clause in LOAD operations.""" - def value(self) -> Dict: + def value(self) -> Dict[str, Any]: return self.first_child().value() or {} diff --git a/flowquery-py/src/parsing/components/null.py b/flowquery-py/src/parsing/components/null.py index 89ab287..bb2b430 100644 --- a/flowquery-py/src/parsing/components/null.py +++ b/flowquery-py/src/parsing/components/null.py @@ -5,6 +5,6 @@ class Null(ASTNode): """Represents a NULL value in the AST.""" - - def value(self): + + def value(self) -> None: return None diff --git a/flowquery-py/src/parsing/context.py b/flowquery-py/src/parsing/context.py index a557e59..0cd1d73 100644 --- a/flowquery-py/src/parsing/context.py +++ b/flowquery-py/src/parsing/context.py @@ -7,22 +7,22 @@ class Context: """Maintains a stack of AST nodes to track parsing context. - + Used during parsing to maintain the current context and check for specific node types in the parsing hierarchy, which helps with context-sensitive parsing decisions. - + Example: context = Context() context.push(node) has_return = context.contains_type(Return) """ - def __init__(self): + def __init__(self) -> None: self._nodes: List[ASTNode] = [] def push(self, node: ASTNode) -> None: """Pushes a node onto the context stack. - + Args: node: The AST node to push """ @@ -30,7 +30,7 @@ def push(self, node: ASTNode) -> None: def pop(self) -> Optional[ASTNode]: """Pops the top node from the context stack. - + Returns: The popped node, or None if the stack is empty """ @@ -40,10 +40,10 @@ def pop(self) -> Optional[ASTNode]: def contains_type(self, type_: Type[ASTNode]) -> bool: """Checks if the nodes stack contains a node of the specified type. - + Args: type_: The class of the node type to search for - + Returns: True if a node of the specified type is found in the stack, False otherwise """ diff --git a/flowquery-py/src/parsing/data_structures/associative_array.py b/flowquery-py/src/parsing/data_structures/associative_array.py index 57a09d4..6453030 100644 --- a/flowquery-py/src/parsing/data_structures/associative_array.py +++ b/flowquery-py/src/parsing/data_structures/associative_array.py @@ -1,6 +1,6 @@ """Represents an associative array (object/dictionary) in the AST.""" -from typing import Any, Dict +from typing import Any, Dict, Generator from ..ast_node import ASTNode from .key_value_pair import KeyValuePair @@ -8,9 +8,9 @@ class AssociativeArray(ASTNode): """Represents an associative array (object/dictionary) in the AST. - + Associative arrays map string keys to values, similar to JSON objects. - + Example: # For { name: "Alice", age: 30 } obj = AssociativeArray() @@ -20,7 +20,7 @@ class AssociativeArray(ASTNode): def add_key_value(self, key_value_pair: KeyValuePair) -> None: """Adds a key-value pair to the associative array. - + Args: key_value_pair: The key-value pair to add """ @@ -29,10 +29,10 @@ def add_key_value(self, key_value_pair: KeyValuePair) -> None: def __str__(self) -> str: return 'AssociativeArray' - def _value(self): + def _value(self) -> Generator[Dict[str, Any], None, None]: for child in self.children: - key_value = child - yield {key_value.key: key_value._value} + if isinstance(child, KeyValuePair): + yield {child.key: child._value} def value(self) -> Dict[str, Any]: result = {} diff --git a/flowquery-py/src/parsing/data_structures/json_array.py b/flowquery-py/src/parsing/data_structures/json_array.py index ade7ff4..0d0c1fd 100644 --- a/flowquery-py/src/parsing/data_structures/json_array.py +++ b/flowquery-py/src/parsing/data_structures/json_array.py @@ -7,9 +7,9 @@ class JSONArray(ASTNode): """Represents a JSON array in the AST. - + JSON arrays are ordered collections of values. - + Example: # For [1, 2, 3] arr = JSONArray() @@ -20,7 +20,7 @@ class JSONArray(ASTNode): def add_value(self, value: ASTNode) -> None: """Adds a value to the array. - + Args: value: The AST node representing the value to add """ diff --git a/flowquery-py/src/parsing/data_structures/key_value_pair.py b/flowquery-py/src/parsing/data_structures/key_value_pair.py index 3ca9831..76bca83 100644 --- a/flowquery-py/src/parsing/data_structures/key_value_pair.py +++ b/flowquery-py/src/parsing/data_structures/key_value_pair.py @@ -8,16 +8,16 @@ class KeyValuePair(ASTNode): """Represents a key-value pair in an associative array. - + Used to build object literals in FlowQuery. - + Example: kvp = KeyValuePair("name", String("Alice")) """ def __init__(self, key: str, value: ASTNode): """Creates a new key-value pair. - + Args: key: The key string value: The AST node representing the value @@ -27,7 +27,7 @@ def __init__(self, key: str, value: ASTNode): self.add_child(value) @property - def key(self) -> str: + def key(self) -> Any: return self.children[0].value() @property diff --git a/flowquery-py/src/parsing/data_structures/lookup.py b/flowquery-py/src/parsing/data_structures/lookup.py index 374b24d..957dfd8 100644 --- a/flowquery-py/src/parsing/data_structures/lookup.py +++ b/flowquery-py/src/parsing/data_structures/lookup.py @@ -7,9 +7,9 @@ class Lookup(ASTNode): """Represents a lookup operation (array/object indexing) in the AST. - + Lookups access elements from arrays or properties from objects using an index or key. - + Example: # For array[0] or obj.property or obj["key"] lookup = Lookup() diff --git a/flowquery-py/src/parsing/data_structures/range_lookup.py b/flowquery-py/src/parsing/data_structures/range_lookup.py index d2882cd..f7e9a45 100644 --- a/flowquery-py/src/parsing/data_structures/range_lookup.py +++ b/flowquery-py/src/parsing/data_structures/range_lookup.py @@ -1,6 +1,6 @@ """Represents a range lookup operation in the AST.""" -from typing import Any, List +from typing import Any from ..ast_node import ASTNode @@ -35,7 +35,7 @@ def variable(self, variable: ASTNode) -> None: def is_operand(self) -> bool: return True - def value(self) -> List[Any]: + def value(self) -> Any: array = self.variable.value() from_val = self.from_.value() or 0 to_val = self.to.value() or len(array) diff --git a/flowquery-py/src/parsing/expressions/__init__.py b/flowquery-py/src/parsing/expressions/__init__.py index 44392b8..573362e 100644 --- a/flowquery-py/src/parsing/expressions/__init__.py +++ b/flowquery-py/src/parsing/expressions/__init__.py @@ -1,32 +1,32 @@ """Expressions module for FlowQuery parsing.""" -from .expression import Expression from .boolean import Boolean -from .number import Number -from .string import String -from .identifier import Identifier -from .reference import Reference -from .f_string import FString +from .expression import Expression from .expression_map import ExpressionMap +from .f_string import FString +from .identifier import Identifier +from .number import Number from .operator import ( - Operator, Add, - Subtract, - Multiply, + And, Divide, - Modulo, - Power, Equals, - NotEquals, GreaterThan, - LessThan, GreaterThanOrEqual, + Is, + LessThan, LessThanOrEqual, - And, - Or, + Modulo, + Multiply, Not, - Is, + NotEquals, + Operator, + Or, + Power, + Subtract, ) +from .reference import Reference +from .string import String __all__ = [ "Expression", diff --git a/flowquery-py/src/parsing/expressions/expression.py b/flowquery-py/src/parsing/expressions/expression.py index 61bc379..87615fc 100644 --- a/flowquery-py/src/parsing/expressions/expression.py +++ b/flowquery-py/src/parsing/expressions/expression.py @@ -1,21 +1,24 @@ """Represents an expression in the FlowQuery AST.""" -from typing import Any, List, Optional, Generator, TYPE_CHECKING +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Generator, List, Optional from ..ast_node import ASTNode +from ..functions.aggregate_function import AggregateFunction +from .reference import Reference if TYPE_CHECKING: - from ..functions.aggregate_function import AggregateFunction from ...graph.pattern_expression import PatternExpression class Expression(ASTNode): """Represents an expression in the FlowQuery AST. - + Expressions are built using the Shunting Yard algorithm to handle operator precedence and associativity. They can contain operands (numbers, strings, identifiers) and operators (arithmetic, logical, comparison). - + Example: expr = Expression() expr.add_node(number_node) @@ -24,7 +27,7 @@ class Expression(ASTNode): expr.finish() """ - def __init__(self): + def __init__(self) -> None: super().__init__() self._operators: List[ASTNode] = [] self._output: List[ASTNode] = [] @@ -35,9 +38,9 @@ def __init__(self): def add_node(self, node: ASTNode) -> None: """Adds a node (operand or operator) to the expression. - + Uses the Shunting Yard algorithm to maintain correct operator precedence. - + Args: node: The AST node to add (operand or operator) """ @@ -58,7 +61,7 @@ def add_node(self, node: ASTNode) -> None: def finish(self) -> None: """Finalizes the expression by converting it to a tree structure. - + Should be called after all nodes have been added. """ while self._operators: @@ -91,9 +94,9 @@ def set_alias(self, alias: str) -> None: @property def alias(self) -> Optional[str]: - from .reference import Reference - if isinstance(self.first_child(), Reference) and self._alias is None: - return self.first_child().identifier + first = self.first_child() + if isinstance(first, Reference) and self._alias is None: + return first.identifier return self._alias @alias.setter @@ -106,14 +109,14 @@ def __str__(self) -> str: return "Expression" def reducers(self) -> List['AggregateFunction']: + from ..functions.aggregate_function import AggregateFunction if self._reducers is None: - from ..functions.aggregate_function import AggregateFunction self._reducers = list(self._extract(self, AggregateFunction)) return self._reducers def patterns(self) -> List['PatternExpression']: + from ...graph.pattern_expression import PatternExpression if self._patterns is None: - from ...graph.pattern_expression import PatternExpression self._patterns = list(self._extract(self, PatternExpression)) return self._patterns diff --git a/flowquery-py/src/parsing/expressions/expression_map.py b/flowquery-py/src/parsing/expressions/expression_map.py index 34fcf81..5aad24e 100644 --- a/flowquery-py/src/parsing/expressions/expression_map.py +++ b/flowquery-py/src/parsing/expressions/expression_map.py @@ -1,26 +1,26 @@ """Expression map for managing named expressions.""" -from typing import Optional, List, TYPE_CHECKING +from __future__ import annotations -if TYPE_CHECKING: - from .expression import Expression +from typing import Any, List, Optional class ExpressionMap: """Maps expression aliases to their corresponding Expression objects.""" - def __init__(self): - self._map: dict[str, Expression] = {} + def __init__(self) -> None: + self._map: dict[str, Any] = {} - def get(self, alias: str) -> Optional['Expression']: + def get(self, alias: str) -> Optional[Any]: return self._map.get(alias) def has(self, alias: str) -> bool: return alias in self._map - def set_map(self, expressions: List['Expression']) -> None: + def set_map(self, expressions: List[Any]) -> None: self._map.clear() for expr in expressions: - if expr.alias is None: + alias = getattr(expr, 'alias', None) + if alias is None: continue - self._map[expr.alias] = expr + self._map[alias] = expr diff --git a/flowquery-py/src/parsing/expressions/f_string.py b/flowquery-py/src/parsing/expressions/f_string.py index 122a46e..609cf74 100644 --- a/flowquery-py/src/parsing/expressions/f_string.py +++ b/flowquery-py/src/parsing/expressions/f_string.py @@ -5,15 +5,15 @@ from ..ast_node import ASTNode if TYPE_CHECKING: - from .expression import Expression + pass class FString(ASTNode): """Represents a formatted string (f-string) in the AST. - + F-strings allow embedding expressions within string literals. Child nodes represent the parts of the f-string (literal strings and expressions). - + Example: # For f"Hello {name}!" fstr = FString() diff --git a/flowquery-py/src/parsing/expressions/identifier.py b/flowquery-py/src/parsing/expressions/identifier.py index c22d8a2..3891674 100644 --- a/flowquery-py/src/parsing/expressions/identifier.py +++ b/flowquery-py/src/parsing/expressions/identifier.py @@ -1,14 +1,15 @@ """Represents an identifier in the AST.""" -from .string import String from typing import Any +from .string import String + class Identifier(String): """Represents an identifier in the AST. - + Identifiers are used for variable names, property names, and similar constructs. - + Example: id = Identifier("myVariable") """ diff --git a/flowquery-py/src/parsing/expressions/number.py b/flowquery-py/src/parsing/expressions/number.py index 5535be5..296cafb 100644 --- a/flowquery-py/src/parsing/expressions/number.py +++ b/flowquery-py/src/parsing/expressions/number.py @@ -5,9 +5,9 @@ class Number(ASTNode): """Represents a numeric literal in the AST. - + Parses string representations of numbers into integer or float values. - + Example: num = Number("42") print(num.value()) # 42 @@ -15,7 +15,7 @@ class Number(ASTNode): def __init__(self, value: str): """Creates a new Number node by parsing the string value. - + Args: value: The string representation of the number """ diff --git a/flowquery-py/src/parsing/expressions/operator.py b/flowquery-py/src/parsing/expressions/operator.py index 1ab47f3..0490619 100644 --- a/flowquery-py/src/parsing/expressions/operator.py +++ b/flowquery-py/src/parsing/expressions/operator.py @@ -39,7 +39,7 @@ def rhs(self) -> ASTNode: class Add(Operator): - def __init__(self): + def __init__(self) -> None: super().__init__(1, True) def value(self) -> Any: @@ -47,7 +47,7 @@ def value(self) -> Any: class Subtract(Operator): - def __init__(self): + def __init__(self) -> None: super().__init__(1, True) def value(self) -> Any: @@ -55,7 +55,7 @@ def value(self) -> Any: class Multiply(Operator): - def __init__(self): + def __init__(self) -> None: super().__init__(2, True) def value(self) -> Any: @@ -63,7 +63,7 @@ def value(self) -> Any: class Divide(Operator): - def __init__(self): + def __init__(self) -> None: super().__init__(2, True) def value(self) -> Any: @@ -71,7 +71,7 @@ def value(self) -> Any: class Modulo(Operator): - def __init__(self): + def __init__(self) -> None: super().__init__(2, True) def value(self) -> Any: @@ -79,7 +79,7 @@ def value(self) -> Any: class Power(Operator): - def __init__(self): + def __init__(self) -> None: super().__init__(3, False) def value(self) -> Any: @@ -87,7 +87,7 @@ def value(self) -> Any: class Equals(Operator): - def __init__(self): + def __init__(self) -> None: super().__init__(0, True) def value(self) -> int: @@ -95,7 +95,7 @@ def value(self) -> int: class NotEquals(Operator): - def __init__(self): + def __init__(self) -> None: super().__init__(0, True) def value(self) -> int: @@ -103,7 +103,7 @@ def value(self) -> int: class GreaterThan(Operator): - def __init__(self): + def __init__(self) -> None: super().__init__(0, True) def value(self) -> int: @@ -111,7 +111,7 @@ def value(self) -> int: class LessThan(Operator): - def __init__(self): + def __init__(self) -> None: super().__init__(0, True) def value(self) -> int: @@ -119,7 +119,7 @@ def value(self) -> int: class GreaterThanOrEqual(Operator): - def __init__(self): + def __init__(self) -> None: super().__init__(0, True) def value(self) -> int: @@ -127,7 +127,7 @@ def value(self) -> int: class LessThanOrEqual(Operator): - def __init__(self): + def __init__(self) -> None: super().__init__(0, True) def value(self) -> int: @@ -135,7 +135,7 @@ def value(self) -> int: class And(Operator): - def __init__(self): + def __init__(self) -> None: super().__init__(-1, True) def value(self) -> int: @@ -143,7 +143,7 @@ def value(self) -> int: class Or(Operator): - def __init__(self): + def __init__(self) -> None: super().__init__(-1, True) def value(self) -> int: @@ -151,7 +151,7 @@ def value(self) -> int: class Not(Operator): - def __init__(self): + def __init__(self) -> None: super().__init__(0, True) def is_operator(self) -> bool: @@ -162,7 +162,7 @@ def value(self) -> int: class Is(Operator): - def __init__(self): + def __init__(self) -> None: super().__init__(-1, True) def value(self) -> int: diff --git a/flowquery-py/src/parsing/expressions/reference.py b/flowquery-py/src/parsing/expressions/reference.py index 9797228..f1fe4d4 100644 --- a/flowquery-py/src/parsing/expressions/reference.py +++ b/flowquery-py/src/parsing/expressions/reference.py @@ -8,9 +8,9 @@ class Reference(Identifier): """Represents a reference to a previously defined variable or expression. - + References point to values defined earlier in the query (e.g., in WITH or LOAD statements). - + Example: ref = Reference("myVar", previous_node) print(ref.value()) # Gets value from referred node @@ -18,7 +18,7 @@ class Reference(Identifier): def __init__(self, value: str, referred: Optional[ASTNode] = None): """Creates a new Reference to a variable. - + Args: value: The identifier name referred: The node this reference points to (optional) diff --git a/flowquery-py/src/parsing/expressions/string.py b/flowquery-py/src/parsing/expressions/string.py index 73fbc9a..e1222be 100644 --- a/flowquery-py/src/parsing/expressions/string.py +++ b/flowquery-py/src/parsing/expressions/string.py @@ -5,7 +5,7 @@ class String(ASTNode): """Represents a string literal in the AST. - + Example: s = String("hello") print(s.value()) # "hello" @@ -13,7 +13,7 @@ class String(ASTNode): def __init__(self, value: str): """Creates a new String node with the given value. - + Args: value: The string value """ diff --git a/flowquery-py/src/parsing/functions/__init__.py b/flowquery-py/src/parsing/functions/__init__.py index 4452ecb..2764fd3 100644 --- a/flowquery-py/src/parsing/functions/__init__.py +++ b/flowquery-py/src/parsing/functions/__init__.py @@ -1,41 +1,41 @@ """Functions module for FlowQuery parsing.""" -from .function import Function from .aggregate_function import AggregateFunction from .async_function import AsyncFunction -from .predicate_function import PredicateFunction -from .reducer_element import ReducerElement -from .value_holder import ValueHolder +from .avg import Avg +from .collect import Collect +from .function import Function +from .function_factory import FunctionFactory from .function_metadata import ( FunctionCategory, - ParameterSchema, - OutputSchema, - FunctionMetadata, FunctionDef, FunctionDefOptions, - get_registered_function_metadata, - get_registered_function_factory, + FunctionMetadata, + OutputSchema, + ParameterSchema, get_function_metadata, + get_registered_function_factory, + get_registered_function_metadata, ) -from .function_factory import FunctionFactory - -# Built-in functions -from .sum import Sum -from .avg import Avg -from .collect import Collect +from .functions import Functions from .join import Join from .keys import Keys +from .predicate_function import PredicateFunction +from .predicate_sum import PredicateSum from .rand import Rand from .range_ import Range +from .reducer_element import ReducerElement from .replace import Replace from .round_ import Round from .size import Size from .split import Split from .stringify import Stringify + +# Built-in functions +from .sum import Sum from .to_json import ToJson from .type_ import Type -from .functions import Functions -from .predicate_sum import PredicateSum +from .value_holder import ValueHolder __all__ = [ # Base classes diff --git a/flowquery-py/src/parsing/functions/aggregate_function.py b/flowquery-py/src/parsing/functions/aggregate_function.py index 136aef9..e497a33 100644 --- a/flowquery-py/src/parsing/functions/aggregate_function.py +++ b/flowquery-py/src/parsing/functions/aggregate_function.py @@ -8,10 +8,10 @@ class AggregateFunction(Function): """Base class for aggregate functions that reduce multiple values to a single value. - + Aggregate functions like SUM, AVG, and COLLECT process multiple input values and produce a single output. They cannot be nested within other aggregate functions. - + Example: sum_func = Sum() # Used in: RETURN SUM(values) @@ -19,19 +19,19 @@ class AggregateFunction(Function): def __init__(self, name: Optional[str] = None): """Creates a new AggregateFunction with the given name. - + Args: name: The function name """ super().__init__(name) self._overridden: Any = None - def reduce(self, value: ReducerElement) -> None: + def reduce(self, value: Any) -> None: """Processes a value during the aggregation phase. - + Args: value: The element to aggregate - + Raises: NotImplementedError: If not implemented by subclass """ @@ -39,10 +39,10 @@ def reduce(self, value: ReducerElement) -> None: def element(self) -> ReducerElement: """Creates a reducer element for this aggregate function. - + Returns: A ReducerElement instance - + Raises: NotImplementedError: If not implemented by subclass """ diff --git a/flowquery-py/src/parsing/functions/async_function.py b/flowquery-py/src/parsing/functions/async_function.py index 3d6bdef..660ab2d 100644 --- a/flowquery-py/src/parsing/functions/async_function.py +++ b/flowquery-py/src/parsing/functions/async_function.py @@ -8,10 +8,10 @@ class AsyncFunction(Function): """Represents an async data provider function call for use in LOAD operations. - + This class holds the function name and arguments, and provides async iteration over the results from a registered async data provider. - + Example: # Used in: LOAD JSON FROM myDataSource('arg1', 'arg2') AS data async_func = AsyncFunction("myDataSource") @@ -27,7 +27,7 @@ def parameters(self) -> List[ASTNode]: @parameters.setter def parameters(self, nodes: List[ASTNode]) -> None: """Sets the function parameters. - + Args: nodes: Array of AST nodes representing the function arguments """ @@ -36,7 +36,7 @@ def parameters(self, nodes: List[ASTNode]) -> None: def get_arguments(self) -> List[Any]: """Evaluates all parameters and returns their values. Used by the framework to pass arguments to generate(). - + Returns: Array of parameter values """ @@ -44,19 +44,22 @@ def get_arguments(self) -> List[Any]: async def generate(self, *args: Any) -> AsyncGenerator[Any, None]: """Generates the async data provider function results. - + Subclasses override this method with their own typed parameters. The framework automatically evaluates the AST children and spreads them as arguments when calling this method. - + Args: args: Arguments passed from the query (e.g., myFunc(arg1, arg2)) - + Yields: Data items from the async provider - + Raises: NotImplementedError: If the function is not registered as an async provider """ raise NotImplementedError("generate method must be overridden in subclasses.") - yield # Make this a generator + # Note: yield is here only to make this a generator function + # It will never be reached due to the raise above + if False: # pragma: no cover + yield # Make this a generator diff --git a/flowquery-py/src/parsing/functions/avg.py b/flowquery-py/src/parsing/functions/avg.py index 404534a..c632b62 100644 --- a/flowquery-py/src/parsing/functions/avg.py +++ b/flowquery-py/src/parsing/functions/avg.py @@ -3,14 +3,14 @@ from typing import Optional from .aggregate_function import AggregateFunction -from .reducer_element import ReducerElement from .function_metadata import FunctionDef +from .reducer_element import ReducerElement class AvgReducerElement(ReducerElement): """Reducer element for Avg aggregate function.""" - def __init__(self): + def __init__(self) -> None: self._count: int = 0 self._sum: Optional[float] = None @@ -40,11 +40,11 @@ def value(self, val: float) -> None: }) class Avg(AggregateFunction): """Avg aggregate function. - + Calculates the average of numeric values across grouped rows. """ - def __init__(self): + def __init__(self) -> None: super().__init__("avg") self._expected_parameter_count = 1 diff --git a/flowquery-py/src/parsing/functions/collect.py b/flowquery-py/src/parsing/functions/collect.py index 7597faf..ae9ebd9 100644 --- a/flowquery-py/src/parsing/functions/collect.py +++ b/flowquery-py/src/parsing/functions/collect.py @@ -1,17 +1,17 @@ """Collect aggregate function.""" -from typing import Any, Dict, List, Union import json +from typing import Any, Dict, List, Union from .aggregate_function import AggregateFunction -from .reducer_element import ReducerElement from .function_metadata import FunctionDef +from .reducer_element import ReducerElement class CollectReducerElement(ReducerElement): """Reducer element for Collect aggregate function.""" - def __init__(self): + def __init__(self) -> None: self._value: List[Any] = [] @property @@ -26,7 +26,7 @@ def value(self, val: Any) -> None: class DistinctCollectReducerElement(ReducerElement): """Reducer element for Collect aggregate function with DISTINCT.""" - def __init__(self): + def __init__(self) -> None: self._value: Dict[str, Any] = {} @property @@ -51,11 +51,11 @@ def value(self, val: Any) -> None: }) class Collect(AggregateFunction): """Collect aggregate function. - + Collects values into an array across grouped rows. """ - def __init__(self): + def __init__(self) -> None: super().__init__("collect") self._expected_parameter_count = 1 self._distinct: bool = False diff --git a/flowquery-py/src/parsing/functions/function.py b/flowquery-py/src/parsing/functions/function.py index a1890a7..376a2a6 100644 --- a/flowquery-py/src/parsing/functions/function.py +++ b/flowquery-py/src/parsing/functions/function.py @@ -1,16 +1,16 @@ """Base class for all functions in FlowQuery.""" -from typing import List, Optional, Any +from typing import List, Optional from ..ast_node import ASTNode class Function(ASTNode): """Base class for all functions in FlowQuery. - + Functions can have parameters and may support the DISTINCT modifier. Subclasses implement specific function logic. - + Example: func = FunctionFactory.create("sum") func.parameters = [expression1, expression2] @@ -18,7 +18,7 @@ class Function(ASTNode): def __init__(self, name: Optional[str] = None): """Creates a new Function with the given name. - + Args: name: The function name """ @@ -35,10 +35,10 @@ def parameters(self) -> List[ASTNode]: @parameters.setter def parameters(self, nodes: List[ASTNode]) -> None: """Sets the function parameters. - + Args: nodes: Array of AST nodes representing the function arguments - + Raises: ValueError: If the number of parameters doesn't match expected count """ diff --git a/flowquery-py/src/parsing/functions/function_factory.py b/flowquery-py/src/parsing/functions/function_factory.py index 7c9def1..3d4ea0b 100644 --- a/flowquery-py/src/parsing/functions/function_factory.py +++ b/flowquery-py/src/parsing/functions/function_factory.py @@ -2,9 +2,6 @@ from typing import Any, Callable, Dict, List, Optional -from .function import Function -from .async_function import AsyncFunction -from .predicate_function import PredicateFunction from .function_metadata import ( FunctionMetadata, get_function_metadata, @@ -15,23 +12,23 @@ class FunctionFactory: """Factory for creating function instances by name. - + All functions are registered via the @FunctionDef decorator. Maps function names (case-insensitive) to their corresponding implementation classes. Supports built-in functions like sum, avg, collect, range, split, join, etc. - + Example: sum_func = FunctionFactory.create("sum") avg_func = FunctionFactory.create("AVG") """ @staticmethod - def get_async_provider(name: str) -> Optional[Callable]: + def get_async_provider(name: str) -> Optional[Callable[..., Any]]: """Gets an async data provider by name. - + Args: name: The function name (case-insensitive) - + Returns: The async data provider, or None if not found """ @@ -40,10 +37,10 @@ def get_async_provider(name: str) -> Optional[Callable]: @staticmethod def is_async_provider(name: str) -> bool: """Checks if a function name is registered as an async data provider. - + Args: name: The function name (case-insensitive) - + Returns: True if the function is an async data provider """ @@ -52,10 +49,10 @@ def is_async_provider(name: str) -> bool: @staticmethod def get_metadata(name: str) -> Optional[FunctionMetadata]: """Gets metadata for a specific function. - + Args: name: The function name (case-insensitive) - + Returns: The function metadata, or None if not found """ @@ -68,17 +65,17 @@ def list_functions( sync_only: bool = False ) -> List[FunctionMetadata]: """Lists all registered functions with their metadata. - + Args: category: Optional category filter async_only: If True, only return async functions sync_only: If True, only return sync functions - + Returns: Array of function metadata """ result: List[FunctionMetadata] = [] - + for meta in get_registered_function_metadata(): if category and meta.category != category: continue @@ -87,13 +84,13 @@ def list_functions( if sync_only and meta.category == "async": continue result.append(meta) - + return result @staticmethod def list_function_names() -> List[str]: """Lists all registered function names. - + Returns: Array of function names """ @@ -102,7 +99,7 @@ def list_function_names() -> List[str]: @staticmethod def to_json() -> Dict[str, Any]: """Gets all function metadata as a JSON-serializable object for LLM consumption. - + Returns: Object with functions grouped by category """ @@ -111,58 +108,58 @@ def to_json() -> Dict[str, Any]: return {"functions": functions, "categories": categories} @staticmethod - def create(name: str) -> Function: + def create(name: str) -> Any: """Creates a function instance by name. - + Args: name: The function name (case-insensitive) - + Returns: A Function instance of the appropriate type - + Raises: ValueError: If the function name is not registered """ lower_name = name.lower() - + # Check decorator-registered functions decorator_factory = get_registered_function_factory(lower_name) if decorator_factory: return decorator_factory() - + raise ValueError(f"Unknown function: {name}") @staticmethod - def create_predicate(name: str) -> PredicateFunction: + def create_predicate(name: str) -> Any: """Creates a predicate function instance by name. - + Args: name: The function name (case-insensitive) - + Returns: A PredicateFunction instance of the appropriate type - + Raises: ValueError: If the predicate function name is not registered """ lower_name = name.lower() - + decorator_factory = get_registered_function_factory(lower_name, "predicate") if decorator_factory: return decorator_factory() - + raise ValueError(f"Unknown predicate function: {name}") @staticmethod - def create_async(name: str) -> AsyncFunction: + def create_async(name: str) -> Any: """Creates an async function instance by name. - + Args: name: The function name (case-insensitive) - + Returns: An AsyncFunction instance of the appropriate type - + Raises: ValueError: If the async function name is not registered """ diff --git a/flowquery-py/src/parsing/functions/function_metadata.py b/flowquery-py/src/parsing/functions/function_metadata.py index 83b418d..f459fff 100644 --- a/flowquery-py/src/parsing/functions/function_metadata.py +++ b/flowquery-py/src/parsing/functions/function_metadata.py @@ -1,8 +1,7 @@ """Function metadata and decorator for FlowQuery functions.""" -from typing import Any, Callable, Dict, List, Optional, TypedDict, Union from dataclasses import dataclass - +from typing import Any, Callable, Dict, List, Optional, TypedDict, cast # Type definitions FunctionCategory = str # "scalar" | "aggregate" | "predicate" | "async" | string @@ -54,7 +53,7 @@ class FunctionDefOptions(TypedDict, total=False): class FunctionRegistry: """Centralized registry for function metadata, factories, and async providers.""" - + _metadata: Dict[str, FunctionMetadata] = {} _factories: Dict[str, Callable[[], Any]] = {} @@ -71,15 +70,15 @@ def register(cls, constructor: type, options: FunctionDefOptions) -> None: description=options.get('description', ''), category=options.get('category', 'scalar'), parameters=options.get('parameters', []), - output=options.get('output', {'description': '', 'type': 'any'}), + output=cast(OutputSchema, options.get('output', {'description': '', 'type': 'any'})), examples=options.get('examples'), notes=options.get('notes'), ) cls._metadata[registry_key] = metadata if category != 'predicate': - cls._factories[display_name] = lambda c=constructor: c() - cls._factories[registry_key] = lambda c=constructor: c() + cls._factories[display_name] = lambda c=constructor: c() # type: ignore[misc] + cls._factories[registry_key] = lambda c=constructor: c() # type: ignore[misc] @classmethod def get_all_metadata(cls) -> List[FunctionMetadata]: @@ -103,17 +102,17 @@ def get_factory(cls, name: str, category: Optional[str] = None) -> Optional[Call return cls._factories.get(lower_name) -def FunctionDef(options: FunctionDefOptions): +def FunctionDef(options: FunctionDefOptions) -> Callable[[type], type]: """Class decorator that registers function metadata. - + The function name is derived from the class's constructor. - + Args: options: Function metadata (excluding name) - + Returns: Class decorator - + Example: @FunctionDef({ 'description': "Adds two numbers", diff --git a/flowquery-py/src/parsing/functions/functions.py b/flowquery-py/src/parsing/functions/functions.py index 62a3eee..9ac9cc1 100644 --- a/flowquery-py/src/parsing/functions/functions.py +++ b/flowquery-py/src/parsing/functions/functions.py @@ -1,6 +1,6 @@ """Functions introspection function.""" -from typing import Any, Dict, List, Optional +from typing import Any from .function import Function from .function_factory import FunctionFactory @@ -8,10 +8,18 @@ @FunctionDef({ - "description": "Lists all registered functions with their metadata. Useful for discovering available functions and their documentation.", + "description": ( + "Lists all registered functions with their metadata. " + "Useful for discovering available functions and their documentation." + ), "category": "scalar", "parameters": [ - {"name": "category", "description": "Optional category to filter by (e.g., 'aggregation', 'string', 'math')", "type": "string", "required": False} + { + "name": "category", + "description": "Optional category to filter by (e.g., 'aggregation', 'string', 'math')", + "type": "string", + "required": False + } ], "output": { "description": "Array of function metadata objects", @@ -35,17 +43,17 @@ }) class Functions(Function): """Functions introspection function. - + Lists all registered functions with their metadata. """ - def __init__(self): + def __init__(self) -> None: super().__init__("functions") self._expected_parameter_count = None # 0 or 1 parameter def value(self) -> Any: children = self.get_children() - + if len(children) == 0: # Return all functions return FunctionFactory.list_functions() diff --git a/flowquery-py/src/parsing/functions/join.py b/flowquery-py/src/parsing/functions/join.py index 7f4fefc..6988ddb 100644 --- a/flowquery-py/src/parsing/functions/join.py +++ b/flowquery-py/src/parsing/functions/join.py @@ -2,9 +2,9 @@ from typing import Any, List -from .function import Function from ..ast_node import ASTNode from ..expressions.string import String +from .function import Function from .function_metadata import FunctionDef @@ -20,11 +20,11 @@ }) class Join(Function): """Join function. - + Joins an array of strings with a delimiter. """ - def __init__(self): + def __init__(self) -> None: super().__init__("join") self._expected_parameter_count = 2 diff --git a/flowquery-py/src/parsing/functions/keys.py b/flowquery-py/src/parsing/functions/keys.py index bfecc47..d1eb6e5 100644 --- a/flowquery-py/src/parsing/functions/keys.py +++ b/flowquery-py/src/parsing/functions/keys.py @@ -1,6 +1,6 @@ """Keys function.""" -from typing import Any, List +from typing import Any from .function import Function from .function_metadata import FunctionDef @@ -17,11 +17,11 @@ }) class Keys(Function): """Keys function. - + Returns the keys of an object (associative array) as an array. """ - def __init__(self): + def __init__(self) -> None: super().__init__("keys") self._expected_parameter_count = 1 diff --git a/flowquery-py/src/parsing/functions/predicate_function.py b/flowquery-py/src/parsing/functions/predicate_function.py index ca2050f..103eb58 100644 --- a/flowquery-py/src/parsing/functions/predicate_function.py +++ b/flowquery-py/src/parsing/functions/predicate_function.py @@ -1,12 +1,13 @@ """Base class for predicate functions in FlowQuery.""" -from typing import Any, Optional +from typing import TYPE_CHECKING, Any, Optional from ..ast_node import ASTNode -from ..expressions.expression import Expression -from ..expressions.reference import Reference from .value_holder import ValueHolder +if TYPE_CHECKING: + pass + class PredicateFunction(ASTNode): """Base class for predicate functions.""" @@ -21,7 +22,7 @@ def name(self) -> str: return self._name @property - def reference(self) -> Reference: + def reference(self) -> ASTNode: return self.first_child() @property @@ -29,12 +30,12 @@ def array(self) -> ASTNode: return self.get_children()[1].first_child() @property - def _return(self) -> Expression: + def _return(self) -> ASTNode: return self.get_children()[2] @property - def where(self) -> Optional['Where']: - from ..operations.where import Where + def where(self) -> Optional[ASTNode]: + # Import at runtime to avoid circular dependency if len(self.get_children()) == 4: return self.get_children()[3] return None diff --git a/flowquery-py/src/parsing/functions/predicate_sum.py b/flowquery-py/src/parsing/functions/predicate_sum.py index 05510e2..a180244 100644 --- a/flowquery-py/src/parsing/functions/predicate_sum.py +++ b/flowquery-py/src/parsing/functions/predicate_sum.py @@ -1,13 +1,16 @@ """PredicateSum function.""" -from typing import Any, List, Optional +from typing import Any, Optional -from .predicate_function import PredicateFunction from .function_metadata import FunctionDef +from .predicate_function import PredicateFunction @FunctionDef({ - "description": "Calculates the sum of values in an array with optional filtering. Uses list comprehension syntax: sum(variable IN array [WHERE condition] | expression)", + "description": ( + "Calculates the sum of values in an array with optional filtering. " + "Uses list comprehension syntax: sum(variable IN array [WHERE condition] | expression)" + ), "category": "predicate", "parameters": [ {"name": "variable", "description": "Variable name to bind each element", "type": "string"}, @@ -23,19 +26,21 @@ }) class PredicateSum(PredicateFunction): """PredicateSum function. - + Calculates the sum of values in an array with optional filtering. """ - def __init__(self): + def __init__(self) -> None: super().__init__("sum") def value(self) -> Any: - self.reference.referred = self._value_holder + ref = self.reference + if hasattr(ref, 'referred'): + ref.referred = self._value_holder array = self.array.value() if array is None or not isinstance(array, list): raise ValueError("Invalid array for sum function") - + _sum: Optional[Any] = None for item in array: self._value_holder.holder = item diff --git a/flowquery-py/src/parsing/functions/rand.py b/flowquery-py/src/parsing/functions/rand.py index b3178c5..8cd6d77 100644 --- a/flowquery-py/src/parsing/functions/rand.py +++ b/flowquery-py/src/parsing/functions/rand.py @@ -16,11 +16,11 @@ }) class Rand(Function): """Rand function. - + Generates a random number between 0 and 1. """ - def __init__(self): + def __init__(self) -> None: super().__init__("rand") self._expected_parameter_count = 0 diff --git a/flowquery-py/src/parsing/functions/range_.py b/flowquery-py/src/parsing/functions/range_.py index 9239107..94cee27 100644 --- a/flowquery-py/src/parsing/functions/range_.py +++ b/flowquery-py/src/parsing/functions/range_.py @@ -1,6 +1,6 @@ """Range function.""" -from typing import Any, List +from typing import Any from .function import Function from .function_metadata import FunctionDef @@ -13,16 +13,21 @@ {"name": "start", "description": "Starting number (inclusive)", "type": "number"}, {"name": "end", "description": "Ending number (inclusive)", "type": "number"} ], - "output": {"description": "Array of integers from start to end", "type": "array", "items": {"type": "number"}, "example": [1, 2, 3, 4, 5]}, + "output": { + "description": "Array of integers from start to end", + "type": "array", + "items": {"type": "number"}, + "example": [1, 2, 3, 4, 5] + }, "examples": ["WITH range(1, 5) AS nums RETURN nums"] }) class Range(Function): """Range function. - + Generates an array of sequential integers. """ - def __init__(self): + def __init__(self) -> None: super().__init__("range") self._expected_parameter_count = 2 diff --git a/flowquery-py/src/parsing/functions/replace.py b/flowquery-py/src/parsing/functions/replace.py index 68b345a..56266eb 100644 --- a/flowquery-py/src/parsing/functions/replace.py +++ b/flowquery-py/src/parsing/functions/replace.py @@ -20,11 +20,11 @@ }) class Replace(Function): """Replace function. - + Replaces occurrences of a pattern in a string. """ - def __init__(self): + def __init__(self) -> None: super().__init__("replace") self._expected_parameter_count = 3 diff --git a/flowquery-py/src/parsing/functions/round_.py b/flowquery-py/src/parsing/functions/round_.py index 9c51f8e..0621261 100644 --- a/flowquery-py/src/parsing/functions/round_.py +++ b/flowquery-py/src/parsing/functions/round_.py @@ -17,11 +17,11 @@ }) class Round(Function): """Round function. - + Rounds a number to the nearest integer. """ - def __init__(self): + def __init__(self) -> None: super().__init__("round") self._expected_parameter_count = 1 diff --git a/flowquery-py/src/parsing/functions/size.py b/flowquery-py/src/parsing/functions/size.py index d985c10..0ea9721 100644 --- a/flowquery-py/src/parsing/functions/size.py +++ b/flowquery-py/src/parsing/functions/size.py @@ -17,11 +17,11 @@ }) class Size(Function): """Size function. - + Returns the length of an array or string. """ - def __init__(self): + def __init__(self) -> None: super().__init__("size") self._expected_parameter_count = 1 diff --git a/flowquery-py/src/parsing/functions/split.py b/flowquery-py/src/parsing/functions/split.py index 0d9c28d..dfab1cb 100644 --- a/flowquery-py/src/parsing/functions/split.py +++ b/flowquery-py/src/parsing/functions/split.py @@ -2,9 +2,9 @@ from typing import Any, List -from .function import Function from ..ast_node import ASTNode from ..expressions.string import String +from .function import Function from .function_metadata import FunctionDef @@ -15,16 +15,21 @@ {"name": "text", "description": "String to split", "type": "string"}, {"name": "delimiter", "description": "Delimiter to split by", "type": "string"} ], - "output": {"description": "Array of string parts", "type": "array", "items": {"type": "string"}, "example": ["a", "b", "c"]}, + "output": { + "description": "Array of string parts", + "type": "array", + "items": {"type": "string"}, + "example": ["a", "b", "c"] + }, "examples": ["WITH 'a,b,c' AS s RETURN split(s, ',')"] }) class Split(Function): """Split function. - + Splits a string into an array by a delimiter. """ - def __init__(self): + def __init__(self) -> None: super().__init__("split") self._expected_parameter_count = 2 diff --git a/flowquery-py/src/parsing/functions/stringify.py b/flowquery-py/src/parsing/functions/stringify.py index 3342093..160e764 100644 --- a/flowquery-py/src/parsing/functions/stringify.py +++ b/flowquery-py/src/parsing/functions/stringify.py @@ -3,9 +3,9 @@ import json from typing import Any, List -from .function import Function from ..ast_node import ASTNode from ..expressions.number import Number +from .function import Function from .function_metadata import FunctionDef @@ -20,11 +20,11 @@ }) class Stringify(Function): """Stringify function. - + Converts a value to its JSON string representation. """ - def __init__(self): + def __init__(self) -> None: super().__init__("stringify") self._expected_parameter_count = 2 diff --git a/flowquery-py/src/parsing/functions/sum.py b/flowquery-py/src/parsing/functions/sum.py index 6419824..209cac9 100644 --- a/flowquery-py/src/parsing/functions/sum.py +++ b/flowquery-py/src/parsing/functions/sum.py @@ -3,14 +3,14 @@ from typing import Any from .aggregate_function import AggregateFunction -from .reducer_element import ReducerElement from .function_metadata import FunctionDef +from .reducer_element import ReducerElement class SumReducerElement(ReducerElement): """Reducer element for Sum aggregate function.""" - def __init__(self): + def __init__(self) -> None: self._value: Any = None @property @@ -36,11 +36,11 @@ def value(self, val: Any) -> None: }) class Sum(AggregateFunction): """Sum aggregate function. - + Calculates the sum of numeric values across grouped rows. """ - def __init__(self): + def __init__(self) -> None: super().__init__("sum") self._expected_parameter_count = 1 diff --git a/flowquery-py/src/parsing/functions/to_json.py b/flowquery-py/src/parsing/functions/to_json.py index 257825c..1fb6a9e 100644 --- a/flowquery-py/src/parsing/functions/to_json.py +++ b/flowquery-py/src/parsing/functions/to_json.py @@ -18,11 +18,11 @@ }) class ToJson(Function): """ToJson function. - + Parses a JSON string into an object. """ - def __init__(self): + def __init__(self) -> None: super().__init__("tojson") self._expected_parameter_count = 1 diff --git a/flowquery-py/src/parsing/functions/type_.py b/flowquery-py/src/parsing/functions/type_.py index 20daa51..1b0f739 100644 --- a/flowquery-py/src/parsing/functions/type_.py +++ b/flowquery-py/src/parsing/functions/type_.py @@ -21,17 +21,17 @@ }) class Type(Function): """Type function. - + Returns the type of a value as a string. """ - def __init__(self): + def __init__(self) -> None: super().__init__("type") self._expected_parameter_count = 1 def value(self) -> Any: val = self.get_children()[0].value() - + if val is None: return "null" if isinstance(val, list): diff --git a/flowquery-py/src/parsing/functions/value_holder.py b/flowquery-py/src/parsing/functions/value_holder.py index 07cc90a..a990c19 100644 --- a/flowquery-py/src/parsing/functions/value_holder.py +++ b/flowquery-py/src/parsing/functions/value_holder.py @@ -8,7 +8,7 @@ class ValueHolder(ASTNode): """Holds a value that can be set and retrieved.""" - def __init__(self): + def __init__(self) -> None: super().__init__() self._holder: Any = None diff --git a/flowquery-py/src/parsing/logic/__init__.py b/flowquery-py/src/parsing/logic/__init__.py index 6dcde2e..72341f9 100644 --- a/flowquery-py/src/parsing/logic/__init__.py +++ b/flowquery-py/src/parsing/logic/__init__.py @@ -1,10 +1,10 @@ """Logic module for FlowQuery parsing.""" from .case import Case -from .when import When -from .then import Then from .else_ import Else from .end import End +from .then import Then +from .when import When __all__ = [ "Case", diff --git a/flowquery-py/src/parsing/logic/case.py b/flowquery-py/src/parsing/logic/case.py index 92ba8a1..4f6a020 100644 --- a/flowquery-py/src/parsing/logic/case.py +++ b/flowquery-py/src/parsing/logic/case.py @@ -4,7 +4,6 @@ from ..ast_node import ASTNode from .when import When -from .then import Then class Case(ASTNode): diff --git a/flowquery-py/src/parsing/logic/when.py b/flowquery-py/src/parsing/logic/when.py index 94b8707..8c9dbea 100644 --- a/flowquery-py/src/parsing/logic/when.py +++ b/flowquery-py/src/parsing/logic/when.py @@ -1,10 +1,12 @@ """Represents a WHEN clause in a CASE expression.""" +from typing import Any + from ..ast_node import ASTNode class When(ASTNode): """Represents a WHEN clause in a CASE expression.""" - def value(self) -> bool: + def value(self) -> Any: return self.get_children()[0].value() diff --git a/flowquery-py/src/parsing/operations/__init__.py b/flowquery-py/src/parsing/operations/__init__.py index ea03767..a9bf2e0 100644 --- a/flowquery-py/src/parsing/operations/__init__.py +++ b/flowquery-py/src/parsing/operations/__init__.py @@ -1,20 +1,20 @@ """Operations module for FlowQuery parsing.""" -from .operation import Operation -from .projection import Projection -from .return_op import Return -from .with_op import With -from .unwind import Unwind -from .load import Load -from .where import Where -from .limit import Limit from .aggregated_return import AggregatedReturn from .aggregated_with import AggregatedWith from .call import Call -from .group_by import GroupBy -from .match import Match from .create_node import CreateNode from .create_relationship import CreateRelationship +from .group_by import GroupBy +from .limit import Limit +from .load import Load +from .match import Match +from .operation import Operation +from .projection import Projection +from .return_op import Return +from .unwind import Unwind +from .where import Where +from .with_op import With __all__ = [ "Operation", diff --git a/flowquery-py/src/parsing/operations/aggregated_return.py b/flowquery-py/src/parsing/operations/aggregated_return.py index 3b37daa..6fd5676 100644 --- a/flowquery-py/src/parsing/operations/aggregated_return.py +++ b/flowquery-py/src/parsing/operations/aggregated_return.py @@ -1,16 +1,14 @@ -"""Represents an aggregated RETURN operation.""" - from typing import Any, Dict, List -from .return_op import Return +from ..ast_node import ASTNode from .group_by import GroupBy -from ..expressions.expression import Expression +from .return_op import Return class AggregatedReturn(Return): """Represents an aggregated RETURN operation that groups and reduces values.""" - def __init__(self, expressions): + def __init__(self, expressions: List[ASTNode]) -> None: super().__init__(expressions) self._group_by = GroupBy(self.children) diff --git a/flowquery-py/src/parsing/operations/aggregated_with.py b/flowquery-py/src/parsing/operations/aggregated_with.py index 11fdd7d..4de2bb9 100644 --- a/flowquery-py/src/parsing/operations/aggregated_with.py +++ b/flowquery-py/src/parsing/operations/aggregated_with.py @@ -1,14 +1,14 @@ -"""Represents an aggregated WITH operation.""" +from typing import List -from .return_op import Return +from ..ast_node import ASTNode from .group_by import GroupBy -from ..expressions.expression import Expression +from .return_op import Return class AggregatedWith(Return): """Represents an aggregated WITH operation that groups and reduces values.""" - def __init__(self, expressions): + def __init__(self, expressions: List[ASTNode]) -> None: super().__init__(expressions) self._group_by = GroupBy(self.children) diff --git a/flowquery-py/src/parsing/operations/call.py b/flowquery-py/src/parsing/operations/call.py index 26a415a..ae22ab0 100644 --- a/flowquery-py/src/parsing/operations/call.py +++ b/flowquery-py/src/parsing/operations/call.py @@ -2,19 +2,18 @@ from typing import Any, Dict, List, Optional -from ..expressions.expression import Expression +from ..ast_node import ASTNode from ..expressions.expression_map import ExpressionMap from ..functions.async_function import AsyncFunction from .projection import Projection - DEFAULT_VARIABLE_NAME = "value" class Call(Projection): """Represents a CALL operation for invoking async functions.""" - def __init__(self): + def __init__(self) -> None: super().__init__([]) self._function: Optional[AsyncFunction] = None self._map = ExpressionMap() @@ -29,13 +28,13 @@ def function(self, async_function: AsyncFunction) -> None: self._function = async_function @property - def yielded(self) -> List[Expression]: + def yielded(self) -> List[ASTNode]: return self.children @yielded.setter - def yielded(self, expressions: List[Expression]) -> None: + def yielded(self, expressions: List[ASTNode]) -> None: self.children = expressions - self._map.set_map(expressions) + self._map.set_map(expressions) # ExpressionMap accepts list of expressions @property def has_yield(self) -> bool: @@ -44,7 +43,7 @@ def has_yield(self) -> bool: async def run(self) -> None: if self._function is None: raise ValueError("No function set for Call operation.") - + args = self._function.get_arguments() async for item in self._function.generate(*args): if not self.is_last: diff --git a/flowquery-py/src/parsing/operations/create_node.py b/flowquery-py/src/parsing/operations/create_node.py index 242b607..1d2e927 100644 --- a/flowquery-py/src/parsing/operations/create_node.py +++ b/flowquery-py/src/parsing/operations/create_node.py @@ -2,20 +2,22 @@ from typing import Any, Dict, List -from .operation import Operation +from ...graph.database import Database +from ...graph.node import Node from ..ast_node import ASTNode +from .operation import Operation class CreateNode(Operation): """Represents a CREATE operation for creating virtual nodes.""" - def __init__(self, node, statement: ASTNode): + def __init__(self, node: Node, statement: ASTNode) -> None: super().__init__() self._node = node self._statement = statement @property - def node(self): + def node(self) -> Node: return self._node @property @@ -25,7 +27,6 @@ def statement(self) -> ASTNode: async def run(self) -> None: if self._node is None: raise ValueError("Node is null") - from ...graph.database import Database db = Database.get_instance() db.add_node(self._node, self._statement) diff --git a/flowquery-py/src/parsing/operations/create_relationship.py b/flowquery-py/src/parsing/operations/create_relationship.py index 4b7a133..e5a6e64 100644 --- a/flowquery-py/src/parsing/operations/create_relationship.py +++ b/flowquery-py/src/parsing/operations/create_relationship.py @@ -2,20 +2,22 @@ from typing import Any, Dict, List -from .operation import Operation +from ...graph.database import Database +from ...graph.relationship import Relationship from ..ast_node import ASTNode +from .operation import Operation class CreateRelationship(Operation): """Represents a CREATE operation for creating virtual relationships.""" - def __init__(self, relationship, statement: ASTNode): + def __init__(self, relationship: Relationship, statement: ASTNode) -> None: super().__init__() self._relationship = relationship self._statement = statement @property - def relationship(self): + def relationship(self) -> Relationship: return self._relationship @property @@ -25,7 +27,6 @@ def statement(self) -> ASTNode: async def run(self) -> None: if self._relationship is None: raise ValueError("Relationship is null") - from ...graph.database import Database db = Database.get_instance() db.add_relationship(self._relationship, self._statement) diff --git a/flowquery-py/src/parsing/operations/group_by.py b/flowquery-py/src/parsing/operations/group_by.py index d4136b0..afb8cdd 100644 --- a/flowquery-py/src/parsing/operations/group_by.py +++ b/flowquery-py/src/parsing/operations/group_by.py @@ -2,7 +2,7 @@ from typing import Any, Dict, Generator, List, Optional -from ..expressions.expression import Expression +from ..ast_node import ASTNode from ..functions.aggregate_function import AggregateFunction from ..functions.reducer_element import ReducerElement from .projection import Projection @@ -36,13 +36,13 @@ def elements(self, elements: List[ReducerElement]) -> None: class GroupBy(Projection): """Implements grouping and aggregation for FlowQuery operations.""" - def __init__(self, expressions: List[Expression]): + def __init__(self, expressions: List[ASTNode]) -> None: super().__init__(expressions) self._root = GroupByNode() self._current = self._root - self._mappers: Optional[List[Expression]] = None + self._mappers: Optional[List[Any]] = None self._reducers: Optional[List[AggregateFunction]] = None - self._where = None + self._where: Optional[ASTNode] = None async def run(self) -> None: self._reset_tree() @@ -71,18 +71,19 @@ def _reduce(self) -> None: if self._current.elements is None: self._current.elements = [reducer.element() for reducer in self.reducers] elements = self._current.elements - for i, reducer in enumerate(self.reducers): - reducer.reduce(elements[i]) + if elements: + for i, reducer in enumerate(self.reducers): + reducer.reduce(elements[i]) @property - def mappers(self) -> List[Expression]: + def mappers(self) -> List[Any]: if self._mappers is None: self._mappers = list(self._generate_mappers()) return self._mappers - def _generate_mappers(self) -> Generator[Expression, None, None]: + def _generate_mappers(self) -> Generator[Any, None, None]: for expression, _ in self.expressions(): - if expression.mappable(): + if hasattr(expression, 'mappable') and expression.mappable(): yield expression @property @@ -90,17 +91,18 @@ def reducers(self) -> List[AggregateFunction]: if self._reducers is None: self._reducers = [] for child in self.children: - self._reducers.extend(child.reducers()) + if hasattr(child, 'reducers'): + self._reducers.extend(child.reducers()) return self._reducers def generate_results( - self, - mapper_index: int = 0, + self, + mapper_index: int = 0, node: Optional[GroupByNode] = None ) -> Generator[Dict[str, Any], None, None]: if node is None: node = self._root - + if len(node.children) > 0: for child in node.children.values(): self.mappers[mapper_index].overridden = child.value @@ -116,15 +118,15 @@ def generate_results( yield record @property - def where(self): + def where(self) -> Optional[ASTNode]: return self._where @where.setter - def where(self, where) -> None: + def where(self, where: Optional[ASTNode]) -> None: self._where = where @property - def where_condition(self) -> bool: + def where_condition(self) -> Any: if self._where is None: return True return self._where.value() diff --git a/flowquery-py/src/parsing/operations/load.py b/flowquery-py/src/parsing/operations/load.py index e2201e2..a3d6196 100644 --- a/flowquery-py/src/parsing/operations/load.py +++ b/flowquery-py/src/parsing/operations/load.py @@ -3,24 +3,31 @@ import json from typing import Any, Dict, Optional -from .operation import Operation +import aiohttp + +from ..ast_node import ASTNode +from ..components.headers import Headers +from ..components.json import JSON as JSONComponent +from ..components.post import Post +from ..components.text import Text from ..functions.async_function import AsyncFunction +from .operation import Operation class Load(Operation): """Represents a LOAD operation that fetches data from external sources.""" - def __init__(self): + def __init__(self) -> None: super().__init__() self._value: Any = None @property - def type(self): + def type(self) -> ASTNode: """Gets the data type (JSON, CSV, or Text).""" return self.children[0] @property - def from_component(self): + def from_component(self) -> ASTNode: """Gets the From component which contains either a URL expression or an AsyncFunction.""" return self.children[1] @@ -36,19 +43,17 @@ def async_function(self) -> Optional[AsyncFunction]: return child if isinstance(child, AsyncFunction) else None @property - def from_(self) -> str: + def from_(self) -> Any: return self.children[1].value() @property def headers(self) -> Dict[str, str]: - from ..components.headers import Headers if self.child_count() > 2 and isinstance(self.children[2], Headers): return self.children[2].value() or {} return {} @property - def payload(self): - from ..components.post import Post + def payload(self) -> Optional[ASTNode]: post = None if self.child_count() > 2 and isinstance(self.children[2], Post): post = self.children[2] @@ -86,26 +91,22 @@ async def _load_from_function(self) -> None: async def _load_from_url(self) -> None: """Loads data from a URL source.""" - import aiohttp - from ..components.json import JSON as JSONComponent - from ..components.text import Text - async with aiohttp.ClientSession() as session: options = self._options() method = options.pop("method") headers = options.pop("headers", {}) body = options.pop("body", None) - + # Set Accept-Encoding to support common compression formats # Note: brotli (br) is excluded due to API incompatibility between # aiohttp 3.13+ and the brotli package's Decompressor.decompress() method if "Accept-Encoding" not in headers: headers["Accept-Encoding"] = "gzip, deflate" - + async with session.request( - method, - self.from_, - headers=headers, + method, + self.from_, + headers=headers, data=body ) as response: if isinstance(self.type, JSONComponent): @@ -114,7 +115,7 @@ async def _load_from_url(self) -> None: data = await response.text() else: data = await response.text() - + if isinstance(data, list): for item in data: self._value = item @@ -139,7 +140,8 @@ async def run(self) -> None: try: await self.load() except Exception as e: - source = self.async_function.name if self.is_async_function else self.from_ + async_func = self.async_function + source = async_func.name if async_func else self.from_ raise RuntimeError(f"Failed to load data from {source}. Error: {e}") def value(self) -> Any: diff --git a/flowquery-py/src/parsing/operations/match.py b/flowquery-py/src/parsing/operations/match.py index 954494b..a7157b7 100644 --- a/flowquery-py/src/parsing/operations/match.py +++ b/flowquery-py/src/parsing/operations/match.py @@ -1,29 +1,30 @@ """Represents a MATCH operation for graph pattern matching.""" -from typing import List +from typing import List, Optional +from ...graph.pattern import Pattern +from ...graph.patterns import Patterns from .operation import Operation class Match(Operation): """Represents a MATCH operation for graph pattern matching.""" - def __init__(self, patterns=None): + def __init__(self, patterns: Optional[List[Pattern]] = None) -> None: super().__init__() - from ...graph.patterns import Patterns self._patterns = Patterns(patterns or []) @property - def patterns(self): + def patterns(self) -> List[Pattern]: return self._patterns.patterns if self._patterns else [] async def run(self) -> None: """Executes the match operation by chaining the patterns together.""" await self._patterns.initialize() - - async def to_do_next(): + + async def to_do_next() -> None: if self.next: await self.next.run() - + self._patterns.to_do_next = to_do_next await self._patterns.traverse() diff --git a/flowquery-py/src/parsing/operations/operation.py b/flowquery-py/src/parsing/operations/operation.py index dc1ced8..4f2b5cc 100644 --- a/flowquery-py/src/parsing/operations/operation.py +++ b/flowquery-py/src/parsing/operations/operation.py @@ -8,12 +8,12 @@ class Operation(ASTNode, ABC): """Base class for all FlowQuery operations. - + Operations represent the main statements in FlowQuery (WITH, UNWIND, RETURN, LOAD, WHERE). They form a linked list structure and can be executed sequentially. """ - def __init__(self): + def __init__(self) -> None: super().__init__() self._previous: Optional[Operation] = None self._next: Optional[Operation] = None @@ -46,7 +46,7 @@ def is_last(self) -> bool: async def run(self) -> None: """Executes this operation. Must be implemented by subclasses. - + Raises: NotImplementedError: If not implemented by subclass """ diff --git a/flowquery-py/src/parsing/operations/projection.py b/flowquery-py/src/parsing/operations/projection.py index e14c10d..7acb52c 100644 --- a/flowquery-py/src/parsing/operations/projection.py +++ b/flowquery-py/src/parsing/operations/projection.py @@ -1,21 +1,21 @@ """Base class for projection operations.""" -from typing import Generator, List, Tuple, Optional +from typing import Any, Generator, List, Tuple -from ..expressions.expression import Expression +from ..ast_node import ASTNode from .operation import Operation class Projection(Operation): """Base class for operations that project expressions.""" - def __init__(self, expressions: List[Expression]): + def __init__(self, expressions: List[ASTNode]): super().__init__() self.children = expressions - def expressions(self) -> Generator[Tuple[Expression, str], None, None]: + def expressions(self) -> Generator[Tuple[Any, str], None, None]: """Yields tuples of (expression, alias) for all child expressions.""" for i, child in enumerate(self.children): - expression: Expression = child - alias = expression.alias or f"expr{i}" + expression = child + alias = getattr(expression, 'alias', None) or f"expr{i}" yield (expression, alias) diff --git a/flowquery-py/src/parsing/operations/return_op.py b/flowquery-py/src/parsing/operations/return_op.py index 914550b..c404705 100644 --- a/flowquery-py/src/parsing/operations/return_op.py +++ b/flowquery-py/src/parsing/operations/return_op.py @@ -1,28 +1,32 @@ """Represents a RETURN operation that produces the final query results.""" import copy -from typing import Any, Dict, List, Optional +from typing import TYPE_CHECKING, Any, Dict, List, Optional +from ..ast_node import ASTNode from .projection import Projection +if TYPE_CHECKING: + from .where import Where + class Return(Projection): """Represents a RETURN operation that produces the final query results. - + The RETURN operation evaluates expressions and collects them into result records. It can optionally have a WHERE clause to filter results. - + Example: # RETURN x, y WHERE x > 0 """ - def __init__(self, expressions): + def __init__(self, expressions: List[ASTNode]) -> None: super().__init__(expressions) self._where: Optional['Where'] = None self._results: List[Dict[str, Any]] = [] @property - def where(self) -> bool: + def where(self) -> Any: if self._where is None: return True return self._where.value() diff --git a/flowquery-py/src/parsing/operations/unwind.py b/flowquery-py/src/parsing/operations/unwind.py index 8505150..6d763fb 100644 --- a/flowquery-py/src/parsing/operations/unwind.py +++ b/flowquery-py/src/parsing/operations/unwind.py @@ -2,6 +2,7 @@ from typing import Any +from ..ast_node import ASTNode from ..expressions.expression import Expression from .operation import Operation @@ -15,11 +16,11 @@ def __init__(self, expression: Expression): self.add_child(expression) @property - def expression(self) -> Expression: + def expression(self) -> ASTNode: return self.children[0] @property - def as_(self) -> str: + def as_(self) -> Any: return self.children[1].value() async def run(self) -> None: diff --git a/flowquery-py/src/parsing/operations/where.py b/flowquery-py/src/parsing/operations/where.py index 6ddb7f9..7034eae 100644 --- a/flowquery-py/src/parsing/operations/where.py +++ b/flowquery-py/src/parsing/operations/where.py @@ -2,23 +2,24 @@ from typing import Any +from ..ast_node import ASTNode from ..expressions.expression import Expression from .operation import Operation class Where(Operation): """Represents a WHERE operation that filters data based on a condition. - + The WHERE operation evaluates a boolean expression and only continues execution to the next operation if the condition is true. - + Example: # RETURN x WHERE x > 0 """ def __init__(self, expression: Expression): """Creates a new WHERE operation with the given condition. - + Args: expression: The boolean expression to evaluate """ @@ -26,13 +27,14 @@ def __init__(self, expression: Expression): self.add_child(expression) @property - def expression(self) -> Expression: + def expression(self) -> ASTNode: return self.children[0] async def run(self) -> None: - for pattern in self.expression.patterns(): - await pattern.fetch_data() - await pattern.evaluate() + if hasattr(self.expression, 'patterns'): + for pattern in self.expression.patterns(): + await pattern.fetch_data() + await pattern.evaluate() if self.expression.value(): if self.next: await self.next.run() diff --git a/flowquery-py/src/parsing/operations/with_op.py b/flowquery-py/src/parsing/operations/with_op.py index e796111..e1e036a 100644 --- a/flowquery-py/src/parsing/operations/with_op.py +++ b/flowquery-py/src/parsing/operations/with_op.py @@ -5,10 +5,10 @@ class With(Projection): """Represents a WITH operation that defines variables or intermediate results. - + The WITH operation creates named expressions that can be referenced later in the query. It passes control to the next operation in the chain. - + Example: # WITH x = 1, y = 2 RETURN x + y """ diff --git a/flowquery-py/src/parsing/parser.py b/flowquery-py/src/parsing/parser.py index 028c664..2a7037f 100644 --- a/flowquery-py/src/parsing/parser.py +++ b/flowquery-py/src/parsing/parser.py @@ -1,18 +1,26 @@ """Main parser for FlowQuery statements.""" -from typing import Dict, Iterator, List, Optional +import sys +from typing import Dict, Iterator, List, Optional, cast +from ..graph.hops import Hops +from ..graph.node import Node +from ..graph.node_reference import NodeReference +from ..graph.pattern import Pattern +from ..graph.pattern_expression import PatternExpression +from ..graph.relationship import Relationship +from ..graph.relationship_reference import RelationshipReference from ..tokenization.token import Token from ..utils.object_utils import ObjectUtils from .alias import Alias from .alias_option import AliasOption from .ast_node import ASTNode from .base_parser import BaseParser -from .context import Context from .components.from_ import From from .components.headers import Headers from .components.null import Null from .components.post import Post +from .context import Context from .data_structures.associative_array import AssociativeArray from .data_structures.json_array import JSONArray from .data_structures.key_value_pair import KeyValuePair @@ -30,12 +38,14 @@ from .functions.function_factory import FunctionFactory from .functions.predicate_function import PredicateFunction from .logic.case import Case -from .logic.when import When -from .logic.then import Then from .logic.else_ import Else +from .logic.then import Then +from .logic.when import When from .operations.aggregated_return import AggregatedReturn from .operations.aggregated_with import AggregatedWith from .operations.call import Call +from .operations.create_node import CreateNode +from .operations.create_relationship import CreateRelationship from .operations.limit import Limit from .operations.load import Load from .operations.match import Match @@ -44,22 +54,15 @@ from .operations.unwind import Unwind from .operations.where import Where from .operations.with_op import With -from ..graph.node import Node -from ..graph.node_reference import NodeReference -from ..graph.pattern import Pattern -from ..graph.pattern_expression import PatternExpression -from ..graph.relationship import Relationship -from .operations.create_node import CreateNode -from .operations.create_relationship import CreateRelationship class Parser(BaseParser): """Main parser for FlowQuery statements. - + Parses FlowQuery declarative query language statements into an Abstract Syntax Tree (AST). Supports operations like WITH, UNWIND, RETURN, LOAD, WHERE, and LIMIT, along with expressions, functions, data structures, and logical constructs. - + Example: parser = Parser() ast = parser.parse("unwind [1, 2, 3, 4, 5] as num return num") @@ -73,13 +76,13 @@ def __init__(self, tokens: Optional[List[Token]] = None): def parse(self, statement: str) -> ASTNode: """Parses a FlowQuery statement into an Abstract Syntax Tree. - + Args: statement: The FlowQuery statement to parse - + Returns: The root AST node containing the parsed structure - + Raises: ValueError: If the statement is malformed or contains syntax errors """ @@ -90,32 +93,32 @@ def _parse_tokenized(self, is_sub_query: bool = False) -> ASTNode: root = ASTNode() previous: Optional[Operation] = None operation: Optional[Operation] = None - + while not self.token.is_eof(): if root.child_count() > 0: self._expect_and_skip_whitespace_and_comments() else: self._skip_whitespace_and_comments() - + operation = self._parse_operation() if operation is None and not is_sub_query: raise ValueError("Expected one of WITH, UNWIND, RETURN, LOAD, OR CALL") elif operation is None and is_sub_query: return root - + if self._returns > 1: raise ValueError("Only one RETURN statement is allowed") - + if isinstance(previous, Call) and not previous.has_yield: raise ValueError( "CALL operations must have a YIELD clause unless they are the last operation" ) - + if previous is not None: previous.add_sibling(operation) else: root.add_child(operation) - + where = self._parse_where() if where is not None: if isinstance(operation, Return): @@ -123,17 +126,17 @@ def _parse_tokenized(self, is_sub_query: bool = False) -> ASTNode: else: operation.add_sibling(where) operation = where - + limit = self._parse_limit() if limit is not None: operation.add_sibling(limit) operation = limit - + previous = operation - + if not isinstance(operation, (Return, Call, CreateNode, CreateRelationship)): raise ValueError("Last statement must be a RETURN, WHERE, CALL, or CREATE statement") - + return root def _parse_operation(self) -> Optional[Operation]: @@ -156,7 +159,7 @@ def _parse_with(self) -> Optional[With]: if len(expressions) == 0: raise ValueError("Expected expression") if any(expr.has_reducers() for expr in expressions): - return AggregatedWith(expressions) + return AggregatedWith(expressions) # type: ignore[return-value] return With(expressions) def _parse_unwind(self) -> Optional[Unwind]: @@ -228,7 +231,7 @@ def _parse_load(self) -> Optional[Load]: self._expect_and_skip_whitespace_and_comments() from_node = From() load.add_child(from_node) - + # Check if source is async function async_func = self._parse_async_function() if async_func is not None: @@ -238,7 +241,7 @@ def _parse_load(self) -> Optional[Load]: if expression is None: raise ValueError("Expected expression or async function") from_node.add_child(expression) - + self._expect_and_skip_whitespace_and_comments() if self.token.is_headers(): headers = Headers() @@ -250,7 +253,7 @@ def _parse_load(self) -> Optional[Load]: headers.add_child(header) load.add_child(headers) self._expect_and_skip_whitespace_and_comments() - + if self.token.is_post(): post = Post() self.set_next_token() @@ -261,7 +264,7 @@ def _parse_load(self) -> Optional[Load]: post.add_child(payload) load.add_child(post) self._expect_and_skip_whitespace_and_comments() - + alias = self._parse_alias() if alias is not None: load.add_child(alias) @@ -288,7 +291,7 @@ def _parse_call(self) -> Optional[Call]: expressions = list(self._parse_expressions(AliasOption.OPTIONAL)) if len(expressions) == 0: raise ValueError("Expected at least one expression") - call.yielded = expressions + call.yielded = expressions # type: ignore[assignment] return call def _parse_match(self) -> Optional[Match]: @@ -311,11 +314,11 @@ def _parse_create(self) -> Optional[Operation]: raise ValueError("Expected VIRTUAL") self.set_next_token() self._expect_and_skip_whitespace_and_comments() - + node = self._parse_node() if node is None: raise ValueError("Expected node definition") - + relationship: Optional[Relationship] = None if self.token.is_subtract() and self.peek() and self.peek().is_opening_bracket(): self.set_next_token() # skip - @@ -341,17 +344,17 @@ def _parse_create(self) -> Optional[Operation]: raise ValueError("Expected target node definition") relationship = Relationship() relationship.type = rel_type - + self._expect_and_skip_whitespace_and_comments() if not self.token.is_as(): raise ValueError("Expected AS") self.set_next_token() self._expect_and_skip_whitespace_and_comments() - + query = self._parse_sub_query() if query is None: raise ValueError("Expected sub-query") - + if relationship is not None: return CreateRelationship(relationship, query) else: @@ -416,7 +419,7 @@ def _parse_pattern(self) -> Optional[Pattern]: def _parse_pattern_expression(self) -> Optional[PatternExpression]: """Parse a pattern expression for WHERE clauses. - + PatternExpression is used to test if a graph pattern exists. It must start with a NodeReference (referencing an existing variable). """ @@ -459,7 +462,7 @@ def _parse_node(self) -> Optional[Node]: raise ValueError("Expected node label identifier") if self.token.is_colon() and peek is not None and peek.is_identifier(): self.set_next_token() - label = self.token.value + label = cast(str, self.token.value) # Guaranteed by is_identifier check self.set_next_token() self._skip_whitespace_and_comments() node = Node() @@ -469,7 +472,6 @@ def _parse_node(self) -> Optional[Node]: self._variables[identifier] = node elif identifier is not None: reference = self._variables.get(identifier) - from ..graph.node_reference import NodeReference if reference is None or not isinstance(reference, Node): raise ValueError(f"Undefined node reference: {identifier}") node = NodeReference(node, reference) @@ -515,7 +517,6 @@ def _parse_relationship(self) -> Optional[Relationship]: self._variables[variable] = relationship elif variable is not None: reference = self._variables.get(variable) - from ..graph.relationship_reference import RelationshipReference if reference is None or not isinstance(reference, Relationship): raise ValueError(f"Undefined relationship reference: {variable}") relationship = RelationshipReference(relationship, reference) @@ -524,9 +525,7 @@ def _parse_relationship(self) -> Optional[Relationship]: relationship.type = rel_type return relationship - def _parse_relationship_hops(self): - import sys - from ..graph.hops import Hops + def _parse_relationship_hops(self) -> Optional[Hops]: if not self.token.is_multiply(): return None hops = Hops() @@ -572,10 +571,11 @@ def _parse_expressions( alias = self._parse_alias() if isinstance(expression.first_child(), Reference) and alias is None: reference = expression.first_child() + assert isinstance(reference, Reference) # For type narrowing expression.set_alias(reference.identifier) self._variables[reference.identifier] = expression - elif (alias_option == AliasOption.REQUIRED and - alias is None and + elif (alias_option == AliasOption.REQUIRED and + alias is None and not isinstance(expression.first_child(), Reference)): raise ValueError("Alias required") elif alias_option == AliasOption.NOT_ALLOWED and alias is not None: @@ -607,7 +607,15 @@ def _parse_operand(self, expression: Expression) -> bool: lookup = self._parse_lookup(func) expression.add_node(lookup) return True - elif self.token.is_left_parenthesis() and self.peek() is not None and (self.peek().is_identifier() or self.peek().is_colon() or self.peek().is_right_parenthesis()): + elif ( + self.token.is_left_parenthesis() + and self.peek() is not None + and ( + self.peek().is_identifier() + or self.peek().is_colon() + or self.peek().is_right_parenthesis() + ) + ): # Possible graph pattern expression pattern = self._parse_pattern_expression() if pattern is not None: @@ -675,7 +683,7 @@ def _parse_expression(self) -> Optional[Expression]: else: break self.set_next_token() - + if expression.nodes_added(): expression.finish() return expression @@ -683,7 +691,7 @@ def _parse_expression(self) -> Optional[Expression]: def _parse_lookup(self, node: ASTNode) -> ASTNode: variable = node - lookup = None + lookup: Lookup | RangeLookup | None = None while True: if self.token.is_dot(): self.set_next_token() @@ -870,30 +878,30 @@ def _parse_function(self) -> Optional[Function]: name = self.token.value or "" if not self.peek() or not self.peek().is_left_parenthesis(): return None - + try: func = FunctionFactory.create(name) except ValueError: raise ValueError(f"Unknown function: {name}") - + # Check for nested aggregate functions if isinstance(func, AggregateFunction) and self._context.contains_type(AggregateFunction): raise ValueError("Aggregate functions cannot be nested") - + self._context.push(func) self.set_next_token() # skip function name self.set_next_token() # skip left parenthesis self._skip_whitespace_and_comments() - + # Check for DISTINCT keyword if self.token.is_distinct(): func.distinct = True self.set_next_token() self._expect_and_skip_whitespace_and_comments() - + params = list(self._parse_function_parameters()) func.parameters = params - + if not self.token.is_right_parenthesis(): raise ValueError("Expected right parenthesis") self.set_next_token() @@ -910,11 +918,11 @@ def _parse_async_function(self) -> Optional[AsyncFunction]: if not self.token.is_left_parenthesis(): raise ValueError("Expected left parenthesis") self.set_next_token() - + func = FunctionFactory.create_async(name) params = list(self._parse_function_parameters()) func.parameters = params - + if not self.token.is_right_parenthesis(): raise ValueError("Expected right parenthesis") self.set_next_token() diff --git a/flowquery-py/src/parsing/token_to_node.py b/flowquery-py/src/parsing/token_to_node.py index ff6f526..39ab517 100644 --- a/flowquery-py/src/parsing/token_to_node.py +++ b/flowquery-py/src/parsing/token_to_node.py @@ -9,7 +9,6 @@ from .expressions.boolean import Boolean from .expressions.identifier import Identifier from .expressions.number import Number -from .expressions.string import String from .expressions.operator import ( Add, And, @@ -28,6 +27,7 @@ Power, Subtract, ) +from .expressions.string import String from .logic.else_ import Else from .logic.end import End from .logic.then import Then @@ -103,7 +103,7 @@ def convert(token: Token) -> ASTNode: elif token.is_null(): return Null() elif token.is_boolean(): - return Boolean(token.value) + return Boolean(token.value or "") else: raise ValueError("Unknown token") return ASTNode() diff --git a/flowquery-py/src/tokenization/__init__.py b/flowquery-py/src/tokenization/__init__.py index a488b8b..76ddea3 100644 --- a/flowquery-py/src/tokenization/__init__.py +++ b/flowquery-py/src/tokenization/__init__.py @@ -1,13 +1,13 @@ """Tokenization module for FlowQuery.""" -from .tokenizer import Tokenizer -from .token import Token -from .token_type import TokenType from .keyword import Keyword from .operator import Operator +from .string_walker import StringWalker from .symbol import Symbol +from .token import Token from .token_mapper import TokenMapper -from .string_walker import StringWalker +from .token_type import TokenType +from .tokenizer import Tokenizer from .trie import Trie __all__ = [ diff --git a/flowquery-py/src/tokenization/keyword.py b/flowquery-py/src/tokenization/keyword.py index 0fa955b..23fa619 100644 --- a/flowquery-py/src/tokenization/keyword.py +++ b/flowquery-py/src/tokenization/keyword.py @@ -5,7 +5,7 @@ class Keyword(Enum): """Enumeration of all keywords in FlowQuery.""" - + RETURN = "RETURN" MATCH = "MATCH" WHERE = "WHERE" diff --git a/flowquery-py/src/tokenization/operator.py b/flowquery-py/src/tokenization/operator.py index 713b403..8467139 100644 --- a/flowquery-py/src/tokenization/operator.py +++ b/flowquery-py/src/tokenization/operator.py @@ -5,7 +5,7 @@ class Operator(Enum): """Enumeration of all operators in FlowQuery.""" - + # Arithmetic ADD = "+" SUBTRACT = "-" diff --git a/flowquery-py/src/tokenization/string_walker.py b/flowquery-py/src/tokenization/string_walker.py index 582b10b..dec618e 100644 --- a/flowquery-py/src/tokenization/string_walker.py +++ b/flowquery-py/src/tokenization/string_walker.py @@ -5,10 +5,10 @@ class StringWalker: """Utility class for walking through a string character by character during tokenization. - + Provides methods to check for specific character patterns, move through the string, and extract substrings. Used by the Tokenizer to process input text. - + Example: walker = StringWalker("WITH x as variable") while not walker.is_at_end: @@ -17,7 +17,7 @@ class StringWalker: def __init__(self, text: str): """Creates a new StringWalker for the given text. - + Args: text: The input text to walk through """ @@ -89,7 +89,7 @@ def escaped(self, char: str) -> bool: return self.current_char == '\\' and self.next_char == char def escaped_brace(self) -> bool: - return ((self.current_char == '{' and self.next_char == '{') or + return ((self.current_char == '{' and self.next_char == '{') or (self.current_char == '}' and self.next_char == '}')) def opening_brace(self) -> bool: diff --git a/flowquery-py/src/tokenization/symbol.py b/flowquery-py/src/tokenization/symbol.py index a8ecc7d..17818bf 100644 --- a/flowquery-py/src/tokenization/symbol.py +++ b/flowquery-py/src/tokenization/symbol.py @@ -5,7 +5,7 @@ class Symbol(Enum): """Enumeration of all symbols in FlowQuery.""" - + LEFT_PARENTHESIS = "(" RIGHT_PARENTHESIS = ")" COMMA = "," diff --git a/flowquery-py/src/tokenization/token.py b/flowquery-py/src/tokenization/token.py index 41d25f2..01f1707 100644 --- a/flowquery-py/src/tokenization/token.py +++ b/flowquery-py/src/tokenization/token.py @@ -1,25 +1,24 @@ """Represents a single token in the FlowQuery language.""" from __future__ import annotations -from typing import TYPE_CHECKING, Optional, Any -from .token_type import TokenType +from typing import Optional + +from ..parsing.ast_node import ASTNode +from ..utils.string_utils import StringUtils from .keyword import Keyword from .operator import Operator from .symbol import Symbol -from ..utils.string_utils import StringUtils - -if TYPE_CHECKING: - from ..parsing.ast_node import ASTNode +from .token_type import TokenType class Token: """Represents a single token in the FlowQuery language. - + Tokens are the atomic units of lexical analysis, produced by the tokenizer and consumed by the parser. Each token has a type (keyword, operator, identifier, etc.) and an optional value. - + Example: with_token = Token.WITH() ident_token = Token.IDENTIFIER("myVar") @@ -28,7 +27,7 @@ class Token: def __init__(self, type_: TokenType, value: Optional[str] = None): """Creates a new Token instance. - + Args: type_: The type of the token value: The optional value associated with the token @@ -41,10 +40,10 @@ def __init__(self, type_: TokenType, value: Optional[str] = None): def equals(self, other: Token) -> bool: """Checks if this token equals another token. - + Args: other: The token to compare against - + Returns: True if tokens are equal, False otherwise """ @@ -82,6 +81,7 @@ def can_be_identifier(self) -> bool: @property def node(self) -> ASTNode: + # Import at runtime to avoid circular dependency from ..parsing.token_to_node import TokenToNode return TokenToNode.convert(self) diff --git a/flowquery-py/src/tokenization/token_mapper.py b/flowquery-py/src/tokenization/token_mapper.py index 0c0143a..0bea554 100644 --- a/flowquery-py/src/tokenization/token_mapper.py +++ b/flowquery-py/src/tokenization/token_mapper.py @@ -1,6 +1,7 @@ """Maps string values to tokens using a Trie for efficient lookup.""" -from typing import Optional +from enum import Enum +from typing import Optional, Type from .token import Token from .trie import Trie @@ -8,24 +9,24 @@ class TokenMapper: """Maps string values to tokens using a Trie for efficient lookup. - + Takes an enum of keywords, operators, or symbols and builds a trie for fast token matching during tokenization. - + Example: mapper = TokenMapper(Keyword) token = mapper.map("WITH") """ - def __init__(self, enum_class): + def __init__(self, enum_class: Type[Enum]) -> None: """Creates a TokenMapper from an enum of token values. - + Args: enum_class: An enum class containing token values """ self._trie = Trie() self._enum = enum_class - + for member in enum_class: token = Token.method(member.name) if token is not None and token.value is not None: @@ -33,10 +34,10 @@ def __init__(self, enum_class): def map(self, value: str) -> Optional[Token]: """Maps a string value to its corresponding token. - + Args: value: The string value to map - + Returns: The matched token, or None if no match found """ @@ -45,7 +46,7 @@ def map(self, value: str) -> Optional[Token]: @property def last_found(self) -> Optional[str]: """Gets the last matched string from the most recent map operation. - + Returns: The last found string, or None if no match """ diff --git a/flowquery-py/src/tokenization/token_type.py b/flowquery-py/src/tokenization/token_type.py index 40e90a9..20e2906 100644 --- a/flowquery-py/src/tokenization/token_type.py +++ b/flowquery-py/src/tokenization/token_type.py @@ -5,7 +5,7 @@ class TokenType(Enum): """Enumeration of all token types in FlowQuery.""" - + KEYWORD = "KEYWORD" BOOLEAN = "BOOLEAN" OPERATOR = "OPERATOR" diff --git a/flowquery-py/src/tokenization/tokenizer.py b/flowquery-py/src/tokenization/tokenizer.py index c273a69..4d8c9a7 100644 --- a/flowquery-py/src/tokenization/tokenizer.py +++ b/flowquery-py/src/tokenization/tokenizer.py @@ -1,6 +1,6 @@ """Tokenizes FlowQuery input strings into a sequence of tokens.""" -from typing import List, Optional, Iterator, Callable +from typing import Callable, Iterator, List, Optional from ..utils.string_utils import StringUtils from .keyword import Keyword @@ -13,11 +13,11 @@ class Tokenizer: """Tokenizes FlowQuery input strings into a sequence of tokens. - + The tokenizer performs lexical analysis, breaking down the input text into meaningful tokens such as keywords, identifiers, operators, strings, numbers, and symbols. It handles comments, whitespace, and f-strings. - + Example: tokenizer = Tokenizer("WITH x = 1 RETURN x") tokens = tokenizer.tokenize() @@ -25,7 +25,7 @@ class Tokenizer: def __init__(self, input_: str): """Creates a new Tokenizer instance for the given input. - + Args: input_: The FlowQuery input string to tokenize """ @@ -36,16 +36,16 @@ def __init__(self, input_: str): def tokenize(self) -> List[Token]: """Tokenizes the input string into an array of tokens. - + Returns: An array of Token objects representing the tokenized input - + Raises: ValueError: If an unrecognized token is encountered """ tokens: List[Token] = [] last: Optional[Token] = None - + while not self._walker.is_at_end: tokens.extend(self._f_string()) last = self._get_last_non_whitespace_or_non_comment_token(tokens) or last @@ -54,7 +54,7 @@ def tokenize(self) -> List[Token]: raise ValueError(f"Unrecognized token at position {self._walker.position}") token.position = self._walker.position tokens.append(token) - + return tokens def _get_last_non_whitespace_or_non_comment_token(self, tokens: List[Token]) -> Optional[Token]: @@ -97,9 +97,9 @@ def _boolean(self) -> Optional[Token]: def _identifier(self) -> Optional[Token]: start_position = self._walker.position if self._walker.check_for_under_score() or self._walker.check_for_letter(): - while (not self._walker.is_at_end and - (self._walker.check_for_letter() or - self._walker.check_for_digit() or + while (not self._walker.is_at_end and + (self._walker.check_for_letter() or + self._walker.check_for_digit() or self._walker.check_for_under_score())): pass return Token.IDENTIFIER(self._walker.get_string(start_position)) @@ -110,7 +110,7 @@ def _string(self) -> Optional[Token]: quote_char = self._walker.check_for_quote() if quote_char is None: return None - + while not self._walker.is_at_end: if self._walker.escaped(quote_char): self._walker.move_next() @@ -122,32 +122,32 @@ def _string(self) -> Optional[Token]: return Token.BACKTICK_STRING(value, quote_char) return Token.STRING(value, quote_char) self._walker.move_next() - + raise ValueError(f"Unterminated string at position {start_position}") def _f_string(self) -> Iterator[Token]: if not self._walker.check_for_f_string_start(): return - + self._walker.move_next() # skip the f position = self._walker.position quote_char = self._walker.check_for_quote() if quote_char is None: return - + while not self._walker.is_at_end: if self._walker.escaped(quote_char) or self._walker.escaped_brace(): self._walker.move_next() self._walker.move_next() continue - + if self._walker.opening_brace(): yield Token.F_STRING(self._walker.get_string(position), quote_char) position = self._walker.position yield Token.OPENING_BRACE() self._walker.move_next() # skip the opening brace position = self._walker.position - + while not self._walker.is_at_end and not self._walker.closing_brace(): token = self._get_next_token() if token is not None: @@ -159,11 +159,11 @@ def _f_string(self) -> Iterator[Token]: self._walker.move_next() # skip the closing brace position = self._walker.position break - + if self._walker.check_for_string(quote_char): yield Token.F_STRING(self._walker.get_string(position), quote_char) return - + self._walker.move_next() def _whitespace(self) -> Optional[Token]: diff --git a/flowquery-py/src/tokenization/trie.py b/flowquery-py/src/tokenization/trie.py index 616fe22..a6f879d 100644 --- a/flowquery-py/src/tokenization/trie.py +++ b/flowquery-py/src/tokenization/trie.py @@ -1,6 +1,7 @@ """Trie (prefix tree) data structure for efficient keyword and operator lookup.""" from __future__ import annotations + from typing import TYPE_CHECKING, Optional if TYPE_CHECKING: @@ -9,12 +10,12 @@ class TrieNode: """Represents a node in a Trie data structure. - + Each node can have children nodes (one per character) and may contain a token if the path to this node represents a complete word. """ - def __init__(self): + def __init__(self) -> None: self._children: dict[str, TrieNode] = {} self._token: Optional[Token] = None @@ -43,59 +44,59 @@ def no_children(self) -> bool: class Trie: """Trie (prefix tree) data structure for efficient keyword and operator lookup. - + Used during tokenization to quickly match input strings against known keywords and operators. Supports case-insensitive matching and tracks the longest match found. - + Example: trie = Trie() trie.insert(Token.WITH) found = trie.find("WITH") """ - def __init__(self): + def __init__(self) -> None: self._root = TrieNode() self._max_length = 0 self._last_found: Optional[str] = None def insert(self, token: Token) -> None: """Inserts a token into the trie. - + Args: token: The token to insert - + Raises: ValueError: If the token value is None or empty """ if token.value is None or len(token.value) == 0: raise ValueError("Token value cannot be null or empty") - + current_node = self._root for char in token.value: current_node = current_node.map(char.lower()) - + if len(token.value) > self._max_length: self._max_length = len(token.value) - + current_node.token = token def find(self, value: str) -> Optional[Token]: """Finds a token by searching for the longest matching prefix in the trie. - + Args: value: The string value to search for - + Returns: The token if found, None otherwise """ if len(value) == 0: return None - + index = 0 current: Optional[TrieNode] = None found: Optional[Token] = None self._last_found = None - + while True: next_node = (current or self._root).retrieve(value[index].lower()) if next_node is None: @@ -107,17 +108,17 @@ def find(self, value: str) -> Optional[Token]: index += 1 if index >= len(value) or index > self._max_length: break - + if current is not None and current.is_end_of_word(): found = current.token self._last_found = value[:index] - + return found @property def last_found(self) -> Optional[str]: """Gets the last matched string from the most recent find operation. - + Returns: The last found string, or None if no match was found """ diff --git a/flowquery-py/src/utils/__init__.py b/flowquery-py/src/utils/__init__.py index 942325e..915beda 100644 --- a/flowquery-py/src/utils/__init__.py +++ b/flowquery-py/src/utils/__init__.py @@ -1,6 +1,6 @@ """Utils module for FlowQuery.""" -from .string_utils import StringUtils from .object_utils import ObjectUtils +from .string_utils import StringUtils __all__ = ["StringUtils", "ObjectUtils"] diff --git a/flowquery-py/src/utils/object_utils.py b/flowquery-py/src/utils/object_utils.py index 1bc19f2..30f2cd7 100644 --- a/flowquery-py/src/utils/object_utils.py +++ b/flowquery-py/src/utils/object_utils.py @@ -7,13 +7,13 @@ class ObjectUtils: """Utility class for object-related operations.""" @staticmethod - def is_instance_of_any(obj: Any, classes: List[Type]) -> bool: + def is_instance_of_any(obj: Any, classes: List[Type[Any]]) -> bool: """Checks if an object is an instance of any of the provided classes. - + Args: obj: The object to check classes: Array of class constructors to test against - + Returns: True if the object is an instance of any class, False otherwise """ diff --git a/flowquery-py/src/utils/string_utils.py b/flowquery-py/src/utils/string_utils.py index e623ab9..4cc478b 100644 --- a/flowquery-py/src/utils/string_utils.py +++ b/flowquery-py/src/utils/string_utils.py @@ -3,11 +3,11 @@ class StringUtils: """Utility class for string manipulation and validation. - + Provides methods for handling quoted strings, comments, escape sequences, and identifier validation. """ - + quotes = ['"', "'", '`'] letters = 'abcdefghijklmnopqrstuvwxyz' digits = '0123456789' @@ -17,10 +17,10 @@ class StringUtils: @staticmethod def unquote(s: str) -> str: """Removes surrounding quotes from a string. - + Args: s: The string to unquote - + Returns: The unquoted string """ @@ -41,10 +41,10 @@ def unquote(s: str) -> str: @staticmethod def uncomment(s: str) -> str: """Removes comment markers from a string. - + Args: s: The comment string - + Returns: The string without comment markers """ @@ -59,11 +59,11 @@ def uncomment(s: str) -> str: @staticmethod def remove_escaped_quotes(s: str, quote_char: str) -> str: """Removes escape sequences before quotes in a string. - + Args: s: The string to process quote_char: The quote character that was escaped - + Returns: The string with escape sequences removed """ @@ -79,10 +79,10 @@ def remove_escaped_quotes(s: str, quote_char: str) -> str: @staticmethod def remove_escaped_braces(s: str) -> str: """Removes escaped braces ({{ and }}) from f-strings. - + Args: s: The string to process - + Returns: The string with escaped braces resolved """ @@ -98,10 +98,10 @@ def remove_escaped_braces(s: str) -> str: @staticmethod def can_be_identifier(s: str) -> bool: """Checks if a string is a valid identifier. - + Args: s: The string to validate - + Returns: True if the string can be used as an identifier, false otherwise """