diff --git a/packages/jumpstarter-driver-corellium/fixtures/http/get-models-200.json b/packages/jumpstarter-driver-corellium/fixtures/http/get-models-200.json index 963966328..e42e72e39 100644 --- a/packages/jumpstarter-driver-corellium/fixtures/http/get-models-200.json +++ b/packages/jumpstarter-driver-corellium/fixtures/http/get-models-200.json @@ -5,10 +5,6 @@ "flavor": "rpi4b", "description": "Raspberry Pi 4", "model": "rpi4b", - "peripherals": false, - "quotas": { - "cores": 4, - "cpus": 4 - } + "peripherals": false } ] diff --git a/packages/jumpstarter-driver-corellium/jumpstarter_driver_corellium/corellium/api.py b/packages/jumpstarter-driver-corellium/jumpstarter_driver_corellium/corellium/api.py index ec21fc99a..ecf56c0e7 100644 --- a/packages/jumpstarter-driver-corellium/jumpstarter_driver_corellium/corellium/api.py +++ b/packages/jumpstarter-driver-corellium/jumpstarter_driver_corellium/corellium/api.py @@ -1,134 +1,90 @@ from typing import Optional -import requests - -from .exceptions import CorelliumApiException -from .types import Device, Instance, Project, Session +import corellium_api +from corellium_api import Instance, Model, Project class ApiClient: """ Corellium ReST API client used by the Corellium driver. """ - session: Session - req: requests.Session def __init__(self, host: str, token: str) -> None: """ Initializes a new client, containing a """ self.host = host - self.token = token - self.session = None - self.req = requests.Session() + + configuration = corellium_api.Configuration(host=self.baseurl, disabled_client_side_validations="multipleOf") + configuration.access_token = token + configuration.client_side_validation = False + corellium_api.Configuration.set_default(configuration) + self.api = corellium_api.CorelliumApi(corellium_api.ApiClient(configuration)) @property def baseurl(self) -> str: """ Return the baseurl path for API calls. """ - return f'https://{self.host}/api' - - def login(self) -> None: - """ - Login against Corellium's ReST API. - - Set an internal Session object instance to be used - in other API calls that require authentication. - - It uses the global requests objects so a new session can be generated. - """ - data = { - 'apiToken': self.token - } - - try: - res = requests.post(f'{self.baseurl}/v1/auth/login', json=data) - data = res.json() - res.raise_for_status() - except (requests.exceptions.RequestException, requests.exceptions.HTTPError) as e: - raise CorelliumApiException(data.get('error', str(e))) from e - - self.session = Session(**data) - self.req.headers.update(self.session.as_header()) + return f"https://{self.host}/api" - def get_project(self, project_ref: str = 'Default Project') -> Optional[Project]: + async def get_project(self, project_ref: str = "Default Project") -> Optional[Project]: """ Retrieve a project based on project_ref, which is either its id or name. """ - try: - res = self.req.get(f'{self.baseurl}/v1/projects') - data = res.json() - res.raise_for_status() - except requests.exceptions.RequestException as e: - raise CorelliumApiException(data.get('error', str(e))) from e - for project in data: - if project['name'] == project_ref or project['id'] == project_ref: - return Project(id=project['id'], name=project['name']) + projects = await self.api.v1_get_projects() + for project in projects: + if project.name == project_ref or project.id == project_ref: + return project return None - def get_device(self, model: str) -> Optional[Device]: + async def get_device(self, model: str) -> Optional[Model]: """ Get a device spec from Corellium's list based on the model name. A device object is used to create a new virtual instance. """ - try: - res = self.req.get(f'{self.baseurl}/v1/models') - data = res.json() - res.raise_for_status() - except requests.exceptions.RequestException as e: - raise CorelliumApiException(data.get('error', str(e))) from e - for device in data: - if device['model'] == model: - return Device(**device) + models = await self.api.v1_get_models() + for device in models: + if device.model == model: + return device return None - def create_instance(self, name: str, project: Project, device: Device, os_version: str, os_build: str) -> Instance: + async def create_instance( + self, name: str, project: Project, device: Model, os_version: str, os_build: str + ) -> Instance: """ Create a new virtual instance from a device spec. """ - data = { - 'name': name, - 'project': project.id, - 'flavor': device.flavor, - 'os': os_version, - 'osbuild': os_build, - } - try: - res = self.req.post(f'{self.baseurl}/v1/instances', json=data) - data = res.json() - res.raise_for_status() - except requests.exceptions.RequestException as e: - raise CorelliumApiException(data.get('error', str(e))) from e + return await self.api.v1_create_instance( + corellium_api.InstanceCreateOptions( + name=name, + project=project.id, + flavor=device.flavor, + os=os_version, + osbuild=os_build, + ) + ) - return Instance(**data) - - def get_instance(self, instance_ref: str) -> Optional[Instance]: + async def get_instance(self, instance_ref: str) -> Optional[Instance]: """ Retrieve an existing instance by its name. Return None if it does not exist. """ - try: - res = self.req.get(f'{self.baseurl}/v1/instances') - data = res.json() - res.raise_for_status() - except requests.exceptions.RequestException as e: - raise CorelliumApiException(data.get('error', str(e))) from e - - for instance in data: - if instance['name'] == instance_ref or instance['id'] == instance_ref: - return Instance(id=instance['id'], state=instance['state']) + instances = await self.api.v1_get_instances() + for instance in instances: + if instance.name == instance_ref or instance.id == instance_ref: + return instance return None - def set_instance_state(self, instance: Instance, instance_state: str) -> None: + async def set_instance_state(self, instance: Instance, instance_state: str) -> None: """ Set the virtual instance state from corellium. @@ -144,30 +100,17 @@ def set_instance_state(self, instance: Instance, instance_state: str) -> None: - rebooting - error """ - data = { - 'state': instance_state - } - - try: - res = self.req.put(f'{self.baseurl}/v1/instances/{instance.id}/state', json=data) - data = res.json() if res.status_code != 204 else None - res.raise_for_status() - except requests.exceptions.RequestException as e: - msgerr = data if data is not None else str(e) - raise CorelliumApiException(msgerr) from e + await self.api.v1_set_instance_state( + instance.id, + corellium_api.V1SetStateBody(state=instance_state), + ) - def destroy_instance(self, instance: Instance) -> None: + async def destroy_instance(self, instance: Instance) -> None: """ Delete a virtual instance. Does not return anything since Corellium's API return a HTTP 204 response. """ - try: - res = self.req.delete(f'{self.baseurl}/v1/instances/{instance.id}') - data = res.json() if res.status_code != 204 else None - res.raise_for_status() - except requests.exceptions.RequestException as e: - msgerr = data if data is not None else str(e) - raise CorelliumApiException(msgerr) from e + await self.api.v1_delete_instance(instance.id) diff --git a/packages/jumpstarter-driver-corellium/jumpstarter_driver_corellium/corellium/api_test.py b/packages/jumpstarter-driver-corellium/jumpstarter_driver_corellium/corellium/api_test.py deleted file mode 100644 index b342b2ee9..000000000 --- a/packages/jumpstarter-driver-corellium/jumpstarter_driver_corellium/corellium/api_test.py +++ /dev/null @@ -1,182 +0,0 @@ -import os - -import pytest - -from .api import ApiClient -from .exceptions import CorelliumApiException -from .types import Device, Instance, Project, Session - - -def fixture(path): - """ - Load file contents from fixtures/$path. - """ - cwd = os.path.dirname(os.path.abspath(__file__)) - fixtures_dir = f'{cwd}/../../fixtures' - - with open(f'{fixtures_dir}/{path}', 'r') as f: - return f.read() - - -def test_login_ok(requests_mock): - requests_mock.post('https://api-host/api/v1/auth/login', text=fixture('http/login-200.json')) - - api = ApiClient('api-host', 'api-token') - api.login() - - assert 'session-token' == api.session.token - assert '2022-03-20T01:50:10.000Z' == api.session.expiration - assert {'Authorization': 'Bearer session-token'} == api.session.as_header() - - -@pytest.mark.parametrize( - 'status_code,data,msg', - [ - (403, fixture('http/403.json'), 'Invalid or missing authorization token'), - (200, fixture('http/json-error.json'), 'Invalid control character at'), - ]) -def test_login_error(requests_mock, status_code, data, msg): - requests_mock.post('https://api-host/api/v1/auth/login', status_code=status_code, text=data) - api = ApiClient('api-host', 'api-token') - - with pytest.raises(CorelliumApiException) as e: - api.login() - - assert msg in str(e.value) - assert api.session is None - - -@pytest.mark.parametrize('project_name,data,has_results', [ - ('OtherProject', fixture('http/get-projects-200.json'), True), - (None, fixture('http/get-projects-200.json'), True), - ('notfound', fixture('http/get-projects-200.json'), False) -]) -def test_get_project_ok(requests_mock, project_name, data, has_results): - requests_mock.get('https://api-host/api/v1/projects', status_code=200, text=data) - api = ApiClient('api-host', 'api-token') - api.session = Session('session-token', '2022-03-20T01:50:10.000Z') - - args = [] - if project_name: - args.append(project_name) - project = api.get_project(*args) - - if has_results: - assert project is not None - assert project.name == project_name if project_name is not None else 'Default Project' - else: - assert project is None - - -@pytest.mark.parametrize( - 'status_code,data,msg', - [ - (403, fixture('http/403.json'), 'Invalid or missing authorization token'), - (404, fixture('http/get-projects-404.json'), ''), - ]) -def test_get_project_error(requests_mock, status_code, data, msg): - requests_mock.get('https://api-host/api/v1/projects', status_code=status_code, text=data) - api = ApiClient('api-host', 'api-token') - api.session = Session('session-token', '2022-03-20T01:50:10.000Z') - - with pytest.raises(CorelliumApiException) as e: - api.get_project() - - assert msg in str(e.value) - - -@pytest.mark.parametrize('model,data,has_results', [ - ('rpi4b', fixture('http/get-models-200.json'), True), - ('notfound', fixture('http/get-models-200.json'), False) -]) -def test_get_device_ok(requests_mock, model, data, has_results): - requests_mock.get('https://api-host/api/v1/models', status_code=200, text=data) - api = ApiClient('api-host', 'api-token') - api.session = Session('session-token', '2022-03-20T01:50:10.000Z') - - device = api.get_device(model) - - if has_results: - assert device is not None - assert device.model == model - else: - assert device is None - - -@pytest.mark.parametrize( - 'status_code,data,msg', - [ - (403, fixture('http/403.json'), 'Invalid or missing authorization token'), - ]) -def test_get_device_error(requests_mock, status_code, data, msg): - requests_mock.get('https://api-host/api/v1/models', status_code=status_code, text=data) - api = ApiClient('api-host', 'api-token') - api.session = Session('session-token', '2022-03-20T01:50:10.000Z') - - with pytest.raises(CorelliumApiException) as e: - api.get_device('mymodel') - - assert msg in str(e.value) - - -def test_create_instance_ok(requests_mock): - data = fixture('http/create-instance-200.json') - requests_mock.post('https://api-host/api/v1/instances', status_code=200, text=data) - api = ApiClient('api-host', 'api-token') - api.session = Session('session-token', '2022-03-20T01:50:10.000Z') - - project = Project('d59db33d-27bd-4b22-878d-49e4758a648e', 'Default Project') - device = Device(name='rd1ae', type='automotive', flavor='kronos', - description='', model='kronos', peripherals=False, quotas={}) - instance = api.create_instance('my-instance', project, device, '1.1.1', 'Critical Application Monitor (Baremetal)') - - assert instance is not None - assert instance.id - - -@pytest.mark.parametrize( - 'status_code,data,msg', - [ - (403, fixture('http/403.json'), 'Invalid or missing authorization token'), - (400, fixture('http/create-instance-400.json'), 'Unsupported device model'), - ]) -def test_create_instance_error(requests_mock, status_code, data, msg): - requests_mock.post('https://api-host/api/v1/instances', status_code=status_code, text=data) - api = ApiClient('api-host', 'api-token') - api.session = Session('session-token', '2022-03-20T01:50:10.000Z') - - with pytest.raises(CorelliumApiException) as e: - project = Project('d59db33d-27bd-4b22-878d-49e4758a648e', 'Default Project') - device = Device(name='rd1ae', type='automotive', flavor='kronos', - description='', model='kronos', peripherals=False, quotas={}) - api.create_instance('my-instance', project, device, '1.1.1', 'Critical Application Monitor (Baremetal)') - - assert msg in str(e.value) - - -def test_destroy_instance_state_ok(requests_mock): - instance = Instance(id='d59db33d-27bd-4b22-878d-49e4758a648e') - - requests_mock.delete(f'https://api-host/api/v1/instances/{instance.id}', status_code=204, text='') - api = ApiClient('api-host', 'api-token') - api.session = Session('session-token', '2022-03-20T01:50:10.000Z') - api.destroy_instance(instance) - - -@pytest.mark.parametrize( - 'status_code,data,msg', - [ - (403, fixture('http/403.json'), 'Invalid or missing authorization token'), - (404, fixture('http/get-instance-state-404.json'), 'No instance associated with this value'), - ]) -def test_destroy_instance_error(requests_mock, status_code, data, msg): - instance = Instance(id='d59db33d-27bd-4b22-878d-49e4758a648e') - - requests_mock.delete(f'https://api-host/api/v1/instances/{instance.id}', status_code=status_code, text=data) - api = ApiClient('api-host', 'api-token') - api.session = Session('session-token', '2022-03-20T01:50:10.000Z') - - with pytest.raises(CorelliumApiException) as e: - api.destroy_instance(instance) - - assert msg in str(e.value) diff --git a/packages/jumpstarter-driver-corellium/jumpstarter_driver_corellium/corellium/exceptions.py b/packages/jumpstarter-driver-corellium/jumpstarter_driver_corellium/corellium/exceptions.py deleted file mode 100644 index dc0876a35..000000000 --- a/packages/jumpstarter-driver-corellium/jumpstarter_driver_corellium/corellium/exceptions.py +++ /dev/null @@ -1,10 +0,0 @@ -""" -Corellium API client exceptions module -""" -from jumpstarter.common.exceptions import JumpstarterException - - -class CorelliumApiException(JumpstarterException): - """ - Exception raised when something goes wrong with Corellium's API. - """ diff --git a/packages/jumpstarter-driver-corellium/jumpstarter_driver_corellium/corellium/types.py b/packages/jumpstarter-driver-corellium/jumpstarter_driver_corellium/corellium/types.py deleted file mode 100644 index 88716e2ee..000000000 --- a/packages/jumpstarter-driver-corellium/jumpstarter_driver_corellium/corellium/types.py +++ /dev/null @@ -1,56 +0,0 @@ -""" -Corellium API types. -""" -from dataclasses import dataclass, field -from typing import Dict, Optional - - -@dataclass -class Session: - """ - Session data class to hold Corellium's API session data. - """ - token: str - expiration: str - - def as_header(self) -> Dict[str, str]: - """ - Return a dict to be used as HTTP header for authenticated requests. - """ - return { - 'Authorization': f'Bearer {self.token}' - } - - -@dataclass -class Project: - """ - Dataclass that represents a Corellium project. - """ - id: str - name: str - - -@dataclass -class Device: - """ - Dataclass to represent a Corellium Device. - - A device object is used to create virtual instances. - """ - name: str - type: str - flavor: str - description: str - model: str - peripherals: bool - quotas: dict - - -@dataclass -class Instance: - """ - Virtual instance dataclass. - """ - id: str - state: Optional[str] = field(default=None) diff --git a/packages/jumpstarter-driver-corellium/jumpstarter_driver_corellium/driver.py b/packages/jumpstarter-driver-corellium/jumpstarter_driver_corellium/driver.py index fbb76332e..d0851f79c 100644 --- a/packages/jumpstarter-driver-corellium/jumpstarter_driver_corellium/driver.py +++ b/packages/jumpstarter-driver-corellium/jumpstarter_driver_corellium/driver.py @@ -1,17 +1,17 @@ """ Jumpstarter corellium driver(s) implementation module. """ + import os -import time from collections.abc import AsyncGenerator from dataclasses import dataclass, field -from datetime import datetime, timedelta from typing import Dict, Optional +from anyio import sleep +from corellium_api import Instance from jumpstarter_driver_power.driver import PowerReading, VirtualPowerInterface from .corellium.api import ApiClient -from .corellium.types import Instance from jumpstarter.common import exceptions as jmp_exceptions from jumpstarter.driver import Driver, export @@ -21,19 +21,20 @@ class Corellium(Driver): """ Corellium top-level driver. """ + _api: ApiClient = field(init=False) project_id: str device_name: str device_flavor: str - device_os: str = field(default='1.1.1') - device_build: str = field(default='Critical Application Monitor (Baremetal)') + device_os: str = field(default="1.1.1") + device_build: str = field(default="Critical Application Monitor (Baremetal)") @classmethod def client(cls) -> str: """ Return the driver's client. """ - return 'jumpstarter_driver_corellium.client.CorelliumClient' + return "jumpstarter_driver_corellium.client.CorelliumClient" def __post_init__(self) -> None: """ @@ -54,11 +55,11 @@ def __post_init__(self) -> None: if hasattr(super(), "__post_init__"): super().__post_init__() - api_host = self.get_env_var('CORELLIUM_API_HOST') - api_token = self.get_env_var('CORELLIUM_API_TOKEN') + api_host = self.get_env_var("CORELLIUM_API_HOST") + api_token = self.get_env_var("CORELLIUM_API_TOKEN") self._api = ApiClient(api_host, api_token) - self.children['power'] = CorelliumPower(parent=self) + self.children["power"] = CorelliumPower(parent=self) def get_env_var(self, name: str) -> str: """ @@ -68,7 +69,7 @@ def get_env_var(self, name: str) -> str: value = os.environ.get(name) if value is None: - raise jmp_exceptions.ConfigurationError(f'Missing "{name}" environment variable') + raise jmp_exceptions.ConfigurationError(f'Missing "{name}" environment variable') value = value.strip() @@ -85,18 +86,6 @@ def api(self): It will also be responsible for creating/refreshing the session token used across different API methods that require authentication. """ - # session does not exist, just login and return - if self._api.session is None: - self._api.login() - - return self._api - - # check if session is about to expire - # currently depends on the magic number of 60 seconds - now = datetime.utcnow() - diff = datetime.strptime(self._api.session.expiration, '%Y-%m-%dT%H:%M:%S.%fZ') - now - if diff > timedelta(seconds=1): - self._api.login() return self._api @@ -108,6 +97,7 @@ class CorelliumPower(VirtualPowerInterface, Driver): This driver will create and destroy virtual instances. """ + parent: Corellium def get_timeout_opts(self) -> Dict[str, int]: @@ -115,11 +105,11 @@ def get_timeout_opts(self) -> Dict[str, int]: Return config/opts to be used when waiting for Corellium's API. """ return { - 'retries': int(os.environ.get('CORELLIUM_API_RETRIES', 12)), - 'interval': os.environ.get('CORELLIUM_API_INTERVAL', 5) + "retries": int(os.environ.get("CORELLIUM_API_RETRIES", 12)), + "interval": os.environ.get("CORELLIUM_API_INTERVAL", 5), } - def wait_instance(self, current: Instance, desired: Optional[Instance]): + async def wait_instance(self, current: Instance, desired: Optional[Instance]): """ Wait for `current` instance to reach the same state as the `desired` instance. @@ -129,74 +119,79 @@ def wait_instance(self, current: Instance, desired: Optional[Instance]): counter = 0 while True: - if counter >= opts['retries']: - raise ValueError(f'Instance took too long to be reach the desired state: {current}') + if counter >= opts["retries"]: + raise ValueError(f"Instance took too long to be reach the desired state: {current}") + + current = await self.parent.api.get_instance(current.id) + + if current == desired: + break - if self.parent.api.get_instance(current.id) == desired: + if current is not None and desired is not None and current.state == desired.state: break counter += 1 - time.sleep(opts['interval']) + await sleep(opts["interval"]) @export - def on(self) -> None: + async def on(self) -> None: """ Power a Corellium virtual device on. It will create an instance if one does not exist, it will just power the existing one on otherwise. """ - self.logger.info('Corellium Device:') - self.logger.info(f'\tDevice Name: {self.parent.device_name}') - self.logger.info(f'\tDevice Flavor: {self.parent.device_flavor}') - self.logger.info(f'\tDevice OS Version: {self.parent.device_os}') + self.logger.info("Corellium Device:") + self.logger.info(f"\tDevice Name: {self.parent.device_name}") + self.logger.info(f"\tDevice Flavor: {self.parent.device_flavor}") + self.logger.info(f"\tDevice OS Version: {self.parent.device_os}") - project = self.parent.api.get_project(self.parent.project_id) + project = await self.parent.api.get_project(self.parent.project_id) if project is None: - raise ValueError(f'Unable to fetch project: {self.parent.project_id}') - self.logger.info(f'Using project: {project.name}') + raise ValueError(f"Unable to fetch project: {self.parent.project_id}") + self.logger.info(f"Using project: {project.name}") - device = self.parent.api.get_device(self.parent.device_flavor) + device = await self.parent.api.get_device(self.parent.device_flavor) if device is None: - raise ValueError('Unable to find a device for this model: {self.parent.device_model}') - self.logger.info(f'Using device spec: {device.name}') + raise ValueError("Unable to find a device for this model: {self.parent.device_model}") + self.logger.info(f"Using device spec: {device.name}") # retrieve an existing instance first - instance = self.parent.api.get_instance(self.parent.device_name) + instance = await self.parent.api.get_instance(self.parent.device_name) if instance: - self.parent.api.set_instance_state(instance, 'on') + await self.parent.api.set_instance_state(instance, "on") # create a new one otherwise else: opts = {} if self.parent.device_os: - opts['os_version'] = self.parent.device_os + opts["os_version"] = self.parent.device_os if self.parent.device_build: - opts['os_build'] = self.parent.device_build - instance = self.parent.api.create_instance(self.parent.device_name, project, device, **opts) - self.logger.info(f'Instance: {self.parent.device_name} (ID: {instance.id})') + opts["os_build"] = self.parent.device_build + instance = await self.parent.api.create_instance(self.parent.device_name, project, device, **opts) + self.logger.info(f"Instance: {self.parent.device_name} (ID: {instance.id})") - self.wait_instance(instance, Instance(id=instance.id, state='on')) + await self.wait_instance(instance, Instance(id=instance.id, state="on")) @export - def off(self, destroy: bool = False) -> None: + async def off(self, destroy: bool = False) -> None: """ Destroy a Corellium virtual device/instance. """ # fail if project does not exist - project = self.parent.api.get_project(self.parent.project_id) + project = await self.parent.api.get_project(self.parent.project_id) if project is None: - raise ValueError(f'Unable to fetch project: {self.parent.project_id}') + raise ValueError(f"Unable to fetch project: {self.parent.project_id}") # get instance and fail if instance does not exist - instance = self.parent.api.get_instance(self.parent.device_name) + instance = await self.parent.api.get_instance(self.parent.device_name) if instance is None: - raise ValueError('Instance does not exist') + raise ValueError("Instance does not exist") - self.parent.api.set_instance_state(instance, 'off') - self.wait_instance(instance, Instance(id=instance.id, state='off')) + await self.parent.api.set_instance_state(instance, "off") + await self.wait_instance(instance, Instance(id=instance.id, state="off")) if destroy: - self.parent.api.destroy_instance(instance) - self.wait_instance(instance, None) + await self.parent.api.destroy_instance(instance) + await self.wait_instance(instance, None) @export def read(self) -> AsyncGenerator[PowerReading, None]: diff --git a/packages/jumpstarter-driver-corellium/jumpstarter_driver_corellium/driver_test.py b/packages/jumpstarter-driver-corellium/jumpstarter_driver_corellium/driver_test.py index 2feb97c57..f3c135621 100644 --- a/packages/jumpstarter-driver-corellium/jumpstarter_driver_corellium/driver_test.py +++ b/packages/jumpstarter-driver-corellium/jumpstarter_driver_corellium/driver_test.py @@ -1,156 +1,100 @@ from unittest.mock import patch import pytest +from corellium_api import Instance, Model, Project -from .corellium.exceptions import CorelliumApiException -from .corellium.types import Device, Instance, Project, Session from .driver import Corellium, CorelliumPower from jumpstarter.common import exceptions as jmp_exceptions +pytestmark = pytest.mark.anyio -def test_driver_corellium_init_ok(monkeypatch): - monkeypatch.setenv('CORELLIUM_API_HOST', 'api-host') - monkeypatch.setenv('CORELLIUM_API_TOKEN', 'api-token') - c = Corellium(project_id='1', device_name='jmp', device_flavor='kronos', device_os='1.0') +@pytest.fixture +def anyio_backend(): + return "asyncio" - assert '1' == c.project_id - assert 'jmp' == c.device_name - assert 'kronos' == c.device_flavor - assert '1.0' == c.device_os - assert 'api-host' == c._api.host - assert 'api-token' == c._api.token + +async def test_driver_corellium_init_ok(monkeypatch): + monkeypatch.setenv("CORELLIUM_API_HOST", "api-host") + monkeypatch.setenv("CORELLIUM_API_TOKEN", "api-token") + + c = Corellium(project_id="1", device_name="jmp", device_flavor="kronos", device_os="1.0") + + assert "1" == c.project_id + assert "jmp" == c.device_name + assert "kronos" == c.device_flavor + assert "1.0" == c.device_os @pytest.mark.parametrize( - 'env,err', - [ - ( - {}, - jmp_exceptions.ConfigurationError('Missing "CORELLIUM_API_HOST" environment variable') - ), - ( - {'CORELLIUM_API_HOST': ' '}, - jmp_exceptions.ConfigurationError('"CORELLIUM_API_HOST" environment variable is empty') - ), - ( - {'CORELLIUM_API_HOST': 'api-host'}, - jmp_exceptions.ConfigurationError('Missing "CORELLIUM_API_TOKEN" environment variable') - ), - ( - {'CORELLIUM_API_HOST': 'api-host', 'CORELLIUM_API_TOKEN': ' '}, - jmp_exceptions.ConfigurationError('"CORELLIUM_API_TOKEN" environment variable is empty') - ), - ]) -def test_driver_corellium_init_error(monkeypatch, env, err): - monkeypatch.delenv('CORELLIUM_API_HOST', raising=False) - monkeypatch.delenv('CORELLIUM_API_TOKEN', raising=False) + "env,err", + [ + ({}, jmp_exceptions.ConfigurationError('Missing "CORELLIUM_API_HOST" environment variable')), + ( + {"CORELLIUM_API_HOST": " "}, + jmp_exceptions.ConfigurationError('"CORELLIUM_API_HOST" environment variable is empty'), + ), + ( + {"CORELLIUM_API_HOST": "api-host"}, + jmp_exceptions.ConfigurationError('Missing "CORELLIUM_API_TOKEN" environment variable'), + ), + ( + {"CORELLIUM_API_HOST": "api-host", "CORELLIUM_API_TOKEN": " "}, + jmp_exceptions.ConfigurationError('"CORELLIUM_API_TOKEN" environment variable is empty'), + ), + ], +) +async def test_driver_corellium_init_error(monkeypatch, env, err): + monkeypatch.delenv("CORELLIUM_API_HOST", raising=False) + monkeypatch.delenv("CORELLIUM_API_TOKEN", raising=False) for k, v in env.items(): monkeypatch.setenv(k, v) with pytest.raises(type(err)) as e: - Corellium(project_id='1', device_name='jmp', device_flavor='kronos', device_os='1.0') + Corellium(project_id="1", device_name="jmp", device_flavor="kronos", device_os="1.0") assert str(err) == str(e.value) -def test_driver_api_client_ok(monkeypatch, requests_mock): - requests_mock.post('https://api-host/api/v1/auth/login', - text='{"token": "token", "expiration": "2022-03-20T01:50:10.000Z"}') - monkeypatch.setenv('CORELLIUM_API_HOST', 'api-host') - monkeypatch.setenv('CORELLIUM_API_TOKEN', 'api-token') - - c = Corellium(project_id='1', device_name='jmp', device_flavor='kronos', device_os='1.0') - - assert Session('token', '2022-03-20T01:50:10.000Z') == c.api.session - - -def test_driver_power_on_ok(monkeypatch): - monkeypatch.setenv('CORELLIUM_API_HOST', 'api-host') - monkeypatch.setenv('CORELLIUM_API_TOKEN', 'api-token') - - project = Project('1', 'Default Project') - device = Device(name='rd1ae', type='automotive', flavor='kronos', - description='', model='kronos', peripherals=False, quotas={}) - instance = Instance(id='7f4f241c-821f-4219-905f-c3b50b0db5dd', state='on') - root = Corellium(project_id='1', device_name='jmp', device_flavor='kronos', device_os='1.0') +async def test_driver_power_on_ok(monkeypatch): + monkeypatch.setenv("CORELLIUM_API_HOST", "api-host") + monkeypatch.setenv("CORELLIUM_API_TOKEN", "api-token") + + project = Project("1", "Default Project") + device = Model( + name="rd1ae", + type="automotive", + flavor="kronos", + description="", + model="kronos", + peripherals=False, + ) + instance = Instance(id="7f4f241c-821f-4219-905f-c3b50b0db5dd", state="on") + root = Corellium(project_id="1", device_name="jmp", device_flavor="kronos", device_os="1.0") power = CorelliumPower(parent=root) - with (patch.object(root._api, 'login', return_value=None), - patch.object(root._api, 'get_project', return_value=project), - patch.object(root._api, 'get_device', return_value=device), - patch.object(root._api, 'get_instance', side_effect=[None, instance]), - patch.object(root._api, 'create_instance', return_value=instance)): - power.on() - - -@pytest.mark.parametrize('mock_data', [ - ({'login': {'side_effect': CorelliumApiException('login error')}}), - ({'get_project': {'return_value': None}}), - ({'get_instance': {'return_value': None}}), - ({'create_instance': {'side_effect': CorelliumApiException('create error')}}), -]) -def test_driver_power_on_error(monkeypatch, mock_data): - monkeypatch.setenv('CORELLIUM_API_HOST', 'api-host') - monkeypatch.setenv('CORELLIUM_API_TOKEN', 'api-token') - - project = Project('1', 'Default Project') - instance = Instance(id='7f4f241c-821f-4219-905f-c3b50b0db5dd', state='on') - root = Corellium(project_id='1', device_name='jmp', device_flavor='kronos', device_os='1.0') - power = CorelliumPower(parent=root) + with ( + patch.object(root._api, "get_project", return_value=project), + patch.object(root._api, "get_device", return_value=device), + patch.object(root._api, "get_instance", side_effect=[None, instance]), + patch.object(root._api, "create_instance", return_value=instance), + ): + await power.on() - with pytest.raises((CorelliumApiException, ValueError)): - with (patch.object(root._api, 'login', - **mock_data.get('login', {'return_value': None})), - patch.object(root._api, 'get_project', - **mock_data.get('get_project', {'return_value': project})), - patch.object(root._api, 'get_instance', - **mock_data.get('get_instance', {'return_value': instance})), - patch.object(root._api, 'create_instance', - **mock_data.get('create_instance', {'return_value': instance}))): - power.off() - - -def test_driver_power_off_ok(monkeypatch): - monkeypatch.setenv('CORELLIUM_API_HOST', 'api-host') - monkeypatch.setenv('CORELLIUM_API_TOKEN', 'api-token') - - project = Project('1', 'Default Project') - instance = Instance(id='7f4f241c-821f-4219-905f-c3b50b0db5dd', state='on') - root = Corellium(project_id='1', device_name='jmp', device_flavor='kronos', device_os='1.0') - power = CorelliumPower(parent=root) - with (patch.object(root._api, 'login', return_value=None), - patch.object(root._api, 'get_project', return_value=project), - patch.object(root._api, 'set_instance_state', return_value=None), - patch.object(root._api, 'get_instance', - side_effect=[instance, Instance(id=instance.id, state='off')])): - power.off() - - -@pytest.mark.parametrize('mock_data',[ - ({'login': {'side_effect': CorelliumApiException('login error')}}), - ({'get_project': {'return_value': None}}), - ({'get_instance': {'return_value': None}}), - ({'destroy_instance': {'side_effect': CorelliumApiException('destroy error')}}), -]) -def test_driver_power_off_error(monkeypatch, mock_data): - monkeypatch.setenv('CORELLIUM_API_HOST', 'api-host') - monkeypatch.setenv('CORELLIUM_API_TOKEN', 'api-token') - - project = Project('1', 'Default Project') - instance = Instance(id='7f4f241c-821f-4219-905f-c3b50b0db5dd', state='on') - root = Corellium(project_id='1', device_name='jmp', device_flavor='kronos', device_os='1.0') +async def test_driver_power_off_ok(monkeypatch): + monkeypatch.setenv("CORELLIUM_API_HOST", "api-host") + monkeypatch.setenv("CORELLIUM_API_TOKEN", "api-token") + + project = Project("1", "Default Project") + instance = Instance(id="7f4f241c-821f-4219-905f-c3b50b0db5dd", state="on") + root = Corellium(project_id="1", device_name="jmp", device_flavor="kronos", device_os="1.0") power = CorelliumPower(parent=root) - with pytest.raises((CorelliumApiException, ValueError)): - with (patch.object(root._api, 'login', - **mock_data.get('login', {'return_value': None})), - patch.object(root._api, 'get_project', - **mock_data.get('get_project', {'return_value': project})), - patch.object(root._api, 'get_instance', - **mock_data.get('get_instance', {'side_effect': [instance, None]})), - patch.object(root._api, 'destroy_instance', - **mock_data.get('destroy_instance', {'return_value': instance}))): - power.off() + with ( + patch.object(root._api, "get_project", return_value=project), + patch.object(root._api, "set_instance_state", return_value=None), + patch.object(root._api, "get_instance", side_effect=[instance, Instance(id=instance.id, state="off")]), + ): + await power.off() diff --git a/packages/jumpstarter-driver-corellium/pyproject.toml b/packages/jumpstarter-driver-corellium/pyproject.toml index 65b13a09f..1ca6f64ec 100644 --- a/packages/jumpstarter-driver-corellium/pyproject.toml +++ b/packages/jumpstarter-driver-corellium/pyproject.toml @@ -12,13 +12,20 @@ dependencies = [ "jumpstarter-driver-power", "pyserial>=3.5", "asyncclick>=8.1.7.2", + "corellium-api>=0.4.0", ] [project.entry-points."jumpstarter.drivers"] Corellium = "jumpstarter_driver_corellium.driver:Corellium" [dependency-groups] -dev = ["pytest>=8.3.2", "pytest-cov>=5.0.0", "trio>=0.28.0", "requests_mock"] +dev = [ + "pytest>=8.3.2", + "pytest-cov>=5.0.0", + "trio>=0.28.0", + "requests_mock", + "pytest-aiohttp>=1.1.0", +] [tool.hatch.metadata.hooks.vcs.urls] Homepage = "https://jumpstarter.dev" diff --git a/uv.lock b/uv.lock index ed9d2fc7b..d4520587d 100644 --- a/uv.lock +++ b/uv.lock @@ -287,15 +287,15 @@ wheels = [ [[package]] name = "beautifulsoup4" -version = "4.13.3" +version = "4.13.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "soupsieve" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f0/3c/adaf39ce1fb4afdd21b611e3d530b183bb7759c9b673d60db0e347fd4439/beautifulsoup4-4.13.3.tar.gz", hash = "sha256:1bd32405dacc920b42b83ba01644747ed77456a65760e285fbc47633ceddaf8b", size = 619516 } +sdist = { url = "https://files.pythonhosted.org/packages/d8/e4/0c4c39e18fd76d6a628d4dd8da40543d136ce2d1752bd6eeeab0791f4d6b/beautifulsoup4-4.13.4.tar.gz", hash = "sha256:dbb3c4e1ceae6aefebdaf2423247260cd062430a410e38c66f2baa50a8437195", size = 621067 } wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/49/6abb616eb3cbab6a7cca303dc02fdf3836de2e0b834bf966a7f5271a34d8/beautifulsoup4-4.13.3-py3-none-any.whl", hash = "sha256:99045d7d3f08f91f0d656bc9b7efbae189426cd913d830294a15eefa0ea4df16", size = 186015 }, + { url = "https://files.pythonhosted.org/packages/50/cd/30110dc0ffcf3b131156077b90e9f60ed75711223f306da4db08eff8403b/beautifulsoup4-4.13.4-py3-none-any.whl", hash = "sha256:9bbbb14bfde9d79f38b8cd5f8c7c85f4b8f2523190ebed90e950a8dea4cb1c4b", size = 187285 }, ] [[package]] @@ -472,6 +472,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7e/a6/ddd0f130e44a7593ac6c55aa93f6e256d2270fd88e9d1b64ab7f22ab8fde/colorzero-2.0-py2.py3-none-any.whl", hash = "sha256:0e60d743a6b8071498a56465f7719c96a5e92928f858bab1be2a0d606c9aa0f8", size = 26573 }, ] +[[package]] +name = "corellium-api" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "python-dateutil" }, + { name = "six" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/61/0a3fbd12fd85e802031a1ba45e20d54e627028db45b8de8bebad49de3298/corellium_api-0.4.0.tar.gz", hash = "sha256:06e98ff7be608c7727c5610d9208c30f5287ceef0e0fe29976ca50d0a68e8711", size = 155422 } + [[package]] name = "coverage" version = "7.8.0" @@ -724,28 +736,28 @@ wheels = [ [[package]] name = "google-auth" -version = "2.38.0" +version = "2.39.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cachetools" }, { name = "pyasn1-modules" }, { name = "rsa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c6/eb/d504ba1daf190af6b204a9d4714d457462b486043744901a6eeea711f913/google_auth-2.38.0.tar.gz", hash = "sha256:8285113607d3b80a3f1543b75962447ba8a09fe85783432a784fdeef6ac094c4", size = 270866 } +sdist = { url = "https://files.pythonhosted.org/packages/cb/8e/8f45c9a32f73e786e954b8f9761c61422955d23c45d1e8c347f9b4b59e8e/google_auth-2.39.0.tar.gz", hash = "sha256:73222d43cdc35a3aeacbfdcaf73142a97839f10de930550d89ebfe1d0a00cde7", size = 274834 } wheels = [ - { url = "https://files.pythonhosted.org/packages/9d/47/603554949a37bca5b7f894d51896a9c534b9eab808e2520a748e081669d0/google_auth-2.38.0-py2.py3-none-any.whl", hash = "sha256:e7dae6694313f434a2727bf2906f27ad259bae090d7aa896590d86feec3d9d4a", size = 210770 }, + { url = "https://files.pythonhosted.org/packages/ce/12/ad37a1ef86006d0a0117fc06a4a00bd461c775356b534b425f00dde208ea/google_auth-2.39.0-py2.py3-none-any.whl", hash = "sha256:0150b6711e97fb9f52fe599f55648950cc4540015565d8fbb31be2ad6e1548a2", size = 212319 }, ] [[package]] name = "googleapis-common-protos" -version = "1.69.2" +version = "1.70.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1b/d7/ee9d56af4e6dbe958562b5020f46263c8a4628e7952070241fc0e9b182ae/googleapis_common_protos-1.69.2.tar.gz", hash = "sha256:3e1b904a27a33c821b4b749fd31d334c0c9c30e6113023d495e48979a3dc9c5f", size = 144496 } +sdist = { url = "https://files.pythonhosted.org/packages/39/24/33db22342cf4a2ea27c9955e6713140fedd51e8b141b5ce5260897020f1a/googleapis_common_protos-1.70.0.tar.gz", hash = "sha256:0e1b44e0ea153e6594f9f394fef15193a68aaaea2d843f83e2742717ca753257", size = 145903 } wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/53/d35476d547a286506f0a6a634ccf1e5d288fffd53d48f0bd5fef61d68684/googleapis_common_protos-1.69.2-py3-none-any.whl", hash = "sha256:0b30452ff9c7a27d80bfc5718954063e8ab53dd3697093d3bc99581f5fd24212", size = 293215 }, + { url = "https://files.pythonhosted.org/packages/86/f1/62a193f0227cf15a920390abe675f386dec35f7ae3ffe6da582d3ade42c7/googleapis_common_protos-1.70.0-py3-none-any.whl", hash = "sha256:b8bfcca8c25a2bb253e0e0b0adaf8c00773e5e6af6fd92397576680b807e0fd8", size = 294530 }, ] [[package]] @@ -1225,6 +1237,7 @@ name = "jumpstarter-driver-corellium" source = { editable = "packages/jumpstarter-driver-corellium" } dependencies = [ { name = "asyncclick" }, + { name = "corellium-api" }, { name = "jumpstarter" }, { name = "jumpstarter-driver-composite" }, { name = "jumpstarter-driver-power" }, @@ -1234,6 +1247,7 @@ dependencies = [ [package.dev-dependencies] dev = [ { name = "pytest" }, + { name = "pytest-aiohttp" }, { name = "pytest-cov" }, { name = "requests-mock" }, { name = "trio" }, @@ -1242,6 +1256,7 @@ dev = [ [package.metadata] requires-dist = [ { name = "asyncclick", specifier = ">=8.1.7.2" }, + { name = "corellium-api", specifier = ">=0.4.0" }, { name = "jumpstarter", editable = "packages/jumpstarter" }, { name = "jumpstarter-driver-composite", editable = "packages/jumpstarter-driver-composite" }, { name = "jumpstarter-driver-power", editable = "packages/jumpstarter-driver-power" }, @@ -1251,6 +1266,7 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ { name = "pytest", specifier = ">=8.3.2" }, + { name = "pytest-aiohttp", specifier = ">=1.1.0" }, { name = "pytest-cov", specifier = ">=5.0.0" }, { name = "requests-mock" }, { name = "trio", specifier = ">=0.28.0" }, @@ -2139,79 +2155,79 @@ wheels = [ [[package]] name = "multidict" -version = "6.4.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2e/91/bd8dfeb233154a87e113385efabb64ffb8fdf0f4316a30a07a86454ba174/multidict-6.4.1.tar.gz", hash = "sha256:6dd762198ec7e8c538fe589615621e42a87c22adfa3b8b7b065a55cc1361080c", size = 88822 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/67/a5/af2e56eca0b08a1c2b53b1eec3621f10d415b315b1c8eeb16efa9abbb1a7/multidict-6.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:05a40d1613d3ff9617b5955d58b846bc403efd8a3c36dcd083d4e3b654257578", size = 65201 }, - { url = "https://files.pythonhosted.org/packages/c0/89/d2ef92eb8bc2296276b1d4c6665167588c3d0e90b8e32f292246c3abed02/multidict-6.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:39e25686a6c1735d68e8cc403f08b78eff0e1d6a10b70511ac8f16f185ab81fe", size = 38392 }, - { url = "https://files.pythonhosted.org/packages/05/eb/7ae777ce4944c12951153d633859717f1532177f587359aec1c9f1991b56/multidict-6.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b896ca989b4ef90acbb116b89a20f7530130ba17ca58e0cd87aec5cfd6cce471", size = 37647 }, - { url = "https://files.pythonhosted.org/packages/57/bc/a84e013a251d7ba64c87a333bfef8e2df12795f574539152992530af27d0/multidict-6.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b9964325fcef6a167236a45a6b253bef42db66c9c622eb15a797ea395471d6f", size = 226605 }, - { url = "https://files.pythonhosted.org/packages/6e/b2/4d2f4d5cbe306fe3c3b0004c703e4467f846a879dcfe4591ed310c1aa935/multidict-6.4.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d2d206047e73f6682665be1519a4e2655c9ff9f7bc3399760d99201a1634bbc1", size = 223115 }, - { url = "https://files.pythonhosted.org/packages/f6/b7/08aa3ddae5f6a4b6b6e33b00f93aa3d397b2c862f3669e7cc9a078609e1e/multidict-6.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:666d9b2320931dab52a954a8a97947516c2a682244c19d52ba721ac66acf8465", size = 234964 }, - { url = "https://files.pythonhosted.org/packages/4b/2e/2c64fcccc54d7743925ce6c7993bd1795d354fb2459f8ea9dd38efc1d7d9/multidict-6.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0a3b90d3e2551401a73151bdfdb7f8fe100551f1dc2c4b65e295773570aea6bb", size = 231410 }, - { url = "https://files.pythonhosted.org/packages/55/a5/4b0dc48f3bbf3004348b4de481a3f975778f710e40e0b793984c3f6aeb56/multidict-6.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e31849ab5969934f7bf2286fd3d9d14ca49ae409b16b06a85c886d3d00b3ba8", size = 223426 }, - { url = "https://files.pythonhosted.org/packages/14/63/6993295d59903a28847a302844b7ee2f51fc25c403a6a832e659c802c4dc/multidict-6.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a0d49e6110f6e62e6eacf37a820eb86944e61ac9e2dae97f3fe1a4240e0b0c9b", size = 217210 }, - { url = "https://files.pythonhosted.org/packages/76/24/35869a686a40a0d3d6553438ac58d921d5fdbaf85b2e27658a8aeca9a16d/multidict-6.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6cae58e593a648657d195d0a53022998bf3167ac079fb165113b0e780e3bff63", size = 228662 }, - { url = "https://files.pythonhosted.org/packages/07/39/b2f75392b24f0e48863276619a2d97b32fa283e8ac1152278523790e873c/multidict-6.4.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:d928adaddc69ede3a21009b88fdf525c45b70fc55e5529fcebd1f9bd04f1dbe9", size = 218025 }, - { url = "https://files.pythonhosted.org/packages/6e/2a/8ba0f9c009e6e407fa5547a04ee5f0c2f82a3f6554b5f62435d309f9e95a/multidict-6.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5704adbb54cef89c94264abe7844d39e0032e2bc6c6ddacfc62f40babd6cdaca", size = 225196 }, - { url = "https://files.pythonhosted.org/packages/45/ae/7301016d94656cd43be9e37dee29572f92a72198548493073a33bed2998c/multidict-6.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:135d116fda76e17b602f73d9e7144024c994d3ce6be51068494a148e3f92a361", size = 234668 }, - { url = "https://files.pythonhosted.org/packages/8d/3b/50df9227301e1201bccbc121931245abf50e1f088688374f28645b0324dd/multidict-6.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9c95a195a821ad88f262a7439d1238b5478c50cac40a07c361ee5bf770823ffa", size = 230024 }, - { url = "https://files.pythonhosted.org/packages/ab/d3/a33e9227cf1eeeeedc344e34cf908f5209256637ab3abd855d94b2036c6d/multidict-6.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7144dc4d54079dd94c8c949d1779e1f97f2e7175bc9a032c9e7bbef29bfaaae0", size = 223072 }, - { url = "https://files.pythonhosted.org/packages/8f/f6/620a01c2098430891b85d444abc9c758f7447e598e4e05dcbf98e53c91a5/multidict-6.4.1-cp311-cp311-win32.whl", hash = "sha256:20081b021bea6e8dfa95b7a0104ad98efdd8dff190353ed4cd8161b07e096f9e", size = 34900 }, - { url = "https://files.pythonhosted.org/packages/3c/6e/95ac75ba05f978877f539ce2e282136bfbc483d347a7d0719f165076439b/multidict-6.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:8f426805883fd44893c0f3f8e1dedf3369608cf391042718fffb3d210ddac601", size = 38484 }, - { url = "https://files.pythonhosted.org/packages/f3/db/494be6e064cc3f8e1decbceb97691692a8652b0f2db9067163d548c8db60/multidict-6.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:dbf404649e128e0e652a377d7df22771c519eaa9ba28e8e4ceba63ba04a8b312", size = 63959 }, - { url = "https://files.pythonhosted.org/packages/c9/9b/2fb7da3a34a4d8f13565b81bb97bb976a0554b7555af0b69b76f82a476c5/multidict-6.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:632c5322dbcc4ad07605a4fa20b71ac1aa576d6bade336185934c69ee2b4d1d0", size = 37862 }, - { url = "https://files.pythonhosted.org/packages/41/b0/616076ecc55951365fd71c30ed2b68c0627bc688c1c7c8f2e256605ed6a9/multidict-6.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e73ac2eb687b0a070b8e50afe7ab16cf4121928801ae5c2be7ca6ce13f854495", size = 36950 }, - { url = "https://files.pythonhosted.org/packages/f7/c9/02ad6a496bd15574eded0d762e784671fd940bfe98389442642620924cb3/multidict-6.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b377a97f7d847dcfc6b93643dd59cbd89ec045879d15405fdd2cb47dc509d0c", size = 224313 }, - { url = "https://files.pythonhosted.org/packages/da/11/f5d031c0dedbbff59f1f0865e1ce273e298141db4465b26513fbefda0339/multidict-6.4.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3e634773edc24e7bc5f6b94dc0e3118829d6d0ae8177784e365495c2c880851c", size = 230808 }, - { url = "https://files.pythonhosted.org/packages/9c/55/24aa91923e564e43875322bb69759eb1d38d7e5e1d16b9abafb832b8406b/multidict-6.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:62b2d7932cfa25854375b867d4b837e2b87f597a7e8a9d6b8b6a10b1d34178d2", size = 231897 }, - { url = "https://files.pythonhosted.org/packages/cd/a1/96a2ec597755c49bb15b67ed599d98d1285fe716c4f67624558a9b7cda23/multidict-6.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd8e579186570b210f42e6a39bc57688f300ab6f5ef543e5be2fb25fa2693645", size = 230973 }, - { url = "https://files.pythonhosted.org/packages/61/d9/89510dfafb6ed3a4b52b648d7f1ccf12fb8983f1c4a59da40445e364db48/multidict-6.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e97990d0035d3e1edf396e6abf8d57c8651160ae2ee8d8daf7cd55d300250ecf", size = 223458 }, - { url = "https://files.pythonhosted.org/packages/8c/df/3e9e1f645fb8daca37be2b7d70d7ca7cdfc44b711fbf5eadf9971f43eddb/multidict-6.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:561bb29c1ef1a878856c560037da9ed9130b77eb74d657671b4e806864751e80", size = 216232 }, - { url = "https://files.pythonhosted.org/packages/d2/cb/7f279bc4120a699459f102c5952157b1cb72fdfbd6ab64bc169eea21e925/multidict-6.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:453185024914a0bc29301f996b242196f75c4622ca2d946f4122addaf4523325", size = 228925 }, - { url = "https://files.pythonhosted.org/packages/2f/e7/4c1dfd72c7b7ddf090ffe5630ba7083ae8b6b6e0aca55a74fcd266b5b308/multidict-6.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:23ecf8f9e960837d616a667bac704ac79e40670a76dd8519142e1d7f583ac796", size = 226763 }, - { url = "https://files.pythonhosted.org/packages/d6/23/9aecad4e8841d2e6c2fe11c19b3c7bb1654196ab3a2c77fb7ef6c19cedba/multidict-6.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d1cd87ff12c2aa348b4ffd8bb7bea4b2661b50ccdd4fff3ce4214181ca52f615", size = 222655 }, - { url = "https://files.pythonhosted.org/packages/ba/b0/1340c1f4caaf2706c4f0e12245d94c897baaad30d5ebc1157940e1681da5/multidict-6.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c51d78684f29a426419629c625f84e0552c4c8548c45b4d409e7268f0d9c77de", size = 233669 }, - { url = "https://files.pythonhosted.org/packages/2d/04/969de4697397b5aa07dc24b312e6084856aa6d79b4795e62b3c1a577299e/multidict-6.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3bf77915c4150d711ad73f218eb1e18b20dde88be3123d6e710baea4b5a487fb", size = 230749 }, - { url = "https://files.pythonhosted.org/packages/60/a1/c06f36688213c2c63e9b2bd5e87505f1651a93de61f3db956a1825d0842b/multidict-6.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:090e381431e48b43cef0af8fce1feef2afaa78713cf3c2c761e25065e4959b6d", size = 226876 }, - { url = "https://files.pythonhosted.org/packages/86/b2/a829a7bc60f00748e12ef8d62c83cd19ff25824c49dd1a2767e78c339f4f/multidict-6.4.1-cp312-cp312-win32.whl", hash = "sha256:43f1d3cef7cdc66a904b07d6e054a94c59ac58fec4e2d2f062494063aa1490d9", size = 35183 }, - { url = "https://files.pythonhosted.org/packages/c8/8b/dcc8529355bd70ada0224f6f6ddf7e2d897b4ca68aba6cc450da134d41da/multidict-6.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:130e06851959f284bf4d695e02c4d6fade04fa1ba11c9a7d5324e9e49271e6ee", size = 38576 }, - { url = "https://files.pythonhosted.org/packages/6d/90/3804144a664e00d31e0624b7cec69a70f493203d772fdd0819eebb3e2e76/multidict-6.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:b62e8f84ec842867e2a45f55fba29f40aa23d38fa9c7dc7d4e693cc03e735fcb", size = 63774 }, - { url = "https://files.pythonhosted.org/packages/85/5e/5768041df23a53c6b6f6dc41d7ac35a08052395d2985141cf471fe5a283b/multidict-6.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:eaa435e07a4935b1aeac74e863b604c731a56b209e5cbd3840e41e186acaca4e", size = 37829 }, - { url = "https://files.pythonhosted.org/packages/6c/44/b843b6a3f4f9bea5d21a376b291dd9fda9758990ef941d13dc4c008e1244/multidict-6.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7bcb89caa256fbb84d7baedbfcaeba6b2a847774eec761c19ab137f2a41de821", size = 36791 }, - { url = "https://files.pythonhosted.org/packages/6d/4a/1c9050a5ca1a3135bc5d62013113a03ca2d782e61bbdd09ed4b2b8a94d4d/multidict-6.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db5d927c4c0ff5dcb8810585f44d5c4e62cc2c0ced7dee4e0c337065b5239acb", size = 223585 }, - { url = "https://files.pythonhosted.org/packages/bf/25/f912720fddc8f7c7314ba4161279fbe60bd3f4a16b98c17a3fbc45c1172d/multidict-6.4.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:056e486c57083300813bca83300f8bdcb30f43abfe6217c93bcf6c2aeeb73b54", size = 230384 }, - { url = "https://files.pythonhosted.org/packages/99/41/1148fd9060caef55695a73ff33a4f82ef0bd151b817f1879830cd9580bb9/multidict-6.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0205063d837f0d9f0423a971291c458c5235b7709cf29772cdc10754c55a4a0e", size = 231030 }, - { url = "https://files.pythonhosted.org/packages/0d/5a/84874b04542c199879d3938414b4c9e2d05a9aaed5e1bd58cc983a8aba6c/multidict-6.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c5df5aa78989cc13c9fa2370c0950ba1696419a6c0a87b53d5b0ebe654d55b34", size = 229914 }, - { url = "https://files.pythonhosted.org/packages/1f/34/1f2dd3a8dfc2e2817cc6c74f4b1ecc8910ce8d97c9617d5b6313f1df5915/multidict-6.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78c9121f7dcb8bf34766a4acebbcedaddc63e7c70888a7d2b5fed749cf43d58b", size = 222536 }, - { url = "https://files.pythonhosted.org/packages/89/e3/a209fed9614ebe8290f28921d84a0b3fd483ff6e8c124c850e4c03b7f6b5/multidict-6.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c0639454a14a75c5aaf05346ceece1445b30c0dfc809bfe19183581307a6b018", size = 216082 }, - { url = "https://files.pythonhosted.org/packages/36/d0/db24ee3f4e0a2ade57c2962b543a7e30ddf7bdfcd35dc48b8b62f85b1fe5/multidict-6.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9eda163d0364cf3db8aeea8950a7d55cd744d37e43b9e3417dbccaa80373dbdd", size = 228078 }, - { url = "https://files.pythonhosted.org/packages/4b/26/c19bc5f2b31294a36b7dff92c6c1ce758195cf94e34e83041eec540cceac/multidict-6.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:60a2f65554369f4b042eb70d7655734cd799aa6f0711de6a7ca95291108e8a2a", size = 226195 }, - { url = "https://files.pythonhosted.org/packages/90/de/c55ec327a1c1c0867db076cf55279ef53bfcb021225cc017624a5a29f140/multidict-6.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:10ae379ef4cd8442311853ec43963ab270c24e9b24361b1760f6af334c77182e", size = 221816 }, - { url = "https://files.pythonhosted.org/packages/49/fe/be4da2ffbc283b951ec06879d81968b1d9126a861a7cd62d9aeb14f44c0d/multidict-6.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:ae62010fb59b5087faccb8aa4f9e55aba2479fb73383f1a0fc4b5ddbe29513d2", size = 233043 }, - { url = "https://files.pythonhosted.org/packages/67/e6/1f5b9caa9f75ef35f1e9b7304ac416021945249f0a5ef3bab08f553ca29a/multidict-6.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:211d87a691506750ca672550fa9d69ae8e622ca1fabd74a0b303018126d29ea8", size = 230109 }, - { url = "https://files.pythonhosted.org/packages/48/a6/e0cb64d8a3cad6bfcef35bc4aa677d42f939bdac68f7e17223faa470ee8f/multidict-6.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1464047f8679b901d560c85c5c6e2465a731f860200014a288b633e43b3e1f16", size = 225854 }, - { url = "https://files.pythonhosted.org/packages/6a/a9/405c90f2ef78d92c5c689a5a373ddf4e2e98e4a510a32e0299efe852b365/multidict-6.4.1-cp313-cp313-win32.whl", hash = "sha256:def4561313db5919f3d19a7c0c2192aa683e64ddf0fe5bc742e9b9aa8e11c4ff", size = 35179 }, - { url = "https://files.pythonhosted.org/packages/b0/55/653155bd97787af2069f777a41dfdb471a26efe503f1ca68983571cce3d4/multidict-6.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:3a21f56fe82ddd1b3e9c7f6d3a9daaacbd9de56a1d538509cc8712e49df3f328", size = 38738 }, - { url = "https://files.pythonhosted.org/packages/3c/14/e076455beb77a7b59296020e9c172406035484803051b41e782eb55592d1/multidict-6.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:8a9dce734e2ec47eeaa28dfa1051d7c56635a44a6679d0edca8cc9b0f500ce24", size = 68586 }, - { url = "https://files.pythonhosted.org/packages/38/a5/d3a63889318d5afcfe0339363cce8b98abc10b4d9862b88a1fab22481f03/multidict-6.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2b884988bb46bf9225d1a54aee110a12a131d94a007587ab7675cd5d23c8c445", size = 39966 }, - { url = "https://files.pythonhosted.org/packages/4d/ee/2c9136ecb1ef98b8a71b7347c1725baa6b0a35d6eaabc2750b206b32ba4a/multidict-6.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:963646c4b63f7416735b9998d28384cd9933ccaf868a5369a173559eb649c141", size = 39365 }, - { url = "https://files.pythonhosted.org/packages/fd/9b/7a02703706566ab29de586c9e9f7e0683ec563740d48fda0ef9b14ff3d96/multidict-6.4.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:229446d8b3c07722d3c6c19a55f0c8ad2be064ffa378efc516443270a205d88c", size = 226109 }, - { url = "https://files.pythonhosted.org/packages/37/a5/e34b0575c3c06e14035199594a9d98758cca88c79f2b96a5145c993ccd99/multidict-6.4.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:506408a4e7904e55100771ac9fd8393ae2ba41f0dd364175a43aa58f8a639b05", size = 222558 }, - { url = "https://files.pythonhosted.org/packages/67/c4/d6ba92659915ac2201db92f720777a4f073f14484a89036748dc3ae15d4f/multidict-6.4.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8e5646e8333452681b593a543bea61061375afd2ce26b308714bcd33c74aeb63", size = 227857 }, - { url = "https://files.pythonhosted.org/packages/11/7f/ed8069f8b34de70cac24f1f6524070b20f43b77bf92749cfd77e3e8e4bf4/multidict-6.4.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:142c3f6061b7ba06541654f78f0a4f09551d720c6986589b8ac5b141303f5e5f", size = 226032 }, - { url = "https://files.pythonhosted.org/packages/2a/03/743c641a673e666fb658507cbb14b87615099b08cde0300459b787381692/multidict-6.4.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bca2d1ca5fb2443773655911ca46b1ba46428bef7a37101a22249d3e259fb3a8", size = 220643 }, - { url = "https://files.pythonhosted.org/packages/9f/02/669f00bcc7418663fb00be3bf72579d37ddc2b1557e16aba59f6b2e2ce98/multidict-6.4.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f9337826b68cc887c6d71445a20c5c495bd93ac77efabc97ed9aebbf875593a2", size = 211666 }, - { url = "https://files.pythonhosted.org/packages/e9/ee/cd2b71bc5ed35f9ed05702aa750682ecc8b7d477e254e9eeefa5b087918e/multidict-6.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8901e2c1b46a496e410375f85ee28a00fdccbf003937f64a32d6976afa582b25", size = 226216 }, - { url = "https://files.pythonhosted.org/packages/42/13/d3ad8c2363841977c46b1bc1b65d39411548a7ba01cb2be333d44d2ede43/multidict-6.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:49e01e4cf3551bcfaec5c4c74d2f7371d802d4a4e8b3a9a4cee1d3698c62dd62", size = 212005 }, - { url = "https://files.pythonhosted.org/packages/b2/75/9f3b62634e0e4850644a7daacdc70e2fe29f66e956b7c88cddea417f7517/multidict-6.4.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:86290e7dbebf35e3caf13b3f85fb9a3b18b57b22d3457e3a02d660b7cfab515c", size = 217797 }, - { url = "https://files.pythonhosted.org/packages/5a/d7/e59f621e9060f6e584a5b11c80be4de6abe3c5e2a1043d5e31b77e4c41fb/multidict-6.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:e3d386591b33a4ce52d824906f9ace10598559a9efeee9825834b0fa8af4fb36", size = 226924 }, - { url = "https://files.pythonhosted.org/packages/8d/bb/e10e869696ab95066a5dcd9dc9ed83ba960633a61f7b111eee53666d8e8e/multidict-6.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:556713cb4bcde6be451df024442ccef80a7f07c93ced2a16e2dfb6dde765afa0", size = 220367 }, - { url = "https://files.pythonhosted.org/packages/34/60/c49e00f651ecebab31b28c30a080f84392ebb189a7f574b7ff97ebfee095/multidict-6.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:200ec9f39cd6aff8544d568214d8fa8d0f81506fd9eca3470acce6a6f2b713fa", size = 221429 }, - { url = "https://files.pythonhosted.org/packages/47/75/a0937b81caec3b7706325a0c5ffc17464fa193c55248a226d1ca06e9c730/multidict-6.4.1-cp313-cp313t-win32.whl", hash = "sha256:4cc12a6de2564a17883cda8d181a44ed7667efb48106e79e6eae9d84b0b854ae", size = 41716 }, - { url = "https://files.pythonhosted.org/packages/be/f5/d04dbb200adaf8bdff1bb57d4c194052738d7a7d542dbbd1a19fc1937340/multidict-6.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:ce82d91c76b1f4bb83db85b92bc84c5cc64bcd8da08d38ec79d1b6db997c1d16", size = 45886 }, - { url = "https://files.pythonhosted.org/packages/40/18/d2c92b348a15f38451c72cf6ae4c449e74c0bd770c8cd0327c6a08e326c2/multidict-6.4.1-py3-none-any.whl", hash = "sha256:795825da6ae4ad2118a9f60b4a8cdc60575cb37cd268e6f4e7f620206232125f", size = 10338 }, +version = "6.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/da/2c/e367dfb4c6538614a0c9453e510d75d66099edf1c4e69da1b5ce691a1931/multidict-6.4.3.tar.gz", hash = "sha256:3ada0b058c9f213c5f95ba301f922d402ac234f1111a7d8fd70f1b99f3c281ec", size = 89372 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/e0/53cf7f27eda48fffa53cfd4502329ed29e00efb9e4ce41362cbf8aa54310/multidict-6.4.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f6f19170197cc29baccd33ccc5b5d6a331058796485857cf34f7635aa25fb0cd", size = 65259 }, + { url = "https://files.pythonhosted.org/packages/44/79/1dcd93ce7070cf01c2ee29f781c42b33c64fce20033808f1cc9ec8413d6e/multidict-6.4.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f2882bf27037eb687e49591690e5d491e677272964f9ec7bc2abbe09108bdfb8", size = 38451 }, + { url = "https://files.pythonhosted.org/packages/f4/35/2292cf29ab5f0d0b3613fad1b75692148959d3834d806be1885ceb49a8ff/multidict-6.4.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fbf226ac85f7d6b6b9ba77db4ec0704fde88463dc17717aec78ec3c8546c70ad", size = 37706 }, + { url = "https://files.pythonhosted.org/packages/f6/d1/6b157110b2b187b5a608b37714acb15ee89ec773e3800315b0107ea648cd/multidict-6.4.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e329114f82ad4b9dd291bef614ea8971ec119ecd0f54795109976de75c9a852", size = 226669 }, + { url = "https://files.pythonhosted.org/packages/40/7f/61a476450651f177c5570e04bd55947f693077ba7804fe9717ee9ae8de04/multidict-6.4.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1f4e0334d7a555c63f5c8952c57ab6f1c7b4f8c7f3442df689fc9f03df315c08", size = 223182 }, + { url = "https://files.pythonhosted.org/packages/51/7b/eaf7502ac4824cdd8edcf5723e2e99f390c879866aec7b0c420267b53749/multidict-6.4.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:740915eb776617b57142ce0bb13b7596933496e2f798d3d15a20614adf30d229", size = 235025 }, + { url = "https://files.pythonhosted.org/packages/3b/f6/facdbbd73c96b67a93652774edd5778ab1167854fa08ea35ad004b1b70ad/multidict-6.4.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255dac25134d2b141c944b59a0d2f7211ca12a6d4779f7586a98b4b03ea80508", size = 231481 }, + { url = "https://files.pythonhosted.org/packages/70/57/c008e861b3052405eebf921fd56a748322d8c44dcfcab164fffbccbdcdc4/multidict-6.4.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4e8535bd4d741039b5aad4285ecd9b902ef9e224711f0b6afda6e38d7ac02c7", size = 223492 }, + { url = "https://files.pythonhosted.org/packages/30/4d/7d8440d3a12a6ae5d6b202d6e7f2ac6ab026e04e99aaf1b73f18e6bc34bc/multidict-6.4.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30c433a33be000dd968f5750722eaa0991037be0be4a9d453eba121774985bc8", size = 217279 }, + { url = "https://files.pythonhosted.org/packages/7f/e7/bca0df4dd057597b94138d2d8af04eb3c27396a425b1b0a52e082f9be621/multidict-6.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4eb33b0bdc50acd538f45041f5f19945a1f32b909b76d7b117c0c25d8063df56", size = 228733 }, + { url = "https://files.pythonhosted.org/packages/88/f5/383827c3f1c38d7c92dbad00a8a041760228573b1c542fbf245c37bbca8a/multidict-6.4.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:75482f43465edefd8a5d72724887ccdcd0c83778ded8f0cb1e0594bf71736cc0", size = 218089 }, + { url = "https://files.pythonhosted.org/packages/36/8a/a5174e8a7d8b94b4c8f9c1e2cf5d07451f41368ffe94d05fc957215b8e72/multidict-6.4.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ce5b3082e86aee80b3925ab4928198450d8e5b6466e11501fe03ad2191c6d777", size = 225257 }, + { url = "https://files.pythonhosted.org/packages/8c/76/1d4b7218f0fd00b8e5c90b88df2e45f8af127f652f4e41add947fa54c1c4/multidict-6.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e413152e3212c4d39f82cf83c6f91be44bec9ddea950ce17af87fbf4e32ca6b2", size = 234728 }, + { url = "https://files.pythonhosted.org/packages/64/44/18372a4f6273fc7ca25630d7bf9ae288cde64f29593a078bff450c7170b6/multidict-6.4.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:8aac2eeff69b71f229a405c0a4b61b54bade8e10163bc7b44fcd257949620618", size = 230087 }, + { url = "https://files.pythonhosted.org/packages/0f/ae/28728c314a698d8a6d9491fcacc897077348ec28dd85884d09e64df8a855/multidict-6.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ab583ac203af1d09034be41458feeab7863c0635c650a16f15771e1386abf2d7", size = 223137 }, + { url = "https://files.pythonhosted.org/packages/22/50/785bb2b3fe16051bc91c70a06a919f26312da45c34db97fc87441d61e343/multidict-6.4.3-cp311-cp311-win32.whl", hash = "sha256:1b2019317726f41e81154df636a897de1bfe9228c3724a433894e44cd2512378", size = 34959 }, + { url = "https://files.pythonhosted.org/packages/2f/63/2a22e099ae2f4d92897618c00c73a09a08a2a9aa14b12736965bf8d59fd3/multidict-6.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:43173924fa93c7486402217fab99b60baf78d33806af299c56133a3755f69589", size = 38541 }, + { url = "https://files.pythonhosted.org/packages/fc/bb/3abdaf8fe40e9226ce8a2ba5ecf332461f7beec478a455d6587159f1bf92/multidict-6.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1f1c2f58f08b36f8475f3ec6f5aeb95270921d418bf18f90dffd6be5c7b0e676", size = 64019 }, + { url = "https://files.pythonhosted.org/packages/7e/b5/1b2e8de8217d2e89db156625aa0fe4a6faad98972bfe07a7b8c10ef5dd6b/multidict-6.4.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:26ae9ad364fc61b936fb7bf4c9d8bd53f3a5b4417142cd0be5c509d6f767e2f1", size = 37925 }, + { url = "https://files.pythonhosted.org/packages/b4/e2/3ca91c112644a395c8eae017144c907d173ea910c913ff8b62549dcf0bbf/multidict-6.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:659318c6c8a85f6ecfc06b4e57529e5a78dfdd697260cc81f683492ad7e9435a", size = 37008 }, + { url = "https://files.pythonhosted.org/packages/60/23/79bc78146c7ac8d1ac766b2770ca2e07c2816058b8a3d5da6caed8148637/multidict-6.4.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1eb72c741fd24d5a28242ce72bb61bc91f8451877131fa3fe930edb195f7054", size = 224374 }, + { url = "https://files.pythonhosted.org/packages/86/35/77950ed9ebd09136003a85c1926ba42001ca5be14feb49710e4334ee199b/multidict-6.4.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3cd06d88cb7398252284ee75c8db8e680aa0d321451132d0dba12bc995f0adcc", size = 230869 }, + { url = "https://files.pythonhosted.org/packages/49/97/2a33c6e7d90bc116c636c14b2abab93d6521c0c052d24bfcc231cbf7f0e7/multidict-6.4.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4543d8dc6470a82fde92b035a92529317191ce993533c3c0c68f56811164ed07", size = 231949 }, + { url = "https://files.pythonhosted.org/packages/56/ce/e9b5d9fcf854f61d6686ada7ff64893a7a5523b2a07da6f1265eaaea5151/multidict-6.4.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:30a3ebdc068c27e9d6081fca0e2c33fdf132ecea703a72ea216b81a66860adde", size = 231032 }, + { url = "https://files.pythonhosted.org/packages/f0/ac/7ced59dcdfeddd03e601edb05adff0c66d81ed4a5160c443e44f2379eef0/multidict-6.4.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b038f10e23f277153f86f95c777ba1958bcd5993194fda26a1d06fae98b2f00c", size = 223517 }, + { url = "https://files.pythonhosted.org/packages/db/e6/325ed9055ae4e085315193a1b58bdb4d7fc38ffcc1f4975cfca97d015e17/multidict-6.4.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c605a2b2dc14282b580454b9b5d14ebe0668381a3a26d0ac39daa0ca115eb2ae", size = 216291 }, + { url = "https://files.pythonhosted.org/packages/fa/84/eeee6d477dd9dcb7691c3bb9d08df56017f5dd15c730bcc9383dcf201cf4/multidict-6.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8bd2b875f4ca2bb527fe23e318ddd509b7df163407b0fb717df229041c6df5d3", size = 228982 }, + { url = "https://files.pythonhosted.org/packages/82/94/4d1f3e74e7acf8b0c85db350e012dcc61701cd6668bc2440bb1ecb423c90/multidict-6.4.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c2e98c840c9c8e65c0e04b40c6c5066c8632678cd50c8721fdbcd2e09f21a507", size = 226823 }, + { url = "https://files.pythonhosted.org/packages/09/f0/1e54b95bda7cd01080e5732f9abb7b76ab5cc795b66605877caeb2197476/multidict-6.4.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:66eb80dd0ab36dbd559635e62fba3083a48a252633164857a1d1684f14326427", size = 222714 }, + { url = "https://files.pythonhosted.org/packages/e7/a2/f6cbca875195bd65a3e53b37ab46486f3cc125bdeab20eefe5042afa31fb/multidict-6.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c23831bdee0a2a3cf21be057b5e5326292f60472fb6c6f86392bbf0de70ba731", size = 233739 }, + { url = "https://files.pythonhosted.org/packages/79/68/9891f4d2b8569554723ddd6154375295f789dc65809826c6fb96a06314fd/multidict-6.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1535cec6443bfd80d028052e9d17ba6ff8a5a3534c51d285ba56c18af97e9713", size = 230809 }, + { url = "https://files.pythonhosted.org/packages/e6/72/a7be29ba1e87e4fc5ceb44dabc7940b8005fd2436a332a23547709315f70/multidict-6.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3b73e7227681f85d19dec46e5b881827cd354aabe46049e1a61d2f9aaa4e285a", size = 226934 }, + { url = "https://files.pythonhosted.org/packages/12/c1/259386a9ad6840ff7afc686da96808b503d152ac4feb3a96c651dc4f5abf/multidict-6.4.3-cp312-cp312-win32.whl", hash = "sha256:8eac0c49df91b88bf91f818e0a24c1c46f3622978e2c27035bfdca98e0e18124", size = 35242 }, + { url = "https://files.pythonhosted.org/packages/06/24/c8fdff4f924d37225dc0c56a28b1dca10728fc2233065fafeb27b4b125be/multidict-6.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:11990b5c757d956cd1db7cb140be50a63216af32cd6506329c2c59d732d802db", size = 38635 }, + { url = "https://files.pythonhosted.org/packages/6c/4b/86fd786d03915c6f49998cf10cd5fe6b6ac9e9a071cb40885d2e080fb90d/multidict-6.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7a76534263d03ae0cfa721fea40fd2b5b9d17a6f85e98025931d41dc49504474", size = 63831 }, + { url = "https://files.pythonhosted.org/packages/45/05/9b51fdf7aef2563340a93be0a663acba2c428c4daeaf3960d92d53a4a930/multidict-6.4.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:805031c2f599eee62ac579843555ed1ce389ae00c7e9f74c2a1b45e0564a88dd", size = 37888 }, + { url = "https://files.pythonhosted.org/packages/0b/43/53fc25394386c911822419b522181227ca450cf57fea76e6188772a1bd91/multidict-6.4.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c56c179839d5dcf51d565132185409d1d5dd8e614ba501eb79023a6cab25576b", size = 36852 }, + { url = "https://files.pythonhosted.org/packages/8a/68/7b99c751e822467c94a235b810a2fd4047d4ecb91caef6b5c60116991c4b/multidict-6.4.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c64f4ddb3886dd8ab71b68a7431ad4aa01a8fa5be5b11543b29674f29ca0ba3", size = 223644 }, + { url = "https://files.pythonhosted.org/packages/80/1b/d458d791e4dd0f7e92596667784fbf99e5c8ba040affe1ca04f06b93ae92/multidict-6.4.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3002a856367c0b41cad6784f5b8d3ab008eda194ed7864aaa58f65312e2abcac", size = 230446 }, + { url = "https://files.pythonhosted.org/packages/e2/46/9793378d988905491a7806d8987862dc5a0bae8a622dd896c4008c7b226b/multidict-6.4.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3d75e621e7d887d539d6e1d789f0c64271c250276c333480a9e1de089611f790", size = 231070 }, + { url = "https://files.pythonhosted.org/packages/a7/b8/b127d3e1f8dd2a5bf286b47b24567ae6363017292dc6dec44656e6246498/multidict-6.4.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:995015cf4a3c0d72cbf453b10a999b92c5629eaf3a0c3e1efb4b5c1f602253bb", size = 229956 }, + { url = "https://files.pythonhosted.org/packages/0c/93/f70a4c35b103fcfe1443059a2bb7f66e5c35f2aea7804105ff214f566009/multidict-6.4.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2b0fabae7939d09d7d16a711468c385272fa1b9b7fb0d37e51143585d8e72e0", size = 222599 }, + { url = "https://files.pythonhosted.org/packages/63/8c/e28e0eb2fe34921d6aa32bfc4ac75b09570b4d6818cc95d25499fe08dc1d/multidict-6.4.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:61ed4d82f8a1e67eb9eb04f8587970d78fe7cddb4e4d6230b77eda23d27938f9", size = 216136 }, + { url = "https://files.pythonhosted.org/packages/72/f5/fbc81f866585b05f89f99d108be5d6ad170e3b6c4d0723d1a2f6ba5fa918/multidict-6.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:062428944a8dc69df9fdc5d5fc6279421e5f9c75a9ee3f586f274ba7b05ab3c8", size = 228139 }, + { url = "https://files.pythonhosted.org/packages/bb/ba/7d196bad6b85af2307d81f6979c36ed9665f49626f66d883d6c64d156f78/multidict-6.4.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:b90e27b4674e6c405ad6c64e515a505c6d113b832df52fdacb6b1ffd1fa9a1d1", size = 226251 }, + { url = "https://files.pythonhosted.org/packages/cc/e2/fae46a370dce79d08b672422a33df721ec8b80105e0ea8d87215ff6b090d/multidict-6.4.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7d50d4abf6729921e9613d98344b74241572b751c6b37feed75fb0c37bd5a817", size = 221868 }, + { url = "https://files.pythonhosted.org/packages/26/20/bbc9a3dec19d5492f54a167f08546656e7aef75d181d3d82541463450e88/multidict-6.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:43fe10524fb0a0514be3954be53258e61d87341008ce4914f8e8b92bee6f875d", size = 233106 }, + { url = "https://files.pythonhosted.org/packages/ee/8d/f30ae8f5ff7a2461177f4d8eb0d8f69f27fb6cfe276b54ec4fd5a282d918/multidict-6.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:236966ca6c472ea4e2d3f02f6673ebfd36ba3f23159c323f5a496869bc8e47c9", size = 230163 }, + { url = "https://files.pythonhosted.org/packages/15/e9/2833f3c218d3c2179f3093f766940ded6b81a49d2e2f9c46ab240d23dfec/multidict-6.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:422a5ec315018e606473ba1f5431e064cf8b2a7468019233dcf8082fabad64c8", size = 225906 }, + { url = "https://files.pythonhosted.org/packages/f1/31/6edab296ac369fd286b845fa5dd4c409e63bc4655ed8c9510fcb477e9ae9/multidict-6.4.3-cp313-cp313-win32.whl", hash = "sha256:f901a5aace8e8c25d78960dcc24c870c8d356660d3b49b93a78bf38eb682aac3", size = 35238 }, + { url = "https://files.pythonhosted.org/packages/23/57/2c0167a1bffa30d9a1383c3dab99d8caae985defc8636934b5668830d2ef/multidict-6.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:1c152c49e42277bc9a2f7b78bd5fa10b13e88d1b0328221e7aef89d5c60a99a5", size = 38799 }, + { url = "https://files.pythonhosted.org/packages/c9/13/2ead63b9ab0d2b3080819268acb297bd66e238070aa8d42af12b08cbee1c/multidict-6.4.3-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:be8751869e28b9c0d368d94f5afcb4234db66fe8496144547b4b6d6a0645cfc6", size = 68642 }, + { url = "https://files.pythonhosted.org/packages/85/45/f1a751e1eede30c23951e2ae274ce8fad738e8a3d5714be73e0a41b27b16/multidict-6.4.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0d4b31f8a68dccbcd2c0ea04f0e014f1defc6b78f0eb8b35f2265e8716a6df0c", size = 40028 }, + { url = "https://files.pythonhosted.org/packages/a7/29/fcc53e886a2cc5595cc4560df333cb9630257bda65003a7eb4e4e0d8f9c1/multidict-6.4.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:032efeab3049e37eef2ff91271884303becc9e54d740b492a93b7e7266e23756", size = 39424 }, + { url = "https://files.pythonhosted.org/packages/f6/f0/056c81119d8b88703971f937b371795cab1407cd3c751482de5bfe1a04a9/multidict-6.4.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e78006af1a7c8a8007e4f56629d7252668344442f66982368ac06522445e375", size = 226178 }, + { url = "https://files.pythonhosted.org/packages/a3/79/3b7e5fea0aa80583d3a69c9d98b7913dfd4fbc341fb10bb2fb48d35a9c21/multidict-6.4.3-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:daeac9dd30cda8703c417e4fddccd7c4dc0c73421a0b54a7da2713be125846be", size = 222617 }, + { url = "https://files.pythonhosted.org/packages/06/db/3ed012b163e376fc461e1d6a67de69b408339bc31dc83d39ae9ec3bf9578/multidict-6.4.3-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f6f90700881438953eae443a9c6f8a509808bc3b185246992c4233ccee37fea", size = 227919 }, + { url = "https://files.pythonhosted.org/packages/b1/db/0433c104bca380989bc04d3b841fc83e95ce0c89f680e9ea4251118b52b6/multidict-6.4.3-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f84627997008390dd15762128dcf73c3365f4ec0106739cde6c20a07ed198ec8", size = 226097 }, + { url = "https://files.pythonhosted.org/packages/c2/95/910db2618175724dd254b7ae635b6cd8d2947a8b76b0376de7b96d814dab/multidict-6.4.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3307b48cd156153b117c0ea54890a3bdbf858a5b296ddd40dc3852e5f16e9b02", size = 220706 }, + { url = "https://files.pythonhosted.org/packages/d1/af/aa176c6f5f1d901aac957d5258d5e22897fe13948d1e69063ae3d5d0ca01/multidict-6.4.3-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ead46b0fa1dcf5af503a46e9f1c2e80b5d95c6011526352fa5f42ea201526124", size = 211728 }, + { url = "https://files.pythonhosted.org/packages/e7/42/d51cc5fc1527c3717d7f85137d6c79bb7a93cd214c26f1fc57523774dbb5/multidict-6.4.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:1748cb2743bedc339d63eb1bca314061568793acd603a6e37b09a326334c9f44", size = 226276 }, + { url = "https://files.pythonhosted.org/packages/28/6b/d836dea45e0b8432343ba4acf9a8ecaa245da4c0960fb7ab45088a5e568a/multidict-6.4.3-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:acc9fa606f76fc111b4569348cc23a771cb52c61516dcc6bcef46d612edb483b", size = 212069 }, + { url = "https://files.pythonhosted.org/packages/55/34/0ee1a7adb3560e18ee9289c6e5f7db54edc312b13e5c8263e88ea373d12c/multidict-6.4.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:31469d5832b5885adeb70982e531ce86f8c992334edd2f2254a10fa3182ac504", size = 217858 }, + { url = "https://files.pythonhosted.org/packages/04/08/586d652c2f5acefe0cf4e658eedb4d71d4ba6dfd4f189bd81b400fc1bc6b/multidict-6.4.3-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:ba46b51b6e51b4ef7bfb84b82f5db0dc5e300fb222a8a13b8cd4111898a869cf", size = 226988 }, + { url = "https://files.pythonhosted.org/packages/82/e3/cc59c7e2bc49d7f906fb4ffb6d9c3a3cf21b9f2dd9c96d05bef89c2b1fd1/multidict-6.4.3-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:389cfefb599edf3fcfd5f64c0410da686f90f5f5e2c4d84e14f6797a5a337af4", size = 220435 }, + { url = "https://files.pythonhosted.org/packages/e0/32/5c3a556118aca9981d883f38c4b1bfae646f3627157f70f4068e5a648955/multidict-6.4.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:64bc2bbc5fba7b9db5c2c8d750824f41c6994e3882e6d73c903c2afa78d091e4", size = 221494 }, + { url = "https://files.pythonhosted.org/packages/b9/3b/1599631f59024b75c4d6e3069f4502409970a336647502aaf6b62fb7ac98/multidict-6.4.3-cp313-cp313t-win32.whl", hash = "sha256:0ecdc12ea44bab2807d6b4a7e5eef25109ab1c82a8240d86d3c1fc9f3b72efd5", size = 41775 }, + { url = "https://files.pythonhosted.org/packages/e8/4e/09301668d675d02ca8e8e1a3e6be046619e30403f5ada2ed5b080ae28d02/multidict-6.4.3-cp313-cp313t-win_amd64.whl", hash = "sha256:7146a8742ea71b5d7d955bffcef58a9e6e04efba704b52a460134fefd10a8208", size = 45946 }, + { url = "https://files.pythonhosted.org/packages/96/10/7d526c8974f017f1e7ca584c71ee62a638e9334d8d33f27d7cdfc9ae79e4/multidict-6.4.3-py3-none-any.whl", hash = "sha256:59fe01ee8e2a1e8ceb3f6dbb216b09c8d9f4ef1c22c4fc825d045a147fa2ebc9", size = 10400 }, ] [[package]] @@ -2417,51 +2433,61 @@ wheels = [ [[package]] name = "pillow" -version = "11.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f3/af/c097e544e7bd278333db77933e535098c259609c4eb3b85381109602fb5b/pillow-11.1.0.tar.gz", hash = "sha256:368da70808b36d73b4b390a8ffac11069f8a5c85f29eff1f1b01bcf3ef5b2a20", size = 46742715 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/dd/d6/2000bfd8d5414fb70cbbe52c8332f2283ff30ed66a9cde42716c8ecbe22c/pillow-11.1.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:e06695e0326d05b06833b40b7ef477e475d0b1ba3a6d27da1bb48c23209bf457", size = 3229968 }, - { url = "https://files.pythonhosted.org/packages/d9/45/3fe487010dd9ce0a06adf9b8ff4f273cc0a44536e234b0fad3532a42c15b/pillow-11.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96f82000e12f23e4f29346e42702b6ed9a2f2fea34a740dd5ffffcc8c539eb35", size = 3101806 }, - { url = "https://files.pythonhosted.org/packages/e3/72/776b3629c47d9d5f1c160113158a7a7ad177688d3a1159cd3b62ded5a33a/pillow-11.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3cd561ded2cf2bbae44d4605837221b987c216cff94f49dfeed63488bb228d2", size = 4322283 }, - { url = "https://files.pythonhosted.org/packages/e4/c2/e25199e7e4e71d64eeb869f5b72c7ddec70e0a87926398785ab944d92375/pillow-11.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f189805c8be5ca5add39e6f899e6ce2ed824e65fb45f3c28cb2841911da19070", size = 4402945 }, - { url = "https://files.pythonhosted.org/packages/c1/ed/51d6136c9d5911f78632b1b86c45241c712c5a80ed7fa7f9120a5dff1eba/pillow-11.1.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:dd0052e9db3474df30433f83a71b9b23bd9e4ef1de13d92df21a52c0303b8ab6", size = 4361228 }, - { url = "https://files.pythonhosted.org/packages/48/a4/fbfe9d5581d7b111b28f1d8c2762dee92e9821bb209af9fa83c940e507a0/pillow-11.1.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:837060a8599b8f5d402e97197d4924f05a2e0d68756998345c829c33186217b1", size = 4484021 }, - { url = "https://files.pythonhosted.org/packages/39/db/0b3c1a5018117f3c1d4df671fb8e47d08937f27519e8614bbe86153b65a5/pillow-11.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:aa8dd43daa836b9a8128dbe7d923423e5ad86f50a7a14dc688194b7be5c0dea2", size = 4287449 }, - { url = "https://files.pythonhosted.org/packages/d9/58/bc128da7fea8c89fc85e09f773c4901e95b5936000e6f303222490c052f3/pillow-11.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0a2f91f8a8b367e7a57c6e91cd25af510168091fb89ec5146003e424e1558a96", size = 4419972 }, - { url = "https://files.pythonhosted.org/packages/5f/bb/58f34379bde9fe197f51841c5bbe8830c28bbb6d3801f16a83b8f2ad37df/pillow-11.1.0-cp311-cp311-win32.whl", hash = "sha256:c12fc111ef090845de2bb15009372175d76ac99969bdf31e2ce9b42e4b8cd88f", size = 2291201 }, - { url = "https://files.pythonhosted.org/packages/3a/c6/fce9255272bcf0c39e15abd2f8fd8429a954cf344469eaceb9d0d1366913/pillow-11.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fbd43429d0d7ed6533b25fc993861b8fd512c42d04514a0dd6337fb3ccf22761", size = 2625686 }, - { url = "https://files.pythonhosted.org/packages/c8/52/8ba066d569d932365509054859f74f2a9abee273edcef5cd75e4bc3e831e/pillow-11.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:f7955ecf5609dee9442cbface754f2c6e541d9e6eda87fad7f7a989b0bdb9d71", size = 2375194 }, - { url = "https://files.pythonhosted.org/packages/95/20/9ce6ed62c91c073fcaa23d216e68289e19d95fb8188b9fb7a63d36771db8/pillow-11.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2062ffb1d36544d42fcaa277b069c88b01bb7298f4efa06731a7fd6cc290b81a", size = 3226818 }, - { url = "https://files.pythonhosted.org/packages/b9/d8/f6004d98579a2596c098d1e30d10b248798cceff82d2b77aa914875bfea1/pillow-11.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a85b653980faad27e88b141348707ceeef8a1186f75ecc600c395dcac19f385b", size = 3101662 }, - { url = "https://files.pythonhosted.org/packages/08/d9/892e705f90051c7a2574d9f24579c9e100c828700d78a63239676f960b74/pillow-11.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9409c080586d1f683df3f184f20e36fb647f2e0bc3988094d4fd8c9f4eb1b3b3", size = 4329317 }, - { url = "https://files.pythonhosted.org/packages/8c/aa/7f29711f26680eab0bcd3ecdd6d23ed6bce180d82e3f6380fb7ae35fcf3b/pillow-11.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7fdadc077553621911f27ce206ffcbec7d3f8d7b50e0da39f10997e8e2bb7f6a", size = 4412999 }, - { url = "https://files.pythonhosted.org/packages/c8/c4/8f0fe3b9e0f7196f6d0bbb151f9fba323d72a41da068610c4c960b16632a/pillow-11.1.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:93a18841d09bcdd774dcdc308e4537e1f867b3dec059c131fde0327899734aa1", size = 4368819 }, - { url = "https://files.pythonhosted.org/packages/38/0d/84200ed6a871ce386ddc82904bfadc0c6b28b0c0ec78176871a4679e40b3/pillow-11.1.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:9aa9aeddeed452b2f616ff5507459e7bab436916ccb10961c4a382cd3e03f47f", size = 4496081 }, - { url = "https://files.pythonhosted.org/packages/84/9c/9bcd66f714d7e25b64118e3952d52841a4babc6d97b6d28e2261c52045d4/pillow-11.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3cdcdb0b896e981678eee140d882b70092dac83ac1cdf6b3a60e2216a73f2b91", size = 4296513 }, - { url = "https://files.pythonhosted.org/packages/db/61/ada2a226e22da011b45f7104c95ebda1b63dcbb0c378ad0f7c2a710f8fd2/pillow-11.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:36ba10b9cb413e7c7dfa3e189aba252deee0602c86c309799da5a74009ac7a1c", size = 4431298 }, - { url = "https://files.pythonhosted.org/packages/e7/c4/fc6e86750523f367923522014b821c11ebc5ad402e659d8c9d09b3c9d70c/pillow-11.1.0-cp312-cp312-win32.whl", hash = "sha256:cfd5cd998c2e36a862d0e27b2df63237e67273f2fc78f47445b14e73a810e7e6", size = 2291630 }, - { url = "https://files.pythonhosted.org/packages/08/5c/2104299949b9d504baf3f4d35f73dbd14ef31bbd1ddc2c1b66a5b7dfda44/pillow-11.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:a697cd8ba0383bba3d2d3ada02b34ed268cb548b369943cd349007730c92bddf", size = 2626369 }, - { url = "https://files.pythonhosted.org/packages/37/f3/9b18362206b244167c958984b57c7f70a0289bfb59a530dd8af5f699b910/pillow-11.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:4dd43a78897793f60766563969442020e90eb7847463eca901e41ba186a7d4a5", size = 2375240 }, - { url = "https://files.pythonhosted.org/packages/b3/31/9ca79cafdce364fd5c980cd3416c20ce1bebd235b470d262f9d24d810184/pillow-11.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ae98e14432d458fc3de11a77ccb3ae65ddce70f730e7c76140653048c71bfcbc", size = 3226640 }, - { url = "https://files.pythonhosted.org/packages/ac/0f/ff07ad45a1f172a497aa393b13a9d81a32e1477ef0e869d030e3c1532521/pillow-11.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cc1331b6d5a6e144aeb5e626f4375f5b7ae9934ba620c0ac6b3e43d5e683a0f0", size = 3101437 }, - { url = "https://files.pythonhosted.org/packages/08/2f/9906fca87a68d29ec4530be1f893149e0cb64a86d1f9f70a7cfcdfe8ae44/pillow-11.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:758e9d4ef15d3560214cddbc97b8ef3ef86ce04d62ddac17ad39ba87e89bd3b1", size = 4326605 }, - { url = "https://files.pythonhosted.org/packages/b0/0f/f3547ee15b145bc5c8b336401b2d4c9d9da67da9dcb572d7c0d4103d2c69/pillow-11.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b523466b1a31d0dcef7c5be1f20b942919b62fd6e9a9be199d035509cbefc0ec", size = 4411173 }, - { url = "https://files.pythonhosted.org/packages/b1/df/bf8176aa5db515c5de584c5e00df9bab0713548fd780c82a86cba2c2fedb/pillow-11.1.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:9044b5e4f7083f209c4e35aa5dd54b1dd5b112b108648f5c902ad586d4f945c5", size = 4369145 }, - { url = "https://files.pythonhosted.org/packages/de/7c/7433122d1cfadc740f577cb55526fdc39129a648ac65ce64db2eb7209277/pillow-11.1.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:3764d53e09cdedd91bee65c2527815d315c6b90d7b8b79759cc48d7bf5d4f114", size = 4496340 }, - { url = "https://files.pythonhosted.org/packages/25/46/dd94b93ca6bd555588835f2504bd90c00d5438fe131cf01cfa0c5131a19d/pillow-11.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:31eba6bbdd27dde97b0174ddf0297d7a9c3a507a8a1480e1e60ef914fe23d352", size = 4296906 }, - { url = "https://files.pythonhosted.org/packages/a8/28/2f9d32014dfc7753e586db9add35b8a41b7a3b46540e965cb6d6bc607bd2/pillow-11.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b5d658fbd9f0d6eea113aea286b21d3cd4d3fd978157cbf2447a6035916506d3", size = 4431759 }, - { url = "https://files.pythonhosted.org/packages/33/48/19c2cbe7403870fbe8b7737d19eb013f46299cdfe4501573367f6396c775/pillow-11.1.0-cp313-cp313-win32.whl", hash = "sha256:f86d3a7a9af5d826744fabf4afd15b9dfef44fe69a98541f666f66fbb8d3fef9", size = 2291657 }, - { url = "https://files.pythonhosted.org/packages/3b/ad/285c556747d34c399f332ba7c1a595ba245796ef3e22eae190f5364bb62b/pillow-11.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:593c5fd6be85da83656b93ffcccc2312d2d149d251e98588b14fbc288fd8909c", size = 2626304 }, - { url = "https://files.pythonhosted.org/packages/e5/7b/ef35a71163bf36db06e9c8729608f78dedf032fc8313d19bd4be5c2588f3/pillow-11.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:11633d58b6ee5733bde153a8dafd25e505ea3d32e261accd388827ee987baf65", size = 2375117 }, - { url = "https://files.pythonhosted.org/packages/79/30/77f54228401e84d6791354888549b45824ab0ffde659bafa67956303a09f/pillow-11.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:70ca5ef3b3b1c4a0812b5c63c57c23b63e53bc38e758b37a951e5bc466449861", size = 3230060 }, - { url = "https://files.pythonhosted.org/packages/ce/b1/56723b74b07dd64c1010fee011951ea9c35a43d8020acd03111f14298225/pillow-11.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8000376f139d4d38d6851eb149b321a52bb8893a88dae8ee7d95840431977081", size = 3106192 }, - { url = "https://files.pythonhosted.org/packages/e1/cd/7bf7180e08f80a4dcc6b4c3a0aa9e0b0ae57168562726a05dc8aa8fa66b0/pillow-11.1.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ee85f0696a17dd28fbcfceb59f9510aa71934b483d1f5601d1030c3c8304f3c", size = 4446805 }, - { url = "https://files.pythonhosted.org/packages/97/42/87c856ea30c8ed97e8efbe672b58c8304dee0573f8c7cab62ae9e31db6ae/pillow-11.1.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:dd0e081319328928531df7a0e63621caf67652c8464303fd102141b785ef9547", size = 4530623 }, - { url = "https://files.pythonhosted.org/packages/ff/41/026879e90c84a88e33fb00cc6bd915ac2743c67e87a18f80270dfe3c2041/pillow-11.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e63e4e5081de46517099dc30abe418122f54531a6ae2ebc8680bcd7096860eab", size = 4465191 }, - { url = "https://files.pythonhosted.org/packages/e5/fb/a7960e838bc5df57a2ce23183bfd2290d97c33028b96bde332a9057834d3/pillow-11.1.0-cp313-cp313t-win32.whl", hash = "sha256:dda60aa465b861324e65a78c9f5cf0f4bc713e4309f83bc387be158b077963d9", size = 2295494 }, - { url = "https://files.pythonhosted.org/packages/d7/6c/6ec83ee2f6f0fda8d4cf89045c6be4b0373ebfc363ba8538f8c999f63fcd/pillow-11.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ad5db5781c774ab9a9b2c4302bbf0c1014960a0a7be63278d13ae6fdf88126fe", size = 2631595 }, - { url = "https://files.pythonhosted.org/packages/cf/6c/41c21c6c8af92b9fea313aa47c75de49e2f9a467964ee33eb0135d47eb64/pillow-11.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:67cd427c68926108778a9005f2a04adbd5e67c442ed21d95389fe1d595458756", size = 2377651 }, +version = "11.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/cb/bb5c01fcd2a69335b86c22142b2bccfc3464087efb7fd382eee5ffc7fdf7/pillow-11.2.1.tar.gz", hash = "sha256:a64dd61998416367b7ef979b73d3a85853ba9bec4c2925f74e588879a58716b6", size = 47026707 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/08/3fbf4b98924c73037a8e8b4c2c774784805e0fb4ebca6c5bb60795c40125/pillow-11.2.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:35ca289f712ccfc699508c4658a1d14652e8033e9b69839edf83cbdd0ba39e70", size = 3198450 }, + { url = "https://files.pythonhosted.org/packages/84/92/6505b1af3d2849d5e714fc75ba9e69b7255c05ee42383a35a4d58f576b16/pillow-11.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0409af9f829f87a2dfb7e259f78f317a5351f2045158be321fd135973fff7bf", size = 3030550 }, + { url = "https://files.pythonhosted.org/packages/3c/8c/ac2f99d2a70ff966bc7eb13dacacfaab57c0549b2ffb351b6537c7840b12/pillow-11.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4e5c5edee874dce4f653dbe59db7c73a600119fbea8d31f53423586ee2aafd7", size = 4415018 }, + { url = "https://files.pythonhosted.org/packages/1f/e3/0a58b5d838687f40891fff9cbaf8669f90c96b64dc8f91f87894413856c6/pillow-11.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b93a07e76d13bff9444f1a029e0af2964e654bfc2e2c2d46bfd080df5ad5f3d8", size = 4498006 }, + { url = "https://files.pythonhosted.org/packages/21/f5/6ba14718135f08fbfa33308efe027dd02b781d3f1d5c471444a395933aac/pillow-11.2.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:e6def7eed9e7fa90fde255afaf08060dc4b343bbe524a8f69bdd2a2f0018f600", size = 4517773 }, + { url = "https://files.pythonhosted.org/packages/20/f2/805ad600fc59ebe4f1ba6129cd3a75fb0da126975c8579b8f57abeb61e80/pillow-11.2.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:8f4f3724c068be008c08257207210c138d5f3731af6c155a81c2b09a9eb3a788", size = 4607069 }, + { url = "https://files.pythonhosted.org/packages/71/6b/4ef8a288b4bb2e0180cba13ca0a519fa27aa982875882392b65131401099/pillow-11.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a0a6709b47019dff32e678bc12c63008311b82b9327613f534e496dacaefb71e", size = 4583460 }, + { url = "https://files.pythonhosted.org/packages/62/ae/f29c705a09cbc9e2a456590816e5c234382ae5d32584f451c3eb41a62062/pillow-11.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f6b0c664ccb879109ee3ca702a9272d877f4fcd21e5eb63c26422fd6e415365e", size = 4661304 }, + { url = "https://files.pythonhosted.org/packages/6e/1a/c8217b6f2f73794a5e219fbad087701f412337ae6dbb956db37d69a9bc43/pillow-11.2.1-cp311-cp311-win32.whl", hash = "sha256:cc5d875d56e49f112b6def6813c4e3d3036d269c008bf8aef72cd08d20ca6df6", size = 2331809 }, + { url = "https://files.pythonhosted.org/packages/e2/72/25a8f40170dc262e86e90f37cb72cb3de5e307f75bf4b02535a61afcd519/pillow-11.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:0f5c7eda47bf8e3c8a283762cab94e496ba977a420868cb819159980b6709193", size = 2676338 }, + { url = "https://files.pythonhosted.org/packages/06/9e/76825e39efee61efea258b479391ca77d64dbd9e5804e4ad0fa453b4ba55/pillow-11.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:4d375eb838755f2528ac8cbc926c3e31cc49ca4ad0cf79cff48b20e30634a4a7", size = 2414918 }, + { url = "https://files.pythonhosted.org/packages/c7/40/052610b15a1b8961f52537cc8326ca6a881408bc2bdad0d852edeb6ed33b/pillow-11.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:78afba22027b4accef10dbd5eed84425930ba41b3ea0a86fa8d20baaf19d807f", size = 3190185 }, + { url = "https://files.pythonhosted.org/packages/e5/7e/b86dbd35a5f938632093dc40d1682874c33dcfe832558fc80ca56bfcb774/pillow-11.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78092232a4ab376a35d68c4e6d5e00dfd73454bd12b230420025fbe178ee3b0b", size = 3030306 }, + { url = "https://files.pythonhosted.org/packages/a4/5c/467a161f9ed53e5eab51a42923c33051bf8d1a2af4626ac04f5166e58e0c/pillow-11.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25a5f306095c6780c52e6bbb6109624b95c5b18e40aab1c3041da3e9e0cd3e2d", size = 4416121 }, + { url = "https://files.pythonhosted.org/packages/62/73/972b7742e38ae0e2ac76ab137ca6005dcf877480da0d9d61d93b613065b4/pillow-11.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c7b29dbd4281923a2bfe562acb734cee96bbb129e96e6972d315ed9f232bef4", size = 4501707 }, + { url = "https://files.pythonhosted.org/packages/e4/3a/427e4cb0b9e177efbc1a84798ed20498c4f233abde003c06d2650a6d60cb/pillow-11.2.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:3e645b020f3209a0181a418bffe7b4a93171eef6c4ef6cc20980b30bebf17b7d", size = 4522921 }, + { url = "https://files.pythonhosted.org/packages/fe/7c/d8b1330458e4d2f3f45d9508796d7caf0c0d3764c00c823d10f6f1a3b76d/pillow-11.2.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b2dbea1012ccb784a65349f57bbc93730b96e85b42e9bf7b01ef40443db720b4", size = 4612523 }, + { url = "https://files.pythonhosted.org/packages/b3/2f/65738384e0b1acf451de5a573d8153fe84103772d139e1e0bdf1596be2ea/pillow-11.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:da3104c57bbd72948d75f6a9389e6727d2ab6333c3617f0a89d72d4940aa0443", size = 4587836 }, + { url = "https://files.pythonhosted.org/packages/6a/c5/e795c9f2ddf3debb2dedd0df889f2fe4b053308bb59a3cc02a0cd144d641/pillow-11.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:598174aef4589af795f66f9caab87ba4ff860ce08cd5bb447c6fc553ffee603c", size = 4669390 }, + { url = "https://files.pythonhosted.org/packages/96/ae/ca0099a3995976a9fce2f423166f7bff9b12244afdc7520f6ed38911539a/pillow-11.2.1-cp312-cp312-win32.whl", hash = "sha256:1d535df14716e7f8776b9e7fee118576d65572b4aad3ed639be9e4fa88a1cad3", size = 2332309 }, + { url = "https://files.pythonhosted.org/packages/7c/18/24bff2ad716257fc03da964c5e8f05d9790a779a8895d6566e493ccf0189/pillow-11.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:14e33b28bf17c7a38eede290f77db7c664e4eb01f7869e37fa98a5aa95978941", size = 2676768 }, + { url = "https://files.pythonhosted.org/packages/da/bb/e8d656c9543276517ee40184aaa39dcb41e683bca121022f9323ae11b39d/pillow-11.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:21e1470ac9e5739ff880c211fc3af01e3ae505859392bf65458c224d0bf283eb", size = 2415087 }, + { url = "https://files.pythonhosted.org/packages/36/9c/447528ee3776e7ab8897fe33697a7ff3f0475bb490c5ac1456a03dc57956/pillow-11.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fdec757fea0b793056419bca3e9932eb2b0ceec90ef4813ea4c1e072c389eb28", size = 3190098 }, + { url = "https://files.pythonhosted.org/packages/b5/09/29d5cd052f7566a63e5b506fac9c60526e9ecc553825551333e1e18a4858/pillow-11.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b0e130705d568e2f43a17bcbe74d90958e8a16263868a12c3e0d9c8162690830", size = 3030166 }, + { url = "https://files.pythonhosted.org/packages/71/5d/446ee132ad35e7600652133f9c2840b4799bbd8e4adba881284860da0a36/pillow-11.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bdb5e09068332578214cadd9c05e3d64d99e0e87591be22a324bdbc18925be0", size = 4408674 }, + { url = "https://files.pythonhosted.org/packages/69/5f/cbe509c0ddf91cc3a03bbacf40e5c2339c4912d16458fcb797bb47bcb269/pillow-11.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d189ba1bebfbc0c0e529159631ec72bb9e9bc041f01ec6d3233d6d82eb823bc1", size = 4496005 }, + { url = "https://files.pythonhosted.org/packages/f9/b3/dd4338d8fb8a5f312021f2977fb8198a1184893f9b00b02b75d565c33b51/pillow-11.2.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:191955c55d8a712fab8934a42bfefbf99dd0b5875078240943f913bb66d46d9f", size = 4518707 }, + { url = "https://files.pythonhosted.org/packages/13/eb/2552ecebc0b887f539111c2cd241f538b8ff5891b8903dfe672e997529be/pillow-11.2.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:ad275964d52e2243430472fc5d2c2334b4fc3ff9c16cb0a19254e25efa03a155", size = 4610008 }, + { url = "https://files.pythonhosted.org/packages/72/d1/924ce51bea494cb6e7959522d69d7b1c7e74f6821d84c63c3dc430cbbf3b/pillow-11.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:750f96efe0597382660d8b53e90dd1dd44568a8edb51cb7f9d5d918b80d4de14", size = 4585420 }, + { url = "https://files.pythonhosted.org/packages/43/ab/8f81312d255d713b99ca37479a4cb4b0f48195e530cdc1611990eb8fd04b/pillow-11.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fe15238d3798788d00716637b3d4e7bb6bde18b26e5d08335a96e88564a36b6b", size = 4667655 }, + { url = "https://files.pythonhosted.org/packages/94/86/8f2e9d2dc3d308dfd137a07fe1cc478df0a23d42a6c4093b087e738e4827/pillow-11.2.1-cp313-cp313-win32.whl", hash = "sha256:3fe735ced9a607fee4f481423a9c36701a39719252a9bb251679635f99d0f7d2", size = 2332329 }, + { url = "https://files.pythonhosted.org/packages/6d/ec/1179083b8d6067a613e4d595359b5fdea65d0a3b7ad623fee906e1b3c4d2/pillow-11.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:74ee3d7ecb3f3c05459ba95eed5efa28d6092d751ce9bf20e3e253a4e497e691", size = 2676388 }, + { url = "https://files.pythonhosted.org/packages/23/f1/2fc1e1e294de897df39fa8622d829b8828ddad938b0eaea256d65b84dd72/pillow-11.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:5119225c622403afb4b44bad4c1ca6c1f98eed79db8d3bc6e4e160fc6339d66c", size = 2414950 }, + { url = "https://files.pythonhosted.org/packages/c4/3e/c328c48b3f0ead7bab765a84b4977acb29f101d10e4ef57a5e3400447c03/pillow-11.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8ce2e8411c7aaef53e6bb29fe98f28cd4fbd9a1d9be2eeea434331aac0536b22", size = 3192759 }, + { url = "https://files.pythonhosted.org/packages/18/0e/1c68532d833fc8b9f404d3a642991441d9058eccd5606eab31617f29b6d4/pillow-11.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9ee66787e095127116d91dea2143db65c7bb1e232f617aa5957c0d9d2a3f23a7", size = 3033284 }, + { url = "https://files.pythonhosted.org/packages/b7/cb/6faf3fb1e7705fd2db74e070f3bf6f88693601b0ed8e81049a8266de4754/pillow-11.2.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9622e3b6c1d8b551b6e6f21873bdcc55762b4b2126633014cea1803368a9aa16", size = 4445826 }, + { url = "https://files.pythonhosted.org/packages/07/94/8be03d50b70ca47fb434a358919d6a8d6580f282bbb7af7e4aa40103461d/pillow-11.2.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63b5dff3a68f371ea06025a1a6966c9a1e1ee452fc8020c2cd0ea41b83e9037b", size = 4527329 }, + { url = "https://files.pythonhosted.org/packages/fd/a4/bfe78777076dc405e3bd2080bc32da5ab3945b5a25dc5d8acaa9de64a162/pillow-11.2.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:31df6e2d3d8fc99f993fd253e97fae451a8db2e7207acf97859732273e108406", size = 4549049 }, + { url = "https://files.pythonhosted.org/packages/65/4d/eaf9068dc687c24979e977ce5677e253624bd8b616b286f543f0c1b91662/pillow-11.2.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:062b7a42d672c45a70fa1f8b43d1d38ff76b63421cbbe7f88146b39e8a558d91", size = 4635408 }, + { url = "https://files.pythonhosted.org/packages/1d/26/0fd443365d9c63bc79feb219f97d935cd4b93af28353cba78d8e77b61719/pillow-11.2.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4eb92eca2711ef8be42fd3f67533765d9fd043b8c80db204f16c8ea62ee1a751", size = 4614863 }, + { url = "https://files.pythonhosted.org/packages/49/65/dca4d2506be482c2c6641cacdba5c602bc76d8ceb618fd37de855653a419/pillow-11.2.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f91ebf30830a48c825590aede79376cb40f110b387c17ee9bd59932c961044f9", size = 4692938 }, + { url = "https://files.pythonhosted.org/packages/b3/92/1ca0c3f09233bd7decf8f7105a1c4e3162fb9142128c74adad0fb361b7eb/pillow-11.2.1-cp313-cp313t-win32.whl", hash = "sha256:e0b55f27f584ed623221cfe995c912c61606be8513bfa0e07d2c674b4516d9dd", size = 2335774 }, + { url = "https://files.pythonhosted.org/packages/a5/ac/77525347cb43b83ae905ffe257bbe2cc6fd23acb9796639a1f56aa59d191/pillow-11.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:36d6b82164c39ce5482f649b437382c0fb2395eabc1e2b1702a6deb8ad647d6e", size = 2681895 }, + { url = "https://files.pythonhosted.org/packages/67/32/32dc030cfa91ca0fc52baebbba2e009bb001122a1daa8b6a79ad830b38d3/pillow-11.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:225c832a13326e34f212d2072982bb1adb210e0cc0b153e688743018c94a2681", size = 2417234 }, + { url = "https://files.pythonhosted.org/packages/a4/ad/2613c04633c7257d9481ab21d6b5364b59fc5d75faafd7cb8693523945a3/pillow-11.2.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:80f1df8dbe9572b4b7abdfa17eb5d78dd620b1d55d9e25f834efdbee872d3aed", size = 3181734 }, + { url = "https://files.pythonhosted.org/packages/a4/fd/dcdda4471ed667de57bb5405bb42d751e6cfdd4011a12c248b455c778e03/pillow-11.2.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ea926cfbc3957090becbcbbb65ad177161a2ff2ad578b5a6ec9bb1e1cd78753c", size = 2999841 }, + { url = "https://files.pythonhosted.org/packages/ac/89/8a2536e95e77432833f0db6fd72a8d310c8e4272a04461fb833eb021bf94/pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:738db0e0941ca0376804d4de6a782c005245264edaa253ffce24e5a15cbdc7bd", size = 3437470 }, + { url = "https://files.pythonhosted.org/packages/9d/8f/abd47b73c60712f88e9eda32baced7bfc3e9bd6a7619bb64b93acff28c3e/pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9db98ab6565c69082ec9b0d4e40dd9f6181dab0dd236d26f7a50b8b9bfbd5076", size = 3460013 }, + { url = "https://files.pythonhosted.org/packages/f6/20/5c0a0aa83b213b7a07ec01e71a3d6ea2cf4ad1d2c686cc0168173b6089e7/pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:036e53f4170e270ddb8797d4c590e6dd14d28e15c7da375c18978045f7e6c37b", size = 3527165 }, + { url = "https://files.pythonhosted.org/packages/58/0e/2abab98a72202d91146abc839e10c14f7cf36166f12838ea0c4db3ca6ecb/pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:14f73f7c291279bd65fda51ee87affd7c1e097709f7fdd0188957a16c264601f", size = 3571586 }, + { url = "https://files.pythonhosted.org/packages/21/2c/5e05f58658cf49b6667762cca03d6e7d85cededde2caf2ab37b81f80e574/pillow-11.2.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:208653868d5c9ecc2b327f9b9ef34e0e42a4cdd172c2988fd81d62d2bc9bc044", size = 2674751 }, ] [[package]] @@ -2803,6 +2829,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634 }, ] +[[package]] +name = "pytest-aiohttp" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/4b/d326890c153f2c4ce1bf45d07683c08c10a1766058a22934620bc6ac6592/pytest_aiohttp-1.1.0.tar.gz", hash = "sha256:147de8cb164f3fc9d7196967f109ab3c0b93ea3463ab50631e56438eab7b5adc", size = 12842 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/0f/e6af71c02e0f1098eaf7d2dbf3ffdf0a69fc1e0ef174f96af05cef161f1b/pytest_aiohttp-1.1.0-py3-none-any.whl", hash = "sha256:f39a11693a0dce08dd6c542d241e199dd8047a6e6596b2bcfa60d373f143456d", size = 8932 }, +] + [[package]] name = "pytest-anyio" version = "0.0.0" @@ -3417,14 +3457,14 @@ wheels = [ [[package]] name = "starlette" -version = "0.46.1" +version = "0.46.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/04/1b/52b27f2e13ceedc79a908e29eac426a63465a1a01248e5f24aa36a62aeb3/starlette-0.46.1.tar.gz", hash = "sha256:3c88d58ee4bd1bb807c0d1acb381838afc7752f9ddaec81bbe4383611d833230", size = 2580102 } +sdist = { url = "https://files.pythonhosted.org/packages/ce/20/08dfcd9c983f6a6f4a1000d934b9e6d626cff8d2eeb77a89a68eef20a2b7/starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5", size = 2580846 } wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/4b/528ccf7a982216885a1ff4908e886b8fb5f19862d1962f56a3fce2435a70/starlette-0.46.1-py3-none-any.whl", hash = "sha256:77c74ed9d2720138b25875133f3a2dae6d854af2ec37dceb56aef370c1d8a227", size = 71995 }, + { url = "https://files.pythonhosted.org/packages/8b/0c/9d30a4ebeb6db2b25a841afbb80f6ef9a854fc3b41be131d249a977b4959/starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35", size = 72037 }, ] [[package]] @@ -3536,11 +3576,11 @@ wheels = [ [[package]] name = "typing-extensions" -version = "4.13.1" +version = "4.13.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/76/ad/cd3e3465232ec2416ae9b983f27b9e94dc8171d56ac99b345319a9475967/typing_extensions-4.13.1.tar.gz", hash = "sha256:98795af00fb9640edec5b8e31fc647597b4691f099ad75f469a2616be1a76dff", size = 106633 } +sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967 } wheels = [ - { url = "https://files.pythonhosted.org/packages/df/c5/e7a0b0f5ed69f94c8ab7379c599e6036886bffcde609969a5325f47f1332/typing_extensions-4.13.1-py3-none-any.whl", hash = "sha256:4b6cf02909eb5495cfbc3f6e8fd49217e6cc7944e145cdda8caa3734777f9e69", size = 45739 }, + { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806 }, ] [[package]] @@ -3574,24 +3614,24 @@ wheels = [ [[package]] name = "urllib3" -version = "2.3.0" +version = "2.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/aa/63/e53da845320b757bf29ef6a9062f5c669fe997973f966045cb019c3f4b66/urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d", size = 307268 } +sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672 } wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369 }, + { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680 }, ] [[package]] name = "uvicorn" -version = "0.34.0" +version = "0.34.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "h11" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4b/4d/938bd85e5bf2edeec766267a5015ad969730bb91e31b44021dfe8b22df6c/uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9", size = 76568 } +sdist = { url = "https://files.pythonhosted.org/packages/86/37/dd92f1f9cedb5eaf74d9999044306e06abe65344ff197864175dbbd91871/uvicorn-0.34.1.tar.gz", hash = "sha256:af981725fc4b7ffc5cb3b0e9eda6258a90c4b52cb2a83ce567ae0a7ae1757afc", size = 76755 } wheels = [ - { url = "https://files.pythonhosted.org/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4", size = 62315 }, + { url = "https://files.pythonhosted.org/packages/5f/38/a5801450940a858c102a7ad9e6150146a25406a119851c993148d56ab041/uvicorn-0.34.1-py3-none-any.whl", hash = "sha256:984c3a8c7ca18ebaad15995ee7401179212c59521e67bfc390c07fa2b8d2e065", size = 62404 }, ] [[package]]