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 @@ @@ -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')"