From df4abbbe2f5b0cf20d35473ae639db3734278790 Mon Sep 17 00:00:00 2001 From: Adam Goldsmith Date: Mon, 9 Dec 2024 13:31:50 -0500 Subject: [PATCH] doorcontrol: Add syncing of members and policies with UniFi Access --- cmsmanage/settings.py | 3 + doorcontrol/admin.py | 20 +- doorcontrol/apps.py | 7 + .../commands/update_unifi_access.py | 21 ++ ...ule_name_flagschedulerule_name_and_more.py | 68 +++++ doorcontrol/models.py | 34 ++- doorcontrol/tasks/update_unifi_access.py | 180 ++++++++++++ pdm.lock | 259 +++++++++++------- pyproject.toml | 3 +- 9 files changed, 491 insertions(+), 104 deletions(-) create mode 100644 doorcontrol/management/commands/update_unifi_access.py create mode 100644 doorcontrol/migrations/0003_attributeschedulerule_name_flagschedulerule_name_and_more.py create mode 100644 doorcontrol/tasks/update_unifi_access.py diff --git a/cmsmanage/settings.py b/cmsmanage/settings.py index 9792629..065d588 100644 --- a/cmsmanage/settings.py +++ b/cmsmanage/settings.py @@ -240,6 +240,9 @@ class NonCIBase(Base): HID_DOOR_USERNAME = values.Value(environ_required=True, 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) INVOICE_HANDLERS = values.ListValue( environ_required=True, environ_prefix="CMSMANAGE" diff --git a/doorcontrol/admin.py b/doorcontrol/admin.py index 39cba0a..008740c 100644 --- a/doorcontrol/admin.py +++ b/doorcontrol/admin.py @@ -3,7 +3,14 @@ from django.contrib import admin from django_object_actions import DjangoObjectActions, action 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 @@ -19,9 +26,18 @@ class AttributeScheduleRuleInline(admin.TabularInline): extra = 0 +class ActiveEventInstructorRuleInline(admin.TabularInline): + model = ActiveEventInstructorRule + extra = 0 + + @admin.register(Schedule) class ScheduleAdmin(admin.ModelAdmin): - inlines = [FlagScheduleRuleInline, AttributeScheduleRuleInline] + inlines = [ + FlagScheduleRuleInline, + AttributeScheduleRuleInline, + ActiveEventInstructorRuleInline, + ] @admin.register(Door) diff --git a/doorcontrol/apps.py b/doorcontrol/apps.py index 5ca5095..2f31fc6 100644 --- a/doorcontrol/apps.py +++ b/doorcontrol/apps.py @@ -9,6 +9,7 @@ def post_migrate_callback(sender, **kwargs): from .tasks.scrapehidevents import q_getMessagesAllDoors from .tasks.update_doors import q_update_all_doors + from .tasks.update_unifi_access import update_access ensure_scheduled( q_getMessagesAllDoors, @@ -22,6 +23,12 @@ def post_migrate_callback(sender, **kwargs): minutes=15, ) + ensure_scheduled( + update_access, + schedule_type=Schedule.MINUTES, + minutes=5, + ) + class DoorControlConfig(AppConfig): default_auto_field = "django.db.models.BigAutoField" diff --git a/doorcontrol/management/commands/update_unifi_access.py b/doorcontrol/management/commands/update_unifi_access.py new file mode 100644 index 0000000..c44c843 --- /dev/null +++ b/doorcontrol/management/commands/update_unifi_access.py @@ -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() diff --git a/doorcontrol/migrations/0003_attributeschedulerule_name_flagschedulerule_name_and_more.py b/doorcontrol/migrations/0003_attributeschedulerule_name_flagschedulerule_name_and_more.py new file mode 100644 index 0000000..5e7e47b --- /dev/null +++ b/doorcontrol/migrations/0003_attributeschedulerule_name_flagschedulerule_name_and_more.py @@ -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, + }, + ), + ] diff --git a/doorcontrol/models.py b/doorcontrol/models.py index 36971df..86180a8 100644 --- a/doorcontrol/models.py +++ b/doorcontrol/models.py @@ -1,4 +1,4 @@ -from datetime import datetime +from datetime import datetime, timedelta from typing import Self 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.functional import cached_property +from membershipworks.models import EventExt, Member, MemberQuerySet from membershipworks.models import Flag as MembershipWorksFlag -from membershipworks.models import Member from .hid.Credential import Credential, InvalidHexCode from .hid.DoorController import DoorController @@ -69,12 +69,20 @@ class Schedule(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) doors = models.ManyToManyField(Door) class Meta: abstract = True + def get_matching_members(self) -> MemberQuerySet: + raise NotImplementedError + class FlagScheduleRule(AbstractScheduleRule): flag = models.ForeignKey( @@ -88,6 +96,25 @@ class FlagScheduleRule(AbstractScheduleRule): def __str__(self) -> str: 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): access_field = models.CharField( @@ -100,6 +127,9 @@ class AttributeScheduleRule(AbstractScheduleRule): def __str__(self) -> str: 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): def with_member_id(self): diff --git a/doorcontrol/tasks/update_unifi_access.py b/doorcontrol/tasks/update_unifi_access.py new file mode 100644 index 0000000..aecac99 --- /dev/null +++ b/doorcontrol/tasks/update_unifi_access.py @@ -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) diff --git a/pdm.lock b/pdm.lock index 660c00a..1e3c810 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "debug", "dev", "lint", "server", "typing"] strategy = ["inherit_metadata"] lock_version = "4.5.0" -content_hash = "sha256:47e7818f755b3b8636d346c37afd7287cec977605c1bcdb3eccbb78e6e2823af" +content_hash = "sha256:3426371550c0b7215623ca93958c30dfa60f948c234edfdea80e7a43941abfc7" [[metadata.targets]] requires_python = "==3.11.*" @@ -15,35 +15,36 @@ gil_disabled = false [[package]] name = "aiohappyeyeballs" -version = "2.4.3" +version = "2.4.4" requires_python = ">=3.8" summary = "Happy Eyeballs for asyncio" groups = ["default"] marker = "python_version == \"3.11\"" files = [ - {file = "aiohappyeyeballs-2.4.3-py3-none-any.whl", hash = "sha256:8a7a83727b2756f394ab2895ea0765a0a8c475e3c71e98d43d76f22b4b435572"}, - {file = "aiohappyeyeballs-2.4.3.tar.gz", hash = "sha256:75cf88a15106a5002a8eb1dab212525c00d1f4c0fa96e551c9fbe6f09a621586"}, + {file = "aiohappyeyeballs-2.4.4-py3-none-any.whl", hash = "sha256:a980909d50efcd44795c4afeca523296716d50cd756ddca6af8c65b996e27de8"}, + {file = "aiohappyeyeballs-2.4.4.tar.gz", hash = "sha256:5fdd7d87889c63183afc18ce9271f9b0a7d32c2303e394468dd45d514a757745"}, ] [[package]] name = "aiohttp" -version = "3.10.10" -requires_python = ">=3.8" +version = "3.11.10" +requires_python = ">=3.9" summary = "Async http client/server framework (asyncio)" groups = ["default"] marker = "python_version == \"3.11\"" dependencies = [ "aiohappyeyeballs>=2.3.0", "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", "frozenlist>=1.1.1", "multidict<7.0,>=4.5", - "yarl<2.0,>=1.12.0", + "propcache>=0.2.0", + "yarl<2.0,>=1.17.0", ] files = [ - {file = "aiohttp-3.10.10-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:679abe5d3858b33c2cf74faec299fda60ea9de62916e8b67e625d65bf069a3b7"}, - {file = "aiohttp-3.10.10.tar.gz", hash = "sha256:0631dd7c9f0822cc61c88586ca76d5b5ada26538097d0f1df510b082bad3411a"}, + {file = "aiohttp-3.11.10-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98f5635f7b74bcd4f6f72fcd85bea2154b323a9f05226a80bc7398d0c90763b0"}, + {file = "aiohttp-3.11.10.tar.gz", hash = "sha256:b1fc6b45010a8d0ff9e88f9f2418c6fd408c99c211257334aff41597ebece42e"}, ] [[package]] @@ -61,9 +62,24 @@ files = [ {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]] name = "anyio" -version = "4.6.2.post1" +version = "4.7.0" requires_python = ">=3.9" summary = "High level compatibility layer for multiple asynchronous event loop implementations" groups = ["server"] @@ -72,11 +88,11 @@ dependencies = [ "exceptiongroup>=1.0.2; python_version < \"3.11\"", "idna>=2.8", "sniffio>=1.1", - "typing-extensions>=4.1; python_version < \"3.11\"", + "typing-extensions>=4.5; python_version < \"3.13\"", ] files = [ - {file = "anyio-4.6.2.post1-py3-none-any.whl", hash = "sha256:6d170c36fba3bdd840c73d3868c1e777e33676a69c3a72cf0a0d5d6d8009b61d"}, - {file = "anyio-4.6.2.post1.tar.gz", hash = "sha256:4c8bc31ccdb51c7f7bd251f51c609e038d63e34219b44aa86e47576389880b4c"}, + {file = "anyio-4.7.0-py3-none-any.whl", hash = "sha256:ea60c3723ab42ba6fff7e8ccb0488c898ec538ff4df1f1d5e642c3601d07e352"}, + {file = "anyio-4.7.0.tar.gz", hash = "sha256:2f834749c602966b7d456a7567cafcb309f96482b5081d14ac93ccd457f9dd48"}, ] [[package]] @@ -96,17 +112,14 @@ files = [ [[package]] name = "asttokens" -version = "2.4.1" +version = "3.0.0" +requires_python = ">=3.8" summary = "Annotate AST trees with source code positions" groups = ["dev"] marker = "python_version == \"3.11\"" -dependencies = [ - "six>=1.12.0", - "typing; python_version < \"3.5\"", -] files = [ - {file = "asttokens-2.4.1-py2.py3-none-any.whl", hash = "sha256:051ed49c3dcae8913ea7cd08e46a606dba30b79993209636c4875bc1d637bc24"}, - {file = "asttokens-2.4.1.tar.gz", hash = "sha256:b03869718ba9a6eb027e134bfdf69f38a236d681c83c160d510768af11254ba0"}, + {file = "asttokens-3.0.0-py3-none-any.whl", hash = "sha256:e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2"}, + {file = "asttokens-3.0.0.tar.gz", hash = "sha256:0dcd8baa8d62b0c1d118b399b2ddba3c4aff271d0d7a9e0d4c1681c79035bbc7"}, ] [[package]] @@ -269,14 +282,14 @@ files = [ [[package]] name = "coverage" -version = "7.6.8" +version = "7.6.9" requires_python = ">=3.9" summary = "Code coverage measurement for Python" groups = ["dev"] marker = "python_version == \"3.11\"" 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.8.tar.gz", hash = "sha256:8b2b8503edb06822c86d82fa64a4a5cb0760bb8f31f26e138ec743f422f37cfc"}, + {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.9.tar.gz", hash = "sha256:4a8d8977b0c6ef5aeadcb644da9e69ae0dcfe66ec7f368c89c72e058bd71164d"}, ] [[package]] @@ -964,20 +977,20 @@ files = [ [[package]] name = "fonttools" -version = "4.54.1" +version = "4.55.2" requires_python = ">=3.8" summary = "Tools to manipulate font files" groups = ["default"] marker = "python_version == \"3.11\"" files = [ - {file = "fonttools-4.54.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82834962b3d7c5ca98cb56001c33cf20eb110ecf442725dc5fdf36d16ed1ab07"}, - {file = "fonttools-4.54.1-py3-none-any.whl", hash = "sha256:37cddd62d83dc4f72f7c3f3c2bcf2697e89a30efb152079896544a93907733bd"}, - {file = "fonttools-4.54.1.tar.gz", hash = "sha256:957f669d4922f92c171ba01bef7f29410668db09f6c02111e22b2bce446f3285"}, + {file = "fonttools-4.55.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:131591ac8d7a47043aaf29581aba755ae151d46e49d2bf49608601efd71e8b4d"}, + {file = "fonttools-4.55.2-py3-none-any.whl", hash = "sha256:8e2d89fbe9b08d96e22c7a81ec04a4e8d8439c31223e2dc6f2f9fc8ff14bdf9f"}, + {file = "fonttools-4.55.2.tar.gz", hash = "sha256:45947e7b3f9673f91df125d375eb57b9a23f2a603f438a1aebf3171bffa7a205"}, ] [[package]] name = "fonttools" -version = "4.54.1" +version = "4.55.2" extras = ["woff"] requires_python = ">=3.8" summary = "Tools to manipulate font files" @@ -986,13 +999,13 @@ marker = "python_version == \"3.11\"" dependencies = [ "brotli>=1.0.1; platform_python_implementation == \"CPython\"", "brotlicffi>=0.8.0; platform_python_implementation != \"CPython\"", - "fonttools==4.54.1", + "fonttools==4.55.2", "zopfli>=0.1.4", ] files = [ - {file = "fonttools-4.54.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82834962b3d7c5ca98cb56001c33cf20eb110ecf442725dc5fdf36d16ed1ab07"}, - {file = "fonttools-4.54.1-py3-none-any.whl", hash = "sha256:37cddd62d83dc4f72f7c3f3c2bcf2697e89a30efb152079896544a93907733bd"}, - {file = "fonttools-4.54.1.tar.gz", hash = "sha256:957f669d4922f92c171ba01bef7f29410668db09f6c02111e22b2bce446f3285"}, + {file = "fonttools-4.55.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:131591ac8d7a47043aaf29581aba755ae151d46e49d2bf49608601efd71e8b4d"}, + {file = "fonttools-4.55.2-py3-none-any.whl", hash = "sha256:8e2d89fbe9b08d96e22c7a81ec04a4e8d8439c31223e2dc6f2f9fc8ff14bdf9f"}, + {file = "fonttools-4.55.2.tar.gz", hash = "sha256:45947e7b3f9673f91df125d375eb57b9a23f2a603f438a1aebf3171bffa7a205"}, ] [[package]] @@ -1010,7 +1023,7 @@ files = [ [[package]] name = "google-api-core" -version = "2.22.0" +version = "2.23.0" requires_python = ">=3.7" summary = "Google API client core library" groups = ["default", "typing"] @@ -1024,8 +1037,8 @@ dependencies = [ "requests<3.0.0.dev0,>=2.18.0", ] files = [ - {file = "google_api_core-2.22.0-py3-none-any.whl", hash = "sha256:a6652b6bd51303902494998626653671703c420f6f4c88cfd3f50ed723e9d021"}, - {file = "google_api_core-2.22.0.tar.gz", hash = "sha256:26f8d76b96477db42b55fd02a33aae4a42ec8b86b98b94969b7333a2c828bf35"}, + {file = "google_api_core-2.23.0-py3-none-any.whl", hash = "sha256:c20100d4c4c41070cf365f1d8ddf5365915291b5eb11b83829fbd1c999b5122f"}, + {file = "google_api_core-2.23.0.tar.gz", hash = "sha256:2ceb087315e6af43f256704b871d99326b1f12a9d6ce99beaedec99ba26a0ace"}, ] [[package]] @@ -1066,7 +1079,7 @@ files = [ [[package]] name = "google-auth" -version = "2.35.0" +version = "2.36.0" requires_python = ">=3.7" summary = "Google Authentication Library" groups = ["default", "typing"] @@ -1077,8 +1090,8 @@ dependencies = [ "rsa<5,>=3.1.4", ] files = [ - {file = "google_auth-2.35.0-py2.py3-none-any.whl", hash = "sha256:25df55f327ef021de8be50bad0dfd4a916ad0de96da86cd05661c9297723ad3f"}, - {file = "google_auth-2.35.0.tar.gz", hash = "sha256:f4c64ed4e01e8e8b646ef34c018f8bf3338df0c8e37d8b3bba40e7f574a3278a"}, + {file = "google_auth-2.36.0-py2.py3-none-any.whl", hash = "sha256:51a15d47028b66fd36e5c64a82d2d57480075bccc7da37cde257fc94177a61fb"}, + {file = "google_auth-2.36.0.tar.gz", hash = "sha256:545e9618f2df0bcbb7dcbc45a546485b1212624716975a1ea5ae8149ce769ab1"}, ] [[package]] @@ -1114,7 +1127,7 @@ files = [ [[package]] name = "googleapis-common-protos" -version = "1.65.0" +version = "1.66.0" requires_python = ">=3.7" summary = "Common protobufs used in Google APIs" 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", ] files = [ - {file = "googleapis_common_protos-1.65.0-py2.py3-none-any.whl", hash = "sha256:2972e6c496f435b92590fd54045060867f3fe9be2c82ab148fc8885035479a63"}, - {file = "googleapis_common_protos-1.65.0.tar.gz", hash = "sha256:334a29d07cddc3aa01dee4988f9afd9b2916ee2ff49d6b757155dc0d197852c0"}, + {file = "googleapis_common_protos-1.66.0-py2.py3-none-any.whl", hash = "sha256:d7abcd75fabb2e0ec9f74466401f6c119a0b498e27370e9be4c94cb7e382b8ed"}, + {file = "googleapis_common_protos-1.66.0.tar.gz", hash = "sha256:c3e7b33d15fdca5374cc0a7346dd92ffa847425cc4ea941d970f13680052ec8c"}, ] [[package]] @@ -1243,17 +1256,17 @@ files = [ [[package]] name = "jedi" -version = "0.19.1" +version = "0.19.2" requires_python = ">=3.6" summary = "An autocompletion tool for Python that can be used for text editors." groups = ["dev"] marker = "python_version == \"3.11\"" dependencies = [ - "parso<0.9.0,>=0.8.3", + "parso<0.9.0,>=0.8.4", ] files = [ - {file = "jedi-0.19.1-py2.py3-none-any.whl", hash = "sha256:e983c654fe5c02867aef4cdfce5a2fbb4a50adc0af145f70504238f18ef5e7e0"}, - {file = "jedi-0.19.1.tar.gz", hash = "sha256:cf0496f3651bc65d7174ac1b7d043eff454892c708a87d1b683e57b569927ffd"}, + {file = "jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9"}, + {file = "jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0"}, ] [[package]] @@ -1272,14 +1285,14 @@ files = [ [[package]] name = "json5" -version = "0.9.25" -requires_python = ">=3.8" +version = "0.10.0" +requires_python = ">=3.8.0" summary = "A Python implementation of the JSON5 data format." groups = ["lint"] marker = "python_version == \"3.11\"" files = [ - {file = "json5-0.9.25-py3-none-any.whl", hash = "sha256:34ed7d834b1341a86987ed52f3f76cd8ee184394906b6e22a1e0deb9ab294e8f"}, - {file = "json5-0.9.25.tar.gz", hash = "sha256:548e41b9be043f9426776f05df8635a00fe06104ea51ed24b67f908856e151ae"}, + {file = "json5-0.10.0-py3-none-any.whl", hash = "sha256:19b23410220a7271e8377f81ba8aacba2fdd56947fbb137ee5977cbe1f5e8dfa"}, + {file = "json5-0.10.0.tar.gz", hash = "sha256:e66941c8f0a02026943c52c2eb34ebeb2a6f819a0be05920a6f5243cd30fd559"}, ] [[package]] @@ -1516,14 +1529,14 @@ files = [ [[package]] name = "packaging" -version = "24.1" +version = "24.2" requires_python = ">=3.8" summary = "Core utilities for Python packages" groups = ["default"] marker = "python_version == \"3.11\"" files = [ - {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, - {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, + {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, + {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, ] [[package]] @@ -1594,15 +1607,15 @@ files = [ [[package]] name = "propcache" -version = "0.2.0" -requires_python = ">=3.8" +version = "0.2.1" +requires_python = ">=3.9" summary = "Accelerated property cache" groups = ["default"] marker = "python_version == \"3.11\"" files = [ - {file = "propcache-0.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c7dde9e533c0a49d802b4f3f218fa9ad0a1ce21f2c2eb80d5216565202acab4"}, - {file = "propcache-0.2.0-py3-none-any.whl", hash = "sha256:2ccc28197af5313706511fab3a8b66dcd6da067a1331372c82ea1cb74285e036"}, - {file = "propcache-0.2.0.tar.gz", hash = "sha256:df81779732feb9d01e5d513fad0122efb3d53bbc75f61b2a4f29a020bc985e70"}, + {file = "propcache-0.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf6c4150f8c0e32d241436526f3c3f9cbd34429492abddbada2ffcff506c51af"}, + {file = "propcache-0.2.1-py3-none-any.whl", hash = "sha256:52277518d6aae65536e9cea52d4e7fd2f7a66f4aa2d30ed3f2fcea620ace3c54"}, + {file = "propcache-0.2.1.tar.gz", hash = "sha256:3f77ce728b19cb537714499928fe800c3dda29e8d9428778fc7c186da4c09a64"}, ] [[package]] @@ -1622,15 +1635,15 @@ files = [ [[package]] name = "protobuf" -version = "5.28.3" +version = "5.29.1" requires_python = ">=3.8" summary = "" groups = ["default", "typing"] marker = "python_version == \"3.11\"" files = [ - {file = "protobuf-5.28.3-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:712319fbdddb46f21abb66cd33cb9e491a5763b2febd8f228251add221981135"}, - {file = "protobuf-5.28.3-py3-none-any.whl", hash = "sha256:cee1757663fa32a1ee673434fcf3bf24dd54763c79690201208bafec62f19eed"}, - {file = "protobuf-5.28.3.tar.gz", hash = "sha256:64badbc49180a5e401f373f9ce7ab1d18b63f7dd4a9cdc43c92b9f0b481cef7b"}, + {file = "protobuf-5.29.1-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:8ee1461b3af56145aca2800e6a3e2f928108c749ba8feccc6f5dd0062c410c0d"}, + {file = "protobuf-5.29.1-py3-none-any.whl", hash = "sha256:32600ddb9c2a53dedc25b8581ea0f1fd8ea04956373c0c07577ce58d312522e0"}, + {file = "protobuf-5.29.1.tar.gz", hash = "sha256:683be02ca21a6ffe80db6dd02c0b5b2892322c59ca57fd6c872d652cb80549cb"}, ] [[package]] @@ -1663,7 +1676,7 @@ files = [ [[package]] name = "psycopg-pool" -version = "3.2.3" +version = "3.2.4" requires_python = ">=3.8" summary = "Connection Pool for Psycopg" groups = ["default"] @@ -1672,8 +1685,8 @@ dependencies = [ "typing-extensions>=4.6", ] files = [ - {file = "psycopg_pool-3.2.3-py3-none-any.whl", hash = "sha256:53bd8e640625e01b2927b2ad96df8ed8e8f91caea4597d45e7673fc7bbb85eb1"}, - {file = "psycopg_pool-3.2.3.tar.gz", hash = "sha256:bb942f123bef4b7fbe4d55421bd3fb01829903c95c0f33fd42b7e94e5ac9b52a"}, + {file = "psycopg_pool-3.2.4-py3-none-any.whl", hash = "sha256:f6a22cff0f21f06d72fb2f5cb48c618946777c49385358e0c88d062c59cbd224"}, + {file = "psycopg_pool-3.2.4.tar.gz", hash = "sha256:61774b5bbf23e8d22bedc7504707135aaf744679f8ef9b3fe29942920746a6ed"}, ] [[package]] @@ -1755,6 +1768,38 @@ files = [ {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]] name = "pydyf" version = "0.11.0" @@ -1874,14 +1919,14 @@ files = [ [[package]] name = "regex" -version = "2024.9.11" +version = "2024.11.6" requires_python = ">=3.8" summary = "Alternative regular expression module, to replace re." groups = ["lint"] marker = "python_version == \"3.11\"" files = [ - {file = "regex-2024.9.11-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6113c008a7780792efc80f9dfe10ba0cd043cbf8dc9a76ef757850f51b4edc50"}, - {file = "regex-2024.9.11.tar.gz", hash = "sha256:6c188c307e8433bcb63dc1915022deb553b4203a70722fc542c363bf120a01fd"}, + {file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a19f302cd1ce5dd01a9099aaa19cae6173306d1302a43b627f62e21cf18ac0"}, + {file = "regex-2024.11.6.tar.gz", hash = "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519"}, ] [[package]] @@ -1988,14 +2033,14 @@ files = [ [[package]] name = "six" -version = "1.16.0" -requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +version = "1.17.0" +requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" summary = "Python 2 and 3 compatibility utilities" -groups = ["default", "dev", "lint"] +groups = ["default", "lint"] marker = "python_version == \"3.11\"" files = [ - {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, - {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, + {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, + {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, ] [[package]] @@ -2035,14 +2080,14 @@ files = [ [[package]] name = "sqlparse" -version = "0.5.1" +version = "0.5.2" requires_python = ">=3.8" summary = "A non-validating SQL parser." groups = ["default", "debug", "dev", "typing"] marker = "python_version == \"3.11\"" files = [ - {file = "sqlparse-0.5.1-py3-none-any.whl", hash = "sha256:773dcbf9a5ab44a090f3441e2180efe2560220203dc2f8c0b0fa141e18b505e4"}, - {file = "sqlparse-0.5.1.tar.gz", hash = "sha256:bb6b4df465655ef332548e24f08e205afc81b9ab86cb1c45657a7ff173a3a00e"}, + {file = "sqlparse-0.5.2-py3-none-any.whl", hash = "sha256:e99bc85c78160918c3e1d9230834ab8d80fc06c59d03f8db2618f65f65dda55e"}, + {file = "sqlparse-0.5.2.tar.gz", hash = "sha256:9e37b35e16d1cc652a2545f0997c1deb23ea28fa1f3eefe609eee3063c3b105f"}, ] [[package]] @@ -2150,7 +2195,7 @@ files = [ [[package]] name = "tqdm" -version = "4.66.6" +version = "4.67.1" requires_python = ">=3.7" summary = "Fast, Extensible Progress Meter" groups = ["lint"] @@ -2159,8 +2204,8 @@ dependencies = [ "colorama; platform_system == \"Windows\"", ] files = [ - {file = "tqdm-4.66.6-py3-none-any.whl", hash = "sha256:223e8b5359c2efc4b30555531f09e9f2f3589bcd7fdd389271191031b49b7a63"}, - {file = "tqdm-4.66.6.tar.gz", hash = "sha256:4bdd694238bef1485ce839d67967ab50af8f9272aab687c0d7702a01da0be090"}, + {file = "tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2"}, + {file = "tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2"}, ] [[package]] @@ -2207,14 +2252,14 @@ files = [ [[package]] name = "types-docutils" -version = "0.21.0.20241005" +version = "0.21.0.20241128" requires_python = ">=3.8" summary = "Typing stubs for docutils" groups = ["typing"] marker = "python_version == \"3.11\"" files = [ - {file = "types-docutils-0.21.0.20241005.tar.gz", hash = "sha256:48f804a2b50da3a1b1681c4ca1b6184416a6e4129e302d15c44e9d97c59b3365"}, - {file = "types_docutils-0.21.0.20241005-py3-none-any.whl", hash = "sha256:4d9021422f2f3fca8b0726fb8949395f66a06c0d951479eb3b1387d75b134430"}, + {file = "types_docutils-0.21.0.20241128-py3-none-any.whl", hash = "sha256:e0409204009639e9b0bf4521eeabe58b5e574ce9c0db08421c2ac26c32be0039"}, + {file = "types_docutils-0.21.0.20241128.tar.gz", hash = "sha256:4dd059805b83ac6ec5a223699195c4e9eeb0446a4f7f2aeff1759a4a7cc17473"}, ] [[package]] @@ -2339,14 +2384,14 @@ files = [ [[package]] name = "types-setuptools" -version = "75.2.0.20241025" +version = "75.6.0.20241126" requires_python = ">=3.8" summary = "Typing stubs for setuptools" groups = ["typing"] marker = "python_version == \"3.11\"" files = [ - {file = "types-setuptools-75.2.0.20241025.tar.gz", hash = "sha256:2949913a518d5285ce00a3b7d88961c80a6e72ffb8f3da0a3f5650ea533bd45e"}, - {file = "types_setuptools-75.2.0.20241025-py3-none-any.whl", hash = "sha256:6721ac0f1a620321e2ccd87a9a747c4a383dc381f78d894ce37f2455b45fcf1c"}, + {file = "types_setuptools-75.6.0.20241126-py3-none-any.whl", hash = "sha256:aaae310a0e27033c1da8457d4d26ac673b0c8a0de7272d6d4708e263f2ea3b9b"}, + {file = "types_setuptools-75.6.0.20241126.tar.gz", hash = "sha256:7bf25ad4be39740e469f9268b6beddda6e088891fa5a27e985c6ce68bf62ace0"}, ] [[package]] @@ -2365,7 +2410,7 @@ name = "typing-extensions" version = "4.12.2" requires_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\"" files = [ {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"}, ] +[[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]] name = "uritemplate" version = "4.1.1" @@ -2469,8 +2530,8 @@ files = [ [[package]] name = "watchfiles" -version = "0.24.0" -requires_python = ">=3.8" +version = "1.0.0" +requires_python = ">=3.9" summary = "Simple, modern and high performance file watching and code reload in python." groups = ["server"] marker = "python_version == \"3.11\"" @@ -2478,8 +2539,8 @@ dependencies = [ "anyio>=3.0.0", ] files = [ - {file = "watchfiles-0.24.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa0fd7248cf533c259e59dc593a60973a73e881162b1a2f73360547132742823"}, - {file = "watchfiles-0.24.0.tar.gz", hash = "sha256:afb72325b74fa7a428c009c1b8be4b4d7c2afedafb2982827ef2156646df2fe1"}, + {file = "watchfiles-1.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:245fab124b9faf58430da547512d91734858df13f2ddd48ecfa5e493455ffccb"}, + {file = "watchfiles-1.0.0.tar.gz", hash = "sha256:37566c844c9ce3b5deb964fe1a23378e575e74b114618d211fbda8f59d7b5dab"}, ] [[package]] @@ -2543,20 +2604,20 @@ files = [ [[package]] name = "websockets" -version = "13.1" -requires_python = ">=3.8" +version = "14.1" +requires_python = ">=3.9" summary = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" groups = ["server"] marker = "python_version == \"3.11\"" 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-13.1-py3-none-any.whl", hash = "sha256:a9a396a6ad26130cdae92ae10c36af09d9bfe6cafe69670fd3b6da9b07b4044f"}, - {file = "websockets-13.1.tar.gz", hash = "sha256:a3b3366087c1bc0a2795111edcadddb8b3b59509d5db5d7ea3fdd69f954a8878"}, + {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-14.1-py3-none-any.whl", hash = "sha256:4d4fc827a20abe6d544a119896f6b78ee13fe81cbfef416f3f2ddf09a03f0e2e"}, + {file = "websockets-14.1.tar.gz", hash = "sha256:398b10c77d471c0aab20a845e7a60076b6390bfdaac7a6d2edb0d2c59d75e8d8"}, ] [[package]] name = "yarl" -version = "1.17.1" +version = "1.18.3" requires_python = ">=3.9" summary = "Yet another URL library" groups = ["default"] @@ -2567,9 +2628,9 @@ dependencies = [ "propcache>=0.2.0", ] files = [ - {file = "yarl-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d216e5d9b8749563c7f2c6f7a0831057ec844c68b4c11cb10fc62d4fd373c26d"}, - {file = "yarl-1.17.1-py3-none-any.whl", hash = "sha256:f1790a4b1e8e8e028c391175433b9c8122c39b46e1663228158e61e6f915bf06"}, - {file = "yarl-1.17.1.tar.gz", hash = "sha256:067a63fcfda82da6b198fa73079b1ca40b7c9b7994995b6ee38acda728b64d47"}, + {file = "yarl-1.18.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75674776d96d7b851b6498f17824ba17849d790a44d282929c42dbb77d4f17ae"}, + {file = "yarl-1.18.3-py3-none-any.whl", hash = "sha256:b57f4f58099328dfb26c6a771d09fb20dbbae81d20cfb66141251ea063bd101b"}, + {file = "yarl-1.18.3.tar.gz", hash = "sha256:ac1801c45cbf77b6c99242eeff4fffb5e4e73a800b5c4ad4fc0be5def634d2e1"}, ] [[package]] diff --git a/pyproject.toml b/pyproject.toml index 72873af..fb07fd6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,6 +56,7 @@ dependencies = [ "semver~=3.0", "tablib[ods,xlsx]~=3.7", "udm-rest-client~=1.2", + "unifi-access~=0.1", "weasyprint~=63.0", ] optional-dependencies.server = [ @@ -105,7 +106,7 @@ name = "pypi" url = "https://git.claremontmakerspace.org/api/packages/CMS/pypi/simple" verify_ssl = true name = "CMS" -include_packages = [ "openapi-client-udm" ] +include_packages = [ "openapi-client-udm", "unifi-access" ] exclude_packages = [ "*" ] [tool.pdm.scripts]