From 76f476077dc5f65fe4f51e41b96228282490aa7b Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 6 Jan 2026 13:07:25 +0000
Subject: [PATCH 1/9] Initial plan
From 14ea9361dd341347ebf4bd6e22fc80d6f5719fd9 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 6 Jan 2026 13:11:33 +0000
Subject: [PATCH 2/9] Fix README image links and add configuration files
Co-authored-by: alwaysvivek <241595348+alwaysvivek@users.noreply.github.com>
---
.env.example | 18 ++++++++++++++++++
README.md | 12 ++++++------
requirements.txt | 2 ++
3 files changed, 26 insertions(+), 6 deletions(-)
create mode 100644 .env.example
diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..a2c256e
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,18 @@
+# Server Configuration
+PORT=8000
+DEBUG=True
+HOST=0.0.0.0
+
+# Database Configuration
+DB_PATH=~/.grepify_history.db
+MAX_HISTORY=200
+
+# Semantic Search Configuration
+SPACY_MODEL=en_core_web_md
+SEMANTIC_THRESHOLD=0.45
+SEMANTIC_TOP_N=10
+
+# Rate Limiting
+RATE_LIMIT_ENABLED=True
+RATE_LIMIT_REQUESTS=100
+RATE_LIMIT_WINDOW=60
diff --git a/README.md b/README.md
index a0b5b01..d6c6c2b 100644
--- a/README.md
+++ b/README.md
@@ -65,24 +65,24 @@
-
Regex Usage: Shows a complex Regex pattern matching date/time data in a log snippet.
-
+
-
Semantic Search Usage: Shows a Semantic search for "happy" returning "proud" with a similarity score.
-
+
-
Server Log: The server terminal output showing multiple successful POST /semantic calls.
-
+
-
History: The history table view, displaying matches, pattern, and timestamp columns.
-
+
-
Postman Output: Shows the Postman interface with the JSON history response from the /search endpoint.
-
+
@@ -248,6 +248,6 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
-[Report Bug](https://github.com/vivekkdagar/deepgrep/issues) · [Request Feature](https://github.com/vivekkdagar/deepgrep/issues)
+[Report Bug](https://github.com/alwaysvivek/deepgrep/issues) · [Request Feature](https://github.com/alwaysvivek/deepgrep/issues)
diff --git a/requirements.txt b/requirements.txt
index 6011bfd..70025f2 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -3,3 +3,5 @@ spacy
nltk
flask-cors
flask
+python-decouple
+flask-limiter
From 7dc5c05904834142d4d52203efecc8477cb57bb4 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 6 Jan 2026 13:13:55 +0000
Subject: [PATCH 3/9] Add logging, environment variables, and rate limiting
Co-authored-by: alwaysvivek <241595348+alwaysvivek@users.noreply.github.com>
---
deepgrep/core/history.py | 21 ++++++++++++--
deepgrep/core/semantic_engine.py | 16 +++++++++--
deepgrep/web/app.py | 47 ++++++++++++++++++++++++++++++--
3 files changed, 76 insertions(+), 8 deletions(-)
diff --git a/deepgrep/core/history.py b/deepgrep/core/history.py
index 0f26b9c..61ee88b 100644
--- a/deepgrep/core/history.py
+++ b/deepgrep/core/history.py
@@ -1,10 +1,16 @@
import sqlite3, json
+import logging
from datetime import datetime, timezone
from pathlib import Path
from typing import List, Tuple
+from decouple import config
-DB_PATH = Path.home() / ".grepify_history.db"
-MAX_HISTORY = 200
+# Configure logging
+logger = logging.getLogger(__name__)
+
+# Load configuration from environment variables
+DB_PATH = Path(config('DB_PATH', default='~/.grepify_history.db')).expanduser()
+MAX_HISTORY = config('MAX_HISTORY', default=200, cast=int)
class SearchHistoryDB:
"""SQLite-backed search history logger."""
@@ -14,6 +20,7 @@ def __init__(self, db_path: Path = DB_PATH):
self._init_db()
def _init_db(self):
+ logger.info(f"Initializing search history database at {self.db_path}")
with sqlite3.connect(self.db_path) as conn:
conn.execute("""
CREATE TABLE IF NOT EXISTS search_logs (
@@ -24,6 +31,7 @@ def _init_db(self):
files TEXT
);
""")
+ logger.info("Database initialized successfully")
def log_search(self, pattern: str, match_count: int, files: List[str]):
with sqlite3.connect(self.db_path) as conn:
@@ -31,12 +39,17 @@ def log_search(self, pattern: str, match_count: int, files: List[str]):
"INSERT INTO search_logs (pattern, timestamp, match_count, files) VALUES (?, ?, ?, ?)",
(pattern, datetime.now(timezone.utc).isoformat(), match_count, json.dumps(files)),
)
- conn.execute("""
+ # Cleanup old records
+ result = conn.execute("""
DELETE FROM search_logs WHERE id NOT IN (
SELECT id FROM search_logs ORDER BY id DESC LIMIT ?
);
""", (MAX_HISTORY,))
+ deleted_count = result.rowcount
conn.commit()
+ logger.info(f"Logged search: pattern='{pattern}', matches={match_count}, files={len(files)}")
+ if deleted_count > 0:
+ logger.debug(f"Cleaned up {deleted_count} old records")
def get_recent(self, limit: int = 5) -> List[Tuple]:
with sqlite3.connect(self.db_path) as conn:
@@ -68,6 +81,7 @@ def export_to_json(self, file_path: Path) -> int:
for r in rows
]
file_path.write_text(json.dumps(data, indent=2), encoding="utf-8")
+ logger.info(f"Exported {len(data)} search records to {file_path}")
return len(data)
def import_from_json(self, file_path: Path) -> int:
@@ -92,4 +106,5 @@ def import_from_json(self, file_path: Path) -> int:
);
""", (MAX_HISTORY,))
conn.commit()
+ logger.info(f"Imported {len(data)} search records from {file_path}")
return len(data)
\ No newline at end of file
diff --git a/deepgrep/core/semantic_engine.py b/deepgrep/core/semantic_engine.py
index 75cb87e..5d4937e 100644
--- a/deepgrep/core/semantic_engine.py
+++ b/deepgrep/core/semantic_engine.py
@@ -1,6 +1,12 @@
import spacy
from nltk.corpus import wordnet
from typing import List, Tuple
+from decouple import config
+
+# Load configuration from environment variables
+SPACY_MODEL = config('SPACY_MODEL', default='en_core_web_md')
+SEMANTIC_THRESHOLD = config('SEMANTIC_THRESHOLD', default=0.45, cast=float)
+SEMANTIC_TOP_N = config('SEMANTIC_TOP_N', default=10, cast=int)
def get_antonyms(word: str) -> set:
@@ -15,7 +21,9 @@ def get_antonyms(word: str) -> set:
class SemanticEngine:
"""Semantic search avoiding antonyms and irrelevant words."""
- def __init__(self, model_name: str = "en_core_web_md"):
+ def __init__(self, model_name: str = None):
+ if model_name is None:
+ model_name = SPACY_MODEL
try:
self.nlp = spacy.load(model_name)
except OSError:
@@ -23,7 +31,9 @@ def __init__(self, model_name: str = "en_core_web_md"):
f"SpaCy model '{model_name}' not found. Run: python -m spacy download {model_name}"
)
- def find_semantic_matches(self, text: str, keyword: str, top_n: int = 10) -> List[Tuple[str, float]]:
+ def find_semantic_matches(self, text: str, keyword: str, top_n: int = None) -> List[Tuple[str, float]]:
+ if top_n is None:
+ top_n = SEMANTIC_TOP_N
doc = self.nlp(text)
key_token = self.nlp(keyword)[0]
antonyms = get_antonyms(keyword.lower())
@@ -43,5 +53,5 @@ def find_semantic_matches(self, text: str, keyword: str, top_n: int = 10) -> Lis
# Sort descending and keep top N
results = sorted(results, key=lambda x: x[1], reverse=True)[:top_n]
# Filter out weak matches
- results = [(w, s) for w, s in results if s > 0.45]
+ results = [(w, s) for w, s in results if s > SEMANTIC_THRESHOLD]
return results
\ No newline at end of file
diff --git a/deepgrep/web/app.py b/deepgrep/web/app.py
index d05afa7..40f57fa 100644
--- a/deepgrep/web/app.py
+++ b/deepgrep/web/app.py
@@ -1,28 +1,62 @@
-# app.py (small changes to init components and avoid nltk download in runtime)
+# app.py - Production-ready Flask application with logging and rate limiting
+import logging
from flask import Flask, request, jsonify, render_template
from flask_cors import CORS
+from flask_limiter import Limiter
+from flask_limiter.util import get_remote_address
+from decouple import config
from deepgrep.core.engine import find_matches
from deepgrep.core.history import SearchHistoryDB
from deepgrep.core.semantic_engine import SemanticEngine
import threading
+# Configure logging
+logging.basicConfig(
+ level=logging.INFO,
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
+)
+logger = logging.getLogger(__name__)
+
+# Load configuration from environment variables
+PORT = config('PORT', default=8000, cast=int)
+DEBUG = config('DEBUG', default=True, cast=bool)
+HOST = config('HOST', default='0.0.0.0')
+RATE_LIMIT_ENABLED = config('RATE_LIMIT_ENABLED', default=True, cast=bool)
+RATE_LIMIT_REQUESTS = config('RATE_LIMIT_REQUESTS', default=100, cast=int)
+RATE_LIMIT_WINDOW = config('RATE_LIMIT_WINDOW', default=60, cast=int)
+
app = Flask(__name__, template_folder="templates", static_folder="static")
CORS(app)
+# Initialize rate limiter
+limiter = Limiter(
+ app=app,
+ key_func=get_remote_address,
+ default_limits=[f"200 per minute"],
+ enabled=RATE_LIMIT_ENABLED
+)
+
# Initialize engines once (heavy models loaded at import time)
+logger.info("Initializing semantic engine and history database...")
semantic_engine = SemanticEngine()
history_db = SearchHistoryDB()
+logger.info("Application initialization complete")
@app.route("/")
def home():
+ logger.info("GET / - Home page requested")
return render_template("index.html")
@app.route("/search", methods=["POST"])
+@limiter.limit(f"{RATE_LIMIT_REQUESTS}/minute")
def search_regex():
+ logger.info("POST /search - Regex search request received")
data = request.get_json()
pattern = data.get("pattern")
text = data.get("text")
+
if not pattern or not text:
+ logger.warning("POST /search - Missing pattern or text in request")
return jsonify({"error": "Missing pattern or text"}), 400
matches = []
@@ -32,6 +66,8 @@ def search_regex():
history_db.log_search(pattern, len(matches), ["web_input"])
all_history = history_db.list_all(limit=50)
+
+ logger.info(f"POST /search - Successfully found {len(matches)} matches for pattern '{pattern}'")
return jsonify({
"matches": matches,
@@ -41,16 +77,22 @@ def search_regex():
})
@app.route("/semantic", methods=["POST"])
+@limiter.limit(f"{RATE_LIMIT_REQUESTS}/minute")
def search_semantic():
+ logger.info("POST /semantic - Semantic search request received")
data = request.get_json()
keyword = data.get("keyword")
text = data.get("text")
+
if not keyword or not text:
+ logger.warning("POST /semantic - Missing keyword or text in request")
return jsonify({"error": "Missing keyword or text"}), 400
matches = semantic_engine.find_semantic_matches(text, keyword)
history_db.log_search(keyword, len(matches), ["web_input"])
all_history = history_db.list_all(limit=50)
+
+ logger.info(f"POST /semantic - Successfully found {len(matches)} semantic matches for keyword '{keyword}'")
return jsonify({
"matches": [{"word": w, "similarity": round(s, 3)} for w, s in matches],
@@ -63,4 +105,5 @@ def search_semantic():
# Remove in-production NLTK downloads; expect preinstalled corpora
# import nltk
# nltk.download('wordnet')
- app.run(port=8000, debug=True)
\ No newline at end of file
+ logger.info(f"Starting Flask server on {HOST}:{PORT} (debug={DEBUG})")
+ app.run(host=HOST, port=PORT, debug=DEBUG)
\ No newline at end of file
From 0edbf3ad29b1cb1a9327ff77c0a99f5b343d5217 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 6 Jan 2026 13:15:35 +0000
Subject: [PATCH 4/9] Add comprehensive API tests with 17 test cases
Co-authored-by: alwaysvivek <241595348+alwaysvivek@users.noreply.github.com>
---
tests/test_api.py | 282 ++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 282 insertions(+)
create mode 100644 tests/test_api.py
diff --git a/tests/test_api.py b/tests/test_api.py
new file mode 100644
index 0000000..3e2ad59
--- /dev/null
+++ b/tests/test_api.py
@@ -0,0 +1,282 @@
+"""
+Comprehensive API tests for DeepGrep web application.
+Tests cover regex search, semantic search, and home route endpoints.
+"""
+import pytest
+import json
+from deepgrep.web.app import app
+from unittest.mock import patch, MagicMock
+
+
+@pytest.fixture
+def client():
+ """Create a test client for the Flask app."""
+ app.config['TESTING'] = True
+ # Disable rate limiting for tests
+ app.config['RATELIMIT_ENABLED'] = False
+ with app.test_client() as client:
+ yield client
+
+
+class TestHomeRoute:
+ """Tests for the home route."""
+
+ def test_home_returns_200(self, client):
+ """Test that the home route returns status 200."""
+ response = client.get('/')
+ assert response.status_code == 200
+
+ def test_home_returns_html(self, client):
+ """Test that the home route returns HTML content."""
+ response = client.get('/')
+ assert b'html' in response.data.lower() or response.content_type == 'text/html; charset=utf-8'
+
+
+class TestRegexSearchEndpoint:
+ """Tests for the /search endpoint (regex matching)."""
+
+ def test_regex_search_with_valid_pattern(self, client):
+ """Test regex search with a valid pattern that matches."""
+ data = {
+ "pattern": r"\d+",
+ "text": "User logged in at 14:32, error code 404"
+ }
+ response = client.post('/search',
+ data=json.dumps(data),
+ content_type='application/json')
+
+ assert response.status_code == 200
+ json_data = response.get_json()
+ assert 'matches' in json_data
+ assert 'history' in json_data
+ assert isinstance(json_data['matches'], list)
+ assert len(json_data['matches']) > 0
+ assert '14' in json_data['matches'] or '32' in json_data['matches']
+
+ def test_regex_search_empty_pattern(self, client):
+ """Test that empty pattern returns 400 error."""
+ data = {
+ "pattern": "",
+ "text": "Some text here"
+ }
+ response = client.post('/search',
+ data=json.dumps(data),
+ content_type='application/json')
+
+ assert response.status_code == 400
+ json_data = response.get_json()
+ assert 'error' in json_data
+
+ def test_regex_search_empty_text(self, client):
+ """Test that empty text returns 400 error."""
+ data = {
+ "pattern": r"\d+",
+ "text": ""
+ }
+ response = client.post('/search',
+ data=json.dumps(data),
+ content_type='application/json')
+
+ assert response.status_code == 400
+ json_data = response.get_json()
+ assert 'error' in json_data
+
+ def test_regex_search_missing_pattern(self, client):
+ """Test that missing pattern field returns 400 error."""
+ data = {
+ "text": "Some text here"
+ }
+ response = client.post('/search',
+ data=json.dumps(data),
+ content_type='application/json')
+
+ assert response.status_code == 400
+
+ def test_regex_search_missing_text(self, client):
+ """Test that missing text field returns 400 error."""
+ data = {
+ "pattern": r"\d+"
+ }
+ response = client.post('/search',
+ data=json.dumps(data),
+ content_type='application/json')
+
+ assert response.status_code == 400
+
+ def test_regex_search_complex_email_pattern(self, client):
+ """Test complex regex pattern for email matching."""
+ data = {
+ "pattern": r"\w+@\w+\.\w+",
+ "text": "Contact us at support@example.com or admin@test.org"
+ }
+ response = client.post('/search',
+ data=json.dumps(data),
+ content_type='application/json')
+
+ assert response.status_code == 200
+ json_data = response.get_json()
+ assert 'matches' in json_data
+ assert len(json_data['matches']) > 0
+
+ def test_regex_search_date_pattern(self, client):
+ """Test complex regex pattern for date matching."""
+ data = {
+ "pattern": r"\d{4}-\d{2}-\d{2}",
+ "text": "Event dates: 2024-01-15 and 2024-12-31"
+ }
+ response = client.post('/search',
+ data=json.dumps(data),
+ content_type='application/json')
+
+ assert response.status_code == 200
+ json_data = response.get_json()
+ assert 'matches' in json_data
+ assert len(json_data['matches']) == 2
+
+ def test_regex_search_response_structure(self, client):
+ """Test that response has correct structure with matches and history arrays."""
+ data = {
+ "pattern": r"\w+",
+ "text": "hello world"
+ }
+ response = client.post('/search',
+ data=json.dumps(data),
+ content_type='application/json')
+
+ assert response.status_code == 200
+ json_data = response.get_json()
+ assert 'matches' in json_data
+ assert 'history' in json_data
+ assert isinstance(json_data['matches'], list)
+ assert isinstance(json_data['history'], list)
+
+
+class TestSemanticSearchEndpoint:
+ """Tests for the /semantic endpoint."""
+
+ @patch('deepgrep.web.app.semantic_engine')
+ def test_semantic_search_with_valid_keyword(self, mock_engine, client):
+ """Test semantic search with a valid keyword."""
+ # Mock the semantic engine to return predictable results
+ mock_engine.find_semantic_matches.return_value = [
+ ("joyful", 0.88),
+ ("delighted", 0.82)
+ ]
+
+ data = {
+ "keyword": "happy",
+ "text": "She felt joyful and delighted after the announcement"
+ }
+ response = client.post('/semantic',
+ data=json.dumps(data),
+ content_type='application/json')
+
+ assert response.status_code == 200
+ json_data = response.get_json()
+ assert 'matches' in json_data
+ assert 'history' in json_data
+ assert isinstance(json_data['matches'], list)
+ assert len(json_data['matches']) == 2
+
+ # Verify match structure
+ first_match = json_data['matches'][0]
+ assert 'word' in first_match
+ assert 'similarity' in first_match
+
+ def test_semantic_search_missing_keyword(self, client):
+ """Test that missing keyword returns 400 error."""
+ data = {
+ "text": "Some text here"
+ }
+ response = client.post('/semantic',
+ data=json.dumps(data),
+ content_type='application/json')
+
+ assert response.status_code == 400
+ json_data = response.get_json()
+ assert 'error' in json_data
+
+ def test_semantic_search_empty_keyword(self, client):
+ """Test that empty keyword returns 400 error."""
+ data = {
+ "keyword": "",
+ "text": "Some text here"
+ }
+ response = client.post('/semantic',
+ data=json.dumps(data),
+ content_type='application/json')
+
+ assert response.status_code == 400
+
+ def test_semantic_search_missing_text(self, client):
+ """Test that missing text returns 400 error."""
+ data = {
+ "keyword": "happy"
+ }
+ response = client.post('/semantic',
+ data=json.dumps(data),
+ content_type='application/json')
+
+ assert response.status_code == 400
+
+ def test_semantic_search_empty_text(self, client):
+ """Test that empty text returns 400 error."""
+ data = {
+ "keyword": "happy",
+ "text": ""
+ }
+ response = client.post('/semantic',
+ data=json.dumps(data),
+ content_type='application/json')
+
+ assert response.status_code == 400
+
+ @patch('deepgrep.web.app.semantic_engine')
+ def test_semantic_search_similarity_scores_present(self, mock_engine, client):
+ """Test that similarity scores are present in the response."""
+ mock_engine.find_semantic_matches.return_value = [
+ ("happy", 0.95),
+ ("glad", 0.78)
+ ]
+
+ data = {
+ "keyword": "joyful",
+ "text": "I feel happy and glad today"
+ }
+ response = client.post('/semantic',
+ data=json.dumps(data),
+ content_type='application/json')
+
+ assert response.status_code == 200
+ json_data = response.get_json()
+
+ for match in json_data['matches']:
+ assert 'similarity' in match
+ assert isinstance(match['similarity'], (int, float))
+ assert 0 <= match['similarity'] <= 1
+
+ @patch('deepgrep.web.app.semantic_engine')
+ def test_semantic_search_response_structure(self, mock_engine, client):
+ """Test that response includes matches with word and similarity fields."""
+ mock_engine.find_semantic_matches.return_value = [
+ ("excited", 0.85)
+ ]
+
+ data = {
+ "keyword": "happy",
+ "text": "I am excited about the news"
+ }
+ response = client.post('/semantic',
+ data=json.dumps(data),
+ content_type='application/json')
+
+ assert response.status_code == 200
+ json_data = response.get_json()
+ assert 'matches' in json_data
+ assert 'history' in json_data
+ assert isinstance(json_data['matches'], list)
+
+ if len(json_data['matches']) > 0:
+ match = json_data['matches'][0]
+ assert 'word' in match
+ assert 'similarity' in match
From 191f7e149e13eed5520d76d5517d9d56f5b16f59 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 6 Jan 2026 13:16:17 +0000
Subject: [PATCH 5/9] Add GitHub Actions CI pipeline
Co-authored-by: alwaysvivek <241595348+alwaysvivek@users.noreply.github.com>
---
.github/workflows/test.yml | 38 ++++++++++++++++++++++++++++++++++++++
1 file changed, 38 insertions(+)
create mode 100644 .github/workflows/test.yml
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 0000000..33765ca
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,38 @@
+name: Run Tests
+
+on:
+ push:
+ branches: [ main, develop ]
+ pull_request:
+ branches: [ main ]
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+
+ strategy:
+ matrix:
+ python-version: ['3.10', '3.11', '3.12']
+
+ steps:
+ - uses: actions/checkout@v3
+
+ - name: Set up Python ${{ matrix.python-version }}
+ uses: actions/setup-python@v4
+ with:
+ python-version: ${{ matrix.python-version }}
+
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ pip install -r requirements.txt
+ python -m spacy download en_core_web_md
+ python -m nltk.downloader wordnet
+
+ - name: Run tests
+ run: |
+ pytest tests/ -v --tb=short
+
+ - name: Check code structure
+ run: |
+ python -c "from deepgrep.web.app import app; print('✅ App imports successfully')"
From fa51ec6b48dfe8527729e121d78346f47b80125c Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 6 Jan 2026 13:19:10 +0000
Subject: [PATCH 6/9] Address code review feedback - remove unused imports
Co-authored-by: alwaysvivek <241595348+alwaysvivek@users.noreply.github.com>
---
deepgrep/web/app.py | 2 --
tests/test_api.py | 2 +-
2 files changed, 1 insertion(+), 3 deletions(-)
diff --git a/deepgrep/web/app.py b/deepgrep/web/app.py
index 40f57fa..aae54f6 100644
--- a/deepgrep/web/app.py
+++ b/deepgrep/web/app.py
@@ -8,7 +8,6 @@
from deepgrep.core.engine import find_matches
from deepgrep.core.history import SearchHistoryDB
from deepgrep.core.semantic_engine import SemanticEngine
-import threading
# Configure logging
logging.basicConfig(
@@ -23,7 +22,6 @@
HOST = config('HOST', default='0.0.0.0')
RATE_LIMIT_ENABLED = config('RATE_LIMIT_ENABLED', default=True, cast=bool)
RATE_LIMIT_REQUESTS = config('RATE_LIMIT_REQUESTS', default=100, cast=int)
-RATE_LIMIT_WINDOW = config('RATE_LIMIT_WINDOW', default=60, cast=int)
app = Flask(__name__, template_folder="templates", static_folder="static")
CORS(app)
diff --git a/tests/test_api.py b/tests/test_api.py
index 3e2ad59..ad215b3 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -5,7 +5,7 @@
import pytest
import json
from deepgrep.web.app import app
-from unittest.mock import patch, MagicMock
+from unittest.mock import patch
@pytest.fixture
From e687e70201136e051fe439f1029356209efab776 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 6 Jan 2026 13:21:04 +0000
Subject: [PATCH 7/9] Fix security issue - add explicit permissions to GitHub
workflow
Co-authored-by: alwaysvivek <241595348+alwaysvivek@users.noreply.github.com>
---
.github/workflows/test.yml | 3 +++
1 file changed, 3 insertions(+)
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 33765ca..1bbf596 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -10,6 +10,9 @@ jobs:
test:
runs-on: ubuntu-latest
+ permissions:
+ contents: read
+
strategy:
matrix:
python-version: ['3.10', '3.11', '3.12']
From 3d2bce74c9b6a3dcc5b2eafafdc8450f7747eb96 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 6 Jan 2026 13:31:08 +0000
Subject: [PATCH 8/9] Fix CI test import error - implement lazy engine
initialization
Co-authored-by: alwaysvivek <241595348+alwaysvivek@users.noreply.github.com>
---
deepgrep/web/app.py | 26 +++++++++++++++++++++-----
tests/test_api.py | 10 ++++++++++
2 files changed, 31 insertions(+), 5 deletions(-)
diff --git a/deepgrep/web/app.py b/deepgrep/web/app.py
index aae54f6..5f30689 100644
--- a/deepgrep/web/app.py
+++ b/deepgrep/web/app.py
@@ -34,11 +34,18 @@
enabled=RATE_LIMIT_ENABLED
)
-# Initialize engines once (heavy models loaded at import time)
-logger.info("Initializing semantic engine and history database...")
-semantic_engine = SemanticEngine()
-history_db = SearchHistoryDB()
-logger.info("Application initialization complete")
+# Initialize engines (lazy initialization to support testing)
+semantic_engine = None
+history_db = None
+
+def init_engines():
+ """Initialize semantic engine and history database (called lazily on first use)."""
+ global semantic_engine, history_db
+ if semantic_engine is None:
+ logger.info("Initializing semantic engine and history database...")
+ semantic_engine = SemanticEngine()
+ history_db = SearchHistoryDB()
+ logger.info("Application initialization complete")
@app.route("/")
def home():
@@ -48,6 +55,9 @@ def home():
@app.route("/search", methods=["POST"])
@limiter.limit(f"{RATE_LIMIT_REQUESTS}/minute")
def search_regex():
+ # Only initialize history_db, not semantic_engine (not needed for regex)
+ if history_db is None:
+ init_engines()
logger.info("POST /search - Regex search request received")
data = request.get_json()
pattern = data.get("pattern")
@@ -82,9 +92,14 @@ def search_semantic():
keyword = data.get("keyword")
text = data.get("text")
+ # Validate input first, before initializing engines
if not keyword or not text:
logger.warning("POST /semantic - Missing keyword or text in request")
return jsonify({"error": "Missing keyword or text"}), 400
+
+ # Only initialize engines after validation passes
+ if semantic_engine is None or history_db is None:
+ init_engines()
matches = semantic_engine.find_semantic_matches(text, keyword)
history_db.log_search(keyword, len(matches), ["web_input"])
@@ -103,5 +118,6 @@ def search_semantic():
# Remove in-production NLTK downloads; expect preinstalled corpora
# import nltk
# nltk.download('wordnet')
+ init_engines() # Ensure engines are initialized when running directly
logger.info(f"Starting Flask server on {HOST}:{PORT} (debug={DEBUG})")
app.run(host=HOST, port=PORT, debug=DEBUG)
\ No newline at end of file
diff --git a/tests/test_api.py b/tests/test_api.py
index ad215b3..98ce21e 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -11,9 +11,19 @@
@pytest.fixture
def client():
"""Create a test client for the Flask app."""
+ from unittest.mock import MagicMock
+ import deepgrep.web.app as app_module
+
app.config['TESTING'] = True
# Disable rate limiting for tests
app.config['RATELIMIT_ENABLED'] = False
+
+ # Mock history_db to avoid database operations during tests
+ mock_history_db = MagicMock()
+ mock_history_db.log_search.return_value = None
+ mock_history_db.list_all.return_value = []
+ app_module.history_db = mock_history_db
+
with app.test_client() as client:
yield client
From 495c257e5be021c10042346896d044058e36ef41 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 6 Jan 2026 13:36:34 +0000
Subject: [PATCH 9/9] Remove GitHub Actions CI workflow - keep tests for local
execution only
Co-authored-by: alwaysvivek <241595348+alwaysvivek@users.noreply.github.com>
---
.github/workflows/test.yml | 41 --------------------------------------
1 file changed, 41 deletions(-)
delete mode 100644 .github/workflows/test.yml
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
deleted file mode 100644
index 1bbf596..0000000
--- a/.github/workflows/test.yml
+++ /dev/null
@@ -1,41 +0,0 @@
-name: Run Tests
-
-on:
- push:
- branches: [ main, develop ]
- pull_request:
- branches: [ main ]
-
-jobs:
- test:
- runs-on: ubuntu-latest
-
- permissions:
- contents: read
-
- strategy:
- matrix:
- python-version: ['3.10', '3.11', '3.12']
-
- steps:
- - uses: actions/checkout@v3
-
- - name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v4
- with:
- python-version: ${{ matrix.python-version }}
-
- - name: Install dependencies
- run: |
- python -m pip install --upgrade pip
- pip install -r requirements.txt
- python -m spacy download en_core_web_md
- python -m nltk.downloader wordnet
-
- - name: Run tests
- run: |
- pytest tests/ -v --tb=short
-
- - name: Check code structure
- run: |
- python -c "from deepgrep.web.app import app; print('✅ App imports successfully')"