doorcontrol: Add syncing of members and policies with UniFi Access

This commit is contained in:
Adam Goldsmith 2024-12-09 13:31:50 -05:00
parent 431dfb723b
commit df4abbbe2f
9 changed files with 491 additions and 104 deletions

View File

@ -240,6 +240,9 @@ class NonCIBase(Base):
HID_DOOR_USERNAME = values.Value(environ_required=True, environ_prefix=None) HID_DOOR_USERNAME = values.Value(environ_required=True, environ_prefix=None)
HID_DOOR_PASSWORD = values.SecretValue(environ_prefix=None) HID_DOOR_PASSWORD = values.SecretValue(environ_prefix=None)
UNIFI_ACCESS_HOST = values.Value(environ_prefix=None)
UNIFI_ACCESS_API_TOKEN = values.SecretValue(environ_prefix=None)
# TODO: should validate emails (but EmailValidator doesn't handle name parts) # TODO: should validate emails (but EmailValidator doesn't handle name parts)
INVOICE_HANDLERS = values.ListValue( INVOICE_HANDLERS = values.ListValue(
environ_required=True, environ_prefix="CMSMANAGE" environ_required=True, environ_prefix="CMSMANAGE"

View File

@ -3,7 +3,14 @@ from django.contrib import admin
from django_object_actions import DjangoObjectActions, action from django_object_actions import DjangoObjectActions, action
from .forms import AttributeScheduleRuleForm, DoorAdminForm from .forms import AttributeScheduleRuleForm, DoorAdminForm
from .models import AttributeScheduleRule, Door, FlagScheduleRule, HIDEvent, Schedule from .models import (
ActiveEventInstructorRule,
AttributeScheduleRule,
Door,
FlagScheduleRule,
HIDEvent,
Schedule,
)
from .tasks.scrapehidevents import q_getMessagesAllDoors from .tasks.scrapehidevents import q_getMessagesAllDoors
@ -19,9 +26,18 @@ class AttributeScheduleRuleInline(admin.TabularInline):
extra = 0 extra = 0
class ActiveEventInstructorRuleInline(admin.TabularInline):
model = ActiveEventInstructorRule
extra = 0
@admin.register(Schedule) @admin.register(Schedule)
class ScheduleAdmin(admin.ModelAdmin): class ScheduleAdmin(admin.ModelAdmin):
inlines = [FlagScheduleRuleInline, AttributeScheduleRuleInline] inlines = [
FlagScheduleRuleInline,
AttributeScheduleRuleInline,
ActiveEventInstructorRuleInline,
]
@admin.register(Door) @admin.register(Door)

View File

@ -9,6 +9,7 @@ def post_migrate_callback(sender, **kwargs):
from .tasks.scrapehidevents import q_getMessagesAllDoors from .tasks.scrapehidevents import q_getMessagesAllDoors
from .tasks.update_doors import q_update_all_doors from .tasks.update_doors import q_update_all_doors
from .tasks.update_unifi_access import update_access
ensure_scheduled( ensure_scheduled(
q_getMessagesAllDoors, q_getMessagesAllDoors,
@ -22,6 +23,12 @@ def post_migrate_callback(sender, **kwargs):
minutes=15, minutes=15,
) )
ensure_scheduled(
update_access,
schedule_type=Schedule.MINUTES,
minutes=5,
)
class DoorControlConfig(AppConfig): class DoorControlConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField" default_auto_field = "django.db.models.BigAutoField"

View File

@ -0,0 +1,21 @@
import logging
from django.core.management.base import BaseCommand
from doorcontrol.tasks.update_unifi_access import logger, update_access
class Command(BaseCommand):
def add_arguments(self, parser):
parser.add_argument("--dry-run", action="store_true")
def handle(self, *args, dry_run: bool, 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))
update_access()

View File

@ -0,0 +1,68 @@
# Generated by Django 5.1.3 on 2024-11-22 04:21
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("doorcontrol", "0002_alter_hidevent_raw_card_number"),
]
operations = [
migrations.AddField(
model_name="attributeschedulerule",
name="name",
field=models.CharField(
default="",
help_text="Used for creating/matching Unifi Access user groups. Do not change after creation without also changing name in Access.",
max_length=255,
unique=True,
),
preserve_default=False,
),
migrations.AddField(
model_name="flagschedulerule",
name="name",
field=models.CharField(
default="",
help_text="Used for creating/matching Unifi Access user groups. Do not change after creation without also changing name in Access.",
max_length=255,
unique=True,
),
preserve_default=False,
),
migrations.CreateModel(
name="ActiveEventInstructorRule",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"name",
models.CharField(
help_text="Used for creating/matching Unifi Access user groups. Do not change after creation without also changing name in Access.",
max_length=255,
unique=True,
),
),
("doors", models.ManyToManyField(to="doorcontrol.door")),
(
"schedule",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="doorcontrol.schedule",
),
),
],
options={
"abstract": False,
},
),
]

View File

