Skip to content

Commit

Permalink
Issue #54-support-for-firebase (#55)
Browse files Browse the repository at this point in the history
* Issue #54-support-for-firebase

* Added all needed packages for firebase to work.

* Fixed  'sqlite' removal.

* Re-Added tests for mongodb + mariadb + postgresql.
  • Loading branch information
Aherontas authored Oct 3, 2024
1 parent 1b85a07 commit 47d743d
Show file tree
Hide file tree
Showing 8 changed files with 150 additions and 26 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# peepDB

**peepDB** is an open-source command-line tool and Python library designed for developers and database administrators who need a fast and efficient way to inspect their database tables without writing SQL queries. With support for MySQL, PostgreSQL, MariaDB, MongoDB and SQLite. peepDB is lightweight, secure, and incredibly easy to use.
**peepDB** is an open-source command-line tool and Python library designed for developers and database administrators who need a fast and efficient way to inspect their database tables without writing SQL queries. With support for MySQL, PostgreSQL, MariaDB, SQLite, MongoDB and Firebase. peepDB is lightweight, secure, and incredibly easy to use.

## 🚀 Features

- **Multi-Database Support**: Works with MySQL, PostgreSQL, MariaDB, MongoDB and SQLite.
- **Multi-Database Support**: Works with MySQL, PostgreSQL, MariaDB, SQLite, MongoDB and Firebase.
- **Quick Data Inspection**: View all tables or a specific table with a simple command.
- **User-Friendly CLI**: Easy-to-use command-line interface powered by Click.
- **Secure Local Storage**: Securely store database connection details with encryption on your local machine.
Expand Down Expand Up @@ -45,7 +45,7 @@ peepdb save mydb --db-type sqlite --host /path/to/mydb.sqlite --database mydb

For other databases:
```bash
peepdb save <connection_name> --db-type [mysql/postgres/mariadb/mongodb] --host <host> --user <user> --password <password> --database <database>
peepdb save <connection_name> --db-type [mysql/postgres/mariadb/mongodb/firebase] --host <host> --user <user> --password <password> --database <database>
```

**Important Note on Password Handling:**
Expand Down
13 changes: 7 additions & 6 deletions peepdb/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ def cli():
peepDB: A quick database table viewer.
This tool allows you to quickly inspect database tables without writing SQL queries.
It supports MySQL, PostgreSQL, MariaDB, MongoDB and SQLite.
It supports MySQL, PostgreSQL, MariaDB, SQLite, MongoDB and FireBase.
Usage examples:
Expand Down Expand Up @@ -84,11 +84,11 @@ def view(connection_name, table, format, page, page_size, scientific):

@cli.command()
@click.argument('connection_name')
@click.option('--db-type', type=click.Choice(['mysql', 'postgres', 'mariadb', 'mongodb', 'sqlite']), required=True, help='Database type')
@click.option('--db-type', type=click.Choice(['mysql', 'postgres', 'mariadb', 'sqlite', 'mongodb', 'firebase']), required=True, help='Database type')
@click.option('--host', required=True, help='Database host or file path for SQLite')
@click.option('--user', required=False, help='Database user (not required for SQLite)')
@click.option('--password', required=False, help='Database password (not required for SQLite)')
@click.option('--database', required=True, help='Database name')
@click.option('--database', required=False, help='Database name (not required for SQLite/Firebase)')
def save(connection_name, db_type, host, user, password, database):
"""
Save a new database connection.
Expand All @@ -98,14 +98,15 @@ def save(connection_name, db_type, host, user, password, database):
Example:
peepdb save postgres1 --db-type postgres --host localhost --user postgres --password YourPassword --database peepdb_test
"""
if db_type == 'sqlite':
user = password = '' # SQLite doesn't use username and password
if db_type in ['sqlite', 'firebase']:
user = password = database = '' # Not used for SQLite and Firebase
else:
if not user:
user = click.prompt('Enter username')
if not password:
password = click.prompt('Enter password', hide_input=True, confirmation_prompt=True)

if not database:
database = click.prompt('Enter database name')
save_connection(connection_name, db_type, host, user, password, database)
click.echo(f"Connection '{connection_name}' saved successfully.")

Expand Down
11 changes: 6 additions & 5 deletions peepdb/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,11 @@ def save_connection(name, db_type, host, user, password, database):
with open(CONFIG_FILE, "r") as f:
config = json.load(f)

if db_type == 'sqlite':
if db_type in ['sqlite', 'firebase']:
# For SQLite and Firebase, store necessary parameters directly
config[name] = {
"db_type": db_type,
"host": host, # Store the file path directly for SQLite
"host": host,
"database": database
}
else:
Expand Down Expand Up @@ -124,13 +125,13 @@ def get_connection(name):

