cloudnoteslib 0.1.0__tar.gz
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.
- cloudnoteslib-0.1.0/PKG-INFO +37 -0
- cloudnoteslib-0.1.0/README.md +18 -0
- cloudnoteslib-0.1.0/cloudnoteslib/__init__.py +128 -0
- cloudnoteslib-0.1.0/cloudnoteslib/analyzers/__init__.py +14 -0
- cloudnoteslib-0.1.0/cloudnoteslib/analyzers/content_analyzer.py +180 -0
- cloudnoteslib-0.1.0/cloudnoteslib/analyzers/search.py +143 -0
- cloudnoteslib-0.1.0/cloudnoteslib/analyzers/statistics.py +88 -0
- cloudnoteslib-0.1.0/cloudnoteslib/config.py +28 -0
- cloudnoteslib-0.1.0/cloudnoteslib/exceptions.py +19 -0
- cloudnoteslib-0.1.0/cloudnoteslib/exporters/__init__.py +11 -0
- cloudnoteslib-0.1.0/cloudnoteslib/exporters/base.py +31 -0
- cloudnoteslib-0.1.0/cloudnoteslib/exporters/json_exporter.py +19 -0
- cloudnoteslib-0.1.0/cloudnoteslib/exporters/markdown_exporter.py +28 -0
- cloudnoteslib-0.1.0/cloudnoteslib/models/__init__.py +18 -0
- cloudnoteslib-0.1.0/cloudnoteslib/models/note.py +323 -0
- cloudnoteslib-0.1.0/cloudnoteslib/models/note_collection.py +233 -0
- cloudnoteslib-0.1.0/cloudnoteslib/models/tag.py +129 -0
- cloudnoteslib-0.1.0/cloudnoteslib/processors/__init__.py +36 -0
- cloudnoteslib-0.1.0/cloudnoteslib/processors/base.py +157 -0
- cloudnoteslib-0.1.0/cloudnoteslib/processors/markdown_processor.py +157 -0
- cloudnoteslib-0.1.0/cloudnoteslib/processors/plaintext_processor.py +103 -0
- cloudnoteslib-0.1.0/cloudnoteslib/processors/richtext_processor.py +122 -0
- cloudnoteslib-0.1.0/cloudnoteslib/security/__init__.py +12 -0
- cloudnoteslib-0.1.0/cloudnoteslib/security/encryptor.py +81 -0
- cloudnoteslib-0.1.0/cloudnoteslib/security/sanitizer.py +56 -0
- cloudnoteslib-0.1.0/cloudnoteslib.egg-info/PKG-INFO +37 -0
- cloudnoteslib-0.1.0/cloudnoteslib.egg-info/SOURCES.txt +31 -0
- cloudnoteslib-0.1.0/cloudnoteslib.egg-info/dependency_links.txt +1 -0
- cloudnoteslib-0.1.0/cloudnoteslib.egg-info/requires.txt +1 -0
- cloudnoteslib-0.1.0/cloudnoteslib.egg-info/top_level.txt +1 -0
- cloudnoteslib-0.1.0/pyproject.toml +25 -0
- cloudnoteslib-0.1.0/setup.cfg +4 -0
- cloudnoteslib-0.1.0/setup.py +25 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cloudnoteslib
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A reusable Object-Oriented generic library for note processing, analysis, and security.
|
|
5
|
+
Home-page: https://github.com/Kavyavegunta04/Cloudnote
|
|
6
|
+
Author: Kavya
|
|
7
|
+
Author-email: Kavya <kavyavegunta27@gmail.com>
|
|
8
|
+
Project-URL: Homepage, https://github.com/Kavyavegunta04/Cloudnote
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Requires-Python: >=3.9
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
Requires-Dist: cryptography>=41.0.0
|
|
16
|
+
Dynamic: author
|
|
17
|
+
Dynamic: home-page
|
|
18
|
+
Dynamic: requires-python
|
|
19
|
+
|
|
20
|
+
# cloudnoteslib
|
|
21
|
+
|
|
22
|
+
A comprehensive, reusable Python Object-Oriented Programming (OOP) library designed for text note processing, content analysis, security, and exports.
|
|
23
|
+
|
|
24
|
+
## Features & OOP Principles
|
|
25
|
+
- **Encapsulation:** Strongly typed `Note` and `Tag` models with data validation via `@property`.
|
|
26
|
+
- **Abstraction:** Abstract `NoteProcessor` base class guaranteeing unified interfaces.
|
|
27
|
+
- **Inheritance & Polymorphism:** `MarkdownProcessor`, `PlainTextProcessor`, and `RichTextProcessor` implementations sharing the same contract.
|
|
28
|
+
- **Design Patterns:**
|
|
29
|
+
- **Facade Pattern:** `CloudNotesClient` acts as the single point of entry.
|
|
30
|
+
- **Strategy Pattern:** Interchangeable search algorithms (`search.py`).
|
|
31
|
+
- **Template Method Pattern:** Processors share generic steps while overriding specific details.
|
|
32
|
+
- **Singleton Pattern:** Global configuration management.
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
```bash
|
|
36
|
+
pip install cloudnoteslib
|
|
37
|
+
```
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# cloudnoteslib
|
|
2
|
+
|
|
3
|
+
A comprehensive, reusable Python Object-Oriented Programming (OOP) library designed for text note processing, content analysis, security, and exports.
|
|
4
|
+
|
|
5
|
+
## Features & OOP Principles
|
|
6
|
+
- **Encapsulation:** Strongly typed `Note` and `Tag` models with data validation via `@property`.
|
|
7
|
+
- **Abstraction:** Abstract `NoteProcessor` base class guaranteeing unified interfaces.
|
|
8
|
+
- **Inheritance & Polymorphism:** `MarkdownProcessor`, `PlainTextProcessor`, and `RichTextProcessor` implementations sharing the same contract.
|
|
9
|
+
- **Design Patterns:**
|
|
10
|
+
- **Facade Pattern:** `CloudNotesClient` acts as the single point of entry.
|
|
11
|
+
- **Strategy Pattern:** Interchangeable search algorithms (`search.py`).
|
|
12
|
+
- **Template Method Pattern:** Processors share generic steps while overriding specific details.
|
|
13
|
+
- **Singleton Pattern:** Global configuration management.
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
```bash
|
|
17
|
+
pip install cloudnoteslib
|
|
18
|
+
```
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""
|
|
2
|
+
cloudnoteslib — A reusable Python OOP library for Note processing and analysis.
|
|
3
|
+
|
|
4
|
+
Features:
|
|
5
|
+
- Encapsulated Note and Tag models
|
|
6
|
+
- Polymorphic content processors (Markdown, Plain, Rich text)
|
|
7
|
+
- Strategy-pattern based Search Engine
|
|
8
|
+
- AES-256 Encryption & XSS Sanitization
|
|
9
|
+
- Export tools (JSON, Markdown)
|
|
10
|
+
|
|
11
|
+
Version: 0.1.0
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
__version__ = "0.1.0"
|
|
15
|
+
__author__ = "Kavya"
|
|
16
|
+
__email__ = "kavyavegunta27@gmail.com"
|
|
17
|
+
|
|
18
|
+
from typing import List, Optional
|
|
19
|
+
|
|
20
|
+
# Core Models
|
|
21
|
+
from .models.note import Note
|
|
22
|
+
from .models.tag import Tag
|
|
23
|
+
from .models.note_collection import NoteCollection
|
|
24
|
+
|
|
25
|
+
# Processors
|
|
26
|
+
from .processors.base import NoteProcessor
|
|
27
|
+
from .processors.markdown_processor import MarkdownProcessor
|
|
28
|
+
from .processors.plaintext_processor import PlainTextProcessor
|
|
29
|
+
from .processors.richtext_processor import RichTextProcessor
|
|
30
|
+
|
|
31
|
+
# Analyzers
|
|
32
|
+
from .analyzers.content_analyzer import ContentAnalyzer
|
|
33
|
+
from .analyzers.statistics import NoteStatistics
|
|
34
|
+
from .analyzers.search import SearchEngine
|
|
35
|
+
|
|
36
|
+
# Security & Exporters
|
|
37
|
+
from .security.encryptor import NoteEncryptor
|
|
38
|
+
from .security.sanitizer import ContentSanitizer
|
|
39
|
+
from .exporters.json_exporter import JSONExporter
|
|
40
|
+
from .exporters.markdown_exporter import MarkdownExporter
|
|
41
|
+
|
|
42
|
+
# Config & Exceptions
|
|
43
|
+
from .config import config, NoteConfig
|
|
44
|
+
from .exceptions import CloudNotesLibError, ProcessorNotSupportedError
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# =========================================================================
|
|
48
|
+
# CloudNotesClient — FACADE PATTERN
|
|
49
|
+
# =========================================================================
|
|
50
|
+
|
|
51
|
+
class CloudNotesClient:
|
|
52
|
+
"""
|
|
53
|
+
High-level Facade for cloudnoteslib.
|
|
54
|
+
|
|
55
|
+
Provides a simplified, unified interface hiding the complexity of
|
|
56
|
+
processors, analyzers, security, and exporters.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
def __init__(self, processor_type: str = "markdown"):
|
|
60
|
+
self.processor_type = processor_type
|
|
61
|
+
self._processor = self._create_processor(processor_type)
|
|
62
|
+
self._analyzer = ContentAnalyzer()
|
|
63
|
+
self._search_engine = SearchEngine()
|
|
64
|
+
self._sanitizer = ContentSanitizer()
|
|
65
|
+
self._encryptor = NoteEncryptor(salt=config.password_salt)
|
|
66
|
+
self._stats = NoteStatistics()
|
|
67
|
+
|
|
68
|
+
def _create_processor(self, type_name: str) -> NoteProcessor:
|
|
69
|
+
"""Factory Method for creating content processors."""
|
|
70
|
+
processors = {
|
|
71
|
+
"markdown": MarkdownProcessor(),
|
|
72
|
+
"plaintext": PlainTextProcessor(),
|
|
73
|
+
"richtext": RichTextProcessor(),
|
|
74
|
+
}
|
|
75
|
+
normalized = type_name.strip().lower()
|
|
76
|
+
if normalized not in processors:
|
|
77
|
+
raise ProcessorNotSupportedError(f"Processor '{type_name}' not supported.")
|
|
78
|
+
return processors[normalized]
|
|
79
|
+
|
|
80
|
+
def process_note(self, note: Note) -> Note:
|
|
81
|
+
"""Sanitize and process note content."""
|
|
82
|
+
clean_content = self._sanitizer.sanitize(note.content)
|
|
83
|
+
processed_content = self._processor.process(clean_content)
|
|
84
|
+
# We don't overwrite the original content in the note with processed plain text,
|
|
85
|
+
# but we could. For now, just return a sanitized version.
|
|
86
|
+
note.content = clean_content
|
|
87
|
+
return note
|
|
88
|
+
|
|
89
|
+
def analyze_content(self, note: Note) -> dict:
|
|
90
|
+
"""Return analytics for a single note."""
|
|
91
|
+
return self._analyzer.analyze(note)
|
|
92
|
+
|
|
93
|
+
def get_collection_stats(self, collection: NoteCollection) -> dict:
|
|
94
|
+
"""Return collection-level statistics."""
|
|
95
|
+
return self._stats.get_summary(collection)
|
|
96
|
+
|
|
97
|
+
def search_notes(self, collection: NoteCollection, query: str, strategy: str = "exact") -> NoteCollection:
|
|
98
|
+
"""Search notes using a specific strategy."""
|
|
99
|
+
self._search_engine.set_strategy(strategy)
|
|
100
|
+
return self._search_engine.search(collection, query)
|
|
101
|
+
|
|
102
|
+
def encrypt_content(self, plain_text: str, password: str) -> str:
|
|
103
|
+
"""Encrypt content string."""
|
|
104
|
+
return self._encryptor.encrypt(plain_text, password)
|
|
105
|
+
|
|
106
|
+
def decrypt_content(self, encrypted_text: str, password: str) -> str:
|
|
107
|
+
"""Decrypt content string."""
|
|
108
|
+
return self._encryptor.decrypt(encrypted_text, password)
|
|
109
|
+
|
|
110
|
+
def export(self, collection: NoteCollection, format: str = "json") -> str:
|
|
111
|
+
"""Export collection to requested format."""
|
|
112
|
+
if format.lower() == "json":
|
|
113
|
+
return JSONExporter().export(collection)
|
|
114
|
+
elif format.lower() in ("md", "markdown"):
|
|
115
|
+
return MarkdownExporter().export(collection)
|
|
116
|
+
else:
|
|
117
|
+
raise ValueError(f"Export format not supported: {format}")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
__all__ = [
|
|
121
|
+
"CloudNotesClient",
|
|
122
|
+
"Note", "Tag", "NoteCollection",
|
|
123
|
+
"NoteProcessor", "MarkdownProcessor", "PlainTextProcessor", "RichTextProcessor",
|
|
124
|
+
"ContentAnalyzer", "NoteStatistics", "SearchEngine",
|
|
125
|
+
"NoteEncryptor", "ContentSanitizer",
|
|
126
|
+
"JSONExporter", "MarkdownExporter",
|
|
127
|
+
"config", "CloudNotesLibError"
|
|
128
|
+
]
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""
|
|
2
|
+
cloudnoteslib.analyzers — Content Analysis and Search.
|
|
3
|
+
|
|
4
|
+
Exports:
|
|
5
|
+
ContentAnalyzer: Single-note content analysis.
|
|
6
|
+
NoteStatistics: Collection-level analytics.
|
|
7
|
+
SearchEngine: Multi-strategy search (Strategy Pattern).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from .content_analyzer import ContentAnalyzer
|
|
11
|
+
from .statistics import NoteStatistics
|
|
12
|
+
from .search import SearchEngine
|
|
13
|
+
|
|
14
|
+
__all__ = ["ContentAnalyzer", "NoteStatistics", "SearchEngine"]
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""
|
|
2
|
+
cloudnoteslib.analyzers.content_analyzer — Single-Note Content Analysis.
|
|
3
|
+
|
|
4
|
+
Provides detailed analytics for individual notes including word frequency,
|
|
5
|
+
readability metrics, and content classification.
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
>>> analyzer = ContentAnalyzer()
|
|
9
|
+
>>> result = analyzer.analyze(note)
|
|
10
|
+
>>> result['word_count']
|
|
11
|
+
150
|
|
12
|
+
>>> result['reading_time']
|
|
13
|
+
0.8
|
|
14
|
+
>>> result['top_words']
|
|
15
|
+
[('project', 5), ('cloud', 3), ('deploy', 2)]
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import re
|
|
19
|
+
from collections import Counter
|
|
20
|
+
from typing import List, Dict
|
|
21
|
+
from ..models.note import Note
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ContentAnalyzer:
|
|
25
|
+
"""
|
|
26
|
+
Performs detailed content analysis on a single Note.
|
|
27
|
+
|
|
28
|
+
Extracts metrics such as word frequency, sentence statistics,
|
|
29
|
+
readability indicators, and content structure information.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
# Common English stop words to exclude from frequency analysis
|
|
33
|
+
STOP_WORDS = frozenset({
|
|
34
|
+
"the", "a", "an", "is", "are", "was", "were", "be", "been", "being",
|
|
35
|
+
"have", "has", "had", "do", "does", "did", "will", "would", "could",
|
|
36
|
+
"should", "may", "might", "shall", "can", "need", "must", "ought",
|
|
37
|
+
"i", "me", "my", "we", "our", "you", "your", "he", "she", "it",
|
|
38
|
+
"they", "them", "their", "this", "that", "these", "those", "what",
|
|
39
|
+
"which", "who", "whom", "and", "but", "or", "nor", "not", "so",
|
|
40
|
+
"if", "then", "than", "too", "very", "just", "about", "above",
|
|
41
|
+
"after", "again", "all", "also", "any", "because", "before",
|
|
42
|
+
"between", "both", "by", "each", "for", "from", "get", "got",
|
|
43
|
+
"how", "in", "into", "its", "more", "most", "no", "of", "off",
|
|
44
|
+
"on", "only", "other", "out", "own", "same", "some", "such",
|
|
45
|
+
"to", "up", "with", "when", "where", "while",
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
def analyze(self, note: Note) -> dict:
|
|
49
|
+
"""
|
|
50
|
+
Perform comprehensive content analysis on a single note.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
note: Note object to analyze.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
Dictionary containing all analysis metrics.
|
|
57
|
+
"""
|
|
58
|
+
content = note.content
|
|
59
|
+
words = self._extract_words(content)
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
"title": note.title,
|
|
63
|
+
"word_count": len(words),
|
|
64
|
+
"char_count": note.char_count,
|
|
65
|
+
"sentence_count": note.sentence_count,
|
|
66
|
+
"paragraph_count": self._count_paragraphs(content),
|
|
67
|
+
"reading_time": note.reading_time,
|
|
68
|
+
"avg_word_length": self._avg_word_length(words),
|
|
69
|
+
"avg_sentence_length": self._avg_sentence_length(content, words),
|
|
70
|
+
"top_words": self._top_words(words, n=10),
|
|
71
|
+
"unique_word_count": len(set(words)),
|
|
72
|
+
"vocabulary_richness": self._vocabulary_richness(words),
|
|
73
|
+
"has_links": self._has_links(content),
|
|
74
|
+
"has_code": self._has_code(content),
|
|
75
|
+
"tags": note.tags,
|
|
76
|
+
"is_pinned": note.is_pinned,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
def get_word_frequency(self, note: Note, top_n: int = 20) -> List[tuple]:
|
|
80
|
+
"""
|
|
81
|
+
Get the most frequently used meaningful words.
|
|
82
|
+
|
|
83
|
+
Excludes stop words and short words to focus on content-bearing terms.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
note: Note to analyze.
|
|
87
|
+
top_n: Number of top words to return.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
List of (word, count) tuples, sorted by frequency.
|
|
91
|
+
"""
|
|
92
|
+
words = self._extract_words(note.content)
|
|
93
|
+
return self._top_words(words, n=top_n)
|
|
94
|
+
|
|
95
|
+
def get_readability_score(self, note: Note) -> dict:
|
|
96
|
+
"""
|
|
97
|
+
Calculate readability metrics for the note.
|
|
98
|
+
|
|
99
|
+
Uses a simplified Flesch-Kincaid-inspired scoring based on
|
|
100
|
+
average sentence length and average word length.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
note: Note to analyze.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
Dictionary with readability metrics and a difficulty level.
|
|
107
|
+
"""
|
|
108
|
+
words = self._extract_words(note.content)
|
|
109
|
+
if not words:
|
|
110
|
+
return {"score": 0, "level": "empty", "avg_sentence_len": 0}
|
|
111
|
+
|
|
112
|
+
avg_sentence_len = self._avg_sentence_length(note.content, words)
|
|
113
|
+
avg_word_len = self._avg_word_length(words)
|
|
114
|
+
|
|
115
|
+
# Simplified readability: lower score = easier to read
|
|
116
|
+
score = round((avg_sentence_len * 0.39) + (avg_word_len * 11.8) - 15.59, 1)
|
|
117
|
+
|
|
118
|
+
if score < 30:
|
|
119
|
+
level = "very_easy"
|
|
120
|
+
elif score < 50:
|
|
121
|
+
level = "easy"
|
|
122
|
+
elif score < 60:
|
|
123
|
+
level = "moderate"
|
|
124
|
+
elif score < 70:
|
|
125
|
+
level = "somewhat_difficult"
|
|
126
|
+
else:
|
|
127
|
+
level = "difficult"
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
"score": max(0, score),
|
|
131
|
+
"level": level,
|
|
132
|
+
"avg_sentence_length": avg_sentence_len,
|
|
133
|
+
"avg_word_length": avg_word_len,
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
# ─── Private Helper Methods ───
|
|
137
|
+
|
|
138
|
+
def _extract_words(self, content: str) -> List[str]:
|
|
139
|
+
"""Extract individual words from content (lowercase, alphanumeric only)."""
|
|
140
|
+
return re.findall(r'[a-zA-Z]+', content.lower())
|
|
141
|
+
|
|
142
|
+
def _count_paragraphs(self, content: str) -> int:
|
|
143
|
+
"""Count paragraphs (blocks separated by blank lines)."""
|
|
144
|
+
if not content.strip():
|
|
145
|
+
return 0
|
|
146
|
+
paragraphs = re.split(r'\n\s*\n', content.strip())
|
|
147
|
+
return len([p for p in paragraphs if p.strip()])
|
|
148
|
+
|
|
149
|
+
def _avg_word_length(self, words: List[str]) -> float:
|
|
150
|
+
"""Average character length of words."""
|
|
151
|
+
if not words:
|
|
152
|
+
return 0.0
|
|
153
|
+
return round(sum(len(w) for w in words) / len(words), 1)
|
|
154
|
+
|
|
155
|
+
def _avg_sentence_length(self, content: str, words: List[str]) -> float:
|
|
156
|
+
"""Average number of words per sentence."""
|
|
157
|
+
sentences = re.split(r'[.!?]+', content.strip())
|
|
158
|
+
sentence_count = len([s for s in sentences if s.strip()])
|
|
159
|
+
if sentence_count == 0:
|
|
160
|
+
return 0.0
|
|
161
|
+
return round(len(words) / sentence_count, 1)
|
|
162
|
+
|
|
163
|
+
def _top_words(self, words: List[str], n: int = 10) -> List[tuple]:
|
|
164
|
+
"""Get top N most frequent non-stop-words."""
|
|
165
|
+
meaningful = [w for w in words if w not in self.STOP_WORDS and len(w) > 2]
|
|
166
|
+
return Counter(meaningful).most_common(n)
|
|
167
|
+
|
|
168
|
+
def _vocabulary_richness(self, words: List[str]) -> float:
|
|
169
|
+
"""Ratio of unique words to total words (type-token ratio)."""
|
|
170
|
+
if not words:
|
|
171
|
+
return 0.0
|
|
172
|
+
return round(len(set(words)) / len(words), 3)
|
|
173
|
+
|
|
174
|
+
def _has_links(self, content: str) -> bool:
|
|
175
|
+
"""Check if content contains URLs."""
|
|
176
|
+
return bool(re.search(r'https?://\S+', content))
|
|
177
|
+
|
|
178
|
+
def _has_code(self, content: str) -> bool:
|
|
179
|
+
"""Check if content contains code blocks or inline code."""
|
|
180
|
+
return bool(re.search(r'```|`[^`]+`', content))
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""
|
|
2
|
+
cloudnoteslib.analyzers.search — Search Engine (Strategy Pattern).
|
|
3
|
+
|
|
4
|
+
Demonstrates the STRATEGY PATTERN:
|
|
5
|
+
SearchEngine uses a configured strategy (Exact, Fuzzy, or Regex)
|
|
6
|
+
to perform searches. The algorithm can be interchanged at runtime
|
|
7
|
+
without modifying the NoteCollection or the SearchEngine itself.
|
|
8
|
+
|
|
9
|
+
Example:
|
|
10
|
+
>>> engine = SearchEngine(strategy="fuzzy")
|
|
11
|
+
>>> results = engine.search(collection, "prjct")
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import re
|
|
15
|
+
from abc import ABC, abstractmethod
|
|
16
|
+
from typing import List
|
|
17
|
+
from ..models.note import Note
|
|
18
|
+
from ..models.note_collection import NoteCollection
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# ─── Strategy Interface ───
|
|
22
|
+
|
|
23
|
+
class SearchStrategy(ABC):
|
|
24
|
+
"""Abstract interface for search algorithms."""
|
|
25
|
+
|
|
26
|
+
@abstractmethod
|
|
27
|
+
def search(self, notes: List[Note], query: str) -> List[Note]:
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# ─── Concrete Strategies ───
|
|
32
|
+
|
|
33
|
+
class ExactMatchStrategy(SearchStrategy):
|
|
34
|
+
"""Simple case-insensitive substring match."""
|
|
35
|
+
|
|
36
|
+
def search(self, notes: List[Note], query: str) -> List[Note]:
|
|
37
|
+
q = query.strip().lower()
|
|
38
|
+
if not q:
|
|
39
|
+
return notes.copy()
|
|
40
|
+
|
|
41
|
+
results = []
|
|
42
|
+
for note in notes:
|
|
43
|
+
if q in note.title.lower() or q in note.content.lower():
|
|
44
|
+
results.append(note)
|
|
45
|
+
return results
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class RegexStrategy(SearchStrategy):
|
|
49
|
+
"""Advanced search using regular expressions."""
|
|
50
|
+
|
|
51
|
+
def search(self, notes: List[Note], pattern: str) -> List[Note]:
|
|
52
|
+
if not pattern.strip():
|
|
53
|
+
return notes.copy()
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
regex = re.compile(pattern, re.IGNORECASE)
|
|
57
|
+
except re.error:
|
|
58
|
+
# Fallback to exact match if regex is invalid
|
|
59
|
+
return ExactMatchStrategy().search(notes, pattern)
|
|
60
|
+
|
|
61
|
+
results = []
|
|
62
|
+
for note in notes:
|
|
63
|
+
if regex.search(note.title) or regex.search(note.content):
|
|
64
|
+
results.append(note)
|
|
65
|
+
return results
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class FuzzyStrategy(SearchStrategy):
|
|
69
|
+
"""
|
|
70
|
+
Very basic fuzzy search.
|
|
71
|
+
Matches if all characters in query appear in the text in order.
|
|
72
|
+
(e.g., 'prt' matches 'project').
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
def search(self, notes: List[Note], query: str) -> List[Note]:
|
|
76
|
+
q = query.strip().lower()
|
|
77
|
+
if not q:
|
|
78
|
+
return notes.copy()
|
|
79
|
+
|
|
80
|
+
# Build regex pattern: p.*r.*t
|
|
81
|
+
escaped_chars = [re.escape(c) for c in q]
|
|
82
|
+
pattern = '.*'.join(escaped_chars)
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
regex = re.compile(pattern, re.IGNORECASE)
|
|
86
|
+
except re.error:
|
|
87
|
+
return ExactMatchStrategy().search(notes, query)
|
|
88
|
+
|
|
89
|
+
results = []
|
|
90
|
+
for note in notes:
|
|
91
|
+
if regex.search(note.title) or regex.search(note.content):
|
|
92
|
+
results.append(note)
|
|
93
|
+
return results
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# ─── Context Class ───
|
|
97
|
+
|
|
98
|
+
class SearchEngine:
|
|
99
|
+
"""
|
|
100
|
+
Context class that executes searches using the configured Strategy.
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
STRATEGIES = {
|
|
104
|
+
"exact": ExactMatchStrategy(),
|
|
105
|
+
"regex": RegexStrategy(),
|
|
106
|
+
"fuzzy": FuzzyStrategy(),
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
def __init__(self, strategy: str = "exact"):
|
|
110
|
+
"""
|
|
111
|
+
Initialize with a specific search strategy.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
strategy: 'exact', 'regex', or 'fuzzy'.
|
|
115
|
+
"""
|
|
116
|
+
self.set_strategy(strategy)
|
|
117
|
+
|
|
118
|
+
def set_strategy(self, strategy_name: str) -> None:
|
|
119
|
+
"""Change internal strategy at runtime."""
|
|
120
|
+
normalized = strategy_name.strip().lower()
|
|
121
|
+
if normalized not in self.STRATEGIES:
|
|
122
|
+
raise ValueError(f"Unknown strategy: {strategy_name}. Choose from: {list(self.STRATEGIES.keys())}")
|
|
123
|
+
self._strategy = self.STRATEGIES[normalized]
|
|
124
|
+
self._current_strategy_name = normalized
|
|
125
|
+
|
|
126
|
+
@property
|
|
127
|
+
def strategy_name(self) -> str:
|
|
128
|
+
return self._current_strategy_name
|
|
129
|
+
|
|
130
|
+
def search(self, collection: NoteCollection, query: str) -> NoteCollection:
|
|
131
|
+
"""
|
|
132
|
+
Execute search on a collection.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
collection: The NoteCollection to search.
|
|
136
|
+
query: The search string.
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
A new NoteCollection with the results.
|
|
140
|
+
"""
|
|
141
|
+
notes_list = list(collection)
|
|
142
|
+
results = self._strategy.search(notes_list, query)
|
|
143
|
+
return NoteCollection(results)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""
|
|
2
|
+
cloudnoteslib.analyzers.statistics — Collection Statistics Analyzer.
|
|
3
|
+
|
|
4
|
+
Analyzes a NoteCollection to produce aggregate statistics across
|
|
5
|
+
multiple notes.
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
>>> stats = NoteStatistics()
|
|
9
|
+
>>> summary = stats.get_summary(collection)
|
|
10
|
+
>>> summary['total_notes']
|
|
11
|
+
42
|
|
12
|
+
>>> len(summary['popular_tags'])
|
|
13
|
+
5
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from typing import List, Dict
|
|
17
|
+
from ..models.note_collection import NoteCollection
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class NoteStatistics:
|
|
21
|
+
"""
|
|
22
|
+
Computes aggregate analytics over a NoteCollection.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def get_summary(self, collection: NoteCollection) -> dict:
|
|
26
|
+
"""
|
|
27
|
+
Produce a comprehensive statistical summary of the collection.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
collection: NoteCollection to analyze.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Dictionary with collection-level metrics.
|
|
34
|
+
"""
|
|
35
|
+
if len(collection) == 0:
|
|
36
|
+
return self._empty_summary()
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
"total_notes": len(collection),
|
|
40
|
+
"total_words": collection.total_words,
|
|
41
|
+
"total_reading_time": collection.total_reading_time,
|
|
42
|
+
"average_word_count": collection.average_word_count,
|
|
43
|
+
|
|
44
|
+
"pinned_count": len(collection.get_pinned()),
|
|
45
|
+
"archived_count": len(collection.get_archived()),
|
|
46
|
+
"active_count": len(collection.get_active()),
|
|
47
|
+
|
|
48
|
+
"total_tags": len(collection.all_tags),
|
|
49
|
+
"popular_tags": self._get_top_tags(collection, count=5),
|
|
50
|
+
|
|
51
|
+
"longest_note": self._note_summary(collection.longest_note),
|
|
52
|
+
"shortest_note": self._note_summary(collection.shortest_note),
|
|
53
|
+
"most_recent": self._note_summary(collection.most_recent),
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
def _get_top_tags(self, collection: NoteCollection, count: int = 5) -> List[dict]:
|
|
57
|
+
"""Get the most frequently used tags with their counts."""
|
|
58
|
+
tag_counts = collection.tag_counts
|
|
59
|
+
top = list(tag_counts.items())[:count]
|
|
60
|
+
return [{"tag": t[0], "count": t[1]} for t in top]
|
|
61
|
+
|
|
62
|
+
def _note_summary(self, note) -> dict:
|
|
63
|
+
"""Return a brief summary of a single note for stats."""
|
|
64
|
+
if not note:
|
|
65
|
+
return {}
|
|
66
|
+
return {
|
|
67
|
+
"id": note.note_id,
|
|
68
|
+
"title": note.title,
|
|
69
|
+
"word_count": note.word_count,
|
|
70
|
+
"created_at": note.created_at.isoformat() if note.created_at else None,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
def _empty_summary(self) -> dict:
|
|
74
|
+
"""Return zeroed summary for empty collections."""
|
|
75
|
+
return {
|
|
76
|
+
"total_notes": 0,
|
|
77
|
+
"total_words": 0,
|
|
78
|
+
"total_reading_time": 0.0,
|
|
79
|
+
"average_word_count": 0.0,
|
|
80
|
+
"pinned_count": 0,
|
|
81
|
+
"archived_count": 0,
|
|
82
|
+
"active_count": 0,
|
|
83
|
+
"total_tags": 0,
|
|
84
|
+
"popular_tags": [],
|
|
85
|
+
"longest_note": {},
|
|
86
|
+
"shortest_note": {},
|
|
87
|
+
"most_recent": {},
|
|
88
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""
|
|
2
|
+
cloudnoteslib.config — Note Configuration Singleton.
|
|
3
|
+
|
|
4
|
+
Provides a global configuration point for the library.
|
|
5
|
+
Demonstrates the SINGLETON pattern.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
class NoteConfig:
|
|
9
|
+
"""
|
|
10
|
+
Singleton configuration manager for cloudnoteslib.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
_instance = None
|
|
14
|
+
|
|
15
|
+
def __new__(cls):
|
|
16
|
+
if cls._instance is None:
|
|
17
|
+
cls._instance = super(NoteConfig, cls).__new__(cls)
|
|
18
|
+
cls._instance._init_defaults()
|
|
19
|
+
return cls._instance
|
|
20
|
+
|
|
21
|
+
def _init_defaults(self):
|
|
22
|
+
self.default_processor = "markdown"
|
|
23
|
+
self.max_note_size_mb = 5
|
|
24
|
+
self.default_search_strategy = "exact"
|
|
25
|
+
self.password_salt = b"cloudnotes_salt_123"
|
|
26
|
+
|
|
27
|
+
# Create the singleton instance
|
|
28
|
+
config = NoteConfig()
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""
|
|
2
|
+
cloudnoteslib.exceptions — Custom Exceptions.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
class CloudNotesLibError(Exception):
|
|
6
|
+
"""Base exception for cloudnoteslib."""
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
class NoteValidationError(CloudNotesLibError):
|
|
10
|
+
"""Raised when note validation fails (e.g. title too long)."""
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
class ProcessorNotSupportedError(CloudNotesLibError):
|
|
14
|
+
"""Raised when an unknown processor type is requested."""
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
class SecurityError(CloudNotesLibError):
|
|
18
|
+
"""Raised for encryption/decryption or authorization failures."""
|
|
19
|
+
pass
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""
|
|
2
|
+
cloudnoteslib.exporters — Note Exporters.
|
|
3
|
+
|
|
4
|
+
Provides mechanisms to export note collections into various formats.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .base import NoteExporter
|
|
8
|
+
from .json_exporter import JSONExporter
|
|
9
|
+
from .markdown_exporter import MarkdownExporter
|
|
10
|
+
|
|
11
|
+
__all__ = ["NoteExporter", "JSONExporter", "MarkdownExporter"]
|