this repo has no description
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

update to not use metadata.json

+129 -143
+6 -14
ARCH.md
··· 53 53 │ │ ├── commands/ # Subcommands 54 54 │ │ │ ├── __init__.py 55 55 │ │ │ ├── init.py # Initialize git store 56 - │ │ │ ├── add.py # Add feed to config 56 + │ │ │ ├── add.py # Add users and feeds 57 57 │ │ │ ├── sync.py # Sync feeds 58 - │ │ │ ├── list.py # List users/feeds 59 - │ │ │ └── search.py # Search entries 58 + │ │ │ ├── list_cmd.py # List users/feeds 59 + │ │ │ └── duplicates.py # Manage duplicate entries 60 60 │ │ └── utils.py # CLI utilities (progress, formatting) 61 61 │ ├── core/ # Core business logic 62 62 │ │ ├── __init__.py 63 63 │ │ ├── feed_parser.py # Feed parsing and normalization 64 - │ │ ├── git_store.py # Git repository operations 65 - │ │ ├── cache.py # Cache management 66 - │ │ └── sanitizer.py # Filename and HTML sanitization 64 + │ │ └── git_store.py # Git repository operations 67 65 │ ├── models/ # Pydantic data models 68 66 │ │ ├── __init__.py 69 67 │ │ ├── config.py # Configuration models 70 68 │ │ ├── feed.py # Feed/Entry models 71 69 │ │ └── user.py # User metadata models 72 70 │ └── utils/ # Shared utilities 73 - │ ├── __init__.py 74 - │ ├── paths.py # Path handling 75 - │ └── network.py # HTTP client wrapper 71 + │ └── __init__.py 76 72 ├── tests/ 77 73 │ ├── __init__.py 78 74 │ ├── conftest.py # pytest configuration ··· 159 155 ├── index.json # User directory index 160 156 ├── duplicates.json # Manual curation of duplicate entries 161 157 ├── user1/ 162 - │ ├── metadata.json # User metadata 163 158 │ ├── entry_id_1.json # Sanitized entry files 164 159 │ ├── entry_id_2.json 165 160 │ └── ... ··· 230 225 thicket list users 231 226 thicket list feeds --user alyssa 232 227 233 - # Search entries 234 - thicket search "keyword" --user alyssa --since 2025-01-01 235 - 236 228 # Manage duplicate entries 237 229 thicket duplicates list 238 230 thicket duplicates add <entry_id_1> <entry_id_2> # Mark as duplicates ··· 329 321 homepage=self.author_uri or self.link, 330 322 icon=self.logo or self.icon or self.image_url 331 323 ) 332 - ``` 324 + ```
+29 -41
src/thicket/cli/commands/add.py
··· 9 9 10 10 from ...core.feed_parser import FeedParser 11 11 from ...core.git_store import GitStore 12 - from ...models import UserConfig 13 12 from ..main import app 14 13 from ..utils import ( 15 14 create_progress, ··· 17 16 print_error, 18 17 print_info, 19 18 print_success, 20 - save_config, 21 19 ) 22 20 23 21 ··· 75 73 # Load configuration 76 74 config = load_config(config_file) 77 75 76 + # Initialize Git store 77 + git_store = GitStore(config.git_store) 78 + 78 79 # Check if user already exists 79 - existing_user = config.find_user(username) 80 + existing_user = git_store.get_user(username) 80 81 if existing_user: 81 82 print_error(f"User '{username}' already exists") 82 83 print_error("Use 'thicket add feed' to add additional feeds") ··· 87 88 if auto_discover: 88 89 discovered_metadata = asyncio.run(discover_feed_metadata(validated_feed_url)) 89 90 90 - # Create user config with manual overrides taking precedence 91 - user_config = UserConfig( 92 - username=username, 93 - feeds=[validated_feed_url], 94 - email=email or (discovered_metadata.author_email if discovered_metadata else None), 95 - homepage=HttpUrl(homepage) if homepage else (discovered_metadata.author_uri or discovered_metadata.link if discovered_metadata else None), 96 - icon=HttpUrl(icon) if icon else (discovered_metadata.logo or discovered_metadata.icon or discovered_metadata.image_url if discovered_metadata else None), 97 - display_name=display_name or (discovered_metadata.author_name or discovered_metadata.title if discovered_metadata else None), 98 - ) 99 - 100 - # Add user to configuration 101 - config.add_user(user_config) 102 - 103 - # Save configuration 104 - save_config(config, config_file) 91 + # Prepare user data with manual overrides taking precedence 92 + user_display_name = display_name or (discovered_metadata.author_name or discovered_metadata.title if discovered_metadata else None) 93 + user_email = email or (discovered_metadata.author_email if discovered_metadata else None) 94 + user_homepage = homepage or (str(discovered_metadata.author_uri or discovered_metadata.link) if discovered_metadata else None) 95 + user_icon = icon or (str(discovered_metadata.logo or discovered_metadata.icon or discovered_metadata.image_url) if discovered_metadata else None) 105 96 106 97 # Add user to Git store 107 - git_store = GitStore(config.git_store) 108 98 git_store.add_user( 109 99 username=username, 110 - display_name=user_config.display_name, 111 - email=user_config.email, 112 - homepage=str(user_config.homepage) if user_config.homepage else None, 113 - icon=str(user_config.icon) if user_config.icon else None, 114 - feeds=[str(f) for f in user_config.feeds], 100 + display_name=user_display_name, 101 + email=user_email, 102 + homepage=user_homepage, 103 + icon=user_icon, 104 + feeds=[str(validated_feed_url)], 115 105 ) 116 106 117 107 # Commit changes ··· 121 111 122 112 if discovered_metadata and auto_discover: 123 113 print_info("Auto-discovered metadata:") 124 - if user_config.display_name: 125 - print_info(f" Display name: {user_config.display_name}") 126 - if user_config.email: 127 - print_info(f" Email: {user_config.email}") 128 - if user_config.homepage: 129 - print_info(f" Homepage: {user_config.homepage}") 130 - if user_config.icon: 131 - print_info(f" Icon: {user_config.icon}") 114 + if user_display_name: 115 + print_info(f" Display name: {user_display_name}") 116 + if user_email: 117 + print_info(f" Email: {user_email}") 118 + if user_homepage: 119 + print_info(f" Homepage: {user_homepage}") 120 + if user_icon: 121 + print_info(f" Icon: {user_icon}") 132 122 133 123 134 124 def add_feed(username: str, feed_url: Optional[str], config_file: Path) -> None: ··· 148 138 # Load configuration 149 139 config = load_config(config_file) 150 140 141 + # Initialize Git store 142 + git_store = GitStore(config.git_store) 143 + 151 144 # Check if user exists 152 - user = config.find_user(username) 145 + user = git_store.get_user(username) 153 146 if not user: 154 147 print_error(f"User '{username}' not found") 155 148 print_error("Use 'thicket add user' to add a new user") 156 149 raise typer.Exit(1) 157 150 158 151 # Check if feed already exists 159 - if validated_feed_url in user.feeds: 152 + if str(validated_feed_url) in user.feeds: 160 153 print_error(f"Feed already exists for user '{username}': {feed_url}") 161 154 raise typer.Exit(1) 162 155 163 156 # Add feed to user 164 - if config.add_feed_to_user(username, validated_feed_url): 165 - save_config(config, config_file) 166 - 167 - # Update Git store 168 - git_store = GitStore(config.git_store) 169 - git_store.update_user(username, feeds=[str(f) for f in user.feeds]) 157 + updated_feeds = user.feeds + [str(validated_feed_url)] 158 + if git_store.update_user(username, feeds=updated_feeds): 170 159 git_store.commit_changes(f"Add feed to user {username}: {feed_url}") 171 - 172 160 print_success(f"Added feed to user '{username}': {feed_url}") 173 161 else: 174 162 print_error(f"Failed to add feed to user '{username}'")
+21 -15
src/thicket/cli/commands/list_cmd.py
··· 13 13 load_config, 14 14 print_error, 15 15 print_feeds_table, 16 + print_feeds_table_from_git, 16 17 print_info, 17 18 print_users_table, 19 + print_users_table_from_git, 18 20 ) 19 21 20 22 ··· 36 38 # Load configuration 37 39 config = load_config(config_file) 38 40 41 + # Initialize Git store 42 + git_store = GitStore(config.git_store) 43 + 39 44 if what == "users": 40 - list_users(config) 45 + list_users(git_store) 41 46 elif what == "feeds": 42 - list_feeds(config, user) 47 + list_feeds(git_store, user) 43 48 elif what == "entries": 44 - list_entries(config, user, limit) 49 + list_entries(git_store, user, limit) 45 50 else: 46 51 print_error(f"Unknown list type: {what}") 47 52 print_error("Use 'users', 'feeds', or 'entries'") 48 53 raise typer.Exit(1) 49 54 50 55 51 - def list_users(config) -> None: 56 + def list_users(git_store: GitStore) -> None: 52 57 """List all users.""" 53 - if not config.users: 58 + index = git_store._load_index() 59 + users = list(index.users.values()) 60 + 61 + if not users: 54 62 print_info("No users configured") 55 63 return 56 64 57 - print_users_table(config) 65 + print_users_table_from_git(users) 58 66 59 67 60 - def list_feeds(config, username: Optional[str] = None) -> None: 68 + def list_feeds(git_store: GitStore, username: Optional[str] = None) -> None: 61 69 """List feeds, optionally filtered by user.""" 62 70 if username: 63 - user = config.find_user(username) 71 + user = git_store.get_user(username) 64 72 if not user: 65 73 print_error(f"User '{username}' not found") 66 74 raise typer.Exit(1) ··· 69 77 print_info(f"No feeds configured for user '{username}'") 70 78 return 71 79 72 - print_feeds_table(config, username) 80 + print_feeds_table_from_git(git_store, username) 73 81 74 82 75 - def list_entries(config, username: Optional[str] = None, limit: Optional[int] = None) -> None: 83 + def list_entries(git_store: GitStore, username: Optional[str] = None, limit: Optional[int] = None) -> None: 76 84 """List entries, optionally filtered by user.""" 77 85 78 - # Initialize Git store 79 - git_store = GitStore(config.git_store) 80 - 81 86 if username: 82 87 # List entries for specific user 83 - user = config.find_user(username) 88 + user = git_store.get_user(username) 84 89 if not user: 85 90 print_error(f"User '{username}' not found") 86 91 raise typer.Exit(1) ··· 97 102 all_entries = [] 98 103 all_usernames = [] 99 104 100 - for user in config.users: 105 + index = git_store._load_index() 106 + for user in index.users.values(): 101 107 entries = git_store.list_entries(user.username, limit) 102 108 if entries: 103 109 all_entries.append(entries)
+15 -14
src/thicket/cli/commands/sync.py
··· 38 38 # Load configuration 39 39 config = load_config(config_file) 40 40 41 - # Determine which users to sync 41 + # Initialize Git store 42 + git_store = GitStore(config.git_store) 43 + 44 + # Determine which users to sync from git repository 42 45 users_to_sync = [] 43 46 if all_users: 44 - users_to_sync = config.users 47 + index = git_store._load_index() 48 + users_to_sync = list(index.users.values()) 45 49 elif user: 46 - user_config = config.find_user(user) 47 - if not user_config: 48 - print_error(f"User '{user}' not found") 50 + user_metadata = git_store.get_user(user) 51 + if not user_metadata: 52 + print_error(f"User '{user}' not found in git repository") 49 53 raise typer.Exit(1) 50 - users_to_sync = [user_config] 54 + users_to_sync = [user_metadata] 51 55 else: 52 56 print_error("Specify --all to sync all users or --user to sync a specific user") 53 57 raise typer.Exit(1) ··· 55 59 if not users_to_sync: 56 60 print_info("No users configured to sync") 57 61 return 58 - 59 - # Initialize Git store 60 - git_store = GitStore(config.git_store) 61 62 62 63 # Sync each user 63 64 total_new_entries = 0 64 65 total_updated_entries = 0 65 66 66 - for user_config in users_to_sync: 67 - print_info(f"Syncing user: {user_config.username}") 67 + for user_metadata in users_to_sync: 68 + print_info(f"Syncing user: {user_metadata.username}") 68 69 69 70 user_new_entries = 0 70 71 user_updated_entries = 0 71 72 72 73 # Sync each feed for the user 73 - for feed_url in track(user_config.feeds, description=f"Syncing {user_config.username}'s feeds"): 74 + for feed_url in track(user_metadata.feeds, description=f"Syncing {user_metadata.username}'s feeds"): 74 75 try: 75 76 new_entries, updated_entries = asyncio.run( 76 - sync_feed(git_store, user_config.username, feed_url, dry_run) 77 + sync_feed(git_store, user_metadata.username, feed_url, dry_run) 77 78 ) 78 79 user_new_entries += new_entries 79 80 user_updated_entries += updated_entries ··· 82 83 print_error(f"Failed to sync feed {feed_url}: {e}") 83 84 continue 84 85 85 - print_info(f"User {user_config.username}: {user_new_entries} new, {user_updated_entries} updated") 86 + print_info(f"User {user_metadata.username}: {user_new_entries} new, {user_updated_entries} updated") 86 87 total_new_entries += user_new_entries 87 88 total_updated_entries += user_updated_entries 88 89
+1
src/thicket/cli/main.py
··· 38 38 39 39 40 40 # Import commands to register them 41 + from .commands import add, duplicates, init, list_cmd, sync 41 42 42 43 if __name__ == "__main__": 43 44 app()
+49 -1
src/thicket/cli/utils.py
··· 8 8 from rich.progress import Progress, SpinnerColumn, TextColumn 9 9 from rich.table import Table 10 10 11 - from ..models import ThicketConfig 11 + from ..models import ThicketConfig, UserMetadata 12 + from ..core.git_store import GitStore 12 13 13 14 console = Console() 14 15 ··· 123 124 def print_info(message: str) -> None: 124 125 """Print an info message.""" 125 126 console.print(f"[blue]ℹ[/blue] {message}") 127 + 128 + 129 + def print_users_table_from_git(users: list[UserMetadata]) -> None: 130 + """Print a table of users from git repository.""" 131 + table = Table(title="Users and Feeds") 132 + table.add_column("Username", style="cyan", no_wrap=True) 133 + table.add_column("Display Name", style="magenta") 134 + table.add_column("Email", style="blue") 135 + table.add_column("Homepage", style="green") 136 + table.add_column("Feeds", style="yellow") 137 + 138 + for user in users: 139 + feeds_str = "\n".join(user.feeds) 140 + table.add_row( 141 + user.username, 142 + user.display_name or "", 143 + user.email or "", 144 + user.homepage or "", 145 + feeds_str, 146 + ) 147 + 148 + console.print(table) 149 + 150 + 151 + def print_feeds_table_from_git(git_store: GitStore, username: Optional[str] = None) -> None: 152 + """Print a table of feeds from git repository.""" 153 + table = Table(title=f"Feeds{f' for {username}' if username else ''}") 154 + table.add_column("Username", style="cyan", no_wrap=True) 155 + table.add_column("Feed URL", style="blue") 156 + table.add_column("Status", style="green") 157 + 158 + if username: 159 + user = git_store.get_user(username) 160 + users = [user] if user else [] 161 + else: 162 + index = git_store._load_index() 163 + users = list(index.users.values()) 164 + 165 + for user in users: 166 + for feed in user.feeds: 167 + table.add_row( 168 + user.username, 169 + feed, 170 + "Active", # TODO: Add actual status checking 171 + ) 172 + 173 + console.print(table)
+3 -14
src/thicket/core/git_store.py
··· 109 109 last_updated=datetime.now(), 110 110 ) 111 111 112 - # Save user metadata 113 - metadata_path = user_dir / "metadata.json" 114 - with open(metadata_path, "w") as f: 115 - json.dump(user_metadata.model_dump(mode="json"), f, indent=2, default=str) 116 112 117 113 # Update index 118 114 index.add_user(user_metadata) ··· 140 136 141 137 user.update_timestamp() 142 138 143 - # Save user metadata 144 - user_dir = self.repo_path / user.directory 145 - metadata_path = user_dir / "metadata.json" 146 - with open(metadata_path, "w") as f: 147 - json.dump(user.model_dump(mode="json"), f, indent=2, default=str) 148 139 149 140 # Update index 150 141 index.add_user(user) ··· 176 167 177 168 # Update user metadata if new entry 178 169 if not entry_exists: 179 - user.increment_entry_count() 180 - self.update_user(username, entry_count=user.entry_count) 170 + index = self._load_index() 171 + index.update_entry_count(username, 1) 172 + self._save_index(index) 181 173 182 174 return True 183 175 ··· 214 206 entries = [] 215 207 entry_files = sorted(user_dir.glob("*.json"), key=lambda p: p.stat().st_mtime, reverse=True) 216 208 217 - # Filter out metadata.json 218 - entry_files = [f for f in entry_files if f.name != "metadata.json"] 219 209 220 210 if limit: 221 211 entry_files = entry_files[:limit] ··· 289 279 continue 290 280 291 281 entry_files = user_dir.glob("*.json") 292 - entry_files = [f for f in entry_files if f.name != "metadata.json"] 293 282 294 283 for entry_file in entry_files: 295 284 try:
-38
src/thicket/models/config.py
··· 31 31 git_store: Path 32 32 cache_dir: Path 33 33 users: list[UserConfig] = [] 34 - 35 - def find_user(self, username: str) -> Optional[UserConfig]: 36 - """Find a user by username.""" 37 - for user in self.users: 38 - if user.username == username: 39 - return user 40 - return None 41 - 42 - def add_user(self, user: UserConfig) -> None: 43 - """Add a new user or update existing user.""" 44 - existing = self.find_user(user.username) 45 - if existing: 46 - # Update existing user 47 - existing.feeds = list(set(existing.feeds + user.feeds)) 48 - existing.email = user.email or existing.email 49 - existing.homepage = user.homepage or existing.homepage 50 - existing.icon = user.icon or existing.icon 51 - existing.display_name = user.display_name or existing.display_name 52 - else: 53 - # Add new user 54 - self.users.append(user) 55 - 56 - def remove_user(self, username: str) -> bool: 57 - """Remove a user by username. Returns True if user was found and removed.""" 58 - for i, user in enumerate(self.users): 59 - if user.username == username: 60 - del self.users[i] 61 - return True 62 - return False 63 - 64 - def add_feed_to_user(self, username: str, feed_url: HttpUrl) -> bool: 65 - """Add a feed to an existing user. Returns True if user was found.""" 66 - user = self.find_user(username) 67 - if user: 68 - if feed_url not in user.feeds: 69 - user.feeds.append(feed_url) 70 - return True 71 - return False
+5 -6
tests/test_git_store.py
··· 65 65 # Check that user directory was created 66 66 user_dir = store.repo_path / "testuser" 67 67 assert user_dir.exists() 68 - assert (user_dir / "metadata.json").exists() 69 68 70 - # Check metadata file content 71 - with open(user_dir / "metadata.json") as f: 72 - metadata = json.load(f) 73 - assert metadata["username"] == "testuser" 74 - assert metadata["display_name"] == "Test User" 69 + # Check user exists in index 70 + stored_user = store.get_user("testuser") 71 + assert stored_user is not None 72 + assert stored_user.username == "testuser" 73 + assert stored_user.display_name == "Test User" 75 74 76 75 def test_get_user(self, temp_dir): 77 76 """Test getting user metadata."""