Compare commits

..

1 Commits

Author SHA1 Message Date
Antoine Mérino
1bee1495cf Performance: Classifier performance optimizations (#10363) 2025-08-06 16:00:11 -04:00
11 changed files with 472 additions and 147 deletions

View File

@@ -15,6 +15,7 @@ env:
DEFAULT_UV_VERSION: "0.8.x"
# This is the default version of Python to use in most steps which aren't specific
DEFAULT_PYTHON_VERSION: "3.11"
NLTK_DATA: "/usr/share/nltk_data"
jobs:
pre-commit:
# We want to run on external PRs, but not on our own internal PRs as they'll be run
@@ -121,8 +122,11 @@ jobs:
- name: List installed Python dependencies
run: |
uv pip list
- name: Install or update NLTK dependencies
run: uv run python -m nltk.downloader punkt punkt_tab snowball_data stopwords -d ${{ env.NLTK_DATA }}
- name: Tests
env:
NLTK_DATA: ${{ env.NLTK_DATA }}
PAPERLESS_CI_TEST: 1
# Enable paperless_mail testing against real server
PAPERLESS_MAIL_TEST_HOST: ${{ secrets.TEST_MAIL_HOST }}

View File

@@ -31,7 +31,7 @@ repos:
rev: v2.4.1
hooks:
- id: codespell
exclude: "(^src-ui/src/locale/)|(^src-ui/pnpm-lock.yaml)|(^src-ui/e2e/)|(^src/paperless_mail/tests/samples/)"
exclude: "(^src-ui/src/locale/)|(^src-ui/pnpm-lock.yaml)|(^src-ui/e2e/)|(^src/paperless_mail/tests/samples/)|(^src/documents/tests/samples/)"
exclude_types:
- pofile
- json

View File

@@ -81,7 +81,7 @@ optional-dependencies.postgres = [
"psycopg-pool==3.2.6",
]
optional-dependencies.webserver = [
"granian[uvloop]~=2.5.0",
"granian[uvloop]~=2.4.1",
]
[dependency-groups]

View File

@@ -1,16 +1,23 @@
from __future__ import annotations
import logging
import pickle
from binascii import hexlify
from collections import OrderedDict
from dataclasses import dataclass
from typing import TYPE_CHECKING
from typing import Any
from typing import Final
from django.conf import settings
from django.core.cache import cache
from django.core.cache import caches
from documents.models import Document
if TYPE_CHECKING:
from django.core.cache.backends.base import BaseCache
from documents.classifier import DocumentClassifier
logger = logging.getLogger("paperless.caching")
@@ -39,6 +46,80 @@ CACHE_1_MINUTE: Final[int] = 60
CACHE_5_MINUTES: Final[int] = 5 * CACHE_1_MINUTE
CACHE_50_MINUTES: Final[int] = 50 * CACHE_1_MINUTE
read_cache = caches["read-cache"]
class LRUCache:
def __init__(self, capacity: int = 128):
self._data = OrderedDict()
self.capacity = capacity
def get(self, key, default=None) -> Any | None:
if key in self._data:
self._data.move_to_end(key)
return self._data[key]
return default
def set(self, key, value) -> None:
self._data[key] = value
self._data.move_to_end(key)
while len(self._data) > self.capacity:
self._data.popitem(last=False)
class StoredLRUCache(LRUCache):
"""
LRU cache that can persist its entire contents as a single entry in a backend cache.
Useful for sharing a cache across multiple workers or processes.
Workflow:
1. Load the cache state from the backend using `load()`.
2. Use `get()` and `set()` locally as usual.
3. Persist changes back to the backend using `save()`.
"""
def __init__(
self,
backend_key: str,
capacity: int = 128,
backend: BaseCache = read_cache,
backend_ttl=settings.CACHALOT_TIMEOUT,
):
if backend_key is None:
raise ValueError("backend_key is mandatory")
super().__init__(capacity)
self._backend_key = backend_key
self._backend = backend
self.backend_ttl = backend_ttl
def load(self) -> None:
"""
Load the whole cache content from backend storage.
If no valid cached data exists in the backend, the local cache is cleared.
"""
serialized_data = self._backend.get(self._backend_key)
try:
self._data = (
pickle.loads(serialized_data) if serialized_data else OrderedDict()
)
except pickle.PickleError:
logger.warning(
"Cache exists in backend but could not be read (possibly invalid format)",
)
def save(self) -> None:
"""Save the entire local cache to the backend as a serialized object.
The backend entry will expire after the configured TTL.
"""
self._backend.set(
self._backend_key,
pickle.dumps(self._data),
self.backend_ttl,
)
def get_suggestion_cache_key(document_id: int) -> str:
"""

View File

@@ -16,16 +16,29 @@ if TYPE_CHECKING:
from django.conf import settings
from django.core.cache import cache
from django.core.cache import caches
from documents.caching import CACHE_5_MINUTES
from documents.caching import CACHE_50_MINUTES
from documents.caching import CLASSIFIER_HASH_KEY
from documents.caching import CLASSIFIER_MODIFIED_KEY
from documents.caching import CLASSIFIER_VERSION_KEY
from documents.caching import StoredLRUCache
from documents.models import Document
from documents.models import MatchingModel
logger = logging.getLogger("paperless.classifier")
ADVANCED_TEXT_PROCESSING_ENABLED = (
settings.NLTK_LANGUAGE is not None and settings.NLTK_ENABLED
)
read_cache = caches["read-cache"]
RE_DIGIT = re.compile(r"\d")
RE_WORD = re.compile(r"\b[\w]+\b") # words that may contain digits
class IncompatibleClassifierVersionError(Exception):
def __init__(self, message: str, *args: object) -> None:
@@ -92,15 +105,28 @@ class DocumentClassifier:
self.last_auto_type_hash: bytes | None = None
self.data_vectorizer = None
self.data_vectorizer_hash = None
self.tags_binarizer = None
self.tags_classifier = None
self.correspondent_classifier = None
self.document_type_classifier = None
self.storage_path_classifier = None
self._stemmer = None
# 10,000 elements roughly use 200 to 500 KB per worker,
# and also in the shared Redis cache,
# Keep this cache small to minimize lookup and I/O latency.
if ADVANCED_TEXT_PROCESSING_ENABLED:
self._stem_cache = StoredLRUCache(
f"stem_cache_v{self.FORMAT_VERSION}",
capacity=10000,
)
self._stop_words = None
def _update_data_vectorizer_hash(self):
self.data_vectorizer_hash = sha256(
pickle.dumps(self.data_vectorizer),
).hexdigest()
def load(self) -> None:
from sklearn.exceptions import InconsistentVersionWarning
@@ -119,6 +145,7 @@ class DocumentClassifier:
self.last_auto_type_hash = pickle.load(f)
self.data_vectorizer = pickle.load(f)
self._update_data_vectorizer_hash()
self.tags_binarizer = pickle.load(f)
self.tags_classifier = pickle.load(f)
@@ -269,7 +296,7 @@ class DocumentClassifier:
Generates the content for documents, but once at a time
"""
for doc in docs_queryset:
yield self.preprocess_content(doc.content)
yield self.preprocess_content(doc.content, shared_cache=False)
self.data_vectorizer = CountVectorizer(
analyzer="word",
@@ -347,6 +374,7 @@ class DocumentClassifier:
self.last_doc_change_time = latest_doc_change
self.last_auto_type_hash = hasher.digest()
self._update_data_vectorizer_hash()
# Set the classifier information into the cache
# Caching for 50 minutes, so slightly less than the normal retrain time
@@ -356,30 +384,15 @@ class DocumentClassifier:
return True
def preprocess_content(self, content: str) -> str: # pragma: no cover
"""
Process to contents of a document, distilling it down into
words which are meaningful to the content
"""
# Lower case the document
content = content.lower().strip()
# Reduce spaces
content = re.sub(r"\s+", " ", content)
# Get only the letters
content = re.sub(r"[^\w\s]", " ", content)
# If the NLTK language is supported, do further processing
if settings.NLTK_LANGUAGE is not None and settings.NLTK_ENABLED:
def _init_advanced_text_processing(self):
if self._stop_words is None or self._stemmer is None:
import nltk
from nltk.corpus import stopwords
from nltk.stem import SnowballStemmer
from nltk.tokenize import word_tokenize
# Not really hacky, since it isn't private and is documented, but
# set the search path for NLTK data to the single location it should be in
nltk.data.path = [settings.NLTK_DIR]
try:
# Preload the corpus early, to force the lazy loader to transform
stopwords.ensure_loaded()
@@ -387,41 +400,100 @@ class DocumentClassifier:
# Do some one time setup
# Sometimes, somehow, there's multiple threads loading the corpus
# and it's not thread safe, raising an AttributeError
if self._stemmer is None:
self._stemmer = SnowballStemmer(settings.NLTK_LANGUAGE)
if self._stop_words is None:
self._stop_words = set(stopwords.words(settings.NLTK_LANGUAGE))
# Tokenize
# This splits the content into tokens, roughly words
words: list[str] = word_tokenize(
content,
language=settings.NLTK_LANGUAGE,
)
meaningful_words = []
for word in words:
# Skip stop words
# These are words like "a", "and", "the" which add little meaning
if word in self._stop_words:
continue
# Stem the words
# This reduces the words to their stems.
# "amazement" returns "amaz"
# "amaze" returns "amaz
# "amazed" returns "amaz"
meaningful_words.append(self._stemmer.stem(word))
return " ".join(meaningful_words)
self._stemmer = SnowballStemmer(settings.NLTK_LANGUAGE)
self._stop_words = frozenset(stopwords.words(settings.NLTK_LANGUAGE))
except AttributeError:
logger.debug("Could not initialize NLTK for advanced text processing.")
return False
return True
def stem_and_skip_stop_words(self, words: list[str], *, shared_cache=True):
"""
Reduce a list of words to their stem. Stop words are converted to empty strings.
:param words: the list of words to stem
"""
def _stem_and_skip_stop_word(word: str):
"""
Reduce a given word to its stem. If it's a stop word, return an empty string.
E.g. "amazement", "amaze" and "amazed" all return "amaz".
"""
cached = self._stem_cache.get(word)
if cached is not None:
return cached
elif word in self._stop_words:
return ""
# Assumption: words that contain numbers are never stemmed
elif RE_DIGIT.search(word):
return word
else:
result = self._stemmer.stem(word)
self._stem_cache.set(word, result)
return result
if shared_cache:
self._stem_cache.load()
# Stem the words and skip stop words
result = " ".join(
filter(None, (_stem_and_skip_stop_word(w) for w in words)),
)
if shared_cache:
self._stem_cache.save()
return result
def preprocess_content(
self,
content: str,
*,
shared_cache=True,
) -> str:
"""
Process the contents of a document, distilling it down into
words which are meaningful to the content.
A stemmer cache is shared across workers with the parameter "shared_cache".
This is unnecessary when training the classifier.
"""
# Lower case the document, reduce space,
# and keep only letters and digits.
content = " ".join(match.group().lower() for match in RE_WORD.finditer(content))
if ADVANCED_TEXT_PROCESSING_ENABLED:
from nltk.tokenize import word_tokenize
if not self._init_advanced_text_processing():
return content
# Tokenize
# This splits the content into tokens, roughly words
words = word_tokenize(content, language=settings.NLTK_LANGUAGE)
# Stem the words and skip stop words
content = self.stem_and_skip_stop_words(words, shared_cache=shared_cache)
return content
def _get_vectorizer_cache_key(self, content: str):
hash = sha256(content.encode())
hash.update(
f"|{self.FORMAT_VERSION}|{settings.NLTK_LANGUAGE}|{settings.NLTK_ENABLED}|{self.data_vectorizer_hash}".encode(),
)
return f"vectorized_content_{hash.hexdigest()}"
def _vectorize(self, content: str):
key = self._get_vectorizer_cache_key(content)
serialized_result = read_cache.get(key)
if serialized_result is None:
result = self.data_vectorizer.transform([self.preprocess_content(content)])
read_cache.set(key, pickle.dumps(result), CACHE_5_MINUTES)
else:
read_cache.touch(key, CACHE_5_MINUTES)
result = pickle.loads(serialized_result)
return result
def predict_correspondent(self, content: str) -> int | None:
if self.correspondent_classifier:
X = self.data_vectorizer.transform([self.preprocess_content(content)])
X = self._vectorize(content)
correspondent_id = self.correspondent_classifier.predict(X)
if correspondent_id != -1:
return correspondent_id
@@ -432,7 +504,7 @@ class DocumentClassifier:
def predict_document_type(self, content: str) -> int | None:
if self.document_type_classifier:
X = self.data_vectorizer.transform([self.preprocess_content(content)])
X = self._vectorize(content)
document_type_id = self.document_type_classifier.predict(X)
if document_type_id != -1:
return document_type_id
@@ -445,7 +517,7 @@ class DocumentClassifier:
from sklearn.utils.multiclass import type_of_target
if self.tags_classifier:
X = self.data_vectorizer.transform([self.preprocess_content(content)])
X = self._vectorize(content)
y = self.tags_classifier.predict(X)
tags_ids = self.tags_binarizer.inverse_transform(y)[0]
if type_of_target(y).startswith("multilabel"):
@@ -464,7 +536,7 @@ class DocumentClassifier:
def predict_storage_path(self, content: str) -> int | None:
if self.storage_path_classifier:
X = self.data_vectorizer.transform([self.preprocess_content(content)])
X = self._vectorize(content)
storage_path_id = self.storage_path_classifier.predict(X)
if storage_path_id != -1:
return storage_path_id

View File

@@ -0,0 +1,34 @@
Sample textual document content.
Include as many characters as possible, to check the classifier's vectorization.
Hey 00, this is "a" test0707 content.
This is an example document — created on 2025-06-25.
Digits: 0123456789
Punctuation: . , ; : ! ? ' " ( ) [ ] { } —
English text: The quick brown fox jumps over the lazy dog.
English stop words: Weve been doing it before.
Accented Latin (diacritics): àâäæçéèêëîïôœùûüÿñ
Arabic: لقد قام المترجم بعمل جيد
Greek: Αλφα, Βήτα, Γάμμα, Δέλτα, Ωμέγα
Cyrillic: Привет, как дела? Добро пожаловать!
Chinese (Simplified): 你好,世界!今天的天气很好。
Chinese (Traditional): 歡迎來到世界,今天天氣很好。
Japanese (Kanji, Hiragana, Katakana): 東京へ行きます。カタカナ、ひらがな、漢字。
Korean (Hangul): 안녕하세요. 오늘 날씨 어때요?
Arabic: مرحبًا، كيف حالك؟
Hebrew: שלום, מה שלומך?
Emoji: 😀 🐍 📘 ✅ ©️ 🇺🇳
Symbols: © ® ™ § ¶ † ‡ ∞ µ ∑ ∆ √
Math: ∫₀^∞ x² dx = ∞, π ≈ 3.14159, ∇·E = ρ/ε₀
Currency: 1$ € ¥ £ ₹
Date formats: 25/06/2025, June 25, 2025, 2025年6月25日
Quote in French: « Bonjour, ça va ? »
Quote in German: „Guten Tag! Wie geht's?“
Newline test:
\r\n
\r
Tab\ttest\tspacing
/ = +) ( []) ~ * #192 +33601010101 § ¤
End of document.

View File

@@ -0,0 +1 @@
sample textual document content include as many characters as possible to check the classifier s vectorization hey 00 this is a test0707 content this is an example document created on 2025 06 25 digits 0123456789 punctuation english text the quick brown fox jumps over the lazy dog english stop words we ve been doing it before accented latin diacritics àâäæçéèêëîïôœùûüÿñ arabic لقد قام المترجم بعمل جيد greek αλφα βήτα γάμμα δέλτα ωμέγα cyrillic привет как дела добро пожаловать chinese simplified 你好 世界 今天的天气很好 chinese traditional 歡迎來到世界 今天天氣很好 japanese kanji hiragana katakana 東京へ行きます カタカナ ひらがな 漢字 korean hangul 안녕하세요 오늘 날씨 어때요 arabic مرحب ا كيف حالك hebrew שלום מה שלומך emoji symbols µ math ₀ x² dx π 3 14159 e ρ ε₀ currency 1 date formats 25 06 2025 june 25 2025 2025年6月25日 quote in french bonjour ça va quote in german guten tag wie geht s newline test r n r tab ttest tspacing 192 33601010101 end of document

View File

@@ -0,0 +1 @@
sampl textual document content includ mani charact possibl check classifi vector hey 00 test0707 content exampl document creat 2025 06 25 digit 0123456789 punctuat english text quick brown fox jump lazi dog english stop word accent latin diacrit àâäæçéèêëîïôœùûüÿñ arab لقد قام المترجم بعمل جيد greek αλφα βήτα γάμμα δέλτα ωμέγα cyril привет как дела добро пожаловать chines simplifi 你好 世界 今天的天气很好 chines tradit 歡迎來到世界 今天天氣很好 japanes kanji hiragana katakana 東京へ行きます カタカナ ひらがな 漢字 korean hangul 안녕하세요 오늘 날씨 어때요 arab مرحب ا كيف حالك hebrew שלום מה שלומך emoji symbol µ math ₀ x² dx π 3 14159 e ρ ε₀ currenc 1 date format 25 06 2025 june 25 2025 2025年6月25日 quot french bonjour ça va quot german guten tag wie geht newlin test r n r tab ttest tspace 192 33601010101 end document

View File

@@ -0,0 +1,45 @@
import pickle
from documents.caching import StoredLRUCache
def test_lru_cache_entries():
CACHE_TTL = 1
# LRU cache with a capacity of 2 elements
cache = StoredLRUCache("test_lru_cache_key", 2, backend_ttl=CACHE_TTL)
cache.set(1, 1)
cache.set(2, 2)
assert cache.get(2) == 2
assert cache.get(1) == 1
# The oldest entry (2) should be removed
cache.set(3, 3)
assert cache.get(3) == 3
assert not cache.get(2)
assert cache.get(1) == 1
# Save the cache, restore it and check it overwrites the current cache in memory
cache.save()
cache.set(4, 4)
assert not cache.get(3)
cache.load()
assert not cache.get(4)
assert cache.get(3) == 3
assert cache.get(1) == 1
def test_stored_lru_cache_key_ttl(mocker):
mock_backend = mocker.Mock()
cache = StoredLRUCache("test_key", backend=mock_backend, backend_ttl=321)
# Simulate storing values
cache.set("x", "X")
cache.set("y", "Y")
cache.save()
# Assert backend.set was called with pickled data, key and TTL
mock_backend.set.assert_called_once()
key, data, timeout = mock_backend.set.call_args[0]
assert key == "test_key"
assert timeout == 321
assert pickle.loads(data) == {"x": "X", "y": "Y"}

View File

@@ -21,7 +21,7 @@ from documents.models import Tag
from documents.tests.utils import DirectoriesMixin
def dummy_preprocess(content: str):
def dummy_preprocess(content: str, **kwargs):
"""
Simpler, faster pre-processing for testing purposes
"""
@@ -223,24 +223,47 @@ class TestClassifier(DirectoriesMixin, TestCase):
self.generate_test_data()
self.classifier.train()
self.assertEqual(
self.classifier.predict_correspondent(self.doc1.content),
self.c1.pk,
)
self.assertEqual(self.classifier.predict_correspondent(self.doc2.content), None)
self.assertListEqual(
self.classifier.predict_tags(self.doc1.content),
[self.t1.pk],
)
self.assertListEqual(
self.classifier.predict_tags(self.doc2.content),
[self.t1.pk, self.t3.pk],
)
self.assertEqual(
self.classifier.predict_document_type(self.doc1.content),
self.dt.pk,
)
self.assertEqual(self.classifier.predict_document_type(self.doc2.content), None)
with (
mock.patch.object(
self.classifier.data_vectorizer,
"transform",
wraps=self.classifier.data_vectorizer.transform,
) as mock_transform,
mock.patch.object(
self.classifier,
"preprocess_content",
wraps=self.classifier.preprocess_content,
) as mock_preprocess_content,
):
self.assertEqual(
self.classifier.predict_correspondent(self.doc1.content),
self.c1.pk,
)
self.assertEqual(
self.classifier.predict_correspondent(self.doc2.content),
None,
)
self.assertListEqual(
self.classifier.predict_tags(self.doc1.content),
[self.t1.pk],
)
self.assertListEqual(
self.classifier.predict_tags(self.doc2.content),
[self.t1.pk, self.t3.pk],
)
self.assertEqual(
self.classifier.predict_document_type(self.doc1.content),
self.dt.pk,
)
self.assertEqual(
self.classifier.predict_document_type(self.doc2.content),
None,
)
# Check that the classifier vectorized content and text preprocessing has been cached
# It should be called once per document (doc1 and doc2)
self.assertEqual(mock_preprocess_content.call_count, 2)
self.assertEqual(mock_transform.call_count, 2)
def test_no_retrain_if_no_change(self):
"""
@@ -694,3 +717,67 @@ class TestClassifier(DirectoriesMixin, TestCase):
mock_load.side_effect = Exception()
with self.assertRaises(Exception):
load_classifier(raise_exception=True)
def test_preprocess_content():
"""
GIVEN:
- Advanced text processing is enabled (default)
WHEN:
- Classifier preprocesses a document's content
THEN:
- Processed content matches the expected output (stemmed words)
"""
with (Path(__file__).parent / "samples" / "content.txt").open("r") as f:
content = f.read()
with (Path(__file__).parent / "samples" / "preprocessed_content_advanced.txt").open(
"r",
) as f:
expected_preprocess_content = f.read().rstrip()
classifier = DocumentClassifier()
result = classifier.preprocess_content(content)
assert result == expected_preprocess_content
def test_preprocess_content_nltk_disabled():
"""
GIVEN:
- Advanced text processing is disabled
WHEN:
- Classifier preprocesses a document's content
THEN:
- Processed content matches the expected output (unstemmed words)
"""
with (Path(__file__).parent / "samples" / "content.txt").open("r") as f:
content = f.read()
with (Path(__file__).parent / "samples" / "preprocessed_content.txt").open(
"r",
) as f:
expected_preprocess_content = f.read().rstrip()
classifier = DocumentClassifier()
with mock.patch("documents.classifier.ADVANCED_TEXT_PROCESSING_ENABLED", new=False):
result = classifier.preprocess_content(content)
assert result == expected_preprocess_content
def test_preprocess_content_nltk_load_fail(mocker):
"""
GIVEN:
- NLTK stop words fail to load
WHEN:
- Classifier preprocesses a document's content
THEN:
- Processed content matches the expected output (unstemmed words)
"""
_module = mocker.MagicMock(name="nltk_corpus_mock")
_module.stopwords.words.side_effect = AttributeError()
mocker.patch.dict("sys.modules", {"nltk.corpus": _module})
classifier = DocumentClassifier()
with (Path(__file__).parent / "samples" / "content.txt").open("r") as f:
content = f.read()
with (Path(__file__).parent / "samples" / "preprocessed_content.txt").open(
"r",
) as f:
expected_preprocess_content = f.read().rstrip()
result = classifier.preprocess_content(content)
assert result == expected_preprocess_content

152
uv.lock generated
View File

@@ -1024,86 +1024,86 @@ wheels = [
[[package]]
name = "granian"
version = "2.5.0"
version = "2.4.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e7/91/6b51c5749a58e5d86063b193c15914700464f0d64eda84178bf432dbbcf9/granian-2.5.0.tar.gz", hash = "sha256:bed0d047c9c0c6c6a5a85ee5b3c7e2683fc63e03ac032eaf3d7654fa96bde102", size = 110336, upload-time = "2025-07-30T18:55:15.161Z" }
sdist = { url = "https://files.pythonhosted.org/packages/b6/95/33666bbf579b36562cdfb66293d0b349e9d28a41a5e473ab61ea565e0859/granian-2.4.1.tar.gz", hash = "sha256:31dd5b28373e330506ae3dd4742880317263a54460046e5303585305ed06a793", size = 105802, upload-time = "2025-07-01T21:49:56.81Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/95/31/2d837432708230fc281fce10d5db2eb1755005c9d8f4fdac0707729d442d/granian-2.5.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:49c1c5059b39db0863e3e967ba18f1ab55fa0f86388537d937aa18baf2934f71", size = 3044791, upload-time = "2025-07-30T18:52:31.448Z" },
{ url = "https://files.pythonhosted.org/packages/ba/c8/0b48fd67dfe88ce7a21de0fe7030789f71c26223f5523bd5e951dd98afde/granian-2.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:aba86bae39b4e8f770d7e6664490e0abdcdcd742300d16cf1cb370dc159a09c1", size = 2602731, upload-time = "2025-07-30T18:52:34.641Z" },
{ url = "https://files.pythonhosted.org/packages/86/21/f40d46af2ce98b7b883d5d34730103afbd5a7bb8fd1af8989cb617275be5/granian-2.5.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0ff75ceeae4c9015c1c5fc5d699bb75b7b10d05f5d69aa091ced11be1e145e43", size = 3347314, upload-time = "2025-07-30T18:52:36.389Z" },
{ url = "https://files.pythonhosted.org/packages/8b/0d/2d0ab45ba4bd68e70129ebe6d14ab8a41263e66a4b3b25b80c8d60ccf704/granian-2.5.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a5f8b1a0b82fa3af2a94572664a3382e3d786981a9cda3bf8b118a0f10338f89", size = 2949913, upload-time = "2025-07-30T18:52:38.036Z" },
{ url = "https://files.pythonhosted.org/packages/8a/a4/c11b542bae8d7d02e1f5f07a0925b4bb1ad084fa1438e9ec2f89369154fd/granian-2.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c5d01fb578ab52b95c8cc39fe09a01ef5682011f4eea67d0e5e33ab941c9ef38", size = 3233880, upload-time = "2025-07-30T18:52:39.447Z" },
{ url = "https://files.pythonhosted.org/packages/21/ca/c67684afa272dbde2898b6eed6b51679c16b40e0f2562ef36030fc63fb45/granian-2.5.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:92271019ee283cd74d137e6d8e4fda836a80846a7a006d986246d3e300d8229a", size = 3108699, upload-time = "2025-07-30T18:52:40.744Z" },
{ url = "https://files.pythonhosted.org/packages/5f/e2/13caaaf3a5f8eea1c265567486927035244a8ae8b5f357d697d8c09a7474/granian-2.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e08ce1768433327720a754a78a3134100b989663c6e924626ba8ce8cdb3ee0a4", size = 3114257, upload-time = "2025-07-30T18:52:42.743Z" },
{ url = "https://files.pythonhosted.org/packages/8f/71/aa85e2ab2d183efcdbc199107d7683629ff5aeabad409cccecc8197c73d7/granian-2.5.0-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d09f33aa20fa0f7f9e48f12e87573b3819b357235cac602ec6cef1b5b3ca33a9", size = 3495152, upload-time = "2025-07-30T18:52:44.451Z" },
{ url = "https://files.pythonhosted.org/packages/a3/dd/5e3bab1c332ca2194ef3c58075b1533d944e2cf66b6a242c60c81957e962/granian-2.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5aec8050fc01706d27d74fffc32c29eb483530ea3ad383e885e8b7315ae1c699", size = 3273112, upload-time = "2025-07-30T18:52:46.387Z" },
{ url = "https://files.pythonhosted.org/packages/e3/f3/7d9cee103d91f4d1b934ebaa0cea944638a7b4940c5af72163e486cd4989/granian-2.5.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0eda4c389c222aa5455b7205640df0207201a86c46e5be98dd0040b6cc45146a", size = 3044837, upload-time = "2025-07-30T18:52:49.893Z" },
{ url = "https://files.pythonhosted.org/packages/ef/b8/fcb93a7bddcedc0af11a446094b33dc99af93a338abd8e95747aae3d1112/granian-2.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:58aea28ebb2cdf7545ee3cb1c8593c8b2f857a9fa6219589ecd3f5a4b365262f", size = 2602770, upload-time = "2025-07-30T18:52:51.588Z" },
{ url = "https://files.pythonhosted.org/packages/2a/d9/5f94af3cbdbe023774d24616648e428cfd307f18232a64d82caa2ad8113a/granian-2.5.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cf9dc480d481ae834a085f1f46213ebf80512b1ace0559f6c0335edb24be0e92", size = 3347657, upload-time = "2025-07-30T18:52:53.694Z" },
{ url = "https://files.pythonhosted.org/packages/82/12/ba9be1b7a9ad28b66735a648a996578b64873c580a3a0681575e60cfa0f8/granian-2.5.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:604c544273b36091b54fdf66d4e9a0f98dc0369b380ba5dd328478ba65cda320", size = 2949970, upload-time = "2025-07-30T18:52:55.359Z" },
{ url = "https://files.pythonhosted.org/packages/07/54/f29100152e7dd6f5dfdff2626b040711735aff2ec9f61cba8e7d04614a5e/granian-2.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f6d70f6e7edd183afb62468a7fc175348145aec303297a41b1714a9b6d8150d", size = 3233777, upload-time = "2025-07-30T18:52:57.117Z" },
{ url = "https://files.pythonhosted.org/packages/04/e0/988993586ba3d5e80cc87fc464601df55634ec440eb10889c8fdd3b613ac/granian-2.5.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:651cbad3a137b762885b7e57dea77b5d11262b0a2c16d4b61b4812bc8bc5ffa4", size = 3108386, upload-time = "2025-07-30T18:52:58.644Z" },
{ url = "https://files.pythonhosted.org/packages/6a/06/815fde5195f40a2a1be4e78ad0c3cdd05727dcca59a5a231d0df95fb6f68/granian-2.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:22bac6aace54e4183831a46a4033c076100150527676e666eeb84df9829e0e1c", size = 3114733, upload-time = "2025-07-30T18:53:00.059Z" },
{ url = "https://files.pythonhosted.org/packages/fb/7f/a8d2fce7f810aa3b7b16550cfbde4756eeb3efcd3646b0adb6c6b7d4bf95/granian-2.5.0-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:0429dd0c4c21c123b06b7f9057ecb4c1fc3d6228f442276e27545a5ad6fc780c", size = 3495857, upload-time = "2025-07-30T18:53:01.43Z" },
{ url = "https://files.pythonhosted.org/packages/29/58/984b53efc3b245f5f9b8d76822061b3fb4c5c1024d526ffe59da55b5405c/granian-2.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c6141582757eb2bb8c8390637a3ac29dbba0c10db93192adb360c7060f149782", size = 3273079, upload-time = "2025-07-30T18:53:02.792Z" },
{ url = "https://files.pythonhosted.org/packages/d1/31/590b932524f43289aa9f735d0b92ccdd97b2d9e388a5acad171fc01382e4/granian-2.5.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0e7627c3b7e3f9c024c4edd80636e8326fbce0420889e0951da349d13742e503", size = 3026668, upload-time = "2025-07-30T18:53:05.505Z" },
{ url = "https://files.pythonhosted.org/packages/50/94/6bcd3d0cec40994112dfd2b3102f4ff3bd2e62928f6524fb95f38fae6647/granian-2.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0a4c317f30c227baf16f541f0c93cb08ee45fbf8a2ef5317ba07b6bd6b7c877", size = 2584723, upload-time = "2025-07-30T18:53:06.865Z" },
{ url = "https://files.pythonhosted.org/packages/2c/5b/44089c2384ba2e3e5a3cdf08e8a000bff07cf7382fa2d9a0e4e1a9ba6451/granian-2.5.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:35eb58b0f80fc9f55d5210d339ba8f5f8d9c126a2e29f051e8b62353e3f84b1d", size = 3326781, upload-time = "2025-07-30T18:53:08.323Z" },
{ url = "https://files.pythonhosted.org/packages/bf/7d/7ca304dde1ce475b83e4add007e36a87284e6838f158db87c11a2c93f379/granian-2.5.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:def250f04c0374069278152bf3e08ecb1f67e0c99d3eb14d902df1e1558b93fa", size = 2937454, upload-time = "2025-07-30T18:53:10.099Z" },
{ url = "https://files.pythonhosted.org/packages/17/6c/858a7cce6ce07adc2f0e7b1809af61d1af2affbc07edaecd127f16207b37/granian-2.5.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eda01a9027f2921f42b4f8bb16e46f8ab67a5345e52b3ffeedd2f921a09c87b6", size = 3229723, upload-time = "2025-07-30T18:53:11.464Z" },
{ url = "https://files.pythonhosted.org/packages/8a/7b/ded645443ec95921d407e3d277418987a4aed955830f67d6b151c8c095f9/granian-2.5.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:c8a51bcb7e533ab75d2d9e13432e9b63c90eeb7fdde700875253efbfb2dcdcbc", size = 3109804, upload-time = "2025-07-30T18:53:13.179Z" },
{ url = "https://files.pythonhosted.org/packages/0a/64/837131cd49e17c219e2a04ed711459e0f93bd5c1db7fc243666e1b9d412a/granian-2.5.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:a9c5670171a97c35aeea79fd7100058e461b0271a7e81fdecd586c770cdd2b41", size = 3099711, upload-time = "2025-07-30T18:53:14.764Z" },
{ url = "https://files.pythonhosted.org/packages/8a/4e/80578d06426a40a2718b3183c5e32ba570001153cbbb3fe523d3f9e89880/granian-2.5.0-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:760275d286142775fb21c85d96fc4996e00de9d3b054c424b2c8519679aa14b5", size = 3468897, upload-time = "2025-07-30T18:53:16.477Z" },
{ url = "https://files.pythonhosted.org/packages/49/8c/7e5d0187a4e53830cc49231a55f01d3d251eec0cbb09209a0ffde8b33741/granian-2.5.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:b1573755288d70f37b55acb14f01cb3a7f7cdca7bf143f908c649ada254e9cb6", size = 3275035, upload-time = "2025-07-30T18:53:17.947Z" },
{ url = "https://files.pythonhosted.org/packages/c9/d8/c3c8a452f1b590400bb2cdef1ca61da8e9913762884cf3e6ba801fd3fdad/granian-2.5.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:50d4dc74ab763c1bf396cf85d93a8202bf1bfb74150b03f9fd62b600cd0c777c", size = 3026123, upload-time = "2025-07-30T18:53:20.94Z" },
{ url = "https://files.pythonhosted.org/packages/89/f0/e7038189d4e3b5f1e10bc23547b687bcdecdefbddef87013db64efea6800/granian-2.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:31705782cd616b9b70536c1b61b7f15815ebc4dcccdb72f58aa806ba7ac5dfa1", size = 2584469, upload-time = "2025-07-30T18:53:22.579Z" },
{ url = "https://files.pythonhosted.org/packages/6a/2d/718620f393b6030d4a9ac5d1bc66cc5d159fb7f7c60c5d4483fd43902f7d/granian-2.5.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bbc4ebc727202ad4b3073ca8148c2af49904710d6fce84872191b2dd5cd36916", size = 3326593, upload-time = "2025-07-30T18:53:24.288Z" },
{ url = "https://files.pythonhosted.org/packages/07/57/c8b5f014673717e850db6d551057e74e330aadad5250d3f312cee432b1ec/granian-2.5.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:af272218076663280fdc293b7da3adb716f23d54211cefad92fcf7e01b3eed19", size = 2937464, upload-time = "2025-07-30T18:53:25.72Z" },
{ url = "https://files.pythonhosted.org/packages/fe/91/8ae8aa9f0c3bbdf42fada15d623a5ab9fffd38acc243ec5619b6cbd60b9a/granian-2.5.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36493c4f2b672d027eb11b05ca6660f9fd4944452841d213cb0cb64da869539b", size = 3229316, upload-time = "2025-07-30T18:53:27.262Z" },
{ url = "https://files.pythonhosted.org/packages/a5/8c/6c294cf3d77dc9524530d30057f9b6e334cc12c5414feb604fb277d030a3/granian-2.5.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:afafac4908d5931e4b2c2a09612e063d7ccd05e531f16b7f11e3bccc4ca8972c", size = 3109818, upload-time = "2025-07-30T18:53:29.004Z" },
{ url = "https://files.pythonhosted.org/packages/4a/49/acc3f1e02e35009d9486e4e00d2c951798a8098935d2374f52c7d2728438/granian-2.5.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:fb157c3d66301ffad4113da4c51aed4d56006b9ebe9d0892c682a634b5fff773", size = 3099384, upload-time = "2025-07-30T18:53:30.448Z" },
{ url = "https://files.pythonhosted.org/packages/c1/87/7cdd96fbeabbceea3820736e65bd6d8c0021983605cee26ef1bf2e11e24b/granian-2.5.0-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:879fdeb71fe279175a25d709d95dd2db01eb67cd12d300e51e3dc704ca5e52fd", size = 3468575, upload-time = "2025-07-30T18:53:31.857Z" },
{ url = "https://files.pythonhosted.org/packages/fd/64/bd41efc6bfbca0ff871ce28a13b9e687055dd70913dfce92c4a21a264bf7/granian-2.5.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:74601bda3aedb249a3d5059d48108acfa61d6f71686162bda0bedc013a443efb", size = 3274703, upload-time = "2025-07-30T18:53:33.378Z" },
{ url = "https://files.pythonhosted.org/packages/e9/43/af71556ea889c28b8c1c74e9f50a64c040a92bae5e4412b8617638a8aa0e/granian-2.5.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:f371dd9eedae26158901fee3eb934e8fa61491cc78d234470ce364b989c78a1f", size = 2955162, upload-time = "2025-07-30T18:53:36.263Z" },
{ url = "https://files.pythonhosted.org/packages/c2/35/14c2c050f3df95eb054f2a44b41a02c30c8a04dc8cca888330f55c43a436/granian-2.5.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f7bf7ed30bcda9bbc9962f187081c5dfa6aa07e06c3a59486bc573b5def35914", size = 2548356, upload-time = "2025-07-30T18:53:38.116Z" },
{ url = "https://files.pythonhosted.org/packages/79/b3/8944acd78ff37a2effcdaf1d6163179e38c46c67c98bb1a68e93a75eb2c2/granian-2.5.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3152037d799ea97e5736de054a48bf75368fb79b7cfee7e6aa46de1076a43882", size = 3091535, upload-time = "2025-07-30T18:53:39.514Z" },
{ url = "https://files.pythonhosted.org/packages/ce/49/cf0c89eaa41ac81271e3ae33834f71db945cc09dba609f6dc0e75247fd35/granian-2.5.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:9a53151c2d31dbcf1acbe6af89ce0282387614b6401650d511ca4260ba0e03c1", size = 2977716, upload-time = "2025-07-30T18:53:41.03Z" },
{ url = "https://files.pythonhosted.org/packages/47/58/e828bd5a02c412484b4056cef9aa505b014cc0bb1882f5dbdaf26782f147/granian-2.5.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:8f9918bee3c21eb1410f4323440d76eaa0c2d2e6ca4fa3e3a20d07cc54b788f6", size = 3094250, upload-time = "2025-07-30T18:53:42.495Z" },
{ url = "https://files.pythonhosted.org/packages/ab/ba/e70f0de5cd6e5bca15ea5e5bc6c5598d34f9d4e9e6707e79e6edb63f1fac/granian-2.5.0-cp313-cp313t-musllinux_1_1_armv7l.whl", hash = "sha256:c28a34951c1ed8eea97948882bdbc374ce111be5a59293693613d25043ba1313", size = 3458806, upload-time = "2025-07-30T18:53:44.308Z" },
{ url = "https://files.pythonhosted.org/packages/a1/c5/4d45042c86a924703f0c9617859742e929cdfe31b644bb16f9845c75342c/granian-2.5.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:944ea3bd400a7ccc8129835eda65bd6a37f8fb77828f4e6ded2f06827d6ec25f", size = 3254382, upload-time = "2025-07-30T18:53:45.769Z" },
{ url = "https://files.pythonhosted.org/packages/6d/7f/b4bbe3b818ec058afc0b5c19d46d4955b1b94297c503d2d92470f60e7e20/granian-2.5.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:2959482f9907206c11a059ababe2705ab10d2ddd68004aaf6f6977e193ffc3e1", size = 3010491, upload-time = "2025-07-30T18:53:48.523Z" },
{ url = "https://files.pythonhosted.org/packages/57/c1/f83ed5c17e66867f0ff91cee530a8c0bf72a31db5dbfcaa41373f5f4bf3b/granian-2.5.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:64327e137b36e648598c595aad4eda92f54e97262cdd3a0ef28dfdaf7998cfed", size = 2569139, upload-time = "2025-07-30T18:53:49.901Z" },
{ url = "https://files.pythonhosted.org/packages/6a/f1/841256d8a21af9a7980a8888e9aa15ce2bcf08d314ca3a0878228fa4bca6/granian-2.5.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b0755ee98a021c223df203f6d5bc7a57131422ba579addfc5f3b5f3d2e362c81", size = 3321682, upload-time = "2025-07-30T18:53:51.342Z" },
{ url = "https://files.pythonhosted.org/packages/f2/66/476ecee6b8fa9c5f61a71f0b26b15c8230fed9ceb4a0e1f2206e7ffe0a40/granian-2.5.0-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee2bb9dc2c93aa441175d7cc1e75aa670a757100945844646de1b7cb3777f50b", size = 2929348, upload-time = "2025-07-30T18:53:53.078Z" },
{ url = "https://files.pythonhosted.org/packages/48/60/9bd62de29df75911f26672bfed13e05273560c51c76cddda25516f53583c/granian-2.5.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd30fe55ba2f9f6ed85b91d6804d042a426480c504b322d42403bec42c1e303c", size = 3222269, upload-time = "2025-07-30T18:53:55.345Z" },
{ url = "https://files.pythonhosted.org/packages/e8/c5/26871d3f8658fdc832768dfe26a005d711c1af358d3b651bab3658220be4/granian-2.5.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:99edb7c9875f91341d40143d14d45919385a29470056f5a2da94e14746cbcf74", size = 3103622, upload-time = "2025-07-30T18:53:57.227Z" },
{ url = "https://files.pythonhosted.org/packages/84/46/ea6c67008ac403fb40d01b8c792827ce8a1625504d1811cd4533219c1e9e/granian-2.5.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:e5142c8dcbe66c9a5c611ba46de47d1ae2cda5f6efdc11d223b586d980158caf", size = 3093618, upload-time = "2025-07-30T18:53:58.695Z" },
{ url = "https://files.pythonhosted.org/packages/1a/de/b344e0237e64d7bd1733b8ff418f616e91028f19e6199169cbffa3b203f6/granian-2.5.0-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:9a11f339ba11940218c0f8ac2c6da872ca1832789e4e2ce10958e8da931bc0b7", size = 3462674, upload-time = "2025-07-30T18:54:00.167Z" },
{ url = "https://files.pythonhosted.org/packages/3a/5b/ec4c8673fba9b240680bf9fbe8c8be049a127a3dbedab926546f3b22e214/granian-2.5.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:846550b371057e3b1e151ef30fe25026e9882a525d0fa66929650eb9ecf75c21", size = 3265050, upload-time = "2025-07-30T18:54:01.79Z" },
{ url = "https://files.pythonhosted.org/packages/2f/7e/b6742dab22285aecbdf59169b3098fb2ec7ad5b98a49b40dde45701bfc97/granian-2.5.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:f69e54a136725e7d2911968571fe9a5685282d0097497799b6453d9d4dffee31", size = 2936812, upload-time = "2025-07-30T18:54:04.878Z" },
{ url = "https://files.pythonhosted.org/packages/db/a6/27e2ea416d87b23df767454b7898bc85f184056f4cbfc208a10b56193e79/granian-2.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d6f209d06723fe20808be7206fc2b86f5fabc015eac1a5d458b1a6e98f005b7a", size = 2531122, upload-time = "2025-07-30T18:54:06.388Z" },
{ url = "https://files.pythonhosted.org/packages/2e/a0/46733fbde3f19bcd07b16619113a898ebe0c2762e6a63eb8eed234c48bfc/granian-2.5.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e5fd23b02bd2c7cc9ca86d780f262b63a53bc5d7846e96a15edf4ac7aa10a79", size = 3085608, upload-time = "2025-07-30T18:54:08.426Z" },
{ url = "https://files.pythonhosted.org/packages/68/6f/28e7ddf79de6244a69a0c5e283a2ab7a4089185f9f3007fb9fed86ec11d0/granian-2.5.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:48968902d351f1bfc70ac5fa69157ee86e5f2947f49df5a84c0965213dbb4ee0", size = 2971321, upload-time = "2025-07-30T18:54:09.886Z" },
{ url = "https://files.pythonhosted.org/packages/1d/1a/3ba12fd7466741cf3fe7e67d36153261099acd94ff29f6096b8ee62db4f2/granian-2.5.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:000952722eb63bb1811e391142872bf1ebe7f4434b1d39bbf4e91090d4af9429", size = 3088487, upload-time = "2025-07-30T18:54:11.4Z" },
{ url = "https://files.pythonhosted.org/packages/15/1c/ed2d4e3029ffd0d3f3522e38c022b5bbe23d4b27ea919c4737fc307b43dc/granian-2.5.0-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:141bb4aff1c83b51b2d34381b515d9db38d0481603119e4519fdc9ae932e7a2e", size = 3452014, upload-time = "2025-07-30T18:54:12.894Z" },
{ url = "https://files.pythonhosted.org/packages/e6/24/0575774a57278db4bf49da156e02ad11aa65e564a2070d66c1b0849f9ae7/granian-2.5.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:fe1f870d24c0f9e4a2dfb6f44ef2173fac90425dcc7e35538028fa5ada75876b", size = 3248972, upload-time = "2025-07-30T18:54:14.279Z" },
{ url = "https://files.pythonhosted.org/packages/39/2c/4e51fe7d7539d5629df74735529220bb7f21155151ad50ff346c117b9199/granian-2.5.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:8e0ba96062b56d76311b2ce990065ae5580539e97e1609ab1949ce9f29114b3e", size = 3034058, upload-time = "2025-07-30T18:54:32.87Z" },
{ url = "https://files.pythonhosted.org/packages/9b/0f/cb32b490e1aff8dde99b730013e4cc429e9b15b884dfbb95fcf423973cab/granian-2.5.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:88de6d16de826897a83eb971af4ff6b82275771f6504e802e36cb2749836502d", size = 2601562, upload-time = "2025-07-30T18:54:34.341Z" },
{ url = "https://files.pythonhosted.org/packages/3f/2f/807528573ddcad450a282133389d83ee4582e3ae0eff1de20e8d70768e6c/granian-2.5.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4df376a99fed695aeadbd239cda670431184a5e69615ee5aab1624b35dbddbdf", size = 3216043, upload-time = "2025-07-30T18:54:35.806Z" },
{ url = "https://files.pythonhosted.org/packages/c9/d8/226cc1864814e738fd53971159662cb9d55c5191681a080a4cb73555b760/granian-2.5.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d0c41cc1c58c75f6bedde20e958a54cd09c40c924cb8e07e516c76a308e9e64d", size = 3106601, upload-time = "2025-07-30T18:54:37.457Z" },
{ url = "https://files.pythonhosted.org/packages/9f/11/25b0b7ac30af79d4390149cc96aa8e8bf4d5dbe605feeaec7b832c05c156/granian-2.5.0-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:ac08d7f6c44643228220c33c3e697601bd785e970e587ab8c7bac11a661e91b6", size = 3106064, upload-time = "2025-07-30T18:54:39.556Z" },
{ url = "https://files.pythonhosted.org/packages/6c/e8/f4f9239f1d932c73a96efb930a8278c36e44d5ec051a08a4d98bd3bc4ce5/granian-2.5.0-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d2258cdc480327a6d0552acdb1005cd2cd65caffc79ebb796c900ece43b66c1d", size = 3533494, upload-time = "2025-07-30T18:54:41.528Z" },
{ url = "https://files.pythonhosted.org/packages/f7/f9/b57edf4f5f90c5bc01880a51ae257810bb479048c333532173677fc2212b/granian-2.5.0-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:1db48cf24f3230c748e59d0f6d40616289e549cfe51fa908e1a2a85575b9ca54", size = 3272111, upload-time = "2025-07-30T18:54:43.07Z" },
{ url = "https://files.pythonhosted.org/packages/17/ee/97010d532c4ac48fb2c6dd03e296c8d6ad5829e09f399bfd0a33b6098953/granian-2.5.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a80f23e904f23ca9a90d613444a477a8df8bb2ef1df7bf279ffa6ab7cbbf042a", size = 3034092, upload-time = "2025-07-30T18:54:46.212Z" },
{ url = "https://files.pythonhosted.org/packages/08/f9/263fd8d1d2f0904ead415a19a1b05980180ee647edcb6b0727e2a942e4ad/granian-2.5.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:53b10f7996c9a732cb3e4cf30890badbac5f9ec4baa2898851a68100767cc754", size = 2601634, upload-time = "2025-07-30T18:54:47.765Z" },
{ url = "https://files.pythonhosted.org/packages/e7/a0/2a3348f6a1291a50cc0b6535b6cdf7fbb73998a5ced6536c0026834a781d/granian-2.5.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f1a05836852ff70745ec094940d9edc62bb3f1a1f7ffb8ee692d6727ebc8b95", size = 3216203, upload-time = "2025-07-30T18:54:49.404Z" },
{ url = "https://files.pythonhosted.org/packages/d9/5f/f18826ae61861c6e20a1887a359c71074f423e4cd7237faf186618d0f7ee/granian-2.5.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:3ce4c44ebb949980cc02e0c8a823fb4352c94f2443004fe4c39eb0262fcb2e6e", size = 3106525, upload-time = "2025-07-30T18:54:50.927Z" },
{ url = "https://files.pythonhosted.org/packages/c7/51/6776580a11e0966af4919735072b7d6fd95feea2907753a77046f1f8fbe3/granian-2.5.0-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:5a8eea72a37c582fe2653587f5b4bb5323bd8882fcbccff3054311b0735d3814", size = 3106320, upload-time = "2025-07-30T18:54:53.034Z" },
{ url = "https://files.pythonhosted.org/packages/37/8f/9910ac8585fe0f8f0d55bb197a08cccea27e7cd778d059302e5d4c78dfdf/granian-2.5.0-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:168762227d94b74dddff066b2f3519f22426e09f8394ed1a2f48072f80be9275", size = 3533584, upload-time = "2025-07-30T18:54:54.837Z" },
{ url = "https://files.pythonhosted.org/packages/3c/49/24c811233d11756182ad13dd30e5323dd88faeb76879493dccb484f8d8fe/granian-2.5.0-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:534a0922c460a8bf9c85937edbc7aac00497d05b64bf9ccc5f5b93006882ecd7", size = 3272305, upload-time = "2025-07-30T18:54:56.948Z" },
{ url = "https://files.pythonhosted.org/packages/6b/5f/a1a68e68e145979a1387fb27918f057758ed98af7ab71dce865bd8de6200/granian-2.4.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:7a5279a4d6664f1aa60826af6e3588d890732067c8f6266946d9810452e616ea", size = 3051532, upload-time = "2025-07-01T21:47:21.13Z" },
{ url = "https://files.pythonhosted.org/packages/3c/9f/1672e33247cfb1128147e38f27e7e226e0e36185a070570480cdd710212b/granian-2.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:42c93f33914d9de8f79ce4bfe50f8b640733865831c4ec020199c9c57bf52cfd", size = 2709147, upload-time = "2025-07-01T21:47:23.553Z" },
{ url = "https://files.pythonhosted.org/packages/70/02/52031944a6c7170ca71c007879ffd6c1ad5e78bd4c9d0ed76b1d3c43916c/granian-2.4.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5468d62131dcc003c944bd4f82cd05e1c3d3c7773e367ef0fd78d197cc7d4d30", size = 3307063, upload-time = "2025-07-01T21:47:25.065Z" },
{ url = "https://files.pythonhosted.org/packages/29/1b/590108fd38356e29b509e32fea25036e1b12ea87e102e08615b01b342e47/granian-2.4.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ab74a8ecb4d94d5dda7b7596fa5e00e10f4d8a22783f7e3b75e73a096bd584f5", size = 3004408, upload-time = "2025-07-01T21:47:26.541Z" },
{ url = "https://files.pythonhosted.org/packages/ed/4f/fbf480554a80217af3428e1a6c6dd613e2c4ab4568839ee2473a9c25e297/granian-2.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6a6582b10d7a9d4a4ef03e89469fbfe779309035e956a197ce40f09de68273a", size = 3219653, upload-time = "2025-07-01T21:47:28.1Z" },
{ url = "https://files.pythonhosted.org/packages/99/21/dc0743099e615c87475d10f4e0713de067279243a432aa407c13d14af40e/granian-2.4.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:5f471deb897631e9c9b104ea7d20bffc3a7d31b5d57d4198aa8e41e6c9e38ac6", size = 3102815, upload-time = "2025-07-01T21:47:29.298Z" },
{ url = "https://files.pythonhosted.org/packages/e0/90/7df59160facda055050bfcf1987cc43f2d67d6d5ce39e23e3bd927978ba0/granian-2.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:522f7649604cd0c661800992357f4f9af9822279f66931bbe8664968ffd49a2a", size = 3094521, upload-time = "2025-07-01T21:47:30.459Z" },
{ url = "https://files.pythonhosted.org/packages/a4/8e/72fa602cc07df284beac01ff2eb9ccbeee23914e9790d7b91ca401edf428/granian-2.4.1-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:2a12f6a6a86376e3dc964eaa5a7321cd984c09b0c408d5af379aa2e4cb1ba661", size = 3444340, upload-time = "2025-07-01T21:47:31.972Z" },
{ url = "https://files.pythonhosted.org/packages/a1/90/73438d52c1cb68f7e80bbdb90aff066167c6ef97053afc26d74f56635775/granian-2.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1c5c1494b0235cf69dc5cac737dc6b1d3a82833efd5c9ef5a756971b49355988", size = 3246331, upload-time = "2025-07-01T21:47:33.089Z" },
{ url = "https://files.pythonhosted.org/packages/12/36/3189cf0aa085732859355e9f0464e83644920fab71429c79e32807f7be32/granian-2.4.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:dc90c780fc3bb45e653ebab41336d053bc05a85eeb2439540b5d1188b55a44a5", size = 3051270, upload-time = "2025-07-01T21:47:35.791Z" },
{ url = "https://files.pythonhosted.org/packages/c0/f2/57311b3c493b3dac84f7bb2d2d2e36bb204efa5963bf64acda2c902165cf/granian-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8303307f26df720b6c9421857478b90b8c404012965f017574bf4ad0baca637b", size = 2709284, upload-time = "2025-07-01T21:47:36.958Z" },
{ url = "https://files.pythonhosted.org/packages/41/c5/a9b9ff4ad4411405a79b18425489b731762a97641b99caddc07577922d12/granian-2.4.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c6e6e501eac6acf8ac5bc6247fa67b3eb2cd59b91e683d96028abbf7cb28b0ed", size = 3306997, upload-time = "2025-07-01T21:47:38.128Z" },
{ url = "https://files.pythonhosted.org/packages/81/3a/35f3fc7134bb1b7ea677adf6506b78723f8356ba4230ca1790d7251e421c/granian-2.4.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66b995a12229de0aa30cbe2a338279ac7e720b35db20592fe7fed7a9249649ac", size = 3004758, upload-time = "2025-07-01T21:47:39.69Z" },
{ url = "https://files.pythonhosted.org/packages/f2/99/ffb3bba665f81ab7e339afbce2c9da14178e4e85ce20ec599791117557af/granian-2.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdf7529847f9aa3f25d89c132fb238853233bfb8e422f39946ebb651cb9f1e6a", size = 3219788, upload-time = "2025-07-01T21:47:41.268Z" },
{ url = "https://files.pythonhosted.org/packages/0d/91/2684c1c29574a39e5436149cc977e092004d0357bca0e03f55264a39299e/granian-2.4.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:6eb47dd316e5e2354e81c514cb58455c37ea84f103756b6f6562181293eee287", size = 3102656, upload-time = "2025-07-01T21:47:42.514Z" },
{ url = "https://files.pythonhosted.org/packages/b7/cc/64dc5d96c5557f1bda25e52eb74284f295a46b4c1660b95bdd212665d5ae/granian-2.4.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9218b0b4e2c0743444d1a84ba222236efd5d67702b024f8ce9fd2c309f6b147b", size = 3094233, upload-time = "2025-07-01T21:47:43.645Z" },
{ url = "https://files.pythonhosted.org/packages/db/53/f4d30b60b628698bce653196c75d369bdc543e2d31a6811fd3a963b396ef/granian-2.4.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:dd07733183eb291769d0929ec58c6f16293f82d09fbc434bc3474f1c5e185c3c", size = 3444746, upload-time = "2025-07-01T21:47:44.984Z" },
{ url = "https://files.pythonhosted.org/packages/c5/0d/737a6185a2db9f662de5b5a06373e1244f354ebc132e6bde5987d34ad169/granian-2.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cf1301875c28bb54d87280473d3b2378fb86339d117913a13df1ab2764a5effe", size = 3246068, upload-time = "2025-07-01T21:47:46.611Z" },
{ url = "https://files.pythonhosted.org/packages/f4/d5/c0e6258b8aa18dbb335cd3a886d07ae64bb661ce3fc655d8efa24043cda5/granian-2.4.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:5e05c62d82f14dec1b36b358d766422423f5d610c414a3c83259424174a3658e", size = 3044572, upload-time = "2025-07-01T21:47:49.627Z" },
{ url = "https://files.pythonhosted.org/packages/a0/d7/f6b6b5a9d59fc13bcf65554e5cee0ff4e8581fd8af0a69a760e495ab9190/granian-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6166ea4b96cfa2409b70579b1c2609f52fa6035999f7f57975b3b9fc0486f2b1", size = 2698583, upload-time = "2025-07-01T21:47:51.241Z" },
{ url = "https://files.pythonhosted.org/packages/3b/b8/714141af2190f49b8aac8f72a55621e1730e104a7afac5f8cb3b6c92ddd2/granian-2.4.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0fc250818064d47c48eb02af7e703bb692ee1d478575fce9659e96cf576f03f3", size = 3303145, upload-time = "2025-07-01T21:47:52.437Z" },
{ url = "https://files.pythonhosted.org/packages/39/6e/1b4b25ab3a734c13e7edb3f219df9d27760ce6b2077c3a29e7db1fd9ff66/granian-2.4.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:019464b5f28a9c475cb4b0aa29d3d1e76f115812b63a03b30fb60b40208e5bf2", size = 2994252, upload-time = "2025-07-01T21:47:53.854Z" },
{ url = "https://files.pythonhosted.org/packages/95/fc/1be24a6e8c64c47516222e1198e407c134ed1596919debc276fd8ebf35c6/granian-2.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82da2bf26c97fd9bc6663bbeda60b469105f5fb4609a5bdc6d9af5e590b703fe", size = 3216855, upload-time = "2025-07-01T21:47:55.923Z" },
{ url = "https://files.pythonhosted.org/packages/95/86/fe782ee6093c92208d1d5caaf4c0af689c67f1d0ade1b4525c199bf2477c/granian-2.4.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:0bd37c7f43a784344291b5288680c57ab8a651c67b188d9f735be59f87531dbd", size = 3096595, upload-time = "2025-07-01T21:47:57.602Z" },
{ url = "https://files.pythonhosted.org/packages/24/e0/c0f21edede864276129471c8fef7ec8b28ef41498ae61a5e204eb5fe09da/granian-2.4.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ddd27ed8b98da83c6833b80f41b05b09351872b4eedfe591eb5b21e46506477", size = 3080317, upload-time = "2025-07-01T21:47:58.797Z" },
{ url = "https://files.pythonhosted.org/packages/9d/0b/18aeb06d9126405716608b1707d174e00b2fd50ea27c7e36a6a0c97eede4/granian-2.4.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:e42d4e1712de2412449771aae1bbedf302b3fedb256bf9a9798a548a2ceddacf", size = 3420134, upload-time = "2025-07-01T21:47:59.993Z" },
{ url = "https://files.pythonhosted.org/packages/21/7a/c63c8c35215d59306eb42639cfedbe656443247ef0f9212717ad40deee8f/granian-2.4.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ba5c9f5a5e21c50856480b0d3fa007c846acee44e5b9692f5803ae5ba1f5d7f3", size = 3242402, upload-time = "2025-07-01T21:48:01.319Z" },
{ url = "https://files.pythonhosted.org/packages/d2/8a/3417812f0cc6e518dcd06b0c6965d69f5e740d7989a976e6531a420fd884/granian-2.4.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:86b3a50ff2b83eb2ad856ef32b544daa4162b5da88926edc3e18d5111c635713", size = 3044274, upload-time = "2025-07-01T21:48:03.809Z" },
{ url = "https://files.pythonhosted.org/packages/f0/df/75f57f08224504260290518501cb25d325a51172adad673843db5f006093/granian-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a8796c39fa0618dd39765fee63776b0ff841986a0caa8aae2d26dce0dae4898c", size = 2698572, upload-time = "2025-07-01T21:48:05.387Z" },
{ url = "https://files.pythonhosted.org/packages/9c/27/c2ffaa57710b39d0fb5f03294033411672d700e78cd641eae5e18139a466/granian-2.4.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:95d48c4aff262c5b31438a70f802fa9592c59d3f04fbf07e0f46efefd1e03bb4", size = 3302180, upload-time = "2025-07-01T21:48:07.061Z" },
{ url = "https://files.pythonhosted.org/packages/aa/c7/a6121c187c762e127367544214041f98963e4e7dfd2c1dfdbfbe1bc46fe3/granian-2.4.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bbe7a9e95335227a741bbfd815594f10d637fc4d6824335bdd09fe8cb7ce9cf5", size = 2994091, upload-time = "2025-07-01T21:48:08.791Z" },
{ url = "https://files.pythonhosted.org/packages/ed/9d/74690dd9cb3541c09b98e1fd75deddcc3885af7ecac3eb813e9f2b4df5e4/granian-2.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e95d58dfd6a4bbf89f826863506a789b7fc12e575b4128b3c095450cffa334d4", size = 3216004, upload-time = "2025-07-01T21:48:10.187Z" },
{ url = "https://files.pythonhosted.org/packages/72/83/e09820a814a3071edb0abccf9ddfe7c7d9be337cfb49987a75c759b281a2/granian-2.4.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:266a036f1de45c01b6518a62e4878b6368bc09bff4ff14e4481eb5c556951a8c", size = 3096136, upload-time = "2025-07-01T21:48:11.488Z" },
{ url = "https://files.pythonhosted.org/packages/a4/0b/a6adefd57834903af73cafafe02a77a324b9422758cc52923a97eba5085a/granian-2.4.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:5aeb00bce5e025fe4b640799c15061aaebc7edf1bd7b8aff6caeed325674fcda", size = 3080194, upload-time = "2025-07-01T21:48:12.765Z" },
{ url = "https://files.pythonhosted.org/packages/dc/1b/b4c62359303ade1e6d5a96b019f0db52da0b545a990cc580a6caacfedacb/granian-2.4.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:8982f76b753f5b3b374aff7e6e3b7061e7e42b934a071ae51e8f616ad38089fe", size = 3419814, upload-time = "2025-07-01T21:48:14.439Z" },
{ url = "https://files.pythonhosted.org/packages/cc/dd/e240acc4390bbe056592d37dfd89384d706572af196551a5d9f7ddd6ff22/granian-2.4.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3059d4577863bcfc06e1036d6542ec5e6d98af6bbd1703c40806756971fee90a", size = 3241894, upload-time = "2025-07-01T21:48:19.284Z" },
{ url = "https://files.pythonhosted.org/packages/29/8c/af2139e6fae75a587ae616acb4abaaf6b87fc0939c1ed18598e1ab9e3fb5/granian-2.4.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:87b5ca8686dae65cb11c12ef06f8eebae31be8f4385ff1b892ffb8ed604b3ce4", size = 2975244, upload-time = "2025-07-01T21:48:22.079Z" },
{ url = "https://files.pythonhosted.org/packages/6b/83/54b31cc7bf578a9fba2112d0fa67b5c87a17198a44fb4ca9588773630bc2/granian-2.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7b0caf3363657913530418e4af115e89f428075bd46c0bf972b1557e417ad9a7", size = 2639421, upload-time = "2025-07-01T21:48:23.395Z" },
{ url = "https://files.pythonhosted.org/packages/3a/1f/007dae5d387a19d52eaee04c58e21c0bd261dfb9bc3d5ba60f956b8818f0/granian-2.4.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e324d5ffe8c8c964d2d909ba68b46395b1179cd4aa0e9950f10df0741f689d4d", size = 3067951, upload-time = "2025-07-01T21:48:24.697Z" },
{ url = "https://files.pythonhosted.org/packages/6c/f2/c9fd583e1f528361c78077e31e377aad96f38e193e1e175525abc1ff5a2f/granian-2.4.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:33fabdd106df6f4de61b018847bc9aaa39fa8e56ced78f516778b33f7ad26a8f", size = 2964829, upload-time = "2025-07-01T21:48:26.286Z" },
{ url = "https://files.pythonhosted.org/packages/d3/95/5e297f7c02f4db5f6681fea8a577921366379d814a3bd2bfd4d184390bac/granian-2.4.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:452ed0de24bcdfc8bc39803650592d38bc728e94819e53c679272a410a1868f8", size = 3070446, upload-time = "2025-07-01T21:48:27.648Z" },
{ url = "https://files.pythonhosted.org/packages/5c/24/933e3d7cfd4e2dc97ae7f1e5be1c5a93b3d664118323d58047a320119667/granian-2.4.1-cp313-cp313t-musllinux_1_1_armv7l.whl", hash = "sha256:b69ff98e5ba85095b88f819525c11118c0f714ff7927ad4157d92a77de873c18", size = 3410970, upload-time = "2025-07-01T21:48:29.558Z" },
{ url = "https://files.pythonhosted.org/packages/02/ff/2bfcb0e8c98ac2abe0c65d6950e35ef2aececb21c1378201591e621c8f96/granian-2.4.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:17517f379b4def0d4ead09cb5febbf07a6f3380065995eb3646f77a67bd0a8d4", size = 3232429, upload-time = "2025-07-01T21:48:31.118Z" },
{ url = "https://files.pythonhosted.org/packages/c9/f3/f275a6d59dc373dba73af73c416b9e4140c5aca2988ba76348f256c389b6/granian-2.4.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:36beed559c729ca24d512de4fd7397a5f04fbd01caafa71bd8d2ca7a96d9aeed", size = 3032351, upload-time = "2025-07-01T21:48:34.144Z" },
{ url = "https://files.pythonhosted.org/packages/27/14/892b86220893c5fe303dbe0f09c99643c44bcfc469f2e1ce827abc353a49/granian-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2891d9e214c7369e1c0eb8004d798a1b9a0b5d4f36de5fc73e8bb30b15786f59", size = 2681597, upload-time = "2025-07-01T21:48:35.497Z" },
{ url = "https://files.pythonhosted.org/packages/4d/89/02a17e1839e339590e81b13024e4ca31232a7038346c3aaaf7f60a59f936/granian-2.4.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bddd37bf65c007befb0d86dc7968e3fc06ebd114df1e3b270627004bdba049d2", size = 3298967, upload-time = "2025-07-01T21:48:37.085Z" },
{ url = "https://files.pythonhosted.org/packages/07/ca/8f8904ef23d19b436bd64eeaae4fc4c35a78b8f44d905e0ded571ff89b1e/granian-2.4.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:acc82f3e8d85f02e495a01e169dc76ab319875c3a6c512ee09769b27871e8268", size = 2988213, upload-time = "2025-07-01T21:48:38.75Z" },
{ url = "https://files.pythonhosted.org/packages/96/45/6f31a58d12e2d938071a245db19bb2ba09c14b4881d531bd9f86c12313aa/granian-2.4.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d4ea691ac19e808c4deb23cc142708a940a1d03af46f8e0abf9169517343613", size = 3211546, upload-time = "2025-07-01T21:48:40.048Z" },
{ url = "https://files.pythonhosted.org/packages/df/8b/111a1735c055f57e8844e20ab6b05db9305c5e7df87b47b95ba4a4f67924/granian-2.4.1-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:f446eabd25995d6688459e1ed959b323aa3d7bf4d501d43c249bf8552f642349", size = 3090038, upload-time = "2025-07-01T21:48:42.291Z" },
{ url = "https://files.pythonhosted.org/packages/0e/e1/959e7fcfbc6752f30ca491ec786e3051a09dc2f50886e7513d6c54ef8c5e/granian-2.4.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:e40f89956c92f6006bc117001a72c799d8739de5ec08a13e550aa7a116ac6ef0", size = 3074937, upload-time = "2025-07-01T21:48:43.978Z" },
{ url = "https://files.pythonhosted.org/packages/b3/5f/9681d9e605f4659b94c13bd12be0324332cbc76a1d9ee369b2fb4f8bb6fb/granian-2.4.1-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:74554a79d59fcec5dbc44485039eedc7364e56437bec9c4704172a2a8cbdc784", size = 3416187, upload-time = "2025-07-01T21:48:45.325Z" },
{ url = "https://files.pythonhosted.org/packages/57/c3/18f49e4c251d624e31ca0bfcb3056c0a162296b904954e8771f122ac42e2/granian-2.4.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:97f79411fe6c9bc82efa2c8875a08adf1dcdf9c0336a1f3858a3835572c40eed", size = 3235677, upload-time = "2025-07-01T21:48:46.752Z" },
{ url = "https://files.pythonhosted.org/packages/b7/61/2640db211a9eaf14d95fc94818c9cdddf8e026ec9ee7bad1b39b2d90a6b4/granian-2.4.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:e53be3efa80bdd8c8ef4e6bd5e22ddc7bfd17fe8a3e37c43c9b4228c05afd075", size = 2968799, upload-time = "2025-07-01T21:48:49.527Z" },
{ url = "https://files.pythonhosted.org/packages/df/b1/cd8138c0f783caef5d2da1bde3f4bc6b71ad8e102acaae173d12e80306d8/granian-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:955e6a861c3de1e510f724d2d07ca5798bfb8fef1de30e166f23caf52d9a4582", size = 2624589, upload-time = "2025-07-01T21:48:50.975Z" },
{ url = "https://files.pythonhosted.org/packages/9e/b3/368282d1f830b8008cdad3a413f81d849b5000213d39ecbfab25f32c405a/granian-2.4.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21a0dddf558fe722d8b1b7dc18b4bff05afa90b25f498da8d7c3403fe4e1e9e0", size = 3063109, upload-time = "2025-07-01T21:48:52.587Z" },
{ url = "https://files.pythonhosted.org/packages/1f/69/578cecd39ff50e9e29f1e74f243ed30fd743301dd88537462f0fb13b803c/granian-2.4.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:a5a6bfd310d7a86b12673b1a1969c44d60a6b9059e8fc86d238aa1d52e5d2268", size = 2959657, upload-time = "2025-07-01T21:48:53.973Z" },
{ url = "https://files.pythonhosted.org/packages/9a/0e/1811d70c0701ef7a969d8d9c5cab3415139aa66660925f48676fc48dad22/granian-2.4.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:e7ad9d0c1a5f07b5e0085a92f94db1e5a617826801b4dce8bfeae2441a13b55f", size = 3065173, upload-time = "2025-07-01T21:48:55.278Z" },
{ url = "https://files.pythonhosted.org/packages/9a/ba/29a554dba7194479b20756075596e387885c91bbfea276375c6fd34797da/granian-2.4.1-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:e7c099a9a431fc6ee05bb89d106045c43413854a1ed646f960bc06385eaefd7e", size = 3405136, upload-time = "2025-07-01T21:48:56.638Z" },
{ url = "https://files.pythonhosted.org/packages/73/37/d6002091509c4f2a14132be702d0ff910b69fda9d88098e6379347420873/granian-2.4.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:1273bebaf9431aa938708e0c87d0b4eb2ff5a445c17d9a7eb320af96f33fa366", size = 3227816, upload-time = "2025-07-01T21:48:58.035Z" },
{ url = "https://files.pythonhosted.org/packages/8d/43/fed39e0611e967934da940435e4ce3bd23835dac8e9811c57eb551e0be05/granian-2.4.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:72f826123561895469b3431db0d96484f52863743181b3f1f41c73b4adbc7807", size = 3049482, upload-time = "2025-07-01T21:49:15.984Z" },
{ url = "https://files.pythonhosted.org/packages/99/13/e7ab0944e82e441d903eafc884b246c25fd2e66e9de01b8c0dde5806ce76/granian-2.4.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:0efdbe606d0b98e2724d90c18e33200870f3eb1b75c33ca384defb7e95bca889", size = 2699245, upload-time = "2025-07-01T21:49:17.397Z" },
{ url = "https://files.pythonhosted.org/packages/46/64/2fb7949494d3d39c1afc26bac9539e129571d5aff54e6ddfad3ebbcaf822/granian-2.4.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64f38d0e0425016b764ef333ed2ddac469eca09d50395ad15059c422d7faa3c0", size = 3212448, upload-time = "2025-07-01T21:49:18.781Z" },
{ url = "https://files.pythonhosted.org/packages/73/09/72d6dbb880f14a5d461a681a9068fce8bd214d4f190cc27d17dff669e5c0/granian-2.4.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:519a9d62fd0a5106b3d316902c315ea65fc8acc5d4c3ba84427dd51367dc251c", size = 3112247, upload-time = "2025-07-01T21:49:20.196Z" },
{ url = "https://files.pythonhosted.org/packages/80/ba/6bd2838e0082fa3b385c94fa4559c847d573d377c3e283c3eadae40a5110/granian-2.4.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d5f336179f010be9bbd2a5999851150e98d31ba3b9baae609eb73c99106dca1e", size = 3092795, upload-time = "2025-07-01T21:49:21.743Z" },
{ url = "https://files.pythonhosted.org/packages/15/55/de4700fbb6d406bd86860f855387e7f3f37e7231429d9e9afb93d04eb2f0/granian-2.4.1-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:e82a41444f2cdf70114fdc7b70b2b20e50276c0003f5535f9031f8f605649cb4", size = 3455186, upload-time = "2025-07-01T21:49:23.126Z" },
{ url = "https://files.pythonhosted.org/packages/c0/45/20d430f2d59e2de3b78577d918a219547930339be6693466d7841b12a7ec/granian-2.4.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:cb728baa8292150c222719d8f1a17eaf4d44d7c1a3e141bc1b9a378373fada5b", size = 3246602, upload-time = "2025-07-01T21:49:24.679Z" },
{ url = "https://files.pythonhosted.org/packages/0f/33/b5c6d733a9f64049eecc84000eda100e76d699d75299bd61d6f134852eca/granian-2.4.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2e902d611e8b2ff72f9c516284e0c4621c7f93b577ae19aea9eb821c6462adcc", size = 3049355, upload-time = "2025-07-01T21:49:27.809Z" },
{ url = "https://files.pythonhosted.org/packages/4e/3e/fb70016f426dc7c6423583d5625391b80e8d479283f7bc0c6452dfb8dfd5/granian-2.4.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:e02ac71af55a9514557b61541baea1b657cf2a11aa33335f292a64e73baef160", size = 2699157, upload-time = "2025-07-01T21:49:29.337Z" },
{ url = "https://files.pythonhosted.org/packages/43/9b/d6ea53cbf3f527d38ad30ffa4304ed566de3e481186bfe9396dc19f76c8c/granian-2.4.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf7daddd6c978726af19db1b5a0c49d0f3abf8ef1f93804fc3912fd1e546c71a", size = 3212442, upload-time = "2025-07-01T21:49:30.872Z" },
{ url = "https://files.pythonhosted.org/packages/fc/ef/5fff01d6cde612469e0e16198afc9027d1e331304adb025db3461afd4baf/granian-2.4.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:54928278eb4b1a225295c06bbfae5dbc1559d6b8c870052f8a5e245583ed4e28", size = 3112239, upload-time = "2025-07-01T21:49:32.322Z" },
{ url = "https://files.pythonhosted.org/packages/1f/64/541b640354e3a12b0125af545fdb138d9c3688b341db2d2cb98540373707/granian-2.4.1-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:afb0a69869b294db49bbbb5c03bc3d8568b9fc224126b6b5a0a45e37bb980c2c", size = 3092835, upload-time = "2025-07-01T21:49:33.882Z" },
{ url = "https://files.pythonhosted.org/packages/c8/b2/c4f6ab5eb28d4cdc611bc10a50c64e959e36a0574ba91ad6eced6fcb8754/granian-2.4.1-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:5f3c94c342fa0239ded5a5d1e855ab3adb9c6ff489458d2648457db047f9a1d8", size = 3455269, upload-time = "2025-07-01T21:49:35.757Z" },
{ url = "https://files.pythonhosted.org/packages/d1/24/86e07e45695bde6dc8a9d878c2be08d5d0dcc41ec8514ecf77ebc9bb3b59/granian-2.4.1-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:51613148b46d90374c7050cc9b8cff3e33119b6f8d2db454362371f79fac62f3", size = 3246476, upload-time = "2025-07-01T21:49:37.33Z" },
]
[package.optional-dependencies]
@@ -2070,7 +2070,7 @@ requires-dist = [
{ name = "filelock", specifier = "~=3.18.0" },
{ name = "flower", specifier = "~=2.0.1" },
{ name = "gotenberg-client", specifier = "~=0.10.0" },
{ name = "granian", extras = ["uvloop"], marker = "extra == 'webserver'", specifier = "~=2.5.0" },
{ name = "granian", extras = ["uvloop"], marker = "extra == 'webserver'", specifier = "~=2.4.1" },
{ name = "httpx-oauth", specifier = "~=0.16" },
{ name = "imap-tools", specifier = "~=1.11.0" },
{ name = "inotifyrecursive", specifier = "~=0.3" },