feat(tagging-system): Add comprehensive tagging system implementation

- Add tagging system Python package with modular architecture (config, core, impl)
- Create core interfaces for file discovery, content analysis, and tag generation
- Implement content analyzer with language detection and topic extraction
- Implement file discovery engine with directory scanning and filtering
- Implement tag generator with hierarchical tag creation and consolidation
- Add configuration management with example config and validation
- Create comprehensive design and requirements documentation in .kiro/specs
- Add pytest test suite with unit tests for models, interfaces, and config
- Add setup.py, requirements.txt, and pytest.ini for package management
- Add README.md with project overview and usage instructions
- Update workspace.json with new project structure
- Add Excalidraw diagram for system architecture visualization
- Establish foundation for automated vault tagging and metadata management
This commit is contained in:
windyboy
2025-12-31 08:37:16 +08:00
parent 6c7a6d0e3a
commit f99056a099
44 changed files with 3209 additions and 14 deletions
@@ -0,0 +1,270 @@
# Design Document: Comprehensive Tagging System
## Overview
The comprehensive tagging system will analyze and tag all files in the Obsidian vault based on their location, content, and existing metadata. The system uses a multi-layered approach combining directory-based tags, content analysis, and hierarchical topic classification to create a consistent and discoverable knowledge base.
## Architecture
The system follows a pipeline architecture with these main components:
```mermaid
graph TD
A[File Discovery] --> B[Content Analysis]
B --> C[Tag Generation]
C --> D[Tag Validation]
D --> E[Frontmatter Update]
E --> F[Batch Processing]
G[Tag Registry] --> C
G --> D
H[Directory Mapping] --> C
I[Language Detection] --> C
J[Content Classification] --> C
```
### Core Components
1. **File Discovery Engine**: Recursively scans vault directories
2. **Content Analyzer**: Extracts topics, language, and content type
3. **Tag Generator**: Creates hierarchical tags based on analysis
4. **Tag Validator**: Ensures consistency and prevents duplication
5. **Frontmatter Manager**: Updates YAML metadata safely
6. **Batch Processor**: Handles large-scale operations with progress tracking
## Components and Interfaces
### FileDiscovery Interface
```typescript
interface FileDiscovery {
scanDirectory(path: string): Promise<FileInfo[]>
filterByType(files: FileInfo[], types: string[]): FileInfo[]
excludeSensitive(files: FileInfo[]): FileInfo[]
}
```
### ContentAnalyzer Interface
```typescript
interface ContentAnalyzer {
analyzeContent(content: string): ContentAnalysis
detectLanguage(content: string): LanguageInfo
extractTopics(content: string): string[]
classifyContentType(content: string, filename: string): ContentType
}
```
### TagGenerator Interface
```typescript
interface TagGenerator {
generateDirectoryTags(filepath: string): string[]
generateContentTags(analysis: ContentAnalysis): string[]
generateHierarchicalTags(topics: string[]): string[]
consolidateTags(tags: string[]): string[]
}
```
### FrontmatterManager Interface
```typescript
interface FrontmatterManager {
parseFrontmatter(content: string): FrontmatterData
updateFrontmatter(content: string, updates: FrontmatterData): string
validateFrontmatter(data: FrontmatterData): ValidationResult
}
```
## Data Models
### FileInfo Model
```typescript
interface FileInfo {
path: string
name: string
directory: string
extension: string
size: number
created: Date
modified: Date
content?: string
}
```
### ContentAnalysis Model
```typescript
interface ContentAnalysis {
language: 'en' | 'zh' | 'mixed' | 'unknown'
contentType: ContentType
topics: string[]
mentions: {
tools: string[]
technologies: string[]
people: string[]
organizations: string[]
}
sentiment?: 'positive' | 'neutral' | 'negative'
complexity: 'basic' | 'intermediate' | 'advanced'
}
```
### TagStructure Model
```typescript
interface TagStructure {
primary: string[] // Main category tags
hierarchical: string[] // Topic/subtopic/detail tags
content: string[] // Content-derived tags
meta: string[] // Metadata tags (language, type, etc.)
custom: string[] // Manually added tags to preserve
}
```
### FrontmatterData Model
```typescript
interface FrontmatterData {
title?: string
tags: string[]
created?: string
updated?: string
type?: string
lang?: string
source?: string
aliases?: string[]
description?: string
}
```
## Tag Hierarchy Design
### Directory-Based Tags
- `project/` - Files in 100-project/
- `project/ai` - AI-related projects
- `project/infrastructure` - Infrastructure projects
- `project/work` - Work-related projects
- `area/` - Files in 200-area/
- `area/productivity` - Productivity area
- `area/health` - Health and fitness
- `area/finance` - Financial management
- `resource/` - Files in 300-resources/
- `resource/development` - Development resources
- `resource/cooking` - Cooking resources
- `archive/` - Files in 400-archive/
- `clipping/` - Web clippings and saved articles
### Content-Based Tags
- `tech/` - Technology-related content
- `tech/ai/llm` - Large Language Models
- `tech/infrastructure/docker` - Docker and containers
- `tech/development/python` - Python development
- `personal/` - Personal content
- `personal/productivity/gtd` - Getting Things Done
- `personal/health/cycling` - Cycling and fitness
- `work/` - Work-related content
- `work/government` - Government projects
- `work/enterprise` - Enterprise solutions
### Meta Tags
- `lang/en`, `lang/zh`, `lang/mixed` - Language tags
- `type/hub`, `type/note`, `type/clipping`, `type/daily-note` - Content type
- `status/draft`, `status/complete`, `status/archived` - Status tags
- `sensitive/credentials`, `sensitive/personal` - Security tags
## Correctness Properties
*A property is a characteristic or behavior that should hold true across all valid executions of a system—essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.*
Now I'll analyze the acceptance criteria to determine which ones can be tested as properties:
### Converting EARS to Properties
Based on the prework analysis, I'll create consolidated properties that eliminate redundancy while maintaining comprehensive coverage:
Property 1: Frontmatter standardization
*For any* processed file, the frontmatter should contain all required fields (title, tags, created, updated) in the correct YAML format, while preserving any existing data
**Validates: Requirements 1.1, 1.2, 1.3, 1.5**
Property 2: Directory-based tag mapping
*For any* file in the vault, the tags should correctly reflect its directory location using the appropriate primary tag (project/area/resource/archive/clipping) and hierarchical subtags based on subdirectory structure
**Validates: Requirements 2.1, 2.2, 2.3, 2.4, 2.5, 2.6**
Property 3: Content type classification
*For any* file, the content analysis should correctly identify and tag the content type (hub, clipping, daily-note, meeting, documentation, tutorial, reference, draft) based on content patterns and filename analysis
**Validates: Requirements 3.1, 3.2, 3.3, 3.4, 3.5**
Property 4: Hierarchical topic tagging
*For any* file with identifiable topic content, the system should generate appropriate hierarchical tags using forward slash notation that correctly categorize the content into topic hierarchies (tech/, personal/, work/, home-automation/)
**Validates: Requirements 4.1, 4.2, 4.3, 4.4, 4.5, 4.6**
Property 5: Language detection and tagging
*For any* file, the language detection should correctly identify the primary language (en/zh/mixed/unknown) based on character analysis and content patterns, and add the appropriate lang/ tag
**Validates: Requirements 5.1, 5.2, 5.3, 5.4, 5.5**
Property 6: Content analysis tag generation
*For any* file, the content analysis should extract and tag relevant topics, technical terms, tools, frameworks, technologies, and methodologies mentioned in the content, while preserving any manually added tags
**Validates: Requirements 6.1, 6.2, 6.3, 6.4, 6.5**
Property 7: Tag consistency and validation
*For any* generated tag set, all tags should follow kebab-case naming conventions, duplicate tags should be consolidated using canonical forms, and hierarchical relationships should be logically valid
**Validates: Requirements 7.1, 7.2, 7.3, 7.4, 7.5**
Property 8: Data preservation during processing
*For any* file being processed, existing tags and metadata should be preserved, backups should be created before modifications, and batch processing should maintain data integrity with proper error handling
**Validates: Requirements 8.1, 8.2, 8.3, 8.5**
Property 9: Sensitive content detection and protection
*For any* file containing sensitive information (credentials, personal data, financial/legal information), the system should add appropriate sensitive/ tags while ensuring the sensitive data itself is not exposed or logged during processing
**Validates: Requirements 9.1, 9.2, 9.3, 9.5**
Property 10: Obsidian compatibility
*For any* generated tag structure, the tags should be compatible with Obsidian's tag pane, search functionality, Dataview queries, graph view, and common plugins, with hub files containing appropriate Dataview queries for tag-based content listing
**Validates: Requirements 10.1, 10.2, 10.3, 10.4, 10.5**
## Error Handling
### File Processing Errors
- **Invalid file formats**: Skip non-text files with logging
- **Corrupted frontmatter**: Attempt repair or create new frontmatter
- **Permission errors**: Log and continue with next file
- **Large files**: Implement streaming analysis for files over 1MB
### Content Analysis Errors
- **Language detection failures**: Default to 'lang/unknown'
- **Topic extraction failures**: Use directory-based fallback tags
- **Encoding issues**: Attempt multiple encoding detection methods
### Tag Generation Errors
- **Invalid characters**: Sanitize and convert to kebab-case
- **Circular hierarchies**: Detect and break circular references
- **Tag conflicts**: Use canonical form resolution
### Batch Processing Errors
- **Interrupted processing**: Resume from last successful batch
- **Memory constraints**: Process in smaller batches
- **Concurrent access**: Implement file locking mechanism
## Testing Strategy
### Dual Testing Approach
The system will use both unit tests and property-based tests for comprehensive coverage:
**Unit Tests** will focus on:
- Specific examples of frontmatter parsing and generation
- Edge cases like empty files, malformed YAML, special characters
- Integration points between components
- Error conditions and recovery mechanisms
**Property-Based Tests** will focus on:
- Universal properties that hold across all file types and content
- Comprehensive input coverage through randomized file generation
- Tag consistency and validation across large datasets
- Data preservation guarantees during batch processing
### Property Test Configuration
- Minimum 100 iterations per property test
- Each property test references its design document property
- Tag format: **Feature: comprehensive-tagging-system, Property {number}: {property_text}**
### Test Data Generation
- **File generators**: Create files with various directory structures, content types, and languages
- **Content generators**: Generate text with different topics, technical terms, and sensitive information patterns
- **Frontmatter generators**: Create valid and invalid YAML frontmatter variations
- **Directory structure generators**: Create nested directory hierarchies matching vault patterns
The testing strategy ensures that the tagging system maintains correctness across the diverse content and organizational patterns found in the actual vault structure.
@@ -0,0 +1,138 @@
# Requirements Document
## Introduction
This specification defines a comprehensive tagging system for an Obsidian vault with mixed organizational patterns. The system will establish consistent tagging conventions, implement hierarchical tag structures based on actual content and directory structure, and ensure all content is properly categorized for improved discoverability and organization.
## Glossary
- **Tag_Hierarchy**: Multi-level tag structure using forward slashes (e.g., tech/ai/llm)
- **Frontmatter**: YAML metadata block at the beginning of Markdown files
- **Hub_File**: Index file that serves as entry point for a category or area
- **Content_Type**: Classification of file purpose (note, project, resource, clipping, etc.)
- **Language_Tag**: Identifier for content language (en, zh, mixed)
- **Directory_Structure**: Existing folder organization (100-project, 200-area, 300-resources, 400-archive, etc.)
## Requirements
### Requirement 1: Standardized Frontmatter Structure
**User Story:** As a vault user, I want consistent frontmatter across all files, so that I can rely on predictable metadata structure.
#### Acceptance Criteria
1. WHEN a file is processed, THE Tagging_System SHALL ensure it has standardized frontmatter with required fields
2. THE Tagging_System SHALL use YAML array format for tags: `tags: [tag1, tag2, tag3]`
3. THE Tagging_System SHALL include these core fields: title, tags, created, updated
4. WHERE content has specific metadata needs, THE Tagging_System SHALL include additional fields like type, lang, source
5. WHEN frontmatter exists but is incomplete, THE Tagging_System SHALL preserve existing data and add missing fields
### Requirement 2: Directory-Based Tag Structure
**User Story:** As a vault user, I want tags that reflect the actual directory organization, so that I can navigate content according to the existing structure.
#### Acceptance Criteria
1. WHEN a file is in 100-project directory, THE Tagging_System SHALL include 'project' tag and specific project subtags
2. WHEN a file is in 200-area directory, THE Tagging_System SHALL include 'area' tag and area-specific subtags
3. WHEN a file is in 300-resources directory, THE Tagging_System SHALL include 'resource' tag and topic-specific subtags
4. WHEN a file is in 400-archive directory, THE Tagging_System SHALL include 'archive' tag
5. WHEN a file is in Clippings or ReadItLater directories, THE Tagging_System SHALL include 'clipping' tag
6. THE Tagging_System SHALL create hierarchical tags based on subdirectory structure (e.g., project/ai, area/productivity)
### Requirement 3: Content-Type Classification
**User Story:** As a content creator, I want files tagged by their content type, so that I can filter and find specific kinds of information.
#### Acceptance Criteria
1. THE Tagging_System SHALL identify and tag hub files with 'hub' type
2. WHEN a file contains clipped web content, THE Tagging_System SHALL add 'clipping' tag
3. WHEN a file is a daily note with date format, THE Tagging_System SHALL add 'daily-note' tag
4. THE Tagging_System SHALL detect and tag meeting notes, documentation, tutorials, and reference materials
5. WHEN file content indicates it's a draft or work-in-progress, THE Tagging_System SHALL add appropriate status tags
### Requirement 4: Topic-Based Hierarchical Tags
**User Story:** As a researcher, I want hierarchical topic tags based on actual content themes, so that I can explore related content at different levels of specificity.
#### Acceptance Criteria
1. THE Tagging_System SHALL implement hierarchical tags using forward slash notation (topic/subtopic/detail)
2. WHEN content relates to AI/technology, THE Tagging_System SHALL use tech hierarchy (tech/ai, tech/infrastructure, tech/development)
3. WHEN content relates to personal topics, THE Tagging_System SHALL use personal hierarchy (personal/health, personal/finance, personal/productivity)
4. WHEN content relates to work projects, THE Tagging_System SHALL use work hierarchy (work/airport, work/ali, work/government)
5. WHEN content relates to home automation, THE Tagging_System SHALL use home-automation hierarchy
6. WHEN content spans multiple topics, THE Tagging_System SHALL include all relevant hierarchical tags
### Requirement 5: Language and Localization Tags
**User Story:** As a multilingual user, I want content tagged by language, so that I can filter content by language preference.
#### Acceptance Criteria
1. WHEN content is primarily in English, THE Tagging_System SHALL add 'lang/en' tag
2. WHEN content is primarily in Chinese, THE Tagging_System SHALL add 'lang/zh' tag
3. WHEN content mixes languages significantly, THE Tagging_System SHALL add 'lang/mixed' tag
4. THE Tagging_System SHALL detect language based on character analysis and content patterns
5. WHEN language cannot be determined, THE Tagging_System SHALL default to 'lang/unknown'
### Requirement 6: Automated Tag Generation
**User Story:** As a vault maintainer, I want automated tag generation based on content analysis, so that I don't have to manually tag every file.
#### Acceptance Criteria
1. THE Tagging_System SHALL analyze file content to suggest relevant topic tags
2. WHEN file contains technical terms, THE Tagging_System SHALL add appropriate technology tags
3. THE Tagging_System SHALL extract and tag mentioned tools, frameworks, and technologies
4. WHEN content discusses specific methodologies or concepts, THE Tagging_System SHALL add conceptual tags
5. THE Tagging_System SHALL preserve manually added tags while adding automated suggestions
### Requirement 7: Tag Consistency and Validation
**User Story:** As a vault organizer, I want consistent tag naming and validation, so that tags remain useful and don't fragment.
#### Acceptance Criteria
1. THE Tagging_System SHALL enforce kebab-case naming for all tags (lowercase with hyphens)
2. WHEN duplicate or similar tags exist, THE Tagging_System SHALL consolidate them using canonical forms
3. THE Tagging_System SHALL validate tag hierarchies to ensure logical parent-child relationships
4. WHEN tags violate naming conventions, THE Tagging_System SHALL suggest corrections
5. THE Tagging_System SHALL maintain a tag registry to prevent fragmentation
### Requirement 8: Batch Processing and Migration
**User Story:** As a vault administrator, I want to process all existing files in batches, so that I can systematically apply the tagging system to the entire vault.
#### Acceptance Criteria
1. THE Tagging_System SHALL process files in logical batches by directory structure
2. WHEN processing existing files, THE Tagging_System SHALL preserve existing tags and metadata
3. THE Tagging_System SHALL create backup copies before making changes to files
4. WHEN conflicts arise between existing and generated tags, THE Tagging_System SHALL prompt for resolution
5. THE Tagging_System SHALL provide progress reporting and error handling for batch operations
### Requirement 9: Security and Sensitive Content Handling
**User Story:** As a security-conscious user, I want sensitive files properly tagged and handled, so that I can manage access and visibility appropriately.
#### Acceptance Criteria
1. WHEN files contain credentials or API keys, THE Tagging_System SHALL add 'sensitive/credentials' tag
2. WHEN files contain personal information, THE Tagging_System SHALL add 'sensitive/personal' tag
3. THE Tagging_System SHALL detect and tag files with financial or legal information
4. WHEN sensitive content is detected, THE Tagging_System SHALL suggest security best practices
5. THE Tagging_System SHALL respect existing security classifications and not expose sensitive data
### Requirement 10: Integration with Obsidian Features
**User Story:** As an Obsidian user, I want tags that work seamlessly with Obsidian's built-in features, so that I can leverage the full power of the application.
#### Acceptance Criteria
1. THE Tagging_System SHALL generate tags compatible with Obsidian's tag pane and search
2. THE Tagging_System SHALL support Dataview queries and tag-based filtering
3. WHEN creating hub files, THE Tagging_System SHALL include appropriate Dataview queries for tag-based content listing
4. THE Tagging_System SHALL ensure tags work with graph view and tag-based navigation
5. THE Tagging_System SHALL maintain compatibility with Obsidian plugins that use tags
@@ -0,0 +1,153 @@
# Implementation Plan: Comprehensive Tagging System
## Overview
This implementation plan creates a Python-based comprehensive tagging system for the Obsidian vault. The system will analyze files, generate appropriate tags based on directory structure and content analysis, and update frontmatter while preserving existing data. The implementation follows a modular architecture with clear separation of concerns.
## Tasks
- [x] 1. Set up project structure and core interfaces
- Create Python package structure with proper modules
- Define core data models and interfaces using dataclasses and protocols
- Set up testing framework with pytest and hypothesis for property-based testing
- Create configuration system for tag hierarchies and rules
- _Requirements: 1.1, 1.2, 1.3_
- [x] 2. Implement file discovery and content analysis
- [x] 2.1 Create FileDiscovery class for vault scanning
- Implement recursive directory scanning with file filtering
- Add support for excluding sensitive directories and file types
- Handle file encoding detection and content reading
- _Requirements: 8.1, 9.5_
- [ ]* 2.2 Write property test for file discovery
- **Property 8: Data preservation during processing**
- **Validates: Requirements 8.1, 8.2, 8.3, 8.5**
- [x] 2.3 Implement ContentAnalyzer class
- Create language detection using character frequency analysis
- Implement content type classification based on patterns and keywords
- Add topic extraction using keyword analysis and NLP techniques
- Build entity extraction for tools, technologies, and frameworks
- _Requirements: 5.4, 3.4, 6.1, 6.2, 6.3_
- [ ]* 2.4 Write property tests for content analysis
- **Property 5: Language detection and tagging**
- **Property 3: Content type classification**
- **Property 6: Content analysis tag generation**
- **Validates: Requirements 5.1-5.5, 3.1-3.5, 6.1-6.5**
- [x] 3. Implement tag generation system
- [x] 3.1 Create TagGenerator class
- Implement directory-based tag generation from file paths
- Build hierarchical tag creation using topic analysis
- Add tag consolidation and deduplication logic
- Create tag validation and naming convention enforcement
- _Requirements: 2.6, 4.1, 7.1, 7.2, 7.3_
- [ ]* 3.2 Write property tests for tag generation
- **Property 2: Directory-based tag mapping**
- **Property 4: Hierarchical topic tagging**
- **Property 7: Tag consistency and validation**
- **Validates: Requirements 2.1-2.6, 4.1-4.6, 7.1-7.5**
- [x] 3.3 Implement sensitive content detection
- Create pattern matching for credentials, API keys, and personal information
- Add financial and legal content detection
- Implement secure handling to avoid exposing sensitive data
- _Requirements: 9.1, 9.2, 9.3, 9.5_
- [ ]* 3.4 Write property test for sensitive content detection
- **Property 9: Sensitive content detection and protection**
- **Validates: Requirements 9.1, 9.2, 9.3, 9.5**
- [ ] 4. Checkpoint - Core functionality validation
- Ensure all core components work together correctly
- Run property tests to validate tag generation logic
- Test with sample files from different vault directories
- Ask the user if questions arise about tag hierarchies or classification rules
- [ ] 5. Implement frontmatter management
- [ ] 5.1 Create FrontmatterManager class
- Implement YAML frontmatter parsing with error handling
- Build frontmatter updating while preserving existing data
- Add validation for frontmatter structure and required fields
- Create backup functionality before file modifications
- _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5, 8.2, 8.3_
- [ ]* 5.2 Write property tests for frontmatter management
- **Property 1: Frontmatter standardization**
- **Validates: Requirements 1.1, 1.2, 1.3, 1.5**
- [ ] 5.3 Implement batch processing system
- Create BatchProcessor class for handling large-scale operations
- Add progress tracking and error handling
- Implement resume functionality for interrupted processing
- Build memory-efficient processing for large vaults
- _Requirements: 8.1, 8.2, 8.5_
- [ ]* 5.4 Write unit tests for batch processing
- Test error recovery and resume functionality
- Test memory management with large file sets
- Test progress reporting accuracy
- _Requirements: 8.1, 8.2, 8.5_
- [ ] 6. Implement Obsidian compatibility features
- [ ] 6.1 Create ObsidianCompatibility class
- Ensure tag format compatibility with Obsidian tag system
- Generate Dataview queries for hub files
- Validate tags work with graph view and common plugins
- Test tag search and filtering functionality
- _Requirements: 10.1, 10.2, 10.3, 10.4, 10.5_
- [ ]* 6.2 Write property test for Obsidian compatibility
- **Property 10: Obsidian compatibility**
- **Validates: Requirements 10.1, 10.2, 10.3, 10.4, 10.5**
- [ ] 7. Create command-line interface and configuration
- [ ] 7.1 Build CLI using argparse or click
- Add commands for full vault processing, single file processing, and validation
- Implement configuration file support for custom tag hierarchies
- Add dry-run mode for testing without file modifications
- Create verbose logging and progress reporting
- _Requirements: 8.1, 8.5_
- [ ]* 7.2 Write integration tests for CLI
- Test full vault processing workflow
- Test configuration file loading and validation
- Test dry-run mode accuracy
- _Requirements: 8.1, 8.5_
- [ ] 8. Integration and comprehensive testing
- [ ] 8.1 Wire all components together in main application
- Create main TaggingSystem class that orchestrates all components
- Implement error handling and logging throughout the system
- Add configuration validation and setup
- _Requirements: All requirements_
- [ ]* 8.2 Write end-to-end property tests
- Test complete workflow from file discovery to frontmatter update
- Validate all properties work together in integrated system
- Test with realistic vault structure and content
- _Requirements: All requirements_
- [ ] 8.3 Create comprehensive test suite
- Add performance tests for large vault processing
- Create regression tests for edge cases
- Build test data generators for various content types
- _Requirements: All requirements_
- [ ] 9. Final checkpoint and documentation
- Ensure all property tests pass with 100+ iterations each
- Validate system works with actual vault content
- Create usage documentation and configuration examples
- Ask the user if questions arise about final implementation
## Notes
- Tasks marked with `*` are optional and can be skipped for faster MVP
- Each task references specific requirements for traceability
- Property tests validate universal correctness properties with randomized inputs
- Unit tests validate specific examples and edge cases
- The system preserves existing data and creates backups before modifications
- All components are designed to be modular and testable independently
+14 -14
View File
@@ -4,11 +4,11 @@
"type": "split",
"children": [
{
"id": "1fb37ab04f75243e",
"id": "7515b9d3e7171289",
"type": "tabs",
"children": [
{
"id": "bad2139edeaee088",
"id": "d5584fd860b652a6",
"type": "leaf",
"state": {
"type": "empty",
@@ -154,7 +154,7 @@
"searchQuery": ""
},
"icon": "lucide-tags",
"title": "Tags"
"title": "标签"
}
},
{
@@ -168,7 +168,7 @@
"searchQuery": ""
},
"icon": "lucide-archive",
"title": "All properties"
"title": "添加笔记属性"
}
},
{
@@ -177,8 +177,8 @@
"state": {
"type": "advanced-tables-toolbar",
"state": {},
"icon": "lucide-ghost",
"title": "advanced-tables-toolbar"
"icon": "spreadsheet",
"title": "Advanced Tables"
}
},
{
@@ -187,8 +187,8 @@
"state": {
"type": "calendar",
"state": {},
"icon": "lucide-ghost",
"title": "calendar"
"icon": "calendar-with-checkmark",
"title": "Calendar"
}
},
{
@@ -197,8 +197,8 @@
"state": {
"type": "smart-connections-view",
"state": {},
"icon": "lucide-ghost",
"title": "smart-connections-view"
"icon": "smart-connections",
"title": "Connections"
}
},
{
@@ -250,8 +250,11 @@
"smart-connections:Smart Connections: Open random connection": false
}
},
"active": "ec72abdaaa8e91a6",
"active": "d5584fd860b652a6",
"lastOpenFiles": [
"未命名看板.md",
"Excalidraw/Drawing 2025-12-30 19.55.02.excalidraw.md",
"Excalidraw",
"200-area/Blog/Writing cheatsheet.md",
"200-area/Blog/Hugo Version Change.md",
"200-area/Blog/Feedback sessions.md",
@@ -276,8 +279,6 @@
"400-archive/security-sensitive/Apple App Password.md",
"400-archive/security-sensitive/Cookies.md",
"400-archive/security-sensitive/Domains.md",
"400-archive/security-sensitive/Matrix Server.md",
"400-archive/security-sensitive/README.md",
"100-project/AI/Agent",
"100-project/Home-Automation/Hardware/Matter",
"100-project/Home-Automation/南电",
@@ -287,7 +288,6 @@
"100-project/Infrastructure/Services/PowerDNS Auth",
"100-project/Infrastructure/Proxy/Clash",
"100-project/Infrastructure/Mail/Config",
"100-project/Infrastructure/VPS",
"300-resources/Development/Architecture/arc42/images/arc42-logo.png",
"300-resources/Development/Architecture/arc42/images/08-Crosscutting-Concepts-Structure-EN.png",
"300-resources/Development/Architecture/arc42/images/05_building_blocks-EN.png",
@@ -0,0 +1,14 @@
---
excalidraw-plugin: parsed
tags: [excalidraw]
---
==⚠ Switch to EXCALIDRAW VIEW in the MORE OPTIONS menu of this document. ⚠== You can decompress Drawing data with the command palette: 'Decompress current Excalidraw file'. For more info check in plugin settings under 'Saving'
## Drawing
```compressed-json
N4IgLgngDgpiBcIYA8DGBDANgSwCYCd0B3EAGhADcZ8BnbAewDsEAmcm+gV31TkQAswYKDXgB6MQHNsYfpwBGAOlT0AtmIBeNCtlQbs6RmPry6uA4wC0KDDgLFLUTJ2lH8MTDHQ0YNMWHRJMRZFAEYADkUAZjIkT1UYRjAaBABtAF1ydCgoAGUAsD5QSXw8XOwNPkZOTExyHRgiACF0VABrEq5GXABhekx6fAQQAGIAMwnJkABfaaA==
```
%%
+103
View File
@@ -0,0 +1,103 @@
# Comprehensive Tagging System
A Python-based comprehensive tagging system for Obsidian vaults that analyzes files, generates appropriate tags based on directory structure and content analysis, and updates frontmatter while preserving existing data.
## Features
- **Directory-based tagging**: Automatically generates tags based on vault directory structure
- **Content analysis**: Analyzes file content to extract topics, technologies, and entities
- **Hierarchical tag structures**: Creates organized tag hierarchies using forward slash notation
- **Language detection**: Identifies content language (English, Chinese, mixed, unknown)
- **Frontmatter management**: Updates YAML frontmatter while preserving existing data
- **Batch processing**: Processes entire vaults efficiently with progress tracking
- **Sensitive content detection**: Identifies and appropriately tags sensitive information
- **Obsidian compatibility**: Ensures tags work with Obsidian's features and plugins
## Installation
```bash
pip install -e .
```
## Development Setup
```bash
# Install development dependencies
pip install -e ".[dev,test]"
# Run tests
pytest
# Run tests with coverage
pytest --cov=tagging_system
# Format code
black tagging_system tests
# Lint code
flake8 tagging_system tests
# Type checking
mypy tagging_system
```
## Project Structure
```
tagging_system/
├── __init__.py
├── core/
│ ├── __init__.py
│ ├── models.py # Core data models
│ └── interfaces.py # Abstract interfaces and protocols
├── config/
│ ├── __init__.py
│ └── config.py # Configuration system
└── implementations/ # Concrete implementations (to be added)
tests/
├── __init__.py
├── conftest.py # Pytest configuration and fixtures
├── test_models.py # Tests for core models
├── test_config.py # Tests for configuration system
└── test_interfaces.py # Tests for interfaces and protocols
```
## Configuration
The system uses a flexible configuration system that supports both YAML and JSON formats. Configuration includes:
- Directory mappings for tag generation
- Tag hierarchies and structures
- Sensitive content detection patterns
- File processing settings
- Language detection parameters
## Testing
The project uses pytest with hypothesis for property-based testing:
- **Unit tests**: Test specific functionality and edge cases
- **Property-based tests**: Test universal properties across randomized inputs
- **Integration tests**: Test component interactions
Run tests with:
```bash
pytest # Run all tests
pytest -m unit # Run only unit tests
pytest -m property # Run only property-based tests
pytest -v # Verbose output
```
## Requirements
This implementation addresses the following requirements:
- 1.1, 1.2, 1.3: Standardized frontmatter structure
- Directory-based tag mapping
- Content analysis and classification
- Tag consistency and validation
- Batch processing capabilities
## License
MIT License
+30
View File
@@ -0,0 +1,30 @@
# Example configuration for the comprehensive tagging system
# Copy this file to config.yaml and customize as needed
# Directories to exclude from processing
excluded_directories:
- ".obsidian"
- ".git"
- ".smart-env"
- "node_modules"
- "__pycache__"
# File patterns to exclude
excluded_file_patterns:
- "*.pyc"
- "*.log"
- "*.tmp"
- ".DS_Store"
# Tag formatting rules
tag_format_rules:
case: "kebab" # Use kebab-case for tags
max_length: 50
allowed_chars: "abcdefghijklmnopqrstuvwxyz0123456789-/"
hierarchy_separator: "/"
# Language detection settings
language_detection:
chinese_threshold: 0.1 # Minimum ratio of Chinese characters to detect Chinese
mixed_threshold: 0.3 # Threshold for mixed language detection
min_content_length: 50 # Minimum content length for reliable detection
+18
View File
@@ -0,0 +1,18 @@
[tool:pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts =
--verbose
--tb=short
--strict-markers
--disable-warnings
--cov=tagging_system
--cov-report=term-missing
--cov-report=html:htmlcov
markers =
unit: Unit tests
property: Property-based tests
integration: Integration tests
slow: Slow running tests
+14
View File
@@ -0,0 +1,14 @@
# Core dependencies
pyyaml>=6.0
python-frontmatter>=1.0.0
chardet>=5.0.0
# Testing dependencies
pytest>=7.0.0
hypothesis>=6.0.0
pytest-cov>=4.0.0
# Development dependencies
black>=22.0.0
flake8>=5.0.0
mypy>=1.0.0
+49
View File
@@ -0,0 +1,49 @@
"""Setup script for the comprehensive tagging system."""
from setuptools import setup, find_packages
with open("README.md", "r", encoding="utf-8") as fh:
long_description = fh.read()
with open("requirements.txt", "r", encoding="utf-8") as fh:
requirements = [line.strip() for line in fh if line.strip() and not line.startswith("#")]
setup(
name="comprehensive-tagging-system",
version="0.1.0",
author="Tagging System",
description="A comprehensive tagging system for Obsidian vaults",
long_description=long_description,
long_description_content_type="text/markdown",
packages=find_packages(),
classifiers=[
"Development Status :: 3 - Alpha",
"Intended Audience :: End Users/Desktop",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
],
python_requires=">=3.8",
install_requires=requirements,
extras_require={
"dev": [
"black>=22.0.0",
"flake8>=5.0.0",
"mypy>=1.0.0",
],
"test": [
"pytest>=7.0.0",
"hypothesis>=6.0.0",
"pytest-cov>=4.0.0",
],
},
entry_points={
"console_scripts": [
"tagging-system=tagging_system.cli:main",
],
},
)
+41
View File
@@ -0,0 +1,41 @@
"""
Comprehensive Tagging System for Obsidian Vault
A Python-based system for analyzing files, generating appropriate tags based on
directory structure and content analysis, and updating frontmatter while
preserving existing data.
"""
__version__ = "0.1.0"
__author__ = "Tagging System"
from .core.models import (
FileInfo,
ContentAnalysis,
TagStructure,
FrontmatterData,
ContentType,
LanguageInfo,
ValidationResult
)
from .core.interfaces import (
FileDiscovery,
ContentAnalyzer,
TagGenerator,
FrontmatterManager
)
__all__ = [
"FileInfo",
"ContentAnalysis",
"TagStructure",
"FrontmatterData",
"ContentType",
"LanguageInfo",
"ValidationResult",
"FileDiscovery",
"ContentAnalyzer",
"TagGenerator",
"FrontmatterManager"
]
Binary file not shown.
Binary file not shown.
+73
View File
@@ -0,0 +1,73 @@
"""Command-line interface for the tagging system."""
import argparse
import sys
from pathlib import Path
from typing import Optional
from .config import load_config
def main():
"""Main entry point for the CLI."""
parser = argparse.ArgumentParser(
description="Comprehensive Tagging System for Obsidian Vaults"
)
parser.add_argument(
"vault_path",
type=str,
help="Path to the Obsidian vault directory"
)
parser.add_argument(
"--config",
type=str,
help="Path to configuration file (YAML or JSON)"
)
parser.add_argument(
"--dry-run",
action="store_true",
help="Show what would be done without making changes"
)
parser.add_argument(
"--verbose",
"-v",
action="store_true",
help="Enable verbose output"
)
args = parser.parse_args()
# Validate vault path
vault_path = Path(args.vault_path)
if not vault_path.exists():
print(f"Error: Vault path '{vault_path}' does not exist")
sys.exit(1)
if not vault_path.is_dir():
print(f"Error: Vault path '{vault_path}' is not a directory")
sys.exit(1)
# Load configuration
config = load_config(args.config)
if args.verbose:
print(f"Vault path: {vault_path}")
print(f"Configuration: {args.config or 'default'}")
print(f"Dry run: {args.dry_run}")
# TODO: Implement actual tagging logic in future tasks
print("Tagging system setup complete!")
print("Note: Core implementation will be added in subsequent tasks.")
if args.dry_run:
print("Dry run mode - no files would be modified")
return 0
if __name__ == "__main__":
sys.exit(main())
+12
View File
@@ -0,0 +1,12 @@
"""Configuration system for the tagging system."""
from .config import TaggingConfig, DirectoryMapping, TagHierarchy, SensitivePatterns, load_config, save_config
__all__ = [
"TaggingConfig",
"DirectoryMapping",
"TagHierarchy",
"SensitivePatterns",
"load_config",
"save_config"
]
+223
View File
@@ -0,0 +1,223 @@
"""Configuration system for tag hierarchies and rules."""
import json
import yaml
from dataclasses import dataclass, field
from pathlib import Path
from typing import Dict, List, Optional, Any, Union
@dataclass
class DirectoryMapping:
"""Configuration for directory-based tag mapping."""
pattern: str # Directory pattern to match
primary_tag: str # Primary tag to assign
hierarchical_tags: List[str] = field(default_factory=list) # Additional hierarchical tags
exclude_patterns: List[str] = field(default_factory=list) # Patterns to exclude
@dataclass
class TagHierarchy:
"""Configuration for hierarchical tag structures."""
root: str # Root tag name
children: Dict[str, 'TagHierarchy'] = field(default_factory=dict) # Child hierarchies
aliases: List[str] = field(default_factory=list) # Alternative names
def get_full_path(self, child_path: str = "") -> str:
"""Get full hierarchical path."""
if child_path:
return f"{self.root}/{child_path}"
return self.root
@dataclass
class SensitivePatterns:
"""Configuration for sensitive content detection."""
credential_patterns: List[str] = field(default_factory=lambda: [
r'api[_-]?key',
r'secret[_-]?key',
r'password',
r'token',
r'auth[_-]?token'
])
personal_patterns: List[str] = field(default_factory=lambda: [
r'\b\d{3}-\d{2}-\d{4}\b', # SSN pattern
r'\b\d{4}[- ]?\d{4}[- ]?\d{4}[- ]?\d{4}\b', # Credit card pattern
r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b' # Email pattern
])
financial_patterns: List[str] = field(default_factory=lambda: [
r'bank[_-]?account',
r'routing[_-]?number',
r'credit[_-]?card',
r'social[_-]?security'
])
@dataclass
class TaggingConfig:
"""Main configuration for the tagging system."""
# Directory mappings
directory_mappings: List[DirectoryMapping] = field(default_factory=lambda: [
DirectoryMapping("100-project", "project"),
DirectoryMapping("200-area", "area"),
DirectoryMapping("300-resources", "resource"),
DirectoryMapping("400-archive", "archive"),
DirectoryMapping("Clippings", "clipping"),
DirectoryMapping("ReadItLater Inbox", "clipping"),
DirectoryMapping("000-inbox", "inbox")
])
# Tag hierarchies
tag_hierarchies: Dict[str, TagHierarchy] = field(default_factory=lambda: {
"tech": TagHierarchy("tech", {
"ai": TagHierarchy("ai", {
"llm": TagHierarchy("llm"),
"ml": TagHierarchy("ml"),
"nlp": TagHierarchy("nlp")
}),
"infrastructure": TagHierarchy("infrastructure", {
"docker": TagHierarchy("docker"),
"kubernetes": TagHierarchy("kubernetes"),
"cloud": TagHierarchy("cloud")
}),
"development": TagHierarchy("development", {
"python": TagHierarchy("python"),
"javascript": TagHierarchy("javascript"),
"typescript": TagHierarchy("typescript")
})
}),
"personal": TagHierarchy("personal", {
"productivity": TagHierarchy("productivity", {
"gtd": TagHierarchy("gtd"),
"pkm": TagHierarchy("pkm")
}),
"health": TagHierarchy("health", {
"cycling": TagHierarchy("cycling"),
"fitness": TagHierarchy("fitness")
}),
"finance": TagHierarchy("finance")
}),
"work": TagHierarchy("work", {
"government": TagHierarchy("government"),
"enterprise": TagHierarchy("enterprise"),
"consulting": TagHierarchy("consulting")
})
})
# Sensitive content patterns
sensitive_patterns: SensitivePatterns = field(default_factory=SensitivePatterns)
# File processing settings
excluded_directories: List[str] = field(default_factory=lambda: [
".obsidian",
".git",
".smart-env",
"node_modules",
"__pycache__"
])
excluded_file_patterns: List[str] = field(default_factory=lambda: [
"*.pyc",
"*.log",
"*.tmp",
".DS_Store"
])
# Tag formatting rules
tag_format_rules: Dict[str, Any] = field(default_factory=lambda: {
"case": "kebab", # kebab-case for tags
"max_length": 50,
"allowed_chars": "abcdefghijklmnopqrstuvwxyz0123456789-/",
"hierarchy_separator": "/"
})
# Language detection settings
language_detection: Dict[str, Any] = field(default_factory=lambda: {
"chinese_threshold": 0.1, # Minimum ratio of Chinese characters
"mixed_threshold": 0.3, # Threshold for mixed language detection
"min_content_length": 50 # Minimum content length for reliable detection
})
def get_directory_mapping(self, directory_path: str) -> Optional[DirectoryMapping]:
"""Get directory mapping for a given path."""
for mapping in self.directory_mappings:
if mapping.pattern in directory_path:
return mapping
return None
def get_tag_hierarchy(self, root_tag: str) -> Optional[TagHierarchy]:
"""Get tag hierarchy for a root tag."""
return self.tag_hierarchies.get(root_tag)
def is_excluded_directory(self, directory: str) -> bool:
"""Check if directory should be excluded."""
return any(excluded in directory for excluded in self.excluded_directories)
def is_excluded_file(self, filename: str) -> bool:
"""Check if file should be excluded based on patterns."""
import fnmatch
return any(fnmatch.fnmatch(filename, pattern) for pattern in self.excluded_file_patterns)
def load_config(config_path: Optional[Union[str, Path]] = None) -> TaggingConfig:
"""Load configuration from file or return default configuration."""
if config_path is None:
return TaggingConfig()
config_path = Path(config_path)
if not config_path.exists():
# Create default config file
default_config = TaggingConfig()
save_config(default_config, config_path)
return default_config
try:
with open(config_path, 'r', encoding='utf-8') as f:
if config_path.suffix.lower() == '.json':
data = json.load(f)
else: # Assume YAML
data = yaml.safe_load(f)
# Convert dict to TaggingConfig (simplified conversion)
# In a full implementation, you'd want more robust deserialization
config = TaggingConfig()
# Update with loaded data
if 'excluded_directories' in data:
config.excluded_directories = data['excluded_directories']
if 'excluded_file_patterns' in data:
config.excluded_file_patterns = data['excluded_file_patterns']
if 'tag_format_rules' in data:
config.tag_format_rules.update(data['tag_format_rules'])
if 'language_detection' in data:
config.language_detection.update(data['language_detection'])
return config
except Exception as e:
print(f"Error loading config from {config_path}: {e}")
return TaggingConfig()
def save_config(config: TaggingConfig, config_path: Union[str, Path]) -> None:
"""Save configuration to file."""
config_path = Path(config_path)
config_path.parent.mkdir(parents=True, exist_ok=True)
# Convert to dict for serialization (simplified)
config_dict = {
'excluded_directories': config.excluded_directories,
'excluded_file_patterns': config.excluded_file_patterns,
'tag_format_rules': config.tag_format_rules,
'language_detection': config.language_detection
}
try:
with open(config_path, 'w', encoding='utf-8') as f:
if config_path.suffix.lower() == '.json':
json.dump(config_dict, f, indent=2)
else: # Save as YAML
yaml.dump(config_dict, f, default_flow_style=False, allow_unicode=True)
except Exception as e:
print(f"Error saving config to {config_path}: {e}")
+32
View File
@@ -0,0 +1,32 @@
"""Core components for the tagging system."""
from .models import (
FileInfo,
ContentAnalysis,
TagStructure,
FrontmatterData,
ContentType,
LanguageInfo,
ValidationResult
)
from .interfaces import (
FileDiscovery,
ContentAnalyzer,
TagGenerator,
FrontmatterManager
)
__all__ = [
"FileInfo",
"ContentAnalysis",
"TagStructure",
"FrontmatterData",
"ContentType",
"LanguageInfo",
"ValidationResult",
"FileDiscovery",
"ContentAnalyzer",
"TagGenerator",
"FrontmatterManager"
]
+167
View File
@@ -0,0 +1,167 @@
"""Core interfaces and protocols for the tagging system."""
from abc import ABC, abstractmethod
from typing import List, Protocol, runtime_checkable
from .models import FileInfo, ContentAnalysis, TagStructure, FrontmatterData, ValidationResult
@runtime_checkable
class FileDiscovery(Protocol):
"""Protocol for file discovery operations."""
def scan_directory(self, path: str) -> List[FileInfo]:
"""Recursively scan directory and return file information."""
...
def filter_by_type(self, files: List[FileInfo], types: List[str]) -> List[FileInfo]:
"""Filter files by extension types."""
...
def exclude_sensitive(self, files: List[FileInfo]) -> List[FileInfo]:
"""Exclude sensitive directories and files."""
...
@runtime_checkable
class ContentAnalyzer(Protocol):
"""Protocol for content analysis operations."""
def analyze_content(self, content: str) -> ContentAnalysis:
"""Analyze file content and return analysis results."""
...
def detect_language(self, content: str) -> str:
"""Detect the primary language of the content."""
...
def extract_topics(self, content: str) -> List[str]:
"""Extract topics from content."""
...
def classify_content_type(self, content: str, filename: str) -> str:
"""Classify the type of content."""
...
@runtime_checkable
class TagGenerator(Protocol):
"""Protocol for tag generation operations."""
def generate_directory_tags(self, filepath: str) -> List[str]:
"""Generate tags based on directory structure."""
...
def generate_content_tags(self, analysis: ContentAnalysis) -> List[str]:
"""Generate tags based on content analysis."""
...
def generate_hierarchical_tags(self, topics: List[str]) -> List[str]:
"""Generate hierarchical tags from topics."""
...
def consolidate_tags(self, tags: List[str]) -> List[str]:
"""Consolidate and deduplicate tags."""
...
@runtime_checkable
class FrontmatterManager(Protocol):
"""Protocol for frontmatter management operations."""
def parse_frontmatter(self, content: str) -> FrontmatterData:
"""Parse YAML frontmatter from content."""
...
def update_frontmatter(self, content: str, updates: FrontmatterData) -> str:
"""Update frontmatter in content while preserving existing data."""
...
def validate_frontmatter(self, data: FrontmatterData) -> ValidationResult:
"""Validate frontmatter structure and content."""
...
class BaseFileDiscovery(ABC):
"""Abstract base class for file discovery implementations."""
@abstractmethod
def scan_directory(self, path: str) -> List[FileInfo]:
"""Recursively scan directory and return file information."""
pass
@abstractmethod
def filter_by_type(self, files: List[FileInfo], types: List[str]) -> List[FileInfo]:
"""Filter files by extension types."""
pass
@abstractmethod
def exclude_sensitive(self, files: List[FileInfo]) -> List[FileInfo]:
"""Exclude sensitive directories and files."""
pass
class BaseContentAnalyzer(ABC):
"""Abstract base class for content analyzer implementations."""
@abstractmethod
def analyze_content(self, content: str) -> ContentAnalysis:
"""Analyze file content and return analysis results."""
pass
@abstractmethod
def detect_language(self, content: str) -> str:
"""Detect the primary language of the content."""
pass
@abstractmethod
def extract_topics(self, content: str) -> List[str]:
"""Extract topics from content."""
pass
@abstractmethod
def classify_content_type(self, content: str, filename: str) -> str:
"""Classify the type of content."""
pass
class BaseTagGenerator(ABC):
"""Abstract base class for tag generator implementations."""
@abstractmethod
def generate_directory_tags(self, filepath: str) -> List[str]:
"""Generate tags based on directory structure."""
pass
@abstractmethod
def generate_content_tags(self, analysis: ContentAnalysis) -> List[str]:
"""Generate tags based on content analysis."""
pass
@abstractmethod
def generate_hierarchical_tags(self, topics: List[str]) -> List[str]:
"""Generate hierarchical tags from topics."""
pass
@abstractmethod
def consolidate_tags(self, tags: List[str]) -> List[str]:
"""Consolidate and deduplicate tags."""
pass
class BaseFrontmatterManager(ABC):
"""Abstract base class for frontmatter manager implementations."""
@abstractmethod
def parse_frontmatter(self, content: str) -> FrontmatterData:
"""Parse YAML frontmatter from content."""
pass
@abstractmethod
def update_frontmatter(self, content: str, updates: FrontmatterData) -> str:
"""Update frontmatter in content while preserving existing data."""
pass
@abstractmethod
def validate_frontmatter(self, data: FrontmatterData) -> ValidationResult:
"""Validate frontmatter structure and content."""
pass
+158
View File
@@ -0,0 +1,158 @@
"""Core data models for the tagging system."""
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
from typing import List, Dict, Optional, Any
class ContentType(Enum):
"""Content type classifications."""
HUB = "hub"
NOTE = "note"
CLIPPING = "clipping"
DAILY_NOTE = "daily-note"
MEETING = "meeting"
DOCUMENTATION = "documentation"
TUTORIAL = "tutorial"
REFERENCE = "reference"
DRAFT = "draft"
PROJECT = "project"
UNKNOWN = "unknown"
class LanguageInfo(Enum):
"""Language detection results."""
ENGLISH = "en"
CHINESE = "zh"
MIXED = "mixed"
UNKNOWN = "unknown"
@dataclass
class FileInfo:
"""Information about a file in the vault."""
path: str
name: str
directory: str
extension: str
size: int
created: datetime
modified: datetime
content: Optional[str] = None
@property
def relative_path(self) -> str:
"""Get the relative path from vault root."""
return self.path
@property
def is_markdown(self) -> bool:
"""Check if file is a markdown file."""
return self.extension.lower() in ['.md', '.markdown']
@dataclass
class ContentAnalysis:
"""Results of content analysis."""
language: LanguageInfo
content_type: ContentType
topics: List[str]
mentions: Dict[str, List[str]] = field(default_factory=lambda: {
'tools': [],
'technologies': [],
'people': [],
'organizations': []
})
sentiment: Optional[str] = None
complexity: str = "basic"
def __post_init__(self):
"""Validate complexity level."""
if self.complexity not in ['basic', 'intermediate', 'advanced']:
self.complexity = 'basic'
@dataclass
class TagStructure:
"""Structured representation of tags."""
primary: List[str] = field(default_factory=list) # Main category tags
hierarchical: List[str] = field(default_factory=list) # Topic/subtopic/detail tags
content: List[str] = field(default_factory=list) # Content-derived tags
meta: List[str] = field(default_factory=list) # Metadata tags (language, type, etc.)
custom: List[str] = field(default_factory=list) # Manually added tags to preserve
def all_tags(self) -> List[str]:
"""Get all tags as a flat list."""
all_tags = []
all_tags.extend(self.primary)
all_tags.extend(self.hierarchical)
all_tags.extend(self.content)
all_tags.extend(self.meta)
all_tags.extend(self.custom)
return list(set(all_tags)) # Remove duplicates
@dataclass
class FrontmatterData:
"""YAML frontmatter data structure."""
title: Optional[str] = None
tags: List[str] = field(default_factory=list)
created: Optional[str] = None
updated: Optional[str] = None
type: Optional[str] = None
lang: Optional[str] = None
source: Optional[str] = None
aliases: List[str] = field(default_factory=list)
description: Optional[str] = None
custom_fields: Dict[str, Any] = field(default_factory=dict)
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary for YAML serialization."""
result = {}
if self.title:
result['title'] = self.title
if self.tags:
result['tags'] = self.tags
if self.created:
result['created'] = self.created
if self.updated:
result['updated'] = self.updated
if self.type:
result['type'] = self.type
if self.lang:
result['lang'] = self.lang
if self.source:
result['source'] = self.source
if self.aliases:
result['aliases'] = self.aliases
if self.description:
result['description'] = self.description
# Add custom fields
result.update(self.custom_fields)
return result
@dataclass
class ValidationResult:
"""Result of validation operations."""
is_valid: bool
errors: List[str] = field(default_factory=list)
warnings: List[str] = field(default_factory=list)
suggestions: List[str] = field(default_factory=list)
def add_error(self, message: str):
"""Add an error message."""
self.errors.append(message)
self.is_valid = False
def add_warning(self, message: str):
"""Add a warning message."""
self.warnings.append(message)
def add_suggestion(self, message: str):
"""Add a suggestion message."""
self.suggestions.append(message)
+7
View File
@@ -0,0 +1,7 @@
"""Implementation modules for the tagging system."""
from .file_discovery import VaultFileDiscovery
from .content_analyzer import VaultContentAnalyzer
from .tag_generator import TagGeneratorImpl
__all__ = ['VaultFileDiscovery', 'VaultContentAnalyzer', 'TagGeneratorImpl']
+303
View File
@@ -0,0 +1,303 @@
"""Content analysis implementation for extracting topics, language, and content type."""
import re
from collections import Counter
from typing import List, Dict, Set, Tuple
from ..core.interfaces import BaseContentAnalyzer
from ..core.models import ContentAnalysis, ContentType, LanguageInfo
class VaultContentAnalyzer(BaseContentAnalyzer):
"""Implementation of content analyzer for Obsidian vault content."""
def __init__(self):
"""Initialize the content analyzer with patterns and keywords."""
# Language detection patterns
self.chinese_chars = re.compile(r'[\u4e00-\u9fff]')
self.english_chars = re.compile(r'[a-zA-Z]')
# Content type patterns
self.content_type_patterns = {
ContentType.HUB: [
r'# .+\n\n.*(?:index|hub|overview|contents?)',
r'## (?:Projects?|Areas?|Resources?|Archive)',
r'dataview\s*```',
r'!\[\[.*\]\].*!\[\[.*\]\]', # Multiple embeds
],
ContentType.CLIPPING: [
r'source:\s*https?://',
r'clipped from:',
r'saved from:',
r'ReadItLater',
r'# .+ - .+\.com',
],
ContentType.DAILY_NOTE: [
r'^\d{4}-\d{2}-\d{2}',
r'# \d{4}-\d{2}-\d{2}',
r'## Daily Notes?',
r'## Today',
],
ContentType.MEETING: [
r'# Meeting:',
r'## Attendees?',
r'## Action Items?',
r'## Minutes',
r'meeting notes?',
],
ContentType.DOCUMENTATION: [
r'# (?:API|Documentation|Guide|Manual)',
r'## Installation',
r'## Usage',
r'## Configuration',
r'```(?:bash|shell|cmd)',
],
ContentType.TUTORIAL: [
r'# (?:How to|Tutorial|Step by Step)',
r'## Step \d+',
r'### Prerequisites?',
r'## Getting Started',
],
ContentType.REFERENCE: [
r'# (?:Reference|Cheat ?Sheet|Quick Reference)',
r'## Commands?',
r'## Syntax',
r'## Examples?',
],
ContentType.DRAFT: [
r'=Draft=',
r'# Draft:',
r'status:\s*draft',
r'TODO:',
r'FIXME:',
],
}
# Technology and tool keywords
self.tech_keywords = {
'ai': ['gpt', 'llm', 'chatgpt', 'openai', 'claude', 'anthropic', 'deepseek', 'ollama'],
'infrastructure': ['docker', 'kubernetes', 'aws', 'azure', 'gcp', 'terraform', 'ansible'],
'development': ['python', 'javascript', 'typescript', 'react', 'vue', 'node', 'npm', 'yarn'],
'database': ['mysql', 'postgresql', 'mongodb', 'redis', 'sqlite', 'oracle'],
'web': ['html', 'css', 'http', 'api', 'rest', 'graphql', 'json', 'xml'],
'devops': ['ci/cd', 'jenkins', 'github actions', 'gitlab', 'git', 'version control'],
'network': ['vpn', 'proxy', 'nginx', 'apache', 'dns', 'ssl', 'tls'],
'security': ['encryption', 'authentication', 'authorization', 'oauth', 'jwt', 'ssl'],
'mobile': ['ios', 'android', 'react native', 'flutter', 'swift', 'kotlin'],
'home-automation': ['home assistant', 'zigbee', 'mqtt', 'esphome', 'tuya'],
}
# Topic extraction patterns
self.topic_patterns = {
'project_management': ['project', 'task', 'milestone', 'deadline', 'planning'],
'productivity': ['gtd', 'productivity', 'workflow', 'automation', 'efficiency'],
'health': ['health', 'fitness', 'exercise', 'cycling', 'nutrition'],
'finance': ['budget', 'investment', 'money', 'financial', 'ynab'],
'cooking': ['recipe', 'cooking', 'ingredient', 'meal', 'food'],
'travel': ['travel', 'trip', 'vacation', 'hotel', 'flight'],
'gaming': ['game', 'gaming', 'steam', 'console', 'multiplayer'],
'writing': ['blog', 'article', 'writing', 'content', 'publish'],
}
# Entity extraction patterns
self.entity_patterns = {
'tools': re.compile(r'\b(?:obsidian|notion|vscode|cursor|docker|kubernetes|git|npm|yarn|pip)\b', re.IGNORECASE),
'technologies': re.compile(r'\b(?:python|javascript|typescript|react|vue|node|html|css|sql|json|yaml)\b', re.IGNORECASE),
'organizations': re.compile(r'\b(?:google|microsoft|apple|amazon|meta|openai|anthropic|github|gitlab)\b', re.IGNORECASE),
'people': re.compile(r'@[a-zA-Z0-9_]+|(?:by|from|author:)\s+([A-Z][a-z]+\s+[A-Z][a-z]+)'),
}
def analyze_content(self, content: str) -> ContentAnalysis:
"""Analyze file content and return comprehensive analysis results."""
if not content or not content.strip():
return ContentAnalysis(
language=LanguageInfo.UNKNOWN,
content_type=ContentType.UNKNOWN,
topics=[],
mentions={'tools': [], 'technologies': [], 'people': [], 'organizations': []},
complexity='basic'
)
# Detect language
language = self._detect_language_enum(content)
# Classify content type
content_type = self._classify_content_type_enum(content, "")
# Extract topics
topics = self.extract_topics(content)
# Extract mentions
mentions = self._extract_mentions(content)
# Determine complexity
complexity = self._determine_complexity(content)
return ContentAnalysis(
language=language,
content_type=content_type,
topics=topics,
mentions=mentions,
complexity=complexity
)
def detect_language(self, content: str) -> str:
"""Detect the primary language of the content."""
return self._detect_language_enum(content).value
def extract_topics(self, content: str) -> List[str]:
"""Extract topics from content using keyword analysis."""
if not content:
return []
content_lower = content.lower()
topics = []
# Check technology topics
for tech_category, keywords in self.tech_keywords.items():
if any(keyword in content_lower for keyword in keywords):
topics.append(f'tech/{tech_category}')
# Check general topics
for topic, keywords in self.topic_patterns.items():
if any(keyword in content_lower for keyword in keywords):
topics.append(topic)
# Extract topics from headers
header_topics = self._extract_header_topics(content)
topics.extend(header_topics)
# Remove duplicates and return
return list(set(topics))
def classify_content_type(self, content: str, filename: str) -> str:
"""Classify the type of content."""
return self._classify_content_type_enum(content, filename).value
def _detect_language_enum(self, content: str) -> LanguageInfo:
"""Detect language and return LanguageInfo enum."""
if not content:
return LanguageInfo.UNKNOWN
# Count character types
chinese_count = len(self.chinese_chars.findall(content))
english_count = len(self.english_chars.findall(content))
total_chars = chinese_count + english_count
if total_chars == 0:
return LanguageInfo.UNKNOWN
chinese_ratio = chinese_count / total_chars
english_ratio = english_count / total_chars
# Determine language based on ratios
if chinese_ratio > 0.3 and english_ratio > 0.3:
return LanguageInfo.MIXED
elif chinese_ratio > 0.1:
return LanguageInfo.CHINESE
elif english_ratio > 0.5:
return LanguageInfo.ENGLISH
else:
return LanguageInfo.UNKNOWN
def _classify_content_type_enum(self, content: str, filename: str) -> ContentType:
"""Classify content type and return ContentType enum."""
if not content:
return ContentType.UNKNOWN
# Check filename patterns first
filename_lower = filename.lower()
if re.match(r'\d{4}-\d{2}-\d{2}', filename_lower):
return ContentType.DAILY_NOTE
# Check content patterns
for content_type, patterns in self.content_type_patterns.items():
for pattern in patterns:
if re.search(pattern, content, re.IGNORECASE | re.MULTILINE):
return content_type
# Default classification based on content characteristics
if len(content.split('\n')) < 10:
return ContentType.NOTE
elif '```' in content and ('##' in content or '###' in content):
return ContentType.DOCUMENTATION
elif content.count('#') > 3:
return ContentType.REFERENCE
else:
return ContentType.NOTE
def _extract_mentions(self, content: str) -> Dict[str, List[str]]:
"""Extract mentions of tools, technologies, people, and organizations."""
mentions = {
'tools': [],
'technologies': [],
'people': [],
'organizations': []
}
for entity_type, pattern in self.entity_patterns.items():
matches = pattern.findall(content)
if entity_type == 'people':
# Special handling for people mentions
people = []
for match in matches:
if isinstance(match, tuple):
people.extend([m for m in match if m])
else:
people.append(match)
mentions[entity_type] = list(set(people))
else:
mentions[entity_type] = list(set(matches))
return mentions
def _extract_header_topics(self, content: str) -> List[str]:
"""Extract topics from markdown headers."""
topics = []
# Find all headers
header_pattern = re.compile(r'^#+\s+(.+)$', re.MULTILINE)
headers = header_pattern.findall(content)
for header in headers:
header_lower = header.lower().strip()
# Skip common header words
skip_words = {'introduction', 'overview', 'conclusion', 'summary', 'notes', 'todo', 'done'}
if header_lower in skip_words:
continue
# Extract meaningful topics from headers
words = re.findall(r'\b[a-zA-Z]{3,}\b', header_lower)
for word in words:
if word not in skip_words and len(word) > 3:
topics.append(word)
return topics[:5] # Limit to top 5 header topics
def _determine_complexity(self, content: str) -> str:
"""Determine content complexity based on various factors."""
if not content:
return 'basic'
# Count various complexity indicators
code_blocks = content.count('```')
links = content.count('http')
technical_terms = sum(1 for category in self.tech_keywords.values()
for term in category if term in content.lower())
word_count = len(content.split())
# Calculate complexity score
complexity_score = 0
complexity_score += min(code_blocks * 2, 10) # Code blocks add complexity
complexity_score += min(links, 5) # External links add complexity
complexity_score += min(technical_terms, 15) # Technical terms add complexity
complexity_score += min(word_count // 500, 10) # Length adds complexity
# Classify based on score
if complexity_score >= 20:
return 'advanced'
elif complexity_score >= 10:
return 'intermediate'
else:
return 'basic'
+234
View File
@@ -0,0 +1,234 @@
"""File discovery implementation for vault scanning."""
import os
import chardet
from datetime import datetime
from pathlib import Path
from typing import List, Set
from ..core.interfaces import BaseFileDiscovery
from ..core.models import FileInfo
class VaultFileDiscovery(BaseFileDiscovery):
"""Implementation of file discovery for Obsidian vault scanning."""
def __init__(self, vault_root: str):
"""Initialize with vault root directory."""
self.vault_root = Path(vault_root).resolve()
# Sensitive directories to exclude
self.sensitive_dirs = {
'.obsidian',
'.git',
'.smart-env',
'.pytest_cache',
'__pycache__',
'node_modules',
'.vscode',
'.idea',
'400-archive/security-sensitive' # From the vault structure
}
# File extensions to include (primarily text files)
self.allowed_extensions = {
'.md', '.markdown', '.txt', '.yaml', '.yml', '.json',
'.py', '.js', '.ts', '.html', '.css', '.xml'
}
# File patterns to exclude
self.excluded_patterns = {
'.DS_Store',
'Thumbs.db',
'.gitignore',
'.gitkeep'
}
def scan_directory(self, path: str) -> List[FileInfo]:
"""Recursively scan directory and return file information."""
scan_path = Path(path)
if not scan_path.is_absolute():
scan_path = self.vault_root / scan_path
files = []
try:
for root, dirs, filenames in os.walk(scan_path):
root_path = Path(root)
# Filter out sensitive directories
dirs[:] = [d for d in dirs if not self._is_sensitive_dir(root_path / d)]
for filename in filenames:
file_path = root_path / filename
# Skip excluded patterns
if filename in self.excluded_patterns:
continue
# Check if file extension is allowed
if not self._is_allowed_file(file_path):
continue
try:
file_info = self._create_file_info(file_path)
if file_info:
files.append(file_info)
except (OSError, PermissionError) as e:
# Log error but continue processing
print(f"Warning: Could not process file {file_path}: {e}")
continue
except (OSError, PermissionError) as e:
print(f"Error scanning directory {scan_path}: {e}")
return files
def filter_by_type(self, files: List[FileInfo], types: List[str]) -> List[FileInfo]:
"""Filter files by extension types."""
if not types:
return files
# Normalize extensions (ensure they start with .)
normalized_types = set()
for ext in types:
if not ext.startswith('.'):
ext = '.' + ext
normalized_types.add(ext.lower())
return [f for f in files if f.extension.lower() in normalized_types]
def exclude_sensitive(self, files: List[FileInfo]) -> List[FileInfo]:
"""Exclude sensitive directories and files."""
filtered_files = []
for file_info in files:
file_path = Path(file_info.path)
# Check if file is in sensitive directory
if self._is_in_sensitive_dir(file_path):
continue
# Check for sensitive content patterns in filename
if self._has_sensitive_filename(file_info.name):
continue
filtered_files.append(file_info)
return filtered_files
def _create_file_info(self, file_path: Path) -> FileInfo:
"""Create FileInfo object from file path."""
try:
stat = file_path.stat()
# Get relative path from vault root
try:
relative_path = file_path.relative_to(self.vault_root)
except ValueError:
# File is outside vault root, use absolute path
relative_path = file_path
# Read content for text files
content = None
if file_path.suffix.lower() in {'.md', '.markdown', '.txt', '.yaml', '.yml'}:
content = self._read_file_content(file_path)
return FileInfo(
path=str(relative_path),
name=file_path.name,
directory=str(relative_path.parent) if relative_path.parent != Path('.') else '',
extension=file_path.suffix,
size=stat.st_size,
created=datetime.fromtimestamp(stat.st_ctime),
modified=datetime.fromtimestamp(stat.st_mtime),
content=content
)
except (OSError, PermissionError):
return None
def _read_file_content(self, file_path: Path) -> str:
"""Read file content with encoding detection."""
try:
# First try UTF-8
with open(file_path, 'r', encoding='utf-8') as f:
return f.read()
except UnicodeDecodeError:
try:
# Detect encoding
with open(file_path, 'rb') as f:
raw_data = f.read()
detected = chardet.detect(raw_data)
encoding = detected.get('encoding', 'utf-8')
# Try detected encoding
return raw_data.decode(encoding, errors='replace')
except Exception:
# Fallback to reading as binary and replacing errors
try:
with open(file_path, 'r', encoding='utf-8', errors='replace') as f:
return f.read()
except Exception:
return ""
except Exception:
return ""
def _is_sensitive_dir(self, dir_path: Path) -> bool:
"""Check if directory should be excluded as sensitive."""
dir_name = dir_path.name
# Check exact matches
if dir_name in self.sensitive_dirs:
return True
# Check relative path matches
try:
relative_path = dir_path.relative_to(self.vault_root)
if str(relative_path) in self.sensitive_dirs:
return True
except ValueError:
pass
# Check for hidden directories (starting with .)
if dir_name.startswith('.') and dir_name not in {'.kiro'}:
return True
return False
def _is_in_sensitive_dir(self, file_path: Path) -> bool:
"""Check if file is in a sensitive directory."""
try:
relative_path = file_path.relative_to(self.vault_root)
path_parts = relative_path.parts
for part in path_parts[:-1]: # Exclude filename
if part in self.sensitive_dirs:
return True
if part.startswith('.') and part not in {'.kiro'}:
return True
# Check full directory path
dir_path = str(relative_path.parent)
if dir_path in self.sensitive_dirs:
return True
except ValueError:
pass
return False
def _is_allowed_file(self, file_path: Path) -> bool:
"""Check if file extension is allowed."""
return file_path.suffix.lower() in self.allowed_extensions
def _has_sensitive_filename(self, filename: str) -> bool:
"""Check if filename indicates sensitive content."""
sensitive_patterns = {
'password', 'secret', 'key', 'token', 'credential',
'private', 'confidential', 'sensitive'
}
filename_lower = filename.lower()
return any(pattern in filename_lower for pattern in sensitive_patterns)
+331
View File
@@ -0,0 +1,331 @@
"""Tag generation implementation for the comprehensive tagging system."""
import re
from pathlib import Path
from typing import List, Dict, Set
from ..core.interfaces import BaseTagGenerator
from ..core.models import ContentAnalysis, TagStructure
class TagGeneratorImpl(BaseTagGenerator):
"""Implementation of tag generation based on directory structure and content analysis."""
def __init__(self):
"""Initialize the tag generator with predefined mappings and patterns."""
# Directory-based tag mappings
self.directory_mappings = {
'100-project': 'project',
'200-area': 'area',
'300-resources': 'resource',
'400-archive': 'archive',
'clippings': 'clipping',
'readitlater inbox': 'clipping'
}
# Hierarchical topic mappings
self.topic_hierarchies = {
# Technology hierarchies
'ai': 'tech/ai',
'llm': 'tech/ai/llm',
'machine learning': 'tech/ai/ml',
'chatgpt': 'tech/ai/llm',
'openai': 'tech/ai/llm',
'claude': 'tech/ai/llm',
'docker': 'tech/infrastructure/docker',
'kubernetes': 'tech/infrastructure/k8s',
'python': 'tech/development/python',
'javascript': 'tech/development/javascript',
'typescript': 'tech/development/typescript',
'react': 'tech/development/react',
'vue': 'tech/development/vue',
'node': 'tech/development/nodejs',
'api': 'tech/development/api',
'database': 'tech/infrastructure/database',
'mysql': 'tech/infrastructure/database',
'postgresql': 'tech/infrastructure/database',
'mongodb': 'tech/infrastructure/database',
'redis': 'tech/infrastructure/database',
'nginx': 'tech/infrastructure/web',
'apache': 'tech/infrastructure/web',
'aws': 'tech/infrastructure/cloud',
'azure': 'tech/infrastructure/cloud',
'gcp': 'tech/infrastructure/cloud',
'linux': 'tech/infrastructure/os',
'ubuntu': 'tech/infrastructure/os',
'centos': 'tech/infrastructure/os',
# Personal hierarchies
'productivity': 'personal/productivity',
'gtd': 'personal/productivity/gtd',
'health': 'personal/health',
'cycling': 'personal/health/cycling',
'fitness': 'personal/health/fitness',
'finance': 'personal/finance',
'investment': 'personal/finance/investment',
'budget': 'personal/finance/budget',
'cooking': 'personal/cooking',
'recipe': 'personal/cooking/recipe',
# Work hierarchies
'government': 'work/government',
'enterprise': 'work/enterprise',
'airport': 'work/airport',
'ali': 'work/ali',
# Home automation hierarchies
'home assistant': 'home-automation/hass',
'esphome': 'home-automation/esphome',
'zigbee': 'home-automation/zigbee',
'mqtt': 'home-automation/mqtt',
'sensor': 'home-automation/sensor',
'automation': 'home-automation/automation'
}
# Sensitive content patterns
self.sensitive_patterns = {
'credentials': [
r'password\s*[:=]\s*["\']?[\w\-@#$%^&*()]+["\']?',
r'api[_\-]?key\s*[:=]\s*["\']?[\w\-]+["\']?',
r'secret\s*[:=]\s*["\']?[\w\-]+["\']?',
r'token\s*[:=]\s*["\']?[\w\-\.]+["\']?',
r'auth[_\-]?token\s*[:=]\s*["\']?[\w\-\.]+["\']?',
r'access[_\-]?key\s*[:=]\s*["\']?[\w\-]+["\']?',
r'private[_\-]?key',
r'ssh[_\-]?key',
r'-----BEGIN.*PRIVATE KEY-----'
],
'personal': [
r'\b\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}\b', # Credit card numbers
r'\b\d{3}-\d{2}-\d{4}\b', # SSN format
r'\b\d{11}\b', # Phone numbers (simplified)
r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}', # Email addresses
r'\b(?:home|personal|private)\s+(?:address|phone|email)',
r'\bbirthdate\b|\bdate\s+of\s+birth\b'
],
'financial': [
r'\b(?:salary|income|wage)\s*[:=]\s*[\$¥€£]?[\d,]+',
r'\b(?:bank|account)\s+(?:number|details)',
r'\b(?:routing|swift)\s+(?:number|code)',
r'\b(?:tax|invoice|receipt)\s+(?:id|number)',
r'\b(?:budget|expense|cost)\s*[:=]\s*[\$¥€£]?[\d,]+',
r'\b(?:investment|portfolio|stock)\s+(?:value|amount)'
],
'legal': [
r'\b(?:contract|agreement|legal)\s+(?:document|file)',
r'\b(?:confidential|proprietary|classified)',
r'\b(?:copyright|trademark|patent)\s+(?:notice|info)',
r'\b(?:license|licensing)\s+(?:agreement|terms)',
r'\bnda\b|\bnon[_\-]?disclosure\b'
]
}
# Tag validation patterns
self.valid_tag_pattern = re.compile(r'^[a-z0-9]+(?:[-/][a-z0-9]+)*$')
def generate_directory_tags(self, filepath: str) -> List[str]:
"""Generate tags based on directory structure."""
tags = []
path = Path(filepath)
parts = [p.lower() for p in path.parts]
# Generate primary directory tags
for part in parts:
if part in self.directory_mappings:
primary_tag = self.directory_mappings[part]
tags.append(primary_tag)
# Add hierarchical subdirectory tags
try:
part_index = parts.index(part)
if part_index + 1 < len(parts):
subdirs = parts[part_index + 1:-1] # Exclude filename
for subdir in subdirs:
# Clean and validate subdirectory name
clean_subdir = self._clean_tag_name(subdir)
if clean_subdir:
hierarchical_tag = f"{primary_tag}/{clean_subdir}"
tags.append(hierarchical_tag)
except ValueError:
continue
# Handle special cases
if any('clipping' in part for part in parts):
tags.append('clipping')
return list(set(tags)) # Remove duplicates
def generate_content_tags(self, analysis: ContentAnalysis) -> List[str]:
"""Generate tags based on content analysis."""
tags = []
# Add language tag
if analysis.language:
tags.append(f"lang/{analysis.language.value}")
# Add content type tag
if analysis.content_type:
tags.append(f"type/{analysis.content_type.value}")
# Add complexity tag if not basic
if analysis.complexity and analysis.complexity != 'basic':
tags.append(f"complexity/{analysis.complexity}")
# Add sentiment tag if available
if analysis.sentiment and analysis.sentiment != 'neutral':
tags.append(f"sentiment/{analysis.sentiment}")
# Add mention-based tags
for category, items in analysis.mentions.items():
for item in items:
clean_item = self._clean_tag_name(item)
if clean_item:
tags.append(f"{category}/{clean_item}")
return tags
def generate_hierarchical_tags(self, topics: List[str]) -> List[str]:
"""Generate hierarchical tags from topics."""
hierarchical_tags = []
for topic in topics:
topic_lower = topic.lower().strip()
# Check for direct mapping
if topic_lower in self.topic_hierarchies:
hierarchical_tags.append(self.topic_hierarchies[topic_lower])
else:
# Try partial matching for compound topics
for key, hierarchy in self.topic_hierarchies.items():
if key in topic_lower or topic_lower in key:
hierarchical_tags.append(hierarchy)
break
else:
# Create a generic hierarchical tag
clean_topic = self._clean_tag_name(topic_lower)
if clean_topic:
# Try to categorize based on common patterns
if any(tech_word in topic_lower for tech_word in ['tech', 'software', 'code', 'dev', 'program']):
hierarchical_tags.append(f"tech/{clean_topic}")
elif any(personal_word in topic_lower for personal_word in ['personal', 'life', 'habit', 'goal']):
hierarchical_tags.append(f"personal/{clean_topic}")
elif any(work_word in topic_lower for work_word in ['work', 'job', 'career', 'business']):
hierarchical_tags.append(f"work/{clean_topic}")
else:
hierarchical_tags.append(clean_topic)
return list(set(hierarchical_tags))
def consolidate_tags(self, tags: List[str]) -> List[str]:
"""Consolidate and deduplicate tags."""
if not tags:
return []
# Clean and validate all tags
cleaned_tags = []
for tag in tags:
clean_tag = self._clean_tag_name(tag)
if clean_tag and self._is_valid_tag(clean_tag):
cleaned_tags.append(clean_tag)
# Remove duplicates while preserving order
seen = set()
consolidated = []
for tag in cleaned_tags:
if tag not in seen:
seen.add(tag)
consolidated.append(tag)
# Apply consolidation rules
consolidated = self._apply_consolidation_rules(consolidated)
# Sort tags for consistency (hierarchical tags first, then alphabetical)
return self._sort_tags(consolidated)
def detect_sensitive_content(self, content: str, filepath: str) -> List[str]:
"""Detect sensitive content and return appropriate tags."""
sensitive_tags = []
content_lower = content.lower()
# Check for sensitive patterns
for category, patterns in self.sensitive_patterns.items():
for pattern in patterns:
if re.search(pattern, content, re.IGNORECASE):
sensitive_tags.append(f"sensitive/{category}")
break # Only add the category once
# Check filepath for sensitive indicators
filepath_lower = filepath.lower()
if any(sensitive_dir in filepath_lower for sensitive_dir in ['security-sensitive', 'private', 'confidential']):
if 'sensitive/personal' not in sensitive_tags:
sensitive_tags.append('sensitive/personal')
return list(set(sensitive_tags))
def _clean_tag_name(self, tag: str) -> str:
"""Clean and normalize tag names to kebab-case."""
if not tag:
return ""
# Convert to lowercase and replace spaces/underscores with hyphens
cleaned = re.sub(r'[_\s]+', '-', tag.lower().strip())
# Remove special characters except hyphens and forward slashes
cleaned = re.sub(r'[^a-z0-9\-/]', '', cleaned)
# Remove multiple consecutive hyphens
cleaned = re.sub(r'-+', '-', cleaned)
# Remove leading/trailing hyphens
cleaned = cleaned.strip('-')
return cleaned
def _is_valid_tag(self, tag: str) -> bool:
"""Validate tag format."""
if not tag:
return False
# Check against valid pattern
if not self.valid_tag_pattern.match(tag):
return False
# Additional validation rules
if len(tag) > 50: # Reasonable length limit
return False
if tag.startswith('/') or tag.endswith('/'):
return False
if '//' in tag: # No empty hierarchy levels
return False
return True
def _apply_consolidation_rules(self, tags: List[str]) -> List[str]:
"""Apply tag consolidation rules to remove redundancy."""
consolidated = tags.copy()
# Remove redundant hierarchical tags
# If we have both 'tech' and 'tech/ai', keep only 'tech/ai'
hierarchical_tags = [tag for tag in consolidated if '/' in tag]
simple_tags = [tag for tag in consolidated if '/' not in tag]
# Remove simple tags that are covered by hierarchical tags
filtered_simple = []
for simple_tag in simple_tags:
is_covered = any(hier_tag.startswith(f"{simple_tag}/") for hier_tag in hierarchical_tags)
if not is_covered:
filtered_simple.append(simple_tag)
return filtered_simple + hierarchical_tags
def _sort_tags(self, tags: List[str]) -> List[str]:
"""Sort tags with hierarchical tags first, then alphabetical."""
hierarchical = [tag for tag in tags if '/' in tag]
simple = [tag for tag in tags if '/' not in tag]
# Sort hierarchical tags by depth then alphabetically
hierarchical.sort(key=lambda x: (x.count('/'), x))
simple.sort()
return hierarchical + simple
+1
View File
@@ -0,0 +1 @@
"""Test package for the tagging system."""
Binary file not shown.
+124
View File
@@ -0,0 +1,124 @@
"""Pytest configuration and fixtures."""
import pytest
from pathlib import Path
from datetime import datetime
from typing import List
from hypothesis import settings, Verbosity
from tagging_system.core.models import (
FileInfo,
ContentAnalysis,
TagStructure,
FrontmatterData,
ContentType,
LanguageInfo
)
from tagging_system.config import TaggingConfig
# Configure hypothesis for property-based testing
settings.register_profile("default", max_examples=100, verbosity=Verbosity.normal)
settings.register_profile("ci", max_examples=1000, verbosity=Verbosity.verbose)
settings.load_profile("default")
@pytest.fixture
def sample_file_info() -> FileInfo:
"""Create a sample FileInfo for testing."""
return FileInfo(
path="100-project/AI/test.md",
name="test.md",
directory="100-project/AI",
extension=".md",
size=1024,
created=datetime(2024, 1, 1, 12, 0, 0),
modified=datetime(2024, 1, 2, 12, 0, 0),
content="# Test File\n\nThis is a test file about AI and machine learning."
)
@pytest.fixture
def sample_content_analysis() -> ContentAnalysis:
"""Create a sample ContentAnalysis for testing."""
return ContentAnalysis(
language=LanguageInfo.ENGLISH,
content_type=ContentType.NOTE,
topics=["ai", "machine-learning", "technology"],
mentions={
'tools': ['python', 'tensorflow'],
'technologies': ['ai', 'ml'],
'people': [],
'organizations': ['openai']
},
sentiment="neutral",
complexity="intermediate"
)
@pytest.fixture
def sample_tag_structure() -> TagStructure:
"""Create a sample TagStructure for testing."""
return TagStructure(
primary=["project"],
hierarchical=["tech/ai", "tech/ml"],
content=["python", "tensorflow"],
meta=["lang/en", "type/note"],
custom=["custom-tag"]
)
@pytest.fixture
def sample_frontmatter_data() -> FrontmatterData:
"""Create a sample FrontmatterData for testing."""
return FrontmatterData(
title="Test File",
tags=["project", "tech/ai", "python"],
created="2024-01-01",
updated="2024-01-02",
type="note",
lang="en",
aliases=["test"],
description="A test file for AI projects"
)
@pytest.fixture
def default_config() -> TaggingConfig:
"""Create a default TaggingConfig for testing."""
return TaggingConfig()
@pytest.fixture
def temp_vault_structure(tmp_path: Path) -> Path:
"""Create a temporary vault structure for testing."""
vault_root = tmp_path / "test_vault"
# Create directory structure
directories = [
"100-project/AI",
"100-project/Infrastructure",
"200-area/Productivity",
"200-area/Health",
"300-resources/Development",
"400-archive",
"Clippings",
"ReadItLater Inbox"
]
for directory in directories:
(vault_root / directory).mkdir(parents=True, exist_ok=True)
# Create sample files
sample_files = [
("100-project/AI/llm-notes.md", "# LLM Notes\n\nNotes about large language models."),
("200-area/Productivity/gtd.md", "# Getting Things Done\n\nProductivity methodology."),
("300-resources/Development/python.md", "# Python Resources\n\nPython development resources."),
("Clippings/article.md", "# Interesting Article\n\nClipped from web.")
]
for file_path, content in sample_files:
file_full_path = vault_root / file_path
file_full_path.write_text(content, encoding='utf-8')
return vault_root
+178
View File
@@ -0,0 +1,178 @@
"""Unit tests for configuration system."""
import pytest
import json
from pathlib import Path
from tagging_system.config import (
TaggingConfig,
DirectoryMapping,
TagHierarchy,
SensitivePatterns,
load_config,
save_config
)
class TestDirectoryMapping:
"""Test DirectoryMapping model."""
def test_directory_mapping_creation(self):
"""Test DirectoryMapping creation."""
mapping = DirectoryMapping(
pattern="100-project",
primary_tag="project",
hierarchical_tags=["tech"],
exclude_patterns=["*.tmp"]
)
assert mapping.pattern == "100-project"
assert mapping.primary_tag == "project"
assert mapping.hierarchical_tags == ["tech"]
assert mapping.exclude_patterns == ["*.tmp"]
class TestTagHierarchy:
"""Test TagHierarchy model."""
def test_tag_hierarchy_creation(self):
"""Test TagHierarchy creation."""
hierarchy = TagHierarchy(
root="tech",
children={"ai": TagHierarchy("ai")},
aliases=["technology"]
)
assert hierarchy.root == "tech"
assert "ai" in hierarchy.children
assert hierarchy.aliases == ["technology"]
def test_get_full_path(self):
"""Test get_full_path method."""
hierarchy = TagHierarchy("tech")
assert hierarchy.get_full_path() == "tech"
assert hierarchy.get_full_path("ai") == "tech/ai"
assert hierarchy.get_full_path("ai/llm") == "tech/ai/llm"
class TestSensitivePatterns:
"""Test SensitivePatterns model."""
def test_sensitive_patterns_defaults(self):
"""Test SensitivePatterns default values."""
patterns = SensitivePatterns()
assert len(patterns.credential_patterns) > 0
assert len(patterns.personal_patterns) > 0
assert len(patterns.financial_patterns) > 0
assert any("api" in pattern for pattern in patterns.credential_patterns)
class TestTaggingConfig:
"""Test TaggingConfig model."""
def test_tagging_config_defaults(self):
"""Test TaggingConfig default values."""
config = TaggingConfig()
assert len(config.directory_mappings) > 0
assert len(config.tag_hierarchies) > 0
assert len(config.excluded_directories) > 0
assert config.tag_format_rules["case"] == "kebab"
def test_get_directory_mapping(self):
"""Test get_directory_mapping method."""
config = TaggingConfig()
mapping = config.get_directory_mapping("100-project/AI/test.md")
assert mapping is not None
assert mapping.primary_tag == "project"
no_mapping = config.get_directory_mapping("unknown/path")
assert no_mapping is None
def test_get_tag_hierarchy(self):
"""Test get_tag_hierarchy method."""
config = TaggingConfig()
tech_hierarchy = config.get_tag_hierarchy("tech")
assert tech_hierarchy is not None
assert tech_hierarchy.root == "tech"
unknown_hierarchy = config.get_tag_hierarchy("unknown")
assert unknown_hierarchy is None
def test_is_excluded_directory(self):
"""Test is_excluded_directory method."""
config = TaggingConfig()
assert config.is_excluded_directory(".obsidian/plugins") is True
assert config.is_excluded_directory("100-project/AI") is False
def test_is_excluded_file(self):
"""Test is_excluded_file method."""
config = TaggingConfig()
assert config.is_excluded_file("test.pyc") is True
assert config.is_excluded_file(".DS_Store") is True
assert config.is_excluded_file("test.md") is False
class TestConfigLoading:
"""Test configuration loading and saving."""
def test_load_default_config(self):
"""Test loading default config when no file exists."""
config = load_config()
assert isinstance(config, TaggingConfig)
assert len(config.directory_mappings) > 0
def test_load_config_creates_default_file(self, tmp_path: Path):
"""Test that load_config creates default file when path doesn't exist."""
config_path = tmp_path / "config.yaml"
config = load_config(config_path)
assert isinstance(config, TaggingConfig)
assert config_path.exists()
def test_save_and_load_config(self, tmp_path: Path):
"""Test saving and loading configuration."""
config_path = tmp_path / "test_config.yaml"
original_config = TaggingConfig()
original_config.excluded_directories.append("test_exclude")
save_config(original_config, config_path)
loaded_config = load_config(config_path)
assert config_path.exists()
assert "test_exclude" in loaded_config.excluded_directories
def test_load_json_config(self, tmp_path: Path):
"""Test loading JSON configuration."""
config_path = tmp_path / "config.json"
config_data = {
"excluded_directories": [".test", ".custom"],
"tag_format_rules": {"case": "snake"}
}
with open(config_path, 'w') as f:
json.dump(config_data, f)
config = load_config(config_path)
assert ".test" in config.excluded_directories
assert ".custom" in config.excluded_directories
assert config.tag_format_rules["case"] == "snake"
def test_load_invalid_config_returns_default(self, tmp_path: Path):
"""Test that invalid config file returns default config."""
config_path = tmp_path / "invalid.yaml"
config_path.write_text("invalid: yaml: content: [")
config = load_config(config_path)
# Should return default config on error
assert isinstance(config, TaggingConfig)
assert len(config.directory_mappings) > 0
+232
View File
@@ -0,0 +1,232 @@
"""Unit tests for core interfaces and protocols."""
import pytest
from typing import List
from tagging_system.core.interfaces import (
FileDiscovery,
ContentAnalyzer,
TagGenerator,
FrontmatterManager,
BaseFileDiscovery,
BaseContentAnalyzer,
BaseTagGenerator,
BaseFrontmatterManager
)
from tagging_system.core.models import (
FileInfo,
ContentAnalysis,
TagStructure,
FrontmatterData,
ValidationResult,
ContentType,
LanguageInfo
)
class MockFileDiscovery(BaseFileDiscovery):
"""Mock implementation of FileDiscovery for testing."""
def scan_directory(self, path: str) -> List[FileInfo]:
"""Mock directory scanning."""
from datetime import datetime
return [
FileInfo(
path=f"{path}/test.md",
name="test.md",
directory=path,
extension=".md",
size=100,
created=datetime.now(),
modified=datetime.now()
)
]
def filter_by_type(self, files: List[FileInfo], types: List[str]) -> List[FileInfo]:
"""Mock file filtering."""
return [f for f in files if f.extension in types]
def exclude_sensitive(self, files: List[FileInfo]) -> List[FileInfo]:
"""Mock sensitive file exclusion."""
return [f for f in files if "sensitive" not in f.path]
class MockContentAnalyzer(BaseContentAnalyzer):
"""Mock implementation of ContentAnalyzer for testing."""
def analyze_content(self, content: str) -> ContentAnalysis:
"""Mock content analysis."""
return ContentAnalysis(
language=LanguageInfo.ENGLISH,
content_type=ContentType.NOTE,
topics=["test"],
mentions={'tools': [], 'technologies': [], 'people': [], 'organizations': []}
)
def detect_language(self, content: str) -> str:
"""Mock language detection."""
return "en"
def extract_topics(self, content: str) -> List[str]:
"""Mock topic extraction."""
return ["test", "mock"]
def classify_content_type(self, content: str, filename: str) -> str:
"""Mock content type classification."""
return "note"
class MockTagGenerator(BaseTagGenerator):
"""Mock implementation of TagGenerator for testing."""
def generate_directory_tags(self, filepath: str) -> List[str]:
"""Mock directory tag generation."""
if "100-project" in filepath:
return ["project"]
return ["unknown"]
def generate_content_tags(self, analysis: ContentAnalysis) -> List[str]:
"""Mock content tag generation."""
return analysis.topics
def generate_hierarchical_tags(self, topics: List[str]) -> List[str]:
"""Mock hierarchical tag generation."""
return [f"topic/{topic}" for topic in topics]
def consolidate_tags(self, tags: List[str]) -> List[str]:
"""Mock tag consolidation."""
return list(set(tags)) # Remove duplicates
class MockFrontmatterManager(BaseFrontmatterManager):
"""Mock implementation of FrontmatterManager for testing."""
def parse_frontmatter(self, content: str) -> FrontmatterData:
"""Mock frontmatter parsing."""
return FrontmatterData(
title="Test",
tags=["test"],
created="2024-01-01"
)
def update_frontmatter(self, content: str, updates: FrontmatterData) -> str:
"""Mock frontmatter updating."""
return f"---\ntitle: {updates.title}\ntags: {updates.tags}\n---\n{content}"
def validate_frontmatter(self, data: FrontmatterData) -> ValidationResult:
"""Mock frontmatter validation."""
result = ValidationResult(is_valid=True)
if not data.title:
result.add_error("Title is required")
return result
class TestProtocolCompliance:
"""Test that mock implementations comply with protocols."""
def test_file_discovery_protocol_compliance(self):
"""Test that MockFileDiscovery implements FileDiscovery protocol."""
mock = MockFileDiscovery()
assert isinstance(mock, FileDiscovery)
# Test method calls
files = mock.scan_directory("test")
assert len(files) == 1
assert files[0].name == "test.md"
filtered = mock.filter_by_type(files, [".md"])
assert len(filtered) == 1
non_sensitive = mock.exclude_sensitive(files)
assert len(non_sensitive) == 1
def test_content_analyzer_protocol_compliance(self):
"""Test that MockContentAnalyzer implements ContentAnalyzer protocol."""
mock = MockContentAnalyzer()
assert isinstance(mock, ContentAnalyzer)
# Test method calls
analysis = mock.analyze_content("test content")
assert analysis.language == LanguageInfo.ENGLISH
assert analysis.content_type == ContentType.NOTE
language = mock.detect_language("test content")
assert language == "en"
topics = mock.extract_topics("test content")
assert "test" in topics
content_type = mock.classify_content_type("test content", "test.md")
assert content_type == "note"
def test_tag_generator_protocol_compliance(self):
"""Test that MockTagGenerator implements TagGenerator protocol."""
mock = MockTagGenerator()
assert isinstance(mock, TagGenerator)
# Test method calls
dir_tags = mock.generate_directory_tags("100-project/test.md")
assert "project" in dir_tags
analysis = ContentAnalysis(
language=LanguageInfo.ENGLISH,
content_type=ContentType.NOTE,
topics=["ai", "ml"]
)
content_tags = mock.generate_content_tags(analysis)
assert "ai" in content_tags
hierarchical = mock.generate_hierarchical_tags(["ai", "ml"])
assert "topic/ai" in hierarchical
consolidated = mock.consolidate_tags(["tag1", "tag1", "tag2"])
assert len(consolidated) == 2
def test_frontmatter_manager_protocol_compliance(self):
"""Test that MockFrontmatterManager implements FrontmatterManager protocol."""
mock = MockFrontmatterManager()
assert isinstance(mock, FrontmatterManager)
# Test method calls
frontmatter = mock.parse_frontmatter("---\ntitle: Test\n---\nContent")
assert frontmatter.title == "Test"
updated = mock.update_frontmatter("Content", frontmatter)
assert "title: Test" in updated
validation = mock.validate_frontmatter(frontmatter)
assert validation.is_valid is True
class TestAbstractBaseClasses:
"""Test abstract base class behavior."""
def test_base_classes_cannot_be_instantiated(self):
"""Test that abstract base classes cannot be instantiated directly."""
with pytest.raises(TypeError):
BaseFileDiscovery()
with pytest.raises(TypeError):
BaseContentAnalyzer()
with pytest.raises(TypeError):
BaseTagGenerator()
with pytest.raises(TypeError):
BaseFrontmatterManager()
def test_concrete_implementations_work(self):
"""Test that concrete implementations of base classes work."""
file_discovery = MockFileDiscovery()
content_analyzer = MockContentAnalyzer()
tag_generator = MockTagGenerator()
frontmatter_manager = MockFrontmatterManager()
# All should be instances of their respective base classes
assert isinstance(file_discovery, BaseFileDiscovery)
assert isinstance(content_analyzer, BaseContentAnalyzer)
assert isinstance(tag_generator, BaseTagGenerator)
assert isinstance(frontmatter_manager, BaseFrontmatterManager)
+193
View File
@@ -0,0 +1,193 @@
"""Unit tests for core data models."""
import pytest
from datetime import datetime
from tagging_system.core.models import (
FileInfo,
ContentAnalysis,
TagStructure,
FrontmatterData,
ValidationResult,
ContentType,
LanguageInfo
)
class TestFileInfo:
"""Test FileInfo model."""
def test_file_info_creation(self):
"""Test FileInfo creation with required fields."""
file_info = FileInfo(
path="test/file.md",
name="file.md",
directory="test",
extension=".md",
size=100,
created=datetime.now(),
modified=datetime.now()
)
assert file_info.path == "test/file.md"
assert file_info.name == "file.md"
assert file_info.is_markdown is True
assert file_info.relative_path == "test/file.md"
def test_is_markdown_detection(self):
"""Test markdown file detection."""
md_file = FileInfo("test.md", "test.md", ".", ".md", 100, datetime.now(), datetime.now())
txt_file = FileInfo("test.txt", "test.txt", ".", ".txt", 100, datetime.now(), datetime.now())
assert md_file.is_markdown is True
assert txt_file.is_markdown is False
class TestContentAnalysis:
"""Test ContentAnalysis model."""
def test_content_analysis_creation(self):
"""Test ContentAnalysis creation."""
analysis = ContentAnalysis(
language=LanguageInfo.ENGLISH,
content_type=ContentType.NOTE,
topics=["ai", "ml"]
)
assert analysis.language == LanguageInfo.ENGLISH
assert analysis.content_type == ContentType.NOTE
assert analysis.topics == ["ai", "ml"]
assert analysis.complexity == "basic" # Default value
def test_complexity_validation(self):
"""Test complexity validation in post_init."""
analysis = ContentAnalysis(
language=LanguageInfo.ENGLISH,
content_type=ContentType.NOTE,
topics=[],
complexity="invalid"
)
assert analysis.complexity == "basic" # Should default to basic
class TestTagStructure:
"""Test TagStructure model."""
def test_tag_structure_creation(self):
"""Test TagStructure creation."""
tags = TagStructure(
primary=["project"],
hierarchical=["tech/ai"],
content=["python"],
meta=["lang/en"],
custom=["custom"]
)
assert tags.primary == ["project"]
assert tags.hierarchical == ["tech/ai"]
assert tags.content == ["python"]
assert tags.meta == ["lang/en"]
assert tags.custom == ["custom"]
def test_all_tags_method(self):
"""Test all_tags method returns unique tags."""
tags = TagStructure(
primary=["project", "duplicate"],
hierarchical=["tech/ai"],
content=["python", "duplicate"], # Duplicate tag
meta=["lang/en"],
custom=["custom"]
)
all_tags = tags.all_tags()
# Expected unique tags: project, duplicate, tech/ai, python, lang/en, custom = 6 tags
assert len(all_tags) == 6
assert "duplicate" in all_tags
assert "project" in all_tags
assert "tech/ai" in all_tags
assert "python" in all_tags
assert "lang/en" in all_tags
assert "custom" in all_tags
# Test that duplicates are actually removed by checking set behavior
unique_tags = set(all_tags)
assert len(unique_tags) == len(all_tags) # No duplicates should exist
# Test with actual duplicates to verify deduplication works
tags_with_more_duplicates = TagStructure(
primary=["tag1", "tag2"],
hierarchical=["tag1"], # Duplicate of primary
content=["tag2", "tag3"], # Duplicate of primary
meta=["tag3"], # Duplicate of content
custom=["tag4"]
)
deduplicated = tags_with_more_duplicates.all_tags()
assert len(deduplicated) == 4 # tag1, tag2, tag3, tag4
assert len(set(deduplicated)) == len(deduplicated)
class TestFrontmatterData:
"""Test FrontmatterData model."""
def test_frontmatter_data_creation(self):
"""Test FrontmatterData creation."""
frontmatter = FrontmatterData(
title="Test",
tags=["tag1", "tag2"],
created="2024-01-01",
type="note"
)
assert frontmatter.title == "Test"
assert frontmatter.tags == ["tag1", "tag2"]
assert frontmatter.created == "2024-01-01"
assert frontmatter.type == "note"
def test_to_dict_method(self):
"""Test to_dict method excludes None values."""
frontmatter = FrontmatterData(
title="Test",
tags=["tag1"],
created="2024-01-01",
updated=None, # Should be excluded
custom_fields={"custom": "value"}
)
result = frontmatter.to_dict()
assert result["title"] == "Test"
assert result["tags"] == ["tag1"]
assert result["created"] == "2024-01-01"
assert "updated" not in result # None values excluded
assert result["custom"] == "value" # Custom fields included
class TestValidationResult:
"""Test ValidationResult model."""
def test_validation_result_creation(self):
"""Test ValidationResult creation."""
result = ValidationResult(is_valid=True)
assert result.is_valid is True
assert result.errors == []
assert result.warnings == []
assert result.suggestions == []
def test_add_error_sets_invalid(self):
"""Test that adding error sets is_valid to False."""
result = ValidationResult(is_valid=True)
result.add_error("Test error")
assert result.is_valid is False
assert "Test error" in result.errors
def test_add_warning_and_suggestion(self):
"""Test adding warnings and suggestions."""
result = ValidationResult(is_valid=True)
result.add_warning("Test warning")
result.add_suggestion("Test suggestion")
assert result.is_valid is True # Warnings don't affect validity
assert "Test warning" in result.warnings
assert "Test suggestion" in result.suggestions
+97
View File
@@ -0,0 +1,97 @@
"""Test overall package structure and imports."""
import pytest
class TestPackageStructure:
"""Test that the package structure is correct."""
def test_main_package_imports(self):
"""Test that main package imports work correctly."""
from tagging_system import (
FileInfo,
ContentAnalysis,
TagStructure,
FrontmatterData,
ContentType,
LanguageInfo,
ValidationResult,
FileDiscovery,
ContentAnalyzer,
TagGenerator,
FrontmatterManager
)
# Test that all imports are available
assert FileInfo is not None
assert ContentAnalysis is not None
assert TagStructure is not None
assert FrontmatterData is not None
assert ContentType is not None
assert LanguageInfo is not None
assert ValidationResult is not None
assert FileDiscovery is not None
assert ContentAnalyzer is not None
assert TagGenerator is not None
assert FrontmatterManager is not None
def test_core_module_imports(self):
"""Test that core module imports work correctly."""
from tagging_system.core import (
FileInfo,
ContentAnalysis,
TagStructure,
FrontmatterData,
ContentType,
LanguageInfo,
ValidationResult,
FileDiscovery,
ContentAnalyzer,
TagGenerator,
FrontmatterManager
)
# All imports should be available
assert all([
FileInfo, ContentAnalysis, TagStructure, FrontmatterData,
ContentType, LanguageInfo, ValidationResult,
FileDiscovery, ContentAnalyzer, TagGenerator, FrontmatterManager
])
def test_config_module_imports(self):
"""Test that config module imports work correctly."""
from tagging_system.config import (
TaggingConfig,
DirectoryMapping,
TagHierarchy,
SensitivePatterns,
load_config,
save_config
)
# All imports should be available
assert all([
TaggingConfig, DirectoryMapping, TagHierarchy,
SensitivePatterns, load_config, save_config
])
def test_cli_module_import(self):
"""Test that CLI module can be imported."""
from tagging_system import cli
assert hasattr(cli, 'main')
assert callable(cli.main)
def test_package_version(self):
"""Test that package version is available."""
import tagging_system
assert hasattr(tagging_system, '__version__')
assert tagging_system.__version__ == "0.1.0"
def test_package_metadata(self):
"""Test that package metadata is available."""
import tagging_system
assert hasattr(tagging_system, '__author__')
assert tagging_system.__author__ == "Tagging System"