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 committed by leiva
parent 19603d6d9f
commit e8503bd073
No known key found for this signature in database
8 changed files with 145 additions and 156 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

@ -18,8 +18,7 @@ dependencies = [
"httpx>=0.28.1", "httpx>=0.28.1",
"ms-active-directory>=1.14.1", "ms-active-directory>=1.14.1",
"python-crontab>=3.2.0", "python-crontab>=3.2.0",
"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

@ -6,37 +6,83 @@ from crontab import CronTab, CronSlices
import shutil import shutil
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())
def create_cron_job(cron_job: str, config_file: Path): # Add AD section
# Check if the script exists and its executable ad = tomlkit.table()
if not shutil.which(__package__): ad.add('DOMAIN', 'ad.example.com')
print( ad['DOMAIN'].comment('The domain of the Active Directory server')
f"No executable found, please add this to your crontab manually: '/path/to/adgroupsync --conf {config_file} >/dev/null 2>&1'" 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)
# Checking if the string is valid # Add CiviCRM section
if not CronSlices.is_valid(cron_job): civicrm = tomlkit.table()
raise Exception(f"Cron job '{cron_job}' is not valid.") 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)
# Creating the cron job # Add Logging section
cron = CronTab(user=True) logging_ = tomlkit.table()
job = cron.new(command=f"adgroupsync --conf {config_file} >/dev/null 2>&1") logging_.add('STDOUT_LOG_LEVEL', 'info')
job.setall(cron_job) logging_['STDOUT_LOG_LEVEL'].comment('The log level for the stdout logger '
cron.write() '(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")
@ -124,39 +170,36 @@ 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']
AD_USER_NAME = AD_USER_NAME or config["AD"]["USER"] AD_USER_NAME = AD_USER_NAME or config['AD']['USER']
AD_PASSWORD = AD_PASSWORD or config["AD"]["PASSWORD"] AD_PASSWORD = AD_PASSWORD or config['AD']['PASSWORD']
AD_LDAP_SERVER = AD_LDAP_SERVER or config["AD"].get("LDAP_SERVER") AD_LDAP_SERVER = AD_LDAP_SERVER or config['AD'].get('LDAP_SERVER')
AD_TIMEZONE = AD_TIMEZONE or pytz.timezone(config["AD"].get("TIMEZONE", "UTC")) AD_TIMEZONE = AD_TIMEZONE \
AD_PARENT_GROUP = AD_PARENT_GROUP or config["AD"]["PARENT_GROUP"] or pytz.timezone(config['AD'].get('TIMEZONE', 'UTC'))
STDOUT_LOG_LEVEL = STDOUT_LOG_LEVEL or config["LOGGING"].get( AD_PARENT_GROUP = AD_PARENT_GROUP or config['AD']['PARENT_GROUP']
"STDOUT_LOG_LEVEL", "INFO" STDOUT_LOG_LEVEL = STDOUT_LOG_LEVEL \
) or config['LOGGING'].get('STDOUT_LOG_LEVEL', 'INFO')
FILE_LOG_LEVEL = FILE_LOG_LEVEL or config["LOGGING"].get( FILE_LOG_LEVEL = FILE_LOG_LEVEL \
"FILE_LOG_LEVEL", "WARNING" or config['LOGGING'].get('FILE_LOG_LEVEL', 'WARNING')
) LOG_DIR = LOG_DIR or config['LOGGING'].get('LOG_DIR')
LOG_DIR = LOG_DIR or config["LOGGING"].get("LOG_DIR") CIVICRM_BASE_URL = CIVICRM_BASE_URL or config['CIVICRM']['BASE_URL']
CIVICRM_BASE_URL = CIVICRM_BASE_URL or config["CIVICRM"]["BASE_URL"] CIVICRM_API_KEY = CIVICRM_API_KEY or config['CIVICRM']['API_KEY']
CIVICRM_API_KEY = CIVICRM_API_KEY or config["CIVICRM"]["API_KEY"] 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 or config["CIVICRM"].get("RETRIES", 3) CIVICRM_RETRIES = CIVICRM_RETRIES \
CIVICRM_IGNORE_SSL = CIVICRM_IGNORE_SSL or bool( or config['CIVICRM'].get('RETRIES', 3)
config["CIVICRM"].get("IGNORE_SSL", False) CIVICRM_IGNORE_SSL = CIVICRM_IGNORE_SSL \
) 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 = ( 'URL') if 'NTFY' in config else None
NTFY_TOPIC or config["NTFY"].get("TOPIC") 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") or config['NTFY'].get(
if "NTFY" in config 'ACCESS_TOKEN') if 'NTFY' in config else None
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,26 +55,34 @@ 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')
self._already_running = toml.get('is-running', False)
recent_run = toml.get('recent-run')
# Set the datetime to the recent run time # Set the datetime to the recent run time
if not recent_run: if not recent_run:

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"