commit a136a20592fd52c9d106d6a95831c34e13dbf366 Author: Jacob Schmidt Date: Fri Apr 11 22:45:43 2025 -0500 Initial Repo Setup diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3f15b64 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +build +dist +.venv +__pycache__ +*.py[cod] +*_pycache_* +*.pyo +*.pyd +*.db +*.sqlite +*.egg-info +*.egg +*.log +*.env +.pypirc \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d97535e --- /dev/null +++ b/LICENSE @@ -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. \ No newline at end of file diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..354bd05 --- /dev/null +++ b/MANIFEST.in @@ -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] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..ec76084 --- /dev/null +++ b/README.md @@ -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 \ No newline at end of file diff --git a/app.py b/app.py new file mode 100644 index 0000000..9838961 --- /dev/null +++ b/app.py @@ -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) \ No newline at end of file diff --git a/backup.7z b/backup.7z new file mode 100644 index 0000000..ee33671 Binary files /dev/null and b/backup.7z differ diff --git a/build.py b/build.py new file mode 100644 index 0000000..a48a3d7 --- /dev/null +++ b/build.py @@ -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") \ No newline at end of file diff --git a/firefly/__init__.py b/firefly/__init__.py new file mode 100644 index 0000000..5bb4021 --- /dev/null +++ b/firefly/__init__.py @@ -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" \ No newline at end of file diff --git a/firefly/cli.py b/firefly/cli.py new file mode 100644 index 0000000..350320d --- /dev/null +++ b/firefly/cli.py @@ -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() \ No newline at end of file diff --git a/firefly/config.py b/firefly/config.py new file mode 100644 index 0000000..2c81a17 --- /dev/null +++ b/firefly/config.py @@ -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" \ No newline at end of file diff --git a/firefly/database.py b/firefly/database.py new file mode 100644 index 0000000..66671e2 --- /dev/null +++ b/firefly/database.py @@ -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 \ No newline at end of file diff --git a/firefly/logger.py b/firefly/logger.py new file mode 100644 index 0000000..3e3638c --- /dev/null +++ b/firefly/logger.py @@ -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() \ No newline at end of file diff --git a/firefly/routes.py b/firefly/routes.py new file mode 100644 index 0000000..3082cf6 --- /dev/null +++ b/firefly/routes.py @@ -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/', 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 + }) \ No newline at end of file diff --git a/firefly/services.py b/firefly/services.py new file mode 100644 index 0000000..0d44cc0 --- /dev/null +++ b/firefly/services.py @@ -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 \ No newline at end of file diff --git a/firefly/templates/index.html b/firefly/templates/index.html new file mode 100644 index 0000000..847fdba --- /dev/null +++ b/firefly/templates/index.html @@ -0,0 +1,304 @@ + + + + + + Firefly Database Viewer + + + + +
+ + +

Firefly Database Viewer

+ +
+

Connection Status

+

Checking connection...

+
+ +
+
+ +
+
+ +
+
+

String Keys

+
+
+ +
+

List Keys

+
+
+ +
+

Hash Keys

+
+
+
+
+ + + + \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..05a4c5c --- /dev/null +++ b/pyproject.toml @@ -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/*"] \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..7536c0a --- /dev/null +++ b/requirements.txt @@ -0,0 +1,7 @@ +Flask +redis +python-dotenv +ifireflylib +click +waitress +setuptools \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..55b4702 --- /dev/null +++ b/setup.py @@ -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", +) \ No newline at end of file