Compare commits

..

No commits in common. "dcf483d19ee38e01e2fa89bd99eb271e04f4a7f0" and "a7e7fafedd0ee622dc0f9782cc58cd0feb58a23e" have entirely different histories.

11 changed files with 222 additions and 451 deletions

View File

@ -1,81 +0,0 @@
from collections.abc import Sequence
from typing import Any, ClassVar, TypeAlias
from django.core import mail
from django.template import loader
import mdformat
from markdownify import markdownify
Context: TypeAlias = dict[str, Any]
class EmailBase:
subject: ClassVar[str]
from_email: ClassVar[str | None] = None
reply_to: ClassVar[Sequence[str] | None] = None
context: Context
def __init__(self, context: Context) -> None:
self.context = context
def render_body(self) -> str:
raise NotImplementedError
@classmethod
def render(
cls,
context: Context,
to: list[str] | None = None,
cc: list[str] | None = None,
bcc: list[str] | None = None,
) -> mail.EmailMessage:
self = cls(context)
body = self.render_body()
return mail.EmailMessage(
self.subject,
body,
from_email=self.from_email,
reply_to=self.reply_to,
to=to,
cc=cc,
bcc=bcc,
)
class TemplatedMultipartEmail(EmailBase):
template: ClassVar[str]
def render_html_body(self) -> str:
template = loader.get_template(self.template)
return template.render(self.context)
def render_body(self, html_body: str) -> str:
return mdformat.text(markdownify(html_body), extensions={"tables"})
@classmethod
def render(
cls,
context: Context,
to: list[str] | None = None,
cc: list[str] | None = None,
bcc: list[str] | None = None,
) -> mail.EmailMessage:
self = cls(context)
html_body = self.render_html_body()
plain_body = self.render_body(html_body)
return mail.EmailMultiAlternatives(
self.subject,
plain_body,
from_email=self.from_email,
reply_to=self.reply_to,
to=to,
cc=cc,
bcc=bcc,
alternatives=[(html_body, "text/html")],
)

View File

@ -7,7 +7,6 @@ def post_migrate_callback(sender, **kwargs):
from cmsmanage.django_q2_helper import ensure_scheduled from cmsmanage.django_q2_helper import ensure_scheduled
from .tasks.event_survey_emails import send_survey_emails
from .tasks.scrape import scrape_events, scrape_membershipworks from .tasks.scrape import scrape_events, scrape_membershipworks
from .tasks.ucsAccounts import sync_accounts from .tasks.ucsAccounts import sync_accounts
@ -30,12 +29,6 @@ def post_migrate_callback(sender, **kwargs):
minutes=15, minutes=15,
) )
ensure_scheduled(
sync_accounts.q_task_group,
send_survey_emails,
schedule_type=Schedule.HOURLY,
)
class MembershipworksConfig(AppConfig): class MembershipworksConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField" default_auto_field = "django.db.models.BigAutoField"

View File

@ -1,53 +1,58 @@
from django.conf import settings from django.conf import settings
from django.core.mail import EmailMessage from django.core.mail import EmailMessage, EmailMultiAlternatives
from django.template import loader
import mdformat
from markdownify import markdownify
from cmsmanage.email import TemplatedMultipartEmail
from membershipworks.models import EventInvoice from membershipworks.models import EventInvoice
class InvoiceEmailBase(TemplatedMultipartEmail): def make_multipart_email(
from_email = "CMS Invoices <invoices@claremontmakerspace.org>" subject: str, html_body: str, to: tuple[str]
reply_to = ["Claremont MakerSpace <Info@ClaremontMakerSpace.org>"] ) -> EmailMultiAlternatives:
plain_body = mdformat.text(markdownify(html_body), extensions={"tables"})
email = EmailMultiAlternatives(
subject,
plain_body,
from_email="CMS Invoices <invoices@claremontmakerspace.org>",
to=to,
reply_to=["Claremont MakerSpace <Info@ClaremontMakerSpace.org>"],
)
email.attach_alternative(html_body, "text/html")
return email
class InstructorInvoiceEmail(InvoiceEmailBase): def make_instructor_email(
template = "membershipworks/email/event_invoice_instructor.dj.html" invoice: EventInvoice, pdf: bytes, event_url: str
@property
def subject(self) -> str:
event = self.context["invoice"].event
return f'Your CMS instructor invoice has been received for event "{event}" {event.start} - {event.end}'
@classmethod
def render_for_invoice(
cls, invoice: EventInvoice, pdf: bytes, event_url: str
) -> EmailMessage: ) -> EmailMessage:
if invoice.event.instructor is None or invoice.event.instructor.member is None: if invoice.event.instructor is None or invoice.event.instructor.member is None:
raise ValueError("Event Instructor not defined or is not member") raise ValueError("Event Instructor not defined or is not member")
message = cls.render( template = loader.get_template(
{"invoice": invoice, "event_url": event_url}, "membershipworks/email/event_invoice_instructor.dj.html"
to=[invoice.event.instructor.member.sanitized_mailbox()], )
html_body = template.render({"invoice": invoice, "event_url": event_url})
message = make_multipart_email(
f'Your CMS instructor invoice has been received for event "{invoice.event}" {invoice.event.start} - {invoice.event.end}',
html_body,
(invoice.event.instructor.member.sanitized_mailbox(),),
) )
message.attach(f"CMS_event_invoice_{invoice.uuid}.pdf", pdf, "application/pdf") message.attach(f"CMS_event_invoice_{invoice.uuid}.pdf", pdf, "application/pdf")
return message return message
class AdminInvoiceEmail(InvoiceEmailBase): def make_admin_email(invoice: EventInvoice, pdf: bytes, event_url: str) -> EmailMessage:
template = "membershipworks/email/event_invoice_admin.dj.html" template = loader.get_template("membershipworks/email/event_invoice_admin.dj.html")
html_body = template.render({"invoice": invoice, "event_url": event_url})
@property message = make_multipart_email(
def subject(self) -> str: f'CMS instructor invoice created for event "{invoice.event}" {invoice.event.start} - {invoice.event.end}',
event = self.context["invoice"].event html_body,
return f'CMS instructor invoice created for event "{event}" {event.start} - {event.end}'
@classmethod
def render_for_invoice(
cls, invoice: EventInvoice, pdf: bytes, event_url: str
) -> EmailMessage:
message = cls.render(
{"invoice": invoice, "event_url": event_url},
# TODO: should this be in database instead? # TODO: should this be in database instead?
to=settings.INVOICE_HANDLERS, settings.INVOICE_HANDLERS,
) )
message.attach(f"CMS_event_invoice_{invoice.uuid}.pdf", pdf, "application/pdf") message.attach(f"CMS_event_invoice_{invoice.uuid}.pdf", pdf, "application/pdf")
return message return message
@ -57,6 +62,6 @@ def make_invoice_emails(
invoice: EventInvoice, pdf: bytes, event_url: str invoice: EventInvoice, pdf: bytes, event_url: str
) -> list[EmailMessage]: ) -> list[EmailMessage]:
return [ return [
InstructorInvoiceEmail.render_for_invoice(invoice, pdf, event_url), make_instructor_email(invoice, pdf, event_url),
AdminInvoiceEmail.render_for_invoice(invoice, pdf, event_url), make_admin_email(invoice, pdf, event_url),
] ]

