diff --git a/LICENSE b/LICENSE
index 64ec6d4..9d3f0ee 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
MIT License
-Copyright (c) 2026 arXiv
+Copyright (c) 2026 arXiv, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/app/email_content.py b/app/email_content.py
index 4dab632..287c58b 100644
--- a/app/email_content.py
+++ b/app/email_content.py
@@ -8,7 +8,7 @@
from arxiv.db.models import Submission, SubmissionCategory
from arxiv.taxonomy.definitions import CATEGORY_ALIASES
-from app.schema import SubEmailData, SimplifiedNotification, CommentData, PromoteData, NewPropData, PropRespData, EmailTask, UserContact
+from app.schema import SubEmailData, SimplifiedNotification, CommentData, PromoteData, NewPropData, PropRespData, CategoryRejectionData, EmailTask, UserContact
_ET = ZoneInfo(arxiv_settings.ARXIV_BUSINESS_TZ)
def _fmt_time(dt: datetime) -> str:
@@ -19,6 +19,7 @@ def _fmt_time(dt: datetime) -> str:
from app.templates.promote import render_promote_block
from app.templates.new_prop import render_new_prop_block
from app.templates.prop_resp import render_prop_resp_block
+from app.templates.category_rejection import render_category_rejection_block
from app.templates.submission import render_submission_block
from app.templates.email_body import render_body
@@ -96,6 +97,8 @@ def render_change_block(change: SimplifiedNotification, user_name: str) -> tuple
return render_new_prop_block(change, user_name)
case PropRespData():
return render_prop_resp_block(change, user_name)
+ case CategoryRejectionData():
+ return render_category_rejection_block(change, user_name)
case _:
raise ValueError(f"unknown change data type: {type(change.data)}")
diff --git a/app/main.py b/app/main.py
index 1988981..4ea19ef 100644
--- a/app/main.py
+++ b/app/main.py
@@ -87,7 +87,7 @@ def main():
subscription_path = subscriber.subscription_path(settings.GCP_PROJECT_ID, settings.PUBSUB_SUBSCRIPTION_ID)
messages=get_messages(subscriber, subscription_path)
if len(messages)==0:
- logger.warning("No messages found.")
+ logger.info("0 messages found.")
return
def ack(ids: list[str]) -> None:
diff --git a/app/moderators.py b/app/moderators.py
index 69b54e8..8066317 100644
--- a/app/moderators.py
+++ b/app/moderators.py
@@ -4,6 +4,7 @@
from sqlalchemy import select
from arxiv.taxonomy.category import Category
+from arxiv.taxonomy.definitions import CATEGORY_ALIASES, CATEGORIES_ACTIVE
from arxiv.db import Session
from arxiv.db.models import t_arXiv_moderators, TapirUser, TapirNickname
@@ -76,22 +77,53 @@ def get_all_moderators() -> tuple [dict[str, ToEmail], dict[str, ToEmail]]:
return all_archives, all_cats
+_ALIAS_BY_CANONICAL = {v: k for k, v in CATEGORY_ALIASES.items()}
+
def who_to_email(category: Category, all_archives: dict[str, ToEmail], all_cats: dict[str, ToEmail])-> tuple[set[int], set[int]]:
"""determines who to include in an email for a given set of categories"""
email: set[int] = set()
reply_to: set[int] = set()
+ rolling_dont_email: set[int] = set()
+ rolling_dont_reply: set[int] = set()
cat_entry = all_cats.get(category.id, ToEmail())
archive_entry = all_archives.get(category.in_archive, ToEmail())
- #add specific category moderators
+ #dont forget alaises
+ alias_id = _ALIAS_BY_CANONICAL.get(category.id)
+ if alias_id:
+ alias=CATEGORIES_ACTIVE[alias_id]
+ alias_cat_entry = all_cats.get(alias_id, ToEmail())
+ alias_archive_entry = all_archives.get(alias.in_archive, ToEmail())
+
+ # factory in email preferences
+ #priority: named category > alias category > named archive > alias archive
+ #each lower-priority group excludes anyone who opted out at a higher-priority level
+
+ #named category moderators
email.update(cat_entry.send_to)
reply_to.update(cat_entry.include_reply_to)
-
- #add archive mods unless they have specifically declined
- email.update(archive_entry.send_to - cat_entry.dont_send_to)
- reply_to.update(archive_entry.include_reply_to - cat_entry.dont_include_reply_to)
+ rolling_dont_email.update(cat_entry.dont_send_to)
+ rolling_dont_reply.update(cat_entry.dont_include_reply_to)
+
+ #alias category moderators
+ if alias_id:
+ email.update(alias_cat_entry.send_to - rolling_dont_email)
+ reply_to.update(alias_cat_entry.include_reply_to - rolling_dont_reply)
+ rolling_dont_email.update(alias_cat_entry.dont_send_to)
+ rolling_dont_reply.update(alias_cat_entry.dont_include_reply_to)
+
+ #named archive moderators
+ email.update(archive_entry.send_to - rolling_dont_email)
+ reply_to.update(archive_entry.include_reply_to - rolling_dont_reply)
+ rolling_dont_email.update(archive_entry.dont_send_to)
+ rolling_dont_reply.update(archive_entry.dont_include_reply_to)
+
+ #alias archive moderators
+ if alias_id:
+ email.update(alias_archive_entry.send_to - rolling_dont_email)
+ reply_to.update(alias_archive_entry.include_reply_to - rolling_dont_reply)
return email, reply_to
diff --git a/app/process.py b/app/process.py
index cfe6a22..5019d4c 100644
--- a/app/process.py
+++ b/app/process.py
@@ -1,6 +1,7 @@
"""processes notifications"""
import logging
import json
+from collections import Counter
from typing import Callable
from google.pubsub import ReceivedMessage
@@ -10,7 +11,7 @@
from app.config import settings
from app.email import send_email
from app.email_content import get_submission_info, render_email
-from app.schema import NotificationParams, SimplifiedNotification, ConsolidatedNotifications, EmailTask, NotificationType, CommentData, PromoteData, NewPropData, PropRespData, UserContact, SubEmailData
+from app.schema import NotificationParams, SimplifiedNotification, ConsolidatedNotifications, EmailTask, NotificationType, CommentData, PromoteData, NewPropData, PropRespData, CategoryRejectionData, UserContact, SubEmailData
from app.moderators import get_all_moderators, get_recipient_ids_for_categories, get_mod_emails
logger = logging.getLogger(__name__)
@@ -29,11 +30,13 @@ def _parse_message(payload)-> tuple[NotificationParams, SimplifiedNotification]:
data = PromoteData.model_validate(full_note.data)
case NotificationType.PROP_RESP:
data = PropRespData.model_validate(full_note.data)
+ case NotificationType.CATEGORY_REJECTION:
+ data = CategoryRejectionData.model_validate(full_note.data)
case _:
logger.error(f"unhandled action type: {full_note.action}, skipping message")
raise ValueError(f"unhandled action: {full_note.action}")
- simple_note=SimplifiedNotification(time=full_note.time, user_id=full_note.user_id, data=data)
+ simple_note=SimplifiedNotification(time=full_note.time, user_id=full_note.user_id, action=full_note.action, data=data)
return full_note, simple_note
def _convert_messages(messages: list[ReceivedMessage], ack_fn: Callable[[list[str]], None]) -> dict[int, ConsolidatedNotifications]:
@@ -113,7 +116,6 @@ def _build_email_tasks(all_notifications: dict[int, ConsolidatedNotifications])
logger.error(f"submission {sub_id}: no tapir_users row for moderator ids {missing}")
if not to_emails:
- logger.info(f"submission {sub_id}: no recipients after exclusions, skipping")
continue
#email data
@@ -131,13 +133,13 @@ def _send_email_tasks(
sub_infos: dict[int, SubEmailData],
ids_to_contact: dict[int, UserContact],
ack_fn: Callable[[list[str]], None],
-) -> None:
+) -> list[int]:
"""render and send emails, acking each submission only after its email sends"""
if settings.REDIRECT_EMAILS:
logger.info(f"REDIRECT_EMAILS active — all emails → {settings.REDIRECT_RECIPIENT}")
- sent = 0
+ sent_sub_ids: list[int] = []
for task in email_tasks:
sub = sub_infos.get(task.submission_id)
if sub is None:
@@ -165,11 +167,11 @@ def _send_email_tasks(
)
ack_fn(task.notifications.ack_ids) # ack on success or all-refused (terminal)
if accepted:
- sent += 1
+ sent_sub_ids.append(task.submission_id)
except Exception:
logger.exception(f"Failed to send email for submission {task.submission_id}, will redeliver")
- logger.info(f"Sent {sent}/{len(email_tasks)} email(s) to relay")
+ return sent_sub_ids
def process_messages(messages: list[ReceivedMessage], ack_fn: Callable[[list[str]], None]) -> None:
@@ -186,7 +188,7 @@ def process_messages(messages: list[ReceivedMessage], ack_fn: Callable[[list[str
return
if not all_notifications:
- logger.info("No valid notifications after parsing, nothing to send")
+ logger.warning("No valid notifications after parsing, nothing to send")
return
#determine who to email what
@@ -201,11 +203,11 @@ def process_messages(messages: list[ReceivedMessage], ack_fn: Callable[[list[str
skipped_sub_ids.append(sub_id)
if not email_tasks:
- logger.warning(f"No emails to send — submissions with no recipients: {sorted(skipped_sub_ids)}")
+ logger.warning(f"No emails to send — no recipients after exclusions for submissions: {sorted(skipped_sub_ids)}")
return
if skipped_sub_ids:
- logger.info(f"Skipped submissions (no recipients): {sorted(skipped_sub_ids)}")
+ logger.info(f"Skipped submissions (no recipients after exclusions): {sorted(skipped_sub_ids)}")
#fetch submission data — if batch query fails, skip all sends (will redeliver)
try:
@@ -215,6 +217,23 @@ def process_messages(messages: list[ReceivedMessage], ack_fn: Callable[[list[str
return
#send emails
- _send_email_tasks(email_tasks, sub_infos, ids_to_contact, ack_fn)
- logger.info(f"Processed {len(messages)} messages, built {len(email_tasks)} email task(s).")
- return
\ No newline at end of file
+ sent_sub_ids = _send_email_tasks(email_tasks, sub_infos, ids_to_contact, ack_fn)
+
+ #stat logging
+ action_counts = Counter(
+ change.action.value
+ for n in all_notifications.values()
+ for change in n.changes
+ )
+ action_summary = ", ".join(f"{k}: {v}" for k, v in sorted(action_counts.items()))
+ total_actions = sum(action_counts.values())
+
+ if len(sent_sub_ids) > 20:
+ sub_summary = f"over 20 submissions"
+ else:
+ sub_summary = f"submission ids: {sorted(sent_sub_ids)}" if sent_sub_ids else "none"
+
+ logger.info(
+ f"Stats: {total_actions} actions ({action_summary}) | "
+ f"{len(sent_sub_ids)}/{len(email_tasks)} emails accepted by relay ({sub_summary})"
+ )
\ No newline at end of file
diff --git a/app/schema.py b/app/schema.py
index f19f179..705204a 100644
--- a/app/schema.py
+++ b/app/schema.py
@@ -11,7 +11,7 @@ class NotificationType(str, Enum):
PROP_RESP = 'proposal-response'
NEW_PROP = 'new-proposal'
PROMOTE = 'category-promotion'
- #TODO should rejections eventually send emails?
+ CATEGORY_REJECTION = 'category-rejection'
#the shape the data comes in the pubsub message
class NotificationParams(BaseModel):
@@ -37,11 +37,17 @@ class PromoteData(BaseModel):
promotion_type: Literal["primary", "secondary"]
category_change: str
+class CategoryRejectionData(BaseModel):
+ category: str
+ rejection_type: Literal["reject", "accept_secondary", "cross_submission"]
+ category_change: str
+
class SimplifiedNotification(BaseModel):
time: datetime
user_id: int
- data: Union[CommentData, PromoteData, PropRespData, NewPropData]
+ action: NotificationType
+ data: Union[CommentData, PromoteData, PropRespData, NewPropData, CategoryRejectionData]
@dataclass
class UserContact:
diff --git a/app/templates/category_rejection.py b/app/templates/category_rejection.py
new file mode 100644
index 0000000..01408db
--- /dev/null
+++ b/app/templates/category_rejection.py
@@ -0,0 +1,24 @@
+from app.schema import SimplifiedNotification, CategoryRejectionData
+from app.email_content import _fmt_time
+
+_REJECTION_LABELS = {
+ "reject": "removed from submission",
+ "accept_secondary": "demoted to secondary",
+ "cross_submission": "removed from cross submission",
+}
+
+
+def render_category_rejection_block(change: SimplifiedNotification, user_name: str) -> tuple[str, str]:
+ data: CategoryRejectionData = change.data
+ when = _fmt_time(change.time)
+ label = _REJECTION_LABELS.get(data.rejection_type, data.rejection_type)
+ text = (
+ f"[{when}] {user_name} rejected {data.category} ({label}):\n"
+ f" Change: {data.category_change}\n"
+ )
+ html_out = (
+ f"
[{when}] {user_name} "
+ f"rejected {data.category} ({label})
\n"
+ f"Change: {data.category_change}
\n"
+ )
+ return text, html_out
diff --git a/poetry.lock b/poetry.lock
index adae212..b501020 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,4 +1,4 @@
-# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
+# This file is automatically @generated by Poetry 2.4.1 and should not be changed by hand.
[[package]]
name = "annotated-types"
@@ -6,6 +6,7 @@ version = "0.7.0"
description = "Reusable constraint types to use with typing.Annotated"
optional = false
python-versions = ">=3.8"
+groups = ["main"]
files = [
{file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"},
{file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"},
@@ -17,6 +18,7 @@ version = "1.0.1"
description = "Common code for arXiv Cloud"
optional = false
python-versions = "^3.11"
+groups = ["main"]
files = []
develop = false
@@ -61,20 +63,21 @@ resolved_reference = "4c53f9bf6b77cceeb8ec71942e53a5b32ceb7cad"
[[package]]
name = "bleach"
-version = "6.3.0"
+version = "6.4.0"
description = "An easy safelist-based HTML-sanitizing tool."
optional = false
python-versions = ">=3.10"
+groups = ["main"]
files = [
- {file = "bleach-6.3.0-py3-none-any.whl", hash = "sha256:fe10ec77c93ddf3d13a73b035abaac7a9f5e436513864ccdad516693213c65d6"},
- {file = "bleach-6.3.0.tar.gz", hash = "sha256:6f3b91b1c0a02bb9a78b5a454c92506aa0fdf197e1d5e114d2e00c6f64306d22"},
+ {file = "bleach-6.4.0-py3-none-any.whl", hash = "sha256:4b6b6a54fff2e69a3dde9d21cc6301220bee3c3cb792187d11403fd795031081"},
+ {file = "bleach-6.4.0.tar.gz", hash = "sha256:4202482733d85cedd04e59fcb2f89f4e4c7c385a78d3c3c23c30446843a37452"},
]
[package.dependencies]
webencodings = "*"
[package.extras]
-css = ["tinycss2 (>=1.1.0,<1.5)"]
+css = ["tinycss2 (>=1.1.0)"]
[[package]]
name = "blinker"
@@ -82,6 +85,7 @@ version = "1.9.0"
description = "Fast, simple object-to-object and broadcast signaling"
optional = false
python-versions = ">=3.9"
+groups = ["main"]
files = [
{file = "blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc"},
{file = "blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf"},
@@ -93,6 +97,7 @@ version = "1.43.1"
description = "The AWS SDK for Python"
optional = false
python-versions = ">=3.10"
+groups = ["main"]
files = [
{file = "boto3-1.43.1-py3-none-any.whl", hash = "sha256:3840bf0345b9aefcc5915176a19d227f63cfba7778c65e6e52d61c6ea0a10fdc"},
{file = "boto3-1.43.1.tar.gz", hash = "sha256:9e4f85a7884797ff0f52c257094730ed228aaa07fa8134775ff8f86909cf4f2a"},
@@ -112,6 +117,7 @@ version = "1.43.1"
description = "Low-level, data-driven core of boto 3."
optional = false
python-versions = ">=3.10"
+groups = ["main"]
files = [
{file = "botocore-1.43.1-py3-none-any.whl", hash = "sha256:955edc6a398b9c4100cf0d5a31433fdba3835500bf38c1ef171e6e75f4b477d2"},
{file = "botocore-1.43.1.tar.gz", hash = "sha256:270d6357d662550fdb84973ec247e02bece0b6283d90bf37319c7753515336e4"},
@@ -131,6 +137,7 @@ version = "2026.4.22"
description = "Python package for providing Mozilla's CA Bundle."
optional = false
python-versions = ">=3.7"
+groups = ["main"]
files = [
{file = "certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a"},
{file = "certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580"},
@@ -142,6 +149,8 @@ version = "2.0.0"
description = "Foreign Function Interface for Python calling C code."
optional = false
python-versions = ">=3.9"
+groups = ["main"]
+markers = "platform_python_implementation != \"PyPy\""
files = [
{file = "cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44"},
{file = "cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49"},
@@ -238,6 +247,7 @@ version = "3.4.7"
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
optional = false
python-versions = ">=3.7"
+groups = ["main"]
files = [
{file = "charset_normalizer-3.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cdd68a1fb318e290a2077696b7eb7a21a49163c455979c639bf5a5dcdc46617d"},
{file = "charset_normalizer-3.4.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e17b8d5d6a8c47c85e68ca8379def1303fd360c3e22093a807cd34a71cd082b8"},
@@ -376,6 +386,7 @@ version = "8.3.3"
description = "Composable command line interface toolkit"
optional = false
python-versions = ">=3.10"
+groups = ["main"]
files = [
{file = "click-8.3.3-py3-none-any.whl", hash = "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613"},
{file = "click-8.3.3.tar.gz", hash = "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2"},
@@ -390,10 +401,12 @@ version = "0.4.6"
description = "Cross-platform colored terminal text."
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
+groups = ["main", "dev"]
files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
+markers = {main = "platform_system == \"Windows\"", dev = "sys_platform == \"win32\""}
[[package]]
name = "coverage"
@@ -401,6 +414,7 @@ version = "7.13.5"
description = "Code coverage measurement for Python"
optional = false
python-versions = ">=3.10"
+groups = ["dev"]
files = [
{file = "coverage-7.13.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0723d2c96324561b9aa76fb982406e11d93cdb388a7a7da2b16e04719cf7ca5"},
{file = "coverage-7.13.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52f444e86475992506b32d4e5ca55c24fc88d73bcbda0e9745095b28ef4dc0cf"},
@@ -511,68 +525,69 @@ files = [
]
[package.extras]
-toml = ["tomli"]
+toml = ["tomli ; python_full_version <= \"3.11.0a6\""]
[[package]]
name = "cryptography"
-version = "47.0.0"
+version = "48.0.1"
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
optional = false
-python-versions = "!=3.9.0,!=3.9.1,>=3.8"
-files = [
- {file = "cryptography-47.0.0-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:160ad728f128972d362e714054f6ba0067cab7fb350c5202a9ae8ae4ce3ef1a0"},
- {file = "cryptography-47.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b9a8943e359b7615db1a3ba587994618e094ff3d6fa5a390c73d079ce18b3973"},
- {file = "cryptography-47.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f5c15764f261394b22aef6b00252f5195f46f2ca300bec57149474e2538b31f8"},
- {file = "cryptography-47.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:9c59ab0e0fa3a180a5a9c59f3a5abe3ef90d474bc56d7fadfbe80359491b615b"},
- {file = "cryptography-47.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:34b4358b925a5ea3e14384ca781a2c0ef7ac219b57bb9eacc4457078e2b19f92"},
- {file = "cryptography-47.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0024b87d47ae2399165a6bfb20d24888881eeab83ae2566d62467c5ff0030ce7"},
- {file = "cryptography-47.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:1e47422b5557bb82d3fff997e8d92cff4e28b9789576984f08c248d2b3535d93"},
- {file = "cryptography-47.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:6f29f36582e6151d9686235e586dd35bb67491f024767d10b842e520dc6a07ac"},
- {file = "cryptography-47.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:a9b761f012a943b7de0e828843c5688d0de94a0578d44d6c85a1bae32f87791f"},
- {file = "cryptography-47.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4e1de79e047e25d6e9f8cea71c86b4a53aced64134f0f003bbcbf3655fd172c8"},
- {file = "cryptography-47.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef6b3634087f18d2155b1e8ce264e5345a753da2c5fa9815e7d41315c90f8318"},
- {file = "cryptography-47.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:11dbb9f50a0f1bb9757b3d8c27c1101780efb8f0bdecfb12439c22a74d64c001"},
- {file = "cryptography-47.0.0-cp311-abi3-win32.whl", hash = "sha256:7fda2f02c9015db3f42bb8a22324a454516ed10a8c29ca6ece6cdbb5efe2a203"},
- {file = "cryptography-47.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:f5c3296dab66202f1b18a91fa266be93d6aa0c2806ea3d67762c69f60adc71aa"},
- {file = "cryptography-47.0.0-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:be12cb6a204f77ed968bcefe68086eb061695b540a3dd05edac507a3111b25f0"},
- {file = "cryptography-47.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2ebd84adf0728c039a3be2700289378e1c164afc6748df1a5ed456767bef9ba7"},
- {file = "cryptography-47.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7f68d6fbc7fbbcfb0939fea72c3b96a9f9a6edfc0e1b1d29778a2066030418b1"},
- {file = "cryptography-47.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:6651d32eff255423503aa276739da98c30f26c40cbeffcc6048e0d54ef704c0c"},
- {file = "cryptography-47.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:3fb8fa48075fad7193f2e5496135c6a76ac4b2aa5a38433df0a539296b377829"},
- {file = "cryptography-47.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:11438c7518132d95f354fa01a4aa2f806d172a061a7bed18cf18cbdacdb204d7"},
- {file = "cryptography-47.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:8c1a736bbb3288005796c3f7ccb9453360d7fed483b13b9f468aea5171432923"},
- {file = "cryptography-47.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:f1557695e5c2b86e204f6ce9470497848634100787935ab7adc5397c54abd7ab"},
- {file = "cryptography-47.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:f9a034b642b960767fb343766ae5ba6ad653f2e890ddd82955aef288ffea8736"},
- {file = "cryptography-47.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:b1c76fca783aa7698eb21eb14f9c4aa09452248ee54a627d125025a43f83e7a7"},
- {file = "cryptography-47.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4f7722c97826770bab8ae92959a2e7b20a5e9e9bf4deae68fd86c3ca457bab52"},
- {file = "cryptography-47.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:09f6d7bf6724f8db8b32f11eccf23efc8e759924bc5603800335cf8859a3ddbd"},
- {file = "cryptography-47.0.0-cp314-cp314t-win32.whl", hash = "sha256:6eebcaf0df1d21ce1f90605c9b432dd2c4f4ab665ac29a40d5e3fc68f51b5e63"},
- {file = "cryptography-47.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:51c9313e90bd1690ec5a75ed047c27c0b8e6c570029712943d6116ef9a90620b"},
- {file = "cryptography-47.0.0-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:14432c8a9bcb37009784f9594a62fae211a2ae9543e96c92b2a8e4c3cd5cd0c4"},
- {file = "cryptography-47.0.0-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:07efe86201817e7d3c18781ca9770bc0db04e1e48c994be384e4602bc38f8f27"},
- {file = "cryptography-47.0.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b45761c6ec22b7c726d6a829558777e32d0f1c8be7c3f3480f9c912d5ee8a10"},
- {file = "cryptography-47.0.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:edd4da498015da5b9f26d38d3bfc2e90257bfa9cbed1f6767c282a0025ae649b"},
- {file = "cryptography-47.0.0-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9af828c0d5a65c70ec729cd7495a4bf1a67ecb66417b8f02ff125ab8a6326a74"},
- {file = "cryptography-47.0.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:256d07c78a04d6b276f5df935a9923275f53bd1522f214447fdf365494e2d515"},
- {file = "cryptography-47.0.0-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:5d0e362ff51041b0c0d219cc7d6924d7b8996f57ce5712bdcef71eb3c65a59cc"},
- {file = "cryptography-47.0.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:1581aef4219f7ca2849d0250edaa3866212fb74bf5667284f46aa92f9e65c1ca"},
- {file = "cryptography-47.0.0-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:a49a3eb5341b9503fa3000a9a0db033161db90d47285291f53c2a9d2cd1b7f76"},
- {file = "cryptography-47.0.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2207a498b03275d0051589e326b79d4cf59985c99031b05bb292ac52631c37fe"},
- {file = "cryptography-47.0.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7a02675e2fabd0c0fc04c868b8781863cbf1967691543c22f5470500ff840b31"},
- {file = "cryptography-47.0.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80887c5cbd1774683cb126f0ab4184567f080071d5acf62205acb354b4b753b7"},
- {file = "cryptography-47.0.0-cp38-abi3-win32.whl", hash = "sha256:ed67ea4e0cfb5faa5bc7ecb6e2b8838f3807a03758eec239d6c21c8769355310"},
- {file = "cryptography-47.0.0-cp38-abi3-win_amd64.whl", hash = "sha256:835d2d7f47cdc53b3224e90810fb1d36ca94ea29cc1801fb4c1bc43876735769"},
- {file = "cryptography-47.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:7f1207974a904e005f762869996cf620e9bf79ecb4622f148550bb48e0eb35a7"},
- {file = "cryptography-47.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:1a405c08857258c11016777e11c02bacbe7ef596faf259305d282272a3a05cbe"},
- {file = "cryptography-47.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:20fdbe3e38fb67c385d233c89371fa27f9909f6ebca1cecc20c13518dae65475"},
- {file = "cryptography-47.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:f7db373287273d8af1414cf95dc4118b13ffdc62be521997b0f2b270771fef50"},
- {file = "cryptography-47.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:9fe6b7c64926c765f9dff301f9c1b867febcda5768868ca084e18589113732ab"},
- {file = "cryptography-47.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:cffbba3392df0fa8629bb7f43454ee2925059ee158e23c54620b9063912b86c8"},
- {file = "cryptography-47.0.0.tar.gz", hash = "sha256:9f8e55fe4e63613a5e1cc5819030f27b97742d720203a087802ce4ce9ceb52bb"},
+python-versions = "!=3.9.0,!=3.9.1,>=3.9"
+groups = ["main"]
+files = [
+ {file = "cryptography-48.0.1-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:3e4a1a3232eef2e6c732827d5722db29a0cc8b27af2a4d865b094cf954be9ca1"},
+ {file = "cryptography-48.0.1-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:32143b24adb918f078134e1e230f1eb8cc04886b92c28b5f0041aaf3e5699225"},
+ {file = "cryptography-48.0.1-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0d27a5696721ef7a672b8c810f6aded391058e0b9486e63e6d93baf765da691"},
+ {file = "cryptography-48.0.1-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:eb86ce1af36fe65041b6db9a8bb064ee621a7e5fded0f80d475ec243477cd242"},
+ {file = "cryptography-48.0.1-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:b024e784ad6c077ee0147b35ea9cbfc1e34e1fd4c1dcca214c2794d73a12df08"},
+ {file = "cryptography-48.0.1-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3752f2dbc8f07a30aad2932c986cea495b03bb554887828225da104f732852b6"},
+ {file = "cryptography-48.0.1-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:bd81490cd5801d755cf97bb68ac191f14b708470b1c7cf4580f669b9c9264cd8"},
+ {file = "cryptography-48.0.1-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:66fd0771e7b9c6dcd44cf1120690d2338d16d72795cf40cae2786a39eba65429"},
+ {file = "cryptography-48.0.1-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:3fd2ca57062b241c856670b073487d2e86c4637937ca5601e48f97bf8e11fc8f"},
+ {file = "cryptography-48.0.1-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:0ee6ea481db1ab889cba043ec1eda17bb9c1ea79db6722f779c3667f9f70322f"},
+ {file = "cryptography-48.0.1-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f2ceef93cb096aa3c4cc4b5c94ca6131f9196d28c64d6111533402a9b2054d41"},
+ {file = "cryptography-48.0.1-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9bd3f92d76217892b15df84ca256c2c113d386fdda7a7d8691aeeced976507c6"},
+ {file = "cryptography-48.0.1-cp311-abi3-win32.whl", hash = "sha256:b9a32b876490d66c8bcc9963ef220199569748434ab01a9d6aaeabf88e7f5158"},
+ {file = "cryptography-48.0.1-cp311-abi3-win_amd64.whl", hash = "sha256:39489bfca54c7a1f6b297efcd8bc608ab92d16c4ca631b0cad4da46724588b24"},
+ {file = "cryptography-48.0.1-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:f817adc181390bd54f2f700107a7419040fb7c1bdf2fc26f36551a06a68c3345"},
+ {file = "cryptography-48.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d5d30989c6917b478b5817902e85fddaea2261efa8648383d965381ccb9e1ac4"},
+ {file = "cryptography-48.0.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:df637c05205ea7c1d7fbcbe54bbfea648a52951155f997af13d895d0ecc96991"},
+ {file = "cryptography-48.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:869c3b8a53bfe27147832df48b32adadf558249d50e76cb3769d40e986b13265"},
+ {file = "cryptography-48.0.1-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:e361afba8918070d376df76f408a4f67fec0ee9cff81a99e48fe9a233ef59e17"},
+ {file = "cryptography-48.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:d069066deead00ac7f090be101be875a06855908f7ec004c27b8fefb4acfb411"},
+ {file = "cryptography-48.0.1-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:09f73a725d582cef64b91281a322cd798d14a33b2b6f2b7ad9531dc336d84c02"},
+ {file = "cryptography-48.0.1-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:15254441469dd6bf027039453288e2072124f8b6603563f5d759e1c9b69273fa"},
+ {file = "cryptography-48.0.1-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:8ace4507d1e6533c125f4fac754f8bb8b6a74c08e92179dabd7e16571a3efbf3"},
+ {file = "cryptography-48.0.1-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:b4e391975f038e66432328639620a4aff2d307513b004f1ca06d6225bced815c"},
+ {file = "cryptography-48.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42fcd8e26fe555d9b3577a135f5091fefa0aa4e99129c23fb56787a1bd4ada72"},
+ {file = "cryptography-48.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c1400da5e32a43253392277eac7490a60e497d810a63dd5608d71bbd7af507c9"},
+ {file = "cryptography-48.0.1-cp314-cp314t-win32.whl", hash = "sha256:0df56b056bc17c1b7d6821dfa65216e62bd232d8ab05eb3db44e71d235651471"},
+ {file = "cryptography-48.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:9de21387aa95e2a895823d0745b430bed4f33503ba9ab5e0b5311f33e37d66d2"},
+ {file = "cryptography-48.0.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:4fdc69f8e4316bcf0c8c8ec1f26f285d12e8142d88d96c876a59a03be3f6ae67"},
+ {file = "cryptography-48.0.1-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48fe40804d4caa2288f24e70ca8c64c42dd826da0ad7e4f1b41b2128d679e6c8"},
+ {file = "cryptography-48.0.1-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:86be3b1b0b6bf09482fb50a979c508d2950ed95f5621ec77f4e385962006b83a"},
+ {file = "cryptography-48.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:4ab0a343c807bbcd90c971cd1ecf072937cd01847a9e002bef88fb47ac6be577"},
+ {file = "cryptography-48.0.1-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9621de99d2da096006b629979efd8ae7eb2d8b822488d0c89ee4000c306c59b1"},
+ {file = "cryptography-48.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:88c852a0ae366e262e5a1744b685e6a433dc8788dd2a277e418bf4904203609d"},
+ {file = "cryptography-48.0.1-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:43c5835e2cb98c8733d86f57d6fc879b613f5c3478607281c3e36daffc6dd8a6"},
+ {file = "cryptography-48.0.1-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:fe0180af5bf9236518a087e35bf2d9a347d5f5f51e63c579d683ddff424e3d46"},
+ {file = "cryptography-48.0.1-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:b7a2d1a937a738a881737cec135a38bb61470589b17515b9f73f571d0ae10401"},
+ {file = "cryptography-48.0.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:b74ca3b8e5ecdd833bf6a002ca41b4793bb27fb8f1c06ffaf2643c9e9140e31b"},
+ {file = "cryptography-48.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2c37f2461406063b417837f5f3daab668652acd82423efcd7f0a9f04be972de1"},
+ {file = "cryptography-48.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:86fe77abb1bd87afb251d4d02ada7ecf53a32cee9b67d976abb2e45a13297475"},
+ {file = "cryptography-48.0.1-cp39-abi3-win32.whl", hash = "sha256:6b2c0c3e6ccf3ade7750f836ef3ee36eea250cc467d45c256895573ac08cc6f1"},
+ {file = "cryptography-48.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:9a49ca6c81417f6a5edb50375a60cccdd70fa0a91a5211829dbea74eba94d2ac"},
+ {file = "cryptography-48.0.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:08a597acce1ff37f347400087776599e2348a3a8bc53b44120e463cd274efe4a"},
+ {file = "cryptography-48.0.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:735824ec41b7f74a7c45fb1591349333e4c696cb6c044e5f46356e560143e4cd"},
+ {file = "cryptography-48.0.1-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:92a46e1d638daa264ba2971c0b0489c9409787943efae4d60ffda3d091ef832c"},
+ {file = "cryptography-48.0.1-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:7e234ac052af99f2700826a5c29ea99d9c1b1f80341cde62d11c8154dc8e0bd9"},
+ {file = "cryptography-48.0.1-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:33842cf0888951cef5bc7ac724ab844a42044c1727b967b7f8997289a0464f92"},
+ {file = "cryptography-48.0.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6184ca7b174f28d7c703f1290d4b297217c45355f77a98f67e9b7f14549ac54a"},
+ {file = "cryptography-48.0.1.tar.gz", hash = "sha256:266f4ee051abb2f725b74ef8072b521ce1feacf685a3364fa6a6b45548db791a"},
]
[package.dependencies]
-cffi = {version = ">=2.0.0", markers = "python_full_version >= \"3.9\" and platform_python_implementation != \"PyPy\""}
+cffi = {version = ">=2.0.0", markers = "platform_python_implementation != \"PyPy\""}
[package.extras]
ssh = ["bcrypt (>=3.1.5)"]
@@ -583,6 +598,7 @@ version = "5.2.1"
description = "Decorators for Humans"
optional = false
python-versions = ">=3.8"
+groups = ["main"]
files = [
{file = "decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a"},
{file = "decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360"},
@@ -594,6 +610,7 @@ version = "13.1.0"
description = "A Python Fastly API client library"
optional = false
python-versions = ">=3.6"
+groups = ["main"]
files = [
{file = "fastly-13.1.0-py3-none-any.whl", hash = "sha256:f1e24761702bb44dca558db27bf23e7e1e90b9da3c680794d5fabf8c484b8c38"},
{file = "fastly-13.1.0.tar.gz", hash = "sha256:2afa5140ac20058dee38e002bafa36becec2767d930f3de98b8aae94513cd5da"},
@@ -609,6 +626,7 @@ version = "0.6.0"
description = "A library for automatically generating command line interfaces."
optional = false
python-versions = "*"
+groups = ["main"]
files = [
{file = "fire-0.6.0.tar.gz", hash = "sha256:54ec5b996ecdd3c0309c800324a0703d6da512241bc73b553db959d98de0aa66"},
]
@@ -623,6 +641,7 @@ version = "3.0.3"
description = "A simple framework for building complex web applications."
optional = false
python-versions = ">=3.8"
+groups = ["main"]
files = [
{file = "flask-3.0.3-py3-none-any.whl", hash = "sha256:34e815dfaa43340d1d15a5c3a02b8476004037eb4840b34910c6e21679d288f3"},
{file = "flask-3.0.3.tar.gz", hash = "sha256:ceb27b0af3823ea2737928a4d99d125a06175b8512c445cbd9a9ce200ef76842"},
@@ -645,6 +664,7 @@ version = "0.3.3"
description = "Seamlessly serve the static files of your Flask app from Amazon S3"
optional = false
python-versions = "*"
+groups = ["main"]
files = [
{file = "Flask-S3-0.3.3.tar.gz", hash = "sha256:1d49061d4b78759df763358a901f4ed32bb43f672c9f8e1ec7226793f6ae0fd2"},
{file = "Flask_S3-0.3.3-py3-none-any.whl", hash = "sha256:23cbbb1db4c29c313455dbe16f25be078d6318f0a11abcbb610f99e116945b62"},
@@ -661,6 +681,7 @@ version = "2.30.3"
description = "Google API client core library"
optional = false
python-versions = ">=3.9"
+groups = ["main"]
files = [
{file = "google_api_core-2.30.3-py3-none-any.whl", hash = "sha256:a85761ba72c444dad5d611c2220633480b2b6be2521eca69cca2dbb3ffd6bfe8"},
{file = "google_api_core-2.30.3.tar.gz", hash = "sha256:e601a37f148585319b26db36e219df68c5d07b6382cff2d580e83404e44d641b"},
@@ -670,23 +691,23 @@ files = [
google-auth = ">=2.14.1,<3.0.0"
googleapis-common-protos = ">=1.63.2,<2.0.0"
grpcio = [
+ {version = ">=1.49.1,<2.0.0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""},
{version = ">=1.75.1,<2.0.0", optional = true, markers = "python_version >= \"3.14\" and extra == \"grpc\""},
- {version = ">=1.49.1,<2.0.0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\" and python_version < \"3.14\""},
]
grpcio-status = [
+ {version = ">=1.49.1,<2.0.0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""},
{version = ">=1.75.1,<2.0.0", optional = true, markers = "python_version >= \"3.14\" and extra == \"grpc\""},
- {version = ">=1.49.1,<2.0.0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\" and python_version < \"3.14\""},
]
proto-plus = [
+ {version = ">=1.22.3,<2.0.0"},
{version = ">=1.25.0,<2.0.0", markers = "python_version >= \"3.13\""},
- {version = ">=1.22.3,<2.0.0", markers = "python_version < \"3.13\""},
]
protobuf = ">=4.25.8,<8.0.0"
requests = ">=2.20.0,<3.0.0"
[package.extras]
async-rest = ["google-auth[aiohttp] (>=2.35.0,<3.0.0)"]
-grpc = ["grpcio (>=1.33.2,<2.0.0)", "grpcio (>=1.49.1,<2.0.0)", "grpcio (>=1.75.1,<2.0.0)", "grpcio-status (>=1.33.2,<2.0.0)", "grpcio-status (>=1.49.1,<2.0.0)", "grpcio-status (>=1.75.1,<2.0.0)"]
+grpc = ["grpcio (>=1.33.2,<2.0.0)", "grpcio (>=1.49.1,<2.0.0) ; python_version >= \"3.11\"", "grpcio (>=1.75.1,<2.0.0) ; python_version >= \"3.14\"", "grpcio-status (>=1.33.2,<2.0.0)", "grpcio-status (>=1.49.1,<2.0.0) ; python_version >= \"3.11\"", "grpcio-status (>=1.75.1,<2.0.0) ; python_version >= \"3.14\""]
[[package]]
name = "google-auth"
@@ -694,6 +715,7 @@ version = "2.50.0"
description = "Google Authentication Library"
optional = false
python-versions = ">=3.8"
+groups = ["main"]
files = [
{file = "google_auth-2.50.0-py3-none-any.whl", hash = "sha256:04382175e28b94f49694977f0a792688b59a668def1499e9d8de996dc9ce5b15"},
{file = "google_auth-2.50.0.tar.gz", hash = "sha256:f35eafb191195328e8ce10a7883970877e7aeb49c2bfaa54aa0e394316d353d0"},
@@ -721,6 +743,7 @@ version = "1.9.0"
description = "Google Cloud Appengine Logging API client library"
optional = false
python-versions = ">=3.9"
+groups = ["main"]
files = [
{file = "google_cloud_appengine_logging-1.9.0-py3-none-any.whl", hash = "sha256:bbf3a7e4dc171678f7f481259d1f68c3ae7d337530f1f2361f8a0b214dbcfe36"},
{file = "google_cloud_appengine_logging-1.9.0.tar.gz", hash = "sha256:ff397f0bbc1485f979ab45767c38e0f676c9598c97c384f7412216e6ea22f805"},
@@ -730,12 +753,12 @@ files = [
google-api-core = {version = ">=2.11.0,<3.0.0", extras = ["grpc"]}
google-auth = ">=2.14.1,<2.24.0 || >2.24.0,<2.25.0 || >2.25.0,<3.0.0"
grpcio = [
+ {version = ">=1.33.2,<2.0.0"},
{version = ">=1.75.1,<2.0.0", markers = "python_version >= \"3.14\""},
- {version = ">=1.33.2,<2.0.0", markers = "python_version < \"3.14\""},
]
proto-plus = [
+ {version = ">=1.22.3,<2.0.0"},
{version = ">=1.25.0,<2.0.0", markers = "python_version >= \"3.13\""},
- {version = ">=1.22.3,<2.0.0", markers = "python_version < \"3.13\""},
]
protobuf = ">=4.25.8,<8.0.0"
@@ -745,6 +768,7 @@ version = "0.5.0"
description = "Google Cloud Audit Protos"
optional = false
python-versions = ">=3.9"
+groups = ["main"]
files = [
{file = "google_cloud_audit_log-0.5.0-py3-none-any.whl", hash = "sha256:3f4632f25bf67446fa9085c52868f3cb42fb1afbab9489ba8978e30991afc79f"},
{file = "google_cloud_audit_log-0.5.0.tar.gz", hash = "sha256:3b32d5e77db634c46fbd6c5e01f5bda836f420dfbb21d730501c75e9fab4e4a4"},
@@ -760,6 +784,7 @@ version = "2.5.1"
description = "Google Cloud API client core library"
optional = false
python-versions = ">=3.9"
+groups = ["main"]
files = [
{file = "google_cloud_core-2.5.1-py3-none-any.whl", hash = "sha256:ea62cdf502c20e3e14be8a32c05ed02113d7bef454e40ff3fab6fe1ec9f1f4e7"},
{file = "google_cloud_core-2.5.1.tar.gz", hash = "sha256:3dc94bdec9d05a31d9f355045ed0f369fbc0d8c665076c734f065d729800f811"},
@@ -770,7 +795,7 @@ google-api-core = ">=2.11.0,<3.0.0"
google-auth = ">=2.14.1,<2.24.0 || >2.24.0,<2.25.0 || >2.25.0,<3.0.0"
[package.extras]
-grpc = ["grpcio (>=1.38.0,<2.0.0)", "grpcio (>=1.75.1,<2.0.0)", "grpcio-status (>=1.38.0,<2.0.0)"]
+grpc = ["grpcio (>=1.38.0,<2.0.0) ; python_version < \"3.14\"", "grpcio (>=1.75.1,<2.0.0) ; python_version >= \"3.14\"", "grpcio-status (>=1.38.0,<2.0.0)"]
[[package]]
name = "google-cloud-logging"
@@ -778,6 +803,7 @@ version = "3.15.0"
description = "Google Cloud Logging API client library"
optional = false
python-versions = ">=3.9"
+groups = ["main"]
files = [
{file = "google_cloud_logging-3.15.0-py3-none-any.whl", hash = "sha256:7dcc67434c4e7181510c133d5ac8fd4ce60c23fa4158661f67e54bf440c32450"},
{file = "google_cloud_logging-3.15.0.tar.gz", hash = "sha256:72168a1e98bbfc27c75f0b8f630a7f5d786065f3f1f7e9e53d2d787a03693a4a"},
@@ -791,13 +817,13 @@ google-cloud-audit-log = ">=0.3.1,<1.0.0"
google-cloud-core = ">=2.0.0,<3.0.0"
grpc-google-iam-v1 = ">=0.12.4,<1.0.0"
grpcio = [
- {version = ">=1.75.1,<2.0.0", markers = "python_version >= \"3.14\""},
{version = ">=1.33.2,<2.0.0", markers = "python_version < \"3.14\""},
+ {version = ">=1.75.1,<2.0.0", markers = "python_version >= \"3.14\""},
]
opentelemetry-api = ">=1.9.0"
proto-plus = [
- {version = ">=1.25.0,<2.0.0", markers = "python_version >= \"3.13\""},
{version = ">=1.22.3,<2.0.0", markers = "python_version < \"3.13\""},
+ {version = ">=1.25.0,<2.0.0", markers = "python_version >= \"3.13\""},
]
protobuf = ">=4.25.8,<8.0.0"
@@ -807,6 +833,7 @@ version = "2.30.0"
description = "Google Cloud Monitoring API client library"
optional = false
python-versions = ">=3.9"
+groups = ["main"]
files = [
{file = "google_cloud_monitoring-2.30.0-py3-none-any.whl", hash = "sha256:2729f3b88a4798b7757b1d9d31b6cb562bb3544e8173765e4e5cd44d8685b1ed"},
{file = "google_cloud_monitoring-2.30.0.tar.gz", hash = "sha256:a9530aa9aa246c490810dfa7be32d67e8340d19108acc99cbc02d1ed494fba76"},
@@ -816,12 +843,12 @@ files = [
google-api-core = {version = ">=2.11.0,<3.0.0", extras = ["grpc"]}
google-auth = ">=2.14.1,<2.24.0 || >2.24.0,<2.25.0 || >2.25.0,<3.0.0"
grpcio = [
+ {version = ">=1.33.2,<2.0.0"},
{version = ">=1.75.1,<2.0.0", markers = "python_version >= \"3.14\""},
- {version = ">=1.33.2,<2.0.0", markers = "python_version < \"3.14\""},
]
proto-plus = [
+ {version = ">=1.22.3,<2.0.0"},
{version = ">=1.25.0,<2.0.0", markers = "python_version >= \"3.13\""},
- {version = ">=1.22.3,<2.0.0", markers = "python_version < \"3.13\""},
]
protobuf = ">=4.25.8,<8.0.0"
@@ -834,6 +861,7 @@ version = "2.37.0"
description = "Google Cloud Pub/Sub API client library"
optional = false
python-versions = ">=3.9"
+groups = ["main"]
files = [
{file = "google_cloud_pubsub-2.37.0-py3-none-any.whl", hash = "sha256:dd912422cf66e4ffb423b0d5391ca81bdfa408eb0f21f57adecdb6fb3b1e0bb1"},
{file = "google_cloud_pubsub-2.37.0.tar.gz", hash = "sha256:7c5ba9beb5236e2b83c091dd6171423dc7d6d0e989391bd09f60dbd242b29f10"},
@@ -848,8 +876,8 @@ grpcio-status = ">=1.33.2"
opentelemetry-api = ">=1.27.0"
opentelemetry-sdk = ">=1.27.0"
proto-plus = [
+ {version = ">=1.22.3,<2.0.0"},
{version = ">=1.25.0,<2.0.0", markers = "python_version >= \"3.13\""},
- {version = ">=1.22.3,<2.0.0", markers = "python_version < \"3.13\""},
]
protobuf = ">=4.25.8,<8.0.0"
@@ -862,6 +890,7 @@ version = "3.10.1"
description = "Google Cloud Storage API client library"
optional = false
python-versions = ">=3.10"
+groups = ["main"]
files = [
{file = "google_cloud_storage-3.10.1-py3-none-any.whl", hash = "sha256:a72f656759b7b99bda700f901adcb3425a828d4a29f911bc26b3ea79c5b1217f"},
{file = "google_cloud_storage-3.10.1.tar.gz", hash = "sha256:97db9aa4460727982040edd2bd13ff3d5e2260b5331ad22895802da1fc2a5286"},
@@ -876,7 +905,7 @@ google-resumable-media = ">=2.7.2,<3.0.0"
requests = ">=2.22.0,<3.0.0"
[package.extras]
-grpc = ["google-api-core[grpc] (>=2.27.0,<3.0.0)", "grpc-google-iam-v1 (>=0.14.0,<1.0.0)", "grpcio (>=1.33.2,<2.0.0)", "grpcio (>=1.75.1,<2.0.0)", "grpcio-status (>=1.76.0,<2.0.0)", "proto-plus (>=1.22.3,<2.0.0)", "proto-plus (>=1.25.0,<2.0.0)", "protobuf (>=3.20.2,!=4.21.0,!=4.21.1,!=4.21.2,!=4.21.3,!=4.21.4,!=4.21.5,<7.0.0)"]
+grpc = ["google-api-core[grpc] (>=2.27.0,<3.0.0)", "grpc-google-iam-v1 (>=0.14.0,<1.0.0)", "grpcio (>=1.33.2,<2.0.0) ; python_version < \"3.14\"", "grpcio (>=1.75.1,<2.0.0) ; python_version >= \"3.14\"", "grpcio-status (>=1.76.0,<2.0.0)", "proto-plus (>=1.22.3,<2.0.0) ; python_version < \"3.13\"", "proto-plus (>=1.25.0,<2.0.0) ; python_version >= \"3.13\"", "protobuf (>=3.20.2,!=4.21.0,!=4.21.1,!=4.21.2,!=4.21.3,!=4.21.4,!=4.21.5,<7.0.0)"]
protobuf = ["protobuf (>=3.20.2,<7.0.0)"]
testing = ["PyYAML", "black", "brotli", "coverage", "flake8", "google-cloud-iam", "google-cloud-kms", "google-cloud-pubsub", "google-cloud-testutils", "google-cloud-testutils", "mock", "numpy", "opentelemetry-sdk", "psutil", "py-cpuinfo", "pyopenssl", "pytest", "pytest-asyncio", "pytest-benchmark", "pytest-cov", "pytest-rerunfailures", "pytest-xdist"]
tracing = ["opentelemetry-api (>=1.1.0,<2.0.0)"]
@@ -887,6 +916,7 @@ version = "1.8.0"
description = "A python wrapper of the C library 'Google CRC32C'"
optional = false
python-versions = ">=3.9"
+groups = ["main"]
files = [
{file = "google_crc32c-1.8.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:0470b8c3d73b5f4e3300165498e4cf25221c7eb37f1159e221d1825b6df8a7ff"},
{file = "google_crc32c-1.8.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:119fcd90c57c89f30040b47c211acee231b25a45d225e3225294386f5d258288"},
@@ -929,6 +959,7 @@ version = "2.8.2"
description = "Utilities for Google Media Downloads and Resumable Uploads"
optional = false
python-versions = ">=3.9"
+groups = ["main"]
files = [
{file = "google_resumable_media-2.8.2-py3-none-any.whl", hash = "sha256:82b6d8ccd11765268cdd2a2123f417ec806b8eef3000a9a38dfe3033da5fb220"},
{file = "google_resumable_media-2.8.2.tar.gz", hash = "sha256:f3354a182ebd193ae3f42e3ef95e6c9b10f128320de23ac7637236713b1acd70"},
@@ -947,6 +978,7 @@ version = "1.74.0"
description = "Common protobufs used in Google APIs"
optional = false
python-versions = ">=3.9"
+groups = ["main"]
files = [
{file = "googleapis_common_protos-1.74.0-py3-none-any.whl", hash = "sha256:702216f78610bb510e3f12ac3cafd281b7ac45cc5d86e90ad87e4d301a3426b5"},
{file = "googleapis_common_protos-1.74.0.tar.gz", hash = "sha256:57971e4eeeba6aad1163c1f0fc88543f965bb49129b8bb55b2b7b26ecab084f1"},
@@ -965,6 +997,8 @@ version = "3.5.0"
description = "Lightweight in-process concurrent programming"
optional = false
python-versions = ">=3.10"
+groups = ["main"]
+markers = "platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\""
files = [
{file = "greenlet-3.5.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:29ea813b2e1f45fa9649a17853b2b5465c4072fbcb072e5af6cd3a288216574a"},
{file = "greenlet-3.5.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:804a70b328e706b785c6ef16187051c394a63dd1a906d89be24b6ad77759f13f"},
@@ -1037,6 +1071,7 @@ version = "0.14.4"
description = "IAM API client library"
optional = false
python-versions = ">=3.9"
+groups = ["main"]
files = [
{file = "grpc_google_iam_v1-0.14.4-py3-none-any.whl", hash = "sha256:412facc320fcbd94034b4df3d557662051d4d8adfa86e0ddb4dca70a3f739964"},
{file = "grpc_google_iam_v1-0.14.4.tar.gz", hash = "sha256:392b3796947ed6334e61171d9ab06bf7eb357f554e5fc7556ad7aab6d0e17038"},
@@ -1053,6 +1088,7 @@ version = "1.80.0"
description = "HTTP/2-based RPC framework"
optional = false
python-versions = ">=3.9"
+groups = ["main"]
files = [
{file = "grpcio-1.80.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:886457a7768e408cdce226ad1ca67d2958917d306523a0e21e1a2fdaa75c9c9c"},
{file = "grpcio-1.80.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:7b641fc3f1dc647bfd80bd713addc68f6d145956f64677e56d9ebafc0bd72388"},
@@ -1129,6 +1165,7 @@ version = "1.80.0"
description = "Status proto mapping for gRPC"
optional = false
python-versions = ">=3.9"
+groups = ["main"]
files = [
{file = "grpcio_status-1.80.0-py3-none-any.whl", hash = "sha256:4b56990363af50dbf2c2ebb80f1967185c07d87aa25aa2bea45ddb75fc181dbe"},
{file = "grpcio_status-1.80.0.tar.gz", hash = "sha256:df73802a4c89a3ea88aa2aff971e886fccce162bc2e6511408b3d67a144381cd"},
@@ -1145,6 +1182,7 @@ version = "3.15"
description = "Internationalized Domain Names in Applications (IDNA)"
optional = false
python-versions = ">=3.8"
+groups = ["main"]
files = [
{file = "idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8"},
{file = "idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc"},
@@ -1159,6 +1197,7 @@ version = "8.7.1"
description = "Read metadata from Python packages"
optional = false
python-versions = ">=3.9"
+groups = ["main"]
files = [
{file = "importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151"},
{file = "importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb"},
@@ -1168,13 +1207,13 @@ files = [
zipp = ">=3.20"
[package.extras]
-check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"]
+check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""]
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 (>=3.4)"]
perf = ["ipython"]
test = ["flufl.flake8", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"]
-type = ["mypy (<1.19)", "pytest-mypy (>=1.0.1)"]
+type = ["mypy (<1.19) ; platform_python_implementation == \"PyPy\"", "pytest-mypy (>=1.0.1)"]
[[package]]
name = "iniconfig"
@@ -1182,6 +1221,7 @@ version = "2.3.0"
description = "brain-dead simple config-ini parsing"
optional = false
python-versions = ">=3.10"
+groups = ["dev"]
files = [
{file = "iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"},
{file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"},
@@ -1193,6 +1233,7 @@ version = "2.2.0"
description = "Safely pass data to untrusted environments and back."
optional = false
python-versions = ">=3.8"
+groups = ["main"]
files = [
{file = "itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef"},
{file = "itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173"},
@@ -1204,6 +1245,7 @@ version = "3.1.6"
description = "A very fast and expressive template engine."
optional = false
python-versions = ">=3.7"
+groups = ["main"]
files = [
{file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"},
{file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"},
@@ -1221,6 +1263,7 @@ version = "1.1.0"
description = "JSON Matching Expressions"
optional = false
python-versions = ">=3.9"
+groups = ["main"]
files = [
{file = "jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64"},
{file = "jmespath-1.1.0.tar.gz", hash = "sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d"},
@@ -1232,6 +1275,7 @@ version = "1.5.7"
description = "Implementation of JOSE Web standards"
optional = false
python-versions = ">=3.8"
+groups = ["main"]
files = [
{file = "jwcrypto-1.5.7-py3-none-any.whl", hash = "sha256:729463fefe28b6de5cf1ebfda3e94f1a1b41d2799148ef98a01cb9678ebe2bb0"},
{file = "jwcrypto-1.5.7.tar.gz", hash = "sha256:70204d7cca406eda8c82352e3c41ba2d946610dafd19e54403f0a1f4f18633c6"},
@@ -1247,6 +1291,7 @@ version = "3.0.3"
description = "Safely add untrusted strings to HTML/XML markup."
optional = false
python-versions = ">=3.9"
+groups = ["main"]
files = [
{file = "markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559"},
{file = "markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419"},
@@ -1345,6 +1390,7 @@ version = "2.2.8"
description = "Python interface to MySQL"
optional = false
python-versions = ">=3.10"
+groups = ["main"]
files = [
{file = "mysqlclient-2.2.8-cp310-cp310-win_amd64.whl", hash = "sha256:86db31bba7b3480fec2751350e9790e24f016f89af33a87bab7e79f7196474e8"},
{file = "mysqlclient-2.2.8-cp311-cp311-win_amd64.whl", hash = "sha256:60c9ed339dc09e3d5380cc2a9f42e86754fee25a661ced77a02df77990664c92"},
@@ -1361,6 +1407,7 @@ version = "1.41.1"
description = "OpenTelemetry Python API"
optional = false
python-versions = ">=3.9"
+groups = ["main"]
files = [
{file = "opentelemetry_api-1.41.1-py3-none-any.whl", hash = "sha256:a22df900e75c76dc08440710e51f52f1aa6b451b429298896023e60db5b3139f"},
{file = "opentelemetry_api-1.41.1.tar.gz", hash = "sha256:0ad1814d73b875f84494387dae86ce0b12c68556331ce6ce8fe789197c949621"},
@@ -1376,6 +1423,7 @@ version = "1.41.1"
description = "OpenTelemetry Python SDK"
optional = false
python-versions = ">=3.9"
+groups = ["main"]
files = [
{file = "opentelemetry_sdk-1.41.1-py3-none-any.whl", hash = "sha256:edee379c126c1bce952b0c812b48fe8ff35b30df0eecf17e98afa4d598b7d85d"},
{file = "opentelemetry_sdk-1.41.1.tar.gz", hash = "sha256:724b615e1215b5aeacda0abb8a6a8922c9a1853068948bd0bd225a56d0c792e6"},
@@ -1395,6 +1443,7 @@ version = "0.62b1"
description = "OpenTelemetry Semantic Conventions"
optional = false
python-versions = ">=3.9"
+groups = ["main"]
files = [
{file = "opentelemetry_semantic_conventions-0.62b1-py3-none-any.whl", hash = "sha256:cf506938103d331fbb78eded0d9788095f7fd59016f2bda813c3324e5a74a93c"},
{file = "opentelemetry_semantic_conventions-0.62b1.tar.gz", hash = "sha256:c5cc6e04a7f8c7cdd30be2ed81499fa4e75bfbd52c9cb70d40af1f9cd3619802"},
@@ -1410,6 +1459,7 @@ version = "26.2"
description = "Core utilities for Python packages"
optional = false
python-versions = ">=3.8"
+groups = ["dev"]
files = [
{file = "packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e"},
{file = "packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661"},
@@ -1421,6 +1471,7 @@ version = "1.6.0"
description = "plugin and hook calling mechanisms for python"
optional = false
python-versions = ">=3.9"
+groups = ["dev"]
files = [
{file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"},
{file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"},
@@ -1436,6 +1487,7 @@ version = "1.27.2"
description = "Beautiful, Pythonic protocol buffers"
optional = false
python-versions = ">=3.9"
+groups = ["main"]
files = [
{file = "proto_plus-1.27.2-py3-none-any.whl", hash = "sha256:6432f75893d3b9e70b9c412f1d2f03f65b11fb164b793d14ae2ca01821d22718"},
{file = "proto_plus-1.27.2.tar.gz", hash = "sha256:b2adde53adadf75737c44d3dcb0104fde65250dfc83ad59168b4aa3e574b6a24"},
@@ -1453,6 +1505,7 @@ version = "6.33.6"
description = ""
optional = false
python-versions = ">=3.9"
+groups = ["main"]
files = [
{file = "protobuf-6.33.6-cp310-abi3-win32.whl", hash = "sha256:7d29d9b65f8afef196f8334e80d6bc1d5d4adedb449971fefd3723824e6e77d3"},
{file = "protobuf-6.33.6-cp310-abi3-win_amd64.whl", hash = "sha256:0cd27b587afca21b7cfa59a74dcbd48a50f0a6400cfb59391340ad729d91d326"},
@@ -1472,6 +1525,7 @@ version = "1.11.0"
description = "library with cross-python path, ini-parsing, io, code, log facilities"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+groups = ["main"]
files = [
{file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"},
{file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"},
@@ -1483,6 +1537,7 @@ version = "0.6.3"
description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)"
optional = false
python-versions = ">=3.8"
+groups = ["main"]
files = [
{file = "pyasn1-0.6.3-py3-none-any.whl", hash = "sha256:a80184d120f0864a52a073acc6fc642847d0be408e7c7252f31390c0f4eadcde"},
{file = "pyasn1-0.6.3.tar.gz", hash = "sha256:697a8ecd6d98891189184ca1fa05d1bb00e2f84b5977c481452050549c8a72cf"},
@@ -1494,6 +1549,7 @@ version = "0.4.2"
description = "A collection of ASN.1-based protocols modules"
optional = false
python-versions = ">=3.8"
+groups = ["main"]
files = [
{file = "pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a"},
{file = "pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6"},
@@ -1508,6 +1564,8 @@ version = "3.0"
description = "C parser in Python"
optional = false
python-versions = ">=3.10"
+groups = ["main"]
+markers = "platform_python_implementation != \"PyPy\" and implementation_name != \"PyPy\""
files = [
{file = "pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992"},
{file = "pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29"},
@@ -1519,6 +1577,7 @@ version = "2.13.3"
description = "Data validation using Python type hints"
optional = false
python-versions = ">=3.9"
+groups = ["main"]
files = [
{file = "pydantic-2.13.3-py3-none-any.whl", hash = "sha256:6db14ac8dfc9a1e57f87ea2c0de670c251240f43cb0c30a5130e9720dc612927"},
{file = "pydantic-2.13.3.tar.gz", hash = "sha256:af09e9d1d09f4e7fe37145c1f577e1d61ceb9a41924bf0094a36506285d0a84d"},
@@ -1532,7 +1591,7 @@ typing-inspection = ">=0.4.2"
[package.extras]
email = ["email-validator (>=2.0.0)"]
-timezone = ["tzdata"]
+timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""]
[[package]]
name = "pydantic-core"
@@ -1540,6 +1599,7 @@ version = "2.46.3"
description = "Core functionality for Pydantic validation and serialization"
optional = false
python-versions = ">=3.9"
+groups = ["main"]
files = [
{file = "pydantic_core-2.46.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:1da3786b8018e60349680720158cc19161cc3b4bdd815beb0a321cd5ce1ad5b1"},
{file = "pydantic_core-2.46.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cc0988cb29d21bf4a9d5cf2ef970b5c0e38d8d8e107a493278c05dc6c1dda69f"},
@@ -1668,13 +1728,14 @@ typing-extensions = ">=4.14.1"
[[package]]
name = "pydantic-settings"
-version = "2.14.0"
+version = "2.14.2"
description = "Settings management using Pydantic"
optional = false
python-versions = ">=3.10"
+groups = ["main"]
files = [
- {file = "pydantic_settings-2.14.0-py3-none-any.whl", hash = "sha256:fc8d5d692eb7092e43c8647c1c35a3ecd00e040fcf02ed86f4cb5458ca62182e"},
- {file = "pydantic_settings-2.14.0.tar.gz", hash = "sha256:24285fd4b0e0c06507dd9fdfd331ee23794305352aaec8fc4eb92d4047aeb67d"},
+ {file = "pydantic_settings-2.14.2-py3-none-any.whl", hash = "sha256:a20c97b37910b6550d5ea50fbcc2d4187defe58cd57070b73863d069419c9440"},
+ {file = "pydantic_settings-2.14.2.tar.gz", hash = "sha256:c19dd64b19097f1de80184f0cc7b0272a13ae6e170cbf240a3e27e381ed14a5f"},
]
[package.dependencies]
@@ -1695,6 +1756,7 @@ version = "2.20.0"
description = "Pygments is a syntax highlighting package written in Python."
optional = false
python-versions = ">=3.9"
+groups = ["dev"]
files = [
{file = "pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176"},
{file = "pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f"},
@@ -1705,20 +1767,18 @@ windows-terminal = ["colorama (>=0.4.6)"]
[[package]]
name = "pyjwt"
-version = "2.12.1"
+version = "2.13.0"
description = "JSON Web Token implementation in Python"
optional = false
python-versions = ">=3.9"
+groups = ["main"]
files = [
- {file = "pyjwt-2.12.1-py3-none-any.whl", hash = "sha256:28ca37c070cad8ba8cd9790cd940535d40274d22f80ab87f3ac6a713e6e8454c"},
- {file = "pyjwt-2.12.1.tar.gz", hash = "sha256:c74a7a2adf861c04d002db713dd85f84beb242228e671280bf709d765b03672b"},
+ {file = "pyjwt-2.13.0-py3-none-any.whl", hash = "sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728"},
+ {file = "pyjwt-2.13.0.tar.gz", hash = "sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423"},
]
[package.extras]
crypto = ["cryptography (>=3.4.0)"]
-dev = ["coverage[toml] (==7.10.7)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=8.4.2,<9.0.0)", "sphinx", "sphinx-rtd-theme", "zope.interface"]
-docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"]
-tests = ["coverage[toml] (==7.10.7)", "pytest (>=8.4.2,<9.0.0)"]
[[package]]
name = "pytest"
@@ -1726,6 +1786,7 @@ version = "9.0.3"
description = "pytest: simple powerful testing with Python"
optional = false
python-versions = ">=3.10"
+groups = ["dev"]
files = [
{file = "pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9"},
{file = "pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c"},
@@ -1747,6 +1808,7 @@ version = "2.9.0.post0"
description = "Extensions to the standard Python datetime module"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
+groups = ["main"]
files = [
{file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"},
{file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"},
@@ -1761,6 +1823,7 @@ version = "1.2.2"
description = "Read key-value pairs from a .env file and set them as environment variables"
optional = false
python-versions = ">=3.10"
+groups = ["main"]
files = [
{file = "python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a"},
{file = "python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3"},
@@ -1775,6 +1838,7 @@ version = "2026.1.post1"
description = "World timezone definitions, modern and historical"
optional = false
python-versions = "*"
+groups = ["main"]
files = [
{file = "pytz-2026.1.post1-py2.py3-none-any.whl", hash = "sha256:f2fd16142fda348286a75e1a524be810bb05d444e5a081f37f7affc635035f7a"},
{file = "pytz-2026.1.post1.tar.gz", hash = "sha256:3378dde6a0c3d26719182142c56e60c7f9af7e968076f31aae569d72a0358ee1"},
@@ -1786,6 +1850,7 @@ version = "2.10.6"
description = "Python client for Redis key-value store"
optional = false
python-versions = "*"
+groups = ["main"]
files = [
{file = "redis-2.10.6-py2.py3-none-any.whl", hash = "sha256:8a1900a9f2a0a44ecf6e8b5eb3e967a9909dfed219ad66df094f27f7d6f330fb"},
{file = "redis-2.10.6.tar.gz", hash = "sha256:a22ca993cea2962dbb588f9f30d0015ac4afcc45bee27d3978c0dbe9e97c6c0f"},
@@ -1797,6 +1862,7 @@ version = "1.3.6"
description = "Library for communicating with Redis Clusters. Built on top of redis-py lib"
optional = false
python-versions = "*"
+groups = ["main"]
files = [
{file = "redis-py-cluster-1.3.6.tar.gz", hash = "sha256:7db54b1de60bd34da3806676b112f07fc9afae556d8260ac02c3335d574ee42c"},
]
@@ -1810,6 +1876,7 @@ version = "2.33.1"
description = "Python HTTP for Humans."
optional = false
python-versions = ">=3.10"
+groups = ["main"]
files = [
{file = "requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a"},
{file = "requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517"},
@@ -1831,6 +1898,7 @@ version = "0.9.2"
description = "Easy to use retry decorator."
optional = false
python-versions = "*"
+groups = ["main"]
files = [
{file = "retry-0.9.2-py2.py3-none-any.whl", hash = "sha256:ccddf89761fa2c726ab29391837d4327f819ea14d244c232a1d24c67a2f98606"},
{file = "retry-0.9.2.tar.gz", hash = "sha256:f8bfa8b99b69c4506d6f5bd3b0aabf77f98cdb17f3c9fc3f5ca820033336fba4"},
@@ -1846,6 +1914,7 @@ version = "0.18.17"
description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order"
optional = false
python-versions = ">=3.9"
+groups = ["main"]
files = [
{file = "ruamel_yaml-0.18.17-py3-none-any.whl", hash = "sha256:9c8ba9eb3e793efdf924b60d521820869d5bf0cb9c6f1b82d82de8295e290b9d"},
{file = "ruamel_yaml-0.18.17.tar.gz", hash = "sha256:9091cd6e2d93a3a4b157ddb8fabf348c3de7f1fb1381346d985b6b247dcd8d3c"},
@@ -1864,6 +1933,8 @@ version = "0.2.15"
description = "C version of reader, parser and emitter for ruamel.yaml derived from libyaml"
optional = false
python-versions = ">=3.9"
+groups = ["main"]
+markers = "platform_python_implementation == \"CPython\" and python_version < \"3.15\""
files = [
{file = "ruamel_yaml_clib-0.2.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:88eea8baf72f0ccf232c22124d122a7f26e8a24110a0273d9bcddcb0f7e1fa03"},
{file = "ruamel_yaml_clib-0.2.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9b6f7d74d094d1f3a4e157278da97752f16ee230080ae331fcc219056ca54f77"},
@@ -1934,16 +2005,17 @@ version = "0.17.0"
description = "An Amazon S3 Transfer Manager"
optional = false
python-versions = ">=3.10"
+groups = ["main"]
files = [
{file = "s3transfer-0.17.0-py3-none-any.whl", hash = "sha256:ce3801712acf4ad3e89fb9990df97b4972e93f4b3b0004d214be5bce12814c20"},
{file = "s3transfer-0.17.0.tar.gz", hash = "sha256:9edeb6d1c3c2f89d6050348548834ad8289610d886e5bf7b7207728bd43ce33a"},
]
[package.dependencies]
-botocore = ">=1.37.4,<2.0a.0"
+botocore = ">=1.37.4,<2.0a0"
[package.extras]
-crt = ["botocore[crt] (>=1.37.4,<2.0a.0)"]
+crt = ["botocore[crt] (>=1.37.4,<2.0a0)"]
[[package]]
name = "setuptools"
@@ -1951,19 +2023,20 @@ version = "78.1.1"
description = "Easily download, build, install, upgrade, and uninstall Python packages"
optional = false
python-versions = ">=3.9"
+groups = ["main"]
files = [
{file = "setuptools-78.1.1-py3-none-any.whl", hash = "sha256:c3a9c4211ff4c309edb8b8c4f1cbfa7ae324c4ba9f91ff254e3d305b9fd54561"},
{file = "setuptools-78.1.1.tar.gz", hash = "sha256:fcc17fd9cd898242f6b4adfaca46137a9edef687f43e6f78469692a5e70d851d"},
]
[package.extras]
-check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.8.0)"]
-core = ["importlib_metadata (>=6)", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"]
+check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.8.0) ; sys_platform != \"cygwin\""]
+core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"]
cover = ["pytest-cov"]
doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"]
enabler = ["pytest-enabler (>=2.2)"]
-test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"]
-type = ["importlib_metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (==1.14.*)", "pytest-mypy"]
+test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"]
+type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.14.*)", "pytest-mypy"]
[[package]]
name = "six"
@@ -1971,6 +2044,7 @@ version = "1.17.0"
description = "Python 2 and 3 compatibility utilities"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
+groups = ["main"]
files = [
{file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"},
{file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"},
@@ -1982,6 +2056,7 @@ version = "2.0.49"
description = "Database Abstraction Library"
optional = false
python-versions = ">=3.7"
+groups = ["main"]
files = [
{file = "sqlalchemy-2.0.49-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:42e8804962f9e6f4be2cbaedc0c3718f08f60a16910fa3d86da5a1e3b1bfe60f"},
{file = "sqlalchemy-2.0.49-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc992c6ed024c8c3c592c5fc9846a03dd68a425674900c70122c77ea16c5fb0b"},
@@ -2083,6 +2158,7 @@ version = "3.3.0"
description = "ANSI color formatting for output in terminal"
optional = false
python-versions = ">=3.10"
+groups = ["main"]
files = [
{file = "termcolor-3.3.0-py3-none-any.whl", hash = "sha256:cf642efadaf0a8ebbbf4bc7a31cec2f9b5f21a9f726f4ccbb08192c9c26f43a5"},
{file = "termcolor-3.3.0.tar.gz", hash = "sha256:348871ca648ec6a9a983a13ab626c0acce02f515b9e1983332b17af7979521c5"},
@@ -2097,6 +2173,7 @@ version = "4.15.0"
description = "Backported and Experimental Type Hints for Python 3.9+"
optional = false
python-versions = ">=3.9"
+groups = ["main"]
files = [
{file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"},
{file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"},
@@ -2108,6 +2185,7 @@ version = "0.4.2"
description = "Runtime typing introspection tools"
optional = false
python-versions = ">=3.9"
+groups = ["main"]
files = [
{file = "typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7"},
{file = "typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464"},
@@ -2122,16 +2200,17 @@ version = "2.7.0"
description = "HTTP library with thread-safe connection pooling, file post, and more."
optional = false
python-versions = ">=3.10"
+groups = ["main"]
files = [
{file = "urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897"},
{file = "urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c"},
]
[package.extras]
-brotli = ["brotli (>=1.2.0)", "brotlicffi (>=1.2.0.0)"]
+brotli = ["brotli (>=1.2.0) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=1.2.0.0) ; platform_python_implementation != \"CPython\""]
h2 = ["h2 (>=4,<5)"]
socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
-zstd = ["backports-zstd (>=1.0.0)"]
+zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""]
[[package]]
name = "validators"
@@ -2139,6 +2218,7 @@ version = "0.35.0"
description = "Python Data Validation for Humans™"
optional = false
python-versions = ">=3.9"
+groups = ["main"]
files = [
{file = "validators-0.35.0-py3-none-any.whl", hash = "sha256:e8c947097eae7892cb3d26868d637f79f47b4a0554bc6b80065dfe5aac3705dd"},
{file = "validators-0.35.0.tar.gz", hash = "sha256:992d6c48a4e77c81f1b4daba10d16c3a9bb0dbb79b3a19ea847ff0928e70497a"},
@@ -2153,6 +2233,7 @@ version = "0.5.1"
description = "Character encoding aliases for legacy web content"
optional = false
python-versions = "*"
+groups = ["main"]
files = [
{file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"},
{file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"},
@@ -2164,6 +2245,7 @@ version = "3.1.8"
description = "The comprehensive WSGI web application library."
optional = false
python-versions = ">=3.9"
+groups = ["main"]
files = [
{file = "werkzeug-3.1.8-py3-none-any.whl", hash = "sha256:63a77fb8892bf28ebc3178683445222aa500e48ebad5ec77b0ad80f8726b1f50"},
{file = "werkzeug-3.1.8.tar.gz", hash = "sha256:9bad61a4268dac112f1c5cd4630a56ede601b6ed420300677a869083d70a4c44"},
@@ -2181,6 +2263,7 @@ version = "3.2.1"
description = "Form validation and rendering for Python web development."
optional = false
python-versions = ">=3.9"
+groups = ["main"]
files = [
{file = "wtforms-3.2.1-py3-none-any.whl", hash = "sha256:583bad77ba1dd7286463f21e11aa3043ca4869d03575921d1a1698d0715e0fd4"},
{file = "wtforms-3.2.1.tar.gz", hash = "sha256:df3e6b70f3192e92623128123ec8dca3067df9cfadd43d59681e210cfb8d4682"},
@@ -2198,13 +2281,14 @@ version = "3.23.1"
description = "Backport of pathlib-compatible object wrapper for zip files"
optional = false
python-versions = ">=3.9"
+groups = ["main"]
files = [
{file = "zipp-3.23.1-py3-none-any.whl", hash = "sha256:0b3596c50a5c700c9cb40ba8d86d9f2cc4807e9bedb06bcdf7fac85633e444dc"},
{file = "zipp-3.23.1.tar.gz", hash = "sha256:32120e378d32cd9714ad503c1d024619063ec28aad2248dc6672ad13edfa5110"},
]
[package.extras]
-check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"]
+check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""]
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)"]
@@ -2212,6 +2296,6 @@ test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more_it
type = ["pytest-mypy"]
[metadata]
-lock-version = "2.0"
+lock-version = "2.1"
python-versions = "^3.11"
content-hash = "852b9585d37baaad287ae9a769374122ce3d3d5c11bd115202f616efc48289d6"
diff --git a/pyproject.toml b/pyproject.toml
index 09a6e83..ea5b8df 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "mod-notification-handler"
-version = "0.1.0"
+version = "1.0"
description = "processes notifications for moderators"
authors = ["Erin Aster "]
readme = "README.md"
diff --git a/tests/data.sql b/tests/data.sql
index b268701..08b07be 100644
--- a/tests/data.sql
+++ b/tests/data.sql
@@ -44,6 +44,15 @@ INSERT INTO `tapir_users` VALUES (77777,'Archive','OptOut','',1,1,'archive-optou
INSERT INTO `arXiv_moderators` VALUES (77777, 'astro-ph', '', 0, 0, 0, 0, 0);
INSERT INTO `arXiv_moderators` VALUES (77777, 'astro-ph', 'HE', 0, 1, 0, 1, 0);
+-- category alias tests: q-fin.EC is the alias of canonical category econ.GN
+INSERT INTO `tapir_users` VALUES (60001,'Alias','CatMod','',1,1,'aliascat@example.com',8,0,2,1384185389,'dedicated','',0,0,0,1,1,0,0,0,0,'',0,0);
+INSERT INTO `arXiv_moderators` VALUES (60001, 'q-fin', 'EC', 0, 0, 0, 0, 0);
+
+-- 60002 opts out of named category econ.GN and would otherwise qualify via the alias archive (q-fin)
+INSERT INTO `tapir_users` VALUES (60002,'Cascade','OptOut','',1,1,'cascadeoptout@example.com',8,0,2,1384185389,'dedicated','',0,0,0,1,1,0,0,0,0,'',0,0);
+INSERT INTO `arXiv_moderators` VALUES (60002, 'econ', 'GN', 0, 1, 0, 0, 0);
+INSERT INTO `arXiv_moderators` VALUES (60002, 'q-fin', '', 0, 0, 0, 0, 0);
+
-- actor users referenced in test messages (user_id=1 and user_id=2)
INSERT INTO `tapir_users` VALUES (1,'Test','Editor','',1,1,'editor-one@example.com',8,0,2,1384185389,'','',0,0,0,1,1,0,0,0,0,'',0,0);
INSERT INTO `tapir_users` VALUES (2,'Jane','Smith','',1,1,'editor-two@example.com',8,0,2,1384185389,'','',0,0,0,1,1,0,0,0,0,'',0,0);
diff --git a/tests/test_email_content.py b/tests/test_email_content.py
index 48731ea..8af1344 100644
--- a/tests/test_email_content.py
+++ b/tests/test_email_content.py
@@ -3,7 +3,7 @@
import pytest
-from app.schema import SimplifiedNotification, CommentData, PromoteData, NewPropData, PropRespData
+from app.schema import SimplifiedNotification, CommentData, PromoteData, NewPropData, PropRespData, CategoryRejectionData, NotificationType
from app.schema import SubEmailData
from app.email_content import get_submission_info, _build_category_string, render_change_block, render_email
from app.schema import EmailTask, ConsolidatedNotifications
@@ -11,6 +11,7 @@
from app.templates.promote import render_promote_block
from app.templates.new_prop import render_new_prop_block
from app.templates.prop_resp import render_prop_resp_block
+from app.templates.category_rejection import render_category_rejection_block
from app.templates.submission import render_submission_block, truncate_authors, MAX_AUTHORS
from app.templates.email_body import render_body, CHECK_GUIDE_URL, HOW_TO_MOD_URL, MOD_HUB_URL
@@ -18,8 +19,8 @@
_USER = "Alice Mod"
-def _note(data) -> SimplifiedNotification:
- return SimplifiedNotification(time=_TIME, user_id=1, data=data)
+def _note(data, action=NotificationType.COMMENT) -> SimplifiedNotification:
+ return SimplifiedNotification(time=_TIME, user_id=1, action=action, data=data)
# ── comment ───────────────────────────────────────────────────────────────────
@@ -69,6 +70,30 @@ def test_render_prop_resp_block():
assert _USER in text and _USER in html_out
+# ── category rejection ────────────────────────────────────────────────────────
+
+def test_render_category_rejection_block_reject():
+ note = _note(CategoryRejectionData(category="cs.LG", rejection_type="reject", category_change="cs.LG cs.AI => no primary cs.AI"))
+ text, html_out = render_category_rejection_block(note, _USER)
+ assert "cs.LG" in text and "cs.LG" in html_out
+ assert "removed from submission" in text and "removed from submission" in html_out
+ assert "cs.LG cs.AI => no primary cs.AI" in text and "cs.LG cs.AI => no primary cs.AI" in html_out
+ assert _USER in text and _USER in html_out
+
+def test_render_category_rejection_block_accept_secondary():
+ note = _note(CategoryRejectionData(category="cs.LG", rejection_type="accept_secondary", category_change="cs.LG cs.AI => no primary cs.AI cs.LG"))
+ text, html_out = render_category_rejection_block(note, _USER)
+ assert "demoted to secondary" in text and "demoted to secondary" in html_out
+ assert _USER in text and _USER in html_out
+
+def test_render_category_rejection_block_cross_submission():
+ note = _note(CategoryRejectionData(category="hep-ph", rejection_type="cross_submission", category_change="cs.LG hep-ph => cs.LG"))
+ text, html_out = render_category_rejection_block(note, _USER)
+ assert "removed from cross submission" in text and "removed from cross submission" in html_out
+ assert "hep-ph" in text and "hep-ph" in html_out
+ assert _USER in text and _USER in html_out
+
+
# ── dispatcher ────────────────────────────────────────────────────────────────
def test_render_change_block_dispatches():
@@ -95,6 +120,12 @@ def test_render_change_block_dispatches():
assert "q-bio.BM" in text and "q-bio.BM" in html_out
assert "cs.LG cs.DC hep-ph => eess.AS cs.DC cs.LG hep-ph q-bio.BM" in text and "cs.LG cs.DC hep-ph => eess.AS cs.DC cs.LG hep-ph q-bio.BM" in html_out
+ rejection = _note(CategoryRejectionData(category="cs.LG", rejection_type="reject", category_change="cs.LG cs.AI => no primary cs.AI"))
+ text, html_out = render_change_block(rejection, _USER)
+ assert "cs.LG" in text and "cs.LG" in html_out
+ assert "removed from submission" in text and "removed from submission" in html_out
+ assert "cs.LG cs.AI => no primary cs.AI" in text and "cs.LG cs.AI => no primary cs.AI" in html_out
+
# ── submission block ──────────────────────────────────────────────────────────
@@ -197,8 +228,8 @@ def test_render_email_contains_all_sections_and_footer():
def test_render_email_changes_oldest_first():
t_old = datetime(2024, 6, 15, 14, 30, tzinfo=timezone.utc)
t_new = datetime(2024, 6, 15, 14, 32, tzinfo=timezone.utc)
- older = SimplifiedNotification(time=t_old, user_id=1, data=CommentData(comment="older comment"))
- newer = SimplifiedNotification(time=t_new, user_id=1, data=CommentData(comment="newer comment"))
+ older = SimplifiedNotification(time=t_old, user_id=1, action=NotificationType.COMMENT, data=CommentData(comment="older comment"))
+ newer = SimplifiedNotification(time=t_new, user_id=1, action=NotificationType.COMMENT, data=CommentData(comment="newer comment"))
notifications = ConsolidatedNotifications(submission_id=123, changes=[newer, older])
task = EmailTask(submission_id=123, to_emails=[], notifications=notifications)
text, _ = render_email(task, _mock_submission(), {})
@@ -281,6 +312,60 @@ def test_prop_resp_exact_html():
)
+def test_rejection_exact_text_reject():
+ note = _note(CategoryRejectionData(category="cs.LG", rejection_type="reject", category_change="cs.LG cs.AI => no primary cs.AI"))
+ text, _ = render_category_rejection_block(note, _USER)
+ assert text == (
+ f"[{_WHEN}] {_USER} rejected cs.LG (removed from submission):\n"
+ f" Change: cs.LG cs.AI => no primary cs.AI\n"
+ )
+
+
+def test_rejection_exact_html_reject():
+ note = _note(CategoryRejectionData(category="cs.LG", rejection_type="reject", category_change="cs.LG cs.AI => no primary cs.AI"))
+ _, html_out = render_category_rejection_block(note, _USER)
+ assert html_out == (
+ f"[{_WHEN}] {_USER} rejected cs.LG (removed from submission)
\n"
+ f"Change: cs.LG cs.AI => no primary cs.AI
\n"
+ )
+
+
+def test_rejection_exact_text_accept_secondary():
+ note = _note(CategoryRejectionData(category="cs.LG", rejection_type="accept_secondary", category_change="cs.LG cs.AI => no primary cs.AI cs.LG"))
+ text, _ = render_category_rejection_block(note, _USER)
+ assert text == (
+ f"[{_WHEN}] {_USER} rejected cs.LG (demoted to secondary):\n"
+ f" Change: cs.LG cs.AI => no primary cs.AI cs.LG\n"
+ )
+
+
+def test_rejection_exact_html_accept_secondary():
+ note = _note(CategoryRejectionData(category="cs.LG", rejection_type="accept_secondary", category_change="cs.LG cs.AI => no primary cs.AI cs.LG"))
+ _, html_out = render_category_rejection_block(note, _USER)
+ assert html_out == (
+ f"[{_WHEN}] {_USER} rejected cs.LG (demoted to secondary)
\n"
+ f"Change: cs.LG cs.AI => no primary cs.AI cs.LG
\n"
+ )
+
+
+def test_rejection_exact_text_cross_submission():
+ note = _note(CategoryRejectionData(category="hep-ph", rejection_type="cross_submission", category_change="cs.LG hep-ph => cs.LG"))
+ text, _ = render_category_rejection_block(note, _USER)
+ assert text == (
+ f"[{_WHEN}] {_USER} rejected hep-ph (removed from cross submission):\n"
+ f" Change: cs.LG hep-ph => cs.LG\n"
+ )
+
+
+def test_rejection_exact_html_cross_submission():
+ note = _note(CategoryRejectionData(category="hep-ph", rejection_type="cross_submission", category_change="cs.LG hep-ph => cs.LG"))
+ _, html_out = render_category_rejection_block(note, _USER)
+ assert html_out == (
+ f"[{_WHEN}] {_USER} rejected hep-ph (removed from cross submission)
\n"
+ f"Change: cs.LG hep-ph => cs.LG
\n"
+ )
+
+
def test_submission_exact_text():
sub = _mock_submission()
text, _ = render_submission_block(sub)
diff --git a/tests/test_moderators.py b/tests/test_moderators.py
index 8d42d79..d386f91 100644
--- a/tests/test_moderators.py
+++ b/tests/test_moderators.py
@@ -117,6 +117,30 @@ def test_who_to_email_category_optout_overrides_archive():
email, _ = who_to_email(CATEGORIES_ACTIVE['astro-ph.HE'], archives, cats)
assert 77777 not in email
+@pytest.mark.usefixtures("db_session")
+def test_who_to_email_alias_category_mod():
+ # 60001 mods q-fin.EC only, the alias of canonical econ.GN
+ archives, cats = get_all_moderators()
+ email, _ = who_to_email(CATEGORIES_ACTIVE['econ.GN'], archives, cats)
+ assert 60001 in email
+
+@pytest.mark.usefixtures("db_session")
+def test_who_to_email_alias_archive_mod():
+ # 246232 mods 'q-fin' archive-wide only -- not listed under econ, econ.GN, or q-fin.EC --
+ # but 'q-fin' is the alias archive of econ.GN's alias category (q-fin.EC), so should still get emailed
+ archives, cats = get_all_moderators()
+ email, reply_to = who_to_email(CATEGORIES_ACTIVE['econ.GN'], archives, cats)
+ assert 246232 in email
+ assert 246232 in reply_to
+
+@pytest.mark.usefixtures("db_session")
+def test_who_to_email_named_category_optout_cascades_to_alias_archive():
+ # 60002 opts out at named category econ.GN and mods alias archive q-fin --
+ # named-category opt-out should suppress the alias-archive inclusion too
+ archives, cats = get_all_moderators()
+ email, _ = who_to_email(CATEGORIES_ACTIVE['econ.GN'], archives, cats)
+ assert 60002 not in email
+
@pytest.mark.usefixtures("db_session")
def test_who_to_email_no_mods_returns_empty():
archives, cats = get_all_moderators()
diff --git a/tests/test_process_message.py b/tests/test_process_message.py
index 3e263e6..0345eee 100644
--- a/tests/test_process_message.py
+++ b/tests/test_process_message.py
@@ -7,7 +7,7 @@
from arxiv.taxonomy.definitions import CATEGORIES_ACTIVE
from app.process import process_messages, _parse_message, _convert_messages, _build_email_tasks
-from app.schema import CommentData, PromoteData, NewPropData, PropRespData, ConsolidatedNotifications, SimplifiedNotification
+from app.schema import CommentData, PromoteData, NewPropData, PropRespData, CategoryRejectionData, ConsolidatedNotifications, SimplifiedNotification, NotificationType
GOOD_COMMENT = {
"time": "2024-01-01T10:00:00Z",
@@ -56,6 +56,32 @@
}
}
+GOOD_REJECTION = {
+ "time": "2024-01-01T10:00:00Z",
+ "submission_id": 125,
+ "user_id": 3,
+ "categories": ["cs.LG", "cs.AI"],
+ "action": "category-rejection",
+ "data": {
+ "category": "cs.LG",
+ "rejection_type": "reject",
+ "category_change": "cs.LG cs.AI => no primary cs.AI"
+ }
+}
+
+BAD_REJECTION = {
+ "time": "2024-01-01T10:00:00Z",
+ "submission_id": 125,
+ "user_id": 3,
+ "categories": ["cs.LG"],
+ "action": "category-rejection",
+ "data": {
+ "category": "cs.LG",
+ "rejection_type": "invalid_type", # bad enum
+ "category_change": "cs.LG => "
+ }
+}
+
def _make_pubsub_message(ack_id: str, payload: dict):
"helper function to model what pubsub messages look like"
return SimpleNamespace(
@@ -184,6 +210,23 @@ def test_parse_promote():
assert simple_note.user_id == 1
assert simple_note.time == datetime(2024, 1, 1, 10, 0, tzinfo=timezone.utc)
+def test_parse_category_rejection():
+ with pytest.raises(Exception):
+ _parse_message(BAD_REJECTION)
+
+ full_note, simple_note = _parse_message(GOOD_REJECTION)
+
+ assert full_note.action == "category-rejection"
+ assert full_note.categories == ["cs.LG", "cs.AI"]
+ assert full_note.submission_id == 125
+
+ assert isinstance(simple_note.data, CategoryRejectionData)
+ assert simple_note.data.category == "cs.LG"
+ assert simple_note.data.rejection_type == "reject"
+ assert simple_note.data.category_change == "cs.LG cs.AI => no primary cs.AI"
+ assert simple_note.user_id == 3
+ assert simple_note.time == datetime(2024, 1, 1, 10, 0, tzinfo=timezone.utc)
+
def test_parse_prop_response():
bad_prop_resp = {
@@ -220,7 +263,8 @@ def test_consolidate_messages():
msg3 = _make_pubsub_message("ack-4", GOOD_PROMOTE)
msg4 = _make_pubsub_message("ack-5", BAD_PROMOTE)
msg5 = _make_pubsub_message("ack-7", GOOD_PROP_RESP)
- messages=[msg1, msg2, msg3, msg4, msg5]
+ msg6 = _make_pubsub_message("ack-8", GOOD_REJECTION)
+ messages=[msg1, msg2, msg3, msg4, msg5, msg6]
mock_ack = Mock()
data = _convert_messages(messages, ack_fn=mock_ack)
@@ -257,14 +301,26 @@ def test_consolidate_messages():
assert sub1.changes[2].data.category_change == 'no primary -> hep-lat'
assert sub1.changes[2].data.responses=="Primary accepted: hep-lat"
+ sub3=data[125]
+ assert sub3.ack_ids == ['ack-8']
+ assert sub3.categories == {CATEGORIES_ACTIVE['cs.LG'], CATEGORIES_ACTIVE['cs.AI']} #type: ignore
+ assert sub3.user_ids == {3}
+ assert len(sub3.changes) == 1
+ assert isinstance(sub3.changes[0].data, CategoryRejectionData)
+ assert sub3.changes[0].data.category == 'cs.LG'
+ assert sub3.changes[0].data.rejection_type == 'reject'
+ assert sub3.changes[0].data.category_change == 'cs.LG cs.AI => no primary cs.AI'
+
_NOTE = SimplifiedNotification(
time=datetime(2024, 1, 1, tzinfo=timezone.utc),
user_id=246231,
+ action=NotificationType.COMMENT,
data=CommentData(comment="test"),
)
_NOTE_2 = SimplifiedNotification(
time=datetime(2024, 1, 2, tzinfo=timezone.utc),
user_id=681201,
+ action=NotificationType.COMMENT,
data=CommentData(comment="test reply"),
)