diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5774b0a4..0704869b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -55,11 +55,6 @@ repos: hooks: - id: biome-check additional_dependencies: ["@biomejs/biome@1.8.3"] - - repo: https://github.com/rbubley/mirrors-prettier - rev: "v3.4.0" - hooks: - - id: prettier - files: \.(html|md|yml|yaml)$ - repo: https://github.com/djlint/djLint rev: v1.36.1 hooks: diff --git a/conftest.py b/conftest.py index 73420b34..f2adf8e8 100644 --- a/conftest.py +++ b/conftest.py @@ -104,14 +104,14 @@ async def test_data(db): rollout_default = await Rollout.create(software_id=software_release.id) device_rollout = await Device.create( - uuid="device1", + id="device1", last_state=UpdateStateEnum.REGISTERED, update_mode=UpdateModeEnum.ROLLOUT, hardware=hardware, ) device_assigned = await Device.create( - uuid="device2", + id="device2", last_state=UpdateStateEnum.REGISTERED, update_mode=UpdateModeEnum.ASSIGNED, assigned_software=software_release, diff --git a/docs/index.md b/docs/index.md index dcb5ed70..fe3cc024 100644 --- a/docs/index.md +++ b/docs/index.md @@ -36,7 +36,7 @@ Assign specific software to a device manually. Once installed, no further update #### 2. Automatic Update to Latest Software -Automatically updates the device to the latest compatible software, based on the reported `hw_model` and `hw_revision`. Note: versions are interpreted as [SemVer](https://semver.org) versions. +Automatically updates the device to the latest compatible software, based on the reported `hw_model` and `hw_revision`. #### 3. Software Rollout diff --git a/goosebit.yaml b/goosebit.yaml index 85bc2824..cac930a2 100644 --- a/goosebit.yaml +++ b/goosebit.yaml @@ -3,6 +3,9 @@ # Port to host the server on, default: #port: 60053 # GOOSE ;) +# Tenant for DDI endpoint +#tenant: DEFAULT + # Database to be used, default: #db_uri: sqlite:////db.sqlite3 #db_ssl_crt: //ca-certificate.crt diff --git a/goosebit/api/v1/devices/routes.py b/goosebit/api/v1/devices/routes.py index 9b035e50..38d17077 100644 --- a/goosebit/api/v1/devices/routes.py +++ b/goosebit/api/v1/devices/routes.py @@ -29,7 +29,7 @@ async def devices_get(_: Request) -> DevicesResponse: response = DevicesResponse(devices=devices) async def set_assigned_sw(d: DeviceSchema): - device = await get_device(d.uuid) + device = await get_device(d.id) _, target = await DeviceManager.get_update(device) if target is not None: await target.fetch_related("compatibility") @@ -54,10 +54,10 @@ async def devices_delete(_: Request, config: DevicesDeleteRequest) -> StatusResp dependencies=[Security(validate_user_permissions, scopes=["device.write"])], ) async def devices_patch(_: Request, config: DevicesPatchRequest) -> StatusResponse: - for uuid in config.devices: - if await Device.get_or_none(uuid=uuid) is None: - raise HTTPException(404, f"Device with UUID {uuid} not found") - device = await DeviceManager.get_device(uuid) + for dev_id in config.devices: + if await Device.get_or_none(id=dev_id) is None: + raise HTTPException(404, f"Device with ID {dev_id} not found") + device = await DeviceManager.get_device(dev_id) if config.software is not None: if config.software == "rollout": await DeviceManager.update_update(device, UpdateModeEnum.ROLLOUT, None) @@ -84,8 +84,8 @@ async def devices_patch(_: Request, config: DevicesPatchRequest) -> StatusRespon dependencies=[Security(validate_user_permissions, scopes=["device.write"])], ) async def devices_put(_: Request, config: DevicesPutRequest) -> StatusResponse: - for uuid in config.devices: - device = await DeviceManager.get_device(uuid) + for dev_id in config.devices: + device = await DeviceManager.get_device(dev_id) if config.software is not None: if config.software == "rollout": await DeviceManager.update_update(device, UpdateModeEnum.ROLLOUT, None) diff --git a/goosebit/db/migrations/models/4_20250402085235_rename_uuid_to_id.py b/goosebit/db/migrations/models/4_20250402085235_rename_uuid_to_id.py new file mode 100644 index 00000000..4647d053 --- /dev/null +++ b/goosebit/db/migrations/models/4_20250402085235_rename_uuid_to_id.py @@ -0,0 +1,11 @@ +from tortoise import BaseDBAsyncClient + + +async def upgrade(db: BaseDBAsyncClient) -> str: + return """ + ALTER TABLE "device" RENAME COLUMN "uuid" TO "id";""" + + +async def downgrade(db: BaseDBAsyncClient) -> str: + return """ + ALTER TABLE "device" RENAME COLUMN "id" TO "uuid";""" diff --git a/goosebit/db/models.py b/goosebit/db/models.py index 2cd763b6..a4c501b6 100644 --- a/goosebit/db/models.py +++ b/goosebit/db/models.py @@ -5,13 +5,12 @@ from urllib.parse import unquote, urlparse from urllib.request import url2pathname -import semver from anyio import Path -from semver import Version from tortoise import Model, fields from tortoise.exceptions import ValidationError from goosebit.api.telemetry.metrics import devices_count +from goosebit.util.version import Version class UpdateModeEnum(IntEnum): @@ -57,7 +56,7 @@ class Tag(Model): class Device(Model): - uuid = fields.CharField(max_length=255, primary_key=True) + id = fields.CharField(max_length=255, primary_key=True) name = fields.CharField(max_length=255, null=True) assigned_software = fields.ForeignKeyField( "models.Software", related_name="assigned_devices", null=True, on_delete=fields.SET_NULL @@ -108,7 +107,7 @@ class Rollout(Model): created_at = fields.DatetimeField(auto_now_add=True) name = fields.CharField(max_length=255, null=True) feed = fields.CharField(max_length=255, default="default") - software = fields.ForeignKeyField("models.Software", related_name="rollouts", index=True) + software = fields.ForeignKeyField("models.Software", related_name="rollouts", db_index=True) paused = fields.BooleanField(default=False) success_count = fields.IntField(default=0) failure_count = fields.IntField(default=0) @@ -134,12 +133,12 @@ class Software(Model): @classmethod async def latest(cls, device: Device) -> Self | None: - updates = await cls.filter(compatibility__devices__uuid=device.uuid) + updates = await cls.filter(compatibility__devices__id=device.id) if len(updates) == 0: return None return sorted( updates, - key=lambda x: semver.Version.parse(x.version, optional_minor_and_patch=True), + key=lambda x: Version.parse(x.version), reverse=True, )[0] @@ -160,4 +159,4 @@ def path_user(self) -> str: @property def parsed_version(self) -> Version: - return semver.Version.parse(self.version, optional_minor_and_patch=True) + return Version.parse(self.version) diff --git a/goosebit/device_manager.py b/goosebit/device_manager.py index 1ca75068..cf6f6f99 100644 --- a/goosebit/device_manager.py +++ b/goosebit/device_manager.py @@ -46,8 +46,8 @@ async def get_device(dev_id: str) -> Device: hardware = (await Hardware.get_or_create(model="default", revision="default"))[0] DeviceManager._hardware_default = hardware - device = (await Device.get_or_create(uuid=dev_id, defaults={"hardware": hardware}))[0] - result = await cache.set(device.uuid, device, ttl=600) + device = (await Device.get_or_create(id=dev_id, defaults={"hardware": hardware}))[0] + result = await cache.set(device.id, device, ttl=600) assert result, "device being cached" return device @@ -57,7 +57,7 @@ async def save_device(device: Device, update_fields: list[str]): await device.save(update_fields=update_fields) # only update cache after a successful database save - result = await caches.get("default").set(device.uuid, device, ttl=600) + result = await caches.get("default").set(device.id, device, ttl=600) assert result, "device being cached" @staticmethod @@ -155,7 +155,7 @@ async def get_rollout(device: Device) -> Rollout | None: await Rollout.filter( feed=device.feed, paused=False, - software__compatibility__devices__uuid=device.uuid, + software__compatibility__devices__id=device.id, ) .order_by("-created_at") .first() @@ -219,7 +219,7 @@ async def update_log(device: Device, log_data: str) -> None: @staticmethod async def delete_devices(ids: list[str]): - await Device.filter(uuid__in=ids).delete() + await Device.filter(id__in=ids).delete() for dev_id in ids: result = await caches.get("default").delete(dev_id) assert result == 1, "device has been cached" diff --git a/goosebit/schema/devices.py b/goosebit/schema/devices.py index 5d8e5123..fd4b21a2 100644 --- a/goosebit/schema/devices.py +++ b/goosebit/schema/devices.py @@ -30,7 +30,7 @@ def enum_factory(name: str, base: type[Enum]) -> type[ConvertableEnum]: class DeviceSchema(BaseModel): model_config = ConfigDict(from_attributes=True) - uuid: str + id: str name: str | None sw_version: str | None diff --git a/goosebit/settings/schema.py b/goosebit/settings/schema.py index f134f64f..8f69a2f0 100644 --- a/goosebit/settings/schema.py +++ b/goosebit/settings/schema.py @@ -48,6 +48,7 @@ class GooseBitSettings(BaseSettings): model_config = SettingsConfigDict(env_prefix="GOOSEBIT_") port: int = 60053 # GOOSE + tenant: str = "DEFAULT" poll_time: str = "00:01:00" diff --git a/goosebit/ui/bff/common/columns.py b/goosebit/ui/bff/common/columns.py index 64f30651..49870d24 100644 --- a/goosebit/ui/bff/common/columns.py +++ b/goosebit/ui/bff/common/columns.py @@ -2,7 +2,7 @@ class DeviceColumns: - uuid = DTColumnDescription(title="UUID", data="uuid", name="uuid", searchable=True, orderable=True) + id = DTColumnDescription(title="ID", data="id", name="id", searchable=True, orderable=True) name = DTColumnDescription(title="Name", data="name", name="name", searchable=True, orderable=True) hw_model = DTColumnDescription(title="Model", data="hw_model") hw_revision = DTColumnDescription(title="Revision", data="hw_revision") diff --git a/goosebit/ui/bff/devices/routes.py b/goosebit/ui/bff/devices/routes.py index 9156a616..6aa0c563 100644 --- a/goosebit/ui/bff/devices/routes.py +++ b/goosebit/ui/bff/devices/routes.py @@ -34,7 +34,7 @@ async def devices_get(dt_query: Annotated[DataTableRequest, Depends(parse_datatables_query)]) -> BFFDeviceResponse: def search_filter(search_value: str): return ( - Q(uuid__icontains=search_value) + Q(id__icontains=search_value) | Q(name__icontains=search_value) | Q(feed__icontains=search_value) | Q(sw_version__icontains=search_value) @@ -47,7 +47,7 @@ def search_filter(search_value: str): response = await BFFDeviceResponse.convert(dt_query, query, search_filter) async def set_assigned_sw(d: DeviceSchema): - device = await get_device(d.uuid) + device = await get_device(d.id) _, target = await DeviceManager.get_update(device) if target is not None: await target.fetch_related("compatibility") @@ -63,10 +63,10 @@ async def set_assigned_sw(d: DeviceSchema): dependencies=[Security(validate_user_permissions, scopes=["device.write"])], ) async def devices_patch(_: Request, config: DevicesPatchRequest) -> StatusResponse: - for uuid in config.devices: - if await Device.get_or_none(uuid=uuid) is None: - raise HTTPException(404, f"Device with UUID {uuid} not found") - device = await get_device(uuid) + for dev_id in config.devices: + if await Device.get_or_none(id=dev_id) is None: + raise HTTPException(404, f"Device with ID {dev_id} not found") + device = await get_device(dev_id) if config.software is not None: if config.software == "rollout": await DeviceManager.update_update(device, UpdateModeEnum.ROLLOUT, None) @@ -108,7 +108,7 @@ async def devices_get_columns(request: Request) -> DTColumns: filter( None, [ - DeviceColumns.uuid, + DeviceColumns.id, DeviceColumns.name, DeviceColumns.hw_model, DeviceColumns.hw_revision, diff --git a/goosebit/ui/bff/software/routes.py b/goosebit/ui/bff/software/routes.py index e38227f9..446b46fd 100644 --- a/goosebit/ui/bff/software/routes.py +++ b/goosebit/ui/bff/software/routes.py @@ -28,7 +28,7 @@ ) async def software_get( dt_query: Annotated[DataTableRequest, Depends(parse_datatables_query)], - uuids: list[str] = Query(default=None), + ids: list[str] = Query(default=None), ) -> BFFSoftwareResponse: filters: list[Q] = [] @@ -38,8 +38,8 @@ def search_filter(search_value): query = Software.all().prefetch_related("compatibility") - if uuids: - hardware = await Hardware.filter(devices__uuid__in=uuids).distinct() + if ids: + hardware = await Hardware.filter(devices__id__in=ids).distinct() filters.append(Q(*[Q(compatibility__id=c.id) for c in hardware], join_type="AND")) return await BFFSoftwareResponse.convert(dt_query, query, search_filter, Q(*filters)) diff --git a/goosebit/ui/static/js/devices.js b/goosebit/ui/static/js/devices.js index 676b6223..dafa594b 100644 --- a/goosebit/ui/static/js/devices.js +++ b/goosebit/ui/static/js/devices.js @@ -44,13 +44,13 @@ document.addEventListener("DOMContentLoaded", async () => { paging: true, processing: false, serverSide: true, - order: { name: "uuid", dir: "asc" }, + order: { name: "id", dir: "asc" }, scrollCollapse: true, scroller: true, scrollY: "65vh", stateSave: true, select: true, - rowId: "uuid", + rowId: "id", ajax: { url: "/ui/bff/devices", data: (data) => { @@ -88,7 +88,7 @@ document.addEventListener("DOMContentLoaded", async () => { text: '', action: () => { const selectedDevice = dataTable.rows({ selected: true }).data().toArray()[0]; - window.location.href = `/ui/logs/${selectedDevice.uuid}`; + window.location.href = `/ui/logs/${selectedDevice.id}`; }, className: "buttons-logs", titleAttr: "View Log", @@ -117,7 +117,7 @@ document.addEventListener("DOMContentLoaded", async () => { .rows({ selected: true }) .data() .toArray() - .map((d) => d.uuid); + .map((d) => d.id); await deleteDevices(selectedDevices); }, className: "buttons-delete", @@ -130,7 +130,7 @@ document.addEventListener("DOMContentLoaded", async () => { .rows({ selected: true }) .data() .toArray() - .map((d) => d.uuid); + .map((d) => d.id); await forceUpdateDevices(selectedDevices); }, className: "buttons-force-update", @@ -143,7 +143,7 @@ document.addEventListener("DOMContentLoaded", async () => { .rows({ selected: true }) .data() .toArray() - .map((d) => d.uuid); + .map((d) => d.id); await pinDevices(selectedDevices); }, className: "buttons-pin", @@ -283,7 +283,7 @@ async function updateDeviceName() { .rows({ selected: true }) .data() .toArray() - .map((d) => d.uuid); + .map((d) => d.id); const name = document.getElementById("device-name").value; try { @@ -300,7 +300,7 @@ async function updateDeviceRollout() { .rows({ selected: true }) .data() .toArray() - .map((d) => d.uuid); + .map((d) => d.id); const feed = document.getElementById("device-selected-feed").value; const software = "rollout"; @@ -318,7 +318,7 @@ async function updateDeviceManualSoftware() { .rows({ selected: true }) .data() .toArray() - .map((d) => d.uuid); + .map((d) => d.id); const feed = null; const software = document.getElementById("selected-sw").value; @@ -336,7 +336,7 @@ async function updateDeviceLatest() { .rows({ selected: true }) .data() .toArray() - .map((d) => d.uuid); + .map((d) => d.id); const feed = null; const software = "latest"; @@ -386,12 +386,12 @@ function updateDeviceList() { .rows({ selected: true }) .data() .toArray() - .map((d) => d.uuid); + .map((d) => d.id); dataTable.ajax.reload(() => { dataTable.rows().every(function () { const rowData = this.data(); - if (selectedRows.includes(rowData.uuid)) { + if (selectedRows.includes(rowData.id)) { this.select(); } }); diff --git a/goosebit/ui/static/js/util.js b/goosebit/ui/static/js/util.js index 943da41e..9f22885a 100644 --- a/goosebit/ui/static/js/util.js +++ b/goosebit/ui/static/js/util.js @@ -25,7 +25,7 @@ async function updateSoftwareSelection(devices = null) { const url = new URL("/ui/bff/software?order[0][dir]=desc&order[0][name]=version", window.location.origin); if (devices != null) { for (const device of devices) { - url.searchParams.append("uuids", device.uuid); + url.searchParams.append("ids", device.id); } } const response = await fetch(url.toString()); diff --git a/goosebit/updater/controller/v1/routes.py b/goosebit/updater/controller/v1/routes.py index 1946aca7..b4dec0de 100644 --- a/goosebit/updater/controller/v1/routes.py +++ b/goosebit/updater/controller/v1/routes.py @@ -37,14 +37,14 @@ async def polling(request: Request, device: Device = Depends(get_device)): "href": str( request.url_for( "config_data", - dev_id=device.uuid, + dev_id=device.id, ) ) } - logger.info(f"Skip: registration required, device={device.uuid}") + logger.info(f"Skip: registration required, device={device.id}") elif device.last_state == UpdateStateEnum.ERROR and not device.force_update: - logger.info(f"Skip: device in error state, device={device.uuid}") + logger.info(f"Skip: device in error state, device={device.id}") else: # provide update if available. Note: this is also required while in state "running", otherwise swupdate @@ -55,12 +55,12 @@ async def polling(request: Request, device: Device = Depends(get_device)): "href": str( request.url_for( "deployment_base", - dev_id=device.uuid, + dev_id=device.id, action_id=software.id, ) ) } - logger.info(f"Forced: update available, device={device.uuid}") + logger.info(f"Forced: update available, device={device.id}") return { "config": {"polling": {"sleep": sleep}}, @@ -71,7 +71,7 @@ async def polling(request: Request, device: Device = Depends(get_device)): @router.put("/{dev_id}/configData") async def config_data(_: Request, cfg: ConfigDataSchema, device: Device = Depends(get_device)): await DeviceManager.update_config_data(device, **cfg.data) - logger.info(f"Updating config data, device={device.uuid}") + logger.info(f"Updating config data, device={device.id}") return {"success": True, "message": "Updated swupdate data."} @@ -83,7 +83,7 @@ async def deployment_base( ): handling_type, software = await DeviceManager.get_update(device) - logger.info(f"Request deployment base, device={device.uuid}") + logger.info(f"Request deployment base, device={device.id}") return { "id": str(action_id), @@ -102,7 +102,7 @@ async def deployment_feedback(_: Request, data: FeedbackSchema, action_id: int, await DeviceManager.deployment_action_start(device) await DeviceManager.update_device_state(device, UpdateStateEnum.RUNNING) - logger.debug(f"Installation in progress, device={device.uuid}") + logger.debug(f"Installation in progress, device={device.id}") elif data.status.execution == FeedbackStatusExecutionState.CLOSED: await DeviceManager.update_force_update(device, False) @@ -123,14 +123,14 @@ async def deployment_feedback(_: Request, data: FeedbackSchema, action_id: int, else: # edge case where device update mode got changed while update was running logging.warning( - f"Updating rollout success stats failed, action_id={action_id}, device={device.uuid}" # noqa: E501 + f"Updating rollout success stats failed, action_id={action_id}, device={device.id}" # noqa: E501 ) if reported_software: await DeviceManager.update_sw_version(device, reported_software.version) software_version = reported_software.version if reported_software else None - logger.debug(f"Installation successful, software={software_version}, device={device.uuid}") + logger.debug(f"Installation successful, software={software_version}, device={device.id}") elif data.status.result.finished == FeedbackStatusResultFinished.FAILURE: await DeviceManager.update_device_state(device, UpdateStateEnum.ERROR) @@ -143,20 +143,20 @@ async def deployment_feedback(_: Request, data: FeedbackSchema, action_id: int, else: # edge case where device update mode got changed while update was running logging.warning( - f"Updating rollout failure stats failed, action_id={action_id}, device={device.uuid}" # noqa: E501 + f"Updating rollout failure stats failed, action_id={action_id}, device={device.id}" # noqa: E501 ) software_version = reported_software.version if reported_software else None - logger.debug(f"Installation failed, software={software_version}, device={device.uuid}") + logger.debug(f"Installation failed, software={software_version}, device={device.id}") else: - logging.error(f"Device reported unhandled execution state, state={data.status.execution}, device={device.uuid}") + logging.error(f"Device reported unhandled execution state, state={data.status.execution}, device={device.id}") try: log = data.status.details if log is not None: await DeviceManager.update_log(device, "\n".join(log)) except AttributeError: - logging.warning(f"No details to update device update log, device={device.uuid}") + logging.warning(f"No details to update device update log, device={device.id}") return {"id": str(action_id)} diff --git a/goosebit/updater/routes.py b/goosebit/updater/routes.py index ae4d100b..dcc7c5c0 100644 --- a/goosebit/updater/routes.py +++ b/goosebit/updater/routes.py @@ -4,6 +4,7 @@ from fastapi.requests import Request from goosebit.device_manager import DeviceManager, get_device +from goosebit.settings import config from goosebit.settings.schema import DeviceAuthMode from ..db import Device @@ -50,7 +51,7 @@ async def validate_device_token(request: Request, dev_id: str): if device_token is None: raise HTTPException(401, "Device authentication token is required in strict mode.") # do not create a device in strict mode - device = await Device.get_or_none(uuid=dev_id) + device = await Device.get_or_none(id=dev_id) if device is None: raise HTTPException(401, "Cannot register a new device in strict mode.") if not device.auth_token == device_token: @@ -58,7 +59,7 @@ async def validate_device_token(request: Request, dev_id: str): router = APIRouter( - prefix="/ddi", + prefix=f"/{config.tenant}", dependencies=[Depends(log_last_connection), Depends(validate_device_token)], tags=["ddi"], ) diff --git a/goosebit/updates/__init__.py b/goosebit/updates/__init__.py index d4a2e0b5..b62de245 100644 --- a/goosebit/updates/__init__.py +++ b/goosebit/updates/__init__.py @@ -100,7 +100,7 @@ async def generate_chunk(request: Request, device: Device) -> list: href = str( request.url_for( "download_artifact", - dev_id=device.uuid, + dev_id=device.id, ) ) else: diff --git a/goosebit/updates/swdesc.py b/goosebit/updates/swdesc.py index 4be5e474..28427eee 100644 --- a/goosebit/updates/swdesc.py +++ b/goosebit/updates/swdesc.py @@ -6,10 +6,10 @@ import httpx import libconf -import semver from anyio import AsyncFile, Path, open_file from goosebit.settings import config +from goosebit.util.version import Version logger = logging.getLogger(__name__) @@ -23,7 +23,7 @@ def _append_compatibility(boardname, value, compatibility): def parse_descriptor(swdesc: libconf.AttrDict[Any, Any | None]): swdesc_attrs = {} try: - swdesc_attrs["version"] = semver.Version.parse(swdesc["software"]["version"], optional_minor_and_patch=True) + swdesc_attrs["version"] = Version.parse(swdesc["software"]["version"]) compatibility: list[dict[str, str]] = [] _append_compatibility("default", swdesc["software"], compatibility) @@ -35,6 +35,10 @@ def parse_descriptor(swdesc: libconf.AttrDict[Any, Any | None]): for key2 in element: _append_compatibility(key, element[key2], compatibility) + if len(compatibility) == 0: + # if nothing is specified, assume compatibility with default / default boards + compatibility.append({"hw_model": "default", "hw_revision": "default"}) + swdesc_attrs["compatibility"] = compatibility except KeyError as e: logging.warning(f"Parsing swu descriptor failed, error={e}") diff --git a/goosebit/util/__init__.py b/goosebit/util/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/goosebit/util/version.py b/goosebit/util/version.py new file mode 100644 index 00000000..c3387c61 --- /dev/null +++ b/goosebit/util/version.py @@ -0,0 +1,92 @@ +from functools import total_ordering + +from semver import Version as SemVersion + + +# Class that replicates version handling of swupdate. For reference see +# * https://sbabic.github.io/swupdate/sw-description.html#versioning-schemas-in-swupdate +# * https://github.com/sbabic/swupdate/blob/60322d5ad668e5603341c5f42b3c51d0a1c60226/core/artifacts_versions.c#L158 +# * https://github.com/sbabic/swupdate/blob/60322d5ad668e5603341c5f42b3c51d0a1c60226/core/artifacts_versions.c#L217 +@total_ordering +class Version: + version_str: str + default_version: int | None + sem_version: SemVersion + + def __init__(self, version_str: str, default_version: int | None = None, sem_version: SemVersion = None): + self.version_str = version_str + self.default_version = default_version + self.sem_version = sem_version + + @staticmethod + def parse(version_str: str): + default_version = Version._default_version_to_number(version_str) + sem_version = None + try: + sem_version = SemVersion.parse(version_str, optional_minor_and_patch=True) + except (TypeError, ValueError): + pass + + if not default_version and not sem_version: + raise ValueError(f"{version_str} is not valid swupdate version") + + return Version(version_str, default_version, sem_version) + + def __str__(self): + return self.version_str + + def __eq__(self, other): + # support comparison with strings as a convenience + if isinstance(other, str): + try: + other = Version.parse(other) + except ValueError: + return False + + if not isinstance(other, Version): + return NotImplemented + + if self.default_version and other.default_version: + return self.default_version == other.default_version + + if self.sem_version and other.sem_version: + return self.sem_version == other.sem_version + + # fallback to lexical comparison of no of the same type + return self.version_str == other.version_str + + def __lt__(self, other): + if not isinstance(other, Version): + return NotImplemented + + if self.default_version and other.default_version: + return self.default_version < other.default_version + + if self.sem_version and other.sem_version: + return self.sem_version < other.sem_version + + # fallback to lexical comparison of no of the same type + return self.version_str < other.version_str + + @staticmethod + def _default_version_to_number(version_string: str) -> int | None: + parts = version_string.split(".") + count = min(len(parts), 4) + version = 0 + + for i in range(count): + try: + fld = int(parts[i]) + except ValueError: + return None + + if fld > 0xFFFF: + print(f"Version {version_string} had an element > 65535, falling back to semver") + return None + + version = (version << 16) | fld + + if count < 4: + version <<= 16 * (4 - count) + + return version diff --git a/poetry.lock b/poetry.lock index 0b581fd6..7320ee34 100644 --- a/poetry.lock +++ b/poetry.lock @@ -69,13 +69,13 @@ files = [ [[package]] name = "anyio" -version = "4.8.0" +version = "4.9.0" description = "High level compatibility layer for multiple asynchronous event loop implementations" optional = false python-versions = ">=3.9" files = [ - {file = "anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a"}, - {file = "anyio-4.8.0.tar.gz", hash = "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a"}, + {file = "anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c"}, + {file = "anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028"}, ] [package.dependencies] @@ -84,8 +84,8 @@ sniffio = ">=1.1" typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} [package.extras] -doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"] -test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21)"] +doc = ["Sphinx (>=8.2,<9.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"] +test = ["anyio[trio]", "blockbuster (>=1.5.23)", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21)"] trio = ["trio (>=0.26.1)"] [[package]] @@ -674,13 +674,13 @@ typing-extensions = "*" [[package]] name = "fastapi" -version = "0.115.11" +version = "0.115.12" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" optional = false python-versions = ">=3.8" files = [ - {file = "fastapi-0.115.11-py3-none-any.whl", hash = "sha256:32e1541b7b74602e4ef4a0260ecaf3aadf9d4f19590bba3e1bf2ac4666aa2c64"}, - {file = "fastapi-0.115.11.tar.gz", hash = "sha256:cc81f03f688678b92600a65a5e618b93592c65005db37157147204d8924bf94f"}, + {file = "fastapi-0.115.12-py3-none-any.whl", hash = "sha256:e94613d6c05e27be7ffebdd6ea5f388112e5e430c8f7d6494a9d1d88d43e814d"}, + {file = "fastapi-0.115.12.tar.gz", hash = "sha256:1e2c2a2646905f9e83d32f04a3f86aff4a286669c6c950ca95b5fd68c2602681"}, ] [package.dependencies] @@ -694,13 +694,13 @@ standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "htt [[package]] name = "filelock" -version = "3.17.0" +version = "3.18.0" description = "A platform independent file lock." optional = false python-versions = ">=3.9" files = [ - {file = "filelock-3.17.0-py3-none-any.whl", hash = "sha256:533dc2f7ba78dc2f0f531fc6c4940addf7b70a481e269a5a3b93be94ffbe8338"}, - {file = "filelock-3.17.0.tar.gz", hash = "sha256:ee4e77401ef576ebb38cd7f13b9b28893194acc20a8e68e18730ba9c0e54660e"}, + {file = "filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de"}, + {file = "filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2"}, ] [package.extras] @@ -850,13 +850,13 @@ type = ["pytest-mypy"] [[package]] name = "iniconfig" -version = "2.0.0" +version = "2.1.0" description = "brain-dead simple config-ini parsing" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, - {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, + {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, + {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, ] [[package]] @@ -1112,20 +1112,20 @@ pyyaml = ">=5.1" [[package]] name = "mkdocs-material" -version = "9.6.7" +version = "9.6.11" description = "Documentation that simply works" optional = false python-versions = ">=3.8" files = [ - {file = "mkdocs_material-9.6.7-py3-none-any.whl", hash = "sha256:8a159e45e80fcaadd9fbeef62cbf928569b93df954d4dc5ba76d46820caf7b47"}, - {file = "mkdocs_material-9.6.7.tar.gz", hash = "sha256:3e2c1fceb9410056c2d91f334a00cdea3215c28750e00c691c1e46b2a33309b4"}, + {file = "mkdocs_material-9.6.11-py3-none-any.whl", hash = "sha256:47f21ef9cbf4f0ebdce78a2ceecaa5d413581a55141e4464902224ebbc0b1263"}, + {file = "mkdocs_material-9.6.11.tar.gz", hash = "sha256:0b7f4a0145c5074cdd692e4362d232fb25ef5b23328d0ec1ab287af77cc0deff"}, ] [package.dependencies] babel = ">=2.10,<3.0" backrefs = ">=5.7.post1,<6.0" colorama = ">=0.4,<1.0" -jinja2 = ">=3.0,<4.0" +jinja2 = ">=3.1,<4.0" markdown = ">=3.2,<4.0" mkdocs = ">=1.6,<2.0" mkdocs-material-extensions = ">=1.3,<2.0" @@ -1430,19 +1430,19 @@ test = ["Faker (>=1.0.8)", "allpairspy (>=2)", "click (>=6.2)", "pytest (>=6.0.1 [[package]] name = "platformdirs" -version = "4.3.6" +version = "4.3.7" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, - {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, + {file = "platformdirs-4.3.7-py3-none-any.whl", hash = "sha256:a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94"}, + {file = "platformdirs-4.3.7.tar.gz", hash = "sha256:eb437d586b6a0986388f0d6f74aa0cde27b48d0e3d66843640bfb6bdcdb6e351"}, ] [package.extras] -docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] -type = ["mypy (>=1.11.2)"] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.4)", "pytest-cov (>=6)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.14.1)"] [[package]] name = "pluggy" @@ -1461,13 +1461,13 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "pre-commit" -version = "4.1.0" +version = "4.2.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." optional = false python-versions = ">=3.9" files = [ - {file = "pre_commit-4.1.0-py2.py3-none-any.whl", hash = "sha256:d29e7cb346295bcc1cc75fc3e92e343495e3ea0196c9ec6ba53f49f10ab6ae7b"}, - {file = "pre_commit-4.1.0.tar.gz", hash = "sha256:ae3f018575a588e30dfddfab9a05448bfbd6b73d78709617b5a2b853549716d4"}, + {file = "pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd"}, + {file = "pre_commit-4.2.0.tar.gz", hash = "sha256:601283b9757afd87d40c4c4a9b2b5de9637a8ea02eaff7adc2d0fb4e04841146"}, ] [package.dependencies] @@ -1515,19 +1515,20 @@ files = [ [[package]] name = "pydantic" -version = "2.10.6" +version = "2.11.1" description = "Data validation using Python type hints" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584"}, - {file = "pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236"}, + {file = "pydantic-2.11.1-py3-none-any.whl", hash = "sha256:5b6c415eee9f8123a14d859be0c84363fec6b1feb6b688d6435801230b56e0b8"}, + {file = "pydantic-2.11.1.tar.gz", hash = "sha256:442557d2910e75c991c39f4b4ab18963d57b9b55122c8b2a9cd176d8c29ce968"}, ] [package.dependencies] annotated-types = ">=0.6.0" -pydantic-core = "2.27.2" +pydantic-core = "2.33.0" typing-extensions = ">=4.12.2" +typing-inspection = ">=0.4.0" [package.extras] email = ["email-validator (>=2.0.0)"] @@ -1535,111 +1536,110 @@ timezone = ["tzdata"] [[package]] name = "pydantic-core" -version = "2.27.2" +version = "2.33.0" description = "Core functionality for Pydantic validation and serialization" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa"}, - {file = "pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236"}, - {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962"}, - {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9"}, - {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af"}, - {file = "pydantic_core-2.27.2-cp310-cp310-win32.whl", hash = "sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4"}, - {file = "pydantic_core-2.27.2-cp310-cp310-win_amd64.whl", hash = "sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31"}, - {file = "pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc"}, - {file = "pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d"}, - {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b"}, - {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474"}, - {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6"}, - {file = "pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c"}, - {file = "pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc"}, - {file = "pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4"}, - {file = "pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0"}, - {file = "pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4"}, - {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3"}, - {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4"}, - {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57"}, - {file = "pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc"}, - {file = "pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9"}, - {file = "pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b"}, - {file = "pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b"}, - {file = "pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4"}, - {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27"}, - {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee"}, - {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1"}, - {file = "pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130"}, - {file = "pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee"}, - {file = "pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b"}, - {file = "pydantic_core-2.27.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d3e8d504bdd3f10835468f29008d72fc8359d95c9c415ce6e767203db6127506"}, - {file = "pydantic_core-2.27.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:521eb9b7f036c9b6187f0b47318ab0d7ca14bd87f776240b90b21c1f4f149320"}, - {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85210c4d99a0114f5a9481b44560d7d1e35e32cc5634c656bc48e590b669b145"}, - {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d716e2e30c6f140d7560ef1538953a5cd1a87264c737643d481f2779fc247fe1"}, - {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f66d89ba397d92f840f8654756196d93804278457b5fbede59598a1f9f90b228"}, - {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:669e193c1c576a58f132e3158f9dfa9662969edb1a250c54d8fa52590045f046"}, - {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdbe7629b996647b99c01b37f11170a57ae675375b14b8c13b8518b8320ced5"}, - {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d262606bf386a5ba0b0af3b97f37c83d7011439e3dc1a9298f21efb292e42f1a"}, - {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cabb9bcb7e0d97f74df8646f34fc76fbf793b7f6dc2438517d7a9e50eee4f14d"}, - {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:d2d63f1215638d28221f664596b1ccb3944f6e25dd18cd3b86b0a4c408d5ebb9"}, - {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bca101c00bff0adb45a833f8451b9105d9df18accb8743b08107d7ada14bd7da"}, - {file = "pydantic_core-2.27.2-cp38-cp38-win32.whl", hash = "sha256:f6f8e111843bbb0dee4cb6594cdc73e79b3329b526037ec242a3e49012495b3b"}, - {file = "pydantic_core-2.27.2-cp38-cp38-win_amd64.whl", hash = "sha256:fd1aea04935a508f62e0d0ef1f5ae968774a32afc306fb8545e06f5ff5cdf3ad"}, - {file = "pydantic_core-2.27.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c10eb4f1659290b523af58fa7cffb452a61ad6ae5613404519aee4bfbf1df993"}, - {file = "pydantic_core-2.27.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ef592d4bad47296fb11f96cd7dc898b92e795032b4894dfb4076cfccd43a9308"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c61709a844acc6bf0b7dce7daae75195a10aac96a596ea1b776996414791ede4"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c5f762659e47fdb7b16956c71598292f60a03aa92f8b6351504359dbdba6cf"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c9775e339e42e79ec99c441d9730fccf07414af63eac2f0e48e08fd38a64d76"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57762139821c31847cfb2df63c12f725788bd9f04bc2fb392790959b8f70f118"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d1e85068e818c73e048fe28cfc769040bb1f475524f4745a5dc621f75ac7630"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:097830ed52fd9e427942ff3b9bc17fab52913b2f50f2880dc4a5611446606a54"}, - {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:044a50963a614ecfae59bb1eaf7ea7efc4bc62f49ed594e18fa1e5d953c40e9f"}, - {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:4e0b4220ba5b40d727c7f879eac379b822eee5d8fff418e9d3381ee45b3b0362"}, - {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5e4f4bb20d75e9325cc9696c6802657b58bc1dbbe3022f32cc2b2b632c3fbb96"}, - {file = "pydantic_core-2.27.2-cp39-cp39-win32.whl", hash = "sha256:cca63613e90d001b9f2f9a9ceb276c308bfa2a43fafb75c8031c4f66039e8c6e"}, - {file = "pydantic_core-2.27.2-cp39-cp39-win_amd64.whl", hash = "sha256:77d1bca19b0f7021b3a982e6f903dcd5b2b06076def36a652e3907f596e29f67"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c33939a82924da9ed65dab5a65d427205a73181d8098e79b6b426bdf8ad4e656"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:00bad2484fa6bda1e216e7345a798bd37c68fb2d97558edd584942aa41b7d278"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c817e2b40aba42bac6f457498dacabc568c3b7a986fc9ba7c8d9d260b71485fb"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:251136cdad0cb722e93732cb45ca5299fb56e1344a833640bf93b2803f8d1bfd"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d2088237af596f0a524d3afc39ab3b036e8adb054ee57cbb1dcf8e09da5b29cc"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d4041c0b966a84b4ae7a09832eb691a35aec90910cd2dbe7a208de59be77965b"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:8083d4e875ebe0b864ffef72a4304827015cff328a1be6e22cc850753bfb122b"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f141ee28a0ad2123b6611b6ceff018039df17f32ada8b534e6aa039545a3efb2"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7d0c8399fcc1848491f00e0314bd59fb34a9c008761bcb422a057670c3f65e35"}, - {file = "pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39"}, + {file = "pydantic_core-2.33.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:71dffba8fe9ddff628c68f3abd845e91b028361d43c5f8e7b3f8b91d7d85413e"}, + {file = "pydantic_core-2.33.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:abaeec1be6ed535a5d7ffc2e6c390083c425832b20efd621562fbb5bff6dc518"}, + {file = "pydantic_core-2.33.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:759871f00e26ad3709efc773ac37b4d571de065f9dfb1778012908bcc36b3a73"}, + {file = "pydantic_core-2.33.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dcfebee69cd5e1c0b76a17e17e347c84b00acebb8dd8edb22d4a03e88e82a207"}, + {file = "pydantic_core-2.33.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1b1262b912435a501fa04cd213720609e2cefa723a07c92017d18693e69bf00b"}, + {file = "pydantic_core-2.33.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4726f1f3f42d6a25678c67da3f0b10f148f5655813c5aca54b0d1742ba821b8f"}, + {file = "pydantic_core-2.33.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e790954b5093dff1e3a9a2523fddc4e79722d6f07993b4cd5547825c3cbf97b5"}, + {file = "pydantic_core-2.33.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:34e7fb3abe375b5c4e64fab75733d605dda0f59827752debc99c17cb2d5f3276"}, + {file = "pydantic_core-2.33.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ecb158fb9b9091b515213bed3061eb7deb1d3b4e02327c27a0ea714ff46b0760"}, + {file = "pydantic_core-2.33.0-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:4d9149e7528af8bbd76cc055967e6e04617dcb2a2afdaa3dea899406c5521faa"}, + {file = "pydantic_core-2.33.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e81a295adccf73477220e15ff79235ca9dcbcee4be459eb9d4ce9a2763b8386c"}, + {file = "pydantic_core-2.33.0-cp310-cp310-win32.whl", hash = "sha256:f22dab23cdbce2005f26a8f0c71698457861f97fc6318c75814a50c75e87d025"}, + {file = "pydantic_core-2.33.0-cp310-cp310-win_amd64.whl", hash = "sha256:9cb2390355ba084c1ad49485d18449b4242da344dea3e0fe10babd1f0db7dcfc"}, + {file = "pydantic_core-2.33.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a608a75846804271cf9c83e40bbb4dab2ac614d33c6fd5b0c6187f53f5c593ef"}, + {file = "pydantic_core-2.33.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e1c69aa459f5609dec2fa0652d495353accf3eda5bdb18782bc5a2ae45c9273a"}, + {file = "pydantic_core-2.33.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9ec80eb5a5f45a2211793f1c4aeddff0c3761d1c70d684965c1807e923a588b"}, + {file = "pydantic_core-2.33.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e925819a98318d17251776bd3d6aa9f3ff77b965762155bdad15d1a9265c4cfd"}, + {file = "pydantic_core-2.33.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5bf68bb859799e9cec3d9dd8323c40c00a254aabb56fe08f907e437005932f2b"}, + {file = "pydantic_core-2.33.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1b2ea72dea0825949a045fa4071f6d5b3d7620d2a208335207793cf29c5a182d"}, + {file = "pydantic_core-2.33.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1583539533160186ac546b49f5cde9ffc928062c96920f58bd95de32ffd7bffd"}, + {file = "pydantic_core-2.33.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:23c3e77bf8a7317612e5c26a3b084c7edeb9552d645742a54a5867635b4f2453"}, + {file = "pydantic_core-2.33.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a7a7f2a3f628d2f7ef11cb6188bcf0b9e1558151d511b974dfea10a49afe192b"}, + {file = "pydantic_core-2.33.0-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:f1fb026c575e16f673c61c7b86144517705865173f3d0907040ac30c4f9f5915"}, + {file = "pydantic_core-2.33.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:635702b2fed997e0ac256b2cfbdb4dd0bf7c56b5d8fba8ef03489c03b3eb40e2"}, + {file = "pydantic_core-2.33.0-cp311-cp311-win32.whl", hash = "sha256:07b4ced28fccae3f00626eaa0c4001aa9ec140a29501770a88dbbb0966019a86"}, + {file = "pydantic_core-2.33.0-cp311-cp311-win_amd64.whl", hash = "sha256:4927564be53239a87770a5f86bdc272b8d1fbb87ab7783ad70255b4ab01aa25b"}, + {file = "pydantic_core-2.33.0-cp311-cp311-win_arm64.whl", hash = "sha256:69297418ad644d521ea3e1aa2e14a2a422726167e9ad22b89e8f1130d68e1e9a"}, + {file = "pydantic_core-2.33.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:6c32a40712e3662bebe524abe8abb757f2fa2000028d64cc5a1006016c06af43"}, + {file = "pydantic_core-2.33.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8ec86b5baa36f0a0bfb37db86c7d52652f8e8aa076ab745ef7725784183c3fdd"}, + {file = "pydantic_core-2.33.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4deac83a8cc1d09e40683be0bc6d1fa4cde8df0a9bf0cda5693f9b0569ac01b6"}, + {file = "pydantic_core-2.33.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:175ab598fb457a9aee63206a1993874badf3ed9a456e0654273e56f00747bbd6"}, + {file = "pydantic_core-2.33.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f36afd0d56a6c42cf4e8465b6441cf546ed69d3a4ec92724cc9c8c61bd6ecf4"}, + {file = "pydantic_core-2.33.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0a98257451164666afafc7cbf5fb00d613e33f7e7ebb322fbcd99345695a9a61"}, + {file = "pydantic_core-2.33.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecc6d02d69b54a2eb83ebcc6f29df04957f734bcf309d346b4f83354d8376862"}, + {file = "pydantic_core-2.33.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a69b7596c6603afd049ce7f3835bcf57dd3892fc7279f0ddf987bebed8caa5a"}, + {file = "pydantic_core-2.33.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ea30239c148b6ef41364c6f51d103c2988965b643d62e10b233b5efdca8c0099"}, + {file = "pydantic_core-2.33.0-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:abfa44cf2f7f7d7a199be6c6ec141c9024063205545aa09304349781b9a125e6"}, + {file = "pydantic_core-2.33.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:20d4275f3c4659d92048c70797e5fdc396c6e4446caf517ba5cad2db60cd39d3"}, + {file = "pydantic_core-2.33.0-cp312-cp312-win32.whl", hash = "sha256:918f2013d7eadea1d88d1a35fd4a1e16aaf90343eb446f91cb091ce7f9b431a2"}, + {file = "pydantic_core-2.33.0-cp312-cp312-win_amd64.whl", hash = "sha256:aec79acc183865bad120b0190afac467c20b15289050648b876b07777e67ea48"}, + {file = "pydantic_core-2.33.0-cp312-cp312-win_arm64.whl", hash = "sha256:5461934e895968655225dfa8b3be79e7e927e95d4bd6c2d40edd2fa7052e71b6"}, + {file = "pydantic_core-2.33.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f00e8b59e1fc8f09d05594aa7d2b726f1b277ca6155fc84c0396db1b373c4555"}, + {file = "pydantic_core-2.33.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1a73be93ecef45786d7d95b0c5e9b294faf35629d03d5b145b09b81258c7cd6d"}, + {file = "pydantic_core-2.33.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ff48a55be9da6930254565ff5238d71d5e9cd8c5487a191cb85df3bdb8c77365"}, + {file = "pydantic_core-2.33.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:26a4ea04195638dcd8c53dadb545d70badba51735b1594810e9768c2c0b4a5da"}, + {file = "pydantic_core-2.33.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:41d698dcbe12b60661f0632b543dbb119e6ba088103b364ff65e951610cb7ce0"}, + {file = "pydantic_core-2.33.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ae62032ef513fe6281ef0009e30838a01057b832dc265da32c10469622613885"}, + {file = "pydantic_core-2.33.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f225f3a3995dbbc26affc191d0443c6c4aa71b83358fd4c2b7d63e2f6f0336f9"}, + {file = "pydantic_core-2.33.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5bdd36b362f419c78d09630cbaebc64913f66f62bda6d42d5fbb08da8cc4f181"}, + {file = "pydantic_core-2.33.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:2a0147c0bef783fd9abc9f016d66edb6cac466dc54a17ec5f5ada08ff65caf5d"}, + {file = "pydantic_core-2.33.0-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:c860773a0f205926172c6644c394e02c25421dc9a456deff16f64c0e299487d3"}, + {file = "pydantic_core-2.33.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:138d31e3f90087f42aa6286fb640f3c7a8eb7bdae829418265e7e7474bd2574b"}, + {file = "pydantic_core-2.33.0-cp313-cp313-win32.whl", hash = "sha256:d20cbb9d3e95114325780f3cfe990f3ecae24de7a2d75f978783878cce2ad585"}, + {file = "pydantic_core-2.33.0-cp313-cp313-win_amd64.whl", hash = "sha256:ca1103d70306489e3d006b0f79db8ca5dd3c977f6f13b2c59ff745249431a606"}, + {file = "pydantic_core-2.33.0-cp313-cp313-win_arm64.whl", hash = "sha256:6291797cad239285275558e0a27872da735b05c75d5237bbade8736f80e4c225"}, + {file = "pydantic_core-2.33.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7b79af799630af263eca9ec87db519426d8c9b3be35016eddad1832bac812d87"}, + {file = "pydantic_core-2.33.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eabf946a4739b5237f4f56d77fa6668263bc466d06a8036c055587c130a46f7b"}, + {file = "pydantic_core-2.33.0-cp313-cp313t-win_amd64.whl", hash = "sha256:8a1d581e8cdbb857b0e0e81df98603376c1a5c34dc5e54039dcc00f043df81e7"}, + {file = "pydantic_core-2.33.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:7c9c84749f5787781c1c45bb99f433402e484e515b40675a5d121ea14711cf61"}, + {file = "pydantic_core-2.33.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:64672fa888595a959cfeff957a654e947e65bbe1d7d82f550417cbd6898a1d6b"}, + {file = "pydantic_core-2.33.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26bc7367c0961dec292244ef2549afa396e72e28cc24706210bd44d947582c59"}, + {file = "pydantic_core-2.33.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ce72d46eb201ca43994303025bd54d8a35a3fc2a3495fac653d6eb7205ce04f4"}, + {file = "pydantic_core-2.33.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:14229c1504287533dbf6b1fc56f752ce2b4e9694022ae7509631ce346158de11"}, + {file = "pydantic_core-2.33.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:085d8985b1c1e48ef271e98a658f562f29d89bda98bf120502283efbc87313eb"}, + {file = "pydantic_core-2.33.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31860fbda80d8f6828e84b4a4d129fd9c4535996b8249cfb8c720dc2a1a00bb8"}, + {file = "pydantic_core-2.33.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f200b2f20856b5a6c3a35f0d4e344019f805e363416e609e9b47c552d35fd5ea"}, + {file = "pydantic_core-2.33.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5f72914cfd1d0176e58ddc05c7a47674ef4222c8253bf70322923e73e14a4ac3"}, + {file = "pydantic_core-2.33.0-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:91301a0980a1d4530d4ba7e6a739ca1a6b31341252cb709948e0aca0860ce0ae"}, + {file = "pydantic_core-2.33.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7419241e17c7fbe5074ba79143d5523270e04f86f1b3a0dff8df490f84c8273a"}, + {file = "pydantic_core-2.33.0-cp39-cp39-win32.whl", hash = "sha256:7a25493320203005d2a4dac76d1b7d953cb49bce6d459d9ae38e30dd9f29bc9c"}, + {file = "pydantic_core-2.33.0-cp39-cp39-win_amd64.whl", hash = "sha256:82a4eba92b7ca8af1b7d5ef5f3d9647eee94d1f74d21ca7c21e3a2b92e008358"}, + {file = "pydantic_core-2.33.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e2762c568596332fdab56b07060c8ab8362c56cf2a339ee54e491cd503612c50"}, + {file = "pydantic_core-2.33.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5bf637300ff35d4f59c006fff201c510b2b5e745b07125458a5389af3c0dff8c"}, + {file = "pydantic_core-2.33.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62c151ce3d59ed56ebd7ce9ce5986a409a85db697d25fc232f8e81f195aa39a1"}, + {file = "pydantic_core-2.33.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ee65f0cc652261744fd07f2c6e6901c914aa6c5ff4dcfaf1136bc394d0dd26b"}, + {file = "pydantic_core-2.33.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:024d136ae44d233e6322027bbf356712b3940bee816e6c948ce4b90f18471b3d"}, + {file = "pydantic_core-2.33.0-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:e37f10f6d4bc67c58fbd727108ae1d8b92b397355e68519f1e4a7babb1473442"}, + {file = "pydantic_core-2.33.0-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:502ed542e0d958bd12e7c3e9a015bce57deaf50eaa8c2e1c439b512cb9db1e3a"}, + {file = "pydantic_core-2.33.0-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:715c62af74c236bf386825c0fdfa08d092ab0f191eb5b4580d11c3189af9d330"}, + {file = "pydantic_core-2.33.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:bccc06fa0372151f37f6b69834181aa9eb57cf8665ed36405fb45fbf6cac3bae"}, + {file = "pydantic_core-2.33.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5d8dc9f63a26f7259b57f46a7aab5af86b2ad6fbe48487500bb1f4b27e051e4c"}, + {file = "pydantic_core-2.33.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:30369e54d6d0113d2aa5aee7a90d17f225c13d87902ace8fcd7bbf99b19124db"}, + {file = "pydantic_core-2.33.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3eb479354c62067afa62f53bb387827bee2f75c9c79ef25eef6ab84d4b1ae3b"}, + {file = "pydantic_core-2.33.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0310524c833d91403c960b8a3cf9f46c282eadd6afd276c8c5edc617bd705dc9"}, + {file = "pydantic_core-2.33.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:eddb18a00bbb855325db27b4c2a89a4ba491cd6a0bd6d852b225172a1f54b36c"}, + {file = "pydantic_core-2.33.0-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:ade5dbcf8d9ef8f4b28e682d0b29f3008df9842bb5ac48ac2c17bc55771cc976"}, + {file = "pydantic_core-2.33.0-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:2c0afd34f928383e3fd25740f2050dbac9d077e7ba5adbaa2227f4d4f3c8da5c"}, + {file = "pydantic_core-2.33.0-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:7da333f21cd9df51d5731513a6d39319892947604924ddf2e24a4612975fb936"}, + {file = "pydantic_core-2.33.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:4b6d77c75a57f041c5ee915ff0b0bb58eabb78728b69ed967bc5b780e8f701b8"}, + {file = "pydantic_core-2.33.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ba95691cf25f63df53c1d342413b41bd7762d9acb425df8858d7efa616c0870e"}, + {file = "pydantic_core-2.33.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:4f1ab031feb8676f6bd7c85abec86e2935850bf19b84432c64e3e239bffeb1ec"}, + {file = "pydantic_core-2.33.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:58c1151827eef98b83d49b6ca6065575876a02d2211f259fb1a6b7757bd24dd8"}, + {file = "pydantic_core-2.33.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a66d931ea2c1464b738ace44b7334ab32a2fd50be023d863935eb00f42be1778"}, + {file = "pydantic_core-2.33.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0bcf0bab28995d483f6c8d7db25e0d05c3efa5cebfd7f56474359e7137f39856"}, + {file = "pydantic_core-2.33.0-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:89670d7a0045acb52be0566df5bc8b114ac967c662c06cf5e0c606e4aadc964b"}, + {file = "pydantic_core-2.33.0-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:b716294e721d8060908dbebe32639b01bfe61b15f9f57bcc18ca9a0e00d9520b"}, + {file = "pydantic_core-2.33.0-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fc53e05c16697ff0c1c7c2b98e45e131d4bfb78068fffff92a82d169cbb4c7b7"}, + {file = "pydantic_core-2.33.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:68504959253303d3ae9406b634997a2123a0b0c1da86459abbd0ffc921695eac"}, + {file = "pydantic_core-2.33.0.tar.gz", hash = "sha256:40eb8af662ba409c3cbf4a8150ad32ae73514cd7cb1f1a2113af39763dd616b3"}, ] [package.dependencies] @@ -1841,13 +1841,13 @@ six = ">=1.5" [[package]] name = "python-dotenv" -version = "1.0.1" +version = "1.1.0" description = "Read key-value pairs from a .env file and set them as environment variables" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, - {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, + {file = "python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d"}, + {file = "python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5"}, ] [package.extras] @@ -1866,13 +1866,13 @@ files = [ [[package]] name = "pytz" -version = "2025.1" +version = "2025.2" description = "World timezone definitions, modern and historical" optional = false python-versions = "*" files = [ - {file = "pytz-2025.1-py2.py3-none-any.whl", hash = "sha256:89dd22dca55b46eac6eda23b2d72721bf1bdfef212645d81513ef5d03038de57"}, - {file = "pytz-2025.1.tar.gz", hash = "sha256:c2db42be2a2518b28e65f9207c4d05e6ff547d1efa4086469ef855e4ab70178e"}, + {file = "pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00"}, + {file = "pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3"}, ] [[package]] @@ -1985,18 +1985,18 @@ files = [ [[package]] name = "setuptools" -version = "76.0.0" +version = "78.1.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.9" files = [ - {file = "setuptools-76.0.0-py3-none-any.whl", hash = "sha256:199466a166ff664970d0ee145839f5582cb9bca7a0a3a2e795b6a9cb2308e9c6"}, - {file = "setuptools-76.0.0.tar.gz", hash = "sha256:43b4ee60e10b0d0ee98ad11918e114c70701bc6051662a9a675a0496c1a158f4"}, + {file = "setuptools-78.1.0-py3-none-any.whl", hash = "sha256:3e386e96793c8702ae83d17b853fb93d3e09ef82ec62722e61da5cd22376dcd8"}, + {file = "setuptools-78.1.0.tar.gz", hash = "sha256:18fd474d4a82a5f83dac888df697af65afa82dec7323d09c3e37d1f14288da54"}, ] [package.extras] check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.8.0)"] -core = ["importlib_metadata (>=6)", "jaraco.collections", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] +core = ["importlib_metadata (>=6)", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] enabler = ["pytest-enabler (>=2.2)"] @@ -2135,26 +2135,40 @@ test = ["packaging", "pytest (>=6.0.1)", "python-dateutil (>=2.8.0,<3.0.0)", "py [[package]] name = "types-pyyaml" -version = "6.0.12.20241230" +version = "6.0.12.20250402" description = "Typing stubs for PyYAML" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "types_PyYAML-6.0.12.20241230-py3-none-any.whl", hash = "sha256:fa4d32565219b68e6dee5f67534c722e53c00d1cfc09c435ef04d7353e1e96e6"}, - {file = "types_pyyaml-6.0.12.20241230.tar.gz", hash = "sha256:7f07622dbd34bb9c8b264fe860a17e0efcad00d50b5f27e93984909d9363498c"}, + {file = "types_pyyaml-6.0.12.20250402-py3-none-any.whl", hash = "sha256:652348fa9e7a203d4b0d21066dfb00760d3cbd5a15ebb7cf8d33c88a49546681"}, + {file = "types_pyyaml-6.0.12.20250402.tar.gz", hash = "sha256:d7c13c3e6d335b6af4b0122a01ff1d270aba84ab96d1a1a1063ecba3e13ec075"}, ] [[package]] name = "typing-extensions" -version = "4.12.2" +version = "4.13.0" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, - {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, + {file = "typing_extensions-4.13.0-py3-none-any.whl", hash = "sha256:c8dd92cc0d6425a97c18fbb9d1954e5ff92c1ca881a309c45f06ebc0b79058e5"}, + {file = "typing_extensions-4.13.0.tar.gz", hash = "sha256:0a4ac55a5820789d87e297727d229866c9650f6521b64206413c4fbada24d95b"}, ] +[[package]] +name = "typing-inspection" +version = "0.4.0" +description = "Runtime typing introspection tools" +optional = false +python-versions = ">=3.9" +files = [ + {file = "typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f"}, + {file = "typing_inspection-0.4.0.tar.gz", hash = "sha256:9765c87de36671694a67904bf2c96e395be9c6439bb6c87b5142569dcdd65122"}, +] + +[package.dependencies] +typing-extensions = ">=4.12.0" + [[package]] name = "urllib3" version = "2.3.0" @@ -2192,13 +2206,13 @@ standard = ["colorama (>=0.4)", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", [[package]] name = "virtualenv" -version = "20.29.3" +version = "20.30.0" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.8" files = [ - {file = "virtualenv-20.29.3-py3-none-any.whl", hash = "sha256:3e3d00f5807e83b234dfb6122bf37cfadf4be216c53a49ac059d02414f819170"}, - {file = "virtualenv-20.29.3.tar.gz", hash = "sha256:95e39403fcf3940ac45bc717597dba16110b74506131845d9b687d5e73d947ac"}, + {file = "virtualenv-20.30.0-py3-none-any.whl", hash = "sha256:e34302959180fca3af42d1800df014b35019490b119eba981af27f2fa486e5d6"}, + {file = "virtualenv-20.30.0.tar.gz", hash = "sha256:800863162bcaa5450a6e4d721049730e7f2dae07720e0902b0e4040bd6f9ada8"}, ] [package.dependencies] diff --git a/sample_data.py b/sample_data.py index d72b5817..5ef8a6da 100644 --- a/sample_data.py +++ b/sample_data.py @@ -57,7 +57,7 @@ async def generate_sample_data(): fake.random_element(software_for_hw1) if hardware == hardware1 else fake.random_element(software_for_hw2) ) await Device.create( - uuid=fake.uuid4(), + id=fake.uuid4(), name=fake.bs(), feed=fake.random_element(["dev", "qa", "live"]) if update_mode == UpdateModeEnum.ROLLOUT else "", last_state=fake.random_element( diff --git a/tests/ui/bff/devices/test_routes.py b/tests/ui/bff/devices/test_routes.py index b301bea8..832a8af1 100644 --- a/tests/ui/bff/devices/test_routes.py +++ b/tests/ui/bff/devices/test_routes.py @@ -2,22 +2,22 @@ @pytest.mark.asyncio -async def test_list_devices_uuid_asc(async_client, test_data): - response = await async_client.get(f"/ui/bff/devices?order[0][dir]=asc&order[0][name]=uuid") +async def test_list_devices_id_asc(async_client, test_data): + response = await async_client.get(f"/ui/bff/devices?order[0][dir]=asc&order[0][name]=id") assert response.status_code == 200 devices = response.json()["data"] assert len(devices) == 2 - assert devices[0]["uuid"] == test_data["device_rollout"].uuid - assert devices[1]["uuid"] == test_data["device_assigned"].uuid + assert devices[0]["id"] == test_data["device_rollout"].id + assert devices[1]["id"] == test_data["device_assigned"].id @pytest.mark.asyncio -async def test_list_devices_uuid_desc(async_client, test_data): - response = await async_client.get(f"/ui/bff/devices?order[0][dir]=desc&order[0][name]=uuid") +async def test_list_devices_id_desc(async_client, test_data): + response = await async_client.get(f"/ui/bff/devices?order[0][dir]=desc&order[0][name]=id") assert response.status_code == 200 devices = response.json()["data"] assert len(devices) == 2 - assert devices[0]["uuid"] == test_data["device_assigned"].uuid - assert devices[1]["uuid"] == test_data["device_rollout"].uuid + assert devices[0]["id"] == test_data["device_assigned"].id + assert devices[1]["id"] == test_data["device_rollout"].id diff --git a/tests/updater/controller/test_device_auth.py b/tests/updater/controller/test_device_auth.py index 958d98e7..8c2e2ed6 100644 --- a/tests/updater/controller/test_device_auth.py +++ b/tests/updater/controller/test_device_auth.py @@ -8,13 +8,13 @@ async def _api_device_get(device_auth_async_client, dev_id): response = await device_auth_async_client.get("/api/v1/devices") assert response.status_code == 200 devices = response.json()["devices"] - return next(d for d in devices if d["uuid"] == dev_id) + return next(d for d in devices if d["id"] == dev_id) async def _api_device_update(device_auth_async_client, device, update_attribute, update_value): response = await device_auth_async_client.patch( f"/ui/bff/devices", - json={"devices": [f"{device.uuid}"], update_attribute: update_value}, + json={"devices": [f"{device.id}"], update_attribute: update_value}, ) assert response.status_code == 200 @@ -25,7 +25,7 @@ async def test_poll_strict_with_no_auth_device_with_no_auth(async_client, test_d monkeypatch.setattr(config.device_auth, "enable", True) monkeypatch.setattr(config.device_auth, "mode", DeviceAuthMode.STRICT) - response = await async_client.get(f"/ddi/controller/v1/{device.uuid}") + response = await async_client.get(f"/DEFAULT/controller/v1/{device.id}") assert response.status_code == 401 @@ -35,7 +35,7 @@ async def test_poll_strict_with_no_auth_device_with_auth(async_client, test_data monkeypatch.setattr(config.device_auth, "enable", True) monkeypatch.setattr(config.device_auth, "mode", DeviceAuthMode.STRICT) - response = await async_client.get(f"/ddi/controller/v1/{device.uuid}") + response = await async_client.get(f"/DEFAULT/controller/v1/{device.id}") assert response.status_code == 401 @@ -46,7 +46,7 @@ async def test_poll_strict_with_auth_device_with_auth(async_client, test_data, m monkeypatch.setattr(config.device_auth, "mode", DeviceAuthMode.STRICT) response = await async_client.get( - f"/ddi/controller/v1/{device.uuid}", headers={"Authorization": f"TargetToken {device.auth_token}"} + f"/DEFAULT/controller/v1/{device.id}", headers={"Authorization": f"TargetToken {device.auth_token}"} ) assert response.status_code == 200 @@ -57,7 +57,7 @@ async def test_poll_lax_with_no_auth_device_with_no_auth(async_client, test_data monkeypatch.setattr(config.device_auth, "enable", True) monkeypatch.setattr(config.device_auth, "mode", DeviceAuthMode.LAX) - response = await async_client.get(f"/ddi/controller/v1/{device.uuid}") + response = await async_client.get(f"/DEFAULT/controller/v1/{device.id}") assert response.status_code == 200 @@ -67,7 +67,7 @@ async def test_poll_lax_with_no_auth_device_with_auth(async_client, test_data, m monkeypatch.setattr(config.device_auth, "enable", True) monkeypatch.setattr(config.device_auth, "mode", DeviceAuthMode.LAX) - response = await async_client.get(f"/ddi/controller/v1/{device.uuid}") + response = await async_client.get(f"/DEFAULT/controller/v1/{device.id}") assert response.status_code == 401 @@ -78,7 +78,7 @@ async def test_poll_lax_with_auth_device_with_auth(async_client, test_data, monk monkeypatch.setattr(config.device_auth, "mode", DeviceAuthMode.LAX) response = await async_client.get( - f"/ddi/controller/v1/{device.uuid}", headers={"Authorization": f"TargetToken {device.auth_token}"} + f"/DEFAULT/controller/v1/{device.id}", headers={"Authorization": f"TargetToken {device.auth_token}"} ) assert response.status_code == 200 @@ -89,11 +89,11 @@ async def test_poll_setup_with_no_auth(async_client, test_data, monkeypatch): monkeypatch.setattr(config.device_auth, "enable", True) monkeypatch.setattr(config.device_auth, "mode", DeviceAuthMode.SETUP) - response = await async_client.get(f"/ddi/controller/v1/{device.uuid}") + response = await async_client.get(f"/DEFAULT/controller/v1/{device.id}") assert response.status_code == 200 # device should not have changed - device_api = await _api_device_get(async_client, device.uuid) + device_api = await _api_device_get(async_client, device.id) assert device_api["auth_token"] is None @@ -103,13 +103,13 @@ async def test_poll_setup_with_auth_add_device_auth(async_client, test_data, mon monkeypatch.setattr(config.device_auth, "enable", True) monkeypatch.setattr(config.device_auth, "mode", DeviceAuthMode.SETUP) - response = await async_client.get(f"/ddi/controller/v1/{device.uuid}") + response = await async_client.get(f"/DEFAULT/controller/v1/{device.id}") assert response.status_code == 200 token = "testing123" - await async_client.get(f"/ddi/controller/v1/{device.uuid}", headers={"Authorization": f"TargetToken {token}"}) + await async_client.get(f"/DEFAULT/controller/v1/{device.id}", headers={"Authorization": f"TargetToken {token}"}) - device_api = await _api_device_get(async_client, device.uuid) + device_api = await _api_device_get(async_client, device.id) assert device_api["auth_token"] == token await _api_device_update(async_client, device, "auth_token", None) @@ -123,13 +123,13 @@ async def test_poll_setup_with_auth_update_device_auth(async_client, test_data, monkeypatch.setattr(config.device_auth, "enable", True) monkeypatch.setattr(config.device_auth, "mode", DeviceAuthMode.SETUP) - response = await async_client.get(f"/ddi/controller/v1/{device.uuid}") + response = await async_client.get(f"/DEFAULT/controller/v1/{device.id}") assert response.status_code == 200 token = "testing123" - await async_client.get(f"/ddi/controller/v1/{device.uuid}", headers={"Authorization": f"TargetToken {token}"}) + await async_client.get(f"/DEFAULT/controller/v1/{device.id}", headers={"Authorization": f"TargetToken {token}"}) - device_api = await _api_device_get(async_client, device.uuid) + device_api = await _api_device_get(async_client, device.id) assert device_api["auth_token"] == token await _api_device_update(async_client, device, "auth_token", old_token) @@ -142,10 +142,10 @@ async def test_poll_setup_with_no_auth_no_change(async_client, test_data, monkey monkeypatch.setattr(config.device_auth, "enable", True) monkeypatch.setattr(config.device_auth, "mode", DeviceAuthMode.SETUP) - response = await async_client.get(f"/ddi/controller/v1/{device.uuid}") + response = await async_client.get(f"/DEFAULT/controller/v1/{device.id}") assert response.status_code == 200 - await async_client.get(f"/ddi/controller/v1/{device.uuid}") + await async_client.get(f"/DEFAULT/controller/v1/{device.id}") - device_api = await _api_device_get(async_client, device.uuid) + device_api = await _api_device_get(async_client, device.id) assert device_api["auth_token"] == device.auth_token diff --git a/tests/updater/controller/v1/test_routes.py b/tests/updater/controller/v1/test_routes.py index 1662e963..c60eb855 100644 --- a/tests/updater/controller/v1/test_routes.py +++ b/tests/updater/controller/v1/test_routes.py @@ -4,7 +4,7 @@ from goosebit.device_manager import DeviceManager, get_device from goosebit.settings import GooseBitSettings -UUID = "221326d9-7873-418e-960c-c074026a3b7c" +DEVICE_ID = "221326d9-7873-418e-960c-c074026a3b7c" config = GooseBitSettings() @@ -12,7 +12,7 @@ async def _api_device_update(async_client, device, update_attribute, update_value): response = await async_client.patch( f"/ui/bff/devices", - json={"devices": [f"{device.uuid}"], update_attribute: update_value}, + json={"devices": [f"{device.id}"], update_attribute: update_value}, ) assert response.status_code == 200 @@ -21,7 +21,7 @@ async def _api_device_get(async_client, dev_id): response = await async_client.get("/api/v1/devices") assert response.status_code == 200 devices = response.json()["devices"] - return next(d for d in devices if d["uuid"] == dev_id) + return next(d for d in devices if d["id"] == dev_id) async def _api_rollout_create(async_client, feed, software, paused): @@ -46,14 +46,14 @@ async def _api_rollouts_get(async_client): async def _poll_first_time(async_client): - response = await async_client.get(f"/ddi/controller/v1/{UUID}") + response = await async_client.get(f"/DEFAULT/controller/v1/{DEVICE_ID}") assert response.status_code == 200 data = response.json() assert "config" in data assert data["config"]["polling"]["sleep"] == "00:00:10" assert "_links" in data config_url = data["_links"]["configData"]["href"] - assert config_url == f"http://test/ddi/controller/v1/{UUID}/configData" + assert config_url == f"http://test/DEFAULT/controller/v1/{DEVICE_ID}/configData" return config_url @@ -80,8 +80,8 @@ async def _register(async_client, config_url): assert data["message"] == "Updated swupdate data." -async def _poll(async_client, device_uuid, software: Software | None, expect_update=True): - response = await async_client.get(f"/ddi/controller/v1/{device_uuid}") +async def _poll(async_client, device_id, software: Software | None, expect_update=True): + response = await async_client.get(f"/DEFAULT/controller/v1/{device_id}") assert response.status_code == 200 data = response.json() @@ -90,7 +90,7 @@ async def _poll(async_client, device_uuid, software: Software | None, expect_upd assert "deploymentBase" in data["_links"], "expected update, but none available" deployment_base = data["_links"]["deploymentBase"]["href"] assert software is not None - assert deployment_base == f"http://test/ddi/controller/v1/{device_uuid}/deploymentBase/{software.id}" + assert deployment_base == f"http://test/DEFAULT/controller/v1/{device_id}/deploymentBase/{software.id}" return deployment_base else: assert data["config"]["polling"]["sleep"] == config.poll_time @@ -98,7 +98,7 @@ async def _poll(async_client, device_uuid, software: Software | None, expect_upd return None -async def _retrieve_software_url(async_client, device_uuid, deployment_base, software): +async def _retrieve_software_url(async_client, device_id, deployment_base, software): response = await async_client.get(deployment_base) assert response.status_code == 200 data = response.json() @@ -107,7 +107,7 @@ async def _retrieve_software_url(async_client, device_uuid, deployment_base, sof assert data["id"] == str(software.id) assert ( data["deployment"]["chunks"][0]["artifacts"][0]["_links"]["download"]["href"] - == f"http://test/ddi/controller/v1/{device_uuid}/download" + == f"http://test/DEFAULT/controller/v1/{device_id}/download" ) assert data["deployment"]["chunks"][0]["artifacts"][0]["hashes"]["sha1"] == software.hash assert data["deployment"]["chunks"][0]["artifacts"][0]["size"] == software.size @@ -115,9 +115,9 @@ async def _retrieve_software_url(async_client, device_uuid, deployment_base, sof return data["deployment"]["chunks"][0]["artifacts"][0]["_links"]["download"]["href"] -async def _feedback(async_client, device_uuid, software, finished, execution, details=""): +async def _feedback(async_client, device_id, software, finished, execution, details=""): response = await async_client.post( - f"/ddi/controller/v1/{device_uuid}/deploymentBase/{software.id}/feedback", + f"/DEFAULT/controller/v1/{device_id}/deploymentBase/{software.id}/feedback", json={ "id": software.id, "status": { @@ -136,9 +136,9 @@ async def test_register_device(async_client, test_data): await _register(async_client, config_url) - await _poll(async_client, UUID, None, False) + await _poll(async_client, DEVICE_ID, None, False) - device_api = await _api_device_get(async_client, UUID) + device_api = await _api_device_get(async_client, DEVICE_ID) assert device_api["last_state"] == "Registered" assert device_api["sw_version"] == "8.8.1-12-g302f635+189128" assert device_api["hw_model"] == "smart-gateway-mt7688" @@ -152,13 +152,13 @@ async def test_rollout_full(async_client, test_data, delete_software): software = test_data["software_release"] rollout = test_data["rollout_default"] - deployment_base = await _poll(async_client, device.uuid, software) + deployment_base = await _poll(async_client, device.id, software) - await _retrieve_software_url(async_client, device.uuid, deployment_base, software) + await _retrieve_software_url(async_client, device.id, deployment_base, software) # confirm installation start (in reality: several of similar posts) - await _feedback(async_client, device.uuid, software, "none", "proceeding") - device_api = await _api_device_get(async_client, device.uuid) + await _feedback(async_client, device.id, software, "none", "proceeding") + device_api = await _api_device_get(async_client, device.id) assert device_api["last_state"] == "Running" # edge case: remove software during update @@ -166,8 +166,8 @@ async def test_rollout_full(async_client, test_data, delete_software): await Software.delete(software) # report finished installation - await _feedback(async_client, device.uuid, software, "success", "closed") - device_api = await _api_device_get(async_client, device.uuid) + await _feedback(async_client, device.id, software, "success", "closed") + device_api = await _api_device_get(async_client, device.id) assert device_api["last_state"] == "Finished" if not delete_software: @@ -184,13 +184,13 @@ async def test_rollout_signalling_download_failure(async_client, test_data): device = test_data["device_rollout"] software = test_data["software_release"] - deployment_base = await _poll(async_client, device.uuid, software) + deployment_base = await _poll(async_client, device.id, software) - software_url = await _retrieve_software_url(async_client, device.uuid, deployment_base, software) + software_url = await _retrieve_software_url(async_client, device.id, deployment_base, software) # confirm installation start (in reality: several of similar posts) - await _feedback(async_client, device.uuid, software, "none", "proceeding") - device_api = await _api_device_get(async_client, device.uuid) + await _feedback(async_client, device.id, software, "none", "proceeding") + device_api = await _api_device_get(async_client, device.id) assert device_api["last_state"] == "Running" # HEAD /api/v1/download/1 HTTP/1.1 (reason not clear) @@ -203,8 +203,8 @@ async def test_rollout_signalling_download_failure(async_client, test_data): assert response.status_code == 200 # report failure - await _feedback(async_client, device.uuid, software, "failure", "closed") - device_api = await _api_device_get(async_client, device.uuid) + await _feedback(async_client, device.id, software, "failure", "closed") + device_api = await _api_device_get(async_client, device.id) assert device_api["last_state"] == "Error" @@ -217,19 +217,19 @@ async def test_rollout_selection(async_client, test_data): software_rc = test_data["software_rc"] software_release = test_data["software_release"] - await _poll(async_client, device.uuid, None, False) + await _poll(async_client, device.id, None, False) await _api_rollout_create(async_client, "qa", software_beta, False) - await _poll(async_client, device.uuid, software_beta) + await _poll(async_client, device.id, software_beta) await _api_rollout_create(async_client, "live", software_rc, False) - await _poll(async_client, device.uuid, software_beta) + await _poll(async_client, device.id, software_beta) await _api_rollout_create(async_client, "qa", software_release, True) - await _poll(async_client, device.uuid, software_beta) + await _poll(async_client, device.id, software_beta) await _api_rollout_create(async_client, "qa", software_release, False) - await _poll(async_client, device.uuid, software_release) + await _poll(async_client, device.id, software_release) @pytest.mark.asyncio @@ -239,18 +239,18 @@ async def test_latest(async_client, test_data): await _api_device_update(async_client, device, "software", "latest") - deployment_base = await _poll(async_client, device.uuid, software) + deployment_base = await _poll(async_client, device.id, software) - await _retrieve_software_url(async_client, device.uuid, deployment_base, software) + await _retrieve_software_url(async_client, device.id, deployment_base, software) # confirm installation start (in reality: several of similar posts) - await _feedback(async_client, device.uuid, software, "none", "proceeding") - device_api = await _api_device_get(async_client, device.uuid) + await _feedback(async_client, device.id, software, "none", "proceeding") + device_api = await _api_device_get(async_client, device.id) assert device_api["last_state"] == "Running" # report finished installation - await _feedback(async_client, device.uuid, software, "success", "closed") - device_api = await _api_device_get(async_client, device.uuid) + await _feedback(async_client, device.id, software, "success", "closed") + device_api = await _api_device_get(async_client, device.id) assert device_api["last_state"] == "Finished" assert device_api["sw_version"] == software.version @@ -265,7 +265,7 @@ async def test_latest_with_no_software_available(async_client, test_data): device.hardware_id = fake_hardware.id await device.save() - await _poll(async_client, device.uuid, None, False) + await _poll(async_client, device.id, None, False) @pytest.mark.asyncio @@ -274,7 +274,7 @@ async def test_pinned(async_client, test_data): await _api_device_update(async_client, device, "pinned", True) - await _poll(async_client, device.uuid, None, False) + await _poll(async_client, device.id, None, False) @pytest.mark.asyncio @@ -284,14 +284,14 @@ async def test_up_to_date(async_client, test_data): await _api_device_update(async_client, device, "software", "latest") - device = await get_device(dev_id=device.uuid) + device = await get_device(dev_id=device.id) await DeviceManager.update_sw_version(device, software.version) - await _poll(async_client, device.uuid, None, False) + await _poll(async_client, device.id, None, False) async def _assert_log_lines(async_client, device, expected_line_count): - response = await async_client.get(f"/ui/bff/devices/{device.uuid}/log") + response = await async_client.get(f"/ui/bff/devices/{device.id}/log") assert response.status_code == 200 log = response.json()["log"] @@ -312,24 +312,24 @@ async def test_update_logs_and_progress(async_client, test_data): await _api_device_update(async_client, device, "software", "latest") - deployment_base = await _poll(async_client, device.uuid, software) + deployment_base = await _poll(async_client, device.id, software) await _assert_log_lines(async_client, device, 0) - await _retrieve_software_url(async_client, device.uuid, deployment_base, software) + await _retrieve_software_url(async_client, device.id, deployment_base, software) # confirm installation start (in reality: several of similar posts) - await _feedback(async_client, device.uuid, software, "none", "proceeding", "Downloaded 7%") + await _feedback(async_client, device.id, software, "none", "proceeding", "Downloaded 7%") await _assert_log_lines(async_client, device, 1) - device_api = await _api_device_get(async_client, device.uuid) + device_api = await _api_device_get(async_client, device.id) assert device_api["last_state"] == "Running" assert device_api["progress"] == 7 - await _feedback(async_client, device.uuid, software, "none", "proceeding", "Installing Update Chunk Artifacts.") + await _feedback(async_client, device.id, software, "none", "proceeding", "Installing Update Chunk Artifacts.") await _assert_log_lines(async_client, device, 2) # report finished installation - await _feedback(async_client, device.uuid, software, "success", "closed") - device_api = await _api_device_get(async_client, device.uuid) + await _feedback(async_client, device.id, software, "success", "closed") + device_api = await _api_device_get(async_client, device.id) assert device_api["progress"] == 100 assert device_api["last_state"] == "Finished" assert device_api["sw_version"] == software.version @@ -337,5 +337,5 @@ async def test_update_logs_and_progress(async_client, test_data): await _assert_log_lines(async_client, device, 3) # fake installation start confirmation to check clearing of logs - await _feedback(async_client, device.uuid, software, "none", "proceeding", "Downloaded 1%") + await _feedback(async_client, device.id, software, "none", "proceeding", "Downloaded 1%") await _assert_log_lines(async_client, device, 1) diff --git a/tests/updates/test_swdesc.py b/tests/updates/test_swdesc.py index ff9dd8b4..0fddb4c7 100644 --- a/tests/updates/test_swdesc.py +++ b/tests/updates/test_swdesc.py @@ -5,6 +5,23 @@ from goosebit.updates.swdesc import parse_descriptor, parse_file +def test_parse_descriptor_no_compatibility_defined(): + desc = AttrDict( + { + "software": { + "version": "1.0", + "description": "Software update for XXXXX Project", + } + } + ) + + swdesc_attrs = parse_descriptor(desc) + assert swdesc_attrs["version"] == "1.0.0" + assert swdesc_attrs["compatibility"] == [ + {"hw_model": "default", "hw_revision": "default"}, + ] + + def test_parse_descriptor_simple(): # simplified example from https://sbabic.github.io/swupdate/sw-description.html#introduction desc = AttrDict( diff --git a/tests/util/__init__.py b/tests/util/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/util/test_version.py b/tests/util/test_version.py new file mode 100644 index 00000000..60db2457 --- /dev/null +++ b/tests/util/test_version.py @@ -0,0 +1,44 @@ +from goosebit.util.version import Version + + +def test_swupdate_default_numbering(): + version_1 = Version.parse("1.2.3.4") + version_2 = Version.parse("1.2.4.4") + version_3 = Version.parse("1.3.3.4") + version_4 = Version.parse("2.2.3.4") + + assert version_1 < version_2 + assert version_2 < version_3 + assert version_3 < version_4 + + +def test_swupdate_default_numbering_ignore_additional_fields(): + version_variant_1 = Version.parse("1.2.3.4") + version_variant_2 = Version.parse("1.2.3.4.5") + version_variant_3 = Version.parse("1.2.3.4.5.6") + + assert version_variant_1 == version_variant_2 + assert version_variant_2 == version_variant_3 + + +def test_semver_ordering(): + version_1 = Version.parse("1.2.3-beta") + version_2 = Version.parse("1.2.4-alpha") + + assert version_1 < version_2 + + +def test_semver_equal(): + version_1 = Version.parse("1") + version_2 = Version.parse("1.0.0+build20") + + assert version_1 == version_2 + + +def test_lexical_order_fallback(): + version_1 = Version.parse("1.2.3.4") + version_2 = Version.parse("1.2.3-beta") + version_3 = Version.parse("1.2.3+tag1") + + assert version_1 > version_2 + assert version_1 > version_3