From e15b73a49939fe27f6d67305bfe721cd29c37e35 Mon Sep 17 00:00:00 2001 From: Cristian Pufu Date: Fri, 12 Dec 2025 16:05:27 +0200 Subject: [PATCH] fix: add textual samples --- pyproject.toml | 1 + samples/textual-aptabase-counter/README.md | 298 ++++++++++++++ samples/textual-aptabase-counter/main.py | 94 +++++ .../textual-aptabase-counter/pyproject.toml | 64 +++ samples/textual-aptabase-dashboard/README.md | 244 +++++++++++ samples/textual-aptabase-dashboard/main.py | 379 ++++++++++++++++++ .../textual-aptabase-dashboard/pyproject.toml | 86 ++++ 7 files changed, 1166 insertions(+) create mode 100644 samples/textual-aptabase-counter/README.md create mode 100644 samples/textual-aptabase-counter/main.py create mode 100644 samples/textual-aptabase-counter/pyproject.toml create mode 100644 samples/textual-aptabase-dashboard/README.md create mode 100644 samples/textual-aptabase-dashboard/main.py create mode 100644 samples/textual-aptabase-dashboard/pyproject.toml diff --git a/pyproject.toml b/pyproject.toml index 94a3a6b..6882079 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,6 +73,7 @@ line-ending = "auto" mypy_path = "src" explicit_package_bases = true namespace_packages = true +exclude = ["samples/", "testcases/"] follow_imports = "silent" warn_redundant_casts = true warn_unused_ignores = true diff --git a/samples/textual-aptabase-counter/README.md b/samples/textual-aptabase-counter/README.md new file mode 100644 index 0000000..639760e --- /dev/null +++ b/samples/textual-aptabase-counter/README.md @@ -0,0 +1,298 @@ +# Simple Counter - Textual + Aptabase + +A minimal example demonstrating how to integrate [Aptabase](https://aptabase.com/) analytics into a [Textual](https://textual.textualize.io/) TUI application. + +Perfect for learning the basics of adding privacy-first analytics to your terminal apps! + +## šŸŽÆ What It Does + +This is a simple counter app that: +- Increments a counter when you click a button +- Resets the counter to zero +- Tracks all interactions with Aptabase analytics +- Shows notifications for user feedback + +## šŸ“ø Features + +- **Clean UI**: Centered layout with large counter display +- **Two Buttons**: + - "Click Me!" - Increments the counter + - "Reset" - Resets to zero +- **Analytics Tracking**: + - App start/stop events + - Button clicks with counter values + - Reset actions with previous count + - Session duration + +## šŸš€ Quick Start + +### Installation + +#### Using uv (recommended) + +```bash +# Install dependencies +uv pip install textual aptabase + +# Run the app +uv run main.py +``` + +#### Using pip + +```bash +# Create and activate virtual environment +python -m venv venv +source venv/bin/activate # On Windows: venv\Scripts\activate + +# Install dependencies +pip install textual aptabase + +# Run the app +python main.py +``` + +#### Using pyproject.toml + +```bash +# If you have the pyproject.toml file +uv sync +uv run counter +``` + +## āš™ļø Configuration + +Before running, you need to set your Aptabase app key: + +1. Sign up at [aptabase.com](https://aptabase.com/) +2. Create a new app +3. Copy your app key (format: `A-EU-XXXXXXXXXX` or `A-US-XXXXXXXXXX`) +4. Update the key in `simple_counter.py`: + +```python +# Replace with your actual Aptabase app key +app = CounterApp(app_key="A-EU-XXXXXXXXXX") +``` + +## šŸ“Š Tracked Events + +The app automatically tracks: + +### 1. **app_started** +Sent when the app launches. + +```python +{ + "event": "app_started" +} +``` + +### 2. **button_clicked** +Sent when "Click Me!" is pressed. + +```python +{ + "event": "button_clicked", + "action": "increment", + "count": 5 # Current counter value +} +``` + +### 3. **counter_reset** +Sent when "Reset" is pressed. + +```python +{ + "event": "counter_reset", + "previous_count": 10 # Value before reset +} +``` + +### 4. **app_closed** +Sent when the app exits. + +```python +{ + "event": "app_closed", + "final_count": 7 # Final counter value +} +``` + +## šŸŽ® Usage + +1. **Start the app**: Run `python simple_counter.py` +2. **Click the button**: Press "Click Me!" to increment (or press `Enter` when focused) +3. **Reset**: Click "Reset" to set counter back to zero +4. **Quit**: Press `q` or `Ctrl+C` + +## šŸ’» Code Structure + +```python +class CounterApp(App): + def __init__(self, app_key: str): + # Initialize with your Aptabase key + + async def on_mount(self): + # Start Aptabase when app starts + + async def on_unmount(self): + # Stop Aptabase and send final events + + async def on_button_pressed(self, event): + # Handle button clicks and track events +``` + +### Key Components + +**Aptabase Initialization:** +```python +self.aptabase = Aptabase( + app_key=self.app_key, + app_version="1.0.0", + is_debug=True # Shows debug info +) +await self.aptabase.start() +``` + +**Tracking Events:** +```python +await self.aptabase.track("button_clicked", { + "action": "increment", + "count": self.counter +}) +``` + +**Cleanup:** +```python +await self.aptabase.stop() # Flushes pending events +``` + +## šŸ”§ Customization Ideas + +Here are some ways to extend this app: + +### Add More Buttons +```python +yield Button("Increment by 5", id="btn-plus5") +yield Button("Decrement", id="btn-decrement") +``` + +### Track Time Between Clicks +```python +import time + +self.last_click = time.time() + +# In button handler +time_since_last = time.time() - self.last_click +await self.aptabase.track("button_clicked", { + "count": self.counter, + "time_since_last_click": round(time_since_last, 2) +}) +``` + +### Add Keyboard Shortcuts +```python +BINDINGS = [ + Binding("space", "increment", "Increment"), + Binding("r", "reset", "Reset"), +] + +async def action_increment(self): + self.counter += 1 + await self.aptabase.track("keyboard_increment") +``` + +### Save High Score +```python +self.high_score = 0 + +if self.counter > self.high_score: + self.high_score = self.counter + await self.aptabase.track("new_high_score", { + "score": self.high_score + }) +``` + +## šŸ› Troubleshooting + +### "Analytics unavailable" message + +**Cause**: Invalid app key or network issues. + +**Solutions**: +1. Check your app key format: Must be `A-EU-*` or `A-US-*` +2. Verify network connectivity +3. Check Aptabase dashboard status +4. Look for error details in the console + +### Import errors + +```bash +ModuleNotFoundError: No module named 'textual' +``` + +**Solution**: Install dependencies +```bash +pip install textual aptabase +``` + +### App won't start + +**Check Python version**: +```bash +python --version # Must be 3.11+ +``` + +## šŸ“š Learn More + +### Next Steps + +Once you're comfortable with this simple example, check out: + +1. **textual_aptabase_demo.py** - Full dashboard with tabs, forms, and real-time stats +2. **advanced_patterns.py** - User identification, error tracking, performance monitoring + +### Resources + +- **Aptabase**: [https://aptabase.com/docs](https://aptabase.com/docs) +- **Aptabase Python SDK**: [https://github.com/aptabase/aptabase-py](https://github.com/aptabase/aptabase-py) +- **Textual**: [https://textual.textualize.io/](https://textual.textualize.io/) +- **Textual Tutorial**: [https://textual.textualize.io/tutorial/](https://textual.textualize.io/tutorial/) + +## šŸ” Privacy + +Aptabase is privacy-first analytics: +- āœ… No personal data collected +- āœ… No IP addresses stored +- āœ… No cookies or tracking +- āœ… GDPR compliant +- āœ… Open source + +This example only tracks: +- Counter values (anonymous) +- Button click events +- Session duration +- App lifecycle events + +## šŸ“„ License + +MIT License - feel free to use this as a starting point for your own projects! + +## šŸ¤ Contributing + +This is a simple example/demo. Feel free to: +- Fork and modify +- Use in your own projects +- Share improvements + +## ā“ Questions? + +- **Aptabase Support**: [https://aptabase.com/](https://aptabase.com/) +- **Textual Discord**: [https://discord.gg/Enf6Z3qhVr](https://discord.gg/Enf6Z3qhVr) + +--- + +**Happy coding!** šŸš€ + +Built with ā¤ļø using [Textual](https://textual.textualize.io/) and [Aptabase](https://aptabase.com/) \ No newline at end of file diff --git a/samples/textual-aptabase-counter/main.py b/samples/textual-aptabase-counter/main.py new file mode 100644 index 0000000..e9395a2 --- /dev/null +++ b/samples/textual-aptabase-counter/main.py @@ -0,0 +1,94 @@ +""" +Minimal Textual + Aptabase Example + +A simple counter app that tracks button clicks. +""" + +from textual.app import App, ComposeResult +from textual.containers import Center +from textual.widgets import Button, Header, Footer, Static +from aptabase import Aptabase + + +class CounterApp(App): + """A simple counter app with analytics""" + + CSS = """ + Screen { + align: center middle; + } + + #counter { + width: 40; + height: 10; + content-align: center middle; + border: solid green; + margin: 1; + } + + Button { + margin: 1 2; + } + """ + + def __init__(self, app_key: str = "A-EU-0000000000"): + super().__init__() + self.app_key = app_key + self.aptabase: Aptabase | None = None + self.counter = 0 + + async def on_mount(self) -> None: + """Initialize Aptabase""" + try: + self.aptabase = Aptabase( + app_key=self.app_key, + app_version="1.0.0", + is_debug=True + ) + await self.aptabase.start() + await self.aptabase.track("app_started") + self.notify("Analytics connected! āœ…") + except Exception as e: + self.notify(f"Analytics unavailable: {e}", severity="warning") + + async def on_unmount(self) -> None: + """Cleanup Aptabase""" + if self.aptabase: + await self.aptabase.track("app_closed", {"final_count": self.counter}) + await self.aptabase.stop() + + def compose(self) -> ComposeResult: + """Create the UI""" + yield Header() + with Center(): + yield Static(f"[bold cyan]Count: {self.counter}[/bold cyan]", id="counter") + yield Button("Click Me!", id="btn-increment", variant="primary") + yield Button("Reset", id="btn-reset", variant="warning") + yield Footer() + + async def on_button_pressed(self, event: Button.Pressed) -> None: + """Handle button clicks""" + if event.button.id == "btn-increment": + self.counter += 1 + if self.aptabase: + await self.aptabase.track("button_clicked", { + "action": "increment", + "count": self.counter + }) + elif event.button.id == "btn-reset": + old_count = self.counter + self.counter = 0 + if self.aptabase: + await self.aptabase.track("counter_reset", { + "previous_count": old_count + }) + + # Update the counter display + counter_widget = self.query_one("#counter", Static) + counter_widget.update(f"[bold cyan]Count: {self.counter}[/bold cyan]") + + +if __name__ == "__main__": + # Replace with your Aptabase app key + app = CounterApp(app_key="A-EU-0000000000") + app.run() \ No newline at end of file diff --git a/samples/textual-aptabase-counter/pyproject.toml b/samples/textual-aptabase-counter/pyproject.toml new file mode 100644 index 0000000..c1694f8 --- /dev/null +++ b/samples/textual-aptabase-counter/pyproject.toml @@ -0,0 +1,64 @@ +[project] +name = "textual-aptabase-counter" +version = "1.0.0" +description = "A simple counter app demonstrating Aptabase analytics integration with Textual TUI" +authors = [ + {name = "Your Name", email = "your.email@example.com"} +] +readme = "README_COUNTER.md" +requires-python = ">=3.11" +dependencies = [ + "textual>=6.7.1", + "aptabase>=0.0.1", +] +license = {text = "MIT"} +keywords = ["textual", "tui", "analytics", "aptabase", "counter"] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Topic :: Software Development :: Libraries :: Python Modules", +] + +[project.urls] +Homepage = "https://github.com/yourusername/textual-aptabase-counter" +Documentation = "https://github.com/yourusername/textual-aptabase-counter#readme" +Repository = "https://github.com/yourusername/textual-aptabase-counter" +"Bug Tracker" = "https://github.com/yourusername/textual-aptabase-counter/issues" + +[project.scripts] +counter = "main:main" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["."] + +[tool.ruff] +line-length = 100 +target-version = "py311" + +[tool.ruff.lint] +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "I", # isort + "B", # flake8-bugbear + "C4", # flake8-comprehensions +] +ignore = [ + "E501", # line too long (handled by formatter) +] + +[tool.mypy] +python_version = "3.11" +strict = true +warn_return_any = true +warn_unused_configs = true +disallow_untyped_defs = true \ No newline at end of file diff --git a/samples/textual-aptabase-dashboard/README.md b/samples/textual-aptabase-dashboard/README.md new file mode 100644 index 0000000..34e4a40 --- /dev/null +++ b/samples/textual-aptabase-dashboard/README.md @@ -0,0 +1,244 @@ +# Advanced Dashboard - Textual + Aptabase Demo App + +A demonstration of integrating [Aptabase](https://aptabase.com/) analytics into a [Textual](https://textual.textualize.io/) TUI application. + +## Features + +This sample app demonstrates tracking various user interactions: + +- šŸŽÆ **Button clicks** - Track different button types and actions +- šŸ“ **Form inputs** - Monitor user input changes and submissions +- šŸ”€ **Tab navigation** - Track tab switches and navigation patterns +- šŸŽØ **Theme changes** - Monitor dark/light mode toggles +- šŸ“Š **Session analytics** - Track app lifecycle and session duration +- šŸ“ˆ **Real-time stats** - Display live statistics in the sidebar +- šŸ“œ **Event log** - View recent tracked events + +## Screenshots + +The app includes: +- **Dashboard tab**: Interactive buttons with different variants +- **Form tab**: Input fields with submission tracking +- **Data Table tab**: Log of form submissions +- **Sidebar**: Real-time statistics and event log + +## Installation + +### Using uv (recommended) + +```bash +# Add dependencies +uv add textual aptabase + +# Run the app +uv run main.py +``` + +### Using pip + +```bash +# Create virtual environment +python -m venv venv +source venv/bin/activate # On Windows: venv\Scripts\activate + +# Install dependencies +pip install + +# Run the app +python main.py +``` + +## Configuration + +Before running, update the `APP_KEY` in `main.py`: + +```python +# Replace with your actual Aptabase app key +APP_KEY = "A-EU-XXXXXXXXXX" # or A-US-XXXXXXXXXX +``` + +Get your app key from the [Aptabase dashboard](https://aptabase.com/). + +## Usage + +### Running the App + +```bash +python main.py +``` + +### Keyboard Shortcuts + +- `q` - Quit the application +- `d` - Toggle dark/light mode +- `r` - Reset statistics + +### Tracked Events + +The app tracks the following events: + +1. **app_started** - When the app launches + - Properties: `platform`, `theme` + +2. **button_clicked** - When any button is pressed + - Properties: `button_id`, `button_text`, `variant`, `total_clicks` + +3. **input_changed** - When input fields are modified + - Properties: `input_id`, `value_length`, `has_content` + +4. **form_submitted** - When the form is submitted + - Properties: `has_name`, `has_email`, `name_length`, `email_length` + +5. **tab_switched** - When switching between tabs + - Properties: `tab_id`, `tab_title`, `total_switches` + +6. **theme_toggled** - When dark/light mode is changed + - Properties: `new_theme` + +7. **stats_reset** - When statistics are reset + - Properties: `previous_clicks`, `previous_inputs`, `previous_tabs` + +8. **app_closed** - When the app exits + - Properties: `session_duration`, `total_clicks`, `total_inputs`, `total_tab_switches` + +## Code Structure + +```python +# Initialize Aptabase +self.aptabase = Aptabase( + app_key=self.app_key, + app_version="1.0.0", + is_debug=True, + max_batch_size=25, + flush_interval=10.0, +) + +# Track an event +await self.track_event("button_clicked", { + "button_id": "my_button", + "total_clicks": 5 +}) +``` + +## Key Implementation Details + +### Async Context Management + +The app properly manages Aptabase lifecycle: + +```python +async def on_mount(self) -> None: + """Initialize Aptabase when the app starts""" + self.aptabase = Aptabase(...) + await self.aptabase.start() + +async def on_unmount(self) -> None: + """Cleanup Aptabase when the app closes""" + await self.aptabase.stop() +``` + +### Event Tracking Helper + +A helper method simplifies event tracking throughout the app: + +```python +async def track_event(self, event_name: str, properties: dict | None = None) -> None: + """Helper method to track events with Aptabase""" + if self.aptabase: + await self.aptabase.track(event_name, properties or {}) +``` + +### Real-time UI Updates + +The app provides visual feedback for tracked events: + +- Stats widget shows cumulative counts +- Event log displays recent events with timestamps +- Notifications confirm actions + +## Extending the App + +### Adding New Tracked Events + +1. Create an async method to handle the event: + +```python +async def on_custom_action(self, event) -> None: + await self.track_event("custom_action", { + "action_type": "something", + "value": event.value + }) +``` + +2. Update the stats and UI as needed: + +```python +self.stats["custom"] += 1 +self.update_stats_display() +``` + +### Adding New Widgets + +The app uses a flexible layout with sidebar and main content area. Add new widgets in the `compose()` method: + +```python +with TabPane("New Tab", id="tab-new"): + yield YourCustomWidget() +``` + +## Privacy Considerations + +Aptabase is privacy-first analytics: + +- No personal data is collected +- No IP addresses stored +- No cookies or tracking scripts +- GDPR compliant + +This demo tracks only: +- Interaction counts and types +- Session duration +- UI navigation patterns +- Non-sensitive form metadata (lengths, not content) + +## Troubleshooting + +### Aptabase Connection Issues + +If you see "Aptabase unavailable" in the event log: + +1. Check your app key format: `A-EU-*` or `A-US-*` +2. Verify network connectivity +3. Check the Aptabase dashboard for service status +4. Review the console logs for detailed error messages + +### Import Errors + +If you get import errors: + +```bash +# Ensure all dependencies are installed +pip install + +# Or with uv +uv sync +``` + +## Resources + +- [Aptabase Documentation](https://aptabase.com/docs) +- [Textual Documentation](https://textual.textualize.io/) +- [Aptabase Python SDK](https://github.com/aptabase/aptabase-python) + +## License + +MIT License - feel free to use this as a starting point for your own projects! + +## Contributing + +This is a demo app. Feel free to fork and modify for your needs! + +## Questions? + +- Aptabase: [https://aptabase.com/](https://aptabase.com/) +- Textual: [https://textual.textualize.io/](https://textual.textualize.io/) \ No newline at end of file diff --git a/samples/textual-aptabase-dashboard/main.py b/samples/textual-aptabase-dashboard/main.py new file mode 100644 index 0000000..bad92be --- /dev/null +++ b/samples/textual-aptabase-dashboard/main.py @@ -0,0 +1,379 @@ +""" +Textual Dashboard Demo with Aptabase Analytics + +A sample application demonstrating how to integrate Aptabase tracking +into a Textual TUI application. +""" + +from datetime import datetime +from textual.app import App, ComposeResult +from textual.containers import Container, Horizontal, Vertical +from textual.widgets import ( + Button, + Header, + Footer, + Static, + Input, + Label, + DataTable, + TabbedContent, + TabPane, +) +from textual.binding import Binding +from aptabase import Aptabase, AptabaseError + + +class StatsWidget(Static): + """Widget to display app statistics""" + + def __init__(self) -> None: + super().__init__() + self.clicks = 0 + self.inputs = 0 + self.tab_switches = 0 + + def update_stats(self, clicks: int, inputs: int, tabs: int) -> None: + self.clicks = clicks + self.inputs = inputs + self.tab_switches = tabs + self.update(self.render()) + + def render(self) -> str: + return f"""[bold cyan]Session Statistics[/bold cyan] +━━━━━━━━━━━━━━━━━━━━━━━━ +Button Clicks: [green]{self.clicks}[/green] +Form Inputs: [yellow]{self.inputs}[/yellow] +Tab Switches: [magenta]{self.tab_switches}[/magenta] +""" + + +class EventLogWidget(Static): + """Widget to display recent events""" + + def __init__(self) -> None: + super().__init__() + self.events: list[str] = [] + self.max_events = 8 + + def add_event(self, event: str) -> None: + timestamp = datetime.now().strftime("%H:%M:%S") + self.events.insert(0, f"[dim]{timestamp}[/dim] {event}") + if len(self.events) > self.max_events: + self.events.pop() + self.update(self.render()) + + def render(self) -> str: + header = "[bold cyan]Recent Events[/bold cyan]\n━━━━━━━━━━━━━━━━━━━━━━━━\n" + if not self.events: + return header + "[dim]No events yet...[/dim]" + return header + "\n".join(self.events) + + +class DashboardApp(App): + """A Textual dashboard app with Aptabase analytics""" + + CSS = """ + Screen { + background: $surface; + } + + #main-container { + height: 100%; + padding: 1 2; + } + + #content-area { + height: 1fr; + } + + #sidebar { + width: 35; + height: 100%; + border: solid $primary; + padding: 1; + } + + #main-content { + width: 1fr; + height: 100%; + margin-left: 1; + } + + .card { + border: solid $accent; + padding: 1 2; + margin-bottom: 1; + height: auto; + } + + Button { + margin: 1 2; + min-width: 20; + } + + Input { + margin: 0 2 1 2; + } + + DataTable { + height: 1fr; + margin: 1 2; + } + + Label { + padding: 0 2; + color: $text-muted; + } + + StatsWidget { + height: 10; + margin-bottom: 1; + } + + EventLogWidget { + height: 1fr; + } + + TabbedContent { + height: 1fr; + } + """ + + BINDINGS = [ + Binding("q", "quit", "Quit", priority=True), + Binding("d", "toggle_dark", "Toggle Dark Mode"), + Binding("r", "reset_stats", "Reset Stats"), + ] + + def __init__(self, app_key: str = "A-EU-0000000000"): + super().__init__() + self.app_key = app_key + self.aptabase: Aptabase | None = None + self.stats = {"clicks": 0, "inputs": 0, "tabs": 0} + self.session_start = datetime.now() + + async def on_mount(self) -> None: + """Initialize Aptabase when the app starts""" + try: + # Initialize Aptabase with async context + self.aptabase = Aptabase( + app_key=self.app_key, + app_version="1.0.0", + is_debug=True, + max_batch_size=25, + flush_interval=10.0, + ) + await self.aptabase.start() + + # Track app start + await self.track_event("app_started", { + "platform": "textual", + "theme": "dark" if self.dark else "light" + }) + + # Update event log + event_log = self.query_one(EventLogWidget) + event_log.add_event("āœ… [green]Aptabase connected[/green]") + + except Exception as e: + self.notify(f"Analytics initialization failed: {e}", severity="warning") + event_log = self.query_one(EventLogWidget) + event_log.add_event("āš ļø [yellow]Aptabase unavailable[/yellow]") + + async def on_unmount(self) -> None: + """Cleanup Aptabase when the app closes""" + if self.aptabase: + try: + # Track session end with summary + session_duration = (datetime.now() - self.session_start).total_seconds() + await self.track_event("app_closed", { + "session_duration": round(session_duration, 2), + "total_clicks": self.stats["clicks"], + "total_inputs": self.stats["inputs"], + "total_tab_switches": self.stats["tabs"] + }) + + await self.aptabase.stop() + except Exception as e: + self.log(f"Error stopping Aptabase: {e}") + + def compose(self) -> ComposeResult: + """Create the UI layout""" + yield Header() + + with Container(id="main-container"): + with Horizontal(id="content-area"): + # Sidebar with stats and event log + with Vertical(id="sidebar"): + yield StatsWidget() + yield EventLogWidget() + + # Main content area with tabs + with Vertical(id="main-content"): + with TabbedContent(): + with TabPane("Dashboard", id="tab-dashboard"): + yield Static( + "[bold]Welcome to Textual + Aptabase Demo![/bold]\n\n" + "This app demonstrates analytics tracking in a TUI.", + classes="card" + ) + with Horizontal(): + yield Button("Click Me! šŸŽÆ", id="btn-track", variant="primary") + yield Button("Success āœ…", id="btn-success", variant="success") + yield Button("Warning āš ļø", id="btn-warning", variant="warning") + + with TabPane("Form", id="tab-form"): + yield Label("Enter some data to track form interactions:") + yield Input(placeholder="Your name", id="input-name") + yield Input(placeholder="Your email", id="input-email") + yield Button("Submit Form", id="btn-submit", variant="primary") + + with TabPane("Data Table", id="tab-table"): + table = DataTable() + table.add_columns("ID", "Action", "Timestamp") + yield table + + yield Footer() + + async def track_event(self, event_name: str, properties: dict | None = None) -> None: + """Helper method to track events with Aptabase""" + if self.aptabase: + try: + await self.aptabase.track(event_name, properties or {}) + + # Update event log + event_log = self.query_one(EventLogWidget) + props_str = f" ({list(properties.keys())})" if properties else "" + event_log.add_event(f"šŸ“Š Tracked: [cyan]{event_name}[/cyan]{props_str}") + + except AptabaseError as e: + self.log(f"Error tracking event: {e}") + + def update_stats_display(self) -> None: + """Update the stats widget""" + stats_widget = self.query_one(StatsWidget) + stats_widget.update_stats( + self.stats["clicks"], + self.stats["inputs"], + self.stats["tabs"] + ) + + async def on_button_pressed(self, event: Button.Pressed) -> None: + """Handle button presses""" + button_id = event.button.id + self.stats["clicks"] += 1 + self.update_stats_display() + + # Track different button types + if button_id == "btn-track": + await self.track_event("button_clicked", { + "button_id": button_id, + "button_text": "Click Me", + "total_clicks": self.stats["clicks"] + }) + self.notify("Button clicked! Event tracked. šŸŽÆ") + + elif button_id == "btn-success": + await self.track_event("button_clicked", { + "button_id": button_id, + "variant": "success" + }) + self.notify("Success action tracked! āœ…", severity="information") + + elif button_id == "btn-warning": + await self.track_event("button_clicked", { + "button_id": button_id, + "variant": "warning" + }) + self.notify("Warning action tracked! āš ļø", severity="warning") + + elif button_id == "btn-submit": + name_input = self.query_one("#input-name", Input) + email_input = self.query_one("#input-email", Input) + + await self.track_event("form_submitted", { + "has_name": bool(name_input.value), + "has_email": bool(email_input.value), + "name_length": len(name_input.value), + "email_length": len(email_input.value) + }) + + # Add to data table + table = self.query_one(DataTable) + row_id = len(table.rows) + 1 + timestamp = datetime.now().strftime("%H:%M:%S") + table.add_row(str(row_id), "Form Submit", timestamp) + + self.notify(f"Form submitted! Data: {name_input.value or 'N/A'}", severity="information") + + # Clear inputs + name_input.value = "" + email_input.value = "" + + async def on_input_changed(self, event: Input.Changed) -> None: + """Track input field changes""" + if event.value: # Only track when there's content + self.stats["inputs"] += 1 + self.update_stats_display() + + await self.track_event("input_changed", { + "input_id": event.input.id, + "value_length": len(event.value), + "has_content": bool(event.value) + }) + + async def on_tabbed_content_tab_activated(self, event: TabbedContent.TabActivated) -> None: + """Track tab switches""" + self.stats["tabs"] += 1 + self.update_stats_display() + + await self.track_event("tab_switched", { + "tab_id": event.pane.id, + "tab_title": event.pane.title if hasattr(event.pane, 'title') else str(event.pane.id), + "total_switches": self.stats["tabs"] + }) + + async def action_toggle_dark(self) -> None: + """Toggle dark mode and track the action""" + self.dark = not self.dark + + await self.track_event("theme_toggled", { + "new_theme": "dark" if self.dark else "light" + }) + + self.notify(f"Switched to {'dark' if self.dark else 'light'} mode") + + async def action_reset_stats(self) -> None: + """Reset statistics""" + old_stats = self.stats.copy() + self.stats = {"clicks": 0, "inputs": 0, "tabs": 0} + self.update_stats_display() + + await self.track_event("stats_reset", { + "previous_clicks": old_stats["clicks"], + "previous_inputs": old_stats["inputs"], + "previous_tabs": old_stats["tabs"] + }) + + self.notify("Statistics reset! šŸ”„") + + async def action_quit(self) -> None: + """Quit the application""" + await self.track_event("app_quit_requested", { + "method": "keyboard_shortcut" + }) + self.exit() + + +def main(): + """Run the dashboard app""" + # Replace with your actual Aptabase app key + # Format: A-EU-XXXXXXXXXX or A-US-XXXXXXXXXX + APP_KEY = "A-EU-0000000000" # Demo key - replace with yours! + + app = DashboardApp(app_key=APP_KEY) + app.run() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/samples/textual-aptabase-dashboard/pyproject.toml b/samples/textual-aptabase-dashboard/pyproject.toml new file mode 100644 index 0000000..9893023 --- /dev/null +++ b/samples/textual-aptabase-dashboard/pyproject.toml @@ -0,0 +1,86 @@ +[project] +name = "textual-aptabase-dashboard" +version = "1.0.0" +description = "A comprehensive dashboard demonstrating Aptabase analytics integration with Textual TUI" +authors = [ + {name = "Your Name", email = "your.email@example.com"} +] +readme = "README.md" +requires-python = ">=3.11" +dependencies = [ + "textual>=6.7.1", + "aptabase>=0.0.1", +] +license = {text = "MIT"} +keywords = ["textual", "tui", "analytics", "aptabase", "dashboard"] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: Terminals", +] + +[project.urls] +Homepage = "https://github.com/yourusername/textual-aptabase-dashboard" +Documentation = "https://github.com/yourusername/textual-aptabase-dashboard#readme" +Repository = "https://github.com/yourusername/textual-aptabase-dashboard" +"Bug Tracker" = "https://github.com/yourusername/textual-aptabase-dashboard/issues" +Aptabase = "https://aptabase.com/" +Textual = "https://textual.textualize.io/" + +[project.scripts] +dashboard = "textual_aptabase_demo:main" + +[project.optional-dependencies] +dev = [ + "pytest>=7.4.0", + "pytest-asyncio>=0.21.0", + "ruff>=0.1.0", + "mypy>=1.7.0", + "textual-dev>=1.0.0", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["."] + +[tool.ruff] +line-length = 100 +target-version = "py311" + +[tool.ruff.lint] +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "I", # isort + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "UP", # pyupgrade + "ARG", # flake8-unused-arguments +] +ignore = [ + "E501", # line too long (handled by formatter) +] + +[tool.ruff.lint.isort] +known-first-party = ["textual_aptabase_demo"] + +[tool.mypy] +python_version = "3.11" +strict = true +warn_return_any = true +warn_unused_configs = true +disallow_untyped_defs = true + +[tool.pytest.ini_options] +asyncio_mode = "auto" +testpaths = ["tests"] +pythonpath = ["."] \ No newline at end of file