PyJONDB
Advanced tools
+21
| MIT License | ||
| Copyright (c) 2024 tagoWorks | ||
| 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. |
+79
| <br /> | ||
| <div align="center"> | ||
| <a href="./pyjondb.png"> | ||
| <img src="./pyjondb.png" alt="Logo" width="300" height="auto"> | ||
| </a> | ||
| <h3 align="center">PyJONDB</h3> | ||
| <p align="center"> | ||
| A lightweight, encrypted JSON-based database with support for collections, document operations, and aggregation. | ||
| <br /> | ||
| <br /> | ||
|  | ||
|  | ||
|  | ||
|  | ||
|  | ||
| </p> | ||
| </div> | ||
| <h2 align="center">Python JavaScript Object Notation Database <sub><sup>(without script since it sounds better)</sup></sub></h2> | ||
| This package provides a lightweight, encrypted JSON-based database with support for collections, document operations, and aggregation. It uses the cryptography library for encryption and decryption of data, ensuring secure storage of your sensitive information. The PyPI package includes docstrings for each function to make it easier to learn. | ||
| ### Use pip to download the latest release | ||
| ```sh | ||
| pip install pyjondb | ||
| ``` | ||
| ### Create a user with admin role | ||
| ```python | ||
| # Initialize authentication | ||
| auth = session.start() | ||
| auth.create_user('admin', 'adminpass', roles=['admin']) | ||
| # Authenticate and get a session ID | ||
| session_id = auth.authenticate('admin', 'adminpass') | ||
| if not session_id: | ||
| raise PermissionError("Authentication failed") | ||
| ``` | ||
| ### Initialize and create the database | ||
| ```python | ||
| # Initialize the database | ||
| # The database function has optional values: | ||
| # pyjondb.database("database", "mypassword", debug=True/False, absolute=True/False) | ||
| db = database.init("database", "mydatabasekey", auth) | ||
| # Create the database | ||
| # Note: you should only create the database if it doesn't already exist | ||
| db.create(session_id) | ||
| ``` | ||
| ### Writing data to the database | ||
| ```python | ||
| # Create a collection | ||
| db.create_collection('my_collection', session_id) | ||
| # Add a document | ||
| db.add_document('my_collection', {'name': 'example'}, session_id) | ||
| ``` | ||
| ### Read the data | ||
| ```python | ||
| # Get all documents in a collection | ||
| print(db.read_collection('my_collection', session_id)) | ||
| ``` | ||
| ### PyJONDB gets way more advanced than writing simple data. To learn more about collections, documents, aggregation, linking, and tree structures read the [docs](https://github.com/t-a-g-o/PyJONDB) | ||
| # Database Viewing Tool | ||
| In the `database-viewer` folder there is an executable which you can run along with any PyJONDB database file (.ndb extension) | ||
| #### Note: you cannot directly edit the database with the viewing tool | ||
| # License & Contact 📃 | ||
| This project is published under the [MIT license](./LICENSE) | ||
| If you are interested in working together, or want to get in contact with me please email me at santiago@tago.works |
+18
-20
| Metadata-Version: 2.1 | ||
| Name: PyJONDB | ||
| Version: 1.5 | ||
| Version: 1.6 | ||
| Summary: A lightweight, encrypted JSON-based database with support for collections, document operations, and aggregation. | ||
@@ -17,2 +17,3 @@ Home-page: https://tago.works/ | ||
| Description-Content-Type: text/markdown | ||
| License-File: LICENSE | ||
| Requires-Dist: cryptography | ||
@@ -35,29 +36,26 @@ | ||
| ```python | ||
| import pyjondb | ||
| from pyjondb import database | ||
| from pyjondb import session | ||
| # Initialize the database | ||
| db = pyjondb.database('mydatabase', 'mysecretkey') | ||
| auth = session.start() | ||
| auth.create_user('admin', 'adminpass', roles=['admin', 'user']) | ||
| # Create the database | ||
| db.create() | ||
| # Authenticate and get a session ID | ||
| session_id = auth.authenticate('admin', 'adminpass') | ||
| # Create a new collection | ||
| db.create_collection('mycollection') | ||
| # Create a database instance with the session ID | ||
| db = database.init('my_database', 'my_secret_key', auth, debug=True) | ||
| # Add a document to the collection | ||
| document = {'name': 'John Doe', 'age': 30} | ||
| db.add_document('mycollection', document) | ||
| # Create a database | ||
| db.create(session_id) | ||
| # Find a document in the collection | ||
| query = {'name': 'John Doe'} | ||
| results = db.find_document('mycollection', query) | ||
| print(results) | ||
| # Create a collection | ||
| db.create_collection('my_collection', session_id) | ||
| # Update a document in the collection | ||
| document_id = results[0]['_id'] | ||
| db.update_document('mycollection', document_id, {'age': 31}) | ||
| # Add a document | ||
| db.add_document('my_collection', {'name': 'example'}, session_id) | ||
| # Delete a document from the collection | ||
| db.delete_document('mycollection', document_id) | ||
| # Read the collection | ||
| print(db.read_collection('my_collection', session_id)) | ||
| ``` | ||
| ### Learn more about it at the docs: https://github.com/t-a-g-o/PyJONDB/wiki |
| Metadata-Version: 2.1 | ||
| Name: PyJONDB | ||
| Version: 1.5 | ||
| Version: 1.6 | ||
| Summary: A lightweight, encrypted JSON-based database with support for collections, document operations, and aggregation. | ||
@@ -17,2 +17,3 @@ Home-page: https://tago.works/ | ||
| Description-Content-Type: text/markdown | ||
| License-File: LICENSE | ||
| Requires-Dist: cryptography | ||
@@ -35,29 +36,26 @@ | ||
| ```python | ||
| import pyjondb | ||
| from pyjondb import database | ||
| from pyjondb import session | ||
| # Initialize the database | ||
| db = pyjondb.database('mydatabase', 'mysecretkey') | ||
| auth = session.start() | ||
| auth.create_user('admin', 'adminpass', roles=['admin', 'user']) | ||
| # Create the database | ||
| db.create() | ||
| # Authenticate and get a session ID | ||
| session_id = auth.authenticate('admin', 'adminpass') | ||
| # Create a new collection | ||
| db.create_collection('mycollection') | ||
| # Create a database instance with the session ID | ||
| db = database.init('my_database', 'my_secret_key', auth, debug=True) | ||
| # Add a document to the collection | ||
| document = {'name': 'John Doe', 'age': 30} | ||
| db.add_document('mycollection', document) | ||
| # Create a database | ||
| db.create(session_id) | ||
| # Find a document in the collection | ||
| query = {'name': 'John Doe'} | ||
| results = db.find_document('mycollection', query) | ||
| print(results) | ||
| # Create a collection | ||
| db.create_collection('my_collection', session_id) | ||
| # Update a document in the collection | ||
| document_id = results[0]['_id'] | ||
| db.update_document('mycollection', document_id, {'age': 31}) | ||
| # Add a document | ||
| db.add_document('my_collection', {'name': 'example'}, session_id) | ||
| # Delete a document from the collection | ||
| db.delete_document('mycollection', document_id) | ||
| # Read the collection | ||
| print(db.read_collection('my_collection', session_id)) | ||
| ``` | ||
| ### Learn more about it at the docs: https://github.com/t-a-g-o/PyJONDB/wiki |
@@ -0,1 +1,3 @@ | ||
| LICENSE | ||
| README.md | ||
| pypi.readme | ||
@@ -7,3 +9,2 @@ setup.py | ||
| PyJONDB.egg-info/requires.txt | ||
| PyJONDB.egg-info/top_level.txt | ||
| pyjondb/__init__.py | ||
| PyJONDB.egg-info/top_level.txt |
@@ -1,1 +0,1 @@ | ||
| pyjondb | ||
+1
-1
@@ -9,3 +9,3 @@ from setuptools import setup, find_packages | ||
| name='PyJONDB', | ||
| version='1.5', | ||
| version='1.6', | ||
| description='A lightweight, encrypted JSON-based database with support for collections, document operations, and aggregation.', | ||
@@ -12,0 +12,0 @@ long_description=desc, |
| import json | ||
| import os | ||
| import base64 | ||
| import datetime | ||
| import uuid | ||
| import hashlib | ||
| import logging | ||
| from cryptography.fernet import Fernet, InvalidToken | ||
| class Encrypt: | ||
| @staticmethod | ||
| def generate_fernet_key(key): | ||
| return base64.urlsafe_b64encode(hashlib.sha256(key.encode()).digest()) | ||
| def encryptpydb(self, json_data, key): | ||
| fernet_key = self.generate_fernet_key(key) | ||
| fernet = Fernet(fernet_key) | ||
| json_bytes = json.dumps(json_data).encode("utf-8") | ||
| encrypted_bytes = fernet.encrypt(json_bytes) | ||
| logging.debug(f"Encrypted bytes: {encrypted_bytes}") | ||
| second_layer_encrypted_bytes = fernet.encrypt(encrypted_bytes) | ||
| logging.debug(f"Second layer encrypted bytes: {second_layer_encrypted_bytes}") | ||
| binary_string = ''.join(format(byte, '08b') for byte in second_layer_encrypted_bytes) | ||
| binary_string = '\0' + binary_string | ||
| logging.debug(f"Binary string: {binary_string}") | ||
| return binary_string.encode('utf-8') | ||
| def exportdb(self, binary_string, filename): | ||
| with open(filename, "wb") as f: | ||
| f.write(binary_string) | ||
| class Decrypt: | ||
| @staticmethod | ||
| def generate_fernet_key(key): | ||
| return base64.urlsafe_b64encode(hashlib.sha256(key.encode()).digest()) | ||
| def import_from_ndb(self, filename): | ||
| with open(filename, "rb") as f: | ||
| binary_string = f.read() | ||
| return binary_string.decode('utf-8') | ||
| def decrypt_json(self, binary_string, key): | ||
| fernet_key = self.generate_fernet_key(key) | ||
| fernet = Fernet(fernet_key) | ||
| try: | ||
| encrypted_bytes = int(binary_string[1:], 2).to_bytes((len(binary_string[1:]) + 7) // 8, byteorder='big') | ||
| decrypted_bytes_first_layer = fernet.decrypt(encrypted_bytes) | ||
| logging.debug(f"Decrypted bytes first layer: {decrypted_bytes_first_layer}") | ||
| decrypted_bytes = fernet.decrypt(decrypted_bytes_first_layer) | ||
| logging.debug(f"Decrypted bytes: {decrypted_bytes}") | ||
| json_data = json.loads(decrypted_bytes.decode("utf-8")) | ||
| except (InvalidToken, ValueError) as e: | ||
| logging.error("Invalid Fernet key, corrupted data, or binary string conversion error") | ||
| return None | ||
| except json.JSONDecodeError: | ||
| logging.error("Corrupted JSON data") | ||
| return None | ||
| return json_data | ||
| class database: | ||
| def __init__(self, database_name, key, debug=False, absolute=False): | ||
| """ | ||
| Initializes a new instance of the Database. | ||
| Parameters: | ||
| - database_name (str): The name of the database. | ||
| - key (str): The encryption key. | ||
| - debug (bool, optional): Whether to enable debug mode. Defaults to False. | ||
| - absolute (bool, optional): Instead of checking a database directory, read from the current. Defaults to False. | ||
| """ | ||
| self.database_name = database_name | ||
| self.debug = debug | ||
| self.absolute = absolute | ||
| if absolute: | ||
| self.database_path = f"{database_name}.ndb" | ||
| else: | ||
| self.database_path = f"./databases/{database_name}.ndb" | ||
| if not os.path.exists("./databases"): | ||
| os.makedirs("./databases") | ||
| self.key = key | ||
| self.encryptor = Encrypt() | ||
| self.decryptor = Decrypt() | ||
| if debug: | ||
| logging.basicConfig(level=logging.DEBUG) | ||
| def create(self): | ||
| """ | ||
| Creates a new database file if it doesn't already exist. | ||
| """ | ||
| if not os.path.exists(self.database_path): | ||
| metadata = { | ||
| 'id': str(uuid.uuid4()), | ||
| 'database': self.database_name, | ||
| 'creation_date': datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'), | ||
| 'collections': {} | ||
| } | ||
| self.write(metadata) | ||
| print(f"Database {self.database_name} created with metadata: {metadata}") | ||
| else: | ||
| print(f"Database {self.database_name} already exists.") | ||
| def read(self): | ||
| """ | ||
| Reads the data from the database file and returns the decrypted data. | ||
| """ | ||
| if not os.path.exists(self.database_path): | ||
| raise FileNotFoundError(f'Database {self.database_name} does not exist. Create it using create()') | ||
| encrypted_data = self.decryptor.import_from_ndb(self.database_path) | ||
| decrypted_data = self.decryptor.decrypt_json(encrypted_data, self.key) | ||
| if decrypted_data is None: | ||
| return {} | ||
| return decrypted_data | ||
| def write(self, data): | ||
| """ | ||
| Encrypts the given data using the encryptor and writes it to the database. | ||
| Parameters: | ||
| - data (Any): The data to be written to the database. | ||
| """ | ||
| encrypted_data = self.encryptor.encryptpydb(data, self.key) | ||
| self.encryptor.exportdb(encrypted_data, self.database_path) | ||
| def create_collection(self, collection_name): | ||
| """ | ||
| Creates a new collection in the database if it doesn't already exist. | ||
| Parameters: | ||
| - collection_name (str): The name of the collection to be created. | ||
| """ | ||
| database = self.read() | ||
| if 'collections' not in database: | ||
| database['collections'] = {} | ||
| if collection_name not in database['collections']: | ||
| database['collections'][collection_name] = {} | ||
| self.write(database) | ||
| def read_collection(self, collection_name): | ||
| """ | ||
| Reads a collection from the database and returns the documents that belong to the specified collection. | ||
| Parameters: | ||
| - collection_name (str): The name of the collection to read. | ||
| Returns: | ||
| - List[Dict[str, Any]]: A list of documents that belong to the specified collection. | ||
| """ | ||
| database = self.read() | ||
| if 'collections' not in database or collection_name not in database['collections']: | ||
| return [] # Return empty list if collection doesn't exist | ||
| return list(database['collections'][collection_name].values()) | ||
| def write_collection(self, collection_name, data): | ||
| """ | ||
| Writes a collection of documents to the database. | ||
| Parameters: | ||
| - collection_name (str): The name of the collection to write the documents to. | ||
| - data (List[Dict[str, Any]]): The list of documents to write. | ||
| """ | ||
| database = self.read() | ||
| if 'collections' not in database: | ||
| database['collections'] = {} | ||
| if collection_name not in database['collections']: | ||
| database['collections'][collection_name] = {} | ||
| for document in data: | ||
| document_id = str(document.get('_id', uuid.uuid4())) | ||
| database['collections'][collection_name][document_id] = document | ||
| self.write(database) | ||
| def add_document(self, collection_name, document): | ||
| """ | ||
| Adds a document to a collection in the database. | ||
| Parameters: | ||
| - collection_name (str): The name of the collection to add the document to. | ||
| - document (Dict[str, Any]): The document to add to the collection. | ||
| """ | ||
| document['_id'] = str(uuid.uuid4()) | ||
| self.write_collection(collection_name, [document]) | ||
| def find_document(self, collection_name, query): | ||
| """ | ||
| Finds documents in a collection that match the given query. | ||
| Args: | ||
| - collection_name (str): The name of the collection to search in. | ||
| - query (Dict[str, Any]): The query to match against the documents. | ||
| Returns: | ||
| - List[Dict[str, Any]]: A list of documents that match the query. | ||
| """ | ||
| collection = self.read_collection(collection_name) | ||
| return [document for document in collection if self.query(document, query)] | ||
| def query(self, document, query): | ||
| """ | ||
| Recursively queries a document to determine if it matches a given query. | ||
| Args: | ||
| - document (dict): The document to be queried. | ||
| - query (dict): The query to match against the document. | ||
| Returns: | ||
| - bool: True if the document matches the query, False otherwise. | ||
| """ | ||
| for field, value in query.items(): | ||
| if field == "$and": | ||
| if not all(self.query(document, sub_query) for sub_query in value): | ||
| return False | ||
| elif field == "$or": | ||
| if not any(self.query(document, sub_query) for sub_query in value): | ||
| return False | ||
| elif field in document: | ||
| if isinstance(value, dict): | ||
| for op, sub_value in value.items(): | ||
| if op == "$gt": | ||
| if not document[field] > sub_value: | ||
| return False | ||
| elif op == "$lt": | ||
| if not document[field] < sub_value: | ||
| return False | ||
| elif op == "$gte": | ||
| if not document[field] >= sub_value: | ||
| return False | ||
| elif op == "$lte": | ||
| if not document[field] <= sub_value: | ||
| return False | ||
| elif op == "$eq": | ||
| if not document[field] == sub_value: | ||
| return False | ||
| elif op == "$ne": | ||
| if not document[field] != sub_value: | ||
| return False | ||
| else: | ||
| if document[field] != value: | ||
| return False | ||
| else: | ||
| return False | ||
| return True | ||
| def update_document(self, collection_name, document_id, data): | ||
| """ | ||
| Updates a document in a collection based on its ID. | ||
| Parameters: | ||
| - collection_name (str): The name of the collection to update. | ||
| - document_id (str): The ID of the document to update. | ||
| - data (Dict[str, Any]): The new data to update the document with. | ||
| """ | ||
| database = self.read() | ||
| if 'collections' not in database or collection_name not in database['collections']: | ||
| return | ||
| if document_id not in database['collections'][collection_name]: | ||
| return | ||
| database['collections'][collection_name][document_id].update(data) | ||
| self.write(database) | ||
| def delete_document(self, collection_name, document_id): | ||
| """ | ||
| Deletes a document from the specified collection based on its ID. | ||
| Parameters: | ||
| - collection_name (str): The name of the collection to delete the document from. | ||
| - document_id (str): The ID of the document to delete. | ||
| """ | ||
| database = self.read() | ||
| if 'collections' not in database or collection_name not in database['collections']: | ||
| return | ||
| if document_id not in database['collections'][collection_name]: | ||
| return | ||
| del database['collections'][collection_name][document_id] | ||
| self.write(database) | ||
| def link_collections(self, collection1_name, collection2_name, field1, field2): | ||
| """ | ||
| Links two collections in the database by creating a reference field in one collection to the other collection. | ||
| Parameters: | ||
| - collection1_name (str): The name of the first collection. | ||
| - collection2_name (str): The name of the second collection. | ||
| - field1 (str): The field in the first collection that will be used to create the reference. | ||
| - field2 (str): The field in the second collection that will be referenced by the first collection. | ||
| """ | ||
| database = self.read() | ||
| if 'collections' not in database or collection1_name not in database['collections'] or collection2_name not in database['collections']: | ||
| return | ||
| collection1 = database['collections'][collection1_name] | ||
| collection2 = database['collections'][collection2_name] | ||
| for doc1_id, doc1 in collection1.items(): | ||
| if field1 in doc1: | ||
| value = doc1[field1] | ||
| if isinstance(value, list): | ||
| linked_docs = [collection2.get(v) for v in value if v in collection2] | ||
| doc1[field2] = linked_docs | ||
| elif value in collection2: | ||
| doc1[field2] = collection2[value] | ||
| self.write(database) | ||
| def create_tree(self, collection_name, root_query, child_collection_name, child_query): | ||
| """ | ||
| Creates a tree structure by linking root documents to their corresponding child documents. | ||
| Args: | ||
| - collection_name (str): The name of the root collection. | ||
| - root_query (dict): The query to find root documents in the root collection. | ||
| - child_collection_name (str): The name of the child collection. | ||
| - child_query (dict): The query to find child documents in the child collection. | ||
| Returns: | ||
| - list: A list of root documents with their corresponding child documents added as "children" field. | ||
| """ | ||
| root_documents = self.find_document(collection_name, root_query) | ||
| for root_document in root_documents: | ||
| child_documents = self.find_document(child_collection_name, child_query) | ||
| root_document["children"] = child_documents | ||
| self.write(self.read()) | ||
| return root_documents | ||
| def aggregate(self, collection_name, pipeline): | ||
| """ | ||
| Aggregates the documents in a collection based on the given pipeline. | ||
| Parameters: | ||
| - collection_name (str): The name of the collection. | ||
| - pipeline (list): The aggregation pipeline consisting of stages. | ||
| Returns: | ||
| - list: The aggregated collection of documents. | ||
| """ | ||
| collection = self.read_collection(collection_name) | ||
| for stage in pipeline: | ||
| for op, expression in stage.items(): | ||
| if op == "$match": | ||
| collection = [doc for doc in collection if self.query(doc, expression)] | ||
| elif op == "$group": | ||
| grouped_collection = {} | ||
| for doc in collection: | ||
| key = tuple(doc[field] for field in expression["_id"].keys()) | ||
| if key not in grouped_collection: | ||
| grouped_collection[key] = {k: 0 for k in expression.keys() if k != "_id"} | ||
| grouped_collection[key]["_id"] = {k: doc[k] for k in expression["_id"].keys()} | ||
| for field, aggregate_op in expression.items(): | ||
| if field != "_id": | ||
| if aggregate_op == "$sum": | ||
| grouped_collection[key][field] += 1 | ||
| collection = list(grouped_collection.values()) | ||
| return collection |
Sorry, the diff of this file is not supported yet
Alert delta unavailable
Currently unable to show alert delta for PyPI packages.
11
10%13138
-46.35%57
-84.38%