pygarble 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pygarble/__init__.py ADDED
@@ -0,0 +1,7 @@
1
+ __version__ = "0.1.0"
2
+ __author__ = "Your Name"
3
+ __email__ = "your.email@example.com"
4
+
5
+ from .core import GarbleDetector, Strategy
6
+
7
+ __all__ = ["GarbleDetector", "Strategy"]
pygarble/core.py ADDED
@@ -0,0 +1,92 @@
1
+ import concurrent.futures
2
+ from enum import Enum
3
+ from typing import List, Union
4
+
5
+ from .strategies import (
6
+ CharacterFrequencyStrategy,
7
+ EntropyBasedStrategy,
8
+ LanguageDetectionStrategy,
9
+ PatternMatchingStrategy,
10
+ StatisticalAnalysisStrategy,
11
+ WordLengthStrategy,
12
+ )
13
+
14
+
15
+ class Strategy(Enum):
16
+ CHARACTER_FREQUENCY = "character_frequency"
17
+ WORD_LENGTH = "word_length"
18
+ PATTERN_MATCHING = "pattern_matching"
19
+ STATISTICAL_ANALYSIS = "statistical_analysis"
20
+ ENTROPY_BASED = "entropy_based"
21
+ LANGUAGE_DETECTION = "language_detection"
22
+
23
+
24
+ class GarbleDetector:
25
+ def __init__(
26
+ self,
27
+ strategy: Strategy,
28
+ threshold: float = 0.5,
29
+ threads: int = None,
30
+ **kwargs,
31
+ ):
32
+ self.strategy = strategy
33
+ self.threshold = threshold
34
+ self.threads = threads
35
+ self.kwargs = kwargs
36
+ self._strategy_instance = self._create_strategy_instance()
37
+
38
+ def _create_strategy_instance(self):
39
+ strategy_map = {
40
+ Strategy.CHARACTER_FREQUENCY: CharacterFrequencyStrategy,
41
+ Strategy.WORD_LENGTH: WordLengthStrategy,
42
+ Strategy.PATTERN_MATCHING: PatternMatchingStrategy,
43
+ Strategy.STATISTICAL_ANALYSIS: StatisticalAnalysisStrategy,
44
+ Strategy.ENTROPY_BASED: EntropyBasedStrategy,
45
+ Strategy.LANGUAGE_DETECTION: LanguageDetectionStrategy,
46
+ }
47
+
48
+ if self.strategy not in strategy_map:
49
+ raise NotImplementedError(
50
+ f"Strategy {self.strategy.value} is not implemented"
51
+ )
52
+
53
+ strategy_class = strategy_map[self.strategy]
54
+ return strategy_class(**self.kwargs)
55
+
56
+ def _process_text_proba(self, text: str) -> float:
57
+ return self._strategy_instance.predict_proba(text)
58
+
59
+ def _process_text_predict(self, text: str) -> bool:
60
+ proba = self._strategy_instance.predict_proba(text)
61
+ return proba >= self.threshold
62
+
63
+ def _process_batch_threaded(self, texts: List[str], process_func) -> List:
64
+ if self.threads is None or self.threads <= 1 or len(texts) < 10:
65
+ return [process_func(text) for text in texts]
66
+
67
+ max_workers = min(self.threads, len(texts))
68
+
69
+ with concurrent.futures.ThreadPoolExecutor(
70
+ max_workers=max_workers
71
+ ) as executor:
72
+ futures = [executor.submit(process_func, text) for text in texts]
73
+ return [future.result() for future in futures]
74
+
75
+ def predict(self, X: Union[str, List[str]]) -> Union[bool, List[bool]]:
76
+ if isinstance(X, str):
77
+ proba = self._strategy_instance.predict_proba(X)
78
+ return proba >= self.threshold
79
+ elif isinstance(X, list):
80
+ return self._process_batch_threaded(X, self._process_text_predict)
81
+ else:
82
+ raise ValueError("Input must be a string or list of strings")
83
+
84
+ def predict_proba(
85
+ self, X: Union[str, List[str]]
86
+ ) -> Union[float, List[float]]:
87
+ if isinstance(X, str):
88
+ return self._strategy_instance.predict_proba(X)
89
+ elif isinstance(X, list):
90
+ return self._process_batch_threaded(X, self._process_text_proba)
91
+ else:
92
+ raise ValueError("Input must be a string or list of strings")
@@ -0,0 +1,17 @@
1
+ from .base import BaseStrategy
2
+ from .character_frequency import CharacterFrequencyStrategy
3
+ from .entropy_based import EntropyBasedStrategy
4
+ from .language_detection import LanguageDetectionStrategy
5
+ from .pattern_matching import PatternMatchingStrategy
6
+ from .statistical_analysis import StatisticalAnalysisStrategy
7
+ from .word_length import WordLengthStrategy
8
+
9
+ __all__ = [
10
+ "BaseStrategy",
11
+ "CharacterFrequencyStrategy",
12
+ "WordLengthStrategy",
13
+ "PatternMatchingStrategy",
14
+ "StatisticalAnalysisStrategy",
15
+ "EntropyBasedStrategy",
16
+ "LanguageDetectionStrategy",
17
+ ]
@@ -0,0 +1,37 @@
1
+ import re
2
+ from abc import ABC, abstractmethod
3
+
4
+
5
+ class BaseStrategy(ABC):
6
+ def __init__(self, **kwargs):
7
+ self.kwargs = kwargs
8
+
9
+ def predict(self, text: str) -> bool:
10
+ if not text or not text.strip():
11
+ return False
12
+
13
+ if self._is_extremely_long_string(text):
14
+ return True
15
+
16
+ return self._predict_impl(text)
17
+
18
+ def predict_proba(self, text: str) -> float:
19
+ if not text or not text.strip():
20
+ return 0.0
21
+
22
+ if self._is_extremely_long_string(text):
23
+ return 1.0
24
+
25
+ return self._predict_proba_impl(text)
26
+
27
+ def _is_extremely_long_string(self, text: str) -> bool:
28
+ max_length = self.kwargs.get("max_string_length", 1000)
29
+ return len(text) > max_length and not re.search(r"\s", text)
30
+
31
+ @abstractmethod
32
+ def _predict_impl(self, text: str) -> bool:
33
+ pass
34
+
35
+ @abstractmethod
36
+ def _predict_proba_impl(self, text: str) -> float:
37
+ pass
@@ -0,0 +1,33 @@
1
+ from .base import BaseStrategy
2
+
3
+
4
+ class CharacterFrequencyStrategy(BaseStrategy):
5
+ def _get_char_counts(self, text: str):
6
+ char_counts = {}
7
+ for char in text.lower():
8
+ if char.isalpha():
9
+ char_counts[char] = char_counts.get(char, 0) + 1
10
+ return char_counts
11
+
12
+ def _predict_impl(self, text: str) -> bool:
13
+ char_counts = self._get_char_counts(text)
14
+ total_chars = sum(char_counts.values())
15
+ if total_chars == 0:
16
+ return False
17
+
18
+ threshold = self.kwargs.get("frequency_threshold", 0.1)
19
+ for count in char_counts.values():
20
+ if count / total_chars > threshold:
21
+ return True
22
+ return False
23
+
24
+ def _predict_proba_impl(self, text: str) -> float:
25
+ char_counts = self._get_char_counts(text)
26
+ total_chars = sum(char_counts.values())
27
+ if total_chars == 0:
28
+ return 0.0
29
+
30
+ max_frequency = (
31
+ max(char_counts.values()) / total_chars if char_counts else 0.0
32
+ )
33
+ return min(max_frequency, 1.0)
@@ -0,0 +1,47 @@
1
+ import math
2
+
3
+ from .base import BaseStrategy
4
+
5
+
6
+ class EntropyBasedStrategy(BaseStrategy):
7
+ def _get_char_counts(self, text: str):
8
+ char_counts = {}
9
+ for char in text.lower():
10
+ if char.isalpha():
11
+ char_counts[char] = char_counts.get(char, 0) + 1
12
+ return char_counts
13
+
14
+ def _calculate_entropy(self, char_counts):
15
+ total_chars = sum(char_counts.values())
16
+ if total_chars == 0:
17
+ return 0.0
18
+
19
+ entropy = 0
20
+ for count in char_counts.values():
21
+ probability = count / total_chars
22
+ entropy -= probability * math.log2(probability)
23
+
24
+ return entropy
25
+
26
+ def _predict_impl(self, text: str) -> bool:
27
+ char_counts = self._get_char_counts(text)
28
+ total_chars = sum(char_counts.values())
29
+ if total_chars == 0:
30
+ return False
31
+
32
+ entropy = self._calculate_entropy(char_counts)
33
+ threshold = self.kwargs.get("entropy_threshold", 3.0)
34
+
35
+ return entropy < threshold
36
+
37
+ def _predict_proba_impl(self, text: str) -> float:
38
+ char_counts = self._get_char_counts(text)
39
+ total_chars = sum(char_counts.values())
40
+ if total_chars == 0:
41
+ return 0.0
42
+
43
+ entropy = self._calculate_entropy(char_counts)
44
+ max_entropy = math.log2(len(char_counts)) if char_counts else 1.0
45
+ normalized_entropy = entropy / max_entropy if max_entropy > 0 else 0.0
46
+
47
+ return 1.0 - normalized_entropy
@@ -0,0 +1,106 @@
1
+ import os
2
+ import urllib.request
3
+
4
+ from .base import BaseStrategy
5
+
6
+
7
+ class LanguageDetectionStrategy(BaseStrategy):
8
+ def __init__(self, **kwargs):
9
+ super().__init__(**kwargs)
10
+ self._model = None
11
+ self._model_path = self._get_model_path()
12
+
13
+ def _get_model_path(self):
14
+ custom_path = self.kwargs.get("model_path")
15
+ if custom_path:
16
+ return custom_path
17
+
18
+ model_dir = os.path.expanduser("~/.pygarble/models")
19
+ os.makedirs(model_dir, exist_ok=True)
20
+ return os.path.join(model_dir, "lid.176.bin")
21
+
22
+ def _download_model(self):
23
+ if os.path.exists(self._model_path):
24
+ return
25
+
26
+ custom_path = self.kwargs.get("model_path")
27
+ if custom_path:
28
+ raise FileNotFoundError(
29
+ f"Model file not found at custom path: {self._model_path}"
30
+ )
31
+
32
+ model_url = (
33
+ "https://dl.fbaipublicfiles.com/fasttext/supervised-models/"
34
+ "lid.176.bin"
35
+ )
36
+ print(
37
+ f"Downloading FastText language detection model to "
38
+ f"{self._model_path}..."
39
+ )
40
+ urllib.request.urlretrieve(model_url, self._model_path)
41
+ print("Model download completed.")
42
+
43
+ def _load_model(self):
44
+ if self._model is None:
45
+ try:
46
+ import fasttext
47
+ import numpy as np
48
+
49
+ self._download_model()
50
+ self._model = fasttext.load_model(self._model_path)
51
+
52
+ # Monkey patch to fix NumPy 2.0 compatibility issue
53
+ original_predict = self._model.predict
54
+
55
+ def patched_predict(text, k=1):
56
+ labels, probs = original_predict(text, k)
57
+ # Convert to list to avoid NumPy 2.0 copy=False issue
58
+ if isinstance(probs, np.ndarray):
59
+ probs = probs.tolist()
60
+ return labels, probs
61
+
62
+ self._model.predict = patched_predict
63
+
64
+ except ImportError:
65
+ raise ImportError(
66
+ "fasttext is required for LanguageDetectionStrategy. "
67
+ "Install it with: pip install fasttext-wheel"
68
+ )
69
+
70
+ def _predict_impl(self, text: str) -> bool:
71
+ self._load_model()
72
+
73
+ predictions = self._model.predict(text, k=1)
74
+ labels, scores = predictions
75
+
76
+ if not labels or not scores:
77
+ return True
78
+
79
+ label = labels[0]
80
+ score = float(scores[0])
81
+
82
+ target_lang = self.kwargs.get("target_language", "en")
83
+
84
+ if label == f"__label__{target_lang}":
85
+ return score < 0.5
86
+ else:
87
+ return True
88
+
89
+ def _predict_proba_impl(self, text: str) -> float:
90
+ self._load_model()
91
+
92
+ predictions = self._model.predict(text, k=1)
93
+ labels, scores = predictions
94
+
95
+ if not labels or not scores:
96
+ return 1.0
97
+
98
+ label = labels[0]
99
+ score = float(scores[0])
100
+
101
+ target_lang = self.kwargs.get("target_language", "en")
102
+
103
+ if label == f"__label__{target_lang}":
104
+ return float(1.0 - score)
105
+ else:
106
+ return 1.0
@@ -0,0 +1,44 @@
1
+ import re
2
+
3
+ from .base import BaseStrategy
4
+
5
+
6
+ class PatternMatchingStrategy(BaseStrategy):
7
+ DEFAULT_PATTERNS = {
8
+ "special_chars": r"[^a-zA-Z0-9\s]{3,}",
9
+ "repeated_chars": r"(.)\1{4,}",
10
+ "uppercase_sequence": r"[A-Z]{5,}",
11
+ "long_numbers": r"[0-9]{8,}",
12
+ }
13
+
14
+ def _get_patterns(self):
15
+ custom_patterns = self.kwargs.get("patterns", {})
16
+ override_defaults = self.kwargs.get("override_defaults", False)
17
+
18
+ if override_defaults:
19
+ patterns = custom_patterns
20
+ else:
21
+ patterns = {**self.DEFAULT_PATTERNS, **custom_patterns}
22
+
23
+ return patterns
24
+
25
+ def _predict_impl(self, text: str) -> bool:
26
+ patterns = self._get_patterns()
27
+
28
+ for pattern_name, pattern_regex in patterns.items():
29
+ if re.search(pattern_regex, text):
30
+ return True
31
+ return False
32
+
33
+ def _predict_proba_impl(self, text: str) -> float:
34
+ patterns = self._get_patterns()
35
+
36
+ if not patterns:
37
+ return 0.0
38
+
39
+ matches = 0
40
+ for pattern_name, pattern_regex in patterns.items():
41
+ if re.search(pattern_regex, text):
42
+ matches += 1
43
+
44
+ return min(matches / len(patterns), 1.0)
@@ -0,0 +1,22 @@
1
+ from .base import BaseStrategy
2
+
3
+
4
+ class StatisticalAnalysisStrategy(BaseStrategy):
5
+ def _get_alpha_ratio(self, text: str) -> float:
6
+ alpha_chars = sum(1 for c in text if c.isalpha())
7
+ content_chars = sum(1 for c in text if not c.isspace())
8
+
9
+ if content_chars == 0:
10
+ return 0.0
11
+
12
+ return alpha_chars / content_chars
13
+
14
+ def _predict_impl(self, text: str) -> bool:
15
+ alpha_ratio = self._get_alpha_ratio(text)
16
+ threshold = self.kwargs.get("alpha_threshold", 0.5)
17
+
18
+ return alpha_ratio < threshold
19
+
20
+ def _predict_proba_impl(self, text: str) -> float:
21
+ alpha_ratio = self._get_alpha_ratio(text)
22
+ return 1.0 - alpha_ratio
@@ -0,0 +1,29 @@
1
+ from .base import BaseStrategy
2
+
3
+
4
+ class WordLengthStrategy(BaseStrategy):
5
+ def _get_words(self, text: str):
6
+ return text.split()
7
+
8
+ def _predict_impl(self, text: str) -> bool:
9
+ words = self._get_words(text)
10
+ if not words:
11
+ return False
12
+
13
+ avg_length = sum(len(word) for word in words) / len(words)
14
+ max_length = self.kwargs.get("max_word_length", 20)
15
+
16
+ return avg_length > max_length
17
+
18
+ def _predict_proba_impl(self, text: str) -> float:
19
+ words = self._get_words(text)
20
+ if not words:
21
+ return 0.0
22
+
23
+ avg_length = sum(len(word) for word in words) / len(words)
24
+ max_length = self.kwargs.get("max_word_length", 20)
25
+
26
+ if avg_length <= max_length:
27
+ return 0.0
28
+
29
+ return min((avg_length - max_length) / max_length, 1.0)