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" },
]