@@ -343,13 +343,6 @@ YANDEX_CLOUD_ID = env.str('YANDEX_CLOUD_ID', 'defaultapikey') OPENAI_PROXY_HOST = env.str('OPENAI_PROXY_HOST', 'neuron-proxy:8080') UPSCALE_MULTIPLIER_HOST = env.str('UPSCALE_MULTIPLIER_HOST', 'packet:8080') - -MAX_UPLOAD_SIZE_PER_MODEL = { - 'raifgpt': 50, - 'default': 8, -} - - # Payments YOOKASSA_ACCOUNT_ID = env.str('YOOKASSA_ACCOUNT_ID', default='defaultapikey') YOOKASSA_SECRET_KEY = env.str('YOOKASSA_SECRET_KEY', default='defaultapikey') @@ -1,4 +1,10 @@ +from typing import Dict, Any + +from backend import settings + +from django.utils.translation import gettext_lazy as _ from rest_framework import serializers +from rest_framework.serializers import ValidationError from messages.models import Message @@ -31,3 +37,12 @@ class MessageSerializer(serializers.ModelSerializer): 'is_sent', 'info', ] + + def validate(self, data: Dict[str, Any]) -> Dict[str, Any]: + file = data.get('file') + max_mb_size = 52 + if file and file.size > (max_mb_size << 10 << 10): + raise ValidationError( + _('The file size cannot exceed %(max_mb_size)d MB') % {'max_mb_size': max_mb_size} + ) + return data @@ -1,12 +1,13 @@ import base64 import json import logging +import httpx +import filetype + from abc import ABC, abstractmethod from io import BytesIO, StringIO from typing import Any, Iterable, Literal -import filetype -import httpx from django.conf import settings from django.core.exceptions import ValidationError from django.utils.translation import gettext_lazy as _ @@ -38,6 +39,7 @@ class OpenAICompatibleRunner(BaseRunner, ABC): @classmethod def generate(cls, content=None, file=None, parameters={}, history=[], scrape_results=[]): + system_prompt = parameters.pop('system_prompt') payload = { 'messages': [ *[ @@ -52,6 +54,9 @@ class OpenAICompatibleRunner(BaseRunner, ABC): 'stream': True, **parameters, } + + payload['messages'].insert(0, {'role': 'system', 'content': system_prompt}) + for result in scrape_results: if isinstance(result, StringIO): payload['messages'].append( @@ -2,21 +2,23 @@ import base64 import logging import subprocess import time +import fitz +import httpx from datetime import timedelta from decimal import Decimal from io import BytesIO, StringIO from math import ceil -from typing import Any, Callable, Iterable, Literal +from typing import Any, Callable, Literal, Tuple, Union, Optional, Iterable from uuid import UUID import docx2txt import filetype from django.core.cache import cache from django.core.files.base import ContentFile -from django.db.models import Prefetch +from django.conf import settings +from django.db.models import Prefetch, QuerySet from django.utils.translation import gettext_lazy as _ from PIL import Image as ImageModule -from PyPDF2 import PdfReader from authentication.models.user import CustomUserModel from messages.models import Message @@ -43,6 +45,9 @@ logger = logging.getLogger(__name__) class InferenceService: def __init__(self, user: CustomUserModel): self.user = user + self.DATA_SIZE_MULTIPLIERS = { + 'mb': 1024 * 1024, + } @classmethod def get_by_id(cls, id: UUID): @@ -122,9 +127,11 @@ class InferenceService: file.seek(0) elif file_extension in ('pdf',): file = StringIO() - reader = PdfReader(file_buf) - for page in reader.pages: - file.write(page.extract_text()) + pdf_data = self.get_pdf_data(file_buf, raw_file.name) + raw_text = pdf_data[0] + image_count = pdf_data[1] + file.write(raw_text) + file.seek(0) elif file_extension in ('doc', 'docx', 'zip'): extractors: dict[Literal['doc', 'docx'], Callable[[], str]] = { 'doc': lambda: subprocess.Popen( @@ -242,6 +249,13 @@ class InferenceService: sum([len(result.getvalue()) * 3 for result in scrape_results]) * payment_rule.cost ) + if (system_prompt := parameters.get('system_prompt')): + calculated_price += ( + sum([len(prompt) * 3 for prompt in system_prompt]) + * payment_rule.cost + ) + if image_count: + calculated_price += image_count * Decimal('0.13') elif ( payment_rule.strategy == PaymentRule.StrategyChoices.PER_TEXT_TOKEN and payment_rule.interaction_type == PaymentRule.InteractionTypeChoices.OUTPUT @@ -352,3 +366,191 @@ class InferenceService: logger.exception(exc) finally: cache.delete(cache_key) + + def get_pdf_data(self, input_data: Union[Message, BytesIO], filename: Optional[str] = None) -> str: + try: + if isinstance(input_data, BytesIO): + file_stream = input_data + actual_filename = filename or getattr(input_data, 'name', '') + else: + file_stream = input_data.file + actual_filename = filename or getattr(input_data.file, 'name', '') + processor = PDFProcessor() + result, image_count = processor.process( + file_stream=file_stream, + filename=actual_filename, + ) + return result, image_count + except Exception as exc: + logger.exception(exc) + return f"Ошибка обработки PDF: {str(exc)}" + finally: + if isinstance(input_data, BytesIO): + input_data.seek(0) + elif hasattr(input_data, 'file'): + input_data.file.seek(0) + + +class PDFProcessor: + MAX_UPLOAD_SIZE_PER_MODEL: dict = { + 'mb': 1024 * 1024, + } + MAX_BATCH_SIZE = 3.9 * MAX_UPLOAD_SIZE_PER_MODEL['mb'] + + def __init__(self): + self.image_count = 0 + + def process(self, file_stream: BytesIO, filename: str = "") -> Tuple[str, int]: + try: + file_stream.seek(0) + pdf_data = file_stream.read() + doc = fitz.open(stream=pdf_data, filetype="pdf") + has_images = any(page.get_images() for page in doc) + if has_images: + text = self._process_ocr(doc) + return text, self.image_count + raw_text = [] + for page in doc: + content = page.get_text("text") + if content: + raw_text.append(content) + return "\n".join(raw_text), 0 + finally: + doc.close() + fitz.TOOLS.store_shrink(100) + + def _process_ocr(self, doc) -> str: + raw_texts, pages_with_image = self._extract_text_and_images(doc) + ocr_texts = self._process_images_with_yandex_vision(doc, pages_with_image) + final_text = self._combine_texts(raw_texts, ocr_texts) + return final_text + + def _extract_text_and_images(self, doc) -> Tuple[dict, list]: + raw_texts = {} + pages_with_image = [] + for page_num, page in enumerate(doc): + text = page.get_text("text") + if text: + raw_texts[page_num] = text + if page.get_images(): + pages_with_image.append(page_num) + return raw_texts, pages_with_image + + def _process_images_with_yandex_vision(self, doc, page_nums) -> dict: + batch_images, page_index_map = self._prepare_image_batches(doc, page_nums) + self.image_count = len(batch_images) + if not batch_images: + return {} + return self._send_to_yandex_vision(batch_images, page_index_map) + + def _prepare_image_batches(self, doc, page_nums) -> Tuple[list, list]: + batch_images = [] + page_index_map = [] + for page_num in page_nums: + try: + page = doc.load_page(page_num) + pix = page.get_pixmap(dpi=150, alpha=False) + img = ImageModule.frombytes("RGB", [pix.width, pix.height], pix.samples) + buffer = BytesIO() + img.save(buffer, format="JPEG", quality=60, optimize=True) + buffer.seek(0) + if buffer.getbuffer().nbytes < self.__class__.MAX_BATCH_SIZE: + batch_images.append(buffer) + page_index_map.append(page_num) + except Exception as e: + logger.error(f"Error processing page {page_num}: {e}") + continue + return batch_images, page_index_map + + def _send_to_yandex_vision(self, batch_images, page_index_map) -> dict: + headers = { + "Authorization": f"Api-Key {settings.YANDEX_CLOUD_API_KEY}", + "Content-Type": "application/json" + } + ocr_results = {} + batches = self._create_batches(batch_images, page_index_map) + for batch, pages in batches: + body = { + "folderId": settings.YANDEX_CLOUD_ID, + "analyze_specs": [{ + "content": base64.b64encode(buf.getvalue()).decode(), + "features": [{ + "type": "TEXT_DETECTION", + "text_detection_config": {"language_codes": ["*"]} + }] + } for buf in batch] + } + try: + resp = httpx.post( + "https://vision.api.cloud.yandex.net/vision/v1/batchAnalyze", + headers=headers, json=body, timeout=60 + ) + if resp.status_code >= 400: + self.image_count = 0 + return {} + response = resp.json() if resp.status_code == 200 else None + if response: + self._parse_vision_response(response, pages, ocr_results) + except Exception as e: + logger.error(f"Yandex Vision API error: {e}") + return {} + return ocr_results + + def _create_batches(self, batch_images, page_index_map) -> list: + batches = [] + current_batch = [] + current_pages = [] + current_size = 0 + for i, buffer in enumerate(batch_images): + size = buffer.getbuffer().nbytes + if current_size + size > self.__class__.MAX_BATCH_SIZE and current_batch: + batches.append((current_batch, current_pages)) + current_batch, current_pages, current_size = [], [], 0 + current_batch.append(buffer) + current_pages.append(page_index_map[i]) + current_size += size + if current_batch: + batches.append((current_batch, current_pages)) + return batches + + def _make_yandex_vision_request(self, batch, headers): + body = { + "folderId": settings.YANDEX_CLOUD_ID, + "analyze_specs": [{ + "content": base64.b64encode(buf.getvalue()).decode(), + "features": [{ + "type": "TEXT_DETECTION", + "text_detection_config": {"language_codes": ["*"]} + }] + } for buf in batch] + } + try: + resp = httpx.post( + "https://vision.api.cloud.yandex.net/vision/v1/batchAnalyze", + headers=headers, json=body, timeout=60 + ) + return resp.json() if resp.status_code == 200 else None + except Exception as e: + logger.error(f"Yandex Vision API error: {e}") + return None + + def _parse_vision_response(self, response, pages, ocr_results): + for i, spec_result in enumerate(response.get("results", [])): + page_text = [] + for res in spec_result.get("results", []): + for page in res.get("textDetection", {}).get("pages", []): + for block in page.get('blocks', []): + for line in block.get('lines', []): + line_text = " ".join( + word.get('text', '') for word in line.get('words', []) + ) + if line_text: + page_text.append(line_text) + ocr_results[pages[i]] = "\n".join(page_text) + + def _combine_texts(self, raw_texts, ocr_texts) -> str: + all_pages = sorted(set(raw_texts) | set(ocr_texts)) + return "\n\n".join( + f"{raw_texts.get(pn, '')}\n{ocr_texts.get(pn, '')}".strip() + for pn in all_pages + ).strip() or "Не удалось распознать текст" @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand. [[package]] name = "amqp" @@ -46,7 +46,7 @@ typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} [package.extras] doc = ["Sphinx (>=8.2,<9.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"] -test = ["anyio[trio]", "blockbuster (>=1.5.23)", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1) ; python_version >= \"3.10\"", "uvloop (>=0.21) ; platform_python_implementation == \"CPython\" and platform_system != \"Windows\" and python_version < \"3.14\""] +test = ["anyio[trio]", "blockbuster (>=1.5.23)", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21)"] trio = ["trio (>=0.26.1)"] [[package]] @@ -171,12 +171,12 @@ files = [ ] [package.extras] -benchmark = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] -cov = ["cloudpickle ; platform_python_implementation == \"CPython\"", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] -dev = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier"] -tests = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] -tests-mypy = ["mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\""] +tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] [[package]] name = "billiard" @@ -219,33 +219,33 @@ vine = ">=5.1.0,<6.0" arangodb = ["pyArango (>=2.0.2)"] auth = ["cryptography (==44.0.2)"] azureblockblob = ["azure-identity (>=1.19.0)", "azure-storage-blob (>=12.15.0)"] -brotli = ["brotli (>=1.0.0) ; platform_python_implementation == \"CPython\"", "brotlipy (>=0.7.0) ; platform_python_implementation == \"PyPy\""] +brotli = ["brotli (>=1.0.0)", "brotlipy (>=0.7.0)"] cassandra = ["cassandra-driver (>=3.25.0,<4)"] consul = ["python-consul2 (==0.1.5)"] cosmosdbsql = ["pydocumentdb (==2.3.5)"] -couchbase = ["couchbase (>=3.0.0) ; platform_python_implementation != \"PyPy\" and (platform_system != \"Windows\" or python_version < \"3.10\")"] +couchbase = ["couchbase (>=3.0.0)"] couchdb = ["pycouchdb (==1.16.0)"] django = ["Django (>=2.2.28)"] dynamodb = ["boto3 (>=1.26.143)"] elasticsearch = ["elastic-transport (<=8.17.1)", "elasticsearch (<=8.17.2)"] -eventlet = ["eventlet (>=0.32.0) ; python_version < \"3.10\""] +eventlet = ["eventlet (>=0.32.0)"] gcs = ["google-cloud-firestore (==2.20.1)", "google-cloud-storage (>=2.10.0)", "grpcio (==1.67.0)"] gevent = ["gevent (>=1.5.0)"] -librabbitmq = ["librabbitmq (>=2.0.0) ; python_version < \"3.11\""] -memcache = ["pylibmc (==1.6.3) ; platform_system != \"Windows\""] +librabbitmq = ["librabbitmq (>=2.0.0)"] +memcache = ["pylibmc (==1.6.3)"] mongodb = ["kombu[mongodb]"] msgpack = ["kombu[msgpack]"] pydantic = ["pydantic (>=2.4)"] pymemcache = ["python-memcached (>=1.61)"] -pyro = ["pyro4 (==4.82) ; python_version < \"3.11\""] +pyro = ["pyro4 (==4.82)"] pytest = ["pytest-celery[all] (>=1.2.0,<1.3.0)"] redis = ["kombu[redis]"] s3 = ["boto3 (>=1.26.143)"] slmq = ["softlayer_messaging (>=1.0.3)"] -solar = ["ephem (==4.2) ; platform_python_implementation != \"PyPy\""] +solar = ["ephem (==4.2)"] sqlalchemy = ["kombu[sqlalchemy]"] sqs = ["boto3 (>=1.26.143)", "kombu[sqs] (>=5.5.0)", "urllib3 (>=1.26.16)"] -tblib = ["tblib (>=1.3.0) ; python_version < \"3.8.0\"", "tblib (>=1.5.0) ; python_version >= \"3.8.0\""] +tblib = ["tblib (>=1.3.0)", "tblib (>=1.5.0)"] yaml = ["kombu[yaml]"] zookeeper = ["kazoo (>=1.3.1)"] zstd = ["zstandard (==0.23.0)"] @@ -632,10 +632,10 @@ files = [ cffi = {version = ">=1.14", markers = "platform_python_implementation != \"PyPy\""} [package.extras] -docs = ["sphinx (>=5.3.0)", "sphinx-inline-tabs ; python_full_version >= \"3.8.0\"", "sphinx-rtd-theme (>=3.0.0) ; python_full_version >= \"3.8.0\""] +docs = ["sphinx (>=5.3.0)", "sphinx-inline-tabs", "sphinx-rtd-theme (>=3.0.0)"] docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"] -nox = ["nox (>=2024.4.15)", "nox[uv] (>=2024.3.2) ; python_full_version >= \"3.8.0\""] -pep8test = ["check-sdist ; python_full_version >= \"3.8.0\"", "click (>=8.0.1)", "mypy (>=1.4)", "ruff (>=0.3.6)"] +nox = ["nox (>=2024.4.15)", "nox[uv] (>=2024.3.2)"] +pep8test = ["check-sdist", "click (>=8.0.1)", "mypy (>=1.4)", "ruff (>=0.3.6)"] sdist = ["build (>=1.0.0)"] ssh = ["bcrypt (>=3.1.5)"] test = ["certifi (>=2024)", "cryptography-vectors (==45.0.5)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] @@ -669,7 +669,7 @@ files = [ wrapt = ">=1.10,<2" [package.extras] -dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "setuptools ; python_version >= \"3.12\"", "tox"] +dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "setuptools", "tox"] [[package]] name = "diff-match-patch" @@ -1447,7 +1447,7 @@ httpcore = "==1.*" idna = "*" [package.extras] -brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""] +brotli = ["brotli", "brotlicffi"] cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] @@ -1496,12 +1496,12 @@ files = [ zipp = ">=3.20" [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] +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) ; python_version < \"3.9\"", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] +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]] @@ -1594,7 +1594,7 @@ azurestoragequeues = ["azure-identity (>=1.12.0)", "azure-storage-queue (>=12.6. confluentkafka = ["confluent-kafka (>=2.2.0)"] consul = ["python-consul2 (==0.1.5)"] gcpubsub = ["google-cloud-monitoring (>=2.16.0)", "google-cloud-pubsub (>=2.18.4)", "grpcio (==1.67.0)", "protobuf (==4.25.5)"] -librabbitmq = ["librabbitmq (>=2.0.0) ; python_version < \"3.11\""] +librabbitmq = ["librabbitmq (>=2.0.0)"] mongodb = ["pymongo (==4.10.1)"] msgpack = ["msgpack (==1.1.0)"] pyro = ["pyro4 (==4.82)"] @@ -1618,10 +1618,12 @@ files = [ {file = "marshmallow-3.25.1.tar.gz", hash = "sha256:f4debda3bb11153d81ac34b0d582bf23053055ee11e791b54b4b35493468040a"}, ] +[package.dependencies] +packaging = ">=17.0" [package.extras] dev = ["marshmallow[tests]", "pre-commit (>=3.5,<5.0)", "tox"] -docs = ["autodocsumm (==0.2.14)", "furo (==2024.8.6)", "sphinx (==8.2.3)", "sphinx-copybutton (==0.5.2)", "sphinx-issues (==5.0.1)", "sphinxext-opengraph (==0.10.0)"] +docs = ["autodocsumm (==0.2.14)", "furo (==2024.8.6)", "sphinx (==8.1.3)", "sphinx-copybutton (==0.5.2)", "sphinx-issues (==5.0.0)", "sphinxext-opengraph (==0.9.1)"] tests = ["pytest", "simplejson"] [[package]] @@ -2129,7 +2131,7 @@ docs = ["furo", "olefile", "sphinx (>=7.3)", "sphinx-copybutton", "sphinx-inline fpx = ["olefile"] mic = ["olefile"] tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] -typing = ["typing-extensions ; python_version < \"3.10\""] +typing = ["typing-extensions"] xmp = ["defusedxml"] [[package]] @@ -2208,8 +2210,8 @@ typing-extensions = {version = ">=4.6", markers = "python_version < \"3.13\""} tzdata = {version = "*", markers = "sys_platform == \"win32\""} [package.extras] -binary = ["psycopg-binary (==3.2.9) ; implementation_name != \"pypy\""] -c = ["psycopg-c (==3.2.9) ; implementation_name != \"pypy\""] +binary = ["psycopg-binary (==3.2.9)"] +c = ["psycopg-c (==3.2.9)"] dev = ["ast-comments (>=1.1.2)", "black (>=24.1.0)", "codespell (>=2.2)", "dnspython (>=2.1)", "flake8 (>=4.0)", "isort-psycopg", "isort[colors] (>=6.0)", "mypy (>=1.14)", "pre-commit (>=4.0.1)", "types-setuptools (>=57.4)", "types-shapely (>=2.0)", "wheel (>=0.37)"] docs = ["Sphinx (>=5.0)", "furo (==2022.6.21)", "sphinx-autobuild (>=2021.3.14)", "sphinx-autodoc-typehints (>=1.12)"] pool = ["psycopg-pool"] @@ -2386,7 +2388,7 @@ typing-inspection = ">=0.4.0" [package.extras] email = ["email-validator (>=2.0.0)"] -timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""] +timezone = ["tzdata"] [[package]] name = "pydantic-core" @@ -3373,7 +3375,7 @@ files = [ ] [package.extras] -brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] @@ -3395,7 +3397,7 @@ click = ">=7.0" h11 = ">=0.8" [package.extras] -standard = ["colorama (>=0.4) ; sys_platform == \"win32\"", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.15.1) ; sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\"", "watchfiles (>=0.13)", "websockets (>=10.4)"] +standard = ["colorama (>=0.4)", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] [[package]] name = "vine" @@ -3556,7 +3558,7 @@ files = [ ] [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] +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)"]