diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index fc93d85..5524706 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -4,9 +4,9 @@ name: CI on: # Triggers the workflow on push or pull request events but only for the "main" branch push: - branches: ["main"] + branches: [ "main" ] pull_request: - branches: ["main"] + branches: [ "main" ] # Allows you to run this workflow manually from the Actions tab workflow_dispatch: @@ -44,7 +44,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install the project - run: uv sync --dev --extra eso --extra lt + run: uv sync --dev --extra eso --extra lt --extra salt - name: Run tests run: uv run pytest diff --git a/README.md b/README.md index ce016aa..c8e0d0e 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ A suite of modules to enable TDA/MMA observations [issues](https://github.com/AEONplus/AEONlib/issues) # Configuration + Many of the facilities and services accessed by AEONlib require specific configuration such as api keys, urls, etc. All configuration can be supplied by either supplying a .env file or setting environmental variables in the execution environment. @@ -33,8 +34,8 @@ Environmental variables take precedence over .env files. See the [pydantic-settings](https://docs.pydantic.dev/latest/concepts/pydantic_settings/) documentation for more details. - # Testing + This project uses [pytest](https://docs.pytest.org/) to run tests: ```bash @@ -59,6 +60,7 @@ pytest -m "not side_effect" CI does not run tests marked as online. ## Viewing logs during tests + Aeonlib turns on the Pytest [Live Logging](https://docs.pytest.org/en/stable/how-to/logging.html#live-logs) feature. By default any logging calls with a level above `WARNING` will be displayed to the console @@ -70,9 +72,11 @@ pytest -m online --log-cli-level=debug ``` # Linting + All code is formatted via [ruff](https://astral.sh/ruff). # Code Generation + Las Cumbres Observatory [instrument classes](src/aeonlib/ocs/lco/instruments.py) are generated via the [generator.py](codegen/lco/generator.py) script. This script takes as input the [OCS instruments api](https://observe.lco.global/api/instruments/) @@ -105,6 +109,7 @@ LCO instruments definition file: ```bash curl https://observe.lco.global/api/instruments/ | codegen/lco/generator.py {facility} > src/aeonlib/ocs/lco/instruments.py ``` + # Supported Facilities This list is a work in progress. @@ -112,15 +117,18 @@ This list is a work in progress. ## Las Cumbres Observatory (LCO) ### Dependency group + Las Cumbres Observatory requires no additional dependency groups to be installed. ### Configuration Values + See [configuration](#configuration) for instructions on setting these values. ```python lco_token: str = "" lco_api_root: str = "https://observe.lco.global/api/" ``` + ### Helpful links * [LCO Observation Portal](https://observe.lco.global/) @@ -132,15 +140,18 @@ lco_api_root: str = "https://observe.lco.global/api/" SOAR is functionally the same as LCO, but has its own set of instruments and can be configured separately. ### Dependency group + SOAR requires no additional dependency groups to be installed. ### Configuration Values + See [configuration](#configuration) for instructions on setting these values. ```python soar_token: str = "" soar_api_root: str = "https://observe.lco.global/api/" ``` + Note: the soar API token will default to the same value as lco_token, if it is set. ## BLANCO @@ -148,15 +159,18 @@ Note: the soar API token will default to the same value as lco_token, if it is s BLANCO is functionally the same as LCO, but has its own set of instruments and can be configured separately. ### Dependency group + BLANCO requires no additional dependency groups to be installed. ### Configuration Values + See [configuration](#configuration) for instructions on setting these values. ```python blanco_token: str = "" blanco_api_root: str = "https://observe.lco.global/api/" ``` + Note: the blanco API token will default to the same value as lco_token, if it is set. ## ESO (European Southern Observatory) @@ -164,7 +178,9 @@ Note: the blanco API token will default to the same value as lco_token, if it is Full documentation: TODO ### Dependency Group + To use the ESO facility, you must install the `eso` group: + ```bash pip install aeonlib[eso] uv sync --extra eso @@ -172,6 +188,7 @@ poetry install --with eso ``` ### Configuration Values + See [configuration](#configuration) for instructions on setting these values. ```python @@ -185,17 +202,20 @@ eso_password: str = "" * [ESO Phase 2 API](https://www.eso.org/sci/observing/phase2/p2intro/Phase2API.html) * [ESO Phase 2 Demo Application](https://www.eso.org/p2demo/home) - ## LT (Liverpool Telescope) ### Dependency Group + To use the LT facility, you must install the `lt` group: + ```bash pip install aeonlib[lt] uv sync --extra lt poetry install --with lt ``` + ### Configuration Values + See [configuration](#configuration) for instructions on setting these values. ```python @@ -204,17 +224,43 @@ lt_password: str = "" lt_host: str = "" lt_port: str = "" ``` + ### Helpful links -* [LT Phase 2 Information](https://telescope.livjm.ac.uk/PropInst/Phase2/) +* [LT Phase 2 Information](https://telescope.livjm.ac.uk/PropInst/Phase2/) ## SAAO (South African Astronomical Observatory) ### Configuration Values + ```python saao_token: str = "" - saao_api_root: str = "https://ocsio.saao.ac.za/api/" +saao_api_root: str = "https://ocsio.saao.ac.za/api/" ``` ### Helpful links + * [SAAO Observatory Control System](https://ocsio.saao.ac.za/create) + +## SALT (Southern African Large Telescope) + +### Dependency Group + +To use the SALT facility, you must install the `salt` group: + +```bash +pip install aeonlib[salt] +uv sync --extra salt +poetry install --with salt +``` + +### Configuration values + +See [configuration](#configuration) for instructions on setting these values. + +```python +salt_username: str = "" +salt_password = "" +``` + +The username and password are those you would use for the [SALT Web Manager](https://www.salt.ac.za/wm/). diff --git a/examples/SALT.ipynb b/examples/SALT.ipynb new file mode 100644 index 0000000..ab19dc2 --- /dev/null +++ b/examples/SALT.ipynb @@ -0,0 +1,323 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "27270ca2181b357f", + "metadata": { + "collapsed": true, + "jupyter": { + "outputs_hidden": true + } + }, + "source": [ + " # Submitting a SALT observation request\n", + "\n", + " This example illustrates how to submit an observation request to the Southern African Large Telescope (SALT). It assumes that you already had a look at the notebook for LCO anmd ESO requests." + ] + }, + { + "cell_type": "markdown", + "id": "213d6d0667c7ed71", + "metadata": {}, + "source": [ + "## Configuration\n", + "\n", + "You need four pieces of configuration details:\n", + "\n", + "1. Your SALT username. This is defined as the `salt_username` property of `aeonlib.conf.Settings`.\n", + "2. Your SALT password. This is defined as the `salt_password` property of `aeonlib.conf.Settings`.\n", + "3. The proposal code. This must be the proposal code of an existing proposal, and you must be allowed to resubmit this proposal.\n", + "4. The semester in the format yyyy-s, with the year yyyy and the semester (1 or 2) s." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "c07b4b623831022f", + "metadata": { + "ExecuteTime": { + "end_time": "2026-05-07T09:38:47.929188Z", + "start_time": "2026-05-07T09:38:46.710824Z" + } + }, + "outputs": [], + "source": [ + "from datetime import datetime, timedelta\n", + "from pprint import pprint\n", + "from time import sleep\n", + "\n", + "from astropy import units as u\n", + "from pyastrosalt.submission import SubmissionStatus\n", + "\n", + "from aeonlib.models import Window\n", + "from aeonlib.salt.facility import SALTFacility\n", + "from aeonlib.salt.models import SaltSiderealTarget, MagnitudeRange, Hrs, HrsDetector, Constraints, Acquisition, Block, \\\n", + " Request\n", + "\n", + "# Replace with the correct proposal code and semester\n", + "proposal_code = \"2025-2-DDT-005\"\n", + "semester = \"2026-1\"" + ] + }, + { + "cell_type": "markdown", + "id": "3865bdbe5db287be", + "metadata": {}, + "source": [ + "## Create the observation request\n", + "\n", + "The observation request is defined using models from `aeonlib.salt.models`. This example is for illustrative purposes only." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "cf9cb87420a39f4c", + "metadata": { + "ExecuteTime": { + "end_time": "2026-05-07T09:38:48.573672Z", + "start_time": "2026-05-07T09:38:48.509144Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'blocks': [{'acquisition': {'do_not_flip_position_angle': False,\n", + " 'exposure_time': np.float64(1.0),\n", + " 'filter': 'Johnson V',\n", + " 'finder_charts': [],\n", + " 'include_focused_image': False,\n", + " 'position_angle': np.float64(50.0),\n", + " 'reference_star': None},\n", + " 'comments': None,\n", + " 'constraints': {'max_lunar_phase_percentage': 50.0,\n", + " 'max_seeing': 2.0,\n", + " 'min_lunar_distance': np.float64(30.0),\n", + " 'transparency': 'Clear'},\n", + " 'data_notification': 'Normal',\n", + " 'identifier': '45ca2cd5-9942-4120-a2d9-ff686e9b0a30',\n", + " 'instrument': {'blue_arm': {'exposure_times': [np.float64(10.0)]},\n", + " 'fibre_separation': np.float64(0.016666666666666666),\n", + " 'instrument_name': 'HRS',\n", + " 'mode': 'LOW RESOLUTION',\n", + " 'num_cycles': 1,\n", + " 'prv_calibration': None,\n", + " 'red_arm': {'exposure_times': [np.float64(10.0)]}},\n", + " 'max_num_visits': None,\n", + " 'min_nights_between_visits': 0,\n", + " 'name': 'Sombrero Galaxy',\n", + " 'num_visits': 1,\n", + " 'pool': None,\n", + " 'priority': 1,\n", + " 'ranking': 'High',\n", + " 'target': {'altitude': None,\n", + " 'azimuth': None,\n", + " 'dec': np.float64(-11.623088333333333),\n", + " 'epoch': 2000,\n", + " 'hour_angle': None,\n", + " 'magnitude_range': {'bandpass': 'V',\n", + " 'max_magnitude': 8.0,\n", + " 'min_magnitude': 8.0},\n", + " 'name': 'Sombrero Galaxy',\n", + " 'parallax': 0,\n", + " 'proper_motion_dec': 0,\n", + " 'proper_motion_ra': 0,\n", + " 'ra': np.float64(189.99763083333332),\n", + " 'target_type': 'Galaxy',\n", + " 'type': 'ICRS'},\n", + " 'windows': [{'end': datetime.datetime(2026, 6, 6, 12, 0, 7, 431184),\n", + " 'start': datetime.datetime(2026, 5, 8, 12, 0, 7, 431175)}]}],\n", + " 'proposal_code': '2025-2-DDT-005',\n", + " 'semester': '2025-2'}\n" + ] + } + ], + "source": [ + "# Observe the Sombrero Galaxy\n", + "sombrero = SaltSiderealTarget(\n", + " name=\"Sombrero Galaxy\",\n", + " type=\"ICRS\",\n", + " ra=\"12h 39m 59.4314s\",\n", + " dec=\"−11° 37′ 23.118\",\n", + " target_type=\"Galaxy\",\n", + " magnitude_range=MagnitudeRange(min_magnitude=8.0, max_magnitude=8.0, bandpass=\"V\")\n", + ")\n", + "\n", + "# Use the High Resolution Spectrograph\n", + "blue_arm = HrsDetector(exposure_times=[10 * u.s])\n", + "red_arm = HrsDetector(exposure_times=[10 * u.s])\n", + "hrs = Hrs(mode=\"low resolution\", blue_arm=blue_arm, red_arm=red_arm)\n", + "\n", + "# Define observing constraints\n", + "constraints = Constraints(\n", + " transparency=\"clear\",\n", + " max_lunar_phase_percentage=50,\n", + " min_lunar_distance=30 * u.deg,\n", + " max_seeing=2,\n", + ")\n", + "\n", + "# Define the acquisition. Finder charts are optional\n", + "acquisition = Acquisition(\n", + " finder_charts=[],\n", + " position_angle=50 * u.deg,\n", + ")\n", + "\n", + "# Window bounds can be specified as datetimes or Astropy.time.Time objects\n", + "window = Window(start=datetime.now() + timedelta(days=1), end=datetime.now() + timedelta(days=30))\n", + "\n", + "# Define the observation block\n", + "block = Block(\n", + " name=f\"Sombrero Galaxy\",\n", + " priority=1,\n", + " ranking=\"high\",\n", + " num_visits=1,\n", + " constraints=constraints,\n", + " windows=[window],\n", + " target=sombrero,\n", + " acquisition=acquisition,\n", + " instrument=hrs,\n", + ")\n", + "\n", + "request = Request(proposal_code=proposal_code, semester=\"2025-2\", blocks=[block])\n", + "\n", + "pprint(request.model_dump())\n" + ] + }, + { + "cell_type": "markdown", + "id": "298840c7a9cffb06", + "metadata": {}, + "source": [ + "## Validate the observation request\n", + "\n", + "The observation request can be validated with the `validate` method of the SALT facility.\n", + "\n", + "Note that we call the facility constructor with its `use_playground` parameter set to `True`. This ensures that a test server rather than the production server is used. The default is to use the production server.\n", + "\n", + "As your observation request is sent to the server, it may take a few moments before the validation completes." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "617c2ad50e26bf7c", + "metadata": { + "ExecuteTime": { + "end_time": "2026-05-07T09:39:29.750203Z", + "start_time": "2026-05-07T09:39:29.688225Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "environ({'COMMAND_MODE': 'unix2003', 'TERM_SESSION_ID': 'ec268349-b3f9-4a8d-b0e0-bc976fb4745d', 'SHELL': '/bin/zsh', 'TMPDIR': '/var/folders/3l/v9_k6g5s1174c0nz5dz00jv80000gn/T/', '__CFBundleIdentifier': 'com.jetbrains.pycharm', 'ENABLE_IDE_INTEGRATION': 'true', 'JEDITERM_SOURCE_ARGS': '', 'HOME': '/Users/christian', 'PATH': '/Users/christian/IdeaProjects/AEONlib/.venv/bin:/Users/christian/.bun/bin:/Users/christian/.pyenv/shims:/Users/christian/Documents/Scripts:/Library/PostgreSQL/13/bin:/usr/local/Cellar/bash/5.2.21/bin:/usr/local/opt/mysql-client@5.7/bin:/usr/local/opt/llvm/bin:/usr/local/Cellar/postgresql@18/18.3/bin:/Library/Frameworks/Python.framework/Versions/3.13/bin:/Library/Frameworks/Python.framework/Versions/3.11/bin:/Library/Frameworks/Python.framework/Versions/2.7/bin:/Library/Frameworks/Python.framework/Versions/3.7/bin:/Library/Frameworks/Python.framework/Versions/3.9/bin:/Library/Frameworks/Python.framework/Versions/3.8/bin:/Users/christian/.deno/bin:/Users/christian/.langflow/uv:/Users/christian/.pyenv/bin:/usr/local/bin:/System/Cryptexes/App/usr/bin:/usr/bin:/bin:/usr/sbin:/sbin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/local/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/appleinternal/bin:/opt/pkg/env/active/bin:/opt/pmk/env/global/bin:/Library/Apple/usr/bin:/usr/local/MacGPG2/bin:/usr/local/go/bin:/Library/Frameworks/Mono.framework/Versions/Current/Commands:/Applications/quarto/bin:/Users/christian/.wasmedge/bin:/Users/christian/.cargo/bin:/Users/christian/.local/bin:/usr/local/bin:/Users/christian/.orbstack/bin', 'PROCESS_LAUNCHED_BY_Q': '1', 'LOGNAME': 'christian', 'TERM': 'xterm-color', 'OSLogRateLimit': '64', 'XPC_FLAGS': '0x0', '__CF_USER_TEXT_ENCODING': '0x1F5:0x0:0x0', 'LC_CTYPE': 'UTF-8', 'JEDITERM_SOURCE': '/Users/christian/IdeaProjects/AEONlib/.venv/bin/activate', 'FIG_TERM': '1', 'SSH_AUTH_SOCK': '/var/run/com.apple.launchd.sz8hMP8NFm/Listeners', 'CLAUDE_CODE_SSE_PORT': '65516', 'XPC_SERVICE_NAME': '0', 'USER': 'christian', 'TERMINAL_EMULATOR': 'JetBrains-JediTerm', 'PROCESS_LAUNCHED_BY_CW': '1', 'SHLVL': '1', 'PWD': '/Users/christian/IdeaProjects/AEONlib', 'OLDPWD': '/Users/christian/IdeaProjects/AEONlib', 'DYLD_LIBRARY_PATH': '/Users/christian/.wasmedge/lib', 'LIBRARY_PATH': '/Users/christian/.wasmedge/lib', 'C_INCLUDE_PATH': '/Users/christian/.wasmedge/include', 'CPLUS_INCLUDE_PATH': '/Users/christian/.wasmedge/include', 'LANG': 'C.UTF-8', 'PYENV_ROOT': '/Users/christian/.pyenv', 'ZSH': '/Users/christian/.oh-my-zsh', 'PAGER': 'cat', 'LESS': '-R', 'LSCOLORS': 'Gxfxcxdxbxegedabagacad', 'LS_COLORS': 'di=1;36:ln=35:so=32:pi=33:ex=31:bd=34;46:cd=34;43:su=30;41:sg=30;46:tw=30;42:ow=30;43', 'VIRTUAL_ENV_DISABLE_PROMPT': '1', 'JAVA_HOME': '/Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home', 'NVM_DIR': '/Users/christian/.nvm', 'NVM_CD_FLAGS': '-q', 'PYENV_SHELL': 'zsh', 'BUN_INSTALL': '/Users/christian/.bun', 'VIRTUAL_ENV': '/Users/christian/IdeaProjects/AEONlib/.venv', 'VIRTUAL_ENV_PROMPT': 'aeonlib', '_': '/Users/christian/IdeaProjects/AEONlib/.venv/bin/jupyter', 'JPY_SESSION_NAME': '/Users/christian/IdeaProjects/AEONlib/examples/SALT.ipynb', 'JPY_PARENT_PID': '82763', 'PYDEVD_USE_FRAME_EVAL': 'NO', 'CLICOLOR': '1', 'FORCE_COLOR': '1', 'CLICOLOR_FORCE': '1', 'GIT_PAGER': 'cat', 'MPLBACKEND': 'module://matplotlib_inline.backend_inline', 'SALT_USERNAME': 'ariba', 'SALT_PASSWORD': 'lookingup'})\n" + ] + }, + { + "ename": "ValueError", + "evalue": "salt_username is not set.", + "output_type": "error", + "traceback": [ + "\u001B[31m---------------------------------------------------------------------------\u001B[39m", + "\u001B[31mValueError\u001B[39m Traceback (most recent call last)", + "\u001B[36mCell\u001B[39m\u001B[36m \u001B[39m\u001B[32mIn[3]\u001B[39m\u001B[32m, line 3\u001B[39m\n\u001B[32m 1\u001B[39m print(os.environ)\n\u001B[32m 2\u001B[39m \n\u001B[32m----> \u001B[39m\u001B[32m3\u001B[39m facility = SALTFacility(use_playground=\u001B[38;5;28;01mTrue\u001B[39;00m)\n\u001B[32m 4\u001B[39m \n\u001B[32m 5\u001B[39m valid, errors = facility.validate(request)\n\u001B[32m 6\u001B[39m \n", + "\u001B[36mFile \u001B[39m\u001B[32m~/IdeaProjects/AEONlib/src/aeonlib/salt/facility.py:28\u001B[39m, in \u001B[36mSALTFacility.__init__\u001B[39m\u001B[34m(self, use_playground)\u001B[39m\n\u001B[32m 26\u001B[39m username = settings.salt_username\n\u001B[32m 27\u001B[39m \u001B[38;5;28;01mif\u001B[39;00m \u001B[38;5;129;01mnot\u001B[39;00m username:\n\u001B[32m---> \u001B[39m\u001B[32m28\u001B[39m \u001B[38;5;28;01mraise\u001B[39;00m \u001B[38;5;167;01mValueError\u001B[39;00m(\u001B[33m\"\u001B[39m\u001B[33msalt_username is not set.\u001B[39m\u001B[33m\"\u001B[39m)\n\u001B[32m 29\u001B[39m password = settings.salt_password\n\u001B[32m 30\u001B[39m \u001B[38;5;28;01mif\u001B[39;00m \u001B[38;5;129;01mnot\u001B[39;00m password:\n", + "\u001B[31mValueError\u001B[39m: salt_username is not set." + ] + } + ], + "source": [ + "facility = SALTFacility(use_playground=True)\n", + "\n", + "valid, errors = facility.validate(request)\n", + "\n", + "if valid:\n", + " print(\"The observation request is valid.\")\n", + "else:\n", + " print(\"Validation of the observation request failed with the following error(s):\")\n", + " for error in errors:\n", + " print(error)" + ] + }, + { + "cell_type": "markdown", + "id": "738d55448112708e", + "metadata": {}, + "source": [ + "## Submit the observation request\n", + "\n", + "The observation request can be submitted with the `submit` method of the SALT facility.\n", + "\n", + "This method returns a `pyastrosalt.submission.Submission` object, which you can use to track the submission progress." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "eedbe7ac27488fc4", + "metadata": { + "ExecuteTime": { + "end_time": "2026-05-07T09:40:31.545105Z", + "start_time": "2026-05-07T09:40:31.509820Z" + } + }, + "outputs": [ + { + "ename": "NameError", + "evalue": "name 'facility' is not defined", + "output_type": "error", + "traceback": [ + "\u001B[31m---------------------------------------------------------------------------\u001B[39m", + "\u001B[31mNameError\u001B[39m Traceback (most recent call last)", + "\u001B[36mCell\u001B[39m\u001B[36m \u001B[39m\u001B[32mIn[7]\u001B[39m\u001B[32m, line 1\u001B[39m\n\u001B[32m----> \u001B[39m\u001B[32m1\u001B[39m submission = facility.submit(request)\n\u001B[32m 2\u001B[39m \n\u001B[32m 3\u001B[39m shown_log_messages = \u001B[32m0\u001B[39m\n\u001B[32m 4\u001B[39m \u001B[38;5;28;01mwhile\u001B[39;00m submission.status == SubmissionStatus.IN_PROGRESS:\n", + "\u001B[31mNameError\u001B[39m: name 'facility' is not defined" + ] + } + ], + "source": [ + "submission = facility.submit(request)\n", + "\n", + "shown_log_messages = 0\n", + "while submission.status == SubmissionStatus.IN_PROGRESS:\n", + " sleep(1)\n", + " log_entries = submission.log\n", + " for entry in log_entries[shown_log_messages:]:\n", + " print(f\"[{str(entry.message_type)}] {entry.message}\")\n", + " shown_log_messages = len(log_entries)\n", + "\n", + "if submission.status == SubmissionStatus.SUCCESS:\n", + " print(\n", + " f\"The submission was successful. The proposal code is {submission.proposal_code}.\"\n", + " )\n", + "else:\n", + " print(\"The submission failed with the following error message:\")\n", + " print(submission.error)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.5" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/pyproject.toml b/pyproject.toml index 091a9c1..5eb2230 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,11 @@ codegen = [ "textcase>=0.2.1", ] -dev = ["pytest>=8.3.5"] +dev = [ + "lxml-stubs>=0.5.1", + "pytest>=8.3.5", + "time-machine>=3.2.0", +] [project.optional-dependencies] eso = ["p2api>=1.0.10"] @@ -31,9 +35,15 @@ lt = [ "lxml-stubs>=0.5.1", "suds>=1.2.0", ] +salt = [ + "beautifulsoup4>=4.14.3", + "jinja2>=3.1.6", + "lxml>=6.0.1", + "pyastrosalt>=0.2.2", +] [tool.pytest.ini_options] -addopts = ["--import-mode=importlib", "-m not online"] +addopts = ["--import-mode=importlib", "-m not online"] markers = [ "online: Marks tests that run online, for example, validating schemas", "side_effect: Online tests that have side effects such as creating observation requests", diff --git a/src/aeonlib/conf.py b/src/aeonlib/conf.py index f5a1d24..e02d58a 100644 --- a/src/aeonlib/conf.py +++ b/src/aeonlib/conf.py @@ -35,5 +35,9 @@ class Settings(BaseSettings): lt_host: str = "" lt_port: str = "" + # Southern African Large Telescope + salt_username: str = "" + salt_password: str = "" + settings = Settings() diff --git a/src/aeonlib/salt/__init__.py b/src/aeonlib/salt/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/aeonlib/salt/facility.py b/src/aeonlib/salt/facility.py new file mode 100644 index 0000000..e4e5c48 --- /dev/null +++ b/src/aeonlib/salt/facility.py @@ -0,0 +1,78 @@ +import io +import logging + +from pyastrosalt.session import Session +from pyastrosalt.submission import Submission, validate, submit + +from aeonlib.conf import settings +from aeonlib.salt.models import Request + +logger = logging.getLogger(__name__) + + +class SALTFacility: + """ + SALT facility for validating and submitting SALT observation requests. + + By default, the facility is using the production server. For testing purposes you + should call the facility constructor with the `use_playground` parameter set to + `True`. + """ + + def __init__(self, use_playground: bool = False): + self._session = Session() + if use_playground: + self._session.use_playground() + username = settings.salt_username + if not username: + raise ValueError("salt_username is not set.") + password = settings.salt_password + if not password: + raise ValueError("salt_password is not set.") + self._session.login(username, password) + + def validate(self, request: Request) -> tuple[bool, list[str]]: + """ + Send an observation request to the server for validation. + + The method waits for the validation request to complete and then returns a tuple + with a boolean indicating whether the request is valid (`True`) or invalid + (`False`) and the list of errors found by the validation. + + Parameters + ---------- + request + The observation request. + + Returns + ------- + A tuple of a boolean indicating whether the request is valid and the list of + errors. + """ + logger.debug(f"Validating request:\n{request.model_dump()}") + zip_content = io.BytesIO() + request.export(zip_content) + zip_content.seek(0) + return validate(self._session, zip_content, request.proposal_code) + + def submit(self, request: Request) -> Submission: + """ + Submit an observation request. + + The method returns a `pyastrosalt.submission.Submision` object, which you can + use to follow the submission progress. + + Parameters + ---------- + request + The observation request. + + Returns + ------- + A `pyastrosalt.submission.Submission` object. + """ + logger.debug(f"Submitting request:\n{request.model_dump()}") + zip_content = io.BytesIO() + request.export(zip_content) + zip_content.seek(0) + return submit(self._session, zip_content, request.proposal_code) diff --git a/src/aeonlib/salt/models/__init__.py b/src/aeonlib/salt/models/__init__.py new file mode 100644 index 0000000..5816a12 --- /dev/null +++ b/src/aeonlib/salt/models/__init__.py @@ -0,0 +1,50 @@ +from .target_models import MagnitudeRange, SaltSiderealTarget +from .hrs_models import Hrs, HrsDetector +from .nirwals_models import Nirwals, NirwalsDitherPatternStep +from .rss_models import ( + Rss, + RssDetector, + RssDitherPattern, + RssImaging, + RssLongslitSpectroscopy, + RssMultiObjectSpectroscopy, + RssPolarimetry, + RssSlitMaskIFUSpectroscopy, + RssSpectroscopy, +) +from .salticam_models import ( + SalticamFilterSequenceStep, + Salticam, + SalticamDetector, + SalticamDitherPattern, +) +from .block_models import Acquisition, Block, Constraints, ReferenceStar +from .request_models import Request + + +__all__ = [ + "Acquisition", + "Block", + "Constraints", + "Hrs", + "HrsDetector", + "Nirwals", + "NirwalsDitherPatternStep", + "SalticamDitherPattern", + "SalticamFilterSequenceStep", + "MagnitudeRange", + "ReferenceStar", + "Request", + "Rss", + "RssDetector", + "RssDitherPattern", + "RssImaging", + "RssLongslitSpectroscopy", + "RssMultiObjectSpectroscopy", + "RssPolarimetry", + "RssSlitMaskIFUSpectroscopy", + "RssSpectroscopy", + "Salticam", + "SalticamDetector", + "SaltSiderealTarget", +] diff --git a/src/aeonlib/salt/models/block_models.py b/src/aeonlib/salt/models/block_models.py new file mode 100644 index 0000000..f3f9535 --- /dev/null +++ b/src/aeonlib/salt/models/block_models.py @@ -0,0 +1,272 @@ +"""This module contains Pydantic models for SALT blocks.""" + +from __future__ import annotations + +import uuid +from typing import Annotated, Literal, Self, TypeAlias + +import astropy.units as u +from pydantic import ( + BaseModel, + FilePath, + NonNegativeInt, + NonNegativeFloat, + PositiveInt, + PositiveFloat, + model_validator, + AfterValidator, + Field, +) + +from aeonlib.models import Angle, Window +from aeonlib.salt.models import SaltSiderealTarget, Salticam, Rss, Hrs, Nirwals +from aeonlib.salt.models.util import LowerCaseValidator, CapitalizingSerializer +from aeonlib.salt.models.types import ( + PositiveDuration, + SalticamFilter, + SalticamFilterSerializer, + SkyTransparency, +) +from aeonlib.salt.validators import GreaterEqual, LessEqual, check_in_visibility_range + + +Instrument: TypeAlias = Salticam | Rss | Hrs | Nirwals + + +class Block(BaseModel, validate_assignment=True): # type: ignore + """ + A block for SALT. + + Blocks are the smallest schedulable unit for an observation; i.e. block is either + observed in total or not ar all. Every block has a unique `identifier`, which should + only be set if you are resubmitting an existing block. + + Observing time for SALT is allocated for different priorities, and you must specify + which is the priority for your block. You also have to rank the block relative to + the other blocks in the proposal. + + The number of visits defines how often the block shall be observed in the + semester for which the submission is made. If you request more than one visit, + you can give a minimum of nights to wait between the observations. For example, + if a block is observed during the night starting on 1 September and this wait + period is 2, the next observation will only take place during the night starting + on 3 September. + + If a block spans multiple semesters, you can provide a maximum number of + observations for all semesters combined. This number must at least be equal to the + number of visits. + + Several blocks can be grouped in a pool. You can specify the pool's name if the + block shall belong to a pool. This pool must exist in the proposal already. + + Other details to specify for the block are the observation constraints, the target + to observe, the acquisition details and the instrument configuration. For time + restricted observation you also may define observing windows. + + By default, observers are notified of new data after the data reduction pipeline has + run for the night of observation. If instead you want to be notified once the data + is transferred to Cape Town (before the pipeline runs), you can set the data + notification accordingly. + + Parameters + ---------- + name + Human-friendly name for the block. This must be unique within a proposal. + identifier + Unique identifier for the block. Only set this if you are resubmitting an + existing block. + comments + Optional comments for the observer. + priority + Priority for the block. + ranking + Ranking (importance) of this block relative to the other blocks in the proposal. + num_visits + Number of visits, i.e. how often the block shall be observed in the semester + for which the submission is made. + max_num_visits + Maximum number of visits, i.e. the maximum number of times the block shall be + observed for all semesters combined. + min_nights_between_visits + Minimum number of nights to wait between subsequent observations of the block. + constraints + Observation constraints. + windows + List of time intervals during which the block shall be observed. + target + Target to observe. + acquisition + Acquisition details. + instrument + Instrument configuration. + pool + Name of the pool to which the block shall belong. The pool must exist in the + proposal already. + data_notification + When you want to be notified about new data for the block. + """ + + name: str + identifier: str = Field(default_factory=lambda: str(uuid.uuid4())) + comments: str | None = None + priority: Annotated[int, GreaterEqual(0), LessEqual(4)] + ranking: Annotated[ + Literal["high", "medium", "low"], LowerCaseValidator, CapitalizingSerializer + ] + num_visits: PositiveInt + max_num_visits: PositiveInt | None = None + min_nights_between_visits: NonNegativeInt = 0 + constraints: Constraints + windows: list[Window] | None = None + target: SaltSiderealTarget + acquisition: Acquisition + instrument: Instrument + pool: str | None = None + data_notification: Annotated[ + Literal["normal", "fast"], LowerCaseValidator, CapitalizingSerializer + ] = "normal" + + @model_validator(mode="after") + def check_max_num_visits_is_at_least_num_visits(self) -> Self: + if self.max_num_visits is not None: + if self.max_num_visits < self.num_visits: + raise ValueError( + "max_num_visits must be greater than or equal to num_visits." + ) + + return self + + +class Constraints(BaseModel, validate_assignment=True): # type: ignore + """ + Observing constraints. + + An observation can be constrained by the sky transparency, the Moon phase, the lunar + distance and the seeing. + + The lunar phase is specified in terms of what percentage p of the lunar disk is + illuminated. For New Moon p is 0, for Full Moon it is 100. In general, p and the + lunar elongation e (i.e., the angle between Sun, observer on Earth and Moon) are + related by p = 100% * (1 - cos(e)) / 2. + + The lunar distance is the angle between the target to observe, Earth and Moon. + + The lunar phase and distance are only relevant if the Moon is above the horizon. + + The seeing must be given for the zenith. + + Parameters + ---------- + transparency + Required sky transparency. + max_lunar_phase_percentage + Maximum allowed Lunar phase, as a percentage. This is the percentage of the + lunar disk which is illuminated. + min_lunar_distance + Minimum required lunar distance. + max_seeing + Maximum allowed seeing. + """ + + transparency: Annotated[SkyTransparency, LowerCaseValidator, CapitalizingSerializer] + max_lunar_phase_percentage: Annotated[NonNegativeFloat, LessEqual(100)] + min_lunar_distance: Annotated[ + Angle, GreaterEqual(0 * u.deg), LessEqual(180 * u.deg) + ] + max_seeing: PositiveFloat + + +class Acquisition(BaseModel, validate_assignment=True): # type: ignore + """ + An acquisition. + + By default, SALT acquisitions are taken with a Johnson V filter and a (nominal) + exposure time of 1 second, but you may choose a different filter or explicitly set + an exposure time. + + The acquisition image is not taken in focus. If you require an in-focus image as + well, you must explicitly request it. + + A finder chart will automatically be generated during submission. However, you may + include additional finder charts, for example if your target is a transient and + hence will not show on the automatically generate finder charts. + + Parameters + ---------- + finder_charts + Additional finder charts. The specified files must exist. + filter + Filter to use for the acquisition. Any Salticam filter may be used. + exposure_time + Exposure time to use for the acquisition. + reference_star + Reference star on which to acquire. This is only needed if acquiring on the + target itself is unfeasible. + position_angle + Position angle for the observation. This can be an angle or the string + "parallactic". + do_not_flip_position_angle + Whether the position angle may be flipped by 180 degrees. This must not be None + if the position angle value is angle (rather than "parallactic" or None). If the + position angle value is not an angle, the value of do_not_flip_position_angle is + ignored. + include_focused_image + Whether an in-focus focused acquisition image is required. + """ + + finder_charts: list[FilePath] + filter: Annotated[SalticamFilter, LowerCaseValidator, SalticamFilterSerializer] = ( + "Johnson V" + ) + exposure_time: PositiveDuration = 1.0 * u.s + position_angle: Annotated[Angle | Literal["parallactic"] | None, LowerCaseValidator] + reference_star: ReferenceStar | None = None + do_not_flip_position_angle: bool | None = Field( + default_factory=lambda data: ( + None + if isinstance(data["position_angle"], str) or data["position_angle"] is None + else False + ) + ) + include_focused_image: bool = False + + @model_validator(mode="after") + def check_do_not_flip_position_angle(self): + if not isinstance(self.position_angle, str) and self.position_angle is not None: + if self.do_not_flip_position_angle is None: + raise ValueError( + "The do_not_flip_position_angle property must not be None if the " + "position_angle property is an angle." + ) + + return self + + +class ReferenceStar(BaseModel, validate_assignment=True): # type: ignore + """ + A reference star on which to acquire. + + In case acquiring on the target itself is not possible (as, for example, the target + is too faint), you can specify a reference star. This will be used for the + acquisition instead, and after the acquisition a telescope offset from the + reference star to the actual target is applied. + + The right ascension and declination can be supplied as a `astropy.coordinates.Angle` + instance, a `astropy.units.Quantity` instance, a string in a format understood by + AstroPy or a float in degrees. + + By default, the equinox is assumed to be 2000.0, but you can choose another oner. + + Arguments + --------- + ra + Right ascension of the reference star. + dec + Declination of the reference star. + equinox + Equinox of the coordinates. + """ + + ra: Angle + dec: Annotated[Angle, AfterValidator(check_in_visibility_range)] + equinox: Annotated[float, LessEqual(2100)] = 2000.0 diff --git a/src/aeonlib/salt/models/hrs_models.py b/src/aeonlib/salt/models/hrs_models.py new file mode 100644 index 0000000..f9762a6 --- /dev/null +++ b/src/aeonlib/salt/models/hrs_models.py @@ -0,0 +1,74 @@ +from __future__ import annotations + +from typing import Annotated, Literal + +from astropy import units as u +from pydantic import ( + BaseModel, + PositiveInt, + computed_field, +) + +from aeonlib.salt.models.util import LowerCaseValidator, UpperCaseSerializer +from aeonlib.salt.models.types import HrsMode, HrsPrvCalibration, PositiveDuration +from aeonlib.salt.validators import GreaterEqual, LessEqual +from aeonlib.types import Angle + + +class Hrs(BaseModel, validate_assignment=True): # type: ignore + """ + An HRS setup. + + HRS can be used in any of four modes, namely low resolution, medium resolution, high + resolution and high stability. A high precision velocity calibration (using a ThAr + kamp) is available for the high stability mode, but not for any of the other modes. + + A sequence of exposure times can be defined for the red and blue detector arm. If + this sequence shall be executed more than once, a number of cycles need to be set. + + Parameters + ---------- + instrument_name: + The instrument name, which is "HRS". This property is not serialized. + num_cycles + How often the exposure time patterns shall be executed. + mode + The instrument mode, such low resolution or high stability. + prv_calibration + The high precision velocity calibration to use. This must be None for all modes + other than high stability, for which it must be "ThAr". + fibre_separation + The angle between the target and sky fibres. This must be between 16 and 63 + arcseconds (both inclusive). + blue_arm + The detector setup for the red arm. + red_arm + The detector setup for the blue arm. + """ + + instrument_name: Literal["HRS"] = "HRS" + num_cycles: PositiveInt = 1 + mode: Annotated[HrsMode, LowerCaseValidator, UpperCaseSerializer] + fibre_separation: Annotated[ + Angle, GreaterEqual(16 * u.arcsec), LessEqual(63 * u.arcsec) + ] = 60 * u.arcsec + blue_arm: HrsDetector + red_arm: HrsDetector + + @computed_field # type: ignore + @property + def prv_calibration(self) -> HrsPrvCalibration | None: + return "ThAr" if self.mode == "high stability" else None + + +class HrsDetector(BaseModel, validate_assignment=True): # type: ignore + """ + An HRS detector setup. + + A list of exposure times for the detector has to be specified. These exposure times + may be different for the blue and the red detector. + + Other detector properties, such as the readout speed or the binning, cannot be set. + """ + + exposure_times: list[PositiveDuration] diff --git a/src/aeonlib/salt/models/nirwals_models.py b/src/aeonlib/salt/models/nirwals_models.py new file mode 100644 index 0000000..b842fbc --- /dev/null +++ b/src/aeonlib/salt/models/nirwals_models.py @@ -0,0 +1,204 @@ +from __future__ import annotations + +import math +from typing import Annotated, Literal, Any + +from astropy import units as u +from pydantic import ( + BaseModel, + PositiveInt, + field_validator, + PlainSerializer, + BeforeValidator, + computed_field, +) + +from aeonlib.salt.models.types import ( + NirwalsCameraFilter, + NirwalsExposureType, + NirwalsFilter, + NirwalsGain, + NirwalsGrating, + NirwalsOffsetType, + NirwalsSampling, + PositiveDuration, +) +from aeonlib.salt.models.util import ( + LowerCaseValidator, + UpperCaseSerializer, + CapitalizingSerializer, +) +from aeonlib.salt.validators import GreaterEqual, LessEqual +from aeonlib.types import Angle + + +class Nirwals(BaseModel, validate_assignment=True): # type: ignore + """ + A NIRWALS configuration. + + Every NIRWALS configuration includes a dither pattern. If you want to repeat this + pattern, you have to specify the number of cycles. + + Parameters + ---------- + instrument_name: + The instrument name, which is "NIRWALS". + num_cycles + The number of times the dither pattern should be done. + grating + The barcode of the grating to use. + grating_angle + The grating angle. This typically is half the articulation angle. + articulation_angle + The articulation angle. This must be a multiple of 0.5 degrees between 0 and 100 + degrees (both inclusive). + filter + The filter. + camera_filter + The camera filter. + dither pattern + The dither pattern. + include_arc + Whether a nighttime arc should be taken for the observation. + include_flat + Whether a nighttime flat should be taken for the observation. + request_spectrophotometric_standard + Whether a spectrophotometric standard should be taken for the observation. + + """ + + instrument_name: Literal["NIRWALS"] = "NIRWALS" + num_cycles: PositiveInt = 1 + grating: Annotated[NirwalsGrating, LowerCaseValidator] + grating_angle: Annotated[Angle, GreaterEqual(0 * u.deg), LessEqual(100 * u.deg)] + articulation_angle: Angle + filter: Annotated[NirwalsFilter, LowerCaseValidator, UpperCaseSerializer] = "empty" + camera_filter: Annotated[ + NirwalsCameraFilter, LowerCaseValidator, CapitalizingSerializer + ] + dither_pattern: list[NirwalsDitherPatternStep] + include_arc: bool = True + include_flat: bool + request_spectrophotometric_standard: bool = False + + @field_validator("articulation_angle", mode="after") + @classmethod + def check_articulation_angle(cls, angle: Angle) -> Angle: + error = "The articulation angle must be a multiple of 0.5 degress between 0 and 100 degrees (both inclusive" + degrees = angle.to(u.deg).value # type: ignore + + if degrees < 0 or degrees > 100: + raise ValueError(error) + + n = 2 * degrees + if abs(n - round(n)) > 1e-6: + raise ValueError(error) + + return angle + + +class NirwalsDitherPatternStep(BaseModel, validate_assignment=True): # type: ignore + """ + A step in a NIRWALS dither pattern. + + Each step os characterised by the offset type and the offsets in horizontal and + vertical direction, the exposure type and time, and other detector-related + properties. The offset directions are the on-telescope directions (i.e. with the + field rotated by the position angle) + + If a reference star is provided for the acquisition, for the first step the + offset type must be "tracker guided offset" and the offsets must be equal to + those from the reference star to the target. + + Parameters + ---------- + offset_type + The offset type. + horizontal offset + The offset in horizontal telescope direction. This must be between -100 and 100 + arcseconds (both inclusive). + vertical_offset + The offset in vertical telescope direction. This must be between -100 and 100 + arcseconds (both inclusive). + exposure_type + The exposure type. + exposure_time + The exposure time. + gain + The gain to use. + sampling + The sampling method to use. + num_reads + The number of detector readouts. This must be 1. + num_ramps + The number of ramps. This must be 1. + """ + + offset_type: Annotated[ + NirwalsOffsetType, + LowerCaseValidator, + PlainSerializer(NirwalsDitherPatternStep.serialize_offset_type), + ] + horizontal_offset: Annotated[ + Angle, GreaterEqual(-100 * u.arcsec), LessEqual(100 * u.arcsec) + ] + vertical_offset: Annotated[ + Angle, GreaterEqual(-100 * u.arcsec), LessEqual(100 * u.arcsec) + ] + exposure_type: Annotated[ + NirwalsExposureType, LowerCaseValidator, CapitalizingSerializer + ] + exposure_time: PositiveDuration + gain: Annotated[NirwalsGain, LowerCaseValidator, CapitalizingSerializer] + sampling: Annotated[ + NirwalsSampling, + BeforeValidator(NirwalsDitherPatternStep.validate_sampling), + PlainSerializer(NirwalsDitherPatternStep.serialize_sampling), + ] + num_reads: Literal[1] = 1 + num_ramps: Literal[1] = 1 + + @computed_field # type: ignore + @property + def num_groups(self) -> int: + """ + The number of groups. + + The number of groups is equal to the ratio of the exposure time and the product + of reads and frame rate. A value of 0.728 seconds is assumed for the frame rate. + + Returns + ------- + The number of groups. + """ + frame_rate = 0.728 * u.s + # The actual frame rate value is 0.727750 s. However, as a safety measure and to + # avoid rounding differences between different pieces of software, a rounded + # value is used. + + groups = round( + math.floor(float(self.exposure_time / (self.num_reads * frame_rate))) + ) + return max(1, groups) + + @staticmethod + def serialize_offset_type(value: NirwalsOffsetType) -> str: + if value == "fif offset": + return "FIF Offset" + else: + return value.title() + + @staticmethod + def serialize_sampling(value: NirwalsSampling) -> str: + if value == "up-the-ramp": + return "Up-the-Ramp Group" + else: + raise ValueError(f"Sampling cannot be serialized: {value}") + + @staticmethod + def validate_sampling(value: Any) -> Any: + if isinstance(value, str): + value = value.lower() + if value == "up-the-ramp group": + value = "up-the-ramp" + return value diff --git a/src/aeonlib/salt/models/proposal.xsd b/src/aeonlib/salt/models/proposal.xsd new file mode 100644 index 0000000..8580db5 --- /dev/null +++ b/src/aeonlib/salt/models/proposal.xsd @@ -0,0 +1,5085 @@ + + + + + + + + + The root node of a SALT proposal. + + + + + + The title of the proposal. + + + + + A text based version of the abstract. + + + + + + + + The semester. + + 1: 1 May to 31 October + 2: 1 November to 30 April + + + + + + + + + + + 1 = Request for time. + + + + + + + + The role played by the South African investigators. + + + + + + + + + + + For targets of opportunity. Act on alert authorisation. + + + + + + + Flag indicating whether the proposal contains a time critical + observation. + + + + + + + Flag indicating whether the proposal is a Priority 4 proposal. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + The time request for a semester. + + + + + + + + + The semester. + + 1: 1 May to 31 October + 2: 1 November to 30 April + + + + + + + + + + + + + + + + + + + The percentage of time requested from a partner. + + + + + + + + + + + + + + + + + + + + Basic details required for a DDT, commissioning or engineering proposal. + + + + + + The title of the proposal. + + + + + + + The total requested time for this proposal. + + + + + + + + + + + + + The root node of a SALT proposal. + + + + + + + + + + 2 = Detailed telescope configuration. + + + + + + + + + + The period for which the data remains proprietary to the investigators. + + + + + + + + + + + + + + + + + + + + + + + Several elements (namely SubBlock, SubSubBlock, Pointing, TelescopeConfig and PayloadConfig + elements may or may not be shown in a GUI representation of the proposal. The + RequiredElements element states which of these elements have to be shown as they are + syntactically required. The RequestedElements element, on the other hand, states which + elements should be shown irrespective of whether they are necessary from a syntactic point + of view. + + + + + + + + + + + + + + + + + + + + + The semester-specific proposal details. + + + + + + + The title of the proposal. + + + + + A text based version of the abstract. + + + + + + + + A short phrase describing the science for the nightlog summary. Examples would be "Study of the + properties of thin and thick disks of galaxy IC2531" or "Tracing the history of LIRGs". + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + The semester. + + 1: 1 May to 31 October + 2: 1 November to 30 April + + + + + + + + + + + + For targets of opportunity. Act on alert authorisation. + + + + + + + + + + + A payload configuration is mainly used to cope with the situation where a + pellicle setup is being used. It is a rather simple matter to cope with a single instrument + being used at a time. The complexity comes in when two instruments are used concurrently. + With the pellicle setup the primary instrument is always SALTICAM. The fold mirrors allow + one other payload port to be used. So we can use SALTICAM and RSS or SALTICAM and FIF + (Fibre Instrument Feed) or SALTICAM and the auxilliary port. + + + + + + + + + + + If true for a pellicle setup, the next step starts only after both instruments + have finished with the current configuration. In case of no pellicle setup the + value of this element is irrelevant. + + + + + + + + + + + + + + + + + + + + + + + + + The type of this payload configuration. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + The main purpose of the TelescopeConfig element is to group PayloadConfig elements + together. + + + + + + + + Position angle of the camera. Degrees from north (being zero) clockwise + for positive angle + + + + + + + + + + A fixed position angle must not be flipped by 180 degrees. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + A time restriction for an observation. + + + + + + + + + + + + A phase constraint cannot be defined without first defining an Ephemeris in + the referenced Target. + + + + + + + + + + + + + + + + + + + + + + + + + + Describes what seeing conditions are acceptable (in arcsecs). + + + + + + + + + + + + + An acquisition for an observation, including a target, an acquisition telescope + configuration and, optionally, a reference star on which to acquire. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + The reference star to use for an acquisition. + + + + + + + + + + + + An acquisition and a list of telescope configurations. + + + + + + + + + + + + + + + All the information required for a single track observation. In general, this will be a + single Observation. However, if several targets in the same field of view are considered (e.g., three + stars in a cluster), more than one Observation might be included. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Whether this block is charged for. + + + + + + + + + The minimum angular distance between the Moon and the target. + + + + + + + + + + + + + + + + + + + + + The maximum number of visits for all semesters together. + + + + + + + Treat the visits as a single continuous visits, i.e. do them all consecutively without any + wait time between them. + + + + + + + + + + + + + + + + + + + + The block type, such as "Science". + + + + + + + + + + + + + Specifies the priority of the time when this block should + be observed. + + + + + + + + + + + + + The number of days to wait between each visit to this block. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Configuration information required for the data reduction pipeline. + + + + + + + + + + + + + Allows the PI to choose when to be notified that an observation has been done. + The following options exist: + 1) Normal: The PI is notified by email when the raw and reduced data from a night is ready + for retrieval from the ftp server. + 2) Fast: After a night of observations, the individual raw data files are immediately made + available for download and the PI is notified by email that the data is available. + 3) User: he user will download the data from the web manager. The PI will be notified when + new raw and reduced data is accessible via the web manager. + 4) Complete: The PI will be notified when the observations for the proposal are complete and + all data is available for download. + + + + + + + + + + + + + + + The format for the reduction documentation. + + + + + + + + + + + + + A pool of Blocks. + + + + + + + + + + + + + + + + + + + + + + + + The pool type, such as "Science". + + + + + + + + + + + + A pool rule. + + + + + + + + + + + + The available pool rule functions. + + + + + + + + + + + + + Relevant information which was provided in the Phase 1 + proposal. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + The time allocated by the TACs for the various priorities. + + + + + + + + + + + + + + A time transfer between observing conditions. + + + + + + + + + + + + + + + + + + + + + + + + + + + + An observation performed already for a block. + + + + + + + + + + + + + + + + + + The status of a block visit. + + + + + + + + + + + + + + + A server assigned unique id and the corresponding unique PIPT assigned id. + + + + + + + + + + + + + + Records which elements are used. + + + + + + + + + + + + + + + + + + The type of proposal (science, commissioning or director's discretionary time). + + + + + + + + + + + + + + + + + + + + + + + + The investigators associated with this proposal. + + + + + + + + + + + + + + + + + + + + + + + Enumeration of all thw SALT partner names. + + + + + + + + + + + + + + + + + + + + + + + + + + The name of the institute. + + + + + + + + + + + Indicates that the PI for the proposal is affiliated to an + African institution (outside South Africa). + + + + + + + + + + + + The file path to the (external) PDF version of the scientific justification. + + + + + + + + + + + + A thesis which this proposal will form part of. + + + + + + + The type of degree awarded for the thesis. + + + + + + + The expected year of completion for the thesis. + + + + + + + + + + + + + A brief description of the importance and contribution to the thesis as well as the implication + if the data are not obtained in time. + + + + + + + + + + + The type of degree awarded for a thesis which this proposal + forms part of. + + + + + + + + + + + + + This element contains the source of funding, if the proposal + is made possible by external support. + + + + + + + + + + + + The status of a previous proposal. + + + + + + + + + + + + SALT-related refereed publications based on the previous proposal, as a list of ADS bibcodes. + + + + + + + + + + + The list of targets. + + + + + + + + The number of optional targets requested to be observed. + + + + + + + + + + + + + + A unique string identifying the target. + + + + + + + + + + + + + + + + + + + + + + + Flag indicating whether the target is mandatory (true) + or optional (false). + + + + + + + + + + + + A magnitude range. + + + + + + + + + + + + + + + + + + + + + + + + + + The bandpass for the magnitude range. + + + + + + + + + + + + + + + + + + A time when the period begins. The available time bases are + + BJD: Baryocentric Julian Date + HJD: Heliocentric Julian Date + JD: Julian Date + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + The available time base values for the TimeZero element + of a periodic target ephemeris. These are + + BJD: Baryocentric Julian date + HJD: Heliocentric Julian date + JD: Julian date + + + + + + + + + + + + + + + A finding chart. + + The finding chart can be specified as a path to an external file, as an image server to use for creating + it automatically, as a URL to it, or by giving its MIME type and its content as a Base64 encoded string. + + + + + + + + + + + + + + + + + + + + + Sky transparency. + + + + + + + + + + + + + + + The maximum lunar phase allowed for this block. + + + + + + + + + + + + + + + + + + + + Sky brightness due to the Moon. + + Dark: Phase angle > 135 degrees, or Moon set + Dark-Gray: Phase angle between 90 and 135 degrees + Bright-Gray: Phase angle between 45 and 90 degrees + Bright: Phase angle between 0 and 45 degrees + Any: Any phase angle + + + + + + + + + + + + + + A seeing value. + + + + + + + + + + + + + + + + + + + The ranking of an observation (for Phase 1) or of a block within + the priority (for Phase 2). + + + + + + + + + + + + + + An instrument simulation obtained with an instrument simulator. + + + + + + + + + + + + + + Details related to public outreach. These include a summary for the general + public as well as a flag indicating whether the summary of the proposal may + be displayed along with the all-sky camera image. + + + + + + + + + + + + + + + + + + + + + A SALTICAM instrument configuration + + + + + + + + + + A Salticam filter. As a list of simple types causes problems for the PIPT, + it is defined as a complex type with a Filter child element. + + + + + + + + + + + + + + + + + + + + + A SALTICAM instrument configuration + + + + + + + + + Minimum signal-to-noise required. + + + + + + + + + + + + + A Salticam Detector setup. + + + + + + Combine this many CCD rows during readout + + + + + + + + + + + Combine this many CCD columns during readout + + + + + + + + + + + + + + + Defines up to 42 CCD windows, used to restrict readout rows and columns +
    +
  • CentreRA - Right ascension for the window centre
  • +
  • CentreDec - Declination of the window centre
  • +
  • Width - Width of the window in arcseconds
  • +
  • NRows - Height of the window in arcseconds
  • +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + + + Type of exposure to be made +
    +
  • Science - Science exposure
  • +
  • Bias - Bias exposure.
  • +
  • Flat Field - Flat field exposure.
  • +
+
+
+ + + + + +
+ + + + + This sets the amplifier gain for the CCD readout amplifiers. +
    +
  • Faint - high gain for faint objects.
  • +
  • Bright - low gain for bright objects.
  • +
+
+
+ + + + +
+ + + + + Sets the readout speed. Faster readouts have more noise. +
    +
  • None - don't do a readout.
  • +
  • Slow - do a low noise, slow readout.
  • +
  • Fast - do a higher noise, faster readout.
  • +
+
+
+ + + + + +
+ + + + + This specifies the way the instrument is operated. + + + + + + + + + + An array of SALTICAM filters to be used in sequence as listed with + associated exposure times. + + + + + + + + + + + + + A Salticam calibration defined within a Salticam configuration. + + + + + + + + + + + + + + + + + + + + + The number of cycles between two flats. This element should only be used if + "Every N Cycles" is chosen as the flat requirement. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Calibration flat lamps. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + A Salticam calibration setup defined outside a Salticam setup. + + + + + + + + + + + + + Describes the readout modes + + + + + + + + + + + + + + + An RSS instrument configuration + + + + + + + + + + + + + + + + + + + + The observing mode. + + + + + + + + + + + + An imaging setup. + + + + + + + A spectroscopy setup. + + + + + + + + + + + Different options are available for the slitmask: + - Longslit: a slit from a predefined list of longslits + - MOS (Multi object spectroscopy): a slit mask id and a PSMF file + + + + + + + + + + + + + + + + + + + + A predefined slit mask, such as a longslit. + + + + + + + + + + + + The type of a predefined slit mask. + + + + + + + + + + + + + + + + + + A Fabry-Perot setup. + + + + + + + + + + + + The polarimetry types. + + + + + + + + + + + + + + + + + + + + + + This describes the mechanical configuration of the instrument + + + + + + This specifies the exposure for the detector controller + + + + + + Minimum signal-to-noise required. + + + + + + + + + + This states whether the RSS setup is a (nighttime) calibration. + + + + + + + + + + + An RSS procedure. + + + + + + + + + + + + + An RSS configuration. + + + + + Slitmask info. + + + + + + + + + + + The focus position in microns. This is referred to the home indicator + of the focus actuator in the camera. + + + + + + + + + + + + + + + + + + + + + + + + The mode for this setup and the corresponding instrument configuration such as (in case of polarimetry) + the waveplate pattern. + + + + + + + + + + + + + + An RSS imaging configuration. + + + + + + + + + + + + + + + + + + + + An RSS Fabry-Perot configuration. + + + + + + + + + + + + An RSS polarimetry configuration. + + + + + + + + + + + + The orientation of the beamsplitter, which can be either normal or parallel. + + + + + + + + + + + + + Different options are available for the slitmask: + - Longslit: a slit from a predefined list of longslits + - MOS (Multi object spectroscopy): a slit mask id and a PSMF file + - SMI (Slit Mask IFU): a slit mask IFU from a predefined list + + + + + + + + + + + + + + + + + + + + + + + + + + + A predefined slit mask, such as a longslit. + + + + + + + + + + + + A slit mask IFU. + + + + + + + + + + + + A list of half and/or quarter waveplate stations. + + + + + + + + + + + + + + + + + + + A calibration region for Fabry-Perot. + + + + + + + + + + + + An RSS Detector setup. + + + + + + + + + CCD windowing as implemented by the RSS detector, requires a + height (FrameSize) for a window spanning the entire length of the CCD. + + + + + + + + + + + + + + + + + + + + + + + Shuffles this many CCD rows before an exposure + + + + + + + + + + Shuffles this many CCD rows after an exposure + + + + + + + + + + Combine this many CCD rows during readout + + + + + + + + + + + Combine this many CCD columns during readout + + + + + + + + + + + States whether or not the CCD will be prepared for the next exposure + after readout - necessary for all CCD modes apart from shuffle. + + + + + + + + + + + + + + + The RSS detector can do various calculations on an image. +
    +
  • None - no calculation.
  • +
  • Aperture - compute the location of backlit slitmask apertures.
  • +
  • Star - compute the location of stars.
  • +
  • Peakup - centre stars onto slitmask apertures.
  • +
+
+
+ + + + + + + + + +
+ + + + + Type of exposure to be made +
    +
  • Science - Science exposure.
  • +
  • Bias - Bias exposure.
  • +
  • Flat Field - Flat field exposure.
  • +
  • Arc - Wavelength calibration exposure using arc lamp.
  • +
  • Dark
  • +
+
+
+ + + + + + + +
+ + + + + This sets the amplifier gain for the CCD readout amplifiers. +
    +
  • Faint - high gain for faint objects.
  • +
  • Bright - low gain for bright objects.
  • +
+
+
+ + + + +
+ + + + + The readout speed. Faster readouts have more noise. +
    +
  • None - don't do a readout.
  • +
  • Slow - do a low noise, slow readout.
  • +
  • Fast - do a higher noise, faster readout.
  • +
+
+
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + An RSS calibration defined within an RSS setup. + + + + + + + + + + + + + + + + + + + + + + + The number of cycles between two arcs. This element should only be used if + "Every N Cycles" is chosen as the arc requirement. + + + + + + + + + + + + Indicates whether this calibration shall be carried out between dithering + steps. If the arc requirement the value of this element is ignored, as in + this case the calibration needs to be done between dithering steps anyway. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + The number of cycles between two flats. This element should only be used if + "Every N Cycles" is chosen as the flat requirement. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Arc lamps. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Calibration flat lamps. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + An RSS calibration setup defined outside an RSS setup. + + + + + + + + + + + + + + A barcode for a predefined slit mask, such as a longslit or a slit mask IFU. + + + + + + + + + + + + The bar codes of the available RSS filters. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + The bar code of the desired grating. + + + + + + + + + + + + + + + + + The grating can be rotated to any angle between 0 and 100 degrees. The + angular choice is quantized only by the resolution of the stepper motor. + + + + + + + + + + + + + + + + + + + There are 132 detented locations. The enumerated values consists of two + integers. The first integer represents the station number. The second gives the camera + angle in hundredths of a degree. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + The available configurations are TF (tunable filter), LR (low resolution), + MR (medium resolution) and HR (high resolution). + + + + + + + + + + + + + + + + A list of wavelength ranges or wavelengths for etalon positions. + + + + + + + + + + + + + + + + + + + + + + + + + + Describes the readout modes + + + + + + + + + + + + + + + + + An HRS setup. + + + + + + + + + + + + + + + An HRS setup. + + + + + + + + + + + + + + + + An HRS detector setup. + + + + + + + + + The number of amplifiers to use when reading out the CCD. + + + + + + + + + + + Combine this many CCD rows during readout + + + + + + + + + + + Combine this many CCD columns during readout + + + + + + + + + + + + + + + + + The readout speed. Faster readouts have more noise. +
    +
  • None - don't do a readout.
  • +
  • Slow - do a low noise, slow readout.
  • +
  • Fast - do a higher noise, faster readout.
  • +
+
+
+ + + + + +
+ + + + + + Configuration of the HRS. + + + + + + + + + + + + + Whether an exposure with the ThAr lamp should be done in addition to the one + with the iodine cell. This is only relevant if the iodine cell is used , i.e. + if the value of the IodineCellPosition element is IN. + + + + + + + + + + + Exposure type. + + + + + + + + + + + + + + + + A nod and shuffle setting. + + + + + + + The time per nod interval. + + + + + + + + + + + + + + + + + + + The number of nods required. + + + + + + + + + + + Iodine cell position. + + + + + + + + + + + + + + + Target location. + + + + + + + + + + + + Fibre separation in arcseconds + + + + + + + + + + + + + + + + + + + + A procedure. + + + + + + + + + + + + + + An exposure pattern. + + + + + + + + + + + An HRS calibration. + + + + + + + An HRS calibration setup defined outside an HRS setup. + + + + + + + + + + + + + + HRS mode. + + + + + + + + + + + + + + + + + + NIR setup. + + + + + + + + + + + + + + + + + + + + + + This states whether the NIRWALS setup is a (nighttime) calibration. + + + + + + + + + + + NIR sampling options. + + + + + + + + + + + + + Gain setting. + + + + + + + + + + + + + NIR detector setup. + + + + + + + + + + How many reads are done for each sample. For Fowler this is how many reads are + done at the beginning and at the end. For Up-the-Ramp Group this is how many reads you do at + each sample (group) up the ramp. + + + + + + + + + + + The number of samples up the ramp for Up-the-Ramp Group mode. + + + + + + + + + + The number of samples up the ramp for Up-the-Ramp Group mode. + + + + + + + + + + + + + + + The grating can be rotated to any angle between 0 and 50 degrees. + + + + + + + + + + + + + + + + + + + + Articulation station. Each value has two numbers. The first represents + the station number. The second gives the camera angle in degrees. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Barcode of the NIR filter. + + + + + + + + + + + + NIR camera filter wheel. + + + + + + + + + + + + + + + + Bundle separation in arcseconds. + + + + + + + + + + + + + NIR configuration. + + + + + + + + + + + + + + + + + Dither offset, in image coordinates. + + + + + + + + Horizontal offset (in image coordinates) from the first + position, in milliarcseconds. Must be 0 for the first + dither step. + + + + + + + + + + + + Vertical offset (in image coordinates) from the first + position, in milliarcseconds. Must be 0 for the first + dither step. + + + + + + + + + + + + + + + Dither offset type. + + + + + + + + + + + + + + + Exposure type. + + + + + + + + + + + + + + + + + Step in a dither pattern. + + + + + + + + + + + + + + + A dither pattern. + + + + + + + + + + + + NIR procedure type. + + + + + + + + + + + + + NIR procedure. + + + + + + + + + + + + + + + + + + + + + + + + Arc lamps. + + + + + + + + + + + + + + + + + + NIR arc setup. + + + + + + + + + + + + + + + + + + + + The number of cycles between two arcs. This element should only be used if + "Every N Cycles" is chosen as the arc requirement. + + + + + + + + + + + + Indicates whether this calibration shall be carried out between dithering + steps. If the arc requirement the value of this element is ignored, as in + this case the calibration needs to be done between dithering steps anyway. + + + + + + + + + + + + + + + + + + + + + + Calibration flat lamps. + + + + + + + + + + + + + + Calibration flat setup. + + + + + + + + + + The number of cycles between two flats. This element should only be used if + "Every N Cycles" is chosen as the flat requirement. + + + + + + + + + + + + + + + + An NIR calibration. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + NIR grating. + + + + + + + + + + + + + + A BVIT configuration. + + + + + + + + + + + + + + + A BVIT setup. + + + + + + + + + + + + + + + + + + + The operational mode. In imaging mode, a normal exposure is made. + In streaming mode, each incoming photon is tagged with the arrival + time. + + + + + + + + + + + + + The value x in the transmission factor 10^-x of the neutral density filter. + For example, the value 0.3 means that the ratio I/I0 of the intensities I + and I0 after and before passing the filter is I/I0=10^-0.3. The value "open" + indicates that no neutral density filter is used. + + + + + + + + + + + + + + + + The iris size to use for the BVIT observation. + + + + + + + + + + + + A BVIT calibration. + + + + + + + A BVIT calibration setup defined outside a BVIT setup. + + + + + + + + + + + + + + A BVIT filter. The value "Open" indicates that no filter is used. + + + + + + + + + + + + + + + + + + The available target types. Most of the types listed here are + standard names taken from the Simbad target classification + (http://simbad.u-strasbg.fr/simbad/sim-display?data=otypes). + See the SALT document on target classification for a detailed + explanation. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + The coordinates of a target. + + + + + + + + + + + + + + + An angle. A value for this type must start with a digit, optionally + preceded by a plus or minus sign. + + + + + + + + + + + + A proper motion of a target. + + + + + + + + + + + + + + A path to a table containing the target coordinates. + + + + + + + + + + + + + Identifier and output interval to use for obtaining the + ephemerides from NASA JPL's Horizons service. + + The identifier may either be an object name (such as "Ubuntu") + or a designation (such as "2005 EW302"). + + + + + + + + + + + + + + + + + + + + + + + + + + + + Mode of tracking (sidereal or non-sidereal). + + + + + + + + + + + + Equatorial Right Ascension of <Target>, measured in hours, + minutes, and seconds. An optional proper motion may be included. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Container for the equatorial celestial coordinate of a Target in degrees, + arcminutes and (decimal) arcseconds. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Exposure time in seconds + + + + + + + + + + + + + + + + + Exposure time in seconds for a semester. + + + + + + The semester (relative to the semester for which the proposal is first + submitted). So if the proposal spans three semesters, the value may bw + 1, 2 or 3. + + + + + + + + + + + + + + + + + + + + + + + A number of iterations, i.e. a positive integer (which mustn't exceed the + upper limit of xs:unsignedInt). + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Dithering, characterized by the number of horizontal and vertical tiles, the number of + dithering steps and the offset between steps (which is the same in both x and y direction). + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + The year and semester for which a block is intended. + + + + + + + + + + + + + A block submission. + + + + + + + The year for which the submission is made. + + + + + + + The semester for which the submission is made. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/src/aeonlib/salt/models/request_models.py b/src/aeonlib/salt/models/request_models.py new file mode 100644 index 0000000..31596ef --- /dev/null +++ b/src/aeonlib/salt/models/request_models.py @@ -0,0 +1,100 @@ +"""This module provides Pydantic models for SALT observation requests.""" + +import os +import pathlib +import zipfile +from typing import Annotated, BinaryIO + +from annotated_types import MinLen +from pydantic import BaseModel, Field + +from aeonlib.salt.models import Block +from aeonlib.salt.models.util import ( + render_template, + attachment_path_replacements, + replace_attachment_paths, +) + + +class Request(BaseModel, validate_assignment=True): # type: ignore + """ + An observation request for SALT. + + A request is made up of a proposal code, which must be the proposal code of a + proposal existing already, the semester for which the request is submitted and a + non-empty list of blocks. + + The semester must be of the form yyyy-s with the year yyyy and its semester (1 or + 2). Semester 1 runs from 1 May to 1 November, semester 2 from November to May (in + the following year). For example, semester 2026-1 runs from noon on 1 May 2026 to + noon on 1 November 2026, whereas semester 2026-2 runs from noon on 1 November 2026 + to noon on 1 May 2027. + + Parameters + ---------- + proposal_code + Unique identifier of the proposal for which this request is submitted. + semester + Semester for which the submission is intended, in the form yyyy-s, where yyyy + denotes the year and s can be 1 or 2. Examples are 2025-1 or 2026-2. + blocks + List of blocks to observe. + + """ + + proposal_code: str + + semester: str = Field(pattern=r"\d{4}-[12]") + + blocks: Annotated[list[Block], MinLen(1)] + + def attachments(self) -> set[pathlib.Path]: + """ + Return the set of attachments for this request. + + All the attachment paths are reso,lved using the `resolve` method of the + `pathlib.Path` class. + + Returns + ------- + The set of attachments. + """ + _attachments: set[pathlib.Path] = set() + + for block in self.blocks: + for finder_chart in block.acquisition.finder_charts: + if not isinstance(finder_chart, pathlib.Path): + raise ValueError("The finder chart value is not a Path instance.") + _attachments.add(finder_chart) + + if block.instrument.instrument_name == "RSS": + if hasattr(block.instrument.configuration, "mask"): + mask = block.instrument.configuration.mask + if not isinstance(mask, pathlib.Path): + raise ValueError("The mask value is not a Path instance.") + _attachments.add(mask) + + # Remove duplicates + return set(a.resolve() for a in _attachments) + + def export(self, out: pathlib.Path | os.PathLike | str | BinaryIO) -> None: + """ + Export this request as a zip file. + + Parameters + ---------- + out + Path of the zip file or a byte stream. + """ + with zipfile.ZipFile(out, "w") as archive: + block_submission_xml = render_template( + "block_submission.xml", request=self.model_dump() + ) + attachments = self.attachments() + replacements = attachment_path_replacements(attachments) + block_submission_xml = replace_attachment_paths( + block_submission_xml, replacements + ) + archive.writestr("Blocks.xml", block_submission_xml) + for path, zip_path in replacements.items(): + archive.write(path, zip_path) diff --git a/src/aeonlib/salt/models/rss_models.py b/src/aeonlib/salt/models/rss_models.py new file mode 100644 index 0000000..e0e1339 --- /dev/null +++ b/src/aeonlib/salt/models/rss_models.py @@ -0,0 +1,446 @@ +from __future__ import annotations + +from typing import Literal, Annotated, Union + +from astropy import units as u +from astropy.units import Quantity +from pydantic import ( + BaseModel, + Field, + FilePath, + PositiveInt, + field_validator, + model_validator, + PlainSerializer, + BeforeValidator, + AfterValidator, + computed_field, +) + +from aeonlib.salt.models.types.salticam import serialize_salticam_filter +from aeonlib.types import Angle +from aeonlib.salt.models.util import ( + TitleCaseSerializer, + LowerCaseValidator, + UpperCaseSerializer, + LINEAR_POLARIMETRY_PATTERN, + LINEAR_HI_POLARIMETRY_PATTERN, + CIRCULAR_POLARIMETRY_PATTERN, + CIRCULAR_HI_POLARIMETRY_PATTRERN, + ALL_STOKES_POLARIMETRY_PATTERN, +) +from aeonlib.salt.models.types import ( + PositiveDuration, + RssGain, + RssGrating, + RssImagingFilter, + RssOrderBlockingFilter, + RssReadoutMode, + RssReadoutSpeed, + RssSlitMaskIFU, + SalticamFilter, + AstropyQuantityTypeAnnotation, +) +from aeonlib.salt.validators import GreaterEqual, GreaterThan, LessEqual + + +class Rss(BaseModel, validate_assignment=True): # type: ignore + """ + An RSS configuration. + + RSS can be used in different configurations: + + - Imaging. + - Longslit spectroscopy. + - Multiobject spectroscopy (MOS). + - Spectroscopy with a Slit Mask Integrated Fibre Unit (IFU). + + All of these may be used for polarimetric observations, i.e. with a wave plate + pattern for a half wave plate H and a quarter wave plate Q. If you want to + perform this pattern more than once, you can specify a number of cycles. These + should not be confused with the number of exposures, which is defined for the + detector. + + For example, assume you perform polarimetry with a pattern (H1, Q1), (H2, Q2). + Then two cycles and 1 exposure would result in the following observing + sequence: + + (H1, Q1) - (H2, Q2) - (H1, Q1) - (H2, Q2) + + On the other hand, one cycle and two exposures would result in the following + sequence: + + (H1, Q1) - (H1, Q1) - (H2, Q2) - (H2, Q2) + + You may define a dither pattern, in which case the wave plate sequence (with its + cycles and exposures) applies to each dither pattern step. + + Parameters + ---------- + instrument_name: + The instrument name, which is "RSS". + num_cycles + How often to cycle through the wave plate sequence. This is only relevant if + you perform polarimetry. + configuration + Imaging, longslit, multiobject spectroscopy or slit mask IFU configuration. + detector + Detector setup. + dither_pattern + Dither pattern. + """ + + instrument_name: Literal["RSS"] = "RSS" + num_cycles: PositiveInt = 1 + configuration: ( + RssImaging + | RssLongslitSpectroscopy + | RssMultiObjectSpectroscopy + | RssSlitMaskIFUSpectroscopy + ) + detector: RssDetector + dither_pattern: RssDitherPattern | None + + +class RssImaging(BaseModel, validate_assignment=True): # type: ignore + """ + An RSS imaging configuration. + + Parameters + ---------- + mode + The configuration mode. This must be "imaging". + filter + The filter to use. This may be one of RSS's own imaging filters or one of the + filters used by Salticam. + polarimetry + The (optional) polarimetry setup. + include_flat + Whether a nighttime flat should be taken for the observation. + """ + + mode: Annotated[Literal["imaging"], LowerCaseValidator] = "imaging" + filter: Annotated[ + RssImagingFilter | SalticamFilter, + LowerCaseValidator, + PlainSerializer(RssImaging.serialize_filter), + ] + polarimetry: RssPolarimetry | None = None + include_flat: bool + + @staticmethod + def serialize_filter(value: str) -> str: + if value.startswith("pi"): + return value + else: + return serialize_salticam_filter(value) + + +_WavePlatePattern = ( + Literal[ + "linear", "linear hi", "circular", "circular hi", "all-Stokes", "all-stokes" + ] + | list[tuple[Angle | None, Angle | None]] +) + + +class RssSpectroscopy(BaseModel, validate_assignment=True): # type: ignore + """ + An RSS spectroscopy configuration. + + While the grating, articulation, polarimetry and calibrations are defined by this + class, the slit mask (or IFU) to use is specified in a child class. + + Parameters + ---------- + mode + The configuration mode. This must be "spectroscopy". + grating + The barcode of the grating, such as "pg0900". + grating_angle + The grating angle. The default is half the articulation angle. + articulation_angle + The articulation angle of the camera. This must be either 0 deg or one of the + values 1.75 deg + (n - 1) * 0.75 deg, where 1 <= n <= 132. + order_blocking_filter + The order blocking filter. + polarimetry + The (optional) polarimetry setup. + include_flat + Whether a nighttime flat should be taken for the observation. + include_arc + Whether a nighttime arc should be taken for the observation. + request_spectrophotometric_standard + Whether a spectrophotometric standard should be taken for the observation. + + """ + + mode: Annotated[Literal["spectroscopy"], LowerCaseValidator] = "spectroscopy" + grating: RssGrating + grating_angle: Annotated[Angle, GreaterEqual(0 * u.deg), LessEqual(100 * u.deg)] + articulation_angle: Angle + order_blocking_filter: RssOrderBlockingFilter + polarimetry: RssPolarimetry | None = None + include_flat: bool + include_arc: bool = True + request_spectrophotometric_standard: bool = False + + @computed_field # type: ignore + @property + def articulation_station(self) -> int: + """Return the articulation station.""" + degrees = self.articulation_angle.to(u.deg).value # type: ignore + if degrees < 1: + return 0 + return round((degrees - 1) / 0.75) + + @field_validator("articulation_angle", mode="after") + @classmethod + def check_articulation_angle(cls, angle: Angle) -> Angle: + error = "The articulation angle must either be 0 deg or a value 1.75 deg + (n - 1) * 0.75 deg with 1 <= n <= 132." + degrees = angle.to(u.deg).value # type: ignore + + if degrees < 0: + raise ValueError(error) + + grace = 1e-6 + if abs(degrees) < grace: + return angle + + n = (degrees - 1.75) / 0.75 + 1 + if n < 1 - grace or n > 132 + grace: + raise ValueError(error) + + if abs(n - round(n)) > grace: + raise ValueError + + return angle + + +class RssLongslitSpectroscopy(RssSpectroscopy): + """ + An RSS longslit spectroscopy setup. + + In addition to the properties required by a generic RSS spectroscopy the user must + specify the barcode of the longslit to use. + + Parameters + ---------- + slit + The barcode of the longslit, such as "PL0125N001". + """ + + slit: str + + +class RssMultiObjectSpectroscopy(RssSpectroscopy): + """ + An RSS multiobject spectroscopy (MOS) setup. + + In addition to the properties required by a generic RSS spectroscopy the user must + specify the path of the file describing the MOS mask. The path must exist and must + be a file. + + Parameters + ---------- + mask + The file path of the file describing the MOS mask. + """ + + mask: FilePath + + +class RssSlitMaskIFUSpectroscopy(RssSpectroscopy): + """ + An RSS slit mask integrated field unit (IFU) setup. + + In addition to the properties required by a generic RSS spectroscopy the user must + specify the barcode of the slit mask IFU to use. + + Parameters + ---------- + slit_mask_ifu + The barcode of the slit mask IFU, such as "PF0200N001". + """ + + slit_mask_ifu: Annotated[RssSlitMaskIFU, LowerCaseValidator, UpperCaseSerializer] + + +class RssPolarimetry(BaseModel, validate_assignment=True): # type: ignore + """ + An RSS polarimetry setup. + + The setup is defined by a wave plate pattern, which may be specified by the name + of a predefined pattern ("linear", "linear hi", "circular" or "all-Stokes") or by + explicitly defining the list of half and quarter wave plate angles. In case of + the latter, the list must consist of pairs of angles, with the first angle being + that of the half wave plate and the second being that of the quarter wave plate. + Each angle must bee a multiple of 11.25 degrees. It may be None if the respective + wave plate is not used. + + For example, the "linear" and "circular" patterns could be given as:: + + linear = RssPolarimetry(wave_plate_pattern="linear") + circular = RssPolarimetry(wave_plate_pattern="circular") + + or as:: + + from astropy import units as u + + linear = RssPolarimetry( + wave_plate_pattern=[ + (0 * u.deg, None), + (45 * u.deg, None), + (22.5 * u.deg, None), + (67.5 * u.deg, None), + ] + ) + circular = RssPolarimetry( + wave_plate_pattern=[(0 * u.deg, 45 * u.deg), (0 * u.deg, 315 * u.deg)] + ) + + A wave plate pattern may have up to 8 steps. + """ + + wave_plate_pattern: Annotated[ + _WavePlatePattern, + BeforeValidator(RssPolarimetry.validate_pattern_before), + AfterValidator(RssPolarimetry.validate_pattern_after), + ] + + @staticmethod + def validate_pattern_before(value: _WavePlatePattern) -> _WavePlatePattern: + if isinstance(value, str): + return value.lower() # type: ignore + return value + + @staticmethod + def validate_pattern_after(value: _WavePlatePattern) -> _WavePlatePattern: + if value == "linear": + value = LINEAR_POLARIMETRY_PATTERN # type: ignore + elif value == "linear hi": + value = LINEAR_HI_POLARIMETRY_PATTERN # type: ignore + elif value == "circular": + value = CIRCULAR_POLARIMETRY_PATTERN # type: ignore + elif value == "circular hi": + value = CIRCULAR_HI_POLARIMETRY_PATTRERN # type: ignore + elif value == "all-stokes": + value = ALL_STOKES_POLARIMETRY_PATTERN # type: ignore + + if isinstance(value, str): + raise ValueError(f"Unsupported string value: {value}") + + if len(value) < 1 or len(value) > 8: + raise ValueError("The wave plate pattern must have between 1 and 8 steps.") + + RssPolarimetry._check_angle_values(value) + + return value + + @staticmethod + def _check_pattern_step(step: tuple[Angle | None, Angle | None]) -> None: + error = ( + "Each angle in a wave plate pattern must be a multiple of 11.25 degrees " + "between 0 degrees (inclusive) and 360 degrees (exclusive)." + ) + for angle in step: + if angle is None: + continue + + if angle < 0 * u.deg or angle >= 360 * u.deg: + raise ValueError(error) + + # Check that the ratio of the angle and 11.25 deg is (very close to) an + # integer. + x = (angle.to(u.deg) / 11.25).value # type: ignore + if abs(round(x) - x) > 1e-6: + raise ValueError(error) + + @staticmethod + def _check_angle_values(value: _WavePlatePattern) -> None: + for step in value: + RssPolarimetry._check_pattern_step(step) # type: ignore + + +class RssDetector(BaseModel, validate_assignment=True): # type: ignore + """ + An Rss detector setup. + + Parameters + ---------- + exposure_time + The exposure time. If multiple exposures are requested, this is the time per + exposure. + num_exposures + The number of exposures to take. + readout_mode + The readout mode. + gain + The gain. + readout_speed + The readout speed. + num_prebinned_rows + The number of prebinned rows, which must be between 1 and 9 (both inclusive). + num_prebinned_columns + The number of prebinned columns, which must be between 1 and 9 (both inclusive). + window_height + The height of the detector window, which must be a positive angle less than + or equal to 518 arcseconds. In most cases there is no need to define a + detector window. + """ + + exposure_time: PositiveDuration + num_exposures: int = 1 + readout_mode: Annotated[RssReadoutMode, TitleCaseSerializer] = "normal" + gain: Annotated[RssGain, TitleCaseSerializer] + readout_speed: Annotated[RssReadoutSpeed, TitleCaseSerializer] + num_prebinned_rows: Annotated[int, GreaterEqual(1), LessEqual(9)] + num_prebinned_columns: Annotated[int, GreaterEqual(1), LessEqual(9)] + window_height: Annotated[ + Angle | None, GreaterThan(0 * u.arcsec), LessEqual(518 * u.arcsec) + ] = None + + +class RssDitherPattern(BaseModel, validate_assignment=True): # type: ignore + """ + A dither pattern for RSS. + + The dither pattern is characterised by the number of rows and columns it covers, + the number of steps to take, and the offset between the steps. + + By default, the number of steps is the product of rows and columns, but you may + specify a multiple of that number if you want to perform the pattern more than once. + + The offset is in detector coordinates, not in right ascension and declination. + Therefore, if a particular object orientation is desired, a suitable position + angle must be chosen so that the dithers coincide with the detector axes. + + Parameters + ---------- + num_rows + Number of rows in the pattern. + num_columns + Number of columns in the pattern. + number_steps + Number of steps to perform. + offset + Offset between steps, as a `astropy.units.Quantity` or as a float in arcsec. + """ + + num_rows: PositiveInt + num_columns: PositiveInt + num_steps: PositiveInt = Field( + default_factory=lambda data: data["num_rows"] * data["num_columns"] + ) + offset: Annotated[ + Union[Quantity, float], AstropyQuantityTypeAnnotation(default_unit=u.arcsec) + ] + + @model_validator(mode="after") + def check_number_of_steps(self): + if self.num_steps % (self.num_rows * self.num_columns) != 0: + raise ValueError( + "The number of steps must be the number of rows times the number of " + "columns, or a multiple thereof." + ) + return self diff --git a/src/aeonlib/salt/models/salticam_models.py b/src/aeonlib/salt/models/salticam_models.py new file mode 100644 index 0000000..53db173 --- /dev/null +++ b/src/aeonlib/salt/models/salticam_models.py @@ -0,0 +1,143 @@ +from __future__ import annotations + +from typing import Annotated, Literal, Union + +from annotated_types import MinLen +from astropy import units as u +from astropy.units import Quantity +from pydantic import BaseModel, PositiveInt, Field, model_validator + +from aeonlib.salt.models.types import ( + PositiveDuration, + SalticamFilter, + SalticamFilterSerializer, + AstropyQuantityTypeAnnotation, +) +from aeonlib.salt.models.util import CapitalizingSerializer, LowerCaseValidator +from aeonlib.salt.validators import GreaterEqual, LessEqual + + +class Salticam(BaseModel, validate_assignment=True): # type: ignore + """ + A Salticam instrument configuration. + + Several filters can be requested in the configuration. If you want to repeat the + sequence, you can set a number of cycles. This should not be confused with the + number of exposures, which is set for the detector. + + For example, if the configuration requests the Johnson U and Johnson V filter, one + cycle and two exposures correspond to the sequence + + U - U - V - V + + whereas two cycles and one exposure corresponds to + + U - V - U - V + + You may define a dither pattern, in which case the filter sequence (with its cycles + and exposures) applies to each dither pattern step. + + Parameters + ---------- + instrument_name: + The instrument name, which is "Salticam". + num_cycles + How often to cycle through the filter sequence. + filter_sequence + Filter sequence. + detector + Detector setup. + dither_pattern + Dither pattern. + include_flat: + Whether a nighttime flat should be taken for the observation. + """ + + instrument_name: Literal["Salticam"] = "Salticam" + num_cycles: PositiveInt = 1 + filter_sequence: Annotated[list[SalticamFilterSequenceStep], MinLen(1)] + detector: SalticamDetector + dither_pattern: SalticamDitherPattern | None = None + include_flat: bool + + +class SalticamFilterSequenceStep(BaseModel, validate_assignment=True): # type: ignore + """ + A step in a filter sequence. + + Parameters + ---------- + filter + Filter for the step. + exposure_time + Exposure time for the step, as a `astropy.units.Quantity` or as a float in + seconds. + """ + + filter: Annotated[SalticamFilter, LowerCaseValidator, SalticamFilterSerializer] + exposure_time: PositiveDuration + + +class SalticamDetector(BaseModel, validate_assignment=True): # type: ignore + """ + A Salticam detector setup. + + Only "normal" readout mode (i.e. a full frame readout) is supported. The readout + speed may be "fast" or "slow", the gain "bright" or "faint". Up to 9 CCD rows and + columns can be binned. + + The setup does not include the exposure time; this is set as part of a filter + sequence step. + """ + + num_exposures: PositiveInt + readout_mode: Annotated[Literal["normal"], CapitalizingSerializer] = "normal" + gain: Annotated[Literal["bright", "faint"], CapitalizingSerializer] + readout_speed: Annotated[Literal["fast", "slow"], CapitalizingSerializer] + num_prebinned_rows: Annotated[int, GreaterEqual(1), LessEqual(9)] + num_prebinned_columns: Annotated[int, GreaterEqual(1), LessEqual(9)] + + +class SalticamDitherPattern(BaseModel, validate_assignment=True): # type: ignore + """ + A dither pattern for Salticam. + + The dither pattern is characterised by the number of rows and columns it covers, + the number of steps to take, and the offset between the steps. + + By default, the number of steps is the product of rows and columns, but you may + specify a multiple of that number if you want to perform the pattern more than once. + + The offset is in detector coordinates, not in right ascension and declination. + Therefore, if a particular object orientation is desired, a suitable position + angle must be chosen so that the dithers coincide with the detector axes. + + Parameters + ---------- + num_rows + Number of rows in the pattern. + num_columns + Number of columns in the pattern. + number_steps + Number of steps to perform. + offset + Offset between steps, as a `astropy.units.Quantity` or as a float in arcsec. + """ + + num_rows: PositiveInt + num_columns: PositiveInt + num_steps: PositiveInt = Field( + default_factory=lambda data: data["num_rows"] * data["num_columns"] + ) + offset: Annotated[ + Union[Quantity, float], AstropyQuantityTypeAnnotation(default_unit=u.arcsec) + ] + + @model_validator(mode="after") + def check_number_of_steps(self): + if self.num_steps % (self.num_rows * self.num_columns) != 0: + raise ValueError( + "The number of steps must be the number of rows times the number of " + "columns, or a multiple thereof." + ) + return self diff --git a/src/aeonlib/salt/models/serialize/__init__.py b/src/aeonlib/salt/models/serialize/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/aeonlib/salt/models/serialize/templates/acquisition.xml b/src/aeonlib/salt/models/serialize/templates/acquisition.xml new file mode 100644 index 0000000..0c0cf75 --- /dev/null +++ b/src/aeonlib/salt/models/serialize/templates/acquisition.xml @@ -0,0 +1,54 @@ + + Acquisition + {% include "target.xml" %} + + Telescope Configuration + 1 + + Payload Configuration + Acquisition + None + false + true + + Salticam Configuration + 1 + + + {{ acquisition.filter }} + + {{ acquisition.exposure_time }} + seconds + + + + + Normal + 2 + 2 + Science + Bright + Fast + 1 + + 0 + false + + + + {% for finder_chart in acquisition.finder_charts %} + + {{ finder_chart }} + + {% endfor %} + {% if acquisition.reference_star %} + + {{ acquisition.reference_star.ra }} + {{ acquisition.reference_star.dec }} + {{ acquisition.reference_star.equinox }} + + {% endif %} + {% if acquisition.include_focused_image %} + + {% endif %} + diff --git a/src/aeonlib/salt/models/serialize/templates/block.xml b/src/aeonlib/salt/models/serialize/templates/block.xml new file mode 100644 index 0000000..1c173d3 --- /dev/null +++ b/src/aeonlib/salt/models/serialize/templates/block.xml @@ -0,0 +1,114 @@ + + {{ block.name }} + {{ block.identifier }} + {{ block.num_visits }} + + {{ block.min_nights_between_visits }} + days + + {% if block.comments %} + {{ block.comments }} + {% endif %} + {{ block.priority }} + {{ block.ranking }} + Science + true + {{ block.constraints.transparency }} + + {{ block.constraints.max_lunar_phase_percentage }} + % + + + {{ block.constraints.min_lunar_distance }} + degrees + + + + {{ block.constraints.max_seeing }} + arcseconds + + + 0.1 + arcseconds + + + + [SubBlock] + 1 + + [SubSubBlock] + 1 + + [Pointing] + + [Observation] + {% with acquisition=block.acquisition, target=block.target %} + {% include "acquisition.xml" %} + {% endwith %} + + [Telescope Configuration] + 1 + {% if block.acquisition.position_angle is not none %} + + {% if block.acquisition.position_angle != "parallactic" %} + {{ block.acquisition.position_angle }} + degrees + + {% if block.acquisition.do_not_flip_position_angle %} + true + {% else %} + false + {% endif %} + + {% else %} + + {% endif %} + + {% endif %} + + [Payload Configuration] + Science + Default + false + true + {% with instrument=block.instrument %} + {% if instrument.instrument_name == "Salticam" %} + {% with salticam=instrument %} + {% include "salticam.xml" %} + {% endwith %} + {% endif %} + {% if instrument.instrument_name == "RSS" %} + {% with rss=instrument %} + {% include "rss.xml" %} + {% endwith %} + {% endif %} + {% if instrument.instrument_name == "HRS" %} + {% with hrs=instrument %} + {% include "hrs.xml" %} + {% endwith %} + {% endif %} + {% if instrument.instrument_name == "NIRWALS" %} + {% with nirwals=instrument %} + {% include "nirwals.xml" %} + {% endwith %} + {% endif %} + {% endwith %} + + + {% if block.windows %} + {% for window in block.windows %} + + {{ window.start | utc }} + {{ window.end | utc }} + + {% endfor %} + {% endif %} + + Normal + HTML + + + + + + diff --git a/src/aeonlib/salt/models/serialize/templates/block_submission.xml b/src/aeonlib/salt/models/serialize/templates/block_submission.xml new file mode 100644 index 0000000..b097948 --- /dev/null +++ b/src/aeonlib/salt/models/serialize/templates/block_submission.xml @@ -0,0 +1,8 @@ + + + {{ request.semester.split("-")[0] }} + {{ request.semester.split("-")[1] }} + {% for block in request.blocks %} + {% include "block.xml" %} + {% endfor %} + diff --git a/src/aeonlib/salt/models/serialize/templates/hrs.xml b/src/aeonlib/salt/models/serialize/templates/hrs.xml new file mode 100644 index 0000000..3515bd2 --- /dev/null +++ b/src/aeonlib/salt/models/serialize/templates/hrs.xml @@ -0,0 +1,53 @@ + + HRS + + {{ hrs.mode }} + Science + {% with v=hrs.prv_calibration %} + {{ v | iodine_cell_position }} + {% endwith %} + Star + + {{ to_angle(hrs.fibre_separation).arcsec }} + arcseconds + + true + + + {{ hrs.num_cycles }} + + {% for exposure_time in hrs.blue_arm.exposure_times %} + + {{ exposure_time }} + seconds + + {% endfor %} + + + {% for exposure_time in hrs.red_arm.exposure_times %} + + {{ exposure_time }} + seconds + + {% endfor %} + + + + 1 + Slow + 1 + 1 + 1 + 0 + 0 + + + 1 + Slow + 1 + 1 + 1 + 0 + 0 + + diff --git a/src/aeonlib/salt/models/serialize/templates/nirwals.xml b/src/aeonlib/salt/models/serialize/templates/nirwals.xml new file mode 100644 index 0000000..08af031 --- /dev/null +++ b/src/aeonlib/salt/models/serialize/templates/nirwals.xml @@ -0,0 +1,41 @@ + + NIRWALS + {{ nirwals.num_cycles }} + + Normal + + {% for step in nirwals.dither_pattern %} + {% include "nirwals_dither_pattern_step.xml" %} + {% endfor %} + + + + {{ nirwals.filter }} + {{ nirwals.camera_filter }} + {{ nirwals.grating }} + + {{ nirwals.grating_angle }} + degrees + + {{ nirwals.articulation_angle|nirwals_articulation_station }} + 97.0 + + {% if nirwals.include_flat %} + + + + {% endif %} + {% if nirwals.include_arc %} + + + + {% endif %} + {% if nirwals.request_spectrophotometric_standard %} + + + Spectrophotometric + + + {% endif %} + false + diff --git a/src/aeonlib/salt/models/serialize/templates/nirwals_dither_pattern_step.xml b/src/aeonlib/salt/models/serialize/templates/nirwals_dither_pattern_step.xml new file mode 100644 index 0000000..9183ad5 --- /dev/null +++ b/src/aeonlib/salt/models/serialize/templates/nirwals_dither_pattern_step.xml @@ -0,0 +1,20 @@ + + + {{ step.horizontal_offset }} + {{ step.vertical_offset }} + + {{ step.offset_type }} + + + {{ step.exposure_time }} + seconds + + 1 + {{ step.gain }} + {{ step.sampling }} + {{ step.num_reads }} + {{ step.num_ramps }} + {{ step.num_groups }} + + {{ step.exposure_type }} + diff --git a/src/aeonlib/salt/models/serialize/templates/rss.xml b/src/aeonlib/salt/models/serialize/templates/rss.xml new file mode 100644 index 0000000..c85775b --- /dev/null +++ b/src/aeonlib/salt/models/serialize/templates/rss.xml @@ -0,0 +1,52 @@ + + RSS + {% if rss.configuration.polarimetry %} + + + {% for step in rss.configuration.polarimetry.wave_plate_pattern %} + + {% if step[0] is not none %} + {{ step[0]|wave_plate_station }} + {% endif %} + {% if step[1] is not none %} + {{ step[1]|wave_plate_station }} + {% endif %} + + {% endfor %} + + + {% endif %} + {% with configuration=rss.configuration %} + {% if configuration.mode == "imaging" %} + {% include "rss_imaging.xml" %} + {% endif %} + {% if configuration.mode == "spectroscopy" %} + {% include "rss_spectroscopy.xml" %} + {% endif %} + {% endwith %} + {% with detector=rss.detector %} + {% include "rss_detector.xml" %} + {% endwith %} + 0 + {{ rss.num_cycles }} + {% if rss.configuration.include_flat %} + + + + {% endif %} + {% if rss.configuration.include_arc %} + + + + {% else %} + true + {% endif %} + {% if rss.configuration.request_spectrophotometric_standard %} + + + Spectrophotometric + + + {% endif %} + false + diff --git a/src/aeonlib/salt/models/serialize/templates/rss_detector.xml b/src/aeonlib/salt/models/serialize/templates/rss_detector.xml new file mode 100644 index 0000000..c194750 --- /dev/null +++ b/src/aeonlib/salt/models/serialize/templates/rss_detector.xml @@ -0,0 +1,25 @@ + + {{ detector.readout_mode }} + {{ detector.num_exposures }} + {% if detector.window_height is not none %} + + + {{ (3600 *detector.window_height)|round|int }} + arcseconds + + + {% endif %} + 0 + 0 + {{ detector.num_prebinned_rows }} + {{ detector.num_prebinned_columns }} + true + Science + + {{ detector.exposure_time }} + seconds + + {{ detector.gain }} + {{ detector.readout_speed }} + None + diff --git a/src/aeonlib/salt/models/serialize/templates/rss_dither_pattern.xml b/src/aeonlib/salt/models/serialize/templates/rss_dither_pattern.xml new file mode 100644 index 0000000..f3640db --- /dev/null +++ b/src/aeonlib/salt/models/serialize/templates/rss_dither_pattern.xml @@ -0,0 +1,9 @@ + + {{ dither_pattern.num_columns }} + {{ dither_pattern.num_rows }} + {{ dither_pattern.num_steps }} + + {{ dither_pattern.offset }} + arcseconds + + diff --git a/src/aeonlib/salt/models/serialize/templates/rss_imaging.xml b/src/aeonlib/salt/models/serialize/templates/rss_imaging.xml new file mode 100644 index 0000000..d122faa --- /dev/null +++ b/src/aeonlib/salt/models/serialize/templates/rss_imaging.xml @@ -0,0 +1,20 @@ + + + + + + {% if configuration.filter.startswith("pi") %} + {{ configuration.filter }} + {% else %} + {{ configuration.filter }} + {% endif %} + {% if configuration.polarimetry %} + + normal + + {% endif %} + + 1000 + microns + + diff --git a/src/aeonlib/salt/models/serialize/templates/rss_spectroscopy.xml b/src/aeonlib/salt/models/serialize/templates/rss_spectroscopy.xml new file mode 100644 index 0000000..db80269 --- /dev/null +++ b/src/aeonlib/salt/models/serialize/templates/rss_spectroscopy.xml @@ -0,0 +1,41 @@ + + + {% if configuration.slit %} + + {{ configuration.slit }} + + {% endif %} + {% if configuration.mask %} + + + {{ configuration.mask.absolute() }} + + + {% endif %} + {% if configuration.slit_mask_ifu %} + + {{ configuration.slit_mask_ifu }} + + {% endif %} + + + + {{ configuration.grating }} + + {{ configuration.grating_angle }} + degrees + + {{ configuration.articulation_station }}_{{ "{:.2f}".format(configuration.articulation_angle) }} + + + {{ configuration.order_blocking_filter }} + {% if configuration.polarimetry %} + + normal + + {% endif %} + + 1000 + microns + + diff --git a/src/aeonlib/salt/models/serialize/templates/salticam.xml b/src/aeonlib/salt/models/serialize/templates/salticam.xml new file mode 100644 index 0000000..d7de1bc --- /dev/null +++ b/src/aeonlib/salt/models/serialize/templates/salticam.xml @@ -0,0 +1,30 @@ + + Salticam + {{ salticam.num_cycles }} + + {% for step in salticam.filter_sequence %} + + {{ step.filter }} + + {{ step.exposure_time }} + seconds + + + {% endfor %} + + {% with detector=salticam.detector %} + {% include "salticam_detector.xml" %} + {% endwith %} + 0 + {% if salticam.dither_pattern %} + {% with dither_pattern=salticam.dither_pattern %} + {% include "salticam_dither_pattern.xml" %} + {% endwith %} + {% endif %} + false + {% if salticam.include_flat %} + + + + {% endif %} + diff --git a/src/aeonlib/salt/models/serialize/templates/salticam_detector.xml b/src/aeonlib/salt/models/serialize/templates/salticam_detector.xml new file mode 100644 index 0000000..0ae0644 --- /dev/null +++ b/src/aeonlib/salt/models/serialize/templates/salticam_detector.xml @@ -0,0 +1,9 @@ + + {{ detector.readout_mode }} + {{ detector.num_prebinned_rows }} + {{ detector.num_prebinned_columns }} + Science + {{ detector.gain }} + {{ detector.readout_speed }} + {{ detector.num_exposures }} + diff --git a/src/aeonlib/salt/models/serialize/templates/salticam_dither_pattern.xml b/src/aeonlib/salt/models/serialize/templates/salticam_dither_pattern.xml new file mode 100644 index 0000000..f3640db --- /dev/null +++ b/src/aeonlib/salt/models/serialize/templates/salticam_dither_pattern.xml @@ -0,0 +1,9 @@ + + {{ dither_pattern.num_columns }} + {{ dither_pattern.num_rows }} + {{ dither_pattern.num_steps }} + + {{ dither_pattern.offset }} + arcseconds + + diff --git a/src/aeonlib/salt/models/serialize/templates/target.xml b/src/aeonlib/salt/models/serialize/templates/target.xml new file mode 100644 index 0000000..9bdb26b --- /dev/null +++ b/src/aeonlib/salt/models/serialize/templates/target.xml @@ -0,0 +1,38 @@ + + {{ target.name }} + {{ uuid() }} + {{ target.target_type }} + + + {{ to_angle(target.ra).hms[0] | round(0) | int }} + {{ to_angle(target.ra).hms[1] | round(0) | int }} + {{ to_angle(target.ra).hms[2] | round(5) }} + + + {{ target.dec | sign }} + {{ to_angle(target.dec).dms[0] | abs | round(0) | int }} + {{ to_angle(target.dec).dms[1] | abs | round(0) | int }} + {{ to_angle(target.dec).dms[2] | abs | round(5) }} + + {% if target.proper_motion_ra or target.proper_motion_dec %} + + + {{ target.proper_motion_ra / 1000 }} + arcseconds/year + + + {{ target.proper_motion_dec / 1000 }} + arcseconds/year + + {{ target.epoch | year_as_iso_timestamp }} + + {% endif %} + 2000.0 + + + {{ target.magnitude_range.bandpass }} + {{ target.magnitude_range.min_magnitude }} + {{ target.magnitude_range.max_magnitude }} + + true + diff --git a/src/aeonlib/salt/models/target_models.py b/src/aeonlib/salt/models/target_models.py new file mode 100644 index 0000000..e8b7081 --- /dev/null +++ b/src/aeonlib/salt/models/target_models.py @@ -0,0 +1,92 @@ +"""This module contains Pydantic models for targets to observe with SALT.""" + +from __future__ import annotations + +from typing import Self + +import astropy.coordinates +from pydantic import BaseModel, field_validator, model_validator + +from aeonlib.models import SiderealTarget +from aeonlib.salt.models.types import MagnitudeBandpass, TargetType +from aeonlib.salt.validators import check_in_visibility_range + + +class SaltSiderealTarget(SiderealTarget, validate_assignment=True): # type: ignore + """ + A sidereal target to observe with SALT. + + This model extends the `SiderealTarget` model by adding a target type and a + magnitude range. + + Parameters + ---------- + target_type + Target type. This must be the label for SIMBAD object type (see + http://simbad.cds.unistra.fr/guide/otypes.htx). Examples are "TTau*" and + "StarburstG". + magnitude_range + Magnitude range for the range. + """ + + target_type: TargetType + magnitude_range: MagnitudeRange + + @field_validator("type", mode="after") + @classmethod + def check_type(cls, value: str): + if value != "ICRS": + raise ValueError("SALT only supports targets of type ICRS.") + return value + + @model_validator(mode="after") + def check_coordinates(self): + if self.hour_angle is not None: + raise ValueError("SALT does not support hour angle values.") + + if self.altitude is not None: + raise ValueError("SALT does not support altitude values.") + + if self.azimuth is not None: + raise ValueError("SALT does not support azimuth values.") + + return self + + @field_validator("dec", mode="after") + @classmethod + def check_declination_viewable(cls, value: astropy.coordinates.Angle): + return check_in_visibility_range(value) + + +class MagnitudeRange(BaseModel, validate_assignment=True): # type: ignore + """ + A magnitude range. + + The minimum (brightest) and maximum (faintest) magnitude must be give for a + particular bandpass filter. + + Parameters + ---------- + min_magnitude + Minimum (brightest) magnitude. + max_magnitude + Maximum (faintest) magnitude. This must be greater than or equal to the minimum + magnitude. + bandpass + Bandpass filter for which the magnitude range is given. + """ + + min_magnitude: float + + max_magnitude: float + + bandpass: MagnitudeBandpass + + @model_validator(mode="after") + def check_max_magnitude_is_at_least_min_magnitude(self) -> Self: + if self.min_magnitude > self.max_magnitude: + raise ValueError( + "max_magnitude must be greater than or equal to min_magnitude." + ) + + return self diff --git a/src/aeonlib/salt/models/types/__init__.py b/src/aeonlib/salt/models/types/__init__.py new file mode 100644 index 0000000..baa5d0d --- /dev/null +++ b/src/aeonlib/salt/models/types/__init__.py @@ -0,0 +1,51 @@ +from .quantity import AstropyQuantityTypeAnnotation +from .block import SkyTransparency +from .duration import Duration, PositiveDuration +from .hrs import HrsMode, HrsPrvCalibration +from .nirwals import ( + NirwalsCameraFilter, + NirwalsExposureType, + NirwalsFilter, + NirwalsGain, + NirwalsGrating, + NirwalsOffsetType, + NirwalsSampling, +) +from .rss import ( + RssGain, + RssGrating, + RssImagingFilter, + RssOrderBlockingFilter, + RssReadoutMode, + RssReadoutSpeed, + RssSlitMaskIFU, +) +from .salticam import SalticamFilter, SalticamFilterSerializer +from .target import MagnitudeBandpass, TargetType + +__all__ = [ + "AstropyQuantityTypeAnnotation", + "Duration", + "HrsMode", + "HrsPrvCalibration", + "MagnitudeBandpass", + "NirwalsCameraFilter", + "NirwalsExposureType", + "NirwalsFilter", + "NirwalsGain", + "NirwalsGrating", + "NirwalsOffsetType", + "NirwalsSampling", + "PositiveDuration", + "RssGain", + "RssGrating", + "RssImagingFilter", + "RssOrderBlockingFilter", + "RssReadoutMode", + "RssReadoutSpeed", + "RssSlitMaskIFU", + "SalticamFilter", + "SalticamFilterSerializer", + "SkyTransparency", + "TargetType", +] diff --git a/src/aeonlib/salt/models/types/block.py b/src/aeonlib/salt/models/types/block.py new file mode 100644 index 0000000..6c7993b --- /dev/null +++ b/src/aeonlib/salt/models/types/block.py @@ -0,0 +1,6 @@ +"""This module contains types for type-hinting blocks.""" + +from typing import Literal + +SkyTransparency = Literal["clear", "thin cloud", "thick cloud", "any"] +"""Sky transparency.""" diff --git a/src/aeonlib/salt/models/types/duration.py b/src/aeonlib/salt/models/types/duration.py new file mode 100644 index 0000000..b91584b --- /dev/null +++ b/src/aeonlib/salt/models/types/duration.py @@ -0,0 +1,11 @@ +from typing import Annotated, Union + +from astropy import units as u +from astropy.units import Quantity + +from aeonlib.salt.models.types import AstropyQuantityTypeAnnotation +from aeonlib.salt.validators import GreaterThan + +Duration = Annotated[Union[Quantity, float], AstropyQuantityTypeAnnotation(u.s)] + +PositiveDuration = Annotated[Duration, GreaterThan(0 * u.s)] diff --git a/src/aeonlib/salt/models/types/hrs.py b/src/aeonlib/salt/models/types/hrs.py new file mode 100644 index 0000000..8bb9634 --- /dev/null +++ b/src/aeonlib/salt/models/types/hrs.py @@ -0,0 +1,10 @@ +from typing import Literal + +HrsMode = Literal[ + "low resolution", "medium resolution", "high resolution", "high stability" +] +"""An HRS instrument mode.""" + + +HrsPrvCalibration = Literal["ThAr"] +"""An HRS precision radial velocity calibration.""" diff --git a/src/aeonlib/salt/models/types/nirwals.py b/src/aeonlib/salt/models/types/nirwals.py new file mode 100644 index 0000000..22b9368 --- /dev/null +++ b/src/aeonlib/salt/models/types/nirwals.py @@ -0,0 +1,31 @@ +from typing import Literal + + +NirwalsGrating = Literal["ng0950"] +"""A NIRWALS grating.""" + + +NirwalsFilter = Literal["empty"] +"""A NIRWALS filter.""" + + +NirwalsCameraFilter = Literal[ + "block", "clear", "cutoff 1.5um", "cutoff 1.7um", "diffuser" +] +"""A NIRWALS camera filter.""" + + +NirwalsOffsetType = Literal["FIF offset", "fif offset", "tracker guided offset"] +"""An offset type for NIRWALS.""" + + +NirwalsExposureType = Literal["science", "sky"] +"""An exposure type for NIRWALS.""" + + +NirwalsGain = Literal["faint"] +"""A gain option for NIRWALS.""" + + +NirwalsSampling = Literal["up-the-ramp", "up-the-ramp group"] +"""A sampling mode for NIRWALS.""" diff --git a/src/aeonlib/salt/models/types/quantity.py b/src/aeonlib/salt/models/types/quantity.py new file mode 100644 index 0000000..145dae6 --- /dev/null +++ b/src/aeonlib/salt/models/types/quantity.py @@ -0,0 +1,90 @@ +import dataclasses +from typing import Any + +from astropy.units import UnitBase, Quantity +from pydantic import GetCoreSchemaHandler, GetJsonSchemaHandler +from pydantic.json_schema import JsonSchemaValue +from pydantic_core import core_schema + + +@dataclasses.dataclass(frozen=True) +class AstropyQuantityTypeAnnotation: + """ + Annotation for defining custom Pydantic types based on a `astropy.units.Quantity`. + + To define such a custom type, instantiate `AstropyQuantityTypeAnnotation` with the + default unit `d` and pass it as a type annotation. Pydantic fields with this type can + be instantiated wth a `float` or a `astropy.units.Quantity` with units that are + compatible with `d`. If a `float` is used, it is assumed to be given with `d` as the + unit. The field is stored as a `astropy.units.Quantity` with unit `d`. + + For example, you can define a ProperMotion type as follows: + + ``` + from typing import Annotated, Union + from astropy import units as u + from astropy.units import Quantity + from aeonlib.salt.models.types import AstropyQuantityTypeAnnotation + + ProperMotion = Annotated[Union[Quantity, float], AstropyQuantityTypeAnnotation(u.arcsec / u.year)] + ``` + + This type can then be used in a Pydantic model: + + ``` + from pydantic import BaseModel + + class MovingObject(BaseModel, validate_assignment=True): + proper_motion: ProperMotion + + # Create the same object in three different ways. + # Note: 1 year = 8766 hours + object1 = MovingObject(proper_motion=8766) # 3 arcsec per year + object2 = MovingObject(proper_motion=8766 * u.arcsec / u.year) + object3 = MovingObject(proper_motion=1 * u.arcsec / u.hour) + """ + + # Based on + # https://docs.pydantic.dev/latest/concepts/types/#handling-third-party-types + + default_unit: UnitBase + + def __get_pydantic_core_schema__( + self, + _source_type: Any, + _handler: GetCoreSchemaHandler, + ) -> core_schema.CoreSchema: + def validate_from_float(value: float) -> Quantity: + return Quantity(value, unit=self.default_unit) + + def validate_from_quantity(value: Quantity) -> Quantity: + return value.to(self.default_unit) + + from_float_schema = core_schema.chain_schema( + [ + core_schema.float_schema(), + core_schema.no_info_plain_validator_function(validate_from_float), + ] + ) + + from_quantity_schema = core_schema.chain_schema( + [ + core_schema.is_instance_schema(Quantity), + core_schema.no_info_plain_validator_function(validate_from_quantity), + ] + ) + + return core_schema.json_or_python_schema( + json_schema=from_float_schema, + python_schema=core_schema.union_schema( + [from_quantity_schema, from_float_schema] + ), + serialization=core_schema.plain_serializer_function_ser_schema( + lambda instance: instance.to(self.default_unit).value + ), + ) + + def __get_pydantic_json_schema( + self, _core_schema: core_schema.CoreSchema, handler: GetJsonSchemaHandler + ) -> JsonSchemaValue: + return handler(core_schema.float_schema()) diff --git a/src/aeonlib/salt/models/types/rss.py b/src/aeonlib/salt/models/types/rss.py new file mode 100644 index 0000000..a6e6a76 --- /dev/null +++ b/src/aeonlib/salt/models/types/rss.py @@ -0,0 +1,66 @@ +from typing import Literal + +RssImagingFilter = Literal[ + "pi04340", + "pi04400", + "pi04465", + "pi04530", + "pi04600", + "pi04670", + "pi04740", + "pi04820", + "pi04895", + "pi04975", + "pi05060", + "pi05145", + "pi05235", + "pi05325", + "pi05420", + "pi05520", + "pi05620", + "pi05725", + "pi05830", + "pi05945", + "pi06055", + "pi06170", + "pi06290", + "pi06410", + "pi06530", + "pi06645", + "pi06765", + "pi06885", + "pi07005", + "pi07130", + "pi07260", + "pi07390", + "pi07535", + "pi07685", + "pi07840", + "pi08005", + "pi08175", + "pi08350", + "pi08535", + "pi08730", +] +"""An imaging filter for RSS.""" + + +RssOrderBlockingFilter = Literal["pc00000", "pc03200", "pc03400", "pc03850", "pc04600"] +"""An order blocking filter for RSS.""" + + +RssGrating = Literal["pg0700", "pg0900", "pg1300", "pg1800", "pg2300", "pg3000"] +"""An RSS grating.""" + + +RssSlitMaskIFU = Literal["PF0200N001", "pf0200n001"] +"""A slit mask integrated field unit (IFU) for RSS.""" + +RssReadoutMode = Literal["normal", "frame transfer", "slot mode"] +"""An RSS readout mode.""" + +RssGain = Literal["faint", "bright"] +"""An RSS detector gain.""" + +RssReadoutSpeed = Literal["fast", "slow"] +"""An RSS detector readout speed.""" diff --git a/src/aeonlib/salt/models/types/salticam.py b/src/aeonlib/salt/models/types/salticam.py new file mode 100644 index 0000000..54f91d5 --- /dev/null +++ b/src/aeonlib/salt/models/types/salticam.py @@ -0,0 +1,89 @@ +from typing import Literal + +from pydantic import PlainSerializer + +SalticamFilter = Literal[ + "Fused silica clear", + "fused silica clear", + "Johnson U", + "johnson u", + "Johnson B", + "johnson b", + "Johnson V", + "johnson v", + "Cousins R", + "cousins r", + "Cousins I", + "cousins i", + "380nm 40nm FWHM", + "380nm 40nm fwhm", + "SDSS u'", + "sdss u'", + "SDSS g'", + "sdss g'", + "SDSS r'", + "sdss r'", + "SDSS i'", + "sdss i'", + "SDSS z'", + "sdss z'", + "H-alpha", + "h-alpha", + "H-beta narrow", + "h-beta narrow", + "H-beta wide", + "h-beta wide", + "Stroemgren u", + "stroemgren u", + "Stroemgren v", + "stroemgren v", + "Stroemgren b", + "stroemgren b", + "Stroemgren y", + "stroemgren y", + "SRE 1", + "sre 1", + "SRE 2", + "sre 2", + "SRE 3", + "sre 3", + "SRE 4", + "sre 4", +] +"""A filter for Salticam.""" + + +def serialize_salticam_filter(value: str) -> str: + value = value.lower() + translation_table = { + "fused silica clear": "Fused silica clear", + "johnson u": "Johnson U", + "johnson b": "Johnson B", + "johnson v": "Johnson V", + "cousins r": "Cousins R", + "cousins i": "Cousins I", + "380nm 40nm fwhm": "380nm 40nm FWHM", + "sdss u'": "SDSS u'", + "sdss g'": "SDSS g'", + "sdss r'": "SDSS r'", + "sdss i'": "SDSS i'", + "sdss z'": "SDSS z'", + "h-alpha": "H-alpha", + "h-beta narrow": "H-beta narrow", + "h-beta wide": "H-beta wide", + "stroemgren u": "Stroemgren u", + "stroemgren v": "Stroemgren v", + "stroemgren b": "Stroemgren b", + "stroemgren y": "Stroemgren y", + "sre 1": "SRE 1", + "sre 2": "SRE 2", + "sre 3": "SRE 3", + "sre 4": "SRE 4", + } + serialized = translation_table.get(value) + if serialized is None: + raise ValueError(f"Filter name cannot be serialized: {value}") + return serialized + + +SalticamFilterSerializer = PlainSerializer(serialize_salticam_filter) diff --git a/src/aeonlib/salt/models/types/target.py b/src/aeonlib/salt/models/types/target.py new file mode 100644 index 0000000..cdd7c40 --- /dev/null +++ b/src/aeonlib/salt/models/types/target.py @@ -0,0 +1,249 @@ +"""This module contains types for type-hinting targets.""" + +from typing import Literal + +MagnitudeBandpass = Literal["U", "B", "V", "R", "I"] +"""The bandpass filters whivh msay be used for specifying a target magnitude.""" + + +TargetType = Literal[ + "Unknown", + "Maser", + "X", + "SuperSoft", + "gamma", + "gammaBurst", + "Inexistant", + "Error", + "Gravitation", + "LensingEv", + "Candidate_Lens", + "Possible_lensImage", + "GravLens", + "GravLensSystem", + "Candidates", + "Possible_SClG", + "Possible_ClG", + "Possible_GrG", + "Candidate_**", + "Candidate_EB*", + "Candidate_CV*", + "Candidate_XB*", + "Candidate_LMXB", + "Candidate_HMXB", + "Candidate_Pec*", + "Candidate_YSO", + "Candidate_pMS*", + "Candidate_TTau*", + "Candidate_C*", + "Candidate_S*", + "Candidate_OH", + "Candidate_CH", + "Candidate_WR*", + "Candidate_Be*", + "Candidate_HB*", + "Candidate_RGB*", + "Candidate_RSG*", + "Candidate_AGB*", + "Candidate_post-AGB*", + "Candidate_BSS", + "Candidate_WD*", + "Candidate_NS", + "Candidate_BH", + "Candidate_SN*", + "Candidate_low-mass*", + "Candidate_brownD*", + "multiple_object", + "Region", + "Void", + "SuperClG", + "ClG", + "GroupG", + "Compact_Gr_G", + "Gr_QSO", + "PairG", + "IG", + "GlCl?", + "Cl*", + "GlCl", + "OpCl", + "Assoc*", + "**", + "EB*", + "EB*Algol", + "EB*betLyr", + "EB*WUMa", + "EB*Planet", + "SB", + "CataclyV*", + "DQHer", + "AMHer", + "Nova-like", + "Nova", + "DwarfNova", + "XB", + "LMXB", + "HMXB", + "***", + "ISM", + "PartofCloud", + "PN?", + "ComGlob", + "Bubble", + "EmObj", + "Cloud", + "GalNeb", + "BrNeb", + "DkNeb", + "RfNeb", + "MolCld", + "Globule", + "denseCore", + "HVCld", + "BiNeb", + "GasNeb", + "HII", + "PN", + "HIshell", + "SNR?", + "SNR", + "Circumstellar", + "outflow?", + "Outflow", + "OutflowJet", + "HH", + "Star", + "*inCl", + "*inNeb", + "*inAssoc", + "*in**", + "V*?", + "Pec*", + "HB*", + "YSO", + "Em*", + "Be*", + "BlueStraggler", + "RGB*", + "AGB*", + "C*", + "S*", + "RSG*", + "post-AGB*", + "WD*", + "pulsWD*", + "low-mass*", + "brownD*", + "OH/IR", + "CH", + "pMS*", + "TTau*", + "WR*", + "NS*", + "BH*", + "PM*", + "near*", + "HV*", + "V*", + "Irregular_V*", + "Orion_V*", + "Rapid_Irreg_V*", + "Eruptive*", + "Flare*", + "FUOr", + "Erupt*RCrB", + "RotV*", + "RotV*alf2CVn", + "RotV*Ell", + "Pulsar", + "BYDra", + "RSCVn", + "PulsV*", + "RRLyr", + "Cepheid", + "PulsV*delSct", + "PulsV*RVTau", + "PulsV*WVir", + "PulsV*bCep", + "deltaCep", + "gammaDor", + "LPV*", + "Mira", + "semi-regV*", + "SN", + "Symbiotic*", + "Sub-stellar", + "Planet?", + "ExG*", + "Galaxy", + "EllipticalG", + "SpiralG", + "DwarfG", + "IrregG", + "PartofG", + "GinCl", + "BClG", + "GinGroup", + "GinPair", + "High_z_G", + "AbsLineSystem", + "Ly-alpha_ALS", + "DLy-alpha_ALS", + "metal_ALS", + "Ly-limit_ALS", + "Broad_ALS", + "RadioG", + "HII_G", + "LSB_G", + "AGN_Candidate", + "QSO_Candidate", + "Blazar_Candidate", + "BLLac_Candidate", + "EmG", + "StarburstG", + "BlueCompG", + "LensedImage", + "LensedG", + "LensedQ", + "AGN", + "LINER", + "Seyfert", + "Seyfert_1", + "Seyfert_2", + "Blazar", + "BLLac", + "OVV", + "QSO", + "GSN", + "Solar_System", + "Planet", + "Mercury", + "Venus", + "Earth", + "Moon", + "Mars", + "Jupiter", + "Saturn", + "Uranus", + "Neptune", + "PMoon", + "PRing", + "DwarfPlanet", + "Pluto", + "Asteroid", + "Comet", + "KBO", + "Calib", + "Calib_S", + "Calib_aS", + "Calib_phS", + "Calib_sS", + "Cal_polS", + "Cal_spS", + "Cal_rvS", + "Cal_Flat", + "Cal_SFlat", + "Cal_DFlat", + "Cal_Guide*", +] +"""The target type.""" diff --git a/src/aeonlib/salt/models/util.py b/src/aeonlib/salt/models/util.py new file mode 100644 index 0000000..203d824 --- /dev/null +++ b/src/aeonlib/salt/models/util.py @@ -0,0 +1,341 @@ +import datetime +import io +import pathlib +import uuid +import xml.etree.ElementTree as ET +import zoneinfo +from typing import Any, Iterable, cast +from uuid import uuid4 +from zoneinfo import ZoneInfo + +import astropy.units as u +from astropy.coordinates import Angle +from bs4 import BeautifulSoup +from jinja2 import Environment, PackageLoader, select_autoescape, BaseLoader +from lxml import etree +from pydantic import PlainSerializer, BeforeValidator + +_schema: etree.XMLSchema | None = None + + +LINEAR_POLARIMETRY_PATTERN = [ + (Angle(0 * u.deg), None), + (Angle(45 * u.deg), None), + (Angle(22.5 * u.deg), None), + (Angle(67.5 * u.deg), None), +] +# The half and quarter wave plate angles for the linear polarimetry pattern. + + +LINEAR_HI_POLARIMETRY_PATTERN = [ + (Angle(0 * u.deg), None), + (Angle(45 * u.deg), None), + (Angle(22.5 * u.deg), None), + (Angle(67.5 * u.deg), None), + (Angle(11.25 * u.deg), None), + (Angle(56.25 * u.deg), None), + (Angle(33.75 * u.deg), None), + (Angle(78.75 * u.deg), None), +] +# The half and quarter wave plate angles for the linear-hi polarimetry pattern. + + +CIRCULAR_POLARIMETRY_PATTERN = [ + (Angle(0 * u.deg), Angle(45 * u.deg)), + (Angle(0 * u.deg), Angle(315 * u.deg)), +] +# The half and quarter wave plate angles for the circular polarimetry pattern. + + +CIRCULAR_HI_POLARIMETRY_PATTRERN = [ + (Angle(0 * u.deg), Angle(45 * u.deg)), + (Angle(0 * u.deg), Angle(315 * u.deg)), + (Angle(22.5 * u.deg), Angle(315 * u.deg)), + (Angle(22.5 * u.deg), Angle(45 * u.deg)), + (Angle(45 * u.deg), Angle(45 * u.deg)), + (Angle(45 * u.deg), Angle(315 * u.deg)), + (Angle(67.5 * u.deg), Angle(315 * u.deg)), + (Angle(67.5 * u.deg), Angle(45 * u.deg)), +] +# The half and quarter wave plate angles for the circular-hi polarimetry pattern. + + +ALL_STOKES_POLARIMETRY_PATTERN = [ + (Angle(0 * u.deg), Angle(0 * u.deg)), + (Angle(45 * u.deg), Angle(0 * u.deg)), + (Angle(22.5 * u.deg), Angle(0 * u.deg)), + (Angle(67.5 * u.deg), Angle(0 * u.deg)), + (Angle(0 * u.deg), Angle(45 * u.deg)), + (Angle(0 * u.deg), Angle(315 * u.deg)), +] +# The half and quarter wave plate angles for the all-Stokes polarimetry pattern. + + +def validate_xml(xml: str) -> None: + """ + Validate an XML string against the SALT XML schema. + + The method raises a `ValueError` if the XML is not well-formed or does not conform + to the schema. + + This method is intended only for use in the serialization of SALT model instances. + + Parameters + ---------- + xml + XML string. + + Raises + ------ + ValueError + If the XML is not well-formed or does not conform to the schema. + + """ + if not _schema: + _load_schema() + + try: + xml_doc = etree.parse(io.BytesIO(xml.encode("utf-8"))) + cast(etree.XMLSchema, _schema).assertValid(xml_doc) # noqa + except (etree.DocumentInvalid, etree.XMLSyntaxError) as e: + raise ValueError(str(e)) + + +def _load_schema(): + with open(pathlib.Path(__file__).parent / "proposal.xsd", "r") as f: + schema_doc = etree.parse(f) + global _schema + _schema = etree.XMLSchema(schema_doc) + + +def _wave_plate_station(angle): + if angle < 1e-5: + return "0_0" + else: + return f"{(angle / 11.25):.0f}_{angle:.2f}" + + +def _iodine_cell_position(value): + return value if value else "OUT" + + +def _nirwals_articulation_station(angle): + if angle < 1e-5: + return "0_0" + else: + return f"{(2 * angle):.0f}_{angle:.1f}" + + +def _year_as_iso_timestamp(year): + t = datetime.datetime(year, 1, 1, 0, 0, 0, 0, tzinfo=zoneinfo.ZoneInfo("UTC")) + return t.isoformat() + + +def _to_utc(t: datetime.datetime | None) -> str: + if t is None: + t = datetime.datetime.now(ZoneInfo("UTC")) + else: + if t.tzinfo: + raise ValueError("The datetime instance must be naive.") + t = t.replace(tzinfo=ZoneInfo("UTC")) + + return cast(datetime.datetime, t).strftime("%Y-%m-%dT%H:%M:%SZ") + + +def _sign(value): + return "+" if value >= 0 else "-" + + +def _uuid() -> str: + return str(uuid.uuid4()) + + +def _to_angle(degrees: float) -> Angle: + return Angle(degrees * u.deg) + + +def render_template( + template_path: str, loader: BaseLoader | None = None, **kwargs +) -> str: + """ + Render a Jinja template. + + The first argument for this method is the path of the template to render, + as needed by the template loader. You may pass a `jinja2.BaseLoader` as the + `loader` argument for specifying how to load the template. The default is to look + in the `templates` folder of the `aeonlib.salt.models.serialize` package. Any + additional keyword arguments are passed on to Jinja's render function. + + This method is intended only for use in the serialization of SALT model instances. + + Parameters + ---------- + template_path + Path of the template to render. + loader + Jinja template loader. + kwargs + Additional keyword arguments passed on to the render function. + + Returns + ------- + The rendered template. + """ + if not loader: + loader = PackageLoader("aeonlib.salt.models.serialize") + + env = Environment(loader=loader, autoescape=select_autoescape()) + env.trim_blocks = True + env.lstrip_blocks = True + env.filters["wave_plate_station"] = _wave_plate_station + env.filters["iodine_cell_position"] = _iodine_cell_position + env.filters["nirwals_articulation_station"] = _nirwals_articulation_station + env.filters["year_as_iso_timestamp"] = _year_as_iso_timestamp + env.filters["utc"] = _to_utc + env.filters["sign"] = _sign + env.globals["uuid"] = _uuid + env.globals["to_angle"] = _to_angle + template = env.get_template(template_path) + return template.render(**kwargs) + + +def _lower(s: Any) -> Any: + if isinstance(s, str): + return s.lower() + return s + + +LowerCaseValidator = BeforeValidator(_lower) + + +def _capitalize(s: str | None) -> str | None: + if s is None: + return None + return s.capitalize() + + +CapitalizingSerializer = PlainSerializer(_capitalize) +""" +A serializer for capitalising string values. + +This serializer is only intended for use in the serialization of SALT data models. +""" + + +def _title(s: str | None) -> str | None: + if s is None: + return None + return s.title() + + +TitleCaseSerializer = PlainSerializer(_title) +""" +A serializer for converting string values to title case. + +This serializer is only intended for use in the serialization of SALT data models. +""" + + +def _upper(s: str | None) -> str | None: + if s is None: + return None + return s.upper() + + +UpperCaseSerializer = PlainSerializer(_upper) +""" +A serializer for converting string values to upper case. + +This serializer is only intended for use in the serialization of SALT data models. +""" + + +def attachment_path_replacements( + attachments: Iterable[pathlib.Path], +) -> dict[pathlib.Path, str]: + """ + Return a dictionary of attachments and corresponding paths to use in submitted XML. + + The paths to use for submitted XML are of the form "Included/", + where denotes a UUID version 4 string and is the file extension + of the attachment, including the dot and in lower case, such as ".jpg" or ".pdf". + + Parameters + ---------- + attachments + List of attachment paths. + + Returns + ------- + Dictionary of attachment paths and paths for submitted XML. + """ + replacements = {} + for attachment in attachments: + extension = attachment.suffix.lower() + replacement = f"Included/{uuid4()}{extension}" + replacements[attachment] = replacement + + return replacements + + +def replace_attachment_paths(xml: str, replacements: dict[pathlib.Path, str]) -> str: + """ + Replace the attachment paths in the given XML. + + Parameters + ---------- + xml + XML. + replacements + Dictionary of attachment paths and their replacements. + + Returns + ------- + The XML with the attachment paths updated. + + Raises + ------ + ValueError + If an attachment is missing in the dictionary of replacements. + """ + + # Resolve all attachment paths + replacements_resolved = {k.resolve(): v for k, v in replacements.items()} + + # Check whether there are duplicate keys or values + if len(set(replacements_resolved.keys())) != len(replacements): + raise ValueError( + "Two or more keys of the replacements dictionary resolve to the same path." + ) + if len(set(replacements_resolved.values())) != len(replacements_resolved.values()): + raise ValueError("There duplicate values in the replacements dictionary.") + + # Replace the attachment paths + soup = BeautifulSoup(xml, "xml") + for path_element in soup.find_all("Path"): + path_text = path_element.text.strip() + path = pathlib.Path(path_text).resolve() + if path not in replacements_resolved: + raise ValueError( + f"Path missing in replacements dictionary: {path_text} (resolved: {str(path)}" + ) + path_element.string = replacements_resolved[path] + xml = soup.prettify() + + # BeautifulSoup inserts extraneous whitespace around text in elements, but this + # renders the XML invalid. + return _remove_whitespace(xml) + + +def _remove_whitespace(xml: str) -> str: + # Remove whitespace around text in XML elements. + # Adapted from https://stackoverflow.com/questions/58344879/how-to-output-xml-from-beautifulsoup-without-extraneous-newlines + tree = ET.fromstring(xml) + + # Remove extraneous newlines and whitespace from text elements. + for element in tree.iter(): + if element.text: + element.text = element.text.strip() + + # Return the updated XML. + return ET.tostring(tree).decode("UTF-8") diff --git a/src/aeonlib/salt/validators.py b/src/aeonlib/salt/validators.py new file mode 100644 index 0000000..e7e3fdf --- /dev/null +++ b/src/aeonlib/salt/validators.py @@ -0,0 +1,160 @@ +"""This module defines some Pydantic validators.""" + +from typing import Any + +import astropy.coordinates +from astropy import units as u +from pydantic import AfterValidator + + +def _check_gt(a: Any, b: Any) -> Any: + if a is not None and b is not None and a <= b: + raise ValueError(f"{a} is not greater than to {b}.") + return a + + +def _check_ge(a: Any, b: Any) -> Any: + if a is not None and b is not None and a < b: + raise ValueError(f"{a} is not greater than or equal to {b}.") + return a + + +def _check_lt(a: Any, b: Any) -> None: + if a is not None and b is not None and a >= b: + raise ValueError(f"{a} is not less than to {b}.") + return a + + +def _check_le(a: Any, b: Any) -> None: + if a is not None and b is not None and a > b: + raise ValueError(f"{a} is not less than or equal to {b}.") + return a + + +def GreaterThan(value: Any): + """ + Return a Pydantic validator for checking a greater than relation. + + The returned validator can be used in a type annotation:: + + import pydantic + + class DummyModel(pydantic.BaseModel): + duration: Annotated[float, GreaterThan(4)] + + Pydantic will first perform its own internal validation and then check whether + the field value is greater than the argument passed to `GreaterThan` (4 in the + example above). + + It is up to the user to ensure that the field value and the argument of + `GreaterThan` can be compared. + + Parameters + ---------- + value + Value against which to compare. + + Returns + ------- + A validator for checking a greater than relation. + """ + return AfterValidator(lambda v: _check_gt(v, value)) + + +def GreaterEqual(value: Any): + """ + Return a Pydantic validator for checking a greater than or equal to relation. + + The returned validator can be used in a type annotation:: + + import pydantic + + class DummyModel(pydantic.BaseModel): + duration: Annotated[float, GreaterEqual(4)] + + Pydantic will first perform its own internal validation and then check whether + the field value is greater than or equal to the argument passed to `GreaterEqual` + (4 in the example above). + + It is up to the user to ensure that the field value and the argument of + `GreaterEqual` can be compared. + + Parameters + ---------- + value + Value against which to compare. + + Returns + ------- + A validator for checking a greater than or equal to relation. + """ + return AfterValidator(lambda v: _check_ge(v, value)) + + +def LessThan(value: Any): + """ + Return a Pydantic validator for checking a less than relation. + + The returned validator can be used in a type annotation:: + + import pydantic + + class DummyModel(pydantic.BaseModel): + height: Annotated[float, LessThan(4)] + + Pydantic will first perform its own internal validation and then check whether + the field value is less than or equal to the argument passed to `LessEqual` (4 in + the example above). + + It is up to the user to ensure that the field value and the argument of + `LessThan` can be compared. + + Parameters + ---------- + value + Value against which to compare. + + Returns + ------- + A validator for checking a less than relation. + """ + return AfterValidator(lambda v: _check_lt(v, value)) + + +def LessEqual(value: Any): + """ + Return a Pydantic validator for checking a less than or equal to relation. + + The returned validator can be used in a type annotation:: + + import pydantic + + class DummyModel(pydantic.BaseModel): + height: Annotated[float, LessEqual(4)] + + Pydantic will first perform its own internal validation and then check whether + the field value is less than or equal to the argument passed to `LessEqual` (4 in + the example above). + + It is up to the user to ensure that the field value and the argument of + `LessEqual` can be compared. + + Parameters + ---------- + value + Value against which to compare. + + Returns + ------- + A validator for checking a less than or equal to relation. + """ + return AfterValidator(lambda v: _check_le(v, value)) + + +def check_in_visibility_range( + dec: astropy.coordinates.Angle, +) -> astropy.coordinates.Angle: + if dec < -76 * u.deg or dec > 11 * u.deg: + raise ValueError("Not in SALT's visibility range (between -76 and 11 degrees).") + + return dec diff --git a/src/tt.txt b/src/tt.txt new file mode 100644 index 0000000..41554d8 --- /dev/null +++ b/src/tt.txt @@ -0,0 +1,238 @@ +"Unknown", +"Maser", +"X", +"SuperSoft", +"gamma", +"gammaBurst", +"Inexistant", +"Error", +"Gravitation", +"LensingEv", +"Candidate_Lens", +"Possible_lensImage", +"GravLens", +"GravLensSystem", +"Candidates", +"Possible_SClG", +"Possible_ClG", +"Possible_GrG", +"Candidate_**", +"Candidate_EB*", +"Candidate_CV*", +"Candidate_XB*", +"Candidate_LMXB", +"Candidate_HMXB", +"Candidate_Pec*", +"Candidate_YSO", +"Candidate_pMS*", +"Candidate_TTau*", +"Candidate_C*", +"Candidate_S*", +"Candidate_OH", +"Candidate_CH", +"Candidate_WR*", +"Candidate_Be*", +"Candidate_HB*", +"Candidate_RGB*", +"Candidate_RSG*", +"Candidate_AGB*", +"Candidate_post-AGB*", +"Candidate_BSS", +"Candidate_WD*", +"Candidate_NS", +"Candidate_BH", +"Candidate_SN*", +"Candidate_low-mass*", +"Candidate_brownD*", +"multiple_object", +"Region", +"Void", +"SuperClG", +"ClG", +"GroupG", +"Compact_Gr_G", +"Gr_QSO", +"PairG", +"IG", +"GlCl?", +"Cl*", +"GlCl", +"OpCl", +"Assoc*", +"**", +"EB*", +"EB*Algol", +"EB*betLyr", +"EB*WUMa", +"EB*Planet", +"SB", +"CataclyV*", +"DQHer", +"AMHer", +"Nova-like", +"Nova", +"DwarfNova", +"XB", +"LMXB", +"HMXB", +"***", +"ISM", +"PartofCloud", +"PN?", +"ComGlob", +"Bubble", +"EmObj", +"Cloud", +"GalNeb", +"BrNeb", +"DkNeb", +"RfNeb", +"MolCld", +"Globule", +"denseCore", +"HVCld", +"BiNeb", +"GasNeb", +"HII", +"PN", +"HIshell", +"SNR?", +"SNR", +"Circumstellar", +"outflow?", +"Outflow", +"OutflowJet", +"HH", +"Star", +"*inCl", +"*inNeb", +"*inAssoc", +"*in**", +"V*?", +"Pec*", +"HB*", +"YSO", +"Em*", +"Be*", +"BlueStraggler", +"RGB*", +"AGB*", +"C*", +"S*", +"RSG*", +"post-AGB*", +"WD*", +"pulsWD*", +"low-mass*", +"brownD*", +"OH/IR", +"CH", +"pMS*", +"TTau*", +"WR*", +"NS*", +"BH*", +"PM*", +"near*", +"HV*", +"V*", +"Irregular_V*", +"Orion_V*", +"Rapid_Irreg_V*", +"Eruptive*", +"Flare*", +"FUOr", +"Erupt*RCrB", +"RotV*", +"RotV*alf2CVn", +"RotV*Ell", +"Pulsar", +"BYDra", +"RSCVn", +"PulsV*", +"RRLyr", +"Cepheid", +"PulsV*delSct", +"PulsV*RVTau", +"PulsV*WVir", +"PulsV*bCep", +"deltaCep", +"gammaDor", +"LPV*", +"Mira", +"semi-regV*", +"SN", +"Symbiotic*", +"Sub-stellar", +"Planet?", +"ExG*", +"Galaxy", +"EllipticalG", +"SpiralG", +"DwarfG", +"IrregG", +"PartofG", +"GinCl", +"BClG", +"GinGroup", +"GinPair", +"High_z_G", +"AbsLineSystem", +"Ly-alpha_ALS", +"DLy-alpha_ALS", +"metal_ALS", +"Ly-limit_ALS", +"Broad_ALS", +"RadioG", +"HII_G", +"LSB_G", +"AGN_Candidate", +"QSO_Candidate", +"Blazar_Candidate", +"BLLac_Candidate", +"EmG", +"StarburstG", +"BlueCompG", +"LensedImage", +"LensedG", +"LensedQ", +"AGN", +"LINER", +"Seyfert", +"Seyfert_1", +"Seyfert_2", +"Blazar", +"BLLac", +"OVV", +"QSO", +"GSN", +"Solar_System", +"Planet", +"Mercury", +"Venus", +"Earth", +"Moon", +"Mars", +"Jupiter", +"Saturn", +"Uranus", +"Neptune", +"PMoon", +"PRing", +"DwarfPlanet", +"Pluto", +"Asteroid", +"Comet", +"KBO", +"Calib", +"Calib_S", +"Calib_aS", +"Calib_phS", +"Calib_sS", +"Cal_polS", +"Cal_spS", +"Cal_rvS", +"Cal_Flat", +"Cal_SFlat", +"Cal_DFlat", +"Cal_Guide*", diff --git a/tests/salt/conftest.py b/tests/salt/conftest.py new file mode 100644 index 0000000..f3d67ce --- /dev/null +++ b/tests/salt/conftest.py @@ -0,0 +1,258 @@ +import pathlib +import uuid + +import astropy.coordinates +import pytest +from astropy import units as u + +from aeonlib.salt.models import ( + Acquisition, + Block, + Constraints, + Hrs, + HrsDetector, + MagnitudeRange, + ReferenceStar, + Request, + Salticam, + SalticamDitherPattern, + SaltSiderealTarget, + SalticamFilterSequenceStep, + SalticamDetector, + Rss, + RssDetector, + RssDitherPattern, + RssImaging, + RssPolarimetry, + RssSpectroscopy, + RssLongslitSpectroscopy, + RssMultiObjectSpectroscopy, + RssSlitMaskIFUSpectroscopy, + Nirwals, + NirwalsDitherPatternStep, +) + + +@pytest.fixture() +def base_request(base_block): + """A simple request to edit or build from.""" + return Request( + proposal_code="2025-1-SCI-042", semester="2026-1", blocks=[base_block] + ) + + +@pytest.fixture() +def base_block(base_acquisition, base_constraints, base_target, base_salticam): + """A simple block to build or edit from.""" + return Block( + name="Test", + priority=1, + ranking="high", + num_visits=1, + min_nights_between_visits=0, + constraints=base_constraints, + target=base_target, + acquisition=base_acquisition, + instrument=base_salticam, + ) + + +@pytest.fixture() +def base_target(base_magnitude_range): + """A simple sidereal target to build or edit from.""" + return SaltSiderealTarget( + name="Test Target", + type="ICRS", + ra=0, + dec=0, + target_type="Nova", + magnitude_range=base_magnitude_range, + ) + + +@pytest.fixture() +def base_magnitude_range(): + """A simple magnitude range to build or edit from.""" + return MagnitudeRange(min_magnitude=17.1, max_magnitude=17.5, bandpass="V") + + +@pytest.fixture() +def base_constraints(): + return Constraints( + transparency="thick cloud", + max_lunar_phase_percentage=50, + min_lunar_distance=astropy.coordinates.Angle("45d"), + max_seeing=3, + ) + + +@pytest.fixture() +def base_acquisition(): + finder_chart = pathlib.Path(__file__).parent / "data" / "dummy_finder_chart_1.pdf" + return Acquisition(finder_charts=[finder_chart], position_angle=45 * u.deg) + + +@pytest.fixture() +def base_reference_star(): + return ReferenceStar(ra=117.564 * u.deg, dec=-63.9 * u.deg) + + +@pytest.fixture() +def base_salticam(base_salticam_detector, base_salticam_filter_sequence_step): + return Salticam( + filter_sequence=[base_salticam_filter_sequence_step], + detector=base_salticam_detector, + include_flat=True, + ) + + +@pytest.fixture() +def base_salticam_filter_sequence_step(): + return SalticamFilterSequenceStep(filter="Johnson B", exposure_time=409 * u.s) + + +@pytest.fixture() +def base_salticam_detector(): + return SalticamDetector( + num_exposures=1, + gain="bright", + readout_speed="fast", + num_prebinned_rows=2, + num_prebinned_columns=2, + ) + + +@pytest.fixture() +def base_salticam_dither_pattern(): + return SalticamDitherPattern(num_rows=3, num_columns=4, offset=12.9) + + +@pytest.fixture() +def base_rss(base_rss_imaging, base_rss_detector, base_rss_dither_pattern): + return Rss( + configuration=base_rss_imaging, + detector=base_rss_detector, + dither_pattern=base_rss_dither_pattern, + ) + + +@pytest.fixture() +def base_rss_polarimetry(): + return RssPolarimetry(wave_plate_pattern="linear") + + +@pytest.fixture() +def base_rss_imaging(): + return RssImaging(filter="pi04400", polarimetry=None, include_flat=True) + + +@pytest.fixture() +def base_rss_spectroscopy(base_rss_polarimetry): + return RssSpectroscopy( + grating="pg0900", + grating_angle=20 * u.deg, + articulation_angle=40 * u.deg, + order_blocking_filter="pc04600", + polarimetry=base_rss_polarimetry, + include_flat=True, + include_arc=True, + request_spectrophotometric_standard=False, + ) + + +@pytest.fixture() +def base_rss_longslit_spectroscopy(base_rss_spectroscopy): + return RssLongslitSpectroscopy( + **base_rss_spectroscopy.model_dump(), slit="PL0125N001" + ) + + +@pytest.fixture() +def base_rss_multi_object_spectroscopy(base_rss_spectroscopy): + return RssMultiObjectSpectroscopy( + **base_rss_spectroscopy.model_dump(), + mask=pathlib.Path(__file__).parent / "data" / "dummy_rss_mos_mask.rsmt", + ) + + +@pytest.fixture() +def base_rss_slit_mask_ifu_spectroscopy(base_rss_spectroscopy): + return RssSlitMaskIFUSpectroscopy( + **base_rss_spectroscopy.model_dump(), + slit_mask_ifu="PF0200N001", + ) + + +@pytest.fixture() +def base_rss_detector(): + return RssDetector( + exposure_time=120 * u.s, + gain="bright", + readout_speed="fast", + num_prebinned_rows=2, + num_prebinned_columns=2, + window_height=100 * u.arcsec, + ) + + +@pytest.fixture() +def base_rss_dither_pattern(): + return RssDitherPattern(num_rows=3, num_columns=4, offset=12.9) + + +@pytest.fixture() +def base_hrs(base_hrs_detector): + return Hrs( + mode="medium resolution", blue_arm=base_hrs_detector, red_arm=base_hrs_detector + ) + + +@pytest.fixture() +def base_hrs_detector(): + return HrsDetector(exposure_times=[50 * u.s, 45]) + + +@pytest.fixture() +def base_nirwals(base_nirwals_dither_pattern_step): + return Nirwals( + grating="NG0950", + grating_angle=25 * u.deg, + articulation_angle=50 * u.deg, + camera_filter="cutoff 1.5um", + dither_pattern=[ + base_nirwals_dither_pattern_step, + base_nirwals_dither_pattern_step, + ], + include_flat=False, + ) + + +@pytest.fixture() +def base_nirwals_dither_pattern_step(): + return NirwalsDitherPatternStep( + offset_type="FIF offset", + horizontal_offset=-20 * u.arcsec, + vertical_offset=35 * u.arcsec, + exposure_type="science", + exposure_time=200 * u.s, + gain="faint", + sampling="up-the-ramp", + ) + + +@pytest.fixture() +def create_test_binary_file(tmp_path: pathlib.Path): + """ + Return a function for generating test files. + + The returned function accepts content (as bytes, such as b"I'm a test file." and a + file extension (such as ".pdf"), generates a temporary file with the given content + and file extension, and returns the path to the generated file. + """ + + def _create_file(content: bytes, extension: str) -> pathlib.Path: + file_path = tmp_path / f"{str(uuid.uuid4())}{extension}" + file_path.write_bytes(content) + return file_path + + return _create_file diff --git a/tests/salt/data/dummy_finder_chart_1.pdf b/tests/salt/data/dummy_finder_chart_1.pdf new file mode 100644 index 0000000..3a6e3e7 Binary files /dev/null and b/tests/salt/data/dummy_finder_chart_1.pdf differ diff --git a/tests/salt/data/dummy_finder_chart_2.pdf b/tests/salt/data/dummy_finder_chart_2.pdf new file mode 100644 index 0000000..3a6e3e7 Binary files /dev/null and b/tests/salt/data/dummy_finder_chart_2.pdf differ diff --git a/tests/salt/data/dummy_finder_chart_3.png b/tests/salt/data/dummy_finder_chart_3.png new file mode 100644 index 0000000..e490487 Binary files /dev/null and b/tests/salt/data/dummy_finder_chart_3.png differ diff --git a/tests/salt/data/dummy_rss_mos_mask.rsmt b/tests/salt/data/dummy_rss_mos_mask.rsmt new file mode 100644 index 0000000..e69de29 diff --git a/tests/salt/data/test.xml b/tests/salt/data/test.xml new file mode 100644 index 0000000..ca9c633 --- /dev/null +++ b/tests/salt/data/test.xml @@ -0,0 +1,4 @@ + + {{ a }} + {{ b }} + diff --git a/tests/salt/models/serialize/test_templates.py b/tests/salt/models/serialize/test_templates.py new file mode 100644 index 0000000..d9ebbdb --- /dev/null +++ b/tests/salt/models/serialize/test_templates.py @@ -0,0 +1,643 @@ +import re +from copy import deepcopy +from datetime import datetime +from typing import Any +from zoneinfo import ZoneInfo + +import astropy.units as u +import pytest +import time_machine +from astropy.coordinates import Angle +from astropy.units import Quantity + +from aeonlib.models import Window +from aeonlib.salt.models import SalticamFilterSequenceStep +from aeonlib.salt.models.util import render_template, validate_xml + + +class TestSalticamTemplates: + def test_salticam_detector_template(self, base_salticam_detector): + """Test that the Salticam detector template generates valid XML.""" + xml = render_template( + "salticam_detector.xml", detector=base_salticam_detector.model_dump() + ) + validate_xml(xml) + assert True + + def test_salticam_dithering_pattern(self, base_salticam_dither_pattern): + """Test that the Salticam dither pattern template generates valid XML.""" + dither_pattern = base_salticam_dither_pattern + + xml = render_template( + "salticam_dither_pattern.xml", dither_pattern=dither_pattern.model_dump() + ) + + validate_xml(xml) + + @pytest.mark.parametrize("full", [False, True]) + def test_salticam_template( + self, full: bool, base_salticam, base_salticam_dither_pattern + ): + """Test that the Salticam template generates valid XML.""" + salticam = base_salticam + salticam.filter_sequence.append( + SalticamFilterSequenceStep(filter="Cousins R", exposure_time=42) + ) + if full: + salticam.dither_pattern = base_salticam_dither_pattern + salticam.include_flat = True + else: + salticam.dither_pattern = None + salticam.include_flat = False + xml = render_template("salticam.xml", salticam=salticam.model_dump()) + + if full: + assert "Dither" in xml + assert "SalticamDefaultCalibrationFlat" in xml + else: + assert "Dither" not in xml + assert "SalticamDefaultCalibrationFlat" not in xml + + validate_xml(xml) + assert True + + +class TestRssTemplates: + @pytest.mark.parametrize("full", [False, True]) + def test_rss_detector_template(self, full: bool, base_rss_detector): + """Test that the RSS detector template generates valid XML.""" + detector = base_rss_detector + + if full: + detector.window_height = 45 * u.arcsec + else: + detector.window_height = None + + xml = render_template("rss_detector.xml", detector=detector.model_dump()) + + if full: + assert "Height" in xml + else: + assert "Height" not in xml + + validate_xml(xml) + assert True + + @pytest.mark.parametrize("full", [False, True]) + def test_rss_imaging(self, full: bool, base_rss_imaging, base_rss_polarimetry): + """Tests that the RSS imaging template generates valid XML.""" + configuration = base_rss_imaging + + if full: + configuration.polarimetry = base_rss_polarimetry + else: + configuration.polarimetry = None + + xml = render_template( + "rss_imaging.xml", configuration=configuration.model_dump() + ) + + if full: + assert "BeamsplitterOrientation" in xml + else: + assert "BeamsplitterOrientation" not in xml + + validate_xml(xml) + assert True + + @pytest.mark.parametrize( + "filter_name, expected_element", + [("pi04340", "FilterId"), ("Johnson V", "SalticamFilter")], + ) + def test_rss_imaging_filter( + self, filter_name: str, expected_element: str, base_rss_imaging + ): + """Tests that RSS imaging filters are handled correctly when generating XML.""" + configuration = base_rss_imaging + configuration.filter = filter_name + + xml = render_template( + "rss_imaging.xml", configuration=configuration.model_dump() + ) + assert expected_element in xml + + validate_xml(xml) + assert True + + @pytest.mark.parametrize("full", [False, True]) + def test_rss_longslit_spectroscopy( + self, full: bool, base_rss_longslit_spectroscopy, base_rss_polarimetry + ): + """Test that the template for RSS longslit spectroscopy generates valid XML.""" + configuration = base_rss_longslit_spectroscopy + + if full: + configuration.polarimetry = base_rss_polarimetry + else: + configuration.polarimetry = None + + xml = render_template( + "rss_spectroscopy.xml", configuration=configuration.model_dump() + ) + + assert "PredefinedMask" in xml + + if full: + assert "BeamsplitterOrientation" in xml + else: + assert "BeamsplitterOrientation" not in xml + + validate_xml(xml) + assert True + + @pytest.mark.parametrize("full", [False, True]) + def test_rss_mos_spectroscopy( + self, full: bool, base_rss_multi_object_spectroscopy, base_rss_polarimetry + ): + """ + Test that the template for RSS multi-object spectroscopy generates valid XML. + """ + configuration = base_rss_multi_object_spectroscopy + + if full: + configuration.polarimetry = base_rss_polarimetry + else: + configuration.polarimetry = None + + xml = render_template( + "rss_spectroscopy.xml", configuration=configuration.model_dump() + ) + + assert "MOS" in xml + assert "Path" in xml + + if full: + assert "BeamsplitterOrientation" in xml + else: + assert "BeamsplitterOrientation" not in xml + + validate_xml(xml) + assert True + + @pytest.mark.parametrize("full", [False, True]) + def test_rss_slit_mask_ifu_spectroscopy( + self, full: bool, base_rss_slit_mask_ifu_spectroscopy, base_rss_polarimetry + ): + """ + Test that the template for RSS slit mask IFY spectroscopy generates valid XML. + """ + configuration = base_rss_slit_mask_ifu_spectroscopy + + if full: + configuration.polarimetry = base_rss_polarimetry + else: + configuration.polarimetry = None + + xml = render_template( + "rss_spectroscopy.xml", configuration=configuration.model_dump() + ) + + assert "SMI" in xml + + if full: + assert "BeamsplitterOrientation" in xml + else: + assert "BeamsplitterOrientation" not in xml + + validate_xml(xml) + assert True + + def test_rss_dithering_pattern(self, base_rss_dither_pattern): + dither_pattern = base_rss_dither_pattern + """Test that the template for RSS dither patterns generates valid XML.""" + + xml = render_template( + "rss_dither_pattern.xml", dither_pattern=dither_pattern.model_dump() + ) + + validate_xml(xml) + + @pytest.mark.parametrize("full", [False, True]) + def test_rss( + self, full: bool, base_rss, base_rss_polarimetry, base_rss_longslit_spectroscopy + ): + """Test that the RSS template generates valid XML.""" + rss = base_rss + rss.configuration = base_rss_longslit_spectroscopy + + if full: + rss.configuration.polarimetry.wave_plate_pattern = ( + "circular" # base_rss_polarimetry + ) + rss.configuration.include_flat = True + rss.configuration.include_arc = True + rss.configuration.request_spectrophotometric_standard = True + else: + rss.configuration.polarimetry = None + rss.configuration.include_flat = False + rss.configuration.include_arc = False + rss.configuration.request_spectrophotometric_standard = False + + xml = render_template("rss.xml", rss=rss.model_dump()) + + if full: + assert "RssProcedure" in xml + assert "WaveplatePattern" in xml + assert "RssDefaultCalibrationFlat" in xml + assert "RssDefaultArc" in xml + assert "RssStandard" in xml + else: + assert "RssProcedure" not in xml + assert "WaveplatePattern" not in xml + assert "RssDefaultCalibrationFlat" not in xml + assert "RssDefaultArc" not in xml + assert "RssStandard" not in xml + + validate_xml(xml) + assert True + + def test_rss_wave_plate_pattern_step_values( + self, base_rss, base_rss_longslit_spectroscopy, base_rss_polarimetry + ): + """Test that the wave plate pattern step values are correct.""" + rss = base_rss + rss.configuration = base_rss_longslit_spectroscopy + rss.configuration.polarimetry = base_rss_polarimetry + rss.configuration.polarimetry.wave_plate_pattern = "circular" + + xml = render_template("rss.xml", rss=rss.model_dump()) + + assert "0_0" in xml + assert "4_45.00" in xml + assert "28_315.00" in xml + + +class TestHrs: + @pytest.mark.parametrize( + "mode, iodine_cell_position", + [ + ("low resolution", "OUT"), + ("medium resolution", "OUT"), + ("high resolution", "OUT"), + ("high stability", "ThAr"), + ], + ) + def test_hrs(self, mode: str, iodine_cell_position: str, base_hrs): + hrs = base_hrs + hrs.mode = mode + + xml = render_template("hrs.xml", hrs=hrs.model_dump()) + + assert f"{iodine_cell_position}" in xml + + validate_xml(xml) + assert True + + +class TestNirwalsTemplates: + def test_nirwals_dither_pattern_step(self, base_nirwals_dither_pattern_step): + """Test that the NIRWALS dither pattern step templates generates valid XML.""" + step = base_nirwals_dither_pattern_step + + xml = render_template("nirwals_dither_pattern_step.xml", step=step.model_dump()) + + validate_xml(xml) + assert True + + @pytest.mark.parametrize("full", [False, True]) + def test_nirwals(self, full: bool, base_nirwals): + """Test that the NIRWALS template generates valid XML.""" + nirwals = base_nirwals + + if full: + nirwals.include_flat = True + nirwals.include_arc = True + nirwals.request_spectrophotometric_standard = True + else: + nirwals.include_flat = False + nirwals.include_arc = False + nirwals.request_spectrophotometric_standard = False + + xml = render_template("nirwals.xml", nirwals=nirwals.model_dump()) + + if full: + assert "NirDefaultCalibrationFlat" in xml + assert "NirDefaultArc" in xml + assert "NirStandard" in xml + else: + assert "NirDefaultCalibrationFlat" not in xml + assert "NirDefaultArc" not in xml + assert "NirStandard" not in xml + + validate_xml(xml) + assert True + + @pytest.mark.parametrize( + "angle, station", + [ + (0 * u.deg, "0_0"), + (0.5 * u.deg, "1_0.5"), + (14 * u.deg, "28_14.0"), + (37.5 * u.deg, "75_37.5"), + (100 * u.deg, "200_100.0"), + ], + ) + def test_nirwals_articulation_station( + self, angle: Quantity, station: str, base_nirwals + ): + """Test that the NIRWALS articulation station is output correctly.""" + nirwals = base_nirwals + nirwals.articulation_angle = angle + + xml = render_template("nirwals.xml", nirwals=nirwals.model_dump()) + + assert f"{station}" in xml + + +class TestTarget: + @pytest.mark.parametrize("full", [False, True]) + def test_target(self, full: bool, base_target): + """Test that the target template generates valid XML.""" + target = base_target + + if full: + target.proper_motion_ra = 17 + target.proper_motion_dec = -3 + else: + target.proper_motion_ra = 0 + target.proper_motion_dec = 0 + + xml = render_template("target.xml", target=target.model_dump()) + + if full: + assert "RightAscensionDot" in xml + assert "DeclinationDot" in xml + else: + assert "RightAscensionDot" not in xml + assert "DeclinationDot" not in xml + + validate_xml(xml) + assert True + + @pytest.mark.parametrize( + "ra, hours, minutes, seconds", + [ + ("0d", "0", "0", "0.0"), + ("13h 24m 34.67s", "13", "24", "34.67"), + ("22.5d", "1", "30", "0.0"), + ], + ) + def test_right_ascension( + self, ra: str, hours: str, minutes: str, seconds: str, base_target + ): + """Test that the right ascension is mapped correctly to XML.""" + target = base_target + target.ra = Angle(ra) + + xml = render_template("target.xml", target=target.model_dump()) + + assert f"{hours}" in xml + assert f"{minutes}" in xml + assert f"{seconds}" in xml + + @pytest.mark.parametrize( + "dec, sign, degrees, arcminutes, arcseconds", + [ + ("0d", "+", "0", "0", "0.0"), + ("-53d 13m 47.44s", "-", "53", "13", "47.44"), + ("+6d 30m 17.6s", "+", "6", "30", "17.6"), + ("10d", "+", "10", "0", "0.0"), + ], + ) + def test_declination( + self, + sign: str, + dec: str, + degrees: str, + arcminutes: str, + arcseconds: str, + base_target, + ): + """Test that the declination is mapped correctly to XML.""" + target = base_target + target.dec = dec + + xml = render_template("target.xml", target=target.model_dump()) + + assert f"{sign}" in xml + assert f"{degrees}" in xml + assert f"{arcminutes}" in xml + assert f"{arcseconds}" in xml + + @pytest.mark.parametrize( + "proper_motion_ra, proper_motion_dec, required", + [(0, 0, False), (0, 34, True), (45, 0, True), (-5, -7, True)], + ) + def test_proper_motion( + self, + proper_motion_ra: float, + proper_motion_dec: float, + required: bool, + base_target, + ): + """Test that the proper motion is mapped correctly to XML.""" + target = base_target + + target.proper_motion_ra = proper_motion_ra + target.proper_motion_dec = proper_motion_dec + + xml = render_template("target.xml", target=target.model_dump()) + + if required: + assert "RightAscensionDot" in xml + assert "DeclinationDot" in xml + else: + assert "RightAscensionDot" not in xml + assert "DeclinationDot" not in xml + + validate_xml(xml) + assert True + + +class TestAcquisition: + @pytest.mark.parametrize("full", [False, True]) + def test_acquisition( + self, full: bool, base_acquisition, base_reference_star, base_target + ): + """Test that the acquisition template generates valid XML.""" + acquisition = base_acquisition + target = base_target + + if full: + acquisition.reference_star = base_reference_star + acquisition.include_focused_image = True + else: + acquisition.reference_star = None + acquisition.include_focused_image = False + + xml = render_template( + "acquisition.xml", + acquisition=acquisition.model_dump(), + target=target.model_dump(), + ) + + if full: + assert "" in xml + assert "" in xml + else: + assert "" not in xml + assert "" not in xml + + validate_xml(xml) + assert True + + +class TestBlock: + @pytest.mark.parametrize("full", [False, True]) + def test_block(self, full: bool, base_block): + """Test that the block template generates valid XML.""" + block = base_block + + if full: + block.comments = "some comment" + block.acquisition.position_angle = 67 * u.deg + else: + block.comments = None + block.acquisition.position_angle = None + + xml = render_template("block.xml", block=block.model_dump()) + + if full: + assert "" in xml + assert "" in xml + + validate_xml(xml) + assert True + + @pytest.mark.parametrize( + "position_angle, do_not_flip, expected, not_expected", + [ + ( + 17 * u.deg, + True, + ["OnSkyPositionAngle", "Fixed"], + ["UseParallacticAngle"], + ), + ( + "parallactic", + False, + ["OnSkyPositionAngle", "UseParallacticAngle"], + ["Fixed"], + ), + ( + None, + True, + [], + ["OnSkyPositionAngle", "Fixed", "UseParallacticAngle"], + ), + ], + ) + def test_position_angle( + self, + position_angle: Any, + do_not_flip: bool, + expected: list[str], + not_expected: list[str], + base_block, + ): + """Test that the position angle is handled correctly in the generated XML.""" + block = base_block + block.acquisition.position_angle = position_angle + block.acquisition.do_not_flip_position_angle = do_not_flip + + xml = render_template("block.xml", block=block.model_dump()) + + for e in expected: + assert e in xml + for ne in not_expected: + assert ne not in xml + + validate_xml(xml) + assert True + + def test_windows_with_start_and_end(self, base_block): + """Test that windows with start and end are serialized correctly.""" + block = base_block + block.windows = [ + Window( + start=datetime( + 2026, 4, 20, 3, 4, 5, 0, tzinfo=ZoneInfo("Africa/Johannesburg") + ), + end=datetime( + 2026, 4, 22, 4, 5, 6, 0, tzinfo=ZoneInfo("Africa/Johannesburg") + ), + ), + Window( + start=datetime( + 2026, 5, 1, 22, 0, 17, 0, tzinfo=ZoneInfo("Africa/Johannesburg") + ), + end=datetime( + 2026, 5, 2, 2, 15, 6, 0, tzinfo=ZoneInfo("Africa/Johannesburg") + ), + ), + ] + + xml = render_template("block.xml", block=block.model_dump()) + + assert "2026-04-20T01:04:05Z" in xml + assert "2026-04-22T02:05:06Z" in xml + assert "2026-05-01T20:00:17Z" in xml + assert "2026-05-02T00:15:06Z" in xml + + validate_xml(xml) + assert True + + def test_window_with_end_only(self, base_block): + """Test that a window with an end only is serialized correctly.""" + block = base_block + block.windows = [ + Window( + end=datetime( + 2026, 5, 21, 2, 21, 7, 0, tzinfo=ZoneInfo("Africa/Johannesburg") + ) + ) + ] + + with time_machine.travel( + datetime(2026, 5, 21, 17, 0, 0, 0, tzinfo=ZoneInfo("UTC")) + ): + xml = render_template("block.xml", block=block.model_dump()) + + assert "2026-05-21T17:00:00Z" in xml + assert "2026-05-21T00:21:07Z" in xml + + +class TestBlockSubmission: + def test_block_submission(self, base_request): + request = base_request + block = base_request.blocks[0] + block_copy = deepcopy(block) + request.blocks.append(block_copy) + + xml = render_template("block_submission.xml", request=request.model_dump()) + + assert "" in xml + assert len(re.findall(r"{year}" in xml + assert f"{semester}" in xml diff --git a/tests/salt/models/serialize/test_util.py b/tests/salt/models/serialize/test_util.py new file mode 100644 index 0000000..3f8c0be --- /dev/null +++ b/tests/salt/models/serialize/test_util.py @@ -0,0 +1,190 @@ +import pathlib +import re + +import pytest +from jinja2 import FileSystemLoader + +from aeonlib.salt.models.util import ( + attachment_path_replacements, + validate_xml, + render_template, + replace_attachment_paths, +) + + +def test_validate_non_well_formed_xml(): + """Test that an error is raised when you validate XML which is not well-formed.""" + xml = "" + with pytest.raises(ValueError): + validate_xml(xml) + + +def test_validate_invalid_xml(): + """ + Test that an error is raised when you validate XNL which does not conform to the + schema. + """ + xml = """ + + A + arcseconds/year + + """ + with pytest.raises(ValueError): + validate_xml(xml) + + +def test_validate_valid_xml(): + """Test that valid XML passes validation.""" + xml = """ + + 4.56 + arcseconds/year + + """ + validate_xml(xml) + assert True + + +def test_render_template(): + """Test rendering a Jinja template.""" + loader = FileSystemLoader(pathlib.Path(__file__).parent.parent.parent / "data") + rendered = render_template("test.xml", loader, a=1, b=2) + assert "1" in rendered + assert "2" in rendered + + +def test_render_template_with_escaping(): + """Test that input is escaped when rendering a Jinja template.""" + loader = FileSystemLoader(pathlib.Path(__file__).parent.parent.parent / "data") + rendered = render_template("test.xml", loader, a="a >= 1 & a <= 5", b=2) + assert "a >= 1 & a <= 5" in rendered + + +def test_attachment_replacement_paths(): + """Test that the replacement paths for attachments are generated correctly.""" + path1 = pathlib.Path("/a/b/c.PNG") + path2 = pathlib.Path("a.pdf") + path3 = pathlib.Path("something") + attachments = [path1, path2, path3] + replacements = attachment_path_replacements(attachments) + + assert len(replacements) == 3 + + for path in [path1, path2, path3]: + assert replacements[path].startswith("Included/") + + # The replacement looks as if it includes a UUID v4 string. + assert len(replacements[path1]) > 36 + assert "-" in replacements[path1] + + # The correct file extension (if any) is used. + assert replacements[path1].endswith(".png") + assert replacements[path2].endswith(".pdf") + assert not replacements[path3].endswith(".") + + +def test_replace_attachment_paths( + base_request, base_block, base_rss, base_rss_multi_object_spectroscopy +): + """Test replacing attachment paths.""" + # finder_chart deliberately includes ".." to test resolution. + finder_chart = ( + pathlib.Path(__file__).parent.parent.parent + / "data/../data" + / "dummy_finder_chart_1.pdf" + ) + mos_mask = ( + pathlib.Path(__file__).parent.parent.parent / "data" / "dummy_rss_mos_mask.rsmt" + ) + request = base_request + block = base_block + rss = base_rss + configuration = base_rss_multi_object_spectroscopy + configuration.mask = mos_mask + block.acquisition.finder_charts = [finder_chart] + rss.configuration = configuration + block.instrument = rss + request.blocks = [block] + + xml = render_template("block_submission.xml", request=request.model_dump()) + + replacements = { + finder_chart.resolve(): "Included/FinderChart.pdf", + mos_mask.resolve(): "Included/MOS.rsmt", + } + + updated_xml = replace_attachment_paths(xml, replacements) + + assert re.search(r"\s*Included/FinderChart.pdf\s*", updated_xml) + assert re.search(r"\s*Included/MOS.rsmt\s*", updated_xml) + + +def test_missing_replacement(base_request, base_block): + """Test that an error is raised if an attachment path replacement is missing.""" + finder_chart = ( + pathlib.Path(__file__).parent.parent.parent + / "data" + / "dummy_finder_chart_1.pdf" + ) + request = base_request + block = base_block + block.acquisition.finder_charts = [finder_chart] + request.blocks = [block] + + xml = render_template("block_submission.xml", request=request.model_dump()) + with pytest.raises(ValueError, match="Path missing"): + replace_attachment_paths(xml, {}) + + +def test_duplicate_replacement_key(base_request): + """ + Test that an error is raised if there is a duplicate key in the dictionary of + replacements. + """ + # file_1a and file_1b are the same file + file_1a = ( + pathlib.Path(__file__).parent.parent.parent + / "data" + / "dummy_finder_chart_1.pdf" + ) + file_1b = ( + pathlib.Path(__file__).parent.parent.parent + / "data/../data" + / "dummy_finder_chart_1.pdf" + ) + + request = base_request + + xml = render_template("block_submission.xml", request=request.model_dump()) + + replacements = {file_1a: "Included/File_1a.pdf", file_1b: "Included/File_1b.pdf"} + + with pytest.raises(ValueError, match="same path"): + replace_attachment_paths(xml, replacements) + + +def test_duplicate_replacement_value(base_request): + """ + Test that an error is raised if there is a duplicate value in the dictionary of + replacements. + """ + file_1 = ( + pathlib.Path(__file__).parent.parent.parent + / "data" + / "dummy_finder_chart_1.pdf" + ) + file_2 = ( + pathlib.Path(__file__).parent.parent.parent + / "data" + / "dummy_finder_chart_2.pdf" + ) + + request = base_request + + xml = render_template("block_submission.xml", request=request.model_dump()) + + replacements = {file_1: "Included/File.pdf", file_2: "Included/File.pdf"} + + with pytest.raises(ValueError, match="duplicate value"): + replace_attachment_paths(xml, replacements) diff --git a/tests/salt/models/test_block_models.py b/tests/salt/models/test_block_models.py new file mode 100644 index 0000000..a3c2d50 --- /dev/null +++ b/tests/salt/models/test_block_models.py @@ -0,0 +1,179 @@ +from contextlib import nullcontext +from typing import Any + +import astropy.coordinates +import pytest +from pydantic import ValidationError + +from aeonlib.salt.models import Acquisition +from aeonlib.salt.models.block_models import ReferenceStar +from aeonlib.salt.models.util import render_template, validate_xml + + +class TestBlock: + def test_block(self, base_block): + """Test that a simple block can be built.""" + assert True + + @pytest.mark.parametrize( + "num_visits, max_num_visits, expectation", + [ + (7, None, nullcontext()), + (7, 8, nullcontext()), + (7, 7, nullcontext()), + (7, 6, pytest.raises(ValidationError, match="greater than")), + ], + ) + def test_max_visits_and_visits( + self, num_visits, max_num_visits, expectation, base_block + ): + """ + Test that the maximum number of visits must not be less than the number of + visits. + """ + block = base_block + + with expectation: + block.num_visits = num_visits + block.max_num_visits = max_num_visits + + assert True + + def test_salticam(self, base_block, base_salticam): + """Test that Salticam as a block instrument is handled correctly.""" + block = base_block + block.instrument = base_salticam + + xml = render_template("block.xml", block=block.model_dump()) + + assert "" in xml + + validate_xml(xml) + assert True + + def test_rss(self, base_block, base_rss): + """Test that RSS as a block instrument is handled correctly.""" + block = base_block + block.instrument = base_rss + + xml = render_template("block.xml", block=block.model_dump()) + + assert "" in xml + + validate_xml(xml) + assert True + + def test_hrs(self, base_block, base_hrs): + """Test that HRS as a block instrument is handled correctly.""" + block = base_block + block.instrument = base_hrs + + xml = render_template("block.xml", block=block.model_dump()) + + assert "" in xml + + validate_xml(xml) + assert True + + def test_nirwals(self, base_block, base_nirwals): + """Test that RSS as a block instrument is handled correctly.""" + block = base_block + block.instrument = base_nirwals + + xml = render_template("block.xml", block=block.model_dump()) + + assert "" in xml + + validate_xml(xml) + assert True + + +class TestConstraints: + def test_constraints(self, base_constraints): + """Test that constraints can be built.""" + assert True + + +class TestAcquisition: + def test_acquisition(self, base_acquisition): + """Test that acquisitions can be built.""" + assert True + + @pytest.mark.parametrize( + "position_angle, do_not_flip, expected", + [ + (45, True, nullcontext()), + (-34, False, nullcontext()), + (124, None, pytest.raises(ValueError)), + ("parallactic", True, nullcontext()), + ("parallactic", False, nullcontext()), + ("parallactic", None, nullcontext()), + (None, True, nullcontext()), + (None, False, nullcontext()), + (None, None, nullcontext()), + ], + ) + def test_do_not_flip_position_angle( + self, position_angle: Any, do_not_flip: bool | None, expected, base_acquisition + ): + """ + Test that the do_not_flip_position_angle field must be True or False if the + position angle value is an actual angle (rather than "parallactic" or None) and + must be None otherwise. + """ + a = base_acquisition + with expected: + Acquisition( + finder_charts=a.finder_charts, + filter=a.filter, + exposure_time=a.exposure_time, + reference_star=a.reference_star, + position_angle=position_angle, + do_not_flip_position_angle=do_not_flip, + include_focused_image=a.include_focused_image, + ) + + @pytest.mark.parametrize( + "position_angle, do_not_flip", + [(63, False), ("parallactic", None), (None, None)], + ) + def test_default_do_not_flip_position_angle( + self, position_angle: Any, do_not_flip: bool | None, base_acquisition + ): + """ + Test that the default value for the do_not_flip_position_angle field is correct. + """ + a = base_acquisition + acquisition = Acquisition( + finder_charts=a.finder_charts, + filter=a.filter, + exposure_time=a.exposure_time, + reference_star=a.reference_star, + position_angle=position_angle, + include_focused_image=a.include_focused_image, + ) + if do_not_flip is not None: + assert acquisition.do_not_flip_position_angle == do_not_flip + else: + assert acquisition.do_not_flip_position_angle is None + + +class TestReferenceStar: + def test_reference_star(self, base_reference_star): + """Test that reference stars can be built.""" + assert True + + @pytest.mark.parametrize( + "dec, expectation", + [ + (astropy.coordinates.Angle("-76.001d"), pytest.raises(ValueError)), + (astropy.coordinates.Angle("-76d"), nullcontext()), + (astropy.coordinates.Angle("11d"), nullcontext()), + (astropy.coordinates.Angle("11.0001d"), pytest.raises(ValueError)), + ], + ) + def test_dec_range(self, dec, expectation, base_reference_star): + ref_star = base_reference_star.model_dump() + ref_star["dec"] = dec + with expectation: + ReferenceStar(**ref_star) # type: ignore diff --git a/tests/salt/models/test_hrs_models.py b/tests/salt/models/test_hrs_models.py new file mode 100644 index 0000000..a51434f --- /dev/null +++ b/tests/salt/models/test_hrs_models.py @@ -0,0 +1,35 @@ +import pytest + +from aeonlib.salt.models.types import HrsMode + + +class TestHrs: + def test_hrs(self, base_hrs): + # Test that HRS configurations can be built. + assert True + + @pytest.mark.parametrize( + "mode, prv_calibration", + [ + ("low resolution", None), + ("medium resolution", None), + ("high resolution", None), + ("high stability", "ThAr"), + ], + ) + def test_prv_calibration(self, mode: HrsMode, prv_calibration, base_hrs): + # Test that the default value for the precision radial velocity calibration is + # correct. + hrs = base_hrs + hrs.mode = mode + + if prv_calibration is not None: + assert hrs.prv_calibration == prv_calibration + else: + assert hrs.prv_calibration is None + + +class TestHrsDetector: + def test_hrs_detector(self, base_hrs_detector): + # Test that HRS detector setups can be built. + assert True diff --git a/tests/salt/models/test_nirwals_models.py b/tests/salt/models/test_nirwals_models.py new file mode 100644 index 0000000..818257f --- /dev/null +++ b/tests/salt/models/test_nirwals_models.py @@ -0,0 +1,58 @@ +from contextlib import nullcontext + +from astropy import units as u +import pytest + +from aeonlib.salt.models import Nirwals, NirwalsDitherPatternStep + + +class TestNirwals: + def test_nirwals(self, base_nirwals): + # Test that NIRWALS configurations can be built. + assert True + + @pytest.mark.parametrize( + "angle, expectation", + [ + (-1e-7 * u.deg, pytest.raises(ValueError)), + (0, nullcontext()), + (0.001, pytest.raises(ValueError)), + (12.499 * u.deg, pytest.raises(ValueError)), + (12.5, nullcontext()), + (12.501 * u.deg, pytest.raises(ValueError)), + ((74 * u.deg).to(u.rad), nullcontext()), + (100 * u.deg, nullcontext()), + ((100 + 1e-7) * u.deg, pytest.raises(ValueError)), + (100.5 * u.deg, pytest.raises(ValueError)), + (370 * u.deg, pytest.raises(ValueError)), + ], + ) + def test_articulation_angle_must_have_correct_value( + self, angle, expectation, base_nirwals + ): + # Test that the articulation anfle must be a multiple of 0.5 degrees between 0 + # and 100 degrees. + data = base_nirwals.model_dump() + data["articulation_angle"] = angle + with expectation: + Nirwals(**data) + + +class TestNirwalsDitherPatternStep: + def test_nirwals_dither_pattern_step(self): + # Test that NIRWALS dither pattern step setups can be built. + assert True + + @pytest.mark.parametrize( + "exposure_time, num_groups", + [(0.001 * u.s, 1), (37 * u.s, 50), (70_000 * u.ms, 96)], + ) + def test_number_of_groups( + self, exposure_time, num_groups, base_nirwals_dither_pattern_step + ): + # Test that the number of groups is calculated correctly. + data = base_nirwals_dither_pattern_step.model_dump() + data["exposure_time"] = exposure_time + step = NirwalsDitherPatternStep(**data) + + assert step.num_groups == num_groups diff --git a/tests/salt/models/test_request_models.py b/tests/salt/models/test_request_models.py new file mode 100644 index 0000000..81dc165 --- /dev/null +++ b/tests/salt/models/test_request_models.py @@ -0,0 +1,172 @@ +import io +import os.path +import pathlib +import zipfile +from copy import deepcopy + +import pytest +from pydantic import ValidationError + +from aeonlib.salt.models import Request + + +class TestRequest: + def test_no_blocks(self): + """Test that at least one block must be supplied.""" + with pytest.raises(ValidationError) as exc_info: + Request(proposal_code="2025-1-SCI-042", semester="2026-1", blocks=[]) + assert exc_info.value.errors()[0]["loc"] == ("blocks",) + assert exc_info.value.errors()[0]["type"] == "too_short" + + def test_no_attachments( + self, base_request, base_block, base_rss, base_rss_longslit_spectroscopy + ): + """Test the case that the request includes no attachments.""" + request = base_request + block = base_block + rss = base_rss + configuration = base_rss_longslit_spectroscopy + block.acquisition.finder_charts = [] + rss.configuration = configuration + block.instrument = rss + request.blocks = [block] + + assert request.attachments() == set() + + def test_multiple_attachments( + self, base_request, base_block, base_rss, base_rss_multi_object_spectroscopy + ): + """Test the case that the request includes multiple attachments.""" + finder_chart_1 = ( + pathlib.Path(__file__).parent.parent / "data" / "dummy_finder_chart_1.pdf" + ) + finder_chart_2 = ( + pathlib.Path(__file__).parent.parent / "data" / "dummy_finder_chart_2.pdf" + ) + mos_mask = ( + pathlib.Path(__file__).parent.parent / "data" / "dummy_rss_mos_mask.rsmt" + ) + request = base_request + block = base_block + rss = base_rss + configuration = base_rss_multi_object_spectroscopy + configuration.mask = mos_mask + block.acquisition.finder_charts = [finder_chart_1, finder_chart_2] + rss.configuration = configuration + block.instrument = rss + request.blocks = [block] + + assert request.attachments() == { + finder_chart_1.resolve(), + finder_chart_2.resolve(), + mos_mask.resolve(), + } + + def test_duplicate_attachments( + self, base_request, base_block, base_rss, base_rss_multi_object_spectroscopy + ): + """Test the case that the request uses the same attachment multiple times.""" + # finder_chart_1a, finder_chart_1b and finder_chart_1c denote the sane file + finder_chart_1a = ( + pathlib.Path(__file__).parent.parent / "data" / "dummy_finder_chart_1.pdf" + ) + finder_chart_1b = ( + pathlib.Path(__file__).parent.parent + / "data/../data" + / "dummy_finder_chart_1.pdf" + ) + finder_chart_1c = ( + pathlib.Path(__file__).parent.parent + / "data/../../salt/data" + / "dummy_finder_chart_1.pdf" + ) + finder_chart_2 = ( + pathlib.Path(__file__).parent.parent / "data" / "dummy_finder_chart_2.pdf" + ) + mos_mask = finder_chart_1c # as we are testing for duplicates + request = base_request + block = base_block + rss = base_rss + configuration = base_rss_multi_object_spectroscopy + configuration.mask = mos_mask + rss.configuration = configuration + block.instrument = rss + + block1 = block + block2 = deepcopy(block1) + + block1.acquisition.finder_charts = [ + finder_chart_1a, + finder_chart_2, + finder_chart_1b, + ] + block2.acquisition.finder_charts = [ + finder_chart_1c, + finder_chart_2, + ] + + request.blocks = [block1, block2] + + assert request.attachments() == { + finder_chart_1a.resolve(), + finder_chart_2.resolve(), + } + + def test_export(self, base_block, base_rss, base_rss_multi_object_spectroscopy): + """Test that a correct zip file is generated.""" + + # Set up the attachments. + finder_chart_1 = ( + pathlib.Path(__file__).parent.parent / "data" / "dummy_finder_chart_1.pdf" + ) + finder_chart_2 = ( + pathlib.Path(__file__).parent.parent / "data" / "dummy_finder_chart_3.png" + ) + mos_file = ( + pathlib.Path(__file__).parent.parent / "data" / "dummy_rss_mos_mask.rsmt" + ) + + # Store the attachment sizes. + finder_chart_1_size = os.path.getsize(finder_chart_1) + finder_chart_2_size = os.path.getsize(finder_chart_2) + mos_file_size = os.path.getsize(mos_file) + + # Set up the first block. + block1 = deepcopy(base_block) + block1.acquisition.finder_charts = [finder_chart_1] + base_rss_multi_object_spectroscopy.mask = mos_file + base_rss.configuration = base_rss_multi_object_spectroscopy + block1.instrument = base_rss + + # Set up the second block. + block2 = deepcopy(base_block) + block2.acquisition.finder_charts = [finder_chart_2] + + # Generate the zip file. + request = Request( + proposal_code="2026-1-SCI-042", semester="2026-1", blocks=[block1, block2] + ) + zip_content = io.BytesIO() + request.export(zip_content) + + # Check the content of the generated zip file. + zip_content.seek(0) + with zipfile.ZipFile(zip_content) as archive: + block_submission = archive.read("Blocks.xml").decode(encoding="utf-8") + for file in archive.namelist(): + if file != "Blocks.xml": + assert file.startswith("Included/") + assert ( + file.endswith(".pdf") + or file.endswith(".png") + or file.endswith(".rsmt") + ) + assert file in block_submission + + content = archive.read(file) + if file.endswith(".pdf"): + assert len(content) == finder_chart_1_size + elif file.endswith(".png"): + assert len(content) == finder_chart_2_size + elif file.endswith(".rsmt"): + assert len(content) == mos_file_size diff --git a/tests/salt/models/test_rss_models.py b/tests/salt/models/test_rss_models.py new file mode 100644 index 0000000..60a7b0a --- /dev/null +++ b/tests/salt/models/test_rss_models.py @@ -0,0 +1,206 @@ +import math +from contextlib import nullcontext + +import pytest +from astropy import units as u +from pydantic import ValidationError + +from aeonlib.salt.models import RssDitherPattern, RssPolarimetry, RssSpectroscopy +from aeonlib.salt.models.util import ( + LINEAR_POLARIMETRY_PATTERN, + LINEAR_HI_POLARIMETRY_PATTERN, + CIRCULAR_POLARIMETRY_PATTERN, + CIRCULAR_HI_POLARIMETRY_PATTRERN, + ALL_STOKES_POLARIMETRY_PATTERN, +) + + +class TestRss: + def test_rss(self, base_rss): + """Test that RSS configurations can be built.""" + assert True + + +class TestRssImaging: + def test_rss_imaging(self, base_rss_imaging): + """Test that RSS imaging configurations can be built.""" + assert True + + +class TestRssSpectroscopy: + def test_rss_spectroscopy(self, base_rss_spectroscopy): + """Test that RSS spectroscopy setups can be built.""" + assert True + + @pytest.mark.parametrize( + "angle, expectation", + [ + (-40, pytest.raises(ValueError)), + (0, nullcontext()), + (0.01, pytest.raises(ValueError)), + (0.125, pytest.raises(ValueError)), + (64.73, pytest.raises(ValueError)), + (64.75, nullcontext()), + (64.75 * u.deg, nullcontext()), + ((64.75 * u.deg).to(u.rad), nullcontext()), + (64.76 * u.deg, pytest.raises(ValueError)), + (100, nullcontext()), + (100.75, pytest.raises(ValueError)), + (400, pytest.raises(ValueError)), + ], + ) + def test_articulation_angle_must_have_allowed_value( + self, angle, expectation, base_rss_spectroscopy + ): + spectroscopy = base_rss_spectroscopy.model_dump() + spectroscopy["articulation_angle"] = angle + with expectation: + RssSpectroscopy(**spectroscopy) + + @pytest.mark.parametrize( + "angle, station", [(0, 0), (1.75, 1), (12.25, 15), (49, 64), (100, 132)] + ) + def test_articulation_station( + self, angle: float, station: int, base_rss_spectroscopy + ): + spectroscopy = base_rss_spectroscopy + spectroscopy.articulation_angle = angle + assert spectroscopy.articulation_station == station + + +class TestRssPolarimetry: + def test_rss_polarimetry(self, base_rss_polarimetry): + """Test that RSS polarimetry setups can be built.""" + assert True + + @pytest.mark.parametrize( + "pattern, expectation", + [ + ("linear", nullcontext()), + ("all-Stokes", nullcontext()), + ([], pytest.raises(ValueError)), + ([(45 * u.deg, 90 * u.deg)], nullcontext()), + ([(45 * u.deg, 90 * u.deg)] * 8, nullcontext()), + ([(45 * u.deg, 90 * u.deg)] * 9, pytest.raises(ValueError)), + ], + ) + def test_pattern_must_have_between_1_and_8_steps(self, pattern, expectation): + """Test that the wave pattern must have between 1 and 8 steps.""" + with expectation: + RssPolarimetry(wave_plate_pattern=pattern) + + @pytest.mark.parametrize( + "angle, expectation", + [ + (0, nullcontext()), + (11.25, nullcontext()), + ((math.pi / 4) * u.rad, nullcontext()), # 45 degrees in radians + (303.75, nullcontext()), + (-0.01, pytest.raises(ValueError)), + (-11.25, pytest.raises(ValueError)), + (11.24, pytest.raises(ValueError)), + (303.76, pytest.raises(ValueError)), + (360, pytest.raises(ValueError)), + (405, pytest.raises(ValueError)), + ], + ) + def test_angles_must_have_allowed_value(self, angle, expectation): + # Test that wave plate pattern angles must be a multiple of 11.25 deg between + # 0 deg (inclusive) and 360 deg (exclusive). + with expectation: + RssPolarimetry(wave_plate_pattern=[(angle, 45 * u.deg)]) + RssPolarimetry(wave_plate_pattern=[(45 * u.deg, angle)]) + + def test_linear_pattern(self): + # That the string "linear" is internally converted into the linear pattern. + polarimetry = RssPolarimetry(wave_plate_pattern="linear") + assert polarimetry.wave_plate_pattern == LINEAR_POLARIMETRY_PATTERN + + def test_linear_hi_pattern(self): + # That the string "linear hi" is internally converted into the linear hi + # pattern. + polarimetry = RssPolarimetry(wave_plate_pattern="linear hi") + assert polarimetry.wave_plate_pattern == LINEAR_HI_POLARIMETRY_PATTERN + + def test_circular_pattern(self): + # That the string "circular" is internally converted into the circular pattern. + polarimetry = RssPolarimetry(wave_plate_pattern="circular") + assert polarimetry.wave_plate_pattern == CIRCULAR_POLARIMETRY_PATTERN + + def test_circular_hi_pattern(self): + # That the string "circular hi" is internally converted into the circular hi + # pattern. + polarimetry = RssPolarimetry(wave_plate_pattern="circular hi") + assert polarimetry.wave_plate_pattern == CIRCULAR_HI_POLARIMETRY_PATTRERN + + def test_all_stokes_pattern(self): + # That the string "all-Stokes" is internally converted into the all-Stokes + # pattern. + polarimetry = RssPolarimetry(wave_plate_pattern="all-Stokes") + assert polarimetry.wave_plate_pattern == ALL_STOKES_POLARIMETRY_PATTERN + + +class TestRssLongslitSpectroscopy: + def test_rss_longslit_spectroscopy(self, base_rss_longslit_spectroscopy): + # Test that RSS longslit spectroscopy setups can be built. + assert True + + +class TestRssMultiObjectSpectroscopy: + def test_rss_multi_object_spectroscopy(self, base_rss_multi_object_spectroscopy): + # Test that RSS multiobject spectroscopy setups can be built. + assert True + + +class TestRssSlitMaskIFUSpectroscopy: + def test_slit_mask_ifu_spectroscopy(self, base_rss_slit_mask_ifu_spectroscopy): + # Test that RSS slit mask IFU spectroscopy setups can be built. + assert True + + +class TestRssDetector: + def test_rss_detector(self, base_rss_detector): + # Test that RSS detector setups can be built. + assert True + + +class TestRssDitherPattern: + def test_rss_dither_pattern(self, base_rss_dither_pattern): + """Test that RSS dither pattern can be built.""" + assert True + + def test_default_number_of_steps(self, base_rss_dither_pattern): + dither_pattern = base_rss_dither_pattern.model_dump() + dither_pattern["num_rows"] = 4 + dither_pattern["num_columns"] = 3 + if "num_steps" in dither_pattern: + del dither_pattern["num_steps"] + assert RssDitherPattern(**dither_pattern).num_steps == 12 # type: ignore + + @pytest.mark.parametrize( + "num_rows, num_columns, num_steps, expectation", + [ + (1, 1, 1, nullcontext()), + (1, 1, 5, nullcontext()), + (1, 2, 2, nullcontext()), + (2, 1, 6, nullcontext()), + (3, 5, 15, nullcontext()), + (5, 3, 45, nullcontext()), + (5, 2, 9, pytest.raises(ValidationError)), + (3, 7, 43, pytest.raises(ValidationError)), + ], + ) + def test_only_complete_patterns_allowed( + self, + num_rows, + num_columns, + num_steps, + expectation, + base_rss_dither_pattern, + ): + dither_pattern = base_rss_dither_pattern.model_dump() + dither_pattern["num_rows"] = num_rows + dither_pattern["num_columns"] = num_columns + dither_pattern["num_steps"] = num_steps + with expectation: + RssDitherPattern(**dither_pattern) # type: ignore diff --git a/tests/salt/models/test_salticam_models.py b/tests/salt/models/test_salticam_models.py new file mode 100644 index 0000000..3fdbd92 --- /dev/null +++ b/tests/salt/models/test_salticam_models.py @@ -0,0 +1,73 @@ +from contextlib import nullcontext + +import pytest +from pydantic import ValidationError + +from aeonlib.salt.models import Salticam, SalticamDitherPattern + + +class TestSalticam: + def test_salticam(self, base_salticam): + """Test that Salticam configurations can be built.""" + assert True + + def test_at_least_one_filter(self, base_salticam): + """Test that the filter sequence must have at least one step.""" + salticam = base_salticam.model_dump() + salticam["filter_sequence"] = [] + with pytest.raises(ValidationError, match="at least 1"): + Salticam(**salticam) # type: ignore + + +class TestSalticamFilterSequenceStep: + def test_salticam_filter_sequence_step(self, base_salticam_filter_sequence_step): + """Test that filter sequence steps can be built.""" + assert True + + +class TestSalticamDetector: + def test_salticam_detector(self, base_salticam_detector): + """Test that Salticam detector setups can be built.""" + assert True + + +class TestSalticamDitherPattern: + def test_salticam_dither_pattern(self, base_salticam_dither_pattern): + """Test that Salticam dither pattern can be built.""" + assert True + + def test_default_number_of_steps(self, base_salticam_dither_pattern): + dither_pattern = base_salticam_dither_pattern.model_dump() + dither_pattern["num_rows"] = 4 + dither_pattern["num_columns"] = 3 + if "num_steps" in dither_pattern: + del dither_pattern["num_steps"] + assert SalticamDitherPattern(**dither_pattern).num_steps == 12 # type: ignore + + @pytest.mark.parametrize( + "num_rows, num_columns, num_steps, expectation", + [ + (1, 1, 1, nullcontext()), + (1, 1, 5, nullcontext()), + (1, 2, 2, nullcontext()), + (2, 1, 6, nullcontext()), + (3, 5, 15, nullcontext()), + (5, 3, 45, nullcontext()), + (5, 2, 9, pytest.raises(ValidationError)), + (3, 7, 43, pytest.raises(ValidationError)), + ], + ) + def test_only_complete_patterns_allowed( + self, + num_rows, + num_columns, + num_steps, + expectation, + base_salticam_dither_pattern, + ): + dither_pattern = base_salticam_dither_pattern.model_dump() + dither_pattern["num_rows"] = num_rows + dither_pattern["num_columns"] = num_columns + dither_pattern["num_steps"] = num_steps + with expectation: + SalticamDitherPattern(**dither_pattern) # type: ignore diff --git a/tests/salt/models/test_target_models.py b/tests/salt/models/test_target_models.py new file mode 100644 index 0000000..55cd3cc --- /dev/null +++ b/tests/salt/models/test_target_models.py @@ -0,0 +1,84 @@ +from contextlib import nullcontext + +import astropy.coordinates +import astropy.units as u +import pytest +from pydantic import ValidationError + +from aeonlib.salt.models import MagnitudeRange, SaltSiderealTarget + + +class TestSaltSiderealTarget: + def test_salt_sidereal_target(self, base_target): + """Test that a simple target can be built.""" + assert True + + @pytest.mark.parametrize("target_type", ["ALTAZ", "HOUR_ANGLE"]) + def test_type(self, target_type: str, base_target): + """Test that the target type must be ICRS.""" + target = base_target + with pytest.raises(ValueError, match="SALT"): + target.type = target_type + + def test_hour_angle_must_not_exist(self, base_target): + """Test that no hour angle must be defined.""" + target = base_target + with pytest.raises(ValueError, match="hour angle"): + target.hour_angle = 45 * u.deg + + def test_altitude_must_not_exist(self, base_target): + """Test that no altitude must be defined.""" + target = base_target + with pytest.raises(ValueError, match="altitude"): + target.altitude = 45 * u.deg + + def test_azimuth_must_not_exist(self, base_target): + """Test that no azimuth must be defined.""" + target = base_target + with pytest.raises(ValueError, match="azimuth"): + target.azimuth = 45 * u.deg + + @pytest.mark.parametrize( + "dec, expectation", + [ + (astropy.coordinates.Angle("-76.001d"), pytest.raises(ValueError)), + (astropy.coordinates.Angle("-76d"), nullcontext()), + (astropy.coordinates.Angle("11d"), nullcontext()), + (astropy.coordinates.Angle("11.0001d"), pytest.raises(ValueError)), + ], + ) + def test_dec_range(self, dec, expectation, base_target): + """Test that the declination must be in SALT's visibility range.""" + target = base_target.model_dump() + target["dec"] = dec + with expectation: + SaltSiderealTarget(**target) # type: ignore + + +class TestMagnitudeRange: + def test_magnitude_range(self, base_magnitude_range): + """Test that a simple magnitude range can be built.""" + assert True + + @pytest.mark.parametrize( + "min_magnitude, max_magnitude, expectation", + [ + (17.1, 17.4, nullcontext()), + (17.1, 17.1, nullcontext()), + (17.1, 17.09, pytest.raises(ValidationError, match="greater than")), + ], + ) + def test_min_and_max_magnitude( + self, min_magnitude, max_magnitude, expectation, base_magnitude_range + ): + """ + Test that the maximum magnitude must not be less than the minimum magnitude. + """ + magnitude_range = base_magnitude_range.model_dump() + magnitude_range["min_magnitude"] = min_magnitude + magnitude_range["max_magnitude"] = max_magnitude + + with expectation: + MagnitudeRange(**magnitude_range) # type: ignore + + assert True diff --git a/tests/salt/test_online.py b/tests/salt/test_online.py new file mode 100644 index 0000000..01fa4e5 --- /dev/null +++ b/tests/salt/test_online.py @@ -0,0 +1,33 @@ +from datetime import datetime, timedelta + +import pytest +from aeonlib.models import Window +from aeonlib.salt.facility import SALTFacility + +window = Window( + start=datetime.now(), + end=datetime.now() + timedelta(days=365), +) + +# Replace with an appropriate proposal code. +proposal_code = "2026-1-DDT-002" + + +@pytest.mark.online +def test_validate(base_request): + # Test online proposal validation.""" + base_request.proposal_code = proposal_code + base_request.blocks[0].windows = [window] + facility = SALTFacility(use_playground=True) + valid, errors = facility.validate(base_request) + assert valid or errors + + +@pytest.mark.online +def test_submit(base_request): + # Test online proposal validation.""" + base_request.proposal_code = proposal_code + base_request.blocks[0].windows = [window] + facility = SALTFacility(use_playground=True) + submission = facility.submit(base_request) + assert submission diff --git a/tests/salt/test_validators.py b/tests/salt/test_validators.py new file mode 100644 index 0000000..78b5c12 --- /dev/null +++ b/tests/salt/test_validators.py @@ -0,0 +1,100 @@ +from contextlib import nullcontext +from typing import Annotated + +import pytest +from pydantic import BaseModel, ValidationError + +from aeonlib.salt.validators import GreaterEqual, GreaterThan, LessEqual, LessThan + + +class GreaterThanModel(BaseModel): + a: Annotated[int | None, GreaterThan(4)] + + +class GreaterEqualModel(BaseModel): + a: Annotated[int | None, GreaterEqual(4)] + + +class LessThanModel(BaseModel): + a: Annotated[int | None, LessThan(4)] + + +class LessEqualModel(BaseModel): + a: Annotated[int | None, LessEqual(4)] + + +class TestValidators: + @pytest.mark.parametrize( + "a, expectation", + [ + (3, pytest.raises(ValidationError)), + (4, pytest.raises(ValidationError)), + (5, nullcontext()), + (None, nullcontext()), + ], + ) + def test_greater_than(self, a, expectation): + """Test that the GreaterThan validator validates correctly.""" + with expectation: + GreaterThanModel(a=a) + + def test_greater_than_does_not_change_field_value(self): + """Test that the field value is not changed by the GreaterThan validator.""" + assert GreaterThanModel(a=7).a == 7 + + @pytest.mark.parametrize( + "a, expectation", + [ + (3, pytest.raises(ValidationError)), + (4, nullcontext()), + (5, nullcontext()), + (None, nullcontext()), + ], + ) + def test_greater_equal(self, a, expectation): + """Test that the GreaterEqual validator validates correctly.""" + with expectation: + GreaterEqualModel(a=a) + + def test_greater_equal_does_not_change_field_value(self): + """Test that the field value is not changed by the GreaterEqual validator.""" + assert GreaterEqualModel(a=7).a == 7 + assert GreaterEqualModel(a=None).a is None + + @pytest.mark.parametrize( + "a, expectation", + [ + (3, nullcontext()), + (4, pytest.raises(ValidationError)), + (5, pytest.raises(ValidationError)), + (None, nullcontext()), + ], + ) + def test_less_than(self, a, expectation): + """Test that the LessThan validator validates correctly.""" + with expectation: + LessThanModel(a=a) + + def test_less_than_does_not_change_field_value(self): + """Test that the field value is not changed by the LessThan validator.""" + assert LessThanModel(a=2).a == 2 + assert LessThanModel(a=None).a is None + + @pytest.mark.parametrize( + "a, expectation", + [ + (3, nullcontext()), + (4, nullcontext()), + (5, pytest.raises(ValidationError)), + (None, nullcontext()), + ], + ) + def test_less_equal(self, a, expectation): + """Test that the LessEqual validator validates correctly.""" + with expectation: + LessEqualModel(a=a) + + def test_less_equal_does_not_change_field_value(self): + """Test that the field value is not changed by the LessEqual validator.""" + assert LessEqualModel(a=2).a == 2 + assert LessEqualModel(a=None).a is None diff --git a/tests/salt/types/test_duration.py b/tests/salt/types/test_duration.py new file mode 100644 index 0000000..fb3027c --- /dev/null +++ b/tests/salt/types/test_duration.py @@ -0,0 +1,43 @@ +from contextlib import nullcontext + +import pytest + +from astropy import units as u +from pydantic import ValidationError, BaseModel + +from aeonlib.salt.models.types import Duration, PositiveDuration + + +class A(BaseModel): + a: Duration + + +class B(BaseModel): + b: PositiveDuration + + +class TestDuration: + @pytest.mark.parametrize("a", [5, 5 * u.s]) + def test_duration(self, a): + """Test that durations are stored with a unit.""" + v = A(a=a) + assert v.a.value == 5 + assert v.a.unit == u.s + + +class TestPositiveDuration: + @pytest.mark.parametrize( + "b, expectation", + [ + (-5.6, pytest.raises(ValidationError)), + (-3 * u.s, pytest.raises(ValidationError)), + (0, pytest.raises(ValidationError)), + (0 * u.s, pytest.raises(ValidationError)), + (0.001, nullcontext()), + (67 * u.s, nullcontext()), + ], + ) + def test_positive_duration_must_be_positive(self, b, expectation): + with expectation: + B(b=b) + assert True diff --git a/tests/salt/types/test_quantity.py b/tests/salt/types/test_quantity.py new file mode 100644 index 0000000..33c0538 --- /dev/null +++ b/tests/salt/types/test_quantity.py @@ -0,0 +1,89 @@ +import json +from typing import Annotated, Union + +import pytest +from astropy import units as u +from astropy.units import Quantity +from pydantic import BaseModel + +from aeonlib.salt.models.types.quantity import AstropyQuantityTypeAnnotation + +Wavelength = Annotated[ + Union[Quantity, float], AstropyQuantityTypeAnnotation(u.Angstrom) +] + +ProperMotion = Annotated[ + Union[Quantity, float], AstropyQuantityTypeAnnotation(u.arcsec / u.year) +] + + +class CelestialObject(BaseModel): + peak_wavelength: Wavelength + proper_motion: ProperMotion + + +class TestAstropyQuantityTypeAnnotation: + @pytest.mark.parametrize( + "peak_wavelength, proper_motion", + [ + # 1 year = 8766 hours + (4107 * u.Angstrom, 4383 * u.arcsec / u.year), + (410.7 * u.nm, 0.5 * u.arcsec / u.hour), + ], + ) + def test_from_quantity(self, peak_wavelength, proper_motion): + """ + Test objects constructed from astropy Quantity objects dump to json as floats + """ + asteroid = CelestialObject( + peak_wavelength=peak_wavelength, proper_motion=proper_motion + ) + dumped = asteroid.model_dump_json() + assert pytest.approx(json.loads(dumped)) == { + "peak_wavelength": 4107.0, + "proper_motion": 4383.0, + } + + def test_from_float(self): + """Test objects constructed from floats dump to json as floats""" + t = CelestialObject(peak_wavelength=7567.6, proper_motion=0.98) + dumped = t.model_dump_json() + assert pytest.approx(json.loads(dumped)) == { + "peak_wavelength": 7567.6, + "proper_motion": 0.98, + } + + @pytest.mark.parametrize( + "peak_wavelength, proper_motion", + [ + # 1 year = 8766 hours + (4107 * u.Angstrom, 4383 * u.arcsec / u.year), + (410.7 * u.nm, 0.5 * u.arcsec / u.hour), + ], + ) + def test_quantity_attributes(self, peak_wavelength, proper_motion): + """Test quantities are accessible on the model""" + asteroid = CelestialObject( + peak_wavelength=peak_wavelength, proper_motion=proper_motion + ) + assert isinstance(asteroid.peak_wavelength, Quantity) + assert pytest.approx(asteroid.peak_wavelength.value) == 4107 + assert asteroid.peak_wavelength.unit == u.Angstrom + assert pytest.approx(asteroid.proper_motion.value) == 4383.0 + assert asteroid.proper_motion.unit == u.arcsec / u.year + + def test_from_json(self): + """Test models can be constructed from json""" + target_json = json.dumps( + { + "peak_wavelength": "5516.89", + "proper_motion": "0.076", + } + ) + target = CelestialObject.model_validate_json(target_json) + assert isinstance(target.peak_wavelength, Quantity) + assert pytest.approx(target.peak_wavelength.value) == 5516.89 + assert target.peak_wavelength.unit == u.Angstrom + assert isinstance(target.proper_motion, Quantity) + assert pytest.approx(target.proper_motion.value) == 0.076 + assert target.proper_motion.unit == u.arcsec / u.year diff --git a/uv.lock b/uv.lock index b529e5e..17ce2b2 100644 --- a/uv.lock +++ b/uv.lock @@ -22,6 +22,12 @@ lt = [ { name = "lxml-stubs" }, { name = "suds" }, ] +salt = [ + { name = "beautifulsoup4" }, + { name = "jinja2" }, + { name = "lxml" }, + { name = "pyastrosalt" }, +] [package.dev-dependencies] codegen = [ @@ -29,28 +35,38 @@ codegen = [ { name = "textcase" }, ] dev = [ + { name = "lxml-stubs" }, { name = "pytest" }, + { name = "time-machine" }, ] [package.metadata] requires-dist = [ { name = "astropy", specifier = ">=6.0" }, + { name = "beautifulsoup4", marker = "extra == 'salt'", specifier = ">=4.14.3" }, { name = "httpx", specifier = ">=0.28.1" }, + { name = "jinja2", marker = "extra == 'salt'", specifier = ">=3.1.6" }, { name = "lxml", marker = "extra == 'lt'", specifier = ">=5.4.0" }, + { name = "lxml", marker = "extra == 'salt'", specifier = ">=6.0.1" }, { name = "lxml-stubs", marker = "extra == 'lt'", specifier = ">=0.5.1" }, { name = "p2api", marker = "extra == 'eso'", specifier = ">=1.0.10" }, + { name = "pyastrosalt", marker = "extra == 'salt'", specifier = ">=0.2.2" }, { name = "pydantic", specifier = ">=2.11.1" }, { name = "pydantic-settings", specifier = ">=2.9.1" }, { name = "suds", marker = "extra == 'lt'", specifier = ">=1.2.0" }, ] -provides-extras = ["eso", "lt"] +provides-extras = ["eso", "lt", "salt"] [package.metadata.requires-dev] codegen = [ { name = "jinja2", specifier = ">=3.1.6" }, { name = "textcase", specifier = ">=0.2.1" }, ] -dev = [{ name = "pytest", specifier = ">=8.3.5" }] +dev = [ + { name = "lxml-stubs", specifier = ">=0.5.1" }, + { name = "pytest", specifier = ">=8.3.5" }, + { name = "time-machine", specifier = ">=3.2.0" }, +] [[package]] name = "annotated-types" @@ -63,21 +79,20 @@ wheels = [ [[package]] name = "anyio" -version = "4.11.0" +version = "4.13.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, - { name = "sniffio" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c6/78/7d432127c41b50bccba979505f272c16cbcadcc33645d5fa3a738110ae75/anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4", size = 219094, upload-time = "2025-09-23T09:19:12.58Z" } +sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", size = 109097, upload-time = "2025-09-23T09:19:10.601Z" }, + { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, ] [[package]] name = "astropy" -version = "7.1.1" +version = "7.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "astropy-iers-data" }, @@ -86,47 +101,47 @@ dependencies = [ { name = "pyerfa" }, { name = "pyyaml" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e9/e1/6b8846dabc54b6fdc0262adefa041259f52ece9c929b10e0e90937691345/astropy-7.1.1.tar.gz", hash = "sha256:6d128f0005e2c34f70113484468bf9d0e4ca1ee15a279cfd08bdd979d38db0f8", size = 6982773, upload-time = "2025-10-10T20:36:49.347Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/15/d5/11031eb9788d35826ef527260cf17d5d6ebe8995ba8d67484c236644ce1e/astropy-7.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:268c6bdfb1c4feef8461070bbfdd6b8c07a9badf91977623168d7d3f0ddaf70c", size = 6403108, upload-time = "2025-10-10T20:36:12.958Z" }, - { url = "https://files.pythonhosted.org/packages/01/ca/57d8eeb6f8f67fb3063d6be1e043920f4f25f8e261042fa47a4ff5764c74/astropy-7.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:48d10b23e5a53afccd3e092d0c78792f8c644197ece4a7d95d83d7e491768d4c", size = 6349935, upload-time = "2025-10-10T20:36:14.67Z" }, - { url = "https://files.pythonhosted.org/packages/9b/d0/d9d33e9cdc10010e3e2f1e30cb9748a77c3a5ca69d4f4fed82d03bcafd79/astropy-7.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f04b4bdef1990e0b4a5cbdd7871ff172e291b9d4ac27a411e240727e4a357616", size = 10233406, upload-time = "2025-10-10T20:36:16.296Z" }, - { url = "https://files.pythonhosted.org/packages/93/9b/14fb6cf65bd18015c6fdab7c71e4ae00318dd325d0ed03441ff2bb2b913a/astropy-7.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:31233556969351c68e104700dbb81e86b02447beeea70028d2d04cd9c2fedd6f", size = 10290578, upload-time = "2025-10-10T20:36:18.595Z" }, - { url = "https://files.pythonhosted.org/packages/c5/1b/994b207601d062f31e67cabbc5827e42b8472ce926ed865f06dba8648429/astropy-7.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:70c69c9a8585d51658837a5643ed9cb4d386f343e2097bce38aba68fbdd48a7f", size = 10228861, upload-time = "2025-10-10T20:36:20.56Z" }, - { url = "https://files.pythonhosted.org/packages/2d/bd/84845404ec729f6e54a94d3f150e5d6c8808dae232f5e12262a72a428c95/astropy-7.1.1-cp312-cp312-win32.whl", hash = "sha256:452be62a2b9f68207f949089937574057415937e5273c37bdaafab0835c21259", size = 6157841, upload-time = "2025-10-10T20:36:22.313Z" }, - { url = "https://files.pythonhosted.org/packages/c1/83/80eca357b28d827f58c688b6c4e3ead88b577af55211676f3e1c13c5bfbd/astropy-7.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:85595f9dce107901ccd3bf493899c4b08a0925abb75d32c72325c4aba5369bd2", size = 6287394, upload-time = "2025-10-10T20:36:24.103Z" }, - { url = "https://files.pythonhosted.org/packages/12/31/77eb5e630da8df9eb30bf9b8234926dd229466cbff01b08590448b3941ae/astropy-7.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:91b8e62cf97d532be28cc794ffe33a394e04e53af24586cea209362aec9ecea1", size = 6397035, upload-time = "2025-10-10T20:36:25.425Z" }, - { url = "https://files.pythonhosted.org/packages/6d/18/af991251dc2f84b1e86b2ec1aa7c77831d90962a69668bba75402da9ad68/astropy-7.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c0c587ff3eab76474e696917273242d434c11de3d729971598b773cf15ef8ff0", size = 6344625, upload-time = "2025-10-10T20:36:26.785Z" }, - { url = "https://files.pythonhosted.org/packages/b9/aa/afd0e18e74fa116469c0ef9aed4c214053412786466f1a1a857af086c616/astropy-7.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fc05407cf0ac68896308bf974bad506f1357821b755d0cb60b87b3a1655d96c5", size = 10165691, upload-time = "2025-10-10T20:36:28.73Z" }, - { url = "https://files.pythonhosted.org/packages/0e/37/0fd95850cc52939498b861501bef8d7dc496c2ec6217c1b85a3d2c34940a/astropy-7.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed17a4895edf118e1348560de9deb0596596a41f33314c0d05edce02eb3d1e8", size = 10226235, upload-time = "2025-10-10T20:36:30.915Z" }, - { url = "https://files.pythonhosted.org/packages/d9/df/4539355f72f240de8498494757ba9ddd8562aca83670c39820b917f616dc/astropy-7.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4dcf39226ecad586a11d19207a8a236a3bb0e0412c06574b90ed8799ef202b8", size = 10165501, upload-time = "2025-10-10T20:36:32.588Z" }, - { url = "https://files.pythonhosted.org/packages/1a/46/d881221dcf7f55a3cd945ad55daf994866b4622e796eb27ad59f144568f9/astropy-7.1.1-cp313-cp313-win32.whl", hash = "sha256:33c359bceaa13758be066f878c37d226e899a4faa8cea2711bf68dbbc4a1cb0f", size = 6156145, upload-time = "2025-10-10T20:36:34.299Z" }, - { url = "https://files.pythonhosted.org/packages/45/58/ba5dad5c22a5338ae2954cc81e895add48f8c9b07961a5842cbc2e1f62f2/astropy-7.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:0a3d70df518cb7f400f4ec496b2cbba887c0a0c596fbb15e9bda4fcba07c3f59", size = 6285781, upload-time = "2025-10-10T20:36:35.779Z" }, - { url = "https://files.pythonhosted.org/packages/e9/0e/97a0f1fd764157d3e5aa540a3ae8e9019af4b1278bd1eed69557839fbe61/astropy-7.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1269d03973a14fa8bc16aa7112b1d39953d00ff1ec3ae42677869a7a48395a06", size = 6397841, upload-time = "2025-10-10T20:36:37.721Z" }, - { url = "https://files.pythonhosted.org/packages/2a/ec/c348ebee630e2ea86755ec101e01dca1fa9269e192dc09e24c9b32982d2d/astropy-7.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d9efc15b02b642fd3c4693855b3c8472ac01433a5a441acee97d2e1f586bb4f8", size = 6348050, upload-time = "2025-10-10T20:36:39.56Z" }, - { url = "https://files.pythonhosted.org/packages/9f/65/60bc3e3cc172547e095ebeb82baec4ac55c0e7a2b0553258dea2e5bf74b7/astropy-7.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:43d6894c720c46ffd8ad994edf0b90059fe7a3d71809efef29814c4c3303d710", size = 10154677, upload-time = "2025-10-10T20:36:41.587Z" }, - { url = "https://files.pythonhosted.org/packages/ac/14/ce8df1d39217c85776fb8b053b53fbec8fd027aeb3e7fd52b9fae46e77e7/astropy-7.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:143af6040723919468aa26a0803e0dd57bb340eacebb1cdc5cc56c5a8ace3a42", size = 10179943, upload-time = "2025-10-10T20:36:43.192Z" }, - { url = "https://files.pythonhosted.org/packages/0d/84/7f46819c8ed6590ac8f1cab1954af89cdac44809a51d3e79c85ede0a2191/astropy-7.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3c4a69ced2f114c25ee0430ff21b94f4065b123922f26179b5201533aca2b16e", size = 10127907, upload-time = "2025-10-10T20:36:45.03Z" }, - { url = "https://files.pythonhosted.org/packages/16/01/c5024f607af20d6e452ecf9c526a7d6a18895cddb0d3d97e3ecaeb0af621/astropy-7.1.1-cp314-cp314-win32.whl", hash = "sha256:4fd130522cc35d87c4d602b08b6022ca323b1bb00922d02aa39853f1f1f4f234", size = 6173122, upload-time = "2025-10-10T20:36:46.74Z" }, - { url = "https://files.pythonhosted.org/packages/6d/a7/e35633fadb45b21b651a29ed77e7c9531b782e90bc519494a448a82698d7/astropy-7.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:d56b83a7e6757ba4936b4bc73284a2ac4bdf94d7b428f7b6ce8829e4a48c37e9", size = 6306980, upload-time = "2025-10-10T20:36:48.036Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/7b/92/2dce2d48347efc3346d08ca7995b152d242ebd170c571f7c9346468d8427/astropy-7.2.0.tar.gz", hash = "sha256:ae48bc26b1feaeb603cd94bd1fa1aa39137a115fe931b7f13787ab420e8c3070", size = 7057774, upload-time = "2025-11-25T22:36:41.916Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/6d/6330a844bad8dfc4875e0f2fa1db1fee87837ba9805aa8a8d048c071363a/astropy-7.2.0-cp311-abi3-macosx_10_9_x86_64.whl", hash = "sha256:efac04df4cc488efe630c2fff1992d6516dfb16a06e197fb68bc9e8e3b85def1", size = 6442332, upload-time = "2025-11-25T22:36:23.6Z" }, + { url = "https://files.pythonhosted.org/packages/a6/ba/3418133ba144dfcd1530bca5a6b695f4cdd21a8abaaa2ac4e5450d11b028/astropy-7.2.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:52e9a7d9c86b21f1af911a2930cd0c4a275fb302d455c89e11eedaffef6f2ad0", size = 6413656, upload-time = "2025-11-25T22:36:26.548Z" }, + { url = "https://files.pythonhosted.org/packages/be/ba/05e43b5a7d738316a097fa78524d3eaaff5986294b4a052d4adb3c45e7c0/astropy-7.2.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97c370421b9bb13d4c762c7af06d172bad7c01bd5bcf88314f6913c3c235b770", size = 9758867, upload-time = "2025-11-25T22:36:28.661Z" }, + { url = "https://files.pythonhosted.org/packages/c3/1c/f06ad85180e7dd9855aa5ede901bfc2be858d7bee17d4e978a14c0ecec14/astropy-7.2.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2f39ce2c80211fbceb005d377a5478cd0d66c42aa1498d252f2239fe5a025c24", size = 9789007, upload-time = "2025-11-25T22:36:31.063Z" }, + { url = "https://files.pythonhosted.org/packages/f8/fb/e4d35194a5009d7a73333079481a4ef1380a255d67b9c1db578151a5fb50/astropy-7.2.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ad4d71db994d45f046a1a5449000cf0f88ab6367cb67658500654a0586d6ab19", size = 9748547, upload-time = "2025-11-25T22:36:33.154Z" }, + { url = "https://files.pythonhosted.org/packages/36/ea/f990730978ae0a7a34705f885d2f3806928c5f0bc22eefd6a1a23539cc32/astropy-7.2.0-cp311-abi3-win32.whl", hash = "sha256:95161f26602433176483e8bde8ab1a8ca09148f5b4bf5190569a26d381091598", size = 6237228, upload-time = "2025-11-25T22:36:35.236Z" }, + { url = "https://files.pythonhosted.org/packages/ec/bc/f4378f586dd63902c37d16f68f35f7d555b3b32e08ac6b1d633eb0a48805/astropy-7.2.0-cp311-abi3-win_amd64.whl", hash = "sha256:dc7c340ba1713e55c93071b32033f3153470a0f663a4d539c03a7c9b44020790", size = 6362868, upload-time = "2025-11-25T22:36:37.784Z" }, + { url = "https://files.pythonhosted.org/packages/77/79/b6d4bf01913cfd4ce0cd4c1be5916beccdb92b2970bab8c827984231eae6/astropy-7.2.0-cp311-abi3-win_arm64.whl", hash = "sha256:0c428735a3f15b05c2095bc6ccb5f98a64bc99fb7015866af19ff8492420ddaf", size = 6221756, upload-time = "2025-11-25T22:36:39.852Z" }, ] [[package]] name = "astropy-iers-data" -version = "0.2025.10.20.0.39.8" +version = "0.2026.5.4.1.4.54" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/1c/1eaeac4da97849c5486211384bfb688dff640d29ea1d2c63db32bf43fe54/astropy_iers_data-0.2026.5.4.1.4.54.tar.gz", hash = "sha256:74b70c810e430c274ff9f0c91680d56f7703add8c367c842902dd53130f70832", size = 1932473, upload-time = "2026-05-04T01:05:39.141Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/f8/077fbdeb102d298dd4e6b9ad024881156c4bbd96e60eeeb13e7d7487448f/astropy_iers_data-0.2026.5.4.1.4.54-py3-none-any.whl", hash = "sha256:9d9014f530020fec2da8de29d5ada776b95cf3616cfac41311853f9f753d1928", size = 1989192, upload-time = "2026-05-04T01:05:36.699Z" }, +] + +[[package]] +name = "beautifulsoup4" +version = "4.14.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b1/b4/e7d6623a9f1f43956b56dcca9c03f99e5c4b54d22a7bb9576c253a4b077c/astropy_iers_data-0.2025.10.20.0.39.8.tar.gz", hash = "sha256:ba292db8d5cff8d7a1b16793dc23c2016d7f6c4355c8002925e454d9af2c1938", size = 1911902, upload-time = "2025-10-20T00:39:59.948Z" } +dependencies = [ + { name = "soupsieve" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/b0/1c6a16426d389813b48d95e26898aff79abbde42ad353958ad95cc8c9b21/beautifulsoup4-4.14.3.tar.gz", hash = "sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86", size = 627737, upload-time = "2025-11-30T15:08:26.084Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/a1/62e43ca7b686f8f1060f30b258a4feed8b2da824b68dff3d90a3ecdd8204/astropy_iers_data-0.2025.10.20.0.39.8-py3-none-any.whl", hash = "sha256:13d8b30b4a65a7a6b60497f5ec263be3b5123321ca8a0333807d5487f7938c3f", size = 1967859, upload-time = "2025-10-20T00:39:58.295Z" }, + { url = "https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", size = 107721, upload-time = "2025-11-30T15:08:24.087Z" }, ] [[package]] name = "certifi" -version = "2025.10.5" +version = "2026.4.22" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4c/5b/b6ce21586237c77ce67d01dc5507039d444b630dd76611bbca2d8e5dcd91/certifi-2025.10.5.tar.gz", hash = "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43", size = 164519, upload-time = "2025-10-05T04:12:15.808Z" } +sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de", size = 163286, upload-time = "2025-10-05T04:12:14.03Z" }, + { url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" }, ] [[package]] @@ -168,59 +183,75 @@ wheels = [ [[package]] name = "charset-normalizer" -version = "3.4.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, - { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, - { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, - { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, - { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, - { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, - { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, - { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, - { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, - { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, - { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, - { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, - { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, - { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, - { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, - { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, - { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, - { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, - { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, - { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, - { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, - { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, - { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, - { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, - { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, - { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, - { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, - { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, - { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, - { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, - { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, - { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, - { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, - { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, - { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, - { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, - { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, - { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, - { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, - { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, - { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, - { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, - { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, - { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, - { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, - { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, - { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, - { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, - { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +version = "3.4.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" }, + { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" }, + { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" }, + { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" }, + { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" }, + { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" }, + { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" }, + { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" }, + { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" }, + { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, + { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, + { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, + { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, + { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, + { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, + { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, + { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, + { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" }, + { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" }, + { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, + { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, + { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, + { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, + { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, + { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" }, + { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" }, + { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, + { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, + { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, + { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, + { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, + { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, + { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, + { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, + { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, + { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, ] [[package]] @@ -234,46 +265,55 @@ wheels = [ [[package]] name = "cryptography" -version = "46.0.3" +version = "48.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9f/33/c00162f49c0e2fe8064a62cb92b93e50c74a72bc370ab92f86112b33ff62/cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1", size = 749258, upload-time = "2025-10-15T23:18:31.74Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1c/67/38769ca6b65f07461eb200e85fc1639b438bdc667be02cf7f2cd6a64601c/cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc", size = 4296667, upload-time = "2025-10-15T23:16:54.369Z" }, - { url = "https://files.pythonhosted.org/packages/5c/49/498c86566a1d80e978b42f0d702795f69887005548c041636df6ae1ca64c/cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d", size = 4450807, upload-time = "2025-10-15T23:16:56.414Z" }, - { url = "https://files.pythonhosted.org/packages/4b/0a/863a3604112174c8624a2ac3c038662d9e59970c7f926acdcfaed8d61142/cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb", size = 4299615, upload-time = "2025-10-15T23:16:58.442Z" }, - { url = "https://files.pythonhosted.org/packages/64/02/b73a533f6b64a69f3cd3872acb6ebc12aef924d8d103133bb3ea750dc703/cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849", size = 4016800, upload-time = "2025-10-15T23:17:00.378Z" }, - { url = "https://files.pythonhosted.org/packages/25/d5/16e41afbfa450cde85a3b7ec599bebefaef16b5c6ba4ec49a3532336ed72/cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8", size = 4984707, upload-time = "2025-10-15T23:17:01.98Z" }, - { url = "https://files.pythonhosted.org/packages/c9/56/e7e69b427c3878352c2fb9b450bd0e19ed552753491d39d7d0a2f5226d41/cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec", size = 4482541, upload-time = "2025-10-15T23:17:04.078Z" }, - { url = "https://files.pythonhosted.org/packages/78/f6/50736d40d97e8483172f1bb6e698895b92a223dba513b0ca6f06b2365339/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91", size = 4299464, upload-time = "2025-10-15T23:17:05.483Z" }, - { url = "https://files.pythonhosted.org/packages/00/de/d8e26b1a855f19d9994a19c702fa2e93b0456beccbcfe437eda00e0701f2/cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e", size = 4950838, upload-time = "2025-10-15T23:17:07.425Z" }, - { url = "https://files.pythonhosted.org/packages/8f/29/798fc4ec461a1c9e9f735f2fc58741b0daae30688f41b2497dcbc9ed1355/cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926", size = 4481596, upload-time = "2025-10-15T23:17:09.343Z" }, - { url = "https://files.pythonhosted.org/packages/15/8d/03cd48b20a573adfff7652b76271078e3045b9f49387920e7f1f631d125e/cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71", size = 4426782, upload-time = "2025-10-15T23:17:11.22Z" }, - { url = "https://files.pythonhosted.org/packages/fa/b1/ebacbfe53317d55cf33165bda24c86523497a6881f339f9aae5c2e13e57b/cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac", size = 4698381, upload-time = "2025-10-15T23:17:12.829Z" }, - { url = "https://files.pythonhosted.org/packages/73/dc/9aa866fbdbb95b02e7f9d086f1fccfeebf8953509b87e3f28fff927ff8a0/cryptography-46.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c8daeb2d2174beb4575b77482320303f3d39b8e81153da4f0fb08eb5fe86a6c5", size = 4288728, upload-time = "2025-10-15T23:17:21.527Z" }, - { url = "https://files.pythonhosted.org/packages/c5/fd/bc1daf8230eaa075184cbbf5f8cd00ba9db4fd32d63fb83da4671b72ed8a/cryptography-46.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:39b6755623145ad5eff1dab323f4eae2a32a77a7abef2c5089a04a3d04366715", size = 4435078, upload-time = "2025-10-15T23:17:23.042Z" }, - { url = "https://files.pythonhosted.org/packages/82/98/d3bd5407ce4c60017f8ff9e63ffee4200ab3e23fe05b765cab805a7db008/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:db391fa7c66df6762ee3f00c95a89e6d428f4d60e7abc8328f4fe155b5ac6e54", size = 4293460, upload-time = "2025-10-15T23:17:24.885Z" }, - { url = "https://files.pythonhosted.org/packages/26/e9/e23e7900983c2b8af7a08098db406cf989d7f09caea7897e347598d4cd5b/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:78a97cf6a8839a48c49271cdcbd5cf37ca2c1d6b7fdd86cc864f302b5e9bf459", size = 3995237, upload-time = "2025-10-15T23:17:26.449Z" }, - { url = "https://files.pythonhosted.org/packages/91/15/af68c509d4a138cfe299d0d7ddb14afba15233223ebd933b4bbdbc7155d3/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:dfb781ff7eaa91a6f7fd41776ec37c5853c795d3b358d4896fdbb5df168af422", size = 4967344, upload-time = "2025-10-15T23:17:28.06Z" }, - { url = "https://files.pythonhosted.org/packages/ca/e3/8643d077c53868b681af077edf6b3cb58288b5423610f21c62aadcbe99f4/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6f61efb26e76c45c4a227835ddeae96d83624fb0d29eb5df5b96e14ed1a0afb7", size = 4466564, upload-time = "2025-10-15T23:17:29.665Z" }, - { url = "https://files.pythonhosted.org/packages/0e/43/c1e8726fa59c236ff477ff2b5dc071e54b21e5a1e51aa2cee1676f1c986f/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:23b1a8f26e43f47ceb6d6a43115f33a5a37d57df4ea0ca295b780ae8546e8044", size = 4292415, upload-time = "2025-10-15T23:17:31.686Z" }, - { url = "https://files.pythonhosted.org/packages/42/f9/2f8fefdb1aee8a8e3256a0568cffc4e6d517b256a2fe97a029b3f1b9fe7e/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b419ae593c86b87014b9be7396b385491ad7f320bde96826d0dd174459e54665", size = 4931457, upload-time = "2025-10-15T23:17:33.478Z" }, - { url = "https://files.pythonhosted.org/packages/79/30/9b54127a9a778ccd6d27c3da7563e9f2d341826075ceab89ae3b41bf5be2/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:50fc3343ac490c6b08c0cf0d704e881d0d660be923fd3076db3e932007e726e3", size = 4466074, upload-time = "2025-10-15T23:17:35.158Z" }, - { url = "https://files.pythonhosted.org/packages/ac/68/b4f4a10928e26c941b1b6a179143af9f4d27d88fe84a6a3c53592d2e76bf/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22d7e97932f511d6b0b04f2bfd818d73dcd5928db509460aaf48384778eb6d20", size = 4420569, upload-time = "2025-10-15T23:17:37.188Z" }, - { url = "https://files.pythonhosted.org/packages/a3/49/3746dab4c0d1979888f125226357d3262a6dd40e114ac29e3d2abdf1ec55/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d55f3dffadd674514ad19451161118fd010988540cee43d8bc20675e775925de", size = 4681941, upload-time = "2025-10-15T23:17:39.236Z" }, - { url = "https://files.pythonhosted.org/packages/27/32/b68d27471372737054cbd34c84981f9edbc24fe67ca225d389799614e27f/cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683", size = 4294089, upload-time = "2025-10-15T23:17:48.269Z" }, - { url = "https://files.pythonhosted.org/packages/26/42/fa8389d4478368743e24e61eea78846a0006caffaf72ea24a15159215a14/cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d", size = 4440029, upload-time = "2025-10-15T23:17:49.837Z" }, - { url = "https://files.pythonhosted.org/packages/5f/eb/f483db0ec5ac040824f269e93dd2bd8a21ecd1027e77ad7bdf6914f2fd80/cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0", size = 4297222, upload-time = "2025-10-15T23:17:51.357Z" }, - { url = "https://files.pythonhosted.org/packages/fd/cf/da9502c4e1912cb1da3807ea3618a6829bee8207456fbbeebc361ec38ba3/cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc", size = 4012280, upload-time = "2025-10-15T23:17:52.964Z" }, - { url = "https://files.pythonhosted.org/packages/6b/8f/9adb86b93330e0df8b3dcf03eae67c33ba89958fc2e03862ef1ac2b42465/cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3", size = 4978958, upload-time = "2025-10-15T23:17:54.965Z" }, - { url = "https://files.pythonhosted.org/packages/d1/a0/5fa77988289c34bdb9f913f5606ecc9ada1adb5ae870bd0d1054a7021cc4/cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971", size = 4473714, upload-time = "2025-10-15T23:17:56.754Z" }, - { url = "https://files.pythonhosted.org/packages/14/e5/fc82d72a58d41c393697aa18c9abe5ae1214ff6f2a5c18ac470f92777895/cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac", size = 4296970, upload-time = "2025-10-15T23:17:58.588Z" }, - { url = "https://files.pythonhosted.org/packages/78/06/5663ed35438d0b09056973994f1aec467492b33bd31da36e468b01ec1097/cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04", size = 4940236, upload-time = "2025-10-15T23:18:00.897Z" }, - { url = "https://files.pythonhosted.org/packages/fc/59/873633f3f2dcd8a053b8dd1d38f783043b5fce589c0f6988bf55ef57e43e/cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506", size = 4472642, upload-time = "2025-10-15T23:18:02.749Z" }, - { url = "https://files.pythonhosted.org/packages/3d/39/8e71f3930e40f6877737d6f69248cf74d4e34b886a3967d32f919cc50d3b/cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963", size = 4423126, upload-time = "2025-10-15T23:18:04.85Z" }, - { url = "https://files.pythonhosted.org/packages/cd/c7/f65027c2810e14c3e7268353b1681932b87e5a48e65505d8cc17c99e36ae/cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4", size = 4686573, upload-time = "2025-10-15T23:18:06.908Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/9f/a9/db8f313fdcd85d767d4973515e1db101f9c71f95fced83233de224673757/cryptography-48.0.0.tar.gz", hash = "sha256:5c3932f4436d1cccb036cb0eaef46e6e2db91035166f1ad6505c3c9d5a635920", size = 832984, upload-time = "2026-05-04T22:59:38.133Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/6e/e90527eef33f309beb811cf7c982c3aeffcce8e3edb178baa4ca3ae4a6fa/cryptography-48.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f5333311663ea94f75dd408665686aaf426563556bb5283554a3539177e03b8c", size = 4690433, upload-time = "2026-05-04T22:57:40.373Z" }, + { url = "https://files.pythonhosted.org/packages/90/04/673510ed51ddff56575f306cf1617d80411ee76831ccd3097599140efdfe/cryptography-48.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7995ef305d7165c3f11ae07f2517e5a4f1d5c18da1376a0a9ed496336b69e5f3", size = 4710620, upload-time = "2026-05-04T22:57:42.935Z" }, + { url = "https://files.pythonhosted.org/packages/14/d5/e9c4ef932c8d800490c34d8bd589d64a31d5890e27ec9e9ad532be893294/cryptography-48.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:40ba1f85eaa6959837b1d51c9767e230e14612eea4ef110ee8854ada22da1bf5", size = 4696283, upload-time = "2026-05-04T22:57:45.294Z" }, + { url = "https://files.pythonhosted.org/packages/0c/29/174b9dfb60b12d59ecfc6cfa04bc88c21b42a54f01b8aae09bb6e51e4c7f/cryptography-48.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:369a6348999f94bbd53435c894377b20ab95f25a9065c283570e70150d8abc3c", size = 5296573, upload-time = "2026-05-04T22:57:47.933Z" }, + { url = "https://files.pythonhosted.org/packages/95/38/0d29a6fd7d0d1373f0c0c88a04ba20e359b257753ac497564cd660fc1d55/cryptography-48.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a0e692c683f4df67815a2d258b324e66f4738bd7a96a218c826dce4f4bd05d8f", size = 4743677, upload-time = "2026-05-04T22:57:50.067Z" }, + { url = "https://files.pythonhosted.org/packages/30/be/eef653013d5c63b6a490529e0316f9ac14a37602965d4903efed1399f32b/cryptography-48.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:18349bbc56f4743c8b12dc32e2bccb2cf83ee8b69a3bba74ef8ae857e26b3d25", size = 4330808, upload-time = "2026-05-04T22:57:52.301Z" }, + { url = "https://files.pythonhosted.org/packages/84/9e/500463e87abb7a0a0f9f256ec21123ecde0a7b5541a15e840ea54551fd81/cryptography-48.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e8eac43dfca5c4cccc6dad9a80504436fca53bb9bc3100a2386d730fbe6b602", size = 4695941, upload-time = "2026-05-04T22:57:54.603Z" }, + { url = "https://files.pythonhosted.org/packages/e3/dc/7303087450c2ec9e7fbb750e17c2abfbc658f23cbd0e54009509b7cc4091/cryptography-48.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9ccdac7d40688ecb5a3b4a604b8a88c8002e3442d6c60aead1db2a89a041560c", size = 5252579, upload-time = "2026-05-04T22:57:57.207Z" }, + { url = "https://files.pythonhosted.org/packages/d0/c0/7101d3b7215edcdc90c45da544961fd8ed2d6448f77577460fa75a8443f7/cryptography-48.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:bd72e68b06bb1e96913f97dd4901119bc17f39d4586a5adf2d3e47bc2b9d58b5", size = 4743326, upload-time = "2026-05-04T22:57:59.535Z" }, + { url = "https://files.pythonhosted.org/packages/ac/d8/5b833bad13016f562ab9d063d68199a4bd121d18458e439515601d3357ec/cryptography-48.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:59baa2cb386c4f0b9905bd6eb4c2a79a69a128408fd31d32ca4d7102d4156321", size = 4826672, upload-time = "2026-05-04T22:58:01.996Z" }, + { url = "https://files.pythonhosted.org/packages/98/e1/7074eb8bf3c135558c73fc2bcf0f5633f912e6fb87e868a55c454080ef09/cryptography-48.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9249e3cd978541d665967ac2cb2787fd6a62bddf1e75b3e347a594d7dacf4f74", size = 4972574, upload-time = "2026-05-04T22:58:03.968Z" }, + { url = "https://files.pythonhosted.org/packages/89/6e/18e07a618bb5442ba10cf4df16e99c071365528aa570dfcb8c02e25a303b/cryptography-48.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c7378637d7d88016fa6791c159f698b3d3eed28ebf844ac36b9dc04a14dae18", size = 4684776, upload-time = "2026-05-04T22:58:13.712Z" }, + { url = "https://files.pythonhosted.org/packages/be/6a/4ea3b4c6c6759794d5ee2103c304a5076dc4b19ae1f9fe47dba439e159e9/cryptography-48.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc90c0b39b2e3c65ef52c804b72e3c58f8a04ab2a1871272798e5f9572c17d20", size = 4698121, upload-time = "2026-05-04T22:58:16.448Z" }, + { url = "https://files.pythonhosted.org/packages/2f/59/6ff6ad6cae03bb887da2a5860b2c9805f8dac969ef01ce563336c49bd1d1/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:76341972e1eff8b4bea859f09c0d3e64b96ce931b084f9b9b7db8ef364c30eff", size = 4690042, upload-time = "2026-05-04T22:58:18.544Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b4/fc334ed8cfd705aca282fe4d8f5ae64a8e0f74932e9feecb344610cf6e4d/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:55b7718303bf06a5753dcdccf2f3945cf18ad7bffde41b61226e4db31ab89a9c", size = 5282526, upload-time = "2026-05-04T22:58:20.75Z" }, + { url = "https://files.pythonhosted.org/packages/11/08/9f8c5386cc4cd90d8255c7cdd0f5baf459a08502a09de30dc51f553d38dc/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:a64697c641c7b1b2178e573cbc31c7c6684cd56883a478d75143dbb7118036db", size = 4733116, upload-time = "2026-05-04T22:58:23.627Z" }, + { url = "https://files.pythonhosted.org/packages/b8/77/99307d7574045699f8805aa500fa0fb83422d115b5400a064ddd306d7750/cryptography-48.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:561215ea3879cb1cbbf272867e2efda62476f240fb58c64de6b393ae19246741", size = 4316030, upload-time = "2026-05-04T22:58:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/fd/36/a608b98337af3cb2aff4818e406649d30572b7031918b04c87d979495348/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ad64688338ed4bc1a6618076ba75fd7194a5f1797ac60b47afe926285adb3166", size = 4689640, upload-time = "2026-05-04T22:58:27.747Z" }, + { url = "https://files.pythonhosted.org/packages/dd/a6/825010a291b4438aecc1f568bc428189fc1175515223632477c07dc0a6df/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:906cbf0670286c6e0044156bc7d4af9cbb0ef6db9f73e52c3ec56ba6bdde5336", size = 5237657, upload-time = "2026-05-04T22:58:29.848Z" }, + { url = "https://files.pythonhosted.org/packages/b9/09/4e76a09b4caa29aad535ddc806f5d4c5d01885bd978bd984fbc6ca032cae/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:ea8990436d914540a40ab24b6a77c0969695ed52f4a4874c5137ccf7045a7057", size = 4732362, upload-time = "2026-05-04T22:58:32.009Z" }, + { url = "https://files.pythonhosted.org/packages/18/78/444fa04a77d0cb95f417dda20d450e13c56ba8e5220fc892a1658f44f882/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c18684a7f0cc9a3cb60328f496b8e3372def7c5d2df39ac267878b05565aaaae", size = 4819580, upload-time = "2026-05-04T22:58:34.254Z" }, + { url = "https://files.pythonhosted.org/packages/38/85/ea67067c70a1fd4be2c63d35eeed82658023021affccc7b17705f8527dd2/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9be5aafa5736574f8f15f262adc81b2a9869e2cfe9014d52a44633905b40d52c", size = 4963283, upload-time = "2026-05-04T22:58:36.376Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ac/f5b5995b87770c693e2596559ffafe195b4033a57f14a82268a2842953f3/cryptography-48.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:614d0949f4790582d2cc25553abd09dd723025f0c0e7c67376a1d77196743d6e", size = 4683266, upload-time = "2026-05-04T22:58:46.064Z" }, + { url = "https://files.pythonhosted.org/packages/ec/c6/8b14f67e18338fbc4adb76f66c001f5c3610b3e2d1837f268f47a347dbbb/cryptography-48.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ce4bfae76319a532a2dc68f82cc32f5676ee792a983187dac07183690e5c66f", size = 4696228, upload-time = "2026-05-04T22:58:48.22Z" }, + { url = "https://files.pythonhosted.org/packages/ea/73/f808fbae9514bd91b47875b003f13e284c8c6bdfd904b7944e803937eec1/cryptography-48.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:2eb992bbd4661238c5a397594c83f5b4dc2bc5b848c365c8f991b6780efcc5c7", size = 4689097, upload-time = "2026-05-04T22:58:50.9Z" }, + { url = "https://files.pythonhosted.org/packages/93/01/d86632d7d28db8ae83221995752eeb6639ffb374c2d22955648cf8d52797/cryptography-48.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:22a5cb272895dce158b2cacdfdc3debd299019659f42947dbdac6f32d68fe832", size = 5283582, upload-time = "2026-05-04T22:58:53.017Z" }, + { url = "https://files.pythonhosted.org/packages/02/e1/50edc7a50334807cc4791fc4a0ce7468b4a1416d9138eab358bfc9a3d70b/cryptography-48.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2b4d59804e8408e2fea7d1fbaf218e5ec984325221db76e6a241a9abd6cdd95c", size = 4730479, upload-time = "2026-05-04T22:58:55.611Z" }, + { url = "https://files.pythonhosted.org/packages/6f/af/99a582b1b1641ff5911ac559beb45097cf79efd4ead4657f578ef1af2d47/cryptography-48.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:984a20b0f62a26f48a3396c72e4bc34c66e356d356bf370053066b3b6d54634a", size = 4326481, upload-time = "2026-05-04T22:58:57.607Z" }, + { url = "https://files.pythonhosted.org/packages/90/ee/89aa26a06ef0a7d7611788ffd571a7c50e368cc6a4d5eef8b4884e866edb/cryptography-48.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5a5ed8fde7a1d09376ca0b40e68cd59c69fe23b1f9768bd5824f54681626032a", size = 4688713, upload-time = "2026-05-04T22:59:00.077Z" }, + { url = "https://files.pythonhosted.org/packages/70/ba/bcb1b0bb7a33d4c7c0c4d4c7874b4a62ae4f56113a5f4baefa362dfb1f0f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:8cd666227ef7af430aa5914a9910e0ddd703e75f039cef0825cd0da71b6b711a", size = 5238165, upload-time = "2026-05-04T22:59:02.317Z" }, + { url = "https://files.pythonhosted.org/packages/c9/70/ca4003b1ce5ca3dc3186ada51908c8a9b9ff7d5cab83cc0d43ee14ec144f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9071196d81abc88b3516ac8cdfad32e2b66dd4a5393a8e68a961e9161ddc6239", size = 4729947, upload-time = "2026-05-04T22:59:05.255Z" }, + { url = "https://files.pythonhosted.org/packages/44/a0/4ec7cf774207905aef1a8d11c3750d5a1db805eb380ee4e16df317870128/cryptography-48.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e2d54c8be6152856a36f0882ab231e70f8ec7f14e93cf87db8a2ed056bf160c", size = 4822059, upload-time = "2026-05-04T22:59:07.802Z" }, + { url = "https://files.pythonhosted.org/packages/1e/75/a2e55f99c16fcac7b5d6c1eb19ad8e00799854d6be5ca845f9259eae1681/cryptography-48.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a5da777e32ffed6f85a7b2b3f7c5cbc88c146bfcd0a1d7baf5fcc6c52ee35dd4", size = 4960575, upload-time = "2026-05-04T22:59:09.851Z" }, +] + +[[package]] +name = "defusedxml" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" }, ] [[package]] @@ -315,11 +355,11 @@ wheels = [ [[package]] name = "idna" -version = "3.11" +version = "3.13" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/cc/762dfb036166873f0059f3b7de4565e1b5bc3d6f28a414c13da27e442f99/idna-3.13.tar.gz", hash = "sha256:585ea8fe5d69b9181ec1afba340451fba6ba764af97026f92a91d4eef164a242", size = 194210, upload-time = "2026-04-22T16:42:42.314Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, + { url = "https://files.pythonhosted.org/packages/5d/13/ad7d7ca3808a898b4612b6fe93cde56b53f3034dcde235acb1f0e1df24c6/idna-3.13-py3-none-any.whl", hash = "sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3", size = 68629, upload-time = "2026-04-22T16:42:40.909Z" }, ] [[package]] @@ -345,23 +385,23 @@ wheels = [ [[package]] name = "jaraco-context" -version = "6.0.1" +version = "6.1.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/df/ad/f3777b81bf0b6e7bc7514a1656d3e637b2e8e15fab2ce3235730b3e7a4e6/jaraco_context-6.0.1.tar.gz", hash = "sha256:9bae4ea555cf0b14938dc0aee7c9f32ed303aa20a3b73e7dc80111628792d1b3", size = 13912, upload-time = "2024-08-20T03:39:27.358Z" } +sdist = { url = "https://files.pythonhosted.org/packages/af/50/4763cd07e722bb6285316d390a164bc7e479db9d90daa769f22578f698b4/jaraco_context-6.1.2.tar.gz", hash = "sha256:f1a6c9d391e661cc5b8d39861ff077a7dc24dc23833ccee564b234b81c82dfe3", size = 16801, upload-time = "2026-03-20T22:13:33.922Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ff/db/0c52c4cf5e4bd9f5d7135ec7669a3a767af21b3a308e1ed3674881e52b62/jaraco.context-6.0.1-py3-none-any.whl", hash = "sha256:f797fc481b490edb305122c9181830a3a5b76d84ef6d1aef2fb9b47ab956f9e4", size = 6825, upload-time = "2024-08-20T03:39:25.966Z" }, + { url = "https://files.pythonhosted.org/packages/f2/58/bc8954bda5fcda97bd7c19be11b85f91973d67a706ed4a3aec33e7de22db/jaraco_context-6.1.2-py3-none-any.whl", hash = "sha256:bf8150b79a2d5d91ae48629d8b427a8f7ba0e1097dd6202a9059f29a36379535", size = 7871, upload-time = "2026-03-20T22:13:32.808Z" }, ] [[package]] name = "jaraco-functools" -version = "4.3.0" +version = "4.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "more-itertools" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f7/ed/1aa2d585304ec07262e1a83a9889880701079dde796ac7b1d1826f40c63d/jaraco_functools-4.3.0.tar.gz", hash = "sha256:cfd13ad0dd2c47a3600b439ef72d8615d482cedcff1632930d6f28924d92f294", size = 19755, upload-time = "2025-08-18T20:05:09.91Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/27/056e0638a86749374d6f57d0b0db39f29509cce9313cf91bdc0ac4d91084/jaraco_functools-4.4.0.tar.gz", hash = "sha256:da21933b0417b89515562656547a77b4931f98176eb173644c0d35032a33d6bb", size = 19943, upload-time = "2025-12-21T09:29:43.6Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b4/09/726f168acad366b11e420df31bf1c702a54d373a83f968d94141a8c3fde0/jaraco_functools-4.3.0-py3-none-any.whl", hash = "sha256:227ff8ed6f7b8f62c56deff101545fa7543cf2c8e7b82a7c2116e672f29c26e8", size = 10408, upload-time = "2025-08-18T20:05:08.69Z" }, + { url = "https://files.pythonhosted.org/packages/fd/c4/813bb09f0985cb21e959f21f2464169eca882656849adf727ac7bb7e1767/jaraco_functools-4.4.0-py3-none-any.whl", hash = "sha256:9eec1e36f45c818d9bf307c8948eb03b2b56cd44087b3cdc989abca1f20b9176", size = 10481, upload-time = "2025-12-21T09:29:42.27Z" }, ] [[package]] @@ -387,7 +427,7 @@ wheels = [ [[package]] name = "keyring" -version = "25.6.0" +version = "25.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jaraco-classes" }, @@ -397,89 +437,89 @@ dependencies = [ { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" }, { name = "secretstorage", marker = "sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/70/09/d904a6e96f76ff214be59e7aa6ef7190008f52a0ab6689760a98de0bf37d/keyring-25.6.0.tar.gz", hash = "sha256:0b39998aa941431eb3d9b0d4b2460bc773b9df6fed7621c2dfb291a7e0187a66", size = 62750, upload-time = "2024-12-25T15:26:45.782Z" } +sdist = { url = "https://files.pythonhosted.org/packages/43/4b/674af6ef2f97d56f0ab5153bf0bfa28ccb6c3ed4d1babf4305449668807b/keyring-25.7.0.tar.gz", hash = "sha256:fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b", size = 63516, upload-time = "2025-11-16T16:26:09.482Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d3/32/da7f44bcb1105d3e88a0b74ebdca50c59121d2ddf71c9e34ba47df7f3a56/keyring-25.6.0-py3-none-any.whl", hash = "sha256:552a3f7af126ece7ed5c89753650eec89c7eaae8617d0aa4d9ad2b75111266bd", size = 39085, upload-time = "2024-12-25T15:26:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl", hash = "sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f", size = 39160, upload-time = "2025-11-16T16:26:08.402Z" }, ] [[package]] name = "lxml" -version = "6.0.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/aa/88/262177de60548e5a2bfc46ad28232c9e9cbde697bd94132aeb80364675cb/lxml-6.0.2.tar.gz", hash = "sha256:cd79f3367bd74b317dda655dc8fcfa304d9eb6e4fb06b7168c5cf27f96e0cd62", size = 4073426, upload-time = "2025-09-22T04:04:59.287Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f3/c8/8ff2bc6b920c84355146cd1ab7d181bc543b89241cfb1ebee824a7c81457/lxml-6.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a59f5448ba2ceccd06995c95ea59a7674a10de0810f2ce90c9006f3cbc044456", size = 8661887, upload-time = "2025-09-22T04:01:17.265Z" }, - { url = "https://files.pythonhosted.org/packages/37/6f/9aae1008083bb501ef63284220ce81638332f9ccbfa53765b2b7502203cf/lxml-6.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e8113639f3296706fbac34a30813929e29247718e88173ad849f57ca59754924", size = 4667818, upload-time = "2025-09-22T04:01:19.688Z" }, - { url = "https://files.pythonhosted.org/packages/f1/ca/31fb37f99f37f1536c133476674c10b577e409c0a624384147653e38baf2/lxml-6.0.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a8bef9b9825fa8bc816a6e641bb67219489229ebc648be422af695f6e7a4fa7f", size = 4950807, upload-time = "2025-09-22T04:01:21.487Z" }, - { url = "https://files.pythonhosted.org/packages/da/87/f6cb9442e4bada8aab5ae7e1046264f62fdbeaa6e3f6211b93f4c0dd97f1/lxml-6.0.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:65ea18d710fd14e0186c2f973dc60bb52039a275f82d3c44a0e42b43440ea534", size = 5109179, upload-time = "2025-09-22T04:01:23.32Z" }, - { url = "https://files.pythonhosted.org/packages/c8/20/a7760713e65888db79bbae4f6146a6ae5c04e4a204a3c48896c408cd6ed2/lxml-6.0.2-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c371aa98126a0d4c739ca93ceffa0fd7a5d732e3ac66a46e74339acd4d334564", size = 5023044, upload-time = "2025-09-22T04:01:25.118Z" }, - { url = "https://files.pythonhosted.org/packages/a2/b0/7e64e0460fcb36471899f75831509098f3fd7cd02a3833ac517433cb4f8f/lxml-6.0.2-cp312-cp312-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:700efd30c0fa1a3581d80a748157397559396090a51d306ea59a70020223d16f", size = 5359685, upload-time = "2025-09-22T04:01:27.398Z" }, - { url = "https://files.pythonhosted.org/packages/b9/e1/e5df362e9ca4e2f48ed6411bd4b3a0ae737cc842e96877f5bf9428055ab4/lxml-6.0.2-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c33e66d44fe60e72397b487ee92e01da0d09ba2d66df8eae42d77b6d06e5eba0", size = 5654127, upload-time = "2025-09-22T04:01:29.629Z" }, - { url = "https://files.pythonhosted.org/packages/c6/d1/232b3309a02d60f11e71857778bfcd4acbdb86c07db8260caf7d008b08f8/lxml-6.0.2-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90a345bbeaf9d0587a3aaffb7006aa39ccb6ff0e96a57286c0cb2fd1520ea192", size = 5253958, upload-time = "2025-09-22T04:01:31.535Z" }, - { url = "https://files.pythonhosted.org/packages/35/35/d955a070994725c4f7d80583a96cab9c107c57a125b20bb5f708fe941011/lxml-6.0.2-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:064fdadaf7a21af3ed1dcaa106b854077fbeada827c18f72aec9346847cd65d0", size = 4711541, upload-time = "2025-09-22T04:01:33.801Z" }, - { url = "https://files.pythonhosted.org/packages/1e/be/667d17363b38a78c4bd63cfd4b4632029fd68d2c2dc81f25ce9eb5224dd5/lxml-6.0.2-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fbc74f42c3525ac4ffa4b89cbdd00057b6196bcefe8bce794abd42d33a018092", size = 5267426, upload-time = "2025-09-22T04:01:35.639Z" }, - { url = "https://files.pythonhosted.org/packages/ea/47/62c70aa4a1c26569bc958c9ca86af2bb4e1f614e8c04fb2989833874f7ae/lxml-6.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6ddff43f702905a4e32bc24f3f2e2edfe0f8fde3277d481bffb709a4cced7a1f", size = 5064917, upload-time = "2025-09-22T04:01:37.448Z" }, - { url = "https://files.pythonhosted.org/packages/bd/55/6ceddaca353ebd0f1908ef712c597f8570cc9c58130dbb89903198e441fd/lxml-6.0.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6da5185951d72e6f5352166e3da7b0dc27aa70bd1090b0eb3f7f7212b53f1bb8", size = 4788795, upload-time = "2025-09-22T04:01:39.165Z" }, - { url = "https://files.pythonhosted.org/packages/cf/e8/fd63e15da5e3fd4c2146f8bbb3c14e94ab850589beab88e547b2dbce22e1/lxml-6.0.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:57a86e1ebb4020a38d295c04fc79603c7899e0df71588043eb218722dabc087f", size = 5676759, upload-time = "2025-09-22T04:01:41.506Z" }, - { url = "https://files.pythonhosted.org/packages/76/47/b3ec58dc5c374697f5ba37412cd2728f427d056315d124dd4b61da381877/lxml-6.0.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2047d8234fe735ab77802ce5f2297e410ff40f5238aec569ad7c8e163d7b19a6", size = 5255666, upload-time = "2025-09-22T04:01:43.363Z" }, - { url = "https://files.pythonhosted.org/packages/19/93/03ba725df4c3d72afd9596eef4a37a837ce8e4806010569bedfcd2cb68fd/lxml-6.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6f91fd2b2ea15a6800c8e24418c0775a1694eefc011392da73bc6cef2623b322", size = 5277989, upload-time = "2025-09-22T04:01:45.215Z" }, - { url = "https://files.pythonhosted.org/packages/c6/80/c06de80bfce881d0ad738576f243911fccf992687ae09fd80b734712b39c/lxml-6.0.2-cp312-cp312-win32.whl", hash = "sha256:3ae2ce7d6fedfb3414a2b6c5e20b249c4c607f72cb8d2bb7cc9c6ec7c6f4e849", size = 3611456, upload-time = "2025-09-22T04:01:48.243Z" }, - { url = "https://files.pythonhosted.org/packages/f7/d7/0cdfb6c3e30893463fb3d1e52bc5f5f99684a03c29a0b6b605cfae879cd5/lxml-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:72c87e5ee4e58a8354fb9c7c84cbf95a1c8236c127a5d1b7683f04bed8361e1f", size = 4011793, upload-time = "2025-09-22T04:01:50.042Z" }, - { url = "https://files.pythonhosted.org/packages/ea/7b/93c73c67db235931527301ed3785f849c78991e2e34f3fd9a6663ffda4c5/lxml-6.0.2-cp312-cp312-win_arm64.whl", hash = "sha256:61cb10eeb95570153e0c0e554f58df92ecf5109f75eacad4a95baa709e26c3d6", size = 3672836, upload-time = "2025-09-22T04:01:52.145Z" }, - { url = "https://files.pythonhosted.org/packages/53/fd/4e8f0540608977aea078bf6d79f128e0e2c2bba8af1acf775c30baa70460/lxml-6.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9b33d21594afab46f37ae58dfadd06636f154923c4e8a4d754b0127554eb2e77", size = 8648494, upload-time = "2025-09-22T04:01:54.242Z" }, - { url = "https://files.pythonhosted.org/packages/5d/f4/2a94a3d3dfd6c6b433501b8d470a1960a20ecce93245cf2db1706adf6c19/lxml-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c8963287d7a4c5c9a432ff487c52e9c5618667179c18a204bdedb27310f022f", size = 4661146, upload-time = "2025-09-22T04:01:56.282Z" }, - { url = "https://files.pythonhosted.org/packages/25/2e/4efa677fa6b322013035d38016f6ae859d06cac67437ca7dc708a6af7028/lxml-6.0.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1941354d92699fb5ffe6ed7b32f9649e43c2feb4b97205f75866f7d21aa91452", size = 4946932, upload-time = "2025-09-22T04:01:58.989Z" }, - { url = "https://files.pythonhosted.org/packages/ce/0f/526e78a6d38d109fdbaa5049c62e1d32fdd70c75fb61c4eadf3045d3d124/lxml-6.0.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb2f6ca0ae2d983ded09357b84af659c954722bbf04dea98030064996d156048", size = 5100060, upload-time = "2025-09-22T04:02:00.812Z" }, - { url = "https://files.pythonhosted.org/packages/81/76/99de58d81fa702cc0ea7edae4f4640416c2062813a00ff24bd70ac1d9c9b/lxml-6.0.2-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb2a12d704f180a902d7fa778c6d71f36ceb7b0d317f34cdc76a5d05aa1dd1df", size = 5019000, upload-time = "2025-09-22T04:02:02.671Z" }, - { url = "https://files.pythonhosted.org/packages/b5/35/9e57d25482bc9a9882cb0037fdb9cc18f4b79d85df94fa9d2a89562f1d25/lxml-6.0.2-cp313-cp313-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:6ec0e3f745021bfed19c456647f0298d60a24c9ff86d9d051f52b509663feeb1", size = 5348496, upload-time = "2025-09-22T04:02:04.904Z" }, - { url = "https://files.pythonhosted.org/packages/a6/8e/cb99bd0b83ccc3e8f0f528e9aa1f7a9965dfec08c617070c5db8d63a87ce/lxml-6.0.2-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:846ae9a12d54e368933b9759052d6206a9e8b250291109c48e350c1f1f49d916", size = 5643779, upload-time = "2025-09-22T04:02:06.689Z" }, - { url = "https://files.pythonhosted.org/packages/d0/34/9e591954939276bb679b73773836c6684c22e56d05980e31d52a9a8deb18/lxml-6.0.2-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef9266d2aa545d7374938fb5c484531ef5a2ec7f2d573e62f8ce722c735685fd", size = 5244072, upload-time = "2025-09-22T04:02:08.587Z" }, - { url = "https://files.pythonhosted.org/packages/8d/27/b29ff065f9aaca443ee377aff699714fcbffb371b4fce5ac4ca759e436d5/lxml-6.0.2-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:4077b7c79f31755df33b795dc12119cb557a0106bfdab0d2c2d97bd3cf3dffa6", size = 4718675, upload-time = "2025-09-22T04:02:10.783Z" }, - { url = "https://files.pythonhosted.org/packages/2b/9f/f756f9c2cd27caa1a6ef8c32ae47aadea697f5c2c6d07b0dae133c244fbe/lxml-6.0.2-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a7c5d5e5f1081955358533be077166ee97ed2571d6a66bdba6ec2f609a715d1a", size = 5255171, upload-time = "2025-09-22T04:02:12.631Z" }, - { url = "https://files.pythonhosted.org/packages/61/46/bb85ea42d2cb1bd8395484fd72f38e3389611aa496ac7772da9205bbda0e/lxml-6.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8f8d0cbd0674ee89863a523e6994ac25fd5be9c8486acfc3e5ccea679bad2679", size = 5057175, upload-time = "2025-09-22T04:02:14.718Z" }, - { url = "https://files.pythonhosted.org/packages/95/0c/443fc476dcc8e41577f0af70458c50fe299a97bb6b7505bb1ae09aa7f9ac/lxml-6.0.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2cbcbf6d6e924c28f04a43f3b6f6e272312a090f269eff68a2982e13e5d57659", size = 4785688, upload-time = "2025-09-22T04:02:16.957Z" }, - { url = "https://files.pythonhosted.org/packages/48/78/6ef0b359d45bb9697bc5a626e1992fa5d27aa3f8004b137b2314793b50a0/lxml-6.0.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:dfb874cfa53340009af6bdd7e54ebc0d21012a60a4e65d927c2e477112e63484", size = 5660655, upload-time = "2025-09-22T04:02:18.815Z" }, - { url = "https://files.pythonhosted.org/packages/ff/ea/e1d33808f386bc1339d08c0dcada6e4712d4ed8e93fcad5f057070b7988a/lxml-6.0.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fb8dae0b6b8b7f9e96c26fdd8121522ce5de9bb5538010870bd538683d30e9a2", size = 5247695, upload-time = "2025-09-22T04:02:20.593Z" }, - { url = "https://files.pythonhosted.org/packages/4f/47/eba75dfd8183673725255247a603b4ad606f4ae657b60c6c145b381697da/lxml-6.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:358d9adae670b63e95bc59747c72f4dc97c9ec58881d4627fe0120da0f90d314", size = 5269841, upload-time = "2025-09-22T04:02:22.489Z" }, - { url = "https://files.pythonhosted.org/packages/76/04/5c5e2b8577bc936e219becb2e98cdb1aca14a4921a12995b9d0c523502ae/lxml-6.0.2-cp313-cp313-win32.whl", hash = "sha256:e8cd2415f372e7e5a789d743d133ae474290a90b9023197fd78f32e2dc6873e2", size = 3610700, upload-time = "2025-09-22T04:02:24.465Z" }, - { url = "https://files.pythonhosted.org/packages/fe/0a/4643ccc6bb8b143e9f9640aa54e38255f9d3b45feb2cbe7ae2ca47e8782e/lxml-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:b30d46379644fbfc3ab81f8f82ae4de55179414651f110a1514f0b1f8f6cb2d7", size = 4010347, upload-time = "2025-09-22T04:02:26.286Z" }, - { url = "https://files.pythonhosted.org/packages/31/ef/dcf1d29c3f530577f61e5fe2f1bd72929acf779953668a8a47a479ae6f26/lxml-6.0.2-cp313-cp313-win_arm64.whl", hash = "sha256:13dcecc9946dca97b11b7c40d29fba63b55ab4170d3c0cf8c0c164343b9bfdcf", size = 3671248, upload-time = "2025-09-22T04:02:27.918Z" }, - { url = "https://files.pythonhosted.org/packages/03/15/d4a377b385ab693ce97b472fe0c77c2b16ec79590e688b3ccc71fba19884/lxml-6.0.2-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:b0c732aa23de8f8aec23f4b580d1e52905ef468afb4abeafd3fec77042abb6fe", size = 8659801, upload-time = "2025-09-22T04:02:30.113Z" }, - { url = "https://files.pythonhosted.org/packages/c8/e8/c128e37589463668794d503afaeb003987373c5f94d667124ffd8078bbd9/lxml-6.0.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4468e3b83e10e0317a89a33d28f7aeba1caa4d1a6fd457d115dd4ffe90c5931d", size = 4659403, upload-time = "2025-09-22T04:02:32.119Z" }, - { url = "https://files.pythonhosted.org/packages/00/ce/74903904339decdf7da7847bb5741fc98a5451b42fc419a86c0c13d26fe2/lxml-6.0.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:abd44571493973bad4598a3be7e1d807ed45aa2adaf7ab92ab7c62609569b17d", size = 4966974, upload-time = "2025-09-22T04:02:34.155Z" }, - { url = "https://files.pythonhosted.org/packages/1f/d3/131dec79ce61c5567fecf82515bd9bc36395df42501b50f7f7f3bd065df0/lxml-6.0.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:370cd78d5855cfbffd57c422851f7d3864e6ae72d0da615fca4dad8c45d375a5", size = 5102953, upload-time = "2025-09-22T04:02:36.054Z" }, - { url = "https://files.pythonhosted.org/packages/3a/ea/a43ba9bb750d4ffdd885f2cd333572f5bb900cd2408b67fdda07e85978a0/lxml-6.0.2-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:901e3b4219fa04ef766885fb40fa516a71662a4c61b80c94d25336b4934b71c0", size = 5055054, upload-time = "2025-09-22T04:02:38.154Z" }, - { url = "https://files.pythonhosted.org/packages/60/23/6885b451636ae286c34628f70a7ed1fcc759f8d9ad382d132e1c8d3d9bfd/lxml-6.0.2-cp314-cp314-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:a4bf42d2e4cf52c28cc1812d62426b9503cdb0c87a6de81442626aa7d69707ba", size = 5352421, upload-time = "2025-09-22T04:02:40.413Z" }, - { url = "https://files.pythonhosted.org/packages/48/5b/fc2ddfc94ddbe3eebb8e9af6e3fd65e2feba4967f6a4e9683875c394c2d8/lxml-6.0.2-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2c7fdaa4d7c3d886a42534adec7cfac73860b89b4e5298752f60aa5984641a0", size = 5673684, upload-time = "2025-09-22T04:02:42.288Z" }, - { url = "https://files.pythonhosted.org/packages/29/9c/47293c58cc91769130fbf85531280e8cc7868f7fbb6d92f4670071b9cb3e/lxml-6.0.2-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98a5e1660dc7de2200b00d53fa00bcd3c35a3608c305d45a7bbcaf29fa16e83d", size = 5252463, upload-time = "2025-09-22T04:02:44.165Z" }, - { url = "https://files.pythonhosted.org/packages/9b/da/ba6eceb830c762b48e711ded880d7e3e89fc6c7323e587c36540b6b23c6b/lxml-6.0.2-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:dc051506c30b609238d79eda75ee9cab3e520570ec8219844a72a46020901e37", size = 4698437, upload-time = "2025-09-22T04:02:46.524Z" }, - { url = "https://files.pythonhosted.org/packages/a5/24/7be3f82cb7990b89118d944b619e53c656c97dc89c28cfb143fdb7cd6f4d/lxml-6.0.2-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8799481bbdd212470d17513a54d568f44416db01250f49449647b5ab5b5dccb9", size = 5269890, upload-time = "2025-09-22T04:02:48.812Z" }, - { url = "https://files.pythonhosted.org/packages/1b/bd/dcfb9ea1e16c665efd7538fc5d5c34071276ce9220e234217682e7d2c4a5/lxml-6.0.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9261bb77c2dab42f3ecd9103951aeca2c40277701eb7e912c545c1b16e0e4917", size = 5097185, upload-time = "2025-09-22T04:02:50.746Z" }, - { url = "https://files.pythonhosted.org/packages/21/04/a60b0ff9314736316f28316b694bccbbabe100f8483ad83852d77fc7468e/lxml-6.0.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:65ac4a01aba353cfa6d5725b95d7aed6356ddc0a3cd734de00124d285b04b64f", size = 4745895, upload-time = "2025-09-22T04:02:52.968Z" }, - { url = "https://files.pythonhosted.org/packages/d6/bd/7d54bd1846e5a310d9c715921c5faa71cf5c0853372adf78aee70c8d7aa2/lxml-6.0.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:b22a07cbb82fea98f8a2fd814f3d1811ff9ed76d0fc6abc84eb21527596e7cc8", size = 5695246, upload-time = "2025-09-22T04:02:54.798Z" }, - { url = "https://files.pythonhosted.org/packages/fd/32/5643d6ab947bc371da21323acb2a6e603cedbe71cb4c99c8254289ab6f4e/lxml-6.0.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d759cdd7f3e055d6bc8d9bec3ad905227b2e4c785dc16c372eb5b5e83123f48a", size = 5260797, upload-time = "2025-09-22T04:02:57.058Z" }, - { url = "https://files.pythonhosted.org/packages/33/da/34c1ec4cff1eea7d0b4cd44af8411806ed943141804ac9c5d565302afb78/lxml-6.0.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:945da35a48d193d27c188037a05fec5492937f66fb1958c24fc761fb9d40d43c", size = 5277404, upload-time = "2025-09-22T04:02:58.966Z" }, - { url = "https://files.pythonhosted.org/packages/82/57/4eca3e31e54dc89e2c3507e1cd411074a17565fa5ffc437c4ae0a00d439e/lxml-6.0.2-cp314-cp314-win32.whl", hash = "sha256:be3aaa60da67e6153eb15715cc2e19091af5dc75faef8b8a585aea372507384b", size = 3670072, upload-time = "2025-09-22T04:03:38.05Z" }, - { url = "https://files.pythonhosted.org/packages/e3/e0/c96cf13eccd20c9421ba910304dae0f619724dcf1702864fd59dd386404d/lxml-6.0.2-cp314-cp314-win_amd64.whl", hash = "sha256:fa25afbadead523f7001caf0c2382afd272c315a033a7b06336da2637d92d6ed", size = 4080617, upload-time = "2025-09-22T04:03:39.835Z" }, - { url = "https://files.pythonhosted.org/packages/d5/5d/b3f03e22b3d38d6f188ef044900a9b29b2fe0aebb94625ce9fe244011d34/lxml-6.0.2-cp314-cp314-win_arm64.whl", hash = "sha256:063eccf89df5b24e361b123e257e437f9e9878f425ee9aae3144c77faf6da6d8", size = 3754930, upload-time = "2025-09-22T04:03:41.565Z" }, - { url = "https://files.pythonhosted.org/packages/5e/5c/42c2c4c03554580708fc738d13414801f340c04c3eff90d8d2d227145275/lxml-6.0.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:6162a86d86893d63084faaf4ff937b3daea233e3682fb4474db07395794fa80d", size = 8910380, upload-time = "2025-09-22T04:03:01.645Z" }, - { url = "https://files.pythonhosted.org/packages/bf/4f/12df843e3e10d18d468a7557058f8d3733e8b6e12401f30b1ef29360740f/lxml-6.0.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:414aaa94e974e23a3e92e7ca5b97d10c0cf37b6481f50911032c69eeb3991bba", size = 4775632, upload-time = "2025-09-22T04:03:03.814Z" }, - { url = "https://files.pythonhosted.org/packages/e4/0c/9dc31e6c2d0d418483cbcb469d1f5a582a1cd00a1f4081953d44051f3c50/lxml-6.0.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48461bd21625458dd01e14e2c38dd0aea69addc3c4f960c30d9f59d7f93be601", size = 4975171, upload-time = "2025-09-22T04:03:05.651Z" }, - { url = "https://files.pythonhosted.org/packages/e7/2b/9b870c6ca24c841bdd887504808f0417aa9d8d564114689266f19ddf29c8/lxml-6.0.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:25fcc59afc57d527cfc78a58f40ab4c9b8fd096a9a3f964d2781ffb6eb33f4ed", size = 5110109, upload-time = "2025-09-22T04:03:07.452Z" }, - { url = "https://files.pythonhosted.org/packages/bf/0c/4f5f2a4dd319a178912751564471355d9019e220c20d7db3fb8307ed8582/lxml-6.0.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5179c60288204e6ddde3f774a93350177e08876eaf3ab78aa3a3649d43eb7d37", size = 5041061, upload-time = "2025-09-22T04:03:09.297Z" }, - { url = "https://files.pythonhosted.org/packages/12/64/554eed290365267671fe001a20d72d14f468ae4e6acef1e179b039436967/lxml-6.0.2-cp314-cp314t-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:967aab75434de148ec80597b75062d8123cadf2943fb4281f385141e18b21338", size = 5306233, upload-time = "2025-09-22T04:03:11.651Z" }, - { url = "https://files.pythonhosted.org/packages/7a/31/1d748aa275e71802ad9722df32a7a35034246b42c0ecdd8235412c3396ef/lxml-6.0.2-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d100fcc8930d697c6561156c6810ab4a508fb264c8b6779e6e61e2ed5e7558f9", size = 5604739, upload-time = "2025-09-22T04:03:13.592Z" }, - { url = "https://files.pythonhosted.org/packages/8f/41/2c11916bcac09ed561adccacceaedd2bf0e0b25b297ea92aab99fd03d0fa/lxml-6.0.2-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ca59e7e13e5981175b8b3e4ab84d7da57993eeff53c07764dcebda0d0e64ecd", size = 5225119, upload-time = "2025-09-22T04:03:15.408Z" }, - { url = "https://files.pythonhosted.org/packages/99/05/4e5c2873d8f17aa018e6afde417c80cc5d0c33be4854cce3ef5670c49367/lxml-6.0.2-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:957448ac63a42e2e49531b9d6c0fa449a1970dbc32467aaad46f11545be9af1d", size = 4633665, upload-time = "2025-09-22T04:03:17.262Z" }, - { url = "https://files.pythonhosted.org/packages/0f/c9/dcc2da1bebd6275cdc723b515f93edf548b82f36a5458cca3578bc899332/lxml-6.0.2-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b7fc49c37f1786284b12af63152fe1d0990722497e2d5817acfe7a877522f9a9", size = 5234997, upload-time = "2025-09-22T04:03:19.14Z" }, - { url = "https://files.pythonhosted.org/packages/9c/e2/5172e4e7468afca64a37b81dba152fc5d90e30f9c83c7c3213d6a02a5ce4/lxml-6.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e19e0643cc936a22e837f79d01a550678da8377d7d801a14487c10c34ee49c7e", size = 5090957, upload-time = "2025-09-22T04:03:21.436Z" }, - { url = "https://files.pythonhosted.org/packages/a5/b3/15461fd3e5cd4ddcb7938b87fc20b14ab113b92312fc97afe65cd7c85de1/lxml-6.0.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:1db01e5cf14345628e0cbe71067204db658e2fb8e51e7f33631f5f4735fefd8d", size = 4764372, upload-time = "2025-09-22T04:03:23.27Z" }, - { url = "https://files.pythonhosted.org/packages/05/33/f310b987c8bf9e61c4dd8e8035c416bd3230098f5e3cfa69fc4232de7059/lxml-6.0.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:875c6b5ab39ad5291588aed6925fac99d0097af0dd62f33c7b43736043d4a2ec", size = 5634653, upload-time = "2025-09-22T04:03:25.767Z" }, - { url = "https://files.pythonhosted.org/packages/70/ff/51c80e75e0bc9382158133bdcf4e339b5886c6ee2418b5199b3f1a61ed6d/lxml-6.0.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:cdcbed9ad19da81c480dfd6dd161886db6096083c9938ead313d94b30aadf272", size = 5233795, upload-time = "2025-09-22T04:03:27.62Z" }, - { url = "https://files.pythonhosted.org/packages/56/4d/4856e897df0d588789dd844dbed9d91782c4ef0b327f96ce53c807e13128/lxml-6.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:80dadc234ebc532e09be1975ff538d154a7fa61ea5031c03d25178855544728f", size = 5257023, upload-time = "2025-09-22T04:03:30.056Z" }, - { url = "https://files.pythonhosted.org/packages/0f/85/86766dfebfa87bea0ab78e9ff7a4b4b45225df4b4d3b8cc3c03c5cd68464/lxml-6.0.2-cp314-cp314t-win32.whl", hash = "sha256:da08e7bb297b04e893d91087df19638dc7a6bb858a954b0cc2b9f5053c922312", size = 3911420, upload-time = "2025-09-22T04:03:32.198Z" }, - { url = "https://files.pythonhosted.org/packages/fe/1a/b248b355834c8e32614650b8008c69ffeb0ceb149c793961dd8c0b991bb3/lxml-6.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:252a22982dca42f6155125ac76d3432e548a7625d56f5a273ee78a5057216eca", size = 4406837, upload-time = "2025-09-22T04:03:34.027Z" }, - { url = "https://files.pythonhosted.org/packages/92/aa/df863bcc39c5e0946263454aba394de8a9084dbaff8ad143846b0d844739/lxml-6.0.2-cp314-cp314t-win_arm64.whl", hash = "sha256:bb4c1847b303835d89d785a18801a883436cdfd5dc3d62947f9c49e24f0f5a2c", size = 3822205, upload-time = "2025-09-22T04:03:36.249Z" }, +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/28/30/9abc9e34c657c33834eaf6cd02124c61bdf5944d802aa48e69be8da3585d/lxml-6.1.0.tar.gz", hash = "sha256:bfd57d8008c4965709a919c3e9a98f76c2c7cb319086b3d26858250620023b13", size = 4197006, upload-time = "2026-04-18T04:32:51.613Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/d4/9326838b59dc36dfae42eec9656b97520f9997eee1de47b8316aaeed169c/lxml-6.1.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d2f17a16cd8751e8eb233a7e41aecdf8e511712e00088bf9be455f604cd0d28d", size = 8570663, upload-time = "2026-04-18T04:27:48.253Z" }, + { url = "https://files.pythonhosted.org/packages/d8/a4/053745ce1f8303ccbb788b86c0db3a91b973675cefc42566a188637b7c40/lxml-6.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f0cea5b1d3e6e77d71bd2b9972eb2446221a69dc52bb0b9c3c6f6e5700592d93", size = 4624024, upload-time = "2026-04-18T04:27:52.594Z" }, + { url = "https://files.pythonhosted.org/packages/90/97/a517944b20f8fd0932ad2109482bee4e29fe721416387a363306667941f6/lxml-6.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fc46da94826188ed45cb53bd8e3fc076ae22675aea2087843d4735627f867c6d", size = 4930895, upload-time = "2026-04-18T04:32:56.29Z" }, + { url = "https://files.pythonhosted.org/packages/94/7c/e08a970727d556caa040a44773c7b7e3ad0f0d73dedc863543e9a8b931f2/lxml-6.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9147d8e386ec3b82c3b15d88927f734f565b0aaadef7def562b853adca45784a", size = 5093820, upload-time = "2026-04-18T04:32:58.94Z" }, + { url = "https://files.pythonhosted.org/packages/88/ee/2a5c2aa2c32016a226ca25d3e1056a8102ea6e1fe308bf50213586635400/lxml-6.1.0-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5715e0e28736a070f3f34a7ccc09e2fdcba0e3060abbcf61a1a5718ff6d6b105", size = 5005790, upload-time = "2026-04-18T04:33:01.272Z" }, + { url = "https://files.pythonhosted.org/packages/e3/38/a0db9be8f38ad6043ab9429487c128dd1d30f07956ef43040402f8da49e8/lxml-6.1.0-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4937460dc5df0cdd2f06a86c285c28afda06aefa3af949f9477d3e8df430c485", size = 5630827, upload-time = "2026-04-18T04:33:04.036Z" }, + { url = "https://files.pythonhosted.org/packages/31/ba/3c13d3fc24b7cacf675f808a3a1baabf43a30d0cd24c98f94548e9aa58eb/lxml-6.1.0-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bc783ee3147e60a25aa0445ea82b3e8aabb83b240f2b95d32cb75587ff781814", size = 5240445, upload-time = "2026-04-18T04:33:06.87Z" }, + { url = "https://files.pythonhosted.org/packages/55/ba/eeef4ccba09b2212fe239f46c1692a98db1878e0872ae320756488878a94/lxml-6.1.0-cp312-cp312-manylinux_2_28_i686.whl", hash = "sha256:40d9189f80075f2e1f88db21ef815a2b17b28adf8e50aaf5c789bfe737027f32", size = 5350121, upload-time = "2026-04-18T04:33:09.365Z" }, + { url = "https://files.pythonhosted.org/packages/7e/01/1da87c7b587c38d0cbe77a01aae3b9c1c49ed47d76918ef3db8fc151b1ca/lxml-6.1.0-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:05b9b8787e35bec69e68daf4952b2e6dfcfb0db7ecf1a06f8cdfbbac4eb71aad", size = 4694949, upload-time = "2026-04-18T04:33:11.628Z" }, + { url = "https://files.pythonhosted.org/packages/a1/88/7db0fe66d5aaf128443ee1623dec3db1576f3e4c17751ec0ef5866468590/lxml-6.1.0-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0f0f08beb0182e3e9a86fae124b3c47a7b41b7b69b225e1377db983802404e54", size = 5243901, upload-time = "2026-04-18T04:33:13.95Z" }, + { url = "https://files.pythonhosted.org/packages/00/a8/1346726af7d1f6fca1f11223ba34001462b0a3660416986d37641708d57c/lxml-6.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73becf6d8c81d4c76b1014dbd3584cb26d904492dcf73ca85dc8bff08dcd6d2d", size = 5048054, upload-time = "2026-04-18T04:33:16.965Z" }, + { url = "https://files.pythonhosted.org/packages/2e/b7/85057012f035d1a0c87e02f8c723ca3c3e6e0728bcf4cb62080b21b1c1e3/lxml-6.1.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1ae225f66e5938f4fa29d37e009a3bb3b13032ac57eb4eb42afa44f6e4054e69", size = 4777324, upload-time = "2026-04-18T04:33:19.832Z" }, + { url = "https://files.pythonhosted.org/packages/75/6c/ad2f94a91073ef570f33718040e8e160d5fb93331cf1ab3ca1323f939e2d/lxml-6.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:690022c7fae793b0489aa68a658822cea83e0d5933781811cabbf5ea3bcfe73d", size = 5645702, upload-time = "2026-04-18T04:33:22.436Z" }, + { url = "https://files.pythonhosted.org/packages/3b/89/0bb6c0bd549c19004c60eea9dc554dd78fd647b72314ef25d460e0d208c6/lxml-6.1.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:63aeafc26aac0be8aff14af7871249e87ea1319be92090bfd632ec68e03b16a5", size = 5232901, upload-time = "2026-04-18T04:33:26.21Z" }, + { url = "https://files.pythonhosted.org/packages/a1/d9/d609a11fb567da9399f525193e2b49847b5a409cdebe737f06a8b7126bdc/lxml-6.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:264c605ab9c0e4aa1a679636f4582c4d3313700009fac3ec9c3412ed0d8f3e1d", size = 5261333, upload-time = "2026-04-18T04:33:28.984Z" }, + { url = "https://files.pythonhosted.org/packages/a6/3a/ac3f99ec8ac93089e7dd556f279e0d14c24de0a74a507e143a2e4b496e7c/lxml-6.1.0-cp312-cp312-win32.whl", hash = "sha256:56971379bc5ee8037c5a0f09fa88f66cdb7d37c3e38af3e45cf539f41131ac1f", size = 3596289, upload-time = "2026-04-18T04:27:42.819Z" }, + { url = "https://files.pythonhosted.org/packages/f2/a7/0a915557538593cb1bbeedcd40e13c7a261822c26fecbbdb71dad0c2f540/lxml-6.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:bba078de0031c219e5dd06cf3e6bf8fb8e6e64a77819b358f53bb132e3e03366", size = 3997059, upload-time = "2026-04-18T04:27:46.764Z" }, + { url = "https://files.pythonhosted.org/packages/92/96/a5dc078cf0126fbfbc35611d77ecd5da80054b5893e28fb213a5613b9e1d/lxml-6.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:c3592631e652afa34999a088f98ba7dfc7d6aff0d535c410bea77a71743f3819", size = 3659552, upload-time = "2026-04-18T04:27:51.133Z" }, + { url = "https://files.pythonhosted.org/packages/08/03/69347590f1cf4a6d5a4944bb6099e6d37f334784f16062234e1f892fdb1d/lxml-6.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a0092f2b107b69601adf562a57c956fbb596e05e3e6651cabd3054113b007e45", size = 8559689, upload-time = "2026-04-18T04:31:57.785Z" }, + { url = "https://files.pythonhosted.org/packages/3f/58/25e00bb40b185c974cfe156c110474d9a8a8390d5f7c92a4e328189bb60e/lxml-6.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fc7140d7a7386e6b545d41b7358f4d02b656d4053f5fa6859f92f4b9c2572c4d", size = 4617892, upload-time = "2026-04-18T04:32:01.78Z" }, + { url = "https://files.pythonhosted.org/packages/f5/54/92ad98a94ac318dc4f97aaac22ff8d1b94212b2ae8af5b6e9b354bf825f7/lxml-6.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:419c58fc92cc3a2c3fa5f78c63dbf5da70c1fa9c1b25f25727ecee89a96c7de2", size = 4923489, upload-time = "2026-04-18T04:33:31.401Z" }, + { url = "https://files.pythonhosted.org/packages/15/3b/a20aecfab42bdf4f9b390590d345857ad3ffd7c51988d1c89c53a0c73faf/lxml-6.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:37fabd1452852636cf38ecdcc9dd5ca4bba7a35d6c53fa09725deeb894a87491", size = 5082162, upload-time = "2026-04-18T04:33:34.262Z" }, + { url = "https://files.pythonhosted.org/packages/45/26/2cdb3d281ac1bd175603e290cbe4bad6eff127c0f8de90bafd6f8548f0fd/lxml-6.1.0-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2853c8b2170cc6cd54a6b4d50d2c1a8a7aeca201f23804b4898525c7a152cfc", size = 4993247, upload-time = "2026-04-18T04:33:36.674Z" }, + { url = "https://files.pythonhosted.org/packages/f6/05/d735aef963740022a08185c84821f689fc903acb3d50326e6b1e9886cc22/lxml-6.1.0-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8e369cbd690e788c8d15e56222d91a09c6a417f49cbc543040cba0fe2e25a79e", size = 5613042, upload-time = "2026-04-18T04:33:39.205Z" }, + { url = "https://files.pythonhosted.org/packages/ee/b8/ead7c10efff731738c72e59ed6eb5791854879fbed7ae98781a12006263a/lxml-6.1.0-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e69aa6805905807186eb00e66c6d97a935c928275182eb02ee40ba00da9623b2", size = 5228304, upload-time = "2026-04-18T04:33:41.647Z" }, + { url = "https://files.pythonhosted.org/packages/6b/10/e9842d2ec322ea65f0a7270aa0315a53abed06058b88ef1b027f620e7a5f/lxml-6.1.0-cp313-cp313-manylinux_2_28_i686.whl", hash = "sha256:4bd1bdb8a9e0e2dd229de19b5f8aebac80e916921b4b2c6ef8a52bc131d0c1f9", size = 5341578, upload-time = "2026-04-18T04:33:44.596Z" }, + { url = "https://files.pythonhosted.org/packages/89/54/40d9403d7c2775fa7301d3ddd3464689bfe9ba71acc17dfff777071b4fdc/lxml-6.1.0-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:cbd7b79cdcb4986ad78a2662625882747f09db5e4cd7b2ae178a88c9c51b3dfe", size = 4700209, upload-time = "2026-04-18T04:33:47.552Z" }, + { url = "https://files.pythonhosted.org/packages/85/b2/bbdcc2cf45dfc7dfffef4fd97e5c47b15919b6a365247d95d6f684ef5e82/lxml-6.1.0-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:43e4d297f11080ec9d64a4b1ad7ac02b4484c9f0e2179d9c4ef78e886e747b88", size = 5232365, upload-time = "2026-04-18T04:33:50.249Z" }, + { url = "https://files.pythonhosted.org/packages/48/5a/b06875665e53aaba7127611a7bed3b7b9658e20b22bc2dd217a0b7ab0091/lxml-6.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cc16682cc987a3da00aa56a3aa3075b08edb10d9b1e476938cfdbee8f3b67181", size = 5043654, upload-time = "2026-04-18T04:33:52.71Z" }, + { url = "https://files.pythonhosted.org/packages/e9/9c/e71a069d09641c1a7abeb30e693f828c7c90a41cbe3d650b2d734d876f85/lxml-6.1.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:d6d8efe71429635f0559579092bb5e60560d7b9115ee38c4adbea35632e7fa24", size = 4769326, upload-time = "2026-04-18T04:33:55.244Z" }, + { url = "https://files.pythonhosted.org/packages/cc/06/7a9cd84b3d4ed79adf35f874750abb697dec0b4a81a836037b36e47c091a/lxml-6.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7e39ab3a28af7784e206d8606ec0e4bcad0190f63a492bca95e94e5a4aef7f6e", size = 5635879, upload-time = "2026-04-18T04:33:58.509Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f0/9d57916befc1e54c451712c7ee48e9e74e80ae4d03bdce49914e0aee42cd/lxml-6.1.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:9eb667bf50856c4a58145f8ca2d5e5be160191e79eb9e30855a476191b3c3495", size = 5224048, upload-time = "2026-04-18T04:34:00.943Z" }, + { url = "https://files.pythonhosted.org/packages/99/75/90c4eefda0c08c92221fe0753db2d6699a4c628f76ff4465ec20dea84cc1/lxml-6.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7f4a77d6f7edf9230cee3e1f7f6764722a41604ee5681844f18db9a81ea0ec33", size = 5250241, upload-time = "2026-04-18T04:34:03.365Z" }, + { url = "https://files.pythonhosted.org/packages/5e/73/16596f7e4e38fa33084b9ccbccc22a15f82a290a055126f2c1541236d2ff/lxml-6.1.0-cp313-cp313-win32.whl", hash = "sha256:28902146ffbe5222df411c5d19e5352490122e14447e98cd118907ee3fd6ee62", size = 3596938, upload-time = "2026-04-18T04:31:56.206Z" }, + { url = "https://files.pythonhosted.org/packages/8e/63/981401c5680c1eb30893f00a19641ac80db5d1e7086c62cb4b13ed813038/lxml-6.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:4a1503c56e4e2b38dc76f2f2da7bae69670c0f1933e27cfa34b2fa5876410b16", size = 3995728, upload-time = "2026-04-18T04:31:58.763Z" }, + { url = "https://files.pythonhosted.org/packages/e7/e8/c358a38ac3e541d16a1b527e4e9cb78c0419b0506a070ace11777e5e8404/lxml-6.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:e0af85773850417d994d019741239b901b22c6680206f46a34766926e466141d", size = 3658372, upload-time = "2026-04-18T04:32:03.629Z" }, + { url = "https://files.pythonhosted.org/packages/eb/45/cee4cf203ef0bab5c52afc118da61d6b460c928f2893d40023cfa27e0b80/lxml-6.1.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:ab863fd37458fed6456525f297d21239d987800c46e67da5ef04fc6b3dd93ac8", size = 8576713, upload-time = "2026-04-18T04:32:06.831Z" }, + { url = "https://files.pythonhosted.org/packages/8a/a7/eda05babeb7e046839204eaf254cd4d7c9130ce2bbf0d9e90ea41af5654d/lxml-6.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6fd8b1df8254ff4fd93fd31da1fc15770bde23ac045be9bb1f87425702f61cc9", size = 4623874, upload-time = "2026-04-18T04:32:10.755Z" }, + { url = "https://files.pythonhosted.org/packages/e7/e9/db5846de9b436b91890a62f29d80cd849ea17948a49bf532d5278ee69a9e/lxml-6.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:47024feaae386a92a146af0d2aeed65229bf6fff738e6a11dda6b0015fb8fd03", size = 4949535, upload-time = "2026-04-18T04:34:06.657Z" }, + { url = "https://files.pythonhosted.org/packages/5a/ba/0d3593373dcae1d68f40dc3c41a5a92f2544e68115eb2f62319a4c2a6500/lxml-6.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3f00972f84450204cd5d93a5395965e348956aaceaadec693a22ec743f8ae3eb", size = 5086881, upload-time = "2026-04-18T04:34:09.556Z" }, + { url = "https://files.pythonhosted.org/packages/43/76/759a7484539ad1af0d125a9afe9c3fb5f82a8779fd1f5f56319d9e4ea2fd/lxml-6.1.0-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97faa0860e13b05b15a51fb4986421ef7a30f0b3334061c416e0981e9450ca4c", size = 5031305, upload-time = "2026-04-18T04:34:12.336Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b9/c1f0daf981a11e47636126901fd4ab82429e18c57aeb0fc3ad2940b42d8b/lxml-6.1.0-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:972a6451204798675407beaad97b868d0c733d9a74dafefc63120b81b8c2de28", size = 5647522, upload-time = "2026-04-18T04:34:14.89Z" }, + { url = "https://files.pythonhosted.org/packages/31/e6/1f533dcd205275363d9ba3511bcec52fa2df86abf8abe6a5f2c599f0dc31/lxml-6.1.0-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fe022f20bc4569ec66b63b3fb275a3d628d9d32da6326b2982584104db6d3086", size = 5239310, upload-time = "2026-04-18T04:34:17.652Z" }, + { url = "https://files.pythonhosted.org/packages/c3/8c/4175fb709c78a6e315ed814ed33be3defd8b8721067e70419a6cf6f971da/lxml-6.1.0-cp314-cp314-manylinux_2_28_i686.whl", hash = "sha256:75c4c7c619a744f972f4451bf5adf6d0fb00992a1ffc9fd78e13b0bc817cc99f", size = 5350799, upload-time = "2026-04-18T04:34:20.529Z" }, + { url = "https://files.pythonhosted.org/packages/fd/77/6ffdebc5994975f0dde4acb59761902bd9d9bb84422b9a0bd239a7da9ca8/lxml-6.1.0-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:3648f20d25102a22b6061c688beb3a805099ea4beb0a01ce62975d926944d292", size = 4697693, upload-time = "2026-04-18T04:34:23.541Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f1/565f36bd5c73294602d48e04d23f81ff4c8736be6ba5e1d1ec670ac9be80/lxml-6.1.0-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:77b9f99b17cbf14026d1e618035077060fc7195dd940d025149f3e2e830fbfcb", size = 5250708, upload-time = "2026-04-18T04:34:26.001Z" }, + { url = "https://files.pythonhosted.org/packages/5a/11/a68ab9dd18c5c499404deb4005f4bc4e0e88e5b72cd755ad96efec81d18d/lxml-6.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:32662519149fd7a9db354175aa5e417d83485a8039b8aaa62f873ceee7ea4cad", size = 5084737, upload-time = "2026-04-18T04:34:28.32Z" }, + { url = "https://files.pythonhosted.org/packages/ab/78/e8f41e2c74f4af564e6a0348aea69fb6daaefa64bc071ef469823d22cc18/lxml-6.1.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:73d658216fc173cf2c939e90e07b941c5e12736b0bf6a99e7af95459cfe8eabb", size = 4737817, upload-time = "2026-04-18T04:34:30.784Z" }, + { url = "https://files.pythonhosted.org/packages/06/2d/aa4e117aa2ce2f3b35d9ff246be74a2f8e853baba5d2a92c64744474603a/lxml-6.1.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ac4db068889f8772a4a698c5980ec302771bb545e10c4b095d4c8be26749616f", size = 5670753, upload-time = "2026-04-18T04:34:33.675Z" }, + { url = "https://files.pythonhosted.org/packages/08/f5/dd745d50c0409031dbfcc4881740542a01e54d6f0110bd420fa7782110b8/lxml-6.1.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:45e9dfbd1b661eb64ba0d4dbe762bd210c42d86dd1e5bd2bdf89d634231beb43", size = 5238071, upload-time = "2026-04-18T04:34:36.12Z" }, + { url = "https://files.pythonhosted.org/packages/3e/74/ad424f36d0340a904665867dab310a3f1f4c96ff4039698de83b77f44c1f/lxml-6.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:89e8d73d09ac696a5ba42ec69787913d53284f12092f651506779314f10ba585", size = 5264319, upload-time = "2026-04-18T04:34:39.035Z" }, + { url = "https://files.pythonhosted.org/packages/53/36/a15d8b3514ec889bfd6aa3609107fcb6c9189f8dc347f1c0b81eded8d87c/lxml-6.1.0-cp314-cp314-win32.whl", hash = "sha256:ebe33f4ec1b2de38ceb225a1749a2965855bffeef435ba93cd2d5d540783bf2f", size = 3657139, upload-time = "2026-04-18T04:32:20.006Z" }, + { url = "https://files.pythonhosted.org/packages/1a/a4/263ebb0710851a3c6c937180a9a86df1206fdfe53cc43005aa2237fd7736/lxml-6.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:398443df51c538bd578529aa7e5f7afc6c292644174b47961f3bf87fe5741120", size = 4064195, upload-time = "2026-04-18T04:32:23.876Z" }, + { url = "https://files.pythonhosted.org/packages/80/68/2000f29d323b6c286de077ad20b429fc52272e44eae6d295467043e56012/lxml-6.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:8c8984e1d8c4b3949e419158fda14d921ff703a9ed8a47236c6eb7a2b6cb4946", size = 3741870, upload-time = "2026-04-18T04:32:27.922Z" }, + { url = "https://files.pythonhosted.org/packages/30/e9/21383c7c8d43799f0da90224c0d7c921870d476ec9b3e01e1b2c0b8237c5/lxml-6.1.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:1081dd10bc6fa437db2500e13993abf7cc30716d0a2f40e65abb935f02ec559c", size = 8827548, upload-time = "2026-04-18T04:32:15.094Z" }, + { url = "https://files.pythonhosted.org/packages/a5/01/c6bc11cd587030dd4f719f65c5657960649fe3e19196c844c75bf32cd0d6/lxml-6.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:dabecc48db5f42ba348d1f5d5afdc54c6c4cc758e676926c7cd327045749517d", size = 4735866, upload-time = "2026-04-18T04:32:18.924Z" }, + { url = "https://files.pythonhosted.org/packages/f3/01/757132fff5f4acf25463b5298f1a46099f3a94480b806547b29ce5e385de/lxml-6.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e3dd5fe19c9e0ac818a9c7f132a5e43c1339ec1cbbfecb1a938bd3a47875b7c9", size = 4969476, upload-time = "2026-04-18T04:34:41.889Z" }, + { url = "https://files.pythonhosted.org/packages/fd/fb/1bc8b9d27ed64be7c8903db6c89e74dc8c2cd9ec630a7462e4654316dc5b/lxml-6.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9e7b0a4ca6dcc007a4cef00a761bba2dea959de4bd2df98f926b33c92ca5dfb9", size = 5103719, upload-time = "2026-04-18T04:34:44.797Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e7/5bf82fa28133536a54601aae633b14988e89ed61d4c1eb6b899b023233aa/lxml-6.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d27bbe326c6b539c64b42638b18bc6003a8d88f76213a97ac9ed4f885efeab7", size = 5027890, upload-time = "2026-04-18T04:34:47.634Z" }, + { url = "https://files.pythonhosted.org/packages/2d/20/e048db5d4b4ea0366648aa595f26bb764b2670903fc585b87436d0a5032c/lxml-6.1.0-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4e425db0c5445ef0ad56b0eec54f89b88b2d884656e536a90b2f52aecb4ca86", size = 5596008, upload-time = "2026-04-18T04:34:51.503Z" }, + { url = "https://files.pythonhosted.org/packages/9a/c2/d10807bc8da4824b39e5bd01b5d05c077b6fd01bd91584167edf6b269d22/lxml-6.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4b89b098105b8599dc57adac95d1813409ac476d3c948a498775d3d0c6124bfb", size = 5224451, upload-time = "2026-04-18T04:34:54.263Z" }, + { url = "https://files.pythonhosted.org/packages/3c/15/2ebea45bea427e7f0057e9ce7b2d62c5aba20c6b001cca89ed0aadb3ad41/lxml-6.1.0-cp314-cp314t-manylinux_2_28_i686.whl", hash = "sha256:c4a699432846df86cc3de502ee85f445ebad748a1c6021d445f3e514d2cd4b1c", size = 5312135, upload-time = "2026-04-18T04:34:56.818Z" }, + { url = "https://files.pythonhosted.org/packages/31/e2/87eeae151b0be2a308d49a7ec444ff3eb192b14251e62addb29d0bf3778f/lxml-6.1.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:30e7b2ed63b6c8e97cca8af048589a788ab5c9c905f36d9cf1c2bb549f450d2f", size = 4639126, upload-time = "2026-04-18T04:34:59.704Z" }, + { url = "https://files.pythonhosted.org/packages/a3/51/8a3f6a20902ad604dd746ec7b4000311b240d389dac5e9d95adefd349e0c/lxml-6.1.0-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:022981127642fe19866d2907d76241bb07ed21749601f727d5d5dd1ce5d1b773", size = 5232579, upload-time = "2026-04-18T04:35:02.658Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d2/650d619bdbe048d2c3f2c31edb00e35670a5e2d65b4fe3b61bce37b19121/lxml-6.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:23cad0cc86046d4222f7f418910e46b89971c5a45d3c8abfad0f64b7b05e4a9b", size = 5084206, upload-time = "2026-04-18T04:35:05.175Z" }, + { url = "https://files.pythonhosted.org/packages/dd/8a/672ca1a3cbeabd1f511ca275a916c0514b747f4b85bdaae103b8fa92f307/lxml-6.1.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:21c3302068f50d1e8728c67c87ba92aa87043abee517aa2576cca1855326b405", size = 4758906, upload-time = "2026-04-18T04:35:08.098Z" }, + { url = "https://files.pythonhosted.org/packages/be/f1/ef4b691da85c916cb2feb1eec7414f678162798ac85e042fa164419ac05c/lxml-6.1.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:be10838781cb3be19251e276910cd508fe127e27c3242e50521521a0f3781690", size = 5620553, upload-time = "2026-04-18T04:35:11.23Z" }, + { url = "https://files.pythonhosted.org/packages/59/17/94e81def74107809755ac2782fdad4404420f1c92ca83433d117a6d5acf0/lxml-6.1.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2173a7bffe97667bbf0767f8a99e587740a8c56fdf3befac4b09cb29a80276fd", size = 5229458, upload-time = "2026-04-18T04:35:14.254Z" }, + { url = "https://files.pythonhosted.org/packages/21/55/c4be91b0f830a871fc1b0d730943d56013b683d4671d5198260e2eae722b/lxml-6.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c6854e9cf99c84beb004eecd7d3a3868ef1109bf2b1df92d7bc11e96a36c2180", size = 5247861, upload-time = "2026-04-18T04:35:17.006Z" }, + { url = "https://files.pythonhosted.org/packages/c2/ca/77123e4d77df3cb1e968ade7b1f808f5d3a5c1c96b18a33895397de292c1/lxml-6.1.0-cp314-cp314t-win32.whl", hash = "sha256:00750d63ef0031a05331b9223463b1c7c02b9004cef2346a5b2877f0f9494dd2", size = 3897377, upload-time = "2026-04-18T04:32:07.656Z" }, + { url = "https://files.pythonhosted.org/packages/64/ce/3554833989d074267c063209bae8b09815e5656456a2d332b947806b05ff/lxml-6.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:80410c3a7e3c617af04de17caa9f9f20adaa817093293d69eae7d7d0522836f5", size = 4392701, upload-time = "2026-04-18T04:32:12.113Z" }, + { url = "https://files.pythonhosted.org/packages/2b/a0/9b916c68c0e57752c07f8f64b30138d9d4059dbeb27b90274dedbea128ff/lxml-6.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:26dd9f57ee3bd41e7d35b4c98a2ffd89ed11591649f421f0ec19f67d50ec67ac", size = 3817120, upload-time = "2026-04-18T04:32:15.803Z" }, ] [[package]] @@ -556,96 +596,94 @@ wheels = [ [[package]] name = "more-itertools" -version = "10.8.0" +version = "11.0.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ea/5d/38b681d3fce7a266dd9ab73c66959406d565b3e85f21d5e66e1181d93721/more_itertools-10.8.0.tar.gz", hash = "sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd", size = 137431, upload-time = "2025-09-02T15:23:11.018Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/f7/139d22fef48ac78127d18e01d80cf1be40236ae489769d17f35c3d425293/more_itertools-11.0.2.tar.gz", hash = "sha256:392a9e1e362cbc106a2457d37cabf9b36e5e12efd4ebff1654630e76597df804", size = 144659, upload-time = "2026-04-09T15:01:33.297Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl", hash = "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b", size = 69667, upload-time = "2025-09-02T15:23:09.635Z" }, + { url = "https://files.pythonhosted.org/packages/cb/98/6af411189d9413534c3eb691182bff1f5c6d44ed2f93f2edfe52a1bbceb8/more_itertools-11.0.2-py3-none-any.whl", hash = "sha256:6e35b35f818b01f691643c6c611bc0902f2e92b46c18fffa77ae1e7c46e912e4", size = 71939, upload-time = "2026-04-09T15:01:32.21Z" }, ] [[package]] name = "numpy" -version = "2.3.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b5/f4/098d2270d52b41f1bd7db9fc288aaa0400cb48c2a3e2af6fa365d9720947/numpy-2.3.4.tar.gz", hash = "sha256:a7d018bfedb375a8d979ac758b120ba846a7fe764911a64465fd87b8729f4a6a", size = 20582187, upload-time = "2025-10-15T16:18:11.77Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/96/7a/02420400b736f84317e759291b8edaeee9dc921f72b045475a9cbdb26b17/numpy-2.3.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ef1b5a3e808bc40827b5fa2c8196151a4c5abe110e1726949d7abddfe5c7ae11", size = 20957727, upload-time = "2025-10-15T16:15:44.9Z" }, - { url = "https://files.pythonhosted.org/packages/18/90/a014805d627aa5750f6f0e878172afb6454552da929144b3c07fcae1bb13/numpy-2.3.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c2f91f496a87235c6aaf6d3f3d89b17dba64996abadccb289f48456cff931ca9", size = 14187262, upload-time = "2025-10-15T16:15:47.761Z" }, - { url = "https://files.pythonhosted.org/packages/c7/e4/0a94b09abe89e500dc748e7515f21a13e30c5c3fe3396e6d4ac108c25fca/numpy-2.3.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:f77e5b3d3da652b474cc80a14084927a5e86a5eccf54ca8ca5cbd697bf7f2667", size = 5115992, upload-time = "2025-10-15T16:15:50.144Z" }, - { url = "https://files.pythonhosted.org/packages/88/dd/db77c75b055c6157cbd4f9c92c4458daef0dd9cbe6d8d2fe7f803cb64c37/numpy-2.3.4-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:8ab1c5f5ee40d6e01cbe96de5863e39b215a4d24e7d007cad56c7184fdf4aeef", size = 6648672, upload-time = "2025-10-15T16:15:52.442Z" }, - { url = "https://files.pythonhosted.org/packages/e1/e6/e31b0d713719610e406c0ea3ae0d90760465b086da8783e2fd835ad59027/numpy-2.3.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77b84453f3adcb994ddbd0d1c5d11db2d6bda1a2b7fd5ac5bd4649d6f5dc682e", size = 14284156, upload-time = "2025-10-15T16:15:54.351Z" }, - { url = "https://files.pythonhosted.org/packages/f9/58/30a85127bfee6f108282107caf8e06a1f0cc997cb6b52cdee699276fcce4/numpy-2.3.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4121c5beb58a7f9e6dfdee612cb24f4df5cd4db6e8261d7f4d7450a997a65d6a", size = 16641271, upload-time = "2025-10-15T16:15:56.67Z" }, - { url = "https://files.pythonhosted.org/packages/06/f2/2e06a0f2adf23e3ae29283ad96959267938d0efd20a2e25353b70065bfec/numpy-2.3.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:65611ecbb00ac9846efe04db15cbe6186f562f6bb7e5e05f077e53a599225d16", size = 16059531, upload-time = "2025-10-15T16:15:59.412Z" }, - { url = "https://files.pythonhosted.org/packages/b0/e7/b106253c7c0d5dc352b9c8fab91afd76a93950998167fa3e5afe4ef3a18f/numpy-2.3.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dabc42f9c6577bcc13001b8810d300fe814b4cfbe8a92c873f269484594f9786", size = 18578983, upload-time = "2025-10-15T16:16:01.804Z" }, - { url = "https://files.pythonhosted.org/packages/73/e3/04ecc41e71462276ee867ccbef26a4448638eadecf1bc56772c9ed6d0255/numpy-2.3.4-cp312-cp312-win32.whl", hash = "sha256:a49d797192a8d950ca59ee2d0337a4d804f713bb5c3c50e8db26d49666e351dc", size = 6291380, upload-time = "2025-10-15T16:16:03.938Z" }, - { url = "https://files.pythonhosted.org/packages/3d/a8/566578b10d8d0e9955b1b6cd5db4e9d4592dd0026a941ff7994cedda030a/numpy-2.3.4-cp312-cp312-win_amd64.whl", hash = "sha256:985f1e46358f06c2a09921e8921e2c98168ed4ae12ccd6e5e87a4f1857923f32", size = 12787999, upload-time = "2025-10-15T16:16:05.801Z" }, - { url = "https://files.pythonhosted.org/packages/58/22/9c903a957d0a8071b607f5b1bff0761d6e608b9a965945411f867d515db1/numpy-2.3.4-cp312-cp312-win_arm64.whl", hash = "sha256:4635239814149e06e2cb9db3dd584b2fa64316c96f10656983b8026a82e6e4db", size = 10197412, upload-time = "2025-10-15T16:16:07.854Z" }, - { url = "https://files.pythonhosted.org/packages/57/7e/b72610cc91edf138bc588df5150957a4937221ca6058b825b4725c27be62/numpy-2.3.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c090d4860032b857d94144d1a9976b8e36709e40386db289aaf6672de2a81966", size = 20950335, upload-time = "2025-10-15T16:16:10.304Z" }, - { url = "https://files.pythonhosted.org/packages/3e/46/bdd3370dcea2f95ef14af79dbf81e6927102ddf1cc54adc0024d61252fd9/numpy-2.3.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a13fc473b6db0be619e45f11f9e81260f7302f8d180c49a22b6e6120022596b3", size = 14179878, upload-time = "2025-10-15T16:16:12.595Z" }, - { url = "https://files.pythonhosted.org/packages/ac/01/5a67cb785bda60f45415d09c2bc245433f1c68dd82eef9c9002c508b5a65/numpy-2.3.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:3634093d0b428e6c32c3a69b78e554f0cd20ee420dcad5a9f3b2a63762ce4197", size = 5108673, upload-time = "2025-10-15T16:16:14.877Z" }, - { url = "https://files.pythonhosted.org/packages/c2/cd/8428e23a9fcebd33988f4cb61208fda832800ca03781f471f3727a820704/numpy-2.3.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:043885b4f7e6e232d7df4f51ffdef8c36320ee9d5f227b380ea636722c7ed12e", size = 6641438, upload-time = "2025-10-15T16:16:16.805Z" }, - { url = "https://files.pythonhosted.org/packages/3e/d1/913fe563820f3c6b079f992458f7331278dcd7ba8427e8e745af37ddb44f/numpy-2.3.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4ee6a571d1e4f0ea6d5f22d6e5fbd6ed1dc2b18542848e1e7301bd190500c9d7", size = 14281290, upload-time = "2025-10-15T16:16:18.764Z" }, - { url = "https://files.pythonhosted.org/packages/9e/7e/7d306ff7cb143e6d975cfa7eb98a93e73495c4deabb7d1b5ecf09ea0fd69/numpy-2.3.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fc8a63918b04b8571789688b2780ab2b4a33ab44bfe8ccea36d3eba51228c953", size = 16636543, upload-time = "2025-10-15T16:16:21.072Z" }, - { url = "https://files.pythonhosted.org/packages/47/6a/8cfc486237e56ccfb0db234945552a557ca266f022d281a2f577b98e955c/numpy-2.3.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:40cc556d5abbc54aabe2b1ae287042d7bdb80c08edede19f0c0afb36ae586f37", size = 16056117, upload-time = "2025-10-15T16:16:23.369Z" }, - { url = "https://files.pythonhosted.org/packages/b1/0e/42cb5e69ea901e06ce24bfcc4b5664a56f950a70efdcf221f30d9615f3f3/numpy-2.3.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ecb63014bb7f4ce653f8be7f1df8cbc6093a5a2811211770f6606cc92b5a78fd", size = 18577788, upload-time = "2025-10-15T16:16:27.496Z" }, - { url = "https://files.pythonhosted.org/packages/86/92/41c3d5157d3177559ef0a35da50f0cda7fa071f4ba2306dd36818591a5bc/numpy-2.3.4-cp313-cp313-win32.whl", hash = "sha256:e8370eb6925bb8c1c4264fec52b0384b44f675f191df91cbe0140ec9f0955646", size = 6282620, upload-time = "2025-10-15T16:16:29.811Z" }, - { url = "https://files.pythonhosted.org/packages/09/97/fd421e8bc50766665ad35536c2bb4ef916533ba1fdd053a62d96cc7c8b95/numpy-2.3.4-cp313-cp313-win_amd64.whl", hash = "sha256:56209416e81a7893036eea03abcb91c130643eb14233b2515c90dcac963fe99d", size = 12784672, upload-time = "2025-10-15T16:16:31.589Z" }, - { url = "https://files.pythonhosted.org/packages/ad/df/5474fb2f74970ca8eb978093969b125a84cc3d30e47f82191f981f13a8a0/numpy-2.3.4-cp313-cp313-win_arm64.whl", hash = "sha256:a700a4031bc0fd6936e78a752eefb79092cecad2599ea9c8039c548bc097f9bc", size = 10196702, upload-time = "2025-10-15T16:16:33.902Z" }, - { url = "https://files.pythonhosted.org/packages/11/83/66ac031464ec1767ea3ed48ce40f615eb441072945e98693bec0bcd056cc/numpy-2.3.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:86966db35c4040fdca64f0816a1c1dd8dbd027d90fca5a57e00e1ca4cd41b879", size = 21049003, upload-time = "2025-10-15T16:16:36.101Z" }, - { url = "https://files.pythonhosted.org/packages/5f/99/5b14e0e686e61371659a1d5bebd04596b1d72227ce36eed121bb0aeab798/numpy-2.3.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:838f045478638b26c375ee96ea89464d38428c69170360b23a1a50fa4baa3562", size = 14302980, upload-time = "2025-10-15T16:16:39.124Z" }, - { url = "https://files.pythonhosted.org/packages/2c/44/e9486649cd087d9fc6920e3fc3ac2aba10838d10804b1e179fb7cbc4e634/numpy-2.3.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d7315ed1dab0286adca467377c8381cd748f3dc92235f22a7dfc42745644a96a", size = 5231472, upload-time = "2025-10-15T16:16:41.168Z" }, - { url = "https://files.pythonhosted.org/packages/3e/51/902b24fa8887e5fe2063fd61b1895a476d0bbf46811ab0c7fdf4bd127345/numpy-2.3.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:84f01a4d18b2cc4ade1814a08e5f3c907b079c847051d720fad15ce37aa930b6", size = 6739342, upload-time = "2025-10-15T16:16:43.777Z" }, - { url = "https://files.pythonhosted.org/packages/34/f1/4de9586d05b1962acdcdb1dc4af6646361a643f8c864cef7c852bf509740/numpy-2.3.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:817e719a868f0dacde4abdfc5c1910b301877970195db9ab6a5e2c4bd5b121f7", size = 14354338, upload-time = "2025-10-15T16:16:46.081Z" }, - { url = "https://files.pythonhosted.org/packages/1f/06/1c16103b425de7969d5a76bdf5ada0804b476fed05d5f9e17b777f1cbefd/numpy-2.3.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85e071da78d92a214212cacea81c6da557cab307f2c34b5f85b628e94803f9c0", size = 16702392, upload-time = "2025-10-15T16:16:48.455Z" }, - { url = "https://files.pythonhosted.org/packages/34/b2/65f4dc1b89b5322093572b6e55161bb42e3e0487067af73627f795cc9d47/numpy-2.3.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2ec646892819370cf3558f518797f16597b4e4669894a2ba712caccc9da53f1f", size = 16134998, upload-time = "2025-10-15T16:16:51.114Z" }, - { url = "https://files.pythonhosted.org/packages/d4/11/94ec578896cdb973aaf56425d6c7f2aff4186a5c00fac15ff2ec46998b46/numpy-2.3.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:035796aaaddfe2f9664b9a9372f089cfc88bd795a67bd1bfe15e6e770934cf64", size = 18651574, upload-time = "2025-10-15T16:16:53.429Z" }, - { url = "https://files.pythonhosted.org/packages/62/b7/7efa763ab33dbccf56dade36938a77345ce8e8192d6b39e470ca25ff3cd0/numpy-2.3.4-cp313-cp313t-win32.whl", hash = "sha256:fea80f4f4cf83b54c3a051f2f727870ee51e22f0248d3114b8e755d160b38cfb", size = 6413135, upload-time = "2025-10-15T16:16:55.992Z" }, - { url = "https://files.pythonhosted.org/packages/43/70/aba4c38e8400abcc2f345e13d972fb36c26409b3e644366db7649015f291/numpy-2.3.4-cp313-cp313t-win_amd64.whl", hash = "sha256:15eea9f306b98e0be91eb344a94c0e630689ef302e10c2ce5f7e11905c704f9c", size = 12928582, upload-time = "2025-10-15T16:16:57.943Z" }, - { url = "https://files.pythonhosted.org/packages/67/63/871fad5f0073fc00fbbdd7232962ea1ac40eeaae2bba66c76214f7954236/numpy-2.3.4-cp313-cp313t-win_arm64.whl", hash = "sha256:b6c231c9c2fadbae4011ca5e7e83e12dc4a5072f1a1d85a0a7b3ed754d145a40", size = 10266691, upload-time = "2025-10-15T16:17:00.048Z" }, - { url = "https://files.pythonhosted.org/packages/72/71/ae6170143c115732470ae3a2d01512870dd16e0953f8a6dc89525696069b/numpy-2.3.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:81c3e6d8c97295a7360d367f9f8553973651b76907988bb6066376bc2252f24e", size = 20955580, upload-time = "2025-10-15T16:17:02.509Z" }, - { url = "https://files.pythonhosted.org/packages/af/39/4be9222ffd6ca8a30eda033d5f753276a9c3426c397bb137d8e19dedd200/numpy-2.3.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7c26b0b2bf58009ed1f38a641f3db4be8d960a417ca96d14e5b06df1506d41ff", size = 14188056, upload-time = "2025-10-15T16:17:04.873Z" }, - { url = "https://files.pythonhosted.org/packages/6c/3d/d85f6700d0a4aa4f9491030e1021c2b2b7421b2b38d01acd16734a2bfdc7/numpy-2.3.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:62b2198c438058a20b6704351b35a1d7db881812d8512d67a69c9de1f18ca05f", size = 5116555, upload-time = "2025-10-15T16:17:07.499Z" }, - { url = "https://files.pythonhosted.org/packages/bf/04/82c1467d86f47eee8a19a464c92f90a9bb68ccf14a54c5224d7031241ffb/numpy-2.3.4-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:9d729d60f8d53a7361707f4b68a9663c968882dd4f09e0d58c044c8bf5faee7b", size = 6643581, upload-time = "2025-10-15T16:17:09.774Z" }, - { url = "https://files.pythonhosted.org/packages/0c/d3/c79841741b837e293f48bd7db89d0ac7a4f2503b382b78a790ef1dc778a5/numpy-2.3.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd0c630cf256b0a7fd9d0a11c9413b42fef5101219ce6ed5a09624f5a65392c7", size = 14299186, upload-time = "2025-10-15T16:17:11.937Z" }, - { url = "https://files.pythonhosted.org/packages/e8/7e/4a14a769741fbf237eec5a12a2cbc7a4c4e061852b6533bcb9e9a796c908/numpy-2.3.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5e081bc082825f8b139f9e9fe42942cb4054524598aaeb177ff476cc76d09d2", size = 16638601, upload-time = "2025-10-15T16:17:14.391Z" }, - { url = "https://files.pythonhosted.org/packages/93/87/1c1de269f002ff0a41173fe01dcc925f4ecff59264cd8f96cf3b60d12c9b/numpy-2.3.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:15fb27364ed84114438fff8aaf998c9e19adbeba08c0b75409f8c452a8692c52", size = 16074219, upload-time = "2025-10-15T16:17:17.058Z" }, - { url = "https://files.pythonhosted.org/packages/cd/28/18f72ee77408e40a76d691001ae599e712ca2a47ddd2c4f695b16c65f077/numpy-2.3.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:85d9fb2d8cd998c84d13a79a09cc0c1091648e848e4e6249b0ccd7f6b487fa26", size = 18576702, upload-time = "2025-10-15T16:17:19.379Z" }, - { url = "https://files.pythonhosted.org/packages/c3/76/95650169b465ececa8cf4b2e8f6df255d4bf662775e797ade2025cc51ae6/numpy-2.3.4-cp314-cp314-win32.whl", hash = "sha256:e73d63fd04e3a9d6bc187f5455d81abfad05660b212c8804bf3b407e984cd2bc", size = 6337136, upload-time = "2025-10-15T16:17:22.886Z" }, - { url = "https://files.pythonhosted.org/packages/dc/89/a231a5c43ede5d6f77ba4a91e915a87dea4aeea76560ba4d2bf185c683f0/numpy-2.3.4-cp314-cp314-win_amd64.whl", hash = "sha256:3da3491cee49cf16157e70f607c03a217ea6647b1cea4819c4f48e53d49139b9", size = 12920542, upload-time = "2025-10-15T16:17:24.783Z" }, - { url = "https://files.pythonhosted.org/packages/0d/0c/ae9434a888f717c5ed2ff2393b3f344f0ff6f1c793519fa0c540461dc530/numpy-2.3.4-cp314-cp314-win_arm64.whl", hash = "sha256:6d9cd732068e8288dbe2717177320723ccec4fb064123f0caf9bbd90ab5be868", size = 10480213, upload-time = "2025-10-15T16:17:26.935Z" }, - { url = "https://files.pythonhosted.org/packages/83/4b/c4a5f0841f92536f6b9592694a5b5f68c9ab37b775ff342649eadf9055d3/numpy-2.3.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:22758999b256b595cf0b1d102b133bb61866ba5ceecf15f759623b64c020c9ec", size = 21052280, upload-time = "2025-10-15T16:17:29.638Z" }, - { url = "https://files.pythonhosted.org/packages/3e/80/90308845fc93b984d2cc96d83e2324ce8ad1fd6efea81b324cba4b673854/numpy-2.3.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9cb177bc55b010b19798dc5497d540dea67fd13a8d9e882b2dae71de0cf09eb3", size = 14302930, upload-time = "2025-10-15T16:17:32.384Z" }, - { url = "https://files.pythonhosted.org/packages/3d/4e/07439f22f2a3b247cec4d63a713faae55e1141a36e77fb212881f7cda3fb/numpy-2.3.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:0f2bcc76f1e05e5ab58893407c63d90b2029908fa41f9f1cc51eecce936c3365", size = 5231504, upload-time = "2025-10-15T16:17:34.515Z" }, - { url = "https://files.pythonhosted.org/packages/ab/de/1e11f2547e2fe3d00482b19721855348b94ada8359aef5d40dd57bfae9df/numpy-2.3.4-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:8dc20bde86802df2ed8397a08d793da0ad7a5fd4ea3ac85d757bf5dd4ad7c252", size = 6739405, upload-time = "2025-10-15T16:17:36.128Z" }, - { url = "https://files.pythonhosted.org/packages/3b/40/8cd57393a26cebe2e923005db5134a946c62fa56a1087dc7c478f3e30837/numpy-2.3.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e199c087e2aa71c8f9ce1cb7a8e10677dc12457e7cc1be4798632da37c3e86e", size = 14354866, upload-time = "2025-10-15T16:17:38.884Z" }, - { url = "https://files.pythonhosted.org/packages/93/39/5b3510f023f96874ee6fea2e40dfa99313a00bf3ab779f3c92978f34aace/numpy-2.3.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85597b2d25ddf655495e2363fe044b0ae999b75bc4d630dc0d886484b03a5eb0", size = 16703296, upload-time = "2025-10-15T16:17:41.564Z" }, - { url = "https://files.pythonhosted.org/packages/41/0d/19bb163617c8045209c1996c4e427bccbc4bbff1e2c711f39203c8ddbb4a/numpy-2.3.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:04a69abe45b49c5955923cf2c407843d1c85013b424ae8a560bba16c92fe44a0", size = 16136046, upload-time = "2025-10-15T16:17:43.901Z" }, - { url = "https://files.pythonhosted.org/packages/e2/c1/6dba12fdf68b02a21ac411c9df19afa66bed2540f467150ca64d246b463d/numpy-2.3.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e1708fac43ef8b419c975926ce1eaf793b0c13b7356cfab6ab0dc34c0a02ac0f", size = 18652691, upload-time = "2025-10-15T16:17:46.247Z" }, - { url = "https://files.pythonhosted.org/packages/f8/73/f85056701dbbbb910c51d846c58d29fd46b30eecd2b6ba760fc8b8a1641b/numpy-2.3.4-cp314-cp314t-win32.whl", hash = "sha256:863e3b5f4d9915aaf1b8ec79ae560ad21f0b8d5e3adc31e73126491bb86dee1d", size = 6485782, upload-time = "2025-10-15T16:17:48.872Z" }, - { url = "https://files.pythonhosted.org/packages/17/90/28fa6f9865181cb817c2471ee65678afa8a7e2a1fb16141473d5fa6bacc3/numpy-2.3.4-cp314-cp314t-win_amd64.whl", hash = "sha256:962064de37b9aef801d33bc579690f8bfe6c5e70e29b61783f60bcba838a14d6", size = 13113301, upload-time = "2025-10-15T16:17:50.938Z" }, - { url = "https://files.pythonhosted.org/packages/54/23/08c002201a8e7e1f9afba93b97deceb813252d9cfd0d3351caed123dcf97/numpy-2.3.4-cp314-cp314t-win_arm64.whl", hash = "sha256:8b5a9a39c45d852b62693d9b3f3e0fe052541f804296ff401a72a1b60edafb29", size = 10547532, upload-time = "2025-10-15T16:17:53.48Z" }, +version = "2.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/9f/b8cef5bffa569759033adda9481211426f12f53299629b410340795c2514/numpy-2.4.4.tar.gz", hash = "sha256:2d390634c5182175533585cc89f3608a4682ccb173cc9bb940b2881c8d6f8fa0", size = 20731587, upload-time = "2026-03-29T13:22:01.298Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/05/32396bec30fb2263770ee910142f49c1476d08e8ad41abf8403806b520ce/numpy-2.4.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15716cfef24d3a9762e3acdf87e27f58dc823d1348f765bbea6bef8c639bfa1b", size = 16689272, upload-time = "2026-03-29T13:18:49.223Z" }, + { url = "https://files.pythonhosted.org/packages/c5/f3/a983d28637bfcd763a9c7aafdb6d5c0ebf3d487d1e1459ffdb57e2f01117/numpy-2.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23cbfd4c17357c81021f21540da84ee282b9c8fba38a03b7b9d09ba6b951421e", size = 14699573, upload-time = "2026-03-29T13:18:52.629Z" }, + { url = "https://files.pythonhosted.org/packages/9b/fd/e5ecca1e78c05106d98028114f5c00d3eddb41207686b2b7de3e477b0e22/numpy-2.4.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:8b3b60bb7cba2c8c81837661c488637eee696f59a877788a396d33150c35d842", size = 5204782, upload-time = "2026-03-29T13:18:55.579Z" }, + { url = "https://files.pythonhosted.org/packages/de/2f/702a4594413c1a8632092beae8aba00f1d67947389369b3777aed783fdca/numpy-2.4.4-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:e4a010c27ff6f210ff4c6ef34394cd61470d01014439b192ec22552ee867f2a8", size = 6552038, upload-time = "2026-03-29T13:18:57.769Z" }, + { url = "https://files.pythonhosted.org/packages/7f/37/eed308a8f56cba4d1fdf467a4fc67ef4ff4bf1c888f5fc980481890104b1/numpy-2.4.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9e75681b59ddaa5e659898085ae0eaea229d054f2ac0c7e563a62205a700121", size = 15670666, upload-time = "2026-03-29T13:19:00.341Z" }, + { url = "https://files.pythonhosted.org/packages/0a/0d/0e3ecece05b7a7e87ab9fb587855548da437a061326fff64a223b6dcb78a/numpy-2.4.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:81f4a14bee47aec54f883e0cad2d73986640c1590eb9bfaaba7ad17394481e6e", size = 16645480, upload-time = "2026-03-29T13:19:03.63Z" }, + { url = "https://files.pythonhosted.org/packages/34/49/f2312c154b82a286758ee2f1743336d50651f8b5195db18cdb63675ff649/numpy-2.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:62d6b0f03b694173f9fcb1fb317f7222fd0b0b103e784c6549f5e53a27718c44", size = 17020036, upload-time = "2026-03-29T13:19:07.428Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e9/736d17bd77f1b0ec4f9901aaec129c00d59f5d84d5e79bba540ef12c2330/numpy-2.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fbc356aae7adf9e6336d336b9c8111d390a05df88f1805573ebb0807bd06fd1d", size = 18368643, upload-time = "2026-03-29T13:19:10.775Z" }, + { url = "https://files.pythonhosted.org/packages/63/f6/d417977c5f519b17c8a5c3bc9e8304b0908b0e21136fe43bf628a1343914/numpy-2.4.4-cp312-cp312-win32.whl", hash = "sha256:0d35aea54ad1d420c812bfa0385c71cd7cc5bcf7c65fed95fc2cd02fe8c79827", size = 5961117, upload-time = "2026-03-29T13:19:13.464Z" }, + { url = "https://files.pythonhosted.org/packages/2d/5b/e1deebf88ff431b01b7406ca3583ab2bbb90972bbe1c568732e49c844f7e/numpy-2.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:b5f0362dc928a6ecd9db58868fca5e48485205e3855957bdedea308f8672ea4a", size = 12320584, upload-time = "2026-03-29T13:19:16.155Z" }, + { url = "https://files.pythonhosted.org/packages/58/89/e4e856ac82a68c3ed64486a544977d0e7bdd18b8da75b78a577ca31c4395/numpy-2.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:846300f379b5b12cc769334464656bc882e0735d27d9726568bc932fdc49d5ec", size = 10221450, upload-time = "2026-03-29T13:19:18.994Z" }, + { url = "https://files.pythonhosted.org/packages/14/1d/d0a583ce4fefcc3308806a749a536c201ed6b5ad6e1322e227ee4848979d/numpy-2.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:08f2e31ed5e6f04b118e49821397f12767934cfdd12a1ce86a058f91e004ee50", size = 16684933, upload-time = "2026-03-29T13:19:22.47Z" }, + { url = "https://files.pythonhosted.org/packages/c1/62/2b7a48fbb745d344742c0277f01286dead15f3f68e4f359fbfcf7b48f70f/numpy-2.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e823b8b6edc81e747526f70f71a9c0a07ac4e7ad13020aa736bb7c9d67196115", size = 14694532, upload-time = "2026-03-29T13:19:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/e5/87/499737bfba066b4a3bebff24a8f1c5b2dee410b209bc6668c9be692580f0/numpy-2.4.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4a19d9dba1a76618dd86b164d608566f393f8ec6ac7c44f0cc879011c45e65af", size = 5199661, upload-time = "2026-03-29T13:19:28.31Z" }, + { url = "https://files.pythonhosted.org/packages/cd/da/464d551604320d1491bc345efed99b4b7034143a85787aab78d5691d5a0e/numpy-2.4.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:d2a8490669bfe99a233298348acc2d824d496dee0e66e31b66a6022c2ad74a5c", size = 6547539, upload-time = "2026-03-29T13:19:30.97Z" }, + { url = "https://files.pythonhosted.org/packages/7d/90/8d23e3b0dafd024bf31bdec225b3bb5c2dbfa6912f8a53b8659f21216cbf/numpy-2.4.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45dbed2ab436a9e826e302fcdcbe9133f9b0006e5af7168afb8963a6520da103", size = 15668806, upload-time = "2026-03-29T13:19:33.887Z" }, + { url = "https://files.pythonhosted.org/packages/d1/73/a9d864e42a01896bb5974475438f16086be9ba1f0d19d0bb7a07427c4a8b/numpy-2.4.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c901b15172510173f5cb310eae652908340f8dede90fff9e3bf6c0d8dfd92f83", size = 16632682, upload-time = "2026-03-29T13:19:37.336Z" }, + { url = "https://files.pythonhosted.org/packages/34/fb/14570d65c3bde4e202a031210475ae9cde9b7686a2e7dc97ee67d2833b35/numpy-2.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:99d838547ace2c4aace6c4f76e879ddfe02bb58a80c1549928477862b7a6d6ed", size = 17019810, upload-time = "2026-03-29T13:19:40.963Z" }, + { url = "https://files.pythonhosted.org/packages/8a/77/2ba9d87081fd41f6d640c83f26fb7351e536b7ce6dd9061b6af5904e8e46/numpy-2.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0aec54fd785890ecca25a6003fd9a5aed47ad607bbac5cd64f836ad8666f4959", size = 18357394, upload-time = "2026-03-29T13:19:44.859Z" }, + { url = "https://files.pythonhosted.org/packages/a2/23/52666c9a41708b0853fa3b1a12c90da38c507a3074883823126d4e9d5b30/numpy-2.4.4-cp313-cp313-win32.whl", hash = "sha256:07077278157d02f65c43b1b26a3886bce886f95d20aabd11f87932750dfb14ed", size = 5959556, upload-time = "2026-03-29T13:19:47.661Z" }, + { url = "https://files.pythonhosted.org/packages/57/fb/48649b4971cde70d817cf97a2a2fdc0b4d8308569f1dd2f2611959d2e0cf/numpy-2.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:5c70f1cc1c4efbe316a572e2d8b9b9cc44e89b95f79ca3331553fbb63716e2bf", size = 12317311, upload-time = "2026-03-29T13:19:50.67Z" }, + { url = "https://files.pythonhosted.org/packages/ba/d8/11490cddd564eb4de97b4579ef6bfe6a736cc07e94c1598590ae25415e01/numpy-2.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:ef4059d6e5152fa1a39f888e344c73fdc926e1b2dd58c771d67b0acfbf2aa67d", size = 10222060, upload-time = "2026-03-29T13:19:54.229Z" }, + { url = "https://files.pythonhosted.org/packages/99/5d/dab4339177a905aad3e2221c915b35202f1ec30d750dd2e5e9d9a72b804b/numpy-2.4.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4bbc7f303d125971f60ec0aaad5e12c62d0d2c925f0ab1273debd0e4ba37aba5", size = 14822302, upload-time = "2026-03-29T13:19:57.585Z" }, + { url = "https://files.pythonhosted.org/packages/eb/e4/0564a65e7d3d97562ed6f9b0fd0fb0a6f559ee444092f105938b50043876/numpy-2.4.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:4d6d57903571f86180eb98f8f0c839fa9ebbfb031356d87f1361be91e433f5b7", size = 5327407, upload-time = "2026-03-29T13:20:00.601Z" }, + { url = "https://files.pythonhosted.org/packages/29/8d/35a3a6ce5ad371afa58b4700f1c820f8f279948cca32524e0a695b0ded83/numpy-2.4.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:4636de7fd195197b7535f231b5de9e4b36d2c440b6e566d2e4e4746e6af0ca93", size = 6647631, upload-time = "2026-03-29T13:20:02.855Z" }, + { url = "https://files.pythonhosted.org/packages/f4/da/477731acbd5a58a946c736edfdabb2ac5b34c3d08d1ba1a7b437fa0884df/numpy-2.4.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ad2e2ef14e0b04e544ea2fa0a36463f847f113d314aa02e5b402fdf910ef309e", size = 15727691, upload-time = "2026-03-29T13:20:06.004Z" }, + { url = "https://files.pythonhosted.org/packages/e6/db/338535d9b152beabeb511579598418ba0212ce77cf9718edd70262cc4370/numpy-2.4.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a285b3b96f951841799528cd1f4f01cd70e7e0204b4abebac9463eecfcf2a40", size = 16681241, upload-time = "2026-03-29T13:20:09.417Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a9/ad248e8f58beb7a0219b413c9c7d8151c5d285f7f946c3e26695bdbbe2df/numpy-2.4.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f8474c4241bc18b750be2abea9d7a9ec84f46ef861dbacf86a4f6e043401f79e", size = 17085767, upload-time = "2026-03-29T13:20:13.126Z" }, + { url = "https://files.pythonhosted.org/packages/b5/1a/3b88ccd3694681356f70da841630e4725a7264d6a885c8d442a697e1146b/numpy-2.4.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4e874c976154687c1f71715b034739b45c7711bec81db01914770373d125e392", size = 18403169, upload-time = "2026-03-29T13:20:17.096Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c9/fcfd5d0639222c6eac7f304829b04892ef51c96a75d479214d77e3ce6e33/numpy-2.4.4-cp313-cp313t-win32.whl", hash = "sha256:9c585a1790d5436a5374bac930dad6ed244c046ed91b2b2a3634eb2971d21008", size = 6083477, upload-time = "2026-03-29T13:20:20.195Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e3/3938a61d1c538aaec8ed6fd6323f57b0c2d2d2219512434c5c878db76553/numpy-2.4.4-cp313-cp313t-win_amd64.whl", hash = "sha256:93e15038125dc1e5345d9b5b68aa7f996ec33b98118d18c6ca0d0b7d6198b7e8", size = 12457487, upload-time = "2026-03-29T13:20:22.946Z" }, + { url = "https://files.pythonhosted.org/packages/97/6a/7e345032cc60501721ef94e0e30b60f6b0bd601f9174ebd36389a2b86d40/numpy-2.4.4-cp313-cp313t-win_arm64.whl", hash = "sha256:0dfd3f9d3adbe2920b68b5cd3d51444e13a10792ec7154cd0a2f6e74d4ab3233", size = 10292002, upload-time = "2026-03-29T13:20:25.909Z" }, + { url = "https://files.pythonhosted.org/packages/6e/06/c54062f85f673dd5c04cbe2f14c3acb8c8b95e3384869bb8cc9bff8cb9df/numpy-2.4.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f169b9a863d34f5d11b8698ead99febeaa17a13ca044961aa8e2662a6c7766a0", size = 16684353, upload-time = "2026-03-29T13:20:29.504Z" }, + { url = "https://files.pythonhosted.org/packages/4c/39/8a320264a84404c74cc7e79715de85d6130fa07a0898f67fb5cd5bd79908/numpy-2.4.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2483e4584a1cb3092da4470b38866634bafb223cbcd551ee047633fd2584599a", size = 14704914, upload-time = "2026-03-29T13:20:33.547Z" }, + { url = "https://files.pythonhosted.org/packages/91/fb/287076b2614e1d1044235f50f03748f31fa287e3dbe6abeb35cdfa351eca/numpy-2.4.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:2d19e6e2095506d1736b7d80595e0f252d76b89f5e715c35e06e937679ea7d7a", size = 5210005, upload-time = "2026-03-29T13:20:36.45Z" }, + { url = "https://files.pythonhosted.org/packages/63/eb/fcc338595309910de6ecabfcef2419a9ce24399680bfb149421fa2df1280/numpy-2.4.4-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:6a246d5914aa1c820c9443ddcee9c02bec3e203b0c080349533fae17727dfd1b", size = 6544974, upload-time = "2026-03-29T13:20:39.014Z" }, + { url = "https://files.pythonhosted.org/packages/44/5d/e7e9044032a716cdfaa3fba27a8e874bf1c5f1912a1ddd4ed071bf8a14a6/numpy-2.4.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:989824e9faf85f96ec9c7761cd8d29c531ad857bfa1daa930cba85baaecf1a9a", size = 15684591, upload-time = "2026-03-29T13:20:42.146Z" }, + { url = "https://files.pythonhosted.org/packages/98/7c/21252050676612625449b4807d6b695b9ce8a7c9e1c197ee6216c8a65c7c/numpy-2.4.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27a8d92cd10f1382a67d7cf4db7ce18341b66438bdd9f691d7b0e48d104c2a9d", size = 16637700, upload-time = "2026-03-29T13:20:46.204Z" }, + { url = "https://files.pythonhosted.org/packages/b1/29/56d2bbef9465db24ef25393383d761a1af4f446a1df9b8cded4fe3a5a5d7/numpy-2.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e44319a2953c738205bf3354537979eaa3998ed673395b964c1176083dd46252", size = 17035781, upload-time = "2026-03-29T13:20:50.242Z" }, + { url = "https://files.pythonhosted.org/packages/e3/2b/a35a6d7589d21f44cea7d0a98de5ddcbb3d421b2622a5c96b1edf18707c3/numpy-2.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e892aff75639bbef0d2a2cfd55535510df26ff92f63c92cd84ef8d4ba5a5557f", size = 18362959, upload-time = "2026-03-29T13:20:54.019Z" }, + { url = "https://files.pythonhosted.org/packages/64/c9/d52ec581f2390e0f5f85cbfd80fb83d965fc15e9f0e1aec2195faa142cde/numpy-2.4.4-cp314-cp314-win32.whl", hash = "sha256:1378871da56ca8943c2ba674530924bb8ca40cd228358a3b5f302ad60cf875fc", size = 6008768, upload-time = "2026-03-29T13:20:56.912Z" }, + { url = "https://files.pythonhosted.org/packages/fa/22/4cc31a62a6c7b74a8730e31a4274c5dc80e005751e277a2ce38e675e4923/numpy-2.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:715d1c092715954784bc79e1174fc2a90093dc4dc84ea15eb14dad8abdcdeb74", size = 12449181, upload-time = "2026-03-29T13:20:59.548Z" }, + { url = "https://files.pythonhosted.org/packages/70/2e/14cda6f4d8e396c612d1bf97f22958e92148801d7e4f110cabebdc0eef4b/numpy-2.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:2c194dd721e54ecad9ad387c1d35e63dce5c4450c6dc7dd5611283dda239aabb", size = 10496035, upload-time = "2026-03-29T13:21:02.524Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e8/8fed8c8d848d7ecea092dc3469643f9d10bc3a134a815a3b033da1d2039b/numpy-2.4.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2aa0613a5177c264ff5921051a5719d20095ea586ca88cc802c5c218d1c67d3e", size = 14824958, upload-time = "2026-03-29T13:21:05.671Z" }, + { url = "https://files.pythonhosted.org/packages/05/1a/d8007a5138c179c2bf33ef44503e83d70434d2642877ee8fbb230e7c0548/numpy-2.4.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:42c16925aa5a02362f986765f9ebabf20de75cdefdca827d14315c568dcab113", size = 5330020, upload-time = "2026-03-29T13:21:08.635Z" }, + { url = "https://files.pythonhosted.org/packages/99/64/ffb99ac6ae93faf117bcbd5c7ba48a7f45364a33e8e458545d3633615dda/numpy-2.4.4-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:874f200b2a981c647340f841730fc3a2b54c9d940566a3c4149099591e2c4c3d", size = 6650758, upload-time = "2026-03-29T13:21:10.949Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6e/795cc078b78a384052e73b2f6281ff7a700e9bf53bcce2ee579d4f6dd879/numpy-2.4.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9b39d38a9bd2ae1becd7eac1303d031c5c110ad31f2b319c6e7d98b135c934d", size = 15729948, upload-time = "2026-03-29T13:21:14.047Z" }, + { url = "https://files.pythonhosted.org/packages/5f/86/2acbda8cc2af5f3d7bfc791192863b9e3e19674da7b5e533fded124d1299/numpy-2.4.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b268594bccac7d7cf5844c7732e3f20c50921d94e36d7ec9b79e9857694b1b2f", size = 16679325, upload-time = "2026-03-29T13:21:17.561Z" }, + { url = "https://files.pythonhosted.org/packages/bc/59/cafd83018f4aa55e0ac6fa92aa066c0a1877b77a615ceff1711c260ffae8/numpy-2.4.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ac6b31e35612a26483e20750126d30d0941f949426974cace8e6b5c58a3657b0", size = 17084883, upload-time = "2026-03-29T13:21:21.106Z" }, + { url = "https://files.pythonhosted.org/packages/f0/85/a42548db84e65ece46ab2caea3d3f78b416a47af387fcbb47ec28e660dc2/numpy-2.4.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8e3ed142f2728df44263aaf5fb1f5b0b99f4070c553a0d7f033be65338329150", size = 18403474, upload-time = "2026-03-29T13:21:24.828Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ad/483d9e262f4b831000062e5d8a45e342166ec8aaa1195264982bca267e62/numpy-2.4.4-cp314-cp314t-win32.whl", hash = "sha256:dddbbd259598d7240b18c9d87c56a9d2fb3b02fe266f49a7c101532e78c1d871", size = 6155500, upload-time = "2026-03-29T13:21:28.205Z" }, + { url = "https://files.pythonhosted.org/packages/c7/03/2fc4e14c7bd4ff2964b74ba90ecb8552540b6315f201df70f137faa5c589/numpy-2.4.4-cp314-cp314t-win_amd64.whl", hash = "sha256:a7164afb23be6e37ad90b2f10426149fd75aee07ca55653d2aa41e66c4ef697e", size = 12637755, upload-time = "2026-03-29T13:21:31.107Z" }, + { url = "https://files.pythonhosted.org/packages/58/78/548fb8e07b1a341746bfbecb32f2c268470f45fa028aacdbd10d9bc73aab/numpy-2.4.4-cp314-cp314t-win_arm64.whl", hash = "sha256:ba203255017337d39f89bdd58417f03c4426f12beed0440cfd933cb15f8669c7", size = 10566643, upload-time = "2026-03-29T13:21:34.339Z" }, ] [[package]] name = "p2api" -version = "1.0.10" +version = "1.0.11" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "keyring" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fd/94/96ece6cf22552578a16f82ccedabc1d067f65ba871cfec60324a2eb51d12/p2api-1.0.10.tar.gz", hash = "sha256:9d9d41d978fb5143c9eb1e7119bd8bd7ff1c0f74b267d2fef425e59c717b35ef", size = 25017, upload-time = "2024-09-12T12:56:51.453Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/54/2181f0b2c85c3515d01b8a99fe70bd8e1dc0c673a4f6f1dbd512f26ce441/p2api-1.0.11.tar.gz", hash = "sha256:779fcb6f5502a210eba39e64026348dcbcfe256208afde5d199645d9d2b943d6", size = 24994, upload-time = "2026-02-19T09:41:41.206Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/a3/07ff4a0be52675076fea31558ed4c4e880887df606bcdd395ac8b2802298/p2api-1.0.10-py2.py3-none-any.whl", hash = "sha256:0d1d1edfbbc9202174147fa34117522db6c27af4b4d3e86adad7458f2df6965b", size = 26400, upload-time = "2024-09-12T12:56:49.809Z" }, + { url = "https://files.pythonhosted.org/packages/1f/ce/7158780102f6878279ee358d36f8ad13e45061a0a8c3bf552d37cfe61c22/p2api-1.0.11-py2.py3-none-any.whl", hash = "sha256:8eb1b51c0cee59b3292434dffc36c4b0b34c787b1728aeba73c036d017d8901c", size = 26415, upload-time = "2026-02-19T09:41:40.154Z" }, ] [[package]] name = "packaging" -version = "25.0" +version = "26.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, ] [[package]] @@ -657,18 +695,32 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] +[[package]] +name = "pyastrosalt" +version = "0.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "defusedxml" }, + { name = "requests" }, + { name = "types-defusedxml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8d/1c/27756fc0b4e058fc9f9d6b5246661a285c671e9e8008236a6fecb3e17220/pyastrosalt-0.2.2.tar.gz", hash = "sha256:ae6debbc552f3592260fde1504e59d46ca56dd82400cd59e3a03f9a4b324e637", size = 18419, upload-time = "2026-05-07T09:48:10.25Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/9f/469888b597ad0017f78228efef10017c9cd7c1f5077c2fa20c9805734dd0/pyastrosalt-0.2.2-py3-none-any.whl", hash = "sha256:7ed089864166b3fd1fe5c79936e2f8f3b547aa696c899bf86d701368a82590e7", size = 14198, upload-time = "2026-05-07T09:48:11.183Z" }, +] + [[package]] name = "pycparser" -version = "2.23" +version = "3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, ] [[package]] name = "pydantic" -version = "2.12.3" +version = "2.13.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, @@ -676,90 +728,98 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f3/1e/4f0a3233767010308f2fd6bd0814597e3f63f1dc98304a9112b8759df4ff/pydantic-2.12.3.tar.gz", hash = "sha256:1da1c82b0fc140bb0103bc1441ffe062154c8d38491189751ee00fd8ca65ce74", size = 819383, upload-time = "2025-10-17T15:04:21.222Z" } +sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a1/6b/83661fa77dcefa195ad5f8cd9af3d1a7450fd57cc883ad04d65446ac2029/pydantic-2.12.3-py3-none-any.whl", hash = "sha256:6986454a854bc3bc6e5443e1369e06a3a456af9d339eda45510f517d9ea5c6bf", size = 462431, upload-time = "2025-10-17T15:04:19.346Z" }, + { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, ] [[package]] name = "pydantic-core" -version = "2.41.4" +version = "2.46.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/df/18/d0944e8eaaa3efd0a91b0f1fc537d3be55ad35091b6a87638211ba691964/pydantic_core-2.41.4.tar.gz", hash = "sha256:70e47929a9d4a1905a67e4b687d5946026390568a8e952b92824118063cee4d5", size = 457557, upload-time = "2025-10-14T10:23:47.909Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/81/d3b3e95929c4369d30b2a66a91db63c8ed0a98381ae55a45da2cd1cc1288/pydantic_core-2.41.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ab06d77e053d660a6faaf04894446df7b0a7e7aba70c2797465a0a1af00fc887", size = 2099043, upload-time = "2025-10-14T10:20:28.561Z" }, - { url = "https://files.pythonhosted.org/packages/58/da/46fdac49e6717e3a94fc9201403e08d9d61aa7a770fab6190b8740749047/pydantic_core-2.41.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c53ff33e603a9c1179a9364b0a24694f183717b2e0da2b5ad43c316c956901b2", size = 1910699, upload-time = "2025-10-14T10:20:30.217Z" }, - { url = "https://files.pythonhosted.org/packages/1e/63/4d948f1b9dd8e991a5a98b77dd66c74641f5f2e5225fee37994b2e07d391/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:304c54176af2c143bd181d82e77c15c41cbacea8872a2225dd37e6544dce9999", size = 1952121, upload-time = "2025-10-14T10:20:32.246Z" }, - { url = "https://files.pythonhosted.org/packages/b2/a7/e5fc60a6f781fc634ecaa9ecc3c20171d238794cef69ae0af79ac11b89d7/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:025ba34a4cf4fb32f917d5d188ab5e702223d3ba603be4d8aca2f82bede432a4", size = 2041590, upload-time = "2025-10-14T10:20:34.332Z" }, - { url = "https://files.pythonhosted.org/packages/70/69/dce747b1d21d59e85af433428978a1893c6f8a7068fa2bb4a927fba7a5ff/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b9f5f30c402ed58f90c70e12eff65547d3ab74685ffe8283c719e6bead8ef53f", size = 2219869, upload-time = "2025-10-14T10:20:35.965Z" }, - { url = "https://files.pythonhosted.org/packages/83/6a/c070e30e295403bf29c4df1cb781317b6a9bac7cd07b8d3acc94d501a63c/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd96e5d15385d301733113bcaa324c8bcf111275b7675a9c6e88bfb19fc05e3b", size = 2345169, upload-time = "2025-10-14T10:20:37.627Z" }, - { url = "https://files.pythonhosted.org/packages/f0/83/06d001f8043c336baea7fd202a9ac7ad71f87e1c55d8112c50b745c40324/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98f348cbb44fae6e9653c1055db7e29de67ea6a9ca03a5fa2c2e11a47cff0e47", size = 2070165, upload-time = "2025-10-14T10:20:39.246Z" }, - { url = "https://files.pythonhosted.org/packages/14/0a/e567c2883588dd12bcbc110232d892cf385356f7c8a9910311ac997ab715/pydantic_core-2.41.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ec22626a2d14620a83ca583c6f5a4080fa3155282718b6055c2ea48d3ef35970", size = 2189067, upload-time = "2025-10-14T10:20:41.015Z" }, - { url = "https://files.pythonhosted.org/packages/f4/1d/3d9fca34273ba03c9b1c5289f7618bc4bd09c3ad2289b5420481aa051a99/pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3a95d4590b1f1a43bf33ca6d647b990a88f4a3824a8c4572c708f0b45a5290ed", size = 2132997, upload-time = "2025-10-14T10:20:43.106Z" }, - { url = "https://files.pythonhosted.org/packages/52/70/d702ef7a6cd41a8afc61f3554922b3ed8d19dd54c3bd4bdbfe332e610827/pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:f9672ab4d398e1b602feadcffcdd3af44d5f5e6ddc15bc7d15d376d47e8e19f8", size = 2307187, upload-time = "2025-10-14T10:20:44.849Z" }, - { url = "https://files.pythonhosted.org/packages/68/4c/c06be6e27545d08b802127914156f38d10ca287a9e8489342793de8aae3c/pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:84d8854db5f55fead3b579f04bda9a36461dab0730c5d570e1526483e7bb8431", size = 2305204, upload-time = "2025-10-14T10:20:46.781Z" }, - { url = "https://files.pythonhosted.org/packages/b0/e5/35ae4919bcd9f18603419e23c5eaf32750224a89d41a8df1a3704b69f77e/pydantic_core-2.41.4-cp312-cp312-win32.whl", hash = "sha256:9be1c01adb2ecc4e464392c36d17f97e9110fbbc906bcbe1c943b5b87a74aabd", size = 1972536, upload-time = "2025-10-14T10:20:48.39Z" }, - { url = "https://files.pythonhosted.org/packages/1e/c2/49c5bb6d2a49eb2ee3647a93e3dae7080c6409a8a7558b075027644e879c/pydantic_core-2.41.4-cp312-cp312-win_amd64.whl", hash = "sha256:d682cf1d22bab22a5be08539dca3d1593488a99998f9f412137bc323179067ff", size = 2031132, upload-time = "2025-10-14T10:20:50.421Z" }, - { url = "https://files.pythonhosted.org/packages/06/23/936343dbcba6eec93f73e95eb346810fc732f71ba27967b287b66f7b7097/pydantic_core-2.41.4-cp312-cp312-win_arm64.whl", hash = "sha256:833eebfd75a26d17470b58768c1834dfc90141b7afc6eb0429c21fc5a21dcfb8", size = 1969483, upload-time = "2025-10-14T10:20:52.35Z" }, - { url = "https://files.pythonhosted.org/packages/13/d0/c20adabd181a029a970738dfe23710b52a31f1258f591874fcdec7359845/pydantic_core-2.41.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:85e050ad9e5f6fe1004eec65c914332e52f429bc0ae12d6fa2092407a462c746", size = 2105688, upload-time = "2025-10-14T10:20:54.448Z" }, - { url = "https://files.pythonhosted.org/packages/00/b6/0ce5c03cec5ae94cca220dfecddc453c077d71363b98a4bbdb3c0b22c783/pydantic_core-2.41.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7393f1d64792763a48924ba31d1e44c2cfbc05e3b1c2c9abb4ceeadd912cced", size = 1910807, upload-time = "2025-10-14T10:20:56.115Z" }, - { url = "https://files.pythonhosted.org/packages/68/3e/800d3d02c8beb0b5c069c870cbb83799d085debf43499c897bb4b4aaff0d/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94dab0940b0d1fb28bcab847adf887c66a27a40291eedf0b473be58761c9799a", size = 1956669, upload-time = "2025-10-14T10:20:57.874Z" }, - { url = "https://files.pythonhosted.org/packages/60/a4/24271cc71a17f64589be49ab8bd0751f6a0a03046c690df60989f2f95c2c/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:de7c42f897e689ee6f9e93c4bec72b99ae3b32a2ade1c7e4798e690ff5246e02", size = 2051629, upload-time = "2025-10-14T10:21:00.006Z" }, - { url = "https://files.pythonhosted.org/packages/68/de/45af3ca2f175d91b96bfb62e1f2d2f1f9f3b14a734afe0bfeff079f78181/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:664b3199193262277b8b3cd1e754fb07f2c6023289c815a1e1e8fb415cb247b1", size = 2224049, upload-time = "2025-10-14T10:21:01.801Z" }, - { url = "https://files.pythonhosted.org/packages/af/8f/ae4e1ff84672bf869d0a77af24fd78387850e9497753c432875066b5d622/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d95b253b88f7d308b1c0b417c4624f44553ba4762816f94e6986819b9c273fb2", size = 2342409, upload-time = "2025-10-14T10:21:03.556Z" }, - { url = "https://files.pythonhosted.org/packages/18/62/273dd70b0026a085c7b74b000394e1ef95719ea579c76ea2f0cc8893736d/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1351f5bbdbbabc689727cb91649a00cb9ee7203e0a6e54e9f5ba9e22e384b84", size = 2069635, upload-time = "2025-10-14T10:21:05.385Z" }, - { url = "https://files.pythonhosted.org/packages/30/03/cf485fff699b4cdaea469bc481719d3e49f023241b4abb656f8d422189fc/pydantic_core-2.41.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1affa4798520b148d7182da0615d648e752de4ab1a9566b7471bc803d88a062d", size = 2194284, upload-time = "2025-10-14T10:21:07.122Z" }, - { url = "https://files.pythonhosted.org/packages/f9/7e/c8e713db32405dfd97211f2fc0a15d6bf8adb7640f3d18544c1f39526619/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7b74e18052fea4aa8dea2fb7dbc23d15439695da6cbe6cfc1b694af1115df09d", size = 2137566, upload-time = "2025-10-14T10:21:08.981Z" }, - { url = "https://files.pythonhosted.org/packages/04/f7/db71fd4cdccc8b75990f79ccafbbd66757e19f6d5ee724a6252414483fb4/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:285b643d75c0e30abda9dc1077395624f314a37e3c09ca402d4015ef5979f1a2", size = 2316809, upload-time = "2025-10-14T10:21:10.805Z" }, - { url = "https://files.pythonhosted.org/packages/76/63/a54973ddb945f1bca56742b48b144d85c9fc22f819ddeb9f861c249d5464/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:f52679ff4218d713b3b33f88c89ccbf3a5c2c12ba665fb80ccc4192b4608dbab", size = 2311119, upload-time = "2025-10-14T10:21:12.583Z" }, - { url = "https://files.pythonhosted.org/packages/f8/03/5d12891e93c19218af74843a27e32b94922195ded2386f7b55382f904d2f/pydantic_core-2.41.4-cp313-cp313-win32.whl", hash = "sha256:ecde6dedd6fff127c273c76821bb754d793be1024bc33314a120f83a3c69460c", size = 1981398, upload-time = "2025-10-14T10:21:14.584Z" }, - { url = "https://files.pythonhosted.org/packages/be/d8/fd0de71f39db91135b7a26996160de71c073d8635edfce8b3c3681be0d6d/pydantic_core-2.41.4-cp313-cp313-win_amd64.whl", hash = "sha256:d081a1f3800f05409ed868ebb2d74ac39dd0c1ff6c035b5162356d76030736d4", size = 2030735, upload-time = "2025-10-14T10:21:16.432Z" }, - { url = "https://files.pythonhosted.org/packages/72/86/c99921c1cf6650023c08bfab6fe2d7057a5142628ef7ccfa9921f2dda1d5/pydantic_core-2.41.4-cp313-cp313-win_arm64.whl", hash = "sha256:f8e49c9c364a7edcbe2a310f12733aad95b022495ef2a8d653f645e5d20c1564", size = 1973209, upload-time = "2025-10-14T10:21:18.213Z" }, - { url = "https://files.pythonhosted.org/packages/36/0d/b5706cacb70a8414396efdda3d72ae0542e050b591119e458e2490baf035/pydantic_core-2.41.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ed97fd56a561f5eb5706cebe94f1ad7c13b84d98312a05546f2ad036bafe87f4", size = 1877324, upload-time = "2025-10-14T10:21:20.363Z" }, - { url = "https://files.pythonhosted.org/packages/de/2d/cba1fa02cfdea72dfb3a9babb067c83b9dff0bbcb198368e000a6b756ea7/pydantic_core-2.41.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a870c307bf1ee91fc58a9a61338ff780d01bfae45922624816878dce784095d2", size = 1884515, upload-time = "2025-10-14T10:21:22.339Z" }, - { url = "https://files.pythonhosted.org/packages/07/ea/3df927c4384ed9b503c9cc2d076cf983b4f2adb0c754578dfb1245c51e46/pydantic_core-2.41.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d25e97bc1f5f8f7985bdc2335ef9e73843bb561eb1fa6831fdfc295c1c2061cf", size = 2042819, upload-time = "2025-10-14T10:21:26.683Z" }, - { url = "https://files.pythonhosted.org/packages/6a/ee/df8e871f07074250270a3b1b82aad4cd0026b588acd5d7d3eb2fcb1471a3/pydantic_core-2.41.4-cp313-cp313t-win_amd64.whl", hash = "sha256:d405d14bea042f166512add3091c1af40437c2e7f86988f3915fabd27b1e9cd2", size = 1995866, upload-time = "2025-10-14T10:21:28.951Z" }, - { url = "https://files.pythonhosted.org/packages/fc/de/b20f4ab954d6d399499c33ec4fafc46d9551e11dc1858fb7f5dca0748ceb/pydantic_core-2.41.4-cp313-cp313t-win_arm64.whl", hash = "sha256:19f3684868309db5263a11bace3c45d93f6f24afa2ffe75a647583df22a2ff89", size = 1970034, upload-time = "2025-10-14T10:21:30.869Z" }, - { url = "https://files.pythonhosted.org/packages/54/28/d3325da57d413b9819365546eb9a6e8b7cbd9373d9380efd5f74326143e6/pydantic_core-2.41.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:e9205d97ed08a82ebb9a307e92914bb30e18cdf6f6b12ca4bedadb1588a0bfe1", size = 2102022, upload-time = "2025-10-14T10:21:32.809Z" }, - { url = "https://files.pythonhosted.org/packages/9e/24/b58a1bc0d834bf1acc4361e61233ee217169a42efbdc15a60296e13ce438/pydantic_core-2.41.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:82df1f432b37d832709fbcc0e24394bba04a01b6ecf1ee87578145c19cde12ac", size = 1905495, upload-time = "2025-10-14T10:21:34.812Z" }, - { url = "https://files.pythonhosted.org/packages/fb/a4/71f759cc41b7043e8ecdaab81b985a9b6cad7cec077e0b92cff8b71ecf6b/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc3b4cc4539e055cfa39a3763c939f9d409eb40e85813257dcd761985a108554", size = 1956131, upload-time = "2025-10-14T10:21:36.924Z" }, - { url = "https://files.pythonhosted.org/packages/b0/64/1e79ac7aa51f1eec7c4cda8cbe456d5d09f05fdd68b32776d72168d54275/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b1eb1754fce47c63d2ff57fdb88c351a6c0150995890088b33767a10218eaa4e", size = 2052236, upload-time = "2025-10-14T10:21:38.927Z" }, - { url = "https://files.pythonhosted.org/packages/e9/e3/a3ffc363bd4287b80f1d43dc1c28ba64831f8dfc237d6fec8f2661138d48/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6ab5ab30ef325b443f379ddb575a34969c333004fca5a1daa0133a6ffaad616", size = 2223573, upload-time = "2025-10-14T10:21:41.574Z" }, - { url = "https://files.pythonhosted.org/packages/28/27/78814089b4d2e684a9088ede3790763c64693c3d1408ddc0a248bc789126/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:31a41030b1d9ca497634092b46481b937ff9397a86f9f51bd41c4767b6fc04af", size = 2342467, upload-time = "2025-10-14T10:21:44.018Z" }, - { url = "https://files.pythonhosted.org/packages/92/97/4de0e2a1159cb85ad737e03306717637842c88c7fd6d97973172fb183149/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a44ac1738591472c3d020f61c6df1e4015180d6262ebd39bf2aeb52571b60f12", size = 2063754, upload-time = "2025-10-14T10:21:46.466Z" }, - { url = "https://files.pythonhosted.org/packages/0f/50/8cb90ce4b9efcf7ae78130afeb99fd1c86125ccdf9906ef64b9d42f37c25/pydantic_core-2.41.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d72f2b5e6e82ab8f94ea7d0d42f83c487dc159c5240d8f83beae684472864e2d", size = 2196754, upload-time = "2025-10-14T10:21:48.486Z" }, - { url = "https://files.pythonhosted.org/packages/34/3b/ccdc77af9cd5082723574a1cc1bcae7a6acacc829d7c0a06201f7886a109/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:c4d1e854aaf044487d31143f541f7aafe7b482ae72a022c664b2de2e466ed0ad", size = 2137115, upload-time = "2025-10-14T10:21:50.63Z" }, - { url = "https://files.pythonhosted.org/packages/ca/ba/e7c7a02651a8f7c52dc2cff2b64a30c313e3b57c7d93703cecea76c09b71/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b568af94267729d76e6ee5ececda4e283d07bbb28e8148bb17adad93d025d25a", size = 2317400, upload-time = "2025-10-14T10:21:52.959Z" }, - { url = "https://files.pythonhosted.org/packages/2c/ba/6c533a4ee8aec6b812c643c49bb3bd88d3f01e3cebe451bb85512d37f00f/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6d55fb8b1e8929b341cc313a81a26e0d48aa3b519c1dbaadec3a6a2b4fcad025", size = 2312070, upload-time = "2025-10-14T10:21:55.419Z" }, - { url = "https://files.pythonhosted.org/packages/22/ae/f10524fcc0ab8d7f96cf9a74c880243576fd3e72bd8ce4f81e43d22bcab7/pydantic_core-2.41.4-cp314-cp314-win32.whl", hash = "sha256:5b66584e549e2e32a1398df11da2e0a7eff45d5c2d9db9d5667c5e6ac764d77e", size = 1982277, upload-time = "2025-10-14T10:21:57.474Z" }, - { url = "https://files.pythonhosted.org/packages/b4/dc/e5aa27aea1ad4638f0c3fb41132f7eb583bd7420ee63204e2d4333a3bbf9/pydantic_core-2.41.4-cp314-cp314-win_amd64.whl", hash = "sha256:557a0aab88664cc552285316809cab897716a372afaf8efdbef756f8b890e894", size = 2024608, upload-time = "2025-10-14T10:21:59.557Z" }, - { url = "https://files.pythonhosted.org/packages/3e/61/51d89cc2612bd147198e120a13f150afbf0bcb4615cddb049ab10b81b79e/pydantic_core-2.41.4-cp314-cp314-win_arm64.whl", hash = "sha256:3f1ea6f48a045745d0d9f325989d8abd3f1eaf47dd00485912d1a3a63c623a8d", size = 1967614, upload-time = "2025-10-14T10:22:01.847Z" }, - { url = "https://files.pythonhosted.org/packages/0d/c2/472f2e31b95eff099961fa050c376ab7156a81da194f9edb9f710f68787b/pydantic_core-2.41.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6c1fe4c5404c448b13188dd8bd2ebc2bdd7e6727fa61ff481bcc2cca894018da", size = 1876904, upload-time = "2025-10-14T10:22:04.062Z" }, - { url = "https://files.pythonhosted.org/packages/4a/07/ea8eeb91173807ecdae4f4a5f4b150a520085b35454350fc219ba79e66a3/pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:523e7da4d43b113bf8e7b49fa4ec0c35bf4fe66b2230bfc5c13cc498f12c6c3e", size = 1882538, upload-time = "2025-10-14T10:22:06.39Z" }, - { url = "https://files.pythonhosted.org/packages/1e/29/b53a9ca6cd366bfc928823679c6a76c7a4c69f8201c0ba7903ad18ebae2f/pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5729225de81fb65b70fdb1907fcf08c75d498f4a6f15af005aabb1fdadc19dfa", size = 2041183, upload-time = "2025-10-14T10:22:08.812Z" }, - { url = "https://files.pythonhosted.org/packages/c7/3d/f8c1a371ceebcaf94d6dd2d77c6cf4b1c078e13a5837aee83f760b4f7cfd/pydantic_core-2.41.4-cp314-cp314t-win_amd64.whl", hash = "sha256:de2cfbb09e88f0f795fd90cf955858fc2c691df65b1f21f0aa00b99f3fbc661d", size = 1993542, upload-time = "2025-10-14T10:22:11.332Z" }, - { url = "https://files.pythonhosted.org/packages/8a/ac/9fc61b4f9d079482a290afe8d206b8f490e9fd32d4fc03ed4fc698214e01/pydantic_core-2.41.4-cp314-cp314t-win_arm64.whl", hash = "sha256:d34f950ae05a83e0ede899c595f312ca976023ea1db100cd5aa188f7005e3ab0", size = 1973897, upload-time = "2025-10-14T10:22:13.444Z" }, - { url = "https://files.pythonhosted.org/packages/c4/48/ae937e5a831b7c0dc646b2ef788c27cd003894882415300ed21927c21efa/pydantic_core-2.41.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:4f5d640aeebb438517150fdeec097739614421900e4a08db4a3ef38898798537", size = 2112087, upload-time = "2025-10-14T10:22:56.818Z" }, - { url = "https://files.pythonhosted.org/packages/5e/db/6db8073e3d32dae017da7e0d16a9ecb897d0a4d92e00634916e486097961/pydantic_core-2.41.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:4a9ab037b71927babc6d9e7fc01aea9e66dc2a4a34dff06ef0724a4049629f94", size = 1920387, upload-time = "2025-10-14T10:22:59.342Z" }, - { url = "https://files.pythonhosted.org/packages/0d/c1/dd3542d072fcc336030d66834872f0328727e3b8de289c662faa04aa270e/pydantic_core-2.41.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4dab9484ec605c3016df9ad4fd4f9a390bc5d816a3b10c6550f8424bb80b18c", size = 1951495, upload-time = "2025-10-14T10:23:02.089Z" }, - { url = "https://files.pythonhosted.org/packages/2b/c6/db8d13a1f8ab3f1eb08c88bd00fd62d44311e3456d1e85c0e59e0a0376e7/pydantic_core-2.41.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8a5028425820731d8c6c098ab642d7b8b999758e24acae03ed38a66eca8335", size = 2139008, upload-time = "2025-10-14T10:23:04.539Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" }, + { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" }, + { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, + { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, + { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, + { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, + { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" }, + { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" }, + { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, + { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, + { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, + { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, + { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, + { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, + { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, + { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, + { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, + { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, + { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" }, + { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" }, + { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" }, + { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" }, + { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" }, + { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" }, + { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" }, + { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" }, + { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" }, + { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" }, + { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" }, + { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" }, + { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" }, + { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" }, + { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" }, + { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" }, + { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" }, + { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, + { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" }, + { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, ] [[package]] name = "pydantic-settings" -version = "2.11.0" +version = "2.9.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, { name = "python-dotenv" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/20/c5/dbbc27b814c71676593d1c3f718e6cd7d4f00652cefa24b75f7aa3efb25e/pydantic_settings-2.11.0.tar.gz", hash = "sha256:d0e87a1c7d33593beb7194adb8470fc426e95ba02af83a0f23474a04c9a08180", size = 188394, upload-time = "2025-09-24T14:19:11.764Z" } +sdist = { url = "https://files.pythonhosted.org/packages/67/1d/42628a2c33e93f8e9acbde0d5d735fa0850f3e6a2f8cb1eb6c40b9a732ac/pydantic_settings-2.9.1.tar.gz", hash = "sha256:c509bf79d27563add44e8446233359004ed85066cd096d8b510f715e6ef5d268", size = 163234, upload-time = "2025-04-18T16:44:48.265Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/83/d6/887a1ff844e64aa823fb4905978d882a633cfe295c32eacad582b78a7d8b/pydantic_settings-2.11.0-py3-none-any.whl", hash = "sha256:fe2cea3413b9530d10f3a5875adffb17ada5c1e1bab0b2885546d7310415207c", size = 48608, upload-time = "2025-09-24T14:19:10.015Z" }, + { url = "https://files.pythonhosted.org/packages/b6/5f/d6d641b490fd3ec2c4c13b4244d68deea3a1b970a97be64f34fb5504ff72/pydantic_settings-2.9.1-py3-none-any.whl", hash = "sha256:59b4f431b1defb26fe620c71a7d3968a710d719f5f4cdbbdb7926edeb770f6ef", size = 44356, upload-time = "2025-04-18T16:44:46.617Z" }, ] [[package]] @@ -782,16 +842,16 @@ wheels = [ [[package]] name = "pygments" -version = "2.19.2" +version = "2.20.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, ] [[package]] name = "pytest" -version = "8.4.2" +version = "9.0.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -800,18 +860,18 @@ dependencies = [ { name = "pluggy" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, + { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, ] [[package]] name = "python-dotenv" -version = "1.1.1" +version = "1.2.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978, upload-time = "2025-06-24T04:21:07.341Z" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" }, + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, ] [[package]] @@ -871,7 +931,7 @@ wheels = [ [[package]] name = "requests" -version = "2.32.5" +version = "2.33.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, @@ -879,31 +939,31 @@ dependencies = [ { name = "idna" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", size = 134120, upload-time = "2026-03-30T16:09:15.531Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, + { url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" }, ] [[package]] name = "secretstorage" -version = "3.4.0" +version = "3.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, { name = "jeepney" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/31/9f/11ef35cf1027c1339552ea7bfe6aaa74a8516d8b5caf6e7d338daf54fd80/secretstorage-3.4.0.tar.gz", hash = "sha256:c46e216d6815aff8a8a18706a2fbfd8d53fcbb0dce99301881687a1b0289ef7c", size = 19748, upload-time = "2025-09-09T16:42:13.859Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", size = 19884, upload-time = "2025-11-23T19:02:53.191Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/91/ff/2e2eed29e02c14a5cb6c57f09b2d5b40e65d6cc71f45b52e0be295ccbc2f/secretstorage-3.4.0-py3-none-any.whl", hash = "sha256:0e3b6265c2c63509fb7415717607e4b2c9ab767b7f344a57473b779ca13bd02e", size = 15272, upload-time = "2025-09-09T16:42:12.744Z" }, + { url = "https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137", size = 15554, upload-time = "2025-11-23T19:02:51.545Z" }, ] [[package]] -name = "sniffio" -version = "1.3.1" +name = "soupsieve" +version = "2.8.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/ae/2d9c981590ed9999a0d91755b47fc74f74de286b0f5cee14c9269041e6c4/soupsieve-2.8.3.tar.gz", hash = "sha256:3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349", size = 118627, upload-time = "2026-01-20T04:27:02.457Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, + { url = "https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95", size = 37016, upload-time = "2026-01-20T04:27:01.012Z" }, ] [[package]] @@ -924,6 +984,78 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5b/48/6ea42c749608cfca883d5d8fae03edbcde636090d2bc751fd5da9157b451/textcase-0.4.5-py3-none-any.whl", hash = "sha256:bd5d6aaf653b339e3ac60ad96cfc960a94219a97da464eddeed7084f41774937", size = 6503, upload-time = "2025-10-10T15:11:24.294Z" }, ] +[[package]] +name = "time-machine" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/02/fc/37b02f6094dbb1f851145330460532176ed2f1dc70511a35828166c41e52/time_machine-3.2.0.tar.gz", hash = "sha256:a4ddd1cea17b8950e462d1805a42b20c81eb9aafc8f66b392dd5ce997e037d79", size = 14804, upload-time = "2025-12-17T23:33:02.599Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/8b/080c8eedcd67921a52ba5bd0e075362062509ab63c86fc1a0442fad241a6/time_machine-3.2.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:cc4bee5b0214d7dc4ebc91f4a4c600f1a598e9b5606ac751f42cb6f6740b1dbb", size = 19255, upload-time = "2025-12-17T23:31:58.057Z" }, + { url = "https://files.pythonhosted.org/packages/66/17/0e5291e9eb705bf8a5a1305f826e979af307bbeb79def4ddbf4b3f9a81e0/time_machine-3.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3ca036304b4460ae2fdc1b52dd8b1fa7cf1464daa427fc49567413c09aa839c1", size = 15360, upload-time = "2025-12-17T23:31:59.048Z" }, + { url = "https://files.pythonhosted.org/packages/8b/e8/9ab87b71d2e2b62463b9b058b7ae7ac09fb57f8fcd88729dec169d304340/time_machine-3.2.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5442735b41d7a2abc2f04579b4ca6047ed4698a8338a4fec92c7c9423e7938cb", size = 33029, upload-time = "2025-12-17T23:32:00.413Z" }, + { url = "https://files.pythonhosted.org/packages/4b/26/b5ca19da6f25ea905b3e10a0ea95d697c1aeba0404803a43c68f1af253e6/time_machine-3.2.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:97da3e971e505cb637079fb07ab0bcd36e33279f8ecac888ff131f45ef1e4d8d", size = 34579, upload-time = "2025-12-17T23:32:01.431Z" }, + { url = "https://files.pythonhosted.org/packages/79/ca/6ac7ad5f10ea18cc1d9de49716ba38c32132c7b64532430d92ef240c116b/time_machine-3.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3cdda6dee4966e38aeb487309bb414c6cb23a81fc500291c77a8fcd3098832e7", size = 35961, upload-time = "2025-12-17T23:32:02.521Z" }, + { url = "https://files.pythonhosted.org/packages/33/67/390dd958bed395ab32d79a9fe61fe111825c0dd4ded54dbba7e867f171e6/time_machine-3.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:33d9efd302a6998bcc8baa4d84f259f8a4081105bd3d7f7af7f1d0abd3b1c8aa", size = 34668, upload-time = "2025-12-17T23:32:03.585Z" }, + { url = "https://files.pythonhosted.org/packages/da/57/c88fff034a4e9538b3ae7c68c9cfb283670b14d17522c5a8bc17d29f9a4b/time_machine-3.2.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3a0b0a33971f14145853c9bd95a6ab0353cf7e0019fa2a7aa1ae9fddfe8eab50", size = 32891, upload-time = "2025-12-17T23:32:04.656Z" }, + { url = "https://files.pythonhosted.org/packages/2d/70/ebbb76022dba0fec8f9156540fc647e4beae1680c787c01b1b6200e56d70/time_machine-3.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2d0be9e5f22c38082d247a2cdcd8a936504e9db60b7b3606855fb39f299e9548", size = 34080, upload-time = "2025-12-17T23:32:06.146Z" }, + { url = "https://files.pythonhosted.org/packages/db/9a/2ca9e7af3df540dc1c79e3de588adeddb7dcc2107829248e6969c4f14167/time_machine-3.2.0-cp312-cp312-win32.whl", hash = "sha256:3f74623648b936fdce5f911caf386c0a0b579456410975de8c0dfeaaffece1d8", size = 17371, upload-time = "2025-12-17T23:32:07.164Z" }, + { url = "https://files.pythonhosted.org/packages/d8/ce/21d23efc9c2151939af1b7ee4e60d86d661b74ef32b8eaa148f6fe8c899c/time_machine-3.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:34e26a41d994b5e4b205136a90e9578470386749cc9a2ecf51ca18f83ce25e23", size = 18132, upload-time = "2025-12-17T23:32:08.447Z" }, + { url = "https://files.pythonhosted.org/packages/2f/34/c2b70be483accf6db9e5d6c3139bce3c38fe51f898ccf64e8d3fe14fbf4d/time_machine-3.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:0615d3d82c418d6293f271c348945c5091a71f37e37173653d5c26d0e74b13a8", size = 16930, upload-time = "2025-12-17T23:32:09.477Z" }, + { url = "https://files.pythonhosted.org/packages/ee/cd/43ad5efc88298af3c59b66769cea7f055567a85071579ed40536188530c1/time_machine-3.2.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c421a8eb85a4418a7675a41bf8660224318c46cc62e4751c8f1ceca752059090", size = 19318, upload-time = "2025-12-17T23:32:10.518Z" }, + { url = "https://files.pythonhosted.org/packages/b0/f6/084010ef7f4a3f38b5a4900923d7c85b29e797655c4f6ee4ce54d903cca8/time_machine-3.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f4e758f7727d0058c4950c66b58200c187072122d6f7a98b610530a4233ea7b", size = 15390, upload-time = "2025-12-17T23:32:11.625Z" }, + { url = "https://files.pythonhosted.org/packages/25/aa/1cabb74134f492270dc6860cb7865859bf40ecf828be65972827646e91ad/time_machine-3.2.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:154bd3f75c81f70218b2585cc12b60762fb2665c507eec5ec5037d8756d9b4e0", size = 33115, upload-time = "2025-12-17T23:32:13.219Z" }, + { url = "https://files.pythonhosted.org/packages/5e/03/78c5d7dfa366924eb4dbfcc3fc917c39a4280ca234b12819cc1f16c03d88/time_machine-3.2.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d50cfe5ebea422c896ad8d278af9648412b7533b8ea6adeeee698a3fd9b1d3b7", size = 34705, upload-time = "2025-12-17T23:32:14.29Z" }, + { url = "https://files.pythonhosted.org/packages/86/93/d5e877c24541f674c6869ff6e9c56833369796010190252e92c9d7ae5f0f/time_machine-3.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:636576501724bd6a9124e69d86e5aef263479e89ef739c5db361469f0463a0a1", size = 36104, upload-time = "2025-12-17T23:32:15.354Z" }, + { url = "https://files.pythonhosted.org/packages/22/1c/d4bae72f388f67efc9609f89b012e434bb19d9549c7a7b47d6c7d9e5c55d/time_machine-3.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:40e6f40c57197fcf7ec32d2c563f4df0a82c42cdcc3cab27f688e98f6060df10", size = 34765, upload-time = "2025-12-17T23:32:16.434Z" }, + { url = "https://files.pythonhosted.org/packages/1d/c3/ac378cf301d527d8dfad2f0db6bad0dfb1ab73212eaa56d6b96ee5d9d20b/time_machine-3.2.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a1bcf0b846bbfc19a79bc19e3fa04d8c7b1e8101c1b70340ffdb689cd801ea53", size = 33010, upload-time = "2025-12-17T23:32:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/06/35/7ce897319accda7a6970b288a9a8c52d25227342a7508505a2b3d235b649/time_machine-3.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ae55a56c179f4fe7a62575ad5148b6ed82f6c7e5cf2f9a9ec65f2f5b067db5f5", size = 34185, upload-time = "2025-12-17T23:32:18.566Z" }, + { url = "https://files.pythonhosted.org/packages/bf/28/f922022269749cb02eee2b62919671153c4088994fa955a6b0e50327ff81/time_machine-3.2.0-cp313-cp313-win32.whl", hash = "sha256:a66fe55a107e46916007a391d4030479df8864ec6ad6f6a6528221befc5c886e", size = 17397, upload-time = "2025-12-17T23:32:19.605Z" }, + { url = "https://files.pythonhosted.org/packages/ee/dc/fd87cde397f4a7bea493152f0aca8fd569ec709cad9e0f2ca7011eb8c7f7/time_machine-3.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:30c9ce57165df913e4f74e285a8ab829ff9b7aa3e5ec0973f88f642b9a7b3d15", size = 18139, upload-time = "2025-12-17T23:32:20.991Z" }, + { url = "https://files.pythonhosted.org/packages/75/81/b8ce58233addc5d7d54d2fabc49dcbc02d79e3f079d150aa1bec3d5275ef/time_machine-3.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:89cad7e179e9bdcc84dcf09efe52af232c4cc7a01b3de868356bbd59d95bd9b8", size = 16964, upload-time = "2025-12-17T23:32:22.075Z" }, + { url = "https://files.pythonhosted.org/packages/67/e7/487f0ba5fe6c58186a5e1af2a118dfa2c160fedb37ef53a7e972d410408e/time_machine-3.2.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:59d71545e62525a4b85b6de9ab5c02ee3c61110fd7f636139914a2335dcbfc9c", size = 20000, upload-time = "2025-12-17T23:32:23.058Z" }, + { url = "https://files.pythonhosted.org/packages/e1/17/eb2c0054c8d44dd42df84ccd434539249a9c7d0b8eb53f799be2102500ab/time_machine-3.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:999672c621c35362bc28e03ca0c7df21500195540773c25993421fd8d6cc5003", size = 15657, upload-time = "2025-12-17T23:32:24.125Z" }, + { url = "https://files.pythonhosted.org/packages/43/21/93443b5d1dd850f8bb9442e90d817a9033dcce6bfbdd3aabbb9786251c80/time_machine-3.2.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5faf7397f0580c7b9d67288522c8d7863e85f0cffadc0f1fccdb2c3dfce5783e", size = 39216, upload-time = "2025-12-17T23:32:25.542Z" }, + { url = "https://files.pythonhosted.org/packages/9f/9e/18544cf8acc72bb1dc03762231c82ecc259733f4bb6770a7bbe5cd138603/time_machine-3.2.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d3dd886ec49f1fa5a00e844f5947e5c0f98ce574750c24b7424c6f77fc1c3e87", size = 40764, upload-time = "2025-12-17T23:32:26.643Z" }, + { url = "https://files.pythonhosted.org/packages/27/f7/9fe9ce2795636a3a7467307af6bdf38bb613ddb701a8a5cd50ec713beb5e/time_machine-3.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:da0ecd96bc7bbe450acaaabe569d84e81688f1be8ad58d1470e42371d145fb53", size = 43526, upload-time = "2025-12-17T23:32:27.693Z" }, + { url = "https://files.pythonhosted.org/packages/03/c1/a93e975ba9dec22e87ec92d18c28e67d36bd536f9119ffa439b2892b0c9c/time_machine-3.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:158220e946c1c4fb8265773a0282c88c35a7e3bb5d78e3561214e3b3231166f3", size = 41727, upload-time = "2025-12-17T23:32:28.985Z" }, + { url = "https://files.pythonhosted.org/packages/5f/fb/e3633e5a6bbed1c76bb2e9810dabc2f8467532ffcd29b9aed404b473061a/time_machine-3.2.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8c1aee29bc54356f248d5d7dfdd131e12ca825e850a08c0ebdb022266d073013", size = 38952, upload-time = "2025-12-17T23:32:30.031Z" }, + { url = "https://files.pythonhosted.org/packages/82/3d/02e9fb2526b3d6b1b45bc8e4d912d95d1cd699d1a3f6df985817d37a0600/time_machine-3.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c8ed2224f09d25b1c2fc98683613aca12f90f682a427eabb68fc824d27014e4a", size = 39829, upload-time = "2025-12-17T23:32:31.075Z" }, + { url = "https://files.pythonhosted.org/packages/85/c8/c14265212436da8e0814c45463987b3f57de3eca4de023cc2eabb0c62ef3/time_machine-3.2.0-cp313-cp313t-win32.whl", hash = "sha256:3498719f8dab51da76d29a20c1b5e52ee7db083dddf3056af7fa69c1b94e1fe6", size = 17852, upload-time = "2025-12-17T23:32:32.079Z" }, + { url = "https://files.pythonhosted.org/packages/1d/bc/8acb13cf6149f47508097b158a9a8bec9ec4530a70cb406124e8023581f5/time_machine-3.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:e0d90bee170b219e1d15e6a58164aa808f5170090e4f090bd0670303e34181b1", size = 18918, upload-time = "2025-12-17T23:32:33.106Z" }, + { url = "https://files.pythonhosted.org/packages/24/87/c443ee508c2708fd2514ccce9052f5e48888783ce690506919629ebc8eb0/time_machine-3.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:051de220fdb6e20d648111bbad423d9506fdbb2e44d4429cef3dc0382abf1fc2", size = 17261, upload-time = "2025-12-17T23:32:34.446Z" }, + { url = "https://files.pythonhosted.org/packages/61/70/b4b980d126ed155c78d1879c50d60c8dcbd47bd11cb14ee7be50e0dfc07f/time_machine-3.2.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:1398980c017fe5744d66f419e0115ee48a53b00b146d738e1416c225eb610b82", size = 19303, upload-time = "2025-12-17T23:32:35.796Z" }, + { url = "https://files.pythonhosted.org/packages/73/73/eaa33603c69a68fe2b6f54f9dd75481693d62f1d29676531002be06e2d1c/time_machine-3.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:4f8f4e35f4191ef70c2ab8ff490761ee9051b891afce2bf86dde3918eb7b537b", size = 15431, upload-time = "2025-12-17T23:32:37.244Z" }, + { url = "https://files.pythonhosted.org/packages/76/10/b81e138e86cc7bab40cdb59d294b341e172201f4a6c84bb0ec080407977a/time_machine-3.2.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6db498686ecf6163c5aa8cf0bcd57bbe0f4081184f247edf3ee49a2612b584f9", size = 33206, upload-time = "2025-12-17T23:32:38.713Z" }, + { url = "https://files.pythonhosted.org/packages/d3/72/4deab446b579e8bd5dca91de98595c5d6bd6a17ce162abf5c5f2ce40d3d8/time_machine-3.2.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:027c1807efb74d0cd58ad16524dec94212fbe900115d70b0123399883657ac0f", size = 34792, upload-time = "2025-12-17T23:32:40.223Z" }, + { url = "https://files.pythonhosted.org/packages/2c/39/439c6b587ddee76d533fe972289d0646e0a5520e14dc83d0a30aeb5565f7/time_machine-3.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92432610c05676edd5e6946a073c6f0c926923123ce7caee1018dc10782c713d", size = 36187, upload-time = "2025-12-17T23:32:41.705Z" }, + { url = "https://files.pythonhosted.org/packages/4b/db/2da4368db15180989bab83746a857bde05ad16e78f326801c142bb747a06/time_machine-3.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c25586b62480eb77ef3d953fba273209478e1ef49654592cd6a52a68dfe56a67", size = 34855, upload-time = "2025-12-17T23:32:42.817Z" }, + { url = "https://files.pythonhosted.org/packages/88/84/120a431fee50bc4c241425bee4d3a4910df4923b7ab5f7dff1bf0c772f08/time_machine-3.2.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:6bf3a2fa738d15e0b95d14469a0b8ea42635467408d8b490e263d5d45c9a177f", size = 33222, upload-time = "2025-12-17T23:32:43.94Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ea/89cfda82bb8c57ff91bb9a26751aa234d6d90e9b4d5ab0ad9dce0f9f0329/time_machine-3.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ce76b82276d7ad2a66cdc85dad4df19d1422b69183170a34e8fbc4c3f35502f7", size = 34270, upload-time = "2025-12-17T23:32:45.037Z" }, + { url = "https://files.pythonhosted.org/packages/8a/aa/235357da4f69a51a8d35fcbfcfa77cdc7dc24f62ae54025006570bda7e2d/time_machine-3.2.0-cp314-cp314-win32.whl", hash = "sha256:14d6778273c543441863dff712cd1d7803dee946b18de35921eb8df10714539d", size = 17544, upload-time = "2025-12-17T23:32:46.099Z" }, + { url = "https://files.pythonhosted.org/packages/7b/51/6c8405a7276be79693b792cff22ce41067ec05db26a7d02f2d5b06324434/time_machine-3.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:cbf821da96dbc80d349fa9e7c36e670b41d68a878d28c8850057992fed430eef", size = 18423, upload-time = "2025-12-17T23:32:47.468Z" }, + { url = "https://files.pythonhosted.org/packages/d9/03/a3cf419e20c35fc203c6e4fed48b5b667c1a2b4da456d9971e605f73ecef/time_machine-3.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:71c75d71f8e68abc8b669bca26ed2ddd558430a6c171e32b8620288565f18c0e", size = 17050, upload-time = "2025-12-17T23:32:48.91Z" }, + { url = "https://files.pythonhosted.org/packages/86/a1/142de946dc4393f910bf4564b5c3ba819906e1f49b06c9cb557519c849e4/time_machine-3.2.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:4e374779021446fc2b5c29d80457ec9a3b1a5df043dc2aae07d7c1415d52323c", size = 19991, upload-time = "2025-12-17T23:32:49.933Z" }, + { url = "https://files.pythonhosted.org/packages/ee/62/7f17def6289901f94726921811a16b9adce46e666362c75d45730c60274f/time_machine-3.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:122310a6af9c36e9a636da32830e591e7923e8a07bdd0a43276c3a36c6821c90", size = 15707, upload-time = "2025-12-17T23:32:50.969Z" }, + { url = "https://files.pythonhosted.org/packages/5d/d3/3502fb9bd3acb159c18844b26c43220201a0d4a622c0c853785d07699a92/time_machine-3.2.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ba3eeb0f018cc362dd8128befa3426696a2e16dd223c3fb695fde184892d4d8c", size = 39207, upload-time = "2025-12-17T23:32:52.033Z" }, + { url = "https://files.pythonhosted.org/packages/5a/be/8b27f4aa296fda14a5a2ad7f588ddd450603c33415ab3f8e85b2f1a44678/time_machine-3.2.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:77d38ba664b381a7793f8786efc13b5004f0d5f672dae814430445b8202a67a6", size = 40764, upload-time = "2025-12-17T23:32:53.167Z" }, + { url = "https://files.pythonhosted.org/packages/42/cd/fe4c4e5c8ab6d48fab3624c32be9116fb120173a35fe67e482e5cf68b3d2/time_machine-3.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f09abeb8f03f044d72712207e0489a62098ad3ad16dac38927fcf80baca4d6a7", size = 43508, upload-time = "2025-12-17T23:32:54.597Z" }, + { url = "https://files.pythonhosted.org/packages/b4/28/5a3ba2fce85b97655a425d6bb20a441550acd2b304c96b2c19d3839f721a/time_machine-3.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6b28367ce4f73987a55e230e1d30a57a3af85da8eb1a140074eb6e8c7e6ef19f", size = 41712, upload-time = "2025-12-17T23:32:55.781Z" }, + { url = "https://files.pythonhosted.org/packages/81/58/e38084be7fdabb4835db68a3a47e58c34182d79fc35df1ecbe0db2c5359f/time_machine-3.2.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:903c7751c904581da9f7861c3015bed7cdc40047321291d3694a3cdc783bbca3", size = 38939, upload-time = "2025-12-17T23:32:56.867Z" }, + { url = "https://files.pythonhosted.org/packages/40/d0/ad3feb0a392ef4e0c08bc32024950373ddc0669002cbdcbb9f3bf0c2d114/time_machine-3.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:528217cad85ede5f85c8bc78b0341868d3c3cfefc6ecb5b622e1cacb6c73247b", size = 39837, upload-time = "2025-12-17T23:32:58.283Z" }, + { url = "https://files.pythonhosted.org/packages/5b/9e/5f4b2ea63b267bd78f3245e76f5528836611b5f2d30b5e7300a722fe4428/time_machine-3.2.0-cp314-cp314t-win32.whl", hash = "sha256:75724762ffd517e7e80aaec1fad1ff5a7414bd84e2b3ee7a0bacfeb67c14926e", size = 18091, upload-time = "2025-12-17T23:32:59.403Z" }, + { url = "https://files.pythonhosted.org/packages/39/6f/456b1f4d2700ae02b19eba830f870596a4b89b74bac3b6c80666f1b108c5/time_machine-3.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2526abbd053c5bca898d1b3e7898eec34626b12206718d8c7ce88fd12c1c9c5c", size = 19208, upload-time = "2025-12-17T23:33:00.488Z" }, + { url = "https://files.pythonhosted.org/packages/2f/22/8063101427ecd3d2652aada4d21d0876b07a3dc789125bca2ee858fec3ed/time_machine-3.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:7f2fb6784b414edbe2c0b558bfaab0c251955ba27edd62946cce4a01675a992c", size = 17359, upload-time = "2025-12-17T23:33:01.54Z" }, +] + +[[package]] +name = "types-defusedxml" +version = "0.7.0.20260504" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/da/d2/4553c8fa9cdebfe1e84b950cf790a4d2376a97fa016e43f7c65323fa6c7d/types_defusedxml-0.7.0.20260504.tar.gz", hash = "sha256:2ab2828a3f97111ba1c16cee273ad4124a831fc9198c41bf8368ff6ea48ad300", size = 10729, upload-time = "2026-05-04T05:22:50.192Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/2d/8a4b5ba1d732b8bc691f0efbb1c6f77cb5f6f473b2afe181d83d910773c5/types_defusedxml-0.7.0.20260504-py3-none-any.whl", hash = "sha256:a959e3a0a43b93e464bd625d91ec9193a2703ddc10443bdd627792485fae0b10", size = 13467, upload-time = "2026-05-04T05:22:49.319Z" }, +] + [[package]] name = "typing-extensions" version = "4.15.0" @@ -947,9 +1079,9 @@ wheels = [ [[package]] name = "urllib3" -version = "2.5.0" +version = "2.6.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, ]