View File

@ -1,17 +0,0 @@
import logging
from django.core.management.base import BaseCommand
from membershipworks.tasks.event_survey_emails import logger, send_survey_emails
class Command(BaseCommand):
def handle(self, *args, verbosity: int, **options):
verbosity_levels = {
0: logging.ERROR,
1: logging.WARNING,
2: logging.INFO,
3: logging.DEBUG,
}
logger.setLevel(verbosity_levels.get(verbosity, logging.WARNING))
send_survey_emails()

View File

@ -1,22 +0,0 @@
# Generated by Django 5.0.6 on 2024-05-20 22:07
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("membershipworks", "0018_eventext_details_timestamp"),
]
operations = [
migrations.AddField(
model_name="eventext",
name="should_survey",
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name="eventext",
name="survey_email_sent",
field=models.BooleanField(default=False),
),
]

View File

@ -554,9 +554,6 @@ class EventExt(Event):
registrations = models.JSONField(null=True, blank=True) registrations = models.JSONField(null=True, blank=True)
should_survey = models.BooleanField(default=False)
survey_email_sent = models.BooleanField(default=False)
def get_absolute_url(self) -> str: def get_absolute_url(self) -> str:
return reverse("membershipworks:event-detail", kwargs={"eid": self.eid}) return reverse("membershipworks:event-detail", kwargs={"eid": self.eid})

View File

@ -1,62 +0,0 @@
import logging
from collections.abc import Iterable
from urllib.parse import quote, urlencode
from django.conf import settings
from django.core import mail
from django.db.models.functions import Now
from cmsmanage.email import TemplatedMultipartEmail
from membershipworks.models import EventExt
logger = logging.getLogger(__name__)
class EventSurveyEmail(TemplatedMultipartEmail):
# TODO: better wording
subject = (
"[Claremont MakerSpace] Please fill out a survey for your recent CMS class!"
)
from_email = "CMS Classes <classes@claremontmakerspace.org>"
template = "membershipworks/email/event_survey.dj.html"
@staticmethod
def survey_url(event: EventExt, attendee_name: str, attendee_email: str) -> str:
return "https://claremontmakerspace.org/class-evaluation-form?" + urlencode(
{
"event_id": event.eid,
"instructor_name": str(event.instructor) if event.instructor else "",
"event_name": event.title,
"event_date": event.start.strftime("%Y-%m-%d %H:%M:%S"),
"participant_name": attendee_name,
"participant_email": attendee_email,
},
quote_via=quote,
)
@classmethod
def render_for_event(cls, event: EventExt) -> Iterable[mail.EmailMessage]:
for name, email in event.attendees.values_list("name", "email"):
sanitized_email = mail.message.sanitize_address(
(name, email), settings.DEFAULT_CHARSET
)
survey_url = cls.survey_url(event, name, email)
yield cls.render(
{"event": event, "attendee_name": name, "survey_url": survey_url},
to=[sanitized_email],
)
def send_survey_emails():
with mail.get_connection() as conn:
for event in EventExt.objects.filter(
occurred=True, should_survey=True, survey_email_sent=False, end__lt=Now()
):
logger.info("Sending survey messages for event: %s", event)
# mark as sent even if we don't finish, to prevent sending duplicates
event.survey_email_sent = True
event.save()
conn.send_messages(list(EventSurveyEmail.render_for_event(event)))

View File

@ -1,19 +0,0 @@
{% load nh3_tags %}
<p>Dear {{ attendee_name }},</p>
<p>
Thank you for recently attending "{{ event.details.ttl|nh3 }}" at CMS on {{ event.start|date }}! We hope you enjoyed the experience and found it both informative and inspiring.
To help us continue to offer high-quality classes and improve our programs, we would greatly appreciate your feedback.
We kindly ask you to take a few minutes to complete a brief survey about your experience.
</p>
<p>
<a href="{{ survey_url }}">Click here to fill out the survey</a>
</p>
<p>
Your insights are invaluable to us and will directly contribute to enhancing our offerings and ensuring that we meet the needs and expectations of our community.
Thank you in advance for your time and feedback. If you have any additional comments or questions, please feel free to reach out to us at <a href="mailto:info@claremontmakerspace.org">info@claremontmakerspace.org</a>.
</p>
<p>
<div>Best regards,</div>
<div>The Claremont MakerSpace Team</div>
</p>

View File

