file_query_text 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3 @@
1
+ """SQL-like interface for querying files in your filesystem."""
2
+
3
+ __version__ = "0.1.0"
file_query_text/cli.py ADDED
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env python3
2
+ import os
3
+ import sys
4
+ import argparse
5
+ from pathlib import Path
6
+ # Fix imports to work when installed as a package
7
+ from file_query.main import parse_query, QueryVisitor, execute_query
8
+
9
+ def main():
10
+ parser = argparse.ArgumentParser(description="SQL-like queries for your filesystem")
11
+ parser.add_argument("query", nargs="?", default="",
12
+ help="SQL query for finding files (default: lists all files in current directory)")
13
+ parser.add_argument(
14
+ "--show-content", "-c",
15
+ action="store_true",
16
+ help="Display content of the matching files"
17
+ )
18
+ args = parser.parse_args()
19
+
20
+ # Get current working directory for the query
21
+ cwd = os.getcwd()
22
+
23
+ # Handle different query formats:
24
+ # 1. Full SQL format: "SELECT * FROM 'path' WHERE condition"
25
+ # 2. Simple condition: "extension == 'py'"
26
+ # 3. Simple path only: "SELECT * FROM 'path'"
27
+ # 4. Empty query: return all files in current directory
28
+ if not args.query.strip():
29
+ # Empty query - list all files in current directory
30
+ query_str = f"SELECT * FROM '{cwd}'"
31
+ elif args.query.strip().upper().startswith("SELECT"):
32
+ # Full SQL format or SELECT * FROM without WHERE - use as is
33
+ query_str = args.query
34
+ else:
35
+ # Simple condition format - assume it's a WHERE condition
36
+ query_str = f"SELECT * FROM '{cwd}' WHERE {args.query}"
37
+
38
+ # Parse and execute the query
39
+ parsed = parse_query(query_str)
40
+ if parsed:
41
+ visitor = QueryVisitor()
42
+ visitor.visit(parsed)
43
+ results = execute_query(visitor.select, visitor.from_dirs, visitor.where)
44
+
45
+ # Display results
46
+ if not results:
47
+ print("No matching files found.")
48
+ return
49
+
50
+ print(f"Found {len(results)} matching files:")
51
+ for file_path in results:
52
+ print(file_path)
53
+
54
+ # Optionally display file contents
55
+ if args.show_content:
56
+ try:
57
+ with open(file_path, 'r') as f:
58
+ content = f.read()
59
+ print("\n--- File Content ---")
60
+ print(content)
61
+ print("--- End Content ---\n")
62
+ except Exception as e:
63
+ print(f"Error reading file: {e}")
64
+
65
+ if __name__ == "__main__":
66
+ main()
@@ -0,0 +1,63 @@
1
+ from pyparsing import (
2
+ Word,
3
+ alphas,
4
+ alphanums,
5
+ QuotedString,
6
+ delimitedList,
7
+ Optional,
8
+ Group,
9
+ Suppress,
10
+ ZeroOrMore,
11
+ oneOf,
12
+ Forward,
13
+ Literal,
14
+ OneOrMore,
15
+ infixNotation,
16
+ opAssoc,
17
+ c_style_comment,
18
+ nums,
19
+ pyparsing_common,
20
+ )
21
+
22
+ # Define keywords
23
+ SELECT = Suppress(Word("SELECT"))
24
+ FROM = Suppress(Word("FROM"))
25
+ WHERE = Suppress(Word("WHERE"))
26
+ AND = Literal("AND")
27
+ OR = Literal("OR")
28
+ NOT = Literal("NOT")
29
+
30
+ # Define identifiers and literals
31
+ IDENTIFIER = Word(alphas + "_")
32
+ STRING_LITERAL = QuotedString("'", unquoteResults=True)
33
+ # Use pyparsing_common for numeric literals
34
+ NUMERIC_LITERAL = pyparsing_common.integer
35
+ DIRECTORY_LIST = Group(delimitedList(STRING_LITERAL))
36
+
37
+ # Define comparison operators
38
+ COMPARISON_OP = oneOf("== != < <= > >=")
39
+ ATTRIBUTE = IDENTIFIER + Suppress("=") + STRING_LITERAL
40
+
41
+ # Define basic condition with support for both string and numeric literals
42
+ VALUE = STRING_LITERAL | NUMERIC_LITERAL
43
+ basic_condition = Group(IDENTIFIER + COMPARISON_OP + VALUE)
44
+
45
+ # Define logical expressions using infixNotation for better handling of AND and OR
46
+ condition_expr = Forward()
47
+ condition_expr <<= infixNotation(
48
+ basic_condition,
49
+ [
50
+ (NOT, 1, opAssoc.RIGHT),
51
+ (AND, 2, opAssoc.LEFT),
52
+ (OR, 2, opAssoc.LEFT),
53
+ ],
54
+ )
55
+
56
+ # Define the full query structure
57
+ query = (
58
+ SELECT
59
+ + (Literal("*") | Group(OneOrMore(IDENTIFIER))).setResultsName("select")
60
+ + FROM
61
+ + DIRECTORY_LIST.setResultsName("from_dirs")
62
+ + Optional(WHERE + condition_expr.setResultsName("where"))
63
+ )
@@ -0,0 +1,110 @@
1
+ import os
2
+ import sys
3
+ from file_query_text.grammar import query # Import the fixed grammar
4
+
5
+
6
+ def parse_query(query_str):
7
+ try:
8
+ # Increase recursion limit temporarily to handle complex queries
9
+ old_limit = sys.getrecursionlimit()
10
+ sys.setrecursionlimit(2000)
11
+
12
+ parsed = query.parseString(query_str, parseAll=True)
13
+
14
+ # Restore original recursion limit
15
+ sys.setrecursionlimit(old_limit)
16
+ return parsed
17
+ except Exception as e:
18
+ print(f"Parse error: {e}")
19
+ return None
20
+
21
+ class QueryVisitor:
22
+ def __init__(self):
23
+ self.select = []
24
+ self.from_dirs = []
25
+ self.where = None
26
+
27
+ def visit(self, parsed_query):
28
+ self.select = parsed_query.get("select", ["*"])
29
+ self.from_dirs = parsed_query.get("from_dirs", [])
30
+ self.where = parsed_query.get("where", None)
31
+
32
+ def execute_query(select, from_dirs, where_conditions):
33
+ matched_files = []
34
+ for directory in from_dirs:
35
+ if not os.path.exists(directory):
36
+ continue
37
+ for root, _, files in os.walk(directory):
38
+ for filename in files:
39
+ file_path = os.path.join(root, filename)
40
+ if evaluate_conditions(file_path, where_conditions):
41
+ matched_files.append(file_path)
42
+ return matched_files
43
+
44
+ def evaluate_conditions(file_path, condition):
45
+ if not condition:
46
+ return True
47
+
48
+ def get_file_attr(attr_name):
49
+ if attr_name == "extension":
50
+ return os.path.splitext(file_path)[1][1:]
51
+ if attr_name == "name":
52
+ return os.path.basename(file_path)
53
+ if attr_name == "size":
54
+ return os.path.getsize(file_path)
55
+ # Add more attributes as needed
56
+ return None
57
+
58
+ # Evaluation function for expressions
59
+ def eval_expr(expr):
60
+ if not isinstance(expr, list):
61
+ return expr # For simple terms like 'AND', 'OR'
62
+
63
+ if len(expr) == 3:
64
+ # Handle three types of expressions:
65
+
66
+ # 1. Basic condition: [attr, op, value]
67
+ if isinstance(expr[0], str) and isinstance(expr[1], str):
68
+ attr_val = get_file_attr(expr[0])
69
+ op = expr[1]
70
+ val = expr[2].strip("'") if isinstance(expr[2], str) else expr[2] # Remove quotes if string
71
+
72
+ if op == "==": return str(attr_val) == val
73
+ if op == "!=": return str(attr_val) != val
74
+ if op == "<": return attr_val is not None and int(attr_val) < int(val)
75
+ if op == "<=": return attr_val is not None and int(attr_val) <= int(val)
76
+ if op == ">": return attr_val is not None and int(attr_val) > int(val)
77
+ if op == ">=": return attr_val is not None and int(attr_val) >= int(val)
78
+
79
+ # 2. Logical operations from infixNotation: [left, op, right]
80
+ elif expr[1] == "AND":
81
+ return eval_expr(expr[0]) and eval_expr(expr[2])
82
+ elif expr[1] == "OR":
83
+ return eval_expr(expr[0]) or eval_expr(expr[2])
84
+
85
+ # 3. NOT operation: ['NOT', expr]
86
+ elif len(expr) == 2 and expr[0] == "NOT":
87
+ return not eval_expr(expr[1])
88
+
89
+ return False
90
+
91
+ return eval_expr(condition.asList())
92
+
93
+ # Example usage
94
+ if __name__ == "__main__":
95
+ # Get project root directory for demonstration
96
+ current_dir = os.path.dirname(os.path.abspath(__file__))
97
+ project_root = os.path.dirname(current_dir)
98
+ src_dir = os.path.join(project_root, "src")
99
+ tests_dir = os.path.join(project_root, "tests")
100
+ query_str = f"SELECT * FROM '{src_dir}', '{tests_dir}' WHERE extension == 'py'"
101
+ parsed = parse_query(query_str)
102
+ if parsed:
103
+ visitor = QueryVisitor()
104
+ visitor.visit(parsed)
105
+ results = execute_query(visitor.select, visitor.from_dirs, visitor.where)
106
+ print("Matching files:")
107
+ for file in results:
108
+ # Skip files in .venv directory
109
+ if ".venv" not in file:
110
+ print(file)
@@ -0,0 +1,110 @@
1
+ Metadata-Version: 2.4
2
+ Name: file_query_text
3
+ Version: 0.1.0
4
+ Summary: SQL-like interface for querying files in your filesystem
5
+ Author-email: nik <42a11b@nikdav.is>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/nikdavis/file_query_text
8
+ Project-URL: Bug Tracker, https://github.com/nikdavis/file_query_text/issues
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.12
12
+ Description-Content-Type: text/markdown
13
+ Requires-Dist: pyparsing>=3.2.3
14
+ Provides-Extra: dev
15
+ Requires-Dist: pytest>=8.3.5; extra == "dev"
16
+
17
+ # File Query
18
+
19
+ A SQL-like interface for querying files in your filesystem.
20
+
21
+ ## Installation
22
+
23
+ ```bash
24
+ # Clone the repository
25
+ git clone https://github.com/yourusername/file-query.git
26
+ cd file-query
27
+
28
+ # Install with pip
29
+ pip install -e .
30
+
31
+ # Or use UV
32
+ uv run python -m src.cli "your query"
33
+
34
+ # Install as a permanent tool with UV
35
+ uv tool install .
36
+ # This will install the 'fq' command
37
+ ```
38
+
39
+ ## Usage
40
+
41
+ ### Command Line
42
+
43
+ The quickest way to run file-query is with UV:
44
+
45
+ ```bash
46
+ uv run python -m src.cli "your query here"
47
+ ```
48
+
49
+ After installation, you can use the shorthand command:
50
+
51
+ ```bash
52
+ fq "your query here"
53
+ ```
54
+
55
+ #### Basic Usage
56
+
57
+ ```bash
58
+ # Find all Python files
59
+ fq "extension == 'py'"
60
+
61
+ # Find all text files and show their content
62
+ fq "extension == 'txt'" --show-content
63
+ ```
64
+
65
+ #### Advanced Queries
66
+
67
+ File Query supports full SQL-like syntax:
68
+
69
+ ```bash
70
+ # Find all Python files in the src directory
71
+ fq "SELECT * FROM 'src' WHERE extension == 'py'"
72
+
73
+ # Find all files larger than 100KB
74
+ fq "SELECT * FROM '.' WHERE size > 102400"
75
+
76
+ # Complex conditions
77
+ fq "SELECT * FROM '.' WHERE (extension == 'pdf' AND size > 1000000) OR (extension == 'txt' AND NOT name == 'README.txt')"
78
+ ```
79
+
80
+ ## Query Syntax
81
+
82
+ File Query uses a SQL-like syntax:
83
+
84
+ ```sql
85
+ SELECT * FROM 'directory_path' WHERE condition
86
+ ```
87
+
88
+ ### Available Attributes
89
+
90
+ - `extension`: File extension (without the dot)
91
+ - `name`: Filename with extension
92
+ - `size`: File size in bytes
93
+
94
+ ### Operators
95
+
96
+ - Comparison: `==`, `!=`, `<`, `<=`, `>`, `>=`
97
+ - Logical: `AND`, `OR`, `NOT`
98
+
99
+ ## Examples
100
+
101
+ ```bash
102
+ # Find all PDF files
103
+ fq "extension == 'pdf'"
104
+
105
+ # Find all files not named "main.py"
106
+ fq "NOT name == 'main.py'"
107
+
108
+ # Find all large image files
109
+ fq "SELECT * FROM '.' WHERE (extension == 'jpg' OR extension == 'png') AND size > 500000"
110
+ ```
@@ -0,0 +1,9 @@
1
+ file_query_text/__init__.py,sha256=WXdvguZ706HG7MXS13Yb8e7VC5UFYyBzuExXbwBOTak,87
2
+ file_query_text/cli.py,sha256=W4Lf9WC4hYIW6DpL57JMcLXIfyTr2v8X-Y-WjPjoN1s,2360
3
+ file_query_text/grammar.py,sha256=lhw2pUq83IMga2cppA0r9RbebioPB7a65Az5f4Lheso,1572
4
+ file_query_text/main.py,sha256=qMuY5YZ2TXrAUyfXaQfPkQOJZawpRE86NIre5Pz88Tk,4011
5
+ file_query_text-0.1.0.dist-info/METADATA,sha256=TjD_kUagIoxHH9lPV0qVDOoYH9ZlkmmpwEv5yXuaD1c,2347
6
+ file_query_text-0.1.0.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
7
+ file_query_text-0.1.0.dist-info/entry_points.txt,sha256=rNFYWzvcIsUZkGNsc_E_B5HyYRnqqdj_u8_IeQpw1wo,48
8
+ file_query_text-0.1.0.dist-info/top_level.txt,sha256=o1FzSvLa6kSV61b7RLHWRhEezc96m05YwIKqjuWUSxU,16
9
+ file_query_text-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (78.1.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ fq = file_query_text.cli:main
@@ -0,0 +1 @@
1
+ file_query_text