@ -1,4 +1,4 @@
from datetime import datetime from datetime import datetime, timedelta
from typing import Self from typing import Self
from django.conf import settings from django.conf import settings
@ -7,8 +7,8 @@ from django.db.models import OuterRef, Q, Subquery
from django.utils import timezone from django.utils import timezone
from django.utils.functional import cached_property from django.utils.functional import cached_property
from membershipworks.models import EventExt, Member, MemberQuerySet
from membershipworks.models import Flag as MembershipWorksFlag from membershipworks.models import Flag as MembershipWorksFlag
from membershipworks.models import Member
from .hid.Credential import Credential, InvalidHexCode from .hid.Credential import Credential, InvalidHexCode
from .hid.DoorController import DoorController from .hid.DoorController import DoorController
@ -69,12 +69,20 @@ class Schedule(models.Model):
class AbstractScheduleRule(models.Model): class AbstractScheduleRule(models.Model):
name = models.CharField(
help_text="Used for creating/matching Unifi Access user groups. Do not change after creation without also changing name in Access.",
max_length=255,
unique=True,
)
schedule = models.ForeignKey(Schedule, on_delete=models.CASCADE) schedule = models.ForeignKey(Schedule, on_delete=models.CASCADE)
doors = models.ManyToManyField(Door) doors = models.ManyToManyField(Door)
class Meta: class Meta:
abstract = True abstract = True
def get_matching_members(self) -> MemberQuerySet:
raise NotImplementedError
class FlagScheduleRule(AbstractScheduleRule): class FlagScheduleRule(AbstractScheduleRule):
flag = models.ForeignKey( flag = models.ForeignKey(
@ -88,6 +96,25 @@ class FlagScheduleRule(AbstractScheduleRule):
def __str__(self) -> str: def __str__(self) -> str:
return f"{self.schedule} [flag: {self.flag}]" return f"{self.schedule} [flag: {self.flag}]"
def get_matching_members(self) -> MemberQuerySet:
return self.flag.members.all()
class ActiveEventInstructorRule(AbstractScheduleRule):
def __str__(self) -> str:
return "{self.schedule} [Active Instructor]"
# grant instructors access for ~1 hour around their class times
def get_matching_members(self) -> MemberQuerySet:
now = timezone.now()
margin = timedelta(hours=1)
active_event_instructors = EventExt.objects.filter(
occurred=True,
meeting_times__start__lt=now + margin,
meeting_times__end__gt=now - margin,
).values_list("instructor", flat=True)
return Member.objects.filter(eventinstructor__in=active_event_instructors)
class AttributeScheduleRule(AbstractScheduleRule): class AttributeScheduleRule(AbstractScheduleRule):
access_field = models.CharField( access_field = models.CharField(
@ -100,6 +127,9 @@ class AttributeScheduleRule(AbstractScheduleRule):
def __str__(self) -> str: def __str__(self) -> str:
return f"{self.schedule} [attribute: {self.access_field}]" return f"{self.schedule} [attribute: {self.access_field}]"
def get_matching_members(self) -> MemberQuerySet:
return Member.objects.filter(**{self.access_field: True})
class HIDEventQuerySet(models.QuerySet): class HIDEventQuerySet(models.QuerySet):
def with_member_id(self): def with_member_id(self):

View File

@ -0,0 +1,180 @@
import logging
from collections.abc import Mapping
from django.conf import settings
from django.db.models import Q
from unifi_access import AccessClient
from unifi_access.schemas import AccessPolicy, DoorResource, UserStatus
from unifi_access.schemas import User as AccessUser
from cmsmanage.django_q2_helper import q_task_group
from doorcontrol.models import (
AbstractScheduleRule,
ActiveEventInstructorRule,
AttributeScheduleRule,
Door,
FlagScheduleRule,
Schedule,
)
from membershipworks.models import Member
logger = logging.getLogger(__name__)
def update_user_groups(
unifi_access: AccessClient,
access_users_by_employee_number: Mapping[str, AccessUser],
) -> None:
groups = unifi_access.fetch_all_user_groups()
top_group = next(g for g in groups if g.up_id == "")
rule_groups = {g.name: g for g in groups if g.up_id == top_group.id}
access_policies_by_name = {
p.name: p for p in unifi_access.fetch_all_access_policies()
}
schedules_by_name = {s.name: s for s in unifi_access.fetch_all_schedules()}
doors_by_name = {d.name: d for d in unifi_access.fetch_all_doors()}
def sync_access_policy(schedule: Schedule, door: Door) -> AccessPolicy:
access_policy_name = f"{rule.schedule.name} | {door.name}"
expected_access_policy = {
"name": access_policy_name,
"resources": (
[DoorResource(id=doors_by_name[door.name].id)]
if door.name in doors_by_name
else []
),
"schedule_id": schedules_by_name[schedule.name].id,
}
if access_policy := access_policies_by_name.get(access_policy_name):
changes = {
k: v
for k, v in expected_access_policy.items()
if getattr(access_policy, k) != v
}
if changes:
logger.debug(
" - updating access policy %s: %s", access_policy_name, changes
)
access_policy = unifi_access.update_access_policy(
access_policy.id, **changes
)
access_policies_by_name[access_policy_name] = access_policy
else:
logger.debug(
" - creating access policy %s: %s",
access_policy_name,
expected_access_policy,
)
access_policy = unifi_access.create_access_policy(**expected_access_policy)
access_policies_by_name[access_policy_name] = access_policy
return access_policy
def update_groups_for_rule(rule: AbstractScheduleRule) -> None:
assert rule.name
logger.info("Syncing user group '%s'", rule.name)
rule_group = rule_groups.get(rule.name)
if not rule_group:
logger.debug(" - creating top level group for rule")
rule_group = unifi_access.create_user_group(rule.name)
# might be None if it already existed, but we already checked for that earlier
assert rule_group
door_groups = {g.name: g for g in groups if g.up_id == rule_group.id}
for door in rule.doors.all():
door_rule_group = door_groups.get(door.name)
if not door_rule_group:
logger.debug(" - creating child group for door %s", door.name)
door_rule_group = unifi_access.create_user_group(
door.name, up_id=rule_group.id
)
# might be None if it already existed, but we already checked for that earlier
assert door_rule_group
access_policy = sync_access_policy(rule.schedule, door)
unifi_access.assign_access_policy_to_user_group(
door_rule_group.id, [access_policy.id]
)
expected_group_members = (
rule.get_matching_members()
.with_is_active()
.filter(
Q(**{door.access_field: True})
& (
Q(is_active=True)
| Member.objects.has_flag("folder", "Misc. Access")
)
)
)
# all members should exist in Access by this point
expected_group_member_ids = [
access_users_by_employee_number[member.uid].id
for member in expected_group_members
]
if expected_group_member_ids:
unifi_access.assign_user_to_user_group(
door_rule_group.id, expected_group_member_ids
)
for rule in FlagScheduleRule.objects.all():
update_groups_for_rule(rule)
for rule in AttributeScheduleRule.objects.all():
update_groups_for_rule(rule)
# TODO: this could probably be done better by creating temporary
# schedules for active events
for rule in ActiveEventInstructorRule.objects.all():
update_groups_for_rule(rule)
def sync_members(access_client: AccessClient):
access_users_by_employee_number = {
user.employee_number: user
for user in access_client.fetch_all_users__unpaged()
if user.employee_number
}
for member in Member.objects.with_is_active().all():
logger.info("Syncing member %s", member)
expected_user = {
"first_name": member.first_name,
"last_name": member.last_name,
# TODO: omitted to avoid spamming members for now
# "user_email": member.email,
"employee_number": member.uid,
}
if access_user := access_users_by_employee_number.get(member.uid):
expected_user["status"] = (
UserStatus.ACTIVE if member.is_active else UserStatus.DEACTIVATED
)
changes = {
k: v for k, v in expected_user.items() if getattr(access_user, k) != v
}
if changes:
logger.debug(" - updating, changes: %s", changes)
access_client.update_user(
**changes,
user_id=access_user.id,
)
else:
logger.debug(" - creating user: %s", expected_user)
access_client.register_user(**expected_user)
update_user_groups(access_client, access_users_by_employee_number)
@q_task_group("Update UniFi Access Data")
def update_access():
access_client = AccessClient(
host=settings.UNIFI_ACCESS_HOST,
api_token=settings.UNIFI_ACCESS_API_TOKEN,
# TODO: fix SSL cert
verify=False,
)
sync_members(access_client)

259
pdm.lock generated
View File

@ -5,7 +5,7 @@
groups = ["default", "debug", "dev", "lint", "server", "typing"] groups = ["default", "debug", "dev", "lint", "server", "typing"]
strategy = ["inherit_metadata"] strategy = ["inherit_metadata"]
lock_version = "4.5.0" lock_version = "4.5.0"
content_hash = "sha256:47e7818f755b3b8636d346c37afd7287cec977605c1bcdb3eccbb78e6e2823af" content_hash = "sha256:3426371550c0b7215623ca93958c30dfa60f948c234edfdea80e7a43941abfc7"
[[metadata.targets]] [[metadata.targets]]
requires_python = "==3.11.*" requires_python = "==3.11.*"
@ -15,35 +15,36 @@ gil_disabled = false
[[package]] [[package]]
name = "aiohappyeyeballs" name = "aiohappyeyeballs"
version = "2.4.3" version = "2.4.4"
requires_python = ">=3.8" requires_python = ">=3.8"
summary = "Happy Eyeballs for asyncio" summary = "Happy Eyeballs for asyncio"
groups = ["default"] groups = ["default"]
marker = "python_version == \"3.11\"" marker = "python_version == \"3.11\""
files = [ files = [
{file = "aiohappyeyeballs-2.4.3-py3-none-any.whl", hash = "sha256:8a7a83727b2756f394ab2895ea0765a0a8c475e3c71e98d43d76f22b4b435572"}, {file = "aiohappyeyeballs-2.4.4-py3-none-any.whl", hash = "sha256:a980909d50efcd44795c4afeca523296716d50cd756ddca6af8c65b996e27de8"},
{file = "aiohappyeyeballs-2.4.3.tar.gz", hash = "sha256:75cf88a15106a5002a8eb1dab212525c00d1f4c0fa96e551c9fbe6f09a621586"}, {file = "aiohappyeyeballs-2.4.4.tar.gz", hash = "sha256:5fdd7d87889c63183afc18ce9271f9b0a7d32c2303e394468dd45d514a757745"},
] ]
[[package]] [[package]]
name = "aiohttp" name = "aiohttp"
version = "3.10.10" version = "3.11.10"
requires_python = ">=3.8" requires_python = ">=3.9"
summary = "Async http client/server framework (asyncio)" summary = "Async http client/server framework (asyncio)"
groups = ["default"] groups = ["default"]
marker = "python_version == \"3.11\"" marker = "python_version == \"3.11\""
dependencies = [ dependencies = [
"aiohappyeyeballs>=2.3.0", "aiohappyeyeballs>=2.3.0",
"aiosignal>=1.1.2", "aiosignal>=1.1.2",
"async-timeout<5.0,>=4.0; python_version < \"3.11\"", "async-timeout<6.0,>=4.0; python_version < \"3.11\"",
"attrs>=17.3.0", "attrs>=17.3.0",
"frozenlist>=1.1.1", "frozenlist>=1.1.1",
"multidict<7.0,>=4.5", "multidict<7.0,>=4.5",
"yarl<2.0,>=1.12.0", "propcache>=0.2.0",
"yarl<2.0,>=1.17.0",
] ]
files = [ files = [
{file = "aiohttp-3.10.10-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:679abe5d3858b33c2cf74faec299fda60ea9de62916e8b67e625d65bf069a3b7"}, {file = "aiohttp-3.11.10-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98f5635f7b74bcd4f6f72fcd85bea2154b323a9f05226a80bc7398d0c90763b0"},
{file = "aiohttp-3.10.10.tar.gz", hash = "sha256:0631dd7c9f0822cc61c88586ca76d5b5ada26538097d0f1df510b082bad3411a"}, {file = "aiohttp-3.11.10.tar.gz", hash = "sha256:b1fc6b45010a8d0ff9e88f9f2418c6fd408c99c211257334aff41597ebece42e"},
] ]
[[package]] [[package]]
@ -61,9 +62,24 @@ files = [
{file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"}, {file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"},
] ]
[[package]]
name = "annotated-types"
version = "0.7.0"
requires_python = ">=3.8"
summary = "Reusable constraint types to use with typing.Annotated"
groups = ["default"]
marker = "python_version == \"3.11\""
dependencies = [
"typing-extensions>=4.0.0; python_version < \"3.9\"",
]
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"},
]
[[package]] [[package]]
name = "anyio" name = "anyio"
version = "4.6.2.post1" version = "4.7.0"
requires_python = ">=3.9" requires_python = ">=3.9"
summary = "High level compatibility layer for multiple asynchronous event loop implementations" summary = "High level compatibility layer for multiple asynchronous event loop implementations"
groups = ["server"] groups = ["server"]
@ -72,11 +88,11 @@ dependencies = [
"exceptiongroup>=1.0.2; python_version < \"3.11\"", "exceptiongroup>=1.0.2; python_version < \"3.11\"",
"idna>=2.8", "idna>=2.8",
"sniffio>=1.1", "sniffio>=1.1",
"typing-extensions>=4.1; python_version < \"3.11\"", "typing-extensions>=4.5; python_version < \"3.13\"",
] ]
files = [ files = [
{file = "anyio-4.6.2.post1-py3-none-any.whl", hash = "sha256:6d170c36fba3bdd840c73d3868c1e777e33676a69c3a72cf0a0d5d6d8009b61d"}, {file = "anyio-4.7.0-py3-none-any.whl", hash = "sha256:ea60c3723ab42ba6fff7e8ccb0488c898ec538ff4df1f1d5e642c3601d07e352"},
{file = "anyio-4.6.2.post1.tar.gz", hash = "sha256:4c8bc31ccdb51c7f7bd251f51c609e038d63e34219b44aa86e47576389880b4c"}, {file = "anyio-4.7.0.tar.gz", hash = "sha256:2f834749c602966b7d456a7567cafcb309f96482b5081d14ac93ccd457f9dd48"},
] ]
[[package]] [[package]]
@ -96,17 +112,14 @@ files = [
[[package]] [[package]]
name = "asttokens" name = "asttokens"
version = "2.4.1" version = "3.0.0"
requires_python = ">=3.8"
summary = "Annotate AST trees with source code positions" summary = "Annotate AST trees with source code positions"
groups = ["dev"] groups = ["dev"]
marker = "python_version == \"3.11\"" marker = "python_version == \"3.11\""
dependencies = [
"six>=1.12.0",
"typing; python_version < \"3.5\"",
]
files = [ files = [
{file = "asttokens-2.4.1-py2.py3-none-any.whl", hash = "sha256:051ed49c3dcae8913ea7cd08e46a606dba30b79993209636c4875bc1d637bc24"}, {file = "asttokens-3.0.0-py3-none-any.whl", hash = "sha256:e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2"},
{file = "asttokens-2.4.1.tar.gz", hash = "sha256:b03869718ba9a6eb027e134bfdf69f38a236d681c83c160d510768af11254ba0"}, {file = "asttokens-3.0.0.tar.gz", hash = "sha256:0dcd8baa8d62b0c1d118b399b2ddba3c4aff271d0d7a9e0d4c1681c79035bbc7"},
] ]
[[package]] [[package]]
@ -269,14 +282,14 @@ files = [
[[package]] [[package]]
name = "coverage" name = "coverage"
version = "7.6.8" version = "7.6.9"
requires_python = ">=3.9" requires_python = ">=3.9"
summary = "Code coverage measurement for Python" summary = "Code coverage measurement for Python"
groups = ["dev"] groups = ["dev"]
marker = "python_version == \"3.11\"" marker = "python_version == \"3.11\""
files = [ files = [
{file = "coverage-7.6.8-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44e6c85bbdc809383b509d732b06419fb4544dca29ebe18480379633623baafb"}, {file = "coverage-7.6.9-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0824a28ec542a0be22f60c6ac36d679e0e262e5353203bea81d44ee81fe9c6d4"},
{file = "coverage-7.6.8.tar.gz", hash = "sha256:8b2b8503edb06822c86d82fa64a4a5cb0760bb8f31f26e138ec743f422f37cfc"}, {file = "coverage-7.6.9.tar.gz", hash = "sha256:4a8d8977b0c6ef5aeadcb644da9e69ae0dcfe66ec7f368c89c72e058bd71164d"},
] ]
[[package]] [[package]]
@ -964,20 +977,20 @@ files = [
[[package]] [[package]]
name = "fonttools" name = "fonttools"
version = "4.54.1" version = "4.55.2"
requires_python = ">=3.8" requires_python = ">=3.8"
summary = "Tools to manipulate font files" summary = "Tools to manipulate font files"
groups = ["default"] groups = ["default"]
marker = "python_version == \"3.11\"" marker = "python_version == \"3.11\""
files = [ files = [
{file = "fonttools-4.54.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82834962b3d7c5ca98cb56001c33cf20eb110ecf442725dc5fdf36d16ed1ab07"}, {file = "fonttools-4.55.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:131591ac8d7a47043aaf29581aba755ae151d46e49d2bf49608601efd71e8b4d"},
{file = "fonttools-4.54.1-py3-none-any.whl", hash = "sha256:37cddd62d83dc4f72f7c3f3c2bcf2697e89a30efb152079896544a93907733bd"}, {file = "fonttools-4.55.2-py3-none-any.whl", hash = "sha256:8e2d89fbe9b08d96e22c7a81ec04a4e8d8439c31223e2dc6f2f9fc8ff14bdf9f"},
{file = "fonttools-4.54.1.tar.gz", hash = "sha256:957f669d4922f92c171ba01bef7f29410668db09f6c02111e22b2bce446f3285"}, {file = "fonttools-4.55.2.tar.gz", hash = "sha256:45947e7b3f9673f91df125d375eb57b9a23f2a603f438a1aebf3171bffa7a205"},
] ]
[[package]] [[package]]
name = "fonttools" name = "fonttools"
version = "4.54.1" version = "4.55.2"
extras = ["woff"] extras = ["woff"]
requires_python = ">=3.8" requires_python = ">=3.8"
summary = "Tools to manipulate font files" summary = "Tools to manipulate font files"
@ -986,13 +999,13 @@ marker = "python_version == \"3.11\""
dependencies = [ dependencies = [
"brotli>=1.0.1; platform_python_implementation == \"CPython\"", "brotli>=1.0.1; platform_python_implementation == \"CPython\"",
"brotlicffi>=0.8.0; platform_python_implementation != \"CPython\"", "brotlicffi>=0.8.0; platform_python_implementation != \"CPython\"",
"fonttools==4.54.1", "fonttools==4.55.2",
"zopfli>=0.1.4", "zopfli>=0.1.4",
] ]
files = [ files = [
{file = "fonttools-4.54.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82834962b3d7c5ca98cb56001c33cf20eb110ecf442725dc5fdf36d16ed1ab07"}, {file = "fonttools-4.55.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:131591ac8d7a47043aaf29581aba755ae151d46e49d2bf49608601efd71e8b4d"},
{file = "fonttools-4.54.1-py3-none-any.whl", hash = "sha256:37cddd62d83dc4f72f7c3f3c2bcf2697e89a30efb152079896544a93907733bd"}, {file = "fonttools-4.55.2-py3-none-any.whl", hash = "sha256:8e2d89fbe9b08d96e22c7a81ec04a4e8d8439c31223e2dc6f2f9fc8ff14bdf9f"},
{file = "fonttools-4.54.1.tar.gz", hash = "sha256:957f669d4922f92c171ba01bef7f29410668db09f6c02111e22b2bce446f3285"}, {file = "fonttools-4.55.2.tar.gz", hash = "sha256:45947e7b3f9673f91df125d375eb57b9a23f2a603f438a1aebf3171bffa7a205"},
] ]
[[package]] [[package]]
@ -1010,7 +1023,7 @@ files = [
[[package]] [[package]]
name = "google-api-core" name = "google-api-core"
version = "2.22.0" version = "2.23.0"
requires_python = ">=3.7" requires_python = ">=3.7"
summary = "Google API client core library" summary = "Google API client core library"
groups = ["default", "typing"] groups = ["default", "typing"]
@ -1024,8 +1037,8 @@ dependencies = [
"requests<3.0.0.dev0,>=2.18.0", "requests<3.0.0.dev0,>=2.18.0",
] ]
files = [ files = [
{file = "google_api_core-2.22.0-py3-none-any.whl", hash = "sha256:a6652b6bd51303902494998626653671703c420f6f4c88cfd3f50ed723e9d021"}, {file = "google_api_core-2.23.0-py3-none-any.whl", hash = "sha256:c20100d4c4c41070cf365f1d8ddf5365915291b5eb11b83829fbd1c999b5122f"},
{file = "google_api_core-2.22.0.tar.gz", hash = "sha256:26f8d76b96477db42b55fd02a33aae4a42ec8b86b98b94969b7333a2c828bf35"}, {file = "google_api_core-2.23.0.tar.gz", hash = "sha256:2ceb087315e6af43f256704b871d99326b1f12a9d6ce99beaedec99ba26a0ace"},
] ]
[[package]] [[package]]
@ -1066,7 +1079,7 @@ files = [
[[package]] [[package]]
name = "google-auth" name = "google-auth"
version = "2.35.0" version = "2.36.0"
requires_python = ">=3.7" requires_python = ">=3.7"
summary = "Google Authentication Library" summary = "Google Authentication Library"
groups = ["default", "typing"] groups = ["default", "typing"]
@ -1077,8 +1090,8 @@ dependencies = [
"rsa<5,>=3.1.4", "rsa<5,>=3.1.4",
] ]
files = [ files = [
{file = "google_auth-2.35.0-py2.py3-none-any.whl", hash = "sha256:25df55f327ef021de8be50bad0dfd4a916ad0de96da86cd05661c9297723ad3f"}, {file = "google_auth-2.36.0-py2.py3-none-any.whl", hash = "sha256:51a15d47028b66fd36e5c64a82d2d57480075bccc7da37cde257fc94177a61fb"},
{file = "google_auth-2.35.0.tar.gz", hash = "sha256:f4c64ed4e01e8e8b646ef34c018f8bf3338df0c8e37d8b3bba40e7f574a3278a"}, {file = "google_auth-2.36.0.tar.gz", hash = "sha256:545e9618f2df0bcbb7dcbc45a546485b1212624716975a1ea5ae8149ce769ab1"},
] ]
[[package]] [[package]]
@ -1114,7 +1127,7 @@ files = [
[[package]] [[package]]
name = "googleapis-common-protos" name = "googleapis-common-protos"
version = "1.65.0" version = "1.66.0"
requires_python = ">=3.7" requires_python = ">=3.7"
summary = "Common protobufs used in Google APIs" summary = "Common protobufs used in Google APIs"
groups = ["default", "typing"] groups = ["default", "typing"]
@ -1123,8 +1136,8 @@ dependencies = [
"protobuf!=3.20.0,!=3.20.1,!=4.21.1,!=4.21.2,!=4.21.3,!=4.21.4,!=4.21.5,<6.0.0.dev0,>=3.20.2", "protobuf!=3.20.0,!=3.20.1,!=4.21.1,!=4.21.2,!=4.21.3,!=4.21.4,!=4.21.5,<6.0.0.dev0,>=3.20.2",
] ]
files = [ files = [
{file = "googleapis_common_protos-1.65.0-py2.py3-none-any.whl", hash = "sha256:2972e6c496f435b92590fd54045060867f3fe9be2c82ab148fc8885035479a63"}, {file = "googleapis_common_protos-1.66.0-py2.py3-none-any.whl", hash = "sha256:d7abcd75fabb2e0ec9f74466401f6c119a0b498e27370e9be4c94cb7e382b8ed"},
{file = "googleapis_common_protos-1.65.0.tar.gz", hash = "sha256:334a29d07cddc3aa01dee4988f9afd9b2916ee2ff49d6b757155dc0d197852c0"}, {file = "googleapis_common_protos-1.66.0.tar.gz", hash = "sha256:c3e7b33d15fdca5374cc0a7346dd92ffa847425cc4ea941d970f13680052ec8c"},
] ]
[[package]] [[package]]
@ -1243,17 +1256,17 @@ files = [
[[package]] [[package]]
name = "jedi" name = "jedi"
version = "0.19.1" version = "0.19.2"
requires_python = ">=3.6" requires_python = ">=3.6"
summary = "An autocompletion tool for Python that can be used for text editors." summary = "An autocompletion tool for Python that can be used for text editors."
groups = ["dev"] groups = ["dev"]
marker = "python_version == \"3.11\"" marker = "python_version == \"3.11\""
dependencies = [ dependencies = [
"parso<0.9.0,>=0.8.3", "parso<0.9.0,>=0.8.4",
] ]
files = [ files = [
{file = "jedi-0.19.1-py2.py3-none-any.whl", hash = "sha256:e983c654fe5c02867aef4cdfce5a2fbb4a50adc0af145f70504238f18ef5e7e0"}, {file = "jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9"},
{file = "jedi-0.19.1.tar.gz", hash = "sha256:cf0496f3651bc65d7174ac1b7d043eff454892c708a87d1b683e57b569927ffd"}, {file = "jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0"},
] ]
[[package]] [[package]]
@ -1272,14 +1285,14 @@ files = [
[[package]] [[package]]
name = "json5" name = "json5"
version = "0.9.25" version = "0.10.0"
requires_python = ">=3.8" requires_python = ">=3.8.0"
summary = "A Python implementation of the JSON5 data format." summary = "A Python implementation of the JSON5 data format."
groups = ["lint"] groups = ["lint"]
marker = "python_version == \"3.11\"" marker = "python_version == \"3.11\""
files = [ files = [
{file = "json5-0.9.25-py3-none-any.whl", hash = "sha256:34ed7d834b1341a86987ed52f3f76cd8ee184394906b6e22a1e0deb9ab294e8f"}, {file = "json5-0.10.0-py3-none-any.whl", hash = "sha256:19b23410220a7271e8377f81ba8aacba2fdd56947fbb137ee5977cbe1f5e8dfa"},
{file = "json5-0.9.25.tar.gz", hash = "sha256:548e41b9be043f9426776f05df8635a00fe06104ea51ed24b67f908856e151ae"}, {file = "json5-0.10.0.tar.gz", hash = "sha256:e66941c8f0a02026943c52c2eb34ebeb2a6f819a0be05920a6f5243cd30fd559"},
] ]
[[package]] [[package]]
@ -1516,14 +1529,14 @@ files = [
[[package]] [[package]]
name = "packaging" name = "packaging"
version = "24.1" version = "24.2"
requires_python = ">=3.8" requires_python = ">=3.8"
summary = "Core utilities for Python packages" summary = "Core utilities for Python packages"
groups = ["default"] groups = ["default"]
marker = "python_version == \"3.11\"" marker = "python_version == \"3.11\""
files = [ files = [
{file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"},
{file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"},
] ]
[[package]] [[package]]
@ -1594,15 +1607,15 @@ files = [
[[package]] [[package]]
name = "propcache" name = "propcache"
version = "0.2.0" version = "0.2.1"
requires_python = ">=3.8" requires_python = ">=3.9"
summary = "Accelerated property cache" summary = "Accelerated property cache"
groups = ["default"] groups = ["default"]
marker = "python_version == \"3.11\"" marker = "python_version == \"3.11\""
files = [ files = [
{file = "propcache-0.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c7dde9e533c0a49d802b4f3f218fa9ad0a1ce21f2c2eb80d5216565202acab4"}, {file = "propcache-0.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf6c4150f8c0e32d241436526f3c3f9cbd34429492abddbada2ffcff506c51af"},
{file = "propcache-0.2.0-py3-none-any.whl", hash = "sha256:2ccc28197af5313706511fab3a8b66dcd6da067a1331372c82ea1cb74285e036"}, {file = "propcache-0.2.1-py3-none-any.whl", hash = "sha256:52277518d6aae65536e9cea52d4e7fd2f7a66f4aa2d30ed3f2fcea620ace3c54"},
{file = "propcache-0.2.0.tar.gz", hash = "sha256:df81779732feb9d01e5d513fad0122efb3d53bbc75f61b2a4f29a020bc985e70"}, {file = "propcache-0.2.1.tar.gz", hash = "sha256:3f77ce728b19cb537714499928fe800c3dda29e8d9428778fc7c186da4c09a64"},
] ]
[[package]] [[package]]
@ -1622,15 +1635,15 @@ files = [
[[package]] [[package]]
name = "protobuf" name = "protobuf"
version = "5.28.3" version = "5.29.1"
requires_python = ">=3.8" requires_python = ">=3.8"
summary = "" summary = ""
groups = ["default", "typing"] groups = ["default", "typing"]
marker = "python_version == \"3.11\"" marker = "python_version == \"3.11\""
files = [ files = [
{file = "protobuf-5.28.3-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:712319fbdddb46f21abb66cd33cb9e491a5763b2febd8f228251add221981135"}, {file = "protobuf-5.29.1-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:8ee1461b3af56145aca2800e6a3e2f928108c749ba8feccc6f5dd0062c410c0d"},
{file = "protobuf-5.28.3-py3-none-any.whl", hash = "sha256:cee1757663fa32a1ee673434fcf3bf24dd54763c79690201208bafec62f19eed"}, {file = "protobuf-5.29.1-py3-none-any.whl", hash = "sha256:32600ddb9c2a53dedc25b8581ea0f1fd8ea04956373c0c07577ce58d312522e0"},
{file = "protobuf-5.28.3.tar.gz", hash = "sha256:64badbc49180a5e401f373f9ce7ab1d18b63f7dd4a9cdc43c92b9f0b481cef7b"}, {file = "protobuf-5.29.1.tar.gz", hash = "sha256:683be02ca21a6ffe80db6dd02c0b5b2892322c59ca57fd6c872d652cb80549cb"},
] ]
[[package]] [[package]]
@ -1663,7 +1676,7 @@ files = [
[[package]] [[package]]
name = "psycopg-pool" name = "psycopg-pool"
version = "3.2.3" version = "3.2.4"
requires_python = ">=3.8" requires_python = ">=3.8"
summary = "Connection Pool for Psycopg" summary = "Connection Pool for Psycopg"
groups = ["default"] groups = ["default"]
@ -1672,8 +1685,8 @@ dependencies = [
"typing-extensions>=4.6", "typing-extensions>=4.6",
] ]
files = [ files = [
{file = "psycopg_pool-3.2.3-py3-none-any.whl", hash = "sha256:53bd8e640625e01b2927b2ad96df8ed8e8f91caea4597d45e7673fc7bbb85eb1"}, {file = "psycopg_pool-3.2.4-py3-none-any.whl", hash = "sha256:f6a22cff0f21f06d72fb2f5cb48c618946777c49385358e0c88d062c59cbd224"},
{file = "psycopg_pool-3.2.3.tar.gz", hash = "sha256:bb942f123bef4b7fbe4d55421bd3fb01829903c95c0f33fd42b7e94e5ac9b52a"}, {file = "psycopg_pool-3.2.4.tar.gz", hash = "sha256:61774b5bbf23e8d22bedc7504707135aaf744679f8ef9b3fe29942920746a6ed"},
] ]
[[package]] [[package]]
@ -1755,6 +1768,38 @@ files = [
{file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"},
] ]
[[package]]
name = "pydantic"
version = "2.10.3"
requires_python = ">=3.8"
summary = "Data validation using Python type hints"
groups = ["default"]
marker = "python_version == \"3.11\""
dependencies = [
"annotated-types>=0.6.0",
"pydantic-core==2.27.1",
"typing-extensions>=4.12.2",
]
files = [
{file = "pydantic-2.10.3-py3-none-any.whl", hash = "sha256:be04d85bbc7b65651c5f8e6b9976ed9c6f41782a55524cef079a34a0bb82144d"},
{file = "pydantic-2.10.3.tar.gz", hash = "sha256:cb5ac360ce894ceacd69c403187900a02c4b20b693a9dd1d643e1effab9eadf9"},
]
[[package]]
name = "pydantic-core"
version = "2.27.1"
requires_python = ">=3.8"
summary = "Core functionality for Pydantic validation and serialization"
groups = ["default"]
marker = "python_version == \"3.11\""
dependencies = [
"typing-extensions!=4.7.0,>=4.6.0",
]
files = [
{file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acc07b2cfc5b835444b44a9956846b578d27beeacd4b52e45489e93276241025"},
{file = "pydantic_core-2.27.1.tar.gz", hash = "sha256:62a763352879b84aa31058fc931884055fd75089cccbd9d58bb6afd01141b235"},
]
[[package]] [[package]]
name = "pydyf" name = "pydyf"
version = "0.11.0" version = "0.11.0"
@ -1874,14 +1919,14 @@ files = [
[[package]] [[package]]
name = "regex" name = "regex"
version = "2024.9.11" version = "2024.11.6"
requires_python = ">=3.8" requires_python = ">=3.8"
summary = "Alternative regular expression module, to replace re." summary = "Alternative regular expression module, to replace re."
groups = ["lint"] groups = ["lint"]
marker = "python_version == \"3.11\"" marker = "python_version == \"3.11\""
files = [ files = [
{file = "regex-2024.9.11-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6113c008a7780792efc80f9dfe10ba0cd043cbf8dc9a76ef757850f51b4edc50"}, {file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a19f302cd1ce5dd01a9099aaa19cae6173306d1302a43b627f62e21cf18ac0"},
{file = "regex-2024.9.11.tar.gz", hash = "sha256:6c188c307e8433bcb63dc1915022deb553b4203a70722fc542c363bf120a01fd"}, {file = "regex-2024.11.6.tar.gz", hash = "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519"},
] ]
[[package]] [[package]]
@ -1988,14 +2033,14 @@ files = [
[[package]] [[package]]
name = "six" name = "six"
version = "1.16.0" version = "1.17.0"
requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
summary = "Python 2 and 3 compatibility utilities" summary = "Python 2 and 3 compatibility utilities"
groups = ["default", "dev", "lint"] groups = ["default", "lint"]
marker = "python_version == \"3.11\"" marker = "python_version == \"3.11\""
files = [ files = [
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"},
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"},
] ]
[[package]] [[package]]
@ -2035,14 +2080,14 @@ files = [
[[package]] [[package]]
name = "sqlparse" name = "sqlparse"
version = "0.5.1" version = "0.5.2"
requires_python = ">=3.8" requires_python = ">=3.8"
summary = "A non-validating SQL parser." summary = "A non-validating SQL parser."
groups = ["default", "debug", "dev", "typing"] groups = ["default", "debug", "dev", "typing"]
marker = "python_version == \"3.11\"" marker = "python_version == \"3.11\""
files = [ files = [
{file = "sqlparse-0.5.1-py3-none-any.whl", hash = "sha256:773dcbf9a5ab44a090f3441e2180efe2560220203dc2f8c0b0fa141e18b505e4"}, {file = "sqlparse-0.5.2-py3-none-any.whl", hash = "sha256:e99bc85c78160918c3e1d9230834ab8d80fc06c59d03f8db2618f65f65dda55e"},
{file = "sqlparse-0.5.1.tar.gz", hash = "sha256:bb6b4df465655ef332548e24f08e205afc81b9ab86cb1c45657a7ff173a3a00e"}, {file = "sqlparse-0.5.2.tar.gz", hash = "sha256:9e37b35e16d1cc652a2545f0997c1deb23ea28fa1f3eefe609eee3063c3b105f"},
] ]
[[package]] [[package]]
@ -2150,7 +2195,7 @@ files = [
[[package]] [[package]]
name = "tqdm" name = "tqdm"
version = "4.66.6" version = "4.67.1"
requires_python = ">=3.7" requires_python = ">=3.7"
summary = "Fast, Extensible Progress Meter" summary = "Fast, Extensible Progress Meter"
groups = ["lint"] groups = ["lint"]
@ -2159,8 +2204,8 @@ dependencies = [
"colorama; platform_system == \"Windows\"", "colorama; platform_system == \"Windows\"",
] ]
files = [ files = [
{file = "tqdm-4.66.6-py3-none-any.whl", hash = "sha256:223e8b5359c2efc4b30555531f09e9f2f3589bcd7fdd389271191031b49b7a63"}, {file = "tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2"},
{file = "tqdm-4.66.6.tar.gz", hash = "sha256:4bdd694238bef1485ce839d67967ab50af8f9272aab687c0d7702a01da0be090"}, {file = "tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2"},
] ]
[[package]] [[package]]
@ -2207,14 +2252,14 @@ files = [
[[package]] [[package]]
name = "types-docutils" name = "types-docutils"
version = "0.21.0.20241005" version = "0.21.0.20241128"
requires_python = ">=3.8" requires_python = ">=3.8"
summary = "Typing stubs for docutils" summary = "Typing stubs for docutils"
groups = ["typing"] groups = ["typing"]
marker = "python_version == \"3.11\"" marker = "python_version == \"3.11\""
files = [ files = [
{file = "types-docutils-0.21.0.20241005.tar.gz", hash = "sha256:48f804a2b50da3a1b1681c4ca1b6184416a6e4129e302d15c44e9d97c59b3365"}, {file = "types_docutils-0.21.0.20241128-py3-none-any.whl", hash = "sha256:e0409204009639e9b0bf4521eeabe58b5e574ce9c0db08421c2ac26c32be0039"},
{file = "types_docutils-0.21.0.20241005-py3-none-any.whl", hash = "sha256:4d9021422f2f3fca8b0726fb8949395f66a06c0d951479eb3b1387d75b134430"}, {file = "types_docutils-0.21.0.20241128.tar.gz", hash = "sha256:4dd059805b83ac6ec5a223699195c4e9eeb0446a4f7f2aeff1759a4a7cc17473"},
] ]
[[package]] [[package]]
@ -2339,14 +2384,14 @@ files = [
[[package]] [[package]]
name = "types-setuptools" name = "types-setuptools"
version = "75.2.0.20241025" version = "75.6.0.20241126"
requires_python = ">=3.8" requires_python = ">=3.8"
summary = "Typing stubs for setuptools" summary = "Typing stubs for setuptools"
groups = ["typing"] groups = ["typing"]
marker = "python_version == \"3.11\"" marker = "python_version == \"3.11\""
files = [ files = [
{file = "types-setuptools-75.2.0.20241025.tar.gz", hash = "sha256:2949913a518d5285ce00a3b7d88961c80a6e72ffb8f3da0a3f5650ea533bd45e"}, {file = "types_setuptools-75.6.0.20241126-py3-none-any.whl", hash = "sha256:aaae310a0e27033c1da8457d4d26ac673b0c8a0de7272d6d4708e263f2ea3b9b"},
{file = "types_setuptools-75.2.0.20241025-py3-none-any.whl", hash = "sha256:6721ac0f1a620321e2ccd87a9a747c4a383dc381f78d894ce37f2455b45fcf1c"}, {file = "types_setuptools-75.6.0.20241126.tar.gz", hash = "sha256:7bf25ad4be39740e469f9268b6beddda6e088891fa5a27e985c6ce68bf62ace0"},
] ]
[[package]] [[package]]
@ -2365,7 +2410,7 @@ name = "typing-extensions"
version = "4.12.2" version = "4.12.2"
requires_python = ">=3.8" requires_python = ">=3.8"
summary = "Backported and Experimental Type Hints for Python 3.8+" summary = "Backported and Experimental Type Hints for Python 3.8+"
groups = ["default", "dev", "typing"] groups = ["default", "dev", "server", "typing"]
marker = "python_version == \"3.11\"" marker = "python_version == \"3.11\""
files = [ files = [
{file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"},
@ -2391,6 +2436,22 @@ files = [
{file = "udm_rest_client-1.2.3-py2.py3-none-any.whl", hash = "sha256:ee29e94e3ba5fba63a694e33d119b1af7450afcdce3a44301d4cd5ddfa1f980b"}, {file = "udm_rest_client-1.2.3-py2.py3-none-any.whl", hash = "sha256:ee29e94e3ba5fba63a694e33d119b1af7450afcdce3a44301d4cd5ddfa1f980b"},
] ]
[[package]]
name = "unifi-access"
version = "0.1.0"
requires_python = ">=3.11"
summary = "Typed wrapper for the Unifi Access Public API"
groups = ["default"]
marker = "python_version == \"3.11\""
dependencies = [
"pydantic>=2.10.1",
"requests>=2.32.3",
]
files = [
{file = "unifi_access-0.1.0-py3-none-any.whl", hash = "sha256:a901099d8b900e0266fce27a8d9a4bf4aee53fe5edf9a4110a7f45431599f7d4"},
{file = "unifi_access-0.1.0.tar.gz", hash = "sha256:6a61a491f9d36d2208bbf73da0e3bb303f385c0b22a708900a4000b58d4532af"},
]
[[package]] [[package]]
name = "uritemplate" name = "uritemplate"
version = "4.1.1" version = "4.1.1"
@ -2469,8 +2530,8 @@ files = [
[[package]] [[package]]
name = "watchfiles" name = "watchfiles"
version = "0.24.0" version = "1.0.0"
requires_python = ">=3.8" requires_python = ">=3.9"
summary = "Simple, modern and high performance file watching and code reload in python." summary = "Simple, modern and high performance file watching and code reload in python."
groups = ["server"] groups = ["server"]
marker = "python_version == \"3.11\"" marker = "python_version == \"3.11\""
@ -2478,8 +2539,8 @@ dependencies = [
"anyio>=3.0.0", "anyio>=3.0.0",
] ]
files = [ files = [
{file = "watchfiles-0.24.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa0fd7248cf533c259e59dc593a60973a73e881162b1a2f73360547132742823"}, {file = "watchfiles-1.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:245fab124b9faf58430da547512d91734858df13f2ddd48ecfa5e493455ffccb"},
{file = "watchfiles-0.24.0.tar.gz", hash = "sha256:afb72325b74fa7a428c009c1b8be4b4d7c2afedafb2982827ef2156646df2fe1"}, {file = "watchfiles-1.0.0.tar.gz", hash = "sha256:37566c844c9ce3b5deb964fe1a23378e575e74b114618d211fbda8f59d7b5dab"},
] ]
[[package]] [[package]]
@ -2543,20 +2604,20 @@ files = [
[[package]] [[package]]
name = "websockets" name = "websockets"
version = "13.1" version = "14.1"
requires_python = ">=3.8" requires_python = ">=3.9"
summary = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" summary = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)"
groups = ["server"] groups = ["server"]
marker = "python_version == \"3.11\"" marker = "python_version == \"3.11\""
files = [ files = [
{file = "websockets-13.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87c6e35319b46b99e168eb98472d6c7d8634ee37750d7693656dc766395df096"}, {file = "websockets-14.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6cf0ad281c979306a6a34242b371e90e891bce504509fb6bb5246bbbf31e7b6"},
{file = "websockets-13.1-py3-none-any.whl", hash = "sha256:a9a396a6ad26130cdae92ae10c36af09d9bfe6cafe69670fd3b6da9b07b4044f"}, {file = "websockets-14.1-py3-none-any.whl", hash = "sha256:4d4fc827a20abe6d544a119896f6b78ee13fe81cbfef416f3f2ddf09a03f0e2e"},
{file = "websockets-13.1.tar.gz", hash = "sha256:a3b3366087c1bc0a2795111edcadddb8b3b59509d5db5d7ea3fdd69f954a8878"}, {file = "websockets-14.1.tar.gz", hash = "sha256:398b10c77d471c0aab20a845e7a60076b6390bfdaac7a6d2edb0d2c59d75e8d8"},
] ]
[[package]] [[package]]
name = "yarl" name = "yarl"
version = "1.17.1" version = "1.18.3"
requires_python = ">=3.9" requires_python = ">=3.9"
summary = "Yet another URL library" summary = "Yet another URL library"
groups = ["default"] groups = ["default"]
@ -2567,9 +2628,9 @@ dependencies = [
"propcache>=0.2.0", "propcache>=0.2.0",
] ]
files = [ files = [
{file = "yarl-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d216e5d9b8749563c7f2c6f7a0831057ec844c68b4c11cb10fc62d4fd373c26d"}, {file = "yarl-1.18.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75674776d96d7b851b6498f17824ba17849d790a44d282929c42dbb77d4f17ae"},
{file = "yarl-1.17.1-py3-none-any.whl", hash = "sha256:f1790a4b1e8e8e028c391175433b9c8122c39b46e1663228158e61e6f915bf06"}, {file = "yarl-1.18.3-py3-none-any.whl", hash = "sha256:b57f4f58099328dfb26c6a771d09fb20dbbae81d20cfb66141251ea063bd101b"},
{file = "yarl-1.17.1.tar.gz", hash = "sha256:067a63fcfda82da6b198fa73079b1ca40b7c9b7994995b6ee38acda728b64d47"}, {file = "yarl-1.18.3.tar.gz", hash = "sha256:ac1801c45cbf77b6c99242eeff4fffb5e4e73a800b5c4ad4fc0be5def634d2e1"},
] ]
[[package]] [[package]]

View File

@ -56,6 +56,7 @@ dependencies = [
"semver~=3.0", "semver~=3.0",
"tablib[ods,xlsx]~=3.7", "tablib[ods,xlsx]~=3.7",
"udm-rest-client~=1.2", "udm-rest-client~=1.2",
"unifi-access~=0.1",
"weasyprint~=63.0", "weasyprint~=63.0",
] ]
optional-dependencies.server = [ optional-dependencies.server = [
@ -105,7 +106,7 @@ name = "pypi"
url = "https://git.claremontmakerspace.org/api/packages/CMS/pypi/simple" url = "https://git.claremontmakerspace.org/api/packages/CMS/pypi/simple"
verify_ssl = true verify_ssl = true
name = "CMS" name = "CMS"
include_packages = [ "openapi-client-udm" ] include_packages = [ "openapi-client-udm", "unifi-access" ]
exclude_packages = [ "*" ] exclude_packages = [ "*" ]
[tool.pdm.scripts] [tool.pdm.scripts]