@ -1,114 +1,93 @@
from abc import abstractmethod
from collections.abc import Iterable, Iterator
from itertools import groupby from itertools import groupby
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.core import mail from django.core import mail
from django.core.mail.message import sanitize_address from django.core.mail.message import sanitize_address
from django.db.models import QuerySet from django.template import loader
from cmsmanage.email import TemplatedMultipartEmail import mdformat
from membershipworks.models import Member from markdownify import markdownify
from paperwork.models import Certification, Department
class CertificationEmailBase(TemplatedMultipartEmail): def make_multipart_email(subject, html_body, to):
from_email = "Claremont MakerSpace Member Certification System <Certifications@ClaremontMakerSpace.org>" plain_body = mdformat.text(markdownify(html_body), extensions={"tables"})
reply_to = ["Claremont MakerSpace <Info@ClaremontMakerSpace.org>"]
@classmethod email = mail.EmailMultiAlternatives(
@abstractmethod subject,
def render_for_certifications( plain_body,
cls, ordered_certifications: QuerySet[Certification] "Claremont MakerSpace Member Certification System <Certifications@ClaremontMakerSpace.org>",
) -> Iterable[mail.EmailMessage]: to,
raise NotImplementedError reply_to=["Claremont MakerSpace <Info@ClaremontMakerSpace.org>"],
)
email.attach_alternative(html_body, "text/html")
return email
class DepartmentCertificationEmail(CertificationEmailBase): def make_department_email(department, certifications):
template = "paperwork/email/department_certifications.dj.html" template = loader.get_template("paperwork/email/department_certifications.dj.html")
@property
def subject(self) -> str:
certification_count = len(self.context["certifications"])
return f"{certification_count} new CMS Certifications issued for {self.context['department']}"
@classmethod
def render_for_department(
cls, department: Department, certifications: Iterator[Certification]
) -> Iterable[mail.EmailMessage]:
if department.shop_lead_flag is not None:
shop_leads = department.shop_lead_flag.members.values_list( shop_leads = department.shop_lead_flag.members.values_list(
"first_name", "account_name", "email", named=True "first_name", "account_name", "email", named=True
) )
yield cls.render(
html_body = template.render(
{ {
"shop_lead_names": [ "shop_lead_names": [shop_lead.first_name for shop_lead in shop_leads],
shop_lead.first_name for shop_lead in shop_leads
],
"department": department, "department": department,
"certifications": list(certifications), "certifications": certifications,
}, }
)
return make_multipart_email(
f"{len(certifications)} new CMS Certifications issued for {department}",
html_body,
to=[ to=[
sanitize_address((shop_lead.account_name, shop_lead.email), "ascii") sanitize_address((shop_lead.account_name, shop_lead.email), "ascii")
for shop_lead in shop_leads for shop_lead in shop_leads
], ],
) )
@classmethod
def render_for_certifications( def department_emails(ordered_queryset):
cls, ordered_certifications: QuerySet[Certification]
) -> Iterable[mail.EmailMessage]:
certifications_by_department = groupby( certifications_by_department = groupby(
ordered_certifications, ordered_queryset, lambda c: c.certification_version.definition.department
lambda c: c.certification_version.definition.department,
) )
for department, certifications in certifications_by_department: for department, certifications in certifications_by_department:
yield from cls.render_for_department(department, certifications) if department.shop_lead_flag is not None:
yield make_department_email(department, list(certifications))
class MemberCertificationEmail(CertificationEmailBase): def make_member_email(member, certifications):
template = "paperwork/email/member_certifications.dj.html" template = loader.get_template("paperwork/email/member_certifications.dj.html")
@property html_body = template.render({"member": member, "certifications": certifications})
def subject(self) -> str:
return f"You have been issued {len(self.context['certifications'])} new CMS Certifications"
@classmethod return make_multipart_email(
def render_for_member( f"You have been issued {len(certifications)} new CMS Certifications",
cls, member: Member, certifications: list[Certification] html_body,
) -> mail.EmailMessage:
return cls.render(
{"member": member, "certifications": certifications},
to=[sanitize_address((member.account_name, member.email), "ascii")], to=[sanitize_address((member.account_name, member.email), "ascii")],
) )
@classmethod
def render_for_certifications( def member_emails(ordered_queryset):
cls, ordered_certifications: QuerySet[Certification]
) -> Iterable[mail.EmailMessage]:
certifications_by_member = groupby( certifications_by_member = groupby(
ordered_certifications.filter(member__isnull=False), lambda c: c.member ordered_queryset.filter(member__isnull=False), lambda c: c.member
) )
for member, certifications in certifications_by_member: for member, certifications in certifications_by_member:
yield cls.render_for_member(member, list(certifications)) yield make_member_email(member, list(certifications))
class AdminCertificationEmail(CertificationEmailBase): def admin_email(ordered_queryset):
template = "paperwork/email/admin_certifications.dj.html" template = loader.get_template("paperwork/email/admin_certifications.dj.html")
html_body = template.render({"certifications": ordered_queryset})
@property return make_multipart_email(
def subject(self) -> str: f"{len(ordered_queryset)} new CMS Certifications issued",
return f"{len(self.context['certifications'])} new CMS Certifications issued" html_body,
to=(
@classmethod get_user_model()
def render_for_certifications(
cls, ordered_certifications: QuerySet[Certification]
) -> Iterable[mail.EmailMessage]:
yield cls.render(
{"certifications": ordered_certifications},
to=get_user_model()
.objects.with_perm( .objects.with_perm(
"paperwork.receive_certification_emails", "paperwork.receive_certification_emails",
include_superusers=False, include_superusers=False,
@ -116,18 +95,16 @@ class AdminCertificationEmail(CertificationEmailBase):
# TODO: LDAPBackend does not support with_perm() directly # TODO: LDAPBackend does not support with_perm() directly
backend="django.contrib.auth.backends.ModelBackend", backend="django.contrib.auth.backends.ModelBackend",
) )
.values_list("email", flat=True), .values_list("email", flat=True)
),
) )
def all_certification_emails(queryset: QuerySet[Certification]): def all_certification_emails(queryset):
ordered_queryset = queryset.select_related( ordered_queryset = queryset.select_related(
"certification_version__definition" "certification_version__definition"
).order_by("certification_version__definition__department") ).order_by("certification_version__definition__department")
for email_type in [ yield from department_emails(ordered_queryset)
DepartmentCertificationEmail, yield from member_emails(ordered_queryset)
MemberCertificationEmail, yield admin_email(ordered_queryset)
AdminCertificationEmail,
]:
yield from email_type.render_for_certifications(ordered_queryset)

194
pdm.lock
View File

