From 5e985b0d459a8d9dac33993423cef1a5d8ccf494 Mon Sep 17 00:00:00 2001 From: devsetgo Date: Mon, 27 May 2024 19:06:21 +0000 Subject: [PATCH 01/18] fixing description for pypi limit --- coverage.xml | 980 ++++++++++++++++++++++++------------------------- pyproject.toml | 12 +- 2 files changed, 491 insertions(+), 501 deletions(-) diff --git a/coverage.xml b/coverage.xml index daf64bd3..a53b63f5 100644 --- a/coverage.xml +++ b/coverage.xml @@ -1,9 +1,9 @@ - + - /github/workspace + /workspaces/devsetgo_lib @@ -16,7 +16,7 @@ - + @@ -30,7 +30,7 @@ - + @@ -41,15 +41,15 @@ - - + + - - - - - - + + + + + + @@ -94,7 +94,7 @@ - + @@ -109,21 +109,21 @@ - - - + + + - - - - + + + + - + @@ -133,219 +133,219 @@ - - - - - - - - - + + + + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + - - - - - - - - - - - + + + + + + + + + + + - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + - - - - - - - - - - - + + + + + + + + + + + - - - - - - - - - - - + + + + + + + + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - + - + - - - - - - - - - - - + + + + + + + + + + + - - - - - - - - - + + + + + + + + + - + @@ -356,46 +356,46 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + @@ -409,164 +409,164 @@ - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - + + + + + + + + + + - - - - - - - - - - - - - + + + + + + + + + + + + + - - - - - - - - - - - + + + + + + + + + + + - + @@ -578,50 +578,50 @@ - - - - - - - - + + + + + + + + - - - - - - - + + + + + + + - - - - - - - - - - - - - + + + + + + + + + + + + + - - - - - - - - - - + + + + + + + + + + - + @@ -629,57 +629,57 @@ - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + - + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - + @@ -691,15 +691,15 @@ - + - - + + @@ -715,7 +715,7 @@ - + @@ -737,33 +737,33 @@ - - + + - - - - - - + + + + + + - - - - - - - - - - - - - + + + + + + + + + + + + + diff --git a/pyproject.toml b/pyproject.toml index 7d9889f6..934d1bc7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,17 +11,7 @@ name = "devsetgo_lib" version = "0.13.0" requires-python = ">=3.9" description = """ -The devsetgo_lib is a comprehensive Python library that provides a collection of reusable functions designed to increase coding efficiency and enhance code reusability across multiple applications. This library aims to save developers time and effort by reducing the need for repetitive code, allowing defects to be addressed quickly and propagated across projects. The key features of devsetgo_lib include: - -1. **File Operations**: Functions for reading, writing, and managing CSV, JSON, and text files, as well as directory handling operations. -2. **Calendar Utilities**: Functions for handling dates and times, including converting between month names and numbers. -3. **Pattern Matching**: Functions for matching and manipulating strings using regular expressions, helping to simplify text processing tasks. -4. **Logging**: Advanced logging configuration and management using the loguru library, allowing for customizable and robust logging solutions. -5. **FastAPI Endpoints**: Functions for creating and managing endpoints in FastAPI applications, including system health checks and HTTP response code generation. -6. **Async Database Handling**: Asynchronous CRUD operations for databases, with support for various databases including SQLite and PostgreSQL. -7. **Email Validation**: Functions to validate and handle email addresses, ensuring data integrity and correctness in applications. - -The devsetgo_lib is designed to be easy to use and versatile, making it a valuable tool for any Python developer looking to improve their workflow and maintain high-quality code across their projects. +DevSetGo Library is a Python library offering reusable functions for efficient coding. It includes file operations, calendar utilities, pattern matching, advanced logging with loguru, FastAPI endpoints, async database handling, and email validation. Designed for ease of use and versatility, it's a valuable tool for Python developers. """ keywords = ["python", "library", "reusable functions", "file operations", "calendar utilities", "pattern matching", "logging", "loguru", "FastAPI", "async database", "CRUD operations", "email validation", "development tools"] readme = "README.md" From f947b6eb3d68f121f9d3c55a7bcd4c982d747de1 Mon Sep 17 00:00:00 2001 From: devsetgo Date: Thu, 13 Jun 2024 15:35:58 +0000 Subject: [PATCH 02/18] adding PDF reading scripts... tbd --- coverage.xml | 4 +- unreleased/pdf_margin.py | 48 +++++++++ unreleased/pdf_processing.py | 179 +++++++++++++++++++++++++++++++ unreleased/pdf_sample.pdf | Bin 0 -> 24450 bytes unreleased/pdf_sample_narrow.pdf | Bin 0 -> 24396 bytes unreleased/pdf_script.py | 51 +++++++++ 6 files changed, 280 insertions(+), 2 deletions(-) create mode 100644 unreleased/pdf_margin.py create mode 100644 unreleased/pdf_processing.py create mode 100644 unreleased/pdf_sample.pdf create mode 100644 unreleased/pdf_sample_narrow.pdf create mode 100644 unreleased/pdf_script.py diff --git a/coverage.xml b/coverage.xml index a53b63f5..bddbae6a 100644 --- a/coverage.xml +++ b/coverage.xml @@ -1,6 +1,6 @@ - - + + /workspaces/devsetgo_lib diff --git a/unreleased/pdf_margin.py b/unreleased/pdf_margin.py new file mode 100644 index 00000000..faa08263 --- /dev/null +++ b/unreleased/pdf_margin.py @@ -0,0 +1,48 @@ +import fitz # PyMuPDF + +def get_margins(pdf_path): + try: + # Open the PDF file + document = fitz.open(pdf_path) + page = document[0] # Get the first page + + # Get page dimensions + page_rect = page.rect + page_width, page_height = page_rect.width, page_rect.height + + # Get text blocks + text_blocks = page.get_text("dict")["blocks"] + + # Initialize bounding box + text_x0, text_y0 = page_width, page_height + text_x1, text_y1 = 0, 0 + + # Iterate through text blocks to find the bounding box + for block in text_blocks: + if block['type'] == 0: # block['type'] == 0 indicates a text block + bbox = block['bbox'] + text_x0 = min(text_x0, bbox[0]) + text_y0 = min(text_y0, bbox[1]) + text_x1 = max(text_x1, bbox[2]) + text_y1 = max(text_y1, bbox[3]) + + # Calculate margins + left_margin = text_x0 + right_margin = page_width - text_x1 + top_margin = text_y0 + bottom_margin = page_height - text_y1 + + return { + "left_margin": left_margin, + "right_margin": right_margin, + "top_margin": top_margin, + "bottom_margin": bottom_margin + } + except Exception as e: + print(f"Error processing {pdf_path}: {e}") + return None +# Measure margins for the provided PDF files +pdf_files = ['pdf_sample.pdf', 'pdf_sample_narrow.pdf'] +for pdf_file in pdf_files: + margins = get_margins(pdf_file) + print(f"Margins for {pdf_file}: {margins}") diff --git a/unreleased/pdf_processing.py b/unreleased/pdf_processing.py new file mode 100644 index 00000000..e3be9e49 --- /dev/null +++ b/unreleased/pdf_processing.py @@ -0,0 +1,179 @@ +from fastapi import FastAPI, UploadFile, File +from fastapi.responses import ORJSONResponse +import time +import io +from pypdf import PdfReader +from loguru import logger + +app = FastAPI() + +@app.post('/validate-pdf', response_class=ORJSONResponse, status_code=201) +async def check_pdf( + file: UploadFile = File(...), + include_text: bool = False, + check_text: bool = False, + include_page_errors: bool = False +): + response = dict() + t0 = time.time() + + response["file_name"] = file.filename + response["content_type"] = file.content_type + response["file_size"] = file.size + filters = { + "include_text": include_text, + "check_text": check_text + } + + if file.content_type != "application/pdf": + message = f"File is not a PDF, but type {file.content_type}" + logger.error(message) + response["message"] = message + return ORJSONResponse(content=response, status_code=400) + + pdf_content = await file.read() + reader = PdfReader(io.BytesIO(pdf_content)) + + if len(reader.pages) == 0: + message = "The PDF is empty" + logger.error(message) + response["message"] = message + return ORJSONResponse(content=response, status_code=400) + + response["page_count"] = len(reader.pages) + + meta = reader.metadata + if meta is None: + message = "The PDF does not contain meta data" + logger.error(message) + response["message"] = message + return ORJSONResponse(content=response, status_code=400) + + cleaned_meta = {k: str(v).replace("\x00", "") for k, v in meta.items()} + response["meta"] = cleaned_meta + + text = "" + if check_text: + results = get_pdf_content(pdf_content=pdf_content) + text = results["text"] + if not text.strip(): + message = "The PDF does not contain readable text" + logger.error(message) + response["message"] = message + return ORJSONResponse(content=response, status_code=400) + + common_words = ["the", "and", "is"] + words_found = [word for word in common_words if word in text] + if len(words_found) == 0: + message = "The PDF does not contain readable text, like the word 'the'" + logger.error(message) + response["message"] = message + return ORJSONResponse(content=response, status_code=400) + + response["characters"] = len(text) + response["words_found"] = words_found + if include_page_errors: + response["errors"] = results["errors"] + + if reader.is_encrypted: + message = "The PDF is encrypted and not allowed" + logger.error(message) + response["message"] = message + return ORJSONResponse(content=response, status_code=400) + + embedded_fonts = [] + for page in tqdm(reader.pages, desc="Finding Fonts"): + fonts = page.get_fonts() + for font in fonts: + font_name = font.get("BaseFont", "").replace("/", "").replace("+", "") + if font_name not in embedded_fonts: + embedded_fonts.append(font_name) + + if not embedded_fonts: + message = "The PDF does not have embedded fonts" + logger.error(message) + response["message"] = message + return ORJSONResponse(content=response, status_code=400) + + response["fonts"] = embedded_fonts + form_fields = any("/AcroForm" in reader.trailer for _ in reader.pages) + if form_fields: + message = "The PDF contains form fields" + logger.error(message) + response["message"] = message + return ORJSONResponse(content=response, status_code=400) + + if include_text: + response["text"] = text + + t1 = time.time() + logger.debug(f"PDF check response: {response}") + response["processing_time_seconds"] = f"{t1 - t0:.2f}" + return ORJSONResponse(content=response, status_code=201) + + +# Function to extract data from a PDF file + + +# coding: utf-8 +import io +import re +from functools import lru_cache + +from loguru import logger # Import the Loguru logger +from pypdf import PdfReader, PaperSize +from tqdm import tqdm +from unsync import unsync + +@unsync +def extract_pdf_text(pdf_content, page_number: int): + try: + reader = get_reader(pdf_content) + page = reader.pages[page_number].extract_text(extraction_mode="layout", layout_mode_strip_rotated=True) + text = reader.pages[page_number].extract_text() + box = reader.pages[page_number].mediabox + + print(f"left {box.left}") + print(f"right {box.right}") + print(f"lower left {box.lower_left}") + print(f"lower right {box.lower_right}") + print(f"upper left {box.upper_left}") + print(f"upper right {box.upper_right}") + print(f"top {box.top}") + print(f"bottom {box.bottom}") + + return {"text": text, "page_num": page_number, "margin": box, "error": None} + except Exception as ex: + logger.error(ex) + return {"text": "", "page_num": page_number, "margin": None, "error": ex} + +@lru_cache(maxsize=300, typed=False) +def get_reader(pdf_content): + reader = PdfReader(io.BytesIO(pdf_content)) + return reader + +def is_valid_ssn(ssn): + ssn_regex = re.compile(r"^(?!000|666)[0-8]\d{2}-(?!00)\d{2}-(?!0000)\d{4}$") + return bool(ssn_regex.match(ssn)) + +def get_pdf_content(pdf_content): + reader = PdfReader(io.BytesIO(pdf_content)) + + tasks = [ + extract_pdf_text(pdf_content=pdf_content, page_number=page_number) + for page_number in tqdm(range(len(reader.pages)), desc="PDF Text Processing") + ] + + results = [task.result() for task in tqdm(tasks, desc="PDF Text Results")] + + results.sort(key=lambda x: x["page_num"]) + combined_text = "\n".join([result["text"] for result in results]) + has_ssn = is_valid_ssn(combined_text) + margins = [result["margin"] for result in results] + error_list = [result for result in results if result["error"] is not None] + + for result in results: + if result["error"] is not None: + error_list.append(f"Error on page {result['page_num']} of {result['error']}") + + return {"text": combined_text, "margins": margins, "errors": error_list, "PII": has_ssn} diff --git a/unreleased/pdf_sample.pdf b/unreleased/pdf_sample.pdf new file mode 100644 index 0000000000000000000000000000000000000000..2c9d3e25f80cccff330237726cf661237e2696fa GIT binary patch literal 24450 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�)$7Z)KmOx@jR%b5%BH;O_l>4S zH*RW}-|T|t%Kc;GfA^=oPn~Kyx@T0HPRZwM&pa{uOT&tmxz!(@$ye_6TW%n~TjMw2 zMriJ|d9PplW9j+fH@?0T&}utUPQDsw&A9dC{+(ylKfH2BQ}ox7w|0NBQkQaLN$%n! zcM9v`H~o|}wy5qwpOpj4N@RZ|De;7*-_L@~Py)fK==lUeClH(f%vgdE5(plP4iSij zk-)=y+fCvx;BbwrX42`fMjbO-A*pfyfnDWw)RpH<`CasdfbJ7r^3Q~Mp zfY~_~2V-zB4&g)?gi+o-l;*{541%;eP!=L^j0&SXOo1pG7~42GMunqrlB){r8BFyh z7!gobknBas&YeEOGYTLs<+evO4$8U9#>q+vAUqX9I5uJ-AS-nPad=h+VN~i2ho?aT zxUudC0(42m;5?9k-B_R*ORum9jCcyWvIvaa)r$h;!bqqnf&nA&_z#SL0uC7?+#^rQ zxkS+ISV758T4xE6bhM;Vr#@7t#8wwwK0a!Cr=Am{1kUr9ubPnT%w$8F$w4dxYPdzK zxb~L{1z|R76>(e?6_u$oEizBbv6*J(M9(thq!~n`A|zOotKJ`QhZ8*0Di%2$nQD@BI-NmI6lArf zlC&s_B*l;nqXY}3eVN6PoUOFjCv-;Wq&EYNg1`}z7h1C%T7`n>h@0sV*J2Nn5(_d| zGe~%Wq=OKN1Ho9`$)O02G8=4GyEVm8xr>Oi+KfaBknFoaSaVK;ogX@ANIbDeA|1tf zAhM|$4P6yyX4%rEdW{Csi&S+2@+{BY$scAD!OWcEWGbG!=PH`szh zW@xoUiBVIOhV-(OJb@%*PFePIJUuKQb>$#|sHp$kmW+(f&O}A2^;Sa`R#k}92WMrO zjcPVRN2zFzQgWesO38+Xg)74tI#j6(qv=pFTqWqkd8y3qzPM08A}w}DGKk|nN;D5& znc|6G;NkxZ%t7={(xWc-wyU>YJ?cP@jC*%iZ@YTbfgTz6?yjD-%l)+v2X8njcAeaV zAD;X{8B42h!nt1l;?Yw+S8ujwrYA3h)6Fci1GJsoqP`v%BR<^W_!v_PIn$hDf=eGV zE`fkcHv$XXy2XMeAuCDfN+M#%kdgWX4F@MB9mPiIDGts_a8`nYlIJ-o_`h3V!KZ&q zPo_C9o+wxUc|avK@TKHZ<+Womn>P%7G4~gKyP&xU>go)ROP1&B*pDR!5 zTk%}$J=gtn1N#jQ>!-a~)u(`ZnVwD*^oxntJtoHgKFJ@wrMuMDczxBEv1hf#_x&s< z-XC(e&&8p`rpT^GHb^B0_)~E03jaGEAiB(R9O4N4%0o;V5tc5D=3y=I!vkX&V8DA} z90kkR9YYKQ!c|@v!Z#MYF%@W#d0`y#nHRy`10Oqq^|cd3kX!SLC*sTD?idSFUp^S( zeCA+zOeAeAelN~eSLa>Wa@w&>T4%c2a>4+$<%>l>R@H+U}frnTQoko!2a{aWa?mL>R@H+ zU}frncc0fBDw%yRAW*LBz9(rx#7bY?_9-pG%?V#j^!8;Kj`!Z<@Xn_^d>yuAkjJx0 zvhVKCYwmykA?>~sm27jkKhzjT 50 and y < 720: + parts.append(text) + +for page_number in range(len(reader.pages)): + try: + text = reader.pages[page_number].extract_text(visitor_text=visitor_body, layout_mode_scale_weight=1.0) + mediabox = reader.pages[page_number].mediabox + cropbox = reader.pages[page_number].cropbox + trimbox = reader.pages[page_number].trimbox + artbox = reader.pages[page_number].artbox + bleedbox = reader.pages[page_number].bleedbox + unit_size = reader.pages[page_number].user_unit + + print(f"Page {page_number}") + # print("begin text.....") + # print(text) + # print("end text.....") + print(f"MediaBox: {mediabox.width}x{mediabox.height} divid by 72 = {mediabox[2] / 72} x {mediabox[3] / 72}") + print(f"BropBox: {cropbox.width}x{cropbox.height} divid by 72 = {cropbox[2] / 72} x {cropbox[3] / 72}") + print(f"TrimBox: {trimbox.width}x{trimbox.height} divid by 72 = {trimbox[2] / 72} x {trimbox[3] / 72}") + print(f"ArtBox: {artbox.width}x{artbox.height} divid by 72 = {artbox[2] / 72} x {artbox[3] / 72}") + print(f"BleedBox: {bleedbox.width}x{bleedbox.height} divid by 72 = {bleedbox[2] / 72} x {bleedbox[3] / 72}") + print(f"Unit Size: {unit_size}") + + except Exception as ex: + print(f"Error on page {page_number}: {ex}") + + + +text_body = "".join(parts) + +print(text_body) +for p in parts: + if len(p) > 100: + print(len(p),p) + +line = "embed code for the video you want to add. You can also type a keyword to search online for the video that best fits" +print(len(line), line) From d8e52e9befe98d2d5bc0e50096f85581d79e658a Mon Sep 17 00:00:00 2001 From: devsetgo Date: Sat, 20 Jul 2024 20:35:16 +0000 Subject: [PATCH 03/18] updating versions --- requirements.txt | 80 ++++++++++++++++++++++++------------------------ 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/requirements.txt b/requirements.txt index 40b84259..9e7268d4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,40 +1,40 @@ -aiomysql==0.2.0 # Vulnerabilities: None -aiosqlite==0.20.0 # Vulnerabilities: None -asyncpg==0.29.0 # Vulnerabilities: None -autoflake==2.3.1 # Vulnerabilities: None -autopep8==2.1.1 # From 2.1.0 | Vulnerabilities: None -black==24.4.2 # Vulnerabilities: None -bump2version==1.0.1 # Vulnerabilities: None -click==8.1.7 # Vulnerabilities: None -cx-Oracle==8.3.0 # Vulnerabilities: None -fastapi[all]==0.111.0 # Vulnerabilities: None -flake8==7.0.0 # Vulnerabilities: None -genbadge[all]==1.1.1 # Vulnerabilities: None -hatchling==1.24.2 # Vulnerabilities: None -loguru==0.7.2 # Vulnerabilities: None -mkdocs-material==9.5.24 # From 9.5.23 | Vulnerabilities: None -mkdocs-print-site-plugin==2.4.1 # Vulnerabilities: None -mkdocstrings[python,shell]==0.25.1 # Vulnerabilities: None -packaging==24.0 # Vulnerabilities: None -pre-commit==3.7.1 # Vulnerabilities: None -psycopg2==2.9.9 # Vulnerabilities: None -Pygments==2.18.0 # Vulnerabilities: None -pylint==3.2.2 # From 3.2.0 | Vulnerabilities: None -pymdown-extensions==10.8.1 # Vulnerabilities: None -pytest==8.2.1 # From 8.2.0 | Vulnerabilities: None -pytest-asyncio==0.23.7 # From 0.23.6 | Vulnerabilities: None -pytest-cov==5.0.0 # Vulnerabilities: None -pytest-mock==3.14.0 # Vulnerabilities: None -pytest-runner==6.0.1 # Vulnerabilities: None -pytest-xdist==3.6.1 # Vulnerabilities: None -pytz==2024.1 # Vulnerabilities: None -PyYAML==6.0.1 # Vulnerabilities: None -ruff==0.4.5 # From 0.4.4 | Vulnerabilities: None -SQLAlchemy==2.0.30 # Vulnerabilities: None -toml==0.10.2 # Vulnerabilities: None -tox==4.15.0 # Vulnerabilities: None -tqdm==4.66.4 # Vulnerabilities: None -twine==5.1.0 # Vulnerabilities: None -watchdog==4.0.1 # From 4.0.0 | Vulnerabilities: None -wheel==0.43.0 # Vulnerabilities: None -xmltodict==0.13.0 # Vulnerabilities: None +aiomysql==0.2.0 # Vulnerabilities: None +aiosqlite==0.20.0 # Vulnerabilities: None +asyncpg==0.29.0 # Vulnerabilities: None +autoflake==2.3.1 # Vulnerabilities: None +autopep8==2.3.1 # From 2.1.1 | Vulnerabilities: None +black==24.4.2 # Vulnerabilities: None +bump2version==1.0.1 # Vulnerabilities: None +click==8.1.7 # Vulnerabilities: None +cx-Oracle==8.3.0 # Vulnerabilities: None +fastapi[all]==0.111.1 # From 0.111.0 | Vulnerabilities: None +flake8==7.1.0 # From 7.0.0 | Vulnerabilities: None +genbadge[all]==1.1.1 # Vulnerabilities: None +hatchling==1.25.0 # From 1.24.2 | Vulnerabilities: None +loguru==0.7.2 # Vulnerabilities: None +mkdocs-material==9.5.29 # From 9.5.24 | Vulnerabilities: None +mkdocs-print-site-plugin==2.5.0 # From 2.4.1 | Vulnerabilities: None +mkdocstrings[python,shell]==0.25.1 # Vulnerabilities: None +packaging==24.1 # From 24.0 | Vulnerabilities: None +pre-commit==3.7.1 # Vulnerabilities: None +psycopg2==2.9.9 # Vulnerabilities: None +Pygments==2.18.0 # Vulnerabilities: None +pylint==3.2.5 # From 3.2.2 | Vulnerabilities: None +pymdown-extensions==10.8.1 # Vulnerabilities: None +pytest==8.3.1 # From 8.2.1 | Vulnerabilities: None +pytest-asyncio==0.23.8 # From 0.23.7 | Vulnerabilities: None +pytest-cov==5.0.0 # Vulnerabilities: None +pytest-mock==3.14.0 # Vulnerabilities: None +pytest-runner==6.0.1 # Vulnerabilities: None +pytest-xdist==3.6.1 # Vulnerabilities: None +pytz==2024.1 # Vulnerabilities: None +PyYAML==6.0.1 # Vulnerabilities: None +ruff==0.5.4 # From 0.4.5 | Vulnerabilities: None +SQLAlchemy==2.0.31 # From 2.0.30 | Vulnerabilities: None +toml==0.10.2 # Vulnerabilities: None +tox==4.16.0 # From 4.15.0 | Vulnerabilities: None +tqdm==4.66.4 # Vulnerabilities: None +twine==5.1.1 # From 5.1.0 | Vulnerabilities: None +watchdog==4.0.1 # Vulnerabilities: None +wheel==0.43.0 # Vulnerabilities: None +xmltodict==0.13.0 # Vulnerabilities: None From 455df152d09edbeaa3f260399bc40b3f2a53d711 Mon Sep 17 00:00:00 2001 From: devsetgo Date: Sat, 20 Jul 2024 20:52:28 +0000 Subject: [PATCH 04/18] adding an unreleased idea --- unreleased/pdf_margin.py | 137 ++++++++++++++++++++++++++++++++------- 1 file changed, 115 insertions(+), 22 deletions(-) diff --git a/unreleased/pdf_margin.py b/unreleased/pdf_margin.py index faa08263..e74760dc 100644 --- a/unreleased/pdf_margin.py +++ b/unreleased/pdf_margin.py @@ -1,48 +1,141 @@ import fitz # PyMuPDF +import os +from tqdm import tqdm +import json +from datetime import datetime +from pytz import timezone -def get_margins(pdf_path): +def check_interference(page, corner, width, height): try: - # Open the PDF file - document = fitz.open(pdf_path) - page = document[0] # Get the first page + page_rect = page.rect + page_width, page_height = page_rect.width, page_rect.height + + if corner == "top_right": + x0, y0 = page_width - width, 0 + x1, y1 = page_width, height + elif corner == "bottom_right": + x0, y0 = page_width - width, page_height - height + x1, y1 = page_width, page_height + else: + raise ValueError("Invalid corner, can only be top_right or bottom_right") + + check_rect = fitz.Rect(x0, y0, x1, y1) + + text_blocks = page.get_text("dict")["blocks"] + for block in text_blocks: + if block["type"] == 0: + bbox = fitz.Rect(block["bbox"]) + if check_rect.intersects(bbox): + return f"Text interference detected in {corner} corner." - # Get page dimensions + images = page.get_images(full=True) + for img in images: + xref = img[0] + img_rect = fitz.Rect(page.get_image_bbox(xref)) + if check_rect.intersects(img_rect): + return f"Image interference detected in {corner} corner." + + return None + except Exception as e: + return f"Error in processing {corner} {page}: {e}" + +def get_margin_by_page(page): + try: page_rect = page.rect page_width, page_height = page_rect.width, page_rect.height - # Get text blocks text_blocks = page.get_text("dict")["blocks"] + text = page.get_text().encode("utf8") - # Initialize bounding box text_x0, text_y0 = page_width, page_height text_x1, text_y1 = 0, 0 - # Iterate through text blocks to find the bounding box for block in text_blocks: - if block['type'] == 0: # block['type'] == 0 indicates a text block - bbox = block['bbox'] + if block["type"] == 0: + bbox = block["bbox"] text_x0 = min(text_x0, bbox[0]) text_y0 = min(text_y0, bbox[1]) text_x1 = max(text_x1, bbox[2]) text_y1 = max(text_y1, bbox[3]) - # Calculate margins left_margin = text_x0 right_margin = page_width - text_x1 top_margin = text_y0 bottom_margin = page_height - text_y1 + corners = [ + {"corner": "top_right", "width": 144, "height": 36}, + {"corner": "bottom_right", "width": 36, "height": 216}, + ] + + interference = [] + for c in corners: + inter = check_interference( + page=page, corner=c["corner"], width=c["width"], height=c["height"] + ) + if inter and not inter.startswith("Error in"): + interference.append(f"{c['corner']} {inter}") + return { - "left_margin": left_margin, - "right_margin": right_margin, - "top_margin": top_margin, - "bottom_margin": bottom_margin + "left_margin": round(left_margin / 72, 2), + "right_margin": round(right_margin / 72, 2), + "top_margin": round(top_margin / 72, 2), + "bottom_margin": round(bottom_margin / 72, 2), + "page_text": text, + "interference": interference, } except Exception as e: - print(f"Error processing {pdf_path}: {e}") - return None -# Measure margins for the provided PDF files -pdf_files = ['pdf_sample.pdf', 'pdf_sample_narrow.pdf'] -for pdf_file in pdf_files: - margins = get_margins(pdf_file) - print(f"Margins for {pdf_file}: {margins}") + print(f"Error processing {page}: {e}") + return {"error": str(e)} + +def run_pdf(pdf_folder, output_folder, safety_margin=0.4): + dir_list = [f for f in os.listdir(pdf_folder) if f.endswith(".pdf")][:200] + + page_count = 0 + document_margins = [] + for pdf_file in tqdm(dir_list, desc="processing file", leave=True): + document = fitz.open(f"{pdf_folder}/{pdf_file}") + + margin_list = [] + for i in tqdm(range(len(document)), desc=f"processing {pdf_file}", leave=False): + page = document[i] + margin_dict = get_margin_by_page(page) + margin_list.append({"page": i + 1, "margin": margin_dict}) + page_count += 1 + + document_margins.append({"file": pdf_file, "margins": margin_list}) + + margin_issues = [] + for da in document_margins: + file_name = da.get("file") + margin_page_list = [] + for a in da.get("margins"): + page_number = a.get("page") + page_margin = a.get("margin") + warnings = [] + + if "interference" in page_margin.items(): + for key, value in page_margin.items(): + if key.endswith("_margin") and value <= safety_margin: + warnings.append(key) + + if page_margin["interference"]: + warnings.extend(page_margin["interference"]) + + if warnings: + margin_page_list.append({"page_number": page_number, "issues": warnings}) + + if margin_page_list: + margin_issues.append({"file": file_name, "issues": margin_page_list}) + + dt = datetime.now().astimezone(timezone("America/New_York")) + timestamp = dt.strftime("%Y-%m-%d-%H%M") + + output_path = os.path.join(output_folder, f"margin_issues_{timestamp}.json") + with open(output_path, "w") as write_file: + json.dump(margin_issues, write_file) + + print(f"Page count: {page_count} | len(margin_issues): {len(margin_issues)}") + +if __name__ == "__main__": + run_pdf(pdf_folder="/your/pdf/folder/", output_folder="/your/output/folder/") From 97c6f234432ebe889df6157de50f5719eaf2c5e2 Mon Sep 17 00:00:00 2001 From: devsetgo Date: Sat, 20 Jul 2024 20:53:21 +0000 Subject: [PATCH 05/18] making parallel processing --- examples/log_example.py | 91 ++++++++++++++++++++++++----------------- 1 file changed, 54 insertions(+), 37 deletions(-) diff --git a/examples/log_example.py b/examples/log_example.py index 1c9ab3c5..2c69089f 100644 --- a/examples/log_example.py +++ b/examples/log_example.py @@ -5,43 +5,29 @@ License: MIT """ import logging -import random import secrets - +import threading from loguru import logger from tqdm import tqdm - from dsg_lib.common_functions import logging_config +# Configure logging as before logging_config.config_log( - logging_directory='log', # Directory where logs will be stored - log_name='log', # Name of the log file - logging_level='DEBUG', # Logging level - log_rotation='500 MB', # Log rotation size - log_retention='10 days', # Log retention period - log_backtrace=True, # Enable backtrace - # log_format="{time:YYYY-MM-DD HH:mm:ss.SSSSSS} | {level: <8} | {name}:{function}:{line} - {message}", # Log format - log_serializer=False, # Disable log serialization - log_diagnose=True, # Enable diagnose - app_name='my_app', # Application name - append_app_name=True, # Append application name to the log file name + logging_directory='log', + log_name='log', + logging_level='DEBUG', + log_rotation='1 MB', + log_retention='10 days', + log_backtrace=True, + log_serializer=False, + log_diagnose=True, + # app_name='my_app', + # append_app_name=True, + file_sink=True, + intercept_standard_logging=True, + enqueue=False ) -# after configuring logging -# user loguru to log messages -logger.debug('This is a debug message') -logger.info('This is an info message') -logger.error('This is an error message') -logger.warning('This is a warning message') -logger.critical('This is a critical message') - -# will intercept all standard logging messages also -logging.debug('This is a debug message') -logging.info('This is an info message') -logging.error('This is an error message') -logging.warning('This is a warning message') -logging.critical('This is a critical message') - def div_zero(x, y): try: @@ -56,12 +42,43 @@ def div_zero_two(x, y): return x / y -a = div_zero(x=1, y=0) -b = div_zero_two(x=1, y=0) -for _ in tqdm(range(5000), ascii=True): - big_string = '' - for _ in range(random.randint(275, 1000)): - big_string += f'{secrets.token_urlsafe(random.randint(1,5))} ' - # log a lot of data - logging.debug(f'Lets make this a big message {big_string}') +def log_big_string(_): + big_string = secrets.token_urlsafe(256) + for _ in range(100): + logging.debug(f'Lets make this a big message {big_string}') + div_zero(x=1, y=0) + div_zero_two(x=1, y=0) + # after configuring logging + # user loguru to log messages + logger.debug('This is a debug message') + logger.info('This is an info message') + logger.error('This is an error message') + logger.warning('This is a warning message') + logger.critical('This is a critical message') + + # will intercept all standard logging messages also + logging.debug('This is a debug message') + logging.info('This is an info message') + logging.error('This is an error message') + logging.warning('This is a warning message') + logging.critical('This is a critical message') + + + +def worker(): + for _ in tqdm(range(100), ascii=True): # Adjusted for demonstration + log_big_string(None) + +def main(): + threads = [] + for _ in range(4): # Create x threads + t = threading.Thread(target=worker) + threads.append(t) + t.start() + + for t in threads: + t.join() + +if __name__ == "__main__": + main() From 5891390a95ca229127ad23bd5f7af0df9d5e6f49 Mon Sep 17 00:00:00 2001 From: devsetgo Date: Sat, 20 Jul 2024 20:53:49 +0000 Subject: [PATCH 06/18] adding help for missing log file issue for high volume --- dsg_lib/common_functions/logging_config.py | 40 +++++++++++++++++++--- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/dsg_lib/common_functions/logging_config.py b/dsg_lib/common_functions/logging_config.py index b1b93c17..e2dd8a7b 100644 --- a/dsg_lib/common_functions/logging_config.py +++ b/dsg_lib/common_functions/logging_config.py @@ -35,7 +35,7 @@ Date: 2024/05/16 License: MIT """ - +import time import logging from pathlib import Path from uuid import uuid4 @@ -55,6 +55,9 @@ def config_log( log_diagnose: bool = False, app_name: str = None, append_app_name: bool = False, + enqueue: bool = True, + intercept_standard_logging: bool = True, + file_sink: bool = True, ): """ Configures and sets up a logger using the loguru package. @@ -140,7 +143,7 @@ def config_log( log_path, level=logging_level.upper(), format=log_format, - enqueue=True, + enqueue=enqueue, backtrace=log_backtrace, rotation=log_rotation, retention=log_retention, @@ -197,9 +200,36 @@ def emit(self, record): # Configure standard logging to use interceptor handler logging.basicConfig(handlers=[InterceptHandler()], level=logging_level.upper()) - # Add interceptor handler to all existing loggers - for name in logging.root.manager.loggerDict: - logging.getLogger(name).addHandler(InterceptHandler()) + if intercept_standard_logging: + # Add interceptor handler to all existing loggers + for name in logging.root.manager.loggerDict: + logging.getLogger(name).addHandler(InterceptHandler()) # Set the root logger's level to the lowest level possible logging.getLogger().setLevel(logging.NOTSET) + + + class ResilientFileSink: + def __init__(self, path, max_retries=5, retry_delay=0.1): + self.path = path + self.max_retries = max_retries + self.retry_delay = retry_delay + + def write(self, message): + for attempt in range(self.max_retries): + try: + with open(self.path, 'a') as file: + file.write(str(message)) + break # Successfully written, break the loop + except FileNotFoundError: + if attempt < self.max_retries - 1: + time.sleep(self.retry_delay) # Wait before retrying + else: + raise # Reraise if max retries exceeded + + if file_sink: + # Create an instance of ResilientFileSink + resilient_sink = ResilientFileSink(str(log_path)) + + # Configure the logger to use the ResilientFileSink + logger.add(resilient_sink, format=log_format) From 44f3cb173662c7a43f0f1b4221e598015068b24b Mon Sep 17 00:00:00 2001 From: devsetgo Date: Sat, 20 Jul 2024 21:01:05 +0000 Subject: [PATCH 07/18] wokring on bug --- dsg_lib/common_functions/logging_config.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/dsg_lib/common_functions/logging_config.py b/dsg_lib/common_functions/logging_config.py index e2dd8a7b..a2464810 100644 --- a/dsg_lib/common_functions/logging_config.py +++ b/dsg_lib/common_functions/logging_config.py @@ -227,9 +227,18 @@ def write(self, message): else: raise # Reraise if max retries exceeded + + basic_config_handlers = [] if file_sink: # Create an instance of ResilientFileSink resilient_sink = ResilientFileSink(str(log_path)) # Configure the logger to use the ResilientFileSink - logger.add(resilient_sink, format=log_format) + basic_config_handlers.append(resilient_sink) + + basic_config_handlers = [] + if intercept_standard_logging: + basic_config_handlers.append(InterceptHandler()) + + if len(basic_config_handlers) > 0: + logging.basicConfig(handlers=basic_config_handlers, level=logging_level.upper()) From 64f3f18c9eb94c5e30ea36bd0bf3313d2ca73dec Mon Sep 17 00:00:00 2001 From: devsetgo Date: Sat, 20 Jul 2024 21:01:14 +0000 Subject: [PATCH 08/18] imrpoving tests --- .../test_logging_config.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/tests/test_common_functions/test_logging_config.py b/tests/test_common_functions/test_logging_config.py index 6693db53..9516ddde 100644 --- a/tests/test_common_functions/test_logging_config.py +++ b/tests/test_common_functions/test_logging_config.py @@ -10,10 +10,20 @@ class TestConfigLog(unittest.TestCase): @patch('dsg_lib.common_functions.logging_config.logger') def test_config_log_with_valid_params(self, mock_logger): config_log( - logging_directory='logs', - log_name='app.log', - logging_level='DEBUG', - log_rotation='1 MB', + logging_directory = 'log', + log_name= 'log', + logging_level= 'INFO', + log_rotation= '2 MB', + log_retention= '30 days', + log_backtrace = False, + log_format= None, + log_serializer = False, + log_diagnose = False, + app_name= None, + append_app_name = False, + enqueue = True, + intercept_standard_logging = True, + file_sink = True, ) mock_logger.configure.assert_called_once() mock_logger.add.assert_called_once() From 2317459ff9358fcbfa89ec46ec5a54bd7c3e89ee Mon Sep 17 00:00:00 2001 From: devsetgo Date: Sat, 20 Jul 2024 21:07:35 +0000 Subject: [PATCH 09/18] run of tests and documenation updates --- coverage-badge.svg | 2 +- coverage.xml | 78 ++++++++++++++-------- dsg_lib/common_functions/logging_config.py | 46 +++++++------ 3 files changed, 79 insertions(+), 47 deletions(-) diff --git a/coverage-badge.svg b/coverage-badge.svg index 792dcf68..e2abb10d 100644 --- a/coverage-badge.svg +++ b/coverage-badge.svg @@ -1 +1 @@ -coverage: 30.44%coverage30.44% +coverage: 13.25%coverage13.25% diff --git a/coverage.xml b/coverage.xml index bddbae6a..bf3dc72e 100644 --- a/coverage.xml +++ b/coverage.xml @@ -1,6 +1,6 @@ - - + + /workspaces/devsetgo_lib @@ -311,7 +311,7 @@ - + @@ -621,38 +621,64 @@ - + + - - - - - - - - + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dsg_lib/common_functions/logging_config.py b/dsg_lib/common_functions/logging_config.py index a2464810..30b78e7f 100644 --- a/dsg_lib/common_functions/logging_config.py +++ b/dsg_lib/common_functions/logging_config.py @@ -1,38 +1,38 @@ # -*- coding: utf-8 -*- """ -This module provides a function to configure and set up a logger using the loguru package. +This module provides a comprehensive logging setup using the loguru library, facilitating easy logging management for Python applications. The `config_log` function, central to this module, allows for extensive customization of logging behavior. It supports specifying the logging directory, log file name, logging level, and controls for log rotation, retention, and formatting among other features. Additionally, it offers advanced options like backtrace and diagnose for in-depth debugging, and the ability to append the application name to the log file for clearer identification. -The `config_log` function takes several optional parameters to customize the logger's behavior, -including the logging directory, log name, logging level, log rotation size, log retention period, -and more. It also provides an option to append the application name to the log file name. - -Example: -```python +Usage example: from dsg_lib.common_functions.logging_config import config_log config_log( - logging_directory='logs', # Directory where logs will be stored - log_name='log', # Name of the log file (extension will be added automatically set v0.12.2) - logging_level='DEBUG', # Logging level - log_rotation='100 MB', # Log rotation size - log_retention='30 days', # Log retention period - log_backtrace=True, # Enable backtrace - log_format="{time:YYYY-MM-DD HH:mm:ss.SSSSSS} | {level: <8} | {name}:{function}:{line} - {message}", # Log format - log_serializer=False, # Disable log serialization - log_diagnose=True, # Enable diagnose - app_name='my_app', # Application name - append_app_name=True # Append application name to the log file name + logging_directory='logs', # Directory for storing logs + log_name='log', # Base name for log files + logging_level='DEBUG', # Minimum logging level + log_rotation='100 MB', # Size threshold for log rotation + log_retention='30 days', # Duration to retain old log files + log_backtrace=True, # Enable detailed backtraces in logs + log_format="{time:YYYY-MM-DD HH:mm:ss.SSSSSS} | {level: <8} | {name}:{function}:{line} - {message}", # Custom log format + log_serializer=False, # Toggle log serialization + log_diagnose=True, # Enable diagnostic information in logs + app_name='my_app', # Application name for log identification + append_app_name=True # Append application name to log file names + enqueue=True, # Enqueue log messages + intercept_standard_logging=True, # Intercept standard Python logging + file_sink=True # Use a file sink for logging ) +# Example log messages logger.debug("This is a debug message") logger.info("This is an info message") logger.error("This is an error message") logger.warning("This is a warning message") logger.critical("This is a critical message") -``` Author: Mike Ryan -Date: 2024/05/16 +DateCreated: 2021/07/16 +DateUpdated: 2024/07/24 + License: MIT """ import time @@ -74,6 +74,9 @@ def config_log( - log_diagnose (bool): Whether to enable diagnose. Default is False. - app_name (str): The application name. Default is None. - append_app_name (bool): Whether to append the application name to the log file name. Default is False. + - enqueue (bool): Whether to enqueue log messages. Default is True. + - intercept_standard_logging (bool): Whether to intercept standard logging. Default is True. + - file_sink (bool): Whether to use a file sink. Default is True. Raises: - ValueError: If the provided logging level is not valid. @@ -94,6 +97,9 @@ def config_log( log_diagnose=True, app_name='my_app', append_app_name=True + enqueue=True, + intercept_standard_logging=True, + file_sink=True ) ``` """ From 7608451e9a9cfadf579d019ccabc006cc393ac45 Mon Sep 17 00:00:00 2001 From: devsetgo Date: Sat, 20 Jul 2024 23:42:40 +0000 Subject: [PATCH 10/18] updates --- dsg_lib/common_functions/logging_config.py | 30 ++++++++++++++-------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/dsg_lib/common_functions/logging_config.py b/dsg_lib/common_functions/logging_config.py index 30b78e7f..7bf23e75 100644 --- a/dsg_lib/common_functions/logging_config.py +++ b/dsg_lib/common_functions/logging_config.py @@ -3,6 +3,8 @@ This module provides a comprehensive logging setup using the loguru library, facilitating easy logging management for Python applications. The `config_log` function, central to this module, allows for extensive customization of logging behavior. It supports specifying the logging directory, log file name, logging level, and controls for log rotation, retention, and formatting among other features. Additionally, it offers advanced options like backtrace and diagnose for in-depth debugging, and the ability to append the application name to the log file for clearer identification. Usage example: + +```python from dsg_lib.common_functions.logging_config import config_log config_log( @@ -11,15 +13,7 @@ logging_level='DEBUG', # Minimum logging level log_rotation='100 MB', # Size threshold for log rotation log_retention='30 days', # Duration to retain old log files - log_backtrace=True, # Enable detailed backtraces in logs - log_format="{time:YYYY-MM-DD HH:mm:ss.SSSSSS} | {level: <8} | {name}:{function}:{line} - {message}", # Custom log format - log_serializer=False, # Toggle log serialization - log_diagnose=True, # Enable diagnostic information in logs - app_name='my_app', # Application name for log identification - append_app_name=True # Append application name to log file names enqueue=True, # Enqueue log messages - intercept_standard_logging=True, # Intercept standard Python logging - file_sink=True # Use a file sink for logging ) # Example log messages @@ -28,6 +22,7 @@ logger.error("This is an error message") logger.warning("This is a warning message") logger.critical("This is a critical message") +``` Author: Mike Ryan DateCreated: 2021/07/16 @@ -89,7 +84,7 @@ def config_log( logging_directory='logs', log_name='app.log', logging_level='DEBUG', - log_rotation='500 MB', + log_rotation='100 MB', log_retention='10 days', log_backtrace=True, log_format="{time:YYYY-MM-DD HH:mm:ss.SSSSSS} | {level: <8} | {name}:{function}:{line} - {message}", @@ -216,6 +211,19 @@ def emit(self, record): class ResilientFileSink: + """ + A file sink designed for resilience, capable of retrying write operations. + + This class implements a resilient file writing mechanism that attempts to write messages to a file, retrying the operation a specified number of times if it fails. This is particularly useful in scenarios where write operations might intermittently fail due to temporary issues such as file system locks or networked file system delays. + + Attributes: + path (str): The path to the file where messages will be written. + max_retries (int): The maximum number of retry attempts for a failed write operation. + retry_delay (float): The delay between retry attempts, in seconds. + + Methods: + write(message): Attempts to write a message to the file, retrying on failure up to `max_retries` times. + """ def __init__(self, path, max_retries=5, retry_delay=0.1): self.path = path self.max_retries = max_retries @@ -234,7 +242,8 @@ def write(self, message): raise # Reraise if max retries exceeded - basic_config_handlers = [] + basic_config_handlers:list = [] + if file_sink: # Create an instance of ResilientFileSink resilient_sink = ResilientFileSink(str(log_path)) @@ -242,7 +251,6 @@ def write(self, message): # Configure the logger to use the ResilientFileSink basic_config_handlers.append(resilient_sink) - basic_config_handlers = [] if intercept_standard_logging: basic_config_handlers.append(InterceptHandler()) From 3bd20fdd691af4dce8a3a385220c3fd540304886 Mon Sep 17 00:00:00 2001 From: devsetgo Date: Sun, 21 Jul 2024 00:22:22 +0000 Subject: [PATCH 11/18] fixes --- dsg_lib/common_functions/logging_config.py | 12 ++++++---- examples/log_example.py | 27 +++++++++++----------- 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/dsg_lib/common_functions/logging_config.py b/dsg_lib/common_functions/logging_config.py index 7bf23e75..56b1f888 100644 --- a/dsg_lib/common_functions/logging_config.py +++ b/dsg_lib/common_functions/logging_config.py @@ -91,7 +91,7 @@ def config_log( log_serializer=False, log_diagnose=True, app_name='my_app', - append_app_name=True + append_app_name=True, enqueue=True, intercept_standard_logging=True, file_sink=True @@ -112,6 +112,7 @@ def config_log( log_format = '{time:YYYY-MM-DD HH:mm:ss.SSSSSS} | {level: <8} | {name}:{function}:{line} - {message}' # pragma: no cover if log_serializer is True: + log_format = '{message}' # pragma: no cover log_name = f'{log_name}.json' # pragma: no cover else: log_name = f'{log_name}.log' # pragma: no cover @@ -153,6 +154,8 @@ def config_log( diagnose=log_diagnose, ) + basic_config_handlers:list = [] + class InterceptHandler(logging.Handler): """ Interceptor for standard logging. @@ -198,14 +201,15 @@ def emit(self, record): level, record.getMessage() ) # pragma: no cover - # Configure standard logging to use interceptor handler - logging.basicConfig(handlers=[InterceptHandler()], level=logging_level.upper()) if intercept_standard_logging: # Add interceptor handler to all existing loggers for name in logging.root.manager.loggerDict: logging.getLogger(name).addHandler(InterceptHandler()) + # Add interceptor handler to the root logger + basic_config_handlers.append(InterceptHandler()) + # Set the root logger's level to the lowest level possible logging.getLogger().setLevel(logging.NOTSET) @@ -242,8 +246,6 @@ def write(self, message): raise # Reraise if max retries exceeded - basic_config_handlers:list = [] - if file_sink: # Create an instance of ResilientFileSink resilient_sink = ResilientFileSink(str(log_path)) diff --git a/examples/log_example.py b/examples/log_example.py index 2c69089f..71329b33 100644 --- a/examples/log_example.py +++ b/examples/log_example.py @@ -16,10 +16,10 @@ logging_directory='log', log_name='log', logging_level='DEBUG', - log_rotation='1 MB', + log_rotation='100 MB', log_retention='10 days', log_backtrace=True, - log_serializer=False, + log_serializer=True, log_diagnose=True, # app_name='my_app', # append_app_name=True, @@ -43,14 +43,14 @@ def div_zero_two(x, y): -def log_big_string(_): - big_string = secrets.token_urlsafe(256) - for _ in range(100): +def log_big_string(lqty=100, size=256): + big_string = secrets.token_urlsafe(size) + for _ in range(lqty): logging.debug(f'Lets make this a big message {big_string}') div_zero(x=1, y=0) div_zero_two(x=1, y=0) # after configuring logging - # user loguru to log messages + # use loguru to log messages logger.debug('This is a debug message') logger.info('This is an info message') logger.error('This is an error message') @@ -65,15 +65,14 @@ def log_big_string(_): logging.critical('This is a critical message') +def worker(wqty=100, lqty=100, size=256): + for _ in tqdm(range(wqty), ascii=True): # Adjusted for demonstration + log_big_string(lqty=lqty, size=size) -def worker(): - for _ in tqdm(range(100), ascii=True): # Adjusted for demonstration - log_big_string(None) - -def main(): +def main(wqty=100, lqty=100, size=256, workers=2): threads = [] - for _ in range(4): # Create x threads - t = threading.Thread(target=worker) + for _ in range(workers): # Create workers threads + t = threading.Thread(target=worker, args=(wqty, lqty, size,)) threads.append(t) t.start() @@ -81,4 +80,4 @@ def main(): t.join() if __name__ == "__main__": - main() + main(wqty=100, lqty=10, size=256, workers=10) From e69ad05dd5c37bd54be9a0eac91e558c47feb157 Mon Sep 17 00:00:00 2001 From: devsetgo Date: Sun, 21 Jul 2024 00:23:24 +0000 Subject: [PATCH 12/18] run of tests --- coverage.xml | 1027 +++++++++++++++++++++++++------------------------- 1 file changed, 513 insertions(+), 514 deletions(-) diff --git a/coverage.xml b/coverage.xml index bf3dc72e..45520ad6 100644 --- a/coverage.xml +++ b/coverage.xml @@ -1,9 +1,9 @@ - + - /workspaces/devsetgo_lib + /github/workspace @@ -16,7 +16,7 @@ - + @@ -30,7 +30,7 @@ - + @@ -41,15 +41,15 @@ - - + + - - - - - - + + + + + + @@ -94,7 +94,7 @@ - + @@ -109,21 +109,21 @@ - - - + + + - - - - + + + + - + @@ -133,219 +133,219 @@ - - - - - - - - - + + + + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + - - - - - - - - - - - + + + + + + + + + + + - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + - - - - - - - - - - - + + + + + + + + + + + - - - - - - - - - - - + + + + + + + + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - + - + - - - - - - - - - - - + + + + + + + + + + + - - - - - - - - - + + + + + + + + + - + @@ -356,46 +356,46 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + @@ -409,164 +409,164 @@ - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - + + + + + + + + + + - - - - - - - - - - - - - + + + + + + + + + + + + + - - - - - - - - - - - + + + + + + + + + + + - + @@ -578,134 +578,133 @@ - - - - - - - - + + + + + + + + - - - - - - - + + + + + + + - - - - - - - - - - - - - + + + + + + + + + + + + + - - - - - - - - - - + + + + + + + + + + - + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - - + + + + + + + - + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - + @@ -717,15 +716,15 @@ - + - - + + @@ -741,7 +740,7 @@ - + @@ -763,33 +762,33 @@ - - + + - - - - - - + + + + + + - - - - - - - - - - - - - + + + + + + + + + + + + + From 50e61fb0b8eae6f4faa124dffe580d4d4d941abd Mon Sep 17 00:00:00 2001 From: devsetgo Date: Sun, 21 Jul 2024 00:28:33 +0000 Subject: [PATCH 13/18] testing --- coverage.xml | 16 +++------------- dsg_lib/common_functions/logging_config.py | 2 +- 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/coverage.xml b/coverage.xml index 45520ad6..e1d5a125 100644 --- a/coverage.xml +++ b/coverage.xml @@ -1,5 +1,5 @@ - + @@ -311,7 +311,7 @@ - + @@ -621,7 +621,7 @@ - + @@ -661,16 +661,6 @@ - - - - - - - - - - diff --git a/dsg_lib/common_functions/logging_config.py b/dsg_lib/common_functions/logging_config.py index 56b1f888..a5093418 100644 --- a/dsg_lib/common_functions/logging_config.py +++ b/dsg_lib/common_functions/logging_config.py @@ -233,7 +233,7 @@ def __init__(self, path, max_retries=5, retry_delay=0.1): self.max_retries = max_retries self.retry_delay = retry_delay - def write(self, message): + def write(self, message): # pragma: no cover for attempt in range(self.max_retries): try: with open(self.path, 'a') as file: From 715d4169aad4b2006eff29ca6047cb8ed9d9bbb9 Mon Sep 17 00:00:00 2001 From: devsetgo Date: Sun, 21 Jul 2024 00:33:45 +0000 Subject: [PATCH 14/18] fixes and tests --- coverage.xml | 2 +- pyproject.toml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/coverage.xml b/coverage.xml index e1d5a125..25b90910 100644 --- a/coverage.xml +++ b/coverage.xml @@ -1,5 +1,5 @@ - + diff --git a/pyproject.toml b/pyproject.toml index 934d1bc7..6e51b7d6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -116,9 +116,9 @@ quote-style = "single" [tool.flake8] -max-line-length = 132 -max-doc-length = 132 -ignore = ["E302","E501"] +max-line-length = 100 +max-doc-length = 100 +ignore = ["E302", "E501","E303"] # Keeping the ignores the same as before since ruff's specific ignores aren't directly transferable exclude = [ ".git", "__pycache__", From 6d0898578c84d26a210b22a85b51d0a9792e0922 Mon Sep 17 00:00:00 2001 From: devsetgo Date: Sun, 21 Jul 2024 00:36:24 +0000 Subject: [PATCH 15/18] bumping versoon to 0.13.1 --- .bumpversion.cfg | 2 +- dsg_lib/__init__.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 5d9ae2e3..19f7a104 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.13.0 +current_version = 0.13.1 commit = False tag = False parse = (?P\d+)\.(?P\d+)\.(?P\d+)(-(?P[a-z]+)(?P\d+))? diff --git a/dsg_lib/__init__.py b/dsg_lib/__init__.py index 20aa0748..682d036b 100644 --- a/dsg_lib/__init__.py +++ b/dsg_lib/__init__.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- -__version__ = '0.13.0' +__version__ = '0.13.1' diff --git a/pyproject.toml b/pyproject.toml index 6e51b7d6..b293a9a1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ build-backend = "hatchling.build" [project] name = "devsetgo_lib" -version = "0.13.0" +version = "0.13.1" requires-python = ">=3.9" description = """ DevSetGo Library is a Python library offering reusable functions for efficient coding. It includes file operations, calendar utilities, pattern matching, advanced logging with loguru, FastAPI endpoints, async database handling, and email validation. Designed for ease of use and versatility, it's a valuable tool for Python developers. From 564fb2d60b9463c096f5f0f45551cc02515b4fcb Mon Sep 17 00:00:00 2001 From: devsetgo Date: Sun, 21 Jul 2024 00:43:49 +0000 Subject: [PATCH 16/18] working on max tokens --- .github/workflows/autofill_pullrequest.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/autofill_pullrequest.yml b/.github/workflows/autofill_pullrequest.yml index 9872baa5..420614df 100644 --- a/.github/workflows/autofill_pullrequest.yml +++ b/.github/workflows/autofill_pullrequest.yml @@ -19,5 +19,5 @@ jobs: with: github_token: ${{ secrets.GITHUB_TOKEN }} openai_api_key: ${{ secrets.OPENAI_API_KEY }} - max_tokens: 4000 - openai_model: gpt-4 + max_tokens: 8192 + openai_model: gpt-4o From 833c45d052b88dd6863ce5cc470e58245b5f7abf Mon Sep 17 00:00:00 2001 From: devsetgo Date: Sun, 21 Jul 2024 00:46:05 +0000 Subject: [PATCH 17/18] changing model and tokens --- .github/workflows/autofill_pullrequest.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/autofill_pullrequest.yml b/.github/workflows/autofill_pullrequest.yml index 420614df..01ab79f8 100644 --- a/.github/workflows/autofill_pullrequest.yml +++ b/.github/workflows/autofill_pullrequest.yml @@ -19,5 +19,5 @@ jobs: with: github_token: ${{ secrets.GITHUB_TOKEN }} openai_api_key: ${{ secrets.OPENAI_API_KEY }} - max_tokens: 8192 - openai_model: gpt-4o + max_tokens: 20000 + openai_model: gpt-4o-mini From 24ba7121faf6db95a2c91f5b55c4db48f3fc240e Mon Sep 17 00:00:00 2001 From: devsetgo Date: Sun, 21 Jul 2024 00:48:10 +0000 Subject: [PATCH 18/18] setting to max tokens --- .github/workflows/autofill_pullrequest.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/autofill_pullrequest.yml b/.github/workflows/autofill_pullrequest.yml index 01ab79f8..c7ba10de 100644 --- a/.github/workflows/autofill_pullrequest.yml +++ b/.github/workflows/autofill_pullrequest.yml @@ -19,5 +19,5 @@ jobs: with: github_token: ${{ secrets.GITHUB_TOKEN }} openai_api_key: ${{ secrets.OPENAI_API_KEY }} - max_tokens: 20000 + max_tokens: 16384 openai_model: gpt-4o-mini