replace tomlib with tomlkit

tomlkit can read and write toml files
This commit is contained in:
Marc Koch 2025-03-21 13:22:23 +01:00
parent 3effd5558f
commit 0d4d6d3f46
Signed by: marc.koch
GPG key ID: 12406554CFB028B9
8 changed files with 129 additions and 117 deletions

2
.gitignore vendored
View file

@ -1,5 +1,5 @@
adgroupsync_config.yml adgroupsync_config.toml
# Created by https://www.toptal.com/developers/gitignore/api/pycharm+all # Created by https://www.toptal.com/developers/gitignore/api/pycharm+all
# Edit at https://www.toptal.com/developers/gitignore?templates=pycharm+all # Edit at https://www.toptal.com/developers/gitignore?templates=pycharm+all

View file

@ -14,7 +14,7 @@ pipx install --include-deps --index-url https://git.propeace.de/api/packages/Pro
Create a new configuration file: Create a new configuration file:
```bash ```bash
adgroupsync --create-config --conf /path/to/adgroupsync_config.yaml adgroupsync --create-config --conf /path/to/adgroupsync_config.toml
``` ```
Edit the configuration file and set the following values: Edit the configuration file and set the following values:
@ -22,37 +22,39 @@ Edit the configuration file and set the following values:
### AD Configuration ### AD Configuration
- `AD.DOMAIN`: The domain of the Active Directory server. - `AD.DOMAIN`: The domain of the Active Directory server.
- `AD.USER`: The username of the user to connect to the Active Directory server. - `AD.LDAP_SERVER`: List of LDAP servers to connect to.
- `AD.PASSWORD`: The password of the user to connect to the Active Directory
server.
- `AD.LDAP_SERVER`: The LDAP server of the Active Directory server.
- `AD.PARENT_GROUP`: The parent group in Active Directory that contains all - `AD.PARENT_GROUP`: The parent group in Active Directory that contains all
groups that should be synchronized. groups that should be synchronized.
- `AD.TIMEZONE`: The timezone of the Active Directory server. - `AD.TIMEZONE`: The timezone of the Active Directory server.
- `AD.USER`: The username of the user to connect to the Active Directory server.
- `AD.PASSWORD`: The password of the user to connect to the Active Directory
server.
### Civicrm Configuration ### Civicrm Configuration
- `CIVICRM.BASE_URL`: The URL of the CiviCRM server.
- `CIVICRM.API_KEY`: The API key of the CiviCRM user. - `CIVICRM.API_KEY`: The API key of the CiviCRM user.
- `CIVICRM.BASE_URL`: The URL of the CiviCRM server.
- `CIVICRM.BATCH_SIZE`: The batch size for the API requests to the CiviCRM - `CIVICRM.BATCH_SIZE`: The batch size for the API requests to the CiviCRM
server (only applied to contact sync). _DEFAULT: 50_ server (only applied to contact sync). _DEFAULT: 50_
- `CIVICRM.RETRIES`: The number of retries for the API requests to the CiviCRM - `CIVICRM.RETRIES`: The number of retries for the API requests to the CiviCRM
server. _DEFAULT: 3_ server. _DEFAULT: 3_
- `CIVICRM.IGNORE_SSL`: Allow insecure connections to the CiviCRM server.
_DEFAULT: False_
### Logging Configuration ### Logging Configuration
- `LOGGING.STDOUT_LOG_LEVEL`: The log level for the stdout logger. _DEFAULT: - `LOGGING.STDOUT_LOG_LEVEL`: The log level for the stdout logger. _DEFAULT:
INFO_ INFO_
- `LOGGING.FILE_LOG_LEVEL`: The log level for the file logger. _DEFAULT: INFO_ - `LOGGING.FILE_LOG_LEVEL`: The log level for the file logger. _DEFAULT: INFO_
- `LOGGING.LOG_DIR`: The path to the log file. _DEFAULT: - `LOGGING.LOG_DIR`: The directory to store the log file. _DEFAULT:
/var/log/adgroupsync.log_ `/var/log/adGroupSync/`
### NTFY (optional) ### NTFY (optional)
If you want to send notifications about failed syncs, you can If you want to send notifications about failed syncs, you can
configure [ntfy](https://ntfy.sh/). configure [ntfy](https://ntfy.sh/).
- `NTFY.URL`: The URL of the NTFY server. - `NTFY.URL`: The URL of the ntfy server.
- `NTFY.TOPIC`: The topic to post the message to. - `NTFY.TOPIC`: The topic to post the message to.
- `NTFY.ACCESS_TOKEN`: The access token for the NTFY server. - `NTFY.ACCESS_TOKEN`: The access token for the NTFY server.
@ -61,7 +63,7 @@ configure [ntfy](https://ntfy.sh/).
### Manual Sync ### Manual Sync
```bash ```bash
adgroupsync --conf /path/to/adgroupsync_config.yaml adgroupsync --conf /path/to/adgroupsync_config.toml
``` ```
### Cron Job ### Cron Job
@ -69,5 +71,5 @@ adgroupsync --conf /path/to/adgroupsync_config.yaml
Synchronize the groups every 10 minutes: Synchronize the groups every 10 minutes:
```bash ```bash
*/10 * * * * adgroupsync --conf /path/to/adgroupsync_config.yaml 2>&1 */10 * * * * adgroupsync --conf /path/to/adgroupsync_config.toml > /dev/null 2>&1
``` ```

View file

@ -17,8 +17,7 @@ dependencies = [
"civifang>=0.2.6", "civifang>=0.2.6",
"httpx>=0.28.1", "httpx>=0.28.1",
"ms-active-directory>=1.14.1", "ms-active-directory>=1.14.1",
"pyyaml>=6.0.2", "tomlkit>=0.13.2",
"tomli-w>=1.2.0",
"validators>=0.34.0", "validators>=0.34.0",
] ]

View file

@ -5,7 +5,6 @@ from pathlib import Path
from ldap3 import SIMPLE from ldap3 import SIMPLE
from ms_active_directory import ADDomain, ADGroup, ADUser, ADObject from ms_active_directory import ADDomain, ADGroup, ADUser, ADObject
from .exceptions import ScriptAlreadyRunningError
from .conf import ( from .conf import (
AD_DOMAIN, AD_DOMAIN,
AD_USER_NAME, AD_USER_NAME,
@ -25,6 +24,7 @@ from .conf import (
NTFY_TOPIC, NTFY_TOPIC,
NTFY_ACCESS_TOKEN, NTFY_ACCESS_TOKEN,
) )
from .exceptions import ScriptAlreadyRunningError
from .logger import setup_logging from .logger import setup_logging
from .models import RecentRun, CiviCrm, Ntfy from .models import RecentRun, CiviCrm, Ntfy

View file

@ -4,19 +4,83 @@ import os
from pathlib import Path from pathlib import Path
import pytz import pytz
import yaml import tomlkit
from tomlkit.toml_file import TOMLFile
logger = logging.getLogger(__package__) logger = logging.getLogger(__package__)
def create_config_file(dest: Path): def create_config_file(dest: Path):
if dest.is_dir(): if dest.is_dir():
dest = dest / f"{__package__}_config.yml" dest = dest / f"{__package__}_config.toml"
example_conf = Path(__file__).parent / 'resources' / 'example_config.yml'
with open(example_conf, "r") as source:
with open(dest, "w") as d:
d.writelines(source)
conf = tomlkit.document()
conf.add(tomlkit.comment(f"Configuration file for {__package__}"))
conf.add(tomlkit.nl())
# Add AD section
ad = tomlkit.table()
ad.add('DOMAIN', 'ad.example.com')
ad['DOMAIN'].comment('The domain of the Active Directory server')
ad.add('LDAP_SERVER', ['ldaps://server1.ad.example.com:636'])
ad['LDAP_SERVER'].comment('List of LDAP servers to connect to')
ad.add('PARENT_GROUP', 'Mailinglists')
ad['PARENT_GROUP'].comment('The parent group in Active Directory that '
'contains all groups that should be '
'synchronized')
ad.add('TIMEZONE', 'UTC')
ad.add('USER', tomlkit.string('example\\username', literal=True))
ad['USER'].comment('The username of the user to connect to the Active '
'Directory server. (use single quotes)')
ad.add('PASSWORD', 'xxxxxxxx')
ad['PASSWORD'].comment('The password of the user to connect to the Active '
'Directory')
conf.add('AD', ad)
# Add CiviCRM section
civicrm = tomlkit.table()
civicrm.add('BASE_URL', 'https://civicrm.example.com')
civicrm.add('API_KEY', 'xxxxxxxx')
civicrm['API_KEY'].comment('The API key of the CiviCRM user')
civicrm['BASE_URL'].comment('The URL of the CiviCRM server')
civicrm.add('BATCH_SIZE', 50)
civicrm['BATCH_SIZE'].comment('The batch size for the API requests to the '
'CiviCRM server - only applied to contact '
'sync (default: 50)')
civicrm.add('RETRIES', 3)
civicrm['RETRIES'].comment('The number of retries for the API requests to '
'the CiviCRM (default: 3)')
civicrm.add('IGNORE_SSL', False)
civicrm.value.item('IGNORE_SSL').comment('Allow insecure connections to '
'the CiviCRM server (default: '
'false)')
conf.add('CIVICRM', civicrm)
# Add Logging section
logging_ = tomlkit.table()
logging_.add('STDOUT_LOG_LEVEL', 'info')
logging_['STDOUT_LOG_LEVEL'].comment('The log level for the stdout logger '
'(default: info)')
logging_.add('FILE_LOG_LEVEL', 'info')
logging_['FILE_LOG_LEVEL'].comment('The log level for the file logger '
'(default: info)')
logging_.add('LOG_DIR', '/var/log/adGroupSync/')
logging_['LOG_DIR'].comment('The directory to store the log file')
conf.add('LOGGING', logging_)
# Add Ntfy section
ntfy = tomlkit.table()
ntfy.add(tomlkit.comment('Optional section for ntfy configuration'))
ntfy.add('URL', 'https://ntfy.example.com')
ntfy['URL'].comment('The URL of the ntfy server')
ntfy.add('TOPIC', 'adGroupSync')
ntfy['TOPIC'].comment('The topic to post the message to')
ntfy.add('ACCESS_TOKEN', 'tk_xxxxxxxxxxxxxxxxxxx')
ntfy['ACCESS_TOKEN'].comment('The access token for the ntfy server')
conf.add('NTFY', ntfy)
# Write configuration file
TOMLFile(dest).write(conf)
# Assign environment variables or configuration file values # Assign environment variables or configuration file values
AD_DOMAIN = os.getenv('AD_DOMAIN') AD_DOMAIN = os.getenv('AD_DOMAIN')
@ -77,8 +141,7 @@ try:
exit(0) exit(0)
# Load configuration file # Load configuration file
with open(config_file, 'r') as file: config = TOMLFile(config_file).read()
config = yaml.safe_load(file)
# Get values from configuration file # Get values from configuration file
AD_DOMAIN = AD_DOMAIN or config['AD']['DOMAIN'] AD_DOMAIN = AD_DOMAIN or config['AD']['DOMAIN']
@ -98,13 +161,16 @@ try:
CIVICRM_BATCH_SIZE = CIVICRM_BATCH_SIZE \ CIVICRM_BATCH_SIZE = CIVICRM_BATCH_SIZE \
or config['CIVICRM']['BATCH_SIZE'] or config['CIVICRM']['BATCH_SIZE']
CIVICRM_RETRIES = CIVICRM_RETRIES \ CIVICRM_RETRIES = CIVICRM_RETRIES \
or config['CIVICRM'].get('RETRIES', 3) or config['CIVICRM'].get('RETRIES', 3)
CIVICRM_IGNORE_SSL = CIVICRM_IGNORE_SSL \ CIVICRM_IGNORE_SSL = CIVICRM_IGNORE_SSL \
or bool(config['CIVICRM'].get('IGNORE_SSL', False)) or bool(config['CIVICRM'].get('IGNORE_SSL', False))
NTFY_URL = NTFY_URL or config['NTFY'].get('URL') if 'NTFY' in config else None NTFY_URL = NTFY_URL or config['NTFY'].get(
NTFY_TOPIC = NTFY_TOPIC or config['NTFY'].get('TOPIC') if 'NTFY' in config else None 'URL') if 'NTFY' in config else None
NTFY_TOPIC = NTFY_TOPIC or config['NTFY'].get(
'TOPIC') if 'NTFY' in config else None
NTFY_ACCESS_TOKEN = NTFY_ACCESS_TOKEN \ NTFY_ACCESS_TOKEN = NTFY_ACCESS_TOKEN \
or config['NTFY'].get('ACCESS_TOKEN') if 'NTFY' in config else None or config['NTFY'].get(
'ACCESS_TOKEN') if 'NTFY' in config else None
# Check if some required values are missing # Check if some required values are missing
required = { required = {

View file

@ -1,16 +1,15 @@
import datetime import datetime
import json import json
import logging import logging
import tomllib
from collections import deque from collections import deque
from datetime import datetime as dt, timezone from datetime import datetime as dt, timezone
from pathlib import Path from pathlib import Path
import pytz import pytz
import tomli_w
from civifang import api from civifang import api
from httpx import post from httpx import post
from ms_active_directory import ADUser, ADGroup from ms_active_directory import ADUser, ADGroup
from tomlkit.toml_file import TOMLFile
from .enums import Priority from .enums import Priority
from .exceptions import ScriptAlreadyRunningError from .exceptions import ScriptAlreadyRunningError
@ -56,38 +55,46 @@ class RecentRun:
:param is_running: :param is_running:
:return: :return:
""" """
rr = recent_run if recent_run else self._datetime # Return if no data is provided
is_running = is_running if is_running is not None else self._already_running if recent_run is None and is_running is None:
new_data = { return
'recent-run': rr,
'is-running': is_running, # Read the existing data
} toml_file = TOMLFile(self._file_path)
toml = toml_file.read()
# Update the values
if recent_run:
toml['recent_run'] = recent_run
if is_running is not None:
toml['is-running'] = is_running
# Write the data to the file # Write the data to the file
with open(self._file_path, 'wb') as f: toml_file.write(toml)
tomli_w.dump(new_data, f)
def _read_data_from_file(self): def _read_data_from_file(self):
""" """
Read the recent run time from the file Read the recent run time from the file
:return: :return:
""" """
with open(self._file_path, 'rb') as f: # Read the data from the file
data = tomllib.load(f) toml_file = TOMLFile(self._file_path)
self._already_running = data.get('is-running', False) toml = toml_file.read()
recent_run = data.get('recent-run')
# Set the datetime to the recent run time self._already_running = toml.get('is-running', False)
if not recent_run: recent_run = toml.get('recent-run')
return
if isinstance(recent_run, datetime.datetime): # Set the datetime to the recent run time
self._datetime = recent_run if not recent_run:
elif isinstance(recent_run, float): return
self._datetime = dt.fromtimestamp(recent_run) \ if isinstance(recent_run, datetime.datetime):
.astimezone(self._timezone) self._datetime = recent_run
else: elif isinstance(recent_run, float):
raise ValueError( self._datetime = dt.fromtimestamp(recent_run) \
f"Invalid recent_run '{recent_run}' in {self._file_path}.") .astimezone(self._timezone)
else:
raise ValueError(
f"Invalid recent_run '{recent_run}' in {self._file_path}.")
@property @property
def datetime(self) -> dt | None: def datetime(self) -> dt | None:

View file

@ -1,25 +0,0 @@
AD:
DOMAIN: ad.example.com
USER: example\username
PASSWORD: xxxxxxxx
LDAP_SERVER:
- ldaps://server1.ad.example.com:636
PARENT_GROUP: Mailinglists
TIMEZONE: UTC
LOGGING:
STDOUT_LOG_LEVEL: info
FILE_LOG_LEVEL: info
LOG_DIR: /var/log/adGroupSync/
CIVICRM:
BASE_URL: https://civicrm.example.com
API_KEY: xxxxxxxx
BATCH_SIZE: 50
RETRIES: 3
# IGNORE_SSL: yes
NTFY:
URL: https://ntfy.example.com
TOPIC: adGroupSync
ACCESS_TOKEN: tk_xxxxxxxxxxxxxxxxxxx

43
uv.lock generated
View file

@ -4,14 +4,13 @@ requires-python = ">=3.12"
[[package]] [[package]]
name = "adgroupsync" name = "adgroupsync"
version = "1.0.0" version = "1.1.0"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "civifang" }, { name = "civifang" },
{ name = "httpx" }, { name = "httpx" },
{ name = "ms-active-directory" }, { name = "ms-active-directory" },
{ name = "pyyaml" }, { name = "tomlkit" },
{ name = "tomli-w" },
{ name = "validators" }, { name = "validators" },
] ]
@ -26,8 +25,7 @@ requires-dist = [
{ name = "civifang", specifier = ">=0.2.6", index = "https://git.propeace.de/api/packages/ProPeace/pypi/simple/" }, { name = "civifang", specifier = ">=0.2.6", index = "https://git.propeace.de/api/packages/ProPeace/pypi/simple/" },
{ name = "httpx", specifier = ">=0.28.1" }, { name = "httpx", specifier = ">=0.28.1" },
{ name = "ms-active-directory", specifier = ">=1.14.1" }, { name = "ms-active-directory", specifier = ">=1.14.1" },
{ name = "pyyaml", specifier = ">=6.0.2" }, { name = "tomlkit", specifier = ">=0.13.2" },
{ name = "tomli-w", specifier = ">=1.2.0" },
{ name = "validators", specifier = ">=0.34.0" }, { name = "validators", specifier = ">=0.34.0" },
] ]
@ -366,32 +364,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/eb/38/ac33370d784287baa1c3d538978b5e2ea064d4c1b93ffbd12826c190dd10/pytz-2025.1-py2.py3-none-any.whl", hash = "sha256:89dd22dca55b46eac6eda23b2d72721bf1bdfef212645d81513ef5d03038de57", size = 507930 }, { url = "https://files.pythonhosted.org/packages/eb/38/ac33370d784287baa1c3d538978b5e2ea064d4c1b93ffbd12826c190dd10/pytz-2025.1-py2.py3-none-any.whl", hash = "sha256:89dd22dca55b46eac6eda23b2d72721bf1bdfef212645d81513ef5d03038de57", size = 507930 },
] ]
[[package]]
name = "pyyaml"
version = "6.0.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 },
{ url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 },
{ url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 },
{ url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 },
{ url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 },
{ url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 },
{ url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 },
{ url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 },
{ url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 },
{ url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 },
{ url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 },
{ url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 },
{ url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 },
{ url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 },
{ url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 },
{ url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 },
{ url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 },
{ url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 },
]
[[package]] [[package]]
name = "questionary" name = "questionary"
version = "2.1.0" version = "2.1.0"
@ -449,15 +421,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 },
] ]
[[package]]
name = "tomli-w"
version = "1.2.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/19/75/241269d1da26b624c0d5e110e8149093c759b7a286138f4efd61a60e75fe/tomli_w-1.2.0.tar.gz", hash = "sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021", size = 7184 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90", size = 6675 },
]
[[package]] [[package]]
name = "tomlkit" name = "tomlkit"
version = "0.13.2" version = "0.13.2"