capcat.core.source_system.add_source_command
File: Application/capcat/core/source_system/add_source_command.py
Description
Professional implementation of the add-source command using clean architecture principles. Separates concerns through dependency injection and follows SOLID principles.
Classes
SourceMetadata
Value object containing all source metadata.
Methods
validate
def validate(self) -> None
Validate source metadata.
Parameters:
self
Returns: None
FeedIntrospector
Inherits from: Protocol
Protocol for RSS feed introspection.
Implementations read a feed URL and expose its title and base URL.
See RssFeedIntrospector for the production implementation.
Methods
feed_title
def feed_title(self) -> str
Human-readable title extracted from the RSS/Atom feed.
Parameters:
self
Returns: str
base_url
def base_url(self) -> str
Root URL of the publisher (stripped of path).
Parameters:
self
Returns: str
UserInterface
Inherits from: Protocol
Protocol for user interaction during the add-source workflow.
Implementations may use questionary (interactive TUI), a mock (tests), or any other mechanism that satisfies this contract.
Methods
get_display_name
def get_display_name(self, suggested: str) -> str
Prompt the user to confirm or override the suggested display name.
Args: suggested: Feed title extracted from the RSS feed.
Returns: The confirmed or overridden display name string.
Parameters:
selfsuggested(str)
Returns: str
get_source_id
def get_source_id(self, suggested: str) -> str
Prompt the user to confirm or override the suggested source ID.
Args:
suggested: Auto-derived source ID (e.g. "mysite").
Returns: The confirmed or overridden source ID string.
Parameters:
selfsuggested(str)
Returns: str
select_category
def select_category(self, categories: List[str]) -> str
Prompt the user to choose a topic category.
Args: categories: Available category names.
Returns: The selected category string.
Parameters:
selfcategories(List[str])
Returns: str
get_article_count
def get_article_count(self) -> int
Prompt user for articles per run. Default: 30.
Returns: Positive integer article count.
Parameters:
self
Returns: int
confirm_bundle_addition
def confirm_bundle_addition(self) -> bool
Ask whether to add the new source to an existing bundle.
Returns:
True if the user wants to add to a bundle.
Parameters:
self
Returns: bool
select_bundle
def select_bundle(self, bundles: List[str]) -> Optional[str]
Prompt the user to pick a bundle to add the source to.
Args: bundles: Available bundle names.
Returns:
Selected bundle name, or None if cancelled.
Parameters:
selfbundles(List[str])
Returns: Optional[str]
confirm_test_fetch
def confirm_test_fetch(self) -> bool
Ask whether to run a test fetch after saving the config.
Returns:
True if the user wants a test fetch.
Parameters:
self
Returns: bool
show_success
def show_success(self, message: str) -> None
Display a success notification.
Args: message: Success text to show the user.
Parameters:
selfmessage(str)
Returns: None
show_error
def show_error(self, message: str) -> None
Display an error notification.
Args: message: Error text to show the user.
Parameters:
selfmessage(str)
Returns: None
ConfigGenerator
Inherits from: Protocol
Protocol for configuration file generation.
Methods
generate_and_save
def generate_and_save(self, metadata: SourceMetadata, config_path: Path) -> Path
Generate a YAML config file and write it to disk.
Args: metadata: Source metadata to serialize. config_path: Directory where the config file should be saved.
Returns: Path to the written config file.
Parameters:
selfmetadata(SourceMetadata)config_path(Path)
Returns: Path
BundleManager
Inherits from: Protocol
Protocol for bundle management.
Methods
get_bundle_names
def get_bundle_names(self) -> List[str]
Return the names of all available bundles.
Returns: List of bundle name strings.
Parameters:
self
Returns: List[str]
add_source_to_bundle
def add_source_to_bundle(self, source_id: str, bundle_name: str) -> None
Add a source to a named bundle in bundles.yml.
Args: source_id: Source identifier to add. bundle_name: Bundle to add the source to.
Parameters:
selfsource_id(str)bundle_name(str)
Returns: None
SourceTester
Inherits from: Protocol
Protocol for testing new sources.
Methods
test_source
def test_source(self, source_id: str, count: int = 1) -> bool
Run a test fetch to verify the source is functional.
Args: source_id: The source identifier to test. count: Number of articles to attempt fetching.
Returns:
True if at least one article was fetched successfully.
Parameters:
selfsource_id(str)count(int) optional
Returns: bool
CategoryProvider
Inherits from: Protocol
Protocol for category management.
Methods
get_available_categories
def get_available_categories(self) -> List[str]
Return all available topic category names.
Returns:
List of category strings (e.g. ["tech", "science", "news"]).
Parameters:
self
Returns: List[str]
AddSourceCommand
Command to add a new RSS source using clean architecture principles.
Follows SOLID principles:
- Single Responsibility: Only orchestrates the add-source workflow
- Open/Closed: Extensible through dependency injection
- Liskov Substitution: Uses protocols for type safety
- Interface Segregation: Small, focused protocols
- Dependency Inversion: Depends on abstractions, not concretions
Methods
init
def __init__(self, introspector_factory: 'IntrospectorFactory', ui: UserInterface, config_generator: ConfigGenerator, bundle_manager: BundleManager, source_tester: SourceTester, category_provider: CategoryProvider, config_path: Path, bundles_path: Path, logger: Optional[Any] = None) -> None
Wire up all dependencies for the add-source workflow.
Args: introspector_factory: Creates FeedIntrospector instances for URLs. ui: User interaction layer (questionary, mock, etc.). config_generator: Writes YAML config files to disk. bundle_manager: Reads and updates bundles.yml. source_tester: Runs test fetches against new sources. category_provider: Returns available topic categories. config_path: Directory where new source configs are saved. bundles_path: Path to bundles.yml. logger: Optional logger; defaults to module logger.
Parameters:
selfintrospector_factory(‘IntrospectorFactory’)ui(UserInterface)config_generator(ConfigGenerator)bundle_manager(BundleManager)source_tester(SourceTester)category_provider(CategoryProvider)config_path(Path)bundles_path(Path)logger(Optional[Any]) optional
Returns: None
execute
def execute(self, url: str) -> Path
Execute the add-source command.
Args: url: RSS feed URL to add
Returns: Path to the written config file.
Raises: CapcatError: If any step in the process fails
Parameters:
selfurl(str)
Returns: Path
_introspect_feed
def _introspect_feed(self, url: str) -> FeedIntrospector
Step 1: Introspect the RSS feed.
Parameters:
selfurl(str)
Returns: FeedIntrospector
_collect_source_metadata
def _collect_source_metadata(self, introspector: FeedIntrospector, url: str) -> SourceMetadata
Step 2: Collect all required metadata from user and introspector.
Parameters:
selfintrospector(FeedIntrospector)url(str)
Returns: SourceMetadata
_generate_configuration
def _generate_configuration(self, metadata: SourceMetadata) -> Path
Step 3: Generate and save configuration file.
Parameters:
selfmetadata(SourceMetadata)
Returns: Path
_handle_bundle_integration
def _handle_bundle_integration(self, source_id: str) -> None
Step 4: Handle optional bundle integration.
Parameters:
selfsource_id(str)
Returns: None
_handle_source_testing
def _handle_source_testing(self, source_id: str) -> None
Step 5: Handle optional source testing.
Parameters:
selfsource_id(str)
Returns: None
_generate_source_id_suggestion
def _generate_source_id_suggestion(self, feed_title: str) -> str
Generate a suggested source ID from feed title.
Parameters:
selffeed_title(str)
Returns: str
IntrospectorFactory
Inherits from: Protocol
Factory for creating feed introspectors.
Methods
create
def create(self, url: str) -> FeedIntrospector
Create a FeedIntrospector for the given feed URL.
Args: url: RSS/Atom feed URL to introspect.
Returns: A FeedIntrospector instance ready to expose feed metadata.
Parameters:
selfurl(str)
Returns: FeedIntrospector
RssFeedIntrospectorAdapter
Adapter to make existing RssFeedIntrospector compatible with protocol.
Methods
init
def __init__(self, introspector)
Wrap an existing RssFeedIntrospector instance.
Args:
introspector: An RssFeedIntrospector instance whose
feed_title and base_url attributes will be proxied.
Parameters:
selfintrospector
feed_title
def feed_title(self) -> str
Human-readable title extracted from the wrapped introspector.
Parameters:
self
Returns: str
base_url
def base_url(self) -> str
Root URL of the publisher from the wrapped introspector.
Parameters:
self
Returns: str
RssFeedIntrospectorFactory
Factory for creating RSS feed introspectors.
Methods
create
def create(self, url: str) -> FeedIntrospector
Create an adapted RSS feed introspector for the given URL.
Instantiates RssFeedIntrospector and wraps it in
RssFeedIntrospectorAdapter so it satisfies the
FeedIntrospector protocol.
Args: url: RSS/Atom feed URL to introspect.
Returns:
An RssFeedIntrospectorAdapter exposing feed_title
and base_url for the given feed.
Parameters:
selfurl(str)
Returns: FeedIntrospector
SourceConfigGeneratorAdapter
Adapter for existing SourceConfigGenerator.
Methods
init
def __init__(self, generator_class)
Store the SourceConfigGenerator class for deferred instantiation.
Args:
generator_class: The SourceConfigGenerator class (not an
instance). It will be instantiated per call to
generate_and_save with the serialized metadata dict.
Parameters:
selfgenerator_class
generate_and_save
def generate_and_save(self, metadata: SourceMetadata, config_path: Path) -> Path
Serialize metadata and delegate to the wrapped generator class.
Converts SourceMetadata to the dict format expected by
SourceConfigGenerator, instantiates it, and calls its own
generate_and_save method.
Args: metadata: Source metadata to serialize into a YAML config file. config_path: Directory where the config file should be written.
Returns: Path to the written YAML config file.
Parameters:
selfmetadata(SourceMetadata)config_path(Path)
Returns: Path
SubprocessSourceTester
Lightweight RSS connectivity tester with live progress display.
Methods
test_source
def test_source(self, source_id: str, count: int = 1) -> bool
Test a source by fetching its RSS feed and counting articles.
Replaces the old subprocess approach: no full article download, no output files created, no 60-second wait. Just an RSS HEAD + GET + parse - typically completes in 2-5 seconds.
Shows the existing ProgressIndicator with live stage updates so the user always knows what is happening.
Args: source_id: The source identifier to test. count: Unused (kept for interface compatibility).
Returns:
True if the feed is reachable and contains at least one
entry, False otherwise.
Parameters:
selfsource_id(str)count(int) optional
Returns: bool
_get_rss_url
def _get_rss_url(self, source_id: str) -> Optional[str]
Return the rss_url from the saved YAML for source_id, or None.
Parameters:
selfsource_id(str)
Returns: Optional[str]
RegistryCategoryProvider
Category provider using source registry.
Methods
get_available_categories
def get_available_categories(self) -> List[str]
Return categories derived from all currently registered sources.
Queries the global SourceRegistry for all active source configs
and collects unique category values. Falls back to a hard-coded
default list if the registry is unavailable or yields no categories.
Returns:
Sorted list of category strings (e.g. ["ai", "news", "tech"]).
Defaults to ['tech', 'news', 'science', 'ai', 'sports', 'general']
if the registry cannot be reached.
Parameters:
self
Returns: List[str]
Functions
_stop_silent
def _stop_silent()
Stop spinner and clear line without printing a summary.