Initial Repo Setup

This commit is contained in:
Jacob Schmidt 2025-04-11 22:45:43 -05:00
commit a136a20592
18 changed files with 883 additions and 0 deletions

15
.gitignore vendored Normal file
View File

@ -0,0 +1,15 @@
build
dist
.venv
__pycache__
*.py[cod]
*_pycache_*
*.pyo
*.pyd
*.db
*.sqlite
*.egg-info
*.egg
*.log
*.env
.pypirc

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 IDSolutions
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

8
MANIFEST.in Normal file
View File

@ -0,0 +1,8 @@
include firefly/templates/*.html
include README.md
include requirements.txt
include LICENSE
recursive-include firefly/templates *
recursive-include firefly/static *
global-exclude __pycache__
global-exclude *.py[cod]

77
README.md Normal file
View File

@ -0,0 +1,77 @@
# Firefly Database Viewer
A web-based viewer for Firefly databases with support for strings, lists, and hash tables.
## Features
- View all Firefly database keys and their values
- Support for different data types (string, list, hash)
- Modern, responsive UI with dark mode support
- Real-time data refresh
- Color-coded data type indicators
## Installation
### From Source
1. Clone the repository and navigate to the project directory
2. Create a virtual environment (recommended):
```bash
python -m venv .venv
source .venv/bin/activate # On Windows: .venv\Scripts\activate
```
3. Install the package in development mode:
```bash
pip install -e .
```
### Using pip (when published)
```bash
pip install firefly-viewer
```
## Configuration
Create a `.env` file in your working directory with your Firefly connection details:
```env
FIREFLY_HOST=localhost
FIREFLY_PORT=6379
FIREFLY_PASSWORD=your_password
FIREFLY_DEBUG=false
```
## Usage
### Command Line Interface
Start the viewer using the command-line interface:
```bash
firefly-viewer
```
Options:
- `--host`: Host to bind to (default: 0.0.0.0)
- `--port`: Port to bind to (default: 5000)
- `--debug/--no-debug`: Enable/disable debug mode
Example:
```bash
firefly-viewer --host 127.0.0.1 --port 8000 --debug
```
### Web Interface
1. Open your web browser and navigate to the URL shown in the console (default: http://localhost:5000)
2. Use the "Refresh Data" button to update the view with the latest database contents
3. Toggle between light and dark themes using the theme button
## Requirements
- Python 3.7+
- Firefly database server
- Modern web browser

13
app.py Normal file
View File

@ -0,0 +1,13 @@
from flask import Flask
from firefly.routes import routes
from firefly.config import DEBUG
def create_app():
app = Flask(__name__)
app.register_blueprint(routes)
return app
app = create_app()
if __name__ == "__main__":
app.run(debug=DEBUG)

BIN
backup.7z Normal file

Binary file not shown.

38
build.py Normal file
View File

@ -0,0 +1,38 @@
import os
import subprocess
import shutil
def clean_build():
"""Clean up build artifacts"""
dirs_to_clean = ['build', 'dist', '*.egg-info']
for dir_pattern in dirs_to_clean:
for item in glob.glob(dir_pattern):
if os.path.isdir(item):
shutil.rmtree(item)
else:
os.remove(item)
def build_package():
"""Build the package"""
clean_build()
subprocess.check_call(['python', 'setup.py', 'sdist', 'bdist_wheel'])
def install_package():
"""Install the package in development mode"""
subprocess.check_call(['pip', 'install', '-e', '.'])
if __name__ == '__main__':
import sys
import glob
command = sys.argv[1] if len(sys.argv) > 1 else 'build'
if command == 'clean':
clean_build()
elif command == 'build':
build_package()
elif command == 'install':
install_package()
else:
print(f"Unknown command: {command}")
print("Available commands: clean, build, install")

23
firefly/__init__.py Normal file
View File

@ -0,0 +1,23 @@
"""
Firefly Database Viewer
A Flask-based web interface for viewing and interacting with Firefly databases.
"""
import os
from flask import Flask
from .config import *
from .database import DatabaseClient
from .routes import routes
from .services import KeyService
from .logger import logger
def create_app():
template_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), 'templates'))
app = Flask(__name__, template_folder=template_dir)
app.debug = DEBUG
app.register_blueprint(routes)
return app
app = create_app()
__version__ = "1.0.0"

27
firefly/cli.py Normal file
View File

@ -0,0 +1,27 @@
import os
import click
from waitress import serve
from .config import DEBUG
from . import app
@click.command()
@click.option('--host', default='0.0.0.0', help='Host to bind to')
@click.option('--port', default=5000, help='Port to bind to')
@click.option('--debug/--no-debug', default=None, help='Enable debug mode')
def main(host, port, debug):
"""Firefly Database Viewer - A web interface for viewing Firefly databases"""
if debug is None:
debug = DEBUG
if debug:
click.echo(f"Starting Firefly Database Viewer in DEBUG mode on http://{host}:{port}")
click.echo("Press CTRL+C to quit")
app.run(host=host, port=port, debug=True)
else:
click.echo(f"Starting Firefly Database Viewer in PRODUCTION mode on http://{host}:{port}")
click.echo("Using Waitress production server")
click.echo("Press CTRL+C to quit")
serve(app, host=host, port=port, threads=4)
if __name__ == '__main__':
main()

13
firefly/config.py Normal file
View File

@ -0,0 +1,13 @@
import os
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
# Database configuration
FIREFLY_HOST = os.getenv("FIREFLY_HOST", "localhost")
FIREFLY_PORT = int(os.getenv("FIREFLY_PORT", 6379))
FIREFLY_PASSWORD = os.getenv("FIREFLY_PASSWORD", None)
# Debug configuration
DEBUG = os.getenv("FIREFLY_DEBUG", "false").lower() == "true"

29
firefly/database.py Normal file
View File

@ -0,0 +1,29 @@
from ifireflylib import IFireflyClient as FireflyDatabase
from .config import FIREFLY_HOST, FIREFLY_PORT, FIREFLY_PASSWORD
from .logger import logger
class DatabaseClient:
@staticmethod
def create_client():
try:
return FireflyDatabase(
host=FIREFLY_HOST,
port=FIREFLY_PORT,
password=FIREFLY_PASSWORD,
)
except Exception as e:
logger.error(f"Failed to create Firefly client: {e}")
return None
@staticmethod
def get_key_type(client, key):
try:
type_result = client.execute_command("TYPE", key)
if type_result and isinstance(type_result, str):
if type_result.startswith(('+', '-', ':', '$', '*')):
type_result = type_result[1:]
return type_result.strip().lower()
return None
except Exception as e:
logger.debug(f"TYPE command failed for {key}: {e}")
return None

22
firefly/logger.py Normal file
View File

@ -0,0 +1,22 @@
import logging
import sys
from .config import DEBUG
def setup_logger():
logger = logging.getLogger("FireflyViewer")
if DEBUG:
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
handlers=[
logging.FileHandler("firefly_debug.log"),
logging.StreamHandler(sys.stdout),
],
)
else:
logger.addHandler(logging.NullHandler())
return logger
logger = setup_logger()

118
firefly/routes.py Normal file
View File

@ -0,0 +1,118 @@
from flask import Blueprint, render_template, jsonify, request
from .database import DatabaseClient
from .services import KeyService
from .logger import logger
routes = Blueprint('routes', __name__)
@routes.route('/')
def index():
return render_template('index.html')
@routes.route('/api/keys', methods=['GET'])
def get_keys():
logger.debug("API endpoint /api/keys called")
try:
client = DatabaseClient.create_client()
if not client:
return jsonify({
'success': False,
'error': 'Could not connect to Firefly database',
'connection_status': False
})
pattern = request.args.get('pattern', '*')
keys = client.string_ops.keys(pattern)
if not keys:
return jsonify({
'success': True,
'data': {'strings': [], 'lists': [], 'hashes': []},
'connection_status': True
})
strings = []
lists = []
hashes = []
for key in keys:
key_type = DatabaseClient.get_key_type(client, key)
if key_type == "string":
value = client.string_ops.string_get(key)
if value is not None:
strings.append({"key": key, "value": value})
elif key_type == "list":
value = client.list_ops.list_range(key, 0, -1)
if value is not None:
is_email = 'email' in key.lower()
processed_value = KeyService.process_list_values(value, is_email)
lists.append({"key": key, "value": processed_value})
elif key_type == "hash":
value = client.hash_ops.hash_get_all(key)
if value is not None:
processed_value = KeyService.parse_hash_data(value)
hashes.append({"key": key, "value": processed_value})
return jsonify({
'success': True,
'data': {
'strings': strings,
'lists': lists,
'hashes': hashes
},
'connection_status': True
})
except Exception as e:
logger.error(f"Error getting keys: {e}")
return jsonify({
'success': False,
'error': str(e),
'connection_status': False
})
@routes.route('/api/key/<key>', methods=['GET'])
def get_key_value(key):
try:
client = DatabaseClient.create_client()
if not client:
return jsonify({
'success': False,
'error': 'Could not connect to Firefly database',
'connection_status': False
})
key_type = DatabaseClient.get_key_type(client, key)
if not key_type:
return jsonify({
'success': False,
'error': 'Key not found or type not determined',
'connection_status': True
}), 404
value = None
if key_type == "string":
value = client.string_ops.string_get(key)
elif key_type == "hash":
value = client.hash_ops.hash_get_all(key)
value = KeyService.parse_hash_data(value)
elif key_type == "list":
value = client.list_ops.list_range(key, 0, -1)
is_email = 'email' in key.lower()
value = KeyService.process_list_values(value, is_email)
return jsonify({
'success': True,
'data': {
'key': key,
'type': key_type,
'value': value
},
'connection_status': True
})
except Exception as e:
logger.error(f"Error getting key value: {e}")
return jsonify({
'success': False,
'error': str(e),
'connection_status': False
})

69
firefly/services.py Normal file
View File

@ -0,0 +1,69 @@
from .logger import logger
class KeyService:
@staticmethod
def parse_hash_data(hash_data):
properly_parsed_hash = {}
if isinstance(hash_data, dict):
all_pairs = []
for k in hash_data.keys():
if '=' in k:
all_pairs.append(k)
for v in hash_data.values():
if isinstance(v, str) and '=' in v:
all_pairs.append(v)
for pair in all_pairs:
if '=' in pair:
field, value = pair.split('=', 1)
properly_parsed_hash[field.strip()] = KeyService._parse_value(value.strip())
elif isinstance(hash_data, str):
lines = hash_data.strip().split('\n')
for line in lines:
if '=' in line:
field, value = line.split('=', 1)
properly_parsed_hash[field.strip()] = KeyService._parse_value(value.strip())
return properly_parsed_hash
@staticmethod
def _parse_value(value):
if value.startswith('[') and value.endswith(']'):
try:
array_str = value[1:-1]
array_values = [v.strip() for v in array_str.split(',')]
return ", ".join(array_values)
except Exception as e:
logger.error(f"Error parsing array value: {e}")
return value
return value
@staticmethod
def clean_redis_value(value):
if isinstance(value, str) and value.startswith(('+', '-', ':', '$', '*')):
return value[1:].strip()
return value
@staticmethod
def format_list(values):
formatted_items = []
for i in range(0, len(values), 2):
if i + 1 < len(values):
field = KeyService.clean_redis_value(values[i])
value = KeyService.clean_redis_value(values[i + 1])
field = field.capitalize()
formatted_items.append(f"{field}: {value}")
return formatted_items
@staticmethod
def process_list_values(values, is_email=False):
if is_email and len(values) >= 2 and len(values) % 2 == 0:
return KeyService.format_list(values)
else:
cleaned_values = []
for v in values:
cleaned_v = KeyService.clean_redis_value(v)
cleaned_values.append(cleaned_v)
return cleaned_values

View File

@ -0,0 +1,304 @@
<!DOCTYPE html>
<html lang="en" data-bs-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Firefly Database Viewer</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
:root {
--bs-body-bg: #ffffff;
--bs-body-color: #212529;
}
[data-bs-theme="dark"] {
--bs-body-bg: #212529;
--bs-body-color: #f8f9fa;
}
body {
transition: background-color 0.3s ease, color 0.3s ease;
}
.key-card {
margin-bottom: 1rem;
transition: all 0.3s ease;
}
.key-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
.value-container {
max-height: 200px;
overflow-y: auto;
}
.type-badge {
font-size: 0.8rem;
padding: 0.3rem 0.6rem;
}
.connection-status {
position: fixed;
top: 1rem;
right: 1rem;
z-index: 1000;
}
.section-header {
margin-top: 2rem;
margin-bottom: 1rem;
padding-bottom: 0.5rem;
border-bottom: 2px solid #eee;
}
.email-format {
background-color: var(--bs-body-bg);
padding: 1rem;
border-radius: 0.25rem;
margin-bottom: 0.5rem;
}
.email-field {
margin-bottom: 0.5rem;
line-height: 1.4;
}
.email-field:last-child {
margin-bottom: 0;
}
.field-value {
padding: 0.5rem;
border-bottom: 1px solid #eee;
line-height: 1.4;
}
.field-value:last-child {
border-bottom: none;
}
.field-value strong {
color: var(--bs-body-color);
min-width: 80px;
display: inline-block;
}
[data-bs-theme="dark"] .card {
background-color: #2c3034;
border-color: #373b3e;
}
[data-bs-theme="dark"] .card-header {
background-color: #343a40;
border-bottom-color: #373b3e;
}
[data-bs-theme="dark"] .field-value {
border-bottom-color: #373b3e;
}
[data-bs-theme="dark"] .section-header {
border-bottom-color: #373b3e;
}
.theme-toggle {
position: fixed;
top: 1rem;
right: 1rem;
z-index: 1001;
}
</style>
</head>
<body>
<div class="container py-4">
<button class="btn btn-outline-secondary theme-toggle" onclick="toggleTheme()">
<span class="theme-icon">🌙</span>
</button>
<h1 class="mb-4">Firefly Database Viewer</h1>
<div class="alert alert-info mb-4">
<h4 class="alert-heading">Connection Status</h4>
<p id="connection-message">Checking connection...</p>
</div>
<div class="row">
<div class="col-12">
<button class="btn btn-primary mb-3" onclick="refreshData()">Refresh Data</button>
</div>
</div>
<div id="data-container">
<div id="strings-section" class="section">
<h2 class="section-header">String Keys</h2>
<div class="row" id="strings-container"></div>
</div>
<div id="lists-section" class="section">
<h2 class="section-header">List Keys</h2>
<div class="row" id="lists-container"></div>
</div>
<div id="hashes-section" class="section">
<h2 class="section-header">Hash Keys</h2>
<div class="row" id="hashes-container"></div>
</div>
</div>
</div>
<script>
// Add theme toggle functionality
function toggleTheme() {
const html = document.documentElement;
const currentTheme = html.getAttribute('data-bs-theme');
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
html.setAttribute('data-bs-theme', newTheme);
// Update theme icon
const themeIcon = document.querySelector('.theme-icon');
themeIcon.textContent = newTheme === 'light' ? '🌙' : '☀️';
// Save preference
localStorage.setItem('theme', newTheme);
}
// Load saved theme preference
document.addEventListener('DOMContentLoaded', () => {
const savedTheme = localStorage.getItem('theme') || 'light';
document.documentElement.setAttribute('data-bs-theme', savedTheme);
const themeIcon = document.querySelector('.theme-icon');
themeIcon.textContent = savedTheme === 'light' ? '🌙' : '☀️';
});
function formatValue(value) {
if (Array.isArray(value)) {
// Display list values with latest values at the top
return value.map(v => typeof v === 'string' ? v.trim() : v).join('<br>');
} else if (typeof value === 'object' && value !== null) {
// Format as key-value pairs
return Object.entries(value)
.map(([key, val]) => {
let displayVal = typeof val === 'string' ? val.trim() : val;
// Special handling for email fields
if (['from', 'to', 'subject', 'content'].includes(key.toLowerCase())) {
return `<div class="field-value">
<strong>${key.charAt(0).toUpperCase() + key.slice(1)}:</strong>
${displayVal}
</div>`;
}
// Regular field formatting
return `<div class="field-value">
<strong>${key}:</strong> ${displayVal}
</div>`;
})
.join('');
}
return String(value).trim();
}
function getTypeBadgeClass(type) {
const classes = {
'string': 'bg-primary',
'list': 'bg-success',
'hash': 'bg-info'
};
return classes[type] || 'bg-secondary';
}
function updateConnectionStatus(connected, message) {
const alertDiv = document.querySelector('.alert');
const messageElement = document.getElementById('connection-message');
if (connected) {
alertDiv.className = 'alert alert-success mb-4';
messageElement.textContent = 'Connected to Firefly database successfully!';
} else {
alertDiv.className = 'alert alert-danger mb-4';
messageElement.textContent = `Connection failed: ${message}`;
}
}
function createKeyCard(key, type, value) {
const card = document.createElement('div');
card.className = 'col-md-6 col-lg-4';
card.innerHTML = `
<div class="card key-card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0">${key}</h5>
<span class="badge ${getTypeBadgeClass(type)} type-badge">${type}</span>
</div>
<div class="card-body">
<div class="value-container">
${formatValue(value)}
</div>
</div>
</div>
`;
return card;
}
function refreshData() {
console.log('Fetching data from /api/keys...');
fetch('/api/keys')
.then(response => {
console.log('Received response:', response.status);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => {
console.log('Parsed data:', data);
updateConnectionStatus(data.connection_status, data.error || '');
if (data.success) {
// Clear existing containers
document.getElementById('strings-container').innerHTML = '';
document.getElementById('lists-container').innerHTML = '';
document.getElementById('hashes-container').innerHTML = '';
const { strings = [], lists = [], hashes = [] } = data.data || {};
// Update strings
if (strings.length > 0) {
console.log(`Found ${strings.length} string keys`);
strings.forEach(item => {
document.getElementById('strings-container')
.appendChild(createKeyCard(item.key, 'string', item.value));
});
} else {
document.getElementById('strings-container').innerHTML =
'<div class="col-12"><div class="alert alert-info">No string keys found.</div></div>';
}
// Update lists
if (lists.length > 0) {
console.log(`Found ${lists.length} list keys`);
lists.forEach(item => {
document.getElementById('lists-container')
.appendChild(createKeyCard(item.key, 'list', item.value));
});
} else {
document.getElementById('lists-container').innerHTML =
'<div class="col-12"><div class="alert alert-info">No list keys found.</div></div>';
}
// Update hashes
if (hashes.length > 0) {
console.log(`Found ${hashes.length} hash keys`);
hashes.forEach(item => {
document.getElementById('hashes-container')
.appendChild(createKeyCard(item.key, 'hash', item.value));
});
} else {
document.getElementById('hashes-container').innerHTML =
'<div class="col-12"><div class="alert alert-info">No hash keys found.</div></div>';
}
} else {
console.error('API request failed:', data.error);
throw new Error(data.error || 'Unknown error occurred');
}
})
.catch(error => {
console.error('Error fetching data:', error);
updateConnectionStatus(false, error.message || 'Failed to fetch data from Firefly database');
});
}
// Initial load
refreshData();
</script>
</body>
</html>

51
pyproject.toml Normal file
View File

@ -0,0 +1,51 @@
[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "firefly-viewer"
version = "1.0.0"
description = "A web-based viewer for Firefly databases"
readme = "README.md"
authors = [{ name = "FireflyViewer", email = "your.email@example.com" }]
license = { file = "LICENSE" }
classifiers = [
"Development Status :: 4 - Beta",
"Environment :: Web Environment",
"Framework :: Flask",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Topic :: Database :: Front-Ends",
]
keywords = ["firefly", "database", "viewer", "web", "flask"]
dependencies = [
"Flask>=3.0.2",
"redis>=5.0.1",
"python-dotenv>=1.0.1",
"ifireflylib>=0.2.5",
"click>=8.1.7",
"waitress>=2.1.2",
]
requires-python = ">=3.7"
[project.urls]
Homepage = "https://gitea.innovativesdevsolutions.org/IDSolutions/firefly-viewer"
Documentation = "https://gitea.innovativesdevsolutions.org/IDSolutions/firefly-viewer#readme"
Repository = "https://gitea.innovativesdevsolutions.org/IDSolutions/firefly-viewer.git"
"Bug Tracker" = "https://gitea.innovativesdevsolutions.org/IDSolutions/firefly-viewer/issues"
[project.scripts]
firefly-viewer = "firefly.cli:main"
[tool.setuptools]
packages = ["firefly"]
[tool.setuptools.package-data]
firefly = ["templates/*"]

7
requirements.txt Normal file
View File

@ -0,0 +1,7 @@
Flask
redis
python-dotenv
ifireflylib
click
waitress
setuptools

48
setup.py Normal file
View File

@ -0,0 +1,48 @@
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="firefly-viewer",
version="1.0.0",
author="FireflyViewer",
author_email="your.email@example.com", # You'll need to change this
description="A web-based viewer for Firefly databases",
long_description=long_description,
long_description_content_type="text/markdown",
url="https://gitea.innovativedevsolutions.org/IDSolutions/firefly-viewer", # You'll need to change this
project_urls={
"Bug Tracker": "https://gitea.innovativedevsolutions.org/IDSolutions/firefly-viewer/issues",
},
packages=find_packages(),
include_package_data=True,
install_requires=requirements,
entry_points={
"console_scripts": [
"firefly-viewer=firefly.cli:main",
],
},
package_data={
"firefly": ["templates/*"],
},
classifiers=[
"Development Status :: 4 - Beta",
"Environment :: Web Environment",
"Framework :: Flask",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Topic :: Database :: Front-Ends",
],
python_requires=">=3.7",
)