conn = config[name]
try:
if conn["db_type"] == 'sqlite':
if conn["db_type"] in ['sqlite', 'firebase']:
return (
conn["db_type"],
conn["host"], # No need to decrypt for SQLite
conn["host"], # For SQLite and Firebase, host is used directly
"", # Empty string for user
"", # Empty string for password
conn["database"]
conn.get("database", "")
)
else:
return (
Expand Down
5 changes: 4 additions & 1 deletion peepdb/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from typing import Dict, Any
from datetime import date, time, datetime
from decimal import Decimal
from .db import MySQLDatabase, PostgreSQLDatabase, MariaDBDatabase, MongoDBDatabase, SQLiteDatabase
from .db import MySQLDatabase, PostgreSQLDatabase, MariaDBDatabase, MongoDBDatabase, SQLiteDatabase, FirebaseDatabase

logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
Expand All @@ -26,6 +26,9 @@ def connect_to_database(db_type: str, host: str, user: str, password: str, datab
return MongoDBDatabase(host, user, password, database, **kwargs)
elif db_type == 'sqlite':
return SQLiteDatabase(host, user, password, database, **kwargs)
elif db_type == 'firebase':
# For Firebase, 'host' will be the path to the service account key
return FirebaseDatabase(host, **kwargs)
else:
raise ValueError("Unsupported database type")

Expand Down
3 changes: 2 additions & 1 deletion peepdb/db/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
from .postgresql import PostgreSQLDatabase
from .mariadb import MariaDBDatabase
from .mongodb import MongoDBDatabase
from .sqlite import SQLiteDatabase
from .sqlite import SQLiteDatabase
from .firebase import FirebaseDatabase
60 changes: 60 additions & 0 deletions peepdb/db/firebase.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import firebase_admin
from firebase_admin import credentials, firestore
from typing import List, Dict, Any
from .base import BaseDatabase
import logging

logger = logging.getLogger(__name__)

class FirebaseDatabase(BaseDatabase):
def __init__(self, service_account_path: str, **kwargs):
self.service_account_path = service_account_path
self.kwargs = kwargs
self.db = None

def connect(self):
try:
# Initialize Firebase Admin SDK
cred = credentials.Certificate(self.service_account_path)
firebase_admin.initialize_app(cred)
self.db = firestore.client()
logger.debug("Connected to Firebase Firestore")
except Exception as e:
logger.error(f"Failed to connect to Firebase: {e}")
raise ConnectionError(f"Failed to connect to Firebase: {e}")

def disconnect(self):
# Firebase Admin SDK does not provide a disconnect method
pass

def fetch_tables(self) -> List[str]:
try:
# In Firestore, collections are equivalent to tables
collections = self.db.collections()
return [collection.id for collection in collections]
except Exception as e:
logger.error(f"Failed to fetch collections: {e}")
raise e

def fetch_data(self, table_name: str, page: int = 1, page_size: int = 100) -> Dict[str, Any]:
try:
collection_ref = self.db.collection(table_name)
documents = collection_ref.stream()
data = [doc.to_dict() for doc in documents]

# Implement pagination manually
total_rows = len(data)
total_pages = (total_rows + page_size - 1) // page_size # Ceiling division
start_index = (page - 1) * page_size
end_index = start_index + page_size
page_data = data[start_index:end_index]

return {
'data': page_data,
'page': page,
'total_pages': total_pages,
'total_rows': total_rows
}
except Exception as e:
logger.error(f"Failed to fetch data from '{table_name}': {e}")
raise e
38 changes: 28 additions & 10 deletions peepdb/tests/test_peepdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,21 @@
from decimal import Decimal
from peepdb.core import peep_db
from peepdb.config import save_connection, get_connection, list_connections, remove_connection, remove_all_connections
from peepdb.db import MySQLDatabase, PostgreSQLDatabase, MariaDBDatabase, MongoDBDatabase
from peepdb.db import MySQLDatabase, PostgreSQLDatabase, MariaDBDatabase, MongoDBDatabase, FirebaseDatabase

@patch('peepdb.core.FirebaseDatabase')
@patch('peepdb.core.MongoDBDatabase')
@patch('peepdb.core.MySQLDatabase')
@patch('peepdb.core.PostgreSQLDatabase')
@patch('peepdb.core.MariaDBDatabase')
def test_peep_db(mock_mariadb, mock_postgresql, mock_mysql, mock_mongodb):
def test_peep_db(mock_mariadb, mock_postgresql, mock_mysql, mock_mongodb, mock_firebase_db):
# Create a mock database object
mock_db = Mock()
mock_db.fetch_tables.return_value = ['table1', 'table2']
mock_db.fetch_data.return_value = {
'data': [
{'id': 1, 'name': 'John', 'big_integer': 9223372036854775807, 'big_decimal': Decimal('9999999999999999.99')},
{'id': 2, 'name': 'Jane', 'big_integer': 123456789, 'big_decimal': Decimal('1234.56789')}
{'id': 1, 'name': 'John', 'big_integer': 9223372036854775807, 'big_decimal': float(Decimal('9999999999999999.99'))},
{'id': 2, 'name': 'Jane', 'big_integer': 123456789, 'big_decimal': float(Decimal('1234.56789'))}
],
'page': 1,
'total_pages': 1,
Expand All @@ -28,9 +29,9 @@ def test_peep_db(mock_mariadb, mock_postgresql, mock_mysql, mock_mongodb):
mock_postgresql.return_value = mock_db
mock_mariadb.return_value = mock_db
mock_mongodb.return_value = mock_db
mock_firebase_db.return_value = mock_db

# Test MySQL without scientific notation
result = peep_db('mysql', 'host', 'user', 'password', 'database', format='json', scientific=False)
# Expected result for non-scientific format
expected_result = {
'table1': {
'data': [
Expand All @@ -51,6 +52,9 @@ def test_peep_db(mock_mariadb, mock_postgresql, mock_mysql, mock_mongodb):
'total_rows': 2
}
}

# Test MySQL without scientific notation
result = peep_db('mysql', 'host', 'user', 'password', 'database', format='json', scientific=False)
assert result == expected_result

# Test PostgreSQL without scientific notation
Expand All @@ -76,10 +80,17 @@ def test_peep_db(mock_mariadb, mock_postgresql, mock_mysql, mock_mongodb):
result = peep_db('mongodb', 'host', 'user', 'password', 'database', format='json', scientific=False)
assert result == expected_result

# Test Firebase without scientific notation
result = peep_db('firebase', 'path/to/serviceAccountKey.json', '', '', '', format='json', scientific=False)
assert result == expected_result

# Test MySQL with scientific notation
result = peep_db('mysql', 'host', 'user', 'password', 'database', format='json', scientific=True)
# Since scientific notation does not affect JSON output, the result should be the same
assert result == expected_result
assert result == expected_result # JSON output unaffected by scientific flag

# Test Firebase with scientific notation
result = peep_db('firebase', 'path/to/serviceAccountKey.json', '', '', '', format='json', scientific=True)
assert result == expected_result # JSON output unaffected by scientific flag

# Test PostgreSQL with scientific notation
result = peep_db('postgres', 'host', 'user', 'password', 'database', table='table1', format='json', scientific=True)
Expand All @@ -93,6 +104,10 @@ def test_peep_db(mock_mariadb, mock_postgresql, mock_mysql, mock_mongodb):
result = peep_db('mongodb', 'host', 'user', 'password', 'database', format='json', scientific=True)
assert result == expected_result

# Test fetching a specific table/collection for Firebase
result = peep_db('firebase', 'path/to/serviceAccountKey.json', '', '', '', table='table1', format='json', scientific=False)
assert result == {'table1': expected_result['table1']}

# Test MySQL with scientific notation and table format
result = peep_db('mysql', 'host', 'user', 'password', 'database', format='table', scientific=True)
assert 'Table: table1' in result
Expand All @@ -103,6 +118,7 @@ def test_peep_db(mock_mariadb, mock_postgresql, mock_mysql, mock_mongodb):
with pytest.raises(ValueError):
peep_db('unsupported', 'host', 'user', 'password', 'database')


# Test configuration functions
@patch('peepdb.config.os.path.exists')
@patch('peepdb.config.open')
Expand Down Expand Up @@ -135,8 +151,9 @@ def test_config_functions(mock_decrypt, mock_encrypt, mock_json_dump, mock_json_
assert result == ('mysql', 'host', 'user', 'password', 'database')

# Test list_connections
list_connections()
# Assert that print was called with the correct arguments
with patch('builtins.print') as mock_print:
list_connections()
mock_print.assert_called()

# Test remove_connection
mock_json_load.return_value = {'test_conn': {}, 'other_conn': {}}
Expand All @@ -148,5 +165,6 @@ def test_config_functions(mock_decrypt, mock_encrypt, mock_json_dump, mock_json_
assert remove_all_connections() == 2
# Assert that os.remove was called with the correct arguments


if __name__ == '__main__':
pytest.main()
40 changes: 40 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,43 @@ cachetools==5.5.0
keyring==25.4.1
dnspython==2.6.1
pymongo==4.9.1
backports.tarfile==1.2.0
CacheControl==0.14.0
certifi==2024.8.30
cffi==1.17.1
charset-normalizer==3.3.2
exceptiongroup==1.2.2
firebase-admin==6.5.0
google-api-core==2.20.0
google-api-python-client==2.147.0
google-auth==2.35.0
google-auth-httplib2==0.2.0
google-cloud-core==2.4.1
google-cloud-firestore==2.19.0
google-cloud-storage==2.18.2
google-crc32c==1.6.0
google-resumable-media==2.7.2
googleapis-common-protos==1.65.0
grpcio==1.66.2
grpcio-status==1.66.2
httplib2==0.22.0
idna==3.10
importlib_metadata==8.5.0
jaraco.classes==3.4.0
jaraco.context==6.0.1
jaraco.functools==4.1.0
more-itertools==10.5.0
msgpack==1.1.0
proto-plus==1.24.0
protobuf==5.28.2
pyasn1==0.6.1
pyasn1_modules==0.4.1
PyJWT==2.9.0
pyparsing==3.1.4
requests==2.32.3
rsa==4.9
setuptools==65.5.0
tomli==2.0.1
uritemplate==4.1.1
urllib3==2.2.3
zipp==3.20.2

0 comments on commit 47d743d

Please sign in to comment.