Initial Commit

Really should have added this to version control earlier, but was
trying to fix all the pre-commit issues first...
This commit is contained in:
Adam Goldsmith 2024-11-16 00:26:00 -05:00
commit 5b0c72dbef
48 changed files with 7011 additions and 0 deletions

10
.gitignore vendored Normal file
View File

@ -0,0 +1,10 @@
# Python-generated files
__pycache__/
*.py[oc]
build/
dist/
wheels/
*.egg-info
# Virtual environments
.venv

25
.pre-commit-config.yaml Normal file
View File

@ -0,0 +1,25 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-toml
- id: check-yaml
- id: check-added-large-files
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.7.3
hooks:
- id: ruff
- id: ruff-format
- repo: https://github.com/astral-sh/uv-pre-commit
rev: 0.5.2
hooks:
- id: uv-lock
- repo: https://github.com/tox-dev/pyproject-fmt
rev: v2.5.0
hooks:
- id: pyproject-fmt

5
LICENSE Normal file
View File

@ -0,0 +1,5 @@
Copyright 2024 Adam Goldsmith
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

13
README.md Normal file
View File

@ -0,0 +1,13 @@
# Python UniFi Access API Client
Unofficial typed wrapper for the public UniFi Access API.
Currently written against V2.2.10 of the [API Reference](https://core-config-gfoz.uid.alpha.ui.com/configs/unifi-access/api_reference.pdf).
## Completion Status
See [docs/completion.md](docs/completion.md)
## Disclaimer
This library is not affiliated with or endorsed by Ubiquiti Networks Inc.
It probably contains at least some errors or mismatch with the actual API behaviour.
Feel free to open issues or submit pull requests.

View File

@ -0,0 +1,3 @@
::: unifi_access.AccessClient
options:
show_root_heading: true

View File

@ -0,0 +1,3 @@
# 3. User
::: unifi_access.schemas._user

View File

@ -0,0 +1,3 @@
# 4. Visitor
::: unifi_access.schemas._visitor

View File

@ -0,0 +1,2 @@
# 5. Access Policy
::: unifi_access.schemas._access_policy

View File

@ -0,0 +1,3 @@
# 6. Credential
::: unifi_access.schemas._credential

View File

@ -0,0 +1,3 @@
# 7. Space
::: unifi_access.schemas._space

View File

@ -0,0 +1,3 @@
# 8. Device
::: unifi_access.schemas._device

View File

@ -0,0 +1,3 @@
# 9. System Log
::: unifi_access.schemas._system_log

View File

@ -0,0 +1,3 @@
# 10. UniFi Identity
::: unifi_access.schemas._identity

View File

@ -0,0 +1,2 @@
# Schemas
::: unifi_access.schemas._base

102
docs/completion.md Normal file
View File

@ -0,0 +1,102 @@
# Completion
* AccessClient -> implemented in the client
* offline/examples test -> has offline tests based on examples in the API reference pdf
* live test -> is used in a test that runs against a real instance
| Section/Endpoint | AccessClient | offline/examples test | live test |
|------------------------------------------------------------|--------------------|-----------------------|--------------------|
| 3. User | :white_check_mark: | :white_check_mark: | |
| └─ 3.2 User Registration | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| └─ 3.3 Update User | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| └─ 3.4 Fetch User | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| └─ 3.5 Fetch All Users | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| └─ 3.6 Assign Access Policy to User | :white_check_mark: | :white_check_mark: | :x: |
| └─ 3.7 Assign NFC Card to User | :white_check_mark: | :white_check_mark: | :x: |
| └─ 3.8 Unassign NFC Card from User | :white_check_mark: | :white_check_mark: | :x: |
| └─ 3.9 Assign PIN Code to User | :white_check_mark: | :white_check_mark: | :x: |
| └─ 3.10 Unassign PIN Code from User | :white_check_mark: | :white_check_mark: | :x: |
| └─ 3.11 Create User Group | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| └─ 3.12 Fetch All User Groups | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| └─ 3.13 Fetch User Group | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| └─ 3.14 Update User Group | :white_check_mark: | :white_check_mark: | :x: |
| └─ 3.15 Delete User Group | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| └─ 3.16 Assign User to User Group | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| └─ 3.17 Unassign User from User Group | :white_check_mark: | :white_check_mark: | :x: |
| └─ 3.18 Fetch Users in a User Group | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| └─ 3.19 Fetch All Users in a User Group | :white_check_mark: | :white_check_mark: | :x: |
| └─ 3.20 Fetch the Access Policies Assigned to a User | :white_check_mark: | :white_check_mark: | :x: |
| └─ 3.21 Assign Access Policy to User Group | :white_check_mark: | :white_check_mark: | :x: |
| └─ 3.22 Fetch the Access Policies Assigned to a User Group | :white_check_mark: | :white_check_mark: | :x: |
| 4. Visitor | :white_check_mark: | :white_check_mark: | |
| └─ 4.2 Create Visitor | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| └─ 4.3 Fetch Visitor | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| └─ 4.4 Fetch All Visitors | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| └─ 4.5 Update Visitor | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| └─ 4.6 Delete Visitor | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| └─ 4.7 Assign NFC Card To Visitor | :white_check_mark: | :white_check_mark: | :x: |
| └─ 4.8 Unassign NFC Card From Visitor | :white_check_mark: | :white_check_mark: | :x: |
| └─ 4.9 Assign PIN Code To Visitor | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| └─ 4.10 Unassign PIN Code From Visitor | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| 5. Access Policy | :white_check_mark: | :white_check_mark: | |
| └─ 5.2 Create Access Policy | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| └─ 5.3 Update Access Policy | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| └─ 5.4 Delete Access Policy | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| └─ 5.5 Fetch Access Policy | :white_check_mark: | :white_check_mark: | :x: |
| └─ 5.6 Fetch All Access Policies | :white_check_mark: | :white_check_mark: | :x: |
| └─ 5.8 Create Holiday Group | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| └─ 5.9 Update Holiday Group | :white_check_mark: | :white_check_mark: | :x: |
| └─ 5.10 Delete Holiday Group | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| └─ 5.11 Fetch Holiday Group | :white_check_mark: | :white_check_mark: | :x: |
| └─ 5.12 Fetch All Holiday Groups | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| └─ 5.14 Create Schedule | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| └─ 5.15 Update Schedule | :white_check_mark: | :white_check_mark: | :x: |
| └─ 5.16 Fetch Schedule | :white_check_mark: | :white_check_mark: | :x: |
| └─ 5.17 Fetch All Schedules | :white_check_mark: | :white_check_mark: | :x: |
| └─ 5.18 Delete Schedule | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| 6. Credential | :white_check_mark: | :white_check_mark: | :x: |
| └─ 6.1 Generate PIN Code | :white_check_mark: | :white_check_mark: | :x: |
| └─ 6.2 Enroll NFC Card | :white_check_mark: | :white_check_mark: | :x: |
| └─ 6.3 Fetch NFC Card Enrollment Status | :white_check_mark: | :white_check_mark: | :x: |
| └─ 6.4 Remove a Session Created for NFC Card Enrollment | :white_check_mark: | :white_check_mark: | :x: |
| └─ 6.7 Fetch NFC Card | :white_check_mark: | :white_check_mark: | :x: |
| └─ 6.8 Fetch All NFC Cards | :white_check_mark: | :white_check_mark: | :x: |
| └─ 6.9 Delete NFC Card | :white_check_mark: | :white_check_mark: | :x: |
| 7. Space | :white_check_mark: | :white_check_mark: | :x: |
| └─ 7.1 Fetch Door Group Topology | :white_check_mark: | :white_check_mark: | :x: |
| └─ 7.2 Create Door Group | :white_check_mark: | :white_check_mark: | :x: |
| └─ 7.3 Fetch Door Group | :white_check_mark: | :white_check_mark: | :x: |
| └─ 7.4 Update Door Group | :white_check_mark: | :white_check_mark: | :x: |
| └─ 7.5 Fetch All Door Groups | :white_check_mark: | :white_check_mark: | :x: |
| └─ 7.6 Delete Door Group | :white_check_mark: | :white_check_mark: | :x: |
| └─ 7.7 Fetch Door | :white_check_mark: | :white_check_mark: | :x: |
| └─ 7.8 Fetch All Doors | :white_check_mark: | :white_check_mark: | :x: |
| └─ 7.9 Remote Door Unlocking | :white_check_mark: | :white_check_mark: | :x: |
| └─ 7.10 Set Temporary Door Locking Rule | :white_check_mark: | :white_check_mark: | :x: |
| └─ 7.11 Fetch Door Locking Rule | :white_check_mark: | :white_check_mark: | :x: |
| └─ 7.12 Set Door Emergency Status | :white_check_mark: | :white_check_mark: | :x: |
| └─ 7.13 Fetch Door Emergency Status | :white_check_mark: | :white_check_mark: | :x: |
| 8. Device | :white_check_mark: | :white_check_mark: | :x: |
| └─ 8.1 Fetch Devices | :white_check_mark: | :white_check_mark: | :x: |
| 9. System Log | :white_check_mark: | | :x: |
| └─ 9.2 Fetch System Logs | :white_check_mark: | :white_check_mark: | :x: |
| └─ 9.3 Export System Logs | :x: | :white_check_mark: | :x: |
| └─ 9.4 Fetch Resources in System Logs | :x: | :x: | :x: |
| └─ 9.5 Fetch Static Resources in System Logs | :x: | :x: | :x: |
| 10. UniFi Identity | :white_check_mark: | :white_check_mark: | :x: |
| └─ 10.1 Send UniFi Identity Invitations | :white_check_mark: | :white_check_mark: | :x: |
| └─ 10.2 Fetch Available Resources | :white_check_mark: | :white_check_mark: | :x: |
| └─ 10.3 Assign Resources to Users | :white_check_mark: | :white_check_mark: | :x: |
| └─ 10.4 Fetch Resources Assigned to Users | :white_check_mark: | :white_check_mark: | :x: |
| └─ 10.5 Assign Resources to User Groups | :white_check_mark: | :white_check_mark: | :x: |
| └─ 10.6 Fetch the Resources Assigned to User Groups | :white_check_mark: | :white_check_mark: | :x: |
| 11. Notification | :x: | :x: | :x: |
| └─ 11.1 Fetch Notifications [WebSocket] | :x: | :x: | :x: |
| └─ 11.2 List of Supported Webhook Events [Webhook] | :x: | :x: | :x: |
| └─ 11.3 Fetch Webhook Endpoints List [Webhook] | :x: | :x: | :x: |
| └─ 11.4 Add Webhook Endpoints [Webhook] | :x: | :x: | :x: |
| └─ 11.5 Update Webhook Endpoints [Webhook] | :x: | :x: | :x: |
| └─ 11.6 Delete Webhook Endpoints [Webhook] | :x: | :x: | :x: |
| 12. API Server | :x: | :x: | :x: |
| └─ 12.1 Upload HTTPS Certificate | :x: | :x: | :x: |
| └─ 12.2 Delete HTTPS Certificate | :x: | :x: | :x: |

1
docs/index.md Normal file
View File

@ -0,0 +1 @@
--8<-- "README.md"

View File

@ -0,0 +1,8 @@
.md-nav--secondary > .md-nav__list > .md-nav__item > .md-nav {
display: none;
}
.md-nav--secondary > .md-nav__list > .md-nav__item:has(.md-nav__link--active) > .md-nav
{
display: initial;
}

58
mkdocs.yml Normal file
View File

@ -0,0 +1,58 @@
site_name: Python UniFi Access Client
plugins:
- search
- mkdocstrings:
handlers:
python:
options:
show_if_no_docstring: true
group_by_category: true
members_order: source
show_symbol_type_heading: true
show_root_toc_entry: false
show_symbol_type_toc: true
docstring_style: sphinx
merge_init_into_class: true
show_signature_annotations: true
separate_signature: true
signature_crossrefs: true
theme:
name: material
features:
- toc.follow
- navigation.tracking
- navigation.indexes
- navigation.expand
- navigation.sections
palette:
- media: "(prefers-color-scheme)"
toggle:
icon: material/brightness-auto
name: Switch to light mode
- media: "(prefers-color-scheme: light)"
scheme: default
toggle:
icon: material/brightness-7
name: Switch to dark mode
- media: "(prefers-color-scheme: dark)"
scheme: slate
toggle:
icon: material/brightness-4
name: Switch to system preference
watch:
- src
markdown_extensions:
- tables
- admonition
- pymdownx.details
- pymdownx.superfences
- pymdownx.snippets
- pymdownx.emoji
extra_css:
- stylesheets/extra.css

95
pyproject.toml Normal file
View File

@ -0,0 +1,95 @@
[build-system]
build-backend = "hatchling.build"
requires = [ "hatchling" ]
[project]
name = "unifi-access"
version = "0.1.0"
description = "Typed wrapper for the Unifi Access Public API"
readme = "README.md"
license = { text = "ISC" }
authors = [
{ name = "Adam Goldsmith", email = "contact@adamgoldsmith.name" },
]
requires-python = ">=3.11"
classifiers = [
"License :: OSI Approved :: ISC License (ISCL)",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
]
dependencies = [
"pydantic>=2.9.2",
"requests>=2.32.3",
]
[dependency-groups]
dev = [
"mypy>=1.13",
"pytest>=8.3.3",
"responses>=0.25.3",
"ruff>=0.7.1",
"types-requests>=2.32.0.20241016",
]
docs = [
"mkdocs-material>=9.5.42",
"mkdocstrings[python]>=0.26.2",
]
[tool.ruff]
line-length = 88
lint.select = [
"A",
"B",
"C4",
"E4",
"E7",
"E9",
"F",
"FIX003",
"FURB",
"I",
"INP",
"ISC",
"LOG",
"PERF",
"PIE",
"PL",
"PT",
"PTH",
"Q",
"RSE",
"SIM",
"T20",
"TCH",
"UP",
]
lint.ignore = [ "ISC001" ]
[tool.pyproject-fmt]
indent = 4
[tool.pytest.ini_options]
filterwarnings = [ "ignore::urllib3.exceptions.InsecureRequestWarning" ]
[tool.mypy]
plugins = [ "pydantic.mypy" ]
follow_imports = "silent"
warn_redundant_casts = true
warn_unused_ignores = true
disallow_any_generics = true
check_untyped_defs = true
no_implicit_reexport = true
disallow_untyped_defs = true
[tool.pydantic-mypy]
init_forbid_extra = true
init_typed = true
warn_required_dynamic_aliases = true
[tool.uv]
default-groups = [ "dev", "docs" ]

View File

@ -0,0 +1,9 @@
"""
Wrapper for Unifi Access API.
See the [official API Reference](https://core-config-gfoz.uid.alpha.ui.com/configs/unifi-access/api_reference.pdf) for more details.
"""
from ._client import AccessClient, Response, ResponseCode
__all__ = ["AccessClient", "Response", "ResponseCode"]

1293
src/unifi_access/_client.py Normal file

File diff suppressed because it is too large Load Diff

View File

View File

@ -0,0 +1,145 @@
from ._access_policy import (
AccessPolicy,
FetchAllHolidayGroupsResponse,
FetchAllSchedulesResponse,
Holiday,
HolidayGroup,
PartialHoliday,
PartialSchedule,
Schedule,
TimePeriod,
WeekSchedule,
)
from ._base import (
AccessPolicyId,
ActorId,
DeviceId,
DoorGroupId,
DoorGroupResource,
DoorId,
DoorResource,
FloorId,
HolidayGroupId,
HolidayId,
NamedDoorGroupResource,
NamedDoorGroupResourceWithIsBindHub,
NamedDoorResource,
NamedDoorResourceWithIsBindHub,
NamedResource,
NamedResourceWithIsBindHub,
NfcCardEnrollmentSessionId,
NfcCardId,
NfcCardToken,
PinCode,
Resource,
ResourceId,
ScheduleId,
UserGroupId,
UserId,
VisitorId,
)
from ._credential import (
EnrollNfcCardResponse,
NfcCard,
NfcCardEnrollmentStatus,
NfcCardUser,
)
from ._device import Device
from ._identity import (
IdentityInvitationEmailFailure,
IdentityInvitationUser,
IdentityResourceType,
)
from ._space import (
Door,
DoorEmergencyStatus,
DoorGroup,
DoorGroupTopology,
DoorGroupType,
DoorLockingRule,
DoorLockingRuleType,
ResourceTopology,
)
from ._system_log import (
SystemLogActor,
SystemLogAuthentication,
SystemLogEntry,
SystemLogEvent,
SystemLogTopic,
)
from ._user import FullUser, User, UserGroup, UserNfcCard, UserStatus
from ._visitor import (
FetchAllVisitorsExpansion,
Visitor,
VisitorStatus,
VisitReason,
)
__all__ = [
"AccessPolicy",
"FetchAllHolidayGroupsResponse",
"FetchAllSchedulesResponse",
"Holiday",
"HolidayGroup",
"PartialHoliday",
"Schedule",
"PartialSchedule",
"TimePeriod",
"WeekSchedule",
"AccessPolicyId",
"DoorId",
"FloorId",
"DoorGroupId",
"ResourceId",
"ScheduleId",
"UserId",
"UserGroupId",
"VisitorId",
"NfcCardId",
"HolidayId",
"HolidayGroupId",
"DeviceId",
"NfcCardEnrollmentSessionId",
"NfcCardToken",
"ActorId",
"Resource",
"NamedResource",
"NamedResourceWithIsBindHub",
"DoorGroupResource",
"DoorResource",
"NamedDoorGroupResource",
"NamedDoorGroupResourceWithIsBindHub",
"NamedDoorResource",
"NamedDoorResourceWithIsBindHub",
"PinCode",
"EnrollNfcCardResponse",
"NfcCard",
"NfcCardEnrollmentStatus",
"NfcCardUser",
"Device",
"IdentityInvitationEmailFailure",
"IdentityInvitationUser",
"IdentityResourceType",
"Door",
"DoorEmergencyStatus",
"DoorGroup",
"DoorGroupTopology",
"DoorGroupType",
"DoorLockingRule",
"DoorLockingRuleType",
"ResourceTopology",
"SystemLogActor",
"SystemLogAuthentication",
"SystemLogEntry",
"SystemLogEvent",
"SystemLogTopic",
"FetchAllVisitorsExpansion",
"User",
"FullUser",
"UserGroup",
"UserNfcCard",
"UserStatus",
"Visitor",
"VisitorStatus",
"VisitReason",
]

View File

@ -0,0 +1,151 @@
from ._base import (
AccessPolicyId,
ForbidExtraBaseModel,
FormattedTime,
HolidayGroupId,
HolidayId,
Resource,
RFC3339Datetime,
ScheduleId,
)
class AccessPolicy(ForbidExtraBaseModel):
"""5.1 Access Policy Schemas"""
id: AccessPolicyId
"""Identity ID of the access policy."""
name: str
"""Name of the access policy."""
resources: list[Resource]
"""Specify the locations that can be accessed."""
schedule_id: ScheduleId
"""Identity ID of the schedule."""
class Holiday(ForbidExtraBaseModel):
description: str | None = None
"""Description of the holiday."""
id: HolidayId
"""Identity ID of the holiday."""
name: str
"""Name of the holiday."""
repeat: bool
"""Indicate whether the holiday repeats annually."""
start_time: RFC3339Datetime
"""Start time of the holiday"""
end_time: RFC3339Datetime
"""End time of the holiday"""
is_template: bool | None = None
"""!!! note "not listed in API docs" """
class PartialHoliday(ForbidExtraBaseModel):
description: str | None = None
"""Description of the holiday."""
id: HolidayId | None = None
"""Identity ID of the holiday."""
name: str | None = None
"""Name of the holiday."""
repeat: bool | None = None
"""Indicate whether the holiday repeats annually."""
start_time: RFC3339Datetime | None = None
"""Start time of the holiday"""
end_time: RFC3339Datetime | None = None
"""End time of the holiday"""
class HolidayGroup(ForbidExtraBaseModel):
id: HolidayGroupId
"""Identity ID of the holiday group."""
name: str
"""Name of the holiday group."""
is_default: bool
"""Indicate whether the holiday group is the system default."""
description: str
"""Description of the holiday group."""
holidays: list[Holiday] | None = None
"""A list of the holidays within the holiday group."""
template_name: str | None = None
"""!!! note "not listed in API docs" """
is_private: bool | None = None
"""!!! note "not listed in API docs" """
class FetchAllHolidayGroupsResponse(ForbidExtraBaseModel):
id: HolidayGroupId
"""Identity ID of the holiday group."""
name: str
"""Name of the holiday group."""
is_default: bool
"""
Indicate whether the holiday group is the system default.
!!! note "not listed in API docs for this call"
"""
description: str
"""Description of the holiday group."""
count: int
"""Total number of holidays"""
is_private: bool | None = None
"""!!! note "not listed in API docs" """
class TimePeriod(ForbidExtraBaseModel):
start_time: FormattedTime
"""Start time of the access time period."""
end_time: FormattedTime
"""End time of the access time period."""
class WeekSchedule(ForbidExtraBaseModel):
sunday: list[TimePeriod]
monday: list[TimePeriod]
tuesday: list[TimePeriod]
wednesday: list[TimePeriod]
thursday: list[TimePeriod]
friday: list[TimePeriod]
saturday: list[TimePeriod]
class _ScheduleBase(ForbidExtraBaseModel):
id: ScheduleId
"""Identity ID of the schedule."""
name: str
"""Name of the schedule."""
is_default: bool
"""Indicate whether the schedule is the system default."""
type: str
"""Contains the access type, which is assigned to a user along with an access policy."""
class Schedule(_ScheduleBase):
weekly: WeekSchedule
"""
The customizable scheduling strategy for each day from Sunday to Saturday.
If not specified, it means access is allowed every day.
"""
holiday_schedule: list[TimePeriod]
"""Specify the accessible period during holidays. UniFi Access Requirement: 1.20.11 or later"""
holiday_group_id: HolidayGroupId
"""Identity ID of the holiday group."""
holiday_group: HolidayGroup
class PartialSchedule(_ScheduleBase):
weekly: WeekSchedule | None = None
"""
The customizable scheduling strategy for each day from Sunday to Saturday.
If not specified, it means access is allowed every day.
"""
holiday_schedule: list[TimePeriod] | None = None
"""Specify the accessible period during holidays. UniFi Access Requirement: 1.20.11 or later"""
holiday_group_id: HolidayGroupId | None = None
"""Identity ID of the holiday group."""
holiday_group: HolidayGroup | None = None
class FetchAllSchedulesResponse(_ScheduleBase):
status: int
"""!!! note "not listed in API docs" """
holiday_group_id: HolidayGroupId
"""!!! note "not listed in API docs" """

View File

@ -0,0 +1,87 @@
import datetime
from typing import Annotated, Literal, NewType
from pydantic import (
AwareDatetime,
BaseModel,
ConfigDict,
PlainSerializer,
)
class ForbidExtraBaseModel(BaseModel):
model_config = ConfigDict(extra="forbid")
# TODO: these are mostly UUIDs
AccessPolicyId = NewType("AccessPolicyId", str)
DoorId = NewType("DoorId", str)
FloorId = NewType("FloorId", str)
DoorGroupId = NewType("DoorGroupId", str)
ResourceId = DoorId | DoorGroupId
ScheduleId = NewType("ScheduleId", str)
UserId = NewType("UserId", str)
UserGroupId = NewType("UserGroupId", str)
VisitorId = NewType("VisitorId", str)
NfcCardId = NewType("NfcCardId", str)
HolidayId = NewType("HolidayId", str)
HolidayGroupId = NewType("HolidayGroupId", str)
DeviceId = NewType("DeviceId", str)
NfcCardEnrollmentSessionId = NewType("NfcCardEnrollmentSessionId", str)
NfcCardToken = NewType("NfcCardToken", str)
IdentityResourceId = NewType("IdentityResourceId", str)
ActorId = UserId | VisitorId | DeviceId
UnixTimestampDateTime = Annotated[
AwareDatetime, PlainSerializer(lambda dt: int(dt.timestamp()), return_type=int)
]
RFC3339Datetime = Annotated[
AwareDatetime,
PlainSerializer(
lambda dt: dt.astimezone(datetime.UTC).strftime("%Y-%m-%dT%H:%M:%SZ"),
return_type=str,
),
]
FormattedTime = Annotated[
datetime.time,
PlainSerializer(lambda t: t.isoformat(timespec="seconds"), return_type=str),
]
class DoorResource(ForbidExtraBaseModel):
type: Literal["door"] = "door"
id: DoorId
class NamedDoorResource(DoorResource):
name: str
class NamedDoorResourceWithIsBindHub(NamedDoorResource):
is_bind_hub: bool
class DoorGroupResource(ForbidExtraBaseModel):
type: Literal["door_group"] = "door_group"
id: DoorGroupId
class NamedDoorGroupResource(DoorGroupResource):
name: str
class NamedDoorGroupResourceWithIsBindHub(NamedDoorGroupResource):
is_bind_hub: bool
Resource = DoorResource | DoorGroupResource
NamedResource = NamedDoorResource | NamedDoorGroupResource
NamedResourceWithIsBindHub = (
NamedDoorResourceWithIsBindHub | NamedDoorGroupResourceWithIsBindHub
)
class PinCode(ForbidExtraBaseModel):
token: str
"""The user's PIN hash code credential for unlocking a door."""

View File

@ -0,0 +1,58 @@
from typing import Literal
from ._base import (
ForbidExtraBaseModel,
NfcCardEnrollmentSessionId,
NfcCardId,
NfcCardToken,
UserId,
)
# TODO: this should be a partial of User
class NfcCardUser(ForbidExtraBaseModel):
id: UserId
"""Identity ID of the user."""
first_name: str
"""First name of the user."""
last_name: str
"""Last name of the user."""
name: str
"""Full name of the user."""
avatar: str
"""!!! note "not listed in API docs" """
class NfcCard(ForbidExtraBaseModel):
token: NfcCardToken
"""Identity token of the NFC card."""
display_id: NfcCardId
"""Display ID of the NFC card."""
status: Literal["assigned", "pending", "disable", "deleted", "loss"]
"""Status of the NFC card."""
alias: str
"""Preferred name of the NFC card."""
card_type: str
"""Type of the NFC card."""
user_id: Literal[""] | UserId
"""Owner ID of the NFC card."""
user_type: Literal["USER", "VISITOR", ""]
"""Type of the owner."""
user: NfcCardUser
"""Owner of the NFC card."""
note: str
"""!!! note "not listed in API docs" """
class EnrollNfcCardResponse(ForbidExtraBaseModel):
session_id: NfcCardEnrollmentSessionId
"""The session for enrolling an NFC card."""
class NfcCardEnrollmentStatus(ForbidExtraBaseModel):
token: NfcCardToken
"""Unique NFC card token."""
id: NfcCardId
"""Display ID of the NFC card.
!!! note "documented as `card_id`"
"""

View File

@ -0,0 +1,12 @@
from ._base import DeviceId, ForbidExtraBaseModel
class Device(ForbidExtraBaseModel):
id: DeviceId
"""Identity ID of the device."""
name: str
"""Name of the device."""
type: str
"""Type of the device."""
full_name: str
"""Full name of the device."""

View File

@ -0,0 +1,36 @@
from enum import StrEnum
from typing import Any
from ._base import ForbidExtraBaseModel, IdentityResourceId, UserId
class IdentityInvitationUser(ForbidExtraBaseModel):
user_id: UserId
email: str | None = None
class IdentityInvitationEmailFailure(ForbidExtraBaseModel):
error_code: str
error_msg: str
user_email: str
user_id: UserId
class IdentityResourceType(StrEnum):
EV_STATION = "ev_station"
VPN = "vpn"
WIFI = "wifi"
class IdentityResource(ForbidExtraBaseModel):
id: IdentityResourceId
"""Identity ID of the resources."""
name: str
"""Name of the resources."""
deleted: bool
"""Indicate whether the resource is disabled."""
short_name: str
"""!!! note "not listed in API docs" """
# XXX: narrower type
metadata: Any | None
"""!!! note "not listed in API docs" """

View File

@ -0,0 +1,104 @@
from enum import StrEnum
from typing import Generic, Literal, TypeVar
from ._base import (
DoorGroupId,
DoorId,
FloorId,
ForbidExtraBaseModel,
NamedResourceWithIsBindHub,
Resource,
UnixTimestampDateTime,
)
class DoorGroupType(StrEnum):
ACCESS = "access"
BUILDING = "building"
class ResourceTopology(ForbidExtraBaseModel):
id: str
"""Identity ID of the floor."""
type: str
"""Type of the floor."""
name: str
"""Name of the floor."""
resources: list[NamedResourceWithIsBindHub]
"""Contains all the doors on the floor."""
is_bind_hub: bool | None = None
"""
Indicate whether the door has bound to a hub device.
It can only # be used for remote opening if it's bound.
"""
class DoorGroupTopology(ForbidExtraBaseModel):
# XXX: is this really optional?
id: str | None = None
"""Identity ID of the door group."""
type: DoorGroupType
"""
The building type contains all the doors; the access type represents all the customized door groups.
"""
name: str
"""!!! note "not listed in API docs" """
resource_topologies: list[ResourceTopology]
"""Contains information about the floor and all its associated doors."""
ResourceType = TypeVar("ResourceType", bound=Resource)
class DoorGroup(ForbidExtraBaseModel, Generic[ResourceType]):
id: DoorGroupId
"""Identity ID of the door group."""
name: str
"""Name of the door group."""
type: DoorGroupType
"""!!! note "not listed in API docs" """
resources: list[ResourceType]
class Door(ForbidExtraBaseModel):
id: DoorId
"""Identity ID of the door."""
name: str
"""Name of the door."""
full_name: str
"""Full name of the door."""
floor_id: FloorId
"""Identity ID of the floor."""
# XXX: is this always 'door'?
type: Literal["door"] = "door"
"""Type of the door."""
is_bind_hub: bool
"""
Indicate whether the door has bound to a hub device.
It can only be used for remote opening if it's bound.
!!! note "api doc describes this as a string"
"""
door_lock_relay_status: Literal["lock", "unlock"]
"""Door lock status."""
# TODO: convert empty string to None?
door_position_status: Literal["open", "close", ""]
"""A null value means that no device is connected"""
class DoorLockingRuleType(StrEnum):
KEEP_LOCK = "keep_lock"
KEEP_UNLOCK = "keep_unlock"
CUSTOM = "custom"
RESET = "reset"
LOCK_EARLY = "lock_early"
class DoorLockingRule(ForbidExtraBaseModel):
type: DoorLockingRuleType
ended_time: UnixTimestampDateTime
class DoorEmergencyStatus(ForbidExtraBaseModel):
# NOTE: the api docs list these as not required, but always include them
lockdown: bool = False
evacuation: bool = False

View File

@ -0,0 +1,78 @@
import datetime
from enum import StrEnum
from typing import Literal
from pydantic import Field
from ._base import ActorId, ForbidExtraBaseModel, UnixTimestampDateTime
class SystemLogTopic(StrEnum):
ALL = "all"
"""All logs"""
DOOR_OPENINGS = "door_openings"
"""Door opening logs"""
CRITICAL = "critical"
"""Device restart, deletion, offline status, and detection"""
UPDATES = "updates"
"""Device update logs"""
DEVICE_EVENTS = "device_events"
"""
Device online status, device updates, access policy synchronization,
and active and inactive door unlock schedules
"""
ADMIN_ACTIVITY = "admin_activity"
"""Admin activity, such as access policy updates, settings changes, and user management"""
VISITOR = "visitor"
"""Visitor-related operations"""
class SystemLogEvent(ForbidExtraBaseModel):
type: str
display_message: str
reason: str
result: str
published: UnixTimestampDateTime
log_key: str
class SystemLogActor(ForbidExtraBaseModel):
id: Literal[""] | ActorId # XXX: check if this is complete
type: str # TODO: at least Literal["user", "service"], maybe more
display_name: str
alternate_id: str # TODO: is this also an ActorId?
alternate_name: str
avatar: str | None = None
sso_picture: str | None = None
class SystemLogAuthentication(ForbidExtraBaseModel):
credential_provider: str
issuer: str
# TODO: some of these could be narrowed
class SystemLogTarget(ForbidExtraBaseModel):
type: str
id: str
alternate_id: str
alternate_name: str
display_name: str
class SystemLogSource(ForbidExtraBaseModel):
actor: SystemLogActor
event: SystemLogEvent
authentication: SystemLogAuthentication | None
target: list[SystemLogTarget]
class SystemLogEntry(ForbidExtraBaseModel):
timestamp: datetime.datetime = Field(validation_alias="@timestamp")
id: str = Field(alias="_id")
source: SystemLogSource = Field(alias="_source")
tag: str
class FetchSystemLogsResponse(ForbidExtraBaseModel):
hits: list[SystemLogEntry]

View File

@ -0,0 +1,84 @@
"""3.1 User Schemas"""
from enum import StrEnum
from ._access_policy import AccessPolicy
from ._base import (
ForbidExtraBaseModel,
NfcCardId,
NfcCardToken,
PinCode,
UnixTimestampDateTime,
UserGroupId,
UserId,
)
class UserStatus(StrEnum):
ACTIVE = "ACTIVE"
"""The user account is in active status."""
PENDING = "PENDING"
"""A new admin account has been invited by the SSO account, but the invitation has not been accepted."""
DEACTIVATED = "DEACTIVATED"
"""The account has been deactivated."""
# TODO: should be a partial of NfcCard
class UserNfcCard(ForbidExtraBaseModel):
id: NfcCardId
"""Display ID of the NFC card."""
token: NfcCardToken
"""Unique NFC card token."""
type: str
"""!!! note "not listed in API docs" """
class User(ForbidExtraBaseModel):
id: UserId
"""Identity ID of the user."""
first_name: str
"""First name of the user."""
last_name: str
"""Last name of the user."""
full_name: str
"""Full name of the user."""
alias: str
"""Preferred name of the user."""
user_email: str
"""Email of the user."""
email: str
"""!!! note "not listed in API docs, Unclear how this is different from `user_email`" """
email_status: str
"""The status of the user's email."""
username: str
"""!!! note "not listed in API docs" """
avatar_relative_path: str
"""!!! note "not listed in API docs" """
phone: str
"""Contact phone number of the user."""
employee_number: str
"""Employee number of the user."""
onboard_time: UnixTimestampDateTime
"""User onboarding date."""
status: UserStatus
class FullUser(User):
nfc_cards: list[UserNfcCard]
"""Token associated with the bound NFC card."""
pin_code: PinCode | None
"""Token associated with the bound PIN code."""
access_policy_ids: list[str] | None = None
"""Collection of the access policy ID."""
access_policies: list[AccessPolicy] | None = None
"""All policies assigned to the user."""
class UserGroup(ForbidExtraBaseModel):
"""!!! note "not listed in API docs" """
id: UserGroupId
full_name: str
name: str
up_id: UserGroupId
up_ids: list[UserGroupId]

View File

@ -0,0 +1,91 @@
from enum import StrEnum
from typing import Annotated
from pydantic import BeforeValidator
from unifi_access.schemas import UserNfcCard
from unifi_access.schemas._access_policy import PartialSchedule
from ._base import (
ForbidExtraBaseModel,
NamedResource,
PinCode,
ScheduleId,
UnixTimestampDateTime,
UserId,
VisitorId,
)
class VisitorStatus(StrEnum):
UPCOMING = "UPCOMING"
VISITED = "VISITED"
VISITING = "VISITING"
CANCELLED = "CANCELLED"
NO_VISIT = "NO_VISIT"
ACTIVE = "ACTIVE"
class VisitReason(StrEnum):
INTERVIEW = "Interview"
BUSINESS = "Business"
COOPERATION = "Cooperation"
OTHERS = "Others"
# TODO: some of these might not be nullable from some endpoints
# Generic to avoid duplication. `fetch_visitor` returns less info in schedule than `get_all_visitors`.
class Visitor(ForbidExtraBaseModel):
id: VisitorId
"""Identity ID of the visitor."""
first_name: str
"""First name of the visitor."""
last_name: str
"""Last name of the visitor."""
status: VisitorStatus
"""The visitor's status."""
inviter_id: UserId | None = None
"""Identity ID of the inviter."""
inviter_name: str | None = None
"""Name of the inviter."""
mobile_phone: str | None = None
"""Contact phone number of the visitor."""
remarks: str | None = None
"""Remarks of the visitor."""
email: str | None = None
"""Email of the visitor."""
visitor_company: str | None = None
"""Company of the visitor."""
visit_reason: VisitReason | None = None
"""Visit reason"""
start_time: UnixTimestampDateTime | None = None
"""Start time of the visit."""
end_time: UnixTimestampDateTime | None = None
"""End time of the visit."""
nfc_cards: list[UserNfcCard]
# coerce empty dict to None, because for some reason that is what `fetch_visitor` returns
pin_code: Annotated[PinCode | None, BeforeValidator(lambda v: v or None)] = None
schedule_id: ScheduleId
"""Identity ID of the schedule."""
schedule: PartialSchedule | None = None
"""
The schedule assigned to the visitor.
If the schedule information is present, it indicates that the visit schedule is recurring.
If the schedule information is not included, it indicates a one-time visit schedule.
"""
resources: list[NamedResource] | None = None
"""Specify the locations that the visitor can access."""
avatar: str | None = None
"""!!! note "not listed in API docs" """
location_id: str | None = None
"""!!! note "not listed in API docs" """
class FetchAllVisitorsExpansion(StrEnum):
NONE = "none"
ACCESS_POLICY = "access_policy"
RESOURCE = "resource"
SCHEDULE = "schedule"
NFC_CARD = "nfc_card"
PIN_CODE = "pin_code"

0
tests/__init__.py Normal file
View File

30
tests/base.py Normal file
View File

@ -0,0 +1,30 @@
import unittest
import unifi_access
class UnifiAccessTests(unittest.TestCase):
def setUp(self) -> None:
self.host = "localhost:12445"
self.api_token = "EXAMPLE_TOKEN"
self.common_headers = {
"Authorization": f"Bearer {self.api_token}",
"Accept": "application/json",
"Content-Type": "application/json",
}
self.client = unifi_access.AccessClient(
host=self.host, api_token=self.api_token
)
# XXX: TODO:
# 11 Notification
# - 11.1 Fetch Notifications [WebSocket]
# - 11.2 List of Supported Webhook Events [Webhook]
# - 11.3 Fetch Webhook Endpoints List [Webhook]
# - 11.4 Add Webhook Endpoints [Webhook]
# - 11.5 Update Webhook Endpoints [Webhook]
# - 11.6 Delete Webhook Endpoints [Webhook]
# - 11.7 Allow Webhook Endpoint Owner to Receive Webhook Events [Webhook]
# 12 API Server
# - 12.1 Upload HTTPS Certificate
# - 12.2 Delete HTTPS Certificate

22
tests/conftest.py Normal file
View File

@ -0,0 +1,22 @@
from typing import cast
import pytest
from unifi_access import AccessClient
host = "localhost:12445"
api_token = "EXAMPLE_TOKEN"
def pytest_addoption(parser: pytest.Parser) -> None:
parser.addoption("--live-access-host", help="Live Unifi Access Host")
parser.addoption("--live-access-api-token", help="Live Unifi Access Host")
@pytest.fixture(scope="session")
def live_access_client(request: pytest.FixtureRequest) -> AccessClient:
return AccessClient(
cast(str, request.config.getoption("--live-access-host", skip=True)),
cast(str, request.config.getoption("--live-access-api-token", skip=True)),
verify=False,
)

0
tests/live/__init__.py Normal file
View File

View File

@ -0,0 +1,87 @@
from collections.abc import Generator
import pytest
from unifi_access import AccessClient
from unifi_access.schemas import AccessPolicy, HolidayGroup, Schedule, WeekSchedule
from unifi_access.schemas._base import DoorResource
@pytest.fixture
def holiday_group(live_access_client: AccessClient) -> Generator[HolidayGroup]:
_holiday_group = (
live_access_client.create_holiday_group("Test Holiday Group")
.success_or_raise()
.data
)
yield _holiday_group
live_access_client.delete_holiday_group(_holiday_group.id)
@pytest.fixture
def schedule(
live_access_client: AccessClient, holiday_group: HolidayGroup
) -> Generator[Schedule]:
_schedule = (
live_access_client.create_schedule(
"Test Schedule",
week_schedule=WeekSchedule(
sunday=[],
monday=[],
tuesday=[],
wednesday=[],
thursday=[],
friday=[],
saturday=[],
),
holiday_group_id=holiday_group.id,
)
.success_or_raise()
.data
)
yield _schedule
live_access_client.delete_schedule(_schedule.id)
@pytest.fixture
def access_policy(
live_access_client: AccessClient, schedule: Schedule
) -> Generator[AccessPolicy]:
_access_policy = (
live_access_client.create_access_policy("Test Access Policy", schedule.id)
.success_or_raise()
.data
)
yield _access_policy
live_access_client.delete_access_policy(_access_policy.id)
def test_access_policy_lifecycle(
live_access_client: AccessClient,
holiday_group: HolidayGroup,
schedule: Schedule,
access_policy: AccessPolicy,
) -> None:
assert access_policy
doors = live_access_client.fetch_all_doors().success_or_raise().data
resources = [DoorResource(id=door.id) for door in doors]
updated_access_policy = (
live_access_client.update_access_policy(access_policy.id, resources=resources)
.success_or_raise()
.data
)
assert updated_access_policy.resources == resources
updated_holiday_group = (
live_access_client.update_holiday_group(
holiday_group.id, description="Test Description"
)
.success_or_raise()
.data
)
assert updated_holiday_group.description == "Test Description"
holiday_groups = (
live_access_client.fetch_all_holiday_groups().success_or_raise().data
)
assert any(g.id == updated_holiday_group.id for g in holiday_groups)

View File

@ -0,0 +1,128 @@
import functools
from collections.abc import Generator
from typing import Protocol
import pytest
from unifi_access import AccessClient
from unifi_access.schemas import User, UserGroup, UserGroupId, UserStatus
@pytest.fixture(scope="module")
def user(live_access_client: AccessClient) -> User:
# XXX: maybe should test email and employeeNumber, but they are
# unique on the server, and I can't delete users. Maybe
# should fetch user if it already exists?
# TODO: add an onboard time?
return live_access_client.register_user("Test", "User").success_or_raise().data
def test_user_lifecycle(live_access_client: AccessClient, user: User) -> None:
assert user.id
assert user.first_name == "Test"
assert user.last_name == "User"
assert user.status == UserStatus.ACTIVE
# Fetch user
fetched_user = live_access_client.fetch_user(user.id).success_or_raise().data
assert fetched_user.id == user.id
assert fetched_user.first_name == "Test"
assert fetched_user.last_name == "User"
assert fetched_user.status == UserStatus.ACTIVE
# Update user
live_access_client.update_user(
user.id, last_name="Updated User", status=UserStatus.DEACTIVATED
).success_or_raise()
# Fetch again, and check changes
fetched_user = live_access_client.fetch_user(user.id).success_or_raise().data
assert fetched_user.id == user.id
assert fetched_user.first_name == "Test"
assert fetched_user.last_name == "Updated User"
assert fetched_user.status == UserStatus.DEACTIVATED
# Check for the user in full user list
# TODO: test pagination
all_users = live_access_client.fetch_all_users().success_or_raise().data
matching_user = next(u for u in all_users if u.id == user.id)
assert matching_user.id == user.id
assert matching_user.first_name == "Test"
assert matching_user.last_name == "Updated User"
assert matching_user.status == UserStatus.DEACTIVATED
class UserGroupFactory(Protocol):
def __call__(
self, name: str, up_id: UserGroupId | None = None
) -> UserGroup | None: ...
@pytest.fixture
def make_user_group(
live_access_client: AccessClient,
) -> Generator[UserGroupFactory]:
created_groups = []
@functools.wraps(live_access_client.create_user_group)
def _make_user_group(
name: str,
up_id: UserGroupId | None = None,
) -> UserGroup | None:
group = (
live_access_client.create_user_group(name, up_id).success_or_raise().data
)
created_groups.append(group)
return group
yield _make_user_group
for group in reversed(created_groups):
if group is not None:
live_access_client.delete_user_group(group.id)
def test_user_groups_lifecycle(
live_access_client: AccessClient,
make_user_group: UserGroupFactory,
user: User,
) -> None:
# Create group
group = make_user_group("Test Group")
assert group, "group 'Test Group' already existed, please delete"
# Fetch group
fetched_group = (
live_access_client.fetch_user_group(group.id).success_or_raise().data
)
assert fetched_group.name == "Test Group"
# Check for group in in full group list
groups = live_access_client.fetch_all_user_groups().success_or_raise().data
matching_group = next(g for g in groups if g.id == group.id)
assert matching_group
# Create child group
child_group = make_user_group("Test Group Child", group.id)
assert child_group, "group 'Test Group / Test Group Child' already existed, which shouldn't be possible"
assert child_group.up_id == group.id
assert child_group.up_ids[-1] == group.id
# Add user to group
live_access_client.assign_user_to_user_group(group.id, [user.id])
# Fetch users in the group
users_in_group = (
live_access_client.fetch_users_in_a_user_group(group.id).success_or_raise().data
)
assert len(users_in_group) == 1
assert users_in_group[0].id == user.id
# Fetch all users in the group
all_users_in_group = (
live_access_client.fetch_all_users_in_a_user_group(group.id)
.success_or_raise()
.data
)
assert len(all_users_in_group) == 1
assert all_users_in_group[0].id == user.id

View File

@ -0,0 +1,104 @@
import datetime
import functools
from collections.abc import Callable, Generator
import pytest
from unifi_access import AccessClient
from unifi_access.schemas import FetchAllVisitorsExpansion, Visitor, VisitReason
@pytest.fixture
def make_visitor(live_access_client: AccessClient) -> Generator[Callable[..., Visitor]]:
created_visitors: list[Visitor] = []
@functools.wraps(live_access_client.create_visitor)
def _make_visitor(*args, **kwargs) -> Visitor:
visitor = (
live_access_client.create_visitor(*args, **kwargs).success_or_raise().data
)
created_visitors.append(visitor)
return visitor
yield _make_visitor
for visitor in created_visitors:
live_access_client.delete_visitor(visitor.id, is_force=True)
def test_visitor_lifecycle(
live_access_client: AccessClient, make_visitor: Callable[..., Visitor]
) -> None:
now = datetime.datetime.now(tz=datetime.UTC)
visitor = make_visitor(
"Test",
"Visitor",
start_time=now + datetime.timedelta(days=1),
end_time=now + datetime.timedelta(days=2),
visit_reason=VisitReason.BUSINESS,
)
assert visitor.first_name == "Test"
fetched_visitor = (
live_access_client.fetch_visitor(visitor.id).success_or_raise().data
)
assert fetched_visitor.first_name == "Test"
assert fetched_visitor.pin_code is None
# pin code assign/unassign
pin_code = live_access_client.generate_pin_code().success_or_raise().data
live_access_client.assign_pin_code_to_visitor(
visitor.id, pin_code
).success_or_raise()
fetched_visitor = (
live_access_client.fetch_visitor(visitor.id).success_or_raise().data
)
assert fetched_visitor.pin_code
assert fetched_visitor.pin_code.token
live_access_client.unassign_pin_code_from_visitor(visitor.id).success_or_raise()
fetched_visitor = (
live_access_client.fetch_visitor(visitor.id).success_or_raise().data
)
assert fetched_visitor.pin_code is None
updated_visitor = (
live_access_client.update_visitor(visitor.id, first_name="Updated Test")
.success_or_raise()
.data
)
assert updated_visitor.first_name == "Updated Test"
all_visitors = live_access_client.fetch_all_visitors().success_or_raise().data
matched_visitor = next(v for v in all_visitors if v.id == visitor.id)
assert matched_visitor.first_name == "Updated Test"
expanded_all_visitors = (
live_access_client.fetch_all_visitors(
expand=[
FetchAllVisitorsExpansion.ACCESS_POLICY,
FetchAllVisitorsExpansion.RESOURCE,
FetchAllVisitorsExpansion.SCHEDULE,
FetchAllVisitorsExpansion.NFC_CARD,
FetchAllVisitorsExpansion.PIN_CODE,
]
)
.success_or_raise()
.data
)
expanded_matched_visitor = next(
v for v in expanded_all_visitors if v.id == visitor.id
)
assert expanded_matched_visitor.first_name == "Updated Test"
# TODO: test expanded contents
non_expanded_all_visitors = (
live_access_client.fetch_all_visitors(expand=[FetchAllVisitorsExpansion.NONE])
.success_or_raise()
.data
)
non_expanded_matched_visitor = next(
v for v in non_expanded_all_visitors if v.id == visitor.id
)
assert non_expanded_matched_visitor.first_name == "Updated Test"
assert non_expanded_matched_visitor.schedule is None
# TODO: test other non-expanded contents

869
tests/test_access_policy.py Normal file
View File

@ -0,0 +1,869 @@
import datetime
import responses
from responses import matchers
from unifi_access.schemas import (
AccessPolicyId,
DoorGroupId,
DoorGroupResource,
DoorId,
DoorResource,
HolidayGroupId,
HolidayId,
PartialHoliday,
ScheduleId,
TimePeriod,
WeekSchedule,
)
from .base import UnifiAccessTests
class AccessPolicyTests(UnifiAccessTests):
@responses.activate
def test_create_access_policy(self) -> None:
"""5.2 Create Access Policy"""
responses.post(
f"https://{self.host}/api/v1/developer/access_policies",
match=[
matchers.header_matcher(self.common_headers),
matchers.json_params_matcher(
{
"name": "test",
"resource": [
{
"id": "6ff875d2-af87-470b-9cb5-774c6596afc8",
"type": "door",
},
{
"id": "5c496423-6d25-4e4f-8cdf-95ad5135188a",
"type": "door_group",
},
{
"id": "d5573467-d6b3-4e8f-8e48-8a322b91664a",
"type": "door_group",
},
],
"schedule_id": "4e108aeb-ec9a-4822-bf86-170ea986f934",
}
),
],
json={
"code": "SUCCESS",
"data": {
"id": "bb5eb965-42dc-4206-9654-88a2d1c3aaa5",
"name": "test",
"resources": [
{"id": "6ff875d2-af87-470b-9cb5-774c6596afc8", "type": "door"},
{
"id": "5c496423-6d25-4e4f-8cdf-95ad5135188a",
"type": "door_group",
},
{
"id": "d5573467-d6b3-4e8f-8e48-8a322b91664a",
"type": "door_group",
},
],
"schedule_id": "4e108aeb-ec9a-4822-bf86-170ea986f934",
},
"msg": "success",
},
)
resp = self.client.create_access_policy(
name="test",
resources=[
DoorResource(
id=DoorId("6ff875d2-af87-470b-9cb5-774c6596afc8"),
),
DoorGroupResource(
id=DoorGroupId("5c496423-6d25-4e4f-8cdf-95ad5135188a"),
),
DoorGroupResource(
id=DoorGroupId("d5573467-d6b3-4e8f-8e48-8a322b91664a"),
),
],
schedule_id=ScheduleId("4e108aeb-ec9a-4822-bf86-170ea986f934"),
).success_or_raise()
assert resp.data.id == "bb5eb965-42dc-4206-9654-88a2d1c3aaa5"
@responses.activate
def test_update_access_policy(self) -> None:
"""5.3 Update Access Policy"""
access_policy_id = AccessPolicyId("242c88e3-0524-42de-8447-45891c5df714")
responses.put(
f"https://{self.host}/api/v1/developer/access_policies/{access_policy_id}",
match=[
matchers.header_matcher(self.common_headers),
matchers.json_params_matcher(
{
"name": "test",
"resource": [
{
"id": "6ff875d2-af87-470b-9cb5-774c6596afc8",
"type": "door",
},
{
"id": "5c496423-6d25-4e4f-8cdf-95ad5135188a",
"type": "door_group",
},
{
"id": "d5573467-d6b3-4e8f-8e48-8a322b91664a",
"type": "door_group",
},
],
"schedule_id": "4e108aeb-ec9a-4822-bf86-170ea986f934",
}
),
],
# NOTE: no example provided in docs, using the same one as `create`
json={
"code": "SUCCESS",
"data": {
"id": "bb5eb965-42dc-4206-9654-88a2d1c3aaa5",
"name": "test",
"resources": [
{"id": "6ff875d2-af87-470b-9cb5-774c6596afc8", "type": "door"},
{
"id": "5c496423-6d25-4e4f-8cdf-95ad5135188a",
"type": "door_group",
},
{
"id": "d5573467-d6b3-4e8f-8e48-8a322b91664a",
"type": "door_group",
},
],
"schedule_id": "4e108aeb-ec9a-4822-bf86-170ea986f934",
},
"msg": "success",
},
)
resp = self.client.update_access_policy(
access_policy_id=access_policy_id,
name="test",
resources=[
DoorResource(
id=DoorId("6ff875d2-af87-470b-9cb5-774c6596afc8"),
),
DoorGroupResource(
id=DoorGroupId("5c496423-6d25-4e4f-8cdf-95ad5135188a"),
),
DoorGroupResource(
id=DoorGroupId("d5573467-d6b3-4e8f-8e48-8a322b91664a"),
),
],
schedule_id=ScheduleId("4e108aeb-ec9a-4822-bf86-170ea986f934"),
).success_or_raise()
assert resp.data.id == "bb5eb965-42dc-4206-9654-88a2d1c3aaa5"
@responses.activate
def test_delete_access_policy(self) -> None:
"""5.4 Delete Access Policy"""
access_policy_id = AccessPolicyId("60d0bcc-5d4f-4e7b-8a3c-8d4502765e11")
responses.delete(
f"https://{self.host}/api/v1/developer/access_policies/{access_policy_id}",
match=[matchers.header_matcher(self.common_headers)],
json={"code": "SUCCESS", "msg": "success", "data": "success"},
)
resp = self.client.delete_access_policy(
access_policy_id=access_policy_id
).success_or_raise()
assert resp.data == "success"
@responses.activate
def test_fetch_access_policy(self) -> None:
"""5.5 Fetch Access Policy"""
access_policy_id = AccessPolicyId("ed09985f-cf52-486e-bc33-377b6ed7bbf2")
responses.get(
f"https://{self.host}/api/v1/developer/access_policies/{access_policy_id}",
match=[matchers.header_matcher(self.common_headers)],
json={
"code": "SUCCESS",
"data": {
"id": "ed09985f-cf52-486e-bc33-377b6ed7bbf2",
"name": "test11",
"resources": [
{"id": "6ff875d2-af87-470b-9cb5-774c6596afc8", "type": "door"},
{
"id": "5c496423-6d25-4e4f-8cdf-95ad5135188a",
"type": "door_group",
},
{
"id": "d5573467-d6b3-4e8f-8e48-8a322b91664a",
"type": "door_group",
},
],
"schedule_id": "4e108aeb-ec9a-4822-bf86-170ea986f934",
},
"msg": "success",
},
)
resp = self.client.fetch_access_policy(
access_policy_id=access_policy_id
).success_or_raise()
assert resp.data.id == "ed09985f-cf52-486e-bc33-377b6ed7bbf2"
@responses.activate
def test_fetch_all_access_policies(self) -> None:
"""5.6 Fetch All Access Policies"""
responses.get(
f"https://{self.host}/api/v1/developer/access_policies",
match=[matchers.header_matcher(self.common_headers)],
json={
"code": "SUCCESS",
"data": [
{
"id": "73f15cab-c725-4a76-a419-a4026d131e96",
"name": "Default Admin Policy",
"resources": [
{
"id": "d5573467-d6b3-4e8f-8e48-8a322b91664a",
"type": "door_group",
},
{
"id": "5c496423-6d25-4e4f-8cdf-95ad5135188a",
"type": "door_group",
},
],
"schedule_id": "73facd6c-839e-4521-a4f4-c07e1d44e748",
},
{
"id": "b96948a4-fed9-40a3-9c4a-e473822a3db7",
"name": "Default UNVR Policy",
"resources": [
{
"id": "d5573467-d6b3-4e8f-8e48-8a322b91664a",
"type": "door_group",
},
{
"id": "6ff875d2-af87-470b-9cb5-774c6596afc8",
"type": "door",
},
],
"schedule_id": "58c0f89b-f35c-4d2c-af7b-8b8918df2c31",
},
{
"id": "edbc80df-3698-49fd-8b53-f1867f104947",
"name": "TEST",
"resources": [
{
"id": "d5573467-d6b3-4e8f-8e48-8a322b91664a",
"type": "door_group",
},
{
"id": "5c496423-6d25-4e4f-8cdf-95ad5135188a",
"type": "door_group",
},
{
"id": "6ff875d2-af87-470b-9cb5-774c6596afc8",
"type": "door",
},
],
"schedule_id": "73facd6c-839e-4521-a4f4-c07e1d44e748",
},
],
"msg": "success",
},
)
resp = self.client.fetch_all_access_policies().success_or_raise()
assert resp.data[0].id == "73f15cab-c725-4a76-a419-a4026d131e96"
@responses.activate
def test_create_holiday_group(self) -> None:
"""5.8 Create Holiday Group"""
responses.post(
f"https://{self.host}/api/v1/developer/access_policies/holiday_groups",
match=[
matchers.header_matcher(self.common_headers),
matchers.json_params_matcher(
{
"name": "Holiday Group-169286791557142",
"holidays": [
{
"name": "Holiday Name 1",
"description": "",
"repeat": False,
"start_time": "2023-08-25T00:00:00Z",
"end_time": "2023-08-26T00:00:00Z",
},
{
"name": "Holiday Name 2",
"description": "",
"repeat": False,
"start_time": "2023-08-26T00:00:00Z",
"end_time": "2023-08-27T00:00:00Z",
},
],
}
),
],
json={
"code": "SUCCESS",
"data": {
"description": "",
"holidays": [
{
"description": "",
"end_time": "2023-08-26 00:00:00Z",
"id": "8900533d-03be-4f84-832d-54ff59905759",
"name": "Holiday Name 1",
"repeat": False,
"start_time": "2023-08-25 00:00:00Z",
},
{
# NOTE: duplicated key in sample data
# "name": "holiday-2023-08-26",
"end_time": "2023-08-27 00:00:00Z",
"id": "9fff81cc-d476-40c4-80f9-d510451ce2cd",
"name": "Holiday Name 2",
"repeat": False,
"start_time": "2023-08-26 00:00:00Z",
},
],
"id": "7be7a7a0-818f-4f76-98c3-1c38957f4dca",
"is_default": False,
"name": "Holiday Group-169286791557142",
"template_name": "",
},
"msg": "success",
},
)
resp = self.client.create_holiday_group(
name="Holiday Group-169286791557142",
holidays=[
PartialHoliday(
name="Holiday Name 1",
description="",
repeat=False,
start_time=datetime.datetime(2023, 8, 25, tzinfo=datetime.UTC),
end_time=datetime.datetime(2023, 8, 26, tzinfo=datetime.UTC),
),
PartialHoliday(
name="Holiday Name 2",
description="",
repeat=False,
start_time=datetime.datetime(2023, 8, 26, tzinfo=datetime.UTC),
end_time=datetime.datetime(2023, 8, 27, tzinfo=datetime.UTC),
),
],
).success_or_raise()
assert resp.data.id == "7be7a7a0-818f-4f76-98c3-1c38957f4dca"
@responses.activate
def test_update_holiday_group(self) -> None:
"""5.9 Update Holiday Group"""
holiday_group_id = HolidayGroupId("7be7a7a0-818f-4f76-98c3-1c38957f4dca")
responses.put(
f"https://{self.host}/api/v1/developer/access_policies/holiday_groups/{holiday_group_id}",
match=[
matchers.header_matcher(self.common_headers),
matchers.json_params_matcher(
{
"name": "Holiday Group-169286791557142",
"holidays": [
# add a new holiday
{
"name": "Holiday Name 1",
"description": "",
"repeat": False,
"start_time": "2023-08-25T00:00:00Z",
"end_time": "2023-08-26T00:00:00Z",
},
# update an existing holiday
{
"id": "d23a4226-765f-4967-b84f-6dfd53f33c89",
"name": "Holiday Name 2",
"description": "",
"repeat": False,
"start_time": "2023-08-26T00:00:00Z",
"end_time": "2023-08-27T00:00:00Z",
},
],
}
),
],
json={
"code": "SUCCESS",
"data": {
"description": "",
"holidays": [
{
"description": "",
"end_time": "2023-08-26 00:00:00Z",
"id": "8900533d-03be-4f84-832d-54ff59905759",
"name": "Holiday Name 1",
"repeat": False,
"start_time": "2023-08-25 00:00:00Z",
},
{
"description": "",
"end_time": "2023-08-27 00:00:00Z",
"id": "9fff81cc-d476-40c4-80f9-d510451ce2cd",
"name": "Holiday Name 2",
"repeat": False,
"start_time": "2023-08-26 00:00:00Z",
},
],
"id": "7be7a7a0-818f-4f76-98c3-1c38957f4dca",
"is_default": False,
"name": "Holiday Group-169286791557142",
"template_name": "",
},
"msg": "success",
},
)
resp = self.client.update_holiday_group(
holiday_group_id=holiday_group_id,
name="Holiday Group-169286791557142",
holidays=[
# add a new holiday
PartialHoliday(
name="Holiday Name 1",
description="",
repeat=False,
start_time=datetime.datetime(2023, 8, 25, tzinfo=datetime.UTC),
end_time=datetime.datetime(2023, 8, 26, tzinfo=datetime.UTC),
),
# update an existing holiday
PartialHoliday(
id=HolidayId("d23a4226-765f-4967-b84f-6dfd53f33c89"),
name="Holiday Name 2",
description="",
repeat=False,
start_time=datetime.datetime(2023, 8, 26, tzinfo=datetime.UTC),
end_time=datetime.datetime(2023, 8, 27, tzinfo=datetime.UTC),
),
],
).success_or_raise()
assert resp.data.id == "7be7a7a0-818f-4f76-98c3-1c38957f4dca"
@responses.activate
def test_delete_holiday_group(self) -> None:
"""5.10 Delete Holiday Group"""
holiday_group_id = HolidayGroupId("7be7a7a0-818f-4f76-98c3-1c38957f4dca")
responses.delete(
f"https://{self.host}/api/v1/developer/access_policies/holiday_groups/{holiday_group_id}",
match=[matchers.header_matcher(self.common_headers)],
json={"code": "SUCCESS", "msg": "success", "data": "success"},
)
resp = self.client.delete_holiday_group(
holiday_group_id=holiday_group_id
).success_or_raise()
assert resp.data == "success"
@responses.activate
def test_fetch_holiday_group(self) -> None:
"""5.11 Fetch Holiday Group"""
holiday_group_id = HolidayGroupId("7be7a7a0-818f-4f76-98c3-1c38957f4dca")
responses.get(
f"https://{self.host}/api/v1/developer/access_policies/holiday_groups/{holiday_group_id}",
match=[matchers.header_matcher(self.common_headers)],
json={
"code": "SUCCESS",
"data": {
"description": "",
"holidays": [
{
"description": "",
"end_time": "2023-08-26 00:00:00Z",
"id": "8900533d-03be-4f84-832d-54ff59905759",
"name": "Holiday Name 1",
"repeat": False,
"start_time": "2023-08-25 00:00:00Z",
},
{
"description": "",
"end_time": "2023-08-27 00:00:00Z",
"id": "9fff81cc-d476-40c4-80f9-d510451ce2cd",
"name": "Holiday Name 2",
"repeat": False,
"start_time": "2023-08-26 00:00:00Z",
},
],
"id": "7be7a7a0-818f-4f76-98c3-1c38957f4dca",
"is_default": False,
"name": "Holiday Group-169286791557142",
"template_name": "",
},
"msg": "success",
},
)
resp = self.client.fetch_holiday_group(
holiday_group_id=holiday_group_id
).success_or_raise()
assert resp.data.id == "7be7a7a0-818f-4f76-98c3-1c38957f4dca"
@responses.activate
def test_fetch_all_holiday_groups(self) -> None:
"""5.12 Fetch All Holiday Groups"""
responses.get(
f"https://{self.host}/api/v1/developer/access_policies/holiday_groups",
match=[matchers.header_matcher(self.common_headers)],
json={
"code": "SUCCESS",
"data": [
{
"count": 0,
"description": "",
"id": "8cc22b49-a7f4-49a6-9f04-044444992d6c",
"is_default": True,
"name": "No Holidays",
},
{
"count": 2,
"description": "",
"id": "86c634da-7b2c-411c-a2c1-1495d089c719",
"is_default": False,
"name": "Holiday Group-1692867312225",
},
],
"msg": "success",
},
)
resp = self.client.fetch_all_holiday_groups().success_or_raise()
assert resp.data[0].id == "8cc22b49-a7f4-49a6-9f04-044444992d6c"
@responses.activate
def test_create_schedule(self) -> None:
"""5.14 Create Schedule"""
responses.post(
f"https://{self.host}/api/v1/developer/access_policies/schedules",
match=[
matchers.header_matcher(self.common_headers),
matchers.json_params_matcher(
{
"name": "schedule-1688977094169",
"week_schedule": {
"sunday": [],
"monday": [
{"start_time": "10:00:00", "end_time": "17:00:59"}
],
"tuesday": [
{"start_time": "10:00:00", "end_time": "17:00:59"}
],
"wednesday": [
{"start_time": "10:00:00", "end_time": "17:00:59"}
],
"thursday": [],
"friday": [
{"start_time": "10:00:00", "end_time": "17:00:59"}
],
"saturday": [],
},
"holiday_group_id": "75660081-431b-4dbe-9b98-e0257877118e",
"holiday_schedule": [
{"start_time": "03:15:00", "end_time": "11:45:59"},
{"start_time": "15:00:00", "end_time": "19:00:59"},
],
}
),
],
json={
"code": "SUCCESS",
"data": {
"id": "1d31b648-b8ff-4bd1-b742-60dbd70592cd",
"is_default": False,
"name": "schedule-1688977094169",
"type": "access",
"weekly": {
"friday": [{"end_time": "17:00:59", "start_time": "10:00:00"}],
"monday": [{"end_time": "17:00:59", "start_time": "10:00:00"}],
"saturday": [],
"sunday": [],
"thursday": [],
"tuesday": [{"end_time": "17:00:59", "start_time": "10:00:00"}],
"wednesday": [
{"end_time": "17:00:59", "start_time": "10:00:00"}
],
},
"holiday_group_id": "75660081-431b-4dbe-9b98-e0257877118e",
"holiday_group": {
"description": "",
"holidays": [
{
"description": "",
"end_time": "2023-08-26 00:00:00Z",
"id": "d51777c4-9559-45aa-8e23-434995d9d2a1",
"is_template": False,
"name": "Holiday Name 1",
"repeat": False,
"start_time": "2023-08-25 00:00:00Z",
},
{
"description": "",
"end_time": "2023-08-27 00:00:00Z",
"id": "d23a4226-765f-4967-b84f-6dfd53f33c89",
"is_template": False,
"name": "Holiday Name 2",
"repeat": False,
"start_time": "2023-08-26 00:00:00Z",
},
],
"id": "75660081-431b-4dbe-9b98-e0257877118e",
"is_default": False,
"name": "Holiday Group-1692867915571423",
"template_name": "",
},
"holiday_schedule": [
{"start_time": "03:15:00", "end_time": "11:45:59"},
{"start_time": "15:00:00", "end_time": "19:00:59"},
],
},
"msg": "success",
},
)
resp = self.client.create_schedule(
name="schedule-1688977094169",
week_schedule=WeekSchedule(
sunday=[],
monday=[
TimePeriod(
start_time=datetime.time(10, 0, 0),
end_time=datetime.time(17, 0, 59),
)
],
tuesday=[
TimePeriod(
start_time=datetime.time(10, 0, 0),
end_time=datetime.time(17, 0, 59),
)
],
wednesday=[
TimePeriod(
start_time=datetime.time(10, 0, 0),
end_time=datetime.time(17, 0, 59),
)
],
thursday=[],
friday=[
TimePeriod(
start_time=datetime.time(10, 0, 0),
end_time=datetime.time(17, 0, 59),
)
],
saturday=[],
),
holiday_group_id=HolidayGroupId("75660081-431b-4dbe-9b98-e0257877118e"),
holiday_schedule=[
TimePeriod(
start_time=datetime.time(3, 15, 0),
end_time=datetime.time(11, 45, 59),
),
TimePeriod(
start_time=datetime.time(15, 0, 0),
end_time=datetime.time(19, 0, 59),
),
],
).success_or_raise()
@responses.activate
def test_update_schedule(self) -> None:
"""5.15 Update Schedule"""
schedule_id = ScheduleId("1d31b648-b8ff-4bd1-b742-60dbd70592cd")
responses.put(
f"https://{self.host}/api/v1/developer/access_policies/schedules/{schedule_id}",
match=[
matchers.header_matcher(self.common_headers),
matchers.json_params_matcher(
{
"name": "schedule-1688977094169",
"holiday_group_id": "75660081-431b-4dbe-9b98-e0257877118e",
"week_schedule": {
"sunday": [],
"monday": [
{"start_time": "10:00:00", "end_time": "17:00:59"}
],
"tuesday": [
{"start_time": "10:00:00", "end_time": "17:00:59"}
],
"wednesday": [
{"start_time": "10:00:00", "end_time": "17:00:59"}
],
"thursday": [
{"start_time": "10:00:00", "end_time": "17:01:59"}
],
"friday": [
{"start_time": "10:00:00", "end_time": "17:00:59"}
],
"saturday": [],
},
"holiday_schedule": [
{"start_time": "03:15:00", "end_time": "11:45:59"}
],
}
),
],
json={"code": "SUCCESS", "data": {}, "msg": "success"},
)
resp = self.client.update_schedule(
schedule_id=schedule_id,
name="schedule-1688977094169",
holiday_group_id=HolidayGroupId("75660081-431b-4dbe-9b98-e0257877118e"),
week_schedule=WeekSchedule(
sunday=[],
monday=[
TimePeriod(
start_time=datetime.time(10, 0, 0),
end_time=datetime.time(17, 0, 59),
)
],
tuesday=[
TimePeriod(
start_time=datetime.time(10, 0, 0),
end_time=datetime.time(17, 0, 59),
)
],
wednesday=[
TimePeriod(
start_time=datetime.time(10, 0, 0),
end_time=datetime.time(17, 0, 59),
)
],
thursday=[
TimePeriod(
start_time=datetime.time(10, 0, 0),
end_time=datetime.time(17, 1, 59),
)
],
friday=[
TimePeriod(
start_time=datetime.time(10, 0, 0),
end_time=datetime.time(17, 0, 59),
)
],
saturday=[],
),
holiday_schedule=[
TimePeriod(
start_time=datetime.time(3, 15, 0),
end_time=datetime.time(11, 45, 59),
)
],
).success_or_raise()
@responses.activate
def test_fetch_schedule(self) -> None:
"""5.16 Fetch Schedule"""
schedule_id = ScheduleId("1d31b648-b8ff-4bd1-b742-60dbd70592cd")
responses.get(
f"https://{self.host}/api/v1/developer/access_policies/schedules/{schedule_id}",
match=[matchers.header_matcher(self.common_headers)],
json={
"code": "SUCCESS",
"data": {
"id": "1d31b648-b8ff-4bd1-b742-60dbd70592cd",
"is_default": False,
"name": "schedule-1688977094169",
"type": "access",
"weekly": {
"friday": [{"end_time": "17:00:59", "start_time": "10:00:00"}],
"monday": [{"end_time": "17:00:59", "start_time": "10:00:00"}],
"saturday": [],
"sunday": [],
"thursday": [
{"end_time": "17:01:59", "start_time": "10:00:00"}
],
"tuesday": [{"end_time": "17:00:59", "start_time": "10:00:00"}],
"wednesday": [
{"end_time": "17:00:59", "start_time": "10:00:00"}
],
},
"holiday_group_id": "75660081-431b-4dbe-9b98-e0257877118e",
"holiday_group": {
"description": "",
"holidays": [
{
"description": "",
"end_time": "2023-08-26 00:00:00Z",
"id": "d51777c4-9559-45aa-8e23-434995d9d2a1",
"is_template": False,
"name": "Holiday Name 1",
"repeat": False,
"start_time": "2023-08-25 00:00:00Z",
},
{
"description": "",
"end_time": "2023-08-27 00:00:00Z",
"id": "d23a4226-765f-4967-b84f-6dfd53f33c89",
"is_template": False,
"name": "Holiday Name 2",
"repeat": False,
"start_time": "2023-08-26 00:00:00Z",
},
],
"id": "75660081-431b-4dbe-9b98-e0257877118e",
"is_default": False,
"name": "Holiday Group-16928679155714",
"template_name": "",
},
"holiday_schedule": [
{"end_time": "11:45:59", "start_time": "09:15:00"}
],
},
"msg": "success",
},
)
resp = self.client.fetch_schedule(schedule_id=schedule_id).success_or_raise()
@responses.activate
def test_fetch_all_schedules(self) -> None:
"""5.17 Fetch All Schedules"""
responses.get(
f"https://{self.host}/api/v1/developer/access_policies/schedules",
match=[matchers.header_matcher(self.common_headers)],
json={
"code": "SUCCESS",
"data": [
{
"id": "73facd6c-839e-4521-a4f4-c07e1d44e748",
"holiday_group_id": "75660081-431b-4dbe-9b98-e0257877118e",
"is_default": True,
"name": "Always Access",
"status": 1,
"type": "access",
},
{
"id": "58c0f89b-f35c-4d2c-af7b-8b8918df2c31",
"holiday_group_id": "75660081-431b-4dbe-9b98-e0257877118e",
"is_default": False,
"name": "UNVR Schedule",
"status": 1,
"type": "access",
},
],
"msg": "success",
},
)
resp = self.client.fetch_all_schedules().success_or_raise()
@responses.activate
def test_delete_schedule(self) -> None:
"""5.18 Delete Schedule"""
schedule_id = ScheduleId("1d31b648-b8ff-4bd1-b742-60dbd70592cd")
responses.delete(
f"https://{self.host}/api/v1/developer/access_policies/schedules/{schedule_id}",
match=[matchers.header_matcher(self.common_headers)],
json={"code": "SUCCESS", "msg": "success", "data": "success"},
)
resp = self.client.delete_schedule(schedule_id=schedule_id).success_or_raise()

189
tests/test_credential.py Normal file
View File

@ -0,0 +1,189 @@
import responses
from responses import matchers
from unifi_access.schemas import DeviceId, NfcCardEnrollmentSessionId, NfcCardToken
from .base import UnifiAccessTests
class CredentialTests(UnifiAccessTests):
@responses.activate
def test_generate_pin_code(self) -> None:
"""6.1 Generate PIN Code"""
responses.post(
f"https://{self.host}/api/v1/developer/credentials/pin_codes",
match=[matchers.header_matcher(self.common_headers)],
json={"code": "SUCCESS", "data": "67203419", "msg": "success"},
)
resp = self.client.generate_pin_code().success_or_raise()
assert resp.data == "67203419"
@responses.activate
def test_begin_enroll_card(self) -> None:
"""6.2 Enroll NFC Card"""
responses.post(
f"https://{self.host}/api/v1/developer/credentials/nfc_cards/sessions",
match=[
matchers.header_matcher(self.common_headers),
matchers.json_params_matcher(
{"device_id": "0418d6a2bb7a", "reset_ua_card": False}
),
],
json={
"code": "SUCCESS",
"msg": "success",
"data": {"session_id": "e8a97c52-6676-4c48-8589-bd518afc4094"},
},
)
resp = self.client.begin_enroll_card(
device_id=DeviceId("0418d6a2bb7a"), reset_ua_card=False
).success_or_raise()
assert resp.data.session_id == "e8a97c52-6676-4c48-8589-bd518afc4094"
@responses.activate
def test_fetch_enroll_card_status(self) -> None:
"""6.3 Fetch NFC Card Enrollment Status"""
session_id = NfcCardEnrollmentSessionId("e8a97c52-6676-4c48-8589-bd518afc4094")
responses.get(
f"https://{self.host}/api/v1/developer/credentials/nfc_cards/sessions/{session_id}",
match=[
matchers.header_matcher(self.common_headers),
],
json={
"code": "SUCCESS",
"msg": "success",
"data": {
# NOTE: altered from API docs to match testing on real instance (was "card_id")
"id": "014A3151",
"token": "821f90b262e90c5c0fbcddf3d6d2f3b94cc015d6e8104ab4fb96e4c8b8e90cb7",
},
},
)
resp = self.client.fetch_enroll_card_status(
session_id=session_id
).success_or_raise()
assert resp.data.id == "014A3151"
assert (
resp.data.token
== "821f90b262e90c5c0fbcddf3d6d2f3b94cc015d6e8104ab4fb96e4c8b8e90cb7"
)
@responses.activate
def test_remove_enrollment_session(self) -> None:
"""6.4 Remove a Session Created for NFC Card Enrollment"""
session_id = NfcCardEnrollmentSessionId("e8a97c52-6676-4c48-8589-bd518afc4094")
responses.delete(
f"https://{self.host}/api/v1/developer/credentials/nfc_cards/sessions/{session_id}",
match=[
matchers.header_matcher(self.common_headers),
],
json={"code": "SUCCESS", "msg": "success", "data": "success"},
)
resp = self.client.remove_enrollment_session(
session_id=session_id
).success_or_raise()
@responses.activate
def test_fetch_nfc_card(self) -> None:
"""6.7 Fetch NFC Card"""
nfc_card_token = NfcCardToken(
"f77d69b08eaf5eb5d647ac1a0a73580f1b27494b345f40f54fa022a8741fa15c"
)
responses.get(
f"https://{self.host}/api/v1/developer/credentials/nfc_cards/tokens/{nfc_card_token}",
match=[matchers.header_matcher(self.common_headers)],
json={
"code": "SUCCESS",
"data": {
"alias": "",
"card_type": "ua_card",
"display_id": "100005",
"note": "100005",
"status": "assigned",
"token": "f77d69b08eaf5eb5d647ac1a0a73580f1b27494b345f40f54fa022a8741fa15c",
"user": {
"avatar": "",
"first_name": "H",
"id": "e0051e08-c4d5-43db-87c8-a9b19cb66513",
"last_name": "L",
"name": "H L",
},
"user_id": "e0051e08-c4d5-43db-87c8-a9b19cb66513",
"user_type": "USER",
},
"msg": "success",
},
)
resp = self.client.fetch_nfc_card(nfc_card_token).success_or_raise()
@responses.activate
def test_fetch_all_nfc_cards(self) -> None:
"""6.8 Fetch All NFC Cards"""
responses.get(
f"https://{self.host}/api/v1/developer/credentials/nfc_cards/tokens",
match=[
matchers.header_matcher(self.common_headers),
matchers.query_param_matcher({"page_num": 1, "page_size": 25}),
],
json={
"code": "SUCCESS",
"data": [
{
"alias": "",
"card_type": "ua_card",
"display_id": "100004",
"note": "100004",
"status": "assigned",
"token": "9e24cdfafebf63e58fd02c5f67732b478948e5793d31124239597d9a86b30dc4",
"user": {
"avatar": "",
"first_name": "H",
"id": "e0051e08-c4d5-43db-87c8-a9b19cb66513",
"last_name": "L",
"name": "H L",
},
"user_id": "e0051e08-c4d5-43db-87c8-a9b19cb66513",
"user_type": "USER",
},
{
"alias": "F77D69B03",
"card_type": "ua_card",
"display_id": "100005",
"note": "100005",
"status": "assigned",
"token": "f77d69b08eaf5eb5d647ac1a0a73580f1b27494b345f40f54fa022a8741fa15c",
"user": {
"avatar": "",
"first_name": "H2",
"id": "34dc90a7-409f-4bf8-a5a8-1c59535a21b9",
"last_name": "L",
"name": "H2 L",
},
"user_id": "34dc90a7-409f-4bf8-a5a8-1c59535a21b9",
"user_type": "VISITOR",
},
],
"msg": "succ",
"pagination": {"page_num": 1, "page_size": 2, "total": 2},
},
)
resp = self.client.fetch_all_nfc_cards(
page_num=1, page_size=25
).success_or_raise()
@responses.activate
def test_delete_nfc_card(self) -> None:
"""6.9 Delete NFC Card"""
nfc_card_token = NfcCardToken(
"f77d69b08eaf5eb5d647ac1a0a73580f1b27494b345f40f54fa022a8741fa15c"
)
responses.delete(
f"https://{self.host}/api/v1/developer/credentials/nfc_cards/tokens/{nfc_card_token}",
match=[matchers.header_matcher(self.common_headers)],
json={"code": "SUCCESS", "data": "success", "msg": "success"},
)
resp = self.client.delete_nfc_card(nfc_card_token).success_or_raise()
assert resp.data == "success"

42
tests/test_device.py Normal file
View File

@ -0,0 +1,42 @@
import responses
from responses import matchers
from .base import UnifiAccessTests
class CredentialTests(UnifiAccessTests):
@responses.activate
def test_fetch_devices(self) -> None:
"""8.1 Fetch Devices"""
responses.get(
f"https://{self.host}/api/v1/developer/devices",
match=[matchers.header_matcher(self.common_headers)],
json={
"code": "SUCCESS",
# XXX: verify correct response, as per note
# NOTE: api docs had this as a list of lists of objects
"data": [
{
"full_name": "UNVR - Main Floor - Door 3855 - UA-HUB-3855",
"id": "7483c2773855",
"name": "UA-HUB-3855",
"type": "UAH",
},
{
"full_name": "UNVR - Main Floor - Door 3855 - out - UA-LITE-8CED",
"id": "f492bfd28ced",
"name": "UA-LITE-8CED",
"type": "UDA-LITE",
},
{
"full_name": "UNVR - Main Floor - Door 3855 - in - UA-G2-PRO-BB7A",
"id": "0418d6a2bb7a",
"name": "UA-G2-PRO-BB7A",
"type": "UA-G2-PRO",
},
],
"msg": "success",
},
)
resp = self.client.fetch_devices().success_or_raise()
assert resp.data[0].name == "UA-HUB-3855"

434
tests/test_space.py Normal file
View File

@ -0,0 +1,434 @@
import responses
from responses import matchers
from unifi_access.schemas import DoorGroupId, DoorId, DoorLockingRuleType
from .base import UnifiAccessTests
class SpaceTests(UnifiAccessTests):
@responses.activate
def test_fetch_door_group_topology(self) -> None:
"""7.1 Fetch Door Group Topology"""
responses.get(
f"https://{self.host}/api/v1/developer/door_groups/topology",
match=[matchers.header_matcher(self.common_headers)],
json={
"code": "SUCCESS",
"data": [
{
"id": "d5573467-d6b3-4e8f-8e48-8a322b91664a",
"name": "All Doors",
"resource_topologies": [
{
"id": "9bee6e0e-108d-4c52-9107-76f2c7dea4f1",
"name": "Main Floor",
"resources": [
{
"id": "6ff875d2-af87-470b-9cb5-774c6596afc8",
"name": "Door 3855",
"type": "door",
"is_bind_hub": True,
}
],
"type": "floor",
}
],
"type": "building",
},
{
"id": "5c496423-6d25-4e4f-8cdf-95ad5135188a",
"name": "customized group",
"resource_topologies": [
{
"id": "9bee6e0e-108d-4c52-9107-76f2c7dea4f1",
"name": "Main Floor",
"resources": [
{
"id": "6ff875d2-af87-470b-9cb5-774c6596afc8",
"name": "Door 3855",
"type": "door",
"is_bind_hub": True,
}
],
"type": "floor",
}
],
"type": "access",
},
],
"msg": "success",
},
)
resp = self.client.fetch_door_group_topology().success_or_raise()
@responses.activate
def test_create_door_group(self) -> None:
"""7.2 Create Door Group"""
responses.post(
f"https://{self.host}/api/v1/developer/door_groups",
match=[
matchers.header_matcher(self.common_headers),
matchers.json_params_matcher(
{
"group_name": "Test",
"resources": ["6ff875d2-af87-470b-9cb5-774c6596afc8"],
}
),
],
json={
"code": "SUCCESS",
"data": {
"id": "0140fa3d-8973-4305-a0ce-5306ae277878",
"name": "Customized Door Group",
"resources": [
{"id": "6ff875d2-af87-470b-9cb5-774c6596afc8", "type": "door"}
],
"type": "access",
},
"msg": "success",
},
)
# XXX: TODO: test if this supports both doors and door groups
resp = self.client.create_door_group(
group_name="Test",
resources=[DoorId("6ff875d2-af87-470b-9cb5-774c6596afc8")],
).success_or_raise()
@responses.activate
def test_fetch_door_group_building(self) -> None:
"""7.3 Fetch Door Group"""
door_group_id = DoorGroupId("d5573467-d6b3-4e8f-8e48-8a322b91664a")
responses.get(
f"https://{self.host}/api/v1/developer/door_groups/{door_group_id}",
match=[matchers.header_matcher(self.common_headers)],
# Group type is building
json={
"code": "SUCCESS",
"data": {
"id": "d5573467-d6b3-4e8f-8e48-8a322b91664a",
"name": "All Doors",
"resources": [
{
"id": "6ff875d2-af87-470b-9cb5-774c6596afc8",
"name": "Door 3855",
"type": "door",
},
{
"id": "7cc1823f-9cdb-447b-b01b-4cb2abc661c0",
"name": "A2 Door",
"type": "door",
},
{
"id": "ececa68e-239f-4b82-adc4-0c9ce70c60ff",
"name": "A3",
"type": "door",
},
],
"type": "building",
},
"msg": "success",
},
)
resp = self.client.fetch_door_group(
door_group_id=door_group_id
).success_or_raise()
@responses.activate
def test_fetch_door_group_customized_groups(self) -> None:
"""7.3 Fetch Door Group"""
door_group_id = DoorGroupId("1be0c995-0347-4cb2-93b3-66a9624af568")
responses.get(
f"https://{self.host}/api/v1/developer/door_groups/{door_group_id}",
match=[matchers.header_matcher(self.common_headers)],
# Customized groups
json={
"code": "SUCCESS",
"data": {
"id": "1be0c995-0347-4cb2-93b3-66a9624af568",
"name": "Door Group 01",
"resources": [
{
"id": "6ff875d2-af87-470b-9cb5-774c6596afc8",
"type": "door",
"name": "Door 385",
}
],
"type": "access",
},
"msg": "success",
},
)
resp = self.client.fetch_door_group(
door_group_id=door_group_id
).success_or_raise()
@responses.activate
def test_update_door_group(self) -> None:
"""7.4 Update Door Group"""
door_group_id = DoorGroupId("0140fa3d-8973-4305-a0ce-5306ae277878")
responses.put(
f"https://{self.host}/api/v1/developer/door_groups/{door_group_id}",
match=[
matchers.header_matcher(self.common_headers),
matchers.json_params_matcher(
{
"resources": [
"6ff875d2-af87-470b-9cb5-774c6596afc8",
"5a2c3d4e-1f6b-4c8d-9e0f-2a3b4c5d6e7f",
"2p3q4r5s-6t7u-8v9w-x0y1-z2a3b4c5d6e",
]
}
),
],
json={
"code": "SUCCESS",
"data": {
"id": "0140fa3d-8973-4305-a0ce-5306ae277878",
"name": "test",
"resources": [
{"id": "6ff875d2-af87-470b-9cb5-774c6596afc8", "type": "door"}
],
"type": "access",
},
"msg": "success",
},
)
# XXX: TODO: verify that this accepts both Doors and Door Groups
resp = self.client.update_door_group(
door_group_id=door_group_id,
resources=[
DoorId("6ff875d2-af87-470b-9cb5-774c6596afc8"),
DoorId("5a2c3d4e-1f6b-4c8d-9e0f-2a3b4c5d6e7f"),
DoorGroupId("2p3q4r5s-6t7u-8v9w-x0y1-z2a3b4c5d6e"),
],
).success_or_raise()
@responses.activate
def test_update_door_group_delete_resources(self) -> None:
"""7.4 Update Door Group"""
door_group_id = DoorGroupId("0140fa3d-8973-4305-a0ce-5306ae277878")
responses.put(
f"https://{self.host}/api/v1/developer/door_groups/{door_group_id}",
match=[
matchers.header_matcher(self.common_headers),
matchers.json_params_matcher({"resources": []}),
],
# NOTE: no example provided in API docs
json={
"code": "SUCCESS",
"data": {
"id": "0140fa3d-8973-4305-a0ce-5306ae277878",
"name": "test",
"resources": [],
"type": "access",
},
"msg": "success",
},
)
resp = self.client.update_door_group(
door_group_id=door_group_id,
resources=[],
).success_or_raise()
@responses.activate
def test_fetch_all_door_groups(self) -> None:
"""7.5 Fetch All Door Groups"""
responses.get(
f"https://{self.host}/api/v1/developer/door_groups",
match=[matchers.header_matcher(self.common_headers)],
json={
"code": "SUCCESS",
"data": [
{
"id": "5c496423-6d25-4e4f-8cdf-95ad5135188a",
"name": "Test",
"resources": [
{
"id": "6ff875d2-af87-470b-9cb5-774c6596afc8",
"type": "door",
}
],
"type": "access",
},
{
"id": "1907cc46-0a73-4077-94c1-95b625bdb0f8",
"name": "Test2",
"resources": [
{
"id": "6ff875d2-af87-470b-9cb5-774c6596afc8",
"type": "door",
}
],
"type": "access",
},
],
"msg": "success",
},
)
resp = self.client.fetch_all_door_groups().success_or_raise()
@responses.activate
def test_delete_door_groups(self) -> None:
"""7.6 Delete Door Group"""
door_group_id = DoorGroupId("0140fa3d-8973-4305-a0ce-5306ae277878")
responses.get(
f"https://{self.host}/api/v1/developer/door_groups/{door_group_id}",
match=[matchers.header_matcher(self.common_headers)],
json={"code": "SUCCESS", "data": "success", "msg": "success"},
)
resp = self.client.delete_door_group(
door_group_id=door_group_id
).success_or_raise()
@responses.activate
def test_fetch_door(self) -> None:
"""7.7 Fetch Door"""
door_id = DoorId("0ed545f8-2fcd-4839-9021-b39e707f6aa9")
responses.get(
f"https://{self.host}/api/v1/developer/doors/{door_id}",
match=[matchers.header_matcher(self.common_headers)],
json={
"code": "SUCCESS",
"data": {
"door_lock_relay_status": "lock",
"door_position_status": "",
"floor_id": "3275af8d-3fa7-4902-a11b-011e41c8464a",
"full_name": "UNVR - 1F - Main Door",
"id": "0ed545f8-2fcd-4839-9021-b39e707f6aa9",
"is_bind_hub": True,
"name": "Main Door",
"type": "door",
},
"msg": "success",
},
)
resp = self.client.fetch_door(door_id=door_id).success_or_raise()
@responses.activate
def test_fetch_all_doors(self) -> None:
"""7.8 Fetch All Doors"""
responses.get(
f"https://{self.host}/api/v1/developer/doors",
match=[matchers.header_matcher(self.common_headers)],
json={
"code": "SUCCESS",
"data": [
{
"door_lock_relay_status": "unlock",
"door_position_status": "open",
"floor_id": "23c5db06-b59b-494d-94f1-23e88fbe4909",
"full_name": "UNVR - 2F - A2 Door",
"id": "0ed545f8-2fcd-4839-9021-b39e707f6aa9",
"is_bind_hub": True,
"name": "A2 Door",
"type": "door",
},
{
"door_lock_relay_status": "lock",
"door_position_status": "close",
"floor_id": "7c62b4b3-692f-44ea-8eb8-e212833b4e0f",
"full_name": "UNVR - 1F - Door 3855",
"id": "5785e97b-6123-4596-ba49-b6e51164db9b",
"is_bind_hub": True,
"name": "Door 3855",
"type": "door",
},
],
"msg": "success",
},
)
resp = self.client.fetch_all_doors().success_or_raise()
@responses.activate
def test_unlock_door(self) -> None:
"""7.9 Remote Door Unlocking"""
door_id = DoorId("5785e97b-6123-4596-ba49-b6e51164db9b")
responses.put(
f"https://{self.host}/api/v1/developer/doors/{door_id}/unlock",
match=[matchers.header_matcher(self.common_headers)],
json={"code": "SUCCESS", "data": "success", "msg": "success"},
)
resp = self.client.unlock_door(door_id=door_id).success_or_raise()
@responses.activate
def test_set_temporary_door_locking_rule(self) -> None:
"""7.10 Set Temporary Door Locking Rule"""
door_id = DoorId("e4978b83-203d-4015-97df-b86efc91cb0c")
responses.put(
f"https://{self.host}/api/v1/developer/doors/{door_id}/lock_rule",
match=[
matchers.header_matcher(self.common_headers),
matchers.json_params_matcher({"type": "custom", "interval": 10}),
],
json={"code": "SUCCESS", "data": "success", "msg": "success"},
)
# Customized 10-minute unlocked
resp = self.client.set_temporary_door_locking_rule(
door_id=door_id, type_=DoorLockingRuleType.CUSTOM, interval=10
).success_or_raise()
# XXX: TODO: test the other examples?
@responses.activate
def test_fetch_door_locking_rule(self) -> None:
"""7.11 Fetch Door Locking Rule"""
door_id = DoorId("e4978b83-203d-4015-97df-b86efc91cb0c")
responses.get(
f"https://{self.host}/api/v1/developer/doors/{door_id}/lock_rule",
match=[matchers.header_matcher(self.common_headers)],
# Keep it locked
json={
"code": "SUCCESS",
"data": {"ended_time": 3602128309, "type": "keep_lock"},
"msg": "success",
},
)
resp = self.client.fetch_door_locking_rule(door_id=door_id).success_or_raise()
# XXX: TODO: test the other examples?
@responses.activate
def test_set_door_emergency_status(self) -> None:
"""7.12 Set Door Emergency Status"""
responses.put(
f"https://{self.host}/api/v1/developer/doors/settings/emergency",
match=[
matchers.header_matcher(self.common_headers),
matchers.json_params_matcher({"lockdown": True, "evacuation": False}),
],
json={"code": "SUCCESS", "data": "success", "msg": "success"},
)
# Keep it locked
resp = self.client.set_door_emergency_status(
lockdown=True, evacuation=False
).success_or_raise()
# XXX: TODO: test the other examples?
@responses.activate
def test_fetch_door_emergency_status(self) -> None:
"""7.13 Fetch Door Emergency Status"""
responses.get(
f"https://{self.host}/api/v1/developer/doors/settings/emergency",
match=[matchers.header_matcher(self.common_headers)],
json={
"code": "SUCCESS",
"data": {"evacuation": True, "lockdown": False},
"msg": "success",
},
)
resp = self.client.fetch_door_emergency_status().success_or_raise()

118
tests/test_system_log.py Normal file
View File

@ -0,0 +1,118 @@
import datetime
import responses
from responses import matchers
from unifi_access.schemas import SystemLogTopic, UserId
from .base import UnifiAccessTests
class SystemLogTests(UnifiAccessTests):
@responses.activate
def test_fetch_system_logs(self) -> None:
"""9.2 Fetch System Logs"""
responses.post(
f"https://{self.host}/api/v1/developer/system/logs",
match=[
matchers.header_matcher(self.common_headers),
matchers.query_param_matcher({"page_size": 25, "page_num": 1}),
matchers.json_params_matcher(
{
"topic": "door_openings",
"since": 1690770546,
"until": 1690771546,
"actor_id": "3e1f196e-c97b-4748-aecb-eab5e9c251b2",
}
),
],
json={
"code": "SUCCESS",
"data": {
"hits": [
{
"@timestamp": "2023-07-11T12:11:27Z",
"_id": "",
"_source": {
"actor": {
"alternate_id": "",
"alternate_name": "",
"display_name": "N/A",
"id": "",
"type": "user",
},
"authentication": {
"credential_provider": "NFC",
"issuer": "6FC02554",
},
"event": {
"display_message": "Access Denied / Unknown (NFC)",
"published": 1689077487000,
"reason": "",
"result": "BLOCKED",
"type": "access.door.unlock",
# NOTE: missing from example, but present in live api
"log_key": "",
},
"target": [
{
"alternate_id": "",
"alternate_name": "",
"display_name": "UA-HUB-3855",
"id": "7483c2773855",
"type": "UAH",
}
],
},
"tag": "access",
}
]
},
# NOTE: example missing `msg` and `pagination`, had `page` and `total` instead
"msg": "succ",
"pagination": {"page_num": 1, "page_size": 10000, "total": 1},
},
)
resp = self.client.fetch_system_logs(
page_num=1,
page_size=25,
topic=SystemLogTopic.DOOR_OPENINGS,
since=datetime.datetime.fromtimestamp(1690770546, tz=datetime.UTC),
until=datetime.datetime.fromtimestamp(1690771546, tz=datetime.UTC),
actor_id=UserId("3e1f196e-c97b-4748-aecb-eab5e9c251b2"),
).success_or_raise()
@responses.activate
def test_export_system_logs(self) -> None:
"""9.3 Export System Logs"""
responses.post(
f"https://{self.host}/api/v1/developer/system/logs/export",
match=[
matchers.header_matcher(self.common_headers),
matchers.json_params_matcher(
{
"topic": "door_openings",
"since": 1690770546,
"until": 1690771546,
"timezone": "America/New_York",
"actor_id": "3e1f196e-c97b-4748-aecb-eab5e9c251b2",
}
),
],
# NOTE: example not provided by API
# TODO: what does the API do on errors?
body=b"time,actor.alternate_id,actor.alternate_name,actor.display_name,actor.id,actor.type,authentication.credential_provider,authentication.issuer,event.display_message,event.published,event.reason,event.result,event.type,target1.alternate_id,target1.alternate_name,target1.display_name,target1.id,target1.type,target2.alternate_id,target2.alternate_name,target2.display_name,target2.id,target2.type,target3.alternate_id,target3.alternate_name,target3.display_name,target3.id,target3.type,target4.alternate_id,target4.alternate_name,target4.display_name,target4.id,target4.type,target5.alternate_id,target5.alternate_name,target5.display_name,target5.id,target5.type\n",
)
resp = self.client.export_system_logs(
topic=SystemLogTopic.DOOR_OPENINGS,
since=datetime.datetime.fromtimestamp(1690770546, tz=datetime.UTC),
until=datetime.datetime.fromtimestamp(1690771546, tz=datetime.UTC),
timezone="America/New_York",
actor_id=UserId("3e1f196e-c97b-4748-aecb-eab5e9c251b2"),
)
# TODO:
# - 9.4 Fetch Resources in System Logs
# - 9.5 Fetch Static Resources in System Logs

View File

@ -0,0 +1,249 @@
import responses
from responses import matchers
from unifi_access.schemas import (
IdentityInvitationUser,
IdentityResourceType,
UserGroupId,
UserId,
)
from unifi_access.schemas._base import IdentityResourceId
from .base import UnifiAccessTests
class UnifiIdentityTests(UnifiAccessTests):
@responses.activate
def test_send_unifi_identity_invitations(self) -> None:
"""10.1 Send UniFi Identity Invitations"""
responses.post(
f"https://{self.host}/api/v1/developer/users/identity/invitations",
match=[
matchers.header_matcher(self.common_headers),
matchers.json_params_matcher(
[
{
"user_id": "e0051e08-c4d5-43db-87c8-a9b19cb66513",
"email": "example@*.com",
}
]
),
],
json={"code": "SUCCESS", "data": [], "msg": "success"},
)
resp = self.client.send_unifi_identity_invitations(
[
IdentityInvitationUser(
user_id=UserId("e0051e08-c4d5-43db-87c8-a9b19cb66513"),
email="example@*.com",
)
]
).success_or_raise()
@responses.activate
def test_send_unifi_identity_invitations_email_failure(self) -> None:
"""10.1 Send UniFi Identity Invitations"""
responses.post(
f"https://{self.host}/api/v1/developer/users/identity/invitations",
match=[
matchers.header_matcher(self.common_headers),
matchers.json_params_matcher(
[
{
"user_id": "e0051e08-c4d5-43db-87c8-a9b19cb66513",
"email": "example@*.com",
}
]
),
],
json={
"code": "SUCCESS",
"data": [
{
"error_code": "",
"error_msg": "invalid email",
"user_email": "example@*.com",
"user_id": "e0051e08-c4d5-43db-87c8-a9b19cb66513",
}
],
"msg": "success",
},
)
# XXX: not sure what behavior here should be...
resp = self.client.send_unifi_identity_invitations(
[
IdentityInvitationUser(
user_id=UserId("e0051e08-c4d5-43db-87c8-a9b19cb66513"),
email="example@*.com",
)
]
).success_or_raise()
@responses.activate
def test_fetch_available_resources(self) -> None:
"""10.2 Fetch Available Resources"""
responses.get(
f"https://{self.host}/api/v1/developer/users/identity/assignments",
match=[
matchers.header_matcher(self.common_headers),
matchers.query_param_matcher({"resource_type": "ev_station,wifi,vpn"}),
],
json={
"code": "SUCCESS",
"data": {
"ev_station": [],
"vpn": [
{
"deleted": False,
"id": "65dff9a9c188cb71cfac8e9d",
"metadata": None,
"name": "UDM Pro",
"short_name": "",
}
],
"wifi": [
{
"deleted": False,
"id": "65dff9a8c188cb71cfac8e9a",
"metadata": None,
"name": "UniFi Identity",
"short_name": "",
}
],
},
"msg": "success",
},
)
resp = self.client.fetch_available_resources(
resource_type=[
IdentityResourceType.EV_STATION,
IdentityResourceType.WIFI,
IdentityResourceType.VPN,
]
).success_or_raise()
@responses.activate
def test_assign_resources_to_user(self) -> None:
"""10.3 Assign Resources to Users"""
user_id = UserId("b602879b-b857-400b-970b-336d4cb881ad")
responses.post(
f"https://{self.host}/api/v1/developer/users/{user_id}/identity/assignments",
match=[
matchers.header_matcher(self.common_headers),
matchers.json_params_matcher(
{
"resource_type": "wifi",
"resource_ids": ["65dff9a8c188cb71cfac8e9a"],
}
),
],
json={"code": "SUCCESS", "data": None, "msg": "success"},
)
resp = self.client.assign_resources_to_user(
user_id=user_id,
resource_type=IdentityResourceType.WIFI,
resource_ids=[IdentityResourceId("65dff9a8c188cb71cfac8e9a")],
).success_or_raise()
@responses.activate
def test_fetch_resources_assigned_to_user(self) -> None:
"""10.4 Fetch Resources Assigned to Users"""
user_id = UserId("b602879b-b857-400b-970b-336d4cb881ad")
responses.get(
f"https://{self.host}/api/v1/developer/users/{user_id}/identity/assignments",
match=[matchers.header_matcher(self.common_headers)],
json={
"code": "SUCCESS",
"data": {
"ev_station": [],
"vpn": [
{
"deleted": False,
"id": "65dff9a9c188cb71cfac8e9d",
"metadata": {"has_ip": True},
"name": "UDM Pro",
"short_name": "",
}
],
"wifi": [
{
"deleted": False,
"id": "65dff9a8c188cb71cfac8e9a",
"metadata": None,
"name": "UniFi Identity",
"short_name": "",
}
],
},
"msg": "success",
},
)
resp = self.client.fetch_resources_assigned_to_user(
user_id=user_id
).success_or_raise()
@responses.activate
def test_assign_resources_to_user_group(self) -> None:
"""10.5 Assign Resources to User Groups"""
user_group_id = UserGroupId("7476c839-8e10-472e-894f-c5b8254c35b5")
responses.post(
f"https://{self.host}/api/v1/developer/user_groups/{user_group_id}/identity/assignments",
match=[
matchers.header_matcher(self.common_headers),
matchers.json_params_matcher(
{
"resource_type": "wifi",
"resource_ids": ["65dff9a8c188cb71cfac8e9a"],
}
),
],
json={"code": "SUCCESS", "data": None, "msg": "success"},
)
resp = self.client.assign_resources_to_user_group(
user_group_id=user_group_id,
resource_type=IdentityResourceType.WIFI,
resource_ids=[IdentityResourceId("65dff9a8c188cb71cfac8e9a")],
).success_or_raise()
@responses.activate
def test_fetch_resources_assigned_to_user_group(self) -> None:
"""10.6 Fetch Resources Assigned to User Groups"""
user_group_id = UserGroupId("b602879b-b857-400b-970b-336d4cb881ad")
responses.get(
f"https://{self.host}/api/v1/developer/user_groups/{user_group_id}/identity/assignments",
match=[matchers.header_matcher(self.common_headers)],
json={
"code": "SUCCESS",
"data": {
"ev_station": [],
"vpn": [
{
"deleted": False,
"id": "65dff9a9c188cb71cfac8e9d",
"metadata": {"has_ip": True},
"name": "UDM Pro",
"short_name": "",
}
],
"wifi": [
{
"deleted": False,
"id": "65dff9a8c188cb71cfac8e9a",
"metadata": None,
"name": "UniFi Identity",
"short_name": "",
}
],
},
"msg": "success",
},
)
resp = self.client.fetch_resources_assigned_to_user_group(
user_group_id=user_group_id
).success_or_raise()

720
tests/test_user.py Normal file
View File

@ -0,0 +1,720 @@
import datetime
import responses
from responses import matchers
from unifi_access.schemas import (
AccessPolicyId,
NfcCardToken,
UserGroupId,
UserId,
UserStatus,
)
from .base import UnifiAccessTests
class UserTests(UnifiAccessTests):
@responses.activate
def test_register_user(self) -> None:
"""3.2 User Registration"""
responses.post(
f"https://{self.host}/api/v1/developer/users",
match=[
matchers.header_matcher(self.common_headers),
matchers.json_params_matcher(
{
"first_name": "First Name",
"last_name": "Last Name",
"employee_number": "100000",
"onboard_time": 1689150139,
"user_email": "example@*.com",
}
),
],
json={
"code": "SUCCESS",
"msg": "success",
"data": {
"id": "37f2b996-c2c5-487b-aa22-8b453ff14a4b",
"first_name": "First Name",
"last_name": "Last Name",
"employee_number": "100000",
"onboard_time": 1689150139,
"user_email": "example@*.com",
# NOTE: following fields missing from example, but present in real responses
"alias": "",
"avatar_relative_path": "",
"email": "",
"email_status": "UNVERIFIED",
"full_name": "First Name Last Name",
"phone": "",
"status": "ACTIVE",
"username": "",
},
},
)
resp = self.client.register_user(
first_name="First Name",
last_name="Last Name",
employee_number="100000",
onboard_time=datetime.datetime.fromtimestamp(1689150139, tz=datetime.UTC),
user_email="example@*.com",
).success_or_raise()
assert resp.data.id == "37f2b996-c2c5-487b-aa22-8b453ff14a4b"
@responses.activate
def test_update_user(self) -> None:
"""3.3 Update User"""
user_id = UserId("37f2b996-c2c5-487b-aa22-8b453ff14a4b")
responses.put(
f"https://{self.host}/api/v1/developer/users/{user_id}",
match=[
matchers.header_matcher(self.common_headers),
matchers.json_params_matcher(
{
"first_name": "H",
"last_name": "L",
"employee_number": "",
"user_email": "example@*.com",
# XXX: TODO: this is not listed as a valid param in their docs...
# "pin_code": "",
"onboard_time": 1689150139,
"status": "ACTIVE",
}
),
],
json={"code": "SUCCESS", "msg": "success", "data": None},
)
resp = self.client.update_user(
user_id=user_id,
first_name="H",
last_name="L",
employee_number="",
onboard_time=datetime.datetime.fromtimestamp(1689150139, tz=datetime.UTC),
user_email="example@*.com",
status=UserStatus.ACTIVE,
).success_or_raise()
assert resp.data is None
@responses.activate
def test_fetch_user(self) -> None:
"""3.4 Fetch User"""
user_id = UserId("37f2b996-c2c5-487b-aa22-8b453ff14a4b")
responses.get(
f"https://{self.host}/api/v1/developer/users/{user_id}",
match=[
matchers.header_matcher(self.common_headers),
matchers.query_param_matcher({"expand[]": "access_policy"}),
],
json={
"code": "SUCCESS",
"data": {
"access_policies": [
{
"id": "edbc80df-3698-49fd-8b53-f1867f104947",
"name": "test",
"resources": [
{
"id": "d5573467-d6b3-4e8f-8e48-8a322b91664a",
"type": "door_group",
},
{
"id": "5c496423-6d25-4e4f-8cdf-95ad5135188a",
"type": "door_group",
},
{
"id": "6ff875d2-af87-470b-9cb5-774c6596afc8",
"type": "door",
},
],
"schedule_id": "73facd6c-839e-4521-a4f4-c07e1d44e748",
}
],
"access_policy_ids": ["edbc80df-3698-49fd-8b53-f1867f104947"],
"employee_number": "",
"first_name": "***",
"id": "37f2b996-c2c5-487b-aa22-8b453ff14a4b",
"last_name": "L",
"user_email": "example@*.com",
"nfc_cards": [
{
"id": "100001",
"token": "d27822fc682b478dc637c6db01813e465174df6e54ca515d8427db623cfda1d0",
"type": "ua_card",
}
],
"onboard_time": 1689047588,
"pin_code": {
"token": "5f742ee4424e5a7dd265de3461009b9ebafa1fb9d6b15018842055cc0466ac56"
},
"status": "ACTIVE",
# NOTE: following fields missing from example, but present in real responses
"alias": "",
"avatar_relative_path": "",
"email": "",
"email_status": "UNVERIFIED",
"full_name": "*** L",
"phone": "",
"username": "",
},
"msg": "success",
},
)
resp = self.client.fetch_user(
user_id=user_id, expand_access_policies=True
).success_or_raise()
# TODO: verify correctness of data?
@responses.activate
def test_fetch_all_users(self) -> None:
"""3.5 Fetch All Users"""
responses.get(
f"https://{self.host}/api/v1/developer/users",
match=[
matchers.header_matcher(self.common_headers),
matchers.query_param_matcher(
{"expand[]": "access_policy", "page_num": 1, "page_size": 25}
),
],
json={
"code": "SUCCESS",
"data": [
{
"access_policies": [
{
"id": "73f15cab-c725-4a76-a419-a4026d131e96",
"name": "Default Admin Policy",
"resources": [
{
"id": "d5573467-d6b3-4e8f-8e48-8a322b91664a",
"type": "door_group",
},
{
"id": "5c496423-6d25-4e4f-8cdf-95ad5135188a",
"type": "door_group",
},
],
"schedule_id": "73facd6c-839e-4521-a4f4-c07e1d44e748",
}
],
"access_policy_ids": ["73f15cab-c725-4a76-a419-a4026d131e96"],
"employee_number": "",
"first_name": "UniFi",
"id": "83569f9b-0096-48ab-b2e4-5c9a598568a8",
"last_name": "User",
"user_email": "",
"nfc_cards": [],
"onboard_time": 0,
"pin_code": None,
"status": "ACTIVE",
# NOTE: following fields missing from example, but present in real responses
"alias": "",
"avatar_relative_path": "",
"email": "",
"email_status": "UNVERIFIED",
"full_name": "UniFi User",
"phone": "",
"username": "",
},
{
"access_policies": [
{
"id": "c1682fb8-ef6e-4fe2-aa8a-b6f29df753ff",
"name": "policy_1690272668035",
"resources": [
{
"id": "6ff875d2-af87-470b-9cb5-774c6596afc8",
"type": "door",
}
],
"schedule_id": "0616ef06-b807-4372-9ae0-7a87e12e4019",
}
],
"access_policy_ids": ["c1682fb8-ef6e-4fe2-aa8a-b6f29df753ff"],
"employee_number": "",
"first_name": "Ttttt",
"id": "3a3ba57a-796e-46e0-b8f3-478bb70a114d",
"last_name": "Tttt",
"nfc_cards": [],
"onboard_time": 1689048000,
"pin_code": None,
"status": "ACTIVE",
# NOTE: following fields missing from example, but present in real responses
"alias": "",
"avatar_relative_path": "",
"user_email": "",
"email": "",
"email_status": "UNVERIFIED",
"full_name": "Ttttt Tttt",
"phone": "",
"username": "",
},
],
"msg": "success",
"pagination": {"page_num": 1, "page_size": 97, "total": 97},
},
)
resp = self.client.fetch_all_users(
expand_access_policies=True, page_num=1, page_size=25
).success_or_raise()
assert resp.pagination
# TODO: verify correctness of data?
@responses.activate
def test_assign_access_policy_to_user(self) -> None:
"""3.6 Assign Access Policy to User"""
user_id = UserId("37f2b996-c2c5-487b-aa22-8b453ff14a4b")
responses.put(
f"https://{self.host}/api/v1/developer/users/{user_id}/access_policies",
match=[
matchers.header_matcher(self.common_headers),
matchers.json_params_matcher(
{
"access_policy_ids": [
"03895c7f-9f53-4334-812b-5db9c122c109",
"3b6bcb0c-7498-44cf-8615-00a96d824cbe",
]
}
),
],
json={"code": "SUCCESS", "msg": "success"},
)
resp = self.client.assign_access_policy_to_user(
user_id=user_id,
access_policy_ids=[
AccessPolicyId("03895c7f-9f53-4334-812b-5db9c122c109"),
AccessPolicyId("3b6bcb0c-7498-44cf-8615-00a96d824cbe"),
],
).success_or_raise()
assert resp.data is None
@responses.activate
def test_assign_nfc_card_to_user(self) -> None:
"""3.7 Assign NFC Card to User"""
user_id = UserId("17d2f099-99df-429b-becb-1399a6937e5a")
responses.put(
f"https://{self.host}/api/v1/developer/users/{user_id}/nfc_cards",
match=[
matchers.header_matcher(self.common_headers),
matchers.json_params_matcher(
{
"token": "d27822fc682b478dc637c6db01813e465174df6e54ca515d8427db623cfda1d0",
"force_add": True,
}
),
],
json={"code": "SUCCESS", "msg": "success"},
)
resp = self.client.assign_nfc_card_to_user(
user_id=user_id,
token=NfcCardToken(
"d27822fc682b478dc637c6db01813e465174df6e54ca515d8427db623cfda1d0"
),
force_add=True,
).success_or_raise()
assert resp.data is None
@responses.activate
def test_unassign_nfc_card_from_user(self) -> None:
"""3.8 Unassign NFC Card from User"""
user_id = UserId("17d2f099-99df-429b-becb-1399a6937e5a")
responses.put(
f"https://{self.host}/api/v1/developer/users/{user_id}/nfc_cards/delete",
match=[
matchers.header_matcher(self.common_headers),
matchers.json_params_matcher(
{
"token": "d27822fc682b478dc637c6db01813e465174df6e54ca515d8427db623cfda1d0",
}
),
],
json={"code": "SUCCESS", "msg": "success"},
)
resp = self.client.unassign_nfc_card_from_user(
user_id=user_id,
token=NfcCardToken(
"d27822fc682b478dc637c6db01813e465174df6e54ca515d8427db623cfda1d0"
),
).success_or_raise()
assert resp.data is None
@responses.activate
def test_assign_pin_code_to_user(self) -> None:
"""3.9 Assign PIN Code to User"""
user_id = UserId("17d2f099-99df-429b-becb-1399a6937e5a")
responses.put(
f"https://{self.host}/api/v1/developer/users/{user_id}/pin_codes",
match=[
matchers.header_matcher(self.common_headers),
matchers.json_params_matcher({"pin_code": "57301208"}),
],
json={"code": "SUCCESS", "msg": "success"},
)
resp = self.client.assign_pin_code_to_user(
user_id=user_id, pin_code="57301208"
).success_or_raise()
assert resp.data is None
@responses.activate
def test_unassign_pin_code_from_user(self) -> None:
"""3.10 Unassign PIN Code from User"""
user_id = UserId("17d2f099-99df-429b-becb-1399a6937e5a")
responses.delete(
f"https://{self.host}/api/v1/developer/users/{user_id}/pin_codes",
match=[
matchers.header_matcher(self.common_headers),
],
json={"code": "SUCCESS", "msg": "success"},
)
resp = self.client.unassign_pin_code_from_user(
user_id=user_id,
).success_or_raise()
assert resp.data is None
@responses.activate
def test_create_user_group(self) -> None:
"""3.11 Create User Group"""
responses.post(
f"https://{self.host}/api/v1/developer/user_groups",
match=[
matchers.header_matcher(self.common_headers),
matchers.json_params_matcher(
{
"name": "Group Name",
"up_id": "013d05d3-7262-4908-ba69-badbbbf8f5a6",
}
),
],
json={
"code": "SUCCESS",
# NOTE: completely missing from docs
"data": {
"full_name": "UniFi-CloudKey-Gen2-Plus / Group Name",
"id": "7a681dd8-4d38-4bbf-a061-2fd5d71db5c1",
"name": "Group Name",
"up_id": "bdec1374-9547-4541-9761-d9e66cb1c367",
"up_ids": ["bdec1374-9547-4541-9761-d9e66cb1c367"],
},
"msg": "success",
},
)
resp = self.client.create_user_group(
name="Group Name", up_id=UserGroupId("013d05d3-7262-4908-ba69-badbbbf8f5a6")
).success_or_raise()
@responses.activate
def test_fetch_all_user_groups(self) -> None:
"""3.12 Fetch All User Groups"""
responses.get(
f"https://{self.host}/api/v1/developer/user_groups",
match=[matchers.header_matcher(self.common_headers)],
json={
"code": "SUCCESS",
"data": [
{
"full_name": "Group Name",
"id": "75011ee6-b7ab-4927-9d9f-dd08ef0a3199",
"name": "Group Name",
"up_id": "a27899fc-a2d1-4797-8d4d-86118f8555f3",
"up_ids": ["a27899fc-a2d1-4797-8d4d-86118f8555f3"],
}
],
"msg": "success",
},
)
resp = self.client.fetch_all_user_groups().success_or_raise()
@responses.activate
def test_fetch_user_group(self) -> None:
"""3.13 Fetch User Group"""
user_group_id = UserGroupId("75011ee6-b7ab-4927-9d9f-dd08ef0a3199")
responses.get(
f"https://{self.host}/api/v1/developer/user_groups/{user_group_id}",
match=[matchers.header_matcher(self.common_headers)],
json={
"code": "SUCCESS",
"data": {
"full_name": "Group Name",
"id": "75011ee6-b7ab-4927-9d9f-dd08ef0a3199",
"name": "Group Name",
"up_id": "a27899fc-a2d1-4797-8d4d-86118f8555f3",
"up_ids": ["a27899fc-a2d1-4797-8d4d-86118f8555f3"],
},
"msg": "success",
},
)
resp = self.client.fetch_user_group(
user_group_id=user_group_id
).success_or_raise()
@responses.activate
def test_update_user_group(self) -> None:
"""3.14 Update User Group"""
user_group_id = UserGroupId("75011ee6-b7ab-4927-9d9f-dd08ef0a3199")
responses.put(
f"https://{self.host}/api/v1/developer/user_groups/{user_group_id}",
match=[
matchers.header_matcher(self.common_headers),
matchers.json_params_matcher(
{
"name": "Group Name",
"up_id": "013d05d3-7262-4908-ba69-badbbbf8f5a6",
}
),
],
json={"code": "SUCCESS", "msg": "success"},
)
resp = self.client.update_user_group(
user_group_id=user_group_id,
name="Group Name",
up_id=UserGroupId("013d05d3-7262-4908-ba69-badbbbf8f5a6"),
).success_or_raise()
@responses.activate
def test_delete_user_group(self) -> None:
"""3.15 Delete User Group"""
user_group_id = UserGroupId("75011ee6-b7ab-4927-9d9f-dd08ef0a3199")
responses.delete(
f"https://{self.host}/api/v1/developer/user_groups/{user_group_id}",
match=[matchers.header_matcher(self.common_headers)],
json={"code": "SUCCESS", "msg": "success"},
)
resp = self.client.delete_user_group(
user_group_id=user_group_id
).success_or_raise()
@responses.activate
def test_assign_user_to_user_group(self) -> None:
"""3.16 Assign User to User Group"""
user_group_id = UserGroupId("75011ee6-b7ab-4927-9d9f-dd08ef0a3199")
responses.post(
f"https://{self.host}/api/v1/developer/user_groups/{user_group_id}/users",
match=[
matchers.header_matcher(self.common_headers),
matchers.json_params_matcher(
[
"7c6e9102-acb7-4b89-8ed4-7561e6fb706c",
"fd63bc4c-52e0-4dbf-a699-e1233339c73b",
]
),
],
json={"code": "SUCCESS", "msg": "success"},
)
resp = self.client.assign_user_to_user_group(
user_group_id=user_group_id,
users=[
UserId("7c6e9102-acb7-4b89-8ed4-7561e6fb706c"),
UserId("fd63bc4c-52e0-4dbf-a699-e1233339c73b"),
],
).success_or_raise()
@responses.activate
def test_unassign_user_from_user_group(self) -> None:
"""3.17 Unassign User from User Group"""
user_group_id = UserGroupId("75011ee6-b7ab-4927-9d9f-dd08ef0a3199")
responses.post(
f"https://{self.host}/api/v1/developer/user_groups/{user_group_id}/users/delete",
match=[
matchers.header_matcher(self.common_headers),
matchers.json_params_matcher(
[
"7c6e9102-acb7-4b89-8ed4-7561e6fb706c",
"fd63bc4c-52e0-4dbf-a699-e1233339c73b",
]
),
],
json={"code": "SUCCESS", "msg": "success"},
)
resp = self.client.unassign_user_from_user_group(
user_group_id=user_group_id,
users=[
UserId("7c6e9102-acb7-4b89-8ed4-7561e6fb706c"),
UserId("fd63bc4c-52e0-4dbf-a699-e1233339c73b"),
],
).success_or_raise()
@responses.activate
def test_fetch_users_in_a_user_group(self) -> None:
"""3.18 Fetch Users in a User Group"""
user_group_id = UserGroupId("23676a54-382e-4121-aa80-878d2d9bacaa")
responses.get(
f"https://{self.host}/api/v1/developer/user_groups/{user_group_id}/users",
match=[matchers.header_matcher(self.common_headers)],
json={
"code": "SUCCESS",
"data": [
{
"alias": "",
"avatar_relative_path": "",
"email": "*@*.com",
"email_status": "UNVERIFIED",
"employee_number": "1000000",
"first_name": "",
"full_name": "",
"id": "27aa91ac-2924-43d4-82e1-24b6a570d29e",
"last_name": "Chen",
"onboard_time": 1689150139,
"phone": "",
"status": "ACTIVE",
"user_email": "",
"username": "",
}
],
"msg": "success",
},
)
resp = self.client.fetch_users_in_a_user_group(
user_group_id=user_group_id,
).success_or_raise()
@responses.activate
def test_fetch_all_users_in_a_user_group(self) -> None:
"""3.19 Fetch All Users in a User Group"""
user_group_id = UserGroupId("23676a54-382e-4121-aa80-878d2d9bacaa")
responses.get(
f"https://{self.host}/api/v1/developer/user_groups/{user_group_id}/users/all",
match=[matchers.header_matcher(self.common_headers)],
json={
"code": "SUCCESS",
"data": [
{
"alias": "",
"avatar_relative_path": "",
"email": "*@*.com",
"email_status": "UNVERIFIED",
"employee_number": "1000000",
"first_name": "",
"full_name": "",
"id": "27aa91ac-2924-43d4-82e1-24b6a570d29e",
"last_name": "Chen",
"onboard_time": 1689150139,
"phone": "",
"status": "ACTIVE",
"user_email": "",
"username": "",
}
],
"msg": "success",
},
)
resp = self.client.fetch_all_users_in_a_user_group(
user_group_id=user_group_id,
).success_or_raise()
@responses.activate
def test_fetch_the_access_policies_assigned_to_a_user(self) -> None:
"""3.20 Fetch the Access Policies Assigned to a User"""
user_id = UserId("27aa91ac-2924-43d4-82e1-24b6a570d29e")
responses.get(
f"https://{self.host}/api/v1/developer/users/{user_id}/access_policies",
match=[
matchers.header_matcher(self.common_headers),
matchers.query_param_matcher({"only_user_policies": "false"}),
],
json={
"code": "SUCCESS",
"data": [
{
"id": "89a4ca95-1502-4ae7-954f-d986b67afe5c",
"name": "Default Site Policy",
"resources": [
{
"id": "fd2a06e2-81af-4cf4-9bd5-8bceb5e7b7d7",
"type": "door_group",
}
],
"schedule_id": "6b79d12a-2a6e-4463-949c-f1a98fff40d2",
},
{
"id": "bbe48a65-2ac1-4bf6-bd65-bc8f9ee7fb75",
"name": "Access Policy Name",
"resources": [],
"schedule_id": "f7414bcd-f0cc-4d3e-811a-b5ac75f7ddb8",
},
],
"msg": "success",
},
)
resp = self.client.fetch_the_access_policies_assigned_to_a_user(
user_id=user_id, only_user_policies=False
).success_or_raise()
@responses.activate
def test_assign_access_policy_to_user_group(self) -> None:
"""3.21 Assign Access Policy to User Group"""
user_group_id = UserGroupId("23676a54-382e-4121-aa80-878d2d9bacaa")
responses.put(
f"https://{self.host}/api/v1/developer/user_groups/{user_group_id}/access_policies",
match=[
matchers.header_matcher(self.common_headers),
matchers.json_params_matcher(
{"access_policy_ids": ["bbe48a65-2ac1-4bf6-bd65-bc8f9ee7fb75"]}
),
],
json={"code": "SUCCESS", "data": None, "msg": "success"},
)
resp = self.client.assign_access_policy_to_user_group(
user_group_id=user_group_id,
access_policy_ids=[AccessPolicyId("bbe48a65-2ac1-4bf6-bd65-bc8f9ee7fb75")],
).success_or_raise()
@responses.activate
def test_fetch_the_access_policies_assigned_to_a_user_group(self) -> None:
"""3.22 Fetch the Access Policies Assigned to a User Group"""
user_group_id = UserGroupId("23676a54-382e-4121-aa80-878d2d9bacaa")
responses.get(
f"https://{self.host}/api/v1/developer/user_groups/{user_group_id}/access_policies",
match=[matchers.header_matcher(self.common_headers)],
json={
"code": "SUCCESS",
"data": [
{
"id": "89a4ca95-1502-4ae7-954f-d986b67afe5c",
"name": "Default Site Policy",
"resources": [
{
"id": "fd2a06e2-81af-4cf4-9bd5-8bceb5e7b7d7",
"type": "door_group",
}
],
"schedule_id": "6b79d12a-2a6e-4463-949c-f1a98fff40d2",
},
{
"id": "bbe48a65-2ac1-4bf6-bd65-bc8f9ee7fb75",
"name": "Access Policy Name",
"resources": [],
"schedule_id": "f7414bcd-f0cc-4d3e-811a-b5ac75f7ddb8",
},
],
"msg": "success",
},
)
resp = self.client.fetch_the_access_policies_assigned_to_a_user_group(
user_group_id=user_group_id,
).success_or_raise()

724
tests/test_visitor.py Normal file
View File

@ -0,0 +1,724 @@
import datetime
import responses
from responses import matchers
from unifi_access.schemas import (
DoorGroupId,
DoorGroupResource,
DoorId,
DoorResource,
FetchAllVisitorsExpansion,
NfcCardToken,
TimePeriod,
VisitorId,
VisitReason,
WeekSchedule,
)
from .base import UnifiAccessTests
class VisitorTests(UnifiAccessTests):
@responses.activate
def test_create_visitor(self) -> None:
"""4.2 Create Visitor"""
responses.post(
f"https://{self.host}/api/v1/developer/visitors",
match=[
matchers.header_matcher(self.common_headers),
matchers.json_params_matcher(
{
"first_name": "H",
"last_name": "L",
"remarks": "",
"mobile_phone": "",
"email": "",
"visitor_company": "",
"start_time": 1688546460,
"end_time": 1788572799,
# NOTE: typo'ed in api docs to "Interviemw"
"visit_reason": "Interview",
"week_schedule": {
"sunday": [],
"monday": [],
"tuesday": [
{"start_time": "10:00:00", "end_time": "17:00:59"}
],
"wednesday": [
{"start_time": "10:00:00", "end_time": "17:00:59"}
],
"thursday": [
{"start_time": "11:00:00", "end_time": "17:00:59"}
],
"friday": [
{"start_time": "10:00:00", "end_time": "17:00:59"}
],
"saturday": [],
},
"resources": [
{
"id": "6ff875d2-af87-470b-9cb5-774c6596afc8",
"type": "door",
},
{
"id": "5c496423-6d25-4e4f-8cdf-95ad5135188a",
"type": "door_group",
},
{
"id": "d5573467-d6b3-4e8f-8e48-8a322b91664a",
"type": "door_group",
},
],
}
),
],
json={
"code": "SUCCESS",
"data": {
"first_name": "H",
"id": "fbe8d920-47d3-4cfd-bda7-bf4b0e26f73c",
"last_name": "L",
"nfc_cards": [],
"resources": [
{
"id": "5c496423-6d25-4e4f-8cdf-95ad5135188a",
"name": "Test Group",
"type": "door_group",
},
{
"id": "d5573467-d6b3-4e8f-8e48-8a322b91664a",
"name": "UNVR",
"type": "door_group",
},
{
"id": "369215b0-cabe-49b6-aeaa-e0b7ec6424d5",
"name": "visitor-1691671529285",
"type": "door_group",
},
],
"schedule": {
"id": "1fb849bb-e7e5-4516-8dd9-b78094a6708a",
"is_default": False,
"name": "schedule-1691671529237",
"type": "access",
"weekly": {
"friday": [
{"end_time": "17:00:59", "start_time": "10:00:00"}
],
"monday": [],
"saturday": [],
"sunday": [],
"thursday": [
{"end_time": "17:00:59", "start_time": "11:00:00"}
],
"tuesday": [
{"end_time": "17:00:59", "start_time": "10:00:00"}
],
"wednesday": [
{"end_time": "17:00:59", "start_time": "10:00:00"}
],
},
},
"schedule_id": "1fb849bb-e7e5-4516-8dd9-b78094a6708a",
"status": "ACTIVE",
},
"msg": "success",
},
)
resp = self.client.create_visitor(
first_name="H",
last_name="L",
remarks="",
mobile_phone="",
email="",
visitor_company="",
start_time=datetime.datetime.fromtimestamp(1688546460, tz=datetime.UTC),
end_time=datetime.datetime.fromtimestamp(1788572799, tz=datetime.UTC),
visit_reason=VisitReason.INTERVIEW,
week_schedule=WeekSchedule(
sunday=[],
monday=[],
tuesday=[
TimePeriod(
start_time=datetime.time(10, 0, 0),
end_time=datetime.time(17, 0, 59),
)
],
wednesday=[
TimePeriod(
start_time=datetime.time(10, 0, 0),
end_time=datetime.time(17, 0, 59),
)
],
thursday=[
TimePeriod(
start_time=datetime.time(11, 0, 0),
end_time=datetime.time(17, 0, 59),
)
],
friday=[
TimePeriod(
start_time=datetime.time(10, 0, 0),
end_time=datetime.time(17, 0, 59),
)
],
saturday=[],
),
resources=[
DoorResource(
id=DoorId("6ff875d2-af87-470b-9cb5-774c6596afc8"),
),
DoorGroupResource(
id=DoorGroupId("5c496423-6d25-4e4f-8cdf-95ad5135188a"),
),
DoorGroupResource(
id=DoorGroupId("d5573467-d6b3-4e8f-8e48-8a322b91664a")
),
],
).success_or_raise()
assert resp.data.id == "fbe8d920-47d3-4cfd-bda7-bf4b0e26f73c"
@responses.activate
def test_fetch_visitor(self) -> None:
"""4.3 Fetch Visitor"""
visitor_id = VisitorId("76794bd8-98c0-49b6-9230-ba8c5812cf29")
responses.get(
f"https://{self.host}/api/v1/developer/visitors/{visitor_id}",
match=[
matchers.header_matcher(self.common_headers),
],
json={
"code": "SUCCESS",
"data": {
"first_name": "Hong+243",
"id": "3566867c-fa04-4752-98f6-43cf9a342d4a",
"last_name": "Lu",
"nfc_cards": [
{
"id": "100001",
"token": "d27822fc682b478dc637c6db01813e465174df6e54ca515d8427db623cfda1d0",
"type": "ua_card",
}
],
"pin_code": {
"token": "bc3e3135013e2dcae119390b7897166e8cec3bcf5becb6b05578ab67634559ed"
},
"resources": [
{
"id": "fd293ecb-98d2-425b-a020-cb9365ea48b3",
"name": "visitor-1690337152955",
"type": "door_group",
}
],
"schedule": {
"id": "6ccf9e1e-b174-476d-b2fe-96817c780fbf",
"is_default": False,
"name": "visitor-1690337152955",
"type": "access",
"weekly": None,
},
"schedule_id": "6ccf9e1e-b174-476d-b2fe-96817c780fbf",
"status": "VISITED",
},
"msg": "success",
},
)
resp = self.client.fetch_visitor(visitor_id=visitor_id).success_or_raise()
# TODO: verify correctness of data?
@responses.activate
def test_fetch_all_visitors(self) -> None:
"""4.4 Fetch All Visitors"""
responses.get(
f"https://{self.host}/api/v1/developer/visitors",
match=[
matchers.header_matcher(self.common_headers),
matchers.query_param_matcher({"page_num": 1, "page_size": 25}),
],
# NOTE: no example data provided in api docs, so this was
# retrieved from a running instance
json={
"code": "SUCCESS",
"data": [
{
"avatar": "",
"email": "",
"end_time": 1731880901,
"first_name": "Test",
"id": "faaffd2e-b555-4991-810f-c18b36407c55",
"inviter_id": "",
"inviter_name": "",
"last_name": "Visitor",
"location_id": "",
"mobile_phone": "",
"nfc_cards": [],
"remarks": "",
"resources": [],
"schedule": {
"holiday_group": None,
"holiday_group_id": "",
"holiday_schedule": [],
"id": "",
"is_default": False,
"name": "",
"type": "",
"weekly": None,
},
"schedule_id": "",
"start_time": 1731794501,
"status": "UPCOMING",
"visit_reason": "Business",
"visitor_company": "",
}
],
"msg": "succ",
"pagination": {"page_num": 1, "page_size": 1, "total": 1},
},
)
resp = self.client.fetch_all_visitors(
page_num=1, page_size=25
).success_or_raise()
assert resp.pagination
# TODO: verify correctness of data?
@responses.activate
def test_fetch_all_visitors_by_keyword(self) -> None:
"""4.4 Fetch All Visitors"""
responses.get(
f"https://{self.host}/api/v1/developer/visitors",
match=[
matchers.header_matcher(self.common_headers),
matchers.query_param_matcher({"keyword": "H"}),
],
# NOTE: no example data provided in api docs, so this was
# retrieved from a running instance
json={
"code": "SUCCESS",
"data": [
{
"avatar": "",
"email": "",
"end_time": 1731880901,
"first_name": "Test H",
"id": "faaffd2e-b555-4991-810f-c18b36407c55",
"inviter_id": "",
"inviter_name": "",
"last_name": "Visitor",
"location_id": "",
"mobile_phone": "",
"nfc_cards": [],
"remarks": "",
"resources": [],
"schedule": {
"holiday_group": None,
"holiday_group_id": "",
"holiday_schedule": [],
"id": "",
"is_default": False,
"name": "",
"type": "",
"weekly": None,
},
"schedule_id": "",
"start_time": 1731794501,
"status": "UPCOMING",
"visit_reason": "Business",
"visitor_company": "",
}
],
"msg": "succ",
"pagination": {"page_num": 1, "page_size": 1, "total": 1},
},
)
resp = self.client.fetch_all_visitors(keyword="H").success_or_raise()
assert resp.pagination
# TODO: verify correctness of data?
@responses.activate
def test_fetch_all_visitors_with_expand(self) -> None:
"""4.4 Fetch All Visitors"""
responses.get(
f"https://{self.host}/api/v1/developer/visitors",
match=[
matchers.header_matcher(self.common_headers),
matchers.query_param_matcher(
{
"expand[]": [
"access_policy",
"resource",
"schedule",
"nfc_card",
"pin_code",
]
}
),
],
# NOTE: no example data provided in api docs, so this was
# retrieved from a running instance
json={
"code": "SUCCESS",
"data": [
{
"avatar": "",
"email": "",
"end_time": 1731880901,
"first_name": "Test",
"id": "faaffd2e-b555-4991-810f-c18b36407c55",
"inviter_id": "",
"inviter_name": "",
"last_name": "Visitor",
"location_id": "",
"mobile_phone": "",
"nfc_cards": [],
"remarks": "",
"resources": [],
"schedule": {
"holiday_group": None,
"holiday_group_id": "",
"holiday_schedule": [],
"id": "",
"is_default": False,
"name": "",
"type": "",
"weekly": None,
},
"schedule_id": "",
"start_time": 1731794501,
"status": "UPCOMING",
"visit_reason": "Business",
"visitor_company": "",
}
],
"msg": "succ",
"pagination": {"page_num": 1, "page_size": 1, "total": 1},
},
)
resp = self.client.fetch_all_visitors(
expand=[
FetchAllVisitorsExpansion.ACCESS_POLICY,
FetchAllVisitorsExpansion.RESOURCE,
FetchAllVisitorsExpansion.SCHEDULE,
FetchAllVisitorsExpansion.NFC_CARD,
FetchAllVisitorsExpansion.PIN_CODE,
]
).success_or_raise()
assert resp.pagination
# TODO: verify correctness of data?
# NOTE: not taken from API example
@responses.activate
def test_fetch_all_visitors_without_expand(self) -> None:
"""4.4 Fetch All Visitors"""
responses.get(
f"https://{self.host}/api/v1/developer/visitors",
match=[
matchers.header_matcher(self.common_headers),
matchers.query_param_matcher({"expand[]": "none"}),
],
# NOTE: retrieved from a running instance
json={
"code": "SUCCESS",
"data": [
{
"avatar": "",
"email": "",
"end_time": 1731907089,
"first_name": "Test",
"id": "173c4cb9-e174-4a83-89fa-01ba8f25362f",
"inviter_id": "",
"inviter_name": "",
"last_name": "Visitor",
"location_id": "",
"mobile_phone": "",
"nfc_cards": [],
"pin_code": {},
"remarks": "",
"resources": [],
"schedule_id": "",
"start_time": 1731820689,
"status": "UPCOMING",
"visit_reason": "Business",
"visitor_company": "",
},
],
"msg": "succ",
"pagination": {"page_num": 1, "page_size": 1, "total": 1},
},
)
resp = self.client.fetch_all_visitors(
expand=[FetchAllVisitorsExpansion.NONE]
).success_or_raise()
assert resp.pagination
# TODO: verify correctness of data?
@responses.activate
def test_update_visitor(self) -> None:
"""4.5 Update Visitor"""
visitor_id = VisitorId("37f2b996-c2c5-487b-aa22-8b453ff14a4b")
responses.put(
f"https://{self.host}/api/v1/developer/visitors/{visitor_id}",
match=[
matchers.header_matcher(self.common_headers),
matchers.json_params_matcher(
{
"first_name": "Test",
"last_name": "L",
"remarks": "",
"mobile_phone": "",
"email": "",
"visitor_company": "",
"start_time": 1688546460,
"end_time": 1788572799,
# NOTE: typo'ed in api docs to "Interviemw"
"visit_reason": "Interview",
"week_schedule": {
"sunday": [],
"monday": [
{"start_time": "10:00:00", "end_time": "17:00:59"}
],
"tuesday": [],
"wednesday": [
{"start_time": "10:00:00", "end_time": "17:00:59"}
],
"thursday": [
{"start_time": "11:00:00", "end_time": "18:00:59"}
],
"friday": [
{"start_time": "10:00:00", "end_time": "17:00:59"}
],
"saturday": [],
},
"resources": [
{
"id": "6ff875d2-af87-470b-9cb5-774c6596afc8",
"type": "door",
},
{
"id": "5c496423-6d25-4e4f-8cdf-95ad5135188a",
"type": "door_group",
},
{
"id": "d5573467-d6b3-4e8f-8e48-8a322b91664a",
"type": "door_group",
},
],
}
),
],
json={
"code": "SUCCESS",
"data": {
"first_name": "H",
"id": "8564ce90-76ba-445f-b78b-6cca39af0130",
"last_name": "L",
"nfc_cards": [],
"pin_code": None,
"resources": [
{
"id": "5c496423-6d25-4e4f-8cdf-95ad5135188a",
"name": "Door-Group-1",
"type": "door_group",
},
{
"id": "d5573467-d6b3-4e8f-8e48-8a322b91664a",
"name": "UNVR",
"type": "door_group",
},
{
"id": "e311ca94-172c-49fe-9c91-49bd8ecef845",
"name": "visitor-1691646856144",
"type": "door_group",
},
],
"schedule": {
"id": "c03bf601-0b90-4341-bce4-6061931e9f4e",
"is_default": False,
"name": "visitor-1691646856097",
"type": "access",
"weekly": {
"friday": [
{"end_time": "17:00:59", "start_time": "10:00:00"}
],
"monday": [
{"end_time": "17:00:59", "start_time": "10:00:00"}
],
"saturday": [],
"sunday": [],
"thursday": [
{"end_time": "18:00:59", "start_time": "11:00:00"}
],
"tuesday": [],
"wednesday": [
{"end_time": "17:00:59", "start_time": "10:00:00"}
],
},
},
"schedule_id": "c03bf601-0b90-4341-bce4-6061931e9f4e",
"status": "ACTIVE",
},
"msg": "success",
},
)
resp = self.client.update_visitor(
visitor_id=visitor_id,
first_name="Test",
last_name="L",
remarks="",
mobile_phone="",
email="",
visitor_company="",
start_time=datetime.datetime.fromtimestamp(1688546460, tz=datetime.UTC),
end_time=datetime.datetime.fromtimestamp(1788572799, tz=datetime.UTC),
visit_reason=VisitReason.INTERVIEW,
week_schedule=WeekSchedule(
sunday=[],
monday=[
TimePeriod(
start_time=datetime.time(10, 0, 0),
end_time=datetime.time(17, 0, 59),
)
],
tuesday=[],
wednesday=[
TimePeriod(
start_time=datetime.time(10, 0, 0),
end_time=datetime.time(17, 0, 59),
)
],
thursday=[
TimePeriod(
start_time=datetime.time(11, 0, 0),
end_time=datetime.time(18, 0, 59),
)
],
friday=[
TimePeriod(
start_time=datetime.time(10, 0, 0),
end_time=datetime.time(17, 0, 59),
)
],
saturday=[],
),
resources=[
DoorResource(
id=DoorId("6ff875d2-af87-470b-9cb5-774c6596afc8"),
),
DoorGroupResource(
id=DoorGroupId("5c496423-6d25-4e4f-8cdf-95ad5135188a"),
),
DoorGroupResource(
id=DoorGroupId("d5573467-d6b3-4e8f-8e48-8a322b91664a")
),
],
).success_or_raise()
@responses.activate
def test_delete_visitor(self) -> None:
"""4.6 Delete Visitor"""
visitor_id = VisitorId("c81dfee6-5970-4938-bd30-40820e16ea01")
responses.delete(
f"https://{self.host}/api/v1/developer/visitors/{visitor_id}",
match=[
matchers.header_matcher(self.common_headers),
matchers.query_param_matcher({"is_force": "true"}),
],
json={"code": "SUCCESS", "data": None, "msg": "success"},
)
resp = self.client.delete_visitor(
visitor_id=visitor_id,
is_force=True,
).success_or_raise()
# TODO: verify correctness of data?
@responses.activate
def test_assign_nfc_card_to_visitor(self) -> None:
"""4.7 Assign NFC Card To Visitor"""
visitor_id = VisitorId("60b5c15e-9aff-4fc8-9547-d21d2e39c1ff")
responses.put(
f"https://{self.host}/api/v1/developer/visitors/{visitor_id}/nfc_cards",
match=[
matchers.header_matcher(self.common_headers),
matchers.json_params_matcher(
{
"token": "d27822fc682b478dc637c6db01813e465174df6e54ca515d8427db623cfda1d0",
"force_add": True,
}
),
],
json={"code": "SUCCESS", "msg": "success"},
)
resp = self.client.assign_nfc_card_to_visitor(
visitor_id=visitor_id,
token=NfcCardToken(
"d27822fc682b478dc637c6db01813e465174df6e54ca515d8427db623cfda1d0"
),
force_add=True,
).success_or_raise()
@responses.activate
def test_unassign_nfc_card_from_visitor(self) -> None:
"""4.8 Unassign NFC Card From Visitor"""
visitor_id = VisitorId("60b5c15e-9aff-4fc8-9547-d21d2e39c1ff")
responses.put(
f"https://{self.host}/api/v1/developer/visitors/{visitor_id}/nfc_cards/delete",
match=[
matchers.header_matcher(self.common_headers),
matchers.json_params_matcher(
{
"token": "d27822fc682b478dc637c6db01813e465174df6e54ca515d8427db623cfda1d0"
}
),
],
json={"code": "SUCCESS", "msg": "success"},
)
resp = self.client.unassign_nfc_card_from_visitor(
visitor_id=visitor_id,
token=NfcCardToken(
"d27822fc682b478dc637c6db01813e465174df6e54ca515d8427db623cfda1d0"
),
).success_or_raise()
@responses.activate
def test_assign_pin_code_to_visitor(self) -> None:
"""4.9 Assign Pin Code To Visitor"""
visitor_id = VisitorId("17d2f099-99df-429b-becb-1399a6937e5a")
responses.put(
f"https://{self.host}/api/v1/developer/visitors/{visitor_id}/pin_codes",
match=[
matchers.header_matcher(self.common_headers),
matchers.json_params_matcher({"pin_code": "57301208"}),
],
json={"code": "SUCCESS", "msg": "success"},
)
resp = self.client.assign_pin_code_to_visitor(
visitor_id=visitor_id,
pin_code="57301208",
).success_or_raise()
@responses.activate
def test_unassign_pin_code_from_visitor(self) -> None:
"""4.10 Unassign Pin Code From Visitor"""
visitor_id = VisitorId("17d2f099-99df-429b-becb-1399a6937e5a")
responses.delete(
f"https://{self.host}/api/v1/developer/visitors/{visitor_id}/pin_codes",
match=[
matchers.header_matcher(self.common_headers),
],
json={"code": "SUCCESS", "msg": "success"},
)
resp = self.client.unassign_pin_code_from_visitor(
visitor_id=visitor_id
).success_or_raise()

802
uv.lock generated Normal file
View File

@ -0,0 +1,802 @@
version = 1
requires-python = ">=3.11"
resolution-markers = [
"python_full_version < '3.13'",
"python_full_version >= '3.13'",
]
[[package]]
name = "annotated-types"
version = "0.7.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 },
]
[[package]]
name = "babel"
version = "2.16.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/2a/74/f1bc80f23eeba13393b7222b11d95ca3af2c1e28edca18af487137eefed9/babel-2.16.0.tar.gz", hash = "sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316", size = 9348104 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ed/20/bc79bc575ba2e2a7f70e8a1155618bb1301eaa5132a8271373a6903f73f8/babel-2.16.0-py3-none-any.whl", hash = "sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b", size = 9587599 },
]
[[package]]
name = "certifi"
version = "2024.8.30"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b0/ee/9b19140fe824b367c04c5e1b369942dd754c4c5462d5674002f75c4dedc1/certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9", size = 168507 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/12/90/3c9ff0512038035f59d279fddeb79f5f1eccd8859f06d6163c58798b9487/certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", size = 167321 },
]
[[package]]
name = "charset-normalizer"
version = "3.4.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f2/4f/e1808dc01273379acc506d18f1504eb2d299bd4131743b9fc54d7be4df1e/charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e", size = 106620 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9c/61/73589dcc7a719582bf56aae309b6103d2762b526bffe189d635a7fcfd998/charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c", size = 193339 },
{ url = "https://files.pythonhosted.org/packages/77/d5/8c982d58144de49f59571f940e329ad6e8615e1e82ef84584c5eeb5e1d72/charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944", size = 124366 },
{ url = "https://files.pythonhosted.org/packages/bf/19/411a64f01ee971bed3231111b69eb56f9331a769072de479eae7de52296d/charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee", size = 118874 },
{ url = "https://files.pythonhosted.org/packages/4c/92/97509850f0d00e9f14a46bc751daabd0ad7765cff29cdfb66c68b6dad57f/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c", size = 138243 },
{ url = "https://files.pythonhosted.org/packages/e2/29/d227805bff72ed6d6cb1ce08eec707f7cfbd9868044893617eb331f16295/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6", size = 148676 },
{ url = "https://files.pythonhosted.org/packages/13/bc/87c2c9f2c144bedfa62f894c3007cd4530ba4b5351acb10dc786428a50f0/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea", size = 141289 },
{ url = "https://files.pythonhosted.org/packages/eb/5b/6f10bad0f6461fa272bfbbdf5d0023b5fb9bc6217c92bf068fa5a99820f5/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc", size = 142585 },
{ url = "https://files.pythonhosted.org/packages/3b/a0/a68980ab8a1f45a36d9745d35049c1af57d27255eff8c907e3add84cf68f/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5", size = 144408 },
{ url = "https://files.pythonhosted.org/packages/d7/a1/493919799446464ed0299c8eef3c3fad0daf1c3cd48bff9263c731b0d9e2/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594", size = 139076 },
{ url = "https://files.pythonhosted.org/packages/fb/9d/9c13753a5a6e0db4a0a6edb1cef7aee39859177b64e1a1e748a6e3ba62c2/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c", size = 146874 },
{ url = "https://files.pythonhosted.org/packages/75/d2/0ab54463d3410709c09266dfb416d032a08f97fd7d60e94b8c6ef54ae14b/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365", size = 150871 },
{ url = "https://files.pythonhosted.org/packages/8d/c9/27e41d481557be53d51e60750b85aa40eaf52b841946b3cdeff363105737/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129", size = 148546 },
{ url = "https://files.pythonhosted.org/packages/ee/44/4f62042ca8cdc0cabf87c0fc00ae27cd8b53ab68be3605ba6d071f742ad3/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236", size = 143048 },
{ url = "https://files.pythonhosted.org/packages/01/f8/38842422988b795220eb8038745d27a675ce066e2ada79516c118f291f07/charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99", size = 94389 },
{ url = "https://files.pythonhosted.org/packages/0b/6e/b13bd47fa9023b3699e94abf565b5a2f0b0be6e9ddac9812182596ee62e4/charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27", size = 101752 },
{ url = "https://files.pythonhosted.org/packages/d3/0b/4b7a70987abf9b8196845806198975b6aab4ce016632f817ad758a5aa056/charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6", size = 194445 },
{ url = "https://files.pythonhosted.org/packages/50/89/354cc56cf4dd2449715bc9a0f54f3aef3dc700d2d62d1fa5bbea53b13426/charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf", size = 125275 },
{ url = "https://files.pythonhosted.org/packages/fa/44/b730e2a2580110ced837ac083d8ad222343c96bb6b66e9e4e706e4d0b6df/charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db", size = 119020 },
{ url = "https://files.pythonhosted.org/packages/9d/e4/9263b8240ed9472a2ae7ddc3e516e71ef46617fe40eaa51221ccd4ad9a27/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1", size = 139128 },
{ url = "https://files.pythonhosted.org/packages/6b/e3/9f73e779315a54334240353eaea75854a9a690f3f580e4bd85d977cb2204/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03", size = 149277 },
{ url = "https://files.pythonhosted.org/packages/1a/cf/f1f50c2f295312edb8a548d3fa56a5c923b146cd3f24114d5adb7e7be558/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284", size = 142174 },
{ url = "https://files.pythonhosted.org/packages/16/92/92a76dc2ff3a12e69ba94e7e05168d37d0345fa08c87e1fe24d0c2a42223/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15", size = 143838 },
{ url = "https://files.pythonhosted.org/packages/a4/01/2117ff2b1dfc61695daf2babe4a874bca328489afa85952440b59819e9d7/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8", size = 146149 },
{ url = "https://files.pythonhosted.org/packages/f6/9b/93a332b8d25b347f6839ca0a61b7f0287b0930216994e8bf67a75d050255/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2", size = 140043 },
{ url = "https://files.pythonhosted.org/packages/ab/f6/7ac4a01adcdecbc7a7587767c776d53d369b8b971382b91211489535acf0/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719", size = 148229 },
{ url = "https://files.pythonhosted.org/packages/9d/be/5708ad18161dee7dc6a0f7e6cf3a88ea6279c3e8484844c0590e50e803ef/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631", size = 151556 },
{ url = "https://files.pythonhosted.org/packages/5a/bb/3d8bc22bacb9eb89785e83e6723f9888265f3a0de3b9ce724d66bd49884e/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b", size = 149772 },
{ url = "https://files.pythonhosted.org/packages/f7/fa/d3fc622de05a86f30beea5fc4e9ac46aead4731e73fd9055496732bcc0a4/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565", size = 144800 },
{ url = "https://files.pythonhosted.org/packages/9a/65/bdb9bc496d7d190d725e96816e20e2ae3a6fa42a5cac99c3c3d6ff884118/charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7", size = 94836 },
{ url = "https://files.pythonhosted.org/packages/3e/67/7b72b69d25b89c0b3cea583ee372c43aa24df15f0e0f8d3982c57804984b/charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9", size = 102187 },
{ url = "https://files.pythonhosted.org/packages/f3/89/68a4c86f1a0002810a27f12e9a7b22feb198c59b2f05231349fbce5c06f4/charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114", size = 194617 },
{ url = "https://files.pythonhosted.org/packages/4f/cd/8947fe425e2ab0aa57aceb7807af13a0e4162cd21eee42ef5b053447edf5/charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed", size = 125310 },
{ url = "https://files.pythonhosted.org/packages/5b/f0/b5263e8668a4ee9becc2b451ed909e9c27058337fda5b8c49588183c267a/charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250", size = 119126 },
{ url = "https://files.pythonhosted.org/packages/ff/6e/e445afe4f7fda27a533f3234b627b3e515a1b9429bc981c9a5e2aa5d97b6/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920", size = 139342 },
{ url = "https://files.pythonhosted.org/packages/a1/b2/4af9993b532d93270538ad4926c8e37dc29f2111c36f9c629840c57cd9b3/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64", size = 149383 },
{ url = "https://files.pythonhosted.org/packages/fb/6f/4e78c3b97686b871db9be6f31d64e9264e889f8c9d7ab33c771f847f79b7/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23", size = 142214 },
{ url = "https://files.pythonhosted.org/packages/2b/c9/1c8fe3ce05d30c87eff498592c89015b19fade13df42850aafae09e94f35/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc", size = 144104 },
{ url = "https://files.pythonhosted.org/packages/ee/68/efad5dcb306bf37db7db338338e7bb8ebd8cf38ee5bbd5ceaaaa46f257e6/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d", size = 146255 },
{ url = "https://files.pythonhosted.org/packages/0c/75/1ed813c3ffd200b1f3e71121c95da3f79e6d2a96120163443b3ad1057505/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88", size = 140251 },
{ url = "https://files.pythonhosted.org/packages/7d/0d/6f32255c1979653b448d3c709583557a4d24ff97ac4f3a5be156b2e6a210/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90", size = 148474 },
{ url = "https://files.pythonhosted.org/packages/ac/a0/c1b5298de4670d997101fef95b97ac440e8c8d8b4efa5a4d1ef44af82f0d/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b", size = 151849 },
{ url = "https://files.pythonhosted.org/packages/04/4f/b3961ba0c664989ba63e30595a3ed0875d6790ff26671e2aae2fdc28a399/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d", size = 149781 },
{ url = "https://files.pythonhosted.org/packages/d8/90/6af4cd042066a4adad58ae25648a12c09c879efa4849c705719ba1b23d8c/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482", size = 144970 },
{ url = "https://files.pythonhosted.org/packages/cc/67/e5e7e0cbfefc4ca79025238b43cdf8a2037854195b37d6417f3d0895c4c2/charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67", size = 94973 },
{ url = "https://files.pythonhosted.org/packages/65/97/fc9bbc54ee13d33dc54a7fcf17b26368b18505500fc01e228c27b5222d80/charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b", size = 102308 },
{ url = "https://files.pythonhosted.org/packages/bf/9b/08c0432272d77b04803958a4598a51e2a4b51c06640af8b8f0f908c18bf2/charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079", size = 49446 },
]
[[package]]
name = "click"
version = "8.1.7"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "platform_system == 'Windows'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", size = 97941 },
]
[[package]]
name = "colorama"
version = "0.4.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
]
[[package]]
name = "ghp-import"
version = "2.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "python-dateutil" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034 },
]
[[package]]
name = "griffe"
version = "1.5.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d4/c9/8167810358ca129839156dc002526e7398b5fad4a9d7b6e88b875e802d0d/griffe-1.5.1.tar.gz", hash = "sha256:72964f93e08c553257706d6cd2c42d1c172213feb48b2be386f243380b405d4b", size = 384113 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ab/00/e693a155da0a2a72fd2df75b8fe338146cae59d590ad6f56800adde90cb5/griffe-1.5.1-py3-none-any.whl", hash = "sha256:ad6a7980f8c424c9102160aafa3bcdf799df0e75f7829d75af9ee5aef656f860", size = 127132 },
]
[[package]]
name = "idna"
version = "3.10"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
]
[[package]]
name = "iniconfig"
version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 },
]
[[package]]
name = "jinja2"
version = "3.1.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markupsafe" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ed/55/39036716d19cab0747a5020fc7e907f362fbf48c984b14e62127f7e68e5d/jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369", size = 240245 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d", size = 133271 },
]
[[package]]
name = "markdown"
version = "3.7"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/54/28/3af612670f82f4c056911fbbbb42760255801b3068c48de792d354ff4472/markdown-3.7.tar.gz", hash = "sha256:2ae2471477cfd02dbbf038d5d9bc226d40def84b4fe2986e49b59b6b472bbed2", size = 357086 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3f/08/83871f3c50fc983b88547c196d11cf8c3340e37c32d2e9d6152abe2c61f7/Markdown-3.7-py3-none-any.whl", hash = "sha256:7eb6df5690b81a1d7942992c97fad2938e956e79df20cbc6186e9c3a77b1c803", size = 106349 },
]
[[package]]
name = "markupsafe"
version = "3.0.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353 },
{ url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392 },
{ url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984 },
{ url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120 },
{ url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032 },
{ url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057 },
{ url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359 },
{ url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306 },
{ url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094 },
{ url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521 },
{ url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274 },
{ url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348 },
{ url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149 },
{ url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118 },
{ url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993 },
{ url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178 },
{ url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319 },
{ url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352 },
{ url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097 },
{ url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601 },
{ url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274 },
{ url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352 },
{ url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122 },
{ url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085 },
{ url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978 },
{ url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208 },
{ url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357 },
{ url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344 },
{ url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101 },
{ url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603 },
{ url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510 },
{ url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486 },
{ url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480 },
{ url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914 },
{ url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796 },
{ url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473 },
{ url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114 },
{ url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098 },
{ url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208 },
{ url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 },
]
[[package]]
name = "mergedeep"
version = "1.3.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354 },
]
[[package]]
name = "mkdocs"
version = "1.6.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "colorama", marker = "platform_system == 'Windows'" },
{ name = "ghp-import" },
{ name = "jinja2" },
{ name = "markdown" },
{ name = "markupsafe" },
{ name = "mergedeep" },
{ name = "mkdocs-get-deps" },
{ name = "packaging" },
{ name = "pathspec" },
{ name = "pyyaml" },
{ name = "pyyaml-env-tag" },
{ name = "watchdog" },
]
sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451 },
]
[[package]]
name = "mkdocs-autorefs"
version = "1.2.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markdown" },
{ name = "markupsafe" },
{ name = "mkdocs" },
]
sdist = { url = "https://files.pythonhosted.org/packages/fb/ae/0f1154c614d6a8b8a36fff084e5b82af3a15f7d2060cf0dcdb1c53297a71/mkdocs_autorefs-1.2.0.tar.gz", hash = "sha256:a86b93abff653521bda71cf3fc5596342b7a23982093915cb74273f67522190f", size = 40262 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/71/26/4d39d52ea2219604053a4d05b98e90d6a335511cc01806436ec4886b1028/mkdocs_autorefs-1.2.0-py3-none-any.whl", hash = "sha256:d588754ae89bd0ced0c70c06f58566a4ee43471eeeee5202427da7de9ef85a2f", size = 16522 },
]
[[package]]
name = "mkdocs-get-deps"
version = "0.2.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "mergedeep" },
{ name = "platformdirs" },
{ name = "pyyaml" },
]
sdist = { url = "https://files.pythonhosted.org/packages/98/f5/ed29cd50067784976f25ed0ed6fcd3c2ce9eb90650aa3b2796ddf7b6870b/mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c", size = 10239 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134", size = 9521 },
]
[[package]]
name = "mkdocs-material"
version = "9.5.44"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "babel" },
{ name = "colorama" },
{ name = "jinja2" },
{ name = "markdown" },
{ name = "mkdocs" },
{ name = "mkdocs-material-extensions" },
{ name = "paginate" },
{ name = "pygments" },
{ name = "pymdown-extensions" },
{ name = "regex" },
{ name = "requests" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f7/56/182d8121db9ab553cdf9bc58d5972b89833f60b63272f693c1f2b849b640/mkdocs_material-9.5.44.tar.gz", hash = "sha256:f3a6c968e524166b3f3ed1fb97d3ed3e0091183b0545cedf7156a2a6804c56c0", size = 3964306 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ed/eb/a801d00e0e210d82184aacce596906ec065422c78a7319244ba0771c4ded/mkdocs_material-9.5.44-py3-none-any.whl", hash = "sha256:47015f9c167d58a5ff5e682da37441fc4d66a1c79334bfc08d774763cacf69ca", size = 8674509 },
]
[[package]]
name = "mkdocs-material-extensions"
version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/79/9b/9b4c96d6593b2a541e1cb8b34899a6d021d208bb357042823d4d2cabdbe7/mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", size = 11847 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728 },
]
[[package]]
name = "mkdocstrings"
version = "0.27.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "jinja2" },
{ name = "markdown" },
{ name = "markupsafe" },
{ name = "mkdocs" },
{ name = "mkdocs-autorefs" },
{ name = "platformdirs" },
{ name = "pymdown-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e2/5a/5de70538c2cefae7ac3a15b5601e306ef3717290cb2aab11d51cbbc2d1c0/mkdocstrings-0.27.0.tar.gz", hash = "sha256:16adca6d6b0a1f9e0c07ff0b02ced8e16f228a9d65a37c063ec4c14d7b76a657", size = 94830 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cd/10/4c27c3063c2b3681a4b7942f8dbdeb4fa34fecb2c19b594e7345ebf4f86f/mkdocstrings-0.27.0-py3-none-any.whl", hash = "sha256:6ceaa7ea830770959b55a16203ac63da24badd71325b96af950e59fd37366332", size = 30658 },
]
[package.optional-dependencies]
python = [
{ name = "mkdocstrings-python" },
]
[[package]]
name = "mkdocstrings-python"
version = "1.12.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "griffe" },
{ name = "mkdocs-autorefs" },
{ name = "mkdocstrings" },
]
sdist = { url = "https://files.pythonhosted.org/packages/23/ec/cb6debe2db77f1ef42b25b21d93b5021474de3037cd82385e586aee72545/mkdocstrings_python-1.12.2.tar.gz", hash = "sha256:7a1760941c0b52a2cd87b960a9e21112ffe52e7df9d0b9583d04d47ed2e186f3", size = 168207 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5b/c1/ac524e1026d9580cbc654b5d19f5843c8b364a66d30f956372cd09fd2f92/mkdocstrings_python-1.12.2-py3-none-any.whl", hash = "sha256:7f7d40d6db3cb1f5d19dbcd80e3efe4d0ba32b073272c0c0de9de2e604eda62a", size = 111759 },
]
[[package]]
name = "mypy"
version = "1.13.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "mypy-extensions" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e8/21/7e9e523537991d145ab8a0a2fd98548d67646dc2aaaf6091c31ad883e7c1/mypy-1.13.0.tar.gz", hash = "sha256:0291a61b6fbf3e6673e3405cfcc0e7650bebc7939659fdca2702958038bd835e", size = 3152532 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d0/19/de0822609e5b93d02579075248c7aa6ceaddcea92f00bf4ea8e4c22e3598/mypy-1.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:581665e6f3a8a9078f28d5502f4c334c0c8d802ef55ea0e7276a6e409bc0d82d", size = 10939027 },
{ url = "https://files.pythonhosted.org/packages/c8/71/6950fcc6ca84179137e4cbf7cf41e6b68b4a339a1f5d3e954f8c34e02d66/mypy-1.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3ddb5b9bf82e05cc9a627e84707b528e5c7caaa1c55c69e175abb15a761cec2d", size = 10108699 },
{ url = "https://files.pythonhosted.org/packages/26/50/29d3e7dd166e74dc13d46050b23f7d6d7533acf48f5217663a3719db024e/mypy-1.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20c7ee0bc0d5a9595c46f38beb04201f2620065a93755704e141fcac9f59db2b", size = 12506263 },
{ url = "https://files.pythonhosted.org/packages/3f/1d/676e76f07f7d5ddcd4227af3938a9c9640f293b7d8a44dd4ff41d4db25c1/mypy-1.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3790ded76f0b34bc9c8ba4def8f919dd6a46db0f5a6610fb994fe8efdd447f73", size = 12984688 },
{ url = "https://files.pythonhosted.org/packages/9c/03/5a85a30ae5407b1d28fab51bd3e2103e52ad0918d1e68f02a7778669a307/mypy-1.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51f869f4b6b538229c1d1bcc1dd7d119817206e2bc54e8e374b3dfa202defcca", size = 9626811 },
{ url = "https://files.pythonhosted.org/packages/fb/31/c526a7bd2e5c710ae47717c7a5f53f616db6d9097caf48ad650581e81748/mypy-1.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5c7051a3461ae84dfb5dd15eff5094640c61c5f22257c8b766794e6dd85e72d5", size = 11077900 },
{ url = "https://files.pythonhosted.org/packages/83/67/b7419c6b503679d10bd26fc67529bc6a1f7a5f220bbb9f292dc10d33352f/mypy-1.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:39bb21c69a5d6342f4ce526e4584bc5c197fd20a60d14a8624d8743fffb9472e", size = 10074818 },
{ url = "https://files.pythonhosted.org/packages/ba/07/37d67048786ae84e6612575e173d713c9a05d0ae495dde1e68d972207d98/mypy-1.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:164f28cb9d6367439031f4c81e84d3ccaa1e19232d9d05d37cb0bd880d3f93c2", size = 12589275 },
{ url = "https://files.pythonhosted.org/packages/1f/17/b1018c6bb3e9f1ce3956722b3bf91bff86c1cefccca71cec05eae49d6d41/mypy-1.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a4c1bfcdbce96ff5d96fc9b08e3831acb30dc44ab02671eca5953eadad07d6d0", size = 13037783 },
{ url = "https://files.pythonhosted.org/packages/cb/32/cd540755579e54a88099aee0287086d996f5a24281a673f78a0e14dba150/mypy-1.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0affb3a79a256b4183ba09811e3577c5163ed06685e4d4b46429a271ba174d2", size = 9726197 },
{ url = "https://files.pythonhosted.org/packages/11/bb/ab4cfdc562cad80418f077d8be9b4491ee4fb257440da951b85cbb0a639e/mypy-1.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a7b44178c9760ce1a43f544e595d35ed61ac2c3de306599fa59b38a6048e1aa7", size = 11069721 },
{ url = "https://files.pythonhosted.org/packages/59/3b/a393b1607cb749ea2c621def5ba8c58308ff05e30d9dbdc7c15028bca111/mypy-1.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d5092efb8516d08440e36626f0153b5006d4088c1d663d88bf79625af3d1d62", size = 10063996 },
{ url = "https://files.pythonhosted.org/packages/d1/1f/6b76be289a5a521bb1caedc1f08e76ff17ab59061007f201a8a18cc514d1/mypy-1.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2904956dac40ced10931ac967ae63c5089bd498542194b436eb097a9f77bc8", size = 12584043 },
{ url = "https://files.pythonhosted.org/packages/a6/83/5a85c9a5976c6f96e3a5a7591aa28b4a6ca3a07e9e5ba0cec090c8b596d6/mypy-1.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:7bfd8836970d33c2105562650656b6846149374dc8ed77d98424b40b09340ba7", size = 13036996 },
{ url = "https://files.pythonhosted.org/packages/b4/59/c39a6f752f1f893fccbcf1bdd2aca67c79c842402b5283563d006a67cf76/mypy-1.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:9f73dba9ec77acb86457a8fc04b5239822df0c14a082564737833d2963677dbc", size = 9737709 },
{ url = "https://files.pythonhosted.org/packages/3b/86/72ce7f57431d87a7ff17d442f521146a6585019eb8f4f31b7c02801f78ad/mypy-1.13.0-py3-none-any.whl", hash = "sha256:9c250883f9fd81d212e0952c92dbfcc96fc237f4b7c92f56ac81fd48460b3e5a", size = 2647043 },
]
[[package]]
name = "mypy-extensions"
version = "1.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 },
]
[[package]]
name = "packaging"
version = "24.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 },
]
[[package]]
name = "paginate"
version = "0.5.7"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ec/46/68dde5b6bc00c1296ec6466ab27dddede6aec9af1b99090e1107091b3b84/paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945", size = 19252 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746 },
]
[[package]]
name = "pathspec"
version = "0.12.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191 },
]
[[package]]
name = "platformdirs"
version = "4.3.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439 },
]
[[package]]
name = "pluggy"
version = "1.5.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 },
]
[[package]]
name = "pydantic"
version = "2.9.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "annotated-types" },
{ name = "pydantic-core" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a9/b7/d9e3f12af310e1120c21603644a1cd86f59060e040ec5c3a80b8f05fae30/pydantic-2.9.2.tar.gz", hash = "sha256:d155cef71265d1e9807ed1c32b4c8deec042a44a50a4188b25ac67ecd81a9c0f", size = 769917 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/df/e4/ba44652d562cbf0bf320e0f3810206149c8a4e99cdbf66da82e97ab53a15/pydantic-2.9.2-py3-none-any.whl", hash = "sha256:f048cec7b26778210e28a0459867920654d48e5e62db0958433636cde4254f12", size = 434928 },
]
[[package]]
name = "pydantic-core"
version = "2.23.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e2/aa/6b6a9b9f8537b872f552ddd46dd3da230367754b6f707b8e1e963f515ea3/pydantic_core-2.23.4.tar.gz", hash = "sha256:2584f7cf844ac4d970fba483a717dbe10c1c1c96a969bf65d61ffe94df1b2863", size = 402156 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5d/30/890a583cd3f2be27ecf32b479d5d615710bb926d92da03e3f7838ff3e58b/pydantic_core-2.23.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:77733e3892bb0a7fa797826361ce8a9184d25c8dffaec60b7ffe928153680ba8", size = 1865160 },
{ url = "https://files.pythonhosted.org/packages/1d/9a/b634442e1253bc6889c87afe8bb59447f106ee042140bd57680b3b113ec7/pydantic_core-2.23.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b84d168f6c48fabd1f2027a3d1bdfe62f92cade1fb273a5d68e621da0e44e6d", size = 1776777 },
{ url = "https://files.pythonhosted.org/packages/75/9a/7816295124a6b08c24c96f9ce73085032d8bcbaf7e5a781cd41aa910c891/pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df49e7a0861a8c36d089c1ed57d308623d60416dab2647a4a17fe050ba85de0e", size = 1799244 },
{ url = "https://files.pythonhosted.org/packages/a9/8f/89c1405176903e567c5f99ec53387449e62f1121894aa9fc2c4fdc51a59b/pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ff02b6d461a6de369f07ec15e465a88895f3223eb75073ffea56b84d9331f607", size = 1805307 },
{ url = "https://files.pythonhosted.org/packages/d5/a5/1a194447d0da1ef492e3470680c66048fef56fc1f1a25cafbea4bc1d1c48/pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:996a38a83508c54c78a5f41456b0103c30508fed9abcad0a59b876d7398f25fd", size = 2000663 },
{ url = "https://files.pythonhosted.org/packages/13/a5/1df8541651de4455e7d587cf556201b4f7997191e110bca3b589218745a5/pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d97683ddee4723ae8c95d1eddac7c192e8c552da0c73a925a89fa8649bf13eea", size = 2655941 },
{ url = "https://files.pythonhosted.org/packages/44/31/a3899b5ce02c4316865e390107f145089876dff7e1dfc770a231d836aed8/pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:216f9b2d7713eb98cb83c80b9c794de1f6b7e3145eef40400c62e86cee5f4e1e", size = 2052105 },
{ url = "https://files.pythonhosted.org/packages/1b/aa/98e190f8745d5ec831f6d5449344c48c0627ac5fed4e5340a44b74878f8e/pydantic_core-2.23.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6f783e0ec4803c787bcea93e13e9932edab72068f68ecffdf86a99fd5918878b", size = 1919967 },
{ url = "https://files.pythonhosted.org/packages/ae/35/b6e00b6abb2acfee3e8f85558c02a0822e9a8b2f2d812ea8b9079b118ba0/pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d0776dea117cf5272382634bd2a5c1b6eb16767c223c6a5317cd3e2a757c61a0", size = 1964291 },
{ url = "https://files.pythonhosted.org/packages/13/46/7bee6d32b69191cd649bbbd2361af79c472d72cb29bb2024f0b6e350ba06/pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d5f7a395a8cf1621939692dba2a6b6a830efa6b3cee787d82c7de1ad2930de64", size = 2109666 },
{ url = "https://files.pythonhosted.org/packages/39/ef/7b34f1b122a81b68ed0a7d0e564da9ccdc9a2924c8d6c6b5b11fa3a56970/pydantic_core-2.23.4-cp311-none-win32.whl", hash = "sha256:74b9127ffea03643e998e0c5ad9bd3811d3dac8c676e47db17b0ee7c3c3bf35f", size = 1732940 },
{ url = "https://files.pythonhosted.org/packages/2f/76/37b7e76c645843ff46c1d73e046207311ef298d3f7b2f7d8f6ac60113071/pydantic_core-2.23.4-cp311-none-win_amd64.whl", hash = "sha256:98d134c954828488b153d88ba1f34e14259284f256180ce659e8d83e9c05eaa3", size = 1916804 },
{ url = "https://files.pythonhosted.org/packages/74/7b/8e315f80666194b354966ec84b7d567da77ad927ed6323db4006cf915f3f/pydantic_core-2.23.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f3e0da4ebaef65158d4dfd7d3678aad692f7666877df0002b8a522cdf088f231", size = 1856459 },
{ url = "https://files.pythonhosted.org/packages/14/de/866bdce10ed808323d437612aca1ec9971b981e1c52e5e42ad9b8e17a6f6/pydantic_core-2.23.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f69a8e0b033b747bb3e36a44e7732f0c99f7edd5cea723d45bc0d6e95377ffee", size = 1770007 },
{ url = "https://files.pythonhosted.org/packages/dc/69/8edd5c3cd48bb833a3f7ef9b81d7666ccddd3c9a635225214e044b6e8281/pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:723314c1d51722ab28bfcd5240d858512ffd3116449c557a1336cbe3919beb87", size = 1790245 },
{ url = "https://files.pythonhosted.org/packages/80/33/9c24334e3af796ce80d2274940aae38dd4e5676298b4398eff103a79e02d/pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb2802e667b7051a1bebbfe93684841cc9351004e2badbd6411bf357ab8d5ac8", size = 1801260 },
{ url = "https://files.pythonhosted.org/packages/a5/6f/e9567fd90104b79b101ca9d120219644d3314962caa7948dd8b965e9f83e/pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d18ca8148bebe1b0a382a27a8ee60350091a6ddaf475fa05ef50dc35b5df6327", size = 1996872 },
{ url = "https://files.pythonhosted.org/packages/2d/ad/b5f0fe9e6cfee915dd144edbd10b6e9c9c9c9d7a56b69256d124b8ac682e/pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33e3d65a85a2a4a0dc3b092b938a4062b1a05f3a9abde65ea93b233bca0e03f2", size = 2661617 },
{ url = "https://files.pythonhosted.org/packages/06/c8/7d4b708f8d05a5cbfda3243aad468052c6e99de7d0937c9146c24d9f12e9/pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:128585782e5bfa515c590ccee4b727fb76925dd04a98864182b22e89a4e6ed36", size = 2071831 },
{ url = "https://files.pythonhosted.org/packages/89/4d/3079d00c47f22c9a9a8220db088b309ad6e600a73d7a69473e3a8e5e3ea3/pydantic_core-2.23.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:68665f4c17edcceecc112dfed5dbe6f92261fb9d6054b47d01bf6371a6196126", size = 1917453 },
{ url = "https://files.pythonhosted.org/packages/e9/88/9df5b7ce880a4703fcc2d76c8c2d8eb9f861f79d0c56f4b8f5f2607ccec8/pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:20152074317d9bed6b7a95ade3b7d6054845d70584216160860425f4fbd5ee9e", size = 1968793 },
{ url = "https://files.pythonhosted.org/packages/e3/b9/41f7efe80f6ce2ed3ee3c2dcfe10ab7adc1172f778cc9659509a79518c43/pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9261d3ce84fa1d38ed649c3638feefeae23d32ba9182963e465d58d62203bd24", size = 2116872 },
{ url = "https://files.pythonhosted.org/packages/63/08/b59b7a92e03dd25554b0436554bf23e7c29abae7cce4b1c459cd92746811/pydantic_core-2.23.4-cp312-none-win32.whl", hash = "sha256:4ba762ed58e8d68657fc1281e9bb72e1c3e79cc5d464be146e260c541ec12d84", size = 1738535 },
{ url = "https://files.pythonhosted.org/packages/88/8d/479293e4d39ab409747926eec4329de5b7129beaedc3786eca070605d07f/pydantic_core-2.23.4-cp312-none-win_amd64.whl", hash = "sha256:97df63000f4fea395b2824da80e169731088656d1818a11b95f3b173747b6cd9", size = 1917992 },
{ url = "https://files.pythonhosted.org/packages/ad/ef/16ee2df472bf0e419b6bc68c05bf0145c49247a1095e85cee1463c6a44a1/pydantic_core-2.23.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7530e201d10d7d14abce4fb54cfe5b94a0aefc87da539d0346a484ead376c3cc", size = 1856143 },
{ url = "https://files.pythonhosted.org/packages/da/fa/bc3dbb83605669a34a93308e297ab22be82dfb9dcf88c6cf4b4f264e0a42/pydantic_core-2.23.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df933278128ea1cd77772673c73954e53a1c95a4fdf41eef97c2b779271bd0bd", size = 1770063 },
{ url = "https://files.pythonhosted.org/packages/4e/48/e813f3bbd257a712303ebdf55c8dc46f9589ec74b384c9f652597df3288d/pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cb3da3fd1b6a5d0279a01877713dbda118a2a4fc6f0d821a57da2e464793f05", size = 1790013 },
{ url = "https://files.pythonhosted.org/packages/b4/e0/56eda3a37929a1d297fcab1966db8c339023bcca0b64c5a84896db3fcc5c/pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c6dcb030aefb668a2b7009c85b27f90e51e6a3b4d5c9bc4c57631292015b0d", size = 1801077 },
{ url = "https://files.pythonhosted.org/packages/04/be/5e49376769bfbf82486da6c5c1683b891809365c20d7c7e52792ce4c71f3/pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:696dd8d674d6ce621ab9d45b205df149399e4bb9aa34102c970b721554828510", size = 1996782 },
{ url = "https://files.pythonhosted.org/packages/bc/24/e3ee6c04f1d58cc15f37bcc62f32c7478ff55142b7b3e6d42ea374ea427c/pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2971bb5ffe72cc0f555c13e19b23c85b654dd2a8f7ab493c262071377bfce9f6", size = 2661375 },
{ url = "https://files.pythonhosted.org/packages/c1/f8/11a9006de4e89d016b8de74ebb1db727dc100608bb1e6bbe9d56a3cbbcce/pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8394d940e5d400d04cad4f75c0598665cbb81aecefaca82ca85bd28264af7f9b", size = 2071635 },
{ url = "https://files.pythonhosted.org/packages/7c/45/bdce5779b59f468bdf262a5bc9eecbae87f271c51aef628d8c073b4b4b4c/pydantic_core-2.23.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0dff76e0602ca7d4cdaacc1ac4c005e0ce0dcfe095d5b5259163a80d3a10d327", size = 1916994 },
{ url = "https://files.pythonhosted.org/packages/d8/fa/c648308fe711ee1f88192cad6026ab4f925396d1293e8356de7e55be89b5/pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7d32706badfe136888bdea71c0def994644e09fff0bfe47441deaed8e96fdbc6", size = 1968877 },
{ url = "https://files.pythonhosted.org/packages/16/16/b805c74b35607d24d37103007f899abc4880923b04929547ae68d478b7f4/pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed541d70698978a20eb63d8c5d72f2cc6d7079d9d90f6b50bad07826f1320f5f", size = 2116814 },
{ url = "https://files.pythonhosted.org/packages/d1/58/5305e723d9fcdf1c5a655e6a4cc2a07128bf644ff4b1d98daf7a9dbf57da/pydantic_core-2.23.4-cp313-none-win32.whl", hash = "sha256:3d5639516376dce1940ea36edf408c554475369f5da2abd45d44621cb616f769", size = 1738360 },
{ url = "https://files.pythonhosted.org/packages/a5/ae/e14b0ff8b3f48e02394d8acd911376b7b66e164535687ef7dc24ea03072f/pydantic_core-2.23.4-cp313-none-win_amd64.whl", hash = "sha256:5a1504ad17ba4210df3a045132a7baeeba5a200e930f57512ee02909fc5c4cb5", size = 1919411 },
]
[[package]]
name = "pygments"
version = "2.18.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/8e/62/8336eff65bcbc8e4cb5d05b55faf041285951b6e80f33e2bff2024788f31/pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199", size = 4891905 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f7/3f/01c8b82017c199075f8f788d0d906b9ffbbc5a47dc9918a945e13d5a2bda/pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a", size = 1205513 },
]
[[package]]
name = "pymdown-extensions"
version = "10.12"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markdown" },
{ name = "pyyaml" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d8/0b/32f05854cfd432e9286bb41a870e0d1a926b72df5f5cdb6dec962b2e369e/pymdown_extensions-10.12.tar.gz", hash = "sha256:b0ee1e0b2bef1071a47891ab17003bfe5bf824a398e13f49f8ed653b699369a7", size = 840790 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/53/32/95a164ddf533bd676cbbe878e36e89b4ade3efde8dd61d0148c90cbbe57e/pymdown_extensions-10.12-py3-none-any.whl", hash = "sha256:49f81412242d3527b8b4967b990df395c89563043bc51a3d2d7d500e52123b77", size = 263448 },
]
[[package]]
name = "pytest"
version = "8.3.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "iniconfig" },
{ name = "packaging" },
{ name = "pluggy" },
]
sdist = { url = "https://files.pythonhosted.org/packages/8b/6c/62bbd536103af674e227c41a8f3dcd022d591f6eed5facb5a0f31ee33bbc/pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181", size = 1442487 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6b/77/7440a06a8ead44c7757a64362dd22df5760f9b12dc5f11b6188cd2fc27a0/pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2", size = 342341 },
]
[[package]]
name = "python-dateutil"
version = "2.9.0.post0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "six" },
]
sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 },
]
[[package]]
name = "pyyaml"
version = "6.0.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612 },
{ url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040 },
{ url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829 },
{ url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167 },
{ url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952 },
{ url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301 },
{ url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638 },
{ url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850 },
{ url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980 },
{ url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 },
{ url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 },
{ url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 },
{ url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 },
{ url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 },
{ url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 },
{ url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 },
{ url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 },
{ url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 },
{ url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 },
{ url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 },
{ url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 },
{ url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 },
{ url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 },
{ url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 },
{ url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 },
{ url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 },
{ url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 },
]
[[package]]
name = "pyyaml-env-tag"
version = "0.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pyyaml" },
]
sdist = { url = "https://files.pythonhosted.org/packages/fb/8e/da1c6c58f751b70f8ceb1eb25bc25d524e8f14fe16edcce3f4e3ba08629c/pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb", size = 5631 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5a/66/bbb1dd374f5c870f59c5bb1db0e18cbe7fa739415a24cbd95b2d1f5ae0c4/pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069", size = 3911 },
]
[[package]]
name = "regex"
version = "2024.11.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/8e/5f/bd69653fbfb76cf8604468d3b4ec4c403197144c7bfe0e6a5fc9e02a07cb/regex-2024.11.6.tar.gz", hash = "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519", size = 399494 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/58/58/7e4d9493a66c88a7da6d205768119f51af0f684fe7be7bac8328e217a52c/regex-2024.11.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5478c6962ad548b54a591778e93cd7c456a7a29f8eca9c49e4f9a806dcc5d638", size = 482669 },
{ url = "https://files.pythonhosted.org/packages/34/4c/8f8e631fcdc2ff978609eaeef1d6994bf2f028b59d9ac67640ed051f1218/regex-2024.11.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c89a8cc122b25ce6945f0423dc1352cb9593c68abd19223eebbd4e56612c5b7", size = 287684 },
{ url = "https://files.pythonhosted.org/packages/c5/1b/f0e4d13e6adf866ce9b069e191f303a30ab1277e037037a365c3aad5cc9c/regex-2024.11.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:94d87b689cdd831934fa3ce16cc15cd65748e6d689f5d2b8f4f4df2065c9fa20", size = 284589 },
{ url = "https://files.pythonhosted.org/packages/25/4d/ab21047f446693887f25510887e6820b93f791992994f6498b0318904d4a/regex-2024.11.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1062b39a0a2b75a9c694f7a08e7183a80c63c0d62b301418ffd9c35f55aaa114", size = 792121 },
{ url = "https://files.pythonhosted.org/packages/45/ee/c867e15cd894985cb32b731d89576c41a4642a57850c162490ea34b78c3b/regex-2024.11.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:167ed4852351d8a750da48712c3930b031f6efdaa0f22fa1933716bfcd6bf4a3", size = 831275 },
{ url = "https://files.pythonhosted.org/packages/b3/12/b0f480726cf1c60f6536fa5e1c95275a77624f3ac8fdccf79e6727499e28/regex-2024.11.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d548dafee61f06ebdb584080621f3e0c23fff312f0de1afc776e2a2ba99a74f", size = 818257 },
{ url = "https://files.pythonhosted.org/packages/bf/ce/0d0e61429f603bac433910d99ef1a02ce45a8967ffbe3cbee48599e62d88/regex-2024.11.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a19f302cd1ce5dd01a9099aaa19cae6173306d1302a43b627f62e21cf18ac0", size = 792727 },
{ url = "https://files.pythonhosted.org/packages/e4/c1/243c83c53d4a419c1556f43777ccb552bccdf79d08fda3980e4e77dd9137/regex-2024.11.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bec9931dfb61ddd8ef2ebc05646293812cb6b16b60cf7c9511a832b6f1854b55", size = 780667 },
{ url = "https://files.pythonhosted.org/packages/c5/f4/75eb0dd4ce4b37f04928987f1d22547ddaf6c4bae697623c1b05da67a8aa/regex-2024.11.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9714398225f299aa85267fd222f7142fcb5c769e73d7733344efc46f2ef5cf89", size = 776963 },
{ url = "https://files.pythonhosted.org/packages/16/5d/95c568574e630e141a69ff8a254c2f188b4398e813c40d49228c9bbd9875/regex-2024.11.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:202eb32e89f60fc147a41e55cb086db2a3f8cb82f9a9a88440dcfc5d37faae8d", size = 784700 },
{ url = "https://files.pythonhosted.org/packages/8e/b5/f8495c7917f15cc6fee1e7f395e324ec3e00ab3c665a7dc9d27562fd5290/regex-2024.11.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:4181b814e56078e9b00427ca358ec44333765f5ca1b45597ec7446d3a1ef6e34", size = 848592 },
{ url = "https://files.pythonhosted.org/packages/1c/80/6dd7118e8cb212c3c60b191b932dc57db93fb2e36fb9e0e92f72a5909af9/regex-2024.11.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:068376da5a7e4da51968ce4c122a7cd31afaaec4fccc7856c92f63876e57b51d", size = 852929 },
{ url = "https://files.pythonhosted.org/packages/11/9b/5a05d2040297d2d254baf95eeeb6df83554e5e1df03bc1a6687fc4ba1f66/regex-2024.11.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ac10f2c4184420d881a3475fb2c6f4d95d53a8d50209a2500723d831036f7c45", size = 781213 },
{ url = "https://files.pythonhosted.org/packages/26/b7/b14e2440156ab39e0177506c08c18accaf2b8932e39fb092074de733d868/regex-2024.11.6-cp311-cp311-win32.whl", hash = "sha256:c36f9b6f5f8649bb251a5f3f66564438977b7ef8386a52460ae77e6070d309d9", size = 261734 },
{ url = "https://files.pythonhosted.org/packages/80/32/763a6cc01d21fb3819227a1cc3f60fd251c13c37c27a73b8ff4315433a8e/regex-2024.11.6-cp311-cp311-win_amd64.whl", hash = "sha256:02e28184be537f0e75c1f9b2f8847dc51e08e6e171c6bde130b2687e0c33cf60", size = 274052 },
{ url = "https://files.pythonhosted.org/packages/ba/30/9a87ce8336b172cc232a0db89a3af97929d06c11ceaa19d97d84fa90a8f8/regex-2024.11.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:52fb28f528778f184f870b7cf8f225f5eef0a8f6e3778529bdd40c7b3920796a", size = 483781 },
{ url = "https://files.pythonhosted.org/packages/01/e8/00008ad4ff4be8b1844786ba6636035f7ef926db5686e4c0f98093612add/regex-2024.11.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdd6028445d2460f33136c55eeb1f601ab06d74cb3347132e1c24250187500d9", size = 288455 },
{ url = "https://files.pythonhosted.org/packages/60/85/cebcc0aff603ea0a201667b203f13ba75d9fc8668fab917ac5b2de3967bc/regex-2024.11.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:805e6b60c54bf766b251e94526ebad60b7de0c70f70a4e6210ee2891acb70bf2", size = 284759 },
{ url = "https://files.pythonhosted.org/packages/94/2b/701a4b0585cb05472a4da28ee28fdfe155f3638f5e1ec92306d924e5faf0/regex-2024.11.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b85c2530be953a890eaffde05485238f07029600e8f098cdf1848d414a8b45e4", size = 794976 },
{ url = "https://files.pythonhosted.org/packages/4b/bf/fa87e563bf5fee75db8915f7352e1887b1249126a1be4813837f5dbec965/regex-2024.11.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb26437975da7dc36b7efad18aa9dd4ea569d2357ae6b783bf1118dabd9ea577", size = 833077 },
{ url = "https://files.pythonhosted.org/packages/a1/56/7295e6bad94b047f4d0834e4779491b81216583c00c288252ef625c01d23/regex-2024.11.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abfa5080c374a76a251ba60683242bc17eeb2c9818d0d30117b4486be10c59d3", size = 823160 },
{ url = "https://files.pythonhosted.org/packages/fb/13/e3b075031a738c9598c51cfbc4c7879e26729c53aa9cca59211c44235314/regex-2024.11.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b7fa6606c2881c1db9479b0eaa11ed5dfa11c8d60a474ff0e095099f39d98e", size = 796896 },
{ url = "https://files.pythonhosted.org/packages/24/56/0b3f1b66d592be6efec23a795b37732682520b47c53da5a32c33ed7d84e3/regex-2024.11.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c32f75920cf99fe6b6c539c399a4a128452eaf1af27f39bce8909c9a3fd8cbe", size = 783997 },
{ url = "https://files.pythonhosted.org/packages/f9/a1/eb378dada8b91c0e4c5f08ffb56f25fcae47bf52ad18f9b2f33b83e6d498/regex-2024.11.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:982e6d21414e78e1f51cf595d7f321dcd14de1f2881c5dc6a6e23bbbbd68435e", size = 781725 },
{ url = "https://files.pythonhosted.org/packages/83/f2/033e7dec0cfd6dda93390089864732a3409246ffe8b042e9554afa9bff4e/regex-2024.11.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a7c2155f790e2fb448faed6dd241386719802296ec588a8b9051c1f5c481bc29", size = 789481 },
{ url = "https://files.pythonhosted.org/packages/83/23/15d4552ea28990a74e7696780c438aadd73a20318c47e527b47a4a5a596d/regex-2024.11.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:149f5008d286636e48cd0b1dd65018548944e495b0265b45e1bffecce1ef7f39", size = 852896 },
{ url = "https://files.pythonhosted.org/packages/e3/39/ed4416bc90deedbfdada2568b2cb0bc1fdb98efe11f5378d9892b2a88f8f/regex-2024.11.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e5364a4502efca094731680e80009632ad6624084aff9a23ce8c8c6820de3e51", size = 860138 },
{ url = "https://files.pythonhosted.org/packages/93/2d/dd56bb76bd8e95bbce684326302f287455b56242a4f9c61f1bc76e28360e/regex-2024.11.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0a86e7eeca091c09e021db8eb72d54751e527fa47b8d5787caf96d9831bd02ad", size = 787692 },
{ url = "https://files.pythonhosted.org/packages/0b/55/31877a249ab7a5156758246b9c59539abbeba22461b7d8adc9e8475ff73e/regex-2024.11.6-cp312-cp312-win32.whl", hash = "sha256:32f9a4c643baad4efa81d549c2aadefaeba12249b2adc5af541759237eee1c54", size = 262135 },
{ url = "https://files.pythonhosted.org/packages/38/ec/ad2d7de49a600cdb8dd78434a1aeffe28b9d6fc42eb36afab4a27ad23384/regex-2024.11.6-cp312-cp312-win_amd64.whl", hash = "sha256:a93c194e2df18f7d264092dc8539b8ffb86b45b899ab976aa15d48214138e81b", size = 273567 },
{ url = "https://files.pythonhosted.org/packages/90/73/bcb0e36614601016552fa9344544a3a2ae1809dc1401b100eab02e772e1f/regex-2024.11.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a6ba92c0bcdf96cbf43a12c717eae4bc98325ca3730f6b130ffa2e3c3c723d84", size = 483525 },
{ url = "https://files.pythonhosted.org/packages/0f/3f/f1a082a46b31e25291d830b369b6b0c5576a6f7fb89d3053a354c24b8a83/regex-2024.11.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:525eab0b789891ac3be914d36893bdf972d483fe66551f79d3e27146191a37d4", size = 288324 },
{ url = "https://files.pythonhosted.org/packages/09/c9/4e68181a4a652fb3ef5099e077faf4fd2a694ea6e0f806a7737aff9e758a/regex-2024.11.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:086a27a0b4ca227941700e0b31425e7a28ef1ae8e5e05a33826e17e47fbfdba0", size = 284617 },
{ url = "https://files.pythonhosted.org/packages/fc/fd/37868b75eaf63843165f1d2122ca6cb94bfc0271e4428cf58c0616786dce/regex-2024.11.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bde01f35767c4a7899b7eb6e823b125a64de314a8ee9791367c9a34d56af18d0", size = 795023 },
{ url = "https://files.pythonhosted.org/packages/c4/7c/d4cd9c528502a3dedb5c13c146e7a7a539a3853dc20209c8e75d9ba9d1b2/regex-2024.11.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b583904576650166b3d920d2bcce13971f6f9e9a396c673187f49811b2769dc7", size = 833072 },
{ url = "https://files.pythonhosted.org/packages/4f/db/46f563a08f969159c5a0f0e722260568425363bea43bb7ae370becb66a67/regex-2024.11.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c4de13f06a0d54fa0d5ab1b7138bfa0d883220965a29616e3ea61b35d5f5fc7", size = 823130 },
{ url = "https://files.pythonhosted.org/packages/db/60/1eeca2074f5b87df394fccaa432ae3fc06c9c9bfa97c5051aed70e6e00c2/regex-2024.11.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cde6e9f2580eb1665965ce9bf17ff4952f34f5b126beb509fee8f4e994f143c", size = 796857 },
{ url = "https://files.pythonhosted.org/packages/10/db/ac718a08fcee981554d2f7bb8402f1faa7e868c1345c16ab1ebec54b0d7b/regex-2024.11.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0d7f453dca13f40a02b79636a339c5b62b670141e63efd511d3f8f73fba162b3", size = 784006 },
{ url = "https://files.pythonhosted.org/packages/c2/41/7da3fe70216cea93144bf12da2b87367590bcf07db97604edeea55dac9ad/regex-2024.11.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59dfe1ed21aea057a65c6b586afd2a945de04fc7db3de0a6e3ed5397ad491b07", size = 781650 },
{ url = "https://files.pythonhosted.org/packages/a7/d5/880921ee4eec393a4752e6ab9f0fe28009435417c3102fc413f3fe81c4e5/regex-2024.11.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b97c1e0bd37c5cd7902e65f410779d39eeda155800b65fc4d04cc432efa9bc6e", size = 789545 },
{ url = "https://files.pythonhosted.org/packages/dc/96/53770115e507081122beca8899ab7f5ae28ae790bfcc82b5e38976df6a77/regex-2024.11.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f9d1e379028e0fc2ae3654bac3cbbef81bf3fd571272a42d56c24007979bafb6", size = 853045 },
{ url = "https://files.pythonhosted.org/packages/31/d3/1372add5251cc2d44b451bd94f43b2ec78e15a6e82bff6a290ef9fd8f00a/regex-2024.11.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:13291b39131e2d002a7940fb176e120bec5145f3aeb7621be6534e46251912c4", size = 860182 },
{ url = "https://files.pythonhosted.org/packages/ed/e3/c446a64984ea9f69982ba1a69d4658d5014bc7a0ea468a07e1a1265db6e2/regex-2024.11.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f51f88c126370dcec4908576c5a627220da6c09d0bff31cfa89f2523843316d", size = 787733 },
{ url = "https://files.pythonhosted.org/packages/2b/f1/e40c8373e3480e4f29f2692bd21b3e05f296d3afebc7e5dcf21b9756ca1c/regex-2024.11.6-cp313-cp313-win32.whl", hash = "sha256:63b13cfd72e9601125027202cad74995ab26921d8cd935c25f09c630436348ff", size = 262122 },
{ url = "https://files.pythonhosted.org/packages/45/94/bc295babb3062a731f52621cdc992d123111282e291abaf23faa413443ea/regex-2024.11.6-cp313-cp313-win_amd64.whl", hash = "sha256:2b3361af3198667e99927da8b84c1b010752fa4b1115ee30beaa332cabc3ef1a", size = 273545 },
]
[[package]]
name = "requests"
version = "2.32.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "charset-normalizer" },
{ name = "idna" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 },
]
[[package]]
name = "responses"
version = "0.25.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pyyaml" },
{ name = "requests" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/67/24/1d67c8974daa502e860b4a5b57ad6de0d7dbc0b1160ef7148189a24a40e1/responses-0.25.3.tar.gz", hash = "sha256:617b9247abd9ae28313d57a75880422d55ec63c29d33d629697590a034358dba", size = 77798 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/12/24/93293d0be0db9da1ed8dfc5e6af700fdd40e8f10a928704dd179db9f03c1/responses-0.25.3-py3-none-any.whl", hash = "sha256:521efcbc82081ab8daa588e08f7e8a64ce79b91c39f6e62199b19159bea7dbcb", size = 55238 },
]
[[package]]
name = "ruff"
version = "0.7.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/0b/8b/bc4e0dfa1245b07cf14300e10319b98e958a53ff074c1dd86b35253a8c2a/ruff-0.7.4.tar.gz", hash = "sha256:cd12e35031f5af6b9b93715d8c4f40360070b2041f81273d0527683d5708fce2", size = 3275547 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e6/4b/f5094719e254829766b807dadb766841124daba75a37da83e292ae5ad12f/ruff-0.7.4-py3-none-linux_armv6l.whl", hash = "sha256:a4919925e7684a3f18e18243cd6bea7cfb8e968a6eaa8437971f681b7ec51478", size = 10447512 },
{ url = "https://files.pythonhosted.org/packages/9e/1d/3d2d2c9f601cf6044799c5349ff5267467224cefed9b35edf5f1f36486e9/ruff-0.7.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:cfb365c135b830778dda8c04fb7d4280ed0b984e1aec27f574445231e20d6c63", size = 10235436 },
{ url = "https://files.pythonhosted.org/packages/62/83/42a6ec6216ded30b354b13e0e9327ef75a3c147751aaf10443756cb690e9/ruff-0.7.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:63a569b36bc66fbadec5beaa539dd81e0527cb258b94e29e0531ce41bacc1f20", size = 9888936 },
{ url = "https://files.pythonhosted.org/packages/4d/26/e1e54893b13046a6ad05ee9b89ee6f71542ba250f72b4c7a7d17c3dbf73d/ruff-0.7.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d06218747d361d06fd2fdac734e7fa92df36df93035db3dc2ad7aa9852cb109", size = 10697353 },
{ url = "https://files.pythonhosted.org/packages/21/24/98d2e109c4efc02bfef144ec6ea2c3e1217e7ce0cfddda8361d268dfd499/ruff-0.7.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e0cea28d0944f74ebc33e9f934238f15c758841f9f5edd180b5315c203293452", size = 10228078 },
{ url = "https://files.pythonhosted.org/packages/ad/b7/964c75be9bc2945fc3172241b371197bb6d948cc69e28bc4518448c368f3/ruff-0.7.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:80094ecd4793c68b2571b128f91754d60f692d64bc0d7272ec9197fdd09bf9ea", size = 11264823 },
{ url = "https://files.pythonhosted.org/packages/12/8d/20abdbf705969914ce40988fe71a554a918deaab62c38ec07483e77866f6/ruff-0.7.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:997512325c6620d1c4c2b15db49ef59543ef9cd0f4aa8065ec2ae5103cedc7e7", size = 11951855 },
{ url = "https://files.pythonhosted.org/packages/b8/fc/6519ce58c57b4edafcdf40920b7273dfbba64fc6ebcaae7b88e4dc1bf0a8/ruff-0.7.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00b4cf3a6b5fad6d1a66e7574d78956bbd09abfd6c8a997798f01f5da3d46a05", size = 11516580 },
{ url = "https://files.pythonhosted.org/packages/37/1a/5ec1844e993e376a86eb2456496831ed91b4398c434d8244f89094758940/ruff-0.7.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7dbdc7d8274e1422722933d1edddfdc65b4336abf0b16dfcb9dedd6e6a517d06", size = 12692057 },
{ url = "https://files.pythonhosted.org/packages/50/90/76867152b0d3c05df29a74bb028413e90f704f0f6701c4801174ba47f959/ruff-0.7.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e92dfb5f00eaedb1501b2f906ccabfd67b2355bdf117fea9719fc99ac2145bc", size = 11085137 },
{ url = "https://files.pythonhosted.org/packages/c8/eb/0a7cb6059ac3555243bd026bb21785bbc812f7bbfa95a36c101bd72b47ae/ruff-0.7.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3bd726099f277d735dc38900b6a8d6cf070f80828877941983a57bca1cd92172", size = 10681243 },
{ url = "https://files.pythonhosted.org/packages/5e/76/2270719dbee0fd35780b05c08a07b7a726c3da9f67d9ae89ef21fc18e2e5/ruff-0.7.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2e32829c429dd081ee5ba39aef436603e5b22335c3d3fff013cd585806a6486a", size = 10319187 },
{ url = "https://files.pythonhosted.org/packages/9f/e5/39100f72f8ba70bec1bd329efc880dea8b6c1765ea1cb9d0c1c5f18b8d7f/ruff-0.7.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:662a63b4971807623f6f90c1fb664613f67cc182dc4d991471c23c541fee62dd", size = 10803715 },
{ url = "https://files.pythonhosted.org/packages/a5/89/40e904784f305fb56850063f70a998a64ebba68796d823dde67e89a24691/ruff-0.7.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:876f5e09eaae3eb76814c1d3b68879891d6fde4824c015d48e7a7da4cf066a3a", size = 11162912 },
{ url = "https://files.pythonhosted.org/packages/8d/1b/dd77503b3875c51e3dbc053fd8367b845ab8b01c9ca6d0c237082732856c/ruff-0.7.4-py3-none-win32.whl", hash = "sha256:75c53f54904be42dd52a548728a5b572344b50d9b2873d13a3f8c5e3b91f5cac", size = 8702767 },
{ url = "https://files.pythonhosted.org/packages/63/76/253ddc3e89e70165bba952ecca424b980b8d3c2598ceb4fc47904f424953/ruff-0.7.4-py3-none-win_amd64.whl", hash = "sha256:745775c7b39f914238ed1f1b0bebed0b9155a17cd8bc0b08d3c87e4703b990d6", size = 9497534 },
{ url = "https://files.pythonhosted.org/packages/aa/70/f8724f31abc0b329ca98b33d73c14020168babcf71b0cba3cded5d9d0e66/ruff-0.7.4-py3-none-win_arm64.whl", hash = "sha256:11bff065102c3ae9d3ea4dc9ecdfe5a5171349cdd0787c1fc64761212fc9cf1f", size = 8851590 },
]
[[package]]
name = "six"
version = "1.16.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/71/39/171f1c67cd00715f190ba0b100d606d440a28c93c7714febeca8b79af85e/six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", size = 34041 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254", size = 11053 },
]
[[package]]
name = "types-requests"
version = "2.32.0.20241016"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/fa/3c/4f2a430c01a22abd49a583b6b944173e39e7d01b688190a5618bd59a2e22/types-requests-2.32.0.20241016.tar.gz", hash = "sha256:0d9cad2f27515d0e3e3da7134a1b6f28fb97129d86b867f24d9c726452634d95", size = 18065 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d7/01/485b3026ff90e5190b5e24f1711522e06c79f4a56c8f4b95848ac072e20f/types_requests-2.32.0.20241016-py3-none-any.whl", hash = "sha256:4195d62d6d3e043a4eaaf08ff8a62184584d2e8684e9d2aa178c7915a7da3747", size = 15836 },
]
[[package]]
name = "typing-extensions"
version = "4.12.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 },
]
[[package]]
name = "unifi-access"
version = "0.1.0"
source = { editable = "." }
dependencies = [
{ name = "pydantic" },
{ name = "requests" },
]
[package.dev-dependencies]
dev = [
{ name = "mypy" },
{ name = "pytest" },
{ name = "responses" },
{ name = "ruff" },
{ name = "types-requests" },
]
docs = [
{ name = "mkdocs-material" },
{ name = "mkdocstrings", extra = ["python"] },
]
[package.metadata]
requires-dist = [
{ name = "pydantic", specifier = ">=2.9.2" },
{ name = "requests", specifier = ">=2.32.3" },
]
[package.metadata.requires-dev]
dev = [
{ name = "mypy", specifier = ">=1.13" },
{ name = "pytest", specifier = ">=8.3.3" },
{ name = "responses", specifier = ">=0.25.3" },
{ name = "ruff", specifier = ">=0.7.1" },
{ name = "types-requests", specifier = ">=2.32.0.20241016" },
]
docs = [
{ name = "mkdocs-material", specifier = ">=9.5.42" },
{ name = "mkdocstrings", extras = ["python"], specifier = ">=0.26.2" },
]
[[package]]
name = "urllib3"
version = "2.2.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ed/63/22ba4ebfe7430b76388e7cd448d5478814d3032121827c12a2cc287e2260/urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9", size = 300677 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ce/d9/5f4c13cecde62396b0d3fe530a50ccea91e7dfc1ccf0e09c228841bb5ba8/urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", size = 126338 },
]
[[package]]
name = "watchdog"
version = "6.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393 },
{ url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392 },
{ url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019 },
{ url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471 },
{ url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449 },
{ url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054 },
{ url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480 },
{ url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451 },
{ url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057 },
{ url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079 },
{ url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078 },
{ url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076 },
{ url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077 },
{ url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078 },
{ url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077 },
{ url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078 },
{ url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065 },
{ url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070 },
{ url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067 },
]