diff --git a/.gitignore b/.gitignore index 199e786..ea6888e 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ **/__pycache__ .pytest_cache .ruff_cache -junit \ No newline at end of file +junit +.idea \ No newline at end of file diff --git a/Makefile b/Makefile index eaa1d6a..ab1f7ec 100644 --- a/Makefile +++ b/Makefile @@ -17,4 +17,20 @@ lint: .PHONY: test test: @echo Running tests - @poetry run pytest -v \ No newline at end of file + @poetry run pytest -v + +.PHONY: generate +generate: + @echo Generating images + @poetry run python image_handler/scripts/generate.py + +.PHONY: health_check_fix +health_check_fix: + @echo Checking database health + @poetry run python image_handler/scripts/health_check.py --fix + + +.PHONY: health_check +health_check: + @echo Checking database health + @poetry run python image_handler/scripts/health_check.py \ No newline at end of file diff --git a/README.md b/README.md index 3c3d063..60c2284 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ ### Useful commands: - Creating the environment: `make local-setup` -- Entering the environment: `poetry shell` -- Leaving the environment: `exit` - Installing libraries in the environment: `make install` -- Run the Flask application: `python image_handler/demo.py` +- Run the health check script: `make health_check` +- Run the health check script with fixes: `make health_check_fix` +- Run the generate images script: `make generate` Download ollama from https://ollama.com/download Once installed, run ollama.exe, then launch terminal and type ''' ollama run llama3''' diff --git a/image_handler/constants.py b/image_handler/constants.py new file mode 100644 index 0000000..d79d40e --- /dev/null +++ b/image_handler/constants.py @@ -0,0 +1,17 @@ +IMAGE_URL = "http://127.0.0.1:5000/image" +DATE_URL = "http://127.0.0.1:5000/date" +IMAGE_VERIFICATION_URL = "http://localhost:5000/image/verification-portal" + +START_DATE = 1725163200 +SECONDS_PER_DAY = 86400 + +NUM_INFERENCE_STEPS = 1 +GUIDANCE_SCALE = 7 + +# ANSI escape codes for colors +GREEN = "\033[32m" +RED = "\033[31m" +WHITE = "\033[37m" +RESET = "\033[0m" + +MAX_IMAGES = 5 diff --git a/image_handler/db.py b/image_handler/db.py new file mode 100644 index 0000000..eb90b63 --- /dev/null +++ b/image_handler/db.py @@ -0,0 +1,96 @@ +import http.client +import io +import json +from http import HTTPStatus +from typing import Union + +import requests +from image_handler.constants import DATE_URL, IMAGE_URL +from image_handler_client.schemas.image_info import ImageInfo +from PIL import Image + + +def get_date(): + response = requests.get( + f"{DATE_URL}/latest", + timeout=10, + ) + return json.loads(response.text) + + +def save_image(image: Union[Image.Image, str], info: ImageInfo): + if isinstance(image, Image.Image): + image_type = "jpeg" + image_bytes = io.BytesIO() + image.save(image_bytes, format=image_type) + image_bytes.seek(0) + + files = {"file": (info.filename, image_bytes, f"image/{image_type}")} + + response = requests.post( + f"{IMAGE_URL}/create", + { + "real": info.real, + "date": info.date, + "theme": info.theme, + "status": info.status, + }, + files=files, + timeout=10, + ) + else: + response = requests.post( + f"{IMAGE_URL}/create", + { + "url": image, + "real": info.real, + "date": info.date, + "theme": info.theme, + "status": info.status, + }, + files=None, + timeout=5, + ) + + status = response.status_code + if status == http.client.OK: + print(f"Image [{info.filename}] successfully saved") + else: + print(f"Failed to save image [{info.filename}]") + print(response.text) + + +def get_grouped_images_by_date() -> dict: + response = requests.get(f"{IMAGE_URL}/all-images", timeout=10) + response = response.json() + + date_image_map = {} + + # Iterate through each image in the response + for image in response: + date = image["date"] + + # If the date is not already a key, create a new list for it + if date not in date_image_map: + date_image_map[date] = [] + + # Append the image to the list associated with this date + date_image_map[date].append(image) + + return date_image_map + + +def delete_rejected_images(date): + """Send a DELETE request to delete the rejected image by date and filename.""" + try: + response = requests.delete( + f"{DATE_URL}/delete-rejected/{date}", + timeout=10, + ) + if response.status_code == HTTPStatus.OK: + print(f"Successfully deleted rejected images for date: {date}") + else: + print(f"Failed to delete rejects for day: {date}. Status code: {response.status_code}") + print(f"Response: {response.text}") + except requests.exceptions.RequestException as e: + print(f"Error deleting image: {e}") diff --git a/image_handler/errors.py b/image_handler/errors.py index 9c8671e..fdd33b7 100644 --- a/image_handler/errors.py +++ b/image_handler/errors.py @@ -3,3 +3,13 @@ def __init__(self, expected, received): super().__init__(f"Expected {expected} images, but received {received}.") self.expected = expected self.received = received + + +class TooManyRequestsError(Exception): + def __init__(self): + super().__init__("Exceeded maximum number of requests.") + + +class OllamaError(Exception): + def __init__(self): + super().__init__("Ensure Ollama is running locally.") diff --git a/image_handler/handle_rectification.py b/image_handler/handle_rectification.py new file mode 100644 index 0000000..e5af0dd --- /dev/null +++ b/image_handler/handle_rectification.py @@ -0,0 +1,171 @@ +from image_handler.constants import GUIDANCE_SCALE, NUM_INFERENCE_STEPS +from image_handler.db import delete_rejected_images, save_image +from image_handler.image_handler_instance import get_image_handler +from image_handler.ollama_bridge import create_prompt +from image_handler.pexel_bridge import fetch_image, resize_image, save_pexel_images +from image_handler.util import calculate_images_to_generate, convert_to_readable_date, count_images, get_image_from_url +from image_handler_client.schemas.image_info import ImageInfo, ImageStatus + + +def handle_missing_images(date, data): + count = count_images(data) + real_count = count["real"] + ai_count = count["ai"] + generate = calculate_images_to_generate(real_count, ai_count) + print( + f"Generating {generate['ai']} AI images and " + f"{generate['real']} real images for date: {convert_to_readable_date(date)}" + ) + generate["real"] += generate["ai"] + generate["ai"] = 0 + + real_counter = 5 + for i in range(generate["real"]): + response_data = fetch_image(data[0]["theme"], 80) + photos_data = response_data["photos"] + photo_urls = [photo["src"]["original"] for photo in photos_data] + + answer = "n" + while answer != "y": + image = resize_image(get_image_from_url(photo_urls[real_counter])) + image.show() + real_counter += 1 + + answer = input(f'accept image for theme [{data[0]["theme"]}]? y/n: ') + if answer == "y": + info = ImageInfo( + filename="filename", + date=date, + theme=data[0]["theme"], + real=True, + status=ImageStatus.VERIFIED.value, + ) + save_image(image, info) + break + + for i in range(generate["ai"]): + image_handler_instance = get_image_handler() + + prompt_dict = create_prompt(theme=data[0]["theme"]) + print(f"Using prompt: {prompt_dict['prompt']}") + image_handler_instance.enqueue_prompt_to_image( + info=ImageInfo( + filename="filename", + date=date, + theme=data[0]["theme"], + real=False, + status=ImageStatus.UNVERIFIED.value, + ), + kwargs={ + "prompt": prompt_dict["prompt"], + "negative_prompt": prompt_dict["negative_prompt"], + "num_inference_steps": NUM_INFERENCE_STEPS, + "guidance_scale": GUIDANCE_SCALE, + "width": 512, + "height": 512, + }, + ) + + image_handler_instance.stop_processing() + + +def handle_rejected_images(date: int, data: list): + real_counter = 5 + + if not any(image["status"] == ImageStatus.REJECTED.value for image in data): + return + + # replace rejected images + delete_rejected_images(date) + + for image_data in data: + image_info = ImageInfo(**image_data) + + # handle real images + if image_info.status == ImageStatus.REJECTED.value: + if image_info.real: + response_data = fetch_image(image_info.theme, 80) + photos_data = response_data["photos"] + photo_urls = [photo["src"]["original"] for photo in photos_data] + + answer = "n" + while answer != "y": + image = resize_image(get_image_from_url(photo_urls[real_counter])) + image.show() + real_counter += 1 + + answer = input("accept? y/n: ") + if answer == "y": + info = ImageInfo( + filename=image_info.filename, + date=date, + theme=image_info.theme, + real=True, + status=ImageStatus.VERIFIED.value, + ) + save_image(image, info) + break + + # handle AI images + else: + image_handler_instance = get_image_handler() + + prompt_dict = create_prompt(theme=image_info.theme) + print(f"Using prompt: {prompt_dict['prompt']}") + image_handler_instance.enqueue_prompt_to_image( + info=ImageInfo( + filename=f"{image_info.theme}_{image_info.filename.split('_')[-1]}", + date=date, + theme=image_info.theme, + real=False, + status=ImageStatus.UNVERIFIED.value, + ), + kwargs={ + "prompt": prompt_dict["prompt"], + "negative_prompt": prompt_dict["negative_prompt"], + "num_inference_steps": NUM_INFERENCE_STEPS, + "guidance_scale": GUIDANCE_SCALE, + "width": 512, + "height": 512, + }, + ) + + image_handler_instance.stop_processing() + + +def add_images(theme: str, date: int, num_ai_images: int, num_pexel_images: int, starting_number: int = 0): + image_handler_instance = get_image_handler() + + save_pexel_images( + info=ImageInfo( + filename=f"{theme}_{starting_number}", + date=date, + theme=theme, + real=True, + status=ImageStatus.UNVERIFIED.value, + ), + num_images=num_pexel_images, + skip=starting_number, + ) + + for i in range(num_ai_images): + prompt_dict = create_prompt(theme=theme) + print(f"Using prompt: {prompt_dict['prompt']}") + + image_handler_instance.enqueue_prompt_to_image( + info=ImageInfo( + filename=f"{theme}_{i+num_pexel_images+starting_number}", + date=date, + theme=theme, + real=False, + status=ImageStatus.UNVERIFIED.value, + ), + kwargs={ + "prompt": prompt_dict["prompt"], + "negative_prompt": prompt_dict["negative_prompt"], + "num_inference_steps": NUM_INFERENCE_STEPS, + "guidance_scale": GUIDANCE_SCALE, + "width": 512, + "height": 512, + }, + ) diff --git a/image_handler/image_generator.py b/image_handler/image_generator.py index 44b6ce5..0e05d70 100644 --- a/image_handler/image_generator.py +++ b/image_handler/image_generator.py @@ -1,16 +1,8 @@ -import http.client -import io -import json -import os import queue -import sys import threading from enum import Enum from typing import Union -import httpx -import ollama -import requests import torch from aws_lambda_powertools import Logger from diffusers import ( @@ -18,16 +10,13 @@ EulerAncestralDiscreteScheduler, ) from dotenv import load_dotenv -from image_handler_client.schemas.image_info import ImageInfo, ImageStatus -from PIL import Image, ImageOps - -from errors import InsufficientImagesError +from image_handler.db import save_image +from image_handler.util import get_image_from_url +from image_handler_client.schemas.image_info import ImageInfo +from PIL import Image load_dotenv() logger = Logger() -PEXELS_BASE_URL = "https://api.pexels.com/v1" -URL = "http://127.0.0.1:5000/image/create" -DATE_URL = "http://127.0.0.1:5000/date/latest" class DataType(Enum): @@ -36,121 +25,19 @@ class DataType(Enum): class ImageHandler: - def __init__(self): + def __init__(self, sync=False): self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu") logger.info(f"Initialized for {self.device}") - # Initialize the asynchronous queue and thread - self.queue = queue.Queue() - self.thread = threading.Thread(target=self._process_queue) - self.thread.daemon = True - self.thread.start() - - # create prompt from theme input - def create_prompt(self, theme: str) -> dict: - try: - response = ollama.chat( - model="llama3", - keep_alive=0, - messages=[ - { - "role": "user", - "content": f"generate a prompt for an ai image model to create a real photograph of {theme}. " - f"Only output the prompt, nothing else, as the response will be fed " - f"directly to the image generator. Make sure the prompt is under 75 words.", - }, - ], - ) - except httpx.ConnectError: - print("Error: 'ollama' service needs to be running. Please start it and try again.") - sys.exit(1) - - prompt_info = { - "prompt": response["message"]["content"], - "negative_prompt": ( - "bad lighting, out of focus, blurred, poorly composed, low resolution, " - "extra elements, hands, people, non-food items, cartoonish, animated, " - "unrealistic, uncentered, over saturated zoomed in" - ), - } - return prompt_info - - def resize_image(self, image): - """ - Resize an image to 512x512 pixels without distorting its aspect ratio. - Crops the image to a centered square before resizing. - - :param image: Path to the input image file. - :return: Resized image object. - """ - min_side = min(image.size) - - left = (image.width - min_side) / 2 - top = (image.height - min_side) / 2 - right = (image.width + min_side) / 2 - bottom = (image.height + min_side) / 2 - - img_cropped = image.crop((left, top, right, bottom)) - resized_img = img_cropped.resize((512, 512)) - - return resized_img - - def get_image_from_pexel(self, info: ImageInfo, theme: str, num_images: int): - response = requests.get( - f"{PEXELS_BASE_URL}/search", - params={ - "query": theme, - "per_page": num_images, - "orientation": "square", - }, - headers={"Authorization": os.getenv("PEXELS_TOKEN")}, - timeout=5, - ) - response_data = response.json() - - if num_images > response_data["total_results"]: - response = requests.get( - f"{PEXELS_BASE_URL}/search", - params={ - "query": theme, - "per_page": num_images - response_data["total_results"], - }, - headers={"Authorization": os.getenv("PEXELS_TOKEN")}, - timeout=5, - ) - new_response_data = response.json() - - response_data["photos"].extend(new_response_data["photos"]) - response_data["total_results"] += len(new_response_data["photos"]) - - if num_images > response_data["total_results"]: - raise InsufficientImagesError(num_images, response_data["total_results"]) + self.sync = sync + logger.info(f"Sync: {self.sync}") - photos_data = response_data["photos"] - photo_urls = [photo["src"]["original"] for photo in photos_data] - - i = 0 - for url in photo_urls: - image = self.get_image_from_url(url) - new_image = self.resize_image(image) - - filename = f"{theme}_{int(info.filename.split("_")[-1])+i}" - info_instance = ImageInfo( - filename=filename, - date=info.date, - theme=info.theme, - real=info.real, - status=ImageStatus.UNVERIFIED.value, - ) - save_image(new_image, info_instance) - i += 1 - - # get image from url - def get_image_from_url(self, url: str) -> Image.Image: - image = Image.open(requests.get(url, stream=True, timeout=10).raw) - image = ImageOps.exif_transpose(image) - image = image.convert("RGB") - return image + # Initialize the asynchronous queue and thread + if sync: + self.queue = queue.Queue() + self.thread = threading.Thread(target=self._process_queue) + self.thread.daemon = True + self.thread.start() # add image to image task to queue def enqueue_image_to_image( @@ -164,7 +51,7 @@ def enqueue_image_to_image( # pylint: disable=unused-argument if isinstance(image, str): - image = self.get_image_from_url(image) + image = get_image_from_url(image) if image is None: raise ValueError("Image is None") @@ -175,8 +62,12 @@ def enqueue_image_to_image( "kwargs": kwargs, "info": info, } - self.queue.put(task) - logger.info(f"\nEnqueued: {info.filename}") + + print(f"\nEnqueued ai: {info.filename}") + if not self.sync: + self.queue.put(task) + else: + self.process(task) # add text to image task to queue def enqueue_prompt_to_image( @@ -201,8 +92,12 @@ def enqueue_prompt_to_image( "kwargs": kwargs, "info": info, } - self.queue.put(task) - logger.info(f"\nEnqueued: {info.filename}") + + print(f"\nEnqueued ai: {info.filename}") + if not self.sync: + self.queue.put(task) + else: + self.process(task) # Continuously process the queue def _process_queue(self): @@ -258,54 +153,3 @@ def process_prompt(self, item): text_pipe.scheduler = EulerAncestralDiscreteScheduler.from_config(text_pipe.scheduler.config) return text_pipe(**item["kwargs"]).images[0] - - -# save image to database -def save_image(image: Union[Image.Image, str], info: ImageInfo): - if isinstance(image, Image.Image): - image_type = "jpeg" - image_bytes = io.BytesIO() - image.save(image_bytes, format=image_type) - image_bytes.seek(0) - - files = {"file": (info.filename, image_bytes, f"image/{image_type}")} - - response = requests.post( - URL, - { - "real": False, - "date": info.date, - "theme": info.theme, - "status": ImageStatus.UNVERIFIED.value, - }, - files=files, - timeout=10, - ) - else: - response = requests.post( - URL, - { - "url": image, - "real": True, - "date": info.date, - "theme": info.theme, - "status": ImageStatus.UNVERIFIED.value, - }, - files=None, - timeout=5, - ) - - status = response.status_code - if status == http.client.OK: - logger.info(f"Image [{info.filename}] successfully saved") - else: - logger.error(f"Failed to save image [{info.filename}]") - logger.error(response.text) - - -def get_date(): - response = requests.get( - DATE_URL, - timeout=10, - ) - return json.loads(response.text) diff --git a/image_handler/image_handler_instance.py b/image_handler/image_handler_instance.py new file mode 100644 index 0000000..4389e6d --- /dev/null +++ b/image_handler/image_handler_instance.py @@ -0,0 +1,11 @@ +from image_handler.image_generator import ImageHandler + +image_handler = None +sync = True + + +def get_image_handler(): + global image_handler + if image_handler is None: + image_handler = ImageHandler(sync=sync) + return image_handler diff --git a/image_handler/main.py b/image_handler/main.py deleted file mode 100644 index 42bc409..0000000 --- a/image_handler/main.py +++ /dev/null @@ -1,67 +0,0 @@ -import secrets - -from image_handler_client.schemas.image_info import ImageInfo, ImageStatus - -from image_generator import ImageHandler, get_date - -image_handler = ImageHandler() -GUIDANCE_SCALE = None - -NUM_INFERENCE_STEPS = 5 - - -def add_images(theme: str): - # get the date to complete - date = get_date().get("date") - if not date: - date = 0 - date += 1 - print(f"Saving to date: {date}") - - # choose number of ai images to generate (1-4) - num_ai_images = secrets.randbelow(4) + 1 - print(f"Generating {num_ai_images} AI images") - - for i in range(num_ai_images): - prompt_dict = image_handler.create_prompt(theme=theme) - print(f"Using prompt: {prompt_dict['prompt']}") - image_handler.enqueue_prompt_to_image( - info=ImageInfo( - filename=f"{theme}_{i}", - date=date, - theme=theme, - real=False, - status=ImageStatus.UNVERIFIED.value, - ), - kwargs={ - "prompt": prompt_dict["prompt"], - "negative_prompt": prompt_dict["negative_prompt"], - "num_inference_steps": NUM_INFERENCE_STEPS, - "guidance_scale": GUIDANCE_SCALE, - "width": 512, - "height": 512, - }, - ) - - # do the rest as pexel images - num_pexel_images = 5 - num_ai_images - print(f"Getting {num_pexel_images} Pexel images") - - image_handler.get_image_from_pexel( - info=ImageInfo( - filename=f"{theme}_{num_ai_images}", - date=date, - theme=theme, - real=True, - status=ImageStatus.UNVERIFIED.value, - ), - theme=theme, - num_images=num_pexel_images, - ) - - image_handler.stop_processing() - - -if __name__ == "__main__": - user_theme = input("Enter theme: ") - add_images(user_theme) diff --git a/image_handler/ollama_bridge.py b/image_handler/ollama_bridge.py new file mode 100644 index 0000000..75ae792 --- /dev/null +++ b/image_handler/ollama_bridge.py @@ -0,0 +1,31 @@ +import httpx +import ollama +from image_handler.errors import OllamaError + + +def create_prompt(theme: str) -> dict: + try: + response = ollama.chat( + model="llama3", + keep_alive=0, + messages=[ + { + "role": "user", + "content": f"generate a prompt for an ai image model to create a real photograph of {theme}. " + f"Only output the prompt, nothing else, as the response will be fed " + f"directly to the image generator. Make sure the prompt is under 75 words.", + }, + ], + ) + except httpx.ConnectError: + raise OllamaError() + + prompt_info = { + "prompt": response["message"]["content"], + "negative_prompt": ( + "bad lighting, out of focus, blurred, poorly composed, low resolution, " + "extra elements, hands, people, non-food items, cartoonish, animated, " + "unrealistic, uncentered, over saturated zoomed in" + ), + } + return prompt_info diff --git a/image_handler/pexel_bridge.py b/image_handler/pexel_bridge.py new file mode 100644 index 0000000..889cc7b --- /dev/null +++ b/image_handler/pexel_bridge.py @@ -0,0 +1,94 @@ +import os +from http import HTTPStatus + +import requests +from image_handler.db import save_image +from image_handler.errors import InsufficientImagesError, TooManyRequestsError +from image_handler.util import get_image_from_url +from image_handler_client.schemas.image_info import ImageInfo, ImageStatus +from PIL import Image + +PEXELS_BASE_URL = "https://api.pexels.com/v1" + + +def fetch_image(theme: str, num_images: int) -> dict | None: + if num_images <= 0: + return None + + params = { + "query": theme, + "per_page": num_images, + } + + response = requests.get( + f"{PEXELS_BASE_URL}/search", + params=params, + headers={ + "Authorization": os.getenv("PEXELS_TOKEN"), + "user-agent": "Mozilla/5.0 Chrome/68.0.3440.106 Safari/537.36", + }, + timeout=5, + ) + + if response.status_code == HTTPStatus.OK: + return response.json() + elif response.status_code == HTTPStatus.TOO_MANY_REQUESTS: + raise TooManyRequestsError() + elif response.status_code == HTTPStatus.UNAUTHORIZED: + raise ValueError("Unauthorized: Pexels token is missing or invalid") + elif response.status_code == HTTPStatus.INTERNAL_SERVER_ERROR: + raise ValueError(f"Internal server error: {response.text}") + else: + raise ValueError(f"Failed to fetch images: {response.text}") + + +def resize_image(image: Image) -> Image: + """ + Resize an image to 512x512 pixels without distorting its aspect ratio. + Crops the image to a centered square before resizing. + + :param image: Path to the input image file. + :return: Resized image object. + """ + min_side = min(image.size) + + left = (image.width - min_side) / 2 + top = (image.height - min_side) / 2 + right = (image.width + min_side) / 2 + bottom = (image.height + min_side) / 2 + + img_cropped = image.crop((left, top, right, bottom)) + resized_img = img_cropped.resize((512, 512)) + + return resized_img + + +def save_pexel_images(info: ImageInfo, num_images: int, skip: int = 0): + response_data = fetch_image(info.theme, num_images + skip) + if response_data is None: + return + + if num_images > response_data["total_results"]: + raise InsufficientImagesError(num_images, response_data["total_results"]) + + photos_data = response_data["photos"] + photo_urls = [photo["src"]["original"] for photo in photos_data] + + i = 0 + for url in photo_urls: + if i < skip: + i += 1 + continue + image = get_image_from_url(url) + new_image = resize_image(image) + + filename = f"{info.theme}_{int(info.filename.split("_")[-1] ) +i}" + info_instance = ImageInfo( + filename=filename, + date=info.date, + theme=info.theme, + real=info.real, + status=ImageStatus.UNVERIFIED.value, + ) + save_image(new_image, info_instance) + i += 1 diff --git a/image_handler/scripts/generate.py b/image_handler/scripts/generate.py new file mode 100644 index 0000000..85fcb6d --- /dev/null +++ b/image_handler/scripts/generate.py @@ -0,0 +1,55 @@ +from image_handler.constants import SECONDS_PER_DAY, START_DATE +from image_handler.db import get_date, get_grouped_images_by_date +from image_handler.handle_rectification import add_images, handle_missing_images, handle_rejected_images +from image_handler.image_handler_instance import get_image_handler +from image_handler.util import calculate_images_to_generate, print_date + + +def check_last_date(): + date = get_date().get("date") + if not date: + print("No images in database") + return + + data = get_grouped_images_by_date() + + valid = print_date(date, data[date]) + if valid: + return + + handle_rejected_images(date, data[date]) + handle_missing_images(date, data[date]) + + +def generate_images(theme: str): + date = get_date().get("date") + if not date: + date = START_DATE + else: + date += SECONDS_PER_DAY + + generate = calculate_images_to_generate(0, 0) + + add_images( + theme=theme, + date=date, + num_ai_images=generate["ai"], + num_pexel_images=generate["real"], + starting_number=0, + ) + + +if __name__ == "__main__": + check_last_date() + + themes = [ + "cat", + "dog", + "flower", + "car", + ] + + for theme in themes: + generate_images(theme) + + get_image_handler().stop_processing() diff --git a/image_handler/scripts/health_check.py b/image_handler/scripts/health_check.py new file mode 100644 index 0000000..1440f46 --- /dev/null +++ b/image_handler/scripts/health_check.py @@ -0,0 +1,36 @@ +import argparse + +from image_handler.constants import SECONDS_PER_DAY, START_DATE +from image_handler.db import get_date, get_grouped_images_by_date +from image_handler.handle_rectification import handle_missing_images, handle_rejected_images +from image_handler.util import print_date + + +def health_check(fix=False): + end_date = get_date().get("date") + if not end_date: + print("No images in database") + return + + data = get_grouped_images_by_date() + + print(f"There are {((end_date - START_DATE) // SECONDS_PER_DAY)+1} days to check, from {START_DATE} to {end_date}") + + current_date = START_DATE + while current_date <= end_date: + valid = print_date(current_date, data[current_date]) + + if fix and not valid: + handle_rejected_images(current_date, data[current_date]) + handle_missing_images(current_date, data[current_date]) + + # Increment date by one day (86400 seconds) + current_date += SECONDS_PER_DAY + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Health check script") + parser.add_argument("--fix", action="store_true", help="Run health check with fix") + args = parser.parse_args() + + health_check(args.fix) diff --git a/image_handler/util.py b/image_handler/util.py new file mode 100644 index 0000000..0915ce1 --- /dev/null +++ b/image_handler/util.py @@ -0,0 +1,122 @@ +import io +import os +import secrets +from datetime import datetime + +import requests +from image_handler.constants import GREEN, MAX_IMAGES, RED, RESET, WHITE +from image_handler_client.schemas.image_info import ImageInfo, ImageStatus +from PIL import Image + + +def get_image_from_url(url: str) -> Image: + headers = { + "Authorization": os.getenv("PEXELS_TOKEN"), + "User-Agent": "Mozilla/5.0 Chrome/58.0.3029.110 Safari/537.3", + } + response = requests.get(url, stream=True, headers=headers, timeout=5) + + return Image.open(io.BytesIO(response.content)) + + +def print_date(date: int, data: list) -> bool: + date_width = 30 + col_width = 10 + + all_verified = True + + count = count_images(data) + + theme = None + try: + theme = data[0]["theme"] + except IndexError: + pass + + # create list of each image, colouring them based on their status + real_ai_list = [] + for image in data: + image_info = ImageInfo(**image) + if image_info.status == ImageStatus.VERIFIED.value: + colour = GREEN + elif image_info.status == ImageStatus.REJECTED.value: + colour = RED + all_verified = False + else: + colour = WHITE + all_verified = False + + real_ai_list.append(f"{colour}{'real' if image_info.real else 'ai':<{col_width}}{RESET}") + + # If the response is shorter than MAX_IMAGES, fill the remaining slots with blank space + if len(real_ai_list) < MAX_IMAGES: + real_ai_list.extend([f"{'':<{col_width}}"] * (MAX_IMAGES - len(real_ai_list))) + real_ai_string = "".join(real_ai_list) + + date_str = f"{convert_to_readable_date(date)} ({theme}):" + date_str += " " * (date_width - len(date_str)) + + # Determine if the day is a success + if all_verified and len(data) == MAX_IMAGES and count["real"] > 0 and count["ai"] > 0: + status = "SUCCESS" + colour = GREEN + elif ( + any(image["status"] == ImageStatus.REJECTED.value for image in data) + or len(data) != MAX_IMAGES + or count["real"] == 0 + or count["ai"] == 0 + ): + status = "FAILURE" + colour = RED + else: + status = "UNVERIFIED" + colour = WHITE + + # Print the final formatted output with all images and the status + print(f"{colour}{date_str}{RESET}{real_ai_string}{colour}{status}{RESET}") + + if status == "SUCCESS": + return True + if status == "UNVERIFIED" and count["real"] > 0 and count["ai"] > 0 and len(data) == MAX_IMAGES: + return True + return False + + +def count_images(response): + real_count = 0 + ai_count = 0 + for image in response: + if image["real"]: + real_count += 1 + else: + ai_count += 1 + + return {"real": real_count, "ai": ai_count} + + +def convert_to_readable_date(unix_timestamp): + return datetime.fromtimestamp(unix_timestamp).strftime("%Y-%m-%d") + + +def calculate_images_to_generate(real_count, ai_count): + remaining_slots = MAX_IMAGES - (real_count + ai_count) + + # Ensure there's at least one of each type + real_to_generate = 1 if real_count == 0 else 0 + ai_to_generate = 1 if ai_count == 0 else 0 + + # Adjust remaining slots after ensuring at least one of each + remaining_slots -= real_to_generate + ai_to_generate + + # Distribute remaining slots randomly if available + if remaining_slots > 0: + rand = secrets.randbelow(remaining_slots + 1) + real_to_generate += rand + ai_to_generate += remaining_slots - rand + elif remaining_slots < 0: + raise ValueError("No remaining slots but one type is missing") + + return { + "real": real_to_generate, + "ai": ai_to_generate, + } diff --git a/poetry.lock b/poetry.lock index f2dbabe..ec16c15 100644 --- a/poetry.lock +++ b/poetry.lock @@ -87,17 +87,17 @@ wrapt = "*" [[package]] name = "boto3" -version = "1.35.14" +version = "1.35.19" description = "The AWS SDK for Python" optional = false python-versions = ">=3.8" files = [ - {file = "boto3-1.35.14-py3-none-any.whl", hash = "sha256:c3e138e9041d59cd34cdc28a587dfdc899dba02ea26ebc3e10fb4bc88e5cf31b"}, - {file = "boto3-1.35.14.tar.gz", hash = "sha256:7bc78d7140c353b10a637927fe4bc4c4d95a464d1b8f515d5844def2ee52cbd5"}, + {file = "boto3-1.35.19-py3-none-any.whl", hash = "sha256:84b3fe1727945bc3cada832d969ddb3dc0d08fce1677064ca8bdc13a89c1a143"}, + {file = "boto3-1.35.19.tar.gz", hash = "sha256:9979fe674780a0b7100eae9156d74ee374cd1638a9f61c77277e3ce712f3e496"}, ] [package.dependencies] -botocore = ">=1.35.14,<1.36.0" +botocore = ">=1.35.19,<1.36.0" jmespath = ">=0.7.1,<2.0.0" s3transfer = ">=0.10.0,<0.11.0" @@ -106,13 +106,13 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.35.14" +version = "1.35.19" description = "Low-level, data-driven core of boto 3." optional = false python-versions = ">=3.8" files = [ - {file = "botocore-1.35.14-py3-none-any.whl", hash = "sha256:24823135232f88266b66ae8e1d0f3d40872c14cd976781f7fe52b8f0d79035a0"}, - {file = "botocore-1.35.14.tar.gz", hash = "sha256:8515a2fc7ca5bcf0b10016ba05ccf2d642b7cb77d8773026ff2fa5aa3bf38d2e"}, + {file = "botocore-1.35.19-py3-none-any.whl", hash = "sha256:c83f7f0cacfe7c19b109b363ebfa8736e570d24922f16ed371681f58ebab44a9"}, + {file = "botocore-1.35.19.tar.gz", hash = "sha256:42d6d8db7250cbd7899f786f9861e02cab17dc238f64d6acb976098ed9809625"}, ] [package.dependencies] @@ -121,7 +121,7 @@ python-dateutil = ">=2.1,<3.0.0" urllib3 = {version = ">=1.25.4,<2.2.0 || >2.2.0,<3", markers = "python_version >= \"3.10\""} [package.extras] -crt = ["awscrt (==0.21.2)"] +crt = ["awscrt (==0.21.5)"] [[package]] name = "certifi" @@ -424,13 +424,13 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "huggingface-hub" -version = "0.24.6" +version = "0.24.7" description = "Client library to download and publish models, datasets and other repos on the huggingface.co hub" optional = false python-versions = ">=3.8.0" files = [ - {file = "huggingface_hub-0.24.6-py3-none-any.whl", hash = "sha256:a990f3232aa985fe749bc9474060cbad75e8b2f115f6665a9fda5b9c97818970"}, - {file = "huggingface_hub-0.24.6.tar.gz", hash = "sha256:cc2579e761d070713eaa9c323e3debe39d5b464ae3a7261c39a9195b27bb8000"}, + {file = "huggingface_hub-0.24.7-py3-none-any.whl", hash = "sha256:a212c555324c8a7b1ffdd07266bb7e7d69ca71aa238d27b7842d65e9a26ac3e5"}, + {file = "huggingface_hub-0.24.7.tar.gz", hash = "sha256:0ad8fb756e2831da0ac0491175b960f341fe06ebcf80ed6f8728313f95fc0207"}, ] [package.dependencies] @@ -458,13 +458,13 @@ typing = ["types-PyYAML", "types-requests", "types-simplejson", "types-toml", "t [[package]] name = "identify" -version = "2.6.0" +version = "2.6.1" description = "File identification library for Python" optional = false python-versions = ">=3.8" files = [ - {file = "identify-2.6.0-py2.py3-none-any.whl", hash = "sha256:e79ae4406387a9d300332b5fd366d8994f1525e8414984e1a59e058b2eda2dd0"}, - {file = "identify-2.6.0.tar.gz", hash = "sha256:cb171c685bdc31bcc4c1734698736a7d5b6c8bf2e0c15117f4d469c8640ae5cf"}, + {file = "identify-2.6.1-py2.py3-none-any.whl", hash = "sha256:53863bcac7caf8d2ed85bd20312ea5dcfc22226800f6d6881f232d861db5a8f0"}, + {file = "identify-2.6.1.tar.gz", hash = "sha256:91478c5fb7c3aac5ff7bf9b4344f803843dc586832d5f110d672b19aa1984c98"}, ] [package.extras] @@ -472,18 +472,21 @@ license = ["ukkonen"] [[package]] name = "idna" -version = "3.8" +version = "3.10" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.6" files = [ - {file = "idna-3.8-py3-none-any.whl", hash = "sha256:050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac"}, - {file = "idna-3.8.tar.gz", hash = "sha256:d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603"}, + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, ] +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + [[package]] name = "image-handler-client" -version = "0.1.0" +version = "1.0.2" description = "Contains schemas with information about images" optional = false python-versions = "^3.12" @@ -499,27 +502,31 @@ ruff = "^0.5.5" [package.source] type = "git" url = "https://github.com/imaginate-ai/image-handler-client.git" -reference = "v1.0.0" -resolved_reference = "9984f182f7df581270d6fbf055826e5961f5cc1b" +reference = "v1.0.2" +resolved_reference = "b90d4032c939631733a1e79a15406d64be95027d" [[package]] name = "importlib-metadata" -version = "8.4.0" +version = "8.5.0" description = "Read metadata from Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "importlib_metadata-8.4.0-py3-none-any.whl", hash = "sha256:66f342cc6ac9818fc6ff340576acd24d65ba0b3efabb2b4ac08b598965a4a2f1"}, - {file = "importlib_metadata-8.4.0.tar.gz", hash = "sha256:9a547d3bc3608b025f93d403fdd1aae741c24fbb8314df4b155675742ce303c5"}, + {file = "importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b"}, + {file = "importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7"}, ] [package.dependencies] -zipp = ">=0.5" +zipp = ">=3.20" [package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=2.2)"] perf = ["ipython"] -test = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-perf (>=0.9.2)", "pytest-ruff (>=0.2.1)"] +test = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] +type = ["pytest-mypy"] [[package]] name = "iniconfig" @@ -916,13 +923,13 @@ files = [ [[package]] name = "ollama" -version = "0.3.2" +version = "0.3.3" description = "The official Python client for Ollama." optional = false python-versions = "<4.0,>=3.8" files = [ - {file = "ollama-0.3.2-py3-none-any.whl", hash = "sha256:ed2a6f752bd91c49b477d84a259c5657785d7777689d4a27ffe0a4d5b5dd3cae"}, - {file = "ollama-0.3.2.tar.gz", hash = "sha256:7deb3287cdefa1c39cc046163096f8597b83f59ca31a1f8ae78e71eccb7af95f"}, + {file = "ollama-0.3.3-py3-none-any.whl", hash = "sha256:ca6242ce78ab34758082b7392df3f9f6c2cb1d070a9dede1a4c545c929e16dba"}, + {file = "ollama-0.3.3.tar.gz", hash = "sha256:f90a6d61803117f40b0e8ff17465cab5e1eb24758a473cfe8101aff38bc13b51"}, ] [package.dependencies] @@ -1038,13 +1045,13 @@ xmp = ["defusedxml"] [[package]] name = "platformdirs" -version = "4.3.2" +version = "4.3.3" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.8" files = [ - {file = "platformdirs-4.3.2-py3-none-any.whl", hash = "sha256:eb1c8582560b34ed4ba105009a4badf7f6f85768b30126f351328507b2beb617"}, - {file = "platformdirs-4.3.2.tar.gz", hash = "sha256:9e5e27a08aa095dd127b9f2e764d74254f482fef22b0970773bfba79d091ab8c"}, + {file = "platformdirs-4.3.3-py3-none-any.whl", hash = "sha256:50a5450e2e84f44539718293cbb1da0a0885c9d14adf21b77bae4e66fc99d9b5"}, + {file = "platformdirs-4.3.3.tar.gz", hash = "sha256:d4e0b7d8ec176b341fb03cb11ca12d0276faa8c485f9cd218f613840463fc2c0"}, ] [package.extras] @@ -1116,13 +1123,13 @@ test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"] [[package]] name = "pytest" -version = "8.3.2" +version = "8.3.3" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-8.3.2-py3-none-any.whl", hash = "sha256:4ba08f9ae7dcf84ded419494d229b48d0903ea6407b030eaec46df5e6a73bba5"}, - {file = "pytest-8.3.2.tar.gz", hash = "sha256:c132345d12ce551242c87269de812483f5bcc87cdbb4722e48487ba194f9fdce"}, + {file = "pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2"}, + {file = "pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181"}, ] [package.dependencies] @@ -1148,6 +1155,20 @@ files = [ [package.dependencies] six = ">=1.5" +[[package]] +name = "python-dotenv" +version = "1.0.1" +description = "Read key-value pairs from a .env file and set them as environment variables" +optional = false +python-versions = ">=3.8" +files = [ + {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, + {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, +] + +[package.extras] +cli = ["click (>=5.0)"] + [[package]] name = "pyyaml" version = "6.0.2" @@ -1212,90 +1233,105 @@ files = [ [[package]] name = "regex" -version = "2024.7.24" +version = "2024.9.11" description = "Alternative regular expression module, to replace re." optional = false python-versions = ">=3.8" files = [ - {file = "regex-2024.7.24-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:228b0d3f567fafa0633aee87f08b9276c7062da9616931382993c03808bb68ce"}, - {file = "regex-2024.7.24-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3426de3b91d1bc73249042742f45c2148803c111d1175b283270177fdf669024"}, - {file = "regex-2024.7.24-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f273674b445bcb6e4409bf8d1be67bc4b58e8b46fd0d560055d515b8830063cd"}, - {file = "regex-2024.7.24-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23acc72f0f4e1a9e6e9843d6328177ae3074b4182167e34119ec7233dfeccf53"}, - {file = "regex-2024.7.24-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65fd3d2e228cae024c411c5ccdffae4c315271eee4a8b839291f84f796b34eca"}, - {file = "regex-2024.7.24-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c414cbda77dbf13c3bc88b073a1a9f375c7b0cb5e115e15d4b73ec3a2fbc6f59"}, - {file = "regex-2024.7.24-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf7a89eef64b5455835f5ed30254ec19bf41f7541cd94f266ab7cbd463f00c41"}, - {file = "regex-2024.7.24-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19c65b00d42804e3fbea9708f0937d157e53429a39b7c61253ff15670ff62cb5"}, - {file = "regex-2024.7.24-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7a5486ca56c8869070a966321d5ab416ff0f83f30e0e2da1ab48815c8d165d46"}, - {file = "regex-2024.7.24-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6f51f9556785e5a203713f5efd9c085b4a45aecd2a42573e2b5041881b588d1f"}, - {file = "regex-2024.7.24-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a4997716674d36a82eab3e86f8fa77080a5d8d96a389a61ea1d0e3a94a582cf7"}, - {file = "regex-2024.7.24-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:c0abb5e4e8ce71a61d9446040c1e86d4e6d23f9097275c5bd49ed978755ff0fe"}, - {file = "regex-2024.7.24-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:18300a1d78cf1290fa583cd8b7cde26ecb73e9f5916690cf9d42de569c89b1ce"}, - {file = "regex-2024.7.24-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:416c0e4f56308f34cdb18c3f59849479dde5b19febdcd6e6fa4d04b6c31c9faa"}, - {file = "regex-2024.7.24-cp310-cp310-win32.whl", hash = "sha256:fb168b5924bef397b5ba13aabd8cf5df7d3d93f10218d7b925e360d436863f66"}, - {file = "regex-2024.7.24-cp310-cp310-win_amd64.whl", hash = "sha256:6b9fc7e9cc983e75e2518496ba1afc524227c163e43d706688a6bb9eca41617e"}, - {file = "regex-2024.7.24-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:382281306e3adaaa7b8b9ebbb3ffb43358a7bbf585fa93821300a418bb975281"}, - {file = "regex-2024.7.24-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4fdd1384619f406ad9037fe6b6eaa3de2749e2e12084abc80169e8e075377d3b"}, - {file = "regex-2024.7.24-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3d974d24edb231446f708c455fd08f94c41c1ff4f04bcf06e5f36df5ef50b95a"}, - {file = "regex-2024.7.24-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a2ec4419a3fe6cf8a4795752596dfe0adb4aea40d3683a132bae9c30b81e8d73"}, - {file = "regex-2024.7.24-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb563dd3aea54c797adf513eeec819c4213d7dbfc311874eb4fd28d10f2ff0f2"}, - {file = "regex-2024.7.24-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:45104baae8b9f67569f0f1dca5e1f1ed77a54ae1cd8b0b07aba89272710db61e"}, - {file = "regex-2024.7.24-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:994448ee01864501912abf2bad9203bffc34158e80fe8bfb5b031f4f8e16da51"}, - {file = "regex-2024.7.24-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3fac296f99283ac232d8125be932c5cd7644084a30748fda013028c815ba3364"}, - {file = "regex-2024.7.24-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7e37e809b9303ec3a179085415cb5f418ecf65ec98cdfe34f6a078b46ef823ee"}, - {file = "regex-2024.7.24-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:01b689e887f612610c869421241e075c02f2e3d1ae93a037cb14f88ab6a8934c"}, - {file = "regex-2024.7.24-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f6442f0f0ff81775eaa5b05af8a0ffa1dda36e9cf6ec1e0d3d245e8564b684ce"}, - {file = "regex-2024.7.24-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:871e3ab2838fbcb4e0865a6e01233975df3a15e6fce93b6f99d75cacbd9862d1"}, - {file = "regex-2024.7.24-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c918b7a1e26b4ab40409820ddccc5d49871a82329640f5005f73572d5eaa9b5e"}, - {file = "regex-2024.7.24-cp311-cp311-win32.whl", hash = "sha256:2dfbb8baf8ba2c2b9aa2807f44ed272f0913eeeba002478c4577b8d29cde215c"}, - {file = "regex-2024.7.24-cp311-cp311-win_amd64.whl", hash = "sha256:538d30cd96ed7d1416d3956f94d54e426a8daf7c14527f6e0d6d425fcb4cca52"}, - {file = "regex-2024.7.24-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:fe4ebef608553aff8deb845c7f4f1d0740ff76fa672c011cc0bacb2a00fbde86"}, - {file = "regex-2024.7.24-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:74007a5b25b7a678459f06559504f1eec2f0f17bca218c9d56f6a0a12bfffdad"}, - {file = "regex-2024.7.24-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7df9ea48641da022c2a3c9c641650cd09f0cd15e8908bf931ad538f5ca7919c9"}, - {file = "regex-2024.7.24-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a1141a1dcc32904c47f6846b040275c6e5de0bf73f17d7a409035d55b76f289"}, - {file = "regex-2024.7.24-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80c811cfcb5c331237d9bad3bea2c391114588cf4131707e84d9493064d267f9"}, - {file = "regex-2024.7.24-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7214477bf9bd195894cf24005b1e7b496f46833337b5dedb7b2a6e33f66d962c"}, - {file = "regex-2024.7.24-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d55588cba7553f0b6ec33130bc3e114b355570b45785cebdc9daed8c637dd440"}, - {file = "regex-2024.7.24-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:558a57cfc32adcf19d3f791f62b5ff564922942e389e3cfdb538a23d65a6b610"}, - {file = "regex-2024.7.24-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a512eed9dfd4117110b1881ba9a59b31433caed0c4101b361f768e7bcbaf93c5"}, - {file = "regex-2024.7.24-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:86b17ba823ea76256b1885652e3a141a99a5c4422f4a869189db328321b73799"}, - {file = "regex-2024.7.24-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5eefee9bfe23f6df09ffb6dfb23809f4d74a78acef004aa904dc7c88b9944b05"}, - {file = "regex-2024.7.24-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:731fcd76bbdbf225e2eb85b7c38da9633ad3073822f5ab32379381e8c3c12e94"}, - {file = "regex-2024.7.24-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:eaef80eac3b4cfbdd6de53c6e108b4c534c21ae055d1dbea2de6b3b8ff3def38"}, - {file = "regex-2024.7.24-cp312-cp312-win32.whl", hash = "sha256:185e029368d6f89f36e526764cf12bf8d6f0e3a2a7737da625a76f594bdfcbfc"}, - {file = "regex-2024.7.24-cp312-cp312-win_amd64.whl", hash = "sha256:2f1baff13cc2521bea83ab2528e7a80cbe0ebb2c6f0bfad15be7da3aed443908"}, - {file = "regex-2024.7.24-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:66b4c0731a5c81921e938dcf1a88e978264e26e6ac4ec96a4d21ae0354581ae0"}, - {file = "regex-2024.7.24-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:88ecc3afd7e776967fa16c80f974cb79399ee8dc6c96423321d6f7d4b881c92b"}, - {file = "regex-2024.7.24-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:64bd50cf16bcc54b274e20235bf8edbb64184a30e1e53873ff8d444e7ac656b2"}, - {file = "regex-2024.7.24-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eb462f0e346fcf41a901a126b50f8781e9a474d3927930f3490f38a6e73b6950"}, - {file = "regex-2024.7.24-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a82465ebbc9b1c5c50738536fdfa7cab639a261a99b469c9d4c7dcbb2b3f1e57"}, - {file = "regex-2024.7.24-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:68a8f8c046c6466ac61a36b65bb2395c74451df2ffb8458492ef49900efed293"}, - {file = "regex-2024.7.24-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac8e84fff5d27420f3c1e879ce9929108e873667ec87e0c8eeb413a5311adfe"}, - {file = "regex-2024.7.24-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba2537ef2163db9e6ccdbeb6f6424282ae4dea43177402152c67ef869cf3978b"}, - {file = "regex-2024.7.24-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:43affe33137fcd679bdae93fb25924979517e011f9dea99163f80b82eadc7e53"}, - {file = "regex-2024.7.24-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:c9bb87fdf2ab2370f21e4d5636e5317775e5d51ff32ebff2cf389f71b9b13750"}, - {file = "regex-2024.7.24-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:945352286a541406f99b2655c973852da7911b3f4264e010218bbc1cc73168f2"}, - {file = "regex-2024.7.24-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:8bc593dcce679206b60a538c302d03c29b18e3d862609317cb560e18b66d10cf"}, - {file = "regex-2024.7.24-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:3f3b6ca8eae6d6c75a6cff525c8530c60e909a71a15e1b731723233331de4169"}, - {file = "regex-2024.7.24-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:c51edc3541e11fbe83f0c4d9412ef6c79f664a3745fab261457e84465ec9d5a8"}, - {file = "regex-2024.7.24-cp38-cp38-win32.whl", hash = "sha256:d0a07763776188b4db4c9c7fb1b8c494049f84659bb387b71c73bbc07f189e96"}, - {file = "regex-2024.7.24-cp38-cp38-win_amd64.whl", hash = "sha256:8fd5afd101dcf86a270d254364e0e8dddedebe6bd1ab9d5f732f274fa00499a5"}, - {file = "regex-2024.7.24-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:0ffe3f9d430cd37d8fa5632ff6fb36d5b24818c5c986893063b4e5bdb84cdf24"}, - {file = "regex-2024.7.24-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:25419b70ba00a16abc90ee5fce061228206173231f004437730b67ac77323f0d"}, - {file = "regex-2024.7.24-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:33e2614a7ce627f0cdf2ad104797d1f68342d967de3695678c0cb84f530709f8"}, - {file = "regex-2024.7.24-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d33a0021893ede5969876052796165bab6006559ab845fd7b515a30abdd990dc"}, - {file = "regex-2024.7.24-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:04ce29e2c5fedf296b1a1b0acc1724ba93a36fb14031f3abfb7abda2806c1535"}, - {file = "regex-2024.7.24-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b16582783f44fbca6fcf46f61347340c787d7530d88b4d590a397a47583f31dd"}, - {file = "regex-2024.7.24-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:836d3cc225b3e8a943d0b02633fb2f28a66e281290302a79df0e1eaa984ff7c1"}, - {file = "regex-2024.7.24-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:438d9f0f4bc64e8dea78274caa5af971ceff0f8771e1a2333620969936ba10be"}, - {file = "regex-2024.7.24-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:973335b1624859cb0e52f96062a28aa18f3a5fc77a96e4a3d6d76e29811a0e6e"}, - {file = "regex-2024.7.24-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c5e69fd3eb0b409432b537fe3c6f44ac089c458ab6b78dcec14478422879ec5f"}, - {file = "regex-2024.7.24-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:fbf8c2f00904eaf63ff37718eb13acf8e178cb940520e47b2f05027f5bb34ce3"}, - {file = "regex-2024.7.24-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ae2757ace61bc4061b69af19e4689fa4416e1a04840f33b441034202b5cd02d4"}, - {file = "regex-2024.7.24-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:44fc61b99035fd9b3b9453f1713234e5a7c92a04f3577252b45feefe1b327759"}, - {file = "regex-2024.7.24-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:84c312cdf839e8b579f504afcd7b65f35d60b6285d892b19adea16355e8343c9"}, - {file = "regex-2024.7.24-cp39-cp39-win32.whl", hash = "sha256:ca5b2028c2f7af4e13fb9fc29b28d0ce767c38c7facdf64f6c2cd040413055f1"}, - {file = "regex-2024.7.24-cp39-cp39-win_amd64.whl", hash = "sha256:7c479f5ae937ec9985ecaf42e2e10631551d909f203e31308c12d703922742f9"}, - {file = "regex-2024.7.24.tar.gz", hash = "sha256:9cfd009eed1a46b27c14039ad5bbc5e71b6367c5b2e6d5f5da0ea91600817506"}, + {file = "regex-2024.9.11-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1494fa8725c285a81d01dc8c06b55287a1ee5e0e382d8413adc0a9197aac6408"}, + {file = "regex-2024.9.11-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0e12c481ad92d129c78f13a2a3662317e46ee7ef96c94fd332e1c29131875b7d"}, + {file = "regex-2024.9.11-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:16e13a7929791ac1216afde26f712802e3df7bf0360b32e4914dca3ab8baeea5"}, + {file = "regex-2024.9.11-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46989629904bad940bbec2106528140a218b4a36bb3042d8406980be1941429c"}, + {file = "regex-2024.9.11-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a906ed5e47a0ce5f04b2c981af1c9acf9e8696066900bf03b9d7879a6f679fc8"}, + {file = "regex-2024.9.11-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e9a091b0550b3b0207784a7d6d0f1a00d1d1c8a11699c1a4d93db3fbefc3ad35"}, + {file = "regex-2024.9.11-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ddcd9a179c0a6fa8add279a4444015acddcd7f232a49071ae57fa6e278f1f71"}, + {file = "regex-2024.9.11-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6b41e1adc61fa347662b09398e31ad446afadff932a24807d3ceb955ed865cc8"}, + {file = "regex-2024.9.11-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ced479f601cd2f8ca1fd7b23925a7e0ad512a56d6e9476f79b8f381d9d37090a"}, + {file = "regex-2024.9.11-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:635a1d96665f84b292e401c3d62775851aedc31d4f8784117b3c68c4fcd4118d"}, + {file = "regex-2024.9.11-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c0256beda696edcf7d97ef16b2a33a8e5a875affd6fa6567b54f7c577b30a137"}, + {file = "regex-2024.9.11-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:3ce4f1185db3fbde8ed8aa223fc9620f276c58de8b0d4f8cc86fd1360829edb6"}, + {file = "regex-2024.9.11-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:09d77559e80dcc9d24570da3745ab859a9cf91953062e4ab126ba9d5993688ca"}, + {file = "regex-2024.9.11-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7a22ccefd4db3f12b526eccb129390942fe874a3a9fdbdd24cf55773a1faab1a"}, + {file = "regex-2024.9.11-cp310-cp310-win32.whl", hash = "sha256:f745ec09bc1b0bd15cfc73df6fa4f726dcc26bb16c23a03f9e3367d357eeedd0"}, + {file = "regex-2024.9.11-cp310-cp310-win_amd64.whl", hash = "sha256:01c2acb51f8a7d6494c8c5eafe3d8e06d76563d8a8a4643b37e9b2dd8a2ff623"}, + {file = "regex-2024.9.11-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2cce2449e5927a0bf084d346da6cd5eb016b2beca10d0013ab50e3c226ffc0df"}, + {file = "regex-2024.9.11-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b37fa423beefa44919e009745ccbf353d8c981516e807995b2bd11c2c77d268"}, + {file = "regex-2024.9.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:64ce2799bd75039b480cc0360907c4fb2f50022f030bf9e7a8705b636e408fad"}, + {file = "regex-2024.9.11-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a4cc92bb6db56ab0c1cbd17294e14f5e9224f0cc6521167ef388332604e92679"}, + {file = "regex-2024.9.11-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d05ac6fa06959c4172eccd99a222e1fbf17b5670c4d596cb1e5cde99600674c4"}, + {file = "regex-2024.9.11-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:040562757795eeea356394a7fb13076ad4f99d3c62ab0f8bdfb21f99a1f85664"}, + {file = "regex-2024.9.11-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6113c008a7780792efc80f9dfe10ba0cd043cbf8dc9a76ef757850f51b4edc50"}, + {file = "regex-2024.9.11-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8e5fb5f77c8745a60105403a774fe2c1759b71d3e7b4ca237a5e67ad066c7199"}, + {file = "regex-2024.9.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:54d9ff35d4515debf14bc27f1e3b38bfc453eff3220f5bce159642fa762fe5d4"}, + {file = "regex-2024.9.11-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:df5cbb1fbc74a8305b6065d4ade43b993be03dbe0f8b30032cced0d7740994bd"}, + {file = "regex-2024.9.11-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:7fb89ee5d106e4a7a51bce305ac4efb981536301895f7bdcf93ec92ae0d91c7f"}, + {file = "regex-2024.9.11-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:a738b937d512b30bf75995c0159c0ddf9eec0775c9d72ac0202076c72f24aa96"}, + {file = "regex-2024.9.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e28f9faeb14b6f23ac55bfbbfd3643f5c7c18ede093977f1df249f73fd22c7b1"}, + {file = "regex-2024.9.11-cp311-cp311-win32.whl", hash = "sha256:18e707ce6c92d7282dfce370cd205098384b8ee21544e7cb29b8aab955b66fa9"}, + {file = "regex-2024.9.11-cp311-cp311-win_amd64.whl", hash = "sha256:313ea15e5ff2a8cbbad96ccef6be638393041b0a7863183c2d31e0c6116688cf"}, + {file = "regex-2024.9.11-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b0d0a6c64fcc4ef9c69bd5b3b3626cc3776520a1637d8abaa62b9edc147a58f7"}, + {file = "regex-2024.9.11-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:49b0e06786ea663f933f3710a51e9385ce0cba0ea56b67107fd841a55d56a231"}, + {file = "regex-2024.9.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5b513b6997a0b2f10e4fd3a1313568e373926e8c252bd76c960f96fd039cd28d"}, + {file = "regex-2024.9.11-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee439691d8c23e76f9802c42a95cfeebf9d47cf4ffd06f18489122dbb0a7ad64"}, + {file = "regex-2024.9.11-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a8f877c89719d759e52783f7fe6e1c67121076b87b40542966c02de5503ace42"}, + {file = "regex-2024.9.11-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:23b30c62d0f16827f2ae9f2bb87619bc4fba2044911e2e6c2eb1af0161cdb766"}, + {file = "regex-2024.9.11-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85ab7824093d8f10d44330fe1e6493f756f252d145323dd17ab6b48733ff6c0a"}, + {file = "regex-2024.9.11-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8dee5b4810a89447151999428fe096977346cf2f29f4d5e29609d2e19e0199c9"}, + {file = "regex-2024.9.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:98eeee2f2e63edae2181c886d7911ce502e1292794f4c5ee71e60e23e8d26b5d"}, + {file = "regex-2024.9.11-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:57fdd2e0b2694ce6fc2e5ccf189789c3e2962916fb38779d3e3521ff8fe7a822"}, + {file = "regex-2024.9.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:d552c78411f60b1fdaafd117a1fca2f02e562e309223b9d44b7de8be451ec5e0"}, + {file = "regex-2024.9.11-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:a0b2b80321c2ed3fcf0385ec9e51a12253c50f146fddb2abbb10f033fe3d049a"}, + {file = "regex-2024.9.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:18406efb2f5a0e57e3a5881cd9354c1512d3bb4f5c45d96d110a66114d84d23a"}, + {file = "regex-2024.9.11-cp312-cp312-win32.whl", hash = "sha256:e464b467f1588e2c42d26814231edecbcfe77f5ac414d92cbf4e7b55b2c2a776"}, + {file = "regex-2024.9.11-cp312-cp312-win_amd64.whl", hash = "sha256:9e8719792ca63c6b8340380352c24dcb8cd7ec49dae36e963742a275dfae6009"}, + {file = "regex-2024.9.11-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c157bb447303070f256e084668b702073db99bbb61d44f85d811025fcf38f784"}, + {file = "regex-2024.9.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4db21ece84dfeefc5d8a3863f101995de646c6cb0536952c321a2650aa202c36"}, + {file = "regex-2024.9.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:220e92a30b426daf23bb67a7962900ed4613589bab80382be09b48896d211e92"}, + {file = "regex-2024.9.11-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eb1ae19e64c14c7ec1995f40bd932448713d3c73509e82d8cd7744dc00e29e86"}, + {file = "regex-2024.9.11-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f47cd43a5bfa48f86925fe26fbdd0a488ff15b62468abb5d2a1e092a4fb10e85"}, + {file = "regex-2024.9.11-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9d4a76b96f398697fe01117093613166e6aa8195d63f1b4ec3f21ab637632963"}, + {file = "regex-2024.9.11-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ea51dcc0835eea2ea31d66456210a4e01a076d820e9039b04ae8d17ac11dee6"}, + {file = "regex-2024.9.11-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7aaa315101c6567a9a45d2839322c51c8d6e81f67683d529512f5bcfb99c802"}, + {file = "regex-2024.9.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c57d08ad67aba97af57a7263c2d9006d5c404d721c5f7542f077f109ec2a4a29"}, + {file = "regex-2024.9.11-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f8404bf61298bb6f8224bb9176c1424548ee1181130818fcd2cbffddc768bed8"}, + {file = "regex-2024.9.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:dd4490a33eb909ef5078ab20f5f000087afa2a4daa27b4c072ccb3cb3050ad84"}, + {file = "regex-2024.9.11-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:eee9130eaad130649fd73e5cd92f60e55708952260ede70da64de420cdcad554"}, + {file = "regex-2024.9.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6a2644a93da36c784e546de579ec1806bfd2763ef47babc1b03d765fe560c9f8"}, + {file = "regex-2024.9.11-cp313-cp313-win32.whl", hash = "sha256:e997fd30430c57138adc06bba4c7c2968fb13d101e57dd5bb9355bf8ce3fa7e8"}, + {file = "regex-2024.9.11-cp313-cp313-win_amd64.whl", hash = "sha256:042c55879cfeb21a8adacc84ea347721d3d83a159da6acdf1116859e2427c43f"}, + {file = "regex-2024.9.11-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:35f4a6f96aa6cb3f2f7247027b07b15a374f0d5b912c0001418d1d55024d5cb4"}, + {file = "regex-2024.9.11-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:55b96e7ce3a69a8449a66984c268062fbaa0d8ae437b285428e12797baefce7e"}, + {file = "regex-2024.9.11-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cb130fccd1a37ed894824b8c046321540263013da72745d755f2d35114b81a60"}, + {file = "regex-2024.9.11-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:323c1f04be6b2968944d730e5c2091c8c89767903ecaa135203eec4565ed2b2b"}, + {file = "regex-2024.9.11-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be1c8ed48c4c4065ecb19d882a0ce1afe0745dfad8ce48c49586b90a55f02366"}, + {file = "regex-2024.9.11-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b5b029322e6e7b94fff16cd120ab35a253236a5f99a79fb04fda7ae71ca20ae8"}, + {file = "regex-2024.9.11-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6fff13ef6b5f29221d6904aa816c34701462956aa72a77f1f151a8ec4f56aeb"}, + {file = "regex-2024.9.11-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:587d4af3979376652010e400accc30404e6c16b7df574048ab1f581af82065e4"}, + {file = "regex-2024.9.11-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:079400a8269544b955ffa9e31f186f01d96829110a3bf79dc338e9910f794fca"}, + {file = "regex-2024.9.11-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:f9268774428ec173654985ce55fc6caf4c6d11ade0f6f914d48ef4719eb05ebb"}, + {file = "regex-2024.9.11-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:23f9985c8784e544d53fc2930fc1ac1a7319f5d5332d228437acc9f418f2f168"}, + {file = "regex-2024.9.11-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:ae2941333154baff9838e88aa71c1d84f4438189ecc6021a12c7573728b5838e"}, + {file = "regex-2024.9.11-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:e93f1c331ca8e86fe877a48ad64e77882c0c4da0097f2212873a69bbfea95d0c"}, + {file = "regex-2024.9.11-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:846bc79ee753acf93aef4184c040d709940c9d001029ceb7b7a52747b80ed2dd"}, + {file = "regex-2024.9.11-cp38-cp38-win32.whl", hash = "sha256:c94bb0a9f1db10a1d16c00880bdebd5f9faf267273b8f5bd1878126e0fbde771"}, + {file = "regex-2024.9.11-cp38-cp38-win_amd64.whl", hash = "sha256:2b08fce89fbd45664d3df6ad93e554b6c16933ffa9d55cb7e01182baaf971508"}, + {file = "regex-2024.9.11-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:07f45f287469039ffc2c53caf6803cd506eb5f5f637f1d4acb37a738f71dd066"}, + {file = "regex-2024.9.11-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4838e24ee015101d9f901988001038f7f0d90dc0c3b115541a1365fb439add62"}, + {file = "regex-2024.9.11-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6edd623bae6a737f10ce853ea076f56f507fd7726bee96a41ee3d68d347e4d16"}, + {file = "regex-2024.9.11-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c69ada171c2d0e97a4b5aa78fbb835e0ffbb6b13fc5da968c09811346564f0d3"}, + {file = "regex-2024.9.11-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:02087ea0a03b4af1ed6ebab2c54d7118127fee8d71b26398e8e4b05b78963199"}, + {file = "regex-2024.9.11-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:69dee6a020693d12a3cf892aba4808fe168d2a4cef368eb9bf74f5398bfd4ee8"}, + {file = "regex-2024.9.11-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:297f54910247508e6e5cae669f2bc308985c60540a4edd1c77203ef19bfa63ca"}, + {file = "regex-2024.9.11-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ecea58b43a67b1b79805f1a0255730edaf5191ecef84dbc4cc85eb30bc8b63b9"}, + {file = "regex-2024.9.11-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:eab4bb380f15e189d1313195b062a6aa908f5bd687a0ceccd47c8211e9cf0d4a"}, + {file = "regex-2024.9.11-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0cbff728659ce4bbf4c30b2a1be040faafaa9eca6ecde40aaff86f7889f4ab39"}, + {file = "regex-2024.9.11-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:54c4a097b8bc5bb0dfc83ae498061d53ad7b5762e00f4adaa23bee22b012e6ba"}, + {file = "regex-2024.9.11-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:73d6d2f64f4d894c96626a75578b0bf7d9e56dcda8c3d037a2118fdfe9b1c664"}, + {file = "regex-2024.9.11-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:e53b5fbab5d675aec9f0c501274c467c0f9a5d23696cfc94247e1fb56501ed89"}, + {file = "regex-2024.9.11-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:0ffbcf9221e04502fc35e54d1ce9567541979c3fdfb93d2c554f0ca583a19b35"}, + {file = "regex-2024.9.11-cp39-cp39-win32.whl", hash = "sha256:e4c22e1ac1f1ec1e09f72e6c44d8f2244173db7eb9629cc3a346a8d7ccc31142"}, + {file = "regex-2024.9.11-cp39-cp39-win_amd64.whl", hash = "sha256:faa3c142464efec496967359ca99696c896c591c56c53506bac1ad465f66e919"}, + {file = "regex-2024.9.11.tar.gz", hash = "sha256:6c188c307e8433bcb63dc1915022deb553b4203a70722fc542c363bf120a01fd"}, ] [[package]] @@ -1842,13 +1878,13 @@ files = [ [[package]] name = "urllib3" -version = "2.2.2" +version = "2.2.3" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.8" files = [ - {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"}, - {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"}, + {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"}, + {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"}, ] [package.extras] @@ -1958,13 +1994,13 @@ files = [ [[package]] name = "zipp" -version = "3.20.1" +version = "3.20.2" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false python-versions = ">=3.8" files = [ - {file = "zipp-3.20.1-py3-none-any.whl", hash = "sha256:9960cd8967c8f85a56f920d5d507274e74f9ff813a0ab8889a5b5be2daf44064"}, - {file = "zipp-3.20.1.tar.gz", hash = "sha256:c22b14cc4763c5a5b04134207736c107db42e9d3ef2d9779d465f5f1bcba572b"}, + {file = "zipp-3.20.2-py3-none-any.whl", hash = "sha256:a817ac80d6cf4b23bf7f2828b7cabf326f15a001bea8b1f9b49631780ba28350"}, + {file = "zipp-3.20.2.tar.gz", hash = "sha256:bc9eb26f4506fda01b81bcde0ca78103b6e62f991b381fec825435c836edbc29"}, ] [package.extras] @@ -1978,4 +2014,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "54efde736fa3030803ff10f4d2a257d694f3c9e13e97955a9d1a9a54be68aab5" +content-hash = "a2d927272ddcfecd572c02433e3812a3df0e7c755050df59f72de88e59660121" diff --git a/pyproject.toml b/pyproject.toml index 1f9e17b..d586a6a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,9 +25,10 @@ pre-commit = "^3.7.1" accelerate = "^0.33.0" ruff = "^0.5.5" pillow = "^10.4.0" -image-handler-client = {git = "https://github.com/imaginate-ai/image-handler-client.git", rev = "v1.0.0"} +image-handler-client = {git = "https://github.com/imaginate-ai/image-handler-client.git", rev = "v1.0.2"} ollama = "^0.3.2" httpx = "^0.27.2" +python-dotenv = "^1.0.1" [tool.ruff] exclude = [] @@ -45,9 +46,7 @@ select = [ "PL", "S", ] -unfixable = [ - "F401" -] + [tool.ruff.lint.per-file-ignores] "tests/*" = [ @@ -56,6 +55,9 @@ unfixable = [ "PLR2004", "ARG001", ] +"image_handler/image_handler_instance.py" = [ + "PLW0603", +] [tool.ruff.lint.pylint] max-args = 8 diff --git a/tests/unit/test_image_handler.py b/tests/unit/test_image_handler.py index b9e6105..e218cb8 100644 --- a/tests/unit/test_image_handler.py +++ b/tests/unit/test_image_handler.py @@ -5,6 +5,7 @@ import pytest import requests from image_handler.image_generator import DataType, ImageHandler +from image_handler.util import calculate_images_to_generate from image_handler_client.schemas.image_info import ImageInfo from PIL import Image @@ -313,3 +314,23 @@ def test_enqueue_image_to_image_task_structure(image_handler, mock_info, mock_im assert "kwargs" in task assert "info" in task assert task["type"] == DataType.IMAGE + + +def test_calculate_images(): + possible = [0, 1, 2, 3, 4] + + for num in possible: + initial_real = num + for num_2 in possible: + initial_ai = num_2 + if (initial_ai + initial_real) <= 5: + for _ in range(100): + result = calculate_images_to_generate(initial_real, initial_ai) + + total_real = initial_real + result["real"] + total_ai = initial_ai + result["ai"] + assert total_real > 0 + assert total_ai > 0 + + total = total_real + total_ai + assert total == 5