@ -5,7 +5,7 @@
groups = ["default", "debug", "lint", "server", "typing", "dev"] groups = ["default", "debug", "lint", "server", "typing", "dev"]
strategy = ["cross_platform"] strategy = ["cross_platform"]
lock_version = "4.4.1" lock_version = "4.4.1"
content_hash = "sha256:8b3bb37d6fbada119262035c0e94ada756c2f43ca8863b50b51f47a232ee84b9" content_hash = "sha256:651200bd58f4159fe99a599564e0f83a89fd149e5e7300abd151d6a3bb6477a9"
[[package]] [[package]]
name = "aiohttp" name = "aiohttp"
@ -224,15 +224,15 @@ files = [
[[package]] [[package]]
name = "bitstring" name = "bitstring"
version = "4.2.2" version = "4.2.1"
requires_python = ">=3.8" requires_python = ">=3.8"
summary = "Simple construction, analysis and modification of binary data." summary = "Simple construction, analysis and modification of binary data."
dependencies = [ dependencies = [
"bitarray<3.0.0,>=2.9.0", "bitarray<3.0.0,>=2.9.0",
] ]
files = [ files = [
{file = "bitstring-4.2.2-py3-none-any.whl", hash = "sha256:8b784373e78e953879c8192589e1ecbcda8f111841633a2aaf88d2b073fd6c35"}, {file = "bitstring-4.2.1-py3-none-any.whl", hash = "sha256:9ae5d89072b065d640d645d37c0efcd27284b2f79f1c48cc1cd38b54e1932b4f"},
{file = "bitstring-4.2.2.tar.gz", hash = "sha256:b40b01d911eebaea6efff40d826580806dced5e04b9d3cbad6aebf9422f4b643"}, {file = "bitstring-4.2.1.tar.gz", hash = "sha256:8abb5a661588c764bacf1a23d64c7bb57517d2841e3e6f54fb8c057119e0540d"},
] ]
[[package]] [[package]]
@ -1079,7 +1079,7 @@ files = [
[[package]] [[package]]
name = "hypothesis" name = "hypothesis"
version = "6.102.4" version = "6.100.5"
requires_python = ">=3.8" requires_python = ">=3.8"
summary = "A library for property-based testing" summary = "A library for property-based testing"
dependencies = [ dependencies = [
@ -1087,23 +1087,23 @@ dependencies = [
"sortedcontainers<3.0.0,>=2.1.0", "sortedcontainers<3.0.0,>=2.1.0",
] ]
files = [ files = [
{file = "hypothesis-6.102.4-py3-none-any.whl", hash = "sha256:013df31b04a4daede13756f497e60e451963d86f426395a79f99c5d692919bbd"}, {file = "hypothesis-6.100.5-py3-none-any.whl", hash = "sha256:d2f875a8791abdf68599e85cc9238f7239a73b72362d34be95e532e811766723"},
{file = "hypothesis-6.102.4.tar.gz", hash = "sha256:59b4d144346d5cffb482cc1bafbd21b13ff31608e8c4b3e4630339aee3e87763"}, {file = "hypothesis-6.100.5.tar.gz", hash = "sha256:14e06081459ee96ca8f1ed996b6fc19f71910281e01f6a9fa3d9d6e68bbe4a25"},
] ]
[[package]] [[package]]
name = "hypothesis" name = "hypothesis"
version = "6.102.4" version = "6.100.5"
extras = ["django"] extras = ["django"]
requires_python = ">=3.8" requires_python = ">=3.8"
summary = "A library for property-based testing" summary = "A library for property-based testing"
dependencies = [ dependencies = [
"django>=3.2", "django>=3.2",
"hypothesis==6.102.4", "hypothesis==6.100.5",
] ]
files = [ files = [
{file = "hypothesis-6.102.4-py3-none-any.whl", hash = "sha256:013df31b04a4daede13756f497e60e451963d86f426395a79f99c5d692919bbd"}, {file = "hypothesis-6.100.5-py3-none-any.whl", hash = "sha256:d2f875a8791abdf68599e85cc9238f7239a73b72362d34be95e532e811766723"},
{file = "hypothesis-6.102.4.tar.gz", hash = "sha256:59b4d144346d5cffb482cc1bafbd21b13ff31608e8c4b3e4630339aee3e87763"}, {file = "hypothesis-6.100.5.tar.gz", hash = "sha256:14e06081459ee96ca8f1ed996b6fc19f71910281e01f6a9fa3d9d6e68bbe4a25"},
] ]
[[package]] [[package]]
@ -1174,77 +1174,77 @@ files = [
[[package]] [[package]]
name = "lxml" name = "lxml"
version = "5.2.2" version = "5.2.1"
requires_python = ">=3.6" requires_python = ">=3.6"
summary = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." summary = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API."
files = [ files = [
{file = "lxml-5.2.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:45f9494613160d0405682f9eee781c7e6d1bf45f819654eb249f8f46a2c22545"}, {file = "lxml-5.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:70ac664a48aa64e5e635ae5566f5227f2ab7f66a3990d67566d9907edcbbf867"},
{file = "lxml-5.2.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b0b3f2df149efb242cee2ffdeb6674b7f30d23c9a7af26595099afaf46ef4e88"}, {file = "lxml-5.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1ae67b4e737cddc96c99461d2f75d218bdf7a0c3d3ad5604d1f5e7464a2f9ffe"},
{file = "lxml-5.2.2-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d28cb356f119a437cc58a13f8135ab8a4c8ece18159eb9194b0d269ec4e28083"}, {file = "lxml-5.2.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f18a5a84e16886898e51ab4b1d43acb3083c39b14c8caeb3589aabff0ee0b270"},
{file = "lxml-5.2.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:657a972f46bbefdbba2d4f14413c0d079f9ae243bd68193cb5061b9732fa54c1"}, {file = "lxml-5.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c6f2c8372b98208ce609c9e1d707f6918cc118fea4e2c754c9f0812c04ca116d"},
{file = "lxml-5.2.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b74b9ea10063efb77a965a8d5f4182806fbf59ed068b3c3fd6f30d2ac7bee734"}, {file = "lxml-5.2.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:394ed3924d7a01b5bd9a0d9d946136e1c2f7b3dc337196d99e61740ed4bc6fe1"},
{file = "lxml-5.2.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:07542787f86112d46d07d4f3c4e7c760282011b354d012dc4141cc12a68cef5f"}, {file = "lxml-5.2.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d077bc40a1fe984e1a9931e801e42959a1e6598edc8a3223b061d30fbd26bbc"},
{file = "lxml-5.2.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:303f540ad2dddd35b92415b74b900c749ec2010e703ab3bfd6660979d01fd4ed"}, {file = "lxml-5.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:764b521b75701f60683500d8621841bec41a65eb739b8466000c6fdbc256c240"},
{file = "lxml-5.2.2-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:2eb2227ce1ff998faf0cd7fe85bbf086aa41dfc5af3b1d80867ecfe75fb68df3"}, {file = "lxml-5.2.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:3a6b45da02336895da82b9d472cd274b22dc27a5cea1d4b793874eead23dd14f"},
{file = "lxml-5.2.2-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:1d8a701774dfc42a2f0b8ccdfe7dbc140500d1049e0632a611985d943fcf12df"}, {file = "lxml-5.2.1-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:5ea7b6766ac2dfe4bcac8b8595107665a18ef01f8c8343f00710b85096d1b53a"},
{file = "lxml-5.2.2-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:56793b7a1a091a7c286b5f4aa1fe4ae5d1446fe742d00cdf2ffb1077865db10d"}, {file = "lxml-5.2.1-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:e196a4ff48310ba62e53a8e0f97ca2bca83cdd2fe2934d8b5cb0df0a841b193a"},
{file = "lxml-5.2.2-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:eb00b549b13bd6d884c863554566095bf6fa9c3cecb2e7b399c4bc7904cb33b5"}, {file = "lxml-5.2.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:200e63525948e325d6a13a76ba2911f927ad399ef64f57898cf7c74e69b71095"},
{file = "lxml-5.2.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a2569a1f15ae6c8c64108a2cd2b4a858fc1e13d25846be0666fc144715e32ab"}, {file = "lxml-5.2.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:dae0ed02f6b075426accbf6b2863c3d0a7eacc1b41fb40f2251d931e50188dad"},
{file = "lxml-5.2.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:8cf85a6e40ff1f37fe0f25719aadf443686b1ac7652593dc53c7ef9b8492b115"}, {file = "lxml-5.2.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:ab31a88a651039a07a3ae327d68ebdd8bc589b16938c09ef3f32a4b809dc96ef"},
{file = "lxml-5.2.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:d237ba6664b8e60fd90b8549a149a74fcc675272e0e95539a00522e4ca688b04"}, {file = "lxml-5.2.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:df2e6f546c4df14bc81f9498bbc007fbb87669f1bb707c6138878c46b06f6510"},
{file = "lxml-5.2.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0b3f5016e00ae7630a4b83d0868fca1e3d494c78a75b1c7252606a3a1c5fc2ad"}, {file = "lxml-5.2.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5dd1537e7cc06efd81371f5d1a992bd5ab156b2b4f88834ca852de4a8ea523fa"},
{file = "lxml-5.2.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:23441e2b5339bc54dc949e9e675fa35efe858108404ef9aa92f0456929ef6fe8"}, {file = "lxml-5.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9b9ec9c9978b708d488bec36b9e4c94d88fd12ccac3e62134a9d17ddba910ea9"},
{file = "lxml-5.2.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:2fb0ba3e8566548d6c8e7dd82a8229ff47bd8fb8c2da237607ac8e5a1b8312e5"}, {file = "lxml-5.2.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:8e77c69d5892cb5ba71703c4057091e31ccf534bd7f129307a4d084d90d014b8"},
{file = "lxml-5.2.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:79d1fb9252e7e2cfe4de6e9a6610c7cbb99b9708e2c3e29057f487de5a9eaefa"}, {file = "lxml-5.2.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:a8d5c70e04aac1eda5c829a26d1f75c6e5286c74743133d9f742cda8e53b9c2f"},
{file = "lxml-5.2.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6dcc3d17eac1df7859ae01202e9bb11ffa8c98949dcbeb1069c8b9a75917e01b"}, {file = "lxml-5.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c94e75445b00319c1fad60f3c98b09cd63fe1134a8a953dcd48989ef42318534"},
{file = "lxml-5.2.2-cp311-cp311-win32.whl", hash = "sha256:4c30a2f83677876465f44c018830f608fa3c6a8a466eb223535035fbc16f3438"}, {file = "lxml-5.2.1-cp311-cp311-win32.whl", hash = "sha256:4951e4f7a5680a2db62f7f4ab2f84617674d36d2d76a729b9a8be4b59b3659be"},
{file = "lxml-5.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:49095a38eb333aaf44c06052fd2ec3b8f23e19747ca7ec6f6c954ffea6dbf7be"}, {file = "lxml-5.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:5c670c0406bdc845b474b680b9a5456c561c65cf366f8db5a60154088c92d102"},
{file = "lxml-5.2.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:7429e7faa1a60cad26ae4227f4dd0459efde239e494c7312624ce228e04f6391"}, {file = "lxml-5.2.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:abc25c3cab9ec7fcd299b9bcb3b8d4a1231877e425c650fa1c7576c5107ab851"},
{file = "lxml-5.2.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:50ccb5d355961c0f12f6cf24b7187dbabd5433f29e15147a67995474f27d1776"}, {file = "lxml-5.2.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6935bbf153f9a965f1e07c2649c0849d29832487c52bb4a5c5066031d8b44fd5"},
{file = "lxml-5.2.2-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc911208b18842a3a57266d8e51fc3cfaccee90a5351b92079beed912a7914c2"}, {file = "lxml-5.2.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d793bebb202a6000390a5390078e945bbb49855c29c7e4d56a85901326c3b5d9"},
{file = "lxml-5.2.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:33ce9e786753743159799fdf8e92a5da351158c4bfb6f2db0bf31e7892a1feb5"}, {file = "lxml-5.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afd5562927cdef7c4f5550374acbc117fd4ecc05b5007bdfa57cc5355864e0a4"},
{file = "lxml-5.2.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ec87c44f619380878bd49ca109669c9f221d9ae6883a5bcb3616785fa8f94c97"}, {file = "lxml-5.2.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0e7259016bc4345a31af861fdce942b77c99049d6c2107ca07dc2bba2435c1d9"},
{file = "lxml-5.2.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08ea0f606808354eb8f2dfaac095963cb25d9d28e27edcc375d7b30ab01abbf6"}, {file = "lxml-5.2.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:530e7c04f72002d2f334d5257c8a51bf409db0316feee7c87e4385043be136af"},
{file = "lxml-5.2.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75a9632f1d4f698b2e6e2e1ada40e71f369b15d69baddb8968dcc8e683839b18"}, {file = "lxml-5.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59689a75ba8d7ffca577aefd017d08d659d86ad4585ccc73e43edbfc7476781a"},
{file = "lxml-5.2.2-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:74da9f97daec6928567b48c90ea2c82a106b2d500f397eeb8941e47d30b1ca85"}, {file = "lxml-5.2.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:f9737bf36262046213a28e789cc82d82c6ef19c85a0cf05e75c670a33342ac2c"},
{file = "lxml-5.2.2-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:0969e92af09c5687d769731e3f39ed62427cc72176cebb54b7a9d52cc4fa3b73"}, {file = "lxml-5.2.1-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:3a74c4f27167cb95c1d4af1c0b59e88b7f3e0182138db2501c353555f7ec57f4"},
{file = "lxml-5.2.2-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:9164361769b6ca7769079f4d426a41df6164879f7f3568be9086e15baca61466"}, {file = "lxml-5.2.1-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:68a2610dbe138fa8c5826b3f6d98a7cfc29707b850ddcc3e21910a6fe51f6ca0"},
{file = "lxml-5.2.2-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:d26a618ae1766279f2660aca0081b2220aca6bd1aa06b2cf73f07383faf48927"}, {file = "lxml-5.2.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:f0a1bc63a465b6d72569a9bba9f2ef0334c4e03958e043da1920299100bc7c08"},
{file = "lxml-5.2.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab67ed772c584b7ef2379797bf14b82df9aa5f7438c5b9a09624dd834c1c1aaf"}, {file = "lxml-5.2.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c2d35a1d047efd68027817b32ab1586c1169e60ca02c65d428ae815b593e65d4"},
{file = "lxml-5.2.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:3d1e35572a56941b32c239774d7e9ad724074d37f90c7a7d499ab98761bd80cf"}, {file = "lxml-5.2.1-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:79bd05260359170f78b181b59ce871673ed01ba048deef4bf49a36ab3e72e80b"},
{file = "lxml-5.2.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:8268cbcd48c5375f46e000adb1390572c98879eb4f77910c6053d25cc3ac2c67"}, {file = "lxml-5.2.1-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:865bad62df277c04beed9478fe665b9ef63eb28fe026d5dedcb89b537d2e2ea6"},
{file = "lxml-5.2.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e282aedd63c639c07c3857097fc0e236f984ceb4089a8b284da1c526491e3f3d"}, {file = "lxml-5.2.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:44f6c7caff88d988db017b9b0e4ab04934f11e3e72d478031efc7edcac6c622f"},
{file = "lxml-5.2.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dfdc2bfe69e9adf0df4915949c22a25b39d175d599bf98e7ddf620a13678585"}, {file = "lxml-5.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:71e97313406ccf55d32cc98a533ee05c61e15d11b99215b237346171c179c0b0"},
{file = "lxml-5.2.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4aefd911793b5d2d7a921233a54c90329bf3d4a6817dc465f12ffdfe4fc7b8fe"}, {file = "lxml-5.2.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:057cdc6b86ab732cf361f8b4d8af87cf195a1f6dc5b0ff3de2dced242c2015e0"},
{file = "lxml-5.2.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:8b8df03a9e995b6211dafa63b32f9d405881518ff1ddd775db4e7b98fb545e1c"}, {file = "lxml-5.2.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:f3bbbc998d42f8e561f347e798b85513ba4da324c2b3f9b7969e9c45b10f6169"},
{file = "lxml-5.2.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f11ae142f3a322d44513de1018b50f474f8f736bc3cd91d969f464b5bfef8836"}, {file = "lxml-5.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:491755202eb21a5e350dae00c6d9a17247769c64dcf62d8c788b5c135e179dc4"},
{file = "lxml-5.2.2-cp312-cp312-win32.whl", hash = "sha256:16a8326e51fcdffc886294c1e70b11ddccec836516a343f9ed0f82aac043c24a"}, {file = "lxml-5.2.1-cp312-cp312-win32.whl", hash = "sha256:8de8f9d6caa7f25b204fc861718815d41cbcf27ee8f028c89c882a0cf4ae4134"},
{file = "lxml-5.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:bbc4b80af581e18568ff07f6395c02114d05f4865c2812a1f02f2eaecf0bfd48"}, {file = "lxml-5.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:f2a9efc53d5b714b8df2b4b3e992accf8ce5bbdfe544d74d5c6766c9e1146a3a"},
{file = "lxml-5.2.2-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b537bd04d7ccd7c6350cdaaaad911f6312cbd61e6e6045542f781c7f8b2e99d2"}, {file = "lxml-5.2.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9b0ff53900566bc6325ecde9181d89afadc59c5ffa39bddf084aaedfe3b06a11"},
{file = "lxml-5.2.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4820c02195d6dfb7b8508ff276752f6b2ff8b64ae5d13ebe02e7667e035000b9"}, {file = "lxml-5.2.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd6037392f2d57793ab98d9e26798f44b8b4da2f2464388588f48ac52c489ea1"},
{file = "lxml-5.2.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a09f6184f17a80897172863a655467da2b11151ec98ba8d7af89f17bf63dae"}, {file = "lxml-5.2.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b9c07e7a45bb64e21df4b6aa623cb8ba214dfb47d2027d90eac197329bb5e94"},
{file = "lxml-5.2.2-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:76acba4c66c47d27c8365e7c10b3d8016a7da83d3191d053a58382311a8bf4e1"}, {file = "lxml-5.2.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:3249cc2989d9090eeac5467e50e9ec2d40704fea9ab72f36b034ea34ee65ca98"},
{file = "lxml-5.2.2-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b128092c927eaf485928cec0c28f6b8bead277e28acf56800e972aa2c2abd7a2"}, {file = "lxml-5.2.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f42038016852ae51b4088b2862126535cc4fc85802bfe30dea3500fdfaf1864e"},
{file = "lxml-5.2.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ae791f6bd43305aade8c0e22f816b34f3b72b6c820477aab4d18473a37e8090b"}, {file = "lxml-5.2.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:533658f8fbf056b70e434dff7e7aa611bcacb33e01f75de7f821810e48d1bb66"},
{file = "lxml-5.2.2-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a2f6a1bc2460e643785a2cde17293bd7a8f990884b822f7bca47bee0a82fc66b"}, {file = "lxml-5.2.1-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:622020d4521e22fb371e15f580d153134bfb68d6a429d1342a25f051ec72df1c"},
{file = "lxml-5.2.2-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e8d351ff44c1638cb6e980623d517abd9f580d2e53bfcd18d8941c052a5a009"}, {file = "lxml-5.2.1-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efa7b51824aa0ee957ccd5a741c73e6851de55f40d807f08069eb4c5a26b2baa"},
{file = "lxml-5.2.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bec4bd9133420c5c52d562469c754f27c5c9e36ee06abc169612c959bd7dbb07"}, {file = "lxml-5.2.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c6ad0fbf105f6bcc9300c00010a2ffa44ea6f555df1a2ad95c88f5656104817"},
{file = "lxml-5.2.2-pp37-pypy37_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:55ce6b6d803890bd3cc89975fca9de1dff39729b43b73cb15ddd933b8bc20484"}, {file = "lxml-5.2.1-pp37-pypy37_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:e233db59c8f76630c512ab4a4daf5a5986da5c3d5b44b8e9fc742f2a24dbd460"},
{file = "lxml-5.2.2-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:8ab6a358d1286498d80fe67bd3d69fcbc7d1359b45b41e74c4a26964ca99c3f8"}, {file = "lxml-5.2.1-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:6a014510830df1475176466b6087fc0c08b47a36714823e58d8b8d7709132a96"},
{file = "lxml-5.2.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:06668e39e1f3c065349c51ac27ae430719d7806c026fec462e5693b08b95696b"}, {file = "lxml-5.2.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:d38c8f50ecf57f0463399569aa388b232cf1a2ffb8f0a9a5412d0db57e054860"},
{file = "lxml-5.2.2-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9cd5323344d8ebb9fb5e96da5de5ad4ebab993bbf51674259dbe9d7a18049525"}, {file = "lxml-5.2.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5aea8212fb823e006b995c4dda533edcf98a893d941f173f6c9506126188860d"},
{file = "lxml-5.2.2-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89feb82ca055af0fe797a2323ec9043b26bc371365847dbe83c7fd2e2f181c34"}, {file = "lxml-5.2.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ff097ae562e637409b429a7ac958a20aab237a0378c42dabaa1e3abf2f896e5f"},
{file = "lxml-5.2.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e481bba1e11ba585fb06db666bfc23dbe181dbafc7b25776156120bf12e0d5a6"}, {file = "lxml-5.2.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f5d65c39f16717a47c36c756af0fb36144069c4718824b7533f803ecdf91138"},
{file = "lxml-5.2.2-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:9d6c6ea6a11ca0ff9cd0390b885984ed31157c168565702959c25e2191674a14"}, {file = "lxml-5.2.1-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:3d0c3dd24bb4605439bf91068598d00c6370684f8de4a67c2992683f6c309d6b"},
{file = "lxml-5.2.2-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3d98de734abee23e61f6b8c2e08a88453ada7d6486dc7cdc82922a03968928db"}, {file = "lxml-5.2.1-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e32be23d538753a8adb6c85bd539f5fd3b15cb987404327c569dfc5fd8366e85"},
{file = "lxml-5.2.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:69ab77a1373f1e7563e0fb5a29a8440367dec051da6c7405333699d07444f511"}, {file = "lxml-5.2.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:cc518cea79fd1e2f6c90baafa28906d4309d24f3a63e801d855e7424c5b34144"},
{file = "lxml-5.2.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:34e17913c431f5ae01d8658dbf792fdc457073dcdfbb31dc0cc6ab256e664a8d"}, {file = "lxml-5.2.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a0af35bd8ebf84888373630f73f24e86bf016642fb8576fba49d3d6b560b7cbc"},
{file = "lxml-5.2.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:05f8757b03208c3f50097761be2dea0aba02e94f0dc7023ed73a7bb14ff11eb0"}, {file = "lxml-5.2.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8aca2e3a72f37bfc7b14ba96d4056244001ddcc18382bd0daa087fd2e68a354"},
{file = "lxml-5.2.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a520b4f9974b0a0a6ed73c2154de57cdfd0c8800f4f15ab2b73238ffed0b36e"}, {file = "lxml-5.2.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ca1e8188b26a819387b29c3895c47a5e618708fe6f787f3b1a471de2c4a94d9"},
{file = "lxml-5.2.2-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5e097646944b66207023bc3c634827de858aebc226d5d4d6d16f0b77566ea182"}, {file = "lxml-5.2.1-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c8ba129e6d3b0136a0f50345b2cb3db53f6bda5dd8c7f5d83fbccba97fb5dcb5"},
{file = "lxml-5.2.2-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b5e4ef22ff25bfd4ede5f8fb30f7b24446345f3e79d9b7455aef2836437bc38a"}, {file = "lxml-5.2.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e998e304036198b4f6914e6a1e2b6f925208a20e2042563d9734881150c6c246"},
{file = "lxml-5.2.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ff69a9a0b4b17d78170c73abe2ab12084bdf1691550c5629ad1fe7849433f324"}, {file = "lxml-5.2.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d3be9b2076112e51b323bdf6d5a7f8a798de55fb8d95fcb64bd179460cdc0704"},
{file = "lxml-5.2.2.tar.gz", hash = "sha256:bb2dc4898180bea79863d5487e5f9c7c34297414bad54bcd0f0852aee9cfdb87"}, {file = "lxml-5.2.1.tar.gz", hash = "sha256:3f7765e69bbce0906a7c74d5fe46d2c7a7596147318dbc08e4a2431f3060e306"},
] ]
[[package]] [[package]]
@ -1833,27 +1833,27 @@ files = [
[[package]] [[package]]
name = "ruff" name = "ruff"
version = "0.4.4" version = "0.4.3"
requires_python = ">=3.7" requires_python = ">=3.7"
summary = "An extremely fast Python linter and code formatter, written in Rust." summary = "An extremely fast Python linter and code formatter, written in Rust."
files = [ files = [
{file = "ruff-0.4.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:29d44ef5bb6a08e235c8249294fa8d431adc1426bfda99ed493119e6f9ea1bf6"}, {file = "ruff-0.4.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b70800c290f14ae6fcbb41bbe201cf62dfca024d124a1f373e76371a007454ce"},
{file = "ruff-0.4.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c4efe62b5bbb24178c950732ddd40712b878a9b96b1d02b0ff0b08a090cbd891"}, {file = "ruff-0.4.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:08a0d6a22918ab2552ace96adeaca308833873a4d7d1d587bb1d37bae8728eb3"},
{file = "ruff-0.4.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c8e2f1e8fc12d07ab521a9005d68a969e167b589cbcaee354cb61e9d9de9c15"}, {file = "ruff-0.4.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba1f14df3c758dd7de5b55fbae7e1c8af238597961e5fb628f3de446c3c40c5"},
{file = "ruff-0.4.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:60ed88b636a463214905c002fa3eaab19795679ed55529f91e488db3fe8976ab"}, {file = "ruff-0.4.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:819fb06d535cc76dfddbfe8d3068ff602ddeb40e3eacbc90e0d1272bb8d97113"},
{file = "ruff-0.4.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b90fc5e170fc71c712cc4d9ab0e24ea505c6a9e4ebf346787a67e691dfb72e85"}, {file = "ruff-0.4.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0bfc9e955e6dc6359eb6f82ea150c4f4e82b660e5b58d9a20a0e42ec3bb6342b"},
{file = "ruff-0.4.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:8e7e6ebc10ef16dcdc77fd5557ee60647512b400e4a60bdc4849468f076f6eef"}, {file = "ruff-0.4.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:510a67d232d2ebe983fddea324dbf9d69b71c4d2dfeb8a862f4a127536dd4cfb"},
{file = "ruff-0.4.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b9ddb2c494fb79fc208cd15ffe08f32b7682519e067413dbaf5f4b01a6087bcd"}, {file = "ruff-0.4.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc9ff11cd9a092ee7680a56d21f302bdda14327772cd870d806610a3503d001f"},
{file = "ruff-0.4.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c51c928a14f9f0a871082603e25a1588059b7e08a920f2f9fa7157b5bf08cfe9"}, {file = "ruff-0.4.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:29efff25bf9ee685c2c8390563a5b5c006a3fee5230d28ea39f4f75f9d0b6f2f"},
{file = "ruff-0.4.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b5eb0a4bfd6400b7d07c09a7725e1a98c3b838be557fee229ac0f84d9aa49c36"}, {file = "ruff-0.4.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18b00e0bcccf0fc8d7186ed21e311dffd19761cb632241a6e4fe4477cc80ef6e"},
{file = "ruff-0.4.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b1867ee9bf3acc21778dcb293db504692eda5f7a11a6e6cc40890182a9f9e595"}, {file = "ruff-0.4.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:262f5635e2c74d80b7507fbc2fac28fe0d4fef26373bbc62039526f7722bca1b"},
{file = "ruff-0.4.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1aecced1269481ef2894cc495647392a34b0bf3e28ff53ed95a385b13aa45768"}, {file = "ruff-0.4.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7363691198719c26459e08cc17c6a3dac6f592e9ea3d2fa772f4e561b5fe82a3"},
{file = "ruff-0.4.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9da73eb616b3241a307b837f32756dc20a0b07e2bcb694fec73699c93d04a69e"}, {file = "ruff-0.4.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:eeb039f8428fcb6725bb63cbae92ad67b0559e68b5d80f840f11914afd8ddf7f"},
{file = "ruff-0.4.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:958b4ea5589706a81065e2a776237de2ecc3e763342e5cc8e02a4a4d8a5e6f95"}, {file = "ruff-0.4.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:927b11c1e4d0727ce1a729eace61cee88a334623ec424c0b1c8fe3e5f9d3c865"},
{file = "ruff-0.4.4-py3-none-win32.whl", hash = "sha256:cb53473849f011bca6e754f2cdf47cafc9c4f4ff4570003a0dad0b9b6890e876"}, {file = "ruff-0.4.3-py3-none-win32.whl", hash = "sha256:25cacda2155778beb0d064e0ec5a3944dcca9c12715f7c4634fd9d93ac33fd30"},
{file = "ruff-0.4.4-py3-none-win_amd64.whl", hash = "sha256:424e5b72597482543b684c11def82669cc6b395aa8cc69acc1858b5ef3e5daae"}, {file = "ruff-0.4.3-py3-none-win_amd64.whl", hash = "sha256:7a1c3a450bc6539ef00da6c819fb1b76b6b065dec585f91456e7c0d6a0bbc725"},
{file = "ruff-0.4.4-py3-none-win_arm64.whl", hash = "sha256:39df0537b47d3b597293edbb95baf54ff5b49589eb7ff41926d8243caa995ea6"}, {file = "ruff-0.4.3-py3-none-win_arm64.whl", hash = "sha256:71ca5f8ccf1121b95a59649482470c5601c60a416bf189d553955b0338e34614"},
{file = "ruff-0.4.4.tar.gz", hash = "sha256:f87ea42d5cdebdc6a69761a9d0bc83ae9b3b30d0ad78952005ba6568d6c022af"}, {file = "ruff-0.4.3.tar.gz", hash = "sha256:ff0a3ef2e3c4b6d133fbedcf9586abfbe38d076041f2dc18ffb2c7e0485d5a07"},
] ]
[[package]] [[package]]

View File

@ -134,7 +134,7 @@ debug = [
dev = [ dev = [
"django-extensions~=3.2", "django-extensions~=3.2",
"ipython~=8.24", "ipython~=8.24",
"hypothesis[django]~=6.102", "hypothesis[django]~=6.100",
"tblib~=3.0", "tblib~=3.0",
] ]