diff --git a/.gitignore b/.gitignore
index 5ac94f3..ebb370b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,7 @@
/.venv/
/.idea/
**/__pycache__/
+__pycache__/
*.pem
*.pyc
diff --git a/README_GIT.md b/README_GIT.md
index 2a9faab..b099415 100644
--- a/README_GIT.md
+++ b/README_GIT.md
@@ -19,11 +19,15 @@ git init
#### Create a `.gitignore` file:
Add files/directories in it which you don't want to sync to git.
Update this as frequently as your project needs you to. Start with this:
-```commandline
+```
/.venv/
/.idea/
**/__pycache__/
+__pycache__/
+
*.pem
+*.pyc
+*.pyd
```
### STEP 2.
diff --git a/api/cred_data/blueprint.py b/api/cred_data/blueprint.py
index ef15906..f61c7ad 100644
--- a/api/cred_data/blueprint.py
+++ b/api/cred_data/blueprint.py
@@ -43,7 +43,7 @@ sys.path.append(".")
sys.path.append("..")
# For using Quart:
-from quart import Blueprint, current_app
+from quart import Blueprint, current_app, jsonify
# My utils:
from utils_v2.string import json
diff --git a/from_git.sh b/from_git.sh
index 70c21a7..a1e0714 100644
--- a/from_git.sh
+++ b/from_git.sh
@@ -3,5 +3,5 @@
# Run this on the server.
# Not suitable for collaborative environments:
echo "Pulling from git."
-git pull https://wtt.ditscentre.in/ditscentre/api_internal.git
+git pull https://wtt.ditscentre.in/ditscentre/api_agg_document.git
echo "Attempt done. Exiting."
\ No newline at end of file
diff --git a/pull_utils.sh b/pull_utils.sh
new file mode 100644
index 0000000..3c05dc1
--- /dev/null
+++ b/pull_utils.sh
@@ -0,0 +1,6 @@
+#!/bin/bash
+
+# To pull any updates made to utils from another project:
+echo "Pulling from git."
+git subtree pull --prefix=utils_v2 https://wtt.ditscentre.in/ditscentre/utils_v2.git master --squash
+echo "Attempt done. Exiting."
\ No newline at end of file
diff --git a/push_utils.sh b/push_utils.sh
new file mode 100644
index 0000000..e2f2eae
--- /dev/null
+++ b/push_utils.sh
@@ -0,0 +1,25 @@
+#!/bin/bash
+
+# Move to the directory of the subtree:
+echo "Changing directory."
+cd "utils_v2" || { echo "Failed to change directory."; exit 1; }
+
+# Accept a comment from the terminal:
+echo "Comment: "
+read -r COMMENT
+
+# Add all the modified files to the intended commit:
+git add .
+echo "Files added."
+
+# Make the commit:
+git commit -m "$COMMENT"
+echo "Commit done."
+
+# Return to the parent directory:
+echo "Returning to parent directory."
+cd ..
+
+echo "Pushing to git."
+git subtree push --prefix=utils_v2 https://wtt.ditscentre.in/ditscentre/utils_v2.git master
+echo "Attempt done. Exiting."
\ No newline at end of file
diff --git a/reset_utils.sh b/reset_utils.sh
new file mode 100644
index 0000000..a40a5f4
--- /dev/null
+++ b/reset_utils.sh
@@ -0,0 +1,12 @@
+#!/bin/bash
+
+# When you need to re-pull the whole utils subtree because of merge conflicts:
+echo "Removing existing utils subtree."
+git rm -r "utils_v2"
+echo "Removing existing utils directory."
+rm -r "utils_v2"
+git add .
+git commit -m "Resetting utils subtree."
+echo "Adding utils subtree from git."
+git subtree add --prefix=utils_v2 https://wtt.ditscentre.in/ditscentre/utils_v2.git master --squash
+echo "Attempt done. Exiting."
\ No newline at end of file
diff --git a/rsync_dir.sh b/rsync_dir.sh
index b8ad8b6..0bc25c8 100644
--- a/rsync_dir.sh
+++ b/rsync_dir.sh
@@ -13,6 +13,9 @@ done
# Prompt the user for their selection
read -rp "Select a server by the number ... : " SELECTION
+# Get a port no.:
+read -rp "Set a port no. .................. : " PORT
+
# Validate the selection
if [[ $SELECTION -gt 0 && $SELECTION -le ${#SERVERS[@]} ]]; then
@@ -27,7 +30,7 @@ if [[ $SELECTION -gt 0 && $SELECTION -le ${#SERVERS[@]} ]]; then
PROJECT_DIRECTORY=$(basename "$PWD")
DESTINATION="$USER@$SELECTED_SERVER://home/$USER/programming/python/$PROJECT_DIRECTORY"
echo "Trying to send '$SOURCE' to '$DESTINATION'"
- rsync -avz --mkpath --progress -e "ssh -p 19991" "$SOURCE" "$DESTINATION"
+ rsync -avz --mkpath --progress -e "ssh -p $PORT" "$SOURCE" "$DESTINATION"
# Exit with success (assuming that the actual data sending went well):
exit 0
diff --git a/to_git.sh b/to_git.sh
index 03ffea7..e235c2f 100644
--- a/to_git.sh
+++ b/to_git.sh
@@ -17,5 +17,5 @@ git commit -m "$COMMENT"
echo "Commit done."
# Push th commit to git:
-git push -u https://wtt.ditscentre.in/ditscentre/api_internal.git master
+git push -u https://wtt.ditscentre.in/ditscentre/api_agg_document.git master
echo "Attempt done. Exiting."
\ No newline at end of file
diff --git a/utils_v2/.gitignore b/utils_v2/.gitignore
deleted file mode 100644
index ccfbee4..0000000
--- a/utils_v2/.gitignore
+++ /dev/null
@@ -1,4 +0,0 @@
-/.venv/
-/.idea/
-/__pycache__/
-*.pem
\ No newline at end of file
diff --git a/utils_v2/.idea/.gitignore b/utils_v2/.idea/.gitignore
deleted file mode 100644
index 26d3352..0000000
--- a/utils_v2/.idea/.gitignore
+++ /dev/null
@@ -1,3 +0,0 @@
-# Default ignored files
-/shelf/
-/workspace.xml
diff --git a/utils_v2/.idea/inspectionProfiles/Project_Default.xml b/utils_v2/.idea/inspectionProfiles/Project_Default.xml
deleted file mode 100644
index 76abec7..0000000
--- a/utils_v2/.idea/inspectionProfiles/Project_Default.xml
+++ /dev/null
@@ -1,38 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/utils_v2/.idea/inspectionProfiles/profiles_settings.xml b/utils_v2/.idea/inspectionProfiles/profiles_settings.xml
deleted file mode 100644
index 105ce2d..0000000
--- a/utils_v2/.idea/inspectionProfiles/profiles_settings.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/utils_v2/.idea/material_theme_project_new.xml b/utils_v2/.idea/material_theme_project_new.xml
deleted file mode 100644
index 2236d19..0000000
--- a/utils_v2/.idea/material_theme_project_new.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/utils_v2/.idea/misc.xml b/utils_v2/.idea/misc.xml
deleted file mode 100644
index 9c49622..0000000
--- a/utils_v2/.idea/misc.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/utils_v2/.idea/modules.xml b/utils_v2/.idea/modules.xml
deleted file mode 100644
index e89cf70..0000000
--- a/utils_v2/.idea/modules.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/utils_v2/.idea/utils_v2.iml b/utils_v2/.idea/utils_v2.iml
deleted file mode 100644
index 2c80e12..0000000
--- a/utils_v2/.idea/utils_v2.iml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/utils_v2/.idea/vcs.xml b/utils_v2/.idea/vcs.xml
deleted file mode 100644
index 94a25f7..0000000
--- a/utils_v2/.idea/vcs.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/utils_v2/__init__.py b/utils_v2/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/utils_v2/ai/__init__.py b/utils_v2/ai/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/utils_v2/ai/image_classification/__init__.py b/utils_v2/ai/image_classification/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/utils_v2/ai/image_classification/async_blur.py b/utils_v2/ai/image_classification/async_blur.py
deleted file mode 100644
index f69af42..0000000
--- a/utils_v2/ai/image_classification/async_blur.py
+++ /dev/null
@@ -1,202 +0,0 @@
-"""
-
- AUTHOR:
-
- Khushal P Soonderji
-
- DATE:
-
- Monday, 2nd Sept., 2024
-
- OBJECTIVE:
-
- To provide an easy way to assess images for blurriness.
- Tried and implemented using HuggingFace behaviour_models.
-
- REFERENCES:
-
- N/A
-
- DOWNLOADS:
-
- N/A
-
-"""
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** IMPORT ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# To make sibling directories accessible for imports:
-import sys
-sys.path.append(".")
-sys.path.append("..")
-
-# System-level activities:
-import io
-
-# For using the AI model:
-import torch
-from transformers import pipeline
-
-# To download images from URLs:
-import requests
-
-# To read images:
-from PIL import Image
-
-# Common:
-from shared import variables
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** MACROS / ONE-TIME INIT ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# --- Nothing Yet
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** VARIABLES ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# --- Nothing Yet
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** CLASSES ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-class AssessImageBlur:
-
- def __init__(
- self,
- model,
- blur_label = "Blur",
- clarity_label = "Normal",
- blur_threshold = 0.25,
- clarity_threshold = 0.65
- ):
-
- """
- This class assesses the input image and tells if it is blurry or clear.
- :param model: The model to use. Either the name of the HuggingFace repo, or the directory where the model is
- stored.
- :param blur_threshold: The max allowed blurriness (0 to 1 range).
- :param clarity_threshold: The minimum needed clarity (0 to 1 range).
- """
-
- # Note down the config:
- self.__model = model
- self.__device = "gpu" if torch.cuda.is_available() else "cpu"
- self.__blur_label = blur_label
- self.__clarity_label = clarity_label
- self.__blur_threshold = blur_threshold
- self.__clarity_threshold = clarity_threshold
-
- # Initialize the classifier:
- self.__classifier = pipeline(
- task = "image-classification",
- model = self.__model,
- device = self.__device
- )
-
- @staticmethod
- async def read_image(source):
-
- """
- Reads an image in whichever format it is provided and returns it as a PIL object.
- :param source: The image as either a path or a URL or a io.BytesIO object.
- :return: The image opened as a PIL object.
- """
-
- # If the input image is already a PIL image:
- if isinstance(source, Image.Image): return source
-
- # If a buffer is provided:
- elif isinstance(source, io.BytesIO):
- source.seek(0)
- return Image.open(source)
-
- # If a string is provided (local path or URL):
- elif isinstance(source, str):
- if source.startswith("http://") or source.startswith("https://"):
- response = await variables.http_client.get(source)
- return Image.open(io.BytesIO(response.content))
- else: return Image.open(source)
-
- async def classify(self, image):
-
- """
- To get the prediction of the model from the given input image.
- :param image: The image (as a PIL object or file path or a URL).
- :return: The dictionary of classes with their respective predictions.
- """
-
- image = await self.read_image(image)
- predictions = self.__classifier(image)
- classes = {p["label"]: p["score"] for p in predictions}
- return classes
-
- async def is_ok(
- self,
- image,
- blur_threshold = None,
- clarity_threshold = None
- ):
-
- """
- Checks if the image given to it can be used, or should be rejected.
- :param image: The image (as a PIL object or file path or a URL).
- :param blur_threshold: A custom threshold to test against. If not provided, the default will be taken that was
- provided when the instance was created.
- :param clarity_threshold: A custom threshold to test against. If not provided, the default will be taken that
- was provided when the instance was created.
- :return: True if the image is okay, else False.
- """
-
- image = await self.read_image(image)
- blur_threshold = blur_threshold or self.__blur_threshold
- clarity_threshold = clarity_threshold or self.__clarity_threshold
- classes = await self.classify(image)
- if (
- classes[self.__blur_label] <= blur_threshold and
- classes[self.__clarity_label] >= clarity_threshold
- ): return True
- return False
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** MAIN PROGRAM ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-if __name__ == "__main__":
-
- import asyncio
-
-
- async def main():
- image = r"/home/developer/Downloads/low-res-check.png"
- assessor = AssessImageBlur(model = r"/home/developer/PycharmProjects/utils/data/ai/behaviour_models/hugging_face/image_classification/BlurOrBokeh")
- usable = await assessor.is_ok(image)
- classes = await assessor.classify(image)
- print("IS OKAY:", usable)
- print("CLASSES:", classes)
-
- asyncio.run(main())
diff --git a/utils_v2/ai/image_classification/async_nsfw.py b/utils_v2/ai/image_classification/async_nsfw.py
deleted file mode 100644
index 446c34a..0000000
--- a/utils_v2/ai/image_classification/async_nsfw.py
+++ /dev/null
@@ -1,188 +0,0 @@
-"""
-
- AUTHOR:
-
- Khushal P Soonderji
-
- DATE:
-
- Monday, 2nd Sept., 2024
-
- OBJECTIVE:
-
- To provide an easy way to assess images for adult content.
- Tried and implemented using HuggingFace behaviour_models.
-
- REFERENCES:
-
- N/A
-
- DOWNLOADS:
-
- N/A
-
-"""
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** IMPORT ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# To make sibling directories accessible for imports:
-import sys
-sys.path.append(".")
-sys.path.append("..")
-
-# System-level activities:
-import io
-
-# For using the AI model:
-import torch
-from transformers import pipeline
-
-# To download images from URLs:
-import requests
-
-# To read images:
-from PIL import Image
-
-# Common:
-from shared import variables
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** MACROS / ONE-TIME INIT ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# --- Nothing Yet
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** VARIABLES ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# --- Nothing Yet
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** CLASSES ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-class AssessImageNSFW:
-
- def __init__(
- self,
- model,
- nsfw_label = "nsfw",
- nsfw_threshold = 0.25,
- ):
-
- """
- This class assesses the input image and tells if it is blurry or clear.
- :param model: The model to use. Either the name of the HuggingFace repo, or the directory where the model is
- stored.
- :param nsfw_threshold: The max allowed blurriness (0 to 1 range).
- """
-
- # Note down the config:
- self.__model = model
- self.__device = "gpu" if torch.cuda.is_available() else "cpu"
- self.__nsfw_label = nsfw_label
- self.__nsfw_threshold = nsfw_threshold
-
- # Initialize the classifier:
- self.__classifier = pipeline(
- task = "image-classification",
- model = self.__model,
- device = self.__device
- )
-
- @staticmethod
- async def read_image(source):
-
- """
- Reads an image in whichever format it is provided and returns it as a PIL object.
- :param source: The image as either a path or a URL or a io.BytesIO object.
- :return: The image opened as a PIL object.
- """
-
- # If the input image is already a PIL image:
- if isinstance(source, Image.Image): return source
-
- # If a buffer is provided:
- elif isinstance(source, io.BytesIO):
- source.seek(0)
- return Image.open(source)
-
- # If a string is provided (local path or URL):
- elif isinstance(source, str):
- if source.startswith("http://") or source.startswith("https://"):
- response = await variables.http_client.get(source)
- return Image.open(io.BytesIO(response.content))
- else: return Image.open(source)
-
- async def classify(self, image):
-
- """
- To get the prediction of the model from the given input image.
- :param image: The image (as a PIL object or file path or a URL).
- :return: The dictionary of classes with their respective predictions.
- """
-
- image = await self.read_image(image)
- predictions = self.__classifier(image)
- classes = {p["label"]: p["score"] for p in predictions}
- return classes
-
- async def is_ok(
- self,
- image,
- nsfw_threshold = None
- ):
-
- """
- Checks if the image given to it can be used, or should be rejected.
- :param image: The image (as a PIL object or file path or a URL).
- :param nsfw_threshold: A custom threshold to test against. If not provided, the default will be taken that was
- provided when the instance was created.
- :return: True if the image is okay, else False.
- """
-
- image = await self.read_image(image)
- nsfw_threshold = nsfw_threshold or self.__nsfw_threshold
- classes = await self.classify(image)
- return True if classes[self.__nsfw_label] <= nsfw_threshold else False
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** MAIN PROGRAM ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-if __name__ == "__main__":
-
- import asyncio
-
-
- async def main():
-
- assessor = AssessImageNSFW(model = r"/path/to/model")
- usable = await assessor.is_ok(r"/path/to/image/img.jpg")
- classes = await assessor.classify(r"https://...")
-
-
- asyncio.run(main())
diff --git a/utils_v2/ai/image_segmentation/__init__.py b/utils_v2/ai/image_segmentation/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/utils_v2/ai/image_segmentation/async_dichotomous_segmentation.py b/utils_v2/ai/image_segmentation/async_dichotomous_segmentation.py
deleted file mode 100644
index fb6d6c7..0000000
--- a/utils_v2/ai/image_segmentation/async_dichotomous_segmentation.py
+++ /dev/null
@@ -1,223 +0,0 @@
-"""
-
- AUTHOR:
-
- Khushal P Soonderji
-
- DATE:
-
- Saturday, 14th Sept., 2024
-
- OBJECTIVE:
-
- To provide an easy way to get masks from dichotomous image segmentation.
-
- This code uses a very specific model from HuggingFace: "ZhengPeng7/BiRefNet-portrait".
- You may experiment with other behaviour_models too, but make sure that model is made for "dichotomous" behaviour. This
- means that the model should have only two classes like "foreground", and "background". The specified model was
- trained for implementing portrait mode style blurring of backgrounds.
-
- The originally tested model has an MIT license as per their GitHub page. The code in this file may or may not
- support drop-in replacement for other behaviour_models, please be aware about this.
-
- REFERENCES:
-
- 01. https://huggingface.co/ZhengPeng7/BiRefNet-portrait
- 02. https://github.com/ZhengPeng7/BiRefNet
-
- DOWNLOADS:
-
- 01. https://huggingface.co/ZhengPeng7/BiRefNet-portrait/tree/main
-
-"""
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** IMPORT ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# To make sibling directories accessible for imports:
-import sys
-sys.path.append(".")
-sys.path.append("..")
-
-# For system-level activities:
-import io
-
-# To work with PIL images:
-from PIL import Image
-
-# To make asynchronous HTTP calls:
-from shared.variables import http_client
-
-# For using the AI model:
-import torch
-from torchvision import transforms
-from transformers import AutoModelForImageSegmentation
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** MACROS / ONE-TIME INIT ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# --- Nothing Yet
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** VARIABLES ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# --- Nothing Yet
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** CLASSES ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-class DichotomousSegmenter:
-
- def __init__(
- self,
- model = r"zhengpeng7/BiRefNet-portrait"
- ):
-
- """
- This class uses Dichotomous Image Segmentation to produce a mask of what is in the foreground (or what is of
- interest in a given image).
- :param model: The path of the model.
- """
-
- # Check if GPU is available for faster predictions:
- self.__cuda_is_available = True if torch.cuda.is_available() else False
-
- # Initialize the model:
- self.__model = AutoModelForImageSegmentation.from_pretrained(
- pretrained_model_name_or_path = model,
- trust_remote_code = True
- )
- torch.set_float32_matmul_precision(["high", "highest"][0])
- self.__model.to("cuda" if self.__cuda_is_available else "cpu")
-
- @staticmethod
- async def read_image(source):
-
- """
- Reads an image in whichever format it is provided and returns it as a PIL object.
- :param source: The image as either a path or a URL or a io.BytesIO object.
- :return: The image opened as a PIL object.
- """
-
- # If the input image is already a PIL image:
- if isinstance(source, Image.Image): return source
-
- # If a buffer is provided:
- elif isinstance(source, io.BytesIO):
- source.seek(0)
- return Image.open(source)
-
- # If a string is provided (local path or URL):
- elif isinstance(source, str):
- if source.startswith("http://") or source.startswith("https://"):
- response = await http_client.get(source)
- return Image.open(io.BytesIO(response.content))
- else: return Image.open(source)
-
- async def get_mask(self, image):
-
- """
- Process the image to figure out the mask. In some cases (like the image of a sunset) you will have no white
- pixels in the mask. White pixels represent the areas that the AI considered to be the foreground (area of
- interest) and the black parts are the background.
- :param image: The image data as either a PIL object, or a path to a file on the local disk, or a URL.
- :return: The mask as a PIL object.
- """
-
- # Prepare the image transformer:
- image_size = (1024, 1024)
- transform_image = transforms.Compose([
- transforms.Resize(image_size),
- transforms.ToTensor(),
- transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
- ])
-
- # Read and transform the image:
- image = await self.read_image(image)
- input_image = transform_image(image).unsqueeze(0).to("cuda" if self.__cuda_is_available else "cpu")
-
- # Make the prediction:
- with torch.no_grad():
- if self.__cuda_is_available: mask = self.__model(input_image)[-1].sigmoid().cuda()
- else: mask = self.__model(input_image)[-1].sigmoid().cpu()
-
- # Create a PIL object of the mask data:
- mask = mask[0].squeeze()
- mask = transforms.ToPILImage()(mask)
- mask = mask.resize(image.size)
-
- # Done here:
- return mask
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** MAIN PROGRAM ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-if __name__ == "__main__":
-
- import asyncio
- import numpy as np
-
- async def main():
-
- model = DichotomousSegmenter()
-
- images = [
- # r"/home/developer/Downloads/kate.jpg",
- # r"/home/developer/Downloads/IMG-20240730-WA0001.jpg",
- # r"/home/developer/Downloads/pexels-pixabay-57416.jpg",
- # r"/home/developer/Downloads/summer_clothes.jpg",
- # r"/home/developer/Downloads/low_res_cat - upscaled.png",
- # r"/home/developer/Downloads/blurry_traffic.jpg",
- r"/home/developer/Downloads/card_0.jpg",
- r"/home/developer/Downloads/card_1.jpg",
- r"/home/developer/Downloads/card_2.jpg",
- r"/home/developer/Downloads/card_3.jpg",
- r"/home/developer/Downloads/sushmita_card.jpg",
- r"/home/developer/Downloads/niranjan_card.jpg",
- r"/home/developer/Downloads/niranjan_card_2.jpg",
- ]
-
- for image in images:
- print(image.split("/")[-1])
- image = await DichotomousSegmenter.read_image(image)
- mask = await model.get_mask(image)
- image.show()
- mask.show()
- print("MASK!")
- array = np.array(mask)
- print("Dimensions of the array:", array.shape)
- masked_count = np.sum(array > 125)
- print(masked_count, type(masked_count), np.size, type(np.size))
- percentage = masked_count / np.prod(array.shape)
- print("Area:", round(percentage * 100, 4))
- # image.putalpha(mask)
- # image.show()
- # mask.show()
-
-
- asyncio.run(main())
diff --git a/utils_v2/ai/object_detection/__init__.py b/utils_v2/ai/object_detection/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/utils_v2/ai/object_detection/yolo.py b/utils_v2/ai/object_detection/yolo.py
deleted file mode 100644
index a435ca6..0000000
--- a/utils_v2/ai/object_detection/yolo.py
+++ /dev/null
@@ -1,176 +0,0 @@
-"""
-
- AUTHOR:
-
- Khushal P Soonderji
-
- DATE:
-
- Thursday, 4th Jul, 2024
-
- OBJECTIVE:
-
- To provide a class to detect objects in images using YOLO behaviour_models.
-
- REFERENCES:
-
- 1) Code Examples: https://docs.ultralytics.com/usage/python/
- 2) Models: https://docs.ultralytics.com/models/yolov8/#supported-tasks-and-modes
-
- DOWNLOADS:
-
- N/A
-
-"""
-
-# *****************************************************************************************************************
-# ***** ****
-# *** IMPORT ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# To make sibling directories accessible for imports:
-import sys
-sys.path.append(".")
-sys.path.append("..")
-
-# System-level activities:
-import os
-
-# To use YOLO architecture:
-from ultralytics import YOLO
-
-# To work with images:
-from PIL import Image
-
-# utils:
-from utils_v2.string import json
-
-# Common:
-from shared import constants
-
-# For debugging:
-from icecream import IceCreamDebugger
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** MACROS / ONE-TIME INIT ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# --- Nothing Yet
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** VARIABLES ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# --- Nothing Yet
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** FUNCTIONS ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-class YoloDetect:
-
- def __init__(
- self,
- model_file,
- debug = True,
- debug_prefix = "YOLO | "
- ):
-
- # Load the model:
- self._model = YOLO(model = model_file)
-
- # Initialize the dbugging tool:
- self._printer = IceCreamDebugger(prefix = debug_prefix, includeContext = True)
- if not debug: self._printer.disable()
-
- def disable_debug(self):
- self._printer.disable()
-
- def enable_debug(self):
- self._printer.enable()
-
- @staticmethod
- def _process_result(raw_result):
-
- # Extract the various types of results available from the inference:
- speed = {k: (v / 100.0) for k, v in raw_result.speed.items()}
- class_mapping = raw_result.names
- probabilities = raw_result.probs
- boxes = raw_result.boxes
- masks = raw_result.masks
-
- # Construct a default response:
- response_json = {
- "speed": speed,
- "classes": class_mapping,
- "boxes": None
- }
-
- # Object-Detection results:
- formatted_boxes = []
- for box in boxes:
- detected_class = int(box.cls.numpy()[0])
- x1, y1, x2, y2 = box.xyxy[0]
- formatted_boxes.append(
- {
- "class": detected_class,
- "className": class_mapping[detected_class],
- "confidence": float(box.conf.numpy()[0]),
- "x1": int(x1.numpy()),
- "y1": int(y1.numpy()),
- "x2": int(x2.numpy()),
- "y2": int(y2.numpy())
- }
- )
- response_json["boxes"] = formatted_boxes
-
- # Done here:
- return response_json
-
- def predict(self, image, show = False, verbose = False):
-
- # Process the input image with the given task:
- results = self._model(
- source = image,
- show = show,
- verbose = verbose
- )
-
- # Based on the task, interpret the results:
- results_json = self._process_result(results[0])
-
- # Done here:
- return results_json
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** MAIN PROGRAM ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-if __name__ == "__main__":
-
- # model_file_path = os.path.join(constants.PROJECT_DIRECTORY, "ai", "yolo", "behaviour_models", "yolov8x.pt")
- model_file_path = r"/home/developer/PycharmProjects/utils/data/ai/models/hugging_face/object_detection/YOLOv10-Document-Layout-Analysis/yolov10x_best.pt"
- # sample_image_path = r"/home/developer/Downloads/2_cats.jpg"
- sample_image_path = r"/home/developer/Downloads/flattened_image.jpg"
- my_yolo = YoloDetect(model_file = model_file_path)
- results = my_yolo.predict(image = Image.open(sample_image_path))
- print("FINAL RESULTS:", json.to_string(results))
-
diff --git a/utils_v2/ai/tts/__init__.py b/utils_v2/ai/tts/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/utils_v2/ai/tts/easy_tts.py b/utils_v2/ai/tts/easy_tts.py
deleted file mode 100644
index fc4bb9b..0000000
--- a/utils_v2/ai/tts/easy_tts.py
+++ /dev/null
@@ -1,175 +0,0 @@
-"""
-
- AUTHOR:
-
- Khushal P Soonderji
-
- DATE:
-
- Thursday, 26th Sept., 2024
-
- OBJECTIVE:
-
- To provide a fast way to make audio from TTS engines.
-
- REFERENCES:
-
- 01. Usage: https://github.com/myshell-ai/MeloTTS/blob/main/docs/install.md#python-api
- 02. Installation: https://github.com/myshell-ai/MeloTTS/blob/main/docs/install.md#linux-and-macos-install
-
- DOWNLOADS:
-
- N/A
-
-"""
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** IMPORT ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# To make sibling directories accessible for imports:
-import sys
-sys.path.append(".")
-sys.path.append("..")
-
-# System-level activities:
-import io
-
-# To use the AI:
-from melo.api import TTS
-import numpy as np
-import soundfile
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** MACROS / ONE-TIME INIT ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# --- Nothing Yet
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** VARIABLES ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# --- Nothing Yet
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** CLASSES ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-class EasyTTS:
-
- __sampling_rate = 44_100
-
- def __init__(
- self,
- language = "EN",
- speed = 1.0
- ):
-
- self.__audio = np.zeros(1)
- self.__language = language
- self.__speed = speed
- self.__model = TTS(language = language, device = "auto")
- self.__speakers = self.__model.hps.data.spk2id
-
- def list_speakers(self):
- return list(self.__speakers.keys())
-
- def speak(
- self,
- text,
- speaker
- ):
-
- this_audio = self.__model.tts_to_file(
- text,
- self.__speakers[speaker],
- speed = self.__speed,
- quiet = True
- )
-
- self.__audio = np.concatenate((self.__audio, this_audio))
-
- def pause(self, seconds):
-
- self.__audio = np.concatenate((
- self.__audio,
- np.zeros(int(self.__sampling_rate * seconds))
- ))
-
- def to_wav(self, path = None):
-
- if path is None:
- audio_buffer = io.BytesIO()
- soundfile.write(audio_buffer, self.__audio, self.__sampling_rate)
- return audio_buffer
-
- else: soundfile.write(path, self.__audio, self.__sampling_rate)
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** MAIN PROGRAM ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-if __name__ == "__main__":
-
- speaker = "EN-BR"
- tts_maker = EasyTTS(language = "EN", speed = 0.9)
-
- tts_maker.speak(
- text = """
- Imagine delighting your doctors with a personalized calendar,
- crafted from their own cherished memories.
- """,
- speaker = speaker
- )
- tts_maker.pause(seconds = 0.3)
- tts_maker.speak(
- text = """
- Every day, as they turn the page,
- they’ll not only relive those special moments but also remember you,
- the one who made it happen.
- """,
- speaker = speaker
- )
- tts_maker.pause(seconds = 0.75)
- tts_maker.speak(
- text = "STEP 1:",
- speaker = speaker
- )
- tts_maker.pause(seconds = 0.3)
- tts_maker.speak(
- text = "Start by identifying the doctors you’d like to engage with, and add them to our app.",
- speaker = speaker
- )
- tts_maker.pause(seconds = 0.3)
- tts_maker.speak(
- text = "No rush, you can add their photographs later as well.",
- speaker = speaker
- )
- tts_maker.pause(seconds = 0.3)
- tts_maker.speak(
- text = "With this, your engagement funnel is created.",
- speaker = speaker
- )
-
- tts_maker.to_wav(r"/home/developer/Downloads/voiceover.wav")
diff --git a/utils_v2/ai/tts/test.wav b/utils_v2/ai/tts/test.wav
deleted file mode 100644
index d38cfc0..0000000
Binary files a/utils_v2/ai/tts/test.wav and /dev/null differ
diff --git a/utils_v2/api/__init__.py b/utils_v2/api/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/utils_v2/api/__pycache__/__init__.cpython-310.pyc b/utils_v2/api/__pycache__/__init__.cpython-310.pyc
deleted file mode 100644
index 4981456..0000000
Binary files a/utils_v2/api/__pycache__/__init__.cpython-310.pyc and /dev/null differ
diff --git a/utils_v2/api/__pycache__/async_quart.cpython-310.pyc b/utils_v2/api/__pycache__/async_quart.cpython-310.pyc
deleted file mode 100644
index b8bf0a8..0000000
Binary files a/utils_v2/api/__pycache__/async_quart.cpython-310.pyc and /dev/null differ
diff --git a/utils_v2/api/__pycache__/codes.cpython-310.pyc b/utils_v2/api/__pycache__/codes.cpython-310.pyc
deleted file mode 100644
index 343b95a..0000000
Binary files a/utils_v2/api/__pycache__/codes.cpython-310.pyc and /dev/null differ
diff --git a/utils_v2/api/__pycache__/metrics_prometheus.cpython-310.pyc b/utils_v2/api/__pycache__/metrics_prometheus.cpython-310.pyc
deleted file mode 100644
index 724908a..0000000
Binary files a/utils_v2/api/__pycache__/metrics_prometheus.cpython-310.pyc and /dev/null differ
diff --git a/utils_v2/api/__pycache__/response.cpython-310.pyc b/utils_v2/api/__pycache__/response.cpython-310.pyc
deleted file mode 100644
index 65cf988..0000000
Binary files a/utils_v2/api/__pycache__/response.cpython-310.pyc and /dev/null differ
diff --git a/utils_v2/api/async_quart.py b/utils_v2/api/async_quart.py
deleted file mode 100644
index 8749a76..0000000
--- a/utils_v2/api/async_quart.py
+++ /dev/null
@@ -1,958 +0,0 @@
-"""
-
- AUTHOR:
-
- Khushal P Soonderji
-
- DATE:
-
- Friday, 2nd Aug., 2024
-
- OBJECTIVE:
-
- To provide an easy way to perform repetitive tasks in quart.
-
- REFERENCES:
-
- N/A
-
- DOWNLOADS:
-
- N/A
-
- NOTES:
-
- 01. PLEASE USE "ResponseModel" AS THE RETURNED VALUE OF THE API ENDPOINT IF YOU ARE USING ANY OF THESE
- DECORATORS.
-
-"""
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** IMPORT ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# To work with Quart:
-from quart import request, current_app, g
-
-# To make decorators:
-from functools import wraps
-
-# My utils:
-from utils_v2.string import json
-from utils_v2.date_time import date_time
-from utils_v2.security import sanitizers
-from utils_v2.api.codes import StatusCodes, HttpCodes
-from utils_v2.api.log import APILogModel
-from utils_v2.api.response import ResponseModel
-
-# To work with date and time:
-import time
-import datetime
-
-# System-level activities:
-import io
-import os
-
-# For Pydantic data-behaviour_models:
-import pydantic
-
-# For hashing and shortening the hash:
-import hashlib
-import base64
-
-# To make things human-readable:
-import humanize
-
-# For debugging:
-import traceback
-import random
-import string
-
-# For asynchronous activities:
-import asyncio
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** MACROS / ONE-TIME INIT ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# Chars to choose from for random strings:
-ALPHANUMERIC_CHARS = string.ascii_letters + string.digits
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** VARIABLES ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# --- Nothing Yet
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** FUNCTIONS ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# --- Nothing Yet
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** EXCEPTIONS ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-class AuthDetailsIncompleteException(Exception):
- def __str__(self):
- return "incomplete auth details"
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** FUNCTIONS ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-async def data_from_request(inbound_request):
-
- """
- Adaptively extract the params from incoming request in whichever way it was provided.
- :param inbound_request: The request that came in.
- :return: A dict (could be empty) of the data/params that came in with the request.
- """
-
- inbound_data = {}
-
- # Extract data from the params in the URL:
- from_args = inbound_request.args.to_dict()
- if isinstance(from_args, dict):
- for k, v in from_args.items():
- inbound_data[k] = v
-
- # Extract data from the raw JSON data:
- from_json = await request.get_json()
- if isinstance(from_json, dict):
- for k, v in from_json.items():
- inbound_data[k] = v
-
- # Extract inputs from the form body:
- from_form = await inbound_request.form
- from_form = from_form.to_dict()
- if isinstance(from_form, dict):
- for k, v in from_form.items():
- inbound_data[k] = v
-
- return inbound_data
-
-
-# ---------------------------------------------------------------------------------------------------------------------
-
-
-async def file_from_request(inbound_request, file_key):
-
- """
- Extracts ONE file from the incoming request's form-data.
- WARNING: NOT RECOMMENDED FOR LARGE FILES. STRICTLY USE FOR SMALL FILES THAT WON'T CRASH THE SCRIPT.
- :param inbound_request: The incoming request.
- :param file_key: The key of the file that you want to extract.
- :return: A tuple of the file's name and data.
- """
-
- file_name = None
- file_data = None
- files = await inbound_request.files
-
- if file_key in files:
- file_name = files[file_key].filename
- file_data = io.BytesIO(files[file_key].read())
-
- return file_name, file_data
-
-
-# ---------------------------------------------------------------------------------------------------------------------
-
-
-async def files_from_request(inbound_request: request):
-
- """
- Extracts ALL files from the incoming request's form-data.
- WARNING: NOT RECOMMENDED FOR LARGE FILES. STRICTLY USE FOR SMALL FILES THAT WON'T CRASH THE SCRIPT.
- :param inbound_request: The incoming request.
- :return: A dict describing the file's name, data, and size.
- """
-
- files = await inbound_request.files
-
- inbound_files = {}
- for file_key in files:
- file_data = io.BytesIO(files[file_key].read())
- file_size = file_data.seek(0, 2)
- file_data.seek(0)
- inbound_files[file_key] = {
- "name": files[file_key].filename,
- "data": file_data,
- "size": file_size,
- "type": files[file_key].content_type
- }
-
- return inbound_files
-
-
-# ---------------------------------------------------------------------------------------------------------------------
-
-
-async def headers_from_request(
- inbound_request,
- mandatory_keys: list = None
-):
-
- """
- Extract custom headers and some extra info. from the incoming request.
- Raises an exception if any mandatory key is missing.
- IMPORTANT: CUSTOMIZE THIS FOR THE NEEDS OF YOUR PROJECT.
- :param inbound_request: The request that came in.
- :param mandatory_keys: The keys that you need to have in the auth.
- :return: The extracted auth details.
- """
-
- # Start by extracting whatever complies with the format of "X-{Header-Name}":
- head_json = {k: v for k, v in request.headers.items() if k.startswith("X-")}
-
- # Now note down things that you want to keep from incoming requests:
- head_json["Remote-IP"] = inbound_request.remote_addr
- head_json["Host"] = inbound_request.headers.get("Host")
- head_json["Origin"] = inbound_request.headers.get("Origin")
- head_json["User-Agent"] = inbound_request.headers.get("User-Agent")
-
- # Raise an exception if any of the mandatory auth details were missing:
- if mandatory_keys is not None:
- available_keys = head_json.keys()
- for mandatory_key in mandatory_keys:
- if mandatory_key not in available_keys: raise AuthDetailsIncompleteException
-
- # Done here:
- return head_json
-
-
-# ---------------------------------------------------------------------------------------------------------------------
-
-
-def cause_exception():
-
- """
- Call this from any function when you want to raise an exception.
- Example use case would be when receiving data from an API call and that field is not supposed to be null.
- :return: None.
- """
-
- return 100/0
-
-
-# ---------------------------------------------------------------------------------------------------------------------
-
-
-def describe_exception(exc):
-
- """
- Describes the exception in detail. It extracts the type of exception, a brief message, and even the entire
- traceback. Useful for debugging in details without the terminal. You could either log the resultant dict or send it
- to the dev team over some service like WhatsApp/Telegram.
- :param exc: The exception that occurred.
- :return: The dict that explains the exception.
- """
-
- exc_desc = {
- "type": type(exc).__name__,
- "msg": str(exc),
- "tb": [str(exc_tb) for exc_tb in traceback.format_exception(exc, value = exc, tb = exc.__traceback__)]
- }
-
- return exc_desc
-
-
-# ---------------------------------------------------------------------------------------------------------------------
-
-
-def messages_from_pydantic_exception(exception, as_str = True, sep = ", "):
-
- """
- Creates a list of readable error messages from Pydantic's validation failure.
- :param exception: Pydantic's ValidationError
- :param as_str: Set to True to receive all messages as one string, False to receive an array of strings.
- :param sep: The separator to use when joining multiple messages as one string.
- :return: A list of messages of all the things that went wrong.
- """
-
- # Make a variable to hold all individual messages:
- messages = []
-
- # Interpret all the problems:
- for error in exception.errors():
- loc = " --> ".join([str(item) for item in error["loc"]])
- if error["type"] == "missing": messages.append(f"missing input: {loc}")
- elif error["type"] == "model_type": messages.append(f"invalid input: {loc}")
- elif error["type"] == "bool_parsing": messages.append(f"invalid bool: {loc}")
- elif error["type"] == "string_type": messages.append(f"invalid string: {loc}")
- elif error["type"] == "float_parsing": messages.append(f"invalid float: {loc}")
- elif error["type"] == "int_parsing": messages.append(f"invalid integer: {loc}")
- elif error["type"] == "extra_forbidden": messages.append(f"extra input: {loc}")
- elif error["type"] == "value_error": messages.append(f"validation failed: {loc}")
- else: messages.append(f"invalid datatype: {loc}")
-
- # Return a response as per the preference of the user:
- if as_str: return sep.join(messages)
- else: return messages
-
-# ---------------------------------------------------------------------------------------------------------------------
-
-
-def set_api_version(api_version):
-
- """
- Use this decorator to automatically note down the API version no. and propagate it throughout the downstream
- decorators. Use this as the entry point if possible.
- :param api_version: The version code to assign to the API.
- :return: The decorator factory.
- """
-
- def decorator(func):
-
- @wraps(func)
- async def wrapper(*args, **kwargs):
-
- # Let the next in-line decorator know that it has been wrapped:
- kwargs["decorator_count"] = kwargs.get("decorator_count", 0) + 1
-
- # set the version information in the variable.
- # This makes it available to the downstream decorators too!
- kwargs["api_version"] = api_version
-
- # we are ready to call the function that we are wrapping:
- response = await func(*args, **kwargs)
- kwargs["decorator_count"] -= 1
- if isinstance(response, ResponseModel): response.api_version = api_version
-
- # Done here:
- if (
- kwargs["decorator_count"] == 0 and
- isinstance(response, ResponseModel)
- ): response = response.for_quart()
- return response
-
- return wrapper
-
- return decorator
-
-
-# ---------------------------------------------------------------------------------------------------------------------
-
-
-def read_input(
- sanitize_headers = True,
- sanitize_data = True
-):
-
- """
- Use this decorator to read the inputs from the incoming request and sanitize them. Sanitization makes the inputs
- safe against certain threats like injections attacks. If you expect to take in inputs that you want to use to run
- database commands, you could disable them manually.
- PLEASE USE "ResponseModel" AS THE RETURNED VALUE OF THE API ENDPOINT IF YOU ARE USING THIS DECORATOR.
- :param sanitize_headers: Whether, or not, you would like to sanitize the params coming in through the headers.
- :param sanitize_data: Whether, or not, you would like to sanitize the params coming in through the body or query.
- :return: The decorator factory.
- """
-
- def decorator(func):
-
- @wraps(func)
- async def wrapper(*args, **kwargs):
-
- # Let the next in-line decorator know that it has been wrapped:
- kwargs["decorator_count"] = kwargs.get("decorator_count", 0) + 1
-
- # Get the headers:
- kwargs["inbound_headers"] = await headers_from_request(request)
- if sanitize_headers: kwargs["inbound_headers"] = sanitizers.for_mongo(kwargs["inbound_headers"])
-
- # Get the data:
- kwargs["inbound_data"] = await data_from_request(request)
- if sanitize_data: kwargs["inbound_data"] = sanitizers.for_mongo(kwargs["inbound_data"])
-
- # Get small files from the request:
- kwargs["inbound_files"] = await files_from_request(request)
-
- # We also make a provision for capturing an identifier
- # for the logs that we make through a sister decorator:
- kwargs["log_id"] = "".join(random.choice(ALPHANUMERIC_CHARS) for _ in range(8))
-
- # Now that we have unpacked the incoming data,
- # we are ready to run the function that we are wrapping:
- response = await func(*args, **kwargs)
- kwargs["decorator_count"] -= 1
-
- # Done here:
- if (
- kwargs["decorator_count"] == 0 and
- isinstance(response, ResponseModel)
- ): response = response.for_quart()
- return response
-
- return wrapper
-
- return decorator
-
-
-# ---------------------------------------------------------------------------------------------------------------------
-
-
-def validate_input(
- mandatory_header_keys = None,
- mandatory_data_keys = None,
- mandatory_file_keys = None,
- header_validator = None,
- data_validator = None
-):
-
- """
- USE THIS ONLY AFTER YOU HAVE USED 'read_input'. This decorator will help you run validation on the inputs that were
- extracted from the request. The mandatory keys will be checked first and the validations will be run after that. If
- your validator already checks for keys, you may skip mentioning mandatory keys. DO NOTE THAT YOUR VALIDATOR
- FUNCTIONS MUST RAISE AN EXCEPTION FOR THIS DECORATOR TO WORK.
- :param mandatory_header_keys: The keys in 'inbound_headers' that are absolutely necessary.
- :param mandatory_data_keys: The keys in 'inbound_data' that are absolutely necessary.
- :param mandatory_file_keys: The keys in 'inbound_files' that are absolutely necessary.
- :param header_validator: The function to use to validate the 'inbound_headers'.
- :param data_validator: The function to use to validate 'inbound_data'.
- :return: The decorator factory.
- """
-
- def decorator(func):
-
- @wraps(func)
- async def wrapper(*args, **kwargs):
-
- # Let the next in-line decorator know that it has been wrapped:
- kwargs["decorator_count"] = kwargs.get("decorator_count", 0) + 1
-
- # We first validate the mandatory header keys.
- # Having a null value in this case is NOT allowed:
- if mandatory_header_keys is not None:
- for mandatory_key in mandatory_header_keys:
- if kwargs["inbound_headers"].get(mandatory_key) is None:
- return ResponseModel(
- status_code = StatusCodes.HEADERS_INCOMPLETE,
- message = f"missing: '{mandatory_key}'"
- )
-
- # Return with failure if any of the mandatory JSON details are missing.
- # Having a null value is allowed, but is should be sent by the user on intention.
- if mandatory_data_keys is not None:
- for mandatory_key in mandatory_data_keys:
- try: kwargs["inbound_data"][mandatory_key]
- except: return ResponseModel(
- status_code = StatusCodes.DATA_INCOMPLETE,
- message = f"missing: '{mandatory_key}'"
- )
-
- # Return with failure if any of the mandatory file-keys details are missing:
- if mandatory_file_keys is not None:
- provided_file_keys = kwargs["inbound_files"].keys()
- for mandatory_key in mandatory_file_keys:
- if mandatory_key not in provided_file_keys:
- return ResponseModel(
- status_code = StatusCodes.FILE_MISSING,
- message = f"missing: '{mandatory_key}'"
- )
-
- # Next we validate the headers:
- if header_validator is not None:
-
- # Try validate the data:
- try: kwargs["inbound_headers"] = header_validator(kwargs["inbound_headers"])
-
- # In case some needed field is missing:
- except KeyError as exception:
- return ResponseModel(
- status_code = StatusCodes.DATA_VALIDATION_FAILURE,
- message = "missing: " + str(exception),
- http_code = HttpCodes.BAD_REQUEST,
- )
-
- # In case some pydantic data model fails validation:
- except pydantic.ValidationError as exception:
- return ResponseModel(
- status_code = StatusCodes.DATA_VALIDATION_FAILURE,
- message = messages_from_pydantic_exception(exception),
- http_code = HttpCodes.BAD_REQUEST
- )
-
- # In case some other exception was raised:
- except Exception as exception:
- return ResponseModel(
- status_code = StatusCodes.DATA_VALIDATION_FAILURE,
- message = str(exception),
- http_code = HttpCodes.BAD_REQUEST
- )
-
- # Finally, we validate the incoming data:
- if data_validator is not None:
-
- # Try validate the data:
- try: kwargs["inbound_data"] = data_validator(kwargs["inbound_data"])
-
- # In case some needed field is missing:
- except KeyError as exception:
- return ResponseModel(
- status_code = StatusCodes.DATA_VALIDATION_FAILURE,
- message = "missing: " + str(exception),
- http_code = HttpCodes.BAD_REQUEST
- )
-
- # In case some pydantic data model fails validation:
- except pydantic.ValidationError as exception:
- return ResponseModel(
- status_code = StatusCodes.DATA_VALIDATION_FAILURE,
- message = messages_from_pydantic_exception(exception),
- http_code = HttpCodes.BAD_REQUEST
- )
-
- # In case some other exception was raised:
- except Exception as exception:
- return ResponseModel(
- status_code = StatusCodes.DATA_VALIDATION_FAILURE,
- message = str(exception),
- http_code = HttpCodes.BAD_REQUEST
- )
-
- # Now that we have unpacked the incoming data,
- # we are ready to run the function that we are wrapping:
- response = await func(*args, **kwargs)
- kwargs["decorator_count"] -= 1
-
- # Done here:
- if (
- kwargs["decorator_count"] == 0 and
- isinstance(response, ResponseModel)
- ): response = response.for_quart()
- return response
-
- return wrapper
-
- return decorator
-
-
-# ---------------------------------------------------------------------------------------------------------------------
-
-
-def log_request_to_mongo(
- attr_name,
- collection: str = "logs",
- api_version: str = None,
- project: str = None,
- log_type: str = None,
- operation: str = None,
- log_input: bool = True,
- log_output: bool = True,
- sensitive_keys: list = None
-):
-
- """
- USE THIS ONLY AFTER YOU HAVE USED 'read_input'. This decorator will log the whole process of the API call to
- MongoDB. The variable that holds the instance of 'AsyncMongo' needs to be accessible in the scope of 'current_app'.
- PLEASE USE "ResponseModel" AS THE RETURNED VALUE OF THE API ENDPOINT IF YOU ARE USING THIS DECORATOR.
- :param attr_name: The name of the variable that holds the instance of 'AsyncMongo'. It should be accessible in the
- scope of 'current_app'.
- :param collection: The name of the collection to write the log into.
- :param api_version: The version code of the API endpoint that is being logged.
- :param project: The name of the project that the endpoint was built for.
- :param log_type: A hint to identify what the log was for.
- :param operation: A hint to identify what was action was being performed.
- :param log_input: Whether, or not, you would like to log the input that came in.
- :param log_output: Whether, or not, you would like to log the output of the API call.
- :param sensitive_keys: The list of keys to not log.
- :return: The decorator factory.
- """
-
- def decorator(func):
-
- @wraps(func)
- async def wrapper(*args, **kwargs):
-
- # Let the next in-line decorator know that it has been wrapped:
- kwargs["decorator_count"] = kwargs.get("decorator_count", 0) + 1
-
- # Set the api version as needed:
- kwargs["api_version"] = kwargs.get("api_version", api_version)
-
- # Make variables and extract available info.:
- exception = None
- response = None
- request_ts = date_time.get_current_utc_date_time()
- start_ts = time.perf_counter()
- cpu_start_ts = time.process_time()
-
- # Execute the function that is being wrapped:
- try:
- response = await func(*args, **kwargs)
- kwargs["decorator_count"] -= 1
- except Exception as exc: exception = exc
-
- # Ensure that the response is not null:
- response = response if response is not None else ResponseModel(
- status_code = StatusCodes.UNKNOWN_ERROR,
- message = "null response for request"
- )
-
- # Add params to the response.
- # THIS IS ONLY APPLICABLE WHEN THE TYPE OF THE RESPONSE IS 'ResponseModel':
- if isinstance(response, ResponseModel):
- response.api_version = kwargs.get("api_version")
- response.log_id = kwargs.get("log_id")
-
- # Extract the response to log:
- response_to_log = "not logged"
- http_code_to_log = 200
- if log_output:
- if isinstance(response, ResponseModel): response_to_log, http_code_to_log = response.for_quart()
- elif isinstance(response, tuple): response_to_log, http_code_to_log = response
- else: response_to_log, http_code_to_log = str(response), 200
-
- # Try to get the information about the request.
- # There will be no data in any of these if the decorator was used to catch start-up and shut-down events.
- request_method = None
- request_url = None
- request_route = None
- try:
- request_method = f"{request.method}"
- request_url = f"{request.url}"
- request_route = str(request.url_rule.rule)
- except: pass
-
- # Redact the sensitive keys:
- if sensitive_keys:
- for k in sensitive_keys:
- for var in ["inbound_headers", "inbound_data"]:
- try: kwargs[var][k] = len(str(kwargs[var][k])) * "*"
- except: pass
-
- # Construct the log:
- api_log = APILogModel(
- project = project,
- log = log_type,
- operation = operation,
- apiVer = kwargs.get("api_version"),
- logId = kwargs.get("log_id"),
- logChain = kwargs.get("inbound_headers", {}).get("X-Log-Chain"),
- method = request_method,
- url = request_url,
- route = request_route,
- ts = request_ts,
- tat = time.perf_counter() - start_ts,
- cpuTime = time.process_time() - cpu_start_ts,
- headers = kwargs.get("inbound_headers"),
- data = kwargs.get("inbound_data") if log_input else "not logged",
- files = {
- k: {
- "name": v["name"],
- "size": v["size"]
- } for k, v in kwargs.get("inbound_files", {}).items()
- },
- exception = None if exception is None else describe_exception(exception),
- response = response_to_log,
- httpCode = http_code_to_log
- )
-
- # Write the log:
- app_attr = getattr(current_app, attr_name)
- inserted_id = await app_attr.insert_one(
- collection = collection,
- document = api_log.model_dump()
- )
-
- # Return the response from the wrapped function.
- if (
- kwargs["decorator_count"] == 0 and
- isinstance(response, ResponseModel)
- ): response = response.for_quart()
- return response
-
- return wrapper
-
- return decorator
-
-
-# ---------------------------------------------------------------------------------------------------------------------
-
-
-def should_not_be_under_maintenance(attr_name):
-
- """
- Use this decorator to reject a request when the app is being marked as "under-maintenance". You will need to create
- a boolean variable within the scope of the 'current_app' for this to work. An alternate to this is to set the value
- in an environment variable named 'IS_UNDER_MAINTENANCE' to a string value of either 'True' or 'False' for
- multi-worker deployments.
- :param attr_name: The name of the boolean variable that will hold the information about the app being under
- maintenance. If its value is True at the time of checking, the incoming request will be rejected.
- :return: The decorator factory.
- """
-
- def decorator(func):
-
- @wraps(func)
- async def wrapper(*args, **kwargs):
-
- # Let the next in-line decorator know that it has been wrapped:
- kwargs["decorator_count"] = kwargs.get("decorator_count", 0) + 1
-
- # Get the attribute and check if it indicates that the app is under maintenance,
- # call the wrapped function if not under maintenance:
- app_attr = getattr(current_app, attr_name)
- env_attr = True if os.environ.get("IS_UNDER_MAINTENANCE", "False").lower() == "true" else False
- if app_attr or env_attr:
- response = ResponseModel(status_code = StatusCodes.DOWN_FOR_MAINTENANCE).for_quart()
- else:
- response = await func(*args, **kwargs)
- kwargs["decorator_count"] -= 1
-
- # Done here:
- if (
- kwargs["decorator_count"] == 0 and
- isinstance(response, ResponseModel)
- ): response = response.for_quart()
- return response
-
- return wrapper
-
- return decorator
-
-
-# ---------------------------------------------------------------------------------------------------------------------
-
-
-def only_whitelisted_ips(attr_name):
-
- """
- Use this decorator to reject any requests coming from unauthorized IPs. The list of IP addresses to allow must be
- in a list that is accessible in the context of 'current_app'.
- :param attr_name: The name of the boolean variable that will hold the information about the app being under
- maintenance. If its value is True at the time of checking, the incoming request will be rejected.
- :return: The decorator factory.
- """
-
- def decorator(func):
-
- @wraps(func)
- async def wrapper(*args, **kwargs):
-
- # Let the next in-line decorator know that it has been wrapped:
- kwargs["decorator_count"] = kwargs.get("decorator_count", 0) + 1
-
- # Get the attribute and check if the request's IP is in the permitted list:
- app_attr = getattr(current_app, attr_name)
- if request.remote_addr not in app_attr:
- return ResponseModel(
- status_code = StatusCodes.AUTHORIZATION_FAILED,
- message = "bad ip",
- http_code = HttpCodes.UNAUTHORIZED
- ).for_quart()
-
- # Now that we have checked that the IP is permitted,
- # we are ready to run the function that we are wrapping:
- response = await func(*args, **kwargs)
- kwargs["decorator_count"] -= 1
-
- # Done here:
- if (
- kwargs["decorator_count"] == 0 and
- isinstance(response, ResponseModel)
- ): response = response.for_quart()
- return response
-
- return wrapper
-
- return decorator
-
-
-# ---------------------------------------------------------------------------------------------------------------------
-
-
-def limit_rate(
- attr_name,
- rate_limit: int = 5,
- seconds: float = 1.0,
- message = None,
- header_keys: list = None,
- data_keys: list = None,
- allow_if_exception = False,
- count_for_http_codes = None
-):
-
- """
- Use this decorator to apply rate-limiting to incoming requests.
- SUGGESTION: WHEN STACKING UP MANY RATE LIMITS, PUT THE SMALLEST TIME PERIOD ON TOP AND LARGEST TIME PERIOD AT THE
- BOTTOM. THIS ENSURES PROPER FUNCTIONALITY.
- WARNING: TIMING STARTS WHEN THE FIRST PERMITTED REQUEST GOES THROUGH. THIS MEANS THAT, IF YOU HAVE A PER-DAY LIMIT,
- AND YOU START MAKING REQUESTS AT 11:00 PM AND EXHAUST YOUR LIMIT AT 11:59 PM, YOUR LIMIT WILL BE REPLENISHED AT
- 11:00 PM OF THE NEXT DAY, NOT AT 12:00 AM.
- :param attr_name: The name of the variable that holds the instance of 'AsyncRedisCache'. Should be available in the
- context of 'current_app'.
- :param rate_limit: The number of requests per unit time.
- :param seconds: The time period in which the rate limit is to be applied.
- :param message: The custom message to respond with.
- :param header_keys: The keys in the header to consider when apply rate limits (like 'sessionToken').
- :param data_keys: The keys in the header to consider when apply rate limits (like 'sessionToken').
- :param allow_if_exception: In case Redis is unresponsive, would you prefer allowing the request to pass through or
- would you prefer the request getting blocked.
- :param count_for_http_codes: If this is provided, the counter will be incremented only if the response code was one
- of these values. If not provided, all requests will be counted. This can be used in cases when you want to count
- only when the request was successfully served.
- :return: The decorator factory.
- """
-
- # Param-cleaning:
- rate_limit = max(1, rate_limit)
- if header_keys is None: header_keys = []
- if data_keys is None: data_keys = []
-
- def decorator(func):
-
- @wraps(func)
- async def wrapper(*args, **kwargs):
-
- # Let the next in-line decorator know that it has been wrapped:
- kwargs["decorator_count"] = kwargs.get("decorator_count", 0) + 1
-
- # Note down the combination of values requested and the limits prescribed:
- params = {"route": str(request.url_rule), "header": {}, "data": {}, "limit": rate_limit, "seconds": seconds}
- for key in header_keys: params["header"][key] = kwargs["inbound_headers"].get(key)
- for key in data_keys: params["data"][key] = kwargs["inbound_data"].get(key)
-
- # Now make a unique key from this combination:
- params_json = json.to_string(params, no_space = True)
- sha256_hash = hashlib.sha256()
- sha256_hash.update(params_json.encode("utf-8"))
- hashed_key = sha256_hash.digest()
- base64_key = base64.b64encode(hashed_key).decode("utf-8")
-
- # We first get the value of the counter:
- app_attr = getattr(current_app, attr_name)
- counter_value = await app_attr.count(base64_key, value = 1, expiry = seconds)
-
- # If any exception occurred in getting the count,
- # and exceptions haven't been allowed:
- if counter_value is None and not allow_if_exception:
- response = ResponseModel(
- status_code = StatusCodes.RATE_LIMIT_EXCEEDED,
- message = "Please contact admin (E)"
- )
-
- # If the rate-limit has already been crossed,
- # or when the counter was not fetched but exceptions are allowed:
- elif (counter_value or 0) > rate_limit:
- response = ResponseModel(
- status_code = StatusCodes.RATE_LIMIT_EXCEEDED,
- message = message or ", ".join([
- f"rate limit: {rate_limit} in {humanize.naturaldelta(datetime.timedelta(seconds = seconds))}",
- f"this is your {humanize.ordinal(counter_value)} request in the given period"
- ])
- )
-
- # If the rate-limit hasn't been crossed:
- else:
- response = await func(*args, **kwargs)
- kwargs["decorator_count"] -= 1
-
- # If we have been told to count only for specific status codes,
- # and if the HTTP code of this response is not in the list of codes, we reduce the counter by one:
- if count_for_http_codes:
- http_code = 200 if not isinstance(response, (list, tuple, set)) else response[1]
- if http_code not in count_for_http_codes:
- await app_attr.count(base64_key, value = -1, expiry = seconds)
-
- # Return the response from the function call:
- if (
- kwargs["decorator_count"] == 0 and
- isinstance(response, ResponseModel)
- ): response = response.for_quart()
- return response
-
- return wrapper
-
- return decorator
-
-
-# ---------------------------------------------------------------------------------------------------------------------
-
-
-def handle_cancelled_request(cleanup_func = None, cleanup_coro = None):
-
- """
- Use this decorator to handle prematurely terminated requests. If your clean-up function needs access to variables,
- consider using 'g' to hold data in the scope of the request.
- :param cleanup_func: The function to call when the cancelled request needs graceful handling.
- :param cleanup_coro: The coroutine to call when the cancelled request needs graceful handling.
- :return: The decorator factory.
- """
-
- def decorator(func):
-
- @wraps(func)
- async def wrapper(*args, **kwargs):
-
- # Let the next in-line decorator know that it has been wrapped:
- kwargs["decorator_count"] = kwargs.get("decorator_count", 0) + 1
-
- try:
-
- # we are ready to run the function that we are wrapping:
- response = await func(*args, **kwargs)
- kwargs["decorator_count"] -= 1
-
- # Done here:
- if (
- kwargs["decorator_count"] == 0 and
- isinstance(response, ResponseModel)
- ): response = response.for_quart()
- return response
-
- # In case the client closes the connection pre-maturely:
- except asyncio.CancelledError as exception:
- if hasattr(current_app, "printer"): getattr(current_app, "printer")(exception)
- if cleanup_func is not None: cleanup_func()
- if cleanup_coro is not None: await cleanup_coro()
- return ResponseModel(
- api_version = kwargs.get("api_version"),
- status_code = StatusCodes.CLIENT_CLOSED_REQUEST
- ).for_quart()
-
- # We propagate any other kind of exception:
- except Exception as exception: raise
-
- return wrapper
-
- return decorator
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** MAIN PROGRAM ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-if __name__ == "__main__":
-
- pass
diff --git a/utils_v2/api/codes.py b/utils_v2/api/codes.py
deleted file mode 100644
index 5773cc9..0000000
--- a/utils_v2/api/codes.py
+++ /dev/null
@@ -1,164 +0,0 @@
-"""
-
- AUTHOR:
-
- Khushal P Soonderji
-
- DATE:
-
- Thursday, 12th Sept., 2024
-
- OBJECTIVE:
-
- To maintain all status codes in one place.
-
- REFERENCES:
-
- N/A
-
- DOWNLOADS:
-
- N/A
-
-"""
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** IMPORT ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-from enum import Enum, unique
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** MACROS / ONE-TIME INIT ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-@unique
-class HttpCodes(Enum):
-
- """
- Commonly used standard HTTP status codes.
- Can be sent after an API Call is processed.
- Refer to: https://http.dev/status
- NOTE: THIS LIST IS NOT EXHAUSTIVE!
- """
-
- # 1XX - Informational:
- CONTINUE = 100 # .............. https://http.dev/102
- SWITCHING_PROTOCOLS = 101 # ... https://http.dev/101
- PROCESSING = 102 # ............ https://http.dev/102
- EARLY_HINTS = 103 # ........... https://http.dev/103
-
- # 2XX - Success:
- SUCCESS = 200 # .................. https://http.dev/200
- CREATED = 201 # .................. https://http.dev/201
- ACCEPTED = 202 # ................. https://http.dev/202
- NON_AUTHORITATIVE_INFO = 203 # ... https://http.dev/203
- NO_CONTENT = 204 # ............... https://http.dev/204
- RESET_CONTENT = 205 # ............ https://http.dev/205
- PARTIAL_CONTENT = 206 # .......... https://http.dev/206
- MULTI_STATUS = 207 # ............. https://http.dev/207
- ALREADY_REPORTED = 208 # ......... https://http.dev/208
- THIS_IS_FINE = 218 # ............. https://http.dev/218
- IM_USED = 226 # .................. https://http.dev/226
-
- # 3XX - Redirection:
- MULTIPLE_CHOICES = 300 # ..... https://http.dev/300
- MOVED_PERMANENTLY = 301 # .... https://http.dev/301
- MOVED_TEMPORARILY = 302 # .... https://http.dev/302
- SEE_OTHER = 303 # ............ https://http.dev/303
- NOT_MODIFIED = 304 # ......... https://http.dev/304
- USE_PROXY = 305 # ............ https://http.dev/305
- SWITCH_PROXY = 306 # ......... https://http.dev/306
- TEMPORARY_REDIRECT = 307 # ... https://http.dev/307
- PERMANENT_REDIRECT = 308 # ... https://http.dev/308
-
- # 4XX - Client Errors:
- BAD_REQUEST = 400 # ..................... https://http.dev/401
- UNAUTHORIZED = 401 # .................... https://http.dev/401
- PAYMENT_REQUIRED = 402 # ................ https://http.dev/402
- FORBIDDEN = 403 # ....................... https://http.dev/403
- NOT_FOUND = 404 # ....................... https://http.dev/404
- METHOD_NOT_ALLOWED = 405 # .............. https://http.dev/405
- NOT_ACCEPTABLE = 406 # .................. https://http.dev/406
- PROXY_AUTH_REQUIRED = 407 # ............. https://http.dev/407
- REQUEST_TIMEOUT = 408 # ................. https://http.dev/408
- CONFLICT = 409 # ........................ https://http.dev/409
- GONE = 410 # ............................ https://http.dev/410
- LENGTH_REQUIRED = 411 # ................. https://http.dev/411
- PRECONDITION_FAILED = 412 # ............. https://http.dev/412
- PAYLOAD_TOO_LARGE = 413 # ............... https://http.dev/413
- URI_TOO_LONG = 414 # .................... https://http.dev/414
- UNSUPPORTED_MEDIA_TYPE = 415 # .......... https://http.dev/415
- PAGE_EXPIRED = 419 # .................... https://http.dev/419
- TOO_MANY_REQUESTS = 429 # ............... https://http.dev/429
- UNAVAILABLE_FOR_LEGAL_REASONS = 451 # ... https://http.dev/451
- INVALID_TOKEN = 498 # ................... https://http.dev/498
- CLIENT_CLOSED_REQUEST = 499 # ........... https://http.dev/499
-
- # 5XX - Server Errors:
- INTERNAL_SERVER_ERROR = 500 # ........... https://http.dev/500
- NOT_IMPLEMENTED = 501 # ................. https://http.dev/501
- BAD_GATEWAY = 502 # ..................... https://http.dev/502
- SERVICE_UNAVAILABLE = 503 # ............. https://http.dev/503
- GATEWAY_TIMEOUT = 504 # ................. https://http.dev/504
- HTTP_VERSION_NOT_SUPPORTED = 505 # ...... https://http.dev/505
- VARIANT_ALSO_NEGOTIATES = 506 # ......... https://http.dev/506
- INSUFFICIENT_STORAGE = 507 # ............ https://http.dev/507
- LOOP_DETECTED = 508 # ................... https://http.dev/508
- BANDWIDTH_LIMIT_EXCEEDED = 509 # ........ https://http.dev/509
- WEB_SERVER_DOWN = 521 # ................. https://http.dev/521
- ORIGIN_IS_UNREACHABLE = 523 # ........... https://http.dev/523
- SERVICE_IS_OVERLOADED = 529 # ........... https://http.dev/529
- NETWORK_READ_TIMEOUT_ERROR = 598 # ...... https://http.dev/598
- NETWORK_CONNECT_TIMEOUT_ERROR = 599 # ... https://http.dev/599
-
-
-# ---------------------------------------------------------------------------------------------------------------------
-
-
-@unique
-class StatusCodes(Enum):
-
- """
- To be used internally within the context of your service. Customize these to match your service.
- The format is: (SUCCESS_INDICATOR, INTERNAL_NUMERIC_CODE, HTTP_CODE)
- Example: (True, 1, 200)
- """
-
- # Legacy Codes:
- OK = (True, 1, HttpCodes.SUCCESS.value)
- FAILED = (False, 0, HttpCodes.INTERNAL_SERVER_ERROR.value)
- PARTIAL_SUCCESS = (True, 2, HttpCodes.PARTIAL_CONTENT.value)
- PARTIAL_FAILURE = (False, 3, HttpCodes.PARTIAL_CONTENT.value)
-
- # Authentication Codes:
- LOGGED_IN_SUCCESSFULLY = (True, 200, HttpCodes.SUCCESS.value)
- LOGIN_FAILED = (False, 201, HttpCodes.UNAUTHORIZED.value)
- INVALID_SESSION_TOKEN = (False, 202, HttpCodes.UNAUTHORIZED.value)
- AUTHENTICATION_DETAILS_INCOMPLETE = (False, 203, HttpCodes.BAD_REQUEST.value)
-
- # Authorization Codes:
- AUTHORIZED_SUCCESSFULLY = (True, 300, HttpCodes.SUCCESS.value)
- NOT_ALLOWED = (False, 301, HttpCodes.FORBIDDEN.value)
- AUTHORIZATION_DETAILS_INCOMPLETE = (False, 302, HttpCodes.BAD_REQUEST.value)
- AUTHORIZATION_FAILED = (False, 303, HttpCodes.UNAUTHORIZED.value)
-
- # General failures:
- DOWN_FOR_MAINTENANCE = (False, 800, HttpCodes.SERVICE_UNAVAILABLE.value)
- UNKNOWN_ERROR = (False, 801, HttpCodes.INTERNAL_SERVER_ERROR.value)
- DATA_INCOMPLETE = (False, 802, HttpCodes.BAD_REQUEST.value)
- HEADERS_INCOMPLETE = (False, 803, HttpCodes.BAD_REQUEST.value)
- FILES_MISSING = (False, 804, HttpCodes.BAD_REQUEST.value)
- CLIENT_CLOSED_REQUEST = (False, 805, HttpCodes.CLIENT_CLOSED_REQUEST.value)
-
- # Validation failure:
- DATA_VALIDATION_FAILURE = (False, 900, HttpCodes.BAD_REQUEST.value)
- RATE_LIMIT_EXCEEDED = (False, 901, HttpCodes.TOO_MANY_REQUESTS.value)
diff --git a/utils_v2/api/log.py b/utils_v2/api/log.py
deleted file mode 100644
index 9f5acef..0000000
--- a/utils_v2/api/log.py
+++ /dev/null
@@ -1,139 +0,0 @@
-"""
-
- AUTHOR:
-
- Khushal P Soonderji
-
- DATE:
-
- Thursday, 12th Sept., 2024
-
- OBJECTIVE:
-
- To have a structure to the response sent from the API calls.
-
- REFERENCES:
-
- N/A
-
- DOWNLOADS:
-
- N/A
-
-"""
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** IMPORT ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# System-level activities:
-import distro
-import socket
-import platform
-
-# For data-modelling:
-from pydantic import BaseModel, Field
-from typing import Any, Optional, List, Literal
-
-# To work with date and time:
-import datetime
-
-# My utils:
-from utils_v2.api.codes import StatusCodes, HttpCodes
-from utils_v2.date_time import date_time
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** MACROS / ONE-TIME INIT ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# Info for logging that will stay constant during runtime:
-SERVER_HOSTNAME = str(socket.gethostname())
-PLATFORM_INFO = platform.uname()
-HOST_OS = str(distro.name(True))
-HOST_CPU = f"{PLATFORM_INFO.processor} ({PLATFORM_INFO.machine})"
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** VARIABLES ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# --- Nothing Yet
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** CLASSES ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-class APILogModel(BaseModel):
-
- # To identify the machine the code is running on.
- # DO NOT MODIFY THESE:
- hostname: str = SERVER_HOSTNAME
- os: str = HOST_OS
- cpu: str = HOST_CPU
-
- # To identify the project and actions:
- project: Optional[str] = None
- log: str
- operation: str
- apiVer: Optional[str] = None
- logId: Optional[str] = None
- logChain: Optional[str] = None
-
- # Timing metrics:
- ts: datetime.datetime
- tat: float
- cpuTime: float
-
- # To understand the request that came in:
- method: Optional[str] = None
- url: Optional[str] = None
- route: Optional[str] = None
- headers: Optional[Any] = None
- data: Optional[Any] = None
- files: Optional[Any] = None
-
- # To understand the output that went out:
- exception: Optional[Any] = None
- response: Optional[Any] = None
- httpCode: Optional[int] = None
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** FUNCTIONS ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# --- Nothing Yet
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** MAIN PROGRAM ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-if __name__ == "__main__":
-
- my_log = APILogModel(
- log = "internal"
- )
-
- print(my_log)
diff --git a/utils_v2/api/response.py b/utils_v2/api/response.py
deleted file mode 100644
index 02e3d72..0000000
--- a/utils_v2/api/response.py
+++ /dev/null
@@ -1,133 +0,0 @@
-"""
-
- AUTHOR:
-
- Khushal P Soonderji
-
- DATE:
-
- Thursday, 12th Sept., 2024
-
- OBJECTIVE:
-
- To have a structure to the response sent from the API calls.
-
- REFERENCES:
-
- N/A
-
- DOWNLOADS:
-
- N/A
-
-"""
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** IMPORT ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# For data-modelling:
-from pydantic import BaseModel
-from typing import Any, Optional, List
-
-# My utils:
-from utils_v2.api.codes import StatusCodes, HttpCodes
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** MACROS / ONE-TIME INIT ***
-# ***** ****
-# *****************************************************************************************************************
-
-# --- Nothing Yet
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** VARIABLES ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# --- Nothing Yet
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** CLASSES ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-class ResponseModel(BaseModel):
-
- """
- A model for how the response should be when developing API endpoints.
- """
-
- # The fields that you want in your response:
- status_code: StatusCodes
- message: Optional[str | List] = None
- data: Optional[Any] = None
- seconds: Optional[float | int] = None
- log_id: Optional[str] = None
- http_code: Optional[HttpCodes] = None
- api_version: Optional[str] = None
-
- def for_quart(self):
-
- """
- Call this when you are using either Flask or Quart as your framework.
- :return: The output as expected by Flask and Quart.
- """
-
- # Construct the basic structure:
- response_dict = {
- "status": 1 if self.status_code.value[0] else 0,
- "code": self.status_code.value[1],
- "message": self.message or self.status_code.name.replace("_", " ").lower(),
- "data": self.data,
- "apiVer": self.api_version
- }
-
- # Now add the additional fields:
- if self.seconds is not None: response_dict["seconds"] = self.seconds
- if self.log_id is not None: response_dict["logId"] = self.log_id
-
- # Figure out the HTTP code:
- response_http_code = self.http_code.value if self.http_code is not None else self.status_code.value[2]
-
- # Done here:
- return response_dict, response_http_code
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** FUNCTIONS ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# --- Nothing Yet
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** MAIN PROGRAM ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-if __name__ == "__main__":
-
- my_response = ResponseModel(
- status_code = StatusCodes.RATE_LIMIT_EXCEEDED
- )
- my_response.log_id = "abc123"
-
- print(my_response.for_quart())
diff --git a/utils_v2/cache/__init__.py b/utils_v2/cache/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/utils_v2/cache/__pycache__/__init__.cpython-310.pyc b/utils_v2/cache/__pycache__/__init__.cpython-310.pyc
deleted file mode 100644
index 06f9307..0000000
Binary files a/utils_v2/cache/__pycache__/__init__.cpython-310.pyc and /dev/null differ
diff --git a/utils_v2/cache/__pycache__/async_redis_cache.cpython-310.pyc b/utils_v2/cache/__pycache__/async_redis_cache.cpython-310.pyc
deleted file mode 100644
index 0a16447..0000000
Binary files a/utils_v2/cache/__pycache__/async_redis_cache.cpython-310.pyc and /dev/null differ
diff --git a/utils_v2/cache/async_redis_cache.py b/utils_v2/cache/async_redis_cache.py
deleted file mode 100644
index a0ab8c0..0000000
--- a/utils_v2/cache/async_redis_cache.py
+++ /dev/null
@@ -1,497 +0,0 @@
-"""
-
- AUTHOR:
-
- Khushal P Soonderji
-
- DATE:
-
- Thursday, 11th April, 2024
-
- OBJECTIVE:
-
- To provide an easy way to work with '.json' files in an async manner.
-
- REFERENCES:
-
- 1) https://www.w3schools.com/python/python_json.asp
-
- DOWNLOADS:
-
- N/A
-
-"""
-
-# *****************************************************************************************************************
-# ***** ****
-# *** IMPORT ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# To use redis:
-import redis.asyncio as redis
-
-# Other utils:
-from utils_v2.string import json
-
-# For debugging:
-from icecream import IceCreamDebugger
-
-# To make a decorator:
-from functools import wraps
-
-# For hashing and shortening the hash:
-import hashlib
-import base64
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** MACROS / ONE-TIME INIT ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# --- Nothing Yet
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** VARIABLES ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# --- Nothing Yet
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** FUNCTIONS ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# --- Nothing Yet
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** WRAPPERS ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-def cache_it(cache = None, expiry = 120):
-
- """
- This decorator factory takes an instance of the async caching class 'AsyncRedisCache' and holds your data there.
- If a subsequent call is made to the same decorated function with the same inputs, the result is fetched from the
- cache instead of going through the whole function again.
- :param cache: The instance of 'AsyncRedisCache'.
- :param expiry: The time in seconds after which the cached data must be cleared.
- :return: The decorator that automatically caches your data.
- """
-
- def decorator(func):
-
- @wraps(func)
- async def wrapper(*args, **kwargs):
-
- # We first use the name of the function and the inputs given to it to generate a key for Redis with the
- # simple hashing and shortening by way of base64 strings:
- inputs_given = func.__name__ + str([_ for _ in args]) + str(kwargs)
- sha256_hash = hashlib.sha256()
- sha256_hash.update(inputs_given.encode("utf-8"))
- hashed_key = sha256_hash.digest()
- base64_key = base64.b64encode(hashed_key).decode("utf-8")
-
- # Now we check if we have the value in cache:
- try: response = await cache.get(base64_key, raise_exception = True)
-
- # If the key doesn't exist, we pass through the function and store the results.
- except:
- response = await func(*args, **kwargs)
- await cache.set(key = base64_key, value = response, expiry = expiry)
-
- # Return the response from the wrapped function.
- return response
-
- return wrapper
-
- return decorator
-
-
-# ---------------------------------------------------------------------------------------------------------------------
-
-
-def cache_class_methods(attr_name, expiry = 120):
-
- """
- This decorator factory takes an instance of the async caching class 'AsyncRedisCache' and holds your data there.
- If a subsequent call is made to the same decorated function with the same inputs, the result is fetched from the
- cache instead of going through the whole function again.
- :param attr_name: The name of the variable that has an instance of "AsyncRedisCache".
- :param expiry: The time in seconds after which the cached data must be cleared.
- :return: The decorator that automatically caches your data.
- """
-
- def decorator(func):
-
- @wraps(func)
- async def wrapper(self, *args, **kwargs):
-
- # Get the cache object first:
- cache_obj = getattr(self, attr_name)
-
- # We first use the name of the function and the inputs given to it to generate a key for Redis with the
- # simple hashing and shortening by way of base64 strings:
- inputs_given = func.__name__ + str([_ for _ in args]) + str(kwargs)
- sha256_hash = hashlib.sha256()
- sha256_hash.update(inputs_given.encode("utf-8"))
- hashed_key = sha256_hash.digest()
- base64_key = base64.b64encode(hashed_key).decode("utf-8")
-
- # Now we check if we have the value in cache:
- try: response = await cache_obj.get(base64_key, raise_exception = True)
-
- # If the key doesn't exist, we pass through the function and store the results.
- except:
- response = await func(self, *args, **kwargs)
- await cache_obj.set(key = base64_key, value = response, expiry = expiry)
-
- # Return the response from the wrapped function.
- return response
-
- return wrapper
-
- return decorator
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** CLASSES ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-class AsyncRedisCache:
-
- # Handle datatypes when setting values to cache:
- __set_converters = {
- "set": lambda x: list(x),
- "tuple": lambda x: list(x),
- "complex": lambda x: {"r": x.real, "i": x.imag}
- }
-
- # Handle datatypes when getting from cache:
- __get_converters = {
- "set": lambda x: set(x),
- "tuple": lambda x: tuple(x),
- "complex": lambda x: complex(x["r"], x["i"])
- }
-
- def __init__(
- self,
- connection_string,
- ping_counter = 1_000,
- debug = False,
- debug_prefix = "R-Cache | "
- ):
-
- """
- Implements a simple cache in Redis which holds and returns all native datatypes like ints, floats, bools,
- strings, dicts, lists, sets, and tuples :)
- :param connection_string: The connection URL for connecting to Redis.
- :param ping_counter: The number of requests to Redis after which you want to ping to ensure connection.
- :param debug: Whether, or not, you want to show debugging messages from the start.
- :param debug_prefix: The prefix text to show with the debugging messages.
- """
-
- # Note down the configuration:
- self.__client = None
- self.__ping_counter = ping_counter
- self.__requests_since_last_ping = 0
- self.__connection_string = connection_string
-
- # For debugging:
- self.__printer = IceCreamDebugger(prefix = debug_prefix, includeContext = True)
- if not debug: self.__printer.disable()
-
- def enable_debug(self):
- self.__printer.enable()
-
- def disable_debug(self):
- self.__printer.disable()
-
- @staticmethod
- def make_key(*args, **kwargs):
-
- """
- Generates a key by hashing the args and kwargs sent to it. Can be useful to generate a predictable key. If the
- inputs stay the same, the output stays the same.
- :param args: Any number of args that you would like to use to generate the key.
- :param kwargs: Any number of kwargs that you would like to use to generate the key.
- :return: A string that can be used as a key to store values on Redis.
- """
-
- # Take everything into one plain text string:
- plain_text = "".join(str(a) for a in args)
- plain_text += json.to_string(kwargs, no_space = True)
-
- # Hash the plain text value, and create a key from it:
- sha256_hash = hashlib.sha256()
- sha256_hash.update(plain_text.encode("utf-8"))
- hashed_key = sha256_hash.digest()
- base64_key = base64.b64encode(hashed_key).decode("utf-8")
-
- # Done here:
- return base64_key
-
- def add_converter(
- self,
- type_name,
- set_converter_func,
- get_converter_func,
- ):
-
- """
- To add custom data converters to handle new values.
- RULE: Each of the converter functions must take in exactly on argument.
- :param type_name: The name of the type of the object to handle. HINT: type(obj).__name__.
- :param set_converter_func: A converter function that converts from the custom datatype to a datatype that Redis
- can work with. Try converting to an bool, int, float, str, list or dict.
- :param get_converter_func: The inverse of the set converter. This function will be used to convert from a
- datatype that Redis can work with to the custom datatype.
- :return: None.
- """
-
- self.__set_converters[type_name] = lambda x: set_converter_func(x)
- self.__get_converters[type_name] = lambda x: get_converter_func(x)
-
- async def connect(self):
-
- """
- To make an asynchronous connection request to the Redis server to establish a connection.
- :return: True if connected, else False.
- """
-
- try:
- self.__client = redis.from_url(
- self.__connection_string
- )
- return True
-
- except Exception as exception:
- self.__printer(exception)
- return False
-
- async def disconnect(self):
-
- """
- Close the connection to the Redis server.
- :return: True if successful, else False.
- """
-
- if self.__client is not None:
- try: await self.__client.close()
- except Exception as exception:
- self.__printer(exception)
- return False
- return True
- return True
-
- async def ensure_connection(self):
-
- """
- To ensure that we are connected. We keep pinging the Redis server every once in a while even when connected.
- :return: True if connected, else False.
- """
-
- # If we are not connected, we try to establish a connection:
- if self.__client is None: return await self.connect()
-
- # Else we check if we are connected. If not, we try to connect.
- # But we check only once in a while. In the meantime, we assume that we are connected.
- elif self.__requests_since_last_ping > self.__ping_counter:
- try:
- await self.__client.ping()
- self.__requests_since_last_ping = 0
- return True
- except Exception as exception:
- self.__printer(exception)
- return await self.connect()
- else:
- self.__requests_since_last_ping += 1
- return True
-
- async def set(self, key, value, expiry: float = 120, raise_exception = False):
-
- """
- Saves some value to the cache.
- :param key: The key with which the data will be stored and retrieved.
- :param value: The value to store.
- :param expiry: The time in seconds after which the data will expire. Must be a positive number.
- :param raise_exception: If you want to raise an exception if the process fails.
- :return: True if cached, else False.
- """
-
- await self.ensure_connection()
-
- try:
-
- # We handle conversion for unsupported Pythonic datatypes:
- original_type = type(value).__name__
- converter = self.__set_converters.get(original_type)
- if converter is not None: value = converter(value)
-
- # Here we actually try to store the data:
- response = await self.__client.setex(
- key,
- int(expiry),
- json.to_string({
- "value": value,
- "type": original_type
- }, indent = 0, no_space = False)
- )
- return response
-
- except Exception as exception:
- self.__printer(exception)
- if raise_exception: raise
- else: return False
-
- async def get(self, key, raise_exception = False, on_fail = None):
-
- """
- Retrieve the cached value.
- :param key: The key with which the data was saved.
- :param raise_exception: If you want to raise an exception if the process fails.
- :param on_fail: What to return if the process fails due to an exception.
- :return: The retrieved data or null if not found.
- """
-
- await self.ensure_connection()
-
- try:
- data = await self.__client.get(key)
- data = data.decode("utf-8")
- data = json.from_string(data)
- converter = self.__get_converters.get(data["type"])
- if converter is not None: return converter(data["value"])
- return data["value"]
-
- except Exception as exception:
- self.__printer(exception)
- if raise_exception: raise
- else: return on_fail
-
- async def delete(self, key, raise_exception = False):
-
- """
- Prematurely delete the value from the cache before it expires.
- :param key: The key with which the data was saved.
- :param raise_exception: If you want to raise an exception if the process fails.
- :return: True if deleted, else False.
- """
-
- await self.ensure_connection()
-
- try:
- response = await self.__client.delete(key)
- return True if response else False
-
- except Exception as exception:
- self.__printer(exception)
- if raise_exception: raise
- else: return False
-
- async def count(self, key, value: int = 1, expiry: float = None, raise_exception = False):
-
- """
- To use simple counters. If the counter (identified by the 'key') exists, it will be incremented, else the
- counter will be created and the value will be incremented from 0.
- :param key: The name of the counter.
- :param value: The amount to increment the value by. Send negative values to count backwards.
- :param expiry: The time (in seconds) in which the counter expires. Starts from the time the counter is created.
- This value has to be an integer. If a float is passed, the value will be rounded off.
- :param raise_exception: If you want to raise an exception if the process fails.
- :return: The latest value of the counter. Will be null if something went wrong and the exception was suppressed.
- """
-
- await self.ensure_connection()
-
- try:
-
- already_existed = await self.__client.exists(key)
- new_value = await self.__client.incrby(key, value)
- if expiry and not already_existed: await self.__client.expire(key, int(expiry))
- return new_value
-
- except Exception as exception:
- self.__printer(exception)
- if raise_exception: raise
- else: return None
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** MAIN PROGRAM ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-if __name__ == "__main__":
-
- import asyncio
-
- def set_complex(x):
- return {"r": x.real, "i": x.imag}
-
- def get_complex(x):
- return complex(x["r"], x["i"])
-
- async def main():
-
- my_cache = AsyncRedisCache(
- connection_string = r"redis://:dc4da94197c843ab6a730113c2b801d9@redis.ditscentre.in/0",
- ping_counter = 100,
- debug = False
- )
-
- my_cache.add_converter(
- type_name = type(2j).__name__,
- set_converter_func = set_complex,
- get_converter_func = get_complex
- )
-
- value = await my_cache.get(key = "6b61afd0-b066-4611-9791-411a30d34624")
- print("GET:", value)
- print("TYP:", type(value), end = "\n\n")
-
- # success = await my_cache.set(
- # key = "name",
- # value = {"first": "John", "last": "Doe"},
- # expiry = 10
- # )
- # print("SET:", success, end = "\n\n")
- #
- # value = await my_cache.get(key = "name")
- # print("GET:", value)
- # print("TYP:", type(value), end = "\n\n")
- #
- # success = await my_cache.delete(key = "name")
- # print("DEL:", success, end = "\n\n")
- #
- # value = await my_cache.get(key = "cnt")
- # print("GET:", value)
- # print("TYP:", type(value), end = "\n\n")
- #
- # await my_cache.delete(key = "cnt")
- # for _ in range(50):
- # await asyncio.sleep(1.0)
- # counter = await my_cache.count(key = "cnt", value = 1, expiry = 10)
- # print("COUNTER:", counter)
-
- asyncio.run(main())
diff --git a/utils_v2/database/__init__.py b/utils_v2/database/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/utils_v2/database/__pycache__/__init__.cpython-310.pyc b/utils_v2/database/__pycache__/__init__.cpython-310.pyc
deleted file mode 100644
index a9d214a..0000000
Binary files a/utils_v2/database/__pycache__/__init__.cpython-310.pyc and /dev/null differ
diff --git a/utils_v2/database/__pycache__/async_mongo_v2.cpython-310.pyc b/utils_v2/database/__pycache__/async_mongo_v2.cpython-310.pyc
deleted file mode 100644
index 2fe27b8..0000000
Binary files a/utils_v2/database/__pycache__/async_mongo_v2.cpython-310.pyc and /dev/null differ
diff --git a/utils_v2/database/__pycache__/async_mysql_v2.cpython-310.pyc b/utils_v2/database/__pycache__/async_mysql_v2.cpython-310.pyc
deleted file mode 100644
index b97ae9b..0000000
Binary files a/utils_v2/database/__pycache__/async_mysql_v2.cpython-310.pyc and /dev/null differ
diff --git a/utils_v2/database/async_firebase.py b/utils_v2/database/async_firebase.py
deleted file mode 100644
index a8065a1..0000000
--- a/utils_v2/database/async_firebase.py
+++ /dev/null
@@ -1,504 +0,0 @@
-"""
-
- AUTHOR:
-
- Khushal P Soonderji
- Bhushan Thakkar
-
- DATE:
-
- Friday, 19th April, 2024
-
- OBJECTIVE:
-
- To provide an easy interface to work with Firebase.
-
- REFERENCES:
-
- N/A
-
- DOWNLOADS:
-
- N/A
-
-"""
-
-# *****************************************************************************************************************
-# ***** ****
-# *** IMPORT ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# To make sibling directories accessible for imports:
-# ---
-import sys
-sys.path.append(".")
-sys.path.append("..")
-
-# For async operations:
-# ---
-import asyncio
-
-# For system-level activities:
-# ---
-import os
-
-# My async utils:
-# ---
-import async_json_utils
-
-# Firebase:
-# ---
-import firebase_admin
-import firebase_admin.firestore_async
-import firebase_admin.auth
-import firebase_admin.db
-
-# For debugging and logging:
-# ---
-from icecream import IceCreamDebugger
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** MACROS / ONE-TIME INIT ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# --- Nothing Yet
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** VARIABLES ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# --- Nothing Yet
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** FUNCTIONS ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-class AsyncFirebase:
-
- def __init__(
- self,
- credentials_json_path,
- app_name,
- max_connections = 5,
- debug = True
- ):
-
- # Debugging print:
- # ---
- self.__printer = IceCreamDebugger(prefix = f"FBase ({app_name[:8]}) | ", includeContext = True)
- if not debug: self.__printer.disable()
- alert = f"Firebase session starting with max. {max_connections} connections."
- self.__printer(alert)
-
- # Initialize the instance:
- # ---
- self.__semaphore = asyncio.Semaphore(max_connections)
- credentials = firebase_admin.credentials.Certificate(credentials_json_path)
- self.__firebase_app = firebase_admin.initialize_app(credentials, name = app_name)
- self.__firestore = firebase_admin.firestore_async.client(self.__firebase_app)
-
- def __del__(self):
- firebase_admin.delete_app(self.__firebase_app)
- self.__firestore.close()
- alert = "Firebase session ended."
- self.__printer(alert)
-
- def __user_to_json(self, firebase_user):
-
- user_json = {
- "uid": firebase_user.uid,
- "email": firebase_user.email,
- "emailVerified": firebase_user.email_verified,
- "displayName": firebase_user.display_name,
- "phoneNo": firebase_user.phone_number,
- "photoUrl": firebase_user.photo_url,
- "customClaims": firebase_user.custom_claims,
- "disabled": firebase_user.disabled,
- "providerId": firebase_user.provider_id,
- "providerData": firebase_user.provider_data,
- "tenantId": firebase_user.tenant_id
- }
-
- return user_json
-
- async def create_custom_token(self, uid):
-
- async with self.__semaphore:
-
- try:
- custom_token = firebase_admin.auth.create_custom_token(
- uid = uid,
- app = self.__firebase_app
- ).decode("utf-8")
- return custom_token
-
- except Exception as exception:
- self.__printer(exception)
- return None
-
- async def authenticate_token(self, token):
-
- """
- To authenticate the given session token.
- :param token: The session token generated by Firebase on a successful sign-in.
- :return: Either the retrieved user information or a blank dictionary.
- """
-
- try:
- user_info = firebase_admin.auth.verify_id_token(token, app = self.__firebase_app)
- return user_info
- except Exception as exception:
- self.__printer(exception)
- return {}
-
- async def create_user(
- self,
- uid,
- display_name = None,
- password = None,
- email = None,
- phone_number = None
- ):
-
- """
- Create a new user.
- :param uid: The id to identify the user by.
- :return: True or False based on the success of the operation.
- """
-
- async with self.__semaphore:
-
- try:
- firebase_admin.auth.create_user(
- uid = uid,
- app = self.__firebase_app
- )
- return True
-
- except firebase_admin.auth.UidAlreadyExistsError as excp:
- return True
-
- except Exception as exception:
- self.__printer(exception)
- return False
-
- async def delete_user(self, uid):
-
- async with self.__semaphore:
-
- try:
- firebase_admin.auth.delete_user(
- uid = uid,
- app = self.__firebase_app
- )
- return True
-
- except firebase_admin.auth.UserNotFoundError as excp:
- return True
-
- except Exception as exception:
- self.__printer(exception)
- return False
-
- async def get_user(self, uid):
-
- async with self.__semaphore:
-
- try:
- user = firebase_admin.auth.get_user(
- uid = uid,
- app = self.__firebase_app
- )
- return self.__user_to_json(user)
-
- except Exception as exception:
- self.__printer(exception)
- return None
-
- async def list_users(self, users_per_page, page_token = None):
-
- """
- To get a list of users. Firebase has a limit of 1000 per call of this API. So we use page-tokens to fetch next
- pages of users.
- :param users_per_page: How many users you want to list in this API call (Max. 1,000).
- :param page_token: To be used in case of pagination.
- :return: The list of users and the page token to be used for the next call. Will be None in case of failure.
- """
-
- async with self.__semaphore:
-
- try:
- users = firebase_admin.auth.list_users(
- page_token = page_token,
- max_results = min(users_per_page, 1000),
- app = self.__firebase_app
- )
- next_page_token = users.next_page_token if users.has_next_page else None
- users = users.users
- users_json = [self.__user_to_json(user) for user in users]
- return users_json, next_page_token
-
- except Exception as exception:
- self.__printer(exception)
- return None, None
-
- async def create_document(self, collection_path, document_name, document_data = None):
-
- """
- To create a new document with the specified data in an existing collection.
- :param collection_path: The path of the collection (can be a sub-collection).
- :param document_name: The name of the document you want to create.
- :param document_data: The data that you want to populate in the document.
- :return: True or False based on the success of the operation.
- """
-
- async with self.__semaphore:
-
- try:
- snapshot = await self.__firestore.collection(
- collection_path
- ).add(
- document_id = document_name,
- document_data = document_data or {}
- )
- return True
-
- except Exception as exception:
- self.__printer(exception)
- return False
-
- async def delete_document(self, path):
-
- """
- To delete a document.
- :param path: The path of the document.
- :return: True or False based on the success of the operation.
- """
-
- async with self.__semaphore:
-
- try:
- snapshot = await self.__firestore.document(path).delete()
- return True
-
- except Exception as excp:
- print("FIRESTORE DOCUMENT DELETION EXCEPTION:", excp)
- return False
-
- async def set_document(self, path, data):
-
- """
- To overwrite the data in a document.
- :param path: The path of the document.
- :param data: The data you want to update as a dictionary.
- :return: True or False based on the success of the operation.
- """
-
- async with self.__semaphore:
-
- try:
- snapshot = await self.__firestore.document(path).set(data)
- return True
-
- except Exception as exception:
- self.__printer(exception)
- return False
-
- async def update_document(self, path, data):
-
- """
- To update the data in a document. Provide only the fields that you want to update.
- :param path: The path of the document.
- :param data: The data you want to update as a dictionary.
- :return: True or False based on the success of the operation.
- """
-
- async with self.__semaphore:
-
- try:
- snapshot = await self.__firestore.document(path).update(data)
- return True
-
- except Exception as exception:
- self.__printer(exception)
- return False
-
- async def get_document(self, path):
-
- """
- To get the data in a document.
- :param path: The path of the document.
- :return: The dictionary of data as found in the path specified or None if the operation failed.
- """
-
- async with self.__semaphore:
-
- try:
- snapshot = await self.__firestore.document(path).get()
- return snapshot.to_dict()
-
- except Exception as exception:
- self.__printer(exception)
- return None
-
- async def get_document_fields(self, path, fields):
-
- """
- To get only specific fields (keys) in a document. Like how projections are used in Mongo.
- :param path: The path of the document.
- :param fields: The list of fields (keys) of the document that you want.
- :return: The dictionary of data as found in the path specified.
- """
-
- async with self.__semaphore:
-
- try:
- if type(fields) is not list: fields = [fields]
- snapshot = await self.__firestore.document(path).get(fields)
- return snapshot.to_dict()
-
- except Exception as exception:
- self.__printer(exception)
- return None
-
- async def create_collection(self, collection_name, document_name, document_data):
-
- """
- Creates a new collection in the root of the database. Note that Firebase doesn't allow creating new empty
- collections, so we must add one first document in it.
- :param collection_name: The name of the collection you want to create.
- :param document_name: The name of the first document you want to put in the collection.
- :param document_data: The data that you want to put in the first document of the new collection.
- :return: True or False based on the success of the operation.
- """
-
- async with self.__semaphore:
-
- try:
- snapshot = await self.__firestore.collection(
- collection_name
- ).add(
- document_id = document_name,
- document_data = document_data
- )
- return True
-
- except Exception as exception:
- self.__printer(exception)
- return False
-
- async def create_sub_collection(
- self,
- document_path,
- sub_collection_name,
- sub_document_name = None,
- sub_document_data = None
- ):
-
- """
- Firebase allows you to create collections inside documents. This method is built for that. Note that Firebase
- doesn't allow creating new empty collections, so we must add one first document in it.
- :param document_path: The path of the document in which you want to create a new collection.
- :param sub_collection_name: The name of the collection you want to create.
- :param sub_document_name: The name of the first document you want to put in the collection.
- :param sub_document_data: The data that you want to put in the first document of the new collection.
- :return: True or False based on the success of the operation.
- """
-
- async with self.__semaphore:
-
- try:
- snapshot = await self.__firestore.document(
- document_path
- ).collection(
- sub_collection_name
- ).add(
- document_id = sub_document_name,
- document_data = sub_document_data or {}
- )
- return True
-
- except Exception as exception:
- self.__printer(exception)
- return False
-
- async def get_collection(self, path):
-
- """
- Get a whole collection's data.
- :param path: The path of the collection.
- :return: The collection.
- """
-
- async with self.__semaphore:
-
- try:
- snapshots = await self.__firestore.collection(path).get()
- return {snapshot.id: snapshot.to_dict() for snapshot in snapshots}
-
- except Exception as exception:
- self.__printer(exception)
- return None
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** MAIN PROGRAM ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-if __name__ == "__main__":
-
- import time
- from shared import constants
-
- user_data = {
- "firebaseUid": None,
- "roles": "ClientAdmin",
- "sessionToken": "siddhesh_20240419"
- }
-
- async def test():
- my_firebase = AsyncFirebase(
- f"{constants.PROJECT_DIRECTORY}/utils/cred/firebase_certs.json",
- "myFire",
- max_connections = 5
- )
- print("\n\n---\n\n")
- results = await my_firebase.get_document("testCollection/testDoc")
- print(async_json_utils.to_json_string(results))
- print("\n\n---\n\n")
- results = await my_firebase.get_document_fields("testCollection/testDoc", ["sampleMap"])
- print(async_json_utils.to_json_string(results))
- # await my_firebase.create_sub_collection(
- # "myDeepCollection/deepDocId",
- # "subCollection3",
- # "subDoc",
- # {"sub_key": "sub_val"}
- # )
- # await my_firebase.create_collection(
- # "rootCollection",
- # "subDoc",
- # {"sub_key": "sub_val"}
- # )
- # await my_firebase.delete_document("activeSessions/9d402f1502dfd55a4326fa7fc8e6cb7d")
- # print(async_json_utils.to_json_string(await my_firebase.get_collection("myDeepCollection")))
-
- start_time = time.time()
- asyncio.run(test())
- print(f"FINISHED IN {time.time() - start_time} SECONDS.")
diff --git a/utils_v2/database/async_mongo.py b/utils_v2/database/async_mongo.py
deleted file mode 100644
index c052fac..0000000
--- a/utils_v2/database/async_mongo.py
+++ /dev/null
@@ -1,843 +0,0 @@
-"""
-
- AUTHOR:
-
- Khushal P Soonderji
-
- DATE:
-
- Tuesday, 28th May, 2024
-
- OBJECTIVE:
-
- To have one central place from where all async database connectivity happens.
-
- REFERENCES:
-
- 01. https://motor.readthedocs.io/en/stable/
-
- DOWNLOADS:
-
- N/A
-
-"""
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** IMPORT ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# To make sibling directories accessible for imports:
-import sys
-sys.path.append(".")
-sys.path.append("..")
-
-# For async behaviour:
-import asyncio
-
-# For datetime handling:
-import pytz
-import datetime
-
-# MongoDB:
-from motor.motor_asyncio import AsyncIOMotorClient
-from bson.objectid import ObjectId
-from bson.json_util import dumps, loads
-
-# For debugging:
-from icecream import IceCreamDebugger
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** MACROS / ONE-TIME INIT ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# --- Nothing Yet
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** VARIABLES ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# --- Nothing Yet
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** EXCEPTIONS ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-class MongoFindException(Exception):
-
- def __init__(self, hint = None, origin = None):
- self.__hint = hint
- self.__origin = origin
-
- def __str__(self):
- message = "mongo find operation failed"
- if self.__hint is not None: message = f"{message} ({self.__hint})"
- if self.__origin is not None: message = f"{self.__origin} --> {message}"
- return message
-
-
-# ---------------------------------------------------------------------------------------------------------------------
-
-
-class MongoInsertException(Exception):
-
- def __init__(self, hint = None, origin = None):
- self.__hint = hint
- self.__origin = origin
-
- def __str__(self):
- message = "mongo insert operation failed"
- if self.__hint is not None: message = f"{message} ({self.__hint})"
- if self.__origin is not None: message = f"{self.__origin} --> {message}"
- return message
-
-
-# ---------------------------------------------------------------------------------------------------------------------
-
-
-class MongoUpdateException(Exception):
-
- def __init__(self, hint = None, origin = None):
- self.__hint = hint
- self.__origin = origin
-
- def __str__(self):
- message = "mongo update operation failed"
- if self.__hint is not None: message = f"{message} ({self.__hint})"
- if self.__origin is not None: message = f"{self.__origin} --> {message}"
- return message
-
-
-# ---------------------------------------------------------------------------------------------------------------------
-
-
-class MongoReplaceException(Exception):
-
- def __init__(self, hint = None, origin = None):
- self.__hint = hint
- self.__origin = origin
-
- def __str__(self):
- message = "mongo replace operation failed"
- if self.__hint is not None: message = f"{message} ({self.__hint})"
- if self.__origin is not None: message = f"{self.__origin} --> {message}"
- return message
-
-
-# ---------------------------------------------------------------------------------------------------------------------
-
-
-class MongoDeleteException(Exception):
-
- def __init__(self, hint = None, origin = None):
- self.__hint = hint
- self.__origin = origin
-
- def __str__(self):
- message = "mongo delete operation failed"
- if self.__hint is not None: message = f"{message} ({self.__hint})"
- if self.__origin is not None: message = f"{self.__origin} --> {message}"
- return message
-
-
-# ---------------------------------------------------------------------------------------------------------------------
-
-
-class MongoException(Exception):
-
- def __init__(self, hint = None, origin = None):
- self.__hint = hint
- self.__origin = origin
-
- def __str__(self):
- message = "mongo operation failed"
- if self.__hint is not None: message = f"{message} ({self.__hint})"
- if self.__origin is not None: message = f"{self.__origin} --> {message}"
- return message
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** FUNCTIONS ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# --- Nothing Yet
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** CLASSES ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-class AsyncMongo:
-
- __db = None
- __db_name = None
- __client = None
-
- def __init__(
- self,
- database_name = "myDb",
- max_connections = 5,
- debug = True,
- debug_only_errors = True,
- host = "localhost",
- port = 27017,
- connection_string = None
- ):
-
- # Basic config:
- self.__max_connections = max_connections
- self.__db_name = database_name
- self.__host = host,
- self.__port = port
- self.__connection_string = connection_string
-
- # For debugging:
- self.__debug_only_errors = debug_only_errors
- self.__printer = IceCreamDebugger(prefix = f"Mongo ({self.__db_name}) | ", includeContext = True)
- if not debug: self.__printer.disable()
-
- # Rate/access control:
- self.__exclusive_semaphore = asyncio.Semaphore(1)
-
- def enable_debug(self):
- self.__printer.enable()
-
- def disable_debug(self):
- self.__printer.disable()
-
- @staticmethod
- def generate_id():
-
- """
- Just generates an '_id' in MongoDB style.
- :return: The '_id' in MongoDB style.
- """
-
- return str(ObjectId())
-
- async def connect(self):
- try:
- if self.__connection_string is None:
- self.__client = AsyncIOMotorClient(
- self.__host,
- self.__port,
- maxPoolSize = self.__max_connections,
- minPoolSize = self.__max_connections
- )
- else:
- self.__client = AsyncIOMotorClient(
- self.__connection_string,
- maxPoolSize = self.__max_connections,
- minPoolSize = self.__max_connections
- )
- self.__db = self.__client.get_database(self.__db_name)
- if not self.__debug_only_errors:
- server_info = await self.__client.server_info()
- self.__printer(server_info)
- except Exception as exception: self.__printer(exception)
-
- async def ensure_connection(self):
- if self.__db is None:
- async with self.__exclusive_semaphore:
- await self.connect()
-
- @property
- async def client(self):
- await self.ensure_connection()
- return self.__client
-
- @staticmethod
- def dict_to_dot_notation(input_dict, parent_key = "", separator = "."):
- items = []
- for k, v in input_dict.items():
- new_key = f"{parent_key}{separator}{k}" if parent_key else k
- if isinstance(v, dict) and v:
- items.extend(AsyncMongo.dict_to_dot_notation(v, new_key, separator = separator).items())
- else:
- items.append((new_key, v))
- return dict(items)
-
- @staticmethod
- def normalize_date_time(document):
-
- """
- MongoDB doesn't support timezones. A good strategy would be to convert everything to UTC format and store it.
- This method does exactly that. Any datetime object is converted to UTC timezone. If the datetime object was
- timezone naive, UTC timezone will be applied to it without changing the time value.
- :param document: The document that you want to normalize the date-time in.
- :return: The document with normalized datetime.
- """
-
- if isinstance(document, datetime.datetime):
- utc_tz = pytz.timezone("UTC")
- if document.tzinfo is None: document = utc_tz.localize(document)
- else: document = document.astimezone(utc_tz)
-
- if type(document) is list:
- document = [AsyncMongo.normalize_date_time(item) for item in document]
-
- if type(document) is dict:
- document = {
- AsyncMongo.normalize_date_time(k): AsyncMongo.normalize_date_time(v)
- for k, v in document.items()
- }
-
- return document
-
- @staticmethod
- def __from_json_string(json_data):
- return loads(json_data)
-
- @staticmethod
- def __to_json_string(python_data, indent = 4, default = None):
- return dumps(python_data, indent = indent, default = default)
-
- async def list_indexes(
- self,
- collection,
- session = None,
- raise_exception = False
- ):
-
- """
- Lists out the indexes of a collection.
- :param collection: The collection whose indexes you want to list out,
- :param session: The session if you need to do this in a transaction.
- :param raise_exception: Whether, or not, you want to raise an exception when something fails.
- :return: The list of indexes or None if the action fails.
- """
-
- # Ensure you are connected:
- await self.ensure_connection()
-
- # Assume failure:
- indexes = None
-
- # Try to list the indexes:
- try:
- responses = await self.__db[collection].list_indexes(session = session).to_list(None)
- indexes = [{key: value for key, value in response.items()} for response in responses]
- except Exception as exception:
- self.__printer(exception)
-
- # Check results and return:
- if raise_exception and not indexes: raise MongoException(hint = "list indexes")
- return indexes
-
- async def create_index(
- self,
- collection,
- keys,
- options = None,
- session = None,
- raise_exception = False
- ):
-
- """
- Creates an index on a collection.
- :param collection: The collection to create the index on.
- :param keys: The keys (and sorting) to implement the index on.
- :param options: Additional config.
- :param session: The session if you need to do this in a transaction.
- :param raise_exception: Whether, or not, you want to raise an exception when something fails.
- :return: True or False based on the success of the execution.
- """
-
- # Ensure you are connected:
- await self.ensure_connection()
-
- # Assume failure:
- success = False
-
- # Try to make the insertion:
- try:
- options = options or {}
- keys = [(k, v) for k, v in keys.items()]
- response = await self.__db[collection].create_index(keys, session = session, **options)
- if response: success = True
- except Exception as exception:
- self.__printer(exception)
-
- # Check results and return:
- if raise_exception and not success: raise MongoException(hint = "create index")
- return success
-
- async def insert_one(
- self,
- collection,
- document,
- session = None,
- raise_exception = False
- ):
-
- """
- Insert data into a collection.
- :param collection: The collection you want to feed the data into.
- :param document: The data to be stored.
- :param session: The session if you need to do this in a transaction.
- :param raise_exception: Whether, or not, you want to raise an exception when something fails.
- :return: The id of the inserted data, or null if the action fails.
- """
-
- # Ensure you are connected:
- await self.ensure_connection()
-
- # Assume failure:
- inserted_id = None
-
- # Try to make the insertion:
- try:
- response = await self.__db[collection].insert_one(document.copy(), session = session)
- inserted_id = response.inserted_id
- except Exception as exception:
- self.__printer(exception)
-
- # Check results and return:
- if raise_exception and not inserted_id: raise MongoInsertException(hint = f"{collection}")
- return inserted_id
-
- async def insert_many(
- self,
- collection,
- documents,
- session = None,
- raise_exception = False
- ):
-
- """
- Insert a lot of data into a collection.
- :param collection: The collection you want to feed the data into.
- :param documents: The data to be stored.
- :param session: The session if you need to do this in a transaction.
- :param raise_exception: Whether, or not, you want to raise an exception when something fails.
- :return: The id of the inserted data, or null if the action fails.
- """
-
- # Ensure you are connected:
- await self.ensure_connection()
-
- # Assume failure:
- inserted_ids = []
-
- # Try to make the insertion:
- try:
- response = await self.__db[collection].insert_many(documents, session = session)
- inserted_ids = response.inserted_ids
- except Exception as exception:
- self.__printer(exception)
-
- # Check results and return:
- if raise_exception and not inserted_ids: raise MongoInsertException(hint = f"{collection}")
- return inserted_ids
-
- async def update_one(
- self,
- collection,
- filter_json = None,
- update_json = None,
- upsert = False,
- session = None,
- raise_exception = False
- ):
-
- """
- Update one document.
- :param collection: The collection you want to update.
- :param filter_json: The selection criteria to locate the document to update.
- :param update_json: The values you want to update.
- :param upsert: If you want to insert if the document doesn't already exist.
- :param session: The session if you need to do this in a transaction.
- :param raise_exception: Whether, or not, you want to raise an exception when something fails.
- :return: True or False based on the success of the operation.
- """
-
- # Ensure you are connected:
- await self.ensure_connection()
-
- # Assume failure:
- success = False
-
- # Try to make the insertion:
- try:
- response = await self.__db[collection].update_one(
- filter_json,
- update_json,
- upsert = upsert,
- session = session
- )
- success = False if response.modified_count == 0 else True
- except Exception as exception:
- self.__printer(exception)
-
- # Check results and return:
- if raise_exception and not success: raise MongoUpdateException(hint = f"{collection}")
- return success
-
- async def update_many(
- self,
- collection,
- filter_json = None,
- update_json = None,
- upsert = False,
- session = None,
- raise_exception = False
- ):
-
- """
- Update many documents.
- :param collection: The collection you want to update.
- :param filter_json: The selection criteria to locate the document to update.
- :param update_json: The values you want to update.
- :param upsert: If you want to insert if the document doesn't already exist.
- :param session: The session if you need to do this in a transaction.
- :param raise_exception: Whether, or not, you want to raise an exception when something fails.
- :return: True or False based on the success of the operation.
- """
-
- # Ensure you are connected:
- await self.ensure_connection()
-
- # Assume failure:
- update_count = 0
-
- # Try to make the insertion:
- try:
- response = await self.__db[collection].update_many(
- filter_json,
- update_json,
- upsert = upsert,
- session = session
- )
- update_count = response.modified_count
- except Exception as exception:
- self.__printer(exception)
-
- # Check results and return:
- if raise_exception and not update_count: raise MongoUpdateException(hint = f"{collection}")
- return update_count
-
- async def count(
- self,
- collection,
- filter_json = None
- ):
-
- # Ensure you are connected:
- await self.ensure_connection()
-
- # Assume failure:
- count = 0
-
- # try to query the data:
- try:
- if filter_json is None: filter_json = {}
- count = await self.__db[collection].count_documents(filter_json)
- except Exception as exception: self.__printer(exception)
-
- # Done here:
- return count
-
- async def bulk_write(
- self,
- collection,
- requests,
- session = None,
- raise_exception = False
- ):
-
- """
- To perform various individual operations in one go. You will have to import individual actions like "UpdateOne"
- and "InsertMany" from PyMongo and pass them as an array of requests (operations) to this method.
- :param collection: The collection you want to run the requests on.
- :param requests: The array of requests (operations) to be performed.
- :param session: The session if you need to do this in a transaction.
- :param raise_exception: Whether, or not, you want to raise an exception when something fails.
- :return:
- """
-
- # Ensure you are connected:
- await self.ensure_connection()
-
- # Assume failure:
- count = 0
-
- # try to query the data:
- try:
- response = await self.__db[collection].bulk_write(requests, session = session)
- count = response.modified_count + response.inserted_count + response.upserted_count + response.deleted_count
- except Exception as exception:
- self.__printer(exception)
-
- # Check results and return:
- if raise_exception and not count: raise MongoException(hint = "bulk write")
- return count
-
- async def find_many(
- self,
- collection,
- filter_json,
- projections = None,
- skip = 0,
- limit = None,
- sort = None,
- session = None,
- as_json_string = False,
- indent = 4,
- raise_exception = False
- ):
-
- """
- Finds one or more records that match the given conditions.
- :param collection: The name of the collection to perform the search in.
- :param filter_json: The filter criteria.
- :param projections: What parts of the matching data you want to fetch.
- :param skip: The no. of records to skip before picking next ones. Needed for pagination.
- :param limit: The max. no. of records you want to fetch.
- :param sort: The sorting rules to apply.
- :param session: The session if you need to do this in a transaction.
- :param as_json_string: Whether you want it as a JSON string or a Python dict/list.
- :param indent: The indentation to use if you want it as a JSON string.
- :param raise_exception: Whether, or not, you want to raise an exception when something fails.
- :return: The array of matching records or null if there was an exception.
- """
-
- # Ensure you are connected:
- await self.ensure_connection()
-
- # Assume failure:
- results = None
-
- # Assume defaults:
- if sort is None: sort = {"_id": -1}
- if limit is None: limit = 10
-
- # try to query the data:
- try:
- results = await self.__db[collection].find(
- filter_json,
- projections,
- session = session
- ).sort(sort).skip(skip).limit(limit).to_list(None)
- if as_json_string: results = self.__to_json_string(results, indent = indent, default = str)
- except Exception as exception:
- self.__printer(exception)
-
- # Check results and return:
- if raise_exception and not results: raise MongoFindException(hint = f"{collection}")
- return results
-
- async def find_one(
- self,
- collection,
- filter_json,
- projections = None,
- session = None,
- as_json_string = False,
- indent = 4,
- raise_exception = False
- ):
-
- """
- Finds one record that matches the given conditions.
- :param collection: The name of the collection to perform the search in.
- :param filter_json: The filter criteria.
- :param projections: What parts of the matching data you want to fetch.
- :param session: The session if you need to do this in a transaction.
- :param as_json_string: Whether you want it as a JSON string or a Python dict/list.
- :param indent: The indentation to use if you want it as a JSON string.
- :param raise_exception: Whether, or not, you want to raise an exception when something fails.
- :return: The array of matching records or null if there was an exception.
- """
-
- # Ensure you are connected:
- await self.ensure_connection()
-
- # Assume failure:
- results = None
-
- # try to query the data:
- try:
- results = await self.__db[collection].find_one(filter_json, projections, session = session)
- if as_json_string: results = self.__to_json_string(results, indent = indent, default = str)
- except Exception as exception:
- self.__printer(exception)
-
- # Check results and return:
- if raise_exception and not results: raise MongoFindException(hint = f"{collection}")
- return results
-
- async def replace_one(
- self,
- collection,
- filter_json,
- replacement_json,
- upsert = False,
- session = None,
- raise_exception = False
- ):
-
- """
- To delete one document from a collection.
- :param collection: The collection from which you want to delete many records.
- :param filter_json: The filter criteria.
- :param replacement_json: The data to put in place of the existing document.
- :param upsert: If you want to insert if the document doesn't already exist.
- :param session: The session if you need to do this in a transaction.
- :param raise_exception: Whether, or not, you want to raise an exception when something fails.
- :return: True or False based on the success of the operation.
- """
-
- # Ensure you are connected:
- await self.ensure_connection()
-
- # Assume failure:
- success = False
-
- # try to query the data:
- try:
- result = await self.__db[collection].replace_one(
- filter_json,
- replacement_json,
- upsert = upsert,
- session = session
- )
- if result.modified_count or result.upserted_id: success = True
- except Exception as exception:
- self.__printer(exception)
-
- # Check results and return:
- if raise_exception and not success: raise MongoReplaceException(hint = f"{collection}")
- return success
-
- async def delete_one(
- self,
- collection,
- filter_json,
- session = None,
- raise_exception = False
- ):
-
- """
- To delete one document from a collection.
- :param collection: The collection from which you want to delete many records.
- :param filter_json: The filter criteria.
- :param session: The session if you need to do this in a transaction.
- :param raise_exception: Whether, or not, you want to raise an exception when something fails.
- :return: True or False based on the success of the operation.
- """
-
- # Ensure you are connected:
- await self.ensure_connection()
-
- # Assume failure:
- count = 0
-
- # try to query the data:
- try:
- result = await self.__db[collection].delete_one(filter_json, session = session)
- count = result.deleted_count
- except Exception as exception:
- self.__printer(exception)
-
- # Check results and return:
- if raise_exception and not count: raise MongoDeleteException(hint = f"{collection}")
- return count
-
- async def delete_many(
- self,
- collection,
- filter_json,
- session = None,
- raise_exception = False
- ):
-
- """
- To delete many documents from a collection.
- WARNING: sending {} in the filter would mean deleting ALL the documents.
- :param collection: The collection from which you want to delete many records.
- :param filter_json: The filter criteria.
- :param session: The session if you need to do this in a transaction.
- :param raise_exception: Whether, or not, you want to raise an exception when something fails.
- :return: True or False based on the success of the operation.
- """
-
- # Ensure you are connected:
- await self.ensure_connection()
-
- # Assume failure:
- count = 0
-
- # try to query the data:
- try:
- result = await self.__db[collection].delete_many(filter_json, session = session)
- count = result.deleted_count
- except Exception as exception:
- self.__printer(exception)
-
- # Check results and return:
- if raise_exception and not count: raise MongoDeleteException(hint = f"{collection}")
- return count
-
- async def aggregate(
- self,
- collection,
- pipeline,
- limit = None,
- raise_exception = False
- ):
-
- """
- Perform an advance query on the data.
- :param collection: The collection to perform the query on.
- :param pipeline: The pipeline of actions to take. Must be a list.
- :param limit: The max. no. of records to retrieve.
- :param raise_exception: Whether, or not, you want to raise an exception when something fails.
- :return: The array of matching records or null if there was an exception.
- """
-
- # Ensure you are connected:
- await self.ensure_connection()
-
- # Assume failure:
- results = None
-
- # try to perform the aggregation action:
- try: results = await self.__db[collection].aggregate(pipeline).to_list(limit)
- except Exception as exception: self.__printer(exception)
-
- # Check results and return:
- if raise_exception and not results: raise MongoException(hint = "aggregation")
- return results
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** MAIN PROGRAM ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-if __name__ == "__main__":
-
- pass
diff --git a/utils_v2/database/async_mongo_storage.py b/utils_v2/database/async_mongo_storage.py
deleted file mode 100644
index 94df2d8..0000000
--- a/utils_v2/database/async_mongo_storage.py
+++ /dev/null
@@ -1,358 +0,0 @@
-"""
-
- AUTHOR:
-
- Khushal P Soonderji
-
- DATE:
-
- Tuesday, 28th May, 2024
-
- OBJECTIVE:
-
- To have one central place from where all async database connectivity happens.
-
- REFERENCES:
-
- 01. https://motor.readthedocs.io/en/stable/
-
- DOWNLOADS:
-
- N/A
-
-"""
-
-# *****************************************************************************************************************
-# ***** ****
-# *** IMPORT ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# To make sibling directories accessible for imports:
-# ---
-import sys
-sys.path.append(".")
-sys.path.append("..")
-
-# For system-level activity:
-import io
-
-# For async behaviour:
-import asyncio
-
-# MongoDB for File Storage:
-from motor.motor_asyncio import AsyncIOMotorClient, AsyncIOMotorGridFSBucket
-from bson.objectid import ObjectId
-
-# For debugging:
-from icecream import IceCreamDebugger
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** MACROS / ONE-TIME INIT ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# --- Nothing Yet
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** VARIABLES ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# --- Nothing Yet
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** FUNCTIONS ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# --- Nothing Yet
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** CLASSES ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-class AsyncMongoStorage:
-
- __db = None
- __db_name = None
- __client = None
- __store = None
-
- def __init__(
- self,
- connection_string = None,
- max_connections = 5,
- host_name = "localhost",
- port = 27017,
- database_name = "fileStore",
- debug = True,
- debug_prefix = "GridFS (M) | ",
- debug_only_errors = True
- ):
-
- # Database Initialization:
- self.__host_name = host_name
- self.__port = port
- self.__db_name = database_name
- self.__connection_string = connection_string
- self.__max_connections = max_connections
-
- # Debugging:
- self.__debug_only_errors = debug_only_errors
- self.__printer = IceCreamDebugger(prefix = debug_prefix, includeContext = True)
- if not debug: self.__printer.disable()
-
- @staticmethod
- def generate_id():
-
- """
- Just generates an '_id' in MongoDB style.
- :return: The '_id' in MongoDB style.
- """
-
- return str(ObjectId())
-
- async def connect(self):
-
- """
- Initialize the database connection.
- :return: Nothing.
- """
-
- if self.__connection_string is None:
- self.__client = AsyncIOMotorClient(
- self.__host_name,
- self.__port,
- maxPoolSize = self.__max_connections,
- minPoolSize = self.__max_connections
- )
- else:
- self.__client = AsyncIOMotorClient(
- self.__connection_string,
- maxPoolSize = self.__max_connections,
- minPoolSize = self.__max_connections
- )
- self.__db = self.__client.get_database(self.__db_name)
- self.__store = AsyncIOMotorGridFSBucket(self.__db)
-
- @property
- def fs(self):
-
- """
- To access the features that have not been wrapped in this reportlab directly.
- This could include things like streaming files chunk-by-chunk.
- :return: The file-store instance.
- """
-
- return self.__store
-
- async def write_from_memory(self, file_name, file_data, metadata_json = None):
-
- """
- Save a file (from RAM) to Mongo. Suitable for smaller files (a few MBs max.).
- :param file_name: The name of the file.
- :param file_data: The data of the file (held in RAM).
- :param metadata_json: A JSON of metadata information that can later be used to search files (RECOMMENDED).
- :return: The file's id as a string (if it gets saved) or None.
- """
-
- if self.__store is None: await self.connect()
-
- file_data.seek(0)
- file_size = file_data.__sizeof__()
-
- file_id = None
- try: file_id = await self.__store.upload_from_stream(file_name, file_data, metadata = metadata_json)
- except Exception as exception: self.__printer(exception, file_name, file_size, file_id)
- if not self.__debug_only_errors: self.__printer(file_name, file_size, file_id)
- return str(file_id)
-
- async def read_to_memory(self, file_identifier, by_id = True):
-
- """
- To retrieve a file (in RAM) based on the provided identifier.
- Suitable for smaller files (a few MBs max.).
- :param file_identifier: Either the name or the "_id" of the file.
- :param by_id: Set to True if you are fetching by the "_id" of the file.
- :return: Either the file (in RAM) or None.
- """
-
- if self.__store is None: await self.connect()
-
- file_data = None
-
- try:
- if by_id: grid_out = await self.__store.open_download_stream(ObjectId(file_identifier))
- else: grid_out = await self.__store.open_download_stream_by_name(file_identifier)
- file_data = io.BytesIO(await grid_out.read())
- file_data.seek(0)
-
- except Exception as exception:
- file_data = None
- self.__printer(exception, file_identifier, by_id)
-
- if not self.__debug_only_errors: self.__printer(file_identifier, by_id)
- return file_data
-
- async def delete_file_by_id(self, file_id):
-
- """
- Tries to delete one file by the id.
- :param file_id: The id of the file in the database.
- :return: True or False based on the success of the operation.
- """
-
- if self.__store is None: await self.connect()
-
- deleted = False
-
- try:
- response = await self.__store.delete(file_id = ObjectId(file_id))
- deleted = True
- except Exception as exception:
- self.__printer(exception, file_id, deleted)
-
- return deleted
-
- def __format_metadata_json(self, metadata_json):
-
- """
- NOTE: ONLY USE WHEN SEARCHING FILES BY METADATA.
- MongoDB expects dot-notation while searching for files by the metadata. We are making a function to search
- files assuming that the conditions are to be applied to the metadata itself. So this function add the
- dot-notation to the right places to conduct a successful search.
- :param metadata_json: The JSON to format.
- :return: The formatted JSON that has the right dot-notation.
- """
-
- formatted_metadata_json = {}
-
- for key, value in metadata_json.items():
- if not key.startswith("$"): key = f"metadata.{key}"
- else:
- if type(value) is dict: value = self.__format_metadata_json(value)
- if type(value) is list: value = [self.__format_metadata_json(item) for item in value]
- formatted_metadata_json[key] = value
-
- return formatted_metadata_json
-
- async def find_file_by_metadata(self, metadata_json, limit = None, skip = None, sort = None):
-
- """
- This method only lists the files that match the criteria mentioned in the metadata JSON.
- :param metadata_json: The JSON that describes what you want to find.
- :param limit: Max. no. of records to retrieve.
- :param skip: No. of starting results to skip. Useful for pagination.
- :param sort: The sorting conditions to follow.
- :return: A list of (JSONs of) files that match the conditions. The list can be empty.
- """
-
- if self.__store is None: await self.connect()
-
- files_list = []
-
- try:
- limit = limit or 10
- skip = skip or 0
- sort = {"_id": -1} if not isinstance(sort, dict) else sort
- formatted_metadata_json = self.__format_metadata_json(metadata_json)
- return await self.__store.find(
- formatted_metadata_json
- ).sort(sort).skip(skip).limit(limit).to_list(None)
-
- except Exception as exception: self.__printer(exception, metadata_json, len(files_list))
-
- if not self.__debug_only_errors: self.__printer(metadata_json, len(files_list))
- return files_list
-
- async def find_file_by_id(self, file_id):
-
- """
- This method allows you to get the file's info from the id of the file.
- :param file_id: The id that was assigned by Mongo during upload.
- :return: The file's info or None if the file doesn't exist.
- """
-
- if self.__store is None: await self.connect()
-
- file_info = None
-
- try:
- formatted_metadata_json = {"_id": ObjectId(file_id)}
- file_info = (await self.__store.find(formatted_metadata_json).to_list(1))[0]
-
- except Exception as exception: self.__printer(exception, file_id, file_info)
-
- return file_info
-
- async def get_file_name(self, file_id):
-
- """
- Returns the file name if the id of the file is known.
- :param file_id: The id of the file as assigned by MongoDB when the file was stored.
- :return: The file's name (if it exists), or None.
- """
-
- # Ensure that we are connected:
- if self.__store is None: await self.connect()
-
- # Ensure that the input given is of 'ObjectId' type:
- if type(file_id) is not ObjectId: file_id = ObjectId(str(file_id))
-
- # Fetch and return the file name:
- files_list = await self.__store.find(
- {"_id": file_id},
- {"filename": True}
- ).sort({"_id": -1}).limit(1).to_list(None)
- try: file_name = files_list[0]["filename"]
- except: file_name = None
- return file_name
-
- async def aggregate(
- self,
- collection,
- pipeline,
- limit = None
- ):
-
- """
- Perform an advance query on the data.
- :param collection: The collection to perform the query on.
- :param pipeline: The pipeline of actions to take. Must be a list.
- :param limit: The max. no. of records to retrieve.
- :return: The array of matching records or null if there was an exception.
- """
-
- if self.__store is None: await self.connect()
-
- results = None
- try: results = await self.__db[collection].aggregate(pipeline).to_list(limit)
- except Exception as exception: self.__printer(exception)
- return results
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** MAIN PROGRAM ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-if __name__ == "__main__":
-
- pass
diff --git a/utils_v2/database/async_mongo_v2.py b/utils_v2/database/async_mongo_v2.py
deleted file mode 100644
index 663ed3a..0000000
--- a/utils_v2/database/async_mongo_v2.py
+++ /dev/null
@@ -1,1733 +0,0 @@
-"""
-
- AUTHOR:
-
- Khushal P Soonderji
-
- DATE:
-
- Original: Tuesday, 28th May, 2024
- Modified: Tuesday, 17th Sept., 2024
-
- OBJECTIVE:
-
- To have one central place from where all async MongoDB activity happens.
-
- REFERENCES:
-
- 01. https://motor.readthedocs.io/en/stable/
-
- DOWNLOADS:
-
- N/A
-
-"""
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** IMPORT ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# To make sibling directories accessible for imports:
-import sys
-sys.path.append(".")
-sys.path.append("..")
-
-# For system-level activity:
-import io
-
-# For async behaviour:
-import asyncio
-
-# MongoDB for File Storage:
-from motor.motor_asyncio import AsyncIOMotorClient, AsyncIOMotorGridFSBucket
-from bson.objectid import ObjectId
-from bson.json_util import dumps, loads
-
-# My utils:
-from utils_v2.string import json
-
-# For datetime handling:
-import pytz
-import datetime
-
-# For debugging:
-from icecream import IceCreamDebugger
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** MACROS / ONE-TIME INIT ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# --- Nothing Yet
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** VARIABLES ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# --- Nothing Yet
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** EXCEPTIONS ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# --- Nothing Yet
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** FUNCTIONS ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# --- Nothing Yet
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** CLASSES ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-class AsyncMongoBase:
-
- def __init__(
- self,
- connection_string = None,
- max_connections = 5,
- host_name = "localhost",
- port = 27017,
- database_name = "myDb",
- debug = True,
- debug_prefix = "Mongo | ",
- debug_only_errors = True
- ):
-
- # Basic variables that will be needed later:
- self._client = None
- self._db = None
- self._fs = None
-
- # Database Initialization:
- self._host_name = host_name
- self._port = port
- self._db_name = database_name
- self._connection_string = connection_string
- self._max_connections = max_connections
-
- # Debugging:
- self._debug_only_errors = debug_only_errors
- self._printer = IceCreamDebugger(prefix = debug_prefix, includeContext = True)
- if not debug: self._printer.disable()
-
- def enable_debug(self):
- self._printer.enable()
-
- def disable_debug(self):
- self._printer.disable()
-
- async def connect(self):
-
- """
- Initialize the database connection.
- :return: None.
- """
-
- # If a connection string is given,
- # give preference to it:
- if self._connection_string is None:
- self._client = AsyncIOMotorClient(
- self._host_name,
- self._port,
- maxPoolSize = self._max_connections,
- minPoolSize = self._max_connections
- )
-
- # In the absense of a connection string,
- # try to connect through the other credentials:
- else:
- self._client = AsyncIOMotorClient(
- self._connection_string,
- maxPoolSize = self._max_connections,
- minPoolSize = self._max_connections
- )
-
- # Debugging print:
- if not self._debug_only_errors:
- server_info = await self._client.server_info()
- self._printer(server_info)
-
- # Now we connect to the database and the file-store:
- self._db = self._client.get_database(self._db_name)
- self._fs = AsyncIOMotorGridFSBucket(self._db)
-
- async def ensure_connection(self):
-
- """
- Call this at the start of every function to automatically connect to the database when the user of the library
- forgets to explicitly connect to the database.
- :return: None.
- """
-
- if self._client is None: await self.connect()
-
- @staticmethod
- def generate_id(as_str = False):
-
- """
- Just generates an '_id' in MongoDB style.
- :param as_str: Set to True to convert the generated id to a string; and False to receive it as an instance of
- 'ObjectId'. This is useful when you need to pre-assign ids to files.
- :return: The '_id' in MongoDB style.
- """
-
- generated_id = ObjectId()
- return str(generated_id) if as_str else generated_id
-
- @property
- async def client(self):
-
- """
- returns the client to perform activities that have not been explicitly wrapped in the class.
- :return: The client's instance.
- """
-
- await self.ensure_connection()
- return self._client
-
- @property
- async def db(self):
-
- """
- returns the database connector to perform activities that have not been explicitly wrapped in the class.
- :return: The database's connection instance.
- """
-
- await self.ensure_connection()
- return self._fs
-
- @property
- async def fs(self):
-
- """
- returns the file-store to perform activities that have not been explicitly wrapped in the class.
- :return: The file-store instance.
- """
-
- await self.ensure_connection()
- return self._fs
-
- @staticmethod
- def from_json_string(json_data):
-
- """
- Converts from a JSON string to BSON.
- :param json_data: The JSON string to convert to BSON.
- :return: The BSON interpretation of the input JSON string.
- """
-
- return loads(json_data)
-
- @staticmethod
- def to_json_string(data, indent = 4, default = None):
-
- """
- Converts from a BSON to JSON string.
- :param data: The input BSON data.
- :param indent: The no. of spaces to put into the string for pretty print.
- :param default: The default function to apply to data that cannot be converted directly.
- :return: The JSON string from the input data.
- """
-
- return dumps(data, indent = indent, default = default)
-
- @staticmethod
- def dict_to_dot_notation(input_dict, pk = "", s = "."):
-
- """
- Converts an input dict to dot notation format. Can be used as a utility to perform searches.
- :param input_dict: The dict that you want to convert to dot notation.
- :param pk: Parent Key. DO NOT TOUCH (meant to be used during recursion).
- :param s: Separator. DO NOT TOUCH (meant to be used during recursion).
- :return: The dot notation representation of the input dict.
- """
-
- items = []
- for k, v in input_dict.items():
- new_key = f"{pk}{s}{k}" if pk else k
- if isinstance(v, dict) and v: items.extend(AsyncMongoBase.dict_to_dot_notation(v, new_key, s = s).items())
- else: items.append((new_key, v))
- return dict(items)
-
- @staticmethod
- def normalize_date_time(document):
-
- """
- MongoDB doesn't support timezones. A good strategy would be to convert everything to UTC format and store it.
- This method does exactly that. Any datetime object is converted to UTC timezone. If the datetime object was
- timezone naive, UTC timezone will be applied to it without changing the time value.
- :param document: The document that you want to normalize the date-time in.
- :return: The document with normalized datetime.
- """
-
- if isinstance(document, datetime.datetime):
- utc_tz = pytz.timezone("UTC")
- if document.tzinfo is None: document = utc_tz.localize(document)
- else: document = document.astimezone(utc_tz)
-
- if type(document) is list:
- document = [AsyncMongoBase.normalize_date_time(item) for item in document]
-
- if type(document) is dict:
- document = {
- AsyncMongoBase.normalize_date_time(k): AsyncMongoBase.normalize_date_time(v)
- for k, v in document.items()
- }
-
- return document
-
- @staticmethod
- def read_to_ram(file_path):
-
- """
- Reads a file into a BytesIO object in RAM.
- :param file_path: The path to the file on disk.
- :return: The file in a BytesIO object.
- """
-
- with open(file_path, "rb") as file: file_data = file.read()
- file_in_ram = io.BytesIO(file_data)
- file_in_ram.seek(0)
- return file_in_ram
-
-
-# ---------------------------------------------------------------------------------------------------------------------
-
-
-class AsyncMongo(AsyncMongoBase):
-
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
-
- async def list_indexes(
- self,
- collection,
- session = None,
- raise_exception = False
- ):
-
- """
- Lists out the indexes of a collection.
- :param collection: The collection whose indexes you want to list out,
- :param session: The session if you need to do this in a transaction.
- :param raise_exception: Whether, or not, you want to raise an exception when something fails.
- :return: The list of indexes or None if the action fails.
- """
-
- # Ensure you are connected:
- await self.ensure_connection()
-
- # Assume failure:
- indexes = None
-
- # Try to list the indexes:
- try:
- responses = await self._db[collection].list_indexes(session = session).to_list(None)
- indexes = [{key: value for key, value in response.items()} for response in responses]
-
- # When something goes wrong:
- except Exception as exception:
- self._printer(exception)
- if raise_exception: raise
-
- # Done here:
- return indexes
-
- async def create_index(
- self,
- collection,
- keys,
- options = None,
- session = None,
- raise_exception = False
- ):
-
- """
- Creates an index on a collection.
- :param collection: The collection to create the index on.
- :param keys: The keys (and sorting) to implement the index on.
- :param options: Additional config.
- :param session: The session if you need to do this in a transaction.
- :param raise_exception: Whether, or not, you want to raise an exception when something fails.
- :return: True or False based on the success of the execution.
- """
-
- # Ensure you are connected:
- await self.ensure_connection()
-
- # Assume failure:
- success = False
-
- # Try to make the insertion:
- try:
- options = options or {}
- keys = [(k, v) for k, v in keys.items()]
- response = await self._db[collection].create_index(keys, session = session, **options)
- if response: success = True
-
- # When something goes wrong:
- except Exception as exception:
- self._printer(exception)
- if raise_exception: raise
-
- # Done here:
- return success
-
- async def count(
- self,
- collection,
- filter,
- session = None,
- raise_exception = False
- ):
-
- """
- Counts the no. of documents that match the given filter condition.
- :param collection: The name of the collection to count in.
- :param filter: The filter criteria that the documents must satisfy to be counted.
- :param session: The session if you need to do this in a transaction.
- :param raise_exception: Whether, or not, you want to raise an exception when something fails.
- :return: The count if the operation was performed successfully or None is something went wrong and the exception
- was suppressed.
- """
-
- # Ensure you are connected:
- await self.ensure_connection()
-
- # Assume failure:
- count = None
-
- # try to query the data:
- try: count = await self._db[collection].count_documents(filter, session = session)
- except Exception as exception:
- self._printer(exception)
- if raise_exception: raise
-
- # Done here:
- return count
-
- async def insert_one(
- self,
- collection,
- document,
- session = None,
- raise_exception = False
- ):
-
- """
- Insert data into a collection.
- :param collection: The collection you want to feed the data into.
- :param document: The data to be stored.
- :param session: The session if you need to do this in a transaction.
- :param raise_exception: Whether, or not, you want to raise an exception when something fails.
- :return: The id of the inserted data, or null if the action fails.
- """
-
- # Ensure you are connected:
- await self.ensure_connection()
-
- # Assume failure:
- inserted_id = None
-
- # Try to make the insertion:
- try:
- response = await self._db[collection].insert_one(document.copy(), session = session)
- inserted_id = response.inserted_id
-
- # When something goes wrong:
- except Exception as exception:
- self._printer(exception)
- if raise_exception: raise
-
- # Done here:
- return inserted_id
-
- async def insert_many(
- self,
- collection,
- documents,
- session = None,
- raise_exception = False
- ):
-
- """
- Insert a lot of data into a collection.
- :param collection: The collection you want to feed the data into.
- :param documents: The list of documents to be stored.
- :param session: The session if you need to do this in a transaction.
- :param raise_exception: Whether, or not, you want to raise an exception when something fails.
- :return: The id of the inserted data, or null if the action fails.
- """
-
- # Ensure you are connected:
- await self.ensure_connection()
-
- # Assume failure:
- inserted_ids = []
-
- # Try to make the insertion:
- try:
- response = await self._db[collection].insert_many(documents, session = session)
- inserted_ids = response.inserted_ids
-
- # When something goes wrong:
- except Exception as exception:
- self._printer(exception)
- if raise_exception: raise
-
- # Done here:
- return inserted_ids
-
- async def find_one(
- self,
- collection,
- filter,
- projection = None,
- session = None,
- raise_exception = False
- ):
-
- """
- Finds one record that matches the given conditions.
- :param collection: The name of the collection to perform the search in.
- :param filter: The filter criteria.
- :param projection: What parts of the matching data you want to fetch.
- :param session: The session if you need to do this in a transaction.
- :param raise_exception: Whether, or not, you want to raise an exception when something fails.
- :return: The matching record or null if there was an exception.
- """
-
- # Ensure you are connected:
- await self.ensure_connection()
-
- # Assume failure:
- results = None
-
- # try to query the data:
- try: results = await self._db[collection].find_one(filter, projection, session = session)
- # When something goes wrong:
- except Exception as exception:
- self._printer(exception)
- if raise_exception: raise
-
- # Check results and return:
- return results
-
- async def find_many(
- self,
- collection,
- filter,
- projection = None,
- skip = 0,
- limit = None,
- sort = None,
- session = None,
- raise_exception = False
- ):
-
- """
- Finds one or more records that match the given conditions.
- :param collection: The name of the collection to perform the search in.
- :param filter: The filter criteria.
- :param projection: What parts of the matching data you want to fetch.
- :param skip: The no. of records to skip before picking next ones. Needed for pagination.
- :param limit: The max. no. of records you want to fetch.
- :param sort: The sorting rules to apply.
- :param session: The session if you need to do this in a transaction.
- :param raise_exception: Whether, or not, you want to raise an exception when something fails.
- :return: The array of matching records or null if there was an exception.
- """
-
- # Ensure you are connected:
- await self.ensure_connection()
-
- # Assume failure:
- results = None
-
- # Assume defaults:
- if sort is None: sort = {"_id": -1}
- if limit is None: limit = 10
-
- # try to query the data:
- try:
- results = await self._db[collection].find(
- filter,
- projection,
- sort = sort,
- skip = skip,
- limit = limit,
- session = session
- ).to_list(None)
-
- # When something goes wrong:
- except Exception as exception:
- self._printer(exception)
- if raise_exception: raise
-
- # Done here:
- return results
-
- async def update_one(
- self,
- collection,
- filter = None,
- update = None,
- upsert = False,
- session = None,
- raise_exception = False
- ):
-
- """
- Update one document.
- :param collection: The collection you want to update.
- :param filter: The selection criteria to locate the document to update.
- :param update: The values you want to update.
- :param upsert: If you want to insert if the document doesn't already exist.
- :param session: The session if you need to do this in a transaction.
- :param raise_exception: Whether, or not, you want to raise an exception when something fails.
- :return: True or False based on the success of the operation.
- """
-
- # Ensure you are connected:
- await self.ensure_connection()
-
- # Assume failure:
- success = False
-
- # Try to make the update:
- try:
- response = await self._db[collection].update_one(
- filter,
- update,
- upsert = upsert,
- session = session
- )
- success = False if response.modified_count == 0 else True
-
- # When something goes wrong:
- except Exception as exception:
- self._printer(exception)
- if raise_exception: raise
-
- # Done here:
- return success
-
- async def update_many(
- self,
- collection,
- filter = None,
- update = None,
- upsert = False,
- session = None,
- raise_exception = False
- ):
-
- """
- Update many documents.
- :param collection: The collection you want to update.
- :param filter: The selection criteria to locate the document to update.
- :param update: The values you want to update.
- :param upsert: If you want to insert if the document doesn't already exist.
- :param session: The session if you need to do this in a transaction.
- :param raise_exception: Whether, or not, you want to raise an exception when something fails.
- :return: True or False based on the success of the operation.
- """
-
- # Ensure you are connected:
- await self.ensure_connection()
-
- # Assume failure:
- update_count = 0
-
- # Try to make the insertion:
- try:
- response = await self._db[collection].update_many(
- filter,
- update,
- upsert = upsert,
- session = session
- )
- update_count = response.modified_count
-
- # When something goes wrong:
- except Exception as exception:
- self._printer(exception)
- if raise_exception: raise
-
- # Done here:
- return update_count
-
- async def find_one_and_update(
- self,
- collection,
- filter = None,
- update = None,
- projection = None,
- return_updated = False,
- upsert = False,
- session = None,
- raise_exception = False
- ):
-
- """
- Update one document.
- :param collection: The collection you want to update.
- :param filter: The selection criteria to locate the document to update.
- :param update: The values you want to update.
- :param projection: What parts of the matching data you want to fetch.
- :param return_updated: To choose whether you want to retrieve the original document or the updated document.
- :param upsert: If you want to insert if the document doesn't already exist.
- :param session: The session if you need to do this in a transaction.
- :param raise_exception: Whether, or not, you want to raise an exception when something fails.
- :return: True or False based on the success of the operation.
- """
-
- # Ensure you are connected:
- await self.ensure_connection()
-
- # Assume failure:
- document = None
-
- # Try to make the update:
- try:
- document = await self._db[collection].find_one_and_update(
- filter,
- update,
- projection = projection,
- return_document = return_updated,
- upsert = upsert,
- session = session
- )
-
- # When something goes wrong:
- except Exception as exception:
- self._printer(exception)
- if raise_exception: raise
-
- # Done here:
- return document
-
- async def replace_one(
- self,
- collection,
- filter,
- replacement,
- upsert = False,
- session = None,
- raise_exception = False
- ):
-
- """
- To delete one document from a collection.
- :param collection: The collection from which you want to delete many records.
- :param filter: The filter criteria.
- :param replacement: The data to put in place of the existing document.
- :param upsert: If you want to insert if the document doesn't already exist.
- :param session: The session if you need to do this in a transaction.
- :param raise_exception: Whether, or not, you want to raise an exception when something fails.
- :return: True or False based on the success of the operation.
- """
-
- # Ensure you are connected:
- await self.ensure_connection()
-
- # Assume failure:
- success = False
-
- # try to query the data:
- try:
- result = await self._db[collection].replace_one(
- filter,
- replacement,
- upsert = upsert,
- session = session
- )
- if result.modified_count or result.upserted_id: success = True
-
- # When something goes wrong:
- except Exception as exception:
- self._printer(exception)
- if raise_exception: raise
-
- # Done here:
- return success
-
- async def find_one_and_replace(
- self,
- collection,
- filter,
- replacement,
- projection = None,
- return_replaced = False,
- upsert = False,
- session = None,
- raise_exception = False
- ):
-
- """
- To delete one document from a collection.
- :param collection: The collection from which you want to delete many records.
- :param filter: The filter criteria.
- :param replacement: The data to put in place of the existing document.
- :param projection: What parts of the matching data you want to fetch.
- :param return_replaced: To choose whether you want to retrieve the original document or the updated document.
- :param upsert: If you want to insert if the document doesn't already exist.
- :param session: The session if you need to do this in a transaction.
- :param raise_exception: Whether, or not, you want to raise an exception when something fails.
- :return: True or False based on the success of the operation.
- """
-
- # Ensure you are connected:
- await self.ensure_connection()
-
- # Assume failure:
- document = None
-
- # try to query the data:
- try:
- document = await self._db[collection].find_one_and_replace(
- filter,
- replacement,
- projection = projection,
- return_document = return_replaced,
- upsert = upsert,
- session = session
- )
-
- # When something goes wrong:
- except Exception as exception:
- self._printer(exception)
- if raise_exception: raise
-
- # Done here:
- return document
-
- async def delete_one(
- self,
- collection,
- filter,
- session = None,
- raise_exception = False
- ):
-
- """
- To delete one document from a collection.
- :param collection: The collection from which you want to delete many records.
- :param filter: The filter criteria.
- :param session: The session if you need to do this in a transaction.
- :param raise_exception: Whether, or not, you want to raise an exception when something fails.
- :return: True or False based on the success of the operation.
- """
-
- # Ensure you are connected:
- await self.ensure_connection()
-
- # Assume failure:
- success = False
-
- # try to query the data:
- try:
- result = await self._db[collection].delete_one(filter, session = session)
- success = True if result.deleted_count else False
-
- # When something goes wrong:
- except Exception as exception:
- self._printer(exception)
- if raise_exception: raise
-
- # Done here:
- return success
-
- async def delete_many(
- self,
- collection,
- filter,
- session = None,
- raise_exception = False
- ):
-
- """
- To delete many documents from a collection.
- WARNING: sending {} in the filter would mean deleting ALL the documents.
- :param collection: The collection from which you want to delete many records.
- :param filter: The filter criteria.
- :param session: The session if you need to do this in a transaction.
- :param raise_exception: Whether, or not, you want to raise an exception when something fails.
- :return: True or False based on the success of the operation.
- """
-
- # Ensure you are connected:
- await self.ensure_connection()
-
- # Assume failure:
- count = 0
-
- # try to query the data:
- try:
- result = await self._db[collection].delete_many(filter, session = session)
- count = result.deleted_count
-
- # When something goes wrong:
- except Exception as exception:
- self._printer(exception)
- if raise_exception: raise
-
- # Done here:
- return count
-
- async def find_one_and_delete(
- self,
- collection,
- filter,
- projection = None,
- session = None,
- raise_exception = False
- ):
-
- """
- To delete one document from a collection.
- :param collection: The collection from which you want to delete many records.
- :param filter: The filter criteria.
- :param projection: What parts of the matching data you want to fetch.
- :param session: The session if you need to do this in a transaction.
- :param raise_exception: Whether, or not, you want to raise an exception when something fails.
- :return: True or False based on the success of the operation.
- """
-
- # Ensure you are connected:
- await self.ensure_connection()
-
- # Assume failure:
- document = None
-
- # try to query the data:
- try:
- document = await self._db[collection].find_one_and_delete(
- filter,
- projection = projection,
- session = session
- )
-
- # When something goes wrong:
- except Exception as exception:
- self._printer(exception)
- if raise_exception: raise
-
- # Done here:
- return document
-
- async def bulk_write(
- self,
- collection,
- requests,
- session = None,
- raise_exception = False
- ):
-
- """
- To perform various individual operations in one go. You will have to import individual actions like "UpdateOne"
- and "InsertMany" from PyMongo and pass them as an array of requests (operations) to this method.
- :param collection: The collection you want to run the requests on.
- :param requests: The array of requests (operations) to be performed.
- :param session: The session if you need to do this in a transaction.
- :param raise_exception: Whether, or not, you want to raise an exception when something fails.
- :return:
- """
-
- # Ensure you are connected:
- await self.ensure_connection()
-
- # Assume failure:
- count = 0
-
- # try to query the data:
- try:
- response = await self._db[collection].bulk_write(requests, session = session)
- count = response.modified_count + response.inserted_count + response.upserted_count + response.deleted_count
-
- # When something goes wrong:
- except Exception as exception:
- self._printer(exception)
- if raise_exception: raise
-
- # Done here:
- return count
-
- async def aggregate(
- self,
- collection,
- pipeline,
- limit = None,
- session = None,
- raise_exception = False
- ):
-
- """
- Perform an advance query on the data.
- :param collection: The collection to perform the query on.
- :param pipeline: The pipeline of actions to take. Must be a list.
- :param limit: The max. no. of records to retrieve. preferably apply the limit from within the pipeline.
- :param session: The session if you need to do this in a transaction.
- :param raise_exception: Whether, or not, you want to raise an exception when something fails.
- :return: The array of matching records or null if there was an exception.
- """
-
- # Ensure you are connected:
- await self.ensure_connection()
-
- # Assume failure:
- results = None
-
- # try to perform the aggregation action:
- try: results = await self._db[collection].aggregate(pipeline, session = session).to_list(limit)
- except Exception as exception:
- self._printer(exception)
- if raise_exception: raise
-
- # Done here:
- return results
-
-
-# ---------------------------------------------------------------------------------------------------------------------
-
-
-class AsyncMongoStorage(AsyncMongoBase):
-
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
-
- async def easy_upload(
- self,
- source,
- file_name: str,
- file_metadata: dict = None,
- file_id = None,
- chunk_size: int = None,
- session = None,
- raise_exception = False
- ):
-
- """
- Easily write one file to MongoDB's GridFS. Ideal for directly uploading small files without having to worry
- about any internal mechanisms.
- :param source: The actual file, supplied as either a path string or a file-like object, to be written to the
- database.
- :param file_name: The name of the file as it will be stored on (and retrieved from) GridFS.
- :param file_metadata: Any metadata to later search the file by.
- :param file_id: Any custom id to be given to the file. TRY STICKING TO THE ID GENERATED BY 'generate_id'.
- :param chunk_size: The chunk size (in bytes) to use for storing this file.
- :param session: The session if you need to do this in a transaction.
- :param raise_exception: Whether, or not, you want to raise an exception when something fails.
- :return: The id of the inserted file or None if the upload failed.
- """
-
- # Ensure you are connected:
- await self.ensure_connection()
-
- # Assume failure:
- inserted_id = None
-
- # Try to write the file:
- try:
-
- # Input pre-processing:
- if isinstance(source, io.BytesIO): source.seek(0)
- elif isinstance(source, str): source = open(source, mode = "rb")
-
- # If no file id was supplied:
- if file_id is None:
- inserted_id = await self._fs.upload_from_stream(
- filename = file_name,
- source = source,
- metadata = file_metadata,
- chunk_size_bytes = chunk_size,
- session = session
- )
-
- # If a file id was supplied:
- else:
- await self._fs.upload_from_stream_with_id(
- file_id = file_id,
- filename = file_name,
- source = source,
- metadata = file_metadata,
- chunk_size_bytes = chunk_size,
- session = session
- )
- inserted_id = file_id
-
- # When something goes wrong:
- except Exception as exception:
- self._printer(exception)
- if raise_exception: raise
-
- # Done here:
- return inserted_id
-
- async def easy_download(
- self,
- destination,
- file_id = None,
- file_name = None,
- session = None,
- raise_exception = False
- ):
-
- """
- Easily read one file from MongoDB's GridFS. Ideal for directly downloading small files without having to worry
- about any internal mechanisms.
- :param destination: The path on the local disk or a buffer in RAM to save the downloaded data to.
- :param file_id: (RECOMMENDED) the id of the save file.
- :param file_name: The name of the saved file. NOT RECOMMENDED because you could have many files with the same
- name. The best way to tell files apart if from the id.
- :param session: The session if you need to do this in a transaction.
- :param raise_exception: Whether, or not, you want to raise an exception when something fails.
- :return: True or False based on the success of the operation. The contents of the stored file are written
- directly to the destination.
- """
-
- # Ensure you are connected:
- await self.ensure_connection()
-
- # Assume failure:
- success = False
-
- # Try to read the file:
- try:
-
- # Input pre-processing:
- if isinstance(destination, str): destination = open(destination, mode = "wb")
-
- # If a file id is supplied (preferred way):
- if file_id is not None:
- await self._fs.download_to_stream(
- destination = destination,
- file_id = file_id,
- session = session
- )
- if isinstance(destination, io.BytesIO): destination.seek(0)
- success = True
-
- # If a file name is supplied:
- elif file_name is not None:
- await self._fs.download_to_stream_by_name(
- destination = destination,
- filename = file_name,
- session = session
- )
- if isinstance(destination, io.BytesIO): destination.seek(0)
- success = True
-
- # When something goes wrong:
- except Exception as exception:
- self._printer(exception)
- if raise_exception: raise
-
- # Done here:
- return success
-
- async def count(
- self,
- filter,
- session = None,
- raise_exception = False
- ):
-
- """
- Counts the no. of documents that match the given filter condition.
- :param filter: The filter criteria that the documents must satisfy to be counted.
- :param session: The session if you need to do this in a transaction.
- :param raise_exception: Whether, or not, you want to raise an exception when something fails.
- :return: The count if the operation was performed successfully or None is something went wrong and the exception
- was suppressed.
- """
-
- # Ensure you are connected:
- await self.ensure_connection()
-
- # Assume failure:
- count = None
-
- # try to query the data:
- try: count = await self._db["fs.files"].count_documents(filter, session = session)
- except Exception as exception:
- self._printer(exception)
- if raise_exception: raise
-
- # Done here:
- return count
-
- async def find_one(
- self,
- filter,
- projection = None,
- session = None,
- raise_exception = False
- ):
-
- """
- Finds one record that matches the given conditions. This does NOT return the file itself, it returns the record
- that describes the file.
- :param filter: The filter criteria.
- :param projection: What parts of the matching data you want to fetch.
- :param session: The session if you need to do this in a transaction.
- :param raise_exception: Whether, or not, you want to raise an exception when something fails.
- :return: The matching record or null if there was an exception.
- """
-
- # Ensure you are connected:
- await self.ensure_connection()
-
- # Assume failure:
- results = None
-
- # try to query the data:
- try: results = await self._db["fs.files"].find_one(filter, projection, session = session)
-
- # When something goes wrong:
- except Exception as exception:
- self._printer(exception)
- if raise_exception: raise
-
- # Check results and return:
- return results
-
- async def find_many(
- self,
- filter,
- projection = None,
- skip = 0,
- limit = None,
- sort = None,
- session = None,
- raise_exception = False
- ):
-
- """
- Finds one or more records that match the given conditions. This doesn't return any actual files directly, it
- returns the records that describe the files.
- :param filter: The filter criteria.
- :param projection: What parts of the matching data you want to fetch.
- :param skip: The no. of records to skip before picking next ones. Needed for pagination.
- :param limit: The max. no. of records you want to fetch.
- :param sort: The sorting rules to apply.
- :param session: The session if you need to do this in a transaction.
- :param raise_exception: Whether, or not, you want to raise an exception when something fails.
- :return: The array of matching records or null if there was an exception.
- """
-
- # Ensure you are connected:
- await self.ensure_connection()
-
- # Assume failure:
- results = None
-
- # Assume defaults:
- if sort is None: sort = {"_id": -1}
- if limit is None: limit = 10
-
- # try to query the data:
- try:
- results = await self._db["fs.files"].find(
- filter,
- projection,
- session = session
- ).sort(sort).skip(skip).limit(limit).to_list(None)
-
- # When something goes wrong:
- except Exception as exception:
- self._printer(exception)
- if raise_exception: raise
-
- # Done here:
- return results
-
- async def update_metadata_for_one(
- self,
- filter: dict,
- unset_data: dict = None,
- set_data: dict = None,
- session = None,
- raise_exception = False
- ):
-
- """
- Updates the metadata for one file.
- :param filter: The conditions to filter the files by.
- :param unset_data: The fields that you want to discard.
- :param set_data: The fields that you want to add or update.
- :param session: The session if you need to do this in a transaction.
- :param raise_exception: Whether, or not, you want to raise an exception when something fails.
- :return: True or False based on the success of the operation.
- """
-
- # Ensure you are connected:
- await self.ensure_connection()
-
- # Assume failure:
- success = False
-
- # Try to make the update:
- try:
-
- # Input pre-processing:
- update = {}
- if unset_data is not None: update["$unset"] = self.dict_to_dot_notation({"metadata": unset_data})
- if set_data is not None: update["$set"] = self.dict_to_dot_notation({"metadata": set_data})
-
- # Actual update happens here:
- if update:
- response = await self._db["fs.files"].update_one(
- filter,
- update,
- session = session
- )
- success = False if response.modified_count == 0 else True
-
- # When something goes wrong:
- except Exception as exception:
- self._printer(exception)
- if raise_exception: raise
-
- # Done here:
- return success
-
- async def update_metadata_for_many(
- self,
- filter: dict,
- unset_data: dict = None,
- set_data: dict = None,
- session = None,
- raise_exception = False
- ):
-
- """
- Updates the metadata for many files. Practically the same as 'update_metadata_for_one' except that the scope of
- the modifications is far wider.
- :param filter: The conditions to filter the files by.
- :param unset_data: The fields that you want to discard.
- :param set_data: The fields that you want to add or update.
- :param session: The session if you need to do this in a transaction.
- :param raise_exception: Whether, or not, you want to raise an exception when something fails.
- :return: True or False based on the success of the operation.
- """
-
- # Ensure you are connected:
- await self.ensure_connection()
-
- # Assume failure:
- count = None
-
- # Try to make the update:
- try:
-
- # Input pre-processing:
- update = {}
- if unset_data is not None: update["$unset"] = self.dict_to_dot_notation({"metadata": unset_data})
- if set_data is not None: update["$set"] = self.dict_to_dot_notation({"metadata": set_data})
-
- # Actual update happens here:
- if update:
- response = await self._db["fs.files"].update_many(
- filter,
- update,
- session = session
- )
- count = response.modified_count
-
- # When something goes wrong:
- except Exception as exception:
- self._printer(exception)
- if raise_exception: raise
-
- # Done here:
- return count
-
- async def replace_metadata_for_one(
- self,
- filter: dict,
- replacement,
- session = None,
- raise_exception = False
- ):
-
- """
- Updates the metadata for one file.
- :param filter: The conditions to filter the files by.
- :param replacement: The new metadata to put inplace of the old one.
- :param session: The session if you need to do this in a transaction.
- :param raise_exception: Whether, or not, you want to raise an exception when something fails.
- :return: True or False based on the success of the operation.
- """
-
- # Ensure you are connected:
- await self.ensure_connection()
-
- # Assume failure:
- success = False
-
- # Try to make the update:
- try:
-
- # Actual update happens here:
- result = await self._db["fs.files"].update_one(
- filter,
- {"$set": {"metadata": replacement}},
- upsert = False,
- session = session
- )
- if result.modified_count or result.upserted_id: success = True
-
- # When something goes wrong:
- except Exception as exception:
- self._printer(exception)
- if raise_exception: raise
-
- # Done here:
- return success
-
- async def replace_metadata_for_many(
- self,
- filter: dict,
- replacement,
- session = None,
- raise_exception = False
- ):
-
- """
- Updates the metadata for one file.
- :param filter: The conditions to filter the files by.
- :param replacement: The new metadata to put inplace of the old one.
- :param session: The session if you need to do this in a transaction.
- :param raise_exception: Whether, or not, you want to raise an exception when something fails.
- :return: True or False based on the success of the operation.
- """
-
- # Ensure you are connected:
- await self.ensure_connection()
-
- # Assume failure:
- count = None
-
- # Try to make the update:
- try:
-
- # Actual update happens here:
- response = await self._db["fs.files"].update_many(
- filter,
- {"$set": {"metadata": replacement}},
- session = session
- )
- count = response.modified_count
-
- # When something goes wrong:
- except Exception as exception:
- self._printer(exception)
- if raise_exception: raise
-
- # Done here:
- return count
-
- async def delete_by_id(
- self,
- file_id,
- session = None,
- raise_exception = False
- ):
-
- """
- Deletes one file by the file's id. Deleting files is different from deleting simple documents because, in files,
- you also need to clear out the chunks (which are stored in a separate collection). The built-in mechanism of
- Motor only provides support to delete by the file's id.
- :param file_id: The id of the file that you want to delete.
- :param session: The session if you need to do this in a transaction.
- :param raise_exception: Whether, or not, you want to raise an exception when something fails.
- :return: True or False based on the success of the operation.
- """
-
- # Ensure you are connected:
- await self.ensure_connection()
-
- # Assume failure:
- success = False
-
- # Try to read the file:
- try:
-
- # Delete the file:
- await self._fs.delete(file_id = file_id, session = session)
- success = True
-
- # When something goes wrong:
- except Exception as exception:
- self._printer(exception)
- if raise_exception: raise
-
- # Done here:
- return success
-
- async def delete_one(
- self,
- filter,
- session = None,
- raise_exception = False
- ):
-
- """
- To delete one document from a collection.
- :param filter: The filter criteria.
- :param session: The session if you need to do this in a transaction.
- :param raise_exception: Whether, or not, you want to raise an exception when something fails.
- :return: True or False based on the success of the operation.
- """
-
- # Ensure you are connected:
- await self.ensure_connection()
-
- # Assume failure:
- success = False
-
- # Find one file that matches the given filter:
- file = await self.find_one(
- filter = filter,
- session = session,
- raise_exception = raise_exception
- )
-
- # If we have a list of files to work with:
- if file: success = await self.delete_by_id(file["_id"])
-
- # Done here:
- return success
-
- async def delete_many(
- self,
- filter,
- session = None,
- raise_exception = False
- ):
-
- """
- To delete many documents from a collection.
- WARNING: sending {} in the filter would mean deleting ALL the documents.
- :param filter: The filter criteria.
- :param session: The session if you need to do this in a transaction.
- :param raise_exception: Whether, or not, you want to raise an exception when something fails.
- :return: The number of files deleted (can be zero) or None if something failed while searching the files.
- """
-
- # Ensure you are connected:
- await self.ensure_connection()
-
- # Assume failure:
- count = 0
-
- # Find all the files that match the given filter:
- files = await self.find_many(
- filter = filter,
- session = session,
- raise_exception = raise_exception
- )
-
- # If we have a list of files to work with:
- if files is not None:
- tasks = [self.delete_by_id(file["_id"]) for file in files]
- results = await asyncio.gather(*tasks)
- count = sum(results)
-
- # Done here:
- return count
-
- async def get_upload_stream(
- self,
- file_name: str,
- file_metadata: dict = None,
- file_id = None,
- chunk_size: int = None,
- session = None,
- raise_exception = False
- ):
-
- """
- Returns a GridIn object so that you can perform your own upload using the built-in writing methods. You must use
- the 'write' method to write data to the file by passing it either a string of bytes or a file-like object. When
- the file has been fully written, you must call the 'close' method to finish the operation. In case you need to
- cancel the operation, you can call the 'abort' method to delete all the already written data and stop uploading
- new data.
- :param file_name: The name of the file as it will be stored on (and retrieved from) GridFS.
- :param file_metadata: Any metadata to later search the file by.
- :param file_id: Any custom id to be given to the file. TRY STICKING TO THE ID GENERATED BY 'generate_id'.
- :param chunk_size: The chunk size (in bytes) to use for storing this file.
- :param session: The session if you need to do this in a transaction.
- :param raise_exception: Whether, or not, you want to raise an exception when something fails.
- :return: The upload stream that implements the 'write', 'close', and 'abort' methods, or None if something
- failed.
- """
-
- # Ensure you are connected:
- await self.ensure_connection()
-
- # Assume failure:
- stream = None
-
- # Try to open the upload stream:
- try:
-
- # If no file id was supplied:
- if file_id is None:
- stream = await self._fs.open_upload_stream(
- filename = file_name,
- metadata = file_metadata,
- chunk_size_bytes = chunk_size,
- session = session
- )
-
- # If a file id was supplied:
- else:
- stream = await self._fs.open_upload_stream_with_id(
- file_id = file_id,
- filename = file_name,
- metadata = file_metadata,
- chunk_size_bytes = chunk_size,
- session = session
- )
-
- # When something goes wrong:
- except Exception as exception:
- self._printer(exception)
- if raise_exception: raise
-
- # Done here:
- return stream
-
- async def get_download_stream(
- self,
- file_id = None,
- file_name = None,
- session = None,
- raise_exception = False
- ):
-
- """
- Returns a GridOut object so that you can implement your own download logic using the built-in 'read' method.
- Once the reading is done, use the 'close' method to release the resources used by the stream.
- :param file_id: (RECOMMENDED) the id of the save file.
- :param file_name: The name of the saved file. NOT RECOMMENDED because you could have many files with the same
- name. The best way to tell files apart if from the id.
- :param session: The session if you need to do this in a transaction.
- :param raise_exception: Whether, or not, you want to raise an exception when something fails.
- :return: The download stream that implements the 'read' and 'close' methods, or None if something failed.
- """
-
- # Ensure you are connected:
- await self.ensure_connection()
-
- # Assume failure:
- stream = None
-
- # Try to open a download stream:
- try:
-
- # If a file id is supplied (preferred way):
- if file_id is not None:
- stream = await self._fs.open_download_stream(
- file_id = file_id,
- session = session
- )
-
- # If a file name is supplied:
- elif file_name is not None:
- stream = await self._fs.open_download_stream_by_name(
- filename = file_name,
- session = session
- )
-
- # When something goes wrong:
- except Exception as exception:
- self._printer(exception)
- if raise_exception: raise
-
- # Done here:
- return stream
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** MAIN PROGRAM ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-if __name__ == "__main__":
-
- from shared import constants
-
- async def main():
-
- # Create an instance of the database connector:
- my_db = AsyncMongo(
- connection_string = constants.MONGO_DATA_CONNECTION_STRING,
- database_name = constants.MONGO_DATA_DATABASE_NAME,
- max_connections = 10,
- debug = True
- )
-
- # Connect to the database:
- await my_db.connect()
-
- # # Get the documents to migrate:
- # documents = await my_db.find_many(
- # collection = "scriptData",
- # filter = {},
- # limit = 50,
- # projection = {"_id": False}
- # )
- # # print(json.to_string(documents, default = str))
- #
- # # Adjust them:
- # adjusted_documents = []
- # for document in documents:
- # script_id = document.pop("scriptId")
- # adjusted_document = {
- # "scriptId": script_id,
- # "desc": "no desc",
- # "content": document
- # }
- # adjusted_documents.append(adjusted_document)
- # print(json.to_string(adjusted_documents, default = str))
- #
- # # Insert the adjusted ones to the new collection:
- # response = await my_db.insert_many(
- # collection = "_scriptData",
- # documents = adjusted_documents
- # )
- # print("RESPONSE:", response)
-
-
- asyncio.run(main())
diff --git a/utils_v2/database/async_mysql_v2.py b/utils_v2/database/async_mysql_v2.py
deleted file mode 100644
index fc977c6..0000000
--- a/utils_v2/database/async_mysql_v2.py
+++ /dev/null
@@ -1,321 +0,0 @@
-"""
-
- AUTHOR:
-
- Khushal P Soonderji
-
- DATE:
-
- Friday, 30th Aug., 2024
-
- OBJECTIVE:
-
- To be able to access SQL-based databases from python in a simple way.
-
- REFERENCES:
-
- N/A
-
- DOWNLOADS:
-
- N/A
-
-"""
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** IMPORT ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# To make sibling directories accessible for imports:
-import sys
-sys.path.append(".")
-sys.path.append("..")
-
-# MySQL Database:
-import aiomysql
-
-# For data-crunching:
-import pandas as pd
-
-# For time-keeping:
-import time
-
-# OS-level operations:
-import os
-
-# My utils:
-from utils_v2.string import json
-
-# For async activities:
-import asyncio
-
-# For debugging:
-from icecream import IceCreamDebugger
-import traceback
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** MACROS / ONE-TIME INIT ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# --- Nothing Yet
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** VARIABLES ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# --- Nothing Yet
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** CLASSES ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-class AsyncMySQL:
-
- def __init__(
- self,
- pool_size,
- *args,
- **kwargs
- ):
-
- """
- A class to work with SQL-based databases. Originally meant to only invoke stored procedures and retrieve them as
- JSON-like structures (list or dict). The format for the results was very specific to our use case for serving
- Bicree's requirement. This may not serve your requirement at all.
- :param pool_size: The number of connections to maintain n a pool.
- :param args: Any arguments to pass. Not used.
- :param kwargs: Pass the connection configuration from here.
- """
-
- # Set up the variables:
- self.__args = args
- self.__kwargs = kwargs
- self.__min_pool_size = 10
- self.__max_pool_size = max(pool_size, self.__min_pool_size)
- self.__pool = None
-
- # Set up the debugging tools:
- self.__printer = IceCreamDebugger(prefix = "MySQL | ", includeContext = True)
-
- def __del__(self):
- pass
-
- async def connect(self):
-
- """
- Establish a connection and create a pool of connections to call from.
- :return: None.
- """
-
- try:
-
- self.__kwargs["db"] = self.__kwargs.pop("database")
- self.__pool = await aiomysql.create_pool(
- minsize = self.__min_pool_size,
- maxsize = self.__max_pool_size,
- loop = asyncio.get_event_loop(),
- **self.__kwargs
- )
-
- except Exception as exception:
- self.__printer(exception)
- self.__pool = None
-
- async def ensure_connection(self):
-
- """
- Tries to ensure that a connection is present.
- Can be called before every function to make sure that our pool is established.
- :return: None.
- """
-
- if self.__pool is None: await self.connect()
-
- async def call_procedure(self, procedure_name, procedure_args):
-
- """
- To call stored procedures and retrieve all the responses.
- :param procedure_name:
- :param procedure_args:
- :return:
- """
-
- # Make sure we have a connection:
- await self.ensure_connection()
-
- # Make a variable to hold all the result sets.
- # Needed for when the procedure responds with many "tables":
- all_result_sets = []
-
- # Call the procedure and get the results:
- async with self.__pool.acquire() as connection:
- async with connection.cursor() as cursor:
- await cursor.callproc(procedure_name, procedure_args)
-
- # Iterate over all result sets,
- # and process them one-by-one:
- while True:
- this_result_set = []
- result = await cursor.fetchall()
- if not result: break
- columns = [desc[0] for desc in cursor.description]
- for row in result: this_result_set.append(dict(zip(columns, row)))
- all_result_sets.append(this_result_set)
- await cursor.nextset()
-
- # Done here:
- return all_result_sets
-
- async def call_procedure_and_get_json(
- self,
- procedure_name,
- procedure_args,
- retry_count = 1,
- backoff_seconds = 0.5,
- backoff_multiplier = 1.1,
- return_exception = False
- ):
-
- # Note down the start time:
- start_ts = time.perf_counter()
-
- # Try to get the data from the database:
- results = []
- exception = None
- for _ in range(retry_count):
- try: results = await self.call_procedure(
- procedure_name = procedure_name,
- procedure_args = procedure_args,
- )
- except Exception as exc: exception = exc
- if exception is None: break
- await asyncio.sleep(backoff_seconds)
- backoff_seconds = backoff_seconds * backoff_multiplier
-
- # If the results are blank:
- if len(results) == 0: return {
- "status": results[0][0]["status"],
- "message": results[0][0].get("message", "please contact admin (E)"),
- "seconds": None,
- "data": {}
- }
-
- # Extract the very basic success or failure indicators:
- formatted_results = {
- "status": results[0][0]["status"],
- "message": results[0][0].get("message", "ok"),
- "seconds": 0.0,
- "data": {}
- }
-
- # Handle the remaining keys of the zeroth result set:
- for key, value in results[0][0].items():
- if key not in formatted_results.keys():
- formatted_results["data"][key] = value
-
- # Format
- for index in range(len(results)):
- if index > 0: formatted_results["data"][f"rs{index-1}"] = results[index]
-
- # Note down the time taken:
- formatted_results["seconds"] = time.perf_counter() - start_ts
-
- # Done here:
- if return_exception: return formatted_results, exception
- else: return formatted_results
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** FUNCTIONS ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# --- Nothing Yet
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** MAIN PROGRAM ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-if __name__ == "__main__":
-
- async def test(connector):
- pass
-
- async def multi_test(count = 1):
-
- """
- Test asynchronous behaviour.
- :return: None.
- """
-
- # cred_json = {
- # "host": "del.ditscentre.in",
- # "user": "bicree",
- # "port": 3306,
- # "password": "9c3b2808a4aa281129d399fe09e69b53",
- # "database": "bicree"
- # }
-
- cred_json = {
- "host": "del.ditscentre.in",
- "user": "caOffice",
- "port": 3306,
- "password": "jstArchon",
- "database": "caOffice"
- }
-
- db_conn = AsyncMySQL(
- pool_size = 25,
- **cred_json
- )
-
- await db_conn.connect()
-
- # result = await db_conn.call_procedure_and_get_json(
- # procedure_name = "login",
- # procedure_args = (
- # "shree",
- # "shree",
- # "aiomysql",
- # "127.0.0.1"
- # )
- # )
-
- # result = await db_conn.call_procedure_and_get_json(
- # procedure_name = "listSummary",
- # procedure_args = ("bd7a6e53-1345-11ef-940c-0cc47a84a0bb",)
- # )
-
- result = await db_conn.call_procedure_and_get_json(
- procedure_name = "campaign_activity_report",
- procedure_args = (10000000,)
- )
- print("RESULT:", json.to_string(result, default = str))
-
-
- start_time = time.time()
- asyncio.run(multi_test(count = 1))
- print(f"ASYNC HITS DONE IN: {time.time() - start_time} seconds.")
-
diff --git a/utils_v2/date_time/__init__.py b/utils_v2/date_time/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/utils_v2/date_time/__pycache__/__init__.cpython-310.pyc b/utils_v2/date_time/__pycache__/__init__.cpython-310.pyc
deleted file mode 100644
index b18eeed..0000000
Binary files a/utils_v2/date_time/__pycache__/__init__.cpython-310.pyc and /dev/null differ
diff --git a/utils_v2/date_time/__pycache__/date_time.cpython-310.pyc b/utils_v2/date_time/__pycache__/date_time.cpython-310.pyc
deleted file mode 100644
index 46643f1..0000000
Binary files a/utils_v2/date_time/__pycache__/date_time.cpython-310.pyc and /dev/null differ
diff --git a/utils_v2/date_time/date_time.py b/utils_v2/date_time/date_time.py
deleted file mode 100644
index 7437fc7..0000000
--- a/utils_v2/date_time/date_time.py
+++ /dev/null
@@ -1,270 +0,0 @@
-"""
-
- AUTHOR:
-
- Khushal P Soonderji
-
- DATE:
-
- Friday, 21st jun, 2024
-
- OBJECTIVE:
-
- To provide an easy way to work with time.
-
- REFERENCES:
-
- N/A
-
- DOWNLOADS:
-
- N/A
-
-"""
-
-# *****************************************************************************************************************
-# ***** ****
-# *** IMPORT ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# To make sibling directories accessible for imports:
-import sys
-sys.path.append(".")
-sys.path.append("..")
-
-# For date and time handling:
-import pytz
-from datetime import datetime, timedelta
-import dateparser
-
-# To handle date-time objects from a Pandas Dataframe:
-import pandas as pd
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** MACROS / ONE-TIME INIT ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# Date-Time Formats:
-DATE_TIME_FORMATS = (
- "%d/%m/%y",
- "%d-%b-%y",
- "%d-%m-%y",
- "%d.%m.%y",
- "%d/%m/%Y",
- "%d-%b-%Y",
- "%d-%m-%Y",
- "%d.%m.%Y",
- "%d/%b",
- "%d%m%Y",
- "%Y%m%d",
- "%Y-%m-%d %H:%M:%S"
-)
-
-# Useful Timezones:
-TIMEZONE_UTC = pytz.timezone("UTC")
-TIMEZONE_IST = pytz.timezone("Asia/Kolkata")
-TIMEZONE_ET = pytz.timezone("America/New_York")
-TIMEZONE_CT = pytz.timezone("America/Chicago")
-TIMEZONE_MT = pytz.timezone("America/Denver")
-TIMEZONE_PT = pytz.timezone("America/Los_Angeles")
-TIMEZONE_JST = pytz.timezone("Asia/Tokyo")
-TIMEZONE_CET = pytz.timezone("Europe/Paris")
-TIMEZONE_GMT = pytz.timezone("GMT")
-TIMEZONE_AEST = pytz.timezone("Australia/Sydney")
-TIMEZONE_NZST = pytz.timezone("Pacific/Auckland")
-TIMEZONE_CST = pytz.timezone("Asia/Shanghai")
-TIMEZONE_KST = pytz.timezone("Asia/Seoul")
-TIMEZONE_MSK = pytz.timezone("Europe/Moscow")
-TIMEZONE_BRT = pytz.timezone("America/Sao_Paulo")
-TIMEZONE_GST = pytz.timezone("Asia/Dubai")
-TIMEZONE_SAST = pytz.timezone("Africa/Johannesburg")
-TIMEZONE_AST = pytz.timezone("Asia/Riyadh")
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** VARIABLES ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# --- Nothing Yet
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** FUNCTIONS ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-def translate_date_time_string(
- datetime_string,
- source_format = None,
- destination_format = "%Y-%m-%dT%H:%M:%S"
-):
-
- """
- To convert an input datetime string to a different format.
- :param datetime_string: The datetime string to translate.
- :param source_format: The current format of the string. If not provided, dateparser will be used.
- :param destination_format: The format to convert to.
- :return: The converted datetime string.
- """
-
- try:
- if source_format is None: datetime_obj = dateparser.parse(datetime_string)
- else: datetime_obj = datetime.strptime(datetime_string, source_format)
- return datetime_obj.strftime(destination_format)
- except Exception as exception:
- return None
-
-
-# ---------------------------------------------------------------------------------------------------------------------
-
-
-def parse_date_time(input_value, timezone = None, date_formats = None):
-
- """
- To take any kind of input and interpret the datetime from it.
- :param input_value: Either a string or an integer or some form of datetime representation.
- :param timezone: The timezone to apply to the interpreted datetime. EXISTING TIMEZONE INFO WILL BE OVERWRITTEN.
- :param date_formats: The string formats to consider when parsing a string input.
- :return: The parsed datetime or null.
- """
-
- datetime_object = None
- date_formats = date_formats or DATE_TIME_FORMATS
-
- # It could either be in seconds or milliseconds from epoch time's base date (January 1, 1970),
- # or it could be days since Microsoft Excel's base date (December 31, 1899).
- if type(input_value) in [int, float]:
- if input_value > 9999999999.0: datetime_object = datetime.fromtimestamp(input_value / 1000.0)
- if input_value > 999999.0: datetime_object = datetime.fromtimestamp(input_value)
- else: datetime_object = datetime.fromtimestamp(input_value * 24 * 60 * 60.0) - timedelta(days = 25569)
-
- # The input can even be a pre-formatted date:
- if type(input_value) is str:
- datetime_object = dateparser.parse(input_value, date_formats = date_formats)
-
- # If the type is a datetime object, then return it as it is:
- if type(input_value) is datetime:
- datetime_object = input_value
-
- # If the type is the native datetime format of pandas:
- if type(input_value) is pd._libs.tslibs.timestamps.Timestamp:
- datetime_object = input_value.to_pydatetime()
-
- # Process the timezone:
- if datetime_object is not None and timezone is not None:
- datetime_object = as_if_timezone(datetime_object, timezone)
-
- # Done here:
- return datetime_object
-
-
-# ---------------------------------------------------------------------------------------------------------------------
-
-
-def get_current_date_time(timezone = None, as_string = False):
-
- """
- Returns the current time as a datetime object.
- :param timezone: The timezone to apply to the returned datetime.
- :param as_string: Whether, or not, you want the output as a string.
- :return: The datetime object/string representing the current time.
- """
-
- if timezone is not None and isinstance(timezone, str): timezone = pytz.timezone(timezone)
- now = datetime.now(timezone)
- return now.isoformat() if as_string else now
-
-
-# ---------------------------------------------------------------------------------------------------------------------
-
-
-def get_current_ist_date_time(as_string = False):
-
- """
- Gives out the current time in IST timezone.
- :param as_string: Whether, or not, you want the output as a string.
- :return: The datetime object or string representing the current time.
- """
-
- return get_current_date_time(
- timezone = TIMEZONE_IST,
- as_string = as_string
- )
-
-
-# ---------------------------------------------------------------------------------------------------------------------
-
-
-def get_current_utc_date_time(as_string = False):
-
- """
- Gives out the current time in UTC timezone.
- :param as_string: Whether, or not, you want the output as a string.
- :return: The datetime object or string representing the current time.
- """
-
- return get_current_date_time(
- timezone = TIMEZONE_UTC,
- as_string = as_string
- )
-
-
-# ---------------------------------------------------------------------------------------------------------------------
-
-
-def as_if_timezone(datetime_object, timezone):
-
- """
- Ignores existing timezone info and applies the intended timezone.
- The time stays the same, only the timezone marker changes.
- e.g. for IST to UTC: 2024-08-09 00:00:00+05:30 --> 2024-08-09 00:00:00+00:00
- HINT: IT PRETENDS "AS IF" THE TIMEZONE WAS THE INPUT TIMEZONE.
- :param datetime_object: The datetime object on which the timezone needs to be applied.
- :param timezone: The timezone that needs to be applied.
- :return: A timezone-aware datetime object.
- """
-
- tz_object = pytz.timezone(timezone) if isinstance(timezone, str) else timezone
- return tz_object.localize(datetime_object.replace(tzinfo = None))
-
-
-# ---------------------------------------------------------------------------------------------------------------------
-
-
-def to_timezone(datetime_object, timezone):
-
- """
- Converts from one timezone to another. The time is adjusted by computing the difference between the two timezones.
- NOTE: THIS FUNCTION ASSUMES THE INPUT WAS IN UTC IF THE INPUT WAS TIMEZONE-NAIVE.
- e.g. for IST to UTC: 2024-08-09 00:00:00+05:30 --> 2024-08-08 18:30:00+00:00
- :param datetime_object: The datetime object on which the timezone needs to be applied.
- :param timezone: The timezone that needs to be applied.
- :return: A timezone-aware datetime object.
- """
-
- if isinstance(timezone, str): timezone = pytz.timezone(timezone)
- if datetime_object.tzinfo is None: return datetime_object.replace(tzinfo = TIMEZONE_UTC)
- return datetime_object.astimezone(timezone)
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** MAIN PROGRAM ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-if __name__ == "__main__":
-
- pass
diff --git a/utils_v2/image/__init__.py b/utils_v2/image/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/utils_v2/image/scanner/__init__.py b/utils_v2/image/scanner/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/utils_v2/image/scanner/scanner.py b/utils_v2/image/scanner/scanner.py
deleted file mode 100644
index 28fe69b..0000000
--- a/utils_v2/image/scanner/scanner.py
+++ /dev/null
@@ -1,410 +0,0 @@
-"""
-
- AUTHOR:
-
- Khushal P Soonderji
-
- DATE:
-
- Thursday, 19th Sept., 2024
-
- OBJECTIVE:
-
- To provide a way to easily scan documents like printouts and visiting cards.
-
- REFERENCES:
-
- 01. https://medium.com/@victorolufemi/build-a-document-scanner-with-opencv-ff9f645a4085
- 02. https://github.com/JaidedAI/EasyOCR
-
- DOWNLOADS:
-
- N/A
-
-"""
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** IMPORT ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# To make sibling directories accessible for imports:
-import sys
-
-from diffusers.utils.import_utils import candidates
-
-sys.path.append(".")
-sys.path.append("..")
-
-# System-level activities:
-import io
-
-# To work with image processing:
-import cv2
-import imutils
-import numpy as np
-
-# To run OCR:
-import easyocr
-
-# To download images from the web:
-import requests
-
-# My utils:
-from utils_v2.string import json
-from utils_v2.ai.object_detection.yolo import YoloDetect
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** MACROS / ONE-TIME INIT ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# --- Nothing Yet
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** VARIABLES ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# --- Nothing Yet
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** CLASSES ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-class DocumentScanner:
-
- def __init__(
- self,
- layout_detection_yolo = None,
- whitelisted_yolo_classes = None,
- ocr_languages = None
- ):
-
- self.__yolo = YoloDetect(
- model_file = layout_detection_yolo,
- debug = False
- )
- self.__whitelisted_yolo_classes = whitelisted_yolo_classes or []
- self.__ocr_engine = easyocr.Reader(ocr_languages or ["en"])
- pass
-
- @staticmethod
- def open_image(image):
-
- """
- Opens an image from various sources.
- :param image: The image either as a path to a file on the local disk, or a URL, or an io.BytesIO buffer.
- :return: The image as a CV2 object.
- """
-
- # When the image is provided as a string,
- # it could be either as a URL or a path to a local file:
- if isinstance(image, str):
- if image.startswith("https://") or image.startswith("http://"):
- image = io.BytesIO(requests.get(image).content)
- else: image = cv2.imread(image)
-
- # If the image is provided as a io.BytesIO buffer:
- if isinstance(image, io.BytesIO):
- image.seek(0)
- image = np.asarray(bytearray(image.read()), dtype = np.uint8)
- image = cv2.imdecode(image, cv2.IMREAD_COLOR)
-
- # Done here:
- return image
-
- @staticmethod
- def polygon_area(points):
-
- """
- Computes the area occupied by a shape described by the array of points.
- Example input: [[x1, y1], [x2, y2], [x3, y3], [x4, y4]]
- :return: The area of the shape.
- """
-
- n = len(points)
- area = 0.0
- for i in range(n):
- j = (i + 1) % n # next vertex
- area += points[i][0] * points[j][1]
- area -= points[j][0] * points[i][1]
- return abs(area) / 2.0
-
- @staticmethod
- def show(image, title = "Preview", wait = True):
-
- """
- Just a quick wrapper to show the image in a window.
- :param image: The image that you want to show.
- :param title: The title of the window.
- :param wait: Set this to True when you want to show the window(s). This is useful when you want to show many
- windows at once. Suppose you want to show a lot of windows, you set this to False for all calls except the
- very last one.
- :return: None.
- """
-
- cv2.imshow(title, image)
- if wait:
- cv2.waitKey()
- cv2.destroyAllWindows()
-
- @staticmethod
- def enhance(image):
-
- """
- Applies a form of contrast boost to make the edges more easily visible.
- :param image: The OpenCV image that needs to be enhanced.
- :return: The enhanced CV2 image.
- """
-
- return cv2.detailEnhance(
- src = image,
- sigma_s = 20,
- sigma_r = 0.15
- )
-
- @staticmethod
- def get_edges(image):
-
- """
- Gets the edges in an image.
- :param image: A CV2 image.
- :return: The image with the edges detected.
- """
-
- # Denoise the image:
- processed_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
- processed_image = cv2.GaussianBlur(
- processed_image,
- ksize = (5, 5),
- sigmaX = 0
- )
-
- # Detect edges in the image:
- processed_image = cv2.Canny(
- processed_image,
- threshold1 = 50,
- threshold2 = 200
- )
-
- # Close small gaps in the edges:
- kernel = np.ones((5, 5), np.uint8)
- processed_image = cv2.dilate(processed_image, kernel, iterations = 1)
- # processed_image = cv2.morphologyEx(processed_image, cv2.MORPH_CLOSE, kernel)
- # processed_image = cv2.erode(processed_image, kernel, iterations = 1)
-
- # Done here:
- return processed_image
-
- def scan(self, image, min_area = 0.125):
-
- """
- Looks for a rectangle in an image and flattens it out. No colour filters are applied here.
- :param image: The image either as a path to a file on the local disk, or a URL, or an io.BytesIO buffer.
- :param min_area: The minimum area occupied by the document in the image.
- :return: The flattened image if any, else None.
- """
-
- # Open the image and read the data:
- image = self.open_image(image)
- if image is None: return None
- height, width = image.shape[:2]
- image_area = height * width
-
- # Enhance the image to have better visibility of edges:
- # processed_image = self.enhance(image.copy())
-
- # Detect edges in the image:
- processed_image = self.get_edges(image.copy())
-
- # Find the contours in the edges,
- # and sort them in ascending order:
- contours = cv2.findContours(processed_image, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
- contours = imutils.grab_contours(contours)
- contours = sorted(contours, key = cv2.contourArea, reverse = True)
-
- # We try to get clean and passable contours.
- # For our purposes, 'clean' is when it can be approximated to exactly 4 sides,
- # and 'passable' is when the approximation comes close to it:
- clean_rects = []
- passable_rects = []
- for contour in contours:
- perimeter = cv2.arcLength(contour, closed = True)
- approximation = cv2.approxPolyDP(contour, 0.025 * perimeter, closed = True)
- if len(approximation) == 4: clean_rects.append(approximation)
- elif len(approximation) <= 32:
- candidate_contour = cv2.minAreaRect(approximation)
- candidate_contour = cv2.boxPoints(candidate_contour)
- candidate_contour = np.intp(candidate_contour)
- passable_rects.append(candidate_contour)
- clean_rects = sorted(clean_rects, key = cv2.contourArea, reverse = True)
- passable_rects = sorted(passable_rects, key = cv2.contourArea, reverse = True)
-
- # Now we decide between the best candidate:
- document_outline = None
- if len(clean_rects) > 0:
- candidate_rect = np.array([p[0] for p in clean_rects[0]])
- rectangle_area = cv2.contourArea(candidate_rect)
- if rectangle_area / image_area >= min_area: document_outline = candidate_rect
- if document_outline is None and len(passable_rects) > 0:
- candidate_rect = cv2.minAreaRect(passable_rects[0])
- candidate_rect = cv2.boxPoints(candidate_rect)
- candidate_rect = np.intp(candidate_rect)
- rectangle_area = cv2.contourArea(candidate_rect)
- if rectangle_area / image_area >= min_area: document_outline = candidate_rect
-
- # If there is no rectangular contour,
- # we exit with failure:
- if document_outline is None: return None
-
- # Since we did get the best candidate for the document,
- # we figure out which point n the contour is which corner of the document:
- document_rectangle = np.zeros((4, 2), dtype = "float32")
- sum_points = document_outline.sum(axis = 1)
- document_rectangle[0] = document_outline[np.argmin(sum_points)]
- document_rectangle[2] = document_outline[np.argmax(sum_points)]
- diff_points = np.diff(document_outline, axis = 1)
- document_rectangle[1] = document_outline[np.argmin(diff_points)]
- document_rectangle[3] = document_outline[np.argmax(diff_points)]
- (top_left, top_right, bottom_right, bottom_left) = document_rectangle
-
- # let's compute the dimensions of the document in the image:
- left_height = np.sqrt(((top_left[0] - bottom_left[0]) ** 2) + ((top_left[1] - bottom_left[1]) ** 2))
- right_height = np.sqrt(((top_right[0] - bottom_right[0]) ** 2) + ((top_right[1] - bottom_right[1]) ** 2))
- top_width = np.sqrt(((top_right[0] - top_left[0]) ** 2) + ((top_right[1] - top_left[1]) ** 2))
- bottom_width = np.sqrt(((bottom_right[0] - bottom_left[0]) ** 2) + ((bottom_right[1] - bottom_left[1]) ** 2))
- max_height = max(int(left_height), int(right_height))
- max_width = max(int(top_width), int(bottom_width))
-
- # We compute the destination of the transform:
- destination = np.array(
- object = [
- [0, 0],
- [max_width - 1, 0],
- [max_width - 1, max_height - 1],
- [0, max_height - 1]
- ],
- dtype = "float32"
- )
-
- # We apply the transform that flattens out the document:
- matrix = cv2.getPerspectiveTransform(document_rectangle, destination)
- flattened_image = cv2.warpPerspective(image, matrix, dsize = (max_width, max_height))
-
- # Done here:
- return flattened_image
-
- def extract_text(
- self,
- image,
- min_confidence = 0.5,
- margin = 0.05
- ):
-
- """
- To run OCR on the input image.
- :param image: The image either as a path to a file on the local disk, or a URL, or an io.BytesIO buffer.
- :param min_confidence: The minimum amount of confidence for detected text to be considered.
- :param margin: The extra pixels to include when cropping into a section for OCR.
- :return: The extracted text.
- """
-
- # Make a variable that will hold the final result:
- extracted_text = {}
-
- # Open the image and read the data:
- image = self.open_image(image)
- height, width = image.shape[:2]
-
- # Run OCR on the entire canvas:
- ocr_result = [r[1] for r in self.__ocr_engine.readtext(image) if r[-1] >= min_confidence]
- extracted_text["fullDoc"] = " ".join(ocr_result)
-
- # Get the doc layout in the image:
- document_sections = self.__yolo.predict(image)
- document_sections = document_sections["boxes"]
-
- # For each section, we run the OCR process individually:
- section_results = []
- for section in document_sections:
- if section["class"] in self.__whitelisted_yolo_classes:
- section_width = section["x2"] - section["x1"]
- section_height = section["y2"] - section["y1"]
- x1 = max(int(section["x1"] - (margin * section_width)), 0)
- y1 = max(int(section["y1"] - (margin * section_height)), 0)
- x2 = min(int(section["x2"] + (margin * section_width)), width)
- y2 = min(int(section["y2"] + (margin * section_height)), height)
- sub_image = image[y1:y2, x1:x2]
- ocr_result = [r[1] for r in self.__ocr_engine.readtext(sub_image) if r[-1] >= min_confidence]
- section_results.append({
- "text": " ".join(ocr_result),
- "class": section["class"],
- "className": section["className"],
- "x1": x1,
- "y1": y1,
- "x2": x2,
- "y2": y2
- })
- extracted_text["bySection"] = section_results
-
- # Done here:
- return extracted_text
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** FUNCTIONS ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# --- Nothing Yet
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** MAIN PROGRAM ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-if __name__ == "__main__":
-
- import time
-
- my_scanner = DocumentScanner(
- layout_detection_yolo = r"/home/developer/PycharmProjects/utils/data/ai/behaviour_models/hugging_face/object_detection/YOLOv10-Document-Layout-Analysis/yolov10x_best.pt",
- whitelisted_yolo_classes = [0, 1, 3, 4, 5, 7, 9, 10],
- # whitelisted_yolo_classes = [0, 1, 3, 4, 5, 6, 7, 8, 9, 10],
- ocr_languages = ["en"]
- )
-
- image = my_scanner.open_image(r"/home/developer/Downloads/talkaholics_card.jpg")
- document_image = my_scanner.scan(image)
- if document_image is not None:
- cv2.imwrite(r"/home/developer/Downloads/flattened_image.jpg", document_image)
- start_time = time.time()
- document_text = my_scanner.extract_text(
- document_image,
- min_confidence = 0.5
- )
- print("OCR RESULT:")
- print(document_text)
-
- print(f"FINISHED IN {time.time() - start_time} SECONDS!")
-
- else: print("No image")
diff --git a/utils_v2/image/scanner/scanner_v2.py b/utils_v2/image/scanner/scanner_v2.py
deleted file mode 100644
index fc63627..0000000
--- a/utils_v2/image/scanner/scanner_v2.py
+++ /dev/null
@@ -1,379 +0,0 @@
-"""
-
- AUTHOR:
-
- Khushal P Soonderji
-
- DATE:
-
- Tuesday, 24th Sept., 2024
-
- OBJECTIVE:
-
- To provide a way to easily scan documents like printouts and visiting cards.
-
- REFERENCES:
-
- 01. https://medium.com/@victorolufemi/build-a-document-scanner-with-opencv-ff9f645a4085
- 02. https://github.com/JaidedAI/
- 03. https://github.com/criistian14/flutter_document_scanner/blob/master/flutter_document_scanner_android/android/src/main/kotlin/com/christian/flutterDocumentScanner/OpenCVPlugin.kt
- 04. https://www.geeksforgeeks.org/python-bilateral-filtering/
-
- DOWNLOADS:
-
- N/A
-
-"""
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** IMPORT ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# To make sibling directories accessible for imports:
-import sys
-sys.path.append(".")
-sys.path.append("..")
-
-# System-level activities:
-import io
-
-# To work with image processing:
-import cv2
-import imutils
-import numpy as np
-
-# To run OCR:
-import easyocr
-
-# To download images from the web:
-import requests
-
-# My utils:
-from utils_v2.string import json
-from utils_v2.ai.object_detection.yolo import YoloDetect
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** MACROS / ONE-TIME INIT ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# --- Nothing Yet
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** VARIABLES ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# --- Nothing Yet
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** CLASSES ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-class DocumentScanner:
-
- def __init__(
- self,
- layout_detection_yolo = None,
- whitelisted_yolo_classes = None,
- ocr_languages = None
- ):
-
- self.__yolo = YoloDetect(
- model_file = layout_detection_yolo,
- debug = False
- )
- self.__whitelisted_yolo_classes = whitelisted_yolo_classes or []
- self.__ocr_engine = easyocr.Reader(ocr_languages or ["en"])
- pass
-
- @staticmethod
- def open_image(image):
-
- """
- Opens an image from various sources.
- :param image: The image either as a path to a file on the local disk, or a URL, or an io.BytesIO buffer.
- :return: The image as a CV2 object.
- """
-
- # When the image is provided as a string,
- # it could be either as a URL or a path to a local file:
- if isinstance(image, str):
- if image.startswith("https://") or image.startswith("http://"):
- image = io.BytesIO(requests.get(image).content)
- else: image = cv2.imread(image)
-
- # If the image is provided as a io.BytesIO buffer:
- if isinstance(image, io.BytesIO):
- image.seek(0)
- image = np.asarray(bytearray(image.read()), dtype = np.uint8)
- image = cv2.imdecode(image, cv2.IMREAD_COLOR)
-
- # Done here:
- return image
-
- @staticmethod
- def show(image, title = "Preview", wait = True):
-
- """
- Just a quick wrapper to show the image in a window.
- :param image: The image that you want to show.
- :param title: The title of the window.
- :param wait: Set this to True when you want to show the window(s). This is useful when you want to show many
- windows at once. Suppose you want to show a lot of windows, you set this to False for all calls except the
- very last one.
- :return: None.
- """
-
- cv2.imshow(title, image)
- if wait:
- cv2.waitKey()
- cv2.destroyAllWindows()
-
- @staticmethod
- def get_edges(image):
-
- """
- Apply all the preprocessing filters on the image before sending it off for contour-finding.
- :param image: The image to pre-process.
- :return: The pre-processed image.
- """
-
- # Denoise the image:
- processed_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
- processed_image = cv2.bilateralFilter(
- processed_image,
- d = 9,
- sigmaColor = 75,
- sigmaSpace = 75
- )
-
- # Detect edges in the image:
- processed_image = cv2.Canny(
- processed_image,
- threshold1 = 75,
- threshold2 = 200
- )
-
- # Close small gaps in the edges:
- kernel = np.ones((5, 5), np.uint8)
- processed_image = cv2.dilate(processed_image, kernel, iterations = 1)
- # processed_image = cv2.morphologyEx(processed_image, cv2.MORPH_CLOSE, kernel)
- # processed_image = cv2.erode(processed_image, kernel, iterations = 1)
-
- # Done here:
- return processed_image
-
- def scan(self, image, min_area = 0.125):
-
- """
- Looks for a rectangle in an image and flattens it out. No colour filters are applied here.
- :param image: The image either as a path to a file on the local disk, or a URL, or an io.BytesIO buffer.
- :param min_area: The minimum area occupied by the document in the image.
- :return: The flattened image if any, else None.
- """
-
- # Open the image and read the data:
- image = self.open_image(image)
- if image is None: return None
- height, width = image.shape[:2]
- image_area = height * width
-
- # image = self.enhance(image)
- processed_image = self.get_edges(image)
- # self.show(processed_image)
-
- # Find the contours in the edges,
- # and sort them in ascending order:
- contours = cv2.findContours(processed_image, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
- contours = imutils.grab_contours(contours)
- contours = sorted(contours, key = cv2.contourArea, reverse = True)
-
- # We try to get clean and passable contours.
- # For our purposes, 'clean' is when it can be approximated to exactly 4 sides,
- # and 'passable' is when the approximation comes close to it:
- clean_rects = []
- passable_rects = []
- for contour in contours:
- perimeter = cv2.arcLength(contour, closed = True)
- approximation = cv2.approxPolyDP(contour, 0.025 * perimeter, closed = True)
- if len(approximation) == 4: clean_rects.append(approximation)
- elif len(approximation) <= 32:
- candidate_contour = cv2.minAreaRect(approximation)
- candidate_contour = cv2.boxPoints(candidate_contour)
- candidate_contour = np.intp(candidate_contour)
- passable_rects.append(candidate_contour)
- clean_rects = sorted(clean_rects, key = cv2.contourArea, reverse = True)
- passable_rects = sorted(passable_rects, key = cv2.contourArea, reverse = True)
-
- # Now we decide between the best candidate:
- document_outline = None
- if len(clean_rects) > 0:
- candidate_rect = np.array([p[0] for p in clean_rects[0]])
- rectangle_area = cv2.contourArea(candidate_rect)
- if rectangle_area / image_area >= min_area: document_outline = candidate_rect
- if document_outline is None and len(passable_rects) > 0:
- candidate_rect = cv2.minAreaRect(passable_rects[0])
- candidate_rect = cv2.boxPoints(candidate_rect)
- candidate_rect = np.intp(candidate_rect)
- rectangle_area = cv2.contourArea(candidate_rect)
- if rectangle_area / image_area >= min_area: document_outline = candidate_rect
-
- # If there is no rectangular contour,
- # we exit with failure:
- if document_outline is None: return None
-
- # Since we did get the best candidate for the document,
- # we figure out which point n the contour is which corner of the document:
- document_rectangle = np.zeros((4, 2), dtype = "float32")
- sum_points = document_outline.sum(axis = 1)
- document_rectangle[0] = document_outline[np.argmin(sum_points)]
- document_rectangle[2] = document_outline[np.argmax(sum_points)]
- diff_points = np.diff(document_outline, axis = 1)
- document_rectangle[1] = document_outline[np.argmin(diff_points)]
- document_rectangle[3] = document_outline[np.argmax(diff_points)]
- (top_left, top_right, bottom_right, bottom_left) = document_rectangle
-
- # let's compute the dimensions of the document in the image:
- left_height = np.sqrt(((top_left[0] - bottom_left[0]) ** 2) + ((top_left[1] - bottom_left[1]) ** 2))
- right_height = np.sqrt(((top_right[0] - bottom_right[0]) ** 2) + ((top_right[1] - bottom_right[1]) ** 2))
- top_width = np.sqrt(((top_right[0] - top_left[0]) ** 2) + ((top_right[1] - top_left[1]) ** 2))
- bottom_width = np.sqrt(((bottom_right[0] - bottom_left[0]) ** 2) + ((bottom_right[1] - bottom_left[1]) ** 2))
- max_height = max(int(left_height), int(right_height))
- max_width = max(int(top_width), int(bottom_width))
-
- # We compute the destination of the transform:
- destination = np.array(
- object = [
- [0, 0],
- [max_width - 1, 0],
- [max_width - 1, max_height - 1],
- [0, max_height - 1]
- ],
- dtype = "float32"
- )
-
- # We apply the transform that flattens out the document:
- matrix = cv2.getPerspectiveTransform(document_rectangle, destination)
- flattened_image = cv2.warpPerspective(image, matrix, dsize = (max_width, max_height))
-
- # Done here:
- return flattened_image
-
- def extract_text(
- self,
- image,
- min_confidence = 0.5,
- margin = 0.05
- ):
-
- """
- To run OCR on the input image.
- :param image: The image either as a path to a file on the local disk, or a URL, or an io.BytesIO buffer.
- :param min_confidence: The minimum amount of confidence for detected text to be considered.
- :param margin: The extra pixels to include when cropping into a section for OCR.
- :return: The extracted text.
- """
-
- # Make a variable that will hold the final result:
- extracted_text = {}
-
- # Open the image and read the data:
- image = self.open_image(image)
- height, width = image.shape[:2]
-
- # Run OCR on the entire canvas:
- ocr_result = [r[1] for r in self.__ocr_engine.readtext(image) if r[-1] >= min_confidence]
- extracted_text["fullDoc"] = " ".join(ocr_result)
-
- # Get the doc layout in the image:
- document_sections = self.__yolo.predict(image)
- document_sections = document_sections["boxes"]
-
- # For each section, we run the OCR process individually:
- section_results = []
- for section in document_sections:
- if section["class"] in self.__whitelisted_yolo_classes:
- section_width = section["x2"] - section["x1"]
- section_height = section["y2"] - section["y1"]
- x1 = max(int(section["x1"] - (margin * section_width)), 0)
- y1 = max(int(section["y1"] - (margin * section_height)), 0)
- x2 = min(int(section["x2"] + (margin * section_width)), width)
- y2 = min(int(section["y2"] + (margin * section_height)), height)
- sub_image = image[y1:y2, x1:x2]
- ocr_result = [r[1] for r in self.__ocr_engine.readtext(sub_image) if r[-1] >= min_confidence]
- section_results.append({
- "text": " ".join(ocr_result),
- "class": section["class"],
- "className": section["className"],
- "x1": x1,
- "y1": y1,
- "x2": x2,
- "y2": y2
- })
- extracted_text["bySection"] = section_results
-
- # Done here:
- return extracted_text
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** FUNCTIONS ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# --- Nothing Yet
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** MAIN PROGRAM ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-if __name__ == "__main__":
-
- import time
-
- my_scanner = DocumentScanner(
- layout_detection_yolo = r"/home/developer/PycharmProjects/utils/data/ai/behaviour_models/hugging_face/object_detection/YOLOv10-Document-Layout-Analysis/yolov10x_best.pt",
- whitelisted_yolo_classes = [0, 1, 3, 4, 5, 7, 9, 10],
- # whitelisted_yolo_classes = [0, 1, 3, 4, 5, 6, 7, 8, 9, 10],
- ocr_languages = ["en"]
- )
-
- image = my_scanner.open_image(r"/home/developer/Downloads/sushmita_card.jpg")
- # image = my_scanner.open_image(r"/home/developer/Downloads/niranjan_card.jpg")
- # image = my_scanner.open_image(r"/home/developer/Downloads/niranjan_card_2.jpg")
- # image = my_scanner.open_image(r"/home/developer/Downloads/card_square.jpg")
- document_image = my_scanner.scan(image)
- if document_image is not None:
- cv2.imwrite(r"/home/developer/Downloads/flattened_image.jpg", document_image)
- start_time = time.time()
- document_text = my_scanner.extract_text(
- document_image,
- min_confidence = 0.5
- )
- print("OCR RESULT:")
- print(document_text)
-
- print(f"FINISHED IN {time.time() - start_time} SECONDS!")
-
- else: print("No image")
diff --git a/utils_v2/mail/__init__.py b/utils_v2/mail/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/utils_v2/mail/async_mail.py b/utils_v2/mail/async_mail.py
deleted file mode 100644
index 839873c..0000000
--- a/utils_v2/mail/async_mail.py
+++ /dev/null
@@ -1,440 +0,0 @@
-"""
-
- AUTHOR:
-
- Khushal P Soonderji
-
- DATE:
-
- Tuesday, 16th Jul, 2024
-
- OBJECTIVE:
-
- To be able to send out mails from code.
-
- REFERENCES:
-
- N/A
-
- DOWNLOADS:
-
- N/A
-
-"""
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** IMPORT ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# To make sibling directories accessible for imports:
-import sys
-sys.path.append(".")
-sys.path.append("..")
-
-# For working with mails:
-import aiosmtplib
-from email.mime.multipart import MIMEMultipart
-from email.mime.image import MIMEImage
-from email.mime.text import MIMEText
-from email.mime.base import MIMEBase
-from email import encoders
-
-# My utils:
-from utils import rate_limit_utils
-
-# Common:
-from shared.statuses import StatusCodes
-
-# For random strings:
-import string
-import random
-
-# For system-level activities:
-import os
-
-# For working with files in RAM:
-import io
-
-# For debugging:
-from icecream import IceCreamDebugger
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** MACROS / ONE-TIME INIT ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# --- Nothing Yet
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** VARIABLES ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# --- Nothing Yet
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** FUNCTIONS ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# --- Nothing Yet
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** CLASSES ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-class MailMessage:
-
- def __init__(self, to_email, subject):
-
- """
- Create an instance of the message that you would like to send.
- :param to_email: The EMail ID of th recipient.
- :param subject: The subject of the mail.
- """
-
- self.message = MIMEMultipart()
- self.message["To"] = to_email
- self.message["Subject"] = subject
-
- def add_text(self, text):
-
- """
- Add plain-text to the mail body.
- :param text: The text to add to the mail body.
- :return: None.
- """
-
- self.message.attach(MIMEText(text, "plain"))
-
- def add_html(self, html_text):
-
- """
- Add HTML text to the mail body.
- :param html_text: The HTML text to add to the mail body.
- :return: None.
- """
-
- self.message.attach(MIMEText(html_text, "html"))
-
- def add_inline_image(self, image_file, content_id = None):
-
- """
- Add an inline image to the body of the mail.
- NOTE: This is NOT the same as sending an image as an attachment.
- :param image_file: The image data to attach to the mail body.
- :param content_id: Inline images are inserted via HTML bocks. This field identifies the image resource. If not
- specified, I will generate a random string. You may write a custom value here if you know what you are
- doing. For most use cases, please ignore this field.
- :return: None.
- """
-
- # Read the image as bytes:
- image_bytes = None
- if type(image_file) is str:
- with open(image_file, "rb") as opened_image_file:
- image_bytes = opened_image_file.read()
- if type(image_file) is io.BytesIO:
- image_file.seek(0)
- image_bytes = image_file.getvalue()
-
- # Declare the part to be attached to the multipart message:
- if image_bytes is not None:
-
- # Create the HTML block if the image pointer is blank:
- if content_id is None:
- content_id = ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(8))
- self.add_html(f"""
-
-
-
-
-
- """)
-
- # Then add the image:
- image_part = MIMEImage(image_bytes)
- image_part.add_header("Content-ID", f"<{content_id}>")
- self.message.attach(image_part)
-
- def add_attachment(self, attachment_file, file_name = None):
-
- # Declare the part to be attached to the multipart message:
- part = MIMEBase("application", "octet-stream")
-
- # If the attachment is a file stored in the local disk:
- if type(attachment_file) is str:
- file_name = file_name or os.path.split(attachment_file)[-1]
- with open(attachment_file, "rb") as attachment:
- part.set_payload(attachment.read())
-
- # If the file is held in RAM:
- if type(attachment_file) is io.BytesIO():
- attachment_file.seek(0)
- part.set_payload(attachment_file.read())
-
- # Encode and attach the file:
- encoders.encode_base64(part)
- part.add_header(
- "Content-Disposition",
- f"attachment; filename= {file_name}",
- )
- self.message.attach(part)
-
- def get_message(self):
- return self.message
-
-
-# ---------------------------------------------------------------------------------------------------------------------
-
-
-class AsyncMailClient:
-
- # Constants:
- SMTP_TLS_PORT = 587
- SMTP_SSL_PORT = 465
-
- # variables:
- __smtp = None
-
- def __init__(
- self,
- email,
- password,
- server,
- port = 587,
- rate_limiters = None,
- wait_for_turn = True,
- debug = True,
- debug_prefix = "Mail (C) | "
- ):
-
- """
- Set up the mail client.
- :param email: The Email ID to use when sending out mails.
- :param password: The password of the EMail ID that is being used.
- :param server: The EMail server.
- :param port: The port number to connect to the host.
- :param rate_limiters: The rate limiters to use. Must have "get_turn" and "has_turn" methods. "get_turn" method
- must wait for the turn, and "has_turn" method must only check if a turn is available.
- :param wait_for_turn: To wait for turn if the rate limit has been exceeded, or to return with failure.
- :param debug: Whether, or not, you want to print debugging messages.
- :param debug_prefix: The prefix to identify the debugging messages.
- """
-
- # Initialize the debugger:
- self.__printer = IceCreamDebugger(prefix = debug_prefix, includeContext = True)
- if not debug: self.__printer.disable()
-
- # Note down the credentials and other details:
- self.__email = email
- self.__password = password
- self.__server = server
- self.__port = port
- self.__rate_limiters = rate_limiters if type(rate_limiters) is list else ([rate_limiters] if rate_limiters is not None else [])
- self.__wait_for_turn = wait_for_turn
-
- def enable_debug(self):
- self.__printer.enable()
-
- def disable_debug(self):
- self.__printer.disable()
-
- async def login(self):
-
- """
- To connect to the mail server and authenticate the user.
- :return: True if authenticated, else False.
- """
-
- # Initialize the SMTP connection,
- # and return with success if all goes well:
- try:
- self.__smtp = aiosmtplib.SMTP(
- hostname = self.__server,
- port = self.__port,
- use_tls = False,
- start_tls = False
- )
- await self.__smtp.connect()
- await self.__smtp.starttls()
- await self.__smtp.login(self.__email, self.__password)
- return True
-
- # Return with failure if something goes wrong:
- except Exception as exception:
- self.__printer(exception)
- try: await self.__smtp.quit()
- except Exception as exception: self.__printer(exception)
- self.__smtp = None
- return False
-
- async def ensure_connection(self):
-
- """
- Can be run before the sending operation to confirm that we are yet connected to the server.
- If not connected, this code will reattempt to connect and log-in.
- :return: True if connected, else False.
- """
-
- # If the login had failed initially, the object will be set to null.
- # In such a case, we make an attempt to login:
- if self.__smtp is None:
- return await self.login()
-
- # If the login was successful, we check if the connection is active.
- # If not, we try to re-login:
- if self.__smtp.is_connected:
- return True
- else:
- try:
- await self.__smtp.connect()
- await self.__smtp.starttls()
- await self.__smtp.login(self.__email, self.__password)
- return True
- except Exception as exception:
- self.__printer(exception)
- try: await self.__smtp.quit()
- except Exception as exception: self.__printer(exception)
- self.__smtp = None
- return False
-
- async def logout(self):
-
- """
- Closes the connection to the SMTP client.
- :return: True by default.
- """
-
- if self.__smtp is not None:
- try: await self.__smtp.quit()
- except Exception as exception: self.__printer(exception)
- self.__smtp = None
- return True
-
- async def send(self, mail: MailMessage):
-
- """
- Send out the mail.
- :param mail: The instance of 'MailMessage' with all the content populated.
- :return: A dict with 'success' and 'message'.
- """
-
- # Return with failure if we aren't connected,
- # and our attempt to (re)connect fails:
- if not await self.ensure_connection():
- return {
- "success": False,
- "message": "login failed"
- }
-
- # Comply with the rate-limit:
- for rate_limiter in self.__rate_limiters:
- if not self.__wait_for_turn:
- if not await rate_limiter.has_turn(): return False
- got_turn = await rate_limiter.get_turn()
- if not got_turn:
- return {
- "success": False,
- "message": "rate-limit wait timeout"
- }
-
- # Try to send the message:
- try:
- mail.message["From"] = self.__email
- await self.__smtp.send_message(mail.message)
- return {
- "success": True,
- "message": "mail sent"
- }
-
- # If something goes wrong:
- except Exception as exception:
- self.__printer(exception)
- return {
- "success": False,
- "message": str(exception)
- }
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** MAIN PROGRAM ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-if __name__ == "__main__":
-
- import asyncio
- from utils_v2.string import json
- from utils_v2.mail.mail_message import MailMessage
-
- async def test():
-
- rate_lim = rate_limit_utils.TokenBucket(
- rate_limit = 1,
- seconds = 60.0,
- )
-
- mail_client = AsyncMailClient(
- email = "sender@gmail.com",
- password = "secret_password",
- server = "smtp.gmail.com",
- rate_limiters = rate_lim
- )
-
- my_mail = MailMessage(
- to_email = "recipient@gmail.com",
- subject = "Bhopli is the best!"
- )
- my_mail.add_html(
- """
-
-
-
-
- Sample HTML String
-
-
-
-
Hello, Bhopli!
-
Bhopli is the best, most well-behaved cat in the known universe.
-
-
- """
- )
- my_mail.add_text("This is how you should pet her 👇")
- my_mail.add_inline_image(r"/path/to/image/cat_petting.png")
- my_mail.add_attachment(r"/path/to/file/sample_label.pdf")
-
- await mail_client.login()
- result = await mail_client.send(my_mail)
- print("MAIL RESULT:", json.to_json_string(result))
- await mail_client.logout()
-
- asyncio.run(test())
diff --git a/utils_v2/mail/mail_message.py b/utils_v2/mail/mail_message.py
deleted file mode 100644
index 5887aae..0000000
--- a/utils_v2/mail/mail_message.py
+++ /dev/null
@@ -1,268 +0,0 @@
-"""
-
- AUTHOR:
-
- Khushal P Soonderji
-
- DATE:
-
- Tuesday, 16th Jul, 2024
-
- OBJECTIVE:
-
- To be able to send out mails from code.
-
- REFERENCES:
-
- N/A
-
- DOWNLOADS:
-
- N/A
-
-"""
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** IMPORT ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# To make sibling directories accessible for imports:
-import sys
-sys.path.append(".")
-sys.path.append("..")
-
-# For working with mails:
-import aiosmtplib
-from email.mime.multipart import MIMEMultipart
-from email.mime.image import MIMEImage
-from email.mime.text import MIMEText
-from email.mime.base import MIMEBase
-from email import encoders
-
-# My utils:
-from utils import rate_limit_utils
-
-# Common:
-from shared.statuses import StatusCodes
-
-# For random strings:
-import string
-import random
-
-# For system-level activities:
-import os
-
-# For working with files in RAM:
-import io
-
-# For debugging:
-from icecream import IceCreamDebugger
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** MACROS / ONE-TIME INIT ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# --- Nothing Yet
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** VARIABLES ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# --- Nothing Yet
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** FUNCTIONS ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# --- Nothing Yet
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** CLASSES ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-class MailMessage:
-
- def __init__(self, to_email, subject):
-
- """
- Create an instance of the message that you would like to send.
- :param to_email: The EMail ID of th recipient.
- :param subject: The subject of the mail.
- """
-
- self.message = MIMEMultipart()
- self.message["To"] = to_email
- self.message["Subject"] = subject
-
- def add_text(self, text):
-
- """
- Add plain-text to the mail body.
- :param text: The text to add to the mail body.
- :return: None.
- """
-
- self.message.attach(MIMEText(text, "plain"))
-
- def add_html(self, html_text):
-
- """
- Add HTML text to the mail body.
- :param html_text: The HTML text to add to the mail body.
- :return: None.
- """
-
- self.message.attach(MIMEText(html_text, "html"))
-
- def add_inline_image(self, image_file, content_id = None):
-
- """
- Add an inline image to the body of the mail.
- NOTE: This is NOT the same as sending an image as an attachment.
- :param image_file: The image data to attach to the mail body.
- :param content_id: Inline images are inserted via HTML bocks. This field identifies the image resource. If not
- specified, I will generate a random string. You may write a custom value here if you know what you are
- doing. For most use cases, please ignore this field.
- :return: None.
- """
-
- # Read the image as bytes:
- image_bytes = None
- if type(image_file) is str:
- with open(image_file, "rb") as opened_image_file:
- image_bytes = opened_image_file.read()
- if type(image_file) is io.BytesIO:
- image_file.seek(0)
- image_bytes = image_file.getvalue()
-
- # Declare the part to be attached to the multipart message:
- if image_bytes is not None:
-
- # Create the HTML block if the image pointer is blank:
- if content_id is None:
- content_id = ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(8))
- self.add_html(f"""
-
-
-
-
-
- """)
-
- # Then add the image:
- image_part = MIMEImage(image_bytes)
- image_part.add_header("Content-ID", f"<{content_id}>")
- self.message.attach(image_part)
-
- def add_attachment(self, attachment_file, file_name = None):
-
- # Declare the part to be attached to the multipart message:
- part = MIMEBase("application", "octet-stream")
-
- # If the attachment is a file stored in the local disk:
- if type(attachment_file) is str:
- file_name = file_name or os.path.split(attachment_file)[-1]
- with open(attachment_file, "rb") as attachment:
- part.set_payload(attachment.read())
-
- # If the file is held in RAM:
- if type(attachment_file) is io.BytesIO():
- attachment_file.seek(0)
- part.set_payload(attachment_file.read())
-
- # Encode and attach the file:
- encoders.encode_base64(part)
- part.add_header(
- "Content-Disposition",
- f"attachment; filename= {file_name}",
- )
- self.message.attach(part)
-
- def get_message(self):
- return self.message
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** MAIN PROGRAM ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-if __name__ == "__main__":
-
- import asyncio
- from utils import json_utils
- from utils_v2.mail.async_mail import AsyncMailClient
-
- async def test():
-
- rate_lim = rate_limit_utils.TokenBucket(
- rate_limit = 1,
- seconds = 60.0,
- )
-
- mail_client = AsyncMailClient(
- email = "sender@gmail.com",
- password = "secret_password",
- server = "smtp.gmail.com",
- rate_limiters = rate_lim
- )
-
- my_mail = MailMessage(
- to_email = "recipient@gmail.com",
- subject = "Bhopli is the best!"
- )
- my_mail.add_html(
- """
-
-
-
-
- Sample HTML String
-
-
-
-
Hello, Bhopli!
-
Bhopli is the best, most well-behaved cat in the known universe.
-
-
- """
- )
- my_mail.add_text("This is how you should pet her 👇")
- my_mail.add_inline_image(r"/path/to/image/cat_petting.png")
- my_mail.add_attachment(r"/path/to/file/sample_label.pdf")
-
- await mail_client.login()
- result = await mail_client.send(my_mail)
- print("MAIL RESULT:", json_utils.to_json_string(result))
- await mail_client.logout()
-
- asyncio.run(test())
diff --git a/utils_v2/network/__init__.py b/utils_v2/network/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/utils_v2/network/ip.py b/utils_v2/network/ip.py
deleted file mode 100644
index 134078f..0000000
--- a/utils_v2/network/ip.py
+++ /dev/null
@@ -1,173 +0,0 @@
-"""
-
- AUTHOR:
-
- Khushal P Soonderji
-
- DATE:
-
- Friday, 12th Jul., 2024
-
- OBJECTIVE:
-
- To provide an easy way to get geolocation information of an IP address.
-
- REFERENCES:
-
- 1) https://medium.com/@tubelwj/how-to-retrieve-ip-geolocation-information-in-python-929e15041e3e
-
- DOWNLOADS:
-
- N/A
-
-"""
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** IMPORT ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# To make sibling directories accessible for imports:
-import sys
-sys.path.append(".")
-sys.path.append("..")
-
-# For working with IP Addresses:
-import ipaddress
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** MACROS / ONE-TIME INIT ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# --- Nothing Yet
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** VARIABLES ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# --- Nothing Yet
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** FUNCTIONS ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-def ipv4_to_int(ip_string):
-
- """
- Converts an IP (v4) string to an integer value.
- :param ip_string: The IP address (v4) that you want to convert to integer format.
- :return: An integer representation of the IP (v4) address.
- """
-
- ip_numerical = int(ipaddress.IPv4Address(ip_string))
- return ip_numerical
-
-
-# ---------------------------------------------------------------------------------------------------------------------
-
-
-def int_to_ipv4(ip_numerical):
-
- """
- Interprets the IP (v4) value from the given integer value.
- :param ip_numerical: The integer value that represents an IP (v4) address.
- :return:
- """
-
- ip_string = str(ipaddress.IPv4Address(ip_numerical))
- return ip_string
-
-
-# ---------------------------------------------------------------------------------------------------------------------
-
-
-def ipv4_to_bin(ip_string):
-
- """
- Converts an input IP (v4) address to the binary string that represents the 32 bits.
- :param ip_string: The IP (v4) string in a format like "192.168.0.1"
- :return: The binary representation (as a string) of the input IP address.
- """
-
- ip_binary = bin(int(ipaddress.IPv4Address(ip_string)))[2:].zfill(32)
- return ip_binary
-
-
-# ---------------------------------------------------------------------------------------------------------------------
-
-
-def bin_to_ipv4(ip_binary):
-
- """
- Interprets the IP (v4) value from the given binary string.
- :param ip_binary: The string of 1s and 0s that represents the IP (v4) address.
- :return: The IP (v4) address as a string.
- """
-
- ip_string = str(ipaddress.IPv4Address(int(ip_binary, 2)))
- return ip_string
-
-
-# ---------------------------------------------------------------------------------------------------------------------
-
-
-def get_ipv4_range(ip_string, as_string = True):
-
- """
- Given a network description in the format "88.95.100.128/25", this function tells you the first and last IP
- addresses of that network. Useful for determining if an IP address lies in a network.
- :param ip_string: The input network description in the format "88.95.100.128/25"
- :param as_string: To select between integer and string formats for the IP range output.
- :return: The first and last IP addresses of the input network, and the count.
- """
-
- # Extract the components of the string:
- ip_components = ip_string.split("/")
- ip_addr = ipv4_to_int(ip_components[0])
- ip_bits = int(ip_components[1])
-
- # Convert the mask number to binary representation:
- ip_mask = (1 << ip_bits) - 1
- ip_mask = ip_mask << (32 - ip_bits)
- inv_ip_mask = (~ip_mask) & 0xFFFF
-
- # Figure out the start and end IP addresses:
- start_ip = ip_addr & ip_mask
- end_ip = ip_addr | inv_ip_mask
- count = end_ip - start_ip + 1
-
- # If the IPs are needed as strings, we perform the conversion:
- if as_string:
- start_ip = int_to_ipv4(start_ip)
- end_ip = int_to_ipv4(end_ip)
-
- # Done here:
- return start_ip, end_ip, count
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** MAIN PROGRAM ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-if __name__ == "__main__":
-
- print(ipv4_to_int("255.255.255.255"))
- print(ipv4_to_int("x.x.x.x"))
diff --git a/utils_v2/network/ping.py b/utils_v2/network/ping.py
deleted file mode 100644
index 39e42c7..0000000
--- a/utils_v2/network/ping.py
+++ /dev/null
@@ -1,218 +0,0 @@
-"""
-
- AUTHOR:
-
- Khushal P Soonderji
-
- DATE:
-
- Friday, 28th Jun, 2024
-
- OBJECTIVE:
-
- To provide a way to ping a server and get the traceroute dump.
-
- REFERENCES:
-
- 1) https://www.geeksforgeeks.org/traceroute-implementation-on-python/
-
- DOWNLOADS:
-
- N/A
-
-"""
-
-# *****************************************************************************************************************
-# ***** ****
-# *** IMPORT ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# To make sibling directories accessible for imports:
-# ---
-import sys
-sys.path.append(".")
-sys.path.append("..")
-
-# Utils:
-# ---
-from utils import json_utils
-from utils import time_utils
-from utils import regex_utils
-
-# For networking:
-# ---
-import socket
-from scapy.all import *
-
-# For running the script from the terminal:
-# ---
-import argparse
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** MACROS / ONE-TIME INIT ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# --- Nothing Yet
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** VARIABLES ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# --- Nothing Yet
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** FUNCTIONS ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-def get_name_and_addr(destination):
-
- # If the user provided the IP address:
- # ---
- if (
- regex_utils.match(destination, regex_utils.REGEX_IPV4) or
- regex_utils.match(destination, regex_utils.REGEX_IPV6)
- ):
- try: destination_name = socket.gethostbyaddr(destination)[0]
- except Exception as exception: destination_name = "*"
- destination_ip = destination
-
- # If the provided destination was the domain name:
- # ---
- else:
- destination_name = destination
- try: destination_ip = socket.gethostbyname(destination)
- except Exception as exception: destination_ip = "*"
-
- # Done here:
- # ---
- return destination_name, destination_ip
-
-
-# ---------------------------------------------------------------------------------------------------------------------
-
-
-def tracert(
- destination,
- max_hops = 30,
- timeout = 2.0,
- port = 33434
-):
-
- # Initialize the variables:
- # ---
- destination_name, destination_ip = get_name_and_addr(destination)
- full_trace = []
- ttl = 1
-
- # Keep noting hops till the limit is reached:
- # ---
- while ttl <= max_hops:
-
- # Create a JSON for this stage:
- # ---
- this_hop = {
- "destAddr": destination_ip,
- "destName": destination_name,
- "hopNo": ttl - 1,
- "isDest": False,
- "hopAddr": None,
- "hopName": None,
- "ts": None
- }
-
- # Create the IP and UDP headers and combine them:
- # ---
- ip_packet = IP(dst = destination, ttl = ttl)
- udp_packet = UDP(dport = port)
- trace_packet = ip_packet / udp_packet
-
- # Send the packet and receive a reply and note down the timestamp:
- # ---
- reply = sr1(trace_packet, timeout = timeout, verbose = 0)
- this_hop["ts"] = time_utils.get_current_utc_datetime(as_string = True)
-
- # No response:
- # ---
- if reply is None: this_hop["hopAddr"] = this_hop["hopName"] = "*"
-
- # If some response was received, we note the values and break out if this was the destination hop:
- # ---
- else:
- this_hop["hopName"], this_hop["hopAddr"] = get_name_and_addr(f"{reply.src}")
- if reply.type == 3:
- this_hop["isDest"] = True
- full_trace.append(this_hop)
- break
-
- # Carry on to the next hop:
- # ---
- full_trace.append(this_hop)
- ttl += 1
-
- # Done here:
- # ---
- return full_trace
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** MAIN PROGRAM ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-if __name__ == "__main__":
-
- parser = argparse.ArgumentParser(description = "Traceroute Implementation in Python!")
-
- parser.add_argument(
- "dest",
- help = "Destination (Name or IP address)."
- )
- parser.add_argument(
- "-m",
- "--max-hops",
- type = int,
- default = 30,
- help =
- "Maximum number of hops (default: 30)."
- )
- parser.add_argument(
- "-t",
- "--timeout",
- type = float,
- default = 2.0,
- help = "Timeout for each packet in seconds (default: 2.0)."
- )
- parser.add_argument(
- "-p",
- "--port",
- type = int,
- default = 33434,
- help = "Timeout for each packet in seconds (default: 33434)."
- )
-
- args = parser.parse_args()
-
- trace = tracert(
- destination = args.dest,
- max_hops = args.max_hops,
- timeout = args.timeout,
- port = args.port
- )
- print("TRACE:")
- print(json_utils.to_json_string(trace))
diff --git a/utils_v2/pdf/__init__.py b/utils_v2/pdf/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/utils_v2/pdf/pdf_maker.py b/utils_v2/pdf/pdf_maker.py
deleted file mode 100644
index 5b42c79..0000000
--- a/utils_v2/pdf/pdf_maker.py
+++ /dev/null
@@ -1,1570 +0,0 @@
-"""
-
- AUTHOR:
-
- Khushal P Soonderji
-
- DATE:
-
- Tuesday, 13th Aug., 2024
-
- OBJECTIVE:
-
- To generate custom PDFs.
-
- REFERENCES:
-
- N/A
-
- DOWNLOADS:
-
- N/A
-
-"""
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** IMPORT ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# To make sibling directories accessible for imports:
-import sys
-sys.path.append(".")
-sys.path.append("..")
-
-# For conversion to PDF:
-from PyPDF2 import PdfMerger, PdfReader, PdfWriter, Transformation
-
-# For barcode and QR code generation:
-import xml.etree.ElementTree as ET
-from barcode import Code128
-from barcode.writer import ImageWriter, SVGWriter
-import segno
-from PIL import Image, ExifTags
-
-# System-level activities:
-import os
-import io
-import inspect
-import contextlib
-
-# For calculations:
-import math
-
-# For PDF generation:
-import fitz
-from reportlab.pdfgen import canvas
-from reportlab.lib.units import mm, cm, inch
-from reportlab.lib.styles import ParagraphStyle
-from reportlab.platypus import Paragraph
-from reportlab.pdfbase import pdfmetrics
-from reportlab.pdfbase.ttfonts import TTFont
-from reportlab.lib.utils import ImageReader
-from reportlab.graphics import renderSVG
-
-# My utils:
-from utils_v2.string import json
-from utils_v2.system import files
-from utils_v2.string import regex
-
-# For debugging:
-from icecream import IceCreamDebugger
-import traceback
-
-# For random string generation:
-import random
-import string
-
-# To validate the font file:
-from fontTools import ttLib
-
-# To make http calls:
-import requests
-import httpx
-
-# To work with base64 coding:
-import base64
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** MACROS / ONE-TIME INIT ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# To make async API calls:
-max_connections = 5
-limits = httpx.Limits(
- max_keepalive_connections = max_connections,
- max_connections = max_connections,
- keepalive_expiry = 3600
-)
-http_client = httpx.AsyncClient(
- limits = limits,
- follow_redirects = True
-)
-
-# Headers for downloading files:
-HEADERS_DOWNLOAD = {
- "User-Agent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:129.0) Gecko/20100101 Firefox/129.0",
- "Accept": "*/*",
- "Accept-Encoding": "gzip, deflate"
-}
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** VARIABLES ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# --- Nothing Yet
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** CLASSES ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-class PDFMaker:
-
- def __init__(
- self,
- width,
- height,
- unit = "mm",
- dpi = 72,
- debug = True,
- debug_prefix = "PDF (M) | ",
- raise_exception = True,
- **kwargs
- ):
-
- """
- IMPORTANT: To keep things intuitively oriented (left-to-right, and top-to-bottom), the internal bottom-up
- coordinates have been flipped mathematically. All measurements start from the top-left corner. Moreover, I have
- ensured that all numbers follow the same units. So, if you set your unit to "inch", everything will be measured
- in the same unit. I hope this creates maximum consistency for you.
- A NOTE ON IMAGES: All images will be measured in pixels when operating on them directly (like cropping and
- resizing), but will be measured in the 'unit' of the canvas when placing it in the PDF.
- A NOTE OF FONT SIZES: Font sizes (and their line spacings) are measured in 'points'. As per the standard, there
- are 72 'points' in one inch. While everything else in this utility uses the unit of measurement defined in the
- 'unit' param, fonts need to be specified in 'points' to maintain compatibility with HTML rendering engines to
- work with well-formatted paragraphs.
- WARNING: I STRONGLY RECOMMEND NOT USING DPI AS A SETTING. THE PDF FORMAT ITSELF DOESN'T HAVE ANY PROVISION FOR
- PIXEL DENSITY. THIS SETTING HAS BEEN MADE AVAILABLE FOR PLACEBO EFFECT WHEN CLIENTS ASK FOR VERY HIGH RESOLUTION
- FILES WITHOUT WANTING TO UNDERSTAND ANY TECHNICALITY. USE WITH CAUTION.
- :param width: [int|float] The width of the page (in the specified unit).
- :param height: [int|float] The height of the page (in the specified unit).
- :param unit: [str] The unit ("mm", "cm", "inch") to use.
- :param dpi: [int|float] The pixel density. AVOID USING THIS.
- :param debug: [bool] Whether, or not, you want to print debugging messages. Can be changed on the fly.
- :param debug_prefix: [str] The prefix to show in debugging messages.
- :param raise_exception: If set to True, any failure will immediately stop the PDF building process. If set to
- False, the PDF building will continue by ignoring failed steps. Set to True by default for an all-or-nothing
- output behaviour.
- :param kwargs: Just a placeholder, does nothing for now (20240904).
- """
-
- # For debugging:
- self.__printer = IceCreamDebugger(prefix = debug_prefix, includeContext = True)
- if not debug: self.__printer.disable()
-
- # Note down the exception-handling choice:
- self.__raise_exception = raise_exception
-
- # We pick the unit from the user's choice:
- self.__unit = {
- "mm": mm,
- "cm": cm,
- "inch": inch
- }[unit]
-
- # To know the canvas size multipliers, we need a few parameters:
- # 01. The DPI Factor is the multiplier to apply to achieve the target DPI considering a default DPI of 72,
- # 02. The Unit Factor is the multiplier to apply to the unit to achieve a high-res result.
- self.__dpi_factor = (dpi / 72.0)
- self.__unit_factor = self.__unit * self.__dpi_factor
-
- # Now we create the canvas:
- self.__pdf_in_ram = io.BytesIO()
- self.__canvas_width = width * self.__unit_factor
- self.__canvas_height = height * self.__unit_factor
- self.__canvas = canvas.Canvas(
- self.__pdf_in_ram,
- pagesize = (self.__canvas_width, self.__canvas_height)
- )
-
- def enable_debug(self):
-
- """
- To enable the debugging text in the terminal.
- :return: None.
- """
-
- self.__printer.enable()
-
- def disable_debug(self):
-
- """
- To disable the debugging text in the terminal.
- :return: None.
- """
-
- self.__printer.disable()
-
- # ┏┓┓ ┓ ┳┳ •┓• •
- # ┗┓┣┓┏┓┏┓┏┓┏┫ ┃┃╋┓┃┓╋┓┏┓┏
- # ┗┛┛┗┗┻┛ ┗ ┗┻ ┗┛┗┗┗┗┗┗┗ ┛
-
- @staticmethod
- async def download_from_url(url, follow_redirects = False):
-
- """
- Download a file from a URL.
- :param url: The URL to download the file from.
- :param follow_redirects: Whether, or not, to follow along with any redirects when downloading the file.
- :return: The downloaded file or None.
- """
-
- file = io.BytesIO((await http_client.get(
- url,
- headers = HEADERS_DOWNLOAD,
- follow_redirects = follow_redirects
- )).content)
- file.seek(0)
- return file
-
- @staticmethod
- def read_to_ram(file_path):
-
- """
- Reads a file into a BytesIO object in RAM.
- NOTE: The method doesn't itself verify if the file is a PDF.
- :param file_path: The path to the file on disk.
- :return: The file in a BytesIO object.
- """
-
- with open(file_path, "rb") as file:
- file_data = file.read()
- file_in_ram = io.BytesIO(file_data)
- file_in_ram.seek(0)
- return file_in_ram
-
- def __parse_color(self, input_color):
-
- """
- The reportlab 'reportlab' needs the colour to be specified in an array of RGS values where each value is a
- number in the range 0-1.
- :param input_color: Either a hex string or an array of numbers.
- :return: An array that can be used by 'reportlab'.
- """
-
- # Start by assuming failure:
- processed_color = None
-
- # If the input color is a string, we convert from hex to an array of hex codes:
- if isinstance(input_color, str):
- input_color = input_color.lstrip("#")
- color_tuple = (
- input_color[0:2].upper(),
- input_color[2:4].upper(),
- input_color[4:6].upper()
- )
- input_color = color_tuple
-
- # If the input color is a list/tuple of hex-strings:
- if isinstance(input_color, (list, tuple)):
- try:
- if all(isinstance(item, str) for item in input_color):
- processed_color = [int(item, 16) / 255.0 for item in input_color]
- elif all(isinstance(item, (float, int)) for item in input_color):
- processed_color = [max(min(item, 255.0), 0.0) / 255.0 for item in input_color]
- except:
- if self.__raise_exception: raise
-
- # Done here:
- return processed_color
-
- def __parse_x(self, *args):
-
- """
- To adjust the horizontal coordinates for the canvas.
- :param args: As many horizontal coordinates as you would like to adjust.
- :return: The adjusted coordinates.
- """
-
- if len(args) == 0: return None
- parsed_x = [x * self.__unit_factor for x in args]
- parsed_x = [x if x >= 0 else x + self.__canvas_width for x in parsed_x]
- return tuple(parsed_x) if len(parsed_x) > 1 else parsed_x[0]
-
- def __parse_y(self, *args, flip = False):
-
- """
- To adjust the vertical coordinates for the canvas.
- :param args: As many vertical coordinates as you would like to adjust.
- :param flip: To convert from bottom-up system to top-down system.
- :return: The adjusted coordinates.
- """
-
- if len(args) == 0: return None
- parsed_y = [y * self.__unit_factor for y in args]
- parsed_y = [y if y >= 0 else y + self.__canvas_height for y in parsed_y]
- if flip: parsed_y = [self.__canvas_height - y for y in parsed_y]
- return tuple(parsed_y) if len(parsed_y) > 1 else parsed_y[0]
-
- def __parse_font_size(self, *args):
-
- """
- Adjust the font size.
- :param args: The font sizes to adjust
- :return: The adjusted font-size.
- """
-
- if len(args) == 0: return None
- parsed_font_sizes = args
- return parsed_font_sizes if len(parsed_font_sizes) > 1 else parsed_font_sizes[0]
-
- @staticmethod
- def _parse_coordinates_for_rotation(
- original_x,
- original_y,
- angle
- ):
-
- """
- Adjusts the x and y coordinates to compensate for the effect of a rotation operation.
- :param original_x: The x coordinate before applying rotation.
- :param original_y: The y coordinate before applying rotation.
- :param angle: The angle (in degrees) by which the rotation will be applied.
- :return: The adjusted x and y coordinates that compensate for the effect of the canvas's rotation.
- """
-
- # Get the angle in radians:
- angle_radians = math.radians(angle)
- angle_cos = math.cos(angle_radians)
- angle_sin = math.sin(angle_radians)
-
- # Compute the inverse rotation:
- adjusted_x = original_x * angle_cos + original_y * angle_sin
- adjusted_y = -original_x * angle_sin + original_y * angle_cos
-
- # Done here:
- return adjusted_x, adjusted_y
-
- # ┳ ┏┓ •
- # ┃┏┓┏╋┏┓┏┓┏┏┓ ┃┃┏┓┏┓┏┓┏┓┏┓╋┓┏┓┏
- # ┻┛┗┛┗┗┻┛┗┗┗ ┣┛┛ ┗┛┣┛┗ ┛ ┗┗┗ ┛
- # ┛
-
- @property
- def canvas(self):
-
- """
- Use this to fetch just the canvas with all the inputs applied to it.
- :return: The canvas (page) that is being used currently in the instance.
- """
-
- return self.__canvas
-
- @property
- def unit_factor(self):
-
- """
- The scaling factor after taking into consideration the physical unit of measurement being use (e.g.: "mm").
- :return: The unit-factor.
- """
-
- return self.__unit_factor
-
- @property
- def dpi_factor(self):
-
- """
- The scaling factor derived from comparing the default DPI of 72 and a custom DPI specified when creating the
- instance. Since the PDF format doesn't have a native DPI configuration, this scaling factor will help in
- adjusting the sizes of whatever you wish to add to the canvas.
- :return: The DPI scaling factor.
- """
-
- return self.__dpi_factor
-
- @property
- def shape(self):
-
- """
- an array of the dimensions (width, height) of the canvas.
- :return: The width and height of the canvas.
- """
-
- return self.__canvas_width, self.__canvas_height
-
- # ┏┓ ┏┓ ┏┓•┓
- # ┃ ┏┓┏┓┓┏┏┓┏ ┣╋ ┣ ┓┃┏┓
- # ┗┛┗┻┛┗┗┛┗┻┛ ┗┻ ┻ ┗┗┗
-
- def save(self, path: str = None):
-
- """
- Save the Canvas as a PDF either to RAM or a file on disk.
- :param path: The path you want to save the PDF to on disk. If not specified, the PDF will be saved to RAM and
- returned as a BytesIO object.
- :return: A BytesIO object, or True/False if a path is supplied and the file gets saved.
- """
-
- # Save the changes to the file in RAM.
- self.__canvas.save()
- self.__pdf_in_ram.seek(0)
-
- # If the user has not specified a path, return the buffer:
- if path is None: return self.__pdf_in_ram
-
- # In case a path was specified,
- # We try to save the file in the specified path:
- try:
- with open(path, "wb") as file: file.write(self.__pdf_in_ram.getvalue())
- except Exception as exception:
- self.__printer(exception)
- if self.__raise_exception: raise
- return False
-
- def next_page(self):
-
- """
- To start working on the next page.
- The library that is being used doesn't support moving back and forth between pages in any random order, it,
- instead, needs to go in a sequential fashion from one page to the next.
- :return: None.
- """
-
- self.__canvas.showPage()
-
- # ┏┓
- # ┣ ┏┓┏┓╋┏
- # ┻ ┗┛┛┗┗┛
-
- @staticmethod
- async def register_font_from_url(name, url, follow_redirects = False):
-
- """
- Download a font from a URL and then register it for use.
- :param name: The name you would later refer to the font by.
- :param url: The URL to download the font from.
- :param follow_redirects: Whether, or not, to follow along with any redirects when downloading the file.
- :return: True if registered, else False.
- """
-
- try: return PDFMaker.register_font(
- name,
- await PDFMaker.download_from_url(url, follow_redirects = follow_redirects)
- )
- except Exception as exception:
- return False
-
- @staticmethod
- def register_fonts_from_directory(directory):
-
- """
- Register all the fonts from a directory to be used later in the PDF.
- :param directory: The directory that has the TTF files to register.
- :return: A dict describing the fonts that were registered.
- """
-
- # List out all the files in the folder:
- all_files = files.list_files(directory, full_path = True)
-
- # Keep only TTF files and make a name-to-path map:
- font_map = {
- os.path.split(file)[-1].split(".")[0].lower(): file
- for file in all_files if file.lower().rstrip().endswith(".ttf")
- }
-
- # Register the custom fonts:
- registered_fonts = []
- for font_name, font_path in font_map.items():
- if PDFMaker.register_font(font_name, font_path):
- registered_fonts.append({"name": font_name, "file": font_path})
-
- # Done here:
- return registered_fonts
-
- @staticmethod
- def register_font(name, file):
-
- """
- Register one TTF font from either a BytesIO object or a path on the disk.
- :param name: The name of the font as you would like to use when invoking it.
- :param file: The font file (TTF format).
- :return: True if registered, else False.
- """
-
- try:
-
- if isinstance(file, io.BytesIO): file.seek(0)
- pdfmetrics.registerFont(TTFont(name, file))
- return True
-
- except Exception as exception:
- return False
-
- @staticmethod
- def get_font_height(font, size, count_ascent = True, count_descent = True):
-
- """
- Compute the height of the font for the given size.
- :param font: [str] The name of the font (as registered).
- :param size: [int|float] The size of the font (in points).
- :param count_ascent: [bool] Whether, or not, you would like to consider the ascent of the font.
- :param count_descent: [bool] Whether, or not, you would like to consider the descent of the font.
- :return: The height of the font in points.
- """
-
- # Get font's information and compute the height:
- ascent, descent = pdfmetrics.getAscentDescent(font, size)
- if not count_ascent: ascent = 0.0
- if not count_descent: descent = 0.0
- height = ascent - descent
- return height
-
- @staticmethod
- def font_is_registered(font):
-
- """
- Checks if a particular font is available for use.
- :param font: [str] The name of the font to check.
- :return: True if registered, else False
- """
-
- return font in pdfmetrics.getRegisteredFontNames()
-
- @staticmethod
- def get_first_registered_font():
-
- """
- Returns the name of the first registered font. Useful for cases like those when your font fails to load and you
- need a default to fall back on.
- :return: The name of the first font that has been registered.
- """
-
- return pdfmetrics.getRegisteredFontNames()[0]
-
- @staticmethod
- def list_registered_fonts():
-
- """
- Returns the list of names of all the registered fonts.
- :return: The list of names of the fonts that have been registered.
- """
-
- return pdfmetrics.getRegisteredFontNames()
-
- @staticmethod
- def is_valid_ttf(file):
-
- """
- Checks if a font file is valid and usable or not.
- :param file: [str|io.BytesIO] The file to check.
- :return: True if valid, else False.
- """
-
- if isinstance(file, io.BytesIO): file.seek(0)
- try: font = ttLib.TTFont(file)["head"].fontRevision
- except Exception as exception: return False
- return True
-
- # ┳
- # ┃┏┳┓┏┓┏┓┏┓┏
- # ┻┛┗┗┗┻┗┫┗ ┛
- # ┛
-
- @staticmethod
- def make_barcode_image(
- data,
- bar_width = 2,
- bar_height = 125,
- border = 10,
- bar_color = "#000000",
- background_color = "#FFFFFF",
- dpi = 300,
- format = "png",
- as_pil = True
- ):
-
- """
- Makes a barcode in Code128 format.
- :param data: [str] The data to encode in the barcode.
- :param bar_width: [int] The base bar-module width in pixels.
- :param bar_height: [int] The bar-module height in pixels.
- :param border: [int] The size of the border around all sides of the barcode in pixels.
- :param bar_color: [str] The colour of the bars as a hex string.
- :param background_color: [str] The colour of the background as a hex string.
- :param dpi: [int] The pixel density to note in the image. Leave it to default for most use cases.
- :param format: [str] The file format to save the image in.
- :param as_pil: [bool] If True, a PIL object will be returned, else a PNG file will be returned in RAM (BytesIO).
- :return: Either a PIL object or a PNG file in RAM.
- """
-
- # Create the barcode in RAM:
- barcode_in_ram = io.BytesIO()
- barcode_options = {
- "module_width": (bar_width / dpi) * 25.4,
- "module_height": (bar_height / dpi) * 25.4,
- "quiet_zone": 0,
- "foreground": bar_color,
- "background": background_color,
- "format": format,
- "dpi": dpi
- }
- Code128(str(data), writer = ImageWriter()).write(barcode_in_ram, options = barcode_options)
- barcode_in_ram.seek(0)
-
- # Remove the rasterized text at the bottom, and create a border of the desired size:
- barcode_image = Image.open(barcode_in_ram, formats = [format])
- width, height = barcode_image.size
- barcode_image = barcode_image.crop((
- 0,
- int(dpi * 0.0393701),
- width,
- int(dpi * 0.0393701) + bar_height
- ))
- width, height = barcode_image.size
- background_image = Image.new(
- mode = "RGB",
- size = (width + border + border, height + border + border),
- color = background_color
- )
- background_image.paste(barcode_image, (border, border))
- barcode_image = background_image
-
- # If the user has asked for a PIL object:
- if as_pil: return barcode_image
-
- # Else, ave the PNG in ram:
- barcode_in_ram = io.BytesIO()
- barcode_image.save(barcode_in_ram, format = "png")
- barcode_in_ram.seek(0)
-
- # Done:
- return barcode_in_ram
-
- @staticmethod
- def make_qr_image(
- data,
- scale = 5,
- border = 2,
- foreground_color = "#000000",
- background_color = "#FFFFFF",
- border_color = "#FFFFFF",
- background_art = None,
- format = "png",
- as_pil = True
- ):
-
- """
- Create a QR code.
- :param data: [str] The data that you want to encode in the QR code.
- :param scale: [int] The size (in pixels) for each tiny block in the QR code.
- :param border: [int] The size (in multiples of scale) of the quiet zone.
- :param foreground_color: [str] The color of the foreground, a.k.a. the dark color.
- :param background_color: [str] The color of the background, a.k.a. the light color.
- :param border_color: [str] The color of the quiet zone.
- :param background_art: [io.BytesIO|PIL.Image] Any image that you would like to feed into the background instead
- of a plain QR code. This can be an animated GIF image as well.
- :param format: [str] The type of output file that you want.
- :param as_pil: [bool] If True, a PIL object will be returned, else a PNG file will be returned in RAM (BytesIO).
- :return: Either a PIL object or a PNG file in RAM.
- """
-
- qr_code = segno.make_qr(str(data))
- params = {
- "scale": scale,
- "border": border,
- "dark": foreground_color,
- "light": background_color,
- "quiet_zone": border_color,
- "kind": format
- }
-
- qr_image = io.BytesIO()
-
- if background_art is not None:
- if isinstance(background_art, Image.Image):
- buffer = io.BytesIO()
- background_art.save(buffer, format = format)
- buffer.seek(0)
- background_art = buffer
- params["background"] = background_art
- params["target"] = qr_image
- qr_code.to_artistic(**params)
-
- else:
- params["out"] = qr_image
- qr_code.save(**params)
-
- qr_image.seek(0)
- if as_pil: qr_image = Image.open(qr_image, formats = [format])
- return qr_image
-
- async def image_from_url(self, url, follow_redirects = False, as_pil = True, format = "png"):
-
- """
- Asynchronously downloads an image and returns it as a PIL object.
- Use this instead of just passing the URL to 'draw_image' for better efficiency.
- :param url: [str] The URL to download the image from.
- :param follow_redirects: [bool] Whether, or not, to follow redirect URLs when downloading the file.
- :param as_pil: [bool] If True, a PIL object will be returned, else a PNG file will be returned in RAM (BytesIO).
- :param format: [str] The type of output file that you want. Not applicable for PIL objects.
- :return: The image as a PIL object or as a file in RAM.
- """
-
- try:
-
- pil_image = Image.open(await PDFMaker.download_from_url(url, follow_redirects = follow_redirects))
- if as_pil: return pil_image
- image_in_memory = io.BytesIO()
- pil_image.save(image_in_memory, format = format)
- image_in_memory.seek(0)
- return image_in_memory
-
- except Exception as exception:
- self.__printer(exception)
- if self.__raise_exception: raise
-
- def image_from_base64(self, data, as_pil = True):
-
- """
- Converts a base64 string to an image.
- :param data: [str] The base-64 representation of the image data.
- :param as_pil: [bool] If True, a PIL object will be returned, else a PNG file will be returned in RAM (BytesIO).
- :return: The image as a PIL object or as a file in RAM.
- """
-
- try:
-
- image = io.BytesIO(base64.b64decode(data))
- image.seek(0)
- if not as_pil: return image
- image = Image.open(image)
- return image
-
- except Exception as exception:
- if self.__raise_exception: raise
-
- @staticmethod
- def crop_image_to_aspect_ratio(image, target_aspect_ratio, anchor = "c"):
-
- """
- Shaves off pixels from an image to achieve the target aspect ratio.
- :param image: [PIL.Image] The image to work on, held in a PIL object.
- :param target_aspect_ratio: [int|float] The aspect ratio you want to achieve.
- :param anchor: [str] Which part of the image you want to retain. Imagine it like a compass - "nw", "n", "ne",
- "e", "se", "s", "sw", "w", and "c".
- :return: The adjusted image.
- """
-
- # Get the existing figures:
- width, height = image.size
- original_aspect_ratio = width / height
-
- # Compute the new dimensions:
- if original_aspect_ratio > target_aspect_ratio:
- scaling_factor = target_aspect_ratio / original_aspect_ratio
- new_width, new_height = int(width * scaling_factor), height
- else:
- scaling_factor = original_aspect_ratio / target_aspect_ratio
- new_width, new_height = width, int(height * scaling_factor)
-
- # Now we figure out the start and end points of cropping:
- match anchor.strip().lower():
- case "nw":
- start_x = 0
- start_y = 0
- case "n":
- start_x = int(abs(new_width - width) / 2.0)
- start_y = 0
- case "ne":
- start_x = int(abs(new_width - width))
- start_y = 0
- case "e":
- start_x = int(abs(new_width - width))
- start_y = int(abs(new_height - height) / 2.0)
- case "se":
- start_x = int(abs(new_width - width))
- start_y = int(abs(new_height - height))
- case "s":
- start_x = int(abs(new_width - width) / 2.0)
- start_y = int(abs(new_height - height))
- case "sw":
- start_x = 0
- start_y = int(abs(new_height - height))
- case "w":
- start_x = 0
- start_y = int(abs(new_height - height) / 2.0)
- case _:
- start_x = int(abs(new_width - width) / 2.0)
- start_y = int(abs(new_height - height) / 2.0)
-
- end_x = start_x + new_width
- end_y = start_y + new_height
-
- # And we finally crop and return the image:
- return image.crop((start_x, start_y, end_x, end_y))
-
- @staticmethod
- def crop_image(image, start_x, start_y, end_x, end_y):
-
- """
- Crops an image.
- :param image: [PIL.Image] The image to work on, held in a PIL object.
- :param start_x: [int|float] The left coordinate (in the same unit as the dimension of the canvas).
- :param start_y: [int|float] The top coordinate (in the same unit as the dimension of the canvas).
- :param end_x: [int|float] The right coordinate (in the same unit as the dimension of the canvas).
- :param end_y: [int|float] The bottom coordinate (in the same unit as the dimension of the canvas).
- :return: The cropped image.
- """
-
- return image.crop((start_x, start_y, end_x, end_y))
-
- @staticmethod
- def resize_image_to_aspect_ratio(image, target_aspect_ratio):
-
- """
- Resizes the image (by stretching and squishing) till it fits the target aspect ratio.
- :param image: [PIL.Image] The image to adjust.
- :param target_aspect_ratio: [int|float] The aspect ratio that you want to achieve.
- :return: The adjusted image.
- """
-
- # Get the existing figures:
- width, height = image.size
- original_aspect_ratio = width / height
-
- # Compute the new dimensions:
- if original_aspect_ratio > target_aspect_ratio:
- new_width, new_height = width, int(width / target_aspect_ratio)
- else:
- new_width, new_height = int(height * target_aspect_ratio), height
-
- # Resize and return the image:
- return image.resize((new_width, new_height))
-
- @staticmethod
- def resize_image(image, width, height):
-
- """
- Resizes an image to the specified width and height.
- :param image: [PIL.Image] The image as a PIL object.
- :param width: [int|float] The new width.
- :param height: [int|float] The new height.
- :return: The resized image.
- """
-
- return image.resize((width, height))
-
- @staticmethod
- def adjust_image_orientation_from_exif(image):
-
- """
- Rotates the image as per the instructions in the EXIF tags.
- :param image: The PIL object that holds the image data.
- :return: The adjusted image.
- """
-
- # Guard clause to return if the attribute is not even found:
- if not hasattr(image, "getexif"): return image
-
- # Else we start extracting EXIF data:
- exif = image.getexif()
- if exif is None: return image
-
- # We look for the tag that indicates orientation:
- # ORIENTATION CODES:
- # 1: Normal (Landscape)
- # 2: Flipped horizontally
- # 3: Upside down (Reverse Landscape)
- # 4: Flipped vertically
- # 5: Rotated 90° clockwise and flipped horizontally
- # 6: Rotated 90° clockwise (Portrait)
- # 7: Rotated 90° counter-clockwise and flipped horizontally
- # 8: Rotated 90° counter-clockwise (Reverse Portrait)
- orientation = exif.get(274, 0)
- if orientation == 1: pass
- elif orientation == 2: image = image.transpose(Image.FLIP_LEFT_RIGHT)
- elif orientation == 3: image = image.rotate(180, expand = True)
- elif orientation == 4: image = image.rotate(180, expand = True).transpose(Image.FLIP_LEFT_RIGHT)
- elif orientation == 5: image = image.rotate(270, expand = True).transpose(Image.FLIP_LEFT_RIGHT)
- elif orientation == 6: image = image.rotate(270, expand = True)
- elif orientation == 7: image = image.rotate(90, expand = True).transpose(Image.FLIP_LEFT_RIGHT)
- elif orientation == 8: image = image.rotate(90, expand = True)
-
- # Done here:
- return image
-
- # ┏┓┳┓┏
- # ┃┃┃┃╋
- # ┣┛┻┛┛
-
- def draw_line(
- self,
- start_x,
- start_y,
- end_x,
- end_y,
- thickness = 1.0,
- color = (0, 0, 0),
- alpha = 1.0
- ):
-
- """
- Draw a line in the PDF page.
- :param start_x: [int|float] The left coordinate.
- :param start_y: [int|float] The top coordinate.
- :param end_x: [int|float] The right coordinate.
- :param end_y: [int|float] The bottom coordinate.
- :param thickness: [int|float] The thickness of the line (in the same unit as the canvas's dimensions).
- :param color: [str] The RGB color in an array.
- :param alpha: [int|float] The opacity of the line. 1 is 100% and 0 is 0%
- :return: True or False based on the success of the operation.
- """
-
- # Handle color:
- color = self.__parse_color(color)
-
- # Adjust the dimensions:
- thickness *= self.__unit_factor
- start_x, end_x = self.__parse_x(start_x, end_x)
- start_y, end_y = self.__parse_y(start_y, end_y, flip = True)
-
- try:
-
- self.__canvas.setLineWidth(thickness)
- self.__canvas.setStrokeColorRGB(color[0], color[1], color[2], alpha)
- self.__canvas.line(
- start_x,
- start_y,
- end_x,
- end_y
- )
- return True
-
- # In case something goes wrong:
- except Exception as exception:
- self.__printer(exception)
- if self.__raise_exception: raise
- return False
-
- def draw_circle(
- self,
- x,
- y,
- radius,
- fill_color = (255, 255, 255),
- fill_alpha = 0.0,
- stroke_color = (0, 0, 0),
- stroke_alpha = 1.0,
- thickness = 1.0
- ):
-
- """
- Draw a circle in the PDF page.
- :param x: [int|float] The horizontal coordinate of the center of the circle.
- :param y: [int|float] The vertical coordinate of the center of the circle.
- :param radius: [int|float] The radius of the circle.
- :param fill_color: [str] The color-array (0-255) or hex string to define the inner fill color of the rectangle.
- :param fill_alpha: [int|float] The opacity of the inner fill color.
- :param stroke_color: [str] The color-array (0-255) or hex string to define the outline color of the rectangle.
- :param stroke_alpha: [int|float] The opacity of the outline color.
- :param thickness: [int|float] The thickness of the line (in the same unit as the canvas's dimensions).
- :return: True or False based on the success of the operation.
- """
-
- try:
-
- # Handle color:
- fill_color = self.__parse_color(fill_color)
- stroke_color = self.__parse_color(stroke_color)
-
- # Adjust the dimensions:
- x = self.__parse_x(x)
- y = self.__parse_y(y, flip = True)
- thickness *= self.__unit_factor
- radius *= self.__unit_factor
-
- # Draw the circle here:
- self.__canvas.setLineWidth(thickness)
- self.__canvas.setFillColorRGB(fill_color[0], fill_color[1], fill_color[2])
- self.__canvas.setFillAlpha(fill_alpha)
- self.__canvas.setStrokeColorRGB(stroke_color[0], stroke_color[1], stroke_color[2])
- self.__canvas.setStrokeAlpha(stroke_alpha)
- self.__canvas.circle(x, y, radius, stroke = 1, fill = 1)
-
- # In case something goes wrong:
- except Exception as exception:
- self.__printer(exception)
- if self.__raise_exception: raise
- return False
-
- def draw_rect(
- self,
- start_x,
- start_y,
- end_x,
- end_y,
- radius = 0.0,
- fill_color = (255, 255, 255),
- fill_alpha = 0.0,
- stroke_color = (0, 0, 0),
- stroke_alpha = 1.0,
- thickness = 1.0,
- ):
-
- """
- Draws a rectangle on the page.
- :param start_x: [int|float] The left coordinate.
- :param start_y: [int|float] The top coordinate.
- :param end_x: [int|float] The right coordinate.
- :param end_y: [int|float] The bottom coordinate.
- :param radius: [int|float] The corner radius.
- :param fill_color: [str] The color-array (0-255) or hex string to define the inner fill color of the rectangle.
- :param fill_alpha: [int|float] The opacity of the inner fill color.
- :param stroke_color: [str] The color-array (0-255) or hex string to define the outline color of the rectangle.
- :param stroke_alpha: [int|float] The opacity of the outline color.
- :param thickness: [int|float] The line thickness of the outline (in the same unit as the canvas's dimensions).
- :return: True or False based on the success of the operation.
- """
-
- try:
-
- # Handle color:
- fill_color = self.__parse_color(fill_color)
- stroke_color = self.__parse_color(stroke_color)
-
- # Adjust the dimensions:
- thickness *= self.__unit_factor
- start_x, end_x = self.__parse_x(start_x, end_x)
- start_y, end_y = self.__parse_y(start_y, end_y, flip = False)
-
- # Compute the width and height desired by the user:
- width = end_x - start_x
- height = end_y - start_y
-
- # Draw the rectangle here:
- self.__canvas.setLineWidth(thickness)
- self.__canvas.setFillColorRGB(fill_color[0], fill_color[1], fill_color[2])
- self.__canvas.setFillAlpha(fill_alpha)
- self.__canvas.setStrokeColorRGB(stroke_color[0], stroke_color[1], stroke_color[2])
- self.__canvas.setStrokeAlpha(stroke_alpha)
- self.__canvas.roundRect(
- start_x,
- self.__canvas_height - start_y - height,
- width,
- height,
- radius * self.__unit_factor,
- stroke = 1,
- fill = 1
- )
-
- return True
-
- # In case something goes wrong:
- except Exception as exception:
- self.__printer(exception)
- if self.__raise_exception: raise
- return False
-
- def make_paragraph_style(
- self,
- font_name = "Helvetica",
- font_size = 12,
- font_color = (0, 0, 0),
- line_spacing = 12,
- border_size = 0.0,
- border_color = (255, 255, 255),
- align = "left",
- style_name = "CustomStyle"
- ):
-
- """
- Creates a text style for using in a paragraph.
- :param font_name: [str] The name of the font. Has to be registered.
- :param font_size: [int|float] The size that you want the font to be printed in.
- :param font_color: [str] The color-array (0-255) or hex string that you want the font to be printed in.
- :param line_spacing: [int|float] The line spacing to apply.
- :param border_size: [int|float] The thickness of the border.
- :param border_color: [str] The color-array (0-255) or hex string of the color you wan the border to be in.
- :param align: [str] Text alignment ("left", "center", or "right").
- :param style_name: [str] A name that you would like to give this style.
- :return: The paragraph style.
- """
-
- # Make adjustments:
- font_color = self.__parse_color(font_color)
- font_size = self.__parse_font_size(font_size)
- border_color = self.__parse_color(border_color)
- border_size *= self.__unit_factor
-
- return ParagraphStyle(
- name = style_name,
- fontName = font_name,
- fontSize = font_size,
- textColor = (font_color[0], font_color[1], font_color[2]),
- alignment = {
- "left": 0,
- "center": 1,
- "right": 2
- }[align],
- leading = line_spacing * font_size,
- spaceBefore = 0,
- spaceAfter = 0,
- borderWidth = border_size,
- borderColor = border_color
- )
-
- def write_paragraph(
- self,
- text,
- start_x,
- start_y,
- end_x,
- end_y,
- alpha = 1.0,
- style = None
- ):
-
- """
- To write text into a bounding box in the PDF.
- This can be either plaintext or an HTML string.
- :param text: [str] The text to type in the bounding box.
- :param start_x: [int|float] The left coordinate (in the same unit as used while setting up the canvas).
- :param start_y: [int|float] The top coordinate (in the same unit as used while setting up the canvas).
- :param end_x: [int|float] The right coordinate (in the same unit as used while setting up the canvas).
- :param end_y: [int|float] The bottom coordinate (in the same unit as used while setting up the canvas).
- :param alpha: [int|float] The opacity of the text. 1 is 100% and 0 is 0%
- :param style: The paragraph-style generated by using "make_paragraph_style" method. Avoid using this if you are
- passing in an HTML string.
- :return: True or False based on the success of the operation. You will get a False even if the text flows out of
- the bounding box (which will cause it to not render).
- """
-
- try:
-
- # Adjust the dimensions:
- start_x, end_x = self.__parse_x(start_x, end_x)
- start_y, end_y = self.__parse_y(start_y, end_y, flip = False)
-
- # Compute the width and height desired by the user:
- width = end_x - start_x
- height = end_y - start_y
-
- # Make the paragraph:
- paragraph = Paragraph(text, style = style)
-
- # Wrap the paragraph to compute the needed dimensions:
- needed_width, needed_height = paragraph.wrap(width, height)
-
- # Adjust the y coordinates (for top-down system):
- start_y = self.__canvas_height - start_y - needed_height
-
- # Write the text to the canvas if the text fits.
- # Return true in case of success, else false:
- if width >= needed_width and height >= needed_height:
- self.__canvas.setFillAlpha(alpha)
- paragraph.drawOn(self.__canvas, start_x, start_y, _sW = 0)
- return True
- else: return False
-
- # In case something goes wrong:
- except Exception as exception:
- self.__printer(exception)
- if self.__raise_exception: raise
- return False
-
- def write_string(
- self,
- text,
- x,
- y,
- font = "Helvetica",
- size = 12,
- color = (0, 0, 0),
- alpha = 1.0,
- align = "left",
- angle = 0
- ):
-
- """
- Writes a string onto the canvas.
- :param text: [str] The text to be written.
- :param x: [int|float] The horizontal coordinate (of the baseline).
- :param y: [int|float] The vertical coordinate (of the baseline).
- :param font: [str] The name of the font to be used.
- :param size: [int|float] The size of the font to be used (in points).
- :param color: [str] The color-array (0-255) or hex string that you want the font to be printed in.
- :param alpha: [int|float] The opacity of the text. 1 is 100% and 0 is 0%
- :param align: [str] The alignment to use ("left", "center", "right").
- :param angle: The angle at which you want the text to be printed in.
- :return: True or False based on the success of the operation.
- """
-
- try:
-
- # Handle the color
- color = self.__parse_color(color)
-
- # Adjust the dimensions:
- x = self.__parse_x(x)
- y = self.__parse_y(y, flip = True)
- size = self.__parse_font_size(size)
-
- # Adjust the canvas's values:
- self.__canvas.setFont(font, size)
- self.__canvas.setFillColorRGB(color[0], color[1], color[2])
- self.__canvas.setFillAlpha(alpha)
-
- # Handle angular adjustments:
- self.__canvas.rotate(angle)
- x, y = self._parse_coordinates_for_rotation(
- original_x = x,
- original_y = y,
- angle = angle
- )
-
- # Adjust the font coordinates as per the angle:
- text_width = self.__canvas.stringWidth(text, fontName = font, fontSize = size)
- if align == "center": x -= text_width / 2
- elif align == "right": x -= text_width
-
- # Draw the string:
- self.__canvas.drawString(x, y, text)
-
- # Reset angular adjustments:
- self.__canvas.rotate(-angle)
-
- # Return with success if nothing broke till here:
- return True
-
- # In case something goes wrong:
- except Exception as exception:
- self.__printer(exception)
- if self.__raise_exception: raise
- return False
-
- def draw_image(
- self,
- image,
- start_x,
- start_y,
- end_x,
- end_y,
- crop = False,
- crop_anchor = "c",
- alpha = 1.0,
- stroke_color = (0, 0, 0),
- stroke_alpha = 0.0,
- thickness = 1.0,
- identifier: str = None,
- format = "jpeg"
- ):
-
- """
- Draw an image onto the PDF page in the specified bounding box. The image can be drawn in either "fit" mode where
- the image is scaled down to fit in the bounding box, or in "fill" mode where the image is cropped to fill up the
- space in the bounding box by matching their aspec ratios.
- :param image: The image data as a PIL object or a valid URL or a file path. NOTE: If given as a URL, the image
- will be downloaded synchronously.
- :param start_x: [int|float] The left coordinate.
- :param start_y: [int|float] The top coordinate.
- :param end_x: [int|float] The right coordinate.
- :param end_y: [int|float] The bottom coordinate.
- :param crop: [bool] Set to True for "fill" mode, False for "fit" mode.
- :param crop_anchor: [str] If cropping is to be performed, what should the anchor be for cropping. Refer to the
- description in 'crop_image_to_aspect_ratio'.
- :param alpha: [int|float] The opacity of the image. 1 is 100% and 0 is 0%
- :param stroke_color: [str] The color-array (0-255) or hex string to define the outline color of the rectangle.
- :param stroke_alpha: [int|float] The opacity of the outline color.
- :param thickness: [int|float] The line thickness of the outline (in the same unit as the canvas's dimensions).
- :param identifier: [str] A unique name for the image.
- :param format: The file format of the image (when it will be embedded in the PDF).
- :return: True or False based on the success of the operation.
- """
-
- try:
-
- # Cleaning:
- format = format.strip().lower().split(".")[-1]
-
- # Handle color:
- stroke_color = self.__parse_color(stroke_color)
-
- # The identifier can allow us to use the same image multiple times in a PDF file.
- # If the identifier is not provided, we give the file a random identifier.
- if identifier is None: identifier = "".join([random.choice(string.ascii_lowercase) for _ in range(8)])
-
- # Adjust the dimensions:
- start_x, end_x = self.__parse_x(start_x, end_x)
- start_y, end_y = self.__parse_y(start_y, end_y, flip = True)
-
- # Compute the width and height desired by the user:
- width = end_x - start_x
- height = end_y - start_y
-
- # In case the image is passed as a URL, we try to download it and open it as a PIL object:
- if isinstance(image, str):
- if image.startswith("https://") or image.startswith("http://"):
- image = Image.open(io.BytesIO(requests.get(image).content))
- else: image = Image.open(image)
-
- # The image can have orientation data in its EXIF tags.
- # If so, we adjust for it so that reportlab can work with it properly:
- image = self.adjust_image_orientation_from_exif(image)
-
- # If the image needs to be cropped, we compute the aspect ratio and get the image adjusted:
- if crop: image = self.crop_image_to_aspect_ratio(
- image,
- abs(end_x - start_x) / abs(end_y - start_y),
- anchor = crop_anchor
- )
-
- # We set the image opacity and outline settings here:
- self.__canvas.setFillAlpha(alpha)
- self.__canvas.setLineWidth(thickness * self.__unit_factor)
- self.__canvas.setStrokeColorRGB(stroke_color[0], stroke_color[1], stroke_color[2])
- self.__canvas.setStrokeAlpha(stroke_alpha)
-
- # Draw the image onto the canvas:
- if (
- image.mode == "P" or
- format in ["jpeg", "jpg"]
- ):
- image = image.convert("RGB")
- image_buffer = io.BytesIO()
- image.save(image_buffer, format = format)
- image_buffer.seek(0)
- self.__canvas.drawImage(
- ImageReader(image_buffer, ident = identifier),
- start_x,
- start_y,
- width = width,
- height = height,
- preserveAspectRatio = True,
- mask = "auto",
- anchor = "c",
- showBoundary = True
- )
-
- return True
-
- # In case something goes wrong:
- except Exception as exception:
- self.__printer(exception)
- if self.__raise_exception: raise
- return False
-
- # def draw_svg(
- # self,
- # svg,
- # start_x,
- # start_y,
- # end_x,
- # end_y
- # ):
- #
- # renderSVG.draw(svg, sta)
-
- # ┏┓┳┓┏ ┏┳┓ ┓
- # ┃┃┃┃╋ ┃ ┏┓┏┓┃┏
- # ┣┛┻┛┛ ┻ ┗┛┗┛┗┛
-
- @staticmethod
- def make_thumbnails(
- pdf_file,
- pages = None,
- scale = 1.0,
- as_pil = True,
- format = "png"
- ):
-
- """
- Makes thumbnails of pages of the PDF and returns them
- :param pdf_file: The PDF file whose pages must be converted to thumbnails.
- :param pages: The list of page nos. to make thumbnails of. If not specified, all pages will be converted.
- :param scale: The scaling multiplier, applied to both axes, to change the output size.
- :param as_pil: If True, the output will be an array of PIL objects, else the output will be an array of
- file-like BytesIO objects.
- :param format: The file format of the output image.
- :return: The array of thumbnails, either as PIL objects, or as BytesIO objects.
- """
-
- # Open the PDF, and ensure that we have the pages numbers:
- if isinstance(pdf_file, io.BytesIO):
- pdf_file.seek(0)
- pdf_document = fitz.open(stream = pdf_file)
- pdf_file.seek(0)
- else: pdf_document = fitz.open(pdf_file)
- if pages is None: pages = list(range(len(pdf_document)))
-
- # Iterate over the pages and make the thumbnails:
- thumbnails = []
- for page_no in pages:
- img = pdf_document.load_page(page_no).get_pixmap(matrix = fitz.Matrix(scale, scale))
- img = io.BytesIO(img.tobytes(format))
- if as_pil: img = Image.open(img, formats = [format])
- thumbnails.append(img)
-
- # Done here:
- return thumbnails
-
- @staticmethod
- def join_from_ram(pdf_files, raise_exception = True):
-
- """
- Joins a set of PDF files held in RAM (as BytesIO objects).
- :param pdf_files: The list of PDF files (as BytesIO objects).
- :param raise_exception: Set to True for an all-or-nothing process.
- :return: The joined PDF file (as a BytesIo object).
- """
-
- try:
-
- # Create a file in RAM that will hold the merged PDF:
- joined_pdf_in_ram = io.BytesIO()
-
- # Merge the files into one:
- merger = PdfMerger()
- for pdf_file in pdf_files: merger.append(pdf_file)
- merger.write(joined_pdf_in_ram)
- merger.close()
-
- # Done here:
- joined_pdf_in_ram.seek(0)
- return joined_pdf_in_ram
-
- # In case something goes wrong:
- except Exception as exception:
- if raise_exception: raise
- return None
-
- @staticmethod
- def grid_from_ram(
- pdf_files,
- input_width,
- input_height,
- output_width,
- output_height,
- cutting_width,
- cutting_height,
- unit = "mm",
- dpi = 72
- ):
-
- """
- Lays out a set of input PDFs into a larger pdf in a grid.
- NOTE: The 'unit' and 'dpi' choice will be used for both (input and output).
- WARNING: I STRONGLY RECOMMEND NOT USING DPI AS A SETTING. THE PDF FORMAT ITSELF DOESN'T HAVE ANY PROVISION FOR
- PIXEL DENSITY. THIS SETTING HAS BEEN MADE AVAILABLE FOR PLACEBO EFFECT WHEN CLIENTS ASK FOR VERY HIGH RESOLUTION
- FILES WITHOUT WANTING TO UNDERSTAND ANY TECHNICALITY. USE WITH CAUTION.
- :param pdf_files: The list of PDF files (as BytesIO objects) to lay onto the larger canvas.
- :param input_width: The width of the input PDFs.
- :param input_height: The height of the input PDFs
- :param output_width: The width of the large PDF on which the smaller PDFs will be laid out.
- :param output_height: The height of the large PDF on which the smaller PDFs will be laid out.
- :param cutting_width: The horizontal buffer to leave between the grid.
- :param cutting_height: The vertical buffer to leave between the grid.
- :param unit: The unit to use (common to the input and output PDFs).
- :param dpi: The pixel density to consider (common to the input and output PDFs).
- :return: The PDF grid.
- """
-
- # Create the base PDF:
- base_canvas = PDFMaker(
- width = output_width,
- height = output_height,
- unit = unit,
- dpi = dpi
- )
- base_canvas.draw_line(0, 1, 1, 0, alpha = 0.0)
- base_pdf = base_canvas.save()
-
- # Adjust all units to match the pixel density adjustments:
- input_width *= base_canvas.unit_factor
- input_height *= base_canvas.unit_factor
- output_width *= base_canvas.unit_factor
- output_height *= base_canvas.unit_factor
- cutting_width *= base_canvas.unit_factor
- cutting_height *= base_canvas.unit_factor
-
- # Calculate how many input pdfs will fit and the page count needed:
- per_row = math.floor((output_width - cutting_width) / (input_width + cutting_width))
- per_column = math.floor((output_height - cutting_height) / (input_height + cutting_height))
- per_page = per_row * per_column
- output_page_count = math.ceil(len(pdf_files) / per_page)
-
- # Calculate the starting point on the page from where the pasting will start:
- page_start_x = (output_width - (per_row * (input_width + cutting_width)) + cutting_width) / 2.0
- page_start_y = (output_height - (per_column * (input_height + cutting_height)) + cutting_height) / 2.0
-
- # We make a writer object to write pages to the output file,
- # and we make a list that will hold the individual output pages:
- grid_pdf = PdfWriter()
-
- # We create each needed page:
- for page_number in range(output_page_count):
-
- # Open the base PDF and create a page here:
- grid_page = PdfReader(base_pdf).pages[0]
-
- # Shortlist the input pdfs to be pasted on this page:
- starting_offset = page_number * per_page
- ending_offset = starting_offset + per_page
- ending_offset = min(ending_offset, len(pdf_files))
- pdf_files_for_page = pdf_files[starting_offset:ending_offset]
-
- # We paste all the input PDFs one-by-one:
- for index, input_pdf in enumerate(pdf_files_for_page):
-
- # Compute the coordinates to paste on:
- row_number = math.floor(index / per_row)
- column_number = index if index < per_row else index % per_row
- paste_x = page_start_x + (column_number * input_width) + (column_number * cutting_width)
- paste_y = page_start_y + (row_number * input_height) + (row_number * cutting_height)
-
- # The default system for the reportlab is bottom-up,
- # we convert to top-down for intuitive operations:
- paste_y = base_canvas.shape[1] - paste_y - input_height
-
- # Open th smaller PDF and paste it on those coordinates:
- input_page = PdfReader(input_pdf).pages[0]
- input_page.add_transformation(Transformation().translate(
- tx = paste_x,
- ty = paste_y
- ), expand = True)
- grid_page.merge_page(input_page)
-
- # Add the grid page to the grid PDF:
- grid_pdf.add_page(grid_page)
-
- # Join and return the result:
- grid_pdf_in_ram = io.BytesIO()
- grid_pdf.write(grid_pdf_in_ram)
- return grid_pdf_in_ram
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** MAIN PROGRAM ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-if __name__ == "__main__":
-
- pass
-
diff --git a/utils_v2/queue/__init__.py b/utils_v2/queue/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/utils_v2/queue/async_kafka.py b/utils_v2/queue/async_kafka.py
deleted file mode 100644
index d6c0a82..0000000
--- a/utils_v2/queue/async_kafka.py
+++ /dev/null
@@ -1,554 +0,0 @@
-"""
-
- AUTHOR:
-
- Khushal P Soonderji
-
- DATE:
-
- Saturday, 13th Jul, 2024
-
- OBJECTIVE:
-
- To be able to work with keys
-
- REFERENCES:
-
- N/A
-
- DOWNLOADS:
-
- N/A
-
-"""
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** IMPORT ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# To make sibling directories accessible for imports:
-import sys
-sys.path.append(".")
-sys.path.append("..")
-
-# To use Kafka:
-from aiokafka import AIOKafkaProducer
-from aiokafka import AIOKafkaConsumer
-
-# For working with JSON strings:
-from utils_v2.string import json
-from utils_v2.serialization.json_serializer import JSONSerializer
-
-# For debugging:
-from icecream import IceCreamDebugger
-
-# For SSL security:
-import ssl
-
-# For asynchronous activities:
-import asyncio
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** MACROS / ONE-TIME INIT ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# --- Nothing Yet
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** VARIABLES ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# --- Nothing Yet
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** FUNCTIONS ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-def get_ssl_context(
- ca_file,
- cert_file,
- key_file
-):
-
- """
- Generate the SSL context to use with the Kafka instances.
- :param ca_file: The Certificate Authority file as a path to a local file.
- :param cert_file: The Certificate file as a path to a local file.
- :param key_file: The Key file as a path to a local file.
- :return: The SSL context instance as a path to a local file.
- """
-
- ssl_context = ssl.create_default_context()
- ssl_context.load_verify_locations(ca_file)
- ssl_context.load_cert_chain(certfile = cert_file, keyfile = key_file)
- return ssl_context
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** CLASSES ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-class ProducerKafka:
-
- def __init__(
- self,
- topic,
- serializer = None,
- debug = True,
- debug_prefix = "Kafka (P) | ",
- **kwargs
- ):
-
- """
- Create a Kafka Producer.
- :param topic: The topic to produce on.
- :param serializer: The serializer to use.
- :param debug: Whether, or not, you want to print the debug strings.
- :param debug_prefix: The prefix to use while debugging.
- :param kwargs: Any configuration parameters for the Kafka instances.
- """
-
- # Initialize the debugger:
- self.__printer = IceCreamDebugger(prefix = debug_prefix, includeContext = True)
- if not debug: self.__printer.disable()
-
- # initialize the Kafka producer:
- self.__topic = topic
- self.__kwargs = kwargs
- self.__producer = None
- self.__connected = False
- self.__serializer = serializer or JSONSerializer()
-
- # For establishing connection:
- self.__exclusive_semaphore = asyncio.Semaphore(1)
-
- def enable_debug(self):
- self.__printer.enable()
-
- def disable_debug(self):
- self.__printer.disable()
-
- async def connect(self):
-
- """
- Connects to the Kafka server if not connected.
- :return: True or False based on the success of the operation.
- """
-
- async with self.__exclusive_semaphore:
- if not self.__connected:
- try:
- self.__producer = AIOKafkaProducer(**self.__kwargs)
- await self.__producer.start()
- self.__connected = True
- except Exception as exception: self.__printer(exception)
- return self.__connected
-
- async def ensure_connection(self):
-
- """
- Connects to the Kafka server if not connected.
- :return: True or False based on the success of the operation.
- """
-
- if not self.__connected: await self.connect()
- return self.__connected
-
- async def close(self):
-
- """
- Terminates the connection.
- :return: None.
- """
-
- if self.__connected:
- try:
- await self.__producer.stop()
- self.__printer("Producer closed!")
- self.__connected = False
- except Exception as exception: self.__printer(exception)
-
- async def produce(self, value, key = None, topic = None, encoding = "utf-8"):
-
- """
- Sends one message to the Kafka server on the topic that has been set for this instance.
- :param value: The message to send.
- :param key: The key to use when you want the messages to follow an order.
- :param topic: A custom topic for this message, else the topic defined during the creation of this instance will
- be used by default.
- :param encoding: The encoding format.
- :return: True or False based on the success of the operation.
- """
-
- # Ensure connectivity to the server.
- # If not connected, return with failure immediately.
- if not await self.ensure_connection(): return False
-
- try:
-
- # Send the message:
- await self.__producer.send_and_wait(
- topic = topic or self.__topic,
- value = self.__serializer.serialize(data = value, encoding = encoding),
- key = key
- )
-
- # Return with success if no exception occurred:
- return True
-
- # Return with failure if something went wrong:
- except Exception as exception:
- self.__printer(exception, self.__topic, type(value), value)
- return False
-
-
-# ---------------------------------------------------------------------------------------------------------------------
-
-
-class ConsumerKafka:
-
- def __init__(
- self,
- topic,
- serializer = None,
- debug = True,
- debug_prefix = "Kafka (C) | ",
- **kwargs
- ):
-
- """
- Create a Kafka Consumer.
- :param topic: The topic to consumer on.
- :param serializer: The serializer to use.
- :param debug: Whether, or not, you want to print the debug strings.
- :param debug_prefix: The prefix to use while debugging.
- :param kwargs: Any configuration parameters for the Kafka instances.
- """
-
- # Initialize the debugger:
- self.__printer = IceCreamDebugger(prefix = debug_prefix, includeContext = True)
- if not debug: self.__printer.disable()
-
- # initialize the Kafka producer:
- self.__topic = topic
- self.__kwargs = kwargs
- self.__consumer = None
- self.__connected = False
- self.__serializer = serializer or JSONSerializer()
-
- # For establishing connection:
- self.__exclusive_semaphore = asyncio.Semaphore(1)
-
- def enable_debug(self):
- self.__printer.enable()
-
- def disable_debug(self):
- self.__printer.disable()
-
- async def connect(self):
-
- """
- Connects to the Kafka server if not connected.
- :return: True or False based on the success of the operation.
- """
-
- async with self.__exclusive_semaphore:
- if not self.__connected:
- try:
- self.__consumer = AIOKafkaConsumer(self.__topic, **self.__kwargs)
- await self.__consumer.start()
- self.__connected = True
- except Exception as exception: self.__printer(exception)
- return self.__connected
-
- async def ensure_connection(self):
-
- """
- Connects to the Kafka server if not connected.
- :return: True or False based on the success of the operation.
- """
-
- if not self.__connected: await self.connect()
- return self.__connected
-
- async def close(self):
-
- """
- Terminates the connection.
- :return: None.
- """
-
- if self.__connected:
- try:
- await self.__consumer.stop()
- self.__printer("Consumer closed!")
- self.__connected = False
- except Exception as exception: self.__printer(exception)
-
- async def consume(self, count = 1, timeout = 0.05, encoding = "utf-8"):
-
- """
- Get messages from the Kafka server.
- :param count: The number of messages to get from the Kafka server.
- :param timeout: The time in seconds to wait for retrieval.
- :param encoding: The encoding to use.
- :return: The messages that were received. If no messages are available, an empty list will be returned.
- """
-
- # Ensure connectivity to the server.
- # If not connected, return with failure immediately.
- if not await self.ensure_connection(): return []
-
- # Make a variable that will hold the final results:
- messages = []
-
- try:
-
- # Read some messages:
- results = await self.__consumer.getmany(
- max_records = max(1, count),
- timeout_ms = int(timeout * 1_000)
- )
-
- # Format the received messages:
- if results:
- for topic_partition, records in results.items():
- for record in records:
- record_dict = record.__dict__
- record_dict["value"] = self.__serializer.deserialize(
- data = record_dict["value"],
- encoding = encoding
- )
- messages.append(record_dict)
-
- # Debugging print if something went wrong:
- except Exception as exception: self.__printer(exception)
-
- # Done here:
- return messages
-
-
-# ---------------------------------------------------------------------------------------------------------------------
-
-
-class BidirectionalKafka:
-
- # The 'roles' that the instance can take.
- # The master talks on the channel (topic) that the slave listens on and vice versa.
- # Master-Slave is only for deciding who talks on which channel and who listens on which.
- # In a two-party system, one must be the master, the other must be the slave.
- # There are no extra privileges that the master enjoys. The naming convention was borrowed from common protocols
- # used in electronics (like I2C).
- ROLE_MASTER = 1
- ROLE_SLAVE = 0
-
- def __init__(
- self,
- role,
- topic,
- ack_topic: str = None,
- group: str = None,
- serializer = None,
- debug = True,
- debug_prefix = "Kafka (B) | ",
- **kwargs
- ):
-
- """
- Creates a walkie-talkie type setup to use Kafka in a bidirectional manner. Fo more information on all the
- individual methods, please read through the doc-strings of the component classes 'ProducerKafka', and
- 'ConsumerKafka'.
- :param role: Select from "ROLE_MASTER" and "ROLE_SLAVE". Between the two parties that are talking, one will be
- the master and the other will be the slave. The channel that the master uses to speak will the one the slave
- uses to listen, and vice versa.
- :param topic: The topic to communicate on. Will be the same between the master and the slave.
- :param ack_topic: Explicitly provide this for the second channel, or it will be created from the name of the
- topic itself. Will be the same between the master and the slave.
- :param group: The group to assign the instance to.
- :param debug: Whether, or not, you want to print the debug strings.
- :param debug_prefix: The prefix to use while debugging.
- :param kwargs: Any configuration parameters for the Kafka instances.
- """
-
- # Not down the basic variables:
- self.__role = role
- self.__topic = topic
- self.__ack_topic = ack_topic or topic + "Ack"
- self.__group = group
-
- # In case the current instance is the master,
- # it will talk on "topic", and listen on "ack_topic":
- if self.__role == self.ROLE_MASTER:
- self.__producer_kwargs = kwargs.copy()
- self.__producer = ProducerKafka(
- topic = self.__topic,
- serializer = serializer,
- debug = debug,
- debug_prefix = debug_prefix.strip() + " (P) | ",
- **self.__producer_kwargs
- )
- self.__consumer_kwargs = kwargs.copy()
- self.__consumer_kwargs["group_id"] = self.__group
- self.__consumer = ConsumerKafka(
- topic = self.__ack_topic,
- group = group,
- serializer = serializer,
- debug = debug,
- debug_prefix = debug_prefix.strip() + " (C) | ",
- **self.__consumer_kwargs
- )
-
- # On the other hand, if the current instance is a slave,
- # It will listen on "topic", and talk on "ack_topic":
- else:
- self.__producer_kwargs = kwargs.copy()
- self.__producer = ProducerKafka(
- topic = self.__ack_topic,
- serializer = serializer,
- debug = debug,
- debug_prefix = debug_prefix.strip() + " (P) | ",
- **self.__producer_kwargs
- )
- self.__consumer_kwargs = kwargs.copy()
- self.__consumer_kwargs["group_id"] = self.__group
- self.__consumer = ConsumerKafka(
- topic = self.__topic,
- group = group,
- serializer = serializer,
- debug = debug,
- debug_prefix = debug_prefix.strip() + " (C) | ",
- **self.__consumer_kwargs
- )
-
- def enable_debug(self):
- self.__producer.enable_debug()
- self.__consumer.enable_debug()
-
- def disable_debug(self):
- self.__producer.disable_debug()
- self.__consumer.disable_debug()
-
- async def ensure_connection(self):
- await self.__producer.ensure_connection()
- await self.__consumer.ensure_connection()
-
- async def close(self):
- await self.__producer.close()
- await self.__consumer.close()
-
- async def produce(self, message, encoding = "utf-8"):
- return await self.__producer.produce(message, encoding = encoding)
-
- async def consume(self, count = 1, timeout = 0.05, encoding = "utf-8"):
- return await self.__consumer.consume(count = count, timeout = timeout, encoding = encoding)
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** MAIN PROGRAM ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-if __name__ == "__main__":
-
- import asyncio
- import time
- from data_models.kafka_message import KafkaMessage
-
- ssl_ctx = get_ssl_context(
- ca_file = r"/home/developer/PycharmProjects/utils/cred/kafka/cert_authority.pem",
- cert_file = r"/home/developer/PycharmProjects/utils/cred/kafka/fullchain.pem",
- key_file = r"/home/developer/PycharmProjects/utils/cred/kafka/privkey.pem"
- )
-
- async def consumer_test():
-
- consumer = ConsumerKafka(
- topic = "kft_file_upload",
- # group_id = "assessImg",
- group_id = "updateMedia",
- bootstrap_servers = "wtt.ditscentre.in:9092",
- security_protocol = "SSL",
- ssl_context = ssl_ctx
- )
- await consumer.connect()
- await asyncio.sleep(1.5)
- print("READY!")
-
- while True:
- messages = await consumer.consume(count = 1)
- if len(messages) > 0: print("MESSAGE:", json.to_string(messages[0], default=str))
- await asyncio.sleep(1.0)
-
- async def producer_test():
-
- producer = ProducerKafka(
- topic = "kft_file_upload",
- bootstrap_servers = "del.ditscentre.in:9092",
- security_protocol = "SSL",
- ssl_context = ssl_ctx
- )
- await producer.connect()
- print("READY!")
-
- while True:
- my_msg = KafkaMessage(
- data = {
- "accepted": False,
- "reason": "low resolution"
- },
- media = {
- "name": "pikachu_poster.jpg",
- "ext": "jpg",
- "url": "https://nexcom.ditscentre.in/utils/files/small/download/66ded1c1c1c05139a618b5ff",
- "attr": {
- "user": "SarangKabir",
- "project": "ACE-PGP",
- "id": 173,
- "campaignActivityId": "25",
- "idCampaign": 49,
- "phoneNo": "7977821877"
- }
- },
- appId = "aceWockhardt",
- proc = {
- "name": "_assessImg",
- "attr": {
- "blurThreshold": 0.25,
- "clarityThreshold": 0.65,
- "nsfwThreshold": 0.25,
- "minWidth": 512,
- "minHeight": 512
- }
- },
- ack = None
- )
- success = await producer.produce(my_msg.model_dump())
- print("produced...")
- time.sleep(1.0)
- break
-
- await producer.close()
-
-
- asyncio.run(consumer_test())
diff --git a/utils_v2/rate_limit/__init__.py b/utils_v2/rate_limit/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/utils_v2/rate_limit/async_token_bucket.py b/utils_v2/rate_limit/async_token_bucket.py
deleted file mode 100644
index 4fb85e7..0000000
--- a/utils_v2/rate_limit/async_token_bucket.py
+++ /dev/null
@@ -1,212 +0,0 @@
-"""
-
- AUTHOR:
-
- Khushal P Soonderji
-
- DATE:
-
- Wednesday, 15th May, 2024
-
- OBJECTIVE:
-
- To provide a rate-limiting mechanism using the simple token bucket algorithm.
-
- REFERENCES:
-
- N/A
-
- DOWNLOADS:
-
- N/A
-
-"""
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** IMPORT ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# To make sibling directories accessible for imports:
-import sys
-sys.path.append(".")
-sys.path.append("..")
-
-# For date and time keeping:
-import datetime
-import time
-
-# For asynchronous activities:
-import asyncio
-
-# For mathematical operations:
-import math
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** MACROS / ONE-TIME INIT ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# --- Nothing Yet
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** VARIABLES ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# --- Nothing Yet
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** CLASSES ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-class TokenBucket:
-
- """
- MODES: "add" - Add 'rate_limit' no. of tokens to the bucket every interval.
- "reset" - Reset the token count to 'rate_limit' every interval
- """
-
- MODE_RESET = 0
- MODE_ADD = 1
-
- ONE_YEAR = 3_15_36_000.0
- ONE_MONTH = 26_78_400.0
- ONE_WEEK = 6_04_800.0
- ONE_DAY = 86_400.0
- ONE_HOUR = 3_600.0
- ONE_MINUTE = 60.0
- ONE_SECOND = 1.0
-
- def __init__(self, rate_limit, seconds = 1.0, mode = "reset", sleep = 0.1, in_sequence = False):
-
- """
- Initialize the rate controller.
- :param rate_limit: The no. of operations allowed per unit of time.
- :param seconds: The time period in seconds in which the tokens get reset or added.
- :param mode: To select what happens when the period is over. 'reset' mode means that the remaining tokens from
- the previous period are discarded and the counter is set back to the rate limit, and 'add' mode means that
- new tokens are added on top of the exiting ones that were unused in the previous period.
- :param sleep: The delay to add before checking back to see if tokens are available. Keep it longer for longer
- periods (which is adjusted by the 'seconds' parameter).
- :param in_sequence: Whether, or not, you want to maintain the sequence in which the requests were made
- (experimental).
- """
-
- if rate_limit is not None:
- rate_limit = int(rate_limit)
- self.__semaphore = asyncio.Semaphore(1 if in_sequence else rate_limit)
- mode = mode.lower()
- if mode not in [self.MODE_RESET, self.MODE_ADD]: mode = self.MODE_RESET
- self.__mode = mode
- self.__rate_limit = rate_limit
- self.__token_count = rate_limit
- self.__last_token_generation_time = time.time()
- self.__sleep = sleep
- self.__seconds = seconds
-
- async def has_turn(self):
-
- """
- Just to check if a token is available.
- Should be used only if you don't want to wait for turn in case it is unavailable.
- :return: True if the token is available, False if not.
- """
-
- # If the user doesn't want any rate-limits, we return immediately.
- # Otherwise, we check if the user has any tokens available:
- if self.__rate_limit is None: return True
- if self.__token_count > 0: return True
- else: return False
-
- async def get_turn(self, timeout = None):
-
- """
- To wait till either you get your turn or the wait gets timed-out.
- :param timeout: The time (in seconds) to wait to get a turn before exiting with failure.
- :return: True if a turn was received, else False if timed-out.
- """
-
- # If the user doesn't want any rate-limits, we return immediately:
- if self.__rate_limit is None: return
-
- # Start by making variables:
- got_turn = False
- wait_start = time.time()
-
- # Otherwise, we wait for the semaphore:
- async with self.__semaphore:
-
- # Wait till you get your turn or the attempt gets timed-out:
- while True:
-
- # If the timeout has been exceeded:
- if (
- timeout is not None and
- time.time() - wait_start >= timeout
- ): break
-
- # If it is time to generate new tokens:
- time_delta = time.time() - self.__last_token_generation_time
- if time_delta >= self.__seconds:
- if self.__mode == self.MODE_RESET: self.__token_count = self.__rate_limit
- else: self.__token_count += int(self.__rate_limit * math.floor(time_delta / self.__seconds))
- self.__last_token_generation_time = time.time()
-
- # If a token is available (or not):
- if self.__token_count > 0:
- self.__token_count -= 1
- got_turn = True
- break
- else: await asyncio.sleep(self.__sleep)
-
- # Done here:
- return got_turn
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** MAIN PROGRAM ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-if __name__ == "__main__":
-
- import random
-
- async def single_test(rate_controller, count):
- has_turn = await rate_controller.has_turn()
- got_turn = await rate_controller.get_turn()
- print(f"TURN: {count:.<5} {'Y' if has_turn else '-'} / {'Y' if got_turn else '-'} ({datetime.datetime.now()})")
- if got_turn: await asyncio.sleep(random.random())
-
- async def multi_test(max_count):
- rate_controller = TokenBucket(
- rate_limit = 10,
- seconds = 1,
- in_sequence = False,
- sleep = 0.025,
- mode = "reset",
- timeout = 3
- )
- tasks = [single_test(rate_controller, count + 1) for count in range(max_count)]
- await asyncio.gather(*tasks)
-
-
- start_time = time.time()
- asyncio.run(multi_test(100))
- print(f"FINISHED IN {time.time() - start_time} SECONDS!")
diff --git a/utils_v2/security/__init__.py b/utils_v2/security/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/utils_v2/security/__pycache__/__init__.cpython-310.pyc b/utils_v2/security/__pycache__/__init__.cpython-310.pyc
deleted file mode 100644
index e6d54d5..0000000
Binary files a/utils_v2/security/__pycache__/__init__.cpython-310.pyc and /dev/null differ
diff --git a/utils_v2/security/__pycache__/sanitizers.cpython-310.pyc b/utils_v2/security/__pycache__/sanitizers.cpython-310.pyc
deleted file mode 100644
index 4768d00..0000000
Binary files a/utils_v2/security/__pycache__/sanitizers.cpython-310.pyc and /dev/null differ
diff --git a/utils_v2/security/hash.py b/utils_v2/security/hash.py
deleted file mode 100644
index b3fc582..0000000
--- a/utils_v2/security/hash.py
+++ /dev/null
@@ -1,245 +0,0 @@
-"""
-
- AUTHOR:
-
- Khushal P Soonderji
-
- DATE:
-
- Monday, 26th Aug., 2024
-
- OBJECTIVE:
-
- To provide an easy way to hash inputs.
-
- REFERENCES:
-
- 1) Book: Full Stack Python Security - Dennis Byrne
-
- DOWNLOADS:
-
- N/A
-
-"""
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** IMPORT ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# For hashing:
-import hashlib
-import hmac
-import secrets
-from bcrypt import hashpw, gensalt
-
-# To work with buffers:
-import io
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** MACROS / ONE-TIME INIT ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# --- Nothing Yet
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** VARIABLES ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# --- Nothing Yet
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** CLASSES ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-class Hasher:
-
- def __init__(
- self,
- algorithm = hashlib.sha256,
- key = None
- ):
-
- """
- hashes data or a message or a file. Uses HMAC if 'key' is specified, else performs simple hashing.
- :param algorithm: The algorithm to use. SHA256 by default.
- :param key: Specify this as either a string or as bytes to use HMAC. Leave as null for simple hashing.
- """
-
- # Initialize the hasher:
- self.__hasher = None,
- self.__algorithm = algorithm
- self.__hmac_key = key
- if self.__hmac_key is not None:
- self.__hmac_key = self.__hmac_key.encode("utf-8") if isinstance(self.__hmac_key, str) else self.__hmac_key
- self.reset()
-
- @staticmethod
- def generate_key(byte_count = 32, url_safe = False):
-
- """
- A mechanism to generate key/salt values.
- NOTE: For a proper salt for passwords, I recommend using "generate_salt" method. It's far better.
- :param byte_count: The number of bytes to have in the key. The hex output (as string) will have 2x the
- characters.
- :param url_safe: Set to True if you need the generated output to be a part of a URL.
- :return: The generate key/salt.
- """
-
- return secrets.token_urlsafe(byte_count) if url_safe else secrets.token_hex(byte_count)
-
- @staticmethod
- def generate_salt():
-
- """
- Generate a salt to use while hashing things like passwords.
- :return: The salt as bytes.
- """
-
- return gensalt()
-
- @staticmethod
- def hash_password(password, salt):
-
- """
- Hashes a password with the given salt.
- :param password: The password to hash, either as a string or as bytes.
- :param salt: The salt to hash the password with, either as a string or as bytes.
- :return: The hashed string.
- """
-
- return hashpw(
- password = password.encode("utf-8") if isinstance(password, str) else password,
- salt = salt.encode("utf-8") if isinstance(salt, str) else salt
- )
-
- @staticmethod
- def compare_hashes(hash_0, hash_1):
-
- """
- Compares two hashes to see if they match.
- Comparison is done in constant time to avoid timing-based side-channel attacks.
- :param hash_0: One of the hashes to compare.
- :param hash_1: The other hash to compare.
- :return: True if they match, else False.
- """
-
- return hmac.compare_digest(hash_0, hash_1)
-
- def reset(self):
-
- """
- Resets the hasher by removing all the data that was fed into it.
- :return: None.
- """
-
- if self.__hmac_key is not None:
- self.__hasher = hmac.new(
- key = self.__hmac_key,
- digestmod = self.__algorithm
- )
- else: self.__hasher = self.__algorithm()
-
- def update(self, data):
-
- """
- Adds data to the hash to update it.
- :param data: The data to be hashed.
- :return: None.
- """
-
- data = data.encode("utf-8") if isinstance(data, str) else data
- self.__hasher.update(data)
-
- def digest(self):
-
- """
- Returns the hexadecimal representation of the hash as a string.
- :return: The hexadecimal representation of the hash as a string
- """
-
- return self.__hasher.digest()
-
- def hexdigest(self):
-
- """
- Returns the hexadecimal representation of the hash as a string.
- :return: The hexadecimal representation of the hash as a string
- """
-
- return self.__hasher.hexdigest()
-
- def hash_message(self, message, as_hex = True):
-
- """
- Hashes one message and returns the result, and then resets the instance.
- :param message: The data you want to hash.
- :param as_hex: Invokes 'hexdigest' if True, else 'digest'.
- :return: The hash of the message in either hexadecimal string form or binary form.
- """
-
- self.update(message)
- hash_result = self.hexdigest() if as_hex else self.digest()
- self.reset()
- return hash_result
-
- def hash_file(self, file, chunk_size = 4096, as_hex = True):
-
- """
- Hashes one file and returns the result, and then resets the instance.
- :param file: The file you want to hash either as a path or as some buffer (like io.BytesIO).
- :param chunk_size: The size of data (in bytes) that you would like to pick at one time.
- :param as_hex: Invokes 'hexdigest' if True, else 'digest'.
- :return: The hash of the file in either hexadecimal string form or binary form.
- """
-
- # In case the file was given as a io.BytesIO buffer:
- if isinstance(file, io.BytesIO):
- file.seek(0)
- while True:
- chunk = file.read(chunk_size)
- if not chunk: break
- self.update(chunk)
-
- # In case the file was given as a path:
- else:
- with open(file, "rb") as f:
- for chunk in iter(lambda: f.read(chunk_size), b""):
- self.update(chunk)
-
- # Now we capture the results, reset the instance, and return the result:
- hash_result = self.hexdigest() if as_hex else self.digest()
- self.reset()
- return hash_result
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** MAIN PROGRAM ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-if __name__ == "__main__":
-
- print(
- Hasher.hash_password(
- password = "mic test, mic test, 123",
- salt = Hasher.generate_salt()
- )
- )
diff --git a/utils_v2/security/otp.py b/utils_v2/security/otp.py
deleted file mode 100644
index 49edcd2..0000000
--- a/utils_v2/security/otp.py
+++ /dev/null
@@ -1,156 +0,0 @@
-"""
-
- AUTHOR:
-
- Khushal P Soonderji
-
- DATE:
-
- Tuesday, 10th Sept., 2024
-
- OBJECTIVE:
-
- To provide a way to generate and verify OTPs.
-
- REFERENCES:
-
- 01. https://pyauth.github.io/pyotp/#
- 02. https://en.wikipedia.org/wiki/Google_Authenticator
-
- DOWNLOADS:
-
- N/A
-
-"""
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** IMPORT ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# To make sibling directories accessible for imports:
-import sys
-sys.path.append(".")
-sys.path.append("..")
-
-# To work with OTPs:
-import pyotp
-import base64
-import hashlib
-
-# To work with date and time:
-import time
-import datetime
-
-# For debugging:
-from icecream import IceCreamDebugger
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** MACROS / ONE-TIME INIT ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# --- Nothing Yet
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** VARIABLES ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# --- Nothing Yet
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** CLASSES ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-class HashedOTP:
-
- def __init__(
- self,
- secret
- ):
-
- """
- Used to generate and verify HMAC-based OTPs.
- :param secret: The key to use to generate and verify OTPs.
- """
-
- # Note down the input variables:
- self.__secret = secret
- self.__otp_client = pyotp.HOTP(secret)
-
- @staticmethod
- def generate_secret(message = None):
-
- """
- Generate a secret key to then use to generate and verify the OTPs.
- You may override the random generator by giving a "message" of any length.
- :param message: A custom value to convert into a key. Avoid using this for better security, but this can be used
- to generate keys based on user identifiers. THERE IS NO RANDOMNESS IF YOU USE THIS FEATURE. IT IS FOR
- CONVENIENCE ONLY. NOT RECOMMENDED.
- :return: The key (as a string) that can be used to generate and verify the OTPs.
- """
-
- # If the user wants to generate a key from a custom input:
- if message:
-
- # Ensure we have a bytes object:
- if not isinstance(message, (str, bytes)): message = str(message)
- if isinstance(message, str): message = message.encode("utf-8")
-
- # Hash the bytes object:
- sha256_hash = hashlib.sha256()
- sha256_hash.update(message)
- hashed_key = sha256_hash.digest()
-
- # Convert to base-32:
- return base64.b32encode(hashed_key).decode("utf-8")
-
- # If the user wants a totally random key:
- else: return pyotp.random_base32()
-
- def generate_otp(self, count: int):
-
- """
- Generates the OTP at a particular step.
- :param count: The step at which the OTP needs to be generated.
- :return: The OTP string (6 digits).
- """
-
- return str(self.__otp_client.at(count))
-
- def verify_otp(self, otp, count: int):
-
- """
- Verifies the claimed OTP.
- :param otp: The OTP as claimed by the end user.
- :param count: The step at which the OTP needs to be verified.
- :return: True if the OTP is valid, else False.
- """
-
- return self.__otp_client.verify(otp, counter = count)
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** MAIN PROGRAM ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-if __name__ == "__main__":
-
- pass
diff --git a/utils_v2/security/sanitizers.py b/utils_v2/security/sanitizers.py
deleted file mode 100644
index 26dfe5b..0000000
--- a/utils_v2/security/sanitizers.py
+++ /dev/null
@@ -1,135 +0,0 @@
-"""
-
- AUTHOR:
-
- Khushal P Soonderji
-
- DATE:
-
- Thursday, 25th Jul., 2024
-
- OBJECTIVE:
-
- To provide a set of data cleaning functions for inputs like phone numbers, emails, etc.
-
- REFERENCES:
-
- N/A
-
- DOWNLOADS:
-
- N/A
-
-"""
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** IMPORT ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# To make sibling directories accessible for imports:
-import sys
-sys.path.append(".")
-sys.path.append("..")
-
-# My utils:
-from utils_v2.string import regex
-
-# For random strings and tokens:
-import string
-import random
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** MACROS / ONE-TIME INIT ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# --- Nothing Yet
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** VARIABLES ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# --- Nothing Yet
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** FUNCTIONS ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-def file_name(input_string: str):
-
- """
- Cleans up the string to allow it to safely become a filename.
- :param input_string: The string that you want to make safe for using as a filename.
- :return: The string that can safely be used as a filename.
- """
-
- return regex.replace(
- text = input_string.replace("\n", " "),
- pattern = r"[^a-zA-Z0-9 \-_\.]",
- substitute_text = ""
- )
-
-
-# ---------------------------------------------------------------------------------------------------------------------
-
-
-def for_mongo(input_document):
-
- """
- Sanitizes and disarms any JSON-like input that could be used for NoSQL-injection attacks.
- :param input_document: The list or dict to be sanitized.
- :return: The sanitized list or dict.
- """
-
- # A special function that disarms any input string by dealing with special characters
- # that Mongo may consider to be instructions:
- def disarm(input_string):
- input_string = regex.replace(
- text = input_string,
- pattern = r"[^a-zA-Z0-9,_\-\.\\\/:;'\(\) ]",
- substitute_text = ""
- )
- return input_string
-
- # Initially we assign the value of the input to the output:
- sanitized_document = input_document
-
- # Handle the case where the input is an array:
- if isinstance(input_document, list):
- sanitized_document = [for_mongo(document) for document in input_document]
-
- # Handle the case when the input is a document:
- elif isinstance(input_document, dict):
- sanitized_document = {}
- for k, v in input_document.items():
- sanitized_document[disarm(k)] = v if type(v) not in [list, dict] else for_mongo(v)
-
- # Done here:
- return sanitized_document
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** MAIN PROGRAM ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-if __name__ == "__main__":
-
- pass
diff --git a/utils_v2/serialization/__init__.py b/utils_v2/serialization/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/utils_v2/serialization/json_serializer.py b/utils_v2/serialization/json_serializer.py
deleted file mode 100644
index 87595e0..0000000
--- a/utils_v2/serialization/json_serializer.py
+++ /dev/null
@@ -1,129 +0,0 @@
-"""
-
- AUTHOR:
-
- Khushal P Soonderji
-
- DATE:
-
- Sunday 1st Sept. 2024.
-
- OBJECTIVE:
-
- To provide a way to convert any input data to serialized bytes, and back.
-
- REFERENCES:
-
- N/A
-
- DOWNLOADS:
-
- N/A
-
-"""
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** IMPORT ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# To make sibling directories accessible for imports:
-import sys
-sys.path.append(".")
-sys.path.append("..")
-
-# To work with files:
-from utils_v2.string import json
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** MACROS / ONE-TIME INIT ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# --- Nothing Yet
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** VARIABLES ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# --- Nothing Yet
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** FUNCTIONS ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# --- Nothing Yet
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** CLASSES ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-class JSONSerializer:
-
- def __init__(self):
-
- """
- Use this serializer when dealing with JSON-compatible data like direct JSON-strings, python dicts, and
- python-lists. Beware that non-compatible data will cause either direct exceptions or unexpected behaviour.
- """
-
- pass
-
- @staticmethod
- def serialize(data, encoding = "utf-8"):
-
- """
- Serializes the data that is given to it.
- The input has to be JSON-compatible.
- :param data: The data to serialize.
- :param encoding: The encoding to use.
- :return: The bytes representing the data.
- """
-
- # If the data is not already a JSON string, parse it. Then return it as bytes:
- data = data if isinstance(data, str) else json.to_string(data, no_space = True)
- return data.encode(encoding)
-
- @staticmethod
- def deserialize(data, encoding = "utf-8"):
-
- """
- Deserializes the bytes that are given to it.
- The input has to be JSON-compatible.
- :param data: The bytes to deserialize.
- :param encoding: The encoding to use.
- :return: The data from the bytes that described it.
- """
-
- data = data.decode(encoding)
- return json.from_string(data)
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** MAIN PROGRAM ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-if __name__ == "__main__":
-
- pass
diff --git a/utils_v2/serialization/universal_serializer.py b/utils_v2/serialization/universal_serializer.py
deleted file mode 100644
index 23818c8..0000000
--- a/utils_v2/serialization/universal_serializer.py
+++ /dev/null
@@ -1,265 +0,0 @@
-"""
-
- AUTHOR:
-
- Khushal P Soonderji
-
- DATE:
-
- Sunday 1st Sept. 2024.
-
- OBJECTIVE:
-
- To provide a way to convert any input data to serialized bytes, and back.
-
- REFERENCES:
-
- N/A
-
- DOWNLOADS:
-
- N/A
-
-"""
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** IMPORT ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# To make sibling directories accessible for imports:
-import sys
-sys.path.append(".")
-sys.path.append("..")
-
-# To work with files:
-from utils_v2.string import json
-
-# To work with tabulated data:
-import pandas as pd
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** MACROS / ONE-TIME INIT ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# --- Nothing Yet
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** VARIABLES ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# --- Nothing Yet
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** FUNCTIONS ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# --- Nothing Yet
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** CLASSES ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-class UniversalSerializer:
-
- def __init__(self):
-
- """
- Use this when you are working with varied datatypes. You can add custom data-converters also using the
- 'add_converters' method. Otherwise, most default pythonic datatypes are supported out of the box. Note that this
- is NOT recommended because of how large the serialized messages become. Try using 'JSONSerializer' when you know
- you will be working specifically with JSOn-compatible inputs.
- """
-
- # These are the converters to use when serializing data:
- self.__forward_converters = {
- "set": lambda x: list(x),
- "tuple": lambda x: list(x),
- "complex": lambda x: {"r": x.real, "i": x.imag},
- "DataFrame": lambda x: x.to_dict()
- }
-
- # These are the converters to use when deserializing data:
- self.__reverse_converters = {
- "set": lambda x: set(x),
- "tuple": lambda x: tuple(x),
- "complex": lambda x: complex(x["r"], x["i"]),
- "DataFrame": lambda x: pd.DataFrame.from_dict(x)
- }
-
- def add_converters(
- self,
- type_name,
- forward_converter_func,
- reverse_converter_func
- ):
-
- """
- Add custom datatype converters.
- RULES:
- 01. Each of the converter functions must take in exactly on argument and return one output of native python
- type. This is very important.
- 02. Each forward and reverse converters must give symmetric results.
- :param type_name: The name of the datatype. HINT: type(obj).__name__
- :param forward_converter_func: The function to handle conversion to bytes. Use when serializing.
- :param reverse_converter_func: The function to handle conversion from bytes. Used when deserializing.
- :return: None.
- """
-
- self.__forward_converters[type_name] = lambda x: forward_converter_func(x)
- self.__reverse_converters[type_name] = lambda x: reverse_converter_func(x)
-
- def __describe(self, data):
-
- """
- Notes down the input datatypes of everything.
- Does everything upto conversion to byes.
- :param data: The data to process.
- :return: The description of the datatypes and values of what was given.
- """
-
- # Note down the type of data that was sent as the input:
- data_type = type(data).__name__
-
- # Handle iterables:
- if isinstance(data, list): data = [self.__describe(item) for item in data]
- elif isinstance(data, set): data = [self.__describe(item) for item in data]
- elif isinstance(data, tuple): data = [self.__describe(item) for item in data]
- elif isinstance(data, dict): data = [
- {
- "k": self.__describe(k),
- "v": self.__describe(v)
- } for k, v in data.items()
- ]
-
- # Convert here, and return:
- conv = self.__forward_converters.get(data_type)
- if conv is not None: data = conv(data)
- return {"d": data, "t": data_type}
-
- def serialize(self, data, encoding = "utf-8"):
-
- """
- Serializes the data that is given to it.
- :param data: The data to serialize.
- :param encoding: The encoding to use.
- :return: The bytes representing the data.
- """
-
- data = self.__describe(data)
- data = json.to_string(data, no_space = True)
- return data.encode(encoding)
-
- def __interpret(self, data):
-
- """
- Interprets the types of data that were serialized originally.
- :param data: The data in the serialized form.
- :return: Data where the appropriate datatypes have been applied.
- """
-
- # Handle iterables:
- if data["t"] == "list": data = [self.__interpret(item) for item in data["d"]]
- elif data["t"] == "set": data = set([self.__interpret(item) for item in data["d"]])
- elif data["t"] == "tuple": data = tuple([self.__interpret(item) for item in data["d"]])
- elif data["t"] == "dict": data = {
- self.__interpret(item["k"]): self.__interpret(item["v"])
- for item in data["d"]
- }
-
- # Handle custom types:
- else:
- conv = self.__reverse_converters.get(data["t"])
- data = data["d"]
- if conv is not None: data = conv(data)
-
- # Done here
- return data
-
- def deserialize(self, data, encoding = "utf-8"):
-
- """
- Deserializes the bytes that are given to it.
- :param data: The bytes to deserialize.
- :param encoding: The encoding to use.
- :return: The data from the bytes that described it.
- """
-
- data = data.decode(encoding)
- data = json.from_string(data)
- return self.__interpret(data)
-
-
-# ---------------------------------------------------------------------------------------------------------------------
-
-
-class JSONSerializer:
-
- def __init__(self):
-
- """
- Use this serializer when dealing with JSON-compatible data like direct JSON-strings, python dicts, and
- python-lists. Beware that non-compatible data will cause either direct exceptions or unexpected behaviour.
- """
-
- pass
-
- @staticmethod
- def serialize(data, encoding = "utf-8"):
-
- """
- Serializes the data that is given to it.
- The input has to be JSON-compatible.
- :param data: The data to serialize.
- :param encoding: The encoding to use.
- :return: The bytes representing the data.
- """
-
- # If the data is not already a JSON string, parse it. Then return it as bytes:
- data = data if isinstance(data, str) else json.to_string(data, no_space = True)
- return data.encode(encoding)
-
- @staticmethod
- def deserialize(data, encoding = "utf-8"):
-
- """
- Deserializes the bytes that are given to it.
- The input has to be JSON-compatible.
- :param data: The bytes to deserialize.
- :param encoding: The encoding to use.
- :return: The data from the bytes that described it.
- """
-
- data = data.decode(encoding)
- return json.from_string(data)
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** MAIN PROGRAM ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-if __name__ == "__main__":
-
- pass
diff --git a/utils_v2/sms/__init__.py b/utils_v2/sms/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/utils_v2/sms/async_nimbus.py b/utils_v2/sms/async_nimbus.py
deleted file mode 100644
index 1d4b926..0000000
--- a/utils_v2/sms/async_nimbus.py
+++ /dev/null
@@ -1,252 +0,0 @@
-"""
-
- AUTHOR:
-
- Khushal P Soonderji
-
- DATE:
-
- Monday, 9th Sept., 2024
-
- OBJECTIVE:
-
- To be able to send SMSs from Nimbus's API and manage the templates and other things from one place.
-
- REFERENCES:
-
- 01. https://github.com/innovativevijay/SmsHitApiSample
- 02. https://nimbusit.net/appforms/apimanual.php
-
- DOWNLOADS:
-
- N/A
-
- WEB-PORTAL:
-
- 01. http://nimbusit.net/
-
-"""
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** IMPORT ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# To make sibling directories accessible for imports:
-import sys
-sys.path.append(".")
-sys.path.append("..")
-
-# To make API Calls:
-import httpx
-
-# For debugging:
-from icecream import IceCreamDebugger
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** MACROS / ONE-TIME INIT ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# --- Nothing yet
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** VARIABLES ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# --- Nothing Yet
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** CLASSES ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-class AsyncNimbusSMS:
-
- MESSAGE_TYPE_REGULAR = 0
- MESSAGE_TYPE_UNICODE = 1
-
- def __init__(
- self,
- entity_id,
- sender_id,
- user_id,
- api_key,
- debug = True,
- debug_prefix = "Nimbus SMS | "
- ):
-
- """
- Sets up an instance of the SMS sender through Nimbus IT.
- :param entity_id: The entity id as registered with DLT.
- :param sender_id: The 6-char code like "HDFCBK", "NSESMS", "ZRODHA" that you see in your SMS inbox.
- :param user_id: The 6-digit id that Nimbus has assigned to you.
- :param api_key: The API key generated through Nimbus's portal.
- :param debug: Whether, or not, you would like to show debugging messages (can be changed on the fly).
- :param debug_prefix: The prefix text to show with the debug string.
- """
-
- # Create the debugging tools:
- self.__printer = IceCreamDebugger(prefix = debug_prefix, includeContext = True)
- if not debug: self.__printer.disable()
-
- # Create an HTTP client to work with:
- limits = httpx.Limits(
- max_connections = 5,
- max_keepalive_connections = 5,
- keepalive_expiry = 3600
- )
- self.__http_client = httpx.AsyncClient(limits = limits, timeout = 120)
-
- # Capture the input config:
- self.__entity_id = entity_id
- self.__sender_id = sender_id
- self.__user_id = user_id,
- self.__api_key = api_key
-
- async def get_balance(self):
-
- """
- Checks the balance in the Nimbus wallet.
- :return: The balance (float) if the request was successful, else None.
- """
-
- balance = None
-
- try:
-
- # Call the API:
- response = await self.__http_client.get(
- url = r"http://nimbusit.net/api/balance",
- params = {"user": self.__user_id, "authkey": self.__api_key}
- )
-
- # The response of a successful API call looks like "BALANCE:599". We need just the number:
- if response.status_code in [200]: balance = float(response.content.decode().split(":")[-1].strip())
-
- except Exception as exception:
- self.__printer(exception)
-
- return balance
-
- async def send_sms(
- self,
- template_id,
- recipient_number,
- message,
- message_type = MESSAGE_TYPE_REGULAR
- ):
-
- """
- Sends one SMS through Nimbus IT's system. The text of the message must match the template that had been
- submitted. A mismatch may cause the message to fail at best, and raise troubles in the real-world with
- government bodies at worst. Be careful.
- :param template_id: The id of the SMS template as registered on Nimbus's portal.
- :param recipient_number: The phone number of the recipient. You can send an array of numbers, too, BUT IT IS
- STRONGLY RECOMMENDED TO NOT DO THAT TO AVOID BEING BLOCKED BY DLT.
- :param message: The message to send to the recipient. Should match the template that is being sent.
- :param message_type: Choose between 'AsyncNimbusSMS.MESSAGE_TYPE_REGULAR' (default) and
- 'AsyncNimbusSMS.MESSAGE_TYPE_UNICODE' based on the type of characters being sent. Both are class variables.
- :return: The dict of all the details of the message that was sent including whether, or not, it was successfully
- sent. Other details depend on the service provider (Nimbus IT in this case).
- """
-
- # Construct the basic structure of the response of this method:
- summary = {
- "success": False,
- "info": None,
- "sender": self.__sender_id,
- "recipient": recipient_number,
- "message": message,
- "length": len(message),
- "template_id": template_id,
- "raw": None
- }
-
- try:
-
- # Pre-process the recipient's number:
- if not isinstance(recipient_number, (list, set, tuple)): recipient_number = [recipient_number]
-
- # Call the API:
- response = await self.__http_client.get(
- url = r"http://nimbusit.net/api/pushsms",
- params = {
- "user": self.__user_id,
- "authkey": self.__api_key,
- "sender": self.__sender_id,
- "mobile": ",".join([str(num) for num in recipient_number]),
- "text": message,
- "entityid": self.__entity_id,
- "templateid": template_id,
- "type": message_type
- }
- )
-
- # For a successful API call:
- if response.status_code == 200:
- response_json = response.json()
- summary["success"] = True if response_json.get("STATUS", "ERROR").lower() in ["ok"] else False
- summary["info"] = response_json.get("RESPONSE", {}).get("INFO")
- summary["raw"] = {
- "http_code": response.status_code,
- "response": response_json,
- }
-
- # For any other code that indicates some form of failure:
- else: summary["raw"] = {
- "http_code": response.status_code,
- "response": response.content.decode()
- }
-
- except Exception as exception:
- self.__printer(exception)
-
- return summary
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** MAIN PROGRAM ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-if __name__ == "__main__":
-
- import asyncio
-
- async def main():
-
- sender = AsyncNimbusSMS(
- entity_id = "",
- sender_id = "",
- user_id = "",
- api_key = ""
- )
-
- response = await sender.send_sms(
- template_id = "",
- recipient_number = "",
- message = ""
- )
- print("SMS API RESPONSE:", response)
-
- my_balance = await sender.get_balance()
- print("REMAINING BALANCE:", my_balance)
-
-
- asyncio.run(main())
diff --git a/utils_v2/string/__init__.py b/utils_v2/string/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/utils_v2/string/__pycache__/__init__.cpython-310.pyc b/utils_v2/string/__pycache__/__init__.cpython-310.pyc
deleted file mode 100644
index 6e9484d..0000000
Binary files a/utils_v2/string/__pycache__/__init__.cpython-310.pyc and /dev/null differ
diff --git a/utils_v2/string/__pycache__/json.cpython-310.pyc b/utils_v2/string/__pycache__/json.cpython-310.pyc
deleted file mode 100644
index 98048fa..0000000
Binary files a/utils_v2/string/__pycache__/json.cpython-310.pyc and /dev/null differ
diff --git a/utils_v2/string/__pycache__/regex.cpython-310.pyc b/utils_v2/string/__pycache__/regex.cpython-310.pyc
deleted file mode 100644
index 1a93e0b..0000000
Binary files a/utils_v2/string/__pycache__/regex.cpython-310.pyc and /dev/null differ
diff --git a/utils_v2/string/fuzzy.py b/utils_v2/string/fuzzy.py
deleted file mode 100644
index 34c08d4..0000000
--- a/utils_v2/string/fuzzy.py
+++ /dev/null
@@ -1,168 +0,0 @@
-"""
-
- AUTHOR:
-
- Khushal P Soonderji
-
- DATE:
-
- Saturday, 18th May, 2024
-
- OBJECTIVE:
-
- To provide a quick set of functions to work with fuzzy logic.
-
- REFERENCES:
-
- 1) https://www.w3schools.com/python/python_json.asp
-
- DOWNLOADS:
-
- N/A
-
-"""
-
-# *****************************************************************************************************************
-# ***** ****
-# *** IMPORT ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# To make sibling directories accessible for imports:
-import sys
-sys.path.append(".")
-sys.path.append("..")
-
-# To apply fuzzy logic:
-from thefuzz import fuzz, process
-
-# To work with tabulated data:
-import pandas as pd
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** MACROS / ONE-TIME INIT ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# --- Nothing Yet
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** VARIABLES ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# --- Nothing Yet
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** FUNCTIONS ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-def get_best_match(
- target,
- choices,
- threshold = 0.70,
- partial = False
-):
-
- if partial: scorer = fuzz.partial_token_sort_ratio
- else: scorer = fuzz.ratio
-
- result = process.extractOne(
- target,
- choices,
- score_cutoff = threshold * 100,
- scorer = scorer
- )
-
- try: return result[0]
- except: return None
-
-
-# ---------------------------------------------------------------------------------------------------------------------
-
-
-def rank(target, choices, partial = True):
-
- if partial: scorer = fuzz.partial_token_sort_ratio
- else: scorer = fuzz.ratio
-
- result = process.extract(
- target,
- choices,
- limit = len(choices),
- scorer = scorer
- )
-
- result = pd.DataFrame(result, columns = ["choice", "closeness"])
- result["closeness"] = result["closeness"] / 100.0
-
- return result
-
-
-# ---------------------------------------------------------------------------------------------------------------------
-
-
-def match(targets, choices, threshold = 0.7, partial = False, allow_null = False):
-
- all_matches_df = None
- all_matches = {target: None for target in targets}
- something_is_null = False
-
- for target in targets:
- match_df = rank(target, choices, partial = partial)
- match_df["target"] = target
- if all_matches_df is None: all_matches_df = match_df
- else: all_matches_df = pd.concat([all_matches_df, match_df])
-
- all_matches_df = all_matches_df.sort_values(by = ["closeness"], ascending = False).reset_index(drop = True)
-
- for target in targets:
- target_df = all_matches_df[all_matches_df["target"] == target].reset_index(drop = True)
- if target_df.empty: continue
- if target_df.at[0, "closeness"] >= threshold:
- choice = target_df.at[0, "choice"]
- all_matches[target] = choice
- all_matches_df = all_matches_df[all_matches_df["choice"] != choice]
- else:
- all_matches[target] = None
- something_is_null = True
-
- # print(all_matches)
- if something_is_null and not allow_null: return None
- else: return all_matches
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** MAIN PROGRAM ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-if __name__ == "__main__":
-
- import async_json_utils
-
- awb_numbers = [
- "SF1111BIC",
- "SF2222BIC",
- "SF3333BIC",
- "SF4444BIC",
- ]
-
- chat_text = "SF1112BIC"
-
- # print(chat_text == names[0])
- best_match = get_best_match(chat_text, awb_numbers, threshold = 0.60, partial = False)
- print(f"Best match for '{chat_text}' is '{best_match}'")
diff --git a/utils_v2/string/help.py b/utils_v2/string/help.py
deleted file mode 100644
index 85dcc97..0000000
--- a/utils_v2/string/help.py
+++ /dev/null
@@ -1,177 +0,0 @@
-"""
-
- AUTHOR:
-
- Khushal P Soonderji
-
- DATE:
-
- Saturday, 24th Aug., 2024
-
- OBJECTIVE:
-
- To provide an overview of any function or class in a string.
- The generated overview can then either be shown on the terminal, or transmitted over some other medium for
- collaborative work.
-
- REFERENCES:
-
- N/A
-
- DOWNLOADS:
-
- N/A
-
-"""
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** IMPORT ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# System-level activities:
-import io
-import inspect
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** MACROS / ONE-TIME INIT ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# --- Nothing Yet
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** VARIABLES ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# --- Nothing Yet
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** FUNCTIONS ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-def get_help_for_class(cls, skip_methods = None):
-
- """
- Returns the help documentation to use this class.
- :param cls: The class whose help string is desired.
- :param skip_methods: A list of methods to NOT include in the help text.
- :return: This help documentation.
- """
-
- separator = "\n\n" + ("=" * 120) + "\n\n"
- if skip_methods is None: skip_methods = []
- elif not isinstance(skip_methods, list): skip_methods = [skip_methods]
-
- # Get class name and docstring:
- class_name = cls.__name__
- docstring = inspect.getdoc(cls) or ""
- help_string = "HELP FOR:\n\n"
- help_string += class_name + "\n\n"
- help_string += "This document has upto 120 chars per line.\n"
- help_string += "Best viewed with monospaced font :)"
- help_string += docstring + separator
-
- # Get all methods and their docstrings.
- # Then note the documentation of the methods while ignoring the blacklisted ones:
- members = inspect.getmembers(cls, predicate = inspect.isfunction)
- func_help = []
- for name, method in members:
-
- # Ignore if asked, or extract the details:
- if name in skip_methods or name.startswith(f"_{class_name}__"): continue
- else: func_help.append(get_help_for_function(method))
-
- # Put all the things together:
- func_help = separator.join(func_help)
- help_string += func_help
-
- # Done here:
- return help_string
-
-
-# ---------------------------------------------------------------------------------------------------------------------
-
-
-def get_help_for_function(func):
-
- """
- Get the help string for one function.
- It could be a standalone function, or a method of a class.
- :param func: The function (or method) whose help string is needed.
- :return: The help string of the function.
- """
-
- # Get the name and documentation:
- func_name = func.__name__
- async_indicator = " (async)" if inspect.iscoroutinefunction(func) else ""
- func_doc = inspect.getdoc(func) or ""
-
- # Create the decorated header:
- func_decorator = "-" * (len(func_name) + 2)
- func_head = "." + func_decorator + f".\n| {func_name} |{async_indicator}\n`" + func_decorator + "`\n\n"
-
- # Add the 'args' and 'kwargs':
- func_args = []
- for name, param in inspect.signature(func).parameters.items():
- default = param.default
- if isinstance(default, str): default = f"\"{default}\""
- if default == inspect.Parameter.empty: func_args.append(f"{name}")
- else: func_args.append(f"{name}: {type(default).__name__} = {default}")
- if len(func_args) > 0: func_args = f"{func_name} (\n\t" + "\n\t".join(func_args) + "\n):\n\n"
- else: func_args = f"{func_name} ():\n\n"
-
- # Get the params and return value part from the doc:
- params_start = func_doc.find(":param")
- return_start = func_doc.find(":return")
- func_params = "\n" + func_doc[params_start:return_start] if params_start >= 0 else ""
- func_return = "\n" + func_doc[return_start:] if return_start >= 0 else ""
-
- # Isolate the documentation part:
- if params_start >= 0: func_doc = func_doc[:params_start]
- elif return_start >= 0: func_doc = func_doc[:return_start]
-
- # Done here:
- return func_head + func_args + func_doc + func_params + func_return
-
-
-# ---------------------------------------------------------------------------------------------------------------------
-
-
-def get_help(entity, skip_methods = None):
-
- """
- Get the help documentation for anything from its docstring.
- :param entity: The entity you want to get help for.
- :param skip_methods: A list of methods to ignore if inspecting a class. Not valid for standalone functions.
- :return: The help string.
- """
-
- if inspect.isclass(entity): return get_help_for_class(entity, skip_methods = skip_methods)
- else: return get_help_for_function(entity)
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** MAIN PROGRAM ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-if __name__ == "__main__":
-
- pass
diff --git a/utils_v2/string/json.py b/utils_v2/string/json.py
deleted file mode 100644
index 67d39f7..0000000
--- a/utils_v2/string/json.py
+++ /dev/null
@@ -1,202 +0,0 @@
-"""
-
- AUTHOR:
-
- Khushal P Soonderji
-
- DATE:
-
- Create: Saturday, 18th May, 2022
- Update: Thursday, 22nd Aug. 2024
-
- OBJECTIVE:
-
- To provide an easy way to work with '.json' data and files.
-
- REFERENCES:
-
- 1) https://www.w3schools.com/python/python_json.asp
-
- DOWNLOADS:
-
- N/A
-
-"""
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** IMPORT ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# To make sibling directories accessible for imports:
-import sys
-sys.path.append(".")
-sys.path.append("..")
-
-# System-level activities:
-import io
-
-# To work with the JSON standard:
-import json
-
-# To work with files:
-from utils_v2.system import files
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** MACROS / ONE-TIME INIT ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# --- Nothing Yet
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** VARIABLES ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# --- Nothing Yet
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** FUNCTIONS ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-def from_string(json_data):
-
- """
- Decodes a JSON string to a pythonic variable like a dict.
- :param json_data: The JSON string to decode.
- :return: The decoded pythonic variable.
- """
-
- python_data = json.loads(json_data)
- return python_data
-
-
-# ---------------------------------------------------------------------------------------------------------------------
-
-
-def to_string(
- python_data,
- indent = 4,
- default = None,
- separators = None,
- no_space = False
-):
-
- """
- Converts the given pythonic data to a JSON string.
- :param python_data: The input data like a dict.
- :param indent: The tab-width for pretty presentation.
- :param default: The function to use on something that cannot be directly parsed into a JSON string.
- :param separators: Custom separators to use.
- :param no_space: If you want a dense JSON string that saves memory by not using spaces or tabs or line-breaks. Not
- good for human readability, very good for saving memory. WARNING: THIS OVERRIDES EVERY OTHER PARAMETER EXCEPT
- 'default'.
- :return: The JSON string representation of the input pythonic data.
- """
-
- if no_space:
- json_data = json.dumps(
- python_data,
- default = default,
- separators = (',', ':')
- )
-
- else:
- json_data = json.dumps(
- python_data,
- indent = indent,
- default = default,
- separators = separators
- )
-
- return json_data
-
-
-# ---------------------------------------------------------------------------------------------------------------------
-
-
-def from_file(file):
-
- """
- Reads a JSON file and returns it as a pythonic variable like a dict.
- :param file: The path to the file on the disk or a file held in RAM as a BytesIO object.
- :return: The decoded pythonic variable.
- """
-
- if isinstance(file, io.BytesIO):
- file.seek(0)
- json_data = file.getvalue()
- else: json_data = files.read_file(file)
- python_data = from_string(json_data)
- return python_data
-
-
-# ---------------------------------------------------------------------------------------------------------------------
-
-
-def to_file(
- file,
- python_data,
- indent = 4,
- default = None,
- separators = None,
- no_space = False
-):
-
- """
-
- :param file: Either a path to a file on disk, or a buffer in RAM in the form of a BytesIO object.
- :param python_data: The pythonic data to be converted to the JSON string.
- :param indent: The tab-width for pretty presentation.
- :param default: The function to use on something that cannot be directly parsed into a JSON string.
- :param separators: Custom separators to use.
- :param no_space: If you want a dense JSON string that saves memory by not using spaces or tabs or line-breaks. Not
- good for human readability, very good for saving memory. WARNING: THIS OVERRIDES EVERY OTHER PARAMETER EXCEPT
- 'default'.
- :return: True/False if a path was given, else the same BytesIO object with the written JSON data.
- """
-
- json_data = to_string(
- python_data,
- indent = indent,
- default = default,
- separators = separators,
- no_space = no_space
- )
-
- if isinstance(file, io.BytesIO):
- file.write(json_data.encode("utf-8"))
- file.seek(0)
- return file
-
- else:
- try:
- files.write_file(file, json_data, mode = "w")
- return True
- except: return False
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** MAIN PROGRAM ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-if __name__ == "__main__":
-
- pass
diff --git a/utils_v2/string/regex.py b/utils_v2/string/regex.py
deleted file mode 100644
index 3f01892..0000000
--- a/utils_v2/string/regex.py
+++ /dev/null
@@ -1,263 +0,0 @@
-"""
-
- AUTHOR:
-
- Khushal P Soonderji
-
- DATE:
-
- Sunday, 28th Apr., 2024
-
- OBJECTIVE:
-
- To provide a convenient way to perform RegEx operations like finding patterns and substituting them.
-
- REFERENCES:
-
- 1) https://www.w3schools.com/python/python_regex.asp
-
- DOWNLOADS:
-
- N/A
-
-"""
-
-# *****************************************************************************************************************
-# ***** ****
-# *** IMPORT ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# To make sibling directories accessible for imports:
-import sys
-sys.path.append(".")
-sys.path.append("..")
-
-# To work with RegEx:
-import re
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** MACROS / ONE-TIME INIT ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# Common RegEx patterns:
-REGEX_EMAIL_ID = r"[\d\w_.+]*@[\d\w_]*.[\d\w]{2,}"
-REGEX_PASSWORD = r"^(?=.*[a-z])(?=.*[A-Z])(?=.*[\d])(?=.*[!@#$%^&*()_+{}\[\]:;<>,.?~\\\/-]).{8,}$"
-REGEX_NAME = r"^[\d\w .\-]{1,30}$"
-REGEX_USERNAME = r"^[\d\w_]{8,25}$"
-REGEX_CONTACT_NUMBER = r"\+?\d{0,3}\s*\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}"
-REGEX_DATE = r"\b(?:\d{4}-\d{2}-\d{2}|(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+\d{1,2},?\s+\d{4}|\d{1,2}\/\d{1,2}\/\d{4}|\d{1,2}-\d{1,2}-\d{2}|\d{1,2}(?:st|nd|rd|th)\s+(?:Jan(?:uary)?|Feb(?:ruary)?|Mar(?:ch)?|Apr(?:il)?|May|Jun(?:e)?|Jul(?:y)?|Aug(?:ust)?|Sep(?:tember)?|Oct(?:ober)?|Nov(?:ember)?|Dec(?:ember)?),?\s+\d{4})\b"
-REGEX_GSTIN = r"[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z]{1}[1-9A-Z]{1}Z[0-9A-Z]{1}"
-REGEX_PAN = r"[A-Z]{5}[0-9]{4}[A-Z]{1}"
-REGEX_IPV4 = (r"[0-9]{1,3}\." * 3) + r"[0-9]{1,3}"
-REGEX_IPV6 = (r"[0-9a-fA-F]{1,4}:" * 7) + r"[0-9a-fA-F]{1,4}"
-REGEX_IFSC = r"[A-Z]{4}0[A-Z0-9]{6}"
-REGEX_UPI = r"[a-zA-Z0-9\.\-]{2,256}@[a-zA-Z][a-zA-Z]{2,64}"
-REGEX_MAC_ADDRESS = r"([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})|([0-9a-fA-F]{4}\\.[0-9a-fA-F]{4}\\.[0-9a-fA-F]{4})"
-REGEX_METRIC_WEIGHT = r"[\d\.]+[ ]?[k]?g"
-
-
-# RegEx chars (append them to the patterns if needed):
-REGEX_START = "^"
-REGEX_END = "$"
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** VARIABLES ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-# --- Nothing Yet
-
-
-# *****************************************************************************************************************
-# ***** ****
-# *** FUNCTIONS ***
-# ***** ****
-# *****************************************************************************************************************
-
-
-def find(text, pattern, case_sensitive = True, dot_all = False):
-
- """
- Returns a list of substrings that match the given RegEx pattern in the input text.
- :param text: The text in which the pattern needs to be found.
- :param pattern: The RegEx pattern to look for.
- :param case_sensitive: Whether, or not, you want the operation to be case-sensitive.
- :param dot_all: Allow all characters to be matched in ".".
- :return: An array (list) of substring that match the pattern. Can be an empty list as well.
- """
-
- # Prepare the flags:
- flags = 0
- if not case_sensitive: flags |= re.IGNORECASE
- if dot_all: flags |= re.DOTALL
-
- # Perform the RegEx operation, and clean the results:
- matches = [match if type(match) is str else match[1] for match in re.findall(pattern, text, flags = flags)]
- matches = [match for match in matches if len(match) > 0]
-
- # Return the results:
- return matches
-
-
-# ---------------------------------------------------------------------------------------------------------------------
-
-
-def replace(text, pattern, substitute_text, case_sensitive = True, dot_all = False):
-
- """
- Replaces any substring in the text that matches the RegEx pattern.
- :param text: The text in which the substitutions need to be made.
- :param pattern: The RegEx pattern that needs to be substituted.
- :param substitute_text: The text that will replace the matches that were found.
- :param case_sensitive: Whether, or not, you want the operation to be case-sensitive.
- :param dot_all: Allow all characters to be matched in ".".
- :return: The text with the substitutions. If no matches are found, the original string is returned.
- """
-
- # Prepare the flags:
- flags = 0
- if not case_sensitive: flags |= re.IGNORECASE
- if dot_all: flags |= re.DOTALL
-
- # Perform the RegEx operation, and return the results:
- return re.sub(pattern, substitute_text, text, flags = flags)
-
-
-# ---------------------------------------------------------------------------------------------------------------------
-
-
-def search(text, pattern, case_sensitive = True, dot_all = False):
-
- """
- Checks if the given RegEx pattern occurs ANYWHERE in the text that was provided.
- :param text: The text that needs to be matched against the pattern.
- :param pattern: The RegEx pattern to look for.
- :param case_sensitive: Whether, or not, you want the operation to be case-sensitive.
- :param dot_all: Allow all characters to be matched in ".".
- :return: True if the pattern matches, else False.
- """
-
- # Prepare the flags:
- flags = 0
- if not case_sensitive: flags |= re.IGNORECASE
- if dot_all: flags |= re.DOTALL
-
- # Perform the RegEx operation, and return the results:
- if re.search(pattern, text, flags = flags): return True
- else: return False
-
-
-# ---------------------------------------------------------------------------------------------------------------------
-
-
-def match(text, pattern, case_sensitive = True, dot_all = False):
-
- """
- Checks if the given text matches the RegEx pattern that was provided. The check is made only at the start of the
- input string.
- :param text: The text that needs to be matched against the pattern.
- :param pattern: The RegEx pattern to look for.
- :param case_sensitive: Whether, or not, you want the operation to be case-sensitive.
- :param dot_all: Allow all characters to be matched in ".".
- :return: True if the pattern matches, else False.
- """
-
- # Prepare the flags:
- flags = 0
- if not case_sensitive: flags |= re.IGNORECASE
- if dot_all: flags |= re.DOTALL
-
- # Perform the RegEx operation, and return the results:
- if re.match(pattern, text, flags = flags): return True
- else: return False
-
-
-# ---------------------------------------------------------------------------------------------------------------------
-
-
-def split(text, pattern, case_sensitive = True, dot_all = False):
-
- """
- Splits an input string based on the pattern that is being matched.
- :param text: The text that needs to be matched against the pattern.
- :param pattern: The RegEx pattern to look for.
- :param case_sensitive: Whether, or not, you want the operation to be case-sensitive.
- :param dot_all: Allow all characters to be matched in ".".
- :return: True if the pattern matches, else False.
- """
-
- # Prepare the flags:
- flags = 0
- if not case_sensitive: flags |= re.IGNORECASE
- if dot_all: flags |= re.DOTALL
-
- # Perform the RegEx operation, and return the results:
- substrings = re.split(pattern, text, flags = flags)
- if len(substrings) > 0 and substrings[0] == "": substrings.pop(0)
- return substrings
-
-
-# ---------------------------------------------------------------------------------------------------------------------
-
-
-def to_json(text, pattern, case_sensitive = True, dot_all = False):
-
- """
- Gives out a dict from the extracted features in a string. It is based on the concept of Named Groups.
- Consider the following example (assuming the search is case-insensitive):
- TEXT: "UPI/309258561479/14:17:35/UPI/omsainurses@okhdfc"
- PATTERN: "upi/.*/(?P