+
+**FiSE (File Search Engine)** is a powerful cross-platform command line utility designed for performing seamless file, directory, and data search and delete operations. It empowers users with the ability to perform comprehensive operations using intuitive SQL-like commands. It streamlines file management tasks, making it simple to locate, query, and modify files and directories with precision and efficiency. Additionally, this utility also allows exporting search records to files and databases in a professional manner. Ideal for developers, data analysts, system administrators, and power users, FiSE enhances productivity by providing a robust and flexible toolset for search and delete operations.
+
+
Features
+
+1. **Cross-Platform Compatibility**: Works seamlessly across multiple operating systems, including Windows, macOS, and Linux.
+
+2. **Comprehensive Search Operations**: Performs detailed searches for files, directories, and data within files with precise and efficient results.
+
+3. **Intuitive SQL-like Commands**: Utilizes familiar SQL-like syntax for conducting searches and managing files, reducing the learning curve.
+
+4. **Case-Insensitive Queries**: Allows queries to be case-insensitive, making searches more flexible and user-friendly.
+
+5. **Advanced File Management**: Provides tools for locating, querying, and modifying files and directories with precision and efficiency.
+
+6. **Professional Export Capabilities**: Offers export functionalities for search results to external files and databases, facilitating better data management and reporting.
+
+7. **Productivity Enhancement Tools**: Enhances workflow and efficiency with a comprehensive and flexible toolset for various file system operations.
+
+
Quick Guide
+
+This guide offers a basic overview of the utility, highlighting some of the commonly used queries. It enables users to efficiently search, query, and manipulate files and directories across different operating systems.
+
+**FiSE** offers two broad categories of operations, namely **Search** and **Delete**. These operations can be performed on files, file data, and directories, with the exception for file data for the Delete operation.
+
+For a deeper insight, please refer to the [Documentation](./doc/getting-started.md).
+
+### Query Syntax Breakdown
+
+The basic syntax of the query is shown below:
+
+- Search Query:
+
+```SQL
+(EXPORT (FILE[]|SQL[])) (R|RECURSIVE) SEARCH([]) FROM (RELATIVE|ABSOLUTE) (|) (WHERE )
+```
+
+- Delete Query:
+
+```SQL
+(R|RECURSIVE) DELETE([]) FROM (RELATIVE|ABSOLUTE) () (WHERE )
+```
+
+Where:
+
+1. `(EXPORT (FILE[]|SQL[]))` is an optional command exclusive to the search operation and is used to export search records to a file or database.
+2. `(R|RECURSIVE)` is an optional command used to recursively include all the files and subdirectories present within the specified directory. If not explicitly specified, operations are only limited to the root directory.
+3. `(SEARCH|DELETE)[]` defines the desired operation to be performed. Additional parameters can be specified within `[]` to toggle operations between different file types, and file-modes explicitly for data search operation.
+4. `` is only limited to search operations for accessing metadata fields related to the searched files, data, or directories. Field names must be separated by commas. For more information about the different metadata fields that can be used in FiSE queries, please refer to the [Query Fields](./doc/query/query-fields.md) guide.
+5. `(RELATIVE|ABSOLUTE)` is an optional command to specify whether to include the absolute path of the files/directories in the operation if the specified path to the directory is relative.
+6. `(|)` defines the path to the file/directory to operate upon. Filepath is only limited to data search operations as other operations cannot be performed on a single file.
+7. `(WHERE )` is an optional query segment and is used for define conditions for filtering files, data, or directories. To know more about the various way for defining query conditions, please refer to the [Query Conditions](./doc/query/query-conditions.md) guide.
+
+For a deeper insight into the query syntax, please refer to the [Query Syntax](./doc/query/syntax.md) guide.
+
+Several example for both query types are defined in the following sections.
+
+### Overview of Search operation
+
+The **Search** operation encompasses the ability to search files, file data, and directories facilitating precise retrieval by allowing users to filter records based on specified search conditions.
+
+#### Search Query Syntax
+
+- File Search Query:
+
+```SQL
+(EXPORT (FILE[]|SQL[])) (R|RECURSIVE) SELECT([TYPE FILE]) FROM (RELATIVE|ABSOLUTE) (WHERE )
+```
+
+- Data Search Query:
+
+```SQL
+(EXPORT (FILE[]|SQL[])) (R|RECURSIVE) SELECT[TYPE DATA(, MODE (TEXT|BYTES))] FROM (RELATIVE|ABSOLUTE) (|) (WHERE )
+```
+
+- Directory Search Query:
+
+```SQL
+(EXPORT (FILE[]|SQL[])) (R|RECURSIVE) SELECT[TYPE DIR] FROM (RELATIVE|ABSOLUTE) (WHERE )
+```
+
+#### Search Query Examples
+
+```SQL
+SELECT * FROM ./fise WHERE name LIKE '^.*\.py$'
+```
+
+```SQL
+EXPORT FILE[./records.csv] R SELECT[TYPE FILE] RELATIVE * FROM .
+```
+
+```SQL
+R SELECT[TYPE DIR] name, parent FROM ABSOLUTE ./fise WHERE name IN ('query', 'common')
+```
+
+```SQL
+EXPORT SQL[mysql] RECURSIVE SELECT[TYPE DIR] * FROM . WHERE name IN ('fise', 'tests', '.github') AND parent LIKE '^.*fise$'
+```
+
+```SQL
+SELECT[TYPE DATA] lineno, data FROM ./fise/query/parsers.py WHERE "This" IN data AND lineno BETWEEN (30, 210)
+```
+
+```SQL
+EXPORT FILE[./data.xlsx] R SELECT[TYPE DATA] * FROM ./fise/query WHERE name IN ('parsers.py', 'operators.py') AND data LIKE '^.*get_files.*$'
+```
+
+### Overview of Delete Operation
+
+The **Delete** operation encompasses the capability to remove files and subdirectories within the designated directory, empowering users to systematically eliminate undesired data based on specified filtering conditions.
+
+#### Delete Query Syntax
+
+- File Deletion Query
+
+```SQL
+(R|RECURSIVE) DELETE([TYPE FILE]) FROM (WHERE )
+```
+
+- Directory Deletion Query
+
+```SQL
+(R|RECURSIVE) DELETE[TYPE DIR] FROM (WHERE )
+```
+
+#### Delete Query Examples
+
+```SQL
+DELETE FROM ./fise WHERE atime < '2015-08-15' AND name != "main.py"
+```
+
+```SQL
+R DELETE[TYPE FILE] FROM . WHERE name LIKE '^.*\.(js|cpp)$' OR SIZE[b] = 0
+```
+
+```SQL
+DELETE[TYPE DIR] FROM . WHERE name IN ("temp", "__pycache__", "test")
+```
+
+```SQL
+R DELETE[TYPE DIR] FROM ./fise
+```
+
+## Legals
+
+**FiSE** is distributed under the MIT License. Refer to [LICENSE](./LICENSE) for more details.
+
+## Call for Contributions
+
+The **FiSE** project always welcomes your precious expertise and enthusiasm!
+The package relies on its community's wisdom and intelligence to investigate bugs and contribute code. We always appreciate improvements and contributions to this project.
diff --git a/assets/fise.svg b/assets/fise.svg
new file mode 100644
index 0000000..5abce5e
--- /dev/null
+++ b/assets/fise.svg
@@ -0,0 +1,89 @@
+
+
diff --git a/doc/build-executable.md b/doc/build-executable.md
new file mode 100644
index 0000000..72338db
--- /dev/null
+++ b/doc/build-executable.md
@@ -0,0 +1,37 @@
+
Executable Build Guide
+
+This comprehensive guide will walk you through the steps to manually build FiSE into an executable file. The procedure is consistent across all platforms, with minor variations detailed below.
+
+### Prerequisites
+
+1. **Source Code**: Ensure you have the FiSE source code downloaded from the **GitHub Repository** on your system.
+
+2. **Dependencies**: Install the necessary dependencies for running the application. Additional dependencies may also be installed for accessing optional features such as exporting records to SQL databases.
+
+**For detailed instructions on installing these prerequisites, refer to the [Getting-Started](./getting-started.md) Guide.**
+
+### Installing Pyinstaller
+
+After installing the prerequisites, you need to install pyinstaller to build the executable file. To do so, run the following command:
+
+```bash
+python -m pip install pyinstaller --no-cache
+```
+
+### Building the Executable
+
+Once you have installed `pyinstaller`, you can build finally build the executable using the following command below:
+
+```bash
+pyinstaller --onefile fise/main.py
+```
+
+**NOTE: Before running the above command, you must ensure that you have set the current working directory to the source code directory.**
+
+This command works the same for all platforms, and will generate an executable file which can run on the current platform.
+
+### Running the Executable
+
+After the build process is complete, navigate to the `dist` directory generated automatically by `pyinstaller` within the source code directory. Inside, you will find the generated executable file ready to be executed.
+
+---
diff --git a/doc/getting-started.md b/doc/getting-started.md
new file mode 100644
index 0000000..c7c5ce8
--- /dev/null
+++ b/doc/getting-started.md
@@ -0,0 +1,117 @@
+
+
+
Getting Started
+
+Welcome to **FiSE (File Search Engine)**! This comprehensive guide will walk you through the steps to install FiSE, set it up, and perform basic operations with it.
+
+
Introduction
+
+**FiSE (File Search Engine)** is a powerful cross-platform command line utility designed for performing seamless file, directory, and data search and delete operations. It empowers users with the ability to perform comprehensive search operations using intuitive SQL-like commands streamlining file management tasks, making it simple to locate, query, and modify files and directories with precision and efficiency. Additionally, this utility also allows exporting search records to files and databases in a professional manner. Ideal for developers, data analysts, system administrators, and power users, FiSE enhances productivity by providing a robust and flexible toolset for advanced search and delete operations.
+
+
Setup
+
+This section will walk you through the steps to install and setup FiSE on your system.
+
+### Prerequisites
+
+Before installing FiSE, please ensure that you have the following prerequisites installed on your system:
+
+- **Python**: FiSE requires **Python 3.10** or higher, ensure it is installed on your system along with a package manager for python such as **pip**. You can download Python from [python.org](https://www.python.org/downloads/).
+
+- **Git** (Optional): While not mandatory, having **Git** installed can facilitate cloning the **FiSE** GitHub repository onto your system. You can download Git from [git-scm.com](https://www.git-scm.com).
+
+### Installation
+
+To install FiSE on your system, follow the steps mentioned below:
+
+1. **Clone the Repository**:
+
+ If you have **Git** installed, you can use the following git command to clone the **FiSE** GitHub repository into the current working directory:
+
+ ```bash
+ git clone https://github.com/rahul4732saini/fise.git
+ ```
+
+ Otherwise, you can download the source code archive file from the [FiSE GitHub Repository](https://www.github.com/rahul4732saini/fise).
+
+2. **Change the current working directory**:
+
+ Change the current directory to the source code directory, i.e., `./fise` to perform the remaining installation:
+
+ ```bash
+ cd ./fise
+ ```
+
+3. **Install Dependencies**:
+
+ All the base requirements are specified within [requirements.txt](../requirements/requirements.txt) which can be installed using the following command:
+
+ ```bash
+ python -m pip install -r requirements/requirements.txt --no-cache-dir
+ ```
+
+ To utilize the additional features offered by FiSE, including the database export functionality, it is necessary to install the supplementary requirements specified within [requirements-extra.txt](../requirements/requirements-extra.txt). These requirements can be installed using the same procedure as mentioned before:
+
+ ```bash
+ python -m pip install -r requirements/requirements-extra.txt --no-cache-dir
+ ```
+
+4. **(Optional) Build Application**:
+
+ To build the application on your current system, follow the steps mentioned under the [Build Guide](./build-executable.md).
+
+
Running FiSE
+
+Once all the steps mentioned above for installation are completed, you can run the program using the following command to run the [main.py](../fise/main.py) file present within the **fise** directory:
+
+```bash
+python fise/main.py
+```
+
+Running this command opens a command-line interface similar to **SQL**, allowing users to type in queries and view the corresponding output.
+
+
Basic Usage
+
+FiSE allows you to perform file, directory, and data searches using SQL-like commands. Here are a few basic examples to get you started:
+
+### Example 1: Perform a File Search
+
+ To search for all Python files `*.py` in the current directory:
+
+ ```SQL
+ SELECT * FROM . WHERE name LIKE ".*\.py"
+ ```
+
+### Example 2: Search for Specific Data
+
+ To search for a specific string `FiSE` within files:
+
+ ```SQL
+ SELECT[TYPE DATA, MODE BYTES] * FROM . WHERE LINENO IN (10, 100) AND DATA LIKE ".*FiSE.*"
+ ```
+
+### Example 3: Delete Files
+
+ To delete all JavaScript files `*.js` from a directory:
+
+ ```SQL
+ DELETE FROM . WHERE name LIKE ".*\.js"
+ ```
+
+### Example 4: Delete Directories
+
+ To delete all directories which were created before `2020-01-01`:
+
+ ```SQL
+ DELETE[TYPE DIR] FROM . WHERE ctime < "2020-01-01"
+ ```
+
+
Next Steps
+
+To explore more advanced features, refer to the following guides for detailed insights into each topic:
+
+1. **[Query Syntax](./query/syntax.md)**: Learn the syntax for executing various queries in FiSE.
+2. **[Query Operations](./query/operations.md)**: Explore the various query operations available in FiSE.
+3. **[Query Conditions](./query/query-conditions.md)**: Discover the ways to write precise and efficient conditions for filtering search/delete records.
+
+---
diff --git a/doc/query/operations.md b/doc/query/operations.md
new file mode 100644
index 0000000..dc59ed9
--- /dev/null
+++ b/doc/query/operations.md
@@ -0,0 +1,82 @@
+
Query Operations Guide
+
+**FiSE** offers two different operations to its users namely **Search** and **Delete**. This guide provides an in-depth description of these query operations and explains their usage and structure, offering practical examples to help users understand and utilize these functionalities effectively.
+
+
Search Operation
+
+The Search operation allows users to query and search files, file contents, and directories based on the specified conditions. This operation also supports exporting search records to external files or databases.
+
+For more information about the search query syntax, please refer to the [Syntax](./syntax.md#1-search-query-syntax) guide.
+
+To know more about the different metadata fields which can be used for the search operation. Please refer to the [Query-Fields](./query-fields.md) guide.
+
+### Examples
+
+**File Search**:
+
+```SQL
+EXPORT FILE[./records.xlsx] R SELECT * FROM .
+```
+
+```SQL
+R SELECT[TYPE FILE] name, size[KB], ctime, atime FROM 'C:/Program Files' WHERE name LIKE 'Python310'
+```
+
+**Data Search**:
+
+```SQL
+SELECT[TYPE DATA] lineno, dataline FROM ./fise/query/conditions.py WHERE dataline LIKE '.*fiSE.*' AND lineno BETWEEN (1, 100)
+```
+
+```SQL
+RECURSIVE SELECT[TYPE DATA, MODE BYTES] * FROM . WHERE name LIKE '.*\.py'
+```
+
+**Directory Search**:
+
+```SQL
+R SELECT[TYPE DIR] name, parent FROM ./fise
+```
+
+```SQL
+EXPORT SQL[postgresql] SELECT[TYPE DIR] * FROM /usr/bin/
+```
+
+
Delete Operation
+
+The Delete operation allows users to remove files and directories based on the specified conditions. Unlike the search operation, the delete operation doesn't allow exporting deleted file/directory records.
+
+For more information about the delete query syntax, please refer to the [Syntax](./syntax.md#2-delete-query-syntax) guide.
+
+To know more about the different metadata fields which can be used for the delete operation. Please refer to the [Query-Fields](./query-fields.md) guide.
+
+### Examples
+
+**File Deletion**:
+
+```SQL
+DELETE FROM . WHERE filetype = '.js'
+```
+
+```SQL
+R DELETE FROM /home/usr/projects WHERE ctime < "2018-02-20" AND mtime < "2019-09-28"
+```
+
+**Directory Deletion**:
+
+```SQL
+DELETE[TYPE DIR] FROM .
+```
+
+```SQL
+RECURSIVE DELETE[TYPE DIR] FROM ../Documents WHERE atime < "2015-10-17"
+```
+
+
Next Steps
+
+To explore more advanced features, refer to the following guides for detailed insights into each topic:
+
+1. **[Query Fields](./query-fields.md)**: Dive deep and know about the different metadata fields which can be used with different query operations.
+2. **[Query Conditions](./query-conditions.md)**: Discover the ways to write precise and efficient conditions for filtering search/delete records.
+
+---
diff --git a/doc/query/query-conditions.md b/doc/query/query-conditions.md
new file mode 100644
index 0000000..2a63477
--- /dev/null
+++ b/doc/query/query-conditions.md
@@ -0,0 +1,137 @@
+
Query Conditions Guide
+
+This guide aims to provide a comprehensive overview of query conditions for filtering records in file, data and directory operations. It explains the various types of operators and operands that can be employed to define conditions, offering examples to illustrate their application. By following this guide, you will enhance your ability to construct precise and effective queries.
+
+### Basic Overview
+
+Conditions are defined at the end of the query after the path specifications and start with the `WHERE` keyword marking the beginning of conditions segment. Following is a basic representation of how the query would look like:
+
+```SQL
+SELECT * FROM . WHERE
+```
+
+All conditions are separated by delimiters listed under [Logical Operators](#logical-operators).
+
+
Operators
+
+Operators are symbols that specify the type of operation to be performed within individual query conditions. The following sections cover all the operators which can be used for defining query conditions in FiSE.
+
+### Comparison Operators
+
+- `=` : Equals
+- `!=`: Not equal
+- `<` : Less than
+- `<=`: Less than or equal to
+- `>` : Greater than
+- `>=`: Greater than or equal to
+
+### Collective Operators
+
+- `IN` : Checks if a value is within an array of values. Eg: `name IN ("main.py", "classes.py")`
+- `BETWEEN` : Checks if a value lies within a range of values. Eg: `ctime BETWEEN ("2022-01-01", "2023-01-01")`
+
+### Logical Operators
+
+Also known as **condition delimiters**, these operators are used for separating conditions from one-another.
+
+- `AND` : Only Evaluates to `true` if both of the adjacent conditions are `true`.
+- `OR` : Evaluates to `true` if either of the adjacent conditions are `true`.
+
+### Miscellaneous Operators
+
+- `LIKE` : Matches a string pattern (Uses standard Regular Expressions. For a deeper insight, please refer to the [Wikipedia](https://en.wikipedia.org/wiki/Regular_expression) page.)
+
+
Operands
+
+Operands are the values or entities on which operators act. FiSE allows operands from various data types to be a part of query conditions including strings, integers, floats, regular expressions, and metadata fields. The following sections provide a deeper insight into individual operand types.
+
+### Strings
+
+Strings are textual data enclosed within single or double quotes (' or ").
+
+### Integers
+
+Integers are whole numbers without any decimal points. They are typically used to represent numerical file attributes such as size in queries.
+
+### Floats
+
+Floats are numbers with decimal points. They can be used for more precise numerical comparisons within FiSE queries.
+
+
+### None
+
+Similar to the `NULL` keyword in SQL, FiSE uses `None` to represent empty values or undefined data. This can also be used within query conditions to verify the presence or absence of data.
+
+### Arrays
+
+Arrays in FiSE queries are collections of values(strings, integers, floats, or metadata fields) enclosed within parentheses `()`, separated by commas. They allow users to specify multiple values for operations like membership checking (`IN` operation) or to check if a value lies within a specific range (`BETWEEN` operation).
+
+### Regular Expressions
+
+Regular expressions act as a powerful tool for pattern matching within strings. These expressions are useful for users to search for files or directories based on complex string patterns.
+
+These regular expressions are also defined as strings enclosed within single or double quotes (' or ").
+
+**NOTE**: Regular expressions are only limited to the `LIKE` operation. These are specified after the operator in the condition. Below is the basic syntax for reference:
+
+```SQL
+ LIKE
+```
+
+### Metadata Fields
+
+Fields refer to attributes or columns within the data being queried. In FiSE queries, fields represent metadata values associated with files or directories, such as name, size, type, or timestamps.
+
+To get more details about the metadata fields available for different query operations, please refer to [Query-Fields](./query-fields.md).
+
+
Defining Conditions
+
+Below is the basic structure for defining query conditions:
+
+```SQL
+ ...
+```
+
+Where each individual condition is defined in the following manner:
+
+```SQL
+
+```
+
+### Nested Conditions
+
+FiSE also allows condition nesting with the use of curly braces `()`. Conditions can be nested as deeply as desired within a query.
+
+
Examples
+
+**Example 1**: Recursively select all files from the current directory whose name is `__init__.py`:
+
+```SQL
+R SELECT parent, size[KB], ctime FROM . WHERE name = "__init__.py"
+```
+
+**Example 2**: Recursively delete all files from `./documents` directory which have not been accessed since `2018-06-13` and have a filetype of `.docx`:
+
+```SQL
+R DELETE FROM ./documents WHERE atime <= "2018-06-13" AND type = ".docx"
+```
+
+**Example 3**: Select all files from `/home/user` directory whose name is in the following names: `roadmap.txt`, `projects.txt` and `specifications.txt`.
+
+```SQL
+SELECT * FROM `/home/user/` WHERE name IN ("roadmap.txt", "projects.txt", "specifications.txt")
+```
+
+**Example 4**: Select all datalines present within the files in the current directory which have a filetype `.py` and have the word `parse` in them.
+
+```SQL
+SELECT[TYPE DATA, MODE BYTES] * FROM . WHERE FILETYPE = '.py' AND DATALINE like ".*parse.*"
+```
+
+**Example 5**: Select all directories from `./media` directory which were created between `2010-01-01` and `2020-12-31` or `2023-01-01` and `2023-12-31` and whose name ends with `-pictures`:
+
+```SQL
+SELECT[TYPE DIR] * FROM ./media WHERE (ctime BETWEEN ("2010-01-01", "2020-12-31") or ctime BETWEEN ("2023-01-01", "2023-12-31")) AND NAME LIKE '.*-pictures$'
+```
+
+---
diff --git a/doc/query/query-fields.md b/doc/query/query-fields.md
new file mode 100644
index 0000000..6f0801e
--- /dev/null
+++ b/doc/query/query-fields.md
@@ -0,0 +1,88 @@
+
Query Fields
+
+This section provides a detailed overview of the various metadata fields that can be used for search and delete operations. Understanding these fields will help you craft precise queries to efficiently manage files, directories, and data.
+
+### Directory Metadata Fields
+
+1. **name**:
+ - Extracts the name of the directory.
+
+2. **path**:
+ - Extracts the relative or absolute path of the directory based on the specified path type or if it is explicitly designated as ABSOLUTE in the query.
+
+3. **parent**:
+ - Extracts the relative or absolute path pf the parent directory based on the specified path type or if it is explicitly designated as ABSOLUTE in the query.
+
+4. **create_time**:
+ - Extracts the creation time of the directory.
+ - **Alias**: *ctime*
+
+5. **modify_time**:
+ - Extracts the last modification time of the directory.
+ - **Alias**: *mtime*
+
+6. **access_time**:
+ - Extracts the last access time of the directory.
+ - **Alias**: *atime*
+
+7. **owner**:
+ - Extracts the name of the owner of the directory.
+ - Only available on POSIX-based systems.
+
+8. **group**:
+ - Extracts the name of the group of the directory.
+ - Only available on POSIX-based systems.
+
+9. **permissions**:
+ - Extracts the 5-digit permissions code of the directory.
+ - Only available on POSIX-based systems.
+ - **Alias**: *perms*
+
+### File Metadata Fields
+
+**NOTE**: All metadata fields applicable to directories are also relevant for files except the *name* and *path* field, with the addition of the following specific fields:
+
+1. **name**:
+ - Extracts the name of the file.
+ - **Alias**: *filename*
+
+2. **path**:
+ - Extracts the relative or absolute path of the file based on the specified path type or if it is explicitly designated as ABSOLUTE in the query.
+ - **Alias**: *filepath*
+
+1. **filetype**:
+ - Extracts the filetype of file.
+ - **Alias**: *type*
+
+2. **size**:
+
+ - Extracts the size of the file.
+
+ - Users can also extract the file size in different units by accompanying the field name 'size' with a square brackets `[]` and typing in a unit specified within the **Different Size Units** section below. Eg: `size[KB]`, `size[GiB]` and `size[b]`.
+
+ - **Different Size Units**:
+
+ b, B, Kib, Kb, KiB, KB, Mib, Mb, MiB, MB, Gib, Gb, GiB, GB, Tib, Tb, TiB, TB
+
+ All units correspond to their standard equivalents in size.
+
+### File-contents Metadata Fields
+
+1. **name**:
+ - Extracts the name of the file.
+
+2. **path**:
+ - Extracts the relative or absolute path to the file based on the specified path type or if it is explicitly designated as ABSOLUTE in the query.
+
+3. **filetype**:
+ - Extracts the filetype of the file.
+ - **Alias**: *type*
+
+4. **dataline**:
+ - Extracts the data at the current line.
+ - **Alias**: *data*, *line*
+
+5. **lineno**:
+ - Extract the line number of the dataline.
+
+---
diff --git a/doc/query/syntax.md b/doc/query/syntax.md
new file mode 100644
index 0000000..2cf0c73
--- /dev/null
+++ b/doc/query/syntax.md
@@ -0,0 +1,98 @@
+
Query Syntax Guide
+
+This comprehensive guide offers detailed insights into the query syntax, aiming to help users grasp the various components and structures of different query types enabling them to perform efficient file, directory, and data search and delete operations using SQL-like commands.
+
+
Query Syntax Breakdown
+
+**FiSE** offers two primary operations to its users namely **Search** and **Delete**. To get deep insight of these operations, please refer to the [Operations](./operations.md) guide.
+
+#### 1. Search Query Syntax
+
+```SQL
+(EXPORT (FILE[]|SQL[])) (R|RECURSIVE) SEARCH[] FROM (RELATIVE|ABSOLUTE) (|) (WHERE )
+```
+
+#### 2. Delete Query Syntax
+
+```SQL
+(R|RECURSIVE) DELETE[] FROM (RELATIVE|ABSOLUTE) (|) (WHERE )
+```
+
+### Individual Components Explanation
+
+1. **EXPORT FILE[ FILEPATH ]** or **EXPORT SQL[ DATABASE ]**:
+
+ - This segment is optional and is used for exporting search records to a file or database.
+
+ - **NOTE**: Currently, exporting records is only limited to search operations.
+
+ - **File Export**: **FiSE** allows search records export only to the file formats mentioned below under the **Available File Formats** section below. To export to a file, the following rules must be followed:
+
+ - The file specified must be non-existant.
+ - The file name must be followed by a allowed suffix, **FiSE** recognizes the file export type explicitly based on the suffix of the file specified.
+
+ - **Available File Formats**: **csv**, **html**, **xlsx** and **json**.
+ - **Available Databases**: **mysql**, **postgresql** and **sqlite**.
+
+ - **Example**: `EXPORT FILE[./results.csv]` and `EXPORT SQL[mysql]`
+
+2. **R** or **RECURSIVE**:
+
+ - This segment is optional and allows recursive inclusion of all the files and sub-directories present within the specified directory.
+
+3. **SELECT[ PARAMETERS ]** or **DELETE[ PARAMETERS ]**:
+
+ - This segments includes the operation specifications. Users can choose between two different query operations: **Search** and **Delete**.
+
+ - **Additional parameters** can be specified to toggle between different file-types or file-modes, especially for data search operations. These can be defined within square brackets `[]` adjoining the name of the operation where each parameter is seperated by commas. Refer to the **Parameters Types** section below to know more about the different types of parameters available for operation configuration.
+
+ - **Parameters Types**: The following are the different types of parameters which can be defined within the operation specifications:
+
+ - **TYPE**: It is used to toggle between file, directory and data operation. The type can only be set to `data` for search operation and is not available for delete operations. To specify this parameter, use the following format: `TYPE (FILE|DIR|DATA)`. Eg: `SELECT[TYPE DIR]` configures the operation to work with directories and `DELETE[TYPE FILE]` toggles the operation to work with files.
+
+ - **MODE**: It is used to toggle between text and bytes filemode. It is only limited to data search operations. To specify this parameter, use the following format: `MODE (TEXT|BYTES)`. Eg: `SELECT[TYPE DATA, MODE BYTES]` toggles the operation to work with bytes data and `SELECT[TYPE DATA, MODE TEXT]` configures the operation to work with text data.
+
+ - **NOTE**: By default, the `TYPE` parameter is set to `file` and the `MODE` is set to `text` for data search operation and do not require explicit mentionings. Users must also note that the `text` filemode can only read text files and will raise an error for bytes data.
+
+ - Operation specifications examples with different parameters:
+
+ - `SELECT` : Select files
+ - `DELETE` : Delete files
+ - `SELECT[TYPE DIR]` : Select directories
+ - `SELECT[TYPE DATA, MODE BYTES]` : Select file-contents in bytes filemode
+ - `DELETE[TYPE FILE]` : Delete files
+ - `SELECT[TYPE DATA]`: Select file-contents in text filemode
+
+4. **FIELDS**:
+
+ - This segment is only limited to search operations and defines the metadata fields related to the searched files, data, or directories which are to be displayed in the output search dataframe.
+
+ - For a deeper insight into the available metdata fields for different search query types, please refer to the [Query-Fields](./query-fields.md) guide.
+
+5. **ABSOLUTE** or **RELATIVE**:
+
+ - This segment is optional and specifies whether to include the absolute path of the files and directories if the specified path is relative.
+
+6. **FILE PATH** or **DIRECTORY PATH**:
+
+ - Defines the path to the file or directory to operate upon. The path can be either absolute or relative as desired and can be specified directly without any other specifications.
+
+ - **Note**: Paths comprising whitespaces must be enclosed within single quotes `'` or double quotes `"`.
+
+ - **Example**: `./src`, `/usr/local/bin` and `"C:/Program Files/Common Files"`
+
+7. **WHERE CONDITIONS**:
+
+ - This segment is optional and is used for filtering search and delete records based on the specified conditions.
+
+ - For a deeper insight into query conditions, please refer to the [Query-Conditions](./query-conditions.md) guide.
+
+
Next Steps
+
+To explore more advanced features, refer to the following guides for detailed insights into each topic:
+
+1. **[Query Fields](./query-fields.md)**: Dive deep and know about the different metadata fields which can be used with different query operations.
+2. **[Query Operations](./query/operations.md)**: Explore the various query operations available in FiSE.
+3. **[Query Conditions](./query/export.md)**: Discover the ways to write precise and efficient conditions for filtering search and delete records.
+
+---
diff --git a/fise/common/__init__.py b/fise/common/__init__.py
index e69de29..808b060 100644
--- a/fise/common/__init__.py
+++ b/fise/common/__init__.py
@@ -0,0 +1,12 @@
+"""
+Common package
+--------------
+
+This packages comprises constants and utility functions designed
+for assisting others classes and functions throughout the project.
+"""
+
+__all__ = "constants", "tools"
+
+from . import constants
+from . import tools
diff --git a/fise/common/constants.py b/fise/common/constants.py
index e69de29..30f7195 100644
--- a/fise/common/constants.py
+++ b/fise/common/constants.py
@@ -0,0 +1,91 @@
+"""
+Constants Module
+----------------
+
+This module comprises constants designed to assist various
+classes and functions defined within the project.
+"""
+
+import re
+import sys
+from typing import Literal
+
+# Additional fields for Posix-based operating systems.
+POSIX_FIELDS = ("owner", "group", "permissions") if sys.platform != "win32" else ()
+
+# Search query fields for various query types.
+DIR_FIELDS = (
+ "name", "path", "parent", "access_time", "create_time", "modify_time"
+) + POSIX_FIELDS
+
+DATA_FIELDS = "name", "path", "lineno", "dataline", "filetype"
+FILE_FIELDS = DIR_FIELDS + ("size", "filetype")
+
+OPERATIONS = Literal["search", "remove"]
+OPERANDS = Literal["file", "data", "dir"]
+SEARCH_QUERY_OPERANDS = {"file", "data", "dir"}
+OPERATION_ALIASES = {"select", "delete"}
+
+FILE_MODES = Literal["text", "bytes"]
+FILE_MODES_MAP = {"text": "r", "bytes": "rb"}
+
+CONDITION_SEPARATORS = {"and", "or"}
+COMPARISON_OPERATORS = {"<", ">", "<=", ">=", "!=", "="}
+CONDITIONAL_OPERATORS = {"in", "between", "like"}
+
+SIZE_UNITS = Literal[
+ "b", "B", "Kb", "KB", "Kib", "KiB", "Mb", "MB", "Mib",
+ "MiB", "Gb", "GB", "Gib", "GiB", "Tb", "TB", "Tib", "TiB"
+]
+
+STRING_PATTERN = re.compile(r"^['\"].*['\"]$")
+
+# Mapping of storage unit string labels mapped with corresponding divisors
+# for storage size conversion into specified units.
+SIZE_CONVERSION_MAP = {
+ "b": 0.125, "B": 1, "Kb": 125, "KB": 1e3, "Kib": 128, "KiB": 1024, "Mb": 1.25e5,
+ "MB": 1e6, "Mib": 131_072, "MiB": 1024**2, "Gb": 1.25e8, "GB": 1e9, "Gib": 134_217_728,
+ "GiB": 1024**3, "Tb": 1.25e11, "TB": 1e12, "Tib": 137_438_953_472, "TiB": 1024**4
+}
+
+# Mapping of file suffixes mapped with corresponding
+# `pandas.DataFrame` methods for exporting data.
+DATA_EXPORT_TYPES_MAP = {
+ ".csv": "to_csv",
+ ".json": "to_json",
+ ".html": "to_html",
+ ".xlsx": "to_excel",
+}
+
+# Additional field aliases for Posix-based operating systems.
+POSIX_FIELD_ALIASES = {"perms": "permissions"} if sys.platform != "win32" else {}
+
+DIR_FIELD_ALIASES = POSIX_FIELD_ALIASES | {
+ "ctime": "create_time",
+ "mtime": "modify_time",
+ "atime": "access_time",
+}
+
+FILE_FIELD_ALIASES = DIR_FIELD_ALIASES | {
+ "filepath": "path",
+ "filename": "name",
+ "type": "filetype",
+}
+
+DATA_FIELD_ALIASES = {
+ "filename": "name",
+ "filepath": "path",
+ "data": "dataline",
+ "line": "dataline",
+ "type": "filetype",
+}
+
+PATH_TYPES = {"absolute", "relative"}
+
+# Supported databases for exporting search records.
+DATABASES = {"postgresql", "mysql", "sqlite"}
+
+DATABASE_URL_DIALECTS = {
+ "postgresql": "postgresql://",
+ "mysql": "mysql+pymysql://",
+}
diff --git a/fise/common/tools.py b/fise/common/tools.py
index e69de29..03d743f 100644
--- a/fise/common/tools.py
+++ b/fise/common/tools.py
@@ -0,0 +1,233 @@
+"""
+Tools module
+------------
+
+This module comprises utility functions supporting
+other classes and functions throughout the project.
+"""
+
+import getpass
+from pathlib import Path
+from typing import Generator, Any
+
+import numpy as np
+import pandas as pd
+from sqlalchemy.exc import OperationalError
+from sqlalchemy.engine import Engine, URL, Connection
+import sqlalchemy
+
+from . import constants
+from errors import QueryParseError, OperationError, QueryHandleError
+from notify import Alert
+
+
+def parse_query(query: str) -> list[str]:
+ """
+ Parses the specified raw string query and converts into
+ a list of tokens for further parsing and evaluation.
+
+ #### Params:
+ - query (str): Query to be parsed.
+ """
+
+ delimiters: dict[str, str] = {"[": "]", "(": ")", "'": "'", '"': '"'}
+ conflicting: set[str] = {"'", '"'}
+ tokens: list[str] = []
+
+ # Temporarily stores the current token.
+ token = ""
+
+ # Stores an array of current starting delimiters in the specified
+ # query which are not yet terminated during iteration.
+ cur: list[str] = []
+
+ # Adds a whitespace at the end of the query to avoid
+ # parsing the last token separately after iteration.
+ for char in query + " ":
+ # Only executes the conditional block if the character is a starting
+ # delimiter and not nested inside or in the conflicting delimiters.
+ if char in delimiters and (not cur or char not in conflicting):
+ cur.append(char)
+ token += char
+
+ # Adds the current character to the list if it is a terminating delimiter
+ # and also pops up its corresponding starting delimiter in the `cur` list.
+ elif cur and char == delimiters.get(cur[-1]):
+ cur.pop()
+ token += char
+
+ # Adds to list if the character is a top-level whitespace
+ # and `token` is not nested or an empty string.
+ elif not cur and char.isspace():
+ if token:
+ tokens.append(token)
+ token = ""
+
+ else:
+ token += char
+
+ if token:
+ raise QueryParseError(f"Invalid query syntax around {token[:-1]!r}")
+
+ return tokens
+
+
+def get_files(directory: Path, recursive: bool) -> Generator[Path, None, None]:
+ """
+ Returns a `typing.Generator` object of all files present within the specified directory.
+ Files present within subdirectories are also extracted if `recursive` is set to `True`.
+
+ #### Params:
+ - directory (pathlib.Path): Path to the directory.
+ - recursive (bool): Whether to include files from subdirectories.
+ """
+
+ try:
+ for path in directory.iterdir():
+ if path.is_file():
+ yield path
+
+ # Extracts files from sub-directories.
+ elif recursive and path.is_dir():
+ yield from get_files(path, recursive)
+
+ except PermissionError:
+ Alert(f"Permission Error: Skipping directory '{directory}'")
+
+ # Yields from an empty tuple to not disrupt
+ # the proper functioning of the function.
+ yield from ()
+
+
+def get_directories(directory: Path, recursive: bool) -> Generator[Path, None, None]:
+ """
+ Returns a `typing.Generator` object of all subdirectories present within the specified
+ directory. Directories present within subdirectories are also extracted if `recursive`
+ is set to `True`.
+
+ #### Params:
+ - directory (pathlib.Path): Path to the directory.
+ - recursive (bool): Whether to include files from subdirectories.
+ """
+
+ try:
+ for path in directory.iterdir():
+ if not path.is_dir():
+ continue
+
+ if recursive:
+ yield from get_directories(path, recursive)
+
+ yield path
+
+ except PermissionError:
+ Alert(f"Permission Error: Skipping directory '{directory}'")
+
+ # Yields from an empty tuple to not disrupt
+ # the proper functioning of the function.
+ yield from ()
+
+
+def export_to_file(data: pd.DataFrame, file: Path) -> None:
+ """
+ Exports search data to the specified file in a suitable format.
+
+ #### Params:
+ - data (pd.DataFrame): pandas DataFrame comprising search records.
+ - file (Path): Path to the file.
+ """
+
+ kwargs: dict[str, Any] = {}
+
+ # String representation of the export method for exporting search records.
+ export_method: str = constants.DATA_EXPORT_TYPES_MAP[file.suffix]
+
+ # Converts datetime objects present in datetime columns into
+ # string objects for better representation in Excel files.
+ if export_method == "to_excel":
+ for col in data.columns[data.dtypes == np.dtype(" Engine:
+ """
+ Connects to a SQLite database file.
+ """
+ database: Path = Path(input("Enter the path to the database file: "))
+ return sqlalchemy.create_engine(f"sqlite:///{database}")
+
+
+def _connect_database(database: str) -> Engine:
+ """
+ Connects to the specified SQL database server.
+
+ #### Params:
+ - database (str): The name of the database to connect.
+ """
+
+ # Inputs database credentials.
+ user: str = input("Username: ")
+ passkey: str = getpass.getpass("Password: ")
+ host: str = input("Host [localhost]: ") or "localhost"
+ port: str = input("Port: ")
+ database: str = input("Database: ")
+
+ if not port:
+ raise QueryHandleError(f"Invalid port number: {port!r}")
+
+ url = URL.create(
+ constants.DATABASE_URL_DIALECTS[database], user, passkey, host, port, database
+ )
+
+ return sqlalchemy.create_engine(url)
+
+
+def export_to_sql(data: pd.DataFrame, database: str) -> None:
+ """
+ Exports search records to the specified database.
+
+ #### Params:
+ - data (pd.DataFrame): pandas DataFrame comprising search records.
+ - database (str): The name of the database to connect.
+ """
+
+ # Creates an `sqlalchemy.Engine` object of the specified SQL database.
+ engine: Engine = (
+ _connect_sqlite() if database == "sqlite" else _connect_database(database)
+ )
+
+ table: str = input("Table name: ")
+ metadata = sqlalchemy.MetaData()
+
+ try:
+ metadata.reflect(bind=engine)
+ conn: Connection = engine.connect()
+
+ except OperationalError:
+ raise OperationError(f"Unable to connect to {database!r} database.")
+
+ else:
+ # Prompts for replacement if the specified table already exists in the database.
+ if table in metadata:
+ force: str = input(
+ "The specified table already exists, would you like to alter it? (Y/N) "
+ )
+
+ if force.lower() != "y":
+ print("Export cancelled!")
+
+ # Raises `QueryHandleError` without any message to terminate the current query.
+ raise QueryHandleError
+
+ data.to_sql(table, conn, if_exists="replace", index=False)
+
+ finally:
+ conn.close()
+ engine.dispose(close=True)
diff --git a/fise/errors.py b/fise/errors.py
index e69de29..7145a52 100644
--- a/fise/errors.py
+++ b/fise/errors.py
@@ -0,0 +1,42 @@
+"""
+Errors Module
+-------------
+
+This module defines error classes used throughout the
+FiSE project to handle various exceptional scenarios.
+"""
+
+import sys
+
+
+class QueryHandleError(Exception):
+ """
+ Exception raised when there is an error in handling the query.
+ """
+
+ _error: str = "QueryHandleError: There is an error in handling the query."
+
+ def __init__(self, description: str = "") -> None:
+
+ # Only prints the error description if specified explicitly.
+ if description:
+ description = "\nDescription: " + description
+
+ print(f"\033[31m{self._error}{description}\033[0m", file=sys.stderr)
+ super().__init__()
+
+
+class QueryParseError(QueryHandleError):
+ """
+ Exception raised when there is an error in parsing the query.
+ """
+
+ _error = "QueryParseError: There is an error in parsing the query."
+
+
+class OperationError(QueryHandleError):
+ """
+ Exception raised when there is an error in processing the query.
+ """
+
+ _error = "OperationError: There is an error in processing the query."
diff --git a/fise/main.py b/fise/main.py
new file mode 100644
index 0000000..0699239
--- /dev/null
+++ b/fise/main.py
@@ -0,0 +1,110 @@
+"""
+Main Module
+-----------
+
+This script serves as the entry point for the FiSE (File Search Engine)
+application. It provides a command-line interface for users to perform
+search and delete queries.
+
+Author: rahul4732saini (github.com/rahul4732saini)
+License: MIT
+"""
+
+import sys
+import time
+
+import pandas as pd
+
+from version import version
+from notify import Message, Alert
+from query import QueryHandler
+from errors import QueryHandleError
+
+
+EXIT = {"exit", "quit"}
+CLEAR = {r"\c", "clear"}
+
+
+def enable_readline() -> None:
+ """
+ Enables readline functionality for enhanced input handling in Linux/Mac terminal.
+ """
+ import readline
+
+ readline.parse_and_bind("tab: complete")
+
+
+def evaluate_query() -> None:
+ """
+ Inputs and evaluates the user specified query.
+ """
+ global EXIT, CLEAR
+
+ query: str = input("FiSE> ")
+ start_time: float = time.perf_counter()
+
+ if not query:
+ return
+
+ elif query.lower() in EXIT:
+ sys.exit(0)
+
+ elif query.lower() in CLEAR:
+ return print("\033c", end="")
+
+ # If none of the above conditions are matched, the input
+ # is assumed to be a query and evaluated accordingly.
+
+ handler = QueryHandler(query)
+ data: pd.DataFrame | None = handler.handle()
+
+ if data is not None:
+
+ if data.shape[0] > 30:
+ Alert(
+ "Displaying a compressed output of the dataset. "
+ "Export the records for a more detailed view."
+ )
+
+ print(data if not data.empty else "Empty Dataset")
+
+ elapsed: float = time.perf_counter() - start_time
+ Message(f"Completed in {elapsed:.2f} seconds")
+
+
+def main() -> None:
+ """Main function for program execution."""
+
+ while True:
+ try:
+ evaluate_query()
+
+ except KeyboardInterrupt:
+ print("^C")
+
+ except EOFError:
+ sys.exit(0)
+
+ except QueryHandleError:
+ ...
+
+ except Exception as e:
+ QueryHandleError(str(e))
+
+
+if __name__ == "__main__":
+
+ # Sets an upper limit for max rows to be displayed in the dataframe.
+ pd.options.display.max_rows = 30
+
+ print(
+ f"Welcome to FiSE(v{version})",
+ r"Type '\c' or 'clear' to clear the terminal window. Type 'exit' or 'quit' to quit.",
+ sep="\n",
+ )
+
+ # Enables readline feature if the current operating system is not windows.
+ if sys.platform != "win32":
+ enable_readline()
+
+ main()
diff --git a/fise/notify.py b/fise/notify.py
new file mode 100644
index 0000000..fcb9dfd
--- /dev/null
+++ b/fise/notify.py
@@ -0,0 +1,21 @@
+"""
+Notify Module
+-------------
+
+This module comprises class for displaying alerts
+and notifications on the command-line interface.
+"""
+
+
+class Message:
+ """Prints a success message on the terminal window."""
+
+ def __init__(self, mesg: str) -> None:
+ print(f"\033[32m{mesg}\033[0m")
+
+
+class Alert:
+ """Prints an alert message on the terminal window."""
+
+ def __init__(self, mesg: str) -> None:
+ print(f"\033[33m{mesg}\033[0m")
diff --git a/fise/ospecs.py b/fise/ospecs.py
new file mode 100644
index 0000000..d370cef
--- /dev/null
+++ b/fise/ospecs.py
@@ -0,0 +1,137 @@
+"""
+OS Specifications Module
+------------------------
+
+This module comprises class and utility functions tailored for different
+operating system facilitating seamless integration and efficient handling
+of platform-specific tasks across diverse environments.
+"""
+
+import os
+from pathlib import Path
+from typing import Callable, Any
+from datetime import datetime
+
+from notify import Alert
+
+
+def _field_extraction_alert() -> None:
+ """
+ Raises an alert indicating an error in metadata fields
+ extraction from the recorded files/directories.
+ """
+
+ if BaseEntity.field_alert:
+ return
+
+ Alert(
+ "ExtractionError: Unable to access specific metadata fields from the "
+ "recorded files/directories. These fields are being assigned as 'None'."
+ )
+
+ # Sets `field_alert` attribute to `True` to avoid alert repetition.
+ BaseEntity.field_alert = True
+
+
+def safe_extract_field(func: Callable[..., Any]) -> Callable[..., Any] | None:
+ """
+ Safely executes the specified field extraction
+ function and returns None in case of an Exception.
+ """
+
+ def wrapper(self) -> Any:
+ try:
+ return func(self)
+
+ except Exception:
+ _field_extraction_alert()
+
+ return wrapper
+
+
+class BaseEntity:
+ """
+ BaseEntity class serves as the base class for accessing all methods and attributes
+ related to a file/directory `pathlib.Path` and `os.stat_result` object.
+ """
+
+ # Boolean value to specify whether a field extraction alert has already
+ # been encountered to only alert the user once during the operation.
+ field_alert = False
+
+ __slots__ = "_path", "_stats"
+
+ def __init__(self, path: Path) -> None:
+ """
+ Creates an instance of the `BaseEntity` class.
+
+ #### Params:
+ - file (pathlib.Path): path to the file/directory.
+ """
+ self._path: Path = path
+ self._stats: os.stat_result = path.stat()
+
+ @property
+ @safe_extract_field
+ def name(self) -> str:
+ return self._path.name
+
+ @property
+ @safe_extract_field
+ def path(self) -> str:
+ return self._path.as_posix()
+
+ @property
+ @safe_extract_field
+ def parent(self) -> str:
+ return self._path.parent.as_posix()
+
+ @property
+ @safe_extract_field
+ def access_time(self) -> datetime:
+ return datetime.fromtimestamp(self._stats.st_atime).replace(microsecond=0)
+
+ @property
+ @safe_extract_field
+ def create_time(self) -> datetime:
+ return datetime.fromtimestamp(self._stats.st_ctime).replace(microsecond=0)
+
+ @property
+ @safe_extract_field
+ def modify_time(self) -> datetime:
+ return datetime.fromtimestamp(self._stats.st_mtime).replace(microsecond=0)
+
+
+class WindowsEntity(BaseEntity):
+ """
+ WindowsEntity class serves as a unified class for accessing
+ all methods and attributes related to a Windows file/directory
+ `pathlib.Path` and `os.stat_result` object.
+ """
+
+ __slots__ = "_path", "_stats"
+
+
+class PosixEntity(BaseEntity):
+ """
+ PosixEntity class serves as a unified class for accessing
+ all methods and attributes related to a Posix file/directory
+ `pathlib.Path` and `os.stat_result` object.
+ """
+
+ __slots__ = "_path", "_stats"
+
+ @property
+ @safe_extract_field
+ def owner(self) -> str:
+ return self._path.owner()
+
+ @property
+ @safe_extract_field
+ def group(self) -> str:
+ return self._path.group()
+
+ @property
+ @safe_extract_field
+ def permissions(self) -> int:
+ return self._stats.st_mode
diff --git a/fise/query/__init__.py b/fise/query/__init__.py
new file mode 100644
index 0000000..b0f9254
--- /dev/null
+++ b/fise/query/__init__.py
@@ -0,0 +1,357 @@
+"""
+Query Package
+-------------
+
+This package provides a collection of classes and functions
+designed for handling user-specified search and delete queries.
+"""
+
+import re
+from pathlib import Path
+from typing import Callable, Generator
+
+import pandas as pd
+
+from common import constants, tools
+from .parsers import FileQueryParser, DirectoryQueryParser, FileDataQueryParser
+from .operators import FileQueryOperator, FileDataQueryOperator, DirectoryQueryOperator
+from shared import QueryInitials, ExportData, OperationData, DeleteQuery, SearchQuery
+from errors import QueryParseError, OperationError
+
+__all__ = ("QueryHandler",)
+
+
+class QueryHandler:
+ """
+ QueryHandler defines methods for parsing and
+ processing user-specified search and delete queries.
+ """
+
+ __slots__ = "_query", "_ctr", "_handler_map"
+
+ # Regular expression patterns for parsing sub-queries.
+ _export_subquery_pattern = re.compile(r"^(sql|file)\[.*]$")
+ _operation_params_pattern = re.compile(r"^(\[.*])?$")
+
+ def __init__(self, query: str) -> None:
+ """
+ Creates an instance of the `QueryHandler` class.
+
+ #### Params:
+ - query (str): Query to be handled.
+ """
+
+ # Maps targeted operands names with corresponding methods for handling the query.
+ self._handler_map: dict[str, Callable[[QueryInitials], pd.DataFrame | None]] = {
+ "file": self._handle_file_query,
+ "dir": self._handle_dir_query,
+ "data": self._handle_data_query,
+ }
+
+ # Keeps track of the current position of the token to be parsed in the query.
+ self._ctr = 0
+
+ self._query = tools.parse_query(query)
+
+ def handle(self) -> pd.DataFrame | None:
+ """
+ Handles the specified search/delete query.
+ """
+
+ self._ctr = 0
+
+ try:
+ initials: QueryInitials = self._parse_initials()
+
+ except IndexError:
+ raise QueryParseError("Invalid query syntax.")
+
+ # Calls the corresponding handler method, extracts, and stores the
+ # search records if search operation is specified else stores `None`.
+ data: pd.DataFrame | None = self._handler_map[initials.operation.operand](
+ initials
+ )
+
+ if not initials.export:
+ return data
+
+ if initials.export.type_ == "file":
+ tools.export_to_file(data, initials.export.target)
+
+ else:
+ tools.export_to_sql(data, initials.export.target)
+
+ def _handle_file_query(self, initials: QueryInitials) -> pd.DataFrame | None:
+ """Parses and handles the specified file search or delete query"""
+
+ parser = FileQueryParser(self._query[self._ctr:], initials.operation.operation)
+ query: SearchQuery | DeleteQuery = parser.parse_query()
+
+ operator = FileQueryOperator(query.path, initials.recursive)
+
+ if initials.operation.operation == "search":
+ return operator.get_dataframe(query.fields, query.columns, query.condition)
+
+ operator.remove_files(query.condition, initials.operation.skip_err)
+
+ def _handle_data_query(self, initials: QueryInitials) -> pd.DataFrame:
+ """Parses and handles the specified data search query"""
+
+ if (
+ initials.export
+ and initials.operation.filemode == "bytes"
+ and initials.export.type_ == "database"
+ ):
+ raise QueryParseError(
+ "Exporting binary data to SQL databases is currently unsupported"
+ )
+
+ parser = FileDataQueryParser(self._query[self._ctr:])
+ query: SearchQuery = parser.parse_query()
+
+ operator = FileDataQueryOperator(
+ query.path, initials.recursive, initials.operation.filemode
+ )
+
+ return operator.get_dataframe(query.fields, query.columns, query.condition)
+
+ def _handle_dir_query(self, initials: QueryInitials) -> pd.DataFrame | None:
+ """
+ Parses and handles the specified directory search/delete query.
+ """
+
+ parser = DirectoryQueryParser(
+ self._query[self._ctr:], initials.operation.operation
+ )
+
+ query: SearchQuery | DeleteQuery = parser.parse_query()
+ operator = DirectoryQueryOperator(query.path, initials.recursive)
+
+ if initials.operation.operation == "search":
+ return operator.get_dataframe(query.fields, query.columns, query.condition)
+
+ operator.remove_directories(query.condition, initials.operation.skip_err)
+
+ def _parse_initials(self) -> QueryInitials:
+ """
+ Parses the query initials.
+ """
+
+ recursive: bool = False
+ export: ExportData | None = self._parse_export_data()
+
+ if self._query[self._ctr].lower() in ("r", "recursive"):
+ recursive = True
+ self._ctr += 1
+
+ # Parses the query operation
+ operation: OperationData = self._parse_operation()
+ self._ctr += 1
+
+ if export and operation.operation == "remove":
+ raise QueryParseError("Cannot export data with delete operation.")
+
+ return QueryInitials(operation, recursive, export)
+
+ @staticmethod
+ def _parse_operation_type(type_: str) -> str:
+ """
+ Parses the query operation type.
+ """
+ if type_ not in constants.SEARCH_QUERY_OPERANDS:
+ raise QueryParseError(f"Invalid value {type_!r} for 'type' parameter.")
+
+ return type_
+
+ def _parse_search_operation(
+ self, params: Generator[list[str], None, None]
+ ) -> OperationData:
+ """
+ Parses the search operation parameters.
+ """
+
+ operand: str = "file"
+ filemode: str | None = None
+
+ # Iterates through the parameters and parses them.
+ for param in params:
+ if len(param) != 2:
+ raise QueryParseError(
+ f"Invalid query syntax around {self._query[self._ctr]!r}"
+ )
+
+ # Splits the key-value pair in the list into variables for better readability.
+ key, value = param
+
+ if key == "type":
+ operand = self._parse_operation_type(value)
+
+ elif key == "mode":
+ if value not in constants.FILE_MODES_MAP:
+ raise QueryParseError(
+ f"Invalid value {value!r} for 'mode' parameter."
+ )
+
+ filemode = value
+
+ else:
+ raise QueryParseError(
+ f"Invalid parameter {key!r} for search operation."
+ )
+
+ if operand != "data" and filemode:
+ raise QueryParseError(
+ "The 'mode' parameter is only valid for filedata search operations."
+ )
+
+ elif operand == "data" and not filemode:
+ filemode = "text"
+
+ return OperationData("search", operand, filemode)
+
+ def _parse_delete_operation(
+ self, params: Generator[list[str], None, None]
+ ) -> OperationData:
+ """
+ Parses the delete operation parameters.
+ """
+
+ operand: str = "file"
+ skip_err: bool = False
+
+ # Iterates through the parameters and parses them.
+ for param in params:
+
+ if param[0] == "type":
+ operand = self._parse_operation_type(param[1])
+
+ elif param[0] == "skip_err":
+ if len(param) != 1:
+ raise QueryParseError(
+ f"Invalid query syntax around {self._query[self._ctr]!r}"
+ )
+
+ skip_err = True
+ else:
+ raise QueryParseError(
+ f"Invalid parameter {param[0]!r} for delete operation."
+ )
+
+ if operand == "data":
+ raise QueryParseError(
+ "Delete operation upon file contents is not supported."
+ )
+
+ return OperationData("remove", operand, skip_err=skip_err)
+
+ def _parse_operation(self) -> OperationData:
+ """
+ Parses the query operation specifications.
+ """
+
+ # Extracts and stores the operation and its parameter specifications.
+ operation, oparams = (
+ self._query[self._ctr][:6].lower(),
+ self._query[self._ctr][6:].lower(),
+ )
+
+ if operation not in constants.OPERATION_ALIASES:
+ raise QueryParseError(
+ f"Invalid operation {operation!r} specified in the query"
+ )
+
+ # Verifying operation parameters syntax.
+ if not self._operation_params_pattern.match(oparams):
+ raise QueryParseError(
+ f"Invalid query syntax around {self._query[self._ctr]!r}."
+ )
+
+ # Splits the parameters subquery about commas, and iterates
+ # through it striping whitespaces from individual parameters.
+ params: Generator[list[str], None, None] = (
+ i.strip().split(" ") for i in oparams[1:-1].split(",") if i
+ )
+
+ try:
+ data: OperationData = (
+ self._parse_search_operation(params)
+ if operation == "select"
+ else self._parse_delete_operation(params)
+ )
+
+ except IndexError:
+ raise QueryParseError(
+ f"Invalid query syntax around {self._query[self._ctr]!r}"
+ )
+
+ else:
+ return data
+
+ @staticmethod
+ def _parse_file_export_specs(export_specs: str) -> ExportData:
+ """
+ Parses and returns the file export specifications.
+ """
+
+ file: Path = Path(export_specs[5:-1])
+
+ if file.suffix not in constants.DATA_EXPORT_TYPES_MAP:
+ raise QueryParseError(
+ f"{file.suffix!r} file type is not supported for exporting search records."
+ )
+
+ if file.is_file():
+ raise OperationError(
+ "The specified path for exporting search "
+ "records must not point to an existing file."
+ )
+
+ elif not file.parent.exists():
+ raise OperationError(
+ f"The specified directory '{file.parent}' "
+ "for exporting search records cannot be found."
+ )
+
+ return ExportData("file", file)
+
+ @staticmethod
+ def _parse_sql_export_specs(export_specs: str) -> ExportData:
+ """
+ Parses and returns the SQL export specifications.
+ """
+
+ database: str = export_specs[4:-1]
+
+ if database not in constants.DATABASES:
+ raise QueryParseError(
+ f"Invalid database {database!r} specified for exporting search records."
+ )
+
+ return ExportData("database", database)
+
+ def _parse_export_data(self) -> ExportData | None:
+ """
+ Parses export specifications from the query if specified else returns `None`.
+ """
+
+ # The method doesn't make much use of the `ctr` attribute
+ # as it only parses the very initial tokens of the query.
+
+ if self._query[0].lower() != "export":
+ return None
+
+ # Increments the counter by 2, as the initial two tokens are to be
+ # parsed here and won't be required by the other parser methods.
+ self._ctr += 2
+ low_target: str = self._query[1].lower()
+
+ if not self._export_subquery_pattern.match(low_target):
+ raise QueryParseError(
+ "Unable to parse the export specifications in the query."
+ )
+
+ if low_target.startswith("file"):
+ return self._parse_file_export_specs(self._query[1])
+
+ # Character case of the specifications is ensured to be lowered for proper parsing.
+ return self._parse_sql_export_specs(low_target)
diff --git a/fise/query/conditions.py b/fise/query/conditions.py
new file mode 100644
index 0000000..4930945
--- /dev/null
+++ b/fise/query/conditions.py
@@ -0,0 +1,526 @@
+"""
+Conditions Module
+-----------------
+
+This module comprises classes for parsing and evaluating query
+conditions, and filtering file, data and directory records.
+"""
+
+import re
+from datetime import datetime
+from typing import Generator, Callable, Any
+
+from common import constants, tools
+from errors import QueryParseError, OperationError
+from shared import File, DataLine, Directory, Field, Condition, Size
+
+
+class ConditionParser:
+ """
+ ConditionParser defined methods for parsing query
+ conditions for search and delete operations.
+ """
+
+ __slots__ = "_query", "_method_map", "_lookup_fields", "_field_aliases"
+
+ # Regular expression patterns for matching fields in query conditions.
+ _tuple_pattern = re.compile(r"^\(.*\)$")
+ _float_pattern = re.compile(r"^-?\d+\.\d+$")
+
+ # This regex pattern only matches date and datetime formats, and does
+ # not explicitly verify the validity of the date and time values.
+ _datetime_pattern = re.compile(r"\d{4}-\d{1,2}-\d{1,2}( \d{1,2}:\d{1,2}:\d{1,2})?$")
+
+ _fields: dict[str, tuple[str, ...]] = {
+ "file": constants.FILE_FIELDS,
+ "dir": constants.DIR_FIELDS,
+ "data": constants.DATA_FIELDS,
+ }
+
+ _aliases: dict[str, dict[str, str]] = {
+ "file": constants.FILE_FIELD_ALIASES,
+ "dir": constants.DIR_FIELD_ALIASES,
+ "data": constants.DATA_FIELD_ALIASES,
+ }
+
+ def __init__(self, subquery: list[str], operand: str) -> None:
+ """
+ Creates an instance of the `ConditionParser` class.
+
+ #### Params:
+ - subquery (list[str]): Subquery comprising the conditions.
+ - operand (str): Targeted operand in the operation (file | data | directory).
+ """
+ self._query = subquery
+
+ self._lookup_fields = set(self._fields[operand])
+ self._field_aliases = self._aliases[operand]
+
+ def _parse_datetime(self, operand: str) -> datetime | None:
+ """
+ Parses date/datetime from the specified operand if it
+ matches the corresponding pattern, else returns None.
+ """
+
+ if not self._datetime_pattern.match(operand):
+ return None
+
+ try:
+ return datetime.strptime(operand, r"%Y-%m-%d %H:%M:%S")
+
+ except ValueError:
+ # Passes without raising an error if in
+ # case the operand matches the date format.
+ ...
+
+ try:
+ return datetime.strptime(operand, r"%Y-%m-%d")
+
+ except ValueError:
+ raise QueryParseError(
+ f"Invalid datetime specifications {operand!r} in query conditions."
+ )
+
+ def _parse_field(self, field: str) -> Field | Size:
+ """Parses the specified string formatted field"""
+
+ if field.startswith("size"):
+ return Size.from_string(field)
+
+ field = field.lower()
+ field = self._field_aliases.get(field, field)
+
+ if field not in self._lookup_fields:
+ raise QueryParseError(f"Found an invalid field {field!r} in the query.")
+
+ return Field(field)
+
+ def _parse_comparison_operand(self, operand: str) -> Any:
+ """
+ Parses individual operands specified within a comparison
+ operation expression for appropriate data type conversion.
+
+ #### Params:
+ - operand (str): Operand to be parsed.
+ """
+
+ if constants.STRING_PATTERN.match(operand):
+ # Strips the leading and trailing quotes in the string.
+ operand = operand[1:-1]
+ timedate: datetime | None = self._parse_datetime(operand)
+
+ return timedate or operand
+
+ elif self._float_pattern.match(operand):
+ return float(operand)
+
+ elif operand.isdigit():
+ return int(operand)
+
+ if operand.lower() == "none":
+ return None
+
+ # If none of the above conditions are matched, the operand is assumed
+ # to be a query field and returned as `Field` object or explicitly as
+ # a `Size` object for size fields.
+ return self._parse_field(operand)
+
+ def _parse_collective_operand(self, operand: str, operator: str) -> Any | list[str]:
+ """
+ Parses the second operand of a query condition as a collective object explicitly
+ for an `IN` or `BETWEEN` operation based on the specified operator.
+ """
+
+ if not self._tuple_pattern.match(operand):
+
+ # In the context of the `IN` operation, the operand might also be a
+ # string or a Field object, the following also handles this situation.
+ if operator == "in":
+ return self._parse_comparison_operand(operand)
+
+ raise QueryParseError(
+ f"Invalid query pattern around {' '.join(self._query)!r}"
+ )
+
+ # Parses and creates a list of individual operands.
+ operands: list[Any] = [
+ self._parse_comparison_operand(i.strip()) for i in operand[1:-1].split(",")
+ ]
+
+ if operator == "between" and len(operands) != 2:
+ raise QueryParseError(
+ "The tuple specified for the `BETWEEN` "
+ "operation must only comprise two elements."
+ )
+
+ return operands
+
+ def _parse_conditional_operand(
+ self, operand: str, operator: str
+ ) -> Any | list[str] | re.Pattern:
+ """
+ Parses the second operand specified within a query
+ condition for an `IN`, `BETWEEN` or `LIKE` operation.
+ """
+
+ # In case of an `IN` or `BETWEEN` operation, the
+ # operand is parsed using the following method.
+ if operator != "like":
+ return self._parse_collective_operand(operand, operator)
+
+ # The operand is parsed using the following
+ # mechanism in case of the `LIKE` operation.
+
+ elif not constants.STRING_PATTERN.match(operand):
+ raise QueryParseError(
+ f"Invalid Regex pattern {operand} specified in query conditions"
+ )
+
+ try:
+ return re.compile(operand[1:-1])
+
+ except re.error:
+ raise QueryParseError(
+ f"Invalid regex pattern {operand} specified in query conditions"
+ )
+
+ def _extract_condition_elements(self, condition: list[str]) -> list[str]:
+ """
+ Extracts the individual elements/token present within the specified query condition.
+ """
+
+ if len(condition) == 3:
+
+ # The operator is defined at the 1st index and is lowered in
+ # case it is a conditional operator and is typed in uppercase.
+ condition[1] = condition[1].lower()
+
+ if condition[1] not in (
+ constants.COMPARISON_OPERATORS | constants.CONDITIONAL_OPERATORS
+ ):
+ raise QueryParseError(
+ f"Invalid query syntax around {' '.join(self._query)!r}"
+ )
+
+ return condition
+
+ # The condition is parsed using differently if all the
+ # tokens are not already separated as individual strings.
+
+ # In such case, the condition is only looked up for comparison operators
+ # for partitioning it into individual tokens as conditional operators
+ # require whitespaces around them which are already parsed beforehand
+ # using the `tools.parse_query` function.
+ for i in constants.COMPARISON_OPERATORS:
+ if i not in condition[0]:
+ continue
+
+ # If the operator is present within the condition, all the
+ # individual tokens are partitioned into individual strings.
+ condition[:] = condition[0].partition(i)
+ break
+
+ else:
+ raise QueryParseError(
+ f"Invalid query syntax around {' '.join(condition)!r}"
+ )
+
+ # Strips out redundant whitespaces around the tokens.
+ for index, value in enumerate(condition):
+ condition[index] = value.strip()
+
+ return condition
+
+ def _parse_condition(
+ self, condition: list[str]
+ ) -> Condition | list[str | Condition]:
+ """
+ Parses individual query conditions.
+
+ #### Params:
+ - condition (list[str]): Condition to be parsed.
+ """
+
+ if len(condition) == 1 and self._tuple_pattern.match(condition[0]):
+ return list(
+ self._parse_conditions(tools.parse_query(condition[0][1:-1])),
+ )
+
+ # All individual strings are combined into a single string to parse them
+ # differently if the length of the list is not 3, i.e., the tokens are
+ # not already separated into individual strings.
+ if len(condition) != 3:
+ condition = ["".join(condition)]
+
+ condition[:] = self._extract_condition_elements(condition)
+
+ operator = condition[1]
+ operand1: Any = self._parse_comparison_operand(condition[0])
+
+ # Parses the second operand accordingly based on the specified operator.
+ operand2: Any = (
+ self._parse_comparison_operand(condition[2])
+ if operator in constants.COMPARISON_OPERATORS
+ else self._parse_conditional_operand(condition[2], operator)
+ )
+
+ return Condition(operand1, operator, operand2)
+
+ def _parse_conditions(
+ self, subquery: list[str]
+ ) -> Generator[Condition | str | list, None, None]:
+ """
+ Parses the query conditions.
+
+ #### Params:
+ - subquery (list): Subquery comprising the query conditions.
+ """
+
+ # Stores individual conditions during iteration.
+ condition: list[str] = []
+
+ for token in subquery:
+ if token.lower() in constants.CONDITION_SEPARATORS:
+ yield self._parse_condition(condition)
+ yield token.lower()
+
+ condition.clear()
+
+ continue
+
+ condition.append(token)
+
+ # Parses the last condition specified in the query.
+ if condition:
+ yield self._parse_condition(condition)
+
+ def parse_conditions(self) -> Generator[Condition | str | list, None, None]:
+ """
+ Parses the query conditions and returns a `typing.Generator` object of the parsed
+ conditions as `Condition` objects also including the condition separators `and`
+ and `or` as string objects or a list of all of the above if nested.
+ """
+ return self._parse_conditions(self._query)
+
+
+class ConditionHandler:
+ """
+ ConditionHandler defines methods for handling and evaluating
+ query conditions for search and delete operations.
+ """
+
+ __slots__ = "_conditions", "_method_map"
+
+ def __init__(self, subquery: list[str], operation_target: str) -> None:
+ """
+ Creates an instance of the `ConditionHandler` class.
+
+ #### Params:
+ - conditions (list): List of parsed query conditions.
+ - operation_target (str): Targeted operand in the operation (file/data/directory).
+ """
+
+ # Maps operator notations with corresponding evaluation methods.
+ self._method_map: dict[str, Callable[[Any, Any], bool]] = {
+ ">=": self._ge,
+ "<=": self._le,
+ "<": self._lt,
+ ">": self._gt,
+ "=": self._eq,
+ "!=": self._ne,
+ "like": self._like,
+ "in": self._contains,
+ "between": self._between,
+ }
+
+ # Parses the conditions and stores them in a list.
+ self._conditions = list(
+ ConditionParser(subquery, operation_target).parse_conditions()
+ )
+
+ @staticmethod
+ def _eval_field(field: Field | Size, obj: File | DataLine | Directory) -> Any:
+ """
+ Evaluates the specified metadata field.
+
+ #### Params:
+ - operand (Any): Operand to be processed.
+ - obj (File | DataLine | Directory): Metadata object for extracting field values.
+ """
+
+ if isinstance(field, Size):
+ field = field.get_size(obj)
+
+ else:
+ field = getattr(obj, field.field)
+
+ return field
+
+ def _eval_operand(self, operand: Any, obj: File | DataLine | Directory) -> Any:
+ """
+ Evaluates the specified condition operand.
+
+ #### Params:
+ - operand (Any): Operand to be processed.
+ - obj (File | DataLine | Directory): Metadata object for extracting field values.
+ """
+
+ if isinstance(operand, Field | Size):
+ operand = self._eval_field(operand, obj)
+
+ elif isinstance(operand, list):
+
+ # Creates a separate copy of the operand list to avoid mutations in it.
+ array: list[Any] = operand.copy()
+
+ for index, val in enumerate(array):
+ if isinstance(val, Field | Size):
+ array[index] = self._eval_field(val, obj)
+
+ return array
+
+ return operand
+
+ def _eval_condition(
+ self, condition: Condition | list, obj: File | DataLine | Directory
+ ) -> bool:
+ """
+ Evaluates the specified condition.
+
+ #### Params:
+ - condition (Condition | list): Condition(s) to be evaluated.
+ - obj (File | DataLine | Directory): Metadata object for extracting field values.
+ """
+
+ # Recursively evaluates if the condition is nested.
+ if isinstance(condition, list):
+ return self._eval_all_conditions(condition, obj)
+
+ # Evaluates the condition operands.
+ operand1, operand2 = self._eval_operand(
+ condition.operand1, obj
+ ), self._eval_operand(condition.operand2, obj)
+
+ try:
+ # Evaluates the operation with a method corresponding to the name
+ # of the operator defined in the `_method_map` instance attribute.
+ response: bool = self._method_map[condition.operator](operand1, operand2)
+ except (TypeError, ValueError):
+ raise OperationError("Unable to process the query conditions.")
+
+ else:
+ return response
+
+ def _eval_condition_segments(
+ self,
+ segment: list[bool | str | Condition | list],
+ obj: File | DataLine | Directory,
+ ) -> bool:
+ """
+ Evaluates the specified condition segment comprising
+ two conditions along with a seperator.
+
+ #### Params:
+ - segment (list): Query condition segment to be evaluated.
+ - obj (File | DataLine | Directory): Metadata object for extracting field values.
+ """
+
+ # Evaluates individual conditions present at the
+ # 0th and 2nd position in the list if not done yet.
+ for i in (0, 2):
+ if not isinstance(segment[i], bool):
+ segment[i] = self._eval_condition(segment[i], obj)
+
+ return (
+ segment[0] and segment[2]
+ if segment[1] == "and"
+ else segment[0] or segment[2]
+ )
+
+ def _eval_all_conditions(
+ self,
+ conditions: list[str | Condition | list],
+ obj: File | DataLine | Directory,
+ ) -> bool:
+ """
+ Evaluates all the query conditions.
+
+ #### Params:
+ - conditions (list): List comprising the conditions along with their separators.
+ - obj (File | DataLine | Directory): Metadata object for extracting field values.
+ """
+
+ # Adds a `True and` condition at the beginning of the list to avoid
+ # explicit definition of a mechanism for evaluating a single condition.
+ segments: list[Any] = [True, "and"] + conditions
+ ctr: int = 0
+
+ # Evaluates conditions separated by `and` operator.
+ for _ in range(len(segments) // 2):
+ segment: list[Any] = segments[ctr : ctr + 3]
+
+ if segment[1] == "or":
+ # Increments the counter by 1 to skip the
+ # conditions separated by the `or` operator.
+ ctr += 2
+
+ else:
+ segments[ctr : ctr + 3] = [self._eval_condition_segments(segment, obj)]
+
+ # Evaluates conditions separated by `or` operator.
+ for _ in range(len(segments) // 2):
+ # Replaces the conditions with the evaluated boolean value.
+ segments[:3] = [self._eval_condition_segments(segments[:3], obj)]
+
+ if segments[0]:
+ return True
+
+ # Extracts the singe-most boolean value from the list.
+ result: bool = segments[0]
+
+ return result
+
+ def eval_conditions(self, obj: File | DataLine | Directory) -> bool:
+ """
+ Evaluates the query conditions
+
+ #### Params:
+ - obj (File | DataLine | Directory): Metadata object for extracting field values.
+ """
+ return self._eval_all_conditions(self._conditions, obj)
+
+ @staticmethod
+ def _gt(x: Any, y: Any, /) -> bool:
+ return x > y
+
+ @staticmethod
+ def _ge(x: Any, y: Any, /) -> bool:
+ return x >= y
+
+ @staticmethod
+ def _lt(x: Any, y: Any, /) -> bool:
+ return x < y
+
+ @staticmethod
+ def _le(x: Any, y: Any, /) -> bool:
+ return x <= y
+
+ @staticmethod
+ def _eq(x: Any, y: Any, /) -> bool:
+ return x == y
+
+ @staticmethod
+ def _ne(x: Any, y: Any, /) -> bool:
+ return x != y
+
+ @staticmethod
+ def _contains(x: Any, y: list[Any], /) -> bool:
+ return x in y
+
+ @staticmethod
+ def _between(x: Any, y: tuple[Any, Any], /) -> bool:
+ return y[0] <= x <= y[1]
+
+ @staticmethod
+ def _like(string: str, pattern: re.Pattern) -> bool:
+ return bool(pattern.match(string))
diff --git a/fise/query/operators.py b/fise/query/operators.py
new file mode 100644
index 0000000..5703232
--- /dev/null
+++ b/fise/query/operators.py
@@ -0,0 +1,328 @@
+"""
+Operators Module
+----------------
+
+This module defines classes tailored for processing user queries and executing file and
+directory search or delete operations. Additionally, it includes specialized classes for
+performing search operations within file contents.
+"""
+
+from typing import Generator, Callable, Any
+from pathlib import Path
+import shutil
+
+import pandas as pd
+
+from errors import OperationError
+from notify import Message, Alert
+from common import tools, constants
+from shared import File, Directory, DataLine, Field, Size
+
+
+class FileQueryOperator:
+ """
+ FileQueryOperator defines methods for performing
+ file search and delete operations.
+ """
+
+ __slots__ = "_directory", "_recursive"
+
+ def __init__(self, directory: Path, recursive: bool) -> None:
+ """
+ Creates an instance of the `FileQueryOperator` class.
+
+ #### Params:
+ - directory (Path): Path to the directory.
+ - recursive (bool): Whether to include files from subdirectories.
+ """
+
+ self._directory = directory
+ self._recursive = recursive
+
+ @staticmethod
+ def _get_field(field: Field | Size, file: File) -> Any:
+ """
+ Extracts the specified field from the specified `File` object.
+
+ #### Params:
+ - field (Field): `Field` object comprising the field to be extracted.
+ - file (File): `File` object to extract data from.
+ """
+
+ if isinstance(field, Size):
+ return field.get_size(file)
+
+ return getattr(file, field.field)
+
+ def get_dataframe(
+ self,
+ fields: list[Field | Size],
+ columns: list[str],
+ condition: Callable[[File], bool],
+ ) -> pd.DataFrame:
+ """
+ Returns a pandas DataFrame comprising search records of all the files
+ present within the directory which match the specified condition.
+
+ #### Params:
+ - fields (list[Field]): List of desired file metadata `Field` objects.
+ - columns (list[str]): List of columns names for the specified fields.
+ - condition (Callable): Function for filtering data records.
+ """
+
+ files: Generator[File, None, None] = (
+ File(file) for file in tools.get_files(self._directory, self._recursive)
+ )
+
+ # Generator object comprising search records of
+ # the files matching the specified condition.
+ records: Generator[list[Any], None, None] = (
+ [self._get_field(field, file) for field in fields]
+ for file in files
+ if condition(file)
+ )
+
+ return pd.DataFrame(records, columns=columns)
+
+ def remove_files(self, condition: Callable[[File], bool], skip_err: bool) -> None:
+ """
+ Removes all the files present within the
+ directory matching the specified condition.
+
+ #### Params:
+ - condition (Callable): Function for filtering data records.
+ - skip_err (bool): Whether to supress permission errors during operation.
+ """
+
+ # `ctr` counts the number of files removed whereas `skipped` counts
+ # the number of skipped files if `skip_err` is set to `True`.
+ ctr = skipped = 0
+
+ # Iterates through the files and deletes individually if the condition is met.
+ for file in tools.get_files(self._directory, self._recursive):
+ if not condition(File(file)):
+ continue
+
+ try:
+ file.unlink()
+
+ except PermissionError:
+ if skip_err:
+ skipped += 1
+ continue
+
+ raise OperationError(f"Permission Error: Cannot delete '{file}'")
+
+ else:
+ ctr += 1
+
+ Message(f"Successfully removed {ctr} files from '{self._directory}'.")
+
+ # Prints the skipped files message only is `skipped` is not zero.
+ if skipped:
+ Alert(
+ f"Skipped {skipped} files from '{self._directory}' due to permission errors."
+ )
+
+
+class FileDataQueryOperator:
+ """
+ FileDataQueryOperator defines methods for performing
+ text and byte search operations within files.
+ """
+
+ __slots__ = "_path", "_recursive", "_filemode"
+
+ def __init__(
+ self, path: Path, recursive: bool, filemode: constants.FILE_MODES
+ ) -> None:
+ """
+ Creates an instance of the `FileDataQueryOperator` class.
+
+ #### Params:
+ - path (pathlib.Path): Path to the file or directory.
+ - recursive (bool): Whether to include files from subdirectories.
+ - filemode (str): Desired filemode to read files.
+ """
+
+ self._path = path
+ self._recursive = recursive
+ self._filemode = constants.FILE_MODES_MAP[filemode]
+
+ def _get_filedata(self) -> Generator[tuple[Path, list[str | bytes]], None, None]:
+ """
+ Yields the file paths along with a list comprising datalines
+ of the corresponding file in the form of strings or bytes.
+ """
+
+ # The following variable stores a Generator object of all the files present within
+ # the directory if the specified path is a directory or creates a tuple comprising
+ # the `pathlib.Path` object of the specified file.
+ files: tuple[Path] | Generator[Path, None, None] = (
+ (self._path,) if self._path.is_file() else tools.get_files(
+ self._path, self._recursive
+ )
+ )
+
+ for i in files:
+ with i.open(self._filemode) as file:
+ try:
+ yield i, file.readlines()
+
+ except UnicodeDecodeError:
+ raise OperationError(
+ "Cannot read bytes with 'text' filemode. Set "
+ "filemode to 'bytes' to read byte data within files."
+ )
+
+ def _search_datalines(self) -> Generator[DataLine, None, None]:
+ """
+ Iterates through the files and their corresponding data-lines, and
+ yields `DataLine` objects comprising the dataline and its metadata.
+ """
+
+ for file, data in self._get_filedata():
+ yield from (
+ DataLine(file, line, index + 1) for index, line in enumerate(data)
+ )
+
+ @staticmethod
+ def _get_field(field: Field, data: DataLine) -> Any:
+ """
+ Extracts the specified field from the specified `DataLine` object.
+
+ #### Params:
+ - field (Field): `Field` object comprising the field to be extracted.
+ - data (DataLine): `DataLine` object to extract data from.
+ """
+ return getattr(data, field.field)
+
+ def get_dataframe(
+ self,
+ fields: list[Field],
+ columns: list[str],
+ condition: Callable[[DataLine], bool],
+ ) -> pd.DataFrame:
+ """
+ Returns a pandas DataFrame comprising the search records of all the
+ datalines matching the specified condition present within the file(s).
+
+ #### Params:
+ - fields (list[str]): List of the desired metadata fields.
+ - condition (Callable): Function for filtering data records.
+ """
+
+ # Generator object comprising search records of
+ # the files matching the specified condition.
+ records: Generator[list[Any], None, None] = (
+ [self._get_field(field, data) for field in fields]
+ for data in self._search_datalines()
+ if condition(data)
+ )
+
+ return pd.DataFrame(records, columns=columns)
+
+
+class DirectoryQueryOperator:
+ """
+ DirectoryQueryOperator defines methods for performing
+ directory search and delete operations.
+ """
+
+ __slots__ = "_directory", "_recursive"
+
+ def __init__(self, directory: Path, recursive: bool) -> None:
+ """
+ Creates an instance of the `FileQueryOperator` class.
+
+ #### Params:
+ - directory (Path): Path to the directory.
+ - recursive (bool): Whether to include files from subdirectories.
+ """
+
+ self._directory = directory
+ self._recursive = recursive
+
+ @staticmethod
+ def _get_field(field: Field, directory: Directory) -> Any:
+ """
+ Extracts the specified field from the specified `Directory` object.
+
+ #### Params:
+ - field (Field): `Field` object comprising the field to be extracted.
+ - directory (Directory): `Directory` object to extract data from.
+ """
+ return getattr(directory, field.field)
+
+ def get_dataframe(
+ self,
+ fields: list[Field],
+ columns: list[str],
+ condition: Callable[[Directory], bool],
+ ) -> pd.DataFrame:
+ """
+ Returns a pandas DataFrame comprising search records of
+ all the subdirectories matching the specified condition.
+
+ #### Params:
+ - fields (list[Field]): List of desired directory metadata `Field` objects.
+ - columns (list[str]): List of columns names for the specified fields.
+ - condition (Callable): Function for filtering data records.
+ """
+
+ directories: Generator[Directory, None, None] = (
+ Directory(directory)
+ for directory in tools.get_directories(self._directory, self._recursive)
+ )
+
+ # Generator object comprising search records of
+ # the files matching the specified condition.
+ records: Generator[list[Any], None, None] = (
+ [self._get_field(field, directory) for field in fields]
+ for directory in directories
+ if condition(directory)
+ )
+
+ return pd.DataFrame(records, columns=columns)
+
+ def remove_directories(
+ self, condition: Callable[[Directory], bool], skip_err: bool
+ ) -> None:
+ """
+ Removes all the subdirectories matching the specified condition.
+
+ #### Params:
+ - condition (Callable): Function for filtering data records.
+ - skip_err (bool): Whether to supress permission errors during operation.
+ """
+
+ # `ctr` counts the number of directories removed whereas `skipped` counts
+ # the number of skipped directories if `skip_err` is set to `True`.
+ ctr = skipped = 0
+
+ # Iterates through the subdirectories and deletes
+ # individual directory tree(s) if the condition is met.
+ for subdir in tools.get_directories(self._directory, self._recursive):
+ if not condition(Directory(subdir)):
+ continue
+
+ try:
+ shutil.rmtree(subdir)
+
+ except PermissionError:
+ if skip_err:
+ skipped += 1
+ continue
+
+ raise OperationError(f"Permission Error: Cannot delete '{subdir}'")
+
+ else:
+ ctr += 1
+
+ Message(f"Successfully removed {ctr} directories from '{self._directory}'.")
+
+ # Prints the skipped files message only is `skipped` is not zero.
+ if skipped:
+ Alert(
+ f"Skipped {skipped} directories from '{self._directory}' due to permission errors."
+ )
diff --git a/fise/query/parsers.py b/fise/query/parsers.py
new file mode 100644
index 0000000..80b3b7f
--- /dev/null
+++ b/fise/query/parsers.py
@@ -0,0 +1,315 @@
+"""
+Parsers Module
+--------------
+
+This module comprises classes and functions for parsing user queries
+extracting relevant data for further processing and evaluation.
+"""
+
+# NOTE
+# The parser classes defined within this module only parse the search fields
+# (explicilty for search operation), path, path-type and the conditions defined
+# within the query. The initials are parsed beforehand.
+
+from pathlib import Path
+from typing import Callable
+
+from errors import QueryParseError
+from common import constants
+from .conditions import ConditionHandler
+from shared import DeleteQuery, SearchQuery, Directory, DataLine, Field, File, Size
+
+
+def _parse_path(subquery: list[str]) -> tuple[Path, int]:
+ """
+ Parses the file/directory path and its type from the specified sub-query.
+ Also returns the index of the file/directory specification in the query
+ relative to the specified subquery.
+ """
+
+ path_type: str = "relative"
+ path_specs_index: str = 0
+
+ if subquery[0].lower() in constants.PATH_TYPES:
+ path_type = subquery[0].lower()
+ path_specs_index = 1
+
+ raw_path: str = subquery[path_specs_index]
+
+ # Removes the leading and trailing quotes if explicitly specified within the path.
+ if constants.STRING_PATTERN.match(raw_path):
+ raw_path = raw_path[1:-1]
+
+ path: Path = Path(raw_path)
+
+ if path_type == "absolute":
+ path = path.resolve()
+
+ return path, path_specs_index
+
+
+def _get_from_keyword_index(subquery: list[str]) -> int:
+ """Returns the index of the `FROM` keyword in the specified subquery."""
+
+ for i, kw in enumerate(subquery):
+ if kw.lower() == "from":
+ return i
+
+ raise QueryParseError("Cannot find the 'FROM' keyword in the query.")
+
+
+def _get_condition_handler(
+ subquery: list[str], operand: str
+) -> Callable[[File | DataLine | Directory], bool]:
+ """
+ Parses the conditions defined in the specified subquery
+ and returns a function for filtering data records.
+
+ #### Params:
+ - subquery (list): Subquery comprising the query conditions.
+ - operand (str): Targeted operand in the query operation.
+ """
+
+ # Returns a lambda function hardcoded to return `True` to include all the records
+ # during evaluation if no conditions are explicitly defined within the query.
+ if not subquery:
+ return lambda _: True
+
+ if subquery[0].lower() != "where":
+ raise QueryParseError(f"Invalid query syntax around {' '.join(subquery)!r}")
+
+ conditions: list[str] = subquery[1:]
+ handler = ConditionHandler(conditions, operand)
+
+ # Returns the evaluation method for filtering records.
+ return handler.eval_conditions
+
+
+class FileQueryParser:
+ """
+ FileQueryParser defines methods for parsing file search and delete queries.
+ """
+
+ __slots__ = "_query", "_operation", "_from_index"
+
+ _operand = "file"
+ _file_fields = set(constants.FILE_FIELDS) | constants.FILE_FIELD_ALIASES.keys()
+
+ def __init__(self, subquery: list[str], operation: constants.OPERATIONS) -> None:
+ self._query = subquery
+ self._operation = operation
+
+ # Stores the index of the `FROM` keyword in the specified subquery.
+ self._from_index = _get_from_keyword_index(subquery)
+
+ def _parse_fields(
+ self, attrs: str | list[str]
+ ) -> tuple[list[Field | Size], list[str]]:
+ """
+ Parses the search query fields and returns an array of parsed fields and columns.
+
+ #### Params:
+ - attrs (str | list[str]): String or a list of strings of query fields.
+ """
+
+ fields: list[Field | Size] = []
+ columns: list[str] = []
+
+ # Iterates through the specified tokens, parses and stores them in the `fields` list.
+ for field in "".join(attrs).split(","):
+
+ # Keep a separate copy of the lowered string to avoid affecting
+ # the case of the field string when adding it to the columns.
+ col: str = field.lower()
+
+ if field == "*":
+ fields += (Field(i) for i in constants.FILE_FIELDS)
+ columns += constants.FILE_FIELDS
+
+ elif col.startswith("size"):
+ # Parses size from the string and adds it to the `fields` list.
+ fields.append(Size.from_string(field))
+ columns.append(field)
+
+ elif col in self._file_fields:
+ fields.append(Field(constants.FILE_FIELD_ALIASES.get(col, col)))
+ columns.append(field)
+
+ else:
+ raise QueryParseError(
+ f"Found an invalid field {field!r} in the search query."
+ )
+
+ return fields, columns
+
+ def _parse_directory(self) -> tuple[Path, int]:
+ """
+ Parses the directory path and its metadata.
+ """
+ path, index = _parse_path(self._query[self._from_index + 1:])
+
+ if not path.is_dir():
+ raise QueryParseError("The specified path for lookup must be a directory.")
+
+ return path, index
+
+ def _parse_remove_query(self) -> DeleteQuery:
+ """
+ Parses the delete query.
+ """
+
+ if self._from_index:
+ raise QueryParseError("Invalid query syntax.")
+
+ path, index = self._parse_directory()
+
+ # Extracts the function for filtering file records.
+ condition: Callable[[File | DataLine | Directory], bool] = (
+ _get_condition_handler(
+ self._query[self._from_index + index + 2:], self._operand
+ )
+ )
+
+ return DeleteQuery(path, condition)
+
+ def _parse_search_query(self) -> SearchQuery:
+ """
+ Parses the search query.
+ """
+
+ fields, columns = self._parse_fields(self._query[: self._from_index])
+ path, index = self._parse_directory()
+
+ # Extracts the function for filtering file records.
+ condition: Callable[[File | DataLine | Directory], bool] = (
+ _get_condition_handler(
+ self._query[self._from_index + index + 2:], self._operand
+ )
+ )
+
+ return SearchQuery(path, condition, fields, columns)
+
+ def parse_query(self) -> SearchQuery | DeleteQuery:
+ """
+ Parses the search/delete query.
+ """
+ return (
+ self._parse_search_query()
+ if self._operation == "search"
+ else self._parse_remove_query()
+ )
+
+
+class FileDataQueryParser:
+ """
+ FileDataQueryParser defines methods for parsing file data search queries.
+ """
+
+ __slots__ = "_query", "_from_index"
+
+ _operand = "data"
+ _data_fields = set(constants.DATA_FIELDS) | constants.DATA_FIELD_ALIASES.keys()
+
+ def __init__(self, subquery: list[str]) -> None:
+ self._query = subquery
+
+ # Stores the index of the `FROM` keyword in the specified subquery.
+ self._from_index = _get_from_keyword_index(subquery)
+
+ def _parse_fields(self, attrs: list[str] | str) -> tuple[list[Field], list[str]]:
+ """
+ Parses the search query fields and returns an array of parsed fields and columns.
+
+ #### Params:
+ - attrs (str | list[str]): String or a list of strings of query fields.
+ """
+
+ fields: list[Field] = []
+ columns: list[str] = []
+
+ # Iterates through the specified tokens, parses and stores them in the `fields` list.
+ for field in "".join(attrs).lower().split(","):
+ if field == "*":
+ fields += (Field(i) for i in constants.DATA_FIELDS)
+ columns += constants.DATA_FIELDS
+
+ elif field in self._data_fields:
+ fields.append(Field(constants.DATA_FIELD_ALIASES.get(field, field)))
+ columns.append(field)
+
+ else:
+ raise QueryParseError(
+ f"Found an invalid field {field!r} in the search query."
+ )
+
+ return fields, columns
+
+ def _parse_path(self) -> tuple[Path, int]:
+ """
+ Parses the file/directory path and its metadata.
+ """
+
+ path, index = _parse_path(self._query[self._from_index + 1:])
+
+ if not (path.is_dir() or path.is_file()):
+ raise QueryParseError(
+ "The specified path for lookup must be a file or directory."
+ )
+
+ return path, index
+
+ def parse_query(self) -> SearchQuery:
+ """
+ Parses the file data search query.
+ """
+
+ fields, columns = self._parse_fields(self._query[: self._from_index])
+ path, index = self._parse_path()
+
+ # Extracts the function for filtering file records.
+ condition: Callable[[File | DataLine | Directory], bool] = (
+ _get_condition_handler(
+ self._query[self._from_index + index + 2:], self._operand
+ )
+ )
+
+ return SearchQuery(path, condition, fields, columns)
+
+
+class DirectoryQueryParser(FileQueryParser):
+ """
+ DirectoryQueryParser defines methods for parsing directory search/delete queries.
+ """
+
+ __slots__ = "_query", "_operation", "_from_index"
+
+ _operand = "dir"
+ _dir_fields = constants.DIR_FIELDS | constants.DIR_FIELD_ALIASES.keys()
+
+ def _parse_fields(self, attrs: list[str] | str) -> tuple[list[Field], list[str]]:
+ """
+ Parses the search query fields and returns an array of parsed fields and columns.
+
+ #### Params:
+ - attrs (str | list[str]): String or a list of strings of query fields to be parsed.
+ """
+
+ fields: list[Field] = []
+ columns: list[str] = []
+
+ # Iterates through the specified tokens, parses and stores them in the `fields` list.
+ for field in "".join(attrs).lower().split(","):
+ if field == "*":
+ fields += (Field(i) for i in constants.DIR_FIELDS)
+ columns += constants.DIR_FIELDS
+
+ elif field in self._dir_fields:
+ fields.append(Field(constants.DIR_FIELD_ALIASES.get(field, field)))
+ columns.append(field)
+
+ else:
+ raise QueryParseError(
+ f"Found an invalid field {field!r} in the search query."
+ )
+
+ return fields, columns
diff --git a/fise/shared.py b/fise/shared.py
new file mode 100644
index 0000000..52e89e8
--- /dev/null
+++ b/fise/shared.py
@@ -0,0 +1,220 @@
+"""
+Shared Module
+-------------
+
+This module comprises data-classes shared across the project
+assisting various other classes and functions defined within it.
+"""
+
+import re
+import sys
+from dataclasses import dataclass
+from typing import ClassVar, Callable, Literal, Any
+from pathlib import Path
+
+import ospecs
+from common import constants
+from errors import QueryParseError
+
+if sys.platform == "win32":
+ from ospecs import WindowsEntity as Entity
+else:
+ from ospecs import PosixEntity as Entity
+
+
+class File(Entity):
+ """
+ File class serves as a unified class for
+ accessing all file metadata attributes.
+ """
+
+ __slots__ = "_path", "_stats"
+
+ @property
+ @ospecs.safe_extract_field
+ def filetype(self) -> str | None:
+ return self._path.suffix or None
+
+ @property
+ @ospecs.safe_extract_field
+ def size(self) -> int | float:
+ return self._stats.st_size
+
+
+class DataLine:
+ """
+ DataLine class serves as a unified class for
+ accessing all dataline metadata attributes.
+ """
+
+ __slots__ = "_file", "_data", "_lineno"
+
+ def __init__(self, file: Path, data: str | bytes, lineno: int) -> None:
+ """
+ Creates an instance of the `DataLine` class.
+
+ #### Params:
+ - file (pathlib.Path): Path to the file.
+ - data (str | bytes): Dataline to be stored.
+ - lineno (int): Line number of the dataline.
+ """
+ self._file = file
+ self._data = data
+ self._lineno = lineno
+
+ @property
+ @ospecs.safe_extract_field
+ def path(self) -> str:
+ return str(self._file)
+
+ @property
+ @ospecs.safe_extract_field
+ def name(self) -> str:
+ return self._file.name
+
+ @property
+ @ospecs.safe_extract_field
+ def dataline(self) -> str:
+ # Strips the leading binary notation and quotes if the dataline is a bytes object.
+ return str(self._data)[2:-1] if isinstance(self._data, bytes) else self._data
+
+ @property
+ @ospecs.safe_extract_field
+ def lineno(self) -> int:
+ return self._lineno
+
+ @property
+ @ospecs.safe_extract_field
+ def filetype(self) -> str | None:
+ return self._file.suffix or None
+
+
+class Directory(Entity):
+ """
+ Directory class serves as a unified class for
+ accessing all directory metadata attributes.
+ """
+
+ __slots__ = "_path", "_stats"
+
+
+@dataclass(slots=True, frozen=True, eq=False)
+class Size:
+ """
+ Size class stores the size unit of the file size
+ field and defines a mechanism for parsing the field.
+ """
+
+ # Regex pattern for matching size field specifications.
+ _size_field_pattern: ClassVar[re.Pattern] = re.compile(rf"^size(\[.*])?$")
+
+ unit: str
+
+ @classmethod
+ def from_string(cls, field: str):
+ """
+ Creates an instance of `Size` object from
+ the specified size field specifications.
+ """
+
+ if not cls._size_field_pattern.match(field.lower()):
+ raise QueryParseError(f"Found an invalid field {field!r} in the query.")
+
+ unit: str = field[5:-1]
+
+ # Only verifies the size unit if explicitly specified.
+ if unit and unit not in constants.SIZE_CONVERSION_MAP:
+ raise QueryParseError(f"Invalid unit {unit!r} specified for 'size' field.")
+
+ # Initializes with "B" -> bytes unit if not explicitly specified.
+ return cls(unit or "B")
+
+ def get_size(self, file: File) -> float:
+ """
+ Extracts the size from the specified `File` object and
+ converts it in accordance with the stored size unit.
+ """
+ return round(file.size / constants.SIZE_CONVERSION_MAP.get(self.unit), 5)
+
+
+@dataclass(slots=True, frozen=True, eq=False)
+class Field:
+ """
+ Field class stores individual search query fields.
+ """
+
+ field: str
+
+
+@dataclass(slots=True, frozen=True, eq=False)
+class BaseQuery:
+ """
+ Base class for all query data classes.
+ """
+
+ path: Path
+ condition: Callable[[File | DataLine | Directory], bool]
+
+
+@dataclass(slots=True, frozen=True, eq=False)
+class SearchQuery(BaseQuery):
+ """
+ SearchQuery class stores search query attributes.
+ """
+
+ fields: list[Field | Size]
+ columns: list[str]
+
+
+class DeleteQuery(BaseQuery):
+ """
+ DeleteQuery class stores delete query attributes.
+ """
+
+
+@dataclass(slots=True, frozen=True, eq=False)
+class ExportData:
+ """
+ ExportData class stores export data attributes.
+ """
+
+ type_: Literal["file", "database"]
+ target: str | Path
+
+
+@dataclass(slots=True, frozen=True, eq=False)
+class OperationData:
+ """
+ OperationData class stores query operation attributes.
+ """
+
+ operation: constants.OPERATIONS
+ operand: constants.OPERANDS
+
+ # The following attributes are optional and are only used for some specific
+ # operations. `filemode` attribute is only used with a data search operation
+ # and `skip_err` attribute is only used in file/directory deletion operation.
+ filemode: constants.FILE_MODES | None = None
+ skip_err: bool = False
+
+
+@dataclass(slots=True, frozen=True, eq=False)
+class QueryInitials:
+ """
+ QueryInitials class stores attributes related to query initials.
+ """
+
+ operation: OperationData
+ recursive: bool
+ export: ExportData | None = None
+
+
+@dataclass(slots=True, frozen=True, eq=False)
+class Condition:
+ """
+ Condition class stores individual query condition attributes.
+ """
+
+ operand1: Any
+ operator: str
+ operand2: Any
diff --git a/fise/version.py b/fise/version.py
new file mode 100644
index 0000000..9f7a875
--- /dev/null
+++ b/fise/version.py
@@ -0,0 +1 @@
+version = "0.1.0"
diff --git a/requirements/requirements-extra.txt b/requirements/requirements-extra.txt
new file mode 100644
index 0000000..f2ef888
--- /dev/null
+++ b/requirements/requirements-extra.txt
@@ -0,0 +1,4 @@
+et-xmlfile==1.1.0
+openpyxl==3.1.3
+psycopg2-binary==2.9.9
+PyMySQL==1.1.1
\ No newline at end of file
diff --git a/requirements/requirements-test.txt b/requirements/requirements-test.txt
new file mode 100644
index 0000000..41dc0c0
--- /dev/null
+++ b/requirements/requirements-test.txt
@@ -0,0 +1,2 @@
+pytest==8.2.2
+tables==3.9.2
\ No newline at end of file
diff --git a/requirements/requirements.txt b/requirements/requirements.txt
new file mode 100644
index 0000000..9f2a19b
--- /dev/null
+++ b/requirements/requirements.txt
@@ -0,0 +1,9 @@
+greenlet==3.0.3
+numpy==1.26.4
+pandas==2.2.2
+python-dateutil==2.9.0.post0
+pytz==2024.1
+six==1.16.0
+SQLAlchemy==2.0.30
+typing_extensions==4.12.1
+tzdata==2024.1
\ No newline at end of file
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..611059a
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,9 @@
+import sys
+from pathlib import Path
+
+ADDITIONAL_PATHS = [
+ str(Path(__file__).parents[1]),
+ str(Path(__file__).parents[1] / "fise"),
+]
+
+sys.path.extend(ADDITIONAL_PATHS)
diff --git a/tests/reset_tests.py b/tests/reset_tests.py
new file mode 100644
index 0000000..a6d894b
--- /dev/null
+++ b/tests/reset_tests.py
@@ -0,0 +1,46 @@
+"""
+Reset Tests Module
+------------------
+
+This module provides utility functions to reset the test directories
+by regenerating them based on the file and directory listings stored
+in the `test_directory.hdf` file.
+"""
+
+import shutil
+from pathlib import Path
+
+import pandas as pd
+
+FILE_DIR_TEST_DIRECTORY = Path(__file__).parent / "test_directory" / "file_dir"
+FILE_DIR_TEST_DIRECTORY_LISTINGS_FILE = Path(__file__).parent / "test_directory.hdf"
+
+
+def reset_file_dir_test_directory(
+ directory: Path = FILE_DIR_TEST_DIRECTORY,
+ listings: Path = FILE_DIR_TEST_DIRECTORY_LISTINGS_FILE,
+):
+ """
+ Resets the `file_dir` test directory.
+
+ Deletes the existing `file_dir` test directory and recreates it based
+ on the file and directory listings stored in the specified HDF5 file.
+ """
+
+ if directory.exists():
+ shutil.rmtree(directory)
+
+ # Extracts and stores the corresponding listings for regeneration.
+ with pd.HDFStore(str(listings)) as store:
+ file_listings: pd.Series[str] = store["/file_dir/files"]
+ dir_listings: pd.Series[str] = store["/file_dir/dirs"]
+
+ for direc in dir_listings:
+ (directory / direc).mkdir()
+
+ for file in file_listings:
+ (directory / file).touch()
+
+
+if __name__ == "__main__":
+ reset_file_dir_test_directory()
diff --git a/tests/test_directory.hdf b/tests/test_directory.hdf
new file mode 100644
index 0000000..faa174e
Binary files /dev/null and b/tests/test_directory.hdf differ
diff --git a/tests/test_directory/data/complaints.txt b/tests/test_directory/data/complaints.txt
new file mode 100644
index 0000000..2c79333
--- /dev/null
+++ b/tests/test_directory/data/complaints.txt
@@ -0,0 +1,14 @@
+Customer Complaints Report
+
+1. Order #1234 - Incorrect item received. Customer requested a refund.
+2. Order #1235 - Delayed shipping. Customer reported delivery 5 days late.
+3. Order #1236 - Damaged product. Customer requested a replacement.
+4. Order #1237 - Missing parts in package. Customer requested missing parts to be shipped.
+5. Order #1238 - Poor customer service experience. Customer expressed dissatisfaction with support.
+
+Resolution Actions:
+- Refund processed for Order #1234
+- Shipping procedures reviewed for Order #1235
+- Replacement shipped for Order #1236
+- Missing parts dispatched for Order #1237
+- Training session scheduled for customer service team to address issues raised in Order #1238
\ No newline at end of file
diff --git a/tests/test_directory/data/documents/Annual Financial Report 2023.txt b/tests/test_directory/data/documents/Annual Financial Report 2023.txt
new file mode 100644
index 0000000..4b53c4b
--- /dev/null
+++ b/tests/test_directory/data/documents/Annual Financial Report 2023.txt
@@ -0,0 +1,26 @@
+Title: Annual Financial Report 2023
+
+Introduction:
+The Annual Financial Report for 2023 provides a comprehensive overview of the financial performance and position of XYZ Corporation. This report includes the income statement, balance sheet, cash flow statement, and a summary of significant accounting policies.
+
+Income Statement:
+
+Total Revenue: $5,000,000
+Cost of Goods Sold: $2,000,000
+Gross Profit: $3,000,000
+Operating Expenses: $1,500,000
+Net Income: $1,000,000
+Balance Sheet:
+
+Assets: $8,000,000
+Liabilities: $3,000,000
+Equity: $5,000,000
+Cash Flow Statement:
+
+Operating Activities: $1,200,000
+Investing Activities: -$500,000
+Financing Activities: $300,000
+Net Increase in Cash: $1,000,000
+
+Conclusion:
+XYZ Corporation has demonstrated strong financial performance in 2023, with significant growth in revenue and net income.
\ No newline at end of file
diff --git a/tests/test_directory/data/documents/Customer Satisfaction Survey Results.txt b/tests/test_directory/data/documents/Customer Satisfaction Survey Results.txt
new file mode 100644
index 0000000..06874fb
--- /dev/null
+++ b/tests/test_directory/data/documents/Customer Satisfaction Survey Results.txt
@@ -0,0 +1,23 @@
+Title: Customer Satisfaction Survey Results
+
+Introduction:
+The Customer Satisfaction Survey was conducted to gauge customer satisfaction with our products and services. The survey received responses from 1,000 customers.
+
+Key Findings:
+
+Overall Satisfaction: 85% of customers are satisfied with our products.
+Product Quality: Rated 4.5 out of 5
+Customer Service: Rated 4.2 out of 5
+Delivery Time: Rated 4.0 out of 5
+Comments and Feedback:
+
+Positive: "Excellent product quality and customer service!"
+Negative: "Delivery time could be improved."
+Action Plan:
+
+Improve delivery logistics to reduce shipping time.
+Continue to enhance product quality and customer service.
+Implement a loyalty program to reward repeat customers.
+
+Conclusion:
+The survey results indicate strong customer satisfaction with our products and services. By addressing the areas for improvement, we aim to further enhance the customer experience.
\ No newline at end of file
diff --git a/tests/test_directory/data/documents/Employee Onboarding Handbook.txt b/tests/test_directory/data/documents/Employee Onboarding Handbook.txt
new file mode 100644
index 0000000..f7b0756
--- /dev/null
+++ b/tests/test_directory/data/documents/Employee Onboarding Handbook.txt
@@ -0,0 +1,22 @@
+Title: Employee Onboarding Handbook
+
+Welcome Message:
+Welcome to XYZ Corporation! We are thrilled to have you join our team. This handbook is designed to help you get acquainted with our company policies, culture, and procedures.
+
+Company Mission and Values:
+Our mission is to deliver innovative solutions that improve the lives of our customers. Our core values include integrity, excellence, collaboration, and customer focus.
+
+Policies and Procedures:
+
+Work Hours: Monday to Friday, 9 AM to 5 PM
+Dress Code: Business casual
+Code of Conduct: Professional behavior and ethical standards
+Benefits:
+
+Health Insurance
+Retirement Plan
+Paid Time Off
+Professional Development Opportunities
+
+Conclusion:
+We look forward to your contributions and hope you find your experience at XYZ Corporation rewarding and fulfilling.
\ No newline at end of file
diff --git a/tests/test_directory/data/documents/Market Analysis and Trends 2024.txt b/tests/test_directory/data/documents/Market Analysis and Trends 2024.txt
new file mode 100644
index 0000000..8c20281
--- /dev/null
+++ b/tests/test_directory/data/documents/Market Analysis and Trends 2024.txt
@@ -0,0 +1,23 @@
+Title: Market Analysis and Trends 2024
+
+Executive Summary:
+This report analyzes the current market conditions and forecasts trends for 2024. It covers key sectors including technology, healthcare, and consumer goods, providing insights into growth opportunities and potential challenges.
+
+Technology Sector:
+
+Emerging Trends: AI and Machine Learning, 5G Technology
+Market Growth: Projected to grow by 15% in 2024
+Key Players: TechCorp, Innovatech, SoftSolutions
+Healthcare Sector:
+
+Emerging Trends: Telemedicine, Personalized Medicine
+Market Growth: Projected to grow by 10% in 2024
+Key Players: HealthInc, MedSolutions, CareFirst
+Consumer Goods Sector:
+
+Emerging Trends: Sustainable Products, E-commerce Expansion
+Market Growth: Projected to grow by 8% in 2024
+Key Players: ShopSmart, EcoGoods, TrendyProducts
+
+Conclusion:
+The market outlook for 2024 is positive, with significant growth expected across key sectors. Companies should focus on innovation and adaptability to capitalize on these opportunities.
\ No newline at end of file
diff --git a/tests/test_directory/data/documents/Project Proposal for Community Garden Initiative.txt b/tests/test_directory/data/documents/Project Proposal for Community Garden Initiative.txt
new file mode 100644
index 0000000..7b401e4
--- /dev/null
+++ b/tests/test_directory/data/documents/Project Proposal for Community Garden Initiative.txt
@@ -0,0 +1,23 @@
+Title: Project Proposal for Community Garden Initiative
+
+Executive Summary:
+The Community Garden Initiative aims to transform unused urban spaces into green, productive gardens that provide fresh produce to local residents. This project will foster community engagement, promote sustainable practices, and improve urban aesthetics.
+
+Objectives:
+
+Convert vacant lots into community gardens.
+Educate residents on sustainable gardening practices.
+Provide fresh produce to the local community.
+Foster a sense of community and collaboration.
+Budget:
+
+Initial setup: $10,000
+Annual maintenance: $5,000
+Timeline:
+
+Phase 1 (Planning): 2 months
+Phase 2 (Implementation): 6 months
+Phase 3 (Maintenance and Evaluation): Ongoing
+
+Conclusion:
+The Community Garden Initiative is a step towards a greener, healthier, and more connected community.
\ No newline at end of file
diff --git a/tests/test_directory/data/reports/report-2020.xlsx b/tests/test_directory/data/reports/report-2020.xlsx
new file mode 100644
index 0000000..b5ac858
Binary files /dev/null and b/tests/test_directory/data/reports/report-2020.xlsx differ
diff --git a/tests/test_directory/data/reports/report-2021.xlsx b/tests/test_directory/data/reports/report-2021.xlsx
new file mode 100644
index 0000000..bf96988
Binary files /dev/null and b/tests/test_directory/data/reports/report-2021.xlsx differ
diff --git a/tests/test_directory/data/reports/report-2022.xlsx b/tests/test_directory/data/reports/report-2022.xlsx
new file mode 100644
index 0000000..f2e3c1a
Binary files /dev/null and b/tests/test_directory/data/reports/report-2022.xlsx differ
diff --git a/tests/test_directory/data/reports/report-2023.xlsx b/tests/test_directory/data/reports/report-2023.xlsx
new file mode 100644
index 0000000..3d1e749
Binary files /dev/null and b/tests/test_directory/data/reports/report-2023.xlsx differ
diff --git a/tests/test_directory/data/reports/report-2024.xlsx b/tests/test_directory/data/reports/report-2024.xlsx
new file mode 100644
index 0000000..854edf7
Binary files /dev/null and b/tests/test_directory/data/reports/report-2024.xlsx differ
diff --git a/tests/test_directory/data/roadmap.txt b/tests/test_directory/data/roadmap.txt
new file mode 100644
index 0000000..8371c53
--- /dev/null
+++ b/tests/test_directory/data/roadmap.txt
@@ -0,0 +1,21 @@
+Product Development Roadmap
+
+Q1 2024:
+- Feature A: Initial development and testing
+- Feature B: Market research and planning
+- Improvement C: User interface redesign
+
+Q2 2024:
+- Feature A: Beta release and feedback collection
+- Feature B: Development starts
+- Improvement D: Performance optimization
+
+Q3 2024:
+- Feature A: Official release
+- Feature B: Beta release and feedback collection
+- Improvement E: Security enhancements
+
+Q4 2024:
+- Feature B: Official release
+- Feature F: New feature planning and design
+- Improvement F: Scalability improvements
\ No newline at end of file
diff --git a/tests/test_directory/data/specs.txt b/tests/test_directory/data/specs.txt
new file mode 100644
index 0000000..cb6a863
--- /dev/null
+++ b/tests/test_directory/data/specs.txt
@@ -0,0 +1,39 @@
+Product Specifications
+
+Product Name: XYZ Gadget
+Model: XG-2024
+
+Dimensions:
+- Height: 150mm
+- Width: 75mm
+- Depth: 10mm
+
+Weight: 180 grams
+
+Display:
+- Type: OLED
+- Size: 6.5 inches
+- Resolution: 1080 x 2400 pixels
+
+Processor:
+- Type: Octa-core
+- Speed: 2.8 GHz
+
+Memory:
+- RAM: 8GB
+- Storage: 128GB (expandable up to 512GB)
+
+Battery:
+- Capacity: 4000mAh
+- Fast Charging: Yes
+
+Operating System: XYZ OS 5.0
+
+Camera:
+- Rear: 48MP + 12MP + 5MP
+- Front: 24MP
+
+Connectivity:
+- 5G: Yes
+- Wi-Fi: 802.11 a/b/g/n/ac
+- Bluetooth: 5.1
\ No newline at end of file
diff --git a/tests/test_directory/data/todo.txt b/tests/test_directory/data/todo.txt
new file mode 100644
index 0000000..39812c1
--- /dev/null
+++ b/tests/test_directory/data/todo.txt
@@ -0,0 +1,12 @@
+To-Do List
+
+1. Finalize project proposal for the Community Garden Initiative.
+2. Review and approve the Annual Financial Report 2023.
+3. Prepare the Employee Onboarding Handbook for new hires.
+4. Conduct market analysis and prepare the Trends 2024 report.
+5. Analyze Customer Satisfaction Survey results and develop action plan.
+6. Follow up with support team on recent customer complaints.
+7. Update product development roadmap with Q2 milestones.
+8. Review and update product specifications for the XYZ Gadget.
+9. Plan team meeting to discuss upcoming projects and deadlines.
+10. Organize files and documents for archiving.
\ No newline at end of file
diff --git a/fise/__main__.py b/tests/test_directory/file_dir/README.md
similarity index 100%
rename from fise/__main__.py
rename to tests/test_directory/file_dir/README.md
diff --git a/fise/operators/__init__.py b/tests/test_directory/file_dir/TODO
similarity index 100%
rename from fise/operators/__init__.py
rename to tests/test_directory/file_dir/TODO
diff --git a/fise/operators/filequery.py b/tests/test_directory/file_dir/docs/config/getting-started.md
similarity index 100%
rename from fise/operators/filequery.py
rename to tests/test_directory/file_dir/docs/config/getting-started.md
diff --git a/fise/operators/textquery.py b/tests/test_directory/file_dir/docs/getting-started.md
similarity index 100%
rename from fise/operators/textquery.py
rename to tests/test_directory/file_dir/docs/getting-started.md
diff --git a/fise/parser/__init__.py b/tests/test_directory/file_dir/docs/query/handlers.md
similarity index 100%
rename from fise/parser/__init__.py
rename to tests/test_directory/file_dir/docs/query/handlers.md
diff --git a/fise/parser/fileparser.py b/tests/test_directory/file_dir/docs/testing/getting-started.md
similarity index 100%
rename from fise/parser/fileparser.py
rename to tests/test_directory/file_dir/docs/testing/getting-started.md
diff --git a/fise/parser/textparser.py b/tests/test_directory/file_dir/media/birthday.avi
similarity index 100%
rename from fise/parser/textparser.py
rename to tests/test_directory/file_dir/media/birthday.avi
diff --git a/tests/test_directory/file_dir/media/faraway.mp4 b/tests/test_directory/file_dir/media/faraway.mp4
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_directory/file_dir/media/galaxy.mp3 b/tests/test_directory/file_dir/media/galaxy.mp3
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_directory/file_dir/media/into the wild.mp3 b/tests/test_directory/file_dir/media/into the wild.mp3
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_directory/file_dir/media/oppenheimer.mp4 b/tests/test_directory/file_dir/media/oppenheimer.mp4
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_directory/file_dir/media/runaway.mp3 b/tests/test_directory/file_dir/media/runaway.mp3
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_directory/file_dir/media/unknown.mp3 b/tests/test_directory/file_dir/media/unknown.mp3
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_directory/file_dir/orders/IN523424323.txt b/tests/test_directory/file_dir/orders/IN523424323.txt
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_directory/file_dir/orders/IN544236423.txt b/tests/test_directory/file_dir/orders/IN544236423.txt
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_directory/file_dir/orders/IN544678326.txt b/tests/test_directory/file_dir/orders/IN544678326.txt
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_directory/file_dir/orders/JP443523223.txt b/tests/test_directory/file_dir/orders/JP443523223.txt
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_directory/file_dir/orders/JP443524523.txt b/tests/test_directory/file_dir/orders/JP443524523.txt
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_directory/file_dir/orders/UK324324235.txt b/tests/test_directory/file_dir/orders/UK324324235.txt
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_directory/file_dir/orders/US434123123.txt b/tests/test_directory/file_dir/orders/US434123123.txt
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_directory/file_dir/orders/US434312321.txt b/tests/test_directory/file_dir/orders/US434312321.txt
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_directory/file_dir/project/Dockerfile b/tests/test_directory/file_dir/project/Dockerfile
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_directory/file_dir/project/LICENSE b/tests/test_directory/file_dir/project/LICENSE
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_directory/file_dir/project/README.md b/tests/test_directory/file_dir/project/README.md
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_directory/file_dir/project/requirements.txt b/tests/test_directory/file_dir/project/requirements.txt
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_directory/file_dir/project/setup.py b/tests/test_directory/file_dir/project/setup.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_directory/file_dir/project/src/handlers.py b/tests/test_directory/file_dir/project/src/handlers.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_directory/file_dir/project/src/main.py b/tests/test_directory/file_dir/project/src/main.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_directory/file_dir/project/src/tools.py b/tests/test_directory/file_dir/project/src/tools.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_directory/file_dir/reports/report-2021/Q1.txt b/tests/test_directory/file_dir/reports/report-2021/Q1.txt
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_directory/file_dir/reports/report-2021/Q2.txt b/tests/test_directory/file_dir/reports/report-2021/Q2.txt
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_directory/file_dir/reports/report-2021/Q3.txt b/tests/test_directory/file_dir/reports/report-2021/Q3.txt
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_directory/file_dir/reports/report-2021/Q4.txt b/tests/test_directory/file_dir/reports/report-2021/Q4.txt
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_directory/file_dir/reports/report-2022/Q1.txt b/tests/test_directory/file_dir/reports/report-2022/Q1.txt
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_directory/file_dir/reports/report-2022/Q2.txt b/tests/test_directory/file_dir/reports/report-2022/Q2.txt
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_directory/file_dir/reports/report-2022/Q3.txt b/tests/test_directory/file_dir/reports/report-2022/Q3.txt
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_directory/file_dir/reports/report-2022/Q4.txt b/tests/test_directory/file_dir/reports/report-2022/Q4.txt
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_directory/file_dir/reports/report-2023/Q1.txt b/tests/test_directory/file_dir/reports/report-2023/Q1.txt
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_directory/file_dir/reports/report-2023/Q2.txt b/tests/test_directory/file_dir/reports/report-2023/Q2.txt
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_directory/file_dir/reports/report-2023/Q3.txt b/tests/test_directory/file_dir/reports/report-2023/Q3.txt
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_directory/file_dir/reports/report-2023/Q4.txt b/tests/test_directory/file_dir/reports/report-2023/Q4.txt
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_directory/file_dir/reports/report-2024/Q1.txt b/tests/test_directory/file_dir/reports/report-2024/Q1.txt
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_directory/file_dir/reports/report-2024/Q2.txt b/tests/test_directory/file_dir/reports/report-2024/Q2.txt
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_directory/file_dir/roadmap.txt b/tests/test_directory/file_dir/roadmap.txt
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_query/conftest.py b/tests/test_query/conftest.py
new file mode 100644
index 0000000..65a43c3
--- /dev/null
+++ b/tests/test_query/conftest.py
@@ -0,0 +1,10 @@
+import sys
+from pathlib import Path
+
+ADDITIONAL_PATHS = [
+ str(Path(__file__).parents[1]),
+ str(Path(__file__).parents[2]),
+ str(Path(__file__).parents[2] / "fise"),
+]
+
+sys.path.extend(ADDITIONAL_PATHS)
diff --git a/tests/test_query/test_delete_query.hdf b/tests/test_query/test_delete_query.hdf
new file mode 100644
index 0000000..72c5a14
Binary files /dev/null and b/tests/test_query/test_delete_query.hdf differ
diff --git a/tests/test_query/test_delete_query.py b/tests/test_query/test_delete_query.py
new file mode 100644
index 0000000..e1b835b
--- /dev/null
+++ b/tests/test_query/test_delete_query.py
@@ -0,0 +1,258 @@
+"""
+This module comprises test cases for verifying
+the functionality of delete queries in FiSE.
+"""
+
+# NOTE
+# The attributes comprising test parameters in this module are organized such
+# that each individual attribute comprises sub-arrays, each of a length of 2,
+# where the first element within the array signifies the index, whereas the
+# second element is a string comprising the delete query.
+
+
+from pathlib import Path
+
+import pytest
+import pandas as pd
+
+import utils
+import reset_tests
+from fise.query import QueryHandler
+
+TEST_DIRECTORY = Path(__file__).parents[1] / "test_directory"
+FILE_DIR_TEST_DIRECTORY = TEST_DIRECTORY / "file_dir"
+
+TEST_DIRECTORY_LISTINGS_FILE = TEST_DIRECTORY.parent / "test_directory.hdf"
+TEST_RECORDS_FILE = Path(__file__).parent / "test_delete_query.hdf"
+
+
+# Pandas series comprising path of all the filses and directories
+# present within the `file_dir` directory within the test directory.
+FILE_DIR_TEST_DIRECTORY_LISTINGS = pd.concat(
+ [
+ utils.read_hdf(TEST_DIRECTORY_LISTINGS_FILE, "/file_dir/dirs"),
+ utils.read_hdf(TEST_DIRECTORY_LISTINGS_FILE, "/file_dir/files"),
+ ],
+ ignore_index=True,
+)
+
+
+def verify_delete_query(path: str) -> None:
+ """
+ Verifies all the files and directories removed from the delete query by matching
+ test records stored at the specified path in the `test_delete_query.hdf` file.
+ """
+ global FILE_DIR_TEST_DIRECTORY, FILE_DIR_TEST_DIRECTORY_LISTINGS, TEST_RECORDS_FILE
+
+ # File and directories to be exempted during verification as
+ # they are meant to be removed during the delete operation.
+ records: pd.Series = utils.read_hdf(TEST_RECORDS_FILE, path)
+
+ for i in FILE_DIR_TEST_DIRECTORY_LISTINGS:
+ if (FILE_DIR_TEST_DIRECTORY / i).exists() or i in records.values:
+ continue
+
+ # Resets the `file_dir` directory within test directory
+ # in case a file or directory is not found unexpectedly.
+ reset_tests.reset_file_dir_test_directory()
+
+ raise FileNotFoundError(
+ f"'{FILE_DIR_TEST_DIRECTORY / i}' was not found in the test directory."
+ )
+
+ else:
+ # Resets the `file_dir` and reverts back all the changes made within the directory.
+ reset_tests.reset_file_dir_test_directory()
+
+
+def examine_delete_query(query: str, records_path: str) -> None:
+ """
+ Tests the specified delete query and verifies it with the file and directory
+ records stored at the specified path in the `test_delete_query.hdf` file.
+ """
+
+ # Handles the specified query, performs the delete operation and verifies it.
+ QueryHandler(query).handle()
+ verify_delete_query(records_path)
+
+
+class TestFileDeleteQuery:
+ """Tests the QueryHandler class with file delete queries."""
+
+ basic_query_syntax_test_params = [
+ (1, f"DELETE FROM '{FILE_DIR_TEST_DIRECTORY / 'project'}'"),
+ (2,f"DELETE[TYPE FILE] FROM '{FILE_DIR_TEST_DIRECTORY / 'media'}' WHERE type = '.mp3'"),
+ (3, rf"R DELETE FROM '{FILE_DIR_TEST_DIRECTORY / 'project'}' WHERE name LIKE '.*\.py'"),
+ ]
+
+ recursive_command_test_params = [
+ (1, f"R DELETE FROM '{FILE_DIR_TEST_DIRECTORY / 'reports'}' WHERE name = 'Q4.txt'"),
+ (2, f"RECURSIVE DELETE FROM '{FILE_DIR_TEST_DIRECTORY / 'docs'}'"),
+ ]
+
+ path_types_test_params = [
+ (1, f"R DELETE FROM ABSOLUTE '{FILE_DIR_TEST_DIRECTORY / 'reports'}' WHERE name = 'Q2.txt'"),
+ (2, f"DELETE FROM ABSOLUTE '{FILE_DIR_TEST_DIRECTORY / 'project'}'"),
+ (3, f"DELETE FROM RELATIVE '{FILE_DIR_TEST_DIRECTORY / 'media'}' WHERE type = '.mp4'"),
+ ]
+
+ mixed_case_query_test_params = [
+ (1, f"DeLeTE FRoM '{FILE_DIR_TEST_DIRECTORY / 'project'}'"),
+ (2, f"DelETE[TYPE FiLE] FrOM '{FILE_DIR_TEST_DIRECTORY / 'media'}' Where TypE = '.mp3'"),
+ (3, rf"r dELETe FrOM '{FILE_DIR_TEST_DIRECTORY / 'project'}' wHERe NaMe liKe '.*\.py'"),
+ ]
+
+ query_conditions_test_params = [
+ (1, f"R DELETE FROM '{FILE_DIR_TEST_DIRECTORY}' WHERE name = 'Q1.txt'"),
+ (2, rf"R DELETE FROM '{FILE_DIR_TEST_DIRECTORY}' WHERE type='.txt' AND name LIKE '^IN.*\.txt$'"),
+ (3, f"R DELETE FROM '{FILE_DIR_TEST_DIRECTORY}' WHERE type IN ('.mp4', '.avi') OR type='.mp3'"),
+ ]
+
+ nested_conditions_test_params = [
+ (
+ 1,
+ f"R DELETE FROM '{FILE_DIR_TEST_DIRECTORY}' WHERE "
+ "size[b] = 0 AND (filetype = '.txt' OR type = '.mp3')",
+ ),
+ (
+ 2,
+ f"R DELETE FROM '{FILE_DIR_TEST_DIRECTORY / 'project'}' WHERE size[b]"
+ "= 0 AND (type = None OR (type IN ('.txt', '.py'))) AND name != 'LICENSE'",
+ ),
+ (
+ 3,
+ f"R DELETE FROM '{FILE_DIR_TEST_DIRECTORY / 'reports'}' WHERE"
+ " type = '.txt' AND (((name = 'Q1.txt' OR name = 'Q3.txt')))",
+ ),
+ ]
+
+ @pytest.mark.parametrize(("index", "query"), basic_query_syntax_test_params)
+ def test_basic_query_syntax(self, index: int, query: str) -> None:
+ """Tests basic syntax for file delete queries"""
+ examine_delete_query(query, f"/file/basic/test{index}")
+
+ @pytest.mark.parametrize(("index", "query"), recursive_command_test_params)
+ def test_recursive_command(self, index: int, query: str) -> None:
+ """Tests file delete queries with the recursive command"""
+ examine_delete_query(query, f"/file/recursive/test{index}")
+
+ @pytest.mark.parametrize(("index", "query"), path_types_test_params)
+ def test_path_types(self, index: int, query: str) -> None:
+ """Tests file delete queries with different path types"""
+ examine_delete_query(query, f"/file/path_types/test{index}")
+
+ @pytest.mark.parametrize(("index", "query"), query_conditions_test_params)
+ def test_query_conditions(self, index: int, query: str) -> None:
+ """Tests file delete query conditions"""
+ examine_delete_query(query, f"/file/conditions/test{index}")
+
+ @pytest.mark.parametrize(("index", "query"), nested_conditions_test_params)
+ def test_nested_query_conditions(self, index: int, query: str) -> None:
+ """Tests nested file delete query conditions"""
+ examine_delete_query(query, f"/file/nested_conditions/test{index}")
+
+ # The following test uses the same delete queries defined for basic query syntax test
+ # comprising characters of mixed cases, and hence uses the file and directory records
+ # stored at the same path in the `test_delete_query.hdf` file.
+
+ @pytest.mark.parametrize(("index", "query"), mixed_case_query_test_params)
+ def test_mixed_case_query(self, index: int, query: str) -> None:
+ """Tests file delete queries comprising mixed case characters."""
+ examine_delete_query(query, f"/file/basic/test{index}")
+
+
+class TestDirDeleteQuery:
+ """Tests the QueryHandler class with directory delete queries"""
+
+ basic_query_syntax_test_params = [
+ (1, f"DELETE[TYPE DIR] FROM '{FILE_DIR_TEST_DIRECTORY / 'docs'}'"),
+ (2, f"DELETE[TYPE DIR] FROM '{FILE_DIR_TEST_DIRECTORY / 'reports'}' WHERE name LIKE '^report.*$'"),
+ (3, f"DELETE[TYPE DIR] FROM '{FILE_DIR_TEST_DIRECTORY}' WHERE name IN ('project', 'media')"),
+ ]
+
+ recursive_command_test_params = [
+ (1, f"R DELETE[TYPE DIR] FROM '{FILE_DIR_TEST_DIRECTORY / 'project'}'"),
+ (2, f"R DELETE[TYPE DIR] FROM '{FILE_DIR_TEST_DIRECTORY / 'reports'}' WHERE name='report-2023'"),
+ (3, f"R DELETE[TYPE DIR] FROM '{FILE_DIR_TEST_DIRECTORY}' WHERE name IN ('media', 'orders')"),
+ ]
+
+ path_types_test_params = [
+ (1, f"DELETE[TYPE DIR] FROM ABSOLUTE '{FILE_DIR_TEST_DIRECTORY / 'docs'}'"),
+ (2, f"DELETE[TYPE DIR] FROM RELATIVE '{FILE_DIR_TEST_DIRECTORY / 'reports'}'"),
+ (3, f"DELETE[TYPE DIR] FROM ABSOLUTE '{FILE_DIR_TEST_DIRECTORY / 'project'}'"),
+ ]
+
+ mixed_case_query_test_params = [
+ (1, f"DeLEtE[TYPe DIR] froM '{FILE_DIR_TEST_DIRECTORY / 'docs'}'"),
+ (2, f"DElEtE[tYPE DiR] From '{FILE_DIR_TEST_DIRECTORY / 'reports'}' Where NamE LikE '^report.*$'"),
+ (3, f"Delete[Type diR] FroM '{FILE_DIR_TEST_DIRECTORY}' WheRE NAMe In ('project', 'media')"),
+ ]
+
+ query_conditions_test_params = [
+ (
+ 1,
+ f"DELETE[TYPE DIR] FROM '{FILE_DIR_TEST_DIRECTORY}' WHERE name = 'media'"
+ ),
+ (
+ 2,
+ f"DELETE[TYPE DIR] FROM '{FILE_DIR_TEST_DIRECTORY}' WHERE "
+ "name IN ('orders', 'media') AND parent LIKE '^.*file_dir[/]?$'",
+ ),
+ (
+ 3,
+ f"DELETE[TYPE DIR] FROM '{FILE_DIR_TEST_DIRECTORY / 'reports'}' WHERE"
+ " name = 'report-2021' OR name = 'report-2022' OR name = 'report-2023'",
+ ),
+ ]
+
+ nested_conditions_test_params = [
+ (
+ 1,
+ f"DELETE[TYPE DIR] FROM '{FILE_DIR_TEST_DIRECTORY}' WHERE parent"
+ " LIKE '^.*file_dir[/]?$' AND (name = 'orders' OR name = 'media')",
+ ),
+ (
+ 2,
+ f"R DELETE[TYPE DIR] FROM '{FILE_DIR_TEST_DIRECTORY}' WHERE"
+ " ((name IN ('src'))) AND path LIKE '^.*file_dir/src[/]?$'",
+ ),
+ (
+ 3,
+ f"DELETE[TYPE DIR] FROM '{FILE_DIR_TEST_DIRECTORY / 'reports'}' WHERE path LIKE "
+ "'^.*report-20(23|24)[/]?$' AND ((name = 'report-2023') OR ((name = 'report-2024')))",
+ ),
+ ]
+
+ @pytest.mark.parametrize(("index", "query"), basic_query_syntax_test_params)
+ def test_basic_query_syntax(self, index: int, query: str) -> None:
+ """Tests basic syntax for directory delete queries"""
+ examine_delete_query(query, f"/dir/basic/test{index}")
+
+ @pytest.mark.parametrize(("index", "query"), recursive_command_test_params)
+ def test_recursive_command(self, index: int, query: str) -> None:
+ """Tests directory delete queries with the recursive command"""
+ examine_delete_query(query, f"/dir/recursive/test{index}")
+
+ @pytest.mark.parametrize(("index", "query"), path_types_test_params)
+ def test_path_types(self, index: int, query: str) -> None:
+ """Tests directory delete queries with different path types"""
+ examine_delete_query(query, f"/dir/path_types/test{index}")
+
+ @pytest.mark.parametrize(("index", "query"), query_conditions_test_params)
+ def test_query_conditions(self, index: int, query: str) -> None:
+ """Tests directory delete query conditions."""
+ examine_delete_query(query, f"/dir/conditions/test{index}")
+
+ @pytest.mark.parametrize(("index", "query"), nested_conditions_test_params)
+ def test_nested_query_conditions(self, index: int, query: str) -> None:
+ """Tests nested directory delete query conditions."""
+ examine_delete_query(query, f"/dir/nested_conditions/test{index}")
+
+ # The following test uses the same delete queries defined for basic query syntax test
+ # comprising characters of mixed cases, and hence uses the file and directory records
+ # stored at the same path in the `test_delete_query.hdf` file.
+
+ @pytest.mark.parametrize(("index", "query"), mixed_case_query_test_params)
+ def test_mixed_case_query(self, index: int, query: str) -> None:
+ """Tests directory delete queries comprising mixed case characters."""
+ examine_delete_query(query, f"/dir/basic/test{index}")
diff --git a/tests/test_query/test_handlers/conftest.py b/tests/test_query/test_handlers/conftest.py
new file mode 100644
index 0000000..4b114d5
--- /dev/null
+++ b/tests/test_query/test_handlers/conftest.py
@@ -0,0 +1,11 @@
+import sys
+from pathlib import Path
+
+ADDITIONAL_PATHS = [
+ str(Path(__file__).parents[1]),
+ str(Path(__file__).parents[2]),
+ str(Path(__file__).parents[3]),
+ str(Path(__file__).parents[3] / "fise"),
+]
+
+sys.path.extend(ADDITIONAL_PATHS)
diff --git a/tests/test_query/test_handlers/test_operators.hdf b/tests/test_query/test_handlers/test_operators.hdf
new file mode 100644
index 0000000..ac726f4
Binary files /dev/null and b/tests/test_query/test_handlers/test_operators.hdf differ
diff --git a/tests/test_query/test_handlers/test_operators.py b/tests/test_query/test_handlers/test_operators.py
new file mode 100644
index 0000000..2793d4d
--- /dev/null
+++ b/tests/test_query/test_handlers/test_operators.py
@@ -0,0 +1,350 @@
+"""
+This module comprises test cases for verifying
+the functionality of operator classes in FiSE.
+"""
+
+# NOTE:
+# The structural format of the attributes comprising test
+# parameters defined within this module is as described below:
+#
+# 1. Test parameters for search operations:
+#
+# These attributes comprise sub-arrays, each with a length of 4 where the first element of each
+# of them signifies the index, the second signifies whether the test is verifiable (True) or
+# non-verifiable (False). The third element is an array comprising parameters for the search
+# operation in the following order: (directory, recursive, fields) whereas the last element is
+# a reference to the function comprising the filtering conditions.
+#
+# 2. Test parameters for delete operations:
+#
+# These attributes also comprise sub-arrays, each with a length of 2 where the first
+# element of each of them signify the index, whereas the second element is another
+# sub-array comprising parameters for the delete operation in the following order:
+# (directory, recursive, condition, skip_err).
+
+# NOTE:
+# Some attributes may not adhere to the attribute structures mentioned above due to some
+# exceptions in the tests and will have an explicit structural description along with them.
+
+
+from pathlib import Path
+from typing import Any
+
+import pytest
+import pandas as pd
+
+import utils
+import reset_tests
+from fise.common import constants
+from fise.shared import File, Directory, Field, DataLine
+from fise.query.operators import (
+ FileDataQueryOperator,
+ FileQueryOperator,
+ DirectoryQueryOperator,
+)
+
+
+TEST_DIRECTORY = Path(__file__).parents[2] / "test_directory"
+
+DATA_TEST_DIRECTORY = TEST_DIRECTORY / "data"
+FILE_DIR_TEST_DIRECTORY = TEST_DIRECTORY / "file_dir"
+
+TEST_DIRECTORY_LISTINGS_FILE = TEST_DIRECTORY.parent / "test_directory.hdf"
+TEST_RECORDS_FILE = Path(__file__).parent / "test_operators.hdf"
+
+# Pandas series comprising path of all the files and directories
+# present within the `file_dir` directory within test directory.
+FILE_DIR_TEST_DIRECTORY_LISTINGS = pd.concat(
+ [
+ utils.read_hdf(TEST_DIRECTORY_LISTINGS_FILE, "/file_dir/dirs"),
+ utils.read_hdf(TEST_DIRECTORY_LISTINGS_FILE, "/file_dir/files"),
+ ],
+ ignore_index=True,
+)
+
+
+def verify_search_operation(path: str, data: pd.DataFrame) -> None:
+ """
+ Verifies the pandas dataframe extracted from the search operation with the
+ records stored at the specified path in the `test_operators.hdf` file.
+ """
+ global TEST_RECORDS_FILE
+
+ assert isinstance(data, pd.DataFrame)
+
+ results: pd.DataFrame = utils.read_hdf(TEST_RECORDS_FILE, path)
+
+ data_set: set[tuple[Any]] = set(tuple(row) for row in data.values)
+ results_set: set[tuple[Any]] = set(tuple(row) for row in results.values)
+
+ assert data_set == results_set
+
+
+def verify_delete_operation(path: str) -> None:
+ """
+ Verifies all the files and directories removed from the delete query by matching
+ records stored at the specified path in the `test_delete_query.hdf` file.
+ """
+ global TEST_RECORDS_FILE, FILE_DIR_TEST_DIRECTORY_LISTINGS
+
+ # File and directories to be exempted during verification as
+ # they are meant to be removed during the delete operation.
+ records: pd.Series = utils.read_hdf(TEST_RECORDS_FILE, path)
+
+ for i in FILE_DIR_TEST_DIRECTORY_LISTINGS:
+ if (FILE_DIR_TEST_DIRECTORY / i).exists() or i in records.values:
+ continue
+
+ # Resets the `file_dir` directory within test directory
+ # in case a file or directory is not found unexpectedly.
+ reset_tests.reset_file_dir_test_directory()
+
+ raise FileNotFoundError(
+ f"'{FILE_DIR_TEST_DIRECTORY / i}' was not found in the test directory."
+ )
+
+ else:
+ # Resets the `file_dir` and reverts back all the changes made within the directory.
+ reset_tests.reset_file_dir_test_directory()
+
+
+class TestFileQueryOperator:
+ """Tests the FileQueryOperator class"""
+
+ @staticmethod
+ def condition1(file: File) -> bool:
+ return file.filetype is not None
+
+ @staticmethod
+ def condition2(file: File) -> bool:
+ return not file.size
+
+ @staticmethod
+ def condition3(file: File) -> bool:
+ return file.name in ("unknown.mp3", "runaway.mp3", "birthday.avi")
+
+ @staticmethod
+ def condition4(file: File) -> bool:
+ return file.name in ("Q1.txt", "Q2.txt")
+
+ search_operation_test_params = [
+ (1, True, ["", False, ["name", "filetype"], condition1]),
+ (2, True, ["reports", True, ["name"], condition2]),
+ (3, False, ["", False, ["path", "access_time"], condition2]),
+ (4, False, ["", False, ["parent", "create_time"], condition1]),
+ ]
+
+ search_operation_with_fields_alias_test_params = [
+ (1, True, ["", True, ["filename", "type"], condition3]),
+ (2, False, ["media", False, ["ctime", "atime", "mtime"], condition1]),
+ (3, False, ["docs", True, ["filepath", "type", "ctime"], condition2]),
+ ]
+
+ delete_operation_test_params = [
+ (1, ["", False, condition1, False]),
+ (2, ["reports", True, condition4, False]),
+ (3, ["media", False, condition3, True]),
+ ]
+
+ @pytest.mark.parametrize(
+ ("index", "verify", "params"), search_operation_test_params
+ )
+ def test_search_operation(
+ self, index: int, verify: bool, params: list[Any]
+ ) -> None:
+ """Tests file query operator with search operations."""
+
+ fields: list[Field] = [Field(name) for name in params[2]]
+
+ operator = FileQueryOperator(FILE_DIR_TEST_DIRECTORY / params[0], params[1])
+ data: pd.DataFrame = operator.get_dataframe(fields, params[2], params[3])
+
+ if verify:
+ verify_search_operation(f"/file/search/test{index}", data)
+
+ @pytest.mark.parametrize(
+ ("index", "verify", "params"), search_operation_with_fields_alias_test_params
+ )
+ def test_search_operation_with_field_aliases(
+ self, index: int, verify: bool, params: list[Any]
+ ) -> None:
+ """
+ Tests file query operator with search
+ operations comprising field aliases.
+ """
+
+ fields: list[Field] = [
+ Field(constants.FILE_FIELD_ALIASES[i]) for i in params[2]
+ ]
+
+ operator = FileQueryOperator(FILE_DIR_TEST_DIRECTORY / params[0], params[1])
+ data: pd.DataFrame = operator.get_dataframe(fields, params[2], params[3])
+
+ if verify:
+ verify_search_operation(f"/file/search2/test{index}", data)
+
+ @pytest.mark.parametrize(("index", "params"), delete_operation_test_params)
+ def test_delete_operation(self, index: int, params: list[Any]) -> None:
+ """Tests file query operation with delete operations."""
+
+ operator = FileQueryOperator(FILE_DIR_TEST_DIRECTORY / params[0], params[1])
+ operator.remove_files(params[2], params[3])
+
+ verify_delete_operation(f"/file/delete/test{index}")
+
+
+class TestFileDataQueryOperator:
+ """Tests the FileDataQueryOperator class"""
+
+ @staticmethod
+ def condition1(data: DataLine) -> bool:
+ return data.name in ("todo.txt", "specs.txt") and data.lineno in range(20)
+
+ @staticmethod
+ def condition2(data: DataLine) -> bool:
+ return data.name in (
+ "Annual Financial Report 2023.txt",
+ "Customer Satisfaction Survey Results.txt",
+ ) and data.lineno in range(10)
+
+ @staticmethod
+ def condition3(data: DataLine) -> bool:
+ return data.lineno in range(10)
+
+ @staticmethod
+ def condition4(data: DataLine) -> bool:
+ return data.name in ("todo.txt", "report-2020.xlsx") and data.lineno in range(8)
+
+ text_search_operation_test_params = [
+ (1, True, ["", False, ["name", "lineno", "dataline"], condition1]),
+ (2, True, ["documents", False, ["lineno", "dataline"], condition2]),
+ ]
+
+ bytes_search_operation_test_params = [
+ (1, True, ["reports", False, ["dataline"], condition3]),
+ (2, True, ["", True, ["name", "dataline", "filetype"], condition4]),
+ ]
+
+ @pytest.mark.parametrize(
+ ("index", "verify", "params"), text_search_operation_test_params
+ )
+ def test_text_search_operation(
+ self, index: int, verify: bool, params: list[Any]
+ ) -> None:
+ """Tests file data query operator with text search operations."""
+
+ fields: list[Field] = [Field(name) for name in params[2]]
+
+ operator = FileDataQueryOperator(
+ DATA_TEST_DIRECTORY / params[0], params[1], "text"
+ )
+ data: pd.DataFrame = operator.get_dataframe(fields, params[2], params[3])
+
+ if verify:
+ verify_search_operation(f"/data/text/search/test{index}", data)
+
+ @pytest.mark.parametrize(
+ ("index", "verify", "params"), bytes_search_operation_test_params
+ )
+ def test_bytes_search_operation(
+ self, index: int, verify: bool, params: list[Any]
+ ) -> None:
+ """Tests file data query operator with bytes search operations."""
+
+ fields: list[Field] = [Field(name) for name in params[2]]
+
+ operator = FileDataQueryOperator(
+ DATA_TEST_DIRECTORY / params[0], params[1], "bytes"
+ )
+ data: pd.DataFrame = operator.get_dataframe(fields, params[2], params[3])
+
+ if verify:
+ verify_search_operation(f"/data/bytes/search/test{index}", data)
+
+
+class TestDirectoryQueryOperator:
+ """Tests the DirectoryQueryOperator class"""
+
+ @staticmethod
+ def condition1(directory: Directory) -> bool:
+ return directory.name in ("docs", "media", "reports")
+
+ @staticmethod
+ def condition2(directory: Directory) -> bool:
+ return directory.name in ("report-2021", "report-2022")
+
+ @staticmethod
+ def condition3(directory: Directory) -> bool:
+ return directory.name == "media"
+
+ @staticmethod
+ def condition4(directory: Directory) -> bool:
+ return directory.name == "src"
+
+ search_operation_test_params = [
+ (1, True, ["", False, ["name"], condition1]),
+ (2, False, ["reports", True, ["name", "access_time"], condition2]),
+ (3, False, ["", True, ["path", "parent", "create_time"], condition1]),
+ (4, False, ["reports", False, ["name", "modify_time"], condition2]),
+ ]
+
+ search_operation_with_fields_alias_test_params = [
+ (["", False, ["atime", "mtime", "ctime"], condition1]),
+ (["reports", True, ["atime", "ctime"], condition2]),
+ ]
+
+ delete_operation_test_params = [
+ (1, ["", False, condition3, False]),
+ (2, ["reports", False, condition2, True]),
+ (3, ["", True, condition4, False]),
+ ]
+
+ @pytest.mark.parametrize(
+ ("index", "verify", "params"), search_operation_test_params
+ )
+ def test_search_operation(
+ self, index: int, verify: bool, params: list[Any]
+ ) -> None:
+ """Tests directory query operator with search operations."""
+
+ fields: list[Field] = [Field(name) for name in params[2]]
+
+ operator = DirectoryQueryOperator(
+ FILE_DIR_TEST_DIRECTORY / params[0], params[1]
+ )
+ data: pd.DataFrame = operator.get_dataframe(fields, params[2], params[3])
+
+ if verify:
+ verify_search_operation(f"/directory/search/test{index}", data)
+
+ # The following method does not verify its tests as the generated data results
+ # are flexible and subject to change based on the execution environment.
+
+ @pytest.mark.parametrize("params", search_operation_with_fields_alias_test_params)
+ def test_search_operation_with_field_aliases(self, params: list[Any]) -> None:
+ """
+ Tests directory query operator with search
+ operations comprising field aliases.
+ """
+
+ fields: list[Field] = [
+ Field(constants.DIR_FIELD_ALIASES[name]) for name in params[2]
+ ]
+
+ operator = DirectoryQueryOperator(
+ FILE_DIR_TEST_DIRECTORY / params[0], params[1]
+ )
+ data: pd.DataFrame = operator.get_dataframe(fields, params[2], params[3])
+
+ assert isinstance(data, pd.DataFrame)
+
+ @pytest.mark.parametrize(("index", "params"), delete_operation_test_params)
+ def test_delete_operation(self, index: int, params: list[Any]) -> None:
+ """Tests file query operation with delete operations."""
+
+ operator = DirectoryQueryOperator(
+ FILE_DIR_TEST_DIRECTORY / params[0], params[1]
+ )
+ operator.remove_directories(params[2], params[3])
+
+ verify_delete_operation(f"/directory/delete/test{index}")
diff --git a/tests/test_query/test_handlers/test_parsers.py b/tests/test_query/test_handlers/test_parsers.py
new file mode 100644
index 0000000..1fe6756
--- /dev/null
+++ b/tests/test_query/test_handlers/test_parsers.py
@@ -0,0 +1,358 @@
+"""
+This module comprises test cases for verifying
+the functionality of parser classes in FiSE.
+"""
+
+# NOTE:
+# The structural format of the attributes comprising test parameters
+# and results defined within this module are as described below:
+#
+# 1. Test parameters for search and delete queries:
+#
+# The attributes comprising parameters for search and delete query tests
+# comprise strings containing the test queries.
+#
+# 2. Test results for search queries:
+#
+# The attributes comprising results for search query tests comprise sub-arrays, each
+# with a variable length where the first element of each of them signifies whether the
+# path is absolute (True) or relatve (False) whereas the last element is another array
+# comprising column names. The remaining elements in the array are test specific and may
+# different with different tests.
+#
+# 3. Test results for delete queries:
+#
+# The attributes comprising parameters for delete query tests comprise boolean objects
+# signifying whether the specified path-type is absolute (True) or relative (False).
+
+
+from pathlib import Path
+from typing import Any
+
+import pytest
+
+from fise.shared import SearchQuery, DeleteQuery
+from fise.common import tools, constants
+from fise.query.parsers import (
+ DirectoryQueryParser,
+ FileQueryParser,
+ FileDataQueryParser,
+)
+
+
+def examine_search_query(
+ parser: FileQueryParser | FileDataQueryParser | DirectoryQueryParser,
+ results: list[Any],
+) -> SearchQuery:
+ """
+ Tests the seach query based on the specified query parser and returns the
+ `SearchQuery` object for cases where additional tests are to be performed.
+ """
+
+ search_query: SearchQuery = parser.parse_query()
+
+ path: Path = Path(".")
+ columns: list[str] = results[-1]
+
+ if results[0]:
+ path = path.resolve()
+
+ assert callable(search_query.condition)
+ assert search_query.path == path
+ assert search_query.columns == columns
+
+ return search_query
+
+
+def examine_delete_query(
+ parser: FileQueryParser | DirectoryQueryParser, is_absolute: bool
+) -> None:
+ """Tests the delete query based on the specified parser and results."""
+
+ delete_query: DeleteQuery = parser.parse_query()
+ path: Path = Path(".")
+
+ if is_absolute:
+ path = path.resolve()
+
+ assert delete_query.path == path
+ assert callable(delete_query.condition)
+
+
+class TestFileQueryParser:
+ """Tests the FileQueryParser class"""
+
+ search_query_test_params = [
+ "* FROM .",
+ "name, path, parent FROM ABSOLUTE '.'",
+ "access_time,modify_time from RELATIVE .",
+ r"* FROM ABSOLUTE . WHERE filetype = '.txt' AND name LIKE '^report-[0-9]*\.txt$'",
+ "name, path,access_time FROM . WHERE access_time >= '2023-04-04'",
+ "* FROM '.' WHERE create_time >= '2024-02-20'",
+ ]
+
+ search_query_with_size_fields_test_params = [
+ "size, size[B] FROM . WHERE filetype = '.py' AND size[KiB] > 512",
+ "size[b],size,size[TiB] FROM ABSOLUTE .",
+ "size[MB], size[GB], size[B] FROM . WHERE size[MiB] > 10",
+ ]
+
+ search_query_with_field_aliases_test_params = [
+ "filename, filepath FROM ABSOLUTE .",
+ "filepath, ctime,atime FROM . WHERE atime <= '2023-01-01' AND ctime >= '2006-02-20'",
+ "filename, type, mtime FROM RELATIVE . WHERE type = '.png'",
+ ]
+
+ delete_query_test_params = [
+ "FROM ABSOLUTE .",
+ "FROM . WHERE type = '.py' and 'fise' in parent",
+ "FROM RELATIVE . WHERE atime <= '2012-02-17' OR ctime <= '2015-03-23'",
+ ]
+
+ search_query_test_results = [
+ [False, list(constants.FILE_FIELDS)],
+ [True, ["name", "path", "parent"]],
+ [False, ["access_time", "modify_time"]],
+ [True, list(constants.FILE_FIELDS)],
+ [False, ["name", "path", "access_time"]],
+ [False, list(constants.FILE_FIELDS)],
+ ]
+
+ search_query_with_size_fields_test_results = [
+ [False, ["B", "B"], ["size", "size[B]"]],
+ [True, ["b", "B", "TiB"], ["size[b]", "size", "size[TiB]"]],
+ [False, ["MB", "GB", "B"], ["size[MB]", "size[GB]", "size[B]"]],
+ ]
+
+ search_query_with_field_aliases_test_results = [
+ [True, ["name", "path"], ["filename", "filepath"]],
+ [False, ["path", "create_time", "access_time"], ["filepath", "ctime", "atime"]],
+ [False, ["name", "filetype", "modify_time"], ["filename", "type", "mtime"]],
+ ]
+
+ delete_query_test_results = [True, False, False]
+
+ @pytest.mark.parametrize(
+ ("subquery", "results"),
+ zip(search_query_test_params, search_query_test_results),
+ )
+ def test_search_query(self, subquery: str, results: list[Any]) -> None:
+ """Tests the file query parser with search queries."""
+
+ query: list[str] = tools.parse_query(subquery)
+ parser = FileQueryParser(query, "search")
+
+ search_query: SearchQuery = examine_search_query(parser, results)
+ fields: list[str] = results[1]
+
+ assert [field.field for field in search_query.fields] == fields
+
+ @pytest.mark.parametrize(
+ ("subquery", "results"),
+ zip(
+ search_query_with_size_fields_test_params,
+ search_query_with_size_fields_test_results,
+ ),
+ )
+ def test_search_query_with_size_fields(
+ self, subquery: str, results: list[Any]
+ ) -> None:
+ """
+ Tests the file query parser with search queries comprising size fields.
+ """
+
+ query: list[str] = tools.parse_query(subquery)
+ parser = FileQueryParser(query, "search")
+
+ search_query: SearchQuery = examine_search_query(parser, results)
+ units: list[str] = results[1]
+
+ assert [field.unit for field in search_query.fields] == units
+
+ @pytest.mark.parametrize(
+ ("subquery", "results"),
+ zip(
+ search_query_with_field_aliases_test_params,
+ search_query_with_field_aliases_test_results,
+ ),
+ )
+ def test_search_query_with_field_aliases(
+ self, subquery: str, results: list[Any]
+ ) -> None:
+ """
+ Tests the file query parser with search queries comprising field aliases.
+ """
+
+ query: list[str] = tools.parse_query(subquery)
+ parser = FileQueryParser(query, "search")
+
+ search_query: SearchQuery = examine_search_query(parser, results)
+ fields: list[str] = results[1]
+
+ assert [field.field for field in search_query.fields] == fields
+
+ @pytest.mark.parametrize(
+ ("subquery", "is_absolute"),
+ zip(delete_query_test_params, delete_query_test_results),
+ )
+ def test_delete_query(self, subquery: str, is_absolute: bool) -> None:
+ """Tests the file query parser with delete queries."""
+
+ query: list[str] = tools.parse_query(subquery)
+ parser = FileQueryParser(query, "delete")
+
+ examine_delete_query(parser, is_absolute)
+
+
+class TestFileDataQueryParser:
+ """Tests the FileDataQueryParser class."""
+
+ search_query_test_params = [
+ "* FROM .",
+ "name, path, dataline FROM ABSOLUTE '.'",
+ "path, lineno, dataline FROM RELATIVE . WHERE type = '.py'",
+ "* FROM '.' WHERE lineno BETWEEN (0, 100)",
+ ]
+
+ search_query_with_field_aliases_test_params = [
+ "filename, data FROM .",
+ "filename, type, line FROM RELATIVE . WHERE type = '.py' AND line > 10",
+ "filename, filepath FROM ABSOLUTE . WHERE 'test' in data",
+ ]
+
+ search_query_test_results = [
+ [False, list(constants.DATA_FIELDS)],
+ [True, ["name", "path", "dataline"]],
+ [False, ["path", "lineno", "dataline"]],
+ [False, list(constants.DATA_FIELDS)],
+ ]
+
+ search_query_with_field_aliases_test_results = [
+ [False, ["name", "dataline"], ["filename", "data"]],
+ [False, ["name", "filetype", "dataline"], ["filename", "type", "line"]],
+ [True, ["name", "path"], ["filename", "filepath"]],
+ ]
+
+ @pytest.mark.parametrize(
+ ("subquery", "results"),
+ zip(search_query_test_params, search_query_test_results),
+ )
+ def test_search_query(self, subquery: str, results: list[Any]) -> None:
+ """
+ Tests the file data query parser with search queries.
+ """
+
+ query: list[str] = tools.parse_query(subquery)
+ parser = FileDataQueryParser(query)
+
+ search_query: SearchQuery = examine_search_query(parser, results)
+ fields: list[str] = results[1]
+
+ assert [field.field for field in search_query.fields] == fields
+
+ @pytest.mark.parametrize(
+ ("subquery", "results"),
+ zip(
+ search_query_with_field_aliases_test_params,
+ search_query_with_field_aliases_test_results,
+ ),
+ )
+ def test_search_query(self, subquery: str, results: list[Any]) -> None:
+ """
+ Tests the file data query parser with search queries comprising field aliases.
+ """
+
+ query: list[str] = tools.parse_query(subquery)
+ parser = FileDataQueryParser(query)
+
+ search_query: SearchQuery = examine_search_query(parser, results)
+ fields: list[str] = results[1]
+
+ assert [field.field for field in search_query.fields] == fields
+
+
+class TestDirectoryQueryParser:
+ """Tests the DirectoryQueryParser class"""
+
+ search_query_test_params = [
+ "* FROM .",
+ "name, path, parent FROM ABSOLUTE '.'",
+ "access_time,modify_time from RELATIVE . WHERE name in ('docs', 'documents')",
+ "name, path,access_time FROM . WHERE atime >= '2023-04-04' OR ctime >= '2023-12-04'",
+ "* FROM ABSOLUTE '.' WHERE atime >= '2024-02-20'",
+ ]
+
+ search_query_with_field_aliases_test_params = [
+ "ctime,atime FROM . WHERE atime <= '2023-01-01' AND ctime >= '2006-02-20'",
+ "mtime, ctime FROM RELATIVE . WHERE mtime BETWEEN ('2021-03-12', '2021-04-12')",
+ ]
+
+ delete_query_test_params = [
+ "FROM ABSOLUTE .",
+ "FROM . WHERE name IN ('reports', 'media') AND 'fise' in parent",
+ "FROM RELATIVE . WHERE atime <= '2012-02-17' OR ctime <= '2015-03-23'",
+ ]
+
+ search_query_test_results = [
+ [False, list(constants.DIR_FIELDS)],
+ [True, ["name", "path", "parent"]],
+ [False, ["access_time", "modify_time"]],
+ [False, ["name", "path", "access_time"]],
+ [True, list(constants.DIR_FIELDS)],
+ ]
+
+ search_query_with_field_aliases_test_results = [
+ [False, ["create_time", "access_time"], ["ctime", "atime"]],
+ [False, ["modify_time", "create_time"], ["mtime", "ctime"]],
+ ]
+
+ delete_query_test_results = [True, False, False]
+
+ @pytest.mark.parametrize(
+ ("subquery", "results"),
+ zip(search_query_test_params, search_query_test_results),
+ )
+ def test_search_query(self, subquery: str, results: list[Any]) -> None:
+ """Tests the directory query parser with search queries."""
+
+ query: list[str] = tools.parse_query(subquery)
+ parser = DirectoryQueryParser(query, "search")
+
+ search_query: SearchQuery = examine_search_query(parser, results)
+ fields: list[str] = results[1]
+
+ assert [field.field for field in search_query.fields] == fields
+
+ @pytest.mark.parametrize(
+ ("subquery", "results"),
+ zip(
+ search_query_with_field_aliases_test_params,
+ search_query_with_field_aliases_test_results,
+ ),
+ )
+ def test_search_query_with_field_aliases(
+ self, subquery: str, results: list[Any]
+ ) -> None:
+ """
+ Tests the file query parser with search queries comprising field aliases.
+ """
+
+ query: list[str] = tools.parse_query(subquery)
+ parser = FileQueryParser(query, "search")
+
+ search_query: SearchQuery = examine_search_query(parser, results)
+ fields: list[str] = results[1]
+
+ assert [field.field for field in search_query.fields] == fields
+
+ @pytest.mark.parametrize(
+ ("subquery", "is_absolute"),
+ zip(delete_query_test_params, delete_query_test_results),
+ )
+ def test_delete_query(self, subquery: str, is_absolute: bool) -> None:
+ """Tests the file query parser with delete queries."""
+
+ query: list[str] = tools.parse_query(subquery)
+ parser = FileQueryParser(query, "delete")
+
+ examine_delete_query(parser, is_absolute)
diff --git a/tests/test_query/test_search_query.py b/tests/test_query/test_search_query.py
new file mode 100644
index 0000000..b7b0456
--- /dev/null
+++ b/tests/test_query/test_search_query.py
@@ -0,0 +1,200 @@
+"""
+This module comprises test cases for verifying
+the functionality of search queries in FiSE.
+"""
+
+# NOTE:
+# The tests defined within this module don't explicitly verify the extracted
+# search data as it is flexible and subject to change depending on the system
+# and path the tests are executed from.
+
+
+from pathlib import Path
+
+import pytest
+import pandas as pd
+
+from fise.common import constants
+from fise.query import QueryHandler
+
+TEST_DIRECTORY = Path(__file__).parents[1] / "test_directory"
+FILE_DIR_TEST_DIRECTORY = TEST_DIRECTORY / "file_dir"
+TEST_RECORDS_FILE = Path(__file__).parent / "test_search_query.hdf"
+
+
+def examine_search_query(query: str) -> None:
+ """
+ Tests the specified search query.
+
+ The extracted data is also verified with the test records stored at the specified path
+ in the `test_search_query.hdf` file if `verify` is explicitly set to `True`.
+ """
+
+ data: pd.DataFrame = QueryHandler(query).handle()
+ assert isinstance(data, pd.DataFrame)
+
+
+class TestFileSearchQuery:
+ """Tests the QueryHandler class with file search queries"""
+
+ basic_query_syntax_test_params = [
+ f"R SELECT * FROM '{FILE_DIR_TEST_DIRECTORY}'",
+ f"SELECT[TYPE FILE] name, filetype FROM '{FILE_DIR_TEST_DIRECTORY}'",
+ f"RECURSIVE SELECT * FROM '{FILE_DIR_TEST_DIRECTORY}' WHERE filetype = '.py'",
+ ]
+
+ recursive_command_test_params = [
+ f"R SELECT name, atime, mtime FROM '{FILE_DIR_TEST_DIRECTORY / 'docs'}'",
+ f"R SELECT name, filetype FROM '{FILE_DIR_TEST_DIRECTORY}' WHERE filetype = None",
+ f"RECURSIVE SELECT path, ctime FROM '{FILE_DIR_TEST_DIRECTORY / 'project'}'",
+ ]
+
+ mixed_case_query_test_params = [
+ f"r Select * FroM '{FILE_DIR_TEST_DIRECTORY}'",
+ f"sELect[TYPE FILE] Name, FileType From '{FILE_DIR_TEST_DIRECTORY}'",
+ f"RecURSive sELECt * From '{FILE_DIR_TEST_DIRECTORY}' wHErE FilETypE = '.py'",
+ ]
+
+ individual_fields_test_params = constants.FILE_FIELDS + ("*",)
+
+ path_types_test_params = [
+ f"SELECT name, filetype FROM ABSOLUTE '{FILE_DIR_TEST_DIRECTORY / 'project'}'",
+ f"R SELECT name, atime, ctime FROM RELATIVE '{FILE_DIR_TEST_DIRECTORY}'",
+ f"SELECT path, mtime FROM ABSOLUTE '{FILE_DIR_TEST_DIRECTORY}' WHERE type != None",
+ ]
+
+ query_conditions_test_params = [
+ f"R SELECT name FROM '{FILE_DIR_TEST_DIRECTORY}' WHERE "
+ "filetype = None AND name IN ('Dockerfile', 'LICENSE', 'TODO')",
+ f"SELECT atime FROM '{FILE_DIR_TEST_DIRECTORY}' WHERE name IN ('REAME.md', 'TODO')",
+ f"R SELECT path FROM '{FILE_DIR_TEST_DIRECTORY}' WHERE name = 'Q1' AND size[b] = 0",
+ ]
+
+ nested_query_conditions_test_params = [
+ f"SELECT name, filetype FROM '{FILE_DIR_TEST_DIRECTORY}' WHERE "
+ "(filetype = '.txt' OR filetype = None) AND mtime > '2024-03-17'",
+ f"SELECT name, atime FROM '{FILE_DIR_TEST_DIRECTORY}' WHERE "
+ "NAME LIKE '.*' AND ((ctime > '2024-01-01'))",
+ f"R SELECT path, ctime FROM ABSOLUTE '{FILE_DIR_TEST_DIRECTORY / 'docs'}' "
+ "WHERE (filetype = '.md') OR atime >= '2024-02-25'",
+ ]
+
+ @pytest.mark.parametrize("query", basic_query_syntax_test_params)
+ def test_basic_query_syntax(self, query: str) -> None:
+ """Tests basic syntax for file search queries"""
+ examine_search_query(query)
+
+ @pytest.mark.parametrize("field", individual_fields_test_params)
+ def test_individual_fields(self, field: str) -> None:
+ """Tests file search queries with all fields individually"""
+
+ query: str = f"SELECT {field} FROM '{FILE_DIR_TEST_DIRECTORY}'"
+ examine_search_query(query)
+
+ @pytest.mark.parametrize("query", recursive_command_test_params)
+ def test_recursive_command(self, query: str) -> None:
+ """Tests file search queries with the recursive command"""
+ examine_search_query(query)
+
+ @pytest.mark.parametrize("query", path_types_test_params)
+ def test_path_types(self, query: str) -> None:
+ """Tests file search queries with different path types"""
+ examine_search_query(query)
+
+ @pytest.mark.parametrize("query", mixed_case_query_test_params)
+ def test_mixed_case_query(self, query: str) -> None:
+ """Tests file search queries comprising mixed case characters"""
+ examine_search_query(query)
+
+ @pytest.mark.parametrize("query", query_conditions_test_params)
+ def test_query_conditions(self, query: str) -> None:
+ """Tests file search query conditions"""
+ examine_search_query(query)
+
+ @pytest.mark.parametrize("query", nested_query_conditions_test_params)
+ def test_nested_query_conditions(self, query: str) -> None:
+ """Tests file nested search query conditions"""
+ examine_search_query(query)
+
+
+class TestDirSearchQuery:
+ """Tests the QueryHandler class with directory search queries"""
+
+ basic_query_syntax_test_params = [
+ f"R SELECT[TYPE DIR] * FROM '{FILE_DIR_TEST_DIRECTORY}'",
+ f"RECURSIVE SELECT[TYPE DIR] name, parent, ctime FROM '{FILE_DIR_TEST_DIRECTORY}'",
+ f"SELECT[TYPE DIR] * FROM '{FILE_DIR_TEST_DIRECTORY}' WHERE name IN ('orders', 'reports')",
+ ]
+
+ recursive_command_test_params = [
+ f"R SELECT[TYPE DIR] name, atime, ctime FROM '{FILE_DIR_TEST_DIRECTORY / 'docs'}'",
+ f"R SELECT[TYPE DIR] name FROM '{FILE_DIR_TEST_DIRECTORY}' WHERE atime > '2022-01-01'",
+ f"RECURSIVE SELECT[TYPE DIR] path, mtime FROM '{FILE_DIR_TEST_DIRECTORY / 'project'}'",
+ ]
+
+ mixed_case_query_test_params = [
+ f"r SELECT[Type DiR] * fROm '{FILE_DIR_TEST_DIRECTORY}'",
+ f"Recursive sEleCt[typE dIr] name, parent, ctime FroM '{FILE_DIR_TEST_DIRECTORY}'",
+ f"Select[TYPE DIR] * From '{FILE_DIR_TEST_DIRECTORY}' Where name In ('orders', 'reports')",
+ ]
+
+ individual_fields_test_params = constants.DIR_FIELDS + ("*",)
+
+ path_types_test_params = [
+ f"SELECT[TYPE DIR] path, atime FROM ABSOLUTE '{FILE_DIR_TEST_DIRECTORY}'",
+ f"R SELECT[TYPE DIR] name, ctime FROM RELATIVE '{FILE_DIR_TEST_DIRECTORY / 'docs'}'",
+ f"SELECT[TYPE DIR] name FROM ABSOLUTE '{FILE_DIR_TEST_DIRECTORY}'",
+ ]
+
+ query_conditions_test_params = [
+ f"SELECT[TYPE DIR] path, ctime FROM '{FILE_DIR_TEST_DIRECTORY}' WHERE "
+ "ctime >= '2022-02-14' AND name IN ('docs', 'reports', 'media')",
+ f"R SELECT[TYPE DIR] name, mtime FROM '{FILE_DIR_TEST_DIRECTORY}' WHERE name LIKE '.*'",
+ f"SELECT[TYPE DIR] name FROM '{FILE_DIR_TEST_DIRECTORY}' WHERE "
+ "atime < '2024-01-01' OR ctime >= '2024-01-01' AND ctime = mtime",
+ ]
+
+ nested_query_conditions_test_params = [
+ f"SELECT[TYPE DIR] name, atime FROM '{FILE_DIR_TEST_DIRECTORY / 'reports'}' "
+ "WHERE name LIKE 'report*' AND (ctime > '2024-02-01')",
+ f"SELECT[TYPE DIR] path FROM '{FILE_DIR_TEST_DIRECTORY}' WHERE ((atime >= '2023-05-28'))",
+ f"R SELECT[TYPE DIR] name, ctime, mtime FROM '{FILE_DIR_TEST_DIRECTORY}' "
+ "WHERE (ctime >= '2024-04-01' OR name IN ('src', 'media', 'docs'))",
+ ]
+
+ @pytest.mark.parametrize("query", basic_query_syntax_test_params)
+ def test_basic_query_syntax(self, query: str) -> None:
+ """Tests basic syntax for directory search queries"""
+ examine_search_query(query)
+
+ @pytest.mark.parametrize("field", individual_fields_test_params)
+ def test_individual_fields(self, field: str) -> None:
+ """Tests directory search queries with all fields individually"""
+
+ query: str = f"SELECT[TYPE DIR] {field} FROM '{FILE_DIR_TEST_DIRECTORY}'"
+ examine_search_query(query)
+
+ @pytest.mark.parametrize("query", recursive_command_test_params)
+ def test_recursive_command(self, query: str) -> None:
+ """Tests directory search queries with the recursive command"""
+ examine_search_query(query)
+
+ @pytest.mark.parametrize("query", path_types_test_params)
+ def test_path_types(self, query: str) -> None:
+ """Tests directory search queries with different path types"""
+ examine_search_query(query)
+
+ @pytest.mark.parametrize("query", mixed_case_query_test_params)
+ def test_mixed_case_query(self, query: str) -> None:
+ """Tests directory search queries comprising mixed case characters."""
+ examine_search_query(query)
+
+ @pytest.mark.parametrize("query", query_conditions_test_params)
+ def test_query_conditions(self, query: str) -> None:
+ """Tests directory search query conditions"""
+ examine_search_query(query)
+
+ @pytest.mark.parametrize("query", nested_query_conditions_test_params)
+ def test_nested_query_conditions(self, query: str) -> None:
+ """Tests directory nested search query conditions"""
+ examine_search_query(query)
diff --git a/tests/test_query/utils.py b/tests/test_query/utils.py
new file mode 100644
index 0000000..650bcb7
--- /dev/null
+++ b/tests/test_query/utils.py
@@ -0,0 +1,21 @@
+"""
+Utils.py
+--------
+
+This module comprises utility functions specifically designed
+to support test functions and methods defined within the project.
+"""
+
+from pathlib import Path
+
+import pandas as pd
+
+
+def read_hdf(file: Path, path: str) -> pd.DataFrame:
+ """
+ Reads the test records stored at the specified
+ path from the specified HDF5 file.
+ """
+
+ with pd.HDFStore(str(file)) as store:
+ return store[path]
diff --git a/tests/test_tools.hdf b/tests/test_tools.hdf
new file mode 100644
index 0000000..a924a6d
Binary files /dev/null and b/tests/test_tools.hdf differ
diff --git a/tests/test_tools.py b/tests/test_tools.py
new file mode 100644
index 0000000..fa177d8
--- /dev/null
+++ b/tests/test_tools.py
@@ -0,0 +1,142 @@
+"""
+This module comprises test cases for verifying the utility
+functions defined within the common/tools.py module in FiSE.
+"""
+
+from typing import Generator
+from pathlib import Path
+
+import pandas as pd
+import pytest
+
+from fise.common import tools
+
+TEST_DIRECTORY = Path(__file__).parent / "test_directory"
+FILE_DIR_TEST_DIRECTORY = TEST_DIRECTORY / "file_dir"
+TEST_RECORDS_FILE = Path(__file__).parent / "test_tools.hdf"
+
+PANDAS_READ_METHODS_MAP = {
+ ".csv": "read_csv", ".xlsx": "read_excel",
+ ".html": "read_html", ".json": "read_json",
+}
+
+# Test parameters for individual functions defined below
+
+PARSE_QUERY_TEST_PARAMS = [
+ r"SELECT[TYPE FILE] * FROM . WHERE name LIKE '^.*\.py$' AND ctime BETWEEN ('2020-01-01', '2022-01-01')",
+ "DELETE[TYPE DIR] FROM '/home/user/Projects 2020' WHERE 'temp' IN name",
+ "SELECT[TYPE DATA, MODE BYTES] lineno, dataline FROM ./fise/main.py WHERE '#TODO' IN dataline",
+ "SELECT path, size FROM . WHERE filetype='.docx' AND (ctime < '2022-01-01' OR ctime > '2023-12-31')",
+]
+
+GET_FILES_TEST_PARAMS = [
+ (1, FILE_DIR_TEST_DIRECTORY / "docs", True),
+ (2, FILE_DIR_TEST_DIRECTORY, False),
+ (3, FILE_DIR_TEST_DIRECTORY / "project", True),
+]
+
+GET_DIRS_TEST_PARAMS = [
+ (1, FILE_DIR_TEST_DIRECTORY / "docs", True),
+ (2, FILE_DIR_TEST_DIRECTORY, False),
+ (3, FILE_DIR_TEST_DIRECTORY / "reports", True),
+]
+
+EXPORT_FILE_TEST_PARAMS = ["export.csv", "output.xlsx", "records.html", "save.json"]
+
+# Sample dataframe for testing `tools.export_to_file` function.
+
+SAMPLE_EXPORT_FILE_DATA = pd.DataFrame(
+ {2023: [87, 95, 98, 82, 84], 2024: [91, 93, 98, 87, 81]}
+)
+
+# Test results for individual functions defined below
+
+PARSE_QUERY_TEST_RESULTS = [
+ [
+ "SELECT[TYPE FILE]", "*", "FROM", ".", "WHERE",
+ "name", "LIKE", r"'^.*\.py$'", "AND", "ctime",
+ "BETWEEN", "('2020-01-01', '2022-01-01')",
+ ],
+ [
+ "DELETE[TYPE DIR]", "FROM",
+ "'/home/user/Projects 2020'",
+ "WHERE", "'temp'", "IN", "name",
+ ],
+ [
+ "SELECT[TYPE DATA, MODE BYTES]", "lineno,",
+ "dataline", "FROM", "./fise/main.py", "WHERE",
+ "'#TODO'", "IN", "dataline",
+ ],
+ [
+ "SELECT", "path,", "size", "FROM", ".",
+ "WHERE", "filetype='.docx'", "AND",
+ "(ctime < '2022-01-01' OR ctime > '2023-12-31')",
+ ],
+]
+
+
+def read_test_tools_hdf_file(path: str) -> pd.Series | pd.DataFrame:
+ """
+ Reads test records stored at the specified path within `test_tools.hdf` file.
+ """
+ global TEST_RECORDS_FILE
+
+ with pd.HDFStore(str(TEST_RECORDS_FILE)) as store:
+ return store[path]
+
+
+def verify_paths(paths: Generator[Path, None, None], records: pd.Series) -> None:
+ """
+ Verifies whether all the specified paths are present in the specified records.
+ """
+ global FILE_DIR_TEST_DIRECTORY
+
+ records = records.apply(lambda path_: FILE_DIR_TEST_DIRECTORY / path_).values
+
+ for path in paths:
+ assert path in records
+
+
+@pytest.mark.parametrize(
+ ("query", "result"), zip(PARSE_QUERY_TEST_PARAMS, PARSE_QUERY_TEST_RESULTS)
+)
+def test_parse_query_function(query: str, result: list[str]) -> None:
+ """Tests the `tools.parse_query` function"""
+
+ parsed_query: list[str] = tools.parse_query(query)
+ assert parsed_query == result
+
+
+@pytest.mark.parametrize(("ctr", "path", "recur"), GET_FILES_TEST_PARAMS)
+def test_get_files_function(ctr: int, path: Path, recur: bool) -> None:
+ """Tests the `tools.get_files` function"""
+
+ verify_paths(
+ tools.get_files(path, recur),
+ read_test_tools_hdf_file(f"/function/get_files/test{ctr}"),
+ )
+
+
+@pytest.mark.parametrize(("ctr", "path", "recur"), GET_DIRS_TEST_PARAMS)
+def test_get_directories_function(ctr: int, path: Path, recur: bool) -> None:
+ """Tests the `tools.get_directories` function"""
+
+ verify_paths(
+ tools.get_directories(path, recur),
+ read_test_tools_hdf_file(f"/function/get_directories/test{ctr}"),
+ )
+
+
+@pytest.mark.parametrize("file", EXPORT_FILE_TEST_PARAMS)
+def test_file_export_function(
+ file: str, data: pd.DataFrame = SAMPLE_EXPORT_FILE_DATA
+) -> None:
+ """Tests the `tools.export_to_file` function with different file formats"""
+ global TEST_DIRECTORY
+
+ path: Path = TEST_DIRECTORY / file
+
+ tools.export_to_file(data, path)
+ assert path.is_file()
+
+ path.unlink()
diff --git a/tests/update_tests.py b/tests/update_tests.py
new file mode 100644
index 0000000..e69de29