diff --git a/.github/files_to_copy b/.github/files_to_copy index ed55a6ce7..d4e5aacb1 100644 --- a/.github/files_to_copy +++ b/.github/files_to_copy @@ -1,4 +1,5 @@ bazarr +custom_libs frontend/build libs bazarr.py diff --git a/bazarr/app/app.py b/bazarr/app/app.py index 6814cd38d..982cea34b 100644 --- a/bazarr/app/app.py +++ b/bazarr/app/app.py @@ -34,6 +34,7 @@ def create_app(): else: app.config["DEBUG"] = False + from engineio.async_drivers import threading # noqa W0611 # required to prevent an import exception in engineio socketio.init_app(app, path=f'{base_url.rstrip("/")}/api/socket.io', cors_allowed_origins='*', async_mode='threading', allow_upgrades=False, transports='polling', engineio_logger=False) diff --git a/bazarr/app/libs.py b/bazarr/app/libs.py index c269cfae5..92ac7682d 100644 --- a/bazarr/app/libs.py +++ b/bazarr/app/libs.py @@ -18,6 +18,7 @@ def clean_libs(): def set_libs(): + sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(__file__)), '../custom_libs/')) sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(__file__)), '../libs/')) diff --git a/bazarr/app/logger.py b/bazarr/app/logger.py index 0b8de2435..8470944d5 100644 --- a/bazarr/app/logger.py +++ b/bazarr/app/logger.py @@ -59,7 +59,7 @@ class NoExceptionFormatter(logging.Formatter): class UnwantedWaitressMessageFilter(logging.Filter): def filter(self, record): - if settings.general.debug is True: + if settings.general.debug: # no filtering in debug mode return True diff --git a/bazarr/app/scheduler.py b/bazarr/app/scheduler.py index 7e438d0a3..f0e73637f 100644 --- a/bazarr/app/scheduler.py +++ b/bazarr/app/scheduler.py @@ -10,9 +10,13 @@ from apscheduler.triggers.date import DateTrigger from apscheduler.events import EVENT_JOB_SUBMITTED, EVENT_JOB_EXECUTED, EVENT_JOB_ERROR from datetime import datetime, timedelta from calendar import day_name +from math import floor from random import randrange from tzlocal import get_localzone -from tzlocal.utils import ZoneInfoNotFoundError +try: + import zoneinfo # pragma: no cover +except ImportError: + from backports import zoneinfo # pragma: no cover from dateutil import tz import logging @@ -60,7 +64,7 @@ class Scheduler: try: self.timezone = get_localzone() - except ZoneInfoNotFoundError as e: + except zoneinfo.ZoneInfoNotFoundError as e: logging.error(f"BAZARR cannot use specified timezone: {e}") self.timezone = tz.gettz("UTC") @@ -219,8 +223,9 @@ class Scheduler: trigger = CronTrigger(day_of_week=settings.backup.day, hour=settings.backup.hour) elif backup == "Manually": trigger = CronTrigger(year=in_a_century()) - self.aps_scheduler.add_job(backup_to_zip, trigger, max_instances=1, coalesce=True, misfire_grace_time=15, - id='backup', name='Backup Database and Configuration File', replace_existing=True) + self.aps_scheduler.add_job(backup_to_zip, trigger, + max_instances=1, coalesce=True, misfire_grace_time=15, id='backup', + name='Backup Database and Configuration File', replace_existing=True) def __sonarr_full_update_task(self): if settings.general.use_sonarr: diff --git a/custom_libs/custom_version.txt b/custom_libs/custom_version.txt new file mode 100644 index 000000000..687b8e37a --- /dev/null +++ b/custom_libs/custom_version.txt @@ -0,0 +1,19 @@ +# Bazarr dependencies +subliminal_patch +subzero +py-pretty==1 # modified version to support Python 3 + +# Bazarr modified dependencies +signalr-client-threads==0.0.12 # Modified to work with Sonarr v3. Not used anymore with v4 +Flask-Compress==1.14 # modified to import brotli only if required + +# Required-by: signalr-client-threads +sseclient==0.0.27 # Modified to work with Sonarr v3 + +# Required-by: subliminal_patch +deathbycaptcha # unknown version, only found on gist +git+https://github.com/pannal/libfilebot#egg=libfilebot +git+https://github.com/RobinDavid/pyADS.git@28a2f6dbfb357f85b2c2f49add770b336e88840d#egg=pyads +py7zr==0.7.0 # modified to prevent importing of modules that can't be vendored +subscene-api==1.0.0 # modified specificaly for Bazarr +subliminal==2.1.0 # modified specifically for Bazarr diff --git a/libs/deathbycaptcha.py b/custom_libs/deathbycaptcha.py similarity index 100% rename from libs/deathbycaptcha.py rename to custom_libs/deathbycaptcha.py diff --git a/libs/flask_compress/__init__.py b/custom_libs/flask_compress/__init__.py similarity index 100% rename from libs/flask_compress/__init__.py rename to custom_libs/flask_compress/__init__.py diff --git a/custom_libs/flask_compress/_version.py b/custom_libs/flask_compress/_version.py new file mode 100644 index 000000000..8194a6a94 --- /dev/null +++ b/custom_libs/flask_compress/_version.py @@ -0,0 +1 @@ +__version__ = "1.14" diff --git a/libs/flask_compress/flask_compress.py b/custom_libs/flask_compress/flask_compress.py similarity index 97% rename from libs/flask_compress/flask_compress.py rename to custom_libs/flask_compress/flask_compress.py index a990a7fb2..0bebff0a8 100644 --- a/libs/flask_compress/flask_compress.py +++ b/custom_libs/flask_compress/flask_compress.py @@ -63,9 +63,14 @@ class Compress(object): def init_app(self, app): defaults = [ - ('COMPRESS_MIMETYPES', ['text/html', 'text/css', 'text/xml', - 'application/json', - 'application/javascript']), + ('COMPRESS_MIMETYPES', [ + 'application/javascript', # Obsolete (RFC 9239) + 'application/json', + 'text/css', + 'text/html', + 'text/javascript', + 'text/xml', + ]), ('COMPRESS_LEVEL', 6), ('COMPRESS_BR_LEVEL', 4), ('COMPRESS_BR_MODE', 0), diff --git a/libs/libfilebot/LICENSE b/custom_libs/libfilebot/LICENSE similarity index 100% rename from libs/libfilebot/LICENSE rename to custom_libs/libfilebot/LICENSE diff --git a/libs/libfilebot/README.md b/custom_libs/libfilebot/README.md similarity index 100% rename from libs/libfilebot/README.md rename to custom_libs/libfilebot/README.md diff --git a/libs/libfilebot/__init__.py b/custom_libs/libfilebot/__init__.py similarity index 100% rename from libs/libfilebot/__init__.py rename to custom_libs/libfilebot/__init__.py diff --git a/libs/libfilebot/lib.py b/custom_libs/libfilebot/lib.py similarity index 100% rename from libs/libfilebot/lib.py rename to custom_libs/libfilebot/lib.py diff --git a/libs/libfilebot/main.py b/custom_libs/libfilebot/main.py similarity index 100% rename from libs/libfilebot/main.py rename to custom_libs/libfilebot/main.py diff --git a/libs/pretty/__init__.py b/custom_libs/pretty/__init__.py similarity index 100% rename from libs/pretty/__init__.py rename to custom_libs/pretty/__init__.py diff --git a/libs/py7zr/__init__.py b/custom_libs/py7zr/__init__.py similarity index 100% rename from libs/py7zr/__init__.py rename to custom_libs/py7zr/__init__.py diff --git a/libs/py7zr/archiveinfo.py b/custom_libs/py7zr/archiveinfo.py similarity index 100% rename from libs/py7zr/archiveinfo.py rename to custom_libs/py7zr/archiveinfo.py diff --git a/libs/py7zr/callbacks.py b/custom_libs/py7zr/callbacks.py similarity index 100% rename from libs/py7zr/callbacks.py rename to custom_libs/py7zr/callbacks.py diff --git a/libs/py7zr/compression.py b/custom_libs/py7zr/compression.py similarity index 100% rename from libs/py7zr/compression.py rename to custom_libs/py7zr/compression.py diff --git a/libs/py7zr/exceptions.py b/custom_libs/py7zr/exceptions.py similarity index 100% rename from libs/py7zr/exceptions.py rename to custom_libs/py7zr/exceptions.py diff --git a/libs/py7zr/extra.py b/custom_libs/py7zr/extra.py similarity index 100% rename from libs/py7zr/extra.py rename to custom_libs/py7zr/extra.py diff --git a/libs/py7zr/helpers.py b/custom_libs/py7zr/helpers.py similarity index 100% rename from libs/py7zr/helpers.py rename to custom_libs/py7zr/helpers.py diff --git a/libs/py7zr/properties.py b/custom_libs/py7zr/properties.py similarity index 100% rename from libs/py7zr/properties.py rename to custom_libs/py7zr/properties.py diff --git a/libs/py7zr/py7zr.py b/custom_libs/py7zr/py7zr.py similarity index 100% rename from libs/py7zr/py7zr.py rename to custom_libs/py7zr/py7zr.py diff --git a/libs/py7zr/win32compat.py b/custom_libs/py7zr/win32compat.py similarity index 100% rename from libs/py7zr/win32compat.py rename to custom_libs/py7zr/win32compat.py diff --git a/libs/pyads.py b/custom_libs/pyads.py similarity index 100% rename from libs/pyads.py rename to custom_libs/pyads.py diff --git a/libs/signalr/__init__.py b/custom_libs/signalr/__init__.py similarity index 100% rename from libs/signalr/__init__.py rename to custom_libs/signalr/__init__.py diff --git a/libs/signalr/_connection.py b/custom_libs/signalr/_connection.py similarity index 100% rename from libs/signalr/_connection.py rename to custom_libs/signalr/_connection.py diff --git a/libs/signalr/events/__init__.py b/custom_libs/signalr/events/__init__.py similarity index 100% rename from libs/signalr/events/__init__.py rename to custom_libs/signalr/events/__init__.py diff --git a/libs/signalr/events/_events.py b/custom_libs/signalr/events/_events.py similarity index 100% rename from libs/signalr/events/_events.py rename to custom_libs/signalr/events/_events.py diff --git a/libs/signalr/hubs/__init__.py b/custom_libs/signalr/hubs/__init__.py similarity index 100% rename from libs/signalr/hubs/__init__.py rename to custom_libs/signalr/hubs/__init__.py diff --git a/libs/signalr/hubs/_hub.py b/custom_libs/signalr/hubs/_hub.py similarity index 100% rename from libs/signalr/hubs/_hub.py rename to custom_libs/signalr/hubs/_hub.py diff --git a/libs/signalr/transports/__init__.py b/custom_libs/signalr/transports/__init__.py similarity index 100% rename from libs/signalr/transports/__init__.py rename to custom_libs/signalr/transports/__init__.py diff --git a/libs/signalr/transports/_auto_transport.py b/custom_libs/signalr/transports/_auto_transport.py similarity index 100% rename from libs/signalr/transports/_auto_transport.py rename to custom_libs/signalr/transports/_auto_transport.py diff --git a/libs/signalr/transports/_sse_transport.py b/custom_libs/signalr/transports/_sse_transport.py similarity index 100% rename from libs/signalr/transports/_sse_transport.py rename to custom_libs/signalr/transports/_sse_transport.py diff --git a/libs/signalr/transports/_transport.py b/custom_libs/signalr/transports/_transport.py similarity index 100% rename from libs/signalr/transports/_transport.py rename to custom_libs/signalr/transports/_transport.py diff --git a/libs/signalr/transports/_ws_transport.py b/custom_libs/signalr/transports/_ws_transport.py similarity index 100% rename from libs/signalr/transports/_ws_transport.py rename to custom_libs/signalr/transports/_ws_transport.py diff --git a/libs/sseclient.py b/custom_libs/sseclient.py similarity index 100% rename from libs/sseclient.py rename to custom_libs/sseclient.py diff --git a/libs/subliminal/__init__.py b/custom_libs/subliminal/__init__.py similarity index 100% rename from libs/subliminal/__init__.py rename to custom_libs/subliminal/__init__.py diff --git a/libs/subliminal/cache.py b/custom_libs/subliminal/cache.py similarity index 100% rename from libs/subliminal/cache.py rename to custom_libs/subliminal/cache.py diff --git a/libs/subliminal/cli.py b/custom_libs/subliminal/cli.py similarity index 100% rename from libs/subliminal/cli.py rename to custom_libs/subliminal/cli.py diff --git a/libs/dateutil/test/__init__.py b/custom_libs/subliminal/converters/__init__.py similarity index 100% rename from libs/dateutil/test/__init__.py rename to custom_libs/subliminal/converters/__init__.py diff --git a/libs/subliminal/converters/addic7ed.py b/custom_libs/subliminal/converters/addic7ed.py similarity index 100% rename from libs/subliminal/converters/addic7ed.py rename to custom_libs/subliminal/converters/addic7ed.py diff --git a/custom_libs/subliminal/converters/legendastv.py b/custom_libs/subliminal/converters/legendastv.py new file mode 100644 index 000000000..c2e13bd39 --- /dev/null +++ b/custom_libs/subliminal/converters/legendastv.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +from babelfish import LanguageReverseConverter + +from ..exceptions import ConfigurationError + + +class LegendasTVConverter(LanguageReverseConverter): + def __init__(self): + self.from_legendastv = {1: ('por', 'BR'), 2: ('eng',), 3: ('spa',), 4: ('fra',), 5: ('deu',), 6: ('jpn',), + 7: ('dan',), 8: ('nor',), 9: ('swe',), 10: ('por',), 11: ('ara',), 12: ('ces',), + 13: ('zho',), 14: ('kor',), 15: ('bul',), 16: ('ita',), 17: ('pol',)} + self.to_legendastv = {v: k for k, v in self.from_legendastv.items()} + self.codes = set(self.from_legendastv.keys()) + + def convert(self, alpha3, country=None, script=None): + if (alpha3, country) in self.to_legendastv: + return self.to_legendastv[(alpha3, country)] + if (alpha3,) in self.to_legendastv: + return self.to_legendastv[(alpha3,)] + + raise ConfigurationError('Unsupported language code for legendastv: %s, %s, %s' % (alpha3, country, script)) + + def reverse(self, legendastv): + if legendastv in self.from_legendastv: + return self.from_legendastv[legendastv] + + raise ConfigurationError('Unsupported language number for legendastv: %s' % legendastv) diff --git a/libs/subliminal/converters/shooter.py b/custom_libs/subliminal/converters/shooter.py similarity index 100% rename from libs/subliminal/converters/shooter.py rename to custom_libs/subliminal/converters/shooter.py diff --git a/libs/subliminal/converters/thesubdb.py b/custom_libs/subliminal/converters/thesubdb.py similarity index 100% rename from libs/subliminal/converters/thesubdb.py rename to custom_libs/subliminal/converters/thesubdb.py diff --git a/libs/subliminal/converters/tvsubtitles.py b/custom_libs/subliminal/converters/tvsubtitles.py similarity index 100% rename from libs/subliminal/converters/tvsubtitles.py rename to custom_libs/subliminal/converters/tvsubtitles.py diff --git a/libs/subliminal/core.py b/custom_libs/subliminal/core.py similarity index 100% rename from libs/subliminal/core.py rename to custom_libs/subliminal/core.py diff --git a/libs/subliminal/exceptions.py b/custom_libs/subliminal/exceptions.py similarity index 100% rename from libs/subliminal/exceptions.py rename to custom_libs/subliminal/exceptions.py diff --git a/libs/subliminal/extensions.py b/custom_libs/subliminal/extensions.py similarity index 100% rename from libs/subliminal/extensions.py rename to custom_libs/subliminal/extensions.py diff --git a/custom_libs/subliminal/matches.py b/custom_libs/subliminal/matches.py new file mode 100644 index 000000000..9b230902f --- /dev/null +++ b/custom_libs/subliminal/matches.py @@ -0,0 +1,229 @@ +# -*- coding: utf-8 -*- +from rebulk.loose import ensure_list + +from .score import get_equivalent_release_groups, score_keys +from .video import Episode, Movie +from .utils import sanitize, sanitize_release_group + + +def series_matches(video, title=None, **kwargs): + """Whether the `video` matches the series title. + + :param video: the video. + :type video: :class:`~subliminal.video.Video` + :param str title: the series name. + :return: whether there's a match + :rtype: bool + + """ + if isinstance(video, Episode): + return video.series and sanitize(title) in ( + sanitize(name) for name in [video.series] + video.alternative_series + ) + + +def title_matches(video, title=None, episode_title=None, **kwargs): + """Whether the movie matches the movie `title` or the series matches the `episode_title`. + + :param video: the video. + :type video: :class:`~subliminal.video.Video` + :param str title: the movie title. + :param str episode_title: the series episode title. + :return: whether there's a match + :rtype: bool + + """ + if isinstance(video, Episode): + return video.title and sanitize(episode_title) == sanitize(video.title) + if isinstance(video, Movie): + return video.title and sanitize(title) == sanitize(video.title) + + +def season_matches(video, season=None, **kwargs): + """Whether the episode matches the `season`. + + :param video: the video. + :type video: :class:`~subliminal.video.Video` + :param int season: the episode season. + :return: whether there's a match + :rtype: bool + + """ + if isinstance(video, Episode): + return video.season and season == video.season + + +def episode_matches(video, episode=None, **kwargs): + """Whether the episode matches the `episode`. + + :param video: the video. + :type video: :class:`~subliminal.video.Video` + :param episode: the episode season. + :type: list of int or int + :return: whether there's a match + :rtype: bool + + """ + if isinstance(video, Episode): + return video.episodes and ensure_list(episode) == video.episodes + + +def year_matches(video, year=None, partial=False, **kwargs): + """Whether the video matches the `year`. + + :param video: the video. + :type video: :class:`~subliminal.video.Video` + :param int year: the video year. + :param bool partial: whether or not the guess is partial. + :return: whether there's a match + :rtype: bool + + """ + if video.year and year == video.year: + return True + if isinstance(video, Episode): + # count "no year" as an information + return not partial and video.original_series and not year + + +def country_matches(video, country=None, partial=False, **kwargs): + """Whether the video matches the `country`. + + :param video: the video. + :type video: :class:`~subliminal.video.Video` + :param country: the video country. + :type country: :class:`~babelfish.country.Country` + :param bool partial: whether or not the guess is partial. + :return: whether there's a match + :rtype: bool + + """ + if video.country and country == video.country: + return True + + if isinstance(video, Episode): + # count "no country" as an information + return not partial and video.original_series and not country + + if isinstance(video, Movie): + # count "no country" as an information + return not video.country and not country + + +def release_group_matches(video, release_group=None, **kwargs): + """Whether the video matches the `release_group`. + + :param video: the video. + :type video: :class:`~subliminal.video.Video` + :param str release_group: the video release group. + :return: whether there's a match + :rtype: bool + + """ + return (video.release_group and release_group and + any(r in sanitize_release_group(release_group) + for r in get_equivalent_release_groups(sanitize_release_group(video.release_group)))) + + +def streaming_service_matches(video, streaming_service=None, **kwargs): + """Whether the video matches the `streaming_service`. + + :param video: the video. + :type video: :class:`~subliminal.video.Video` + :param str streaming_service: the video streaming service + :return: whether there's a match + :rtype: bool + + """ + return video.streaming_service and streaming_service == video.streaming_service + + +def resolution_matches(video, screen_size=None, **kwargs): + """Whether the video matches the `resolution`. + + :param video: the video. + :type video: :class:`~subliminal.video.Video` + :param str screen_size: the video resolution + :return: whether there's a match + :rtype: bool + + """ + return video.resolution and screen_size == video.resolution + + +def source_matches(video, source=None, **kwargs): + """Whether the video matches the `source`. + + :param video: the video. + :type video: :class:`~subliminal.video.Video` + :param str source: the video source + :return: whether there's a match + :rtype: bool + + """ + return video.source and source == video.source + + +def video_codec_matches(video, video_codec=None, **kwargs): + """Whether the video matches the `video_codec`. + + :param video: the video. + :type video: :class:`~subliminal.video.Video` + :param str video_codec: the video codec + :return: whether there's a match + :rtype: bool + + """ + return video.video_codec and video_codec == video.video_codec + + +def audio_codec_matches(video, audio_codec=None, **kwargs): + """Whether the video matches the `audio_codec`. + + :param video: the video. + :type video: :class:`~subliminal.video.Video` + :param str audio_codec: the video audio codec + :return: whether there's a match + :rtype: bool + + """ + return video.audio_codec and audio_codec == video.audio_codec + + +#: Available matches functions +matches_manager = { + 'series': series_matches, + 'title': title_matches, + 'season': season_matches, + 'episode': episode_matches, + 'year': year_matches, + 'country': country_matches, + 'release_group': release_group_matches, + 'streaming_service': streaming_service_matches, + 'resolution': resolution_matches, + 'source': source_matches, + 'video_codec': video_codec_matches, + 'audio_codec': audio_codec_matches +} + + +def guess_matches(video, guess, partial=False): + """Get matches between a `video` and a `guess`. + + If a guess is `partial`, the absence information won't be counted as a match. + + :param video: the video. + :type video: :class:`~subliminal.video.Video` + :param guess: the guess. + :type guess: dict + :param bool partial: whether or not the guess is partial. + :return: matches between the `video` and the `guess`. + :rtype: set + + """ + matches = set() + for key in score_keys: + if key in matches_manager and matches_manager[key](video, partial=partial, **guess): + matches.add(key) + + return matches diff --git a/libs/subliminal/providers/__init__.py b/custom_libs/subliminal/providers/__init__.py similarity index 100% rename from libs/subliminal/providers/__init__.py rename to custom_libs/subliminal/providers/__init__.py diff --git a/libs/subliminal/providers/addic7ed.py b/custom_libs/subliminal/providers/addic7ed.py similarity index 100% rename from libs/subliminal/providers/addic7ed.py rename to custom_libs/subliminal/providers/addic7ed.py diff --git a/custom_libs/subliminal/providers/argenteam.py b/custom_libs/subliminal/providers/argenteam.py new file mode 100644 index 000000000..59f3fc567 --- /dev/null +++ b/custom_libs/subliminal/providers/argenteam.py @@ -0,0 +1,135 @@ +# -*- coding: utf-8 -*- +import io +import json +import logging +from zipfile import ZipFile + +from babelfish import Language +from guessit import guessit +from requests import Session +from six.moves import urllib + +from . import Provider +from ..cache import EPISODE_EXPIRATION_TIME, region +from ..exceptions import ProviderError +from ..matches import guess_matches +from ..subtitle import Subtitle, fix_line_ending +from ..video import Episode + +logger = logging.getLogger(__name__) + + +class ArgenteamSubtitle(Subtitle): + provider_name = 'argenteam' + + def __init__(self, language, download_link, series, season, episode, release, version): + super(ArgenteamSubtitle, self).__init__(language, download_link) + self.download_link = download_link + self.series = series + self.season = season + self.episode = episode + self.release = release + self.version = version + + @property + def id(self): + return self.download_link + + @property + def info(self): + return urllib.parse.unquote(self.download_link.rsplit('/')[-1]) + + def get_matches(self, video): + matches = guess_matches(video, { + 'title': self.series, + 'season': self.season, + 'episode': self.episode, + 'release_group': self.version + }) + + # resolution + if video.resolution and self.version and video.resolution in self.version.lower(): + matches.add('resolution') + + matches |= guess_matches(video, guessit(self.version, {'type': 'episode'}), partial=True) + return matches + + +class ArgenteamProvider(Provider): + provider_name = 'argenteam' + language = Language.fromalpha2('es') + languages = {language} + video_types = (Episode,) + server_url = "http://argenteam.net/api/v1/" + subtitle_class = ArgenteamSubtitle + + def __init__(self): + self.session = None + + def initialize(self): + self.session = Session() + self.session.headers['User-Agent'] = self.user_agent + + def terminate(self): + self.session.close() + + @region.cache_on_arguments(expiration_time=EPISODE_EXPIRATION_TIME, should_cache_fn=lambda value: value) + def search_episode_id(self, series, season, episode): + """Search the episode id from the `series`, `season` and `episode`. + + :param str series: series of the episode. + :param int season: season of the episode. + :param int episode: episode number. + :return: the episode id, if any. + :rtype: int or None + + """ + # make the search + query = '%s S%#02dE%#02d' % (series, season, episode) + logger.info('Searching episode id for %r', query) + r = self.session.get(self.server_url + 'search', params={'q': query}, timeout=10) + r.raise_for_status() + results = json.loads(r.text) + if results['total'] == 1: + return results['results'][0]['id'] + + logger.error('No episode id found for %r', series) + + def query(self, series, season, episode): + episode_id = self.search_episode_id(series, season, episode) + if episode_id is None: + return [] + + response = self.session.get(self.server_url + 'episode', params={'id': episode_id}, timeout=10) + response.raise_for_status() + content = json.loads(response.text) + subtitles = [] + for r in content['releases']: + for s in r['subtitles']: + subtitle = self.subtitle_class(self.language, s['uri'], series, season, episode, r['team'], r['tags']) + logger.debug('Found subtitle %r', subtitle) + subtitles.append(subtitle) + + return subtitles + + def list_subtitles(self, video, languages): + titles = [video.series] + video.alternative_series + for title in titles: + subs = self.query(title, video.season, video.episode) + if subs: + return subs + + return [] + + def download_subtitle(self, subtitle): + # download as a zip + logger.info('Downloading subtitle %r', subtitle) + r = self.session.get(subtitle.download_link, timeout=10) + r.raise_for_status() + + # open the zip + with ZipFile(io.BytesIO(r.content)) as zf: + if len(zf.namelist()) > 1: + raise ProviderError('More than one file to unzip') + + subtitle.content = fix_line_ending(zf.read(zf.namelist()[0])) diff --git a/custom_libs/subliminal/providers/legendastv.py b/custom_libs/subliminal/providers/legendastv.py new file mode 100644 index 000000000..9d696ca24 --- /dev/null +++ b/custom_libs/subliminal/providers/legendastv.py @@ -0,0 +1,514 @@ +# -*- coding: utf-8 -*- +import io +import json +import logging +import os +import re + +from babelfish import Language, language_converters +from datetime import datetime, timedelta +from dogpile.cache.api import NO_VALUE +from guessit import guessit +import pytz +import rarfile +from rarfile import RarFile, is_rarfile +from rebulk.loose import ensure_list +from requests import Session +from zipfile import ZipFile, is_zipfile + +from . import ParserBeautifulSoup, Provider +from ..cache import SHOW_EXPIRATION_TIME, region +from ..exceptions import AuthenticationError, ConfigurationError, ProviderError, ServiceUnavailable +from ..matches import guess_matches +from ..subtitle import SUBTITLE_EXTENSIONS, Subtitle, fix_line_ending +from ..utils import sanitize +from ..video import Episode, Movie + +logger = logging.getLogger(__name__) + +language_converters.register('legendastv = subliminal.converters.legendastv:LegendasTVConverter') + +# Configure :mod:`rarfile` to use the same path separator as :mod:`zipfile` +rarfile.PATH_SEP = '/' + +#: Conversion map for types +type_map = {'M': 'movie', 'S': 'episode', 'C': 'episode'} + +#: BR title season parsing regex +season_re = re.compile(r' - (?P\d+)(\xaa|a|st|nd|rd|th) (temporada|season)', re.IGNORECASE) + +#: Downloads parsing regex +downloads_re = re.compile(r'(?P\d+) downloads') + +#: Rating parsing regex +rating_re = re.compile(r'nota (?P\d+)') + +#: Timestamp parsing regex +timestamp_re = re.compile(r'(?P\d+)/(?P\d+)/(?P\d+) - (?P\d+):(?P\d+)') + +#: Title with year/country regex +title_re = re.compile(r'^(?P.*?)(?: \((?:(?P\d{4})|(?P[A-Z]{2}))\))?$') + +#: Cache key for releases +releases_key = __name__ + ':releases|{archive_id}|{archive_name}' + + +class LegendasTVArchive(object): + """LegendasTV Archive. + + :param str id: identifier. + :param str name: name. + :param bool pack: contains subtitles for multiple episodes. + :param bool pack: featured. + :param str link: link. + :param int downloads: download count. + :param int rating: rating (0-10). + :param timestamp: timestamp. + :type timestamp: datetime.datetime + """ + + def __init__(self, id, name, pack, featured, link, downloads=0, rating=0, timestamp=None): + #: Identifier + self.id = id + + #: Name + self.name = name + + #: Pack + self.pack = pack + + #: Featured + self.featured = featured + + #: Link + self.link = link + + #: Download count + self.downloads = downloads + + #: Rating (0-10) + self.rating = rating + + #: Timestamp + self.timestamp = timestamp + + #: Compressed content as :class:`rarfile.RarFile` or :class:`zipfile.ZipFile` + self.content = None + + def __repr__(self): + return '<%s [%s] %r>' % (self.__class__.__name__, self.id, self.name) + + +class LegendasTVSubtitle(Subtitle): + """LegendasTV Subtitle.""" + + provider_name = 'legendastv' + + def __init__(self, language, type, title, year, imdb_id, season, archive, name): + super(LegendasTVSubtitle, self).__init__(language, page_link=archive.link) + self.type = type + self.title = title + self.year = year + self.imdb_id = imdb_id + self.season = season + self.archive = archive + self.name = name + + @property + def id(self): + return '%s-%s' % (self.archive.id, self.name.lower()) + + @property + def info(self): + return self.name + + def get_matches(self, video, hearing_impaired=False): + matches = guess_matches(video, { + 'title': self.title, + 'year': self.year + }) + + # episode + if isinstance(video, Episode) and self.type == 'episode': + # imdb_id + if video.series_imdb_id and self.imdb_id == video.series_imdb_id: + matches.add('series_imdb_id') + + # movie + elif isinstance(video, Movie) and self.type == 'movie': + # imdb_id + if video.imdb_id and self.imdb_id == video.imdb_id: + matches.add('imdb_id') + + # name + matches |= guess_matches(video, guessit(self.name, {'type': self.type})) + + return matches + + +class LegendasTVProvider(Provider): + """LegendasTV Provider. + + :param str username: username. + :param str password: password. + """ + + languages = {Language.fromlegendastv(l) for l in language_converters['legendastv'].codes} + server_url = 'http://legendas.tv/' + subtitle_class = LegendasTVSubtitle + + def __init__(self, username=None, password=None): + + # Provider needs UNRAR installed. If not available raise ConfigurationError + try: + rarfile.custom_check([rarfile.UNRAR_TOOL], True) + except rarfile.RarExecError: + raise ConfigurationError('UNRAR tool not available') + + if any((username, password)) and not all((username, password)): + raise ConfigurationError('Username and password must be specified') + + self.username = username + self.password = password + self.logged_in = False + self.session = None + + def initialize(self): + self.session = Session() + self.session.headers['User-Agent'] = self.user_agent + + # login + if self.username and self.password: + logger.info('Logging in') + data = {'_method': 'POST', 'data[User][username]': self.username, 'data[User][password]': self.password} + r = self.session.post(self.server_url + 'login', data, allow_redirects=False, timeout=10) + raise_for_status(r) + + soup = ParserBeautifulSoup(r.content, ['html.parser']) + if soup.find('div', {'class': 'alert-error'}, string=re.compile(u'Usuário ou senha inválidos')): + raise AuthenticationError(self.username) + + logger.debug('Logged in') + self.logged_in = True + + def terminate(self): + # logout + if self.logged_in: + logger.info('Logging out') + r = self.session.get(self.server_url + 'users/logout', allow_redirects=False, timeout=10) + raise_for_status(r) + logger.debug('Logged out') + self.logged_in = False + + self.session.close() + + @staticmethod + def is_valid_title(title, title_id, sanitized_title, season, year): + """Check if is a valid title.""" + sanitized_result = sanitize(title['title']) + if sanitized_result != sanitized_title: + logger.debug("Mismatched title, discarding title %d (%s)", + title_id, sanitized_result) + return + + # episode type + if season: + # discard mismatches on type + if title['type'] != 'episode': + logger.debug("Mismatched 'episode' type, discarding title %d (%s)", title_id, sanitized_result) + return + + # discard mismatches on season + if 'season' not in title or title['season'] != season: + logger.debug('Mismatched season %s, discarding title %d (%s)', + title.get('season'), title_id, sanitized_result) + return + # movie type + else: + # discard mismatches on type + if title['type'] != 'movie': + logger.debug("Mismatched 'movie' type, discarding title %d (%s)", title_id, sanitized_result) + return + + # discard mismatches on year + if year is not None and 'year' in title and title['year'] != year: + logger.debug("Mismatched movie year, discarding title %d (%s)", title_id, sanitized_result) + return + return True + + @region.cache_on_arguments(expiration_time=SHOW_EXPIRATION_TIME, should_cache_fn=lambda value: value) + def search_titles(self, title, season, title_year): + """Search for titles matching the `title`. + + For episodes, each season has it own title + :param str title: the title to search for. + :param int season: season of the title + :param int title_year: year of the title + :return: found titles. + :rtype: dict + """ + titles = {} + sanitized_titles = [sanitize(title)] + ignore_characters = {'\'', '.'} + if any(c in title for c in ignore_characters): + sanitized_titles.append(sanitize(title, ignore_characters=ignore_characters)) + + for sanitized_title in sanitized_titles: + # make the query + if season: + logger.info('Searching episode title %r for season %r', sanitized_title, season) + else: + logger.info('Searching movie title %r', sanitized_title) + + r = self.session.get(self.server_url + 'legenda/sugestao/{}'.format(sanitized_title), timeout=10) + raise_for_status(r) + results = json.loads(r.text) + + # loop over results + for result in results: + source = result['_source'] + + # extract id + title_id = int(source['id_filme']) + + # extract type + title = {'type': type_map[source['tipo']]} + + # extract title, year and country + name, year, country = title_re.match(source['dsc_nome']).groups() + title['title'] = name + + # extract imdb_id + if source['id_imdb'] != '0': + if not source['id_imdb'].startswith('tt'): + title['imdb_id'] = 'tt' + source['id_imdb'].zfill(7) + else: + title['imdb_id'] = source['id_imdb'] + + # extract season + if title['type'] == 'episode': + if source['temporada'] and source['temporada'].isdigit(): + title['season'] = int(source['temporada']) + else: + match = season_re.search(source['dsc_nome_br']) + if match: + title['season'] = int(match.group('season')) + else: + logger.debug('No season detected for title %d (%s)', title_id, name) + + # extract year + if year: + title['year'] = int(year) + elif source['dsc_data_lancamento'] and source['dsc_data_lancamento'].isdigit(): + # year is based on season air date hence the adjustment + title['year'] = int(source['dsc_data_lancamento']) - title.get('season', 1) + 1 + + # add title only if is valid + # Check against title without ignored chars + if self.is_valid_title(title, title_id, sanitized_titles[0], season, title_year): + titles[title_id] = title + + logger.debug('Found %d titles', len(titles)) + + return titles + + @region.cache_on_arguments(expiration_time=timedelta(minutes=15).total_seconds()) + def get_archives(self, title_id, language_code, title_type, season, episodes): + """Get the archive list from a given `title_id`, `language_code`, `title_type`, `season` and `episode`. + + :param int title_id: title id. + :param int language_code: language code. + :param str title_type: episode or movie + :param int season: season + :param list episodes: episodes + :return: the archives. + :rtype: list of :class:`LegendasTVArchive` + + """ + archives = [] + page = 0 + while True: + # get the archive page + url = self.server_url + 'legenda/busca/-/{language}/-/{page}/{title}'.format( + language=language_code, page=page, title=title_id) + r = self.session.get(url) + raise_for_status(r) + + # parse the results + soup = ParserBeautifulSoup(r.content, ['lxml', 'html.parser']) + for archive_soup in soup.select('div.list_element > article > div > div.f_left'): + # create archive + archive = LegendasTVArchive(archive_soup.a['href'].split('/')[2], + archive_soup.a.text, + 'pack' in archive_soup.parent['class'], + 'destaque' in archive_soup.parent['class'], + self.server_url + archive_soup.a['href'][1:]) + # clean name of path separators and pack flags + clean_name = archive.name.replace('/', '-') + if archive.pack and clean_name.startswith('(p)'): + clean_name = clean_name[3:] + + # guess from name + guess = guessit(clean_name, {'type': title_type}) + + # episode + if season and episodes: + # discard mismatches on episode in non-pack archives + + # Guessit may return int for single episode or list for multi-episode + # Check if archive name has multiple episodes releases on it + if not archive.pack and 'episode' in guess: + wanted_episode = set(episodes) + archive_episode = set(ensure_list(guess['episode'])) + + if not wanted_episode.intersection(archive_episode): + logger.debug('Mismatched episode %s, discarding archive: %s', guess['episode'], clean_name) + continue + + # extract text containing downloads, rating and timestamp + data_text = archive_soup.find('p', class_='data').text + + # match downloads + archive.downloads = int(downloads_re.search(data_text).group('downloads')) + + # match rating + match = rating_re.search(data_text) + if match: + archive.rating = int(match.group('rating')) + + # match timestamp and validate it + time_data = {k: int(v) for k, v in timestamp_re.search(data_text).groupdict().items()} + archive.timestamp = pytz.timezone('America/Sao_Paulo').localize(datetime(**time_data)) + if archive.timestamp > datetime.utcnow().replace(tzinfo=pytz.utc): + raise ProviderError('Archive timestamp is in the future') + + # add archive + logger.info('Found archive for title %d and language %d at page %s: %s', + title_id, language_code, page, archive) + archives.append(archive) + + # stop on last page + if soup.find('a', attrs={'class': 'load_more'}, string='carregar mais') is None: + break + + # increment page count + page += 1 + + logger.debug('Found %d archives', len(archives)) + + return archives + + def download_archive(self, archive): + """Download an archive's :attr:`~LegendasTVArchive.content`. + + :param archive: the archive to download :attr:`~LegendasTVArchive.content` of. + :type archive: :class:`LegendasTVArchive` + + """ + logger.info('Downloading archive %s', archive.id) + r = self.session.get(self.server_url + 'downloadarquivo/{}'.format(archive.id)) + raise_for_status(r) + + # open the archive + archive_stream = io.BytesIO(r.content) + if is_rarfile(archive_stream): + logger.debug('Identified rar archive') + archive.content = RarFile(archive_stream) + elif is_zipfile(archive_stream): + logger.debug('Identified zip archive') + archive.content = ZipFile(archive_stream) + else: + raise ValueError('Not a valid archive') + + def query(self, language, title, season=None, episodes=None, year=None): + # search for titles + titles = self.search_titles(title, season, year) + + subtitles = [] + # iterate over titles + for title_id, t in titles.items(): + + logger.info('Getting archives for title %d and language %d', title_id, language.legendastv) + archives = self.get_archives(title_id, language.legendastv, t['type'], season, episodes or []) + if not archives: + logger.info('No archives found for title %d and language %d', title_id, language.legendastv) + + # iterate over title's archives + for a in archives: + + # compute an expiration time based on the archive timestamp + expiration_time = (datetime.utcnow().replace(tzinfo=pytz.utc) - a.timestamp).total_seconds() + + # attempt to get the releases from the cache + cache_key = releases_key.format(archive_id=a.id, archive_name=a.name) + releases = region.get(cache_key, expiration_time=expiration_time) + + # the releases are not in cache or cache is expired + if releases == NO_VALUE: + logger.info('Releases not found in cache') + + # download archive + self.download_archive(a) + + # extract the releases + releases = [] + for name in a.content.namelist(): + # discard the legendastv file + if name.startswith('Legendas.tv'): + continue + + # discard hidden files + if os.path.split(name)[-1].startswith('.'): + continue + + # discard non-subtitle files + if not name.lower().endswith(SUBTITLE_EXTENSIONS): + continue + + releases.append(name) + + # cache the releases + region.set(cache_key, releases) + + # iterate over releases + for r in releases: + subtitle = self.subtitle_class(language, t['type'], t['title'], t.get('year'), t.get('imdb_id'), + t.get('season'), a, r) + logger.debug('Found subtitle %r', subtitle) + subtitles.append(subtitle) + + return subtitles + + def list_subtitles(self, video, languages): + season = None + episodes = [] + if isinstance(video, Episode): + titles = [video.series] + video.alternative_series + season = video.season + episodes = video.episodes + else: + titles = [video.title] + video.alternative_titles + + for title in titles: + subtitles = [s for l in languages for s in + self.query(l, title, season=season, episodes=episodes, year=video.year)] + if subtitles: + return subtitles + + return [] + + def download_subtitle(self, subtitle): + # download archive in case we previously hit the releases cache and didn't download it + if subtitle.archive.content is None: + self.download_archive(subtitle.archive) + + # extract subtitle's content + subtitle.content = fix_line_ending(subtitle.archive.content.read(subtitle.name)) + + +def raise_for_status(r): + # When site is under maintaince and http status code 200. + if 'Em breve estaremos de volta' in r.text: + raise ServiceUnavailable + else: + r.raise_for_status() diff --git a/libs/subliminal/providers/napiprojekt.py b/custom_libs/subliminal/providers/napiprojekt.py similarity index 100% rename from libs/subliminal/providers/napiprojekt.py rename to custom_libs/subliminal/providers/napiprojekt.py diff --git a/libs/subliminal/providers/opensubtitles.py b/custom_libs/subliminal/providers/opensubtitles.py similarity index 100% rename from libs/subliminal/providers/opensubtitles.py rename to custom_libs/subliminal/providers/opensubtitles.py diff --git a/libs/subliminal/providers/podnapisi.py b/custom_libs/subliminal/providers/podnapisi.py similarity index 100% rename from libs/subliminal/providers/podnapisi.py rename to custom_libs/subliminal/providers/podnapisi.py diff --git a/libs/subliminal/providers/shooter.py b/custom_libs/subliminal/providers/shooter.py similarity index 100% rename from libs/subliminal/providers/shooter.py rename to custom_libs/subliminal/providers/shooter.py diff --git a/libs/subliminal/providers/subscenter.py b/custom_libs/subliminal/providers/subscenter.py similarity index 100% rename from libs/subliminal/providers/subscenter.py rename to custom_libs/subliminal/providers/subscenter.py diff --git a/libs/subliminal/providers/thesubdb.py b/custom_libs/subliminal/providers/thesubdb.py similarity index 100% rename from libs/subliminal/providers/thesubdb.py rename to custom_libs/subliminal/providers/thesubdb.py diff --git a/libs/subliminal/providers/tvsubtitles.py b/custom_libs/subliminal/providers/tvsubtitles.py similarity index 100% rename from libs/subliminal/providers/tvsubtitles.py rename to custom_libs/subliminal/providers/tvsubtitles.py diff --git a/libs/subliminal/refiners/__init__.py b/custom_libs/subliminal/refiners/__init__.py similarity index 100% rename from libs/subliminal/refiners/__init__.py rename to custom_libs/subliminal/refiners/__init__.py diff --git a/custom_libs/subliminal/refiners/hash.py b/custom_libs/subliminal/refiners/hash.py new file mode 100644 index 000000000..88e31f5a7 --- /dev/null +++ b/custom_libs/subliminal/refiners/hash.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +import logging + +from ..extensions import provider_manager, default_providers +from ..utils import hash_napiprojekt, hash_opensubtitles, hash_shooter, hash_thesubdb + +logger = logging.getLogger(__name__) + +hash_functions = { + 'napiprojekt': hash_napiprojekt, + 'opensubtitles': hash_opensubtitles, + 'opensubtitlesvip': hash_opensubtitles, + 'shooter': hash_shooter, + 'thesubdb': hash_thesubdb +} + + +def refine(video, providers=None, languages=None, **kwargs): + """Refine a video computing required hashes for the given providers. + + The following :class:`~subliminal.video.Video` attribute can be found: + + * :attr:`~subliminal.video.Video.hashes` + + """ + if video.size <= 10485760: + logger.warning('Size is lower than 10MB: hashes not computed') + return + + logger.debug('Computing hashes for %r', video.name) + for name in providers or default_providers: + provider = provider_manager[name].plugin + if name not in hash_functions: + continue + + if not provider.check_types(video): + continue + + if languages and not provider.check_languages(languages): + continue + + video.hashes[name] = hash_functions[name](video.name) + + logger.debug('Computed hashes %r', video.hashes) diff --git a/libs/subliminal/refiners/metadata.py b/custom_libs/subliminal/refiners/metadata.py similarity index 100% rename from libs/subliminal/refiners/metadata.py rename to custom_libs/subliminal/refiners/metadata.py diff --git a/libs/subliminal/refiners/omdb.py b/custom_libs/subliminal/refiners/omdb.py similarity index 100% rename from libs/subliminal/refiners/omdb.py rename to custom_libs/subliminal/refiners/omdb.py diff --git a/libs/subliminal/refiners/tvdb.py b/custom_libs/subliminal/refiners/tvdb.py similarity index 100% rename from libs/subliminal/refiners/tvdb.py rename to custom_libs/subliminal/refiners/tvdb.py diff --git a/libs/subliminal/score.py b/custom_libs/subliminal/score.py similarity index 100% rename from libs/subliminal/score.py rename to custom_libs/subliminal/score.py diff --git a/libs/subliminal/subtitle.py b/custom_libs/subliminal/subtitle.py similarity index 100% rename from libs/subliminal/subtitle.py rename to custom_libs/subliminal/subtitle.py diff --git a/libs/subliminal/utils.py b/custom_libs/subliminal/utils.py similarity index 100% rename from libs/subliminal/utils.py rename to custom_libs/subliminal/utils.py diff --git a/libs/subliminal/video.py b/custom_libs/subliminal/video.py similarity index 100% rename from libs/subliminal/video.py rename to custom_libs/subliminal/video.py diff --git a/libs/subliminal_patch/__init__.py b/custom_libs/subliminal_patch/__init__.py similarity index 100% rename from libs/subliminal_patch/__init__.py rename to custom_libs/subliminal_patch/__init__.py diff --git a/libs/ffmpeg/tests/__init__.py b/custom_libs/subliminal_patch/converters/__init__.py similarity index 100% rename from libs/ffmpeg/tests/__init__.py rename to custom_libs/subliminal_patch/converters/__init__.py diff --git a/libs/subliminal_patch/converters/assrt.py b/custom_libs/subliminal_patch/converters/assrt.py similarity index 100% rename from libs/subliminal_patch/converters/assrt.py rename to custom_libs/subliminal_patch/converters/assrt.py diff --git a/libs/subliminal_patch/converters/hosszupuska.py b/custom_libs/subliminal_patch/converters/hosszupuska.py similarity index 100% rename from libs/subliminal_patch/converters/hosszupuska.py rename to custom_libs/subliminal_patch/converters/hosszupuska.py diff --git a/libs/subliminal_patch/converters/subscene.py b/custom_libs/subliminal_patch/converters/subscene.py similarity index 100% rename from libs/subliminal_patch/converters/subscene.py rename to custom_libs/subliminal_patch/converters/subscene.py diff --git a/libs/subliminal_patch/converters/supersubtitles.py b/custom_libs/subliminal_patch/converters/supersubtitles.py similarity index 100% rename from libs/subliminal_patch/converters/supersubtitles.py rename to custom_libs/subliminal_patch/converters/supersubtitles.py diff --git a/libs/subliminal_patch/converters/titlovi.py b/custom_libs/subliminal_patch/converters/titlovi.py similarity index 100% rename from libs/subliminal_patch/converters/titlovi.py rename to custom_libs/subliminal_patch/converters/titlovi.py diff --git a/libs/subliminal_patch/converters/zimuku.py b/custom_libs/subliminal_patch/converters/zimuku.py similarity index 100% rename from libs/subliminal_patch/converters/zimuku.py rename to custom_libs/subliminal_patch/converters/zimuku.py diff --git a/libs/subliminal_patch/core.py b/custom_libs/subliminal_patch/core.py similarity index 100% rename from libs/subliminal_patch/core.py rename to custom_libs/subliminal_patch/core.py diff --git a/libs/subliminal_patch/core_persistent.py b/custom_libs/subliminal_patch/core_persistent.py similarity index 100% rename from libs/subliminal_patch/core_persistent.py rename to custom_libs/subliminal_patch/core_persistent.py diff --git a/libs/subliminal_patch/exceptions.py b/custom_libs/subliminal_patch/exceptions.py similarity index 100% rename from libs/subliminal_patch/exceptions.py rename to custom_libs/subliminal_patch/exceptions.py diff --git a/libs/subliminal_patch/extensions.py b/custom_libs/subliminal_patch/extensions.py similarity index 100% rename from libs/subliminal_patch/extensions.py rename to custom_libs/subliminal_patch/extensions.py diff --git a/libs/subliminal_patch/http.py b/custom_libs/subliminal_patch/http.py similarity index 100% rename from libs/subliminal_patch/http.py rename to custom_libs/subliminal_patch/http.py diff --git a/libs/subliminal_patch/language.py b/custom_libs/subliminal_patch/language.py similarity index 100% rename from libs/subliminal_patch/language.py rename to custom_libs/subliminal_patch/language.py diff --git a/libs/subliminal_patch/pitcher.py b/custom_libs/subliminal_patch/pitcher.py similarity index 100% rename from libs/subliminal_patch/pitcher.py rename to custom_libs/subliminal_patch/pitcher.py diff --git a/libs/subliminal_patch/providers/__init__.py b/custom_libs/subliminal_patch/providers/__init__.py similarity index 100% rename from libs/subliminal_patch/providers/__init__.py rename to custom_libs/subliminal_patch/providers/__init__.py diff --git a/libs/subliminal_patch/providers/_agent_list.py b/custom_libs/subliminal_patch/providers/_agent_list.py similarity index 100% rename from libs/subliminal_patch/providers/_agent_list.py rename to custom_libs/subliminal_patch/providers/_agent_list.py diff --git a/libs/subliminal_patch/providers/addic7ed.py b/custom_libs/subliminal_patch/providers/addic7ed.py similarity index 100% rename from libs/subliminal_patch/providers/addic7ed.py rename to custom_libs/subliminal_patch/providers/addic7ed.py diff --git a/libs/subliminal_patch/providers/argenteamdump.py b/custom_libs/subliminal_patch/providers/argenteamdump.py similarity index 100% rename from libs/subliminal_patch/providers/argenteamdump.py rename to custom_libs/subliminal_patch/providers/argenteamdump.py diff --git a/libs/subliminal_patch/providers/assrt.py b/custom_libs/subliminal_patch/providers/assrt.py similarity index 100% rename from libs/subliminal_patch/providers/assrt.py rename to custom_libs/subliminal_patch/providers/assrt.py diff --git a/libs/subliminal_patch/providers/avistaz.py b/custom_libs/subliminal_patch/providers/avistaz.py similarity index 100% rename from libs/subliminal_patch/providers/avistaz.py rename to custom_libs/subliminal_patch/providers/avistaz.py diff --git a/libs/subliminal_patch/providers/avistaz_network.py b/custom_libs/subliminal_patch/providers/avistaz_network.py similarity index 100% rename from libs/subliminal_patch/providers/avistaz_network.py rename to custom_libs/subliminal_patch/providers/avistaz_network.py diff --git a/libs/subliminal_patch/providers/betaseries.py b/custom_libs/subliminal_patch/providers/betaseries.py similarity index 100% rename from libs/subliminal_patch/providers/betaseries.py rename to custom_libs/subliminal_patch/providers/betaseries.py diff --git a/libs/subliminal_patch/providers/bsplayer.py b/custom_libs/subliminal_patch/providers/bsplayer.py similarity index 100% rename from libs/subliminal_patch/providers/bsplayer.py rename to custom_libs/subliminal_patch/providers/bsplayer.py diff --git a/libs/subliminal_patch/providers/cinemaz.py b/custom_libs/subliminal_patch/providers/cinemaz.py similarity index 100% rename from libs/subliminal_patch/providers/cinemaz.py rename to custom_libs/subliminal_patch/providers/cinemaz.py diff --git a/libs/subliminal_patch/providers/embeddedsubtitles.py b/custom_libs/subliminal_patch/providers/embeddedsubtitles.py similarity index 100% rename from libs/subliminal_patch/providers/embeddedsubtitles.py rename to custom_libs/subliminal_patch/providers/embeddedsubtitles.py diff --git a/libs/subliminal_patch/providers/gestdown.py b/custom_libs/subliminal_patch/providers/gestdown.py similarity index 100% rename from libs/subliminal_patch/providers/gestdown.py rename to custom_libs/subliminal_patch/providers/gestdown.py diff --git a/libs/subliminal_patch/providers/greeksubs.py b/custom_libs/subliminal_patch/providers/greeksubs.py similarity index 100% rename from libs/subliminal_patch/providers/greeksubs.py rename to custom_libs/subliminal_patch/providers/greeksubs.py diff --git a/libs/subliminal_patch/providers/greeksubtitles.py b/custom_libs/subliminal_patch/providers/greeksubtitles.py similarity index 100% rename from libs/subliminal_patch/providers/greeksubtitles.py rename to custom_libs/subliminal_patch/providers/greeksubtitles.py diff --git a/libs/subliminal_patch/providers/hdbits.py b/custom_libs/subliminal_patch/providers/hdbits.py similarity index 100% rename from libs/subliminal_patch/providers/hdbits.py rename to custom_libs/subliminal_patch/providers/hdbits.py diff --git a/libs/subliminal_patch/providers/hosszupuska.py b/custom_libs/subliminal_patch/providers/hosszupuska.py similarity index 100% rename from libs/subliminal_patch/providers/hosszupuska.py rename to custom_libs/subliminal_patch/providers/hosszupuska.py diff --git a/libs/subliminal_patch/providers/karagarga.py b/custom_libs/subliminal_patch/providers/karagarga.py similarity index 100% rename from libs/subliminal_patch/providers/karagarga.py rename to custom_libs/subliminal_patch/providers/karagarga.py diff --git a/libs/subliminal_patch/providers/ktuvit.py b/custom_libs/subliminal_patch/providers/ktuvit.py similarity index 100% rename from libs/subliminal_patch/providers/ktuvit.py rename to custom_libs/subliminal_patch/providers/ktuvit.py diff --git a/libs/subliminal_patch/providers/legendasdivx.py b/custom_libs/subliminal_patch/providers/legendasdivx.py similarity index 100% rename from libs/subliminal_patch/providers/legendasdivx.py rename to custom_libs/subliminal_patch/providers/legendasdivx.py diff --git a/libs/subliminal_patch/providers/mixins.py b/custom_libs/subliminal_patch/providers/mixins.py similarity index 100% rename from libs/subliminal_patch/providers/mixins.py rename to custom_libs/subliminal_patch/providers/mixins.py diff --git a/libs/subliminal_patch/providers/napiprojekt.py b/custom_libs/subliminal_patch/providers/napiprojekt.py similarity index 100% rename from libs/subliminal_patch/providers/napiprojekt.py rename to custom_libs/subliminal_patch/providers/napiprojekt.py diff --git a/libs/subliminal_patch/providers/napisy24.py b/custom_libs/subliminal_patch/providers/napisy24.py similarity index 100% rename from libs/subliminal_patch/providers/napisy24.py rename to custom_libs/subliminal_patch/providers/napisy24.py diff --git a/libs/subliminal_patch/providers/nekur.py b/custom_libs/subliminal_patch/providers/nekur.py similarity index 100% rename from libs/subliminal_patch/providers/nekur.py rename to custom_libs/subliminal_patch/providers/nekur.py diff --git a/libs/subliminal_patch/providers/opensubtitles.py b/custom_libs/subliminal_patch/providers/opensubtitles.py similarity index 100% rename from libs/subliminal_patch/providers/opensubtitles.py rename to custom_libs/subliminal_patch/providers/opensubtitles.py diff --git a/libs/subliminal_patch/providers/opensubtitlescom.py b/custom_libs/subliminal_patch/providers/opensubtitlescom.py similarity index 100% rename from libs/subliminal_patch/providers/opensubtitlescom.py rename to custom_libs/subliminal_patch/providers/opensubtitlescom.py diff --git a/libs/subliminal_patch/providers/podnapisi.py b/custom_libs/subliminal_patch/providers/podnapisi.py similarity index 100% rename from libs/subliminal_patch/providers/podnapisi.py rename to custom_libs/subliminal_patch/providers/podnapisi.py diff --git a/libs/subliminal_patch/providers/regielive.py b/custom_libs/subliminal_patch/providers/regielive.py similarity index 100% rename from libs/subliminal_patch/providers/regielive.py rename to custom_libs/subliminal_patch/providers/regielive.py diff --git a/libs/subliminal_patch/providers/shooter.py b/custom_libs/subliminal_patch/providers/shooter.py similarity index 100% rename from libs/subliminal_patch/providers/shooter.py rename to custom_libs/subliminal_patch/providers/shooter.py diff --git a/libs/subliminal_patch/providers/soustitreseu.py b/custom_libs/subliminal_patch/providers/soustitreseu.py similarity index 100% rename from libs/subliminal_patch/providers/soustitreseu.py rename to custom_libs/subliminal_patch/providers/soustitreseu.py diff --git a/libs/subliminal_patch/providers/subdivx.py b/custom_libs/subliminal_patch/providers/subdivx.py similarity index 100% rename from libs/subliminal_patch/providers/subdivx.py rename to custom_libs/subliminal_patch/providers/subdivx.py diff --git a/libs/subliminal_patch/providers/subf2m.py b/custom_libs/subliminal_patch/providers/subf2m.py similarity index 100% rename from libs/subliminal_patch/providers/subf2m.py rename to custom_libs/subliminal_patch/providers/subf2m.py diff --git a/libs/subliminal_patch/providers/subs4free.py b/custom_libs/subliminal_patch/providers/subs4free.py similarity index 100% rename from libs/subliminal_patch/providers/subs4free.py rename to custom_libs/subliminal_patch/providers/subs4free.py diff --git a/libs/subliminal_patch/providers/subs4series.py b/custom_libs/subliminal_patch/providers/subs4series.py similarity index 100% rename from libs/subliminal_patch/providers/subs4series.py rename to custom_libs/subliminal_patch/providers/subs4series.py diff --git a/libs/subliminal_patch/providers/subscene.py b/custom_libs/subliminal_patch/providers/subscene.py similarity index 100% rename from libs/subliminal_patch/providers/subscene.py rename to custom_libs/subliminal_patch/providers/subscene.py diff --git a/libs/subliminal_patch/providers/subscene_cloudscraper.py b/custom_libs/subliminal_patch/providers/subscene_cloudscraper.py similarity index 100% rename from libs/subliminal_patch/providers/subscene_cloudscraper.py rename to custom_libs/subliminal_patch/providers/subscene_cloudscraper.py diff --git a/libs/subliminal_patch/providers/subscenter.py b/custom_libs/subliminal_patch/providers/subscenter.py similarity index 100% rename from libs/subliminal_patch/providers/subscenter.py rename to custom_libs/subliminal_patch/providers/subscenter.py diff --git a/libs/subliminal_patch/providers/subssabbz.py b/custom_libs/subliminal_patch/providers/subssabbz.py similarity index 100% rename from libs/subliminal_patch/providers/subssabbz.py rename to custom_libs/subliminal_patch/providers/subssabbz.py diff --git a/libs/subliminal_patch/providers/subsunacs.py b/custom_libs/subliminal_patch/providers/subsunacs.py similarity index 100% rename from libs/subliminal_patch/providers/subsunacs.py rename to custom_libs/subliminal_patch/providers/subsunacs.py diff --git a/libs/subliminal_patch/providers/subsynchro.py b/custom_libs/subliminal_patch/providers/subsynchro.py similarity index 100% rename from libs/subliminal_patch/providers/subsynchro.py rename to custom_libs/subliminal_patch/providers/subsynchro.py diff --git a/libs/subliminal_patch/providers/subtitrarinoi.py b/custom_libs/subliminal_patch/providers/subtitrarinoi.py similarity index 100% rename from libs/subliminal_patch/providers/subtitrarinoi.py rename to custom_libs/subliminal_patch/providers/subtitrarinoi.py diff --git a/libs/subliminal_patch/providers/subtitriid.py b/custom_libs/subliminal_patch/providers/subtitriid.py similarity index 100% rename from libs/subliminal_patch/providers/subtitriid.py rename to custom_libs/subliminal_patch/providers/subtitriid.py diff --git a/libs/subliminal_patch/providers/subtitulamostv.py b/custom_libs/subliminal_patch/providers/subtitulamostv.py similarity index 100% rename from libs/subliminal_patch/providers/subtitulamostv.py rename to custom_libs/subliminal_patch/providers/subtitulamostv.py diff --git a/libs/subliminal_patch/providers/supersubtitles.py b/custom_libs/subliminal_patch/providers/supersubtitles.py similarity index 100% rename from libs/subliminal_patch/providers/supersubtitles.py rename to custom_libs/subliminal_patch/providers/supersubtitles.py diff --git a/libs/subliminal_patch/providers/titlovi.py b/custom_libs/subliminal_patch/providers/titlovi.py similarity index 100% rename from libs/subliminal_patch/providers/titlovi.py rename to custom_libs/subliminal_patch/providers/titlovi.py diff --git a/libs/subliminal_patch/providers/titrari.py b/custom_libs/subliminal_patch/providers/titrari.py similarity index 100% rename from libs/subliminal_patch/providers/titrari.py rename to custom_libs/subliminal_patch/providers/titrari.py diff --git a/libs/subliminal_patch/providers/titulky.py b/custom_libs/subliminal_patch/providers/titulky.py similarity index 100% rename from libs/subliminal_patch/providers/titulky.py rename to custom_libs/subliminal_patch/providers/titulky.py diff --git a/libs/subliminal_patch/providers/tusubtitulo.py b/custom_libs/subliminal_patch/providers/tusubtitulo.py similarity index 100% rename from libs/subliminal_patch/providers/tusubtitulo.py rename to custom_libs/subliminal_patch/providers/tusubtitulo.py diff --git a/libs/subliminal_patch/providers/tvsubtitles.py b/custom_libs/subliminal_patch/providers/tvsubtitles.py similarity index 100% rename from libs/subliminal_patch/providers/tvsubtitles.py rename to custom_libs/subliminal_patch/providers/tvsubtitles.py diff --git a/libs/subliminal_patch/providers/utils.py b/custom_libs/subliminal_patch/providers/utils.py similarity index 100% rename from libs/subliminal_patch/providers/utils.py rename to custom_libs/subliminal_patch/providers/utils.py diff --git a/libs/subliminal_patch/providers/whisperai.py b/custom_libs/subliminal_patch/providers/whisperai.py similarity index 100% rename from libs/subliminal_patch/providers/whisperai.py rename to custom_libs/subliminal_patch/providers/whisperai.py diff --git a/libs/subliminal_patch/providers/wizdom.py b/custom_libs/subliminal_patch/providers/wizdom.py similarity index 100% rename from libs/subliminal_patch/providers/wizdom.py rename to custom_libs/subliminal_patch/providers/wizdom.py diff --git a/libs/subliminal_patch/providers/xsubs.py b/custom_libs/subliminal_patch/providers/xsubs.py similarity index 100% rename from libs/subliminal_patch/providers/xsubs.py rename to custom_libs/subliminal_patch/providers/xsubs.py diff --git a/libs/subliminal_patch/providers/yavkanet.py b/custom_libs/subliminal_patch/providers/yavkanet.py similarity index 100% rename from libs/subliminal_patch/providers/yavkanet.py rename to custom_libs/subliminal_patch/providers/yavkanet.py diff --git a/libs/subliminal_patch/providers/yifysubtitles.py b/custom_libs/subliminal_patch/providers/yifysubtitles.py similarity index 100% rename from libs/subliminal_patch/providers/yifysubtitles.py rename to custom_libs/subliminal_patch/providers/yifysubtitles.py diff --git a/libs/subliminal_patch/providers/zimuku.py b/custom_libs/subliminal_patch/providers/zimuku.py similarity index 100% rename from libs/subliminal_patch/providers/zimuku.py rename to custom_libs/subliminal_patch/providers/zimuku.py diff --git a/libs/subliminal_patch/refiners/__init__.py b/custom_libs/subliminal_patch/refiners/__init__.py similarity index 100% rename from libs/subliminal_patch/refiners/__init__.py rename to custom_libs/subliminal_patch/refiners/__init__.py diff --git a/libs/subliminal_patch/refiners/common.py b/custom_libs/subliminal_patch/refiners/common.py similarity index 100% rename from libs/subliminal_patch/refiners/common.py rename to custom_libs/subliminal_patch/refiners/common.py diff --git a/libs/subliminal_patch/refiners/drone.py b/custom_libs/subliminal_patch/refiners/drone.py similarity index 100% rename from libs/subliminal_patch/refiners/drone.py rename to custom_libs/subliminal_patch/refiners/drone.py diff --git a/libs/subliminal_patch/refiners/file_info_file.py b/custom_libs/subliminal_patch/refiners/file_info_file.py similarity index 100% rename from libs/subliminal_patch/refiners/file_info_file.py rename to custom_libs/subliminal_patch/refiners/file_info_file.py diff --git a/libs/subliminal_patch/refiners/filebot.py b/custom_libs/subliminal_patch/refiners/filebot.py similarity index 100% rename from libs/subliminal_patch/refiners/filebot.py rename to custom_libs/subliminal_patch/refiners/filebot.py diff --git a/libs/subliminal_patch/refiners/metadata.py b/custom_libs/subliminal_patch/refiners/metadata.py similarity index 100% rename from libs/subliminal_patch/refiners/metadata.py rename to custom_libs/subliminal_patch/refiners/metadata.py diff --git a/libs/subliminal_patch/refiners/omdb.py b/custom_libs/subliminal_patch/refiners/omdb.py similarity index 100% rename from libs/subliminal_patch/refiners/omdb.py rename to custom_libs/subliminal_patch/refiners/omdb.py diff --git a/libs/subliminal_patch/refiners/symlinks.py b/custom_libs/subliminal_patch/refiners/symlinks.py similarity index 100% rename from libs/subliminal_patch/refiners/symlinks.py rename to custom_libs/subliminal_patch/refiners/symlinks.py diff --git a/libs/subliminal_patch/refiners/tvdb.py b/custom_libs/subliminal_patch/refiners/tvdb.py similarity index 100% rename from libs/subliminal_patch/refiners/tvdb.py rename to custom_libs/subliminal_patch/refiners/tvdb.py diff --git a/libs/subliminal_patch/refiners/util.py b/custom_libs/subliminal_patch/refiners/util.py similarity index 100% rename from libs/subliminal_patch/refiners/util.py rename to custom_libs/subliminal_patch/refiners/util.py diff --git a/libs/subliminal_patch/score.py b/custom_libs/subliminal_patch/score.py similarity index 100% rename from libs/subliminal_patch/score.py rename to custom_libs/subliminal_patch/score.py diff --git a/libs/subliminal_patch/subtitle.py b/custom_libs/subliminal_patch/subtitle.py similarity index 100% rename from libs/subliminal_patch/subtitle.py rename to custom_libs/subliminal_patch/subtitle.py diff --git a/libs/subliminal_patch/utils.py b/custom_libs/subliminal_patch/utils.py similarity index 100% rename from libs/subliminal_patch/utils.py rename to custom_libs/subliminal_patch/utils.py diff --git a/libs/subliminal_patch/video.py b/custom_libs/subliminal_patch/video.py similarity index 100% rename from libs/subliminal_patch/video.py rename to custom_libs/subliminal_patch/video.py diff --git a/libs/importlib_resources/tests/zipdata01/__init__.py b/custom_libs/subscene_api/__init__.py similarity index 100% rename from libs/importlib_resources/tests/zipdata01/__init__.py rename to custom_libs/subscene_api/__init__.py diff --git a/libs/subscene_api/subscene.py b/custom_libs/subscene_api/subscene.py similarity index 100% rename from libs/subscene_api/subscene.py rename to custom_libs/subscene_api/subscene.py diff --git a/libs/subzero/__init__.py b/custom_libs/subzero/__init__.py similarity index 100% rename from libs/subzero/__init__.py rename to custom_libs/subzero/__init__.py diff --git a/libs/subzero/analytics.py b/custom_libs/subzero/analytics.py similarity index 100% rename from libs/subzero/analytics.py rename to custom_libs/subzero/analytics.py diff --git a/libs/subzero/cache_backends/__init__.py b/custom_libs/subzero/cache_backends/__init__.py similarity index 100% rename from libs/subzero/cache_backends/__init__.py rename to custom_libs/subzero/cache_backends/__init__.py diff --git a/libs/subzero/cache_backends/file.py b/custom_libs/subzero/cache_backends/file.py similarity index 100% rename from libs/subzero/cache_backends/file.py rename to custom_libs/subzero/cache_backends/file.py diff --git a/libs/subzero/constants.py b/custom_libs/subzero/constants.py similarity index 100% rename from libs/subzero/constants.py rename to custom_libs/subzero/constants.py diff --git a/libs/subzero/history_storage.py b/custom_libs/subzero/history_storage.py similarity index 100% rename from libs/subzero/history_storage.py rename to custom_libs/subzero/history_storage.py diff --git a/libs/subzero/intent.py b/custom_libs/subzero/intent.py similarity index 100% rename from libs/subzero/intent.py rename to custom_libs/subzero/intent.py diff --git a/libs/subzero/language.py b/custom_libs/subzero/language.py similarity index 100% rename from libs/subzero/language.py rename to custom_libs/subzero/language.py diff --git a/libs/subzero/lib/__init__.py b/custom_libs/subzero/lib/__init__.py similarity index 100% rename from libs/subzero/lib/__init__.py rename to custom_libs/subzero/lib/__init__.py diff --git a/libs/subzero/lib/dict.py b/custom_libs/subzero/lib/dict.py similarity index 100% rename from libs/subzero/lib/dict.py rename to custom_libs/subzero/lib/dict.py diff --git a/libs/subzero/lib/geezip.py b/custom_libs/subzero/lib/geezip.py similarity index 100% rename from libs/subzero/lib/geezip.py rename to custom_libs/subzero/lib/geezip.py diff --git a/libs/subzero/lib/httpfake.py b/custom_libs/subzero/lib/httpfake.py similarity index 100% rename from libs/subzero/lib/httpfake.py rename to custom_libs/subzero/lib/httpfake.py diff --git a/libs/subzero/lib/io.py b/custom_libs/subzero/lib/io.py similarity index 100% rename from libs/subzero/lib/io.py rename to custom_libs/subzero/lib/io.py diff --git a/libs/subzero/lib/json.py b/custom_libs/subzero/lib/json.py similarity index 100% rename from libs/subzero/lib/json.py rename to custom_libs/subzero/lib/json.py diff --git a/libs/subzero/lib/rar.py b/custom_libs/subzero/lib/rar.py similarity index 100% rename from libs/subzero/lib/rar.py rename to custom_libs/subzero/lib/rar.py diff --git a/libs/subzero/lib/which.py b/custom_libs/subzero/lib/which.py similarity index 100% rename from libs/subzero/lib/which.py rename to custom_libs/subzero/lib/which.py diff --git a/libs/subzero/modification/__init__.py b/custom_libs/subzero/modification/__init__.py similarity index 100% rename from libs/subzero/modification/__init__.py rename to custom_libs/subzero/modification/__init__.py diff --git a/libs/subzero/modification/dictionaries/__init__.py b/custom_libs/subzero/modification/dictionaries/__init__.py similarity index 100% rename from libs/subzero/modification/dictionaries/__init__.py rename to custom_libs/subzero/modification/dictionaries/__init__.py diff --git a/libs/subzero/modification/dictionaries/data.py b/custom_libs/subzero/modification/dictionaries/data.py similarity index 100% rename from libs/subzero/modification/dictionaries/data.py rename to custom_libs/subzero/modification/dictionaries/data.py diff --git a/libs/subzero/modification/dictionaries/make_data.py b/custom_libs/subzero/modification/dictionaries/make_data.py similarity index 100% rename from libs/subzero/modification/dictionaries/make_data.py rename to custom_libs/subzero/modification/dictionaries/make_data.py diff --git a/libs/subzero/modification/dictionaries/test_data.py b/custom_libs/subzero/modification/dictionaries/test_data.py similarity index 100% rename from libs/subzero/modification/dictionaries/test_data.py rename to custom_libs/subzero/modification/dictionaries/test_data.py diff --git a/libs/subzero/modification/dictionaries/xml/bos_OCRFixReplaceList.xml b/custom_libs/subzero/modification/dictionaries/xml/bos_OCRFixReplaceList.xml similarity index 100% rename from libs/subzero/modification/dictionaries/xml/bos_OCRFixReplaceList.xml rename to custom_libs/subzero/modification/dictionaries/xml/bos_OCRFixReplaceList.xml diff --git a/libs/subzero/modification/dictionaries/xml/dan_OCRFixReplaceList.xml b/custom_libs/subzero/modification/dictionaries/xml/dan_OCRFixReplaceList.xml similarity index 100% rename from libs/subzero/modification/dictionaries/xml/dan_OCRFixReplaceList.xml rename to custom_libs/subzero/modification/dictionaries/xml/dan_OCRFixReplaceList.xml diff --git a/libs/subzero/modification/dictionaries/xml/deu_OCRFixReplaceList.xml b/custom_libs/subzero/modification/dictionaries/xml/deu_OCRFixReplaceList.xml similarity index 100% rename from libs/subzero/modification/dictionaries/xml/deu_OCRFixReplaceList.xml rename to custom_libs/subzero/modification/dictionaries/xml/deu_OCRFixReplaceList.xml diff --git a/libs/subzero/modification/dictionaries/xml/eng_OCRFixReplaceList.xml b/custom_libs/subzero/modification/dictionaries/xml/eng_OCRFixReplaceList.xml similarity index 100% rename from libs/subzero/modification/dictionaries/xml/eng_OCRFixReplaceList.xml rename to custom_libs/subzero/modification/dictionaries/xml/eng_OCRFixReplaceList.xml diff --git a/libs/subzero/modification/dictionaries/xml/fin_OCRFixReplaceList.xml b/custom_libs/subzero/modification/dictionaries/xml/fin_OCRFixReplaceList.xml similarity index 100% rename from libs/subzero/modification/dictionaries/xml/fin_OCRFixReplaceList.xml rename to custom_libs/subzero/modification/dictionaries/xml/fin_OCRFixReplaceList.xml diff --git a/libs/subzero/modification/dictionaries/xml/fra_OCRFixReplaceList.xml b/custom_libs/subzero/modification/dictionaries/xml/fra_OCRFixReplaceList.xml similarity index 100% rename from libs/subzero/modification/dictionaries/xml/fra_OCRFixReplaceList.xml rename to custom_libs/subzero/modification/dictionaries/xml/fra_OCRFixReplaceList.xml diff --git a/libs/subzero/modification/dictionaries/xml/hrv_OCRFixReplaceList.xml b/custom_libs/subzero/modification/dictionaries/xml/hrv_OCRFixReplaceList.xml similarity index 100% rename from libs/subzero/modification/dictionaries/xml/hrv_OCRFixReplaceList.xml rename to custom_libs/subzero/modification/dictionaries/xml/hrv_OCRFixReplaceList.xml diff --git a/libs/subzero/modification/dictionaries/xml/hrv_diacriticOCRFixReplaceList.xml b/custom_libs/subzero/modification/dictionaries/xml/hrv_diacriticOCRFixReplaceList.xml similarity index 100% rename from libs/subzero/modification/dictionaries/xml/hrv_diacriticOCRFixReplaceList.xml rename to custom_libs/subzero/modification/dictionaries/xml/hrv_diacriticOCRFixReplaceList.xml diff --git a/libs/subzero/modification/dictionaries/xml/hun_OCRFixReplaceList.xml b/custom_libs/subzero/modification/dictionaries/xml/hun_OCRFixReplaceList.xml similarity index 100% rename from libs/subzero/modification/dictionaries/xml/hun_OCRFixReplaceList.xml rename to custom_libs/subzero/modification/dictionaries/xml/hun_OCRFixReplaceList.xml diff --git a/libs/subzero/modification/dictionaries/xml/nld_OCRFixReplaceList.xml b/custom_libs/subzero/modification/dictionaries/xml/nld_OCRFixReplaceList.xml similarity index 100% rename from libs/subzero/modification/dictionaries/xml/nld_OCRFixReplaceList.xml rename to custom_libs/subzero/modification/dictionaries/xml/nld_OCRFixReplaceList.xml diff --git a/libs/subzero/modification/dictionaries/xml/nob_OCRFixReplaceList.xml b/custom_libs/subzero/modification/dictionaries/xml/nob_OCRFixReplaceList.xml similarity index 100% rename from libs/subzero/modification/dictionaries/xml/nob_OCRFixReplaceList.xml rename to custom_libs/subzero/modification/dictionaries/xml/nob_OCRFixReplaceList.xml diff --git a/libs/subzero/modification/dictionaries/xml/nor_OCRFixReplaceList.xml b/custom_libs/subzero/modification/dictionaries/xml/nor_OCRFixReplaceList.xml similarity index 100% rename from libs/subzero/modification/dictionaries/xml/nor_OCRFixReplaceList.xml rename to custom_libs/subzero/modification/dictionaries/xml/nor_OCRFixReplaceList.xml diff --git a/libs/subzero/modification/dictionaries/xml/por_OCRFixReplaceList.xml b/custom_libs/subzero/modification/dictionaries/xml/por_OCRFixReplaceList.xml similarity index 100% rename from libs/subzero/modification/dictionaries/xml/por_OCRFixReplaceList.xml rename to custom_libs/subzero/modification/dictionaries/xml/por_OCRFixReplaceList.xml diff --git a/libs/subzero/modification/dictionaries/xml/rus_OCRFixReplaceList.xml b/custom_libs/subzero/modification/dictionaries/xml/rus_OCRFixReplaceList.xml similarity index 100% rename from libs/subzero/modification/dictionaries/xml/rus_OCRFixReplaceList.xml rename to custom_libs/subzero/modification/dictionaries/xml/rus_OCRFixReplaceList.xml diff --git a/libs/subzero/modification/dictionaries/xml/spa_OCRFixReplaceList.xml b/custom_libs/subzero/modification/dictionaries/xml/spa_OCRFixReplaceList.xml similarity index 100% rename from libs/subzero/modification/dictionaries/xml/spa_OCRFixReplaceList.xml rename to custom_libs/subzero/modification/dictionaries/xml/spa_OCRFixReplaceList.xml diff --git a/libs/subzero/modification/dictionaries/xml/srp_OCRFixReplaceList.xml b/custom_libs/subzero/modification/dictionaries/xml/srp_OCRFixReplaceList.xml similarity index 100% rename from libs/subzero/modification/dictionaries/xml/srp_OCRFixReplaceList.xml rename to custom_libs/subzero/modification/dictionaries/xml/srp_OCRFixReplaceList.xml diff --git a/libs/subzero/modification/dictionaries/xml/swe_OCRFixReplaceList.xml b/custom_libs/subzero/modification/dictionaries/xml/swe_OCRFixReplaceList.xml similarity index 100% rename from libs/subzero/modification/dictionaries/xml/swe_OCRFixReplaceList.xml rename to custom_libs/subzero/modification/dictionaries/xml/swe_OCRFixReplaceList.xml diff --git a/libs/subzero/modification/exc.py b/custom_libs/subzero/modification/exc.py similarity index 100% rename from libs/subzero/modification/exc.py rename to custom_libs/subzero/modification/exc.py diff --git a/libs/subzero/modification/main.py b/custom_libs/subzero/modification/main.py similarity index 100% rename from libs/subzero/modification/main.py rename to custom_libs/subzero/modification/main.py diff --git a/libs/subzero/modification/mods/__init__.py b/custom_libs/subzero/modification/mods/__init__.py similarity index 100% rename from libs/subzero/modification/mods/__init__.py rename to custom_libs/subzero/modification/mods/__init__.py diff --git a/libs/subzero/modification/mods/color.py b/custom_libs/subzero/modification/mods/color.py similarity index 100% rename from libs/subzero/modification/mods/color.py rename to custom_libs/subzero/modification/mods/color.py diff --git a/libs/subzero/modification/mods/common.py b/custom_libs/subzero/modification/mods/common.py similarity index 100% rename from libs/subzero/modification/mods/common.py rename to custom_libs/subzero/modification/mods/common.py diff --git a/libs/subzero/modification/mods/fps.py b/custom_libs/subzero/modification/mods/fps.py similarity index 100% rename from libs/subzero/modification/mods/fps.py rename to custom_libs/subzero/modification/mods/fps.py diff --git a/libs/subzero/modification/mods/hearing_impaired.py b/custom_libs/subzero/modification/mods/hearing_impaired.py similarity index 100% rename from libs/subzero/modification/mods/hearing_impaired.py rename to custom_libs/subzero/modification/mods/hearing_impaired.py diff --git a/libs/subzero/modification/mods/ocr_fixes.py b/custom_libs/subzero/modification/mods/ocr_fixes.py similarity index 100% rename from libs/subzero/modification/mods/ocr_fixes.py rename to custom_libs/subzero/modification/mods/ocr_fixes.py diff --git a/libs/subzero/modification/mods/offset.py b/custom_libs/subzero/modification/mods/offset.py similarity index 100% rename from libs/subzero/modification/mods/offset.py rename to custom_libs/subzero/modification/mods/offset.py diff --git a/libs/subzero/modification/processors/__init__.py b/custom_libs/subzero/modification/processors/__init__.py similarity index 100% rename from libs/subzero/modification/processors/__init__.py rename to custom_libs/subzero/modification/processors/__init__.py diff --git a/libs/subzero/modification/processors/re_processor.py b/custom_libs/subzero/modification/processors/re_processor.py similarity index 100% rename from libs/subzero/modification/processors/re_processor.py rename to custom_libs/subzero/modification/processors/re_processor.py diff --git a/libs/subzero/modification/processors/string_processor.py b/custom_libs/subzero/modification/processors/string_processor.py similarity index 100% rename from libs/subzero/modification/processors/string_processor.py rename to custom_libs/subzero/modification/processors/string_processor.py diff --git a/libs/subzero/modification/registry.py b/custom_libs/subzero/modification/registry.py similarity index 100% rename from libs/subzero/modification/registry.py rename to custom_libs/subzero/modification/registry.py diff --git a/libs/subzero/prefs.py b/custom_libs/subzero/prefs.py similarity index 100% rename from libs/subzero/prefs.py rename to custom_libs/subzero/prefs.py diff --git a/libs/subzero/sandbox.py b/custom_libs/subzero/sandbox.py similarity index 100% rename from libs/subzero/sandbox.py rename to custom_libs/subzero/sandbox.py diff --git a/libs/subzero/score_range.py b/custom_libs/subzero/score_range.py similarity index 100% rename from libs/subzero/score_range.py rename to custom_libs/subzero/score_range.py diff --git a/libs/subzero/subtitle_storage.py b/custom_libs/subzero/subtitle_storage.py similarity index 100% rename from libs/subzero/subtitle_storage.py rename to custom_libs/subzero/subtitle_storage.py diff --git a/libs/subzero/util.py b/custom_libs/subzero/util.py similarity index 100% rename from libs/subzero/util.py rename to custom_libs/subzero/util.py diff --git a/libs/subzero/video.py b/custom_libs/subzero/video.py similarity index 100% rename from libs/subzero/video.py rename to custom_libs/subzero/video.py diff --git a/frontend/config/configReader.ts b/frontend/config/configReader.ts index 78d682b51..e48396992 100644 --- a/frontend/config/configReader.ts +++ b/frontend/config/configReader.ts @@ -3,7 +3,7 @@ import { readFile } from "fs/promises"; import { get } from "lodash"; -import YAML from "yaml"; +import { parse } from "yaml"; class ConfigReader { config: object; @@ -15,7 +15,7 @@ class ConfigReader { async open(path: string) { try { const rawConfig = await readFile(path, "utf8"); - this.config = YAML.parse(rawConfig); + this.config = parse(rawConfig); } catch (err) { // We don't want to catch the error here, handle it on getValue method } @@ -53,7 +53,7 @@ export default async function overrideEnv(env: Record) { process.env["VITE_API_KEY"] = apiKey; } catch (err) { throw new Error( - `No API key found, please run the backend first, (error: ${err.message})` + `No API key found, please run the backend first, (error: ${err.message})`, ); } } @@ -71,7 +71,7 @@ export default async function overrideEnv(env: Record) { process.env["VITE_PROXY_URL"] = url; } catch (err) { throw new Error( - `No proxy url found, please run the backend first, (error: ${err.message})` + `No proxy url found, please run the backend first, (error: ${err.message})`, ); } } diff --git a/frontend/index.html b/frontend/index.html index 37beb6ce9..128ee35bc 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1,4 +1,4 @@ - + Bazarr diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 4de17a101..a4db05743 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -10,62 +10,72 @@ "license": "GPL-3", "dependencies": { "@mantine/core": "^6.0.0", - "@mantine/dropzone": "^6.0.0", - "@mantine/form": "^6.0.0", + "@mantine/dropzone": "^6.0.21", + "@mantine/form": "^6.0.21", "@mantine/hooks": "^6.0.0", - "@mantine/modals": "^6.0.0", - "@mantine/notifications": "^6.0.0", - "axios": "^0.27.2", + "@mantine/modals": "^6.0.21", + "@mantine/notifications": "^6.0.21", + "axios": "^1.6.7", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-query": "^3.39.2", - "react-router-dom": "~6.10.0", - "socket.io-client": "^4.5.3" + "react-query": "^3.39.3", + "react-router-dom": "^6.21.3", + "socket.io-client": "^4.7.4" }, "devDependencies": { - "@fontsource/roboto": "^4.5.0", - "@fortawesome/fontawesome-svg-core": "^6.2.0", - "@fortawesome/free-brands-svg-icons": "^6.2.0", - "@fortawesome/free-regular-svg-icons": "^6.2.0", - "@fortawesome/free-solid-svg-icons": "^6.2.0", + "@fontsource/roboto": "^5.0.8", + "@fortawesome/fontawesome-svg-core": "^6.5.1", + "@fortawesome/free-brands-svg-icons": "^6.5.1", + "@fortawesome/free-regular-svg-icons": "^6.5.1", + "@fortawesome/free-solid-svg-icons": "^6.5.1", "@fortawesome/react-fontawesome": "^0.2.0", - "@testing-library/jest-dom": "^5.16.5", - "@testing-library/react": "^14.0.0", - "@testing-library/user-event": "^14.4.3", - "@types/lodash": "^4.14.0", - "@types/node": "^18.16.0", - "@types/react": "^18.2.0", - "@types/react-dom": "^18.2.0", - "@types/react-table": "^7.7.0", - "@vitejs/plugin-react": "^4.0.0", - "@vitest/coverage-c8": "^0.30.0", - "@vitest/ui": "^0.30.0", - "clsx": "^1.2.0", - "eslint": "^8.39.0", + "@testing-library/jest-dom": "^6.3.0", + "@testing-library/react": "^14.1.2", + "@testing-library/user-event": "^14.5.2", + "@types/jest": "^29.5.11", + "@types/lodash": "^4.14.202", + "@types/node": "^20.11.7", + "@types/react": "^18.2.48", + "@types/react-dom": "^18.2.18", + "@types/react-table": "^7.7.19", + "@vitejs/plugin-react": "^4.2.1", + "@vitest/coverage-v8": "^1.2.2", + "@vitest/ui": "^1.2.2", + "clsx": "^2.1.0", + "eslint": "^8.56.0", "eslint-config-react-app": "^7.0.1", "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-testing-library": "^5.9.0", - "husky": "^8.0.2", - "jsdom": "^21.0.0", - "lodash": "^4.17.0", - "moment": "^2.29", - "prettier": "^2.8.0", - "prettier-plugin-organize-imports": "^3.1.0", - "pretty-quick": "^3.1.0", + "eslint-plugin-testing-library": "^6.2.0", + "husky": "^9.0.6", + "jsdom": "^24.0.0", + "lodash": "^4.17.21", + "moment": "^2.30.1", + "prettier": "^3.2.4", + "prettier-plugin-organize-imports": "^3.2.4", + "pretty-quick": "^4.0.0", "react-table": "^7.8.0", - "recharts": "~2.5.0", - "sass": "^1.62.0", - "typescript": "^5", - "vite": "^4.3.0", - "vite-plugin-checker": "^0.5.5", - "vitest": "^0.30.1", - "yaml": "^2.3.1" + "recharts": "^2.10.4", + "sass": "^1.70.0", + "typescript": "^5.3.3", + "vite": "^5.0.12", + "vite-plugin-checker": "^0.6.2", + "vitest": "^1.2.2", + "yaml": "^2.3.4" + } + }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" } }, "node_modules/@adobe/css-tools": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.2.0.tgz", - "integrity": "sha512-E09FiIft46CmH5Qnjb0wsW54/YQd69LsxeKUOWawmws1XWvyFGURnAChH0mlr7YPFR1ofwvUQfcL0J3lMxXqPA==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.3.3.tgz", + "integrity": "sha512-rE0Pygv0sEZ4vBWHlAgJLGDU7Pm8xoO6p3wsEceb7GYAjScrOHpEo8KK/eVkAcnSM+slAEtXjA2JpdjLp4fJQQ==", "dev": true }, "node_modules/@ampproject/remapping": { @@ -82,46 +92,111 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.21.4.tgz", - "integrity": "sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", + "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", "dependencies": { - "@babel/highlight": "^7.18.6" + "@babel/highlight": "^7.23.4", + "chalk": "^2.4.2" }, "engines": { "node": ">=6.9.0" } }, + "node_modules/@babel/code-frame/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/code-frame/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "node_modules/@babel/code-frame/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/code-frame/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/@babel/compat-data": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.21.4.tgz", - "integrity": "sha512-/DYyDpeCfaVinT40FPGdkkb+lYSKvsVuMjDAG7jPOWWiM1ibOaB9CXJAlc4d1QpP/U2q2P9jbrSlClKSErd55g==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz", + "integrity": "sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.21.4.tgz", - "integrity": "sha512-qt/YV149Jman/6AfmlxJ04LMIu8bMoyl3RB91yTFrxQmgbrSvQMy7cI8Q62FHx1t8wJ8B5fu0UDoLwHAhUo1QA==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.9.tgz", + "integrity": "sha512-5q0175NOjddqpvvzU+kDiSOAk4PfdO6FvwCWoQ6RO7rTzEe8vlo+4HVfcnAREhD4npMs0e9uZypjTwzZPCf/cw==", "dev": true, "dependencies": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.21.4", - "@babel/generator": "^7.21.4", - "@babel/helper-compilation-targets": "^7.21.4", - "@babel/helper-module-transforms": "^7.21.2", - "@babel/helpers": "^7.21.0", - "@babel/parser": "^7.21.4", - "@babel/template": "^7.20.7", - "@babel/traverse": "^7.21.4", - "@babel/types": "^7.21.4", - "convert-source-map": "^1.7.0", + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.6", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helpers": "^7.23.9", + "@babel/parser": "^7.23.9", + "@babel/template": "^7.23.9", + "@babel/traverse": "^7.23.9", + "@babel/types": "^7.23.9", + "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", - "json5": "^2.2.2", - "semver": "^6.3.0" + "json5": "^2.2.3", + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" @@ -131,21 +206,27 @@ "url": "https://opencollective.com/babel" } }, + "node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, "node_modules/@babel/eslint-parser": { - "version": "7.21.3", - "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.21.3.tgz", - "integrity": "sha512-kfhmPimwo6k4P8zxNs8+T7yR44q1LdpsZdE1NkCsVlfiuTPRfnGgjaF8Qgug9q9Pou17u6wneYF0lDCZJATMFg==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.23.9.tgz", + "integrity": "sha512-xPndlO7qxiJbn0ATvfXQBjCS7qApc9xmKHArgI/FTEFxXas5dnjC/VqM37lfZun9dclRYcn+YQAr6uDFy0bB2g==", "dev": true, "dependencies": { "@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1", "eslint-visitor-keys": "^2.1.0", - "semver": "^6.3.0" + "semver": "^6.3.1" }, "engines": { "node": "^10.13.0 || ^12.13.0 || >=14.0.0" }, "peerDependencies": { - "@babel/core": ">=7.11.0", + "@babel/core": "^7.11.0", "eslint": "^7.5.0 || ^8.0.0" } }, @@ -159,12 +240,12 @@ } }, "node_modules/@babel/generator": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.21.4.tgz", - "integrity": "sha512-NieM3pVIYW2SwGzKoqfPrQsf4xGs9M9AIG3ThppsSRmO+m7eQhmI6amajKMUeIO37wFfsvnvcxQFx6x6iqxDnA==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", + "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", "dev": true, "dependencies": { - "@babel/types": "^7.21.4", + "@babel/types": "^7.23.6", "@jridgewell/gen-mapping": "^0.3.2", "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" @@ -174,63 +255,60 @@ } }, "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz", - "integrity": "sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz", + "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==", "dev": true, "dependencies": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.18.9.tgz", - "integrity": "sha512-yFQ0YCHoIqarl8BCRwBL8ulYUaZpz3bNsA7oFepAzee+8/+ImtADXNOmO5vJvsPff3qi+hvpkY/NYBTrBQgdNw==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.15.tgz", + "integrity": "sha512-QkBXwGgaoC2GtGZRoma6kv7Szfv06khvhFav67ZExau2RaXzy8MpHSMO2PNoP2XtmQphJQRHFfg77Bq731Yizw==", "dev": true, "dependencies": { - "@babel/helper-explode-assignable-expression": "^7.18.6", - "@babel/types": "^7.18.9" + "@babel/types": "^7.22.15" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.21.4.tgz", - "integrity": "sha512-Fa0tTuOXZ1iL8IeDFUWCzjZcn+sJGd9RZdH9esYVjEejGmzf+FFYQpMi/kZUk2kPy/q1H3/GPw7np8qar/stfg==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", + "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", "dev": true, "dependencies": { - "@babel/compat-data": "^7.21.4", - "@babel/helper-validator-option": "^7.21.0", - "browserslist": "^4.21.3", + "@babel/compat-data": "^7.23.5", + "@babel/helper-validator-option": "^7.23.5", + "browserslist": "^4.22.2", "lru-cache": "^5.1.1", - "semver": "^6.3.0" + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.21.4.tgz", - "integrity": "sha512-46QrX2CQlaFRF4TkwfTt6nJD7IHq8539cCL7SDpqWSDeJKY1xylKKY5F/33mJhLZ3mFvKv2gGrVS6NkyF6qs+Q==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.23.9.tgz", + "integrity": "sha512-B2L9neXTIyPQoXDm+NtovPvG6VOLWnaXu3BIeVDWwdKFgG30oNa6CqVGiJPDWQwIAK49t9gnQI9c6K6RzabiKw==", "dev": true, "dependencies": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.21.0", - "@babel/helper-member-expression-to-functions": "^7.21.0", - "@babel/helper-optimise-call-expression": "^7.18.6", - "@babel/helper-replace-supers": "^7.20.7", - "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", - "@babel/helper-split-export-declaration": "^7.18.6" + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-member-expression-to-functions": "^7.23.0", + "@babel/helper-optimise-call-expression": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.20", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" @@ -240,13 +318,14 @@ } }, "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.21.4.tgz", - "integrity": "sha512-M00OuhU+0GyZ5iBBN9czjugzWrEq2vDpf/zCYHxxf93ul/Q5rv+a5h+/+0WnI1AebHNVtl5bFV0qsJoH23DbfA==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.15.tgz", + "integrity": "sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w==", "dev": true, "dependencies": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "regexpu-core": "^5.3.1" + "@babel/helper-annotate-as-pure": "^7.22.5", + "regexpu-core": "^5.3.1", + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" @@ -256,141 +335,127 @@ } }, "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.3.tgz", - "integrity": "sha512-z5aQKU4IzbqCC1XH0nAqfsFLMVSo22SBKUc0BxGrLkolTdPTructy0ToNnlO2zA4j9Q/7pjMZf0DSY+DSTYzww==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.5.0.tgz", + "integrity": "sha512-NovQquuQLAQ5HuyjCz7WQP9MjRj7dx++yspwiyUiGl9ZyadHRSql1HZh5ogRd8W8w6YM6EQ/NTB8rgjLt5W65Q==", "dev": true, "dependencies": { - "@babel/helper-compilation-targets": "^7.17.7", - "@babel/helper-plugin-utils": "^7.16.7", + "@babel/helper-compilation-targets": "^7.22.6", + "@babel/helper-plugin-utils": "^7.22.5", "debug": "^4.1.1", "lodash.debounce": "^4.0.8", - "resolve": "^1.14.2", - "semver": "^6.1.2" + "resolve": "^1.14.2" }, "peerDependencies": { - "@babel/core": "^7.4.0-0" + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "node_modules/@babel/helper-environment-visitor": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", - "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", "dev": true, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/helper-explode-assignable-expression": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.18.6.tgz", - "integrity": "sha512-eyAYAsQmB80jNfg4baAtLeWAQHfHFiR483rzFK+BhETlGZaQC9bsfrugfXDCbRHLQbIA7U5NxhhOxN7p/dWIcg==", - "dev": true, - "dependencies": { - "@babel/types": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-function-name": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.21.0.tgz", - "integrity": "sha512-HfK1aMRanKHpxemaY2gqBmL04iAPOPRj7DxtNbiDOrJK+gdwkiNRVpCpUJYbUT+aZyemKN8brqTOxzCaG6ExRg==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "dev": true, "dependencies": { - "@babel/template": "^7.20.7", - "@babel/types": "^7.21.0" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-hoist-variables": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", - "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", "dev": true, "dependencies": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.21.0.tgz", - "integrity": "sha512-Muu8cdZwNN6mRRNG6lAYErJ5X3bRevgYR2O8wN0yn7jJSnGDu6eG59RfT29JHxGUovyfrh6Pj0XzmR7drNVL3Q==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.23.0.tgz", + "integrity": "sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA==", "dev": true, "dependencies": { - "@babel/types": "^7.21.0" + "@babel/types": "^7.23.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-imports": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.21.4.tgz", - "integrity": "sha512-orajc5T2PsRYUN3ZryCEFeMDYwyw09c/pZeaQEZPH0MpKzSvn3e0uXsDBu3k03VI+9DBiRo+l22BfKTpKwa/Wg==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", + "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", "dependencies": { - "@babel/types": "^7.21.4" + "@babel/types": "^7.22.15" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.21.2", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.21.2.tgz", - "integrity": "sha512-79yj2AR4U/Oqq/WOV7Lx6hUjau1Zfo4cI+JLAVYeMV5XIlbOhmjEk5ulbTc9fMpmlojzZHkUUxAiK+UKn+hNQQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz", + "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==", "dev": true, "dependencies": { - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-module-imports": "^7.18.6", - "@babel/helper-simple-access": "^7.20.2", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/helper-validator-identifier": "^7.19.1", - "@babel/template": "^7.20.7", - "@babel/traverse": "^7.21.2", - "@babel/types": "^7.21.2" + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-validator-identifier": "^7.22.20" }, "engines": { "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.18.6.tgz", - "integrity": "sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz", + "integrity": "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==", "dev": true, "dependencies": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz", - "integrity": "sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", + "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-remap-async-to-generator": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.18.9.tgz", - "integrity": "sha512-dI7q50YKd8BAv3VEfgg7PS7yD3Rtbi2J1XMXaalXO0W0164hYLnh8zpjRS0mte9MfVp/tltvr/cfdXPvJr1opA==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.20.tgz", + "integrity": "sha512-pBGyV4uBqOns+0UvhsTO8qgl8hO89PmiDYv+/COyp1aeMcmfrfruz+/nCMFiYyFF/Knn0yfrC85ZzNFjembFTw==", "dev": true, "dependencies": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-wrap-function": "^7.18.9", - "@babel/types": "^7.18.9" + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-wrap-function": "^7.22.20" }, "engines": { "node": ">=6.9.0" @@ -400,119 +465,118 @@ } }, "node_modules/@babel/helper-replace-supers": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.20.7.tgz", - "integrity": "sha512-vujDMtB6LVfNW13jhlCrp48QNslK6JXi7lQG736HVbHz/mbf4Dc7tIRh1Xf5C0rF7BP8iiSxGMCmY6Ci1ven3A==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.22.20.tgz", + "integrity": "sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw==", "dev": true, "dependencies": { - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-member-expression-to-functions": "^7.20.7", - "@babel/helper-optimise-call-expression": "^7.18.6", - "@babel/template": "^7.20.7", - "@babel/traverse": "^7.20.7", - "@babel/types": "^7.20.7" + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-member-expression-to-functions": "^7.22.15", + "@babel/helper-optimise-call-expression": "^7.22.5" }, "engines": { "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, "node_modules/@babel/helper-simple-access": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.20.2.tgz", - "integrity": "sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", + "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", "dev": true, "dependencies": { - "@babel/types": "^7.20.2" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.20.0", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.20.0.tgz", - "integrity": "sha512-5y1JYeNKfvnT8sZcK9DVRtpTbGiomYIHviSP3OQWmDPU3DeH4a1ZlT/N2lyQ5P8egjcRaT/Y9aNqUxK0WsnIIg==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz", + "integrity": "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==", "dev": true, "dependencies": { - "@babel/types": "^7.20.0" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-split-export-declaration": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", - "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", "dev": true, "dependencies": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.19.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", - "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", + "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", - "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.21.0.tgz", - "integrity": "sha512-rmL/B8/f0mKS2baE9ZpyTcTavvEuWhTTW8amjzXNvYG4AwBsqTLikfXsEofsJEfKHf+HQVQbFOHy6o+4cnC/fQ==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", + "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-wrap-function": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.20.5.tgz", - "integrity": "sha512-bYMxIWK5mh+TgXGVqAtnu5Yn1un+v8DDZtqyzKRLUzrh70Eal2O3aZ7aPYiMADO4uKlkzOiRiZ6GX5q3qxvW9Q==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.22.20.tgz", + "integrity": "sha512-pms/UwkOpnQe/PDAEdV/d7dVCoBbB+R4FvYoHGZz+4VPcg7RtYy2KP7S2lbuWM6FCSgob5wshfGESbC/hzNXZw==", "dev": true, "dependencies": { - "@babel/helper-function-name": "^7.19.0", - "@babel/template": "^7.18.10", - "@babel/traverse": "^7.20.5", - "@babel/types": "^7.20.5" + "@babel/helper-function-name": "^7.22.5", + "@babel/template": "^7.22.15", + "@babel/types": "^7.22.19" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.21.0.tgz", - "integrity": "sha512-XXve0CBtOW0pd7MRzzmoyuSj0e3SEzj8pgyFxnTT1NJZL38BD1MK7yYrm8yefRPIDvNNe14xR4FdbHwpInD4rA==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.9.tgz", + "integrity": "sha512-87ICKgU5t5SzOT7sBMfCOZQ2rHjRU+Pcb9BoILMYz600W6DkVRLFBPwQ18gwUVvggqXivaUakpnxWQGbpywbBQ==", "dev": true, "dependencies": { - "@babel/template": "^7.20.7", - "@babel/traverse": "^7.21.0", - "@babel/types": "^7.21.0" + "@babel/template": "^7.23.9", + "@babel/traverse": "^7.23.9", + "@babel/types": "^7.23.9" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/highlight": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", - "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", + "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", "dependencies": { - "@babel/helper-validator-identifier": "^7.18.6", - "chalk": "^2.0.0", + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", "js-tokens": "^4.0.0" }, "engines": { @@ -584,9 +648,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.21.4.tgz", - "integrity": "sha512-alVJj7k7zIxqBZ7BTRhz0IqJFxW1VJbm6N8JbcYhQ186df9ZBPbZBmWSqAMXwHGsCJdYks7z/voa3ibiS5bCIw==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.9.tgz", + "integrity": "sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA==", "dev": true, "bin": { "parser": "bin/babel-parser.js" @@ -596,12 +660,12 @@ } }, "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz", - "integrity": "sha512-Dgxsyg54Fx1d4Nge8UnvTrED63vrwOdPmyvPzlNN/boaliRP54pm3pGzZD1SJUwrBA+Cs/xdG8kXX6Mn/RfISQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.23.3.tgz", + "integrity": "sha512-iRkKcCqb7iGnq9+3G6rZ+Ciz5VywC4XNRHe57lKM+jOeYAoR0lVqdeeDRfh0tQcTfw/+vBhHn926FmQhLtlFLQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -611,14 +675,14 @@ } }, "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.20.7.tgz", - "integrity": "sha512-sbr9+wNE5aXMBBFBICk01tt7sBf2Oc9ikRFEcem/ZORup9IMUdNhW7/wVLEbbtlWOsEubJet46mHAL2C8+2jKQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.23.3.tgz", + "integrity": "sha512-WwlxbfMNdVEpQjZmK5mhm7oSwD3dS6eU+Iwsi4Knl9wAletWem7kaRsGOG+8UEbRyqxY4SS5zvtfXwX+jMxUwQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", - "@babel/plugin-proposal-optional-chaining": "^7.20.7" + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/plugin-transform-optional-chaining": "^7.23.3" }, "engines": { "node": ">=6.9.0" @@ -627,28 +691,27 @@ "@babel/core": "^7.13.0" } }, - "node_modules/@babel/plugin-proposal-async-generator-functions": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.20.7.tgz", - "integrity": "sha512-xMbiLsn/8RK7Wq7VeVytytS2L6qE69bXPB10YCmMdDZbKF4okCqY74pI/jJQ/8U0b/F6NrT2+14b8/P9/3AMGA==", + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.23.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.23.7.tgz", + "integrity": "sha512-LlRT7HgaifEpQA1ZgLVOIJZZFVPWN5iReq/7/JixwBtwcoeVGDBD53ZV28rrsLYOZs1Y/EHhA8N/Z6aazHR8cw==", "dev": true, "dependencies": { - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-remap-async-to-generator": "^7.18.9", - "@babel/plugin-syntax-async-generators": "^7.8.4" + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@babel/core": "^7.0.0" } }, "node_modules/@babel/plugin-proposal-class-properties": { "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz", "integrity": "sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-properties instead.", "dev": true, "dependencies": { "@babel/helper-create-class-features-plugin": "^7.18.6", @@ -661,98 +724,15 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-proposal-class-static-block": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.21.0.tgz", - "integrity": "sha512-XP5G9MWNUskFuP30IfFSEFB0Z6HzLIUcjYM4bYOPHXl7eiJ9HFv8tWj6TXTN5QODiEhDZAeI4hLok2iHFFV4hw==", - "dev": true, - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.21.0", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/plugin-syntax-class-static-block": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.12.0" - } - }, "node_modules/@babel/plugin-proposal-decorators": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.21.0.tgz", - "integrity": "sha512-MfgX49uRrFUTL/HvWtmx3zmpyzMMr4MTj3d527MLlr/4RTT9G/ytFFP7qet2uM2Ve03b+BkpWUpK+lRXnQ+v9w==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.23.9.tgz", + "integrity": "sha512-hJhBCb0+NnTWybvWq2WpbCYDOcflSbx0t+BYP65e5R9GVnukiDTi+on5bFkk4p7QGuv190H6KfNiV9Knf/3cZA==", "dev": true, "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.21.0", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-replace-supers": "^7.20.7", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/plugin-syntax-decorators": "^7.21.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-dynamic-import": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.18.6.tgz", - "integrity": "sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-dynamic-import": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-export-namespace-from": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.18.9.tgz", - "integrity": "sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-json-strings": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.18.6.tgz", - "integrity": "sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-json-strings": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-logical-assignment-operators": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.20.7.tgz", - "integrity": "sha512-y7C7cZgpMIjWlKE5T7eJwp+tnRYM89HmRvWM5EQuB5BoHEONjmQ8lSNmBUwOyy/GFRsohJED51YBF79hE1djug==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + "@babel/helper-create-class-features-plugin": "^7.23.9", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-decorators": "^7.23.3" }, "engines": { "node": ">=6.9.0" @@ -765,6 +745,7 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz", "integrity": "sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-nullish-coalescing-operator instead.", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.18.6", @@ -781,6 +762,7 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.18.6.tgz", "integrity": "sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-numeric-separator instead.", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.18.6", @@ -793,45 +775,11 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-proposal-object-rest-spread": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.7.tgz", - "integrity": "sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg==", - "dev": true, - "dependencies": { - "@babel/compat-data": "^7.20.5", - "@babel/helper-compilation-targets": "^7.20.7", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-transform-parameters": "^7.20.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-optional-catch-binding": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.18.6.tgz", - "integrity": "sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-proposal-optional-chaining": { "version": "7.21.0", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.21.0.tgz", "integrity": "sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-optional-chaining instead.", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.20.2", @@ -849,6 +797,7 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz", "integrity": "sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-methods instead.", "dev": true, "dependencies": { "@babel/helper-create-class-features-plugin": "^7.18.6", @@ -862,16 +811,10 @@ } }, "node_modules/@babel/plugin-proposal-private-property-in-object": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0.tgz", - "integrity": "sha512-ha4zfehbJjc5MmXBlHec1igel5TJXXLDDRbuJ4+XT2TJcyD9/V1919BA8gMvsdHcNMBy4WBUBiRb3nw/EQUtBw==", + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", "dev": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-create-class-features-plugin": "^7.21.0", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5" - }, "engines": { "node": ">=6.9.0" }, @@ -879,22 +822,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-proposal-unicode-property-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz", - "integrity": "sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==", - "dev": true, - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-syntax-async-generators": { "version": "7.8.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", @@ -935,12 +862,12 @@ } }, "node_modules/@babel/plugin-syntax-decorators": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.21.0.tgz", - "integrity": "sha512-tIoPpGBR8UuM4++ccWN3gifhVvQu7ZizuR1fklhRJrd5ewgbkUS+0KVFeWWxELtn18NTLoW32XV7zyOgIAiz+w==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.23.3.tgz", + "integrity": "sha512-cf7Niq4/+/juY67E0PbgH0TDhLQ5J7zS8C/Q5FFx+DWyrRa9sUQdTXkjqKu8zGvuqr7vw1muKiukseihU+PJDA==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -974,12 +901,12 @@ } }, "node_modules/@babel/plugin-syntax-flow": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.21.4.tgz", - "integrity": "sha512-l9xd3N+XG4fZRxEP3vXdK6RW7vN1Uf5dxzRC/09wV86wqZ/YYQooBIGNsiRdfNR3/q2/5pPzV4B54J/9ctX5jw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.23.3.tgz", + "integrity": "sha512-YZiAIpkJAwQXBJLIQbRFayR5c+gJ35Vcz3bg954k7cd73zqjvhacJuL9RbrzPz8qPmZdgqP6EUKwy0PCNhaaPA==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -989,12 +916,12 @@ } }, "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.20.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.20.0.tgz", - "integrity": "sha512-IUh1vakzNoWalR8ch/areW7qFopR2AEw03JlG7BbrDqmQ4X3q9uuipQwSGrUn7oGiemKjtSLDhNtQHzMHr1JdQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.23.3.tgz", + "integrity": "sha512-lPgDSU+SJLK3xmFDTV2ZRQAiM7UuUjGidwBywFavObCiZc1BeAAcMtHJKUya92hPHO+at63JJPLygilZard8jw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.19.0" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1003,6 +930,33 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.23.3.tgz", + "integrity": "sha512-pawnE0P9g10xgoP7yKr6CK63K2FMsTE+FZidZO/1PwRdzmAPVs+HS1mAURUsgaoxammTJvULUdIkEK0gOcU2tA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-syntax-json-strings": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", @@ -1016,12 +970,12 @@ } }, "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.21.4.tgz", - "integrity": "sha512-5hewiLct5OKyh6PLKEYaFclcqtIgCb6bmELouxjF6up5q3Sov7rOayW4RwhbaBL0dit8rA80GNfY+UuDp2mBbQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.23.3.tgz", + "integrity": "sha512-EB2MELswq55OHUoRZLGg/zC7QWUKfNLpE57m/S2yr1uEneIgsTgrSzXP3NXEsMkVn76OlaVVnzN+ugObuYGwhg==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1133,12 +1087,12 @@ } }, "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.21.4.tgz", - "integrity": "sha512-xz0D39NvhQn4t4RNsHmDnnsaQizIlUkdtYvLs8La1BlfjQ6JEwxkJGeqJMW2tAXx+q6H+WFuUTXNdYVpEya0YA==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.23.3.tgz", + "integrity": "sha512-9EiNjVJOMwCO+43TqoTrgQ8jMwcAd0sWyXi9RPfIsLTj4R2MADDDQXELhffaUx/uJv2AYcxBgPwH6j4TIA4ytQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1147,13 +1101,47 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-arrow-functions": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.20.7.tgz", - "integrity": "sha512-3poA5E7dzDomxj9WXWwuD6A5F3kc7VXwIJO+E+J8qtDtS+pXPAhrgEyh+9GBwBgPq1Z+bB+/JD60lp5jsN7JPQ==", + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2" + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.23.3.tgz", + "integrity": "sha512-NzQcQrzaQPkaEwoTm4Mhyl8jI1huEL/WWIEvudjTCMJ9aBZNpsJbMASx7EQECtQQPS/DcnFpo0FIh3LvEO9cxQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.23.9.tgz", + "integrity": "sha512-8Q3veQEDGe14dTYuwagbRtwxQDnytyg1JFu4/HwEMETeofocrB0U0ejBJIXoeG/t2oXZ8kzCyI0ZZfbT80VFNQ==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-remap-async-to-generator": "^7.22.20", + "@babel/plugin-syntax-async-generators": "^7.8.4" }, "engines": { "node": ">=6.9.0" @@ -1163,14 +1151,14 @@ } }, "node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.20.7.tgz", - "integrity": "sha512-Uo5gwHPT9vgnSXQxqGtpdufUiWp96gk7yiP4Mp5bm1QMkEmLXBO7PAGYbKoJ6DhAwiNkcHFBol/x5zZZkL/t0Q==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.23.3.tgz", + "integrity": "sha512-A7LFsKi4U4fomjqXJlZg/u0ft/n8/7n7lpffUP/ZULx/DtV9SGlNKZolHH6PE8Xl1ngCc0M11OaeZptXVkfKSw==", "dev": true, "dependencies": { - "@babel/helper-module-imports": "^7.18.6", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-remap-async-to-generator": "^7.18.9" + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-remap-async-to-generator": "^7.22.20" }, "engines": { "node": ">=6.9.0" @@ -1180,12 +1168,12 @@ } }, "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.18.6.tgz", - "integrity": "sha512-ExUcOqpPWnliRcPqves5HJcJOvHvIIWfuS4sroBUenPuMdmW+SMHDakmtS7qOo13sVppmUijqeTv7qqGsvURpQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.23.3.tgz", + "integrity": "sha512-vI+0sIaPIO6CNuM9Kk5VmXcMVRiOpDh7w2zZt9GXzmE/9KD70CUEVhvPR/etAeNK/FAEkhxQtXOzVF3EuRL41A==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1195,12 +1183,12 @@ } }, "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.21.0.tgz", - "integrity": "sha512-Mdrbunoh9SxwFZapeHVrwFmri16+oYotcZysSzhNIVDwIAb1UV+kvnxULSYq9J3/q5MDG+4X6w8QVgD1zhBXNQ==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.23.4.tgz", + "integrity": "sha512-0QqbP6B6HOh7/8iNR4CQU2Th/bbRtBp4KS9vcaZd1fZ0wSh5Fyssg0UCIHwxh+ka+pNDREbVLQnHCMHKZfPwfw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1209,20 +1197,52 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-classes": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.21.0.tgz", - "integrity": "sha512-RZhbYTCEUAe6ntPehC4hlslPWosNHDox+vAs4On/mCLRLfoDVHf6hVEd7kuxr1RnHwJmxFfUM3cZiZRmPxJPXQ==", + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.23.3.tgz", + "integrity": "sha512-uM+AN8yCIjDPccsKGlw271xjJtGii+xQIF/uMPS8H15L12jZTsLfF4o5vNO7d/oUguOyfdikHGc/yi9ge4SGIg==", "dev": true, "dependencies": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-compilation-targets": "^7.20.7", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.21.0", - "@babel/helper-optimise-call-expression": "^7.18.6", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-replace-supers": "^7.20.7", - "@babel/helper-split-export-declaration": "^7.18.6", + "@babel/helper-create-class-features-plugin": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.23.4.tgz", + "integrity": "sha512-nsWu/1M+ggti1SOALj3hfx5FXzAY06fwPJsUZD4/A5e1bWi46VUIWtD+kOX6/IdhXGsXBWllLFDSnqSCdUNydQ==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-class-static-block": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.23.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.23.8.tgz", + "integrity": "sha512-yAYslGsY1bX6Knmg46RjiCiNSwJKv2IUC8qOdYKqMMr0491SXFhcHqOdRDeCRohOOIzwN/90C6mQ9qAKgrP7dg==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.20", + "@babel/helper-split-export-declaration": "^7.22.6", "globals": "^11.1.0" }, "engines": { @@ -1233,13 +1253,13 @@ } }, "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.20.7.tgz", - "integrity": "sha512-Lz7MvBK6DTjElHAmfu6bfANzKcxpyNPeYBGEafyA6E5HtRpjpZwU+u7Qrgz/2OR0z+5TvKYbPdphfSaAcZBrYQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.23.3.tgz", + "integrity": "sha512-dTj83UVTLw/+nbiHqQSFdwO9CbTtwq1DsDqm3CUEtDrZNET5rT5E6bIdTlOftDTDLMYxvxHNEYO4B9SLl8SLZw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/template": "^7.20.7" + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/template": "^7.22.15" }, "engines": { "node": ">=6.9.0" @@ -1249,12 +1269,12 @@ } }, "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.21.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.21.3.tgz", - "integrity": "sha512-bp6hwMFzuiE4HqYEyoGJ/V2LeIWn+hLVKc4pnj++E5XQptwhtcGmSayM029d/j2X1bPKGTlsyPwAubuU22KhMA==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.23.3.tgz", + "integrity": "sha512-n225npDqjDIr967cMScVKHXJs7rout1q+tt50inyBCPkyZ8KxeI6d+GIbSBTT/w/9WdlWDOej3V9HE5Lgk57gw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1264,13 +1284,13 @@ } }, "node_modules/@babel/plugin-transform-dotall-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.18.6.tgz", - "integrity": "sha512-6S3jpun1eEbAxq7TdjLotAsl4WpQI9DxfkycRcKrjhQYzU87qpXdknpBg/e+TdcMehqGnLFi7tnFUBR02Vq6wg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.23.3.tgz", + "integrity": "sha512-vgnFYDHAKzFaTVp+mneDsIEbnJ2Np/9ng9iviHw3P/KVcgONxpNULEW/51Z/BaFojG2GI2GwwXck5uV1+1NOYQ==", "dev": true, "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-create-regexp-features-plugin": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1280,12 +1300,28 @@ } }, "node_modules/@babel/plugin-transform-duplicate-keys": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.18.9.tgz", - "integrity": "sha512-d2bmXCtZXYc59/0SanQKbiWINadaJXqtvIQIzd4+hNwkWBgyCd5F/2t1kXoUdvPMrxzPvhK6EMQRROxsue+mfw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.23.3.tgz", + "integrity": "sha512-RrqQ+BQmU3Oyav3J+7/myfvRCq7Tbz+kKLLshUmMwNlDHExbGL7ARhajvoBJEvc+fCguPPu887N+3RRXBVKZUA==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9" + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.23.4.tgz", + "integrity": "sha512-V6jIbLhdJK86MaLh4Jpghi8ho5fGzt3imHOBu/x0jlBaPYqDoWz4RDXjmMOfnh+JWNaQleEAByZLV0QzBT4YQQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3" }, "engines": { "node": ">=6.9.0" @@ -1295,13 +1331,29 @@ } }, "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.18.6.tgz", - "integrity": "sha512-wzEtc0+2c88FVR34aQmiz56dxEkxr2g8DQb/KfaFa1JYXOFVsbhvAonFN6PwVWj++fKmku8NP80plJ5Et4wqHw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.23.3.tgz", + "integrity": "sha512-5fhCsl1odX96u7ILKHBj4/Y8vipoqwsJMh4csSA8qFfxrZDEA4Ssku2DyNvMJSmZNOEBT750LfFPbtrnTP90BQ==", "dev": true, "dependencies": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.23.4.tgz", + "integrity": "sha512-GzuSBcKkx62dGzZI1WVgTWvkkz84FZO5TC5T8dl/Tht/rAla6Dg/Mz9Yhypg+ezVACf/rgDuQt3kbWEv7LdUDQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" }, "engines": { "node": ">=6.9.0" @@ -1311,13 +1363,13 @@ } }, "node_modules/@babel/plugin-transform-flow-strip-types": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.21.0.tgz", - "integrity": "sha512-FlFA2Mj87a6sDkW4gfGrQQqwY/dLlBAyJa2dJEZ+FHXUVHBflO2wyKvg+OOEzXfrKYIa4HWl0mgmbCzt0cMb7w==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.23.3.tgz", + "integrity": "sha512-26/pQTf9nQSNVJCrLB1IkHUKyPxR+lMrH2QDPG89+Znu9rAMbtrybdbWeE9bb7gzjmE5iXHEY+e0HUwM6Co93Q==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/plugin-syntax-flow": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-flow": "^7.23.3" }, "engines": { "node": ">=6.9.0" @@ -1327,12 +1379,13 @@ } }, "node_modules/@babel/plugin-transform-for-of": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.21.0.tgz", - "integrity": "sha512-LlUYlydgDkKpIY7mcBWvyPPmMcOphEyYA27Ef4xpbh1IiDNLr0kZsos2nf92vz3IccvJI25QUwp86Eo5s6HmBQ==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.23.6.tgz", + "integrity": "sha512-aYH4ytZ0qSuBbpfhuofbg/e96oQ7U2w1Aw/UQmKT+1l39uEhUPoFS3fHevDc1G0OvewyDudfMKY1OulczHzWIw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2" + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1342,14 +1395,30 @@ } }, "node_modules/@babel/plugin-transform-function-name": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.18.9.tgz", - "integrity": "sha512-WvIBoRPaJQ5yVHzcnJFor7oS5Ls0PYixlTYE63lCj2RtdQEl15M68FXQlxnG6wdraJIXRdR7KI+hQ7q/9QjrCQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.23.3.tgz", + "integrity": "sha512-I1QXp1LxIvt8yLaib49dRW5Okt7Q4oaxao6tFVKS/anCdEOMtYwWVKoiOA1p34GOWIZjUK0E+zCp7+l1pfQyiw==", "dev": true, "dependencies": { - "@babel/helper-compilation-targets": "^7.18.9", - "@babel/helper-function-name": "^7.18.9", - "@babel/helper-plugin-utils": "^7.18.9" + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.23.4.tgz", + "integrity": "sha512-81nTOqM1dMwZ/aRXQ59zVubN9wHGqk6UtqRK+/q+ciXmRy8fSolhGVvG09HHRGo4l6fr/c4ZhXUQH0uFW7PZbg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-json-strings": "^7.8.3" }, "engines": { "node": ">=6.9.0" @@ -1359,12 +1428,28 @@ } }, "node_modules/@babel/plugin-transform-literals": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.18.9.tgz", - "integrity": "sha512-IFQDSRoTPnrAIrI5zoZv73IFeZu2dhu6irxQjY9rNjTT53VmKg9fenjvoiOWOkJ6mm4jKVPtdMzBY98Fp4Z4cg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.23.3.tgz", + "integrity": "sha512-wZ0PIXRxnwZvl9AYpqNUxpZ5BiTGrYt7kueGQ+N5FiQ7RCOD4cm8iShd6S6ggfVIWaJf2EMk8eRzAh52RfP4rQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9" + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.23.4.tgz", + "integrity": "sha512-Mc/ALf1rmZTP4JKKEhUwiORU+vcfarFVLfcFiolKUo6sewoxSEgl36ak5t+4WamRsNr6nzjZXQjM35WsU+9vbg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" }, "engines": { "node": ">=6.9.0" @@ -1374,12 +1459,12 @@ } }, "node_modules/@babel/plugin-transform-member-expression-literals": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.18.6.tgz", - "integrity": "sha512-qSF1ihLGO3q+/g48k85tUjD033C29TNTVB2paCwZPVmOsjn9pClvYYrM2VeJpBY2bcNkuny0YUyTNRyRxJ54KA==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.23.3.tgz", + "integrity": "sha512-sC3LdDBDi5x96LA+Ytekz2ZPk8i/Ck+DEuDbRAll5rknJ5XRTSaPKEYwomLcs1AA8wg9b3KjIQRsnApj+q51Ag==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1389,13 +1474,13 @@ } }, "node_modules/@babel/plugin-transform-modules-amd": { - "version": "7.20.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.20.11.tgz", - "integrity": "sha512-NuzCt5IIYOW0O30UvqktzHYR2ud5bOWbY0yaxWZ6G+aFzOMJvrs5YHNikrbdaT15+KNO31nPOy5Fim3ku6Zb5g==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.23.3.tgz", + "integrity": "sha512-vJYQGxeKM4t8hYCKVBlZX/gtIY2I7mRGFNcm85sgXGMTBcoV3QdVtdpbcWEbzbfUIUZKwvgFT82mRvaQIebZzw==", "dev": true, "dependencies": { - "@babel/helper-module-transforms": "^7.20.11", - "@babel/helper-plugin-utils": "^7.20.2" + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1405,14 +1490,14 @@ } }, "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.21.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.21.2.tgz", - "integrity": "sha512-Cln+Yy04Gxua7iPdj6nOV96smLGjpElir5YwzF0LBPKoPlLDNJePNlrGGaybAJkd0zKRnOVXOgizSqPYMNYkzA==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.23.3.tgz", + "integrity": "sha512-aVS0F65LKsdNOtcz6FRCpE4OgsP2OFnW46qNxNIX9h3wuzaNcSQsJysuMwqSibC98HPrf2vCgtxKNwS0DAlgcA==", "dev": true, "dependencies": { - "@babel/helper-module-transforms": "^7.21.2", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-simple-access": "^7.20.2" + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-simple-access": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1422,15 +1507,15 @@ } }, "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.20.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.20.11.tgz", - "integrity": "sha512-vVu5g9BPQKSFEmvt2TA4Da5N+QVS66EX21d8uoOihC+OCpUoGvzVsXeqFdtAEfVa5BILAeFt+U7yVmLbQnAJmw==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.23.9.tgz", + "integrity": "sha512-KDlPRM6sLo4o1FkiSlXoAa8edLXFsKKIda779fbLrvmeuc3itnjCtaO6RrtoaANsIJANj+Vk1zqbZIMhkCAHVw==", "dev": true, "dependencies": { - "@babel/helper-hoist-variables": "^7.18.6", - "@babel/helper-module-transforms": "^7.20.11", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-validator-identifier": "^7.19.1" + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20" }, "engines": { "node": ">=6.9.0" @@ -1440,13 +1525,13 @@ } }, "node_modules/@babel/plugin-transform-modules-umd": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.18.6.tgz", - "integrity": "sha512-dcegErExVeXcRqNtkRU/z8WlBLnvD4MRnHgNs3MytRO1Mn1sHRyhbcpYbVMGclAqOjdW+9cfkdZno9dFdfKLfQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.23.3.tgz", + "integrity": "sha512-zHsy9iXX2nIsCBFPud3jKn1IRPWg3Ing1qOZgeKV39m1ZgIdpJqvlWVeiHBZC6ITRG0MfskhYe9cLgntfSFPIg==", "dev": true, "dependencies": { - "@babel/helper-module-transforms": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1456,13 +1541,13 @@ } }, "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.20.5.tgz", - "integrity": "sha512-mOW4tTzi5iTLnw+78iEq3gr8Aoq4WNRGpmSlrogqaiCBoR1HFhpU4JkpQFOHfeYx3ReVIFWOQJS4aZBRvuZ6mA==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.22.5.tgz", + "integrity": "sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ==", "dev": true, "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.20.5", - "@babel/helper-plugin-utils": "^7.20.2" + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1472,12 +1557,63 @@ } }, "node_modules/@babel/plugin-transform-new-target": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.18.6.tgz", - "integrity": "sha512-DjwFA/9Iu3Z+vrAn+8pBUGcjhxKguSMlsFqeCKbhb9BAV756v0krzVK04CRDi/4aqmk8BsHb4a/gFcaA5joXRw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.23.3.tgz", + "integrity": "sha512-YJ3xKqtJMAT5/TIZnpAR3I+K+WaDowYbN3xyxI8zxx/Gsypwf9B9h0VB+1Nh6ACAAPRS5NSRje0uVv5i79HYGQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.23.4.tgz", + "integrity": "sha512-jHE9EVVqHKAQx+VePv5LLGHjmHSJR76vawFPTdlxR/LVJPfOEGxREQwQfjuZEOPTwG92X3LINSh3M40Rv4zpVA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.23.4.tgz", + "integrity": "sha512-mps6auzgwjRrwKEZA05cOwuDc9FAzoyFS4ZsG/8F43bTLf/TgkJg7QXOrPO1JO599iA3qgK9MXdMGOEC8O1h6Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.23.4.tgz", + "integrity": "sha512-9x9K1YyeQVw0iOXJlIzwm8ltobIIv7j2iLyP2jIhEbqPRQ7ScNgwQufU2I0Gq11VjyG4gI4yMXt2VFags+1N3g==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.23.3", + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.23.3" }, "engines": { "node": ">=6.9.0" @@ -1487,13 +1623,46 @@ } }, "node_modules/@babel/plugin-transform-object-super": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.18.6.tgz", - "integrity": "sha512-uvGz6zk+pZoS1aTZrOvrbj6Pp/kK2mp45t2B+bTDre2UgsZZ8EZLSJtUg7m/no0zOJUWgFONpB7Zv9W2tSaFlA==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.23.3.tgz", + "integrity": "sha512-BwQ8q0x2JG+3lxCVFohg+KbQM7plfpBwThdW9A6TMtWwLsbDA01Ek2Zb/AgDN39BiZsExm4qrXxjk+P1/fzGrA==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/helper-replace-supers": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.20" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.23.4.tgz", + "integrity": "sha512-XIq8t0rJPHf6Wvmbn9nFxU6ao4c7WhghTR5WyV8SrJfUFzyxhCm4nhC+iAp3HFhbAKLfYpgzhJ6t4XCtVwqO5A==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.23.4.tgz", + "integrity": "sha512-ZU8y5zWOfjM5vZ+asjgAPwDaBjJzgufjES89Rs4Lpq63O300R/kOz30WCLo6BxxX6QVEilwSlpClnG5cZaikTA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" }, "engines": { "node": ">=6.9.0" @@ -1503,12 +1672,46 @@ } }, "node_modules/@babel/plugin-transform-parameters": { - "version": "7.21.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.21.3.tgz", - "integrity": "sha512-Wxc+TvppQG9xWFYatvCGPvZ6+SIUxQ2ZdiBP+PHYMIjnPXD+uThCshaz4NZOnODAtBjjcVQQ/3OKs9LW28purQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.23.3.tgz", + "integrity": "sha512-09lMt6UsUb3/34BbECKVbVwrT9bO6lILWln237z7sLaWnMsTi7Yc9fhX5DLpkJzAGfaReXI22wP41SZmnAA3Vw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2" + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.23.3.tgz", + "integrity": "sha512-UzqRcRtWsDMTLrRWFvUBDwmw06tCQH9Rl1uAjfh6ijMSmGYQ+fpdB+cnqRC8EMh5tuuxSv0/TejGL+7vyj+50g==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.23.4.tgz", + "integrity": "sha512-9G3K1YqTq3F4Vt88Djx1UZ79PDyj+yKRnUy7cZGSMe+a7jkwD259uKKuUzQlPkGam7R+8RJwh5z4xO27fA1o2A==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-create-class-features-plugin": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" }, "engines": { "node": ">=6.9.0" @@ -1518,12 +1721,12 @@ } }, "node_modules/@babel/plugin-transform-property-literals": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.18.6.tgz", - "integrity": "sha512-cYcs6qlgafTud3PAzrrRNbQtfpQ8+y/+M5tKmksS9+M1ckbH6kzY8MrexEM9mcA6JDsukE19iIRvAyYl463sMg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.23.3.tgz", + "integrity": "sha512-jR3Jn3y7cZp4oEWPFAlRsSWjxKe4PZILGBSd4nis1TsC5qeSpb+nrtihJuDhNI7QHiVbUaiXa0X2RZY3/TI6Nw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1533,12 +1736,12 @@ } }, "node_modules/@babel/plugin-transform-react-display-name": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.18.6.tgz", - "integrity": "sha512-TV4sQ+T013n61uMoygyMRm+xf04Bd5oqFpv2jAEQwSZ8NwQA7zeRPg1LMVg2PWi3zWBz+CLKD+v5bcpZ/BS0aA==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.23.3.tgz", + "integrity": "sha512-GnvhtVfA2OAtzdX58FJxU19rhoGeQzyVndw3GgtdECQvQFXPEZIOVULHVZGAYmOgmqjXpVpfocAbSjh99V/Fqw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1548,16 +1751,16 @@ } }, "node_modules/@babel/plugin-transform-react-jsx": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.21.0.tgz", - "integrity": "sha512-6OAWljMvQrZjR2DaNhVfRz6dkCAVV+ymcLUmaf8bccGOHn2v5rHJK3tTpij0BuhdYWP4LLaqj5lwcdlpAAPuvg==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.23.4.tgz", + "integrity": "sha512-5xOpoPguCZCRbo/JeHlloSkTA8Bld1J/E1/kLfD1nsuiW1m8tduTA1ERCgIZokDflX/IBzKcqR3l7VlRgiIfHA==", "dev": true, "dependencies": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-module-imports": "^7.18.6", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/plugin-syntax-jsx": "^7.18.6", - "@babel/types": "^7.21.0" + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-jsx": "^7.23.3", + "@babel/types": "^7.23.4" }, "engines": { "node": ">=6.9.0" @@ -1567,12 +1770,12 @@ } }, "node_modules/@babel/plugin-transform-react-jsx-development": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.18.6.tgz", - "integrity": "sha512-SA6HEjwYFKF7WDjWcMcMGUimmw/nhNRDWxr+KaLSCrkD/LMDBvWRmHAYgE1HDeF8KUuI8OAu+RT6EOtKxSW2qA==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.22.5.tgz", + "integrity": "sha512-bDhuzwWMuInwCYeDeMzyi7TaBgRQei6DqxhbyniL7/VG4RSS7HtSL2QbY4eESy1KJqlWt8g3xeEBGPuo+XqC8A==", "dev": true, "dependencies": { - "@babel/plugin-transform-react-jsx": "^7.18.6" + "@babel/plugin-transform-react-jsx": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1582,12 +1785,12 @@ } }, "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.21.0.tgz", - "integrity": "sha512-f/Eq+79JEu+KUANFks9UZCcvydOOGMgF7jBrcwjHa5jTZD8JivnhCJYvmlhR/WTXBWonDExPoW0eO/CR4QJirA==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.23.3.tgz", + "integrity": "sha512-qXRvbeKDSfwnlJnanVRp0SfuWE5DQhwQr5xtLBzp56Wabyo+4CMosF6Kfp+eOD/4FYpql64XVJ2W0pVLlJZxOQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1597,12 +1800,12 @@ } }, "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.19.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.19.6.tgz", - "integrity": "sha512-RpAi004QyMNisst/pvSanoRdJ4q+jMCWyk9zdw/CyLB9j8RXEahodR6l2GyttDRyEVWZtbN+TpLiHJ3t34LbsQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.23.3.tgz", + "integrity": "sha512-91RS0MDnAWDNvGC6Wio5XYkyWI39FMFO+JK9+4AlgaTH+yWwVTsw7/sn6LK0lH7c5F+TFkpv/3LfCJ1Ydwof/g==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.19.0" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1612,13 +1815,13 @@ } }, "node_modules/@babel/plugin-transform-react-pure-annotations": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.18.6.tgz", - "integrity": "sha512-I8VfEPg9r2TRDdvnHgPepTKvuRomzA8+u+nhY7qSI1fR2hRNebasZEETLyM5mAUr0Ku56OkXJ0I7NHJnO6cJiQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.23.3.tgz", + "integrity": "sha512-qMFdSS+TUhB7Q/3HVPnEdYJDQIk57jkntAwSuz9xfSE4n+3I+vHYCli3HoHawN1Z3RfCz/y1zXA/JXjG6cVImQ==", "dev": true, "dependencies": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1628,13 +1831,13 @@ } }, "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.20.5.tgz", - "integrity": "sha512-kW/oO7HPBtntbsahzQ0qSE3tFvkFwnbozz3NWFhLGqH75vLEg+sCGngLlhVkePlCs3Jv0dBBHDzCHxNiFAQKCQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.23.3.tgz", + "integrity": "sha512-KP+75h0KghBMcVpuKisx3XTu9Ncut8Q8TuvGO4IhY+9D5DFEckQefOuIsB/gQ2tG71lCke4NMrtIPS8pOj18BQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2", - "regenerator-transform": "^0.15.1" + "@babel/helper-plugin-utils": "^7.22.5", + "regenerator-transform": "^0.15.2" }, "engines": { "node": ">=6.9.0" @@ -1644,12 +1847,12 @@ } }, "node_modules/@babel/plugin-transform-reserved-words": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.18.6.tgz", - "integrity": "sha512-oX/4MyMoypzHjFrT1CdivfKZ+XvIPMFXwwxHp/r0Ddy2Vuomt4HDFGmft1TAY2yiTKiNSsh3kjBAzcM8kSdsjA==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.23.3.tgz", + "integrity": "sha512-QnNTazY54YqgGxwIexMZva9gqbPa15t/x9VS+0fsEFWplwVpXYZivtgl43Z1vMpc1bdPP2PP8siFeVcnFvA3Cg==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1659,17 +1862,17 @@ } }, "node_modules/@babel/plugin-transform-runtime": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.21.4.tgz", - "integrity": "sha512-1J4dhrw1h1PqnNNpzwxQ2UBymJUF8KuPjAAnlLwZcGhHAIqUigFW7cdK6GHoB64ubY4qXQNYknoUeks4Wz7CUA==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.23.9.tgz", + "integrity": "sha512-A7clW3a0aSjm3ONU9o2HAILSegJCYlEZmOhmBRReVtIpY/Z/p7yIZ+wR41Z+UipwdGuqwtID/V/dOdZXjwi9gQ==", "dev": true, "dependencies": { - "@babel/helper-module-imports": "^7.21.4", - "@babel/helper-plugin-utils": "^7.20.2", - "babel-plugin-polyfill-corejs2": "^0.3.3", - "babel-plugin-polyfill-corejs3": "^0.6.0", - "babel-plugin-polyfill-regenerator": "^0.4.1", - "semver": "^6.3.0" + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5", + "babel-plugin-polyfill-corejs2": "^0.4.8", + "babel-plugin-polyfill-corejs3": "^0.9.0", + "babel-plugin-polyfill-regenerator": "^0.5.5", + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" @@ -1679,12 +1882,12 @@ } }, "node_modules/@babel/plugin-transform-shorthand-properties": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.18.6.tgz", - "integrity": "sha512-eCLXXJqv8okzg86ywZJbRn19YJHU4XUa55oz2wbHhaQVn/MM+XhukiT7SYqp/7o00dg52Rj51Ny+Ecw4oyoygw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.23.3.tgz", + "integrity": "sha512-ED2fgqZLmexWiN+YNFX26fx4gh5qHDhn1O2gvEhreLW2iI63Sqm4llRLCXALKrCnbN4Jy0VcMQZl/SAzqug/jg==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1694,13 +1897,13 @@ } }, "node_modules/@babel/plugin-transform-spread": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.20.7.tgz", - "integrity": "sha512-ewBbHQ+1U/VnH1fxltbJqDeWBU1oNLG8Dj11uIv3xVf7nrQu0bPGe5Rf716r7K5Qz+SqtAOVswoVunoiBtGhxw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.23.3.tgz", + "integrity": "sha512-VvfVYlrlBVu+77xVTOAoxQ6mZbnIq5FM0aGBSFEcIh03qHf+zNqA4DC/3XMUozTg7bZV3e3mZQ0i13VB6v5yUg==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0" + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1710,12 +1913,12 @@ } }, "node_modules/@babel/plugin-transform-sticky-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.18.6.tgz", - "integrity": "sha512-kfiDrDQ+PBsQDO85yj1icueWMfGfJFKN1KCkndygtu/C9+XUfydLC8Iv5UYJqRwy4zk8EcplRxEOeLyjq1gm6Q==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.23.3.tgz", + "integrity": "sha512-HZOyN9g+rtvnOU3Yh7kSxXrKbzgrm5X4GncPY1QOquu7epga5MxKHVpYu2hvQnry/H+JjckSYRb93iNfsioAGg==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1725,12 +1928,12 @@ } }, "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.18.9.tgz", - "integrity": "sha512-S8cOWfT82gTezpYOiVaGHrCbhlHgKhQt8XH5ES46P2XWmX92yisoZywf5km75wv5sYcXDUCLMmMxOLCtthDgMA==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.23.3.tgz", + "integrity": "sha512-Flok06AYNp7GV2oJPZZcP9vZdszev6vPBkHLwxwSpaIqx75wn6mUd3UFWsSsA0l8nXAKkyCmL/sR02m8RYGeHg==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1740,12 +1943,12 @@ } }, "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.18.9.tgz", - "integrity": "sha512-SRfwTtF11G2aemAZWivL7PD+C9z52v9EvMqH9BuYbabyPuKUvSWks3oCg6041pT925L4zVFqaVBeECwsmlguEw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.23.3.tgz", + "integrity": "sha512-4t15ViVnaFdrPC74be1gXBSMzXk3B4Us9lP7uLRQHTFpV5Dvt33pn+2MyyNxmN3VTTm3oTrZVMUmuw3oBnQ2oQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1755,15 +1958,15 @@ } }, "node_modules/@babel/plugin-transform-typescript": { - "version": "7.21.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.21.3.tgz", - "integrity": "sha512-RQxPz6Iqt8T0uw/WsJNReuBpWpBqs/n7mNo18sKLoTbMp+UrEekhH+pKSVC7gWz+DNjo9gryfV8YzCiT45RgMw==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.23.6.tgz", + "integrity": "sha512-6cBG5mBvUu4VUD04OHKnYzbuHNP8huDsD3EDqqpIpsswTDoqHCjLoHb6+QgsV1WsT2nipRqCPgxD3LXnEO7XfA==", "dev": true, "dependencies": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-create-class-features-plugin": "^7.21.0", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/plugin-syntax-typescript": "^7.20.0" + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-create-class-features-plugin": "^7.23.6", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-typescript": "^7.23.3" }, "engines": { "node": ">=6.9.0" @@ -1773,12 +1976,28 @@ } }, "node_modules/@babel/plugin-transform-unicode-escapes": { - "version": "7.18.10", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.18.10.tgz", - "integrity": "sha512-kKAdAI+YzPgGY/ftStBFXTI1LZFju38rYThnfMykS+IXy8BVx+res7s2fxf1l8I35DV2T97ezo6+SGrXz6B3iQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.23.3.tgz", + "integrity": "sha512-OMCUx/bU6ChE3r4+ZdylEqAjaQgHAgipgW8nsCfu5pGqDcFytVd91AwRvUJSBZDz0exPGgnjoqhgRYLRjFZc9Q==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9" + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.23.3.tgz", + "integrity": "sha512-KcLIm+pDZkWZQAFJ9pdfmh89EwVfmNovFBcXko8szpBeF8z68kWIPeKlmSOkT9BXJxs2C0uk+5LxoxIv62MROA==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1788,13 +2007,13 @@ } }, "node_modules/@babel/plugin-transform-unicode-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.18.6.tgz", - "integrity": "sha512-gE7A6Lt7YLnNOL3Pb9BNeZvi+d8l7tcRrG4+pwJjK9hD2xX4mEvjlQW60G9EEmfXVYRPv9VRQcyegIVHCql/AA==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.23.3.tgz", + "integrity": "sha512-wMHpNA4x2cIA32b/ci3AfwNgheiva2W0WUKWTK7vBHBhDKfPsc5cFGNWm69WBqpwd86u1qwZ9PWevKqm1A3yAw==", "dev": true, "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-create-regexp-features-plugin": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1803,39 +2022,44 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/preset-env": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.21.4.tgz", - "integrity": "sha512-2W57zHs2yDLm6GD5ZpvNn71lZ0B/iypSdIeq25OurDKji6AdzV07qp4s3n1/x5BqtiGaTrPN3nerlSCaC5qNTw==", + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.23.3.tgz", + "integrity": "sha512-W7lliA/v9bNR83Qc3q1ip9CQMZ09CcHDbHfbLRDNuAhn1Mvkr1ZNF7hPmztMQvtTGVLJ9m8IZqWsTkXOml8dbw==", "dev": true, "dependencies": { - "@babel/compat-data": "^7.21.4", - "@babel/helper-compilation-targets": "^7.21.4", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-validator-option": "^7.21.0", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.18.6", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.20.7", - "@babel/plugin-proposal-async-generator-functions": "^7.20.7", - "@babel/plugin-proposal-class-properties": "^7.18.6", - "@babel/plugin-proposal-class-static-block": "^7.21.0", - "@babel/plugin-proposal-dynamic-import": "^7.18.6", - "@babel/plugin-proposal-export-namespace-from": "^7.18.9", - "@babel/plugin-proposal-json-strings": "^7.18.6", - "@babel/plugin-proposal-logical-assignment-operators": "^7.20.7", - "@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6", - "@babel/plugin-proposal-numeric-separator": "^7.18.6", - "@babel/plugin-proposal-object-rest-spread": "^7.20.7", - "@babel/plugin-proposal-optional-catch-binding": "^7.18.6", - "@babel/plugin-proposal-optional-chaining": "^7.21.0", - "@babel/plugin-proposal-private-methods": "^7.18.6", - "@babel/plugin-proposal-private-property-in-object": "^7.21.0", - "@babel/plugin-proposal-unicode-property-regex": "^7.18.6", + "@babel/helper-create-regexp-features-plugin": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.23.9.tgz", + "integrity": "sha512-3kBGTNBBk9DQiPoXYS0g0BYlwTQYUTifqgKTjxUwEUkduRT2QOa0FPGBJ+NROQhGyYO5BuTJwGvBnqKDykac6A==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.23.5", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-validator-option": "^7.23.5", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.23.3", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.23.3", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.23.7", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", "@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-class-properties": "^7.12.13", "@babel/plugin-syntax-class-static-block": "^7.14.5", "@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-syntax-export-namespace-from": "^7.8.3", - "@babel/plugin-syntax-import-assertions": "^7.20.0", + "@babel/plugin-syntax-import-assertions": "^7.23.3", + "@babel/plugin-syntax-import-attributes": "^7.23.3", + "@babel/plugin-syntax-import-meta": "^7.10.4", "@babel/plugin-syntax-json-strings": "^7.8.3", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", @@ -1845,45 +2069,61 @@ "@babel/plugin-syntax-optional-chaining": "^7.8.3", "@babel/plugin-syntax-private-property-in-object": "^7.14.5", "@babel/plugin-syntax-top-level-await": "^7.14.5", - "@babel/plugin-transform-arrow-functions": "^7.20.7", - "@babel/plugin-transform-async-to-generator": "^7.20.7", - "@babel/plugin-transform-block-scoped-functions": "^7.18.6", - "@babel/plugin-transform-block-scoping": "^7.21.0", - "@babel/plugin-transform-classes": "^7.21.0", - "@babel/plugin-transform-computed-properties": "^7.20.7", - "@babel/plugin-transform-destructuring": "^7.21.3", - "@babel/plugin-transform-dotall-regex": "^7.18.6", - "@babel/plugin-transform-duplicate-keys": "^7.18.9", - "@babel/plugin-transform-exponentiation-operator": "^7.18.6", - "@babel/plugin-transform-for-of": "^7.21.0", - "@babel/plugin-transform-function-name": "^7.18.9", - "@babel/plugin-transform-literals": "^7.18.9", - "@babel/plugin-transform-member-expression-literals": "^7.18.6", - "@babel/plugin-transform-modules-amd": "^7.20.11", - "@babel/plugin-transform-modules-commonjs": "^7.21.2", - "@babel/plugin-transform-modules-systemjs": "^7.20.11", - "@babel/plugin-transform-modules-umd": "^7.18.6", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.20.5", - "@babel/plugin-transform-new-target": "^7.18.6", - "@babel/plugin-transform-object-super": "^7.18.6", - "@babel/plugin-transform-parameters": "^7.21.3", - "@babel/plugin-transform-property-literals": "^7.18.6", - "@babel/plugin-transform-regenerator": "^7.20.5", - "@babel/plugin-transform-reserved-words": "^7.18.6", - "@babel/plugin-transform-shorthand-properties": "^7.18.6", - "@babel/plugin-transform-spread": "^7.20.7", - "@babel/plugin-transform-sticky-regex": "^7.18.6", - "@babel/plugin-transform-template-literals": "^7.18.9", - "@babel/plugin-transform-typeof-symbol": "^7.18.9", - "@babel/plugin-transform-unicode-escapes": "^7.18.10", - "@babel/plugin-transform-unicode-regex": "^7.18.6", - "@babel/preset-modules": "^0.1.5", - "@babel/types": "^7.21.4", - "babel-plugin-polyfill-corejs2": "^0.3.3", - "babel-plugin-polyfill-corejs3": "^0.6.0", - "babel-plugin-polyfill-regenerator": "^0.4.1", - "core-js-compat": "^3.25.1", - "semver": "^6.3.0" + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.23.3", + "@babel/plugin-transform-async-generator-functions": "^7.23.9", + "@babel/plugin-transform-async-to-generator": "^7.23.3", + "@babel/plugin-transform-block-scoped-functions": "^7.23.3", + "@babel/plugin-transform-block-scoping": "^7.23.4", + "@babel/plugin-transform-class-properties": "^7.23.3", + "@babel/plugin-transform-class-static-block": "^7.23.4", + "@babel/plugin-transform-classes": "^7.23.8", + "@babel/plugin-transform-computed-properties": "^7.23.3", + "@babel/plugin-transform-destructuring": "^7.23.3", + "@babel/plugin-transform-dotall-regex": "^7.23.3", + "@babel/plugin-transform-duplicate-keys": "^7.23.3", + "@babel/plugin-transform-dynamic-import": "^7.23.4", + "@babel/plugin-transform-exponentiation-operator": "^7.23.3", + "@babel/plugin-transform-export-namespace-from": "^7.23.4", + "@babel/plugin-transform-for-of": "^7.23.6", + "@babel/plugin-transform-function-name": "^7.23.3", + "@babel/plugin-transform-json-strings": "^7.23.4", + "@babel/plugin-transform-literals": "^7.23.3", + "@babel/plugin-transform-logical-assignment-operators": "^7.23.4", + "@babel/plugin-transform-member-expression-literals": "^7.23.3", + "@babel/plugin-transform-modules-amd": "^7.23.3", + "@babel/plugin-transform-modules-commonjs": "^7.23.3", + "@babel/plugin-transform-modules-systemjs": "^7.23.9", + "@babel/plugin-transform-modules-umd": "^7.23.3", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5", + "@babel/plugin-transform-new-target": "^7.23.3", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.23.4", + "@babel/plugin-transform-numeric-separator": "^7.23.4", + "@babel/plugin-transform-object-rest-spread": "^7.23.4", + "@babel/plugin-transform-object-super": "^7.23.3", + "@babel/plugin-transform-optional-catch-binding": "^7.23.4", + "@babel/plugin-transform-optional-chaining": "^7.23.4", + "@babel/plugin-transform-parameters": "^7.23.3", + "@babel/plugin-transform-private-methods": "^7.23.3", + "@babel/plugin-transform-private-property-in-object": "^7.23.4", + "@babel/plugin-transform-property-literals": "^7.23.3", + "@babel/plugin-transform-regenerator": "^7.23.3", + "@babel/plugin-transform-reserved-words": "^7.23.3", + "@babel/plugin-transform-shorthand-properties": "^7.23.3", + "@babel/plugin-transform-spread": "^7.23.3", + "@babel/plugin-transform-sticky-regex": "^7.23.3", + "@babel/plugin-transform-template-literals": "^7.23.3", + "@babel/plugin-transform-typeof-symbol": "^7.23.3", + "@babel/plugin-transform-unicode-escapes": "^7.23.3", + "@babel/plugin-transform-unicode-property-regex": "^7.23.3", + "@babel/plugin-transform-unicode-regex": "^7.23.3", + "@babel/plugin-transform-unicode-sets-regex": "^7.23.3", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.8", + "babel-plugin-polyfill-corejs3": "^0.9.0", + "babel-plugin-polyfill-regenerator": "^0.5.5", + "core-js-compat": "^3.31.0", + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" @@ -1893,33 +2133,31 @@ } }, "node_modules/@babel/preset-modules": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.5.tgz", - "integrity": "sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA==", + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", - "@babel/plugin-transform-dotall-regex": "^7.4.4", "@babel/types": "^7.4.4", "esutils": "^2.0.2" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" } }, "node_modules/@babel/preset-react": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.18.6.tgz", - "integrity": "sha512-zXr6atUmyYdiWRVLOZahakYmOBHtWc2WGCkP8PYTgZi0iJXDY2CN180TdrIW4OGOAdLc7TifzDIvtx6izaRIzg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.23.3.tgz", + "integrity": "sha512-tbkHOS9axH6Ysf2OUEqoSZ6T3Fa2SrNH6WTWSPBboxKzdxNc9qOICeLXkNG0ZEwbQ1HY8liwOce4aN/Ceyuq6w==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/helper-validator-option": "^7.18.6", - "@babel/plugin-transform-react-display-name": "^7.18.6", - "@babel/plugin-transform-react-jsx": "^7.18.6", - "@babel/plugin-transform-react-jsx-development": "^7.18.6", - "@babel/plugin-transform-react-pure-annotations": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-validator-option": "^7.22.15", + "@babel/plugin-transform-react-display-name": "^7.23.3", + "@babel/plugin-transform-react-jsx": "^7.22.15", + "@babel/plugin-transform-react-jsx-development": "^7.22.5", + "@babel/plugin-transform-react-pure-annotations": "^7.23.3" }, "engines": { "node": ">=6.9.0" @@ -1929,16 +2167,16 @@ } }, "node_modules/@babel/preset-typescript": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.21.4.tgz", - "integrity": "sha512-sMLNWY37TCdRH/bJ6ZeeOH1nPuanED7Ai9Y/vH31IPqalioJ6ZNFUWONsakhv4r4n+I6gm5lmoE0olkgib/j/A==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.23.3.tgz", + "integrity": "sha512-17oIGVlqz6CchO9RFYn5U6ZpWRZIngayYCtrPRSgANSwC2V1Jb+iP74nVxzzXJte8b8BYxrL1yY96xfhTBrNNQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-validator-option": "^7.21.0", - "@babel/plugin-syntax-jsx": "^7.21.4", - "@babel/plugin-transform-modules-commonjs": "^7.21.2", - "@babel/plugin-transform-typescript": "^7.21.3" + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-validator-option": "^7.22.15", + "@babel/plugin-syntax-jsx": "^7.23.3", + "@babel/plugin-transform-modules-commonjs": "^7.23.3", + "@babel/plugin-transform-typescript": "^7.23.3" }, "engines": { "node": ">=6.9.0" @@ -1954,45 +2192,45 @@ "dev": true }, "node_modules/@babel/runtime": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.0.tgz", - "integrity": "sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.9.tgz", + "integrity": "sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==", "dependencies": { - "regenerator-runtime": "^0.13.11" + "regenerator-runtime": "^0.14.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/template": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz", - "integrity": "sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.23.9.tgz", + "integrity": "sha512-+xrD2BWLpvHKNmX2QbpdpsBaWnRxahMwJjO+KZk2JOElj5nSmKezyS1B4u+QbHMTX69t4ukm6hh9lsYQ7GHCKA==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.18.6", - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7" + "@babel/code-frame": "^7.23.5", + "@babel/parser": "^7.23.9", + "@babel/types": "^7.23.9" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.21.4.tgz", - "integrity": "sha512-eyKrRHKdyZxqDm+fV1iqL9UAHMoIg0nDaGqfIOd8rKH17m5snv7Gn4qgjBoFfLz9APvjFU/ICT00NVCv1Epp8Q==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.9.tgz", + "integrity": "sha512-I/4UJ9vs90OkBtY6iiiTORVMyIhJ4kAVmsKo9KFc8UOxMeUfi2hvtIBsET5u9GizXE6/GFSuKCTNfgCswuEjRg==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.21.4", - "@babel/generator": "^7.21.4", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.21.0", - "@babel/helper-hoist-variables": "^7.18.6", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/parser": "^7.21.4", - "@babel/types": "^7.21.4", - "debug": "^4.1.0", + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.6", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.9", + "@babel/types": "^7.23.9", + "debug": "^4.3.1", "globals": "^11.1.0" }, "engines": { @@ -2000,12 +2238,12 @@ } }, "node_modules/@babel/types": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.21.4.tgz", - "integrity": "sha512-rU2oY501qDxE8Pyo7i/Orqma4ziCOrby0/9mvbDUGEfvZjb279Nk9k19e2fiCxHbRRpY2ZyrgW1eq22mvmOIzA==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.9.tgz", + "integrity": "sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==", "dependencies": { - "@babel/helper-string-parser": "^7.19.4", - "@babel/helper-validator-identifier": "^7.19.1", + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" }, "engines": { @@ -2019,62 +2257,62 @@ "dev": true }, "node_modules/@emotion/babel-plugin": { - "version": "11.10.6", - "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.10.6.tgz", - "integrity": "sha512-p2dAqtVrkhSa7xz1u/m9eHYdLi+en8NowrmXeF/dKtJpU8lCWli8RUAati7NcSl0afsBott48pdnANuD0wh9QQ==", + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz", + "integrity": "sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==", "peer": true, "dependencies": { "@babel/helper-module-imports": "^7.16.7", "@babel/runtime": "^7.18.3", - "@emotion/hash": "^0.9.0", - "@emotion/memoize": "^0.8.0", - "@emotion/serialize": "^1.1.1", + "@emotion/hash": "^0.9.1", + "@emotion/memoize": "^0.8.1", + "@emotion/serialize": "^1.1.2", "babel-plugin-macros": "^3.1.0", "convert-source-map": "^1.5.0", "escape-string-regexp": "^4.0.0", "find-root": "^1.1.0", "source-map": "^0.5.7", - "stylis": "4.1.3" + "stylis": "4.2.0" } }, "node_modules/@emotion/cache": { - "version": "11.10.7", - "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.10.7.tgz", - "integrity": "sha512-VLl1/2D6LOjH57Y8Vem1RoZ9haWF4jesHDGiHtKozDQuBIkJm2gimVo0I02sWCuzZtVACeixTVB4jeE8qvCBoQ==", + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.11.0.tgz", + "integrity": "sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ==", "peer": true, "dependencies": { - "@emotion/memoize": "^0.8.0", - "@emotion/sheet": "^1.2.1", - "@emotion/utils": "^1.2.0", - "@emotion/weak-memoize": "^0.3.0", - "stylis": "4.1.3" + "@emotion/memoize": "^0.8.1", + "@emotion/sheet": "^1.2.2", + "@emotion/utils": "^1.2.1", + "@emotion/weak-memoize": "^0.3.1", + "stylis": "4.2.0" } }, "node_modules/@emotion/hash": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.0.tgz", - "integrity": "sha512-14FtKiHhy2QoPIzdTcvh//8OyBlknNs2nXRwIhG904opCby3l+9Xaf/wuPvICBF0rc1ZCNBd3nKe9cd2mecVkQ==", + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz", + "integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==", "peer": true }, "node_modules/@emotion/memoize": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.0.tgz", - "integrity": "sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA==", + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==", "peer": true }, "node_modules/@emotion/react": { - "version": "11.10.6", - "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.10.6.tgz", - "integrity": "sha512-6HT8jBmcSkfzO7mc+N1L9uwvOnlcGoix8Zn7srt+9ga0MjREo6lRpuVX0kzo6Jp6oTqDhREOFsygN6Ew4fEQbw==", + "version": "11.11.3", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.11.3.tgz", + "integrity": "sha512-Cnn0kuq4DoONOMcnoVsTOR8E+AdnKFf//6kUWc4LCdnxj31pZWn7rIULd6Y7/Js1PiPHzn7SKCM9vB/jBni8eA==", "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", - "@emotion/babel-plugin": "^11.10.6", - "@emotion/cache": "^11.10.5", - "@emotion/serialize": "^1.1.1", - "@emotion/use-insertion-effect-with-fallbacks": "^1.0.0", - "@emotion/utils": "^1.2.0", - "@emotion/weak-memoize": "^0.3.0", + "@emotion/babel-plugin": "^11.11.0", + "@emotion/cache": "^11.11.0", + "@emotion/serialize": "^1.1.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1", + "@emotion/utils": "^1.2.1", + "@emotion/weak-memoize": "^0.3.1", "hoist-non-react-statics": "^3.3.1" }, "peerDependencies": { @@ -2087,55 +2325,71 @@ } }, "node_modules/@emotion/serialize": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.1.tgz", - "integrity": "sha512-Zl/0LFggN7+L1liljxXdsVSVlg6E/Z/olVWpfxUTxOAmi8NU7YoeWeLfi1RmnB2TATHoaWwIBRoL+FvAJiTUQA==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.3.tgz", + "integrity": "sha512-iD4D6QVZFDhcbH0RAG1uVu1CwVLMWUkCvAqqlewO/rxf8+87yIBAlt4+AxMiiKPLs5hFc0owNk/sLLAOROw3cA==", "peer": true, "dependencies": { - "@emotion/hash": "^0.9.0", - "@emotion/memoize": "^0.8.0", - "@emotion/unitless": "^0.8.0", - "@emotion/utils": "^1.2.0", + "@emotion/hash": "^0.9.1", + "@emotion/memoize": "^0.8.1", + "@emotion/unitless": "^0.8.1", + "@emotion/utils": "^1.2.1", "csstype": "^3.0.2" } }, "node_modules/@emotion/sheet": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.1.tgz", - "integrity": "sha512-zxRBwl93sHMsOj4zs+OslQKg/uhF38MB+OMKoCrVuS0nyTkqnau+BM3WGEoOptg9Oz45T/aIGs1qbVAsEFo3nA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.2.tgz", + "integrity": "sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA==", "peer": true }, "node_modules/@emotion/unitless": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.0.tgz", - "integrity": "sha512-VINS5vEYAscRl2ZUDiT3uMPlrFQupiKgHz5AA4bCH1miKBg4qtwkim1qPmJj/4WG6TreYMY111rEFsjupcOKHw==", + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", + "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==", "peer": true }, "node_modules/@emotion/use-insertion-effect-with-fallbacks": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.0.tgz", - "integrity": "sha512-1eEgUGmkaljiBnRMTdksDV1W4kUnmwgp7X9G8B++9GYwl1lUdqSndSriIrTJ0N7LQaoauY9JJ2yhiOYK5+NI4A==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz", + "integrity": "sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==", "peer": true, "peerDependencies": { "react": ">=16.8.0" } }, "node_modules/@emotion/utils": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.0.tgz", - "integrity": "sha512-sn3WH53Kzpw8oQ5mgMmIzzyAaH2ZqFEbozVVBSYp538E06OSE6ytOp7pRAjNQR+Q/orwqdQYJSe2m3hCOeznkw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.1.tgz", + "integrity": "sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg==", "peer": true }, "node_modules/@emotion/weak-memoize": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.0.tgz", - "integrity": "sha512-AHPmaAx+RYfZz0eYu6Gviiagpmiyw98ySSlQvCUhVGDRtDFe4DBS0x1bSjdF3gqUDYOczB+yYvBTtEylYSdRhg==", + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz", + "integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==", "peer": true }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", + "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/@esbuild/android-arm": { - "version": "0.17.18", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.18.tgz", - "integrity": "sha512-EmwL+vUBZJ7mhFCs5lA4ZimpUH3WMAoqvOIYhVQwdIgSpHC8ImHdsRyhHAVxpDYUSm0lWvd63z0XH1IlImS2Qw==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", + "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", "cpu": [ "arm" ], @@ -2149,9 +2403,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.17.18", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.18.tgz", - "integrity": "sha512-/iq0aK0eeHgSC3z55ucMAHO05OIqmQehiGay8eP5l/5l+iEr4EIbh4/MI8xD9qRFjqzgkc0JkX0LculNC9mXBw==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", + "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", "cpu": [ "arm64" ], @@ -2165,9 +2419,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.17.18", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.18.tgz", - "integrity": "sha512-x+0efYNBF3NPW2Xc5bFOSFW7tTXdAcpfEg2nXmxegm4mJuVeS+i109m/7HMiOQ6M12aVGGFlqJX3RhNdYM2lWg==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", + "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", "cpu": [ "x64" ], @@ -2181,9 +2435,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.17.18", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.18.tgz", - "integrity": "sha512-6tY+djEAdF48M1ONWnQb1C+6LiXrKjmqjzPNPWXhu/GzOHTHX2nh8Mo2ZAmBFg0kIodHhciEgUBtcYCAIjGbjQ==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", + "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", "cpu": [ "arm64" ], @@ -2197,9 +2451,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.17.18", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.18.tgz", - "integrity": "sha512-Qq84ykvLvya3dO49wVC9FFCNUfSrQJLbxhoQk/TE1r6MjHo3sFF2tlJCwMjhkBVq3/ahUisj7+EpRSz0/+8+9A==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", + "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", "cpu": [ "x64" ], @@ -2213,9 +2467,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.17.18", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.18.tgz", - "integrity": "sha512-fw/ZfxfAzuHfaQeMDhbzxp9mc+mHn1Y94VDHFHjGvt2Uxl10mT4CDavHm+/L9KG441t1QdABqkVYwakMUeyLRA==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", + "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", "cpu": [ "arm64" ], @@ -2229,9 +2483,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.17.18", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.18.tgz", - "integrity": "sha512-FQFbRtTaEi8ZBi/A6kxOC0V0E9B/97vPdYjY9NdawyLd4Qk5VD5g2pbWN2VR1c0xhzcJm74HWpObPszWC+qTew==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", + "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", "cpu": [ "x64" ], @@ -2245,9 +2499,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.17.18", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.18.tgz", - "integrity": "sha512-jW+UCM40LzHcouIaqv3e/oRs0JM76JfhHjCavPxMUti7VAPh8CaGSlS7cmyrdpzSk7A+8f0hiedHqr/LMnfijg==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", + "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", "cpu": [ "arm" ], @@ -2261,9 +2515,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.17.18", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.18.tgz", - "integrity": "sha512-R7pZvQZFOY2sxUG8P6A21eq6q+eBv7JPQYIybHVf1XkQYC+lT7nDBdC7wWKTrbvMXKRaGudp/dzZCwL/863mZQ==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", + "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", "cpu": [ "arm64" ], @@ -2277,9 +2531,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.17.18", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.18.tgz", - "integrity": "sha512-ygIMc3I7wxgXIxk6j3V00VlABIjq260i967Cp9BNAk5pOOpIXmd1RFQJQX9Io7KRsthDrQYrtcx7QCof4o3ZoQ==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", + "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", "cpu": [ "ia32" ], @@ -2293,9 +2547,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.17.18", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.18.tgz", - "integrity": "sha512-bvPG+MyFs5ZlwYclCG1D744oHk1Pv7j8psF5TfYx7otCVmcJsEXgFEhQkbhNW8otDHL1a2KDINW20cfCgnzgMQ==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", + "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", "cpu": [ "loong64" ], @@ -2309,9 +2563,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.17.18", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.18.tgz", - "integrity": "sha512-oVqckATOAGuiUOa6wr8TXaVPSa+6IwVJrGidmNZS1cZVx0HqkTMkqFGD2HIx9H1RvOwFeWYdaYbdY6B89KUMxA==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", + "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", "cpu": [ "mips64el" ], @@ -2325,9 +2579,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.17.18", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.18.tgz", - "integrity": "sha512-3dLlQO+b/LnQNxgH4l9rqa2/IwRJVN9u/bK63FhOPB4xqiRqlQAU0qDU3JJuf0BmaH0yytTBdoSBHrb2jqc5qQ==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", + "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", "cpu": [ "ppc64" ], @@ -2341,9 +2595,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.17.18", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.18.tgz", - "integrity": "sha512-/x7leOyDPjZV3TcsdfrSI107zItVnsX1q2nho7hbbQoKnmoeUWjs+08rKKt4AUXju7+3aRZSsKrJtaRmsdL1xA==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", + "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", "cpu": [ "riscv64" ], @@ -2357,9 +2611,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.17.18", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.18.tgz", - "integrity": "sha512-cX0I8Q9xQkL/6F5zWdYmVf5JSQt+ZfZD2bJudZrWD+4mnUvoZ3TDDXtDX2mUaq6upMFv9FlfIh4Gfun0tbGzuw==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", + "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", "cpu": [ "s390x" ], @@ -2373,9 +2627,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.17.18", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.18.tgz", - "integrity": "sha512-66RmRsPlYy4jFl0vG80GcNRdirx4nVWAzJmXkevgphP1qf4dsLQCpSKGM3DUQCojwU1hnepI63gNZdrr02wHUA==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", + "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", "cpu": [ "x64" ], @@ -2389,9 +2643,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.17.18", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.18.tgz", - "integrity": "sha512-95IRY7mI2yrkLlTLb1gpDxdC5WLC5mZDi+kA9dmM5XAGxCME0F8i4bYH4jZreaJ6lIZ0B8hTrweqG1fUyW7jbg==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", + "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", "cpu": [ "x64" ], @@ -2405,9 +2659,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.17.18", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.18.tgz", - "integrity": "sha512-WevVOgcng+8hSZ4Q3BKL3n1xTv5H6Nb53cBrtzzEjDbbnOmucEVcZeGCsCOi9bAOcDYEeBZbD2SJNBxlfP3qiA==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", + "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", "cpu": [ "x64" ], @@ -2421,9 +2675,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.17.18", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.18.tgz", - "integrity": "sha512-Rzf4QfQagnwhQXVBS3BYUlxmEbcV7MY+BH5vfDZekU5eYpcffHSyjU8T0xucKVuOcdCsMo+Ur5wmgQJH2GfNrg==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", + "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", "cpu": [ "x64" ], @@ -2437,9 +2691,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.17.18", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.18.tgz", - "integrity": "sha512-Kb3Ko/KKaWhjeAm2YoT/cNZaHaD1Yk/pa3FTsmqo9uFh1D1Rfco7BBLIPdDOozrObj2sahslFuAQGvWbgWldAg==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", + "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", "cpu": [ "arm64" ], @@ -2453,9 +2707,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.17.18", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.18.tgz", - "integrity": "sha512-0/xUMIdkVHwkvxfbd5+lfG7mHOf2FRrxNbPiKWg9C4fFrB8H0guClmaM3BFiRUYrznVoyxTIyC/Ou2B7QQSwmw==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", + "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", "cpu": [ "ia32" ], @@ -2469,9 +2723,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.17.18", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.18.tgz", - "integrity": "sha512-qU25Ma1I3NqTSHJUOKi9sAH1/Mzuvlke0ioMJRthLXKm7JiSKVwFghlGbDLOO2sARECGhja4xYfRAZNPAkooYg==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", + "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", "cpu": [ "x64" ], @@ -2500,23 +2754,23 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.0.tgz", - "integrity": "sha512-vITaYzIcNmjn5tF5uxcZ/ft7/RXGrMUIS9HalWckEOF6ESiwXKoMzAQf2UW0aVd6rnOeExTJVd5hmWXucBKGXQ==", + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", + "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", "dev": true, "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, "node_modules/@eslint/eslintrc": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.2.tgz", - "integrity": "sha512-3W4f5tDUra+pA+FzgugqL2pRimUTDJWKr7BINqOpkZrC0uYI0NIc0/JFgBROCU07HR6GieA5m3/rsPIhDmCXTQ==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "dev": true, "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.5.1", + "espree": "^9.6.0", "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", @@ -2532,9 +2786,9 @@ } }, "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "13.20.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", - "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, "dependencies": { "type-fest": "^0.20.2" @@ -2559,25 +2813,29 @@ } }, "node_modules/@eslint/js": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.39.0.tgz", - "integrity": "sha512-kf9RB0Fg7NZfap83B3QOqOGg9QmD9yBudqQXzzOtn3i4y7ZUXe5ONeW34Gwi+TxhH4mvj72R1Zc300KUMa9Bng==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz", + "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, "node_modules/@floating-ui/core": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.2.6.tgz", - "integrity": "sha512-EvYTiXet5XqweYGClEmpu3BoxmsQ4hkj3QaYA6qEnigCWffTP3vNRwBReTdrwDwo7OoJ3wM8Uoe9Uk4n+d4hfg==" + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.0.tgz", + "integrity": "sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g==", + "dependencies": { + "@floating-ui/utils": "^0.2.1" + } }, "node_modules/@floating-ui/dom": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.2.6.tgz", - "integrity": "sha512-02vxFDuvuVPs22iJICacezYJyf7zwwOCWkPNkWNBr1U0Qt1cKFYzWvxts0AmqcOQGwt/3KJWcWIgtbUU38keyw==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.0.tgz", + "integrity": "sha512-SZ0BEXzsaaS6THZfZJUcAobbZTD+MvfGM42bxgeg0Tnkp4/an/avqwAXiVLsFtIBZtfsx3Ymvwx0+KnnhdA/9g==", "dependencies": { - "@floating-ui/core": "^1.2.6" + "@floating-ui/core": "^1.6.0", + "@floating-ui/utils": "^0.2.1" } }, "node_modules/@floating-ui/react": { @@ -2606,16 +2864,21 @@ "react-dom": ">=16.8.0" } }, + "node_modules/@floating-ui/utils": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.1.tgz", + "integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==" + }, "node_modules/@fontsource/roboto": { - "version": "4.5.8", - "resolved": "https://registry.npmjs.org/@fontsource/roboto/-/roboto-4.5.8.tgz", - "integrity": "sha512-CnD7zLItIzt86q4Sj3kZUiLcBk1dSk81qcqgMGaZe7SQ1P8hFNxhMl5AZthK1zrDM5m74VVhaOpuMGIL4gagaA==", + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/@fontsource/roboto/-/roboto-5.0.8.tgz", + "integrity": "sha512-XxPltXs5R31D6UZeLIV1td3wTXU3jzd3f2DLsXI8tytMGBkIsGcc9sIyiupRtA8y73HAhuSCeweOoBqf6DbWCA==", "dev": true }, "node_modules/@fortawesome/fontawesome-common-types": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.4.0.tgz", - "integrity": "sha512-HNii132xfomg5QVZw0HwXXpN22s7VBHQBv9CeOu9tfJnhsWQNd2lmTNi8CSrnw5B+5YOmzu1UoPAyxaXsJ6RgQ==", + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.5.1.tgz", + "integrity": "sha512-GkWzv+L6d2bI5f/Vk6ikJ9xtl7dfXtoRu3YGE6nq0p/FFqA1ebMOAWg3XgRyb0I6LYyYkiAo+3/KrwuBp8xG7A==", "dev": true, "hasInstallScript": true, "engines": { @@ -2623,52 +2886,52 @@ } }, "node_modules/@fortawesome/fontawesome-svg-core": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.4.0.tgz", - "integrity": "sha512-Bertv8xOiVELz5raB2FlXDPKt+m94MQ3JgDfsVbrqNpLU9+UE2E18GKjLKw+d3XbeYPqg1pzyQKGsrzbw+pPaw==", + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.5.1.tgz", + "integrity": "sha512-MfRCYlQPXoLlpem+egxjfkEuP9UQswTrlCOsknus/NcMoblTH2g0jPrapbcIb04KGA7E2GZxbAccGZfWoYgsrQ==", "dev": true, "hasInstallScript": true, "dependencies": { - "@fortawesome/fontawesome-common-types": "6.4.0" + "@fortawesome/fontawesome-common-types": "6.5.1" }, "engines": { "node": ">=6" } }, "node_modules/@fortawesome/free-brands-svg-icons": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.4.0.tgz", - "integrity": "sha512-qvxTCo0FQ5k2N+VCXb/PZQ+QMhqRVM4OORiO6MXdG6bKolIojGU/srQ1ptvKk0JTbRgaJOfL2qMqGvBEZG7Z6g==", + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.5.1.tgz", + "integrity": "sha512-093l7DAkx0aEtBq66Sf19MgoZewv1zeY9/4C7vSKPO4qMwEsW/2VYTUTpBtLwfb9T2R73tXaRDPmE4UqLCYHfg==", "dev": true, "hasInstallScript": true, "dependencies": { - "@fortawesome/fontawesome-common-types": "6.4.0" + "@fortawesome/fontawesome-common-types": "6.5.1" }, "engines": { "node": ">=6" } }, "node_modules/@fortawesome/free-regular-svg-icons": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.4.0.tgz", - "integrity": "sha512-ZfycI7D0KWPZtf7wtMFnQxs8qjBXArRzczABuMQqecA/nXohquJ5J/RCR77PmY5qGWkxAZDxpnUFVXKwtY/jPw==", + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.5.1.tgz", + "integrity": "sha512-m6ShXn+wvqEU69wSP84coxLbNl7sGVZb+Ca+XZq6k30SzuP3X4TfPqtycgUh9ASwlNh5OfQCd8pDIWxl+O+LlQ==", "dev": true, "hasInstallScript": true, "dependencies": { - "@fortawesome/fontawesome-common-types": "6.4.0" + "@fortawesome/fontawesome-common-types": "6.5.1" }, "engines": { "node": ">=6" } }, "node_modules/@fortawesome/free-solid-svg-icons": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.4.0.tgz", - "integrity": "sha512-kutPeRGWm8V5dltFP1zGjQOEAzaLZj4StdQhWVZnfGFCvAPVvHh8qk5bRrU4KXnRRRNni5tKQI9PBAdI6MP8nQ==", + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.5.1.tgz", + "integrity": "sha512-S1PPfU3mIJa59biTtXJz1oI0+KAXW6bkAb31XKhxdxtuXDiUIFsih4JR1v5BbxY7hVHsD1RKq+jRkVRaf773NQ==", "dev": true, "hasInstallScript": true, "dependencies": { - "@fortawesome/fontawesome-common-types": "6.4.0" + "@fortawesome/fontawesome-common-types": "6.5.1" }, "engines": { "node": ">=6" @@ -2688,13 +2951,13 @@ } }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.8", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz", - "integrity": "sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==", + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", "dev": true, "dependencies": { - "@humanwhocodes/object-schema": "^1.2.1", - "debug": "^4.1.1", + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", "minimatch": "^3.0.5" }, "engines": { @@ -2715,9 +2978,9 @@ } }, "node_modules/@humanwhocodes/object-schema": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", - "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", + "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", "dev": true }, "node_modules/@istanbuljs/schema": { @@ -2730,36 +2993,36 @@ } }, "node_modules/@jest/expect-utils": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.5.0.tgz", - "integrity": "sha512-fmKzsidoXQT2KwnrwE0SQq3uj8Z763vzR8LnLBwC2qYWEFpjX8daRsk6rHUM1QvNlEW/UJXNXm59ztmJJWs2Mg==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", "dev": true, "dependencies": { - "jest-get-type": "^29.4.3" + "jest-get-type": "^29.6.3" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/schemas": { - "version": "29.4.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.4.3.tgz", - "integrity": "sha512-VLYKXQmtmuEz6IxJsrZwzG9NvtkQsWNnWMsKxqWNu3+CnfzJQhp0WDDKWLVV9hLKr0l3SLLFRqcYHjhtyuDVxg==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", "dev": true, "dependencies": { - "@sinclair/typebox": "^0.25.16" + "@sinclair/typebox": "^0.27.8" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/types": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.5.0.tgz", - "integrity": "sha512-qbu7kN6czmVRc3xWFQcAN03RAUamgppVUdXrvl1Wr3jlNF93o9mJbGcDWrwGB6ht44u7efB1qCFgVQmca24Uog==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", "dev": true, "dependencies": { - "@jest/schemas": "^29.4.3", + "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", @@ -2785,9 +3048,9 @@ } }, "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", - "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", "dev": true, "engines": { "node": ">=6.0.0" @@ -2809,58 +3072,52 @@ "dev": true }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.18", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", - "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", + "version": "0.3.22", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.22.tgz", + "integrity": "sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw==", "dev": true, "dependencies": { - "@jridgewell/resolve-uri": "3.1.0", - "@jridgewell/sourcemap-codec": "1.4.14" + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@jridgewell/trace-mapping/node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.14", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", - "dev": true - }, "node_modules/@mantine/core": { - "version": "6.0.9", - "resolved": "https://registry.npmjs.org/@mantine/core/-/core-6.0.9.tgz", - "integrity": "sha512-v0g59ajqFOcSlXO3bNpRaZg3VSX3hVAJ/MyaAvzdMYtTiN0ogaBJH3RXQscxr7Us2ega4psG1rW7ybUP7WrZKg==", + "version": "6.0.21", + "resolved": "https://registry.npmjs.org/@mantine/core/-/core-6.0.21.tgz", + "integrity": "sha512-Kx4RrRfv0I+cOCIcsq/UA2aWcYLyXgW3aluAuW870OdXnbII6qg7RW28D+r9D76SHPxWFKwIKwmcucAG08Divg==", "dependencies": { "@floating-ui/react": "^0.19.1", - "@mantine/styles": "6.0.9", - "@mantine/utils": "6.0.9", + "@mantine/styles": "6.0.21", + "@mantine/utils": "6.0.21", "@radix-ui/react-scroll-area": "1.0.2", "react-remove-scroll": "^2.5.5", "react-textarea-autosize": "8.3.4" }, "peerDependencies": { - "@mantine/hooks": "6.0.9", + "@mantine/hooks": "6.0.21", "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "node_modules/@mantine/dropzone": { - "version": "6.0.9", - "resolved": "https://registry.npmjs.org/@mantine/dropzone/-/dropzone-6.0.9.tgz", - "integrity": "sha512-hJxoFSDUvhMI8Bma4b0DLL7Y+aYzxZNioOni8+cSOg9+5FGU0UxM6JZ6JUXP6dTKvpdAzwhZdT7no5EeWQo26Q==", + "version": "6.0.21", + "resolved": "https://registry.npmjs.org/@mantine/dropzone/-/dropzone-6.0.21.tgz", + "integrity": "sha512-v63tL4x7R1CvBNnxJVaVPhBVnQcfROQvyOV0xK/v0ZGNAzFxjJoiCRMGdlBjxnEawM0dRhNs/46ItpBgjQIr6g==", "dependencies": { - "@mantine/utils": "6.0.9", + "@mantine/utils": "6.0.21", "react-dropzone": "14.2.3" }, "peerDependencies": { - "@mantine/core": "6.0.9", - "@mantine/hooks": "6.0.9", + "@mantine/core": "6.0.21", + "@mantine/hooks": "6.0.21", "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "node_modules/@mantine/form": { - "version": "6.0.9", - "resolved": "https://registry.npmjs.org/@mantine/form/-/form-6.0.9.tgz", - "integrity": "sha512-JB6zzSPDNUH9yKIORplqUxP063y2jUfLUzH3kty7UaEt2YHMLDU8ttAF77uF3namTdp0kDsiyp/VYJtUidaPyQ==", + "version": "6.0.21", + "resolved": "https://registry.npmjs.org/@mantine/form/-/form-6.0.21.tgz", + "integrity": "sha512-d4tlxyZic7MSDnaPx/WliCX1sRFDkUd2nxx4MxxO2T4OSek0YDqTlSBCxeoveu60P+vrQQN5rbbsVsaOJBe4SQ==", "dependencies": { "fast-deep-equal": "^3.1.3", "klona": "^2.0.5" @@ -2870,46 +3127,46 @@ } }, "node_modules/@mantine/hooks": { - "version": "6.0.9", - "resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-6.0.9.tgz", - "integrity": "sha512-01FvXnXed0Hni4WvmtTs0HMulyd7sibEG52uS+oy4ziYumKb8eMMTnvs4Ov7meKAvtA9/pmW+Yif1pm2GB4bnw==", + "version": "6.0.21", + "resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-6.0.21.tgz", + "integrity": "sha512-sYwt5wai25W6VnqHbS5eamey30/HD5dNXaZuaVEAJ2i2bBv8C0cCiczygMDpAFiSYdXoSMRr/SZ2CrrPTzeNew==", "peerDependencies": { "react": ">=16.8.0" } }, "node_modules/@mantine/modals": { - "version": "6.0.9", - "resolved": "https://registry.npmjs.org/@mantine/modals/-/modals-6.0.9.tgz", - "integrity": "sha512-azdb++XlXheM1Ai4XPBk5d9fJOGv/PnLg4KciZZripkPZ5+xXrBwBxmCXlMogNYNfp6o+K5Q8BaiO16UbDvCQQ==", + "version": "6.0.21", + "resolved": "https://registry.npmjs.org/@mantine/modals/-/modals-6.0.21.tgz", + "integrity": "sha512-Gx2D/ZHMUuYF197JKMWey4K9FeGP9rxYp4lmAEXUrjXiST2fEhLZOdiD75KuOHXd1/sYAU9NcNRo9wXrlF/gUA==", "dependencies": { - "@mantine/utils": "6.0.9" + "@mantine/utils": "6.0.21" }, "peerDependencies": { - "@mantine/core": "6.0.9", - "@mantine/hooks": "6.0.9", + "@mantine/core": "6.0.21", + "@mantine/hooks": "6.0.21", "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "node_modules/@mantine/notifications": { - "version": "6.0.9", - "resolved": "https://registry.npmjs.org/@mantine/notifications/-/notifications-6.0.9.tgz", - "integrity": "sha512-X0Nyv9Fe9/1wrjxkyGTCE5VlOfzCbQjVWJ9uCWshqGkDSs8HTaWWZ9FM8fUeIXtI/LVB65OwPBegKw4/POgZQQ==", + "version": "6.0.21", + "resolved": "https://registry.npmjs.org/@mantine/notifications/-/notifications-6.0.21.tgz", + "integrity": "sha512-qsrqxuJHK8b67sf9Pfk+xyhvpf9jMsivW8vchfnJfjv7yz1lLvezjytMFp4fMDoYhjHnDPOEc/YFockK4muhOw==", "dependencies": { - "@mantine/utils": "6.0.9", + "@mantine/utils": "6.0.21", "react-transition-group": "4.4.2" }, "peerDependencies": { - "@mantine/core": "6.0.9", - "@mantine/hooks": "6.0.9", + "@mantine/core": "6.0.21", + "@mantine/hooks": "6.0.21", "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "node_modules/@mantine/styles": { - "version": "6.0.9", - "resolved": "https://registry.npmjs.org/@mantine/styles/-/styles-6.0.9.tgz", - "integrity": "sha512-jTtSicZalxw5fHD4p2c5RJdsb1DlBTKFwMmPWSTt5IgQ3ADucQtQWl2sVtVCpR/b5w9/Ihzgju84bPmOlAWh0g==", + "version": "6.0.21", + "resolved": "https://registry.npmjs.org/@mantine/styles/-/styles-6.0.21.tgz", + "integrity": "sha512-PVtL7XHUiD/B5/kZ/QvZOZZQQOj12QcRs3Q6nPoqaoPcOX5+S7bMZLMH0iLtcGq5OODYk0uxlvuJkOZGoPj8Mg==", "dependencies": { "clsx": "1.1.1", "csstype": "3.0.9" @@ -2934,9 +3191,9 @@ "integrity": "sha512-rpw6JPxK6Rfg1zLOYCSwle2GFOOsnjmDYDaBwEcwoOg4qlsIVCN789VkBZDJAGi4T07gI4YSutR43t9Zz4Lzuw==" }, "node_modules/@mantine/utils": { - "version": "6.0.9", - "resolved": "https://registry.npmjs.org/@mantine/utils/-/utils-6.0.9.tgz", - "integrity": "sha512-zEDZixO8dw2ZSNZiQ1g7S+Bq2q4E8ps6bvpyZsxpiXBdh3g2H0WOEdJdDpBTiTRPWotSlU1kaKN3tOplCRDQ2Q==", + "version": "6.0.21", + "resolved": "https://registry.npmjs.org/@mantine/utils/-/utils-6.0.21.tgz", + "integrity": "sha512-33RVDRop5jiWFao3HKd3Yp7A9mEq4HAJxJPTuYm1NkdqX6aTKOQK7wT8v8itVodBp+sb4cJK6ZVdD1UurK/txQ==", "peerDependencies": { "react": ">=16.8.0" } @@ -3008,9 +3265,9 @@ } }, "node_modules/@polka/url": { - "version": "1.0.0-next.21", - "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.21.tgz", - "integrity": "sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==", + "version": "1.0.0-next.24", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.24.tgz", + "integrity": "sha512-2LuNTFBIO0m7kKIQvvPHN6UE63VjpmL9rnEEaOOaiSPbZK+zUOYIzBAWcED+3XYzhYsd/0mD57VdxAEqqV52CQ==", "dev": true }, "node_modules/@radix-ui/number": { @@ -3145,23 +3402,192 @@ } }, "node_modules/@remix-run/router": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.5.0.tgz", - "integrity": "sha512-bkUDCp8o1MvFO+qxkODcbhSqRa6P2GXgrGZVpt0dCXNW2HCSCqYI0ZoAqEOSAjRWmmlKcYgFvN4B4S+zo/f8kg==", + "version": "1.14.2", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.14.2.tgz", + "integrity": "sha512-ACXpdMM9hmKZww21yEqWwiLws/UPLhNKvimN8RrYSqPSvB3ov7sLvAcfvaxePeLvccTQKGdkDIhLYApZVDFuKg==", "engines": { - "node": ">=14" + "node": ">=14.0.0" } }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.9.6.tgz", + "integrity": "sha512-MVNXSSYN6QXOulbHpLMKYi60ppyO13W9my1qogeiAqtjb2yR4LSmfU2+POvDkLzhjYLXz9Rf9+9a3zFHW1Lecg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.9.6.tgz", + "integrity": "sha512-T14aNLpqJ5wzKNf5jEDpv5zgyIqcpn1MlwCrUXLrwoADr2RkWA0vOWP4XxbO9aiO3dvMCQICZdKeDrFl7UMClw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.9.6.tgz", + "integrity": "sha512-CqNNAyhRkTbo8VVZ5R85X73H3R5NX9ONnKbXuHisGWC0qRbTTxnF1U4V9NafzJbgGM0sHZpdO83pLPzq8uOZFw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.9.6.tgz", + "integrity": "sha512-zRDtdJuRvA1dc9Mp6BWYqAsU5oeLixdfUvkTHuiYOHwqYuQ4YgSmi6+/lPvSsqc/I0Omw3DdICx4Tfacdzmhog==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.9.6.tgz", + "integrity": "sha512-oNk8YXDDnNyG4qlNb6is1ojTOGL/tRhbbKeE/YuccItzerEZT68Z9gHrY3ROh7axDc974+zYAPxK5SH0j/G+QQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.9.6.tgz", + "integrity": "sha512-Z3O60yxPtuCYobrtzjo0wlmvDdx2qZfeAWTyfOjEDqd08kthDKexLpV97KfAeUXPosENKd8uyJMRDfFMxcYkDQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.9.6.tgz", + "integrity": "sha512-gpiG0qQJNdYEVad+1iAsGAbgAnZ8j07FapmnIAQgODKcOTjLEWM9sRb+MbQyVsYCnA0Im6M6QIq6ax7liws6eQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.9.6.tgz", + "integrity": "sha512-+uCOcvVmFUYvVDr27aiyun9WgZk0tXe7ThuzoUTAukZJOwS5MrGbmSlNOhx1j80GdpqbOty05XqSl5w4dQvcOA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.9.6.tgz", + "integrity": "sha512-HUNqM32dGzfBKuaDUBqFB7tP6VMN74eLZ33Q9Y1TBqRDn+qDonkAUyKWwF9BR9unV7QUzffLnz9GrnKvMqC/fw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.9.6.tgz", + "integrity": "sha512-ch7M+9Tr5R4FK40FHQk8VnML0Szi2KRujUgHXd/HjuH9ifH72GUmw6lStZBo3c3GB82vHa0ZoUfjfcM7JiiMrQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.9.6.tgz", + "integrity": "sha512-VD6qnR99dhmTQ1mJhIzXsRcTBvTjbfbGGwKAHcu+52cVl15AC/kplkhxzW/uT0Xl62Y/meBKDZvoJSJN+vTeGA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.9.6.tgz", + "integrity": "sha512-J9AFDq/xiRI58eR2NIDfyVmTYGyIZmRcvcAoJ48oDld/NTR8wyiPUu2X/v1navJ+N/FGg68LEbX3Ejd6l8B7MQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.9.6.tgz", + "integrity": "sha512-jqzNLhNDvIZOrt69Ce4UjGRpXJBzhUBzawMwnaDAwyHriki3XollsewxWzOzz+4yOFDkuJHtTsZFwMxhYJWmLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@rushstack/eslint-patch": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.2.0.tgz", - "integrity": "sha512-sXo/qW2/pAcmT43VoRKOJbDOfV3cYpq3szSVfIThQXNt+E4DfKj361vaAt3c88U5tPUxzEswam7GW48PJqtKAg==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.7.2.tgz", + "integrity": "sha512-RbhOOTCNoCrbfkRyoXODZp75MlpiHMgbE5MEBZAnnnLyQNgrigEj4p0lzsMDyc1zVsJDLrivB58tgg3emX0eEA==", "dev": true }, "node_modules/@sinclair/typebox": { - "version": "0.25.24", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz", - "integrity": "sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ==", + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", "dev": true }, "node_modules/@socket.io/component-emitter": { @@ -3170,15 +3596,15 @@ "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==" }, "node_modules/@testing-library/dom": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.2.0.tgz", - "integrity": "sha512-xTEnpUKiV/bMyEsE5bT4oYA0x0Z/colMtxzUY8bKyPXBNLn/e0V4ZjBZkEhms0xE4pv9QsPfSRu9AWS4y5wGvA==", + "version": "9.3.4", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.4.tgz", + "integrity": "sha512-FlS4ZWlp97iiNWig0Muq8p+3rVDjRiYE+YKGbAqXOu9nwJFFOdL00kFpz42M+4huzYi86vAK1sOOfyOG45muIQ==", "dev": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", - "aria-query": "^5.0.0", + "aria-query": "5.1.3", "chalk": "^4.1.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", @@ -3189,25 +3615,48 @@ } }, "node_modules/@testing-library/jest-dom": { - "version": "5.16.5", - "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.16.5.tgz", - "integrity": "sha512-N5ixQ2qKpi5OLYfwQmUb/5mSV9LneAcaUfp32pn4yCnpb8r/Yz0pXFPck21dIicKmi+ta5WRAknkZCfA8refMA==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.3.0.tgz", + "integrity": "sha512-hJVIrkFizEQxoWsGBlycTcQhrpoCH4DhXfrnHFFXgkx3Xdm15zycsq5Ep+vpw4W8S0NJa8cxDHcuJib+1tEbhg==", "dev": true, "dependencies": { - "@adobe/css-tools": "^4.0.1", + "@adobe/css-tools": "^4.3.2", "@babel/runtime": "^7.9.2", - "@types/testing-library__jest-dom": "^5.9.1", "aria-query": "^5.0.0", "chalk": "^3.0.0", "css.escape": "^1.5.1", - "dom-accessibility-api": "^0.5.6", + "dom-accessibility-api": "^0.6.3", "lodash": "^4.17.15", "redent": "^3.0.0" }, "engines": { - "node": ">=8", + "node": ">=14", "npm": ">=6", "yarn": ">=1" + }, + "peerDependencies": { + "@jest/globals": ">= 28", + "@types/bun": "latest", + "@types/jest": ">= 28", + "jest": ">= 28", + "vitest": ">= 0.32" + }, + "peerDependenciesMeta": { + "@jest/globals": { + "optional": true + }, + "@types/bun": { + "optional": true + }, + "@types/jest": { + "optional": true + }, + "jest": { + "optional": true + }, + "vitest": { + "optional": true + } } }, "node_modules/@testing-library/jest-dom/node_modules/chalk": { @@ -3223,10 +3672,16 @@ "node": ">=8" } }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true + }, "node_modules/@testing-library/react": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-14.0.0.tgz", - "integrity": "sha512-S04gSNJbYE30TlIMLTzv6QCTzt9AqIF5y6s6SzVFILNcNvbV/jU96GeiTPillGQo+Ny64M/5PV7klNYYgv5Dfg==", + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-14.1.2.tgz", + "integrity": "sha512-z4p7DVBTPjKM5qDZ0t5ZjzkpSNb+fZy1u6bzO7kk8oeGagpPCAtgh4cx1syrfp7a+QWkM021jGqjJaxJJnXAZg==", "dev": true, "dependencies": { "@babel/runtime": "^7.12.5", @@ -3242,9 +3697,9 @@ } }, "node_modules/@testing-library/user-event": { - "version": "14.4.3", - "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.4.3.tgz", - "integrity": "sha512-kCUc5MEwaEMakkO5x7aoD+DLi02ehmEM2QCGWvNqAS1dV/fAvORWEjnjsEIvml59M7Y5kCkWN6fCCyPOe8OL6Q==", + "version": "14.5.2", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.5.2.tgz", + "integrity": "sha512-YAh82Wh4TIrxYLmfGcixwD18oIjyC1pFQC2Y01F2lzV2HTMiYrI0nze0FD0ocB//CKS/7jIUgae+adPqxK5yCQ==", "dev": true, "engines": { "node": ">=12", @@ -3254,127 +3709,150 @@ "@testing-library/dom": ">=7.21.4" } }, - "node_modules/@tootallnate/once": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", - "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", - "dev": true, - "engines": { - "node": ">= 10" - } - }, "node_modules/@types/aria-query": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.1.tgz", - "integrity": "sha512-XTIieEY+gvJ39ChLcB4If5zHtPxt3Syj5rgZR+e1ctpmK8NjPf0zFqsz4JpLJT0xla9GFDKjy8Cpu331nrmE1Q==", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true }, - "node_modules/@types/chai": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.4.tgz", - "integrity": "sha512-KnRanxnpfpjUTqTCXslZSEdLfXExwgNxYPdiO2WGUj8+HDjFi8R3k5RVKPeSCzLjCcshCAtVO2QBbVuAV4kTnw==", - "dev": true - }, - "node_modules/@types/chai-subset": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@types/chai-subset/-/chai-subset-1.3.3.tgz", - "integrity": "sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw==", + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", "dev": true, "dependencies": { - "@types/chai": "*" + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.5.tgz", + "integrity": "sha512-WXCyOcRtH37HAUkpXhUduaxdm82b4GSlyTqajXviN4EfiuPgNYR109xMCKvpl6zPIpua0DGlMEDCq+g8EdoheQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.20.7" } }, "node_modules/@types/d3-array": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.0.4.tgz", - "integrity": "sha512-nwvEkG9vYOc0Ic7G7kwgviY4AQlTfYGIZ0fqB7CQHXGyYM6nO7kJh5EguSNA3jfh4rq7Sb7eMVq8isuvg2/miQ==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", + "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==", "dev": true }, "node_modules/@types/d3-color": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.0.tgz", - "integrity": "sha512-HKuicPHJuvPgCD+np6Se9MQvS6OCbJmOjGvylzMJRlDwUXjKTTXs6Pwgk79O09Vj/ho3u1ofXnhFOaEWWPrlwA==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", "dev": true }, "node_modules/@types/d3-ease": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.0.tgz", - "integrity": "sha512-aMo4eaAOijJjA6uU+GIeW018dvy9+oH5Y2VPPzjjfxevvGQ/oRDs+tfYC9b50Q4BygRR8yE2QCLsrT0WtAVseA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", "dev": true }, "node_modules/@types/d3-interpolate": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.1.tgz", - "integrity": "sha512-jx5leotSeac3jr0RePOH1KdR9rISG91QIE4Q2PYTu4OymLTZfA3SrnURSLzKH48HmXVUru50b8nje4E79oQSQw==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", "dev": true, "dependencies": { "@types/d3-color": "*" } }, "node_modules/@types/d3-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.0.0.tgz", - "integrity": "sha512-0g/A+mZXgFkQxN3HniRDbXMN79K3CdTpLsevj+PXiTcb2hVyvkZUBg37StmgCQkaD84cUJ4uaDAWq7UJOQy2Tg==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.0.2.tgz", + "integrity": "sha512-WAIEVlOCdd/NKRYTsqCpOMHQHemKBEINf8YXMYOtXH0GA7SY0dqMB78P3Uhgfy+4X+/Mlw2wDtlETkN6kQUCMA==", "dev": true }, "node_modules/@types/d3-scale": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.3.tgz", - "integrity": "sha512-PATBiMCpvHJSMtZAMEhc2WyL+hnzarKzI6wAHYjhsonjWJYGq5BXTzQjv4l8m2jO183/4wZ90rKvSeT7o72xNQ==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.8.tgz", + "integrity": "sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==", "dev": true, "dependencies": { "@types/d3-time": "*" } }, "node_modules/@types/d3-shape": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.1.tgz", - "integrity": "sha512-6Uh86YFF7LGg4PQkuO2oG6EMBRLuW9cbavUW46zkIO5kuS2PfTqo2o9SkgtQzguBHbLgNnU90UNsITpsX1My+A==", + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.6.tgz", + "integrity": "sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA==", "dev": true, "dependencies": { "@types/d3-path": "*" } }, "node_modules/@types/d3-time": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.0.tgz", - "integrity": "sha512-sZLCdHvBUcNby1cB6Fd3ZBrABbjz3v1Vm90nysCQ6Vt7vd6e/h9Lt7SiJUoEX0l4Dzc7P5llKyhqSi1ycSf1Hg==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.3.tgz", + "integrity": "sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw==", "dev": true }, "node_modules/@types/d3-timer": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.0.tgz", - "integrity": "sha512-HNB/9GHqu7Fo8AQiugyJbv6ZxYz58wef0esl4Mv828w1ZKpAshw/uFWVDUcIB9KKFeFKoxS3cHY07FFgtTRZ1g==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "dev": true + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", "dev": true }, "node_modules/@types/istanbul-lib-coverage": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", - "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==", + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", "dev": true }, "node_modules/@types/istanbul-lib-report": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", - "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", "dev": true, "dependencies": { "@types/istanbul-lib-coverage": "*" } }, "node_modules/@types/istanbul-reports": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz", - "integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", "dev": true, "dependencies": { "@types/istanbul-lib-report": "*" } }, "node_modules/@types/jest": { - "version": "29.5.1", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.1.tgz", - "integrity": "sha512-tEuVcHrpaixS36w7hpsfLBLpjtMRJUE09/MHXn923LOVojDwyC14cWcfc0rDs0VEfUyYmt/+iX1kxxp+gZMcaQ==", + "version": "29.5.11", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.11.tgz", + "integrity": "sha512-S2mHmYIVe13vrm6q4kN6fLYYAka15ALQki/vgDC3mIukEOx8WJlv0kQPM+d4w8Gp6u0uSdKND04IlTXBv0rwnQ==", "dev": true, "dependencies": { "expect": "^29.0.0", @@ -3394,12 +3872,12 @@ } }, "node_modules/@types/jest/node_modules/pretty-format": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.5.0.tgz", - "integrity": "sha512-V2mGkI31qdttvTFX7Mt4efOqHXqJWMu4/r66Xh3Z3BwZaPfPJgp6/gbwoujRpPUtfEF6AUUWx3Jim3GCw5g/Qw==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", "dev": true, "dependencies": { - "@jest/schemas": "^29.4.3", + "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" }, @@ -3414,9 +3892,9 @@ "dev": true }, "node_modules/@types/json-schema": { - "version": "7.0.11", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", - "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, "node_modules/@types/json5": { @@ -3426,38 +3904,35 @@ "dev": true }, "node_modules/@types/lodash": { - "version": "4.14.194", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.194.tgz", - "integrity": "sha512-r22s9tAS7imvBt2lyHC9B8AGwWnXaYb1tY09oyLkXDs4vArpYJzw09nj8MLx5VfciBPGIb+ZwG0ssYnEPJxn/g==", - "dev": true - }, - "node_modules/@types/minimatch": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", - "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", + "version": "4.14.202", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.202.tgz", + "integrity": "sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==", "dev": true }, "node_modules/@types/node": { - "version": "18.16.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.1.tgz", - "integrity": "sha512-DZxSZWXxFfOlx7k7Rv4LAyiMroaxa3Ly/7OOzZO8cBNho0YzAi4qlbrx8W27JGqG57IgR/6J7r+nOJWw6kcvZA==", - "dev": true + "version": "20.11.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.7.tgz", + "integrity": "sha512-GPmeN1C3XAyV5uybAf4cMLWT9fDWcmQhZVtMFu7OR32WjrqGG+Wnk2V1d0bmtUyE/Zy1QJ9BxyiTih9z8Oks8A==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } }, "node_modules/@types/parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==" + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==" }, "node_modules/@types/prop-types": { - "version": "15.7.5", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", - "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==", + "version": "15.7.11", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", + "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==", "devOptional": true }, "node_modules/@types/react": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.0.tgz", - "integrity": "sha512-0FLj93y5USLHdnhIhABk83rm8XEGA7kH3cr+YUlvxoUGp1xNt/DINUMvqPxLyOQMzLmZe8i4RTHbvb8MC7NmrA==", + "version": "18.2.48", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.48.tgz", + "integrity": "sha512-qboRCl6Ie70DQQG9hhNREz81jqC1cs9EVNcjQ1AU+jH6NFfSAhVVbrrY/+nSF+Bsk4AOwm9Qa61InvMCyV+H3w==", "devOptional": true, "dependencies": { "@types/prop-types": "*", @@ -3466,77 +3941,68 @@ } }, "node_modules/@types/react-dom": { - "version": "18.2.1", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.1.tgz", - "integrity": "sha512-8QZEV9+Kwy7tXFmjJrp3XUKQSs9LTnE0KnoUb0YCguWBiNW0Yfb2iBMYZ08WPg35IR6P3Z0s00B15SwZnO26+w==", + "version": "18.2.18", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.18.tgz", + "integrity": "sha512-TJxDm6OfAX2KJWJdMEVTwWke5Sc/E/RlnPGvGfS0W7+6ocy2xhDVQVh/KvC2Uf7kACs+gDytdusDSdWfWkaNzw==", "dev": true, "dependencies": { "@types/react": "*" } }, "node_modules/@types/react-table": { - "version": "7.7.14", - "resolved": "https://registry.npmjs.org/@types/react-table/-/react-table-7.7.14.tgz", - "integrity": "sha512-TYrv7onCiakaG1uAu/UpQ9FojNEt/4/ht87EgJQaEGFoWV606ZLWUZAcUHzMxgc3v1mywP1cDyz3qB4ho3hWOw==", + "version": "7.7.19", + "resolved": "https://registry.npmjs.org/@types/react-table/-/react-table-7.7.19.tgz", + "integrity": "sha512-47jMa1Pai7ily6BXJCW33IL5ghqmCWs2VM9s+h1D4mCaK5P4uNkZOW3RMMg8MCXBvAJ0v9+sPqKjhid0PaJPQA==", "dev": true, "dependencies": { "@types/react": "*" } }, "node_modules/@types/scheduler": { - "version": "0.16.3", - "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", - "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==", + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", + "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==", "devOptional": true }, "node_modules/@types/semver": { - "version": "7.3.13", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz", - "integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==", + "version": "7.5.6", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz", + "integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==", "dev": true }, "node_modules/@types/stack-utils": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", - "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", "dev": true }, - "node_modules/@types/testing-library__jest-dom": { - "version": "5.14.5", - "resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.5.tgz", - "integrity": "sha512-SBwbxYoyPIvxHbeHxTZX2Pe/74F/tX2/D3mMvzabdeJ25bBojfW0TyB8BHrbq/9zaaKICJZjLP+8r6AeZMFCuQ==", - "dev": true, - "dependencies": { - "@types/jest": "*" - } - }, "node_modules/@types/yargs": { - "version": "17.0.24", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz", - "integrity": "sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw==", + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", + "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", "dev": true, "dependencies": { "@types/yargs-parser": "*" } }, "node_modules/@types/yargs-parser": { - "version": "21.0.0", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz", - "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==", + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "5.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.1.tgz", - "integrity": "sha512-AVi0uazY5quFB9hlp2Xv+ogpfpk77xzsgsIEWyVS7uK/c7MZ5tw7ZPbapa0SbfkqE0fsAMkz5UwtgMLVk2BQAg==", + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", + "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.59.1", - "@typescript-eslint/type-utils": "5.59.1", - "@typescript-eslint/utils": "5.59.1", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/type-utils": "5.62.0", + "@typescript-eslint/utils": "5.62.0", "debug": "^4.3.4", - "grapheme-splitter": "^1.0.4", + "graphemer": "^1.4.0", "ignore": "^5.2.0", "natural-compare-lite": "^1.4.0", "semver": "^7.3.7", @@ -3572,9 +4038,9 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz", - "integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -3593,12 +4059,12 @@ "dev": true }, "node_modules/@typescript-eslint/experimental-utils": { - "version": "5.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-5.59.1.tgz", - "integrity": "sha512-KVtKcHEizCIRx//LC9tBi6xp94ULKbU5StVHBVWURJQOVa2qw6HP28Hu7LmHrQM3p9I3q5Y2VR4wKllCJ3IWrw==", + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-5.62.0.tgz", + "integrity": "sha512-RTXpeB3eMkpoclG3ZHft6vG/Z30azNHuqY6wKPBHlVMZFuEvrtlEDe8gMqDb+SO+9hjC/pLekeSCryf9vMZlCw==", "dev": true, "dependencies": { - "@typescript-eslint/utils": "5.59.1" + "@typescript-eslint/utils": "5.62.0" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -3612,14 +4078,14 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "5.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.59.1.tgz", - "integrity": "sha512-nzjFAN8WEu6yPRDizIFyzAfgK7nybPodMNFGNH0M9tei2gYnYszRDqVA0xlnRjkl7Hkx2vYrEdb6fP2a21cG1g==", + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", + "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "5.59.1", - "@typescript-eslint/types": "5.59.1", - "@typescript-eslint/typescript-estree": "5.59.1", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", "debug": "^4.3.4" }, "engines": { @@ -3639,13 +4105,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "5.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.1.tgz", - "integrity": "sha512-mau0waO5frJctPuAzcxiNWqJR5Z8V0190FTSqRw1Q4Euop6+zTwHAf8YIXNwDOT29tyUDrQ65jSg9aTU/H0omA==", + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", + "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.59.1", - "@typescript-eslint/visitor-keys": "5.59.1" + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -3656,13 +4122,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "5.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.59.1.tgz", - "integrity": "sha512-ZMWQ+Oh82jWqWzvM3xU+9y5U7MEMVv6GLioM3R5NJk6uvP47kZ7YvlgSHJ7ERD6bOY7Q4uxWm25c76HKEwIjZw==", + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz", + "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "5.59.1", - "@typescript-eslint/utils": "5.59.1", + "@typescript-eslint/typescript-estree": "5.62.0", + "@typescript-eslint/utils": "5.62.0", "debug": "^4.3.4", "tsutils": "^3.21.0" }, @@ -3683,9 +4149,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "5.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.1.tgz", - "integrity": "sha512-dg0ICB+RZwHlysIy/Dh1SP+gnXNzwd/KS0JprD3Lmgmdq+dJAJnUPe1gNG34p0U19HvRlGX733d/KqscrGC1Pg==", + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", + "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -3696,13 +4162,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "5.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.1.tgz", - "integrity": "sha512-lYLBBOCsFltFy7XVqzX0Ju+Lh3WPIAWxYpmH/Q7ZoqzbscLiCW00LeYCdsUnnfnj29/s1WovXKh2gwCoinHNGA==", + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", + "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.59.1", - "@typescript-eslint/visitor-keys": "5.59.1", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -3735,9 +4201,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz", - "integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -3756,17 +4222,17 @@ "dev": true }, "node_modules/@typescript-eslint/utils": { - "version": "5.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.59.1.tgz", - "integrity": "sha512-MkTe7FE+K1/GxZkP5gRj3rCztg45bEhsd8HYjczBuYm+qFHP5vtZmjx3B0yUCDotceQ4sHgTyz60Ycl225njmA==", + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", + "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@types/json-schema": "^7.0.9", "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.59.1", - "@typescript-eslint/types": "5.59.1", - "@typescript-eslint/typescript-estree": "5.59.1", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", "eslint-scope": "^5.1.1", "semver": "^7.3.7" }, @@ -3816,9 +4282,9 @@ } }, "node_modules/@typescript-eslint/utils/node_modules/semver": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz", - "integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -3837,12 +4303,12 @@ "dev": true }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "5.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.1.tgz", - "integrity": "sha512-6waEYwBTCWryx0VJmP7JaM4FpipLsFl9CvYf2foAE8Qh/Y0s+bxWysciwOs0LTBED4JCaNxTZ5rGadB14M6dwA==", + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", + "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.59.1", + "@typescript-eslint/types": "5.62.0", "eslint-visitor-keys": "^3.3.0" }, "engines": { @@ -3853,74 +4319,96 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, "node_modules/@vitejs/plugin-react": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.0.0.tgz", - "integrity": "sha512-HX0XzMjL3hhOYm+0s95pb0Z7F8O81G7joUHgfDd/9J/ZZf5k4xX6QAMFkKsHFxaHlf6X7GD7+XuaZ66ULiJuhQ==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.2.1.tgz", + "integrity": "sha512-oojO9IDc4nCUUi8qIR11KoQm0XFFLIwsRBwHRR4d/88IWghn1y6ckz/bJ8GHDCsYEJee8mDzqtJxh15/cisJNQ==", "dev": true, "dependencies": { - "@babel/core": "^7.21.4", - "@babel/plugin-transform-react-jsx-self": "^7.21.0", - "@babel/plugin-transform-react-jsx-source": "^7.19.6", + "@babel/core": "^7.23.5", + "@babel/plugin-transform-react-jsx-self": "^7.23.3", + "@babel/plugin-transform-react-jsx-source": "^7.23.3", + "@types/babel__core": "^7.20.5", "react-refresh": "^0.14.0" }, "engines": { "node": "^14.18.0 || >=16.0.0" }, "peerDependencies": { - "vite": "^4.2.0" + "vite": "^4.2.0 || ^5.0.0" } }, - "node_modules/@vitest/coverage-c8": { - "version": "0.30.1", - "resolved": "https://registry.npmjs.org/@vitest/coverage-c8/-/coverage-c8-0.30.1.tgz", - "integrity": "sha512-/Wa3dtSuckpdngAmiCwowaEXXgJkqPrtfvrs9HTB9QoEfNbZWPu4E4cjEn4lJZb4qcGf4fxFtUA2f9DnDNAzBA==", + "node_modules/@vitest/coverage-v8": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.2.2.tgz", + "integrity": "sha512-IHyKnDz18SFclIEEAHb9Y4Uxx0sPKC2VO1kdDCs1BF6Ip4S8rQprs971zIsooLUn7Afs71GRxWMWpkCGZpRMhw==", "dev": true, "dependencies": { - "c8": "^7.13.0", + "@ampproject/remapping": "^2.2.1", + "@bcoe/v8-coverage": "^0.2.3", + "debug": "^4.3.4", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^4.0.1", + "istanbul-reports": "^3.1.6", + "magic-string": "^0.30.5", + "magicast": "^0.3.3", "picocolors": "^1.0.0", - "std-env": "^3.3.2" + "std-env": "^3.5.0", + "test-exclude": "^6.0.0", + "v8-to-istanbul": "^9.2.0" }, "funding": { - "url": "https://github.com/sponsors/antfu" + "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "vitest": ">=0.30.0 <1" + "vitest": "^1.0.0" } }, "node_modules/@vitest/expect": { - "version": "0.30.1", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.30.1.tgz", - "integrity": "sha512-c3kbEtN8XXJSeN81iDGq29bUzSjQhjES2WR3aColsS4lPGbivwLtas4DNUe0jD9gg/FYGIteqOenfU95EFituw==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.2.2.tgz", + "integrity": "sha512-3jpcdPAD7LwHUUiT2pZTj2U82I2Tcgg2oVPvKxhn6mDI2On6tfvPQTjAI4628GUGDZrCm4Zna9iQHm5cEexOAg==", "dev": true, "dependencies": { - "@vitest/spy": "0.30.1", - "@vitest/utils": "0.30.1", - "chai": "^4.3.7" + "@vitest/spy": "1.2.2", + "@vitest/utils": "1.2.2", + "chai": "^4.3.10" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/runner": { - "version": "0.30.1", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-0.30.1.tgz", - "integrity": "sha512-W62kT/8i0TF1UBCNMRtRMOBWJKRnNyv9RrjIgdUryEe0wNpGZvvwPDLuzYdxvgSckzjp54DSpv1xUbv4BQ0qVA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.2.2.tgz", + "integrity": "sha512-JctG7QZ4LSDXr5CsUweFgcpEvrcxOV1Gft7uHrvkQ+fsAVylmWQvnaAr/HDp3LAH1fztGMQZugIheTWjaGzYIg==", "dev": true, "dependencies": { - "@vitest/utils": "0.30.1", - "concordance": "^5.0.4", - "p-limit": "^4.0.0", - "pathe": "^1.1.0" + "@vitest/utils": "1.2.2", + "p-limit": "^5.0.0", + "pathe": "^1.1.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/runner/node_modules/p-limit": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", - "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz", + "integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==", "dev": true, "dependencies": { "yocto-queue": "^1.0.0" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -3939,61 +4427,135 @@ } }, "node_modules/@vitest/snapshot": { - "version": "0.30.1", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-0.30.1.tgz", - "integrity": "sha512-fJZqKrE99zo27uoZA/azgWyWbFvM1rw2APS05yB0JaLwUIg9aUtvvnBf4q7JWhEcAHmSwbrxKFgyBUga6tq9Tw==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.2.2.tgz", + "integrity": "sha512-SmGY4saEw1+bwE1th6S/cZmPxz/Q4JWsl7LvbQIky2tKE35US4gd0Mjzqfr84/4OD0tikGWaWdMja/nWL5NIPA==", "dev": true, "dependencies": { - "magic-string": "^0.30.0", - "pathe": "^1.1.0", - "pretty-format": "^27.5.1" + "magic-string": "^0.30.5", + "pathe": "^1.1.1", + "pretty-format": "^29.7.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/spy": { - "version": "0.30.1", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-0.30.1.tgz", - "integrity": "sha512-YfJeIf37GvTZe04ZKxzJfnNNuNSmTEGnla2OdL60C8od16f3zOfv9q9K0nNii0NfjDJRt/CVN/POuY5/zTS+BA==", + "node_modules/@vitest/snapshot/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@vitest/snapshot/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", "dev": true, "dependencies": { - "tinyspy": "^2.1.0" + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@vitest/snapshot/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, + "node_modules/@vitest/spy": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.2.2.tgz", + "integrity": "sha512-k9Gcahssw8d7X3pSLq3e3XEu/0L78mUkCjivUqCQeXJm9clfXR/Td8+AP+VC1O6fKPIDLcHDTAmBOINVuv6+7g==", + "dev": true, + "dependencies": { + "tinyspy": "^2.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/ui": { - "version": "0.30.1", - "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-0.30.1.tgz", - "integrity": "sha512-Izz4ElDmdvX02KImSC2nCJI6CsGo9aETbKqxli55M0rbbPPAMtF0zDcJIqgEP5V6Y+4Ysf6wvsjLbLCTnaBvKw==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-1.2.2.tgz", + "integrity": "sha512-CG+5fa8lyoBr+9i+UZGS31Qw81v33QlD10uecHxN2CLJVN+jLnqx4pGzGvFFeJ7jSnUCT0AlbmVWY6fU6NJZmw==", "dev": true, "dependencies": { - "@vitest/utils": "0.30.1", - "fast-glob": "^3.2.12", - "fflate": "^0.7.4", - "flatted": "^3.2.7", - "pathe": "^1.1.0", + "@vitest/utils": "1.2.2", + "fast-glob": "^3.3.2", + "fflate": "^0.8.1", + "flatted": "^3.2.9", + "pathe": "^1.1.1", "picocolors": "^1.0.0", - "sirv": "^2.0.2" + "sirv": "^2.0.4" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "vitest": "^1.0.0" } }, "node_modules/@vitest/utils": { - "version": "0.30.1", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.30.1.tgz", - "integrity": "sha512-/c8Xv2zUVc+rnNt84QF0Y0zkfxnaGhp87K2dYJMLtLOIckPzuxLVzAtFCicGFdB4NeBHNzTRr1tNn7rCtQcWFA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.2.2.tgz", + "integrity": "sha512-WKITBHLsBHlpjnDQahr+XK6RE7MiAsgrIkr0pGhQ9ygoxBfUeG0lUG5iLlzqjmKSlBv3+j5EGsriBzh+C3Tq9g==", "dev": true, "dependencies": { - "concordance": "^5.0.4", - "loupe": "^2.3.6", - "pretty-format": "^27.5.1" + "diff-sequences": "^29.6.3", + "estree-walker": "^3.0.3", + "loupe": "^2.3.7", + "pretty-format": "^29.7.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/abab": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", - "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", + "node_modules/@vitest/utils/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@vitest/utils/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@vitest/utils/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", "dev": true }, "node_modules/acorn": { - "version": "8.8.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", - "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -4002,16 +4564,6 @@ "node": ">=0.4.0" } }, - "node_modules/acorn-globals": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz", - "integrity": "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==", - "dev": true, - "dependencies": { - "acorn": "^8.1.0", - "acorn-walk": "^8.0.2" - } - }, "node_modules/acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", @@ -4022,24 +4574,24 @@ } }, "node_modules/acorn-walk": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", - "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", + "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", "dev": true, "engines": { "node": ">=0.4.0" } }, "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", + "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", "dev": true, "dependencies": { - "debug": "4" + "debug": "^4.3.4" }, "engines": { - "node": ">= 6.0.0" + "node": ">= 14" } }, "node_modules/ajv": { @@ -4149,25 +4701,16 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array-differ": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-3.0.0.tgz", - "integrity": "sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/array-includes": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.6.tgz", - "integrity": "sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==", + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.7.tgz", + "integrity": "sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", - "get-intrinsic": "^1.1.3", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", "is-string": "^1.0.7" }, "engines": { @@ -4186,15 +4729,34 @@ "node": ">=8" } }, - "node_modules/array.prototype.flat": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.1.tgz", - "integrity": "sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA==", + "node_modules/array.prototype.findlastindex": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.3.tgz", + "integrity": "sha512-LzLoiOMAxvy+Gd3BAq3B7VeIgPdo+Q8hthvKtXybMvRV0jrXfJM/t8mw7nNlpEcVlVUnCnM2KSX4XU5HmpodOA==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0", + "get-intrinsic": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", + "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", "es-shim-unscopables": "^1.0.0" }, "engines": { @@ -4205,14 +4767,14 @@ } }, "node_modules/array.prototype.flatmap": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.1.tgz", - "integrity": "sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", + "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", "es-shim-unscopables": "^1.0.0" }, "engines": { @@ -4223,25 +4785,37 @@ } }, "node_modules/array.prototype.tosorted": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.1.tgz", - "integrity": "sha512-pZYPXPRl2PqWcsUs6LOMn+1f1532nEoPTYowBtqLwAW+W8vSVhkIGnmOX1t/UQjD6YGI0vcD2B1U7ZFGQH9jnQ==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.2.tgz", + "integrity": "sha512-HuQCHOlk1Weat5jzStICBCd83NxiIMwqDg/dHEsoefabn/hJRj5pVdWcPUSpRrwhwxZOsQassMpgN/xRYFBMIg==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", "es-shim-unscopables": "^1.0.0", - "get-intrinsic": "^1.1.3" + "get-intrinsic": "^1.2.1" } }, - "node_modules/arrify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", - "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz", + "integrity": "sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==", "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", + "is-array-buffer": "^3.0.2", + "is-shared-array-buffer": "^1.0.2" + }, "engines": { - "node": ">=8" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/assertion-error": { @@ -4254,11 +4828,20 @@ } }, "node_modules/ast-types-flow": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz", - "integrity": "sha512-eBvWn1lvIApYMhzQMsu9ciLfkBY499mFZlNqG+/9WR7PVlroQw0vG30cOQQbaKz3sCEc44TAOu2ykzqXSNnwag==", + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", + "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", "dev": true }, + "node_modules/asynciterator.prototype": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/asynciterator.prototype/-/asynciterator.prototype-1.0.0.tgz", + "integrity": "sha512-wwHYEIS0Q80f5mosx3L/dfG5t5rjEa9Ft51GTaNt862EnpyGHpgz2RkZvLPp1oF5TnAiTohkEKVEu8pQPJI7Vg==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.3" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -4294,21 +4877,22 @@ } }, "node_modules/axios": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", - "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz", + "integrity": "sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==", "dependencies": { - "follow-redirects": "^1.14.9", - "form-data": "^4.0.0" + "follow-redirects": "^1.15.4", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" } }, "node_modules/axobject-query": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.1.1.tgz", - "integrity": "sha512-goKlv8DZrK9hUh975fnHzhNIO4jUnFCfv/dszV5VwUGDFjI6vQ2VwoyjYjYNEbBE8AH87TduWP5uyDR1D+Iteg==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz", + "integrity": "sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==", "dev": true, "dependencies": { - "deep-equal": "^2.0.5" + "dequal": "^2.0.3" } }, "node_modules/babel-plugin-macros": { @@ -4326,42 +4910,42 @@ } }, "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.3.tgz", - "integrity": "sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q==", + "version": "0.4.8", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.8.tgz", + "integrity": "sha512-OtIuQfafSzpo/LhnJaykc0R/MMnuLSSVjVYy9mHArIZ9qTCSZ6TpWCuEKZYVoN//t8HqBNScHrOtCrIK5IaGLg==", "dev": true, "dependencies": { - "@babel/compat-data": "^7.17.7", - "@babel/helper-define-polyfill-provider": "^0.3.3", - "semver": "^6.1.1" + "@babel/compat-data": "^7.22.6", + "@babel/helper-define-polyfill-provider": "^0.5.0", + "semver": "^6.3.1" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.6.0.tgz", - "integrity": "sha512-+eHqR6OPcBhJOGgsIar7xoAB1GcSwVUA3XjAd7HJNzOXT4wv6/H7KIdA/Nc60cvUlDbKApmqNvD1B1bzOt4nyA==", + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.9.0.tgz", + "integrity": "sha512-7nZPG1uzK2Ymhy/NbaOWTg3uibM2BmGASS4vHS4szRZAIR8R6GwA/xAujpdrXU5iyklrimWnLWU+BLF9suPTqg==", "dev": true, "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.3.3", - "core-js-compat": "^3.25.1" + "@babel/helper-define-polyfill-provider": "^0.5.0", + "core-js-compat": "^3.34.0" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.4.1.tgz", - "integrity": "sha512-NtQGmyQDXjQqQ+IzRkBVwEOz9lQ4zxAQZgoAYEtU9dJjnl1Oc98qnN7jcp+bE7O7aYzVpavXE3/VKXNzUbh7aw==", + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.5.tgz", + "integrity": "sha512-OJGYZlhLqBh2DDHeqAxWB1XIvr49CxiJ2gIt61/PU55CQK4Z58OzMqjDe1zwQdQk+rBYsRc+1rJmdajM3gimHg==", "dev": true, "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.3.3" + "@babel/helper-define-polyfill-provider": "^0.5.0" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "node_modules/babel-plugin-transform-react-remove-prop-types": { @@ -4400,9 +4984,9 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, "node_modules/big-integer": { - "version": "1.6.51", - "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", - "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==", + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", "engines": { "node": ">=0.6" } @@ -4416,12 +5000,6 @@ "node": ">=8" } }, - "node_modules/blueimp-md5": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/blueimp-md5/-/blueimp-md5-2.19.0.tgz", - "integrity": "sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w==", - "dev": true - }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -4459,9 +5037,9 @@ } }, "node_modules/browserslist": { - "version": "4.21.5", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.5.tgz", - "integrity": "sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w==", + "version": "4.22.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.3.tgz", + "integrity": "sha512-UAp55yfwNv0klWNapjs/ktHoguxuQNGnOzxYmfnXIS+8AsRDZkSDxg7R1AX3GKzn078SBI5dzwzj/Yx0Or0e3A==", "dev": true, "funding": [ { @@ -4471,13 +5049,17 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], "dependencies": { - "caniuse-lite": "^1.0.30001449", - "electron-to-chromium": "^1.4.284", - "node-releases": "^2.0.8", - "update-browserslist-db": "^1.0.10" + "caniuse-lite": "^1.0.30001580", + "electron-to-chromium": "^1.4.648", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.13" }, "bin": { "browserslist": "cli.js" @@ -4486,32 +5068,6 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/c8": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/c8/-/c8-7.13.0.tgz", - "integrity": "sha512-/NL4hQTv1gBL6J6ei80zu3IiTrmePDKXKXOTLpHvcIWZTVYQlDhVWjjWvkhICylE8EwwnMVzDZugCvdx0/DIIA==", - "dev": true, - "dependencies": { - "@bcoe/v8-coverage": "^0.2.3", - "@istanbuljs/schema": "^0.1.3", - "find-up": "^5.0.0", - "foreground-child": "^2.0.0", - "istanbul-lib-coverage": "^3.2.0", - "istanbul-lib-report": "^3.0.0", - "istanbul-reports": "^3.1.4", - "rimraf": "^3.0.2", - "test-exclude": "^6.0.0", - "v8-to-istanbul": "^9.0.0", - "yargs": "^16.2.0", - "yargs-parser": "^20.2.9" - }, - "bin": { - "c8": "bin/c8.js" - }, - "engines": { - "node": ">=10.12.0" - } - }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -4522,13 +5078,14 @@ } }, "node_modules/call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", "dev": true, "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -4543,9 +5100,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001481", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001481.tgz", - "integrity": "sha512-KCqHwRnaa1InZBtqXzP98LPg0ajCVujMKjqKDhZEthIpAsJl/YEIa3YvXjGXPVqzZVguccuu7ga9KOE1J9rKPQ==", + "version": "1.0.30001580", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001580.tgz", + "integrity": "sha512-mtj5ur2FFPZcCEpXFy8ADXbDACuNFXg6mxVDqp7tqooX6l3zwm+d8EPoeOSIFRDvHs8qu7/SLFOGniULkcH2iA==", "dev": true, "funding": [ { @@ -4563,18 +5120,18 @@ ] }, "node_modules/chai": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.7.tgz", - "integrity": "sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.4.1.tgz", + "integrity": "sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==", "dev": true, "dependencies": { "assertion-error": "^1.1.0", - "check-error": "^1.0.2", - "deep-eql": "^4.1.2", - "get-func-name": "^2.0.0", - "loupe": "^2.3.1", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", "pathval": "^1.1.1", - "type-detect": "^4.0.5" + "type-detect": "^4.0.8" }, "engines": { "node": ">=4" @@ -4597,10 +5154,13 @@ } }, "node_modules/check-error": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", - "integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", "dev": true, + "dependencies": { + "get-func-name": "^2.0.2" + }, "engines": { "node": "*" } @@ -4645,9 +5205,9 @@ } }, "node_modules/ci-info": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz", - "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==", + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", "dev": true, "funding": [ { @@ -4659,27 +5219,10 @@ "node": ">=8" } }, - "node_modules/classnames": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz", - "integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==", - "dev": true - }, - "node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dev": true, - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, "node_modules/clsx": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", - "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz", + "integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==", "dev": true, "engines": { "node": ">=6" @@ -4728,58 +5271,6 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, - "node_modules/concordance": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/concordance/-/concordance-5.0.4.tgz", - "integrity": "sha512-OAcsnTEYu1ARJqWVGwf4zh4JDfHZEaSNlNccFmt8YjB2l/n19/PF2viLINHc57vO4FKIAFl2FWASIGZZWZ2Kxw==", - "dev": true, - "dependencies": { - "date-time": "^3.1.0", - "esutils": "^2.0.3", - "fast-diff": "^1.2.0", - "js-string-escape": "^1.0.1", - "lodash": "^4.17.15", - "md5-hex": "^3.0.1", - "semver": "^7.3.2", - "well-known-symbols": "^2.0.0" - }, - "engines": { - "node": ">=10.18.0 <11 || >=12.14.0 <13 || >=14" - } - }, - "node_modules/concordance/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/concordance/node_modules/semver": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz", - "integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/concordance/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/confusing-browser-globals": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz", @@ -4789,15 +5280,16 @@ "node_modules/convert-source-map": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "peer": true }, "node_modules/core-js-compat": { - "version": "3.30.1", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.30.1.tgz", - "integrity": "sha512-d690npR7MC6P0gq4npTl5n2VQeNAmUrJ90n+MHiKS7W2+xno4o3F5GDEuylSdi6EJ3VssibSGXOa1r3YXD3Mhw==", + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.35.1.tgz", + "integrity": "sha512-sftHa5qUJY3rs9Zht1WEnmkvXputCyDBczPnr7QDgL8n3qrF3CMXY4VPSYtOLLiOUJcah2WNXREd48iOl6mQIw==", "dev": true, "dependencies": { - "browserslist": "^4.21.5" + "browserslist": "^4.22.2" }, "funding": { "type": "opencollective", @@ -4841,12 +5333,6 @@ "node": ">= 8" } }, - "node_modules/css-unit-converter": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/css-unit-converter/-/css-unit-converter-1.1.2.tgz", - "integrity": "sha512-IiJwMC8rdZE0+xiEZHeru6YoONC4rfPMqGm2W85jMIbkFvv5nFTwJVFHam2eFrN6txmoUYFAFXiv8ICVeTO0MA==", - "dev": true - }, "node_modules/css.escape": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", @@ -4854,26 +5340,26 @@ "dev": true }, "node_modules/cssstyle": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-3.0.0.tgz", - "integrity": "sha512-N4u2ABATi3Qplzf0hWbVCdjenim8F3ojEXpBDF5hBpjzW182MjNGLqfmQ0SkSPeQ+V86ZXgeH8aXj6kayd4jgg==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.0.1.tgz", + "integrity": "sha512-8ZYiJ3A/3OkDd093CBT/0UKDWry7ak4BdPTFP2+QEP7cmhouyq/Up709ASSj2cK02BbZiMgk7kYjZNS4QP5qrQ==", "dev": true, "dependencies": { "rrweb-cssom": "^0.6.0" }, "engines": { - "node": ">=14" + "node": ">=18" } }, "node_modules/csstype": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", - "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, "node_modules/d3-array": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.3.tgz", - "integrity": "sha512-JRHwbQQ84XuAESWhvIPaUV4/1UYTBOLiOPGWqgFDHZS1D5QN9c57FbH3QpEnQMYiOXNzKUQyGTZf+EVO7RT5TQ==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", "dev": true, "dependencies": { "internmap": "1 - 2" @@ -4998,29 +5484,16 @@ "dev": true }, "node_modules/data-urls": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-4.0.0.tgz", - "integrity": "sha512-/mMTei/JXPqvFqQtfyTowxmJVwr2PVAeCcDxyFf6LhoOu/09TX2OX3kb2wzi4DMXcfj4OItwDOnhl5oziPnT6g==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", "dev": true, "dependencies": { - "abab": "^2.0.6", - "whatwg-mimetype": "^3.0.0", - "whatwg-url": "^12.0.0" + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" }, "engines": { - "node": ">=14" - } - }, - "node_modules/date-time": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/date-time/-/date-time-3.1.0.tgz", - "integrity": "sha512-uqCUKXE5q1PNBXjPqvwhwJf9SwMoAHBgWJ6DcrnS5o+W2JOiIILl0JEdVD8SGujrNS02GGxgwAg2PN2zONgtjg==", - "dev": true, - "dependencies": { - "time-zone": "^1.0.0" - }, - "engines": { - "node": ">=6" + "node": ">=18" } }, "node_modules/debug": { @@ -5064,16 +5537,17 @@ } }, "node_modules/deep-equal": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.0.tgz", - "integrity": "sha512-RdpzE0Hv4lhowpIUKKMJfeH6C1pXdtT1/it80ubgWqwI3qpuxUBpC1S4hnHg+zjnuOoDkzUtUCEEkG+XG5l3Mw==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz", + "integrity": "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "es-get-iterator": "^1.1.2", - "get-intrinsic": "^1.1.3", + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.5", + "es-get-iterator": "^1.1.3", + "get-intrinsic": "^1.2.2", "is-arguments": "^1.1.1", - "is-array-buffer": "^3.0.1", + "is-array-buffer": "^3.0.2", "is-date-object": "^1.0.5", "is-regex": "^1.1.4", "is-shared-array-buffer": "^1.0.2", @@ -5081,11 +5555,14 @@ "object-is": "^1.1.5", "object-keys": "^1.1.1", "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.4.3", + "regexp.prototype.flags": "^1.5.1", "side-channel": "^1.0.4", "which-boxed-primitive": "^1.0.2", "which-collection": "^1.0.1", - "which-typed-array": "^1.1.9" + "which-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -5097,12 +5574,27 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, - "node_modules/define-properties": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", - "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==", + "node_modules/define-data-property": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", + "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", "dev": true, "dependencies": { + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" }, @@ -5121,6 +5613,15 @@ "node": ">=0.4.0" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/detect-node": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", @@ -5132,9 +5633,9 @@ "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==" }, "node_modules/diff-sequences": { - "version": "29.4.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.4.3.tgz", - "integrity": "sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", "dev": true, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -5179,22 +5680,10 @@ "csstype": "^3.0.2" } }, - "node_modules/domexception": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", - "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", - "dev": true, - "dependencies": { - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/electron-to-chromium": { - "version": "1.4.372", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.372.tgz", - "integrity": "sha512-MrlFq/j+TYHOjeWsWGYfzevc25HNeJdsF6qaLFrqBTRWZQtWkb1myq/Q2veLWezVaa5OcSZ99CFwTT4aF4Mung==", + "version": "1.4.648", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.648.tgz", + "integrity": "sha512-EmFMarXeqJp9cUKu/QEciEApn0S/xRcpZWuAm32U7NgoZCimjsilKXHRO9saeEW55eHZagIDg6XTUOv32w9pjg==", "dev": true }, "node_modules/emoji-regex": { @@ -5203,23 +5692,14 @@ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "dev": true }, - "node_modules/end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dev": true, - "dependencies": { - "once": "^1.4.0" - } - }, "node_modules/engine.io-client": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.4.0.tgz", - "integrity": "sha512-GyKPDyoEha+XZ7iEqam49vz6auPnNJ9ZBfy89f+rMMas8AuiMWOZ9PVzu8xb9ZC6rafUqiGHSCfu22ih66E+1g==", + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.3.tgz", + "integrity": "sha512-9Z0qLB0NIisTRt1DZ/8U2k12RJn8yls/nXMZLn+/N8hANT3TcYjKFKcwbw5zFQiN4NTde3TSY9zb79e1ij6j9Q==", "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.1", - "engine.io-parser": "~5.0.3", + "engine.io-parser": "~5.2.1", "ws": "~8.11.0", "xmlhttprequest-ssl": "~2.0.0" } @@ -5245,9 +5725,9 @@ } }, "node_modules/engine.io-parser": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.6.tgz", - "integrity": "sha512-tjuoZDMAdEhVnSFleYPCtdL2GXwVTGtNjoeJd9IhIG3C1xs9uwxqRNEu5WpnDZCaozwVlK/nuQhpodhXSIMaxw==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.1.tgz", + "integrity": "sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ==", "engines": { "node": ">=10.0.0" } @@ -5273,25 +5753,26 @@ } }, "node_modules/es-abstract": { - "version": "1.21.2", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.21.2.tgz", - "integrity": "sha512-y/B5POM2iBnIxCiernH1G7rC9qQoM77lLIMQLuob0zhp8C56Po81+2Nj0WFKnd0pNReDTnkYryc+zhOzpEIROg==", + "version": "1.22.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.3.tgz", + "integrity": "sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA==", "dev": true, "dependencies": { "array-buffer-byte-length": "^1.0.0", + "arraybuffer.prototype.slice": "^1.0.2", "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", + "call-bind": "^1.0.5", "es-set-tostringtag": "^2.0.1", "es-to-primitive": "^1.2.1", - "function.prototype.name": "^1.1.5", - "get-intrinsic": "^1.2.0", + "function.prototype.name": "^1.1.6", + "get-intrinsic": "^1.2.2", "get-symbol-description": "^1.0.0", "globalthis": "^1.0.3", "gopd": "^1.0.1", - "has": "^1.0.3", "has-property-descriptors": "^1.0.0", "has-proto": "^1.0.1", "has-symbols": "^1.0.3", + "hasown": "^2.0.0", "internal-slot": "^1.0.5", "is-array-buffer": "^3.0.2", "is-callable": "^1.2.7", @@ -5299,19 +5780,23 @@ "is-regex": "^1.1.4", "is-shared-array-buffer": "^1.0.2", "is-string": "^1.0.7", - "is-typed-array": "^1.1.10", + "is-typed-array": "^1.1.12", "is-weakref": "^1.0.2", - "object-inspect": "^1.12.3", + "object-inspect": "^1.13.1", "object-keys": "^1.1.1", "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.4.3", + "regexp.prototype.flags": "^1.5.1", + "safe-array-concat": "^1.0.1", "safe-regex-test": "^1.0.0", - "string.prototype.trim": "^1.2.7", - "string.prototype.trimend": "^1.0.6", - "string.prototype.trimstart": "^1.0.6", + "string.prototype.trim": "^1.2.8", + "string.prototype.trimend": "^1.0.7", + "string.prototype.trimstart": "^1.0.7", + "typed-array-buffer": "^1.0.0", + "typed-array-byte-length": "^1.0.0", + "typed-array-byte-offset": "^1.0.0", "typed-array-length": "^1.0.4", "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.9" + "which-typed-array": "^1.1.13" }, "engines": { "node": ">= 0.4" @@ -5340,27 +5825,49 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/es-set-tostringtag": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz", - "integrity": "sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==", + "node_modules/es-iterator-helpers": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.15.tgz", + "integrity": "sha512-GhoY8uYqd6iwUl2kgjTm4CZAf6oo5mHK7BPqx3rKgx893YSsy0LGHV6gfqqQvZt/8xM8xeOnfXBCfqclMKkJ5g==", "dev": true, "dependencies": { - "get-intrinsic": "^1.1.3", - "has": "^1.0.3", - "has-tostringtag": "^1.0.0" + "asynciterator.prototype": "^1.0.0", + "call-bind": "^1.0.2", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.1", + "es-set-tostringtag": "^2.0.1", + "function-bind": "^1.1.1", + "get-intrinsic": "^1.2.1", + "globalthis": "^1.0.3", + "has-property-descriptors": "^1.0.0", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.5", + "iterator.prototype": "^1.1.2", + "safe-array-concat": "^1.0.1" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz", + "integrity": "sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.2", + "has-tostringtag": "^1.0.0", + "hasown": "^2.0.0" }, "engines": { "node": ">= 0.4" } }, "node_modules/es-shim-unscopables": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz", - "integrity": "sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", + "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", "dev": true, "dependencies": { - "has": "^1.0.3" + "hasown": "^2.0.0" } }, "node_modules/es-to-primitive": { @@ -5381,9 +5888,9 @@ } }, "node_modules/esbuild": { - "version": "0.17.18", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.18.tgz", - "integrity": "sha512-z1lix43jBs6UKjcZVKOw2xx69ffE2aG0PygLL5qJ9OS/gy0Ewd1gW/PUQIOIQGXBHWNywSc0floSKoMFF8aK2w==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", + "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", "dev": true, "hasInstallScript": true, "bin": { @@ -5393,28 +5900,29 @@ "node": ">=12" }, "optionalDependencies": { - "@esbuild/android-arm": "0.17.18", - "@esbuild/android-arm64": "0.17.18", - "@esbuild/android-x64": "0.17.18", - "@esbuild/darwin-arm64": "0.17.18", - "@esbuild/darwin-x64": "0.17.18", - "@esbuild/freebsd-arm64": "0.17.18", - "@esbuild/freebsd-x64": "0.17.18", - "@esbuild/linux-arm": "0.17.18", - "@esbuild/linux-arm64": "0.17.18", - "@esbuild/linux-ia32": "0.17.18", - "@esbuild/linux-loong64": "0.17.18", - "@esbuild/linux-mips64el": "0.17.18", - "@esbuild/linux-ppc64": "0.17.18", - "@esbuild/linux-riscv64": "0.17.18", - "@esbuild/linux-s390x": "0.17.18", - "@esbuild/linux-x64": "0.17.18", - "@esbuild/netbsd-x64": "0.17.18", - "@esbuild/openbsd-x64": "0.17.18", - "@esbuild/sunos-x64": "0.17.18", - "@esbuild/win32-arm64": "0.17.18", - "@esbuild/win32-ia32": "0.17.18", - "@esbuild/win32-x64": "0.17.18" + "@esbuild/aix-ppc64": "0.19.12", + "@esbuild/android-arm": "0.19.12", + "@esbuild/android-arm64": "0.19.12", + "@esbuild/android-x64": "0.19.12", + "@esbuild/darwin-arm64": "0.19.12", + "@esbuild/darwin-x64": "0.19.12", + "@esbuild/freebsd-arm64": "0.19.12", + "@esbuild/freebsd-x64": "0.19.12", + "@esbuild/linux-arm": "0.19.12", + "@esbuild/linux-arm64": "0.19.12", + "@esbuild/linux-ia32": "0.19.12", + "@esbuild/linux-loong64": "0.19.12", + "@esbuild/linux-mips64el": "0.19.12", + "@esbuild/linux-ppc64": "0.19.12", + "@esbuild/linux-riscv64": "0.19.12", + "@esbuild/linux-s390x": "0.19.12", + "@esbuild/linux-x64": "0.19.12", + "@esbuild/netbsd-x64": "0.19.12", + "@esbuild/openbsd-x64": "0.19.12", + "@esbuild/sunos-x64": "0.19.12", + "@esbuild/win32-arm64": "0.19.12", + "@esbuild/win32-ia32": "0.19.12", + "@esbuild/win32-x64": "0.19.12" } }, "node_modules/escalade": { @@ -5437,111 +5945,29 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/escodegen": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.0.0.tgz", - "integrity": "sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw==", - "dev": true, - "dependencies": { - "esprima": "^4.0.1", - "estraverse": "^5.2.0", - "esutils": "^2.0.2", - "optionator": "^0.8.1" - }, - "bin": { - "escodegen": "bin/escodegen.js", - "esgenerate": "bin/esgenerate.js" - }, - "engines": { - "node": ">=6.0" - }, - "optionalDependencies": { - "source-map": "~0.6.1" - } - }, - "node_modules/escodegen/node_modules/levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", - "dev": true, - "dependencies": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/escodegen/node_modules/optionator": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", - "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", - "dev": true, - "dependencies": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.6", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "word-wrap": "~1.2.3" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/escodegen/node_modules/prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", - "dev": true, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/escodegen/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/escodegen/node_modules/type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", - "dev": true, - "dependencies": { - "prelude-ls": "~1.1.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/eslint": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.39.0.tgz", - "integrity": "sha512-mwiok6cy7KTW7rBpo05k6+p4YVZByLNjAZ/ACB9DRCu4YDRwjXI01tWHp6KAUWelsBetTxKK/2sHB0vdS8Z2Og==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz", + "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.4.0", - "@eslint/eslintrc": "^2.0.2", - "@eslint/js": "8.39.0", - "@humanwhocodes/config-array": "^0.11.8", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.56.0", + "@humanwhocodes/config-array": "^0.11.13", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", - "ajv": "^6.10.0", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.0", - "eslint-visitor-keys": "^3.4.0", - "espree": "^9.5.1", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -5549,22 +5975,19 @@ "find-up": "^5.0.0", "glob-parent": "^6.0.2", "globals": "^13.19.0", - "grapheme-splitter": "^1.0.4", + "graphemer": "^1.4.0", "ignore": "^5.2.0", - "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", - "js-sdsl": "^4.1.4", "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.9.1", + "optionator": "^0.9.3", "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", "text-table": "^0.2.0" }, "bin": { @@ -5605,15 +6028,31 @@ "eslint": "^8.0.0" } }, + "node_modules/eslint-config-react-app/node_modules/eslint-plugin-testing-library": { + "version": "5.11.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-testing-library/-/eslint-plugin-testing-library-5.11.1.tgz", + "integrity": "sha512-5eX9e1Kc2PqVRed3taaLnAAqPZGEX75C+M/rXzUAI3wIg/ZxzUm1OVAwfe/O+vE+6YXOLetSe9g5GKD2ecXipw==", + "dev": true, + "dependencies": { + "@typescript-eslint/utils": "^5.58.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0", + "npm": ">=6" + }, + "peerDependencies": { + "eslint": "^7.5.0 || ^8.0.0" + } + }, "node_modules/eslint-import-resolver-node": { - "version": "0.3.7", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.7.tgz", - "integrity": "sha512-gozW2blMLJCeFpBwugLTGyvVjNoeo1knonXAcatC6bjPBZitotxdWf7Gimr25N4c0AAOo4eOUfaG82IJPDpqCA==", + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", "dev": true, "dependencies": { "debug": "^3.2.7", - "is-core-module": "^2.11.0", - "resolve": "^1.22.1" + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" } }, "node_modules/eslint-import-resolver-node/node_modules/debug": { @@ -5670,26 +6109,28 @@ } }, "node_modules/eslint-plugin-import": { - "version": "2.27.5", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.27.5.tgz", - "integrity": "sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow==", + "version": "2.29.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz", + "integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==", "dev": true, "dependencies": { - "array-includes": "^3.1.6", - "array.prototype.flat": "^1.3.1", - "array.prototype.flatmap": "^1.3.1", + "array-includes": "^3.1.7", + "array.prototype.findlastindex": "^1.2.3", + "array.prototype.flat": "^1.3.2", + "array.prototype.flatmap": "^1.3.2", "debug": "^3.2.7", "doctrine": "^2.1.0", - "eslint-import-resolver-node": "^0.3.7", - "eslint-module-utils": "^2.7.4", - "has": "^1.0.3", - "is-core-module": "^2.11.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.8.0", + "hasown": "^2.0.0", + "is-core-module": "^2.13.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", - "object.values": "^1.1.6", - "resolve": "^1.22.1", - "semver": "^6.3.0", - "tsconfig-paths": "^3.14.1" + "object.fromentries": "^2.0.7", + "object.groupby": "^1.0.1", + "object.values": "^1.1.7", + "semver": "^6.3.1", + "tsconfig-paths": "^3.15.0" }, "engines": { "node": ">=4" @@ -5744,27 +6185,27 @@ } }, "node_modules/eslint-plugin-jsx-a11y": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.7.1.tgz", - "integrity": "sha512-63Bog4iIethyo8smBklORknVjB0T2dwB8Mr/hIC+fBS0uyHdYYpzM/Ed+YC8VxTjlXHEWFOdmgwcDn1U2L9VCA==", + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.8.0.tgz", + "integrity": "sha512-Hdh937BS3KdwwbBaKd5+PLCOmYY6U4f2h9Z2ktwtNKvIdIEu137rjYbcb9ApSbVJfWxANNuiKTD/9tOKjK9qOA==", "dev": true, "dependencies": { - "@babel/runtime": "^7.20.7", - "aria-query": "^5.1.3", - "array-includes": "^3.1.6", - "array.prototype.flatmap": "^1.3.1", - "ast-types-flow": "^0.0.7", - "axe-core": "^4.6.2", - "axobject-query": "^3.1.1", + "@babel/runtime": "^7.23.2", + "aria-query": "^5.3.0", + "array-includes": "^3.1.7", + "array.prototype.flatmap": "^1.3.2", + "ast-types-flow": "^0.0.8", + "axe-core": "=4.7.0", + "axobject-query": "^3.2.1", "damerau-levenshtein": "^1.0.8", "emoji-regex": "^9.2.2", - "has": "^1.0.3", - "jsx-ast-utils": "^3.3.3", - "language-tags": "=1.0.5", + "es-iterator-helpers": "^1.0.15", + "hasown": "^2.0.0", + "jsx-ast-utils": "^3.3.5", + "language-tags": "^1.0.9", "minimatch": "^3.1.2", - "object.entries": "^1.1.6", - "object.fromentries": "^2.0.6", - "semver": "^6.3.0" + "object.entries": "^1.1.7", + "object.fromentries": "^2.0.7" }, "engines": { "node": ">=4.0" @@ -5773,16 +6214,26 @@ "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" } }, + "node_modules/eslint-plugin-jsx-a11y/node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, "node_modules/eslint-plugin-react": { - "version": "7.32.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.32.2.tgz", - "integrity": "sha512-t2fBMa+XzonrrNkyVirzKlvn5RXzzPwRHtMvLAtVZrt8oxgnTQaYbU6SXTOO1mwQgp1y5+toMSKInnzGr0Knqg==", + "version": "7.33.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.33.2.tgz", + "integrity": "sha512-73QQMKALArI8/7xGLNI/3LylrEYrlKZSb5C9+q3OtOewTnMQi5cT+aE9E41sLCmli3I9PGGmD1yiZydyo4FEPw==", "dev": true, "dependencies": { "array-includes": "^3.1.6", "array.prototype.flatmap": "^1.3.1", "array.prototype.tosorted": "^1.1.1", "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.0.12", "estraverse": "^5.3.0", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", @@ -5792,7 +6243,7 @@ "object.values": "^1.1.6", "prop-types": "^15.8.1", "resolve": "^2.0.0-next.4", - "semver": "^6.3.0", + "semver": "^6.3.1", "string.prototype.matchall": "^4.0.8" }, "engines": { @@ -5827,12 +6278,12 @@ } }, "node_modules/eslint-plugin-react/node_modules/resolve": { - "version": "2.0.0-next.4", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.4.tgz", - "integrity": "sha512-iMDbmAWtfU+MHpxt/I5iWI7cY6YVEZUQ3MBgPQ++XD1PELuJHIl82xBmObyP2KyQmkNB2dsqF7seoQQiAn5yDQ==", + "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", "dev": true, "dependencies": { - "is-core-module": "^2.9.0", + "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, @@ -5844,9 +6295,9 @@ } }, "node_modules/eslint-plugin-testing-library": { - "version": "5.10.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-testing-library/-/eslint-plugin-testing-library-5.10.3.tgz", - "integrity": "sha512-0yhsKFsjHLud5PM+f2dWr9K3rqYzMy4cSHs3lcmFYMa1CdSzRvHGgXvsFarBjZ41gU8jhTdMIkg8jHLxGJqLqw==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-testing-library/-/eslint-plugin-testing-library-6.2.0.tgz", + "integrity": "sha512-+LCYJU81WF2yQ+Xu4A135CgK8IszcFcyMF4sWkbiu6Oj+Nel0TrkZq/HvDw0/1WuO3dhDQsZA/OpEMGd0NfcUw==", "dev": true, "dependencies": { "@typescript-eslint/utils": "^5.58.0" @@ -5860,9 +6311,9 @@ } }, "node_modules/eslint-scope": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz", - "integrity": "sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==", + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", "dev": true, "dependencies": { "esrecurse": "^4.3.0", @@ -5876,9 +6327,9 @@ } }, "node_modules/eslint-visitor-keys": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.0.tgz", - "integrity": "sha512-HPpKPUBQcAsZOsHAFwTtIKcYlCje62XB7SEAcxjtmW6TD1WVpkS6i6/hOVtTZIl4zGj/mBqpFVGvaDneik+VoQ==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -5888,9 +6339,9 @@ } }, "node_modules/eslint/node_modules/globals": { - "version": "13.20.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", - "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, "dependencies": { "type-fest": "^0.20.2" @@ -5915,14 +6366,14 @@ } }, "node_modules/espree": { - "version": "9.5.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.1.tgz", - "integrity": "sha512-5yxtHSZXRSW5pvv3hAlXM5+/Oswi1AUFqBmbibKb5s6bp3rGIDkyXU6xCoyuuLhijr4SFwPrXRoZjz0AZDN9tg==", + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", "dev": true, "dependencies": { - "acorn": "^8.8.0", + "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.0" + "eslint-visitor-keys": "^3.4.1" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -5931,19 +6382,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/esquery": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", @@ -5977,6 +6415,15 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -5993,19 +6440,19 @@ "dev": true }, "node_modules/execa": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", - "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", "dev": true, "dependencies": { - "cross-spawn": "^7.0.0", - "get-stream": "^5.0.0", - "human-signals": "^1.1.1", + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.0", - "onetime": "^5.1.0", - "signal-exit": "^3.0.2", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" }, "engines": { @@ -6016,16 +6463,16 @@ } }, "node_modules/expect": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/expect/-/expect-29.5.0.tgz", - "integrity": "sha512-yM7xqUrCO2JdpFo4XpM82t+PJBFybdqoQuJLDGeDX2ij8NZzqRHyu3Hp188/JX7SWqud+7t4MUdvcgGBICMHZg==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", "dev": true, "dependencies": { - "@jest/expect-utils": "^29.5.0", - "jest-get-type": "^29.4.3", - "jest-matcher-utils": "^29.5.0", - "jest-message-util": "^29.5.0", - "jest-util": "^29.5.0" + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -6036,22 +6483,19 @@ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, - "node_modules/fast-diff": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz", - "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", - "dev": true - }, "node_modules/fast-equals": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-4.0.3.tgz", - "integrity": "sha512-G3BSX9cfKttjr+2o1O22tYMLq0DPluZnYtq1rXumE1SpL/F/SLIfHx08WYQoWSIpeMYf8sRbJ8++71+v6Pnxfg==", - "dev": true + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.0.1.tgz", + "integrity": "sha512-WF1Wi8PwwSY7/6Kx0vKXtw8RwuSGoM1bvDaJbu7MxDlR1vovZjIAKrnzyrThgAjm6JDTu0fVgWXDlMGspodfoQ==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } }, "node_modules/fast-glob": { - "version": "3.2.12", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", - "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", "dev": true, "dependencies": { "@nodelib/fs.stat": "^2.0.2", @@ -6089,18 +6533,18 @@ "dev": true }, "node_modules/fastq": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", - "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.0.tgz", + "integrity": "sha512-zGygtijUMT7jnk3h26kUms3BkSDp4IfIKjmnqI2tvx6nuBfiF1UqOxbnLfzdv+apBy+53oaImsKtMw/xYbW+1w==", "dev": true, "dependencies": { "reusify": "^1.0.4" } }, "node_modules/fflate": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.7.4.tgz", - "integrity": "sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw==", + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.1.tgz", + "integrity": "sha512-/exOvEuc+/iaUm105QIiOt4LpBdMTWsXxqR0HDF35vx3fmaKzw7354gTilCh5rkzEt8WYyG//ku3h3nRmd7CHQ==", "dev": true }, "node_modules/file-entry-cache": { @@ -6161,12 +6605,13 @@ } }, "node_modules/flat-cache": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", - "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", "dev": true, "dependencies": { - "flatted": "^3.1.0", + "flatted": "^3.2.9", + "keyv": "^4.5.3", "rimraf": "^3.0.2" }, "engines": { @@ -6174,15 +6619,15 @@ } }, "node_modules/flatted": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", - "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", + "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", "dev": true }, "node_modules/follow-redirects": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", - "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", + "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", "funding": [ { "type": "individual", @@ -6207,19 +6652,6 @@ "is-callable": "^1.1.3" } }, - "node_modules/foreground-child": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", - "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==", - "dev": true, - "dependencies": { - "cross-spawn": "^7.0.0", - "signal-exit": "^3.0.2" - }, - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/form-data": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", @@ -6234,9 +6666,9 @@ } }, "node_modules/fs-extra": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz", - "integrity": "sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==", + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", "dev": true, "dependencies": { "graceful-fs": "^4.2.0", @@ -6248,9 +6680,9 @@ } }, "node_modules/fs-extra/node_modules/universalify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "dev": true, "engines": { "node": ">= 10.0.0" @@ -6262,9 +6694,9 @@ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, "hasInstallScript": true, "optional": true, @@ -6276,20 +6708,23 @@ } }, "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/function.prototype.name": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", - "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", + "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.0", - "functions-have-names": "^1.2.2" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "functions-have-names": "^1.2.3" }, "engines": { "node": ">= 0.4" @@ -6316,33 +6751,25 @@ "node": ">=6.9.0" } }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, "node_modules/get-func-name": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", - "integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", "dev": true, "engines": { "node": "*" } }, "node_modules/get-intrinsic": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", - "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", + "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", "dev": true, "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.3" + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -6357,15 +6784,12 @@ } }, "node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", "dev": true, - "dependencies": { - "pump": "^3.0.0" - }, "engines": { - "node": ">=8" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -6480,23 +6904,12 @@ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "dev": true }, - "node_modules/grapheme-splitter": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", - "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/has-bigints": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", @@ -6516,12 +6929,12 @@ } }, "node_modules/has-property-descriptors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", - "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", + "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", "dev": true, "dependencies": { - "get-intrinsic": "^1.1.1" + "get-intrinsic": "^1.2.2" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -6566,6 +6979,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/hoist-non-react-statics": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", @@ -6576,15 +7000,15 @@ } }, "node_modules/html-encoding-sniffer": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", - "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", "dev": true, "dependencies": { - "whatwg-encoding": "^2.0.0" + "whatwg-encoding": "^3.1.1" }, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/html-escaper": { @@ -6594,51 +7018,50 @@ "dev": true }, "node_modules/http-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", - "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.0.tgz", + "integrity": "sha512-+ZT+iBxVUQ1asugqnD6oWoRiS25AkjNfG085dKJGtGxkdwLQrMKU5wJr2bOOFAXzKcTuqq+7fZlTMgG3SRfIYQ==", "dev": true, "dependencies": { - "@tootallnate/once": "2", - "agent-base": "6", - "debug": "4" + "agent-base": "^7.1.0", + "debug": "^4.3.4" }, "engines": { - "node": ">= 6" + "node": ">= 14" } }, "node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.2.tgz", + "integrity": "sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==", "dev": true, "dependencies": { - "agent-base": "6", + "agent-base": "^7.0.2", "debug": "4" }, "engines": { - "node": ">= 6" + "node": ">= 14" } }, "node_modules/human-signals": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", - "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", "dev": true, "engines": { - "node": ">=8.12.0" + "node": ">=10.17.0" } }, "node_modules/husky": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/husky/-/husky-8.0.3.tgz", - "integrity": "sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg==", + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.0.6.tgz", + "integrity": "sha512-EEuw/rfTiMjOfuL7pGO/i9otg1u36TXxqjIA6D9qxVjd/UXoDOsLor/BSFf5hTK50shwzCU3aVVwdXDp/lp7RA==", "dev": true, "bin": { - "husky": "lib/bin.js" + "husky": "bin.js" }, "engines": { - "node": ">=14" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/typicode" @@ -6657,18 +7080,18 @@ } }, "node_modules/ignore": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", - "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz", + "integrity": "sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==", "dev": true, "engines": { "node": ">= 4" } }, "node_modules/immutable": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.0.tgz", - "integrity": "sha512-0AOCmOip+xgJwEVTQj1EfiDDOkPmuyllDuTuEX+DDXUgapLAsBIfkg3sxCYyCEA8mQqZrrxPUGjcOQ2JS3WLkg==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.5.tgz", + "integrity": "sha512-8eabxkth9gZatlwl5TBuJnCsoTADlL6ftEr7A4qgdaTsPyreilDSnUk57SO+jfKcNtxPa22U5KK6DSeAYhpBJw==", "dev": true }, "node_modules/import-fresh": { @@ -6719,13 +7142,13 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "node_modules/internal-slot": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", - "integrity": "sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.6.tgz", + "integrity": "sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==", "dev": true, "dependencies": { - "get-intrinsic": "^1.2.0", - "has": "^1.0.3", + "get-intrinsic": "^1.2.2", + "hasown": "^2.0.0", "side-channel": "^1.0.4" }, "engines": { @@ -6784,6 +7207,21 @@ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" }, + "node_modules/is-async-function": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.0.0.tgz", + "integrity": "sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-bigint": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", @@ -6837,11 +7275,11 @@ } }, "node_modules/is-core-module": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.0.tgz", - "integrity": "sha512-RECHCBCd/viahWmwj6enj19sKbHfJrddi/6cBDsNTKbNq0f7VeaUkBo60BqzvPqo/W54ChS62Z5qyun7cfOMqQ==", + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", "dependencies": { - "has": "^1.0.3" + "hasown": "^2.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -6871,13 +7309,31 @@ "node": ">=0.10.0" } }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "node_modules/is-finalizationregistry": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.0.2.tgz", + "integrity": "sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw==", "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-generator-function": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", + "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, "engines": { - "node": ">=8" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-glob": { @@ -7032,16 +7488,12 @@ } }, "node_modules/is-typed-array": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz", - "integrity": "sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", + "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", "dev": true, "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" + "which-typed-array": "^1.1.11" }, "engines": { "node": ">= 0.4" @@ -7097,32 +7549,55 @@ "dev": true }, "node_modules/istanbul-lib-coverage": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", - "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", "dev": true, "engines": { "node": ">=8" } }, "node_modules/istanbul-lib-report": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", - "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", "dev": true, "dependencies": { "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^3.0.0", + "make-dir": "^4.0.0", "supports-color": "^7.1.0" }, "engines": { - "node": ">=8" + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" } }, "node_modules/istanbul-reports": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.5.tgz", - "integrity": "sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w==", + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.6.tgz", + "integrity": "sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==", "dev": true, "dependencies": { "html-escaper": "^2.0.0", @@ -7132,16 +7607,29 @@ "node": ">=8" } }, + "node_modules/iterator.prototype": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.2.tgz", + "integrity": "sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w==", + "dev": true, + "dependencies": { + "define-properties": "^1.2.1", + "get-intrinsic": "^1.2.1", + "has-symbols": "^1.0.3", + "reflect.getprototypeof": "^1.0.4", + "set-function-name": "^2.0.1" + } + }, "node_modules/jest-diff": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.5.0.tgz", - "integrity": "sha512-LtxijLLZBduXnHSniy0WMdaHjmQnt3g5sa16W4p0HqukYTTsyTW3GD1q41TyGl5YFXj/5B2U6dlh5FM1LIMgxw==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", "dev": true, "dependencies": { "chalk": "^4.0.0", - "diff-sequences": "^29.4.3", - "jest-get-type": "^29.4.3", - "pretty-format": "^29.5.0" + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -7160,12 +7648,12 @@ } }, "node_modules/jest-diff/node_modules/pretty-format": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.5.0.tgz", - "integrity": "sha512-V2mGkI31qdttvTFX7Mt4efOqHXqJWMu4/r66Xh3Z3BwZaPfPJgp6/gbwoujRpPUtfEF6AUUWx3Jim3GCw5g/Qw==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", "dev": true, "dependencies": { - "@jest/schemas": "^29.4.3", + "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" }, @@ -7180,24 +7668,24 @@ "dev": true }, "node_modules/jest-get-type": { - "version": "29.4.3", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.4.3.tgz", - "integrity": "sha512-J5Xez4nRRMjk8emnTpWrlkyb9pfRQQanDrvWHhsR1+VUfbwxi30eVcZFlcdGInRibU4G5LwHXpI7IRHU0CY+gg==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", "dev": true, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-matcher-utils": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.5.0.tgz", - "integrity": "sha512-lecRtgm/rjIK0CQ7LPQwzCs2VwW6WAahA55YBuI+xqmhm7LAaxokSB8C97yJeYyT+HvQkH741StzpU41wohhWw==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", "dev": true, "dependencies": { "chalk": "^4.0.0", - "jest-diff": "^29.5.0", - "jest-get-type": "^29.4.3", - "pretty-format": "^29.5.0" + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -7216,12 +7704,12 @@ } }, "node_modules/jest-matcher-utils/node_modules/pretty-format": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.5.0.tgz", - "integrity": "sha512-V2mGkI31qdttvTFX7Mt4efOqHXqJWMu4/r66Xh3Z3BwZaPfPJgp6/gbwoujRpPUtfEF6AUUWx3Jim3GCw5g/Qw==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", "dev": true, "dependencies": { - "@jest/schemas": "^29.4.3", + "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" }, @@ -7236,18 +7724,18 @@ "dev": true }, "node_modules/jest-message-util": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.5.0.tgz", - "integrity": "sha512-Kijeg9Dag6CKtIDA7O21zNTACqD5MD/8HfIV8pdD94vFyFuer52SigdC3IQMhab3vACxXMiFk+yMHNdbqtyTGA==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", "dev": true, "dependencies": { "@babel/code-frame": "^7.12.13", - "@jest/types": "^29.5.0", + "@jest/types": "^29.6.3", "@types/stack-utils": "^2.0.0", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "micromatch": "^4.0.4", - "pretty-format": "^29.5.0", + "pretty-format": "^29.7.0", "slash": "^3.0.0", "stack-utils": "^2.0.3" }, @@ -7268,12 +7756,12 @@ } }, "node_modules/jest-message-util/node_modules/pretty-format": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.5.0.tgz", - "integrity": "sha512-V2mGkI31qdttvTFX7Mt4efOqHXqJWMu4/r66Xh3Z3BwZaPfPJgp6/gbwoujRpPUtfEF6AUUWx3Jim3GCw5g/Qw==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", "dev": true, "dependencies": { - "@jest/schemas": "^29.4.3", + "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" }, @@ -7288,12 +7776,12 @@ "dev": true }, "node_modules/jest-util": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.5.0.tgz", - "integrity": "sha512-RYMgG/MTadOr5t8KdhejfvUU82MxsCu5MF6KuDUHl+NuwzUt+Sm6jJWxTJVrDR1j5M/gJVCPKQEpWXY+yIQ6lQ==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", "dev": true, "dependencies": { - "@jest/types": "^29.5.0", + "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "ci-info": "^3.2.0", @@ -7304,30 +7792,11 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/js-sdsl": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.4.0.tgz", - "integrity": "sha512-FfVSdx6pJ41Oa+CF7RDaFmTnCaFhua+SNYQX74riGOpl96x+2jQCqEfQ2bnXu/5DPCqlRuiqyvTJM0Qjz26IVg==", - "dev": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/js-sdsl" - } - }, "node_modules/js-sha3": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==" }, - "node_modules/js-string-escape": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/js-string-escape/-/js-string-escape-1.0.1.tgz", - "integrity": "sha512-Smw4xcfIQ5LVjAOuJCvN/zIodzA/BBSsluuoSykP+lUvScIi4U6RJLfwHet5cxFnCswUjISV8oAXaqaJDY3chg==", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -7346,43 +7815,38 @@ } }, "node_modules/jsdom": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-21.1.1.tgz", - "integrity": "sha512-Jjgdmw48RKcdAIQyUD1UdBh2ecH7VqwaXPN3ehoZN6MqgVbMn+lRm1aAT1AsdJRAJpwfa4IpwgzySn61h2qu3w==", + "version": "24.0.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-24.0.0.tgz", + "integrity": "sha512-UDS2NayCvmXSXVP6mpTj+73JnNQadZlr9N68189xib2tx5Mls7swlTNao26IoHv46BZJFvXygyRtyXd1feAk1A==", "dev": true, "dependencies": { - "abab": "^2.0.6", - "acorn": "^8.8.2", - "acorn-globals": "^7.0.0", - "cssstyle": "^3.0.0", - "data-urls": "^4.0.0", + "cssstyle": "^4.0.1", + "data-urls": "^5.0.0", "decimal.js": "^10.4.3", - "domexception": "^4.0.0", - "escodegen": "^2.0.0", "form-data": "^4.0.0", - "html-encoding-sniffer": "^3.0.0", - "http-proxy-agent": "^5.0.0", - "https-proxy-agent": "^5.0.1", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.2", "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.2", + "nwsapi": "^2.2.7", "parse5": "^7.1.2", "rrweb-cssom": "^0.6.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", - "tough-cookie": "^4.1.2", - "w3c-xmlserializer": "^4.0.0", + "tough-cookie": "^4.1.3", + "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^7.0.0", - "whatwg-encoding": "^2.0.0", - "whatwg-mimetype": "^3.0.0", - "whatwg-url": "^12.0.1", - "ws": "^8.13.0", - "xml-name-validator": "^4.0.0" + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0", + "ws": "^8.16.0", + "xml-name-validator": "^5.0.0" }, "engines": { - "node": ">=14" + "node": ">=18" }, "peerDependencies": { - "canvas": "^2.5.0" + "canvas": "^2.11.2" }, "peerDependenciesMeta": { "canvas": { @@ -7402,6 +7866,12 @@ "node": ">=4" } }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -7432,9 +7902,9 @@ } }, "node_modules/jsonc-parser": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", - "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz", + "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==", "dev": true }, "node_modules/jsonfile": { @@ -7450,27 +7920,38 @@ } }, "node_modules/jsonfile/node_modules/universalify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "dev": true, "engines": { "node": ">= 10.0.0" } }, "node_modules/jsx-ast-utils": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz", - "integrity": "sha512-fYQHZTZ8jSfmWZ0iyzfwiU4WDX4HpHbMCZ3gPlWYiCl3BoeOTsqKBqnTVfH2rYT7eP5c3sVbeSPHnnJOaTrWiw==", + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", "dev": true, "dependencies": { - "array-includes": "^3.1.5", - "object.assign": "^4.1.3" + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" }, "engines": { "node": ">=4.0" } }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, "node_modules/klona": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", @@ -7486,12 +7967,15 @@ "dev": true }, "node_modules/language-tags": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.5.tgz", - "integrity": "sha512-qJhlO9cGXi6hBGKoxEG/sKZDAHD5Hnu9Hs4WbOY3pCWXDhw0N8x1NenNzm2EnNLkLkk7J2SdxAkDSbb6ftT+UQ==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", + "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", "dev": true, "dependencies": { - "language-subtag-registry": "~0.3.2" + "language-subtag-registry": "^0.3.20" + }, + "engines": { + "node": ">=0.10" } }, "node_modules/levn": { @@ -7513,10 +7997,14 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" }, "node_modules/local-pkg": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.4.3.tgz", - "integrity": "sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.0.tgz", + "integrity": "sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==", "dev": true, + "dependencies": { + "mlly": "^1.4.2", + "pkg-types": "^1.0.3" + }, "engines": { "node": ">=14" }, @@ -7575,12 +8063,12 @@ } }, "node_modules/loupe": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.6.tgz", - "integrity": "sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==", + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", "dev": true, "dependencies": { - "get-func-name": "^2.0.0" + "get-func-name": "^2.0.1" } }, "node_modules/lru-cache": { @@ -7602,51 +8090,83 @@ } }, "node_modules/magic-string": { - "version": "0.30.0", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.0.tgz", - "integrity": "sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ==", + "version": "0.30.5", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz", + "integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==", "dev": true, "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.13" + "@jridgewell/sourcemap-codec": "^1.4.15" }, "engines": { "node": ">=12" } }, - "node_modules/make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "node_modules/magicast": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.3.tgz", + "integrity": "sha512-ZbrP1Qxnpoes8sz47AM0z08U+jW6TyRgZzcWy3Ma3vDhJttwMwAFDMMQFobwdBxByBD46JYmxRzeF7w2+wJEuw==", "dev": true, "dependencies": { - "semver": "^6.0.0" + "@babel/parser": "^7.23.6", + "@babel/types": "^7.23.6", + "source-map-js": "^1.0.2" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" }, "engines": { - "node": ">=8" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/match-sorter": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/match-sorter/-/match-sorter-6.3.1.tgz", - "integrity": "sha512-mxybbo3pPNuA+ZuCUhm5bwNkXrJTbsk5VWbR5wiwz/GC6LIiegBGn2w3O08UG/jdbYLinw51fSQ5xNU1U3MgBw==", - "dependencies": { - "@babel/runtime": "^7.12.5", - "remove-accents": "0.4.2" - } - }, - "node_modules/md5-hex": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/md5-hex/-/md5-hex-3.0.1.tgz", - "integrity": "sha512-BUiRtTtV39LIJwinWBjqVsU9xhdnz7/i889V859IBFpuqGAj6LuOvHv5XLbgZ2R7ptJoJaEcxkv88/h25T7Ciw==", + "node_modules/make-dir/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "dev": true, "dependencies": { - "blueimp-md5": "^2.10.0" + "yallist": "^4.0.0" }, "engines": { - "node": ">=8" + "node": ">=10" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-dir/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/match-sorter": { + "version": "6.3.3", + "resolved": "https://registry.npmjs.org/match-sorter/-/match-sorter-6.3.3.tgz", + "integrity": "sha512-sgiXxrRijEe0SzHKGX4HouCpfHRPnqteH42UdMEW7BlWy990ZkzcvonJGv4Uu9WE7Y1f8Yocm91+4qFPCbmNww==", + "dependencies": { + "@babel/runtime": "^7.23.8", + "remove-accents": "0.5.0" } }, "node_modules/merge-stream": { @@ -7740,21 +8260,21 @@ } }, "node_modules/mlly": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.2.0.tgz", - "integrity": "sha512-+c7A3CV0KGdKcylsI6khWyts/CYrGTrRVo4R/I7u/cUsy0Conxa6LUhiEzVKIw14lc2L5aiO4+SeVe4TeGRKww==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.5.0.tgz", + "integrity": "sha512-NPVQvAY1xr1QoVeG0cy8yUYC7FQcOx6evl/RjT1wL5FvzPnzOysoqB/jmx/DhssT2dYa8nxECLAaFI/+gVLhDQ==", "dev": true, "dependencies": { - "acorn": "^8.8.2", - "pathe": "^1.1.0", - "pkg-types": "^1.0.2", - "ufo": "^1.1.1" + "acorn": "^8.11.3", + "pathe": "^1.1.2", + "pkg-types": "^1.0.3", + "ufo": "^1.3.2" } }, "node_modules/moment": { - "version": "2.29.4", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", - "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", "dev": true, "engines": { "node": "*" @@ -7770,9 +8290,9 @@ } }, "node_modules/mrmime": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-1.0.1.tgz", - "integrity": "sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", + "integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==", "dev": true, "engines": { "node": ">=10" @@ -7783,22 +8303,6 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, - "node_modules/multimatch": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/multimatch/-/multimatch-4.0.0.tgz", - "integrity": "sha512-lDmx79y1z6i7RNx0ZGCPq1bzJ6ZoDDKbvh7jxr9SJcWLkShMzXrHbYVpTdnhNM5MXpDUxCQ4DgqVttVXlBgiBQ==", - "dev": true, - "dependencies": { - "@types/minimatch": "^3.0.3", - "array-differ": "^3.0.0", - "array-union": "^2.1.0", - "arrify": "^2.0.1", - "minimatch": "^3.0.4" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/nano-time": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/nano-time/-/nano-time-1.0.0.tgz", @@ -7808,9 +8312,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", - "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", "dev": true, "funding": [ { @@ -7838,9 +8342,9 @@ "dev": true }, "node_modules/node-releases": { - "version": "2.0.10", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.10.tgz", - "integrity": "sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==", + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", "dev": true }, "node_modules/normalize-path": { @@ -7865,9 +8369,9 @@ } }, "node_modules/nwsapi": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.4.tgz", - "integrity": "sha512-NHj4rzRo0tQdijE9ZqAx6kYDcoRwYwSYzCA8MY3JzfxlrvEU0jhnhJT9BhqhJs7I/dKcrDm6TyulaRqZPIhN5g==", + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.7.tgz", + "integrity": "sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==", "dev": true }, "node_modules/object-assign": { @@ -7879,9 +8383,9 @@ } }, "node_modules/object-inspect": { - "version": "1.12.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", - "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -7913,13 +8417,13 @@ } }, "node_modules/object.assign": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", - "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", + "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", "has-symbols": "^1.0.3", "object-keys": "^1.1.1" }, @@ -7931,28 +8435,28 @@ } }, "node_modules/object.entries": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.6.tgz", - "integrity": "sha512-leTPzo4Zvg3pmbQ3rDK69Rl8GQvIqMWubrkxONG9/ojtFE2rD9fjMKfSI5BxW3osRH1m6VdzmqK8oAY9aT4x5w==", + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.7.tgz", + "integrity": "sha512-jCBs/0plmPsOnrKAfFQXRG2NFjlhZgjjcBLSmTnEhU8U6vVTsVe8ANeQJCHTl3gSsI4J+0emOoCgoKlmQPMgmA==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" }, "engines": { "node": ">= 0.4" } }, "node_modules/object.fromentries": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.6.tgz", - "integrity": "sha512-VciD13dswC4j1Xt5394WR4MzmAQmlgN72phd/riNp9vtD7tp4QQWJ0R4wvclXcafgcYK8veHRed2W6XeGBvcfg==", + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.7.tgz", + "integrity": "sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" }, "engines": { "node": ">= 0.4" @@ -7961,28 +8465,40 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/object.hasown": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.2.tgz", - "integrity": "sha512-B5UIT3J1W+WuWIU55h0mjlwaqxiE5vYENJXIXZ4VFe05pNYrkKuK0U/6aFcb0pKywYJh7IhfoqUfKVmrJJHZHw==", + "node_modules/object.groupby": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.1.tgz", + "integrity": "sha512-HqaQtqLnp/8Bn4GL16cj+CUYbnpe1bh0TtEaWvybszDG4tgxCJuRpV8VGuvNaI1fAnI4lUJzDG55MXcOH4JZcQ==", "dev": true, "dependencies": { - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1" + } + }, + "node_modules/object.hasown": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.3.tgz", + "integrity": "sha512-fFI4VcYpRHvSLXxP7yiZOMAd331cPfd2p7PFDVbgUsYOfCT3tICVqXWngbjr4m49OvsBwUBQ6O2uQoJvy3RexA==", + "dev": true, + "dependencies": { + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/object.values": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.6.tgz", - "integrity": "sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw==", + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.7.tgz", + "integrity": "sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" }, "engines": { "node": ">= 0.4" @@ -8020,17 +8536,17 @@ } }, "node_modules/optionator": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", - "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", "dev": true, "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" + "type-check": "^0.4.0" }, "engines": { "node": ">= 0.8.0" @@ -8066,15 +8582,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -8155,9 +8662,9 @@ } }, "node_modules/pathe": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.0.tgz", - "integrity": "sha512-ODbEPR0KKHqECXW1GoxdDb+AZvULmXjVPy4rt+pGo2+TnjJTIPJQSVS6N63n8T2Ip+syHhbn52OewKicV0373w==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", "dev": true }, "node_modules/pathval": { @@ -8188,20 +8695,20 @@ } }, "node_modules/pkg-types": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.0.2.tgz", - "integrity": "sha512-hM58GKXOcj8WTqUXnsQyJYXdeAPbythQgEF3nTcEo+nkD49chjQ9IKm/QJy9xf6JakXptz86h7ecP2024rrLaQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.0.3.tgz", + "integrity": "sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==", "dev": true, "dependencies": { "jsonc-parser": "^3.2.0", - "mlly": "^1.1.1", + "mlly": "^1.2.0", "pathe": "^1.1.0" } }, "node_modules/postcss": { - "version": "8.4.23", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.23.tgz", - "integrity": "sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA==", + "version": "8.4.33", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.33.tgz", + "integrity": "sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==", "dev": true, "funding": [ { @@ -8218,7 +8725,7 @@ } ], "dependencies": { - "nanoid": "^3.3.6", + "nanoid": "^3.3.7", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" }, @@ -8226,12 +8733,6 @@ "node": "^10 || ^12 || >=14" } }, - "node_modules/postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -8242,24 +8743,24 @@ } }, "node_modules/prettier": { - "version": "2.8.8", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", - "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.4.tgz", + "integrity": "sha512-FWu1oLHKCrtpO1ypU6J0SbK2d9Ckwysq6bHj/uaCP26DxrPpppCLQRGVuqAxSTvhF00AcvDRyYrLNW7ocBhFFQ==", "dev": true, "bin": { - "prettier": "bin-prettier.js" + "prettier": "bin/prettier.cjs" }, "engines": { - "node": ">=10.13.0" + "node": ">=14" }, "funding": { "url": "https://github.com/prettier/prettier?sponsor=1" } }, "node_modules/prettier-plugin-organize-imports": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-3.2.2.tgz", - "integrity": "sha512-e97lE6odGSiHonHJMTYC0q0iLXQyw0u5z/PJpvP/3vRy6/Zi9kLBwFAbEGjDzIowpjQv8b+J04PDamoUSQbzGA==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-3.2.4.tgz", + "integrity": "sha512-6m8WBhIp0dfwu0SkgfOxJqh+HpdyfqSSLfKKRZSFbDuEQXDDndb8fTpRWkUrX/uBenkex3MgnVk0J3b3Y5byog==", "dev": true, "peerDependencies": { "@volar/vue-language-plugin-pug": "^1.0.4", @@ -8309,91 +8810,39 @@ "dev": true }, "node_modules/pretty-quick": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/pretty-quick/-/pretty-quick-3.1.3.tgz", - "integrity": "sha512-kOCi2FJabvuh1as9enxYmrnBC6tVMoVOenMaBqRfsvBHB0cbpYHjdQEpSglpASDFEXVwplpcGR4CLEaisYAFcA==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pretty-quick/-/pretty-quick-4.0.0.tgz", + "integrity": "sha512-M+2MmeufXb/M7Xw3Afh1gxcYpj+sK0AxEfnfF958ktFeAyi5MsKY5brymVURQLgPLV1QaF5P4pb2oFJ54H3yzQ==", "dev": true, "dependencies": { - "chalk": "^3.0.0", - "execa": "^4.0.0", - "find-up": "^4.1.0", - "ignore": "^5.1.4", - "mri": "^1.1.5", - "multimatch": "^4.0.0" + "execa": "^5.1.1", + "find-up": "^5.0.0", + "ignore": "^5.3.0", + "mri": "^1.2.0", + "picocolors": "^1.0.0", + "picomatch": "^3.0.1", + "tslib": "^2.6.2" }, "bin": { - "pretty-quick": "bin/pretty-quick.js" + "pretty-quick": "lib/cli.mjs" }, "engines": { - "node": ">=10.13" + "node": ">=14" }, "peerDependencies": { - "prettier": ">=2.0.0" + "prettier": "^3.0.0" } }, - "node_modules/pretty-quick/node_modules/chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "node_modules/pretty-quick/node_modules/picomatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-3.0.1.tgz", + "integrity": "sha512-I3EurrIQMlRc9IaAZnqRR044Phh2DXY+55o7uJ0V+hYZAcQYSuFWsc9q5PvyDHUSCe1Qxn/iBz+78s86zWnGag==", "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, "engines": { - "node": ">=8" - } - }, - "node_modules/pretty-quick/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pretty-quick/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pretty-quick/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pretty-quick/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" + "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/prop-types": { @@ -8406,26 +8855,21 @@ "react-is": "^16.13.1" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/psl": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", "dev": true }, - "node_modules/pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, "node_modules/punycode": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", - "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, "engines": { "node": ">=6" @@ -8542,11 +8986,11 @@ } }, "node_modules/react-remove-scroll": { - "version": "2.5.5", - "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz", - "integrity": "sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==", + "version": "2.5.7", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.7.tgz", + "integrity": "sha512-FnrTWO4L7/Bhhf3CYBNArEG/yROV0tKmTv7/3h9QCFvH6sndeFf1wPqOcbFVu5VAulS5dV1wGT3GZZ/1GawqiA==", "dependencies": { - "react-remove-scroll-bar": "^2.3.3", + "react-remove-scroll-bar": "^2.3.4", "react-style-singleton": "^2.2.1", "tslib": "^2.1.0", "use-callback-ref": "^1.3.0", @@ -8586,43 +9030,30 @@ } } }, - "node_modules/react-resize-detector": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/react-resize-detector/-/react-resize-detector-8.1.0.tgz", - "integrity": "sha512-S7szxlaIuiy5UqLhLL1KY3aoyGHbZzsTpYal9eYMwCyKqoqoVLCmIgAgNyIM1FhnP2KyBygASJxdhejrzjMb+w==", - "dev": true, - "dependencies": { - "lodash": "^4.17.21" - }, - "peerDependencies": { - "react": "^16.0.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0" - } - }, "node_modules/react-router": { - "version": "6.10.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.10.0.tgz", - "integrity": "sha512-Nrg0BWpQqrC3ZFFkyewrflCud9dio9ME3ojHCF/WLsprJVzkq3q3UeEhMCAW1dobjeGbWgjNn/PVF6m46ANxXQ==", + "version": "6.21.3", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.21.3.tgz", + "integrity": "sha512-a0H638ZXULv1OdkmiK6s6itNhoy33ywxmUFT/xtSoVyf9VnC7n7+VT4LjVzdIHSaF5TIh9ylUgxMXksHTgGrKg==", "dependencies": { - "@remix-run/router": "1.5.0" + "@remix-run/router": "1.14.2" }, "engines": { - "node": ">=14" + "node": ">=14.0.0" }, "peerDependencies": { "react": ">=16.8" } }, "node_modules/react-router-dom": { - "version": "6.10.0", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.10.0.tgz", - "integrity": "sha512-E5dfxRPuXKJqzwSe/qGcqdwa18QiWC6f3H3cWXM24qj4N0/beCIf/CWTipop2xm7mR0RCS99NnaqPNjHtrAzCg==", + "version": "6.21.3", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.21.3.tgz", + "integrity": "sha512-kNzubk7n4YHSrErzjLK72j0B5i969GsuCGazRl3G6j1zqZBLjuSlYBdVdkDOgzGdPIffUOc9nmgiadTEVoq91g==", "dependencies": { - "@remix-run/router": "1.5.0", - "react-router": "6.10.0" + "@remix-run/router": "1.14.2", + "react-router": "6.21.3" }, "engines": { - "node": ">=14" + "node": ">=14.0.0" }, "peerDependencies": { "react": ">=16.8", @@ -8630,12 +9061,12 @@ } }, "node_modules/react-smooth": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-2.0.2.tgz", - "integrity": "sha512-pgqSp1q8rAGtF1bXQE0m3CHGLNfZZh5oA5o1tsPLXRHnKtkujMIJ8Ws5nO1mTySZf1c4vgwlEk+pHi3Ln6eYLw==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-2.0.5.tgz", + "integrity": "sha512-BMP2Ad42tD60h0JW6BFaib+RJuV5dsXJK9Baxiv/HlNFjvRLqA9xrNKxVWnUIZPQfzUwGXIlU/dSYLU+54YGQA==", "dev": true, "dependencies": { - "fast-equals": "^4.0.3", + "fast-equals": "^5.0.0", "react-transition-group": "2.9.0" }, "peerDependencies": { @@ -8748,23 +9179,22 @@ } }, "node_modules/recharts": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.5.0.tgz", - "integrity": "sha512-0EQYz3iA18r1Uq8VqGZ4dABW52AKBnio37kJgnztIqprELJXpOEsa0SzkqU1vjAhpCXCv52Dx1hiL9119xsqsQ==", + "version": "2.10.4", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.10.4.tgz", + "integrity": "sha512-/Q7/wdf8bW91lN3NEeCjL9RWfaiXQViJFgdnas4Eix/I8B9HAI3tHHK/CW/zDfgRMh4fzW1zlfjoz1IAapLO1Q==", "dev": true, "dependencies": { - "classnames": "^2.2.5", + "clsx": "^2.0.0", "eventemitter3": "^4.0.1", "lodash": "^4.17.19", "react-is": "^16.10.2", - "react-resize-detector": "^8.0.4", - "react-smooth": "^2.0.2", + "react-smooth": "^2.0.5", "recharts-scale": "^0.4.4", - "reduce-css-calc": "^2.1.8", + "tiny-invariant": "^1.3.1", "victory-vendor": "^36.6.8" }, "engines": { - "node": ">=12" + "node": ">=14" }, "peerDependencies": { "prop-types": "^15.6.0", @@ -8794,14 +9224,24 @@ "node": ">=8" } }, - "node_modules/reduce-css-calc": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/reduce-css-calc/-/reduce-css-calc-2.1.8.tgz", - "integrity": "sha512-8liAVezDmUcH+tdzoEGrhfbGcP7nOV4NkGE3a74+qqvE7nt9i4sKLGBuZNOnpI4WiGksiNPklZxva80061QiPg==", + "node_modules/reflect.getprototypeof": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.4.tgz", + "integrity": "sha512-ECkTw8TmJwW60lOTR+ZkODISW6RQ8+2CL3COqtiJKLd6MmB45hN51HprHFziKLGkAuTGQhBb91V8cy+KHlaCjw==", "dev": true, "dependencies": { - "css-unit-converter": "^1.1.1", - "postcss-value-parser": "^3.3.0" + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", + "globalthis": "^1.0.3", + "which-builtin-type": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/regenerate": { @@ -8811,9 +9251,9 @@ "dev": true }, "node_modules/regenerate-unicode-properties": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz", - "integrity": "sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==", + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz", + "integrity": "sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q==", "dev": true, "dependencies": { "regenerate": "^1.4.2" @@ -8823,28 +9263,28 @@ } }, "node_modules/regenerator-runtime": { - "version": "0.13.11", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", - "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" }, "node_modules/regenerator-transform": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.1.tgz", - "integrity": "sha512-knzmNAcuyxV+gQCufkYcvOqX/qIIfHLv0u5x79kRxuGojfYVky1f15TzZEu2Avte8QGepvUNTnLskf8E6X6Vyg==", + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", + "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==", "dev": true, "dependencies": { "@babel/runtime": "^7.8.4" } }, "node_modules/regexp.prototype.flags": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.0.tgz", - "integrity": "sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", + "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==", "dev": true, "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.2.0", - "functions-have-names": "^1.2.3" + "set-function-name": "^2.0.0" }, "engines": { "node": ">= 0.4" @@ -8892,18 +9332,9 @@ } }, "node_modules/remove-accents": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.4.2.tgz", - "integrity": "sha512-7pXIJqJOq5tFgG1A2Zxti3Ht8jJF337m4sowbuHsW30ZnkQFnDzy9qBNhgzX8ZLW4+UBcXiiR7SwR6pokHsxiA==" - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.5.0.tgz", + "integrity": "sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A==" }, "node_modules/requires-port": { "version": "1.0.0", @@ -8912,11 +9343,11 @@ "dev": true }, "node_modules/resolve": { - "version": "1.22.2", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", - "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==", + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", "dependencies": { - "is-core-module": "^2.11.0", + "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, @@ -8960,18 +9391,34 @@ } }, "node_modules/rollup": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.21.0.tgz", - "integrity": "sha512-ANPhVcyeHvYdQMUyCbczy33nbLzI7RzrBje4uvNiTDJGIMtlKoOStmympwr9OtS1LZxiDmE2wvxHyVhoLtf1KQ==", + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.9.6.tgz", + "integrity": "sha512-05lzkCS2uASX0CiLFybYfVkwNbKZG5NFQ6Go0VWyogFTXXbR039UVsegViTntkk4OglHBdF54ccApXRRuXRbsg==", "dev": true, + "dependencies": { + "@types/estree": "1.0.5" + }, "bin": { "rollup": "dist/bin/rollup" }, "engines": { - "node": ">=14.18.0", + "node": ">=18.0.0", "npm": ">=8.0.0" }, "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.9.6", + "@rollup/rollup-android-arm64": "4.9.6", + "@rollup/rollup-darwin-arm64": "4.9.6", + "@rollup/rollup-darwin-x64": "4.9.6", + "@rollup/rollup-linux-arm-gnueabihf": "4.9.6", + "@rollup/rollup-linux-arm64-gnu": "4.9.6", + "@rollup/rollup-linux-arm64-musl": "4.9.6", + "@rollup/rollup-linux-riscv64-gnu": "4.9.6", + "@rollup/rollup-linux-x64-gnu": "4.9.6", + "@rollup/rollup-linux-x64-musl": "4.9.6", + "@rollup/rollup-win32-arm64-msvc": "4.9.6", + "@rollup/rollup-win32-ia32-msvc": "4.9.6", + "@rollup/rollup-win32-x64-msvc": "4.9.6", "fsevents": "~2.3.2" } }, @@ -9004,16 +9451,37 @@ "queue-microtask": "^1.2.2" } }, - "node_modules/safe-regex-test": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", - "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==", + "node_modules/safe-array-concat": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.0.tgz", + "integrity": "sha512-ZdQ0Jeb9Ofti4hbt5lX3T2JcAamT9hfzYU1MNB+z/jaEbB6wfFfPIR/zEORmZqobkCCJhSjodobH6WHNmJ97dg==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.3", + "call-bind": "^1.0.5", + "get-intrinsic": "^1.2.2", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.2.tgz", + "integrity": "sha512-83S9w6eFq12BBIJYvjMux6/dkirb8+4zJRA9cxNBVb7Wq5fJBW+Xze48WqR8pxua7bDuAaaAxtVVd4Idjp1dBQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.5", + "get-intrinsic": "^1.2.2", "is-regex": "^1.1.4" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -9025,9 +9493,9 @@ "dev": true }, "node_modules/sass": { - "version": "1.62.1", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.62.1.tgz", - "integrity": "sha512-NHpxIzN29MXvWiuswfc1W3I0N8SXBd8UR26WntmDlRYf0bSADnwnOjsyMZ3lMezSlArD33Vs3YFhp7dWvL770A==", + "version": "1.70.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.70.0.tgz", + "integrity": "sha512-uUxNQ3zAHeAx5nRFskBnrWzDUJrrvpCPD5FNAoRvTi0WwremlheES3tg+56PaVtCs5QDRX5CBLxxKMDJMEa1WQ==", "dev": true, "dependencies": { "chokidar": ">=3.0.0 <4.0.0", @@ -9062,14 +9530,44 @@ } }, "node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" } }, + "node_modules/set-function-length": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.0.tgz", + "integrity": "sha512-4DBHDoyHlM1IRPGYcoxexgh67y4ueR53FKV1yyxwFMY7aCqcN/38M1+SwZ/qJQ8iLv7+ck385ot4CcisOAPT9w==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.1", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.2", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz", + "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==", + "dev": true, + "dependencies": { + "define-data-property": "^1.0.1", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -9118,13 +9616,13 @@ "dev": true }, "node_modules/sirv": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.3.tgz", - "integrity": "sha512-O9jm9BsID1P+0HOi81VpXPoDxYP374pkOLzACAoyUQ/3OUVndNpsz6wMnY2z+yOxzbllCKZrM+9QrWsv4THnyA==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", + "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==", "dev": true, "dependencies": { - "@polka/url": "^1.0.0-next.20", - "mrmime": "^1.0.0", + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", "totalist": "^3.0.0" }, "engines": { @@ -9141,23 +9639,23 @@ } }, "node_modules/socket.io-client": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.6.1.tgz", - "integrity": "sha512-5UswCV6hpaRsNg5kkEHVcbBIXEYoVbMQaHJBXJCyEQ+CiFPV1NIOY0XOFWG4XR4GZcB8Kn6AsRs/9cy9TbqVMQ==", + "version": "4.7.4", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.4.tgz", + "integrity": "sha512-wh+OkeF0rAVCrABWQBaEjLfb7DVPotMbu0cgWgyR0v6eA4EoVnAwcIeIbcdTE3GT/H3kbdLl7OoH2+asoDRIIg==", "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.2", - "engine.io-client": "~6.4.0", - "socket.io-parser": "~4.2.1" + "engine.io-client": "~6.5.2", + "socket.io-parser": "~4.2.4" }, "engines": { "node": ">=10.0.0" } }, "node_modules/socket.io-parser": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.3.tgz", - "integrity": "sha512-JMafRntWVO2DCJimKsRTh/wnqVvO4hrfwOqtO7f+uzwsQMuxO6VwImtYxaQ+ieoyshWOTJyV0fA21lccEXRPpQ==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.1" @@ -9212,9 +9710,9 @@ "dev": true }, "node_modules/std-env": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.3.2.tgz", - "integrity": "sha512-uUZI65yrV2Qva5gqE0+A7uVAvO40iPo6jGhs7s8keRfHCmtg+uB2X6EiLGCI9IgL1J17xGhvoOqSz79lzICPTA==", + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.7.0.tgz", + "integrity": "sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==", "dev": true }, "node_modules/stop-iteration-iterator": { @@ -9235,39 +9733,20 @@ "integrity": "sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==", "dev": true }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, "node_modules/string.prototype.matchall": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.8.tgz", - "integrity": "sha512-6zOCOcJ+RJAQshcTvXPHoxoQGONa3e/Lqx90wUA+wEzX78sg5Bo+1tQo4N0pohS0erG9qtCqJDjNCQBjeWVxyg==", + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.10.tgz", + "integrity": "sha512-rGXbGmOEosIQi6Qva94HUjgPs9vKW+dkG7Y8Q5O2OYkWL6wFaTRZO8zM4mhP94uX55wgyrXzfS2aGtGzUL7EJQ==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", - "get-intrinsic": "^1.1.3", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", "has-symbols": "^1.0.3", - "internal-slot": "^1.0.3", - "regexp.prototype.flags": "^1.4.3", + "internal-slot": "^1.0.5", + "regexp.prototype.flags": "^1.5.0", + "set-function-name": "^2.0.0", "side-channel": "^1.0.4" }, "funding": { @@ -9275,14 +9754,14 @@ } }, "node_modules/string.prototype.trim": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.7.tgz", - "integrity": "sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz", + "integrity": "sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" }, "engines": { "node": ">= 0.4" @@ -9292,28 +9771,28 @@ } }, "node_modules/string.prototype.trimend": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz", - "integrity": "sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz", + "integrity": "sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/string.prototype.trimstart": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz", - "integrity": "sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz", + "integrity": "sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -9374,21 +9853,21 @@ } }, "node_modules/strip-literal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-1.0.1.tgz", - "integrity": "sha512-QZTsipNpa2Ppr6v1AmJHESqJ3Uz247MUS0OjrnnZjFAvEoWqxuyFuXn2xLgMtRnijJShAa1HL0gtJyUs7u7n3Q==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-1.3.0.tgz", + "integrity": "sha512-PugKzOsyXpArk0yWmUwqOZecSO0GH0bPoctLcqNDH9J04pVW3lflYE0ujElBGTloevcxF5MofAOZ7C5l2b+wLg==", "dev": true, "dependencies": { - "acorn": "^8.8.2" + "acorn": "^8.10.0" }, "funding": { "url": "https://github.com/sponsors/antfu" } }, "node_modules/stylis": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.1.3.tgz", - "integrity": "sha512-GP6WDNWf+o403jrEp9c5jibKavrtLW+/qYGhFxFrG8maXhwTBI7gLLhiBb0o7uFccWN+EOS9aMO6cGHWAO07OA==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", "peer": true }, "node_modules/supports-color": { @@ -9421,9 +9900,9 @@ "dev": true }, "node_modules/tabbable": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.1.1.tgz", - "integrity": "sha512-4kl5w+nCB44EVRdO0g/UGoOp3vlwgycUVtkk/7DPyeLZUCuNFFKCFG6/t/DgHLrUPHjrZg6s5tNm+56Q2B0xyg==" + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==" }, "node_modules/test-exclude": { "version": "6.0.0", @@ -9445,15 +9924,6 @@ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, - "node_modules/time-zone": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/time-zone/-/time-zone-1.0.0.tgz", - "integrity": "sha512-TIsDdtKo6+XrPtiTm1ssmMngN1sAhyKnTO2kunQWqNPWIVvCm15Wmw4SWInwTVgJ5u/Tr04+8Ei9TNcw4x4ONA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/tiny-invariant": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz", @@ -9461,24 +9931,24 @@ "dev": true }, "node_modules/tinybench": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.4.0.tgz", - "integrity": "sha512-iyziEiyFxX4kyxSp+MtY1oCH/lvjH3PxFN8PGCDeqcZWAJ/i+9y+nL85w99PxVzrIvew/GSkSbDYtiGVa85Afg==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.6.0.tgz", + "integrity": "sha512-N8hW3PG/3aOoZAN5V/NSAEDz0ZixDSSt5b/a05iqtpgfLWMSVuCo7w0k2vVvEjdrIoeGqZzweX2WlyioNIHchA==", "dev": true }, "node_modules/tinypool": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.4.0.tgz", - "integrity": "sha512-2ksntHOKf893wSAH4z/+JbPpi92esw8Gn9N2deXX+B0EO92hexAVI9GIZZPx7P5aYo5KULfeOSt3kMOmSOy6uA==", + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.2.tgz", + "integrity": "sha512-SUszKYe5wgsxnNOVlBYO6IC+8VGWdVGZWAqUxp3UErNBtptZvWbwyUOyzNL59zigz2rCA92QiL3wvG+JDSdJdQ==", "dev": true, "engines": { "node": ">=14.0.0" } }, "node_modules/tinyspy": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.1.0.tgz", - "integrity": "sha512-7eORpyqImoOvkQJCSkL0d0mB4NHHIFAy4b1u8PHdDa7SjGS2njzl6/lyGoZLm+eyYEtlUmFGE0rFj66SWxZgQQ==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.0.tgz", + "integrity": "sha512-d2eda04AN/cPOR89F7Xv5bK/jrQEhmcLFe6HFldoeO9AJtps+fqEnh486vnT/8y4bw38pSyxDcTCAq+Ks2aJTg==", "dev": true, "engines": { "node": ">=14.0.0" @@ -9514,9 +9984,9 @@ } }, "node_modules/tough-cookie": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.2.tgz", - "integrity": "sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", + "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", "dev": true, "dependencies": { "psl": "^1.1.33", @@ -9529,21 +9999,21 @@ } }, "node_modules/tr46": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", - "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz", + "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", "dev": true, "dependencies": { - "punycode": "^2.3.0" + "punycode": "^2.3.1" }, "engines": { - "node": ">=14" + "node": ">=18" } }, "node_modules/tsconfig-paths": { - "version": "3.14.2", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz", - "integrity": "sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==", + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", "dev": true, "dependencies": { "@types/json5": "^0.0.29", @@ -9565,9 +10035,9 @@ } }, "node_modules/tslib": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", - "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "node_modules/tsutils": { "version": "3.21.0", @@ -9623,6 +10093,57 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/typed-array-buffer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz", + "integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz", + "integrity": "sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz", + "integrity": "sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/typed-array-length": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", @@ -9638,22 +10159,22 @@ } }, "node_modules/typescript": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", - "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", + "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=12.20" + "node": ">=14.17" } }, "node_modules/ufo": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.1.1.tgz", - "integrity": "sha512-MvlCc4GHrmZdAllBc0iUDowff36Q9Ndw/UzqmEKyrfSzokTd9ZCy1i+IIk5hrYKkjoYVQyNbrw7/F8XJ2rEwTg==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.3.2.tgz", + "integrity": "sha512-o+ORpgGwaYQXgqGDwd+hkS4PuZ3QnmqMMxRuajK/a38L6fTpcE5GPIfrf+L/KemFzfUpeUQc1rRS1iDBozvnFA==", "dev": true }, "node_modules/unbox-primitive": { @@ -9671,6 +10192,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", @@ -9730,9 +10257,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", - "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", "dev": true, "funding": [ { @@ -9779,9 +10306,9 @@ } }, "node_modules/use-callback-ref": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.0.tgz", - "integrity": "sha512-3FT9PRuRdbB9HfXhEq35u4oZkvpJ5kuYbpqhCfmiZyReuRgpnhDlbr2ZEnnuS0RrJAPn6l23xjFg9kpDM+Ms7w==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.1.tgz", + "integrity": "sha512-Lg4Vx1XZQauB42Hw3kK7JM6yjVjgFmFC5/Ab797s79aARomD2nEErc4mCgM8EZrARLmmbWpi5DGCadmK50DcAQ==", "dependencies": { "tslib": "^2.0.0" }, @@ -9857,23 +10384,29 @@ } }, "node_modules/v8-to-istanbul": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz", - "integrity": "sha512-6z3GW9x8G1gd+JIIgQQQxXuiJtCXeAjp6RaPEPLv62mH3iPHPxV6W3robxtCzNErRo6ZwTmzWhsbNvjyEBKzKA==", + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.2.0.tgz", + "integrity": "sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA==", "dev": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.12", "@types/istanbul-lib-coverage": "^2.0.1", - "convert-source-map": "^1.6.0" + "convert-source-map": "^2.0.0" }, "engines": { "node": ">=10.12.0" } }, + "node_modules/v8-to-istanbul/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, "node_modules/victory-vendor": { - "version": "36.6.8", - "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.6.8.tgz", - "integrity": "sha512-H3kyQ+2zgjMPvbPqAl7Vwm2FD5dU7/4bCTQakFQnpIsfDljeOMDojRsrmJfwh4oAlNnWhpAf+mbAoLh8u7dwyQ==", + "version": "36.8.2", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.8.2.tgz", + "integrity": "sha512-NfSQi7ISCdBbDpn3b6rg+8RpFZmWIM9mcks48BbogHE2F6h1XKdA34oiCKP5hP1OGvTotDRzsexiJKzrK4Exuw==", "dev": true, "dependencies": { "@types/d3-array": "^3.0.3", @@ -9893,27 +10426,31 @@ } }, "node_modules/vite": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.3.2.tgz", - "integrity": "sha512-9R53Mf+TBoXCYejcL+qFbZde+eZveQLDYd9XgULILLC1a5ZwPaqgmdVpL8/uvw2BM/1TzetWjglwm+3RO+xTyw==", + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.0.12.tgz", + "integrity": "sha512-4hsnEkG3q0N4Tzf1+t6NdN9dg/L3BM+q8SWgbSPnJvrgH2kgdyzfVJwbR1ic69/4uMJJ/3dqDZZE5/WwqW8U1w==", "dev": true, "dependencies": { - "esbuild": "^0.17.5", - "postcss": "^8.4.21", - "rollup": "^3.21.0" + "esbuild": "^0.19.3", + "postcss": "^8.4.32", + "rollup": "^4.2.0" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^14.18.0 || >=16.0.0" + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" }, "optionalDependencies": { - "fsevents": "~2.3.2" + "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": ">= 14", + "@types/node": "^18.0.0 || >=20.0.0", "less": "*", + "lightningcss": "^1.21.0", "sass": "*", "stylus": "*", "sugarss": "*", @@ -9926,6 +10463,9 @@ "less": { "optional": true }, + "lightningcss": { + "optional": true + }, "sass": { "optional": true }, @@ -9941,32 +10481,31 @@ } }, "node_modules/vite-node": { - "version": "0.30.1", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.30.1.tgz", - "integrity": "sha512-vTikpU/J7e6LU/8iM3dzBo8ZhEiKZEKRznEMm+mJh95XhWaPrJQraT/QsT2NWmuEf+zgAoMe64PKT7hfZ1Njmg==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.2.2.tgz", + "integrity": "sha512-1as4rDTgVWJO3n1uHmUYqq7nsFgINQ9u+mRcXpjeOMJUmviqNKjcZB7UfRZrlM7MjYXMKpuWp5oGkjaFLnjawg==", "dev": true, "dependencies": { "cac": "^6.7.14", "debug": "^4.3.4", - "mlly": "^1.2.0", - "pathe": "^1.1.0", + "pathe": "^1.1.1", "picocolors": "^1.0.0", - "vite": "^3.0.0 || ^4.0.0" + "vite": "^5.0.0" }, "bin": { "vite-node": "vite-node.mjs" }, "engines": { - "node": ">=v14.18.0" + "node": "^18.0.0 || >=20.0.0" }, "funding": { - "url": "https://github.com/sponsors/antfu" + "url": "https://opencollective.com/vitest" } }, "node_modules/vite-plugin-checker": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/vite-plugin-checker/-/vite-plugin-checker-0.5.6.tgz", - "integrity": "sha512-ftRyON0gORUHDxcDt2BErmsikKSkfvl1i2DoP6Jt2zDO9InfvM6tqO1RkXhSjkaXEhKPea6YOnhFaZxW3BzudQ==", + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/vite-plugin-checker/-/vite-plugin-checker-0.6.2.tgz", + "integrity": "sha512-YvvvQ+IjY09BX7Ab+1pjxkELQsBd4rPhWNw8WLBeFVxu/E7O+n6VYAqNsKdK/a2luFlX/sMpoWdGFfg4HvwdJQ==", "dev": true, "dependencies": { "@babel/code-frame": "^7.12.13", @@ -9979,6 +10518,7 @@ "lodash.debounce": "^4.0.8", "lodash.pick": "^4.4.0", "npm-run-path": "^4.0.1", + "semver": "^7.5.0", "strip-ansi": "^6.0.0", "tiny-invariant": "^1.1.0", "vscode-languageclient": "^7.0.0", @@ -9998,7 +10538,7 @@ "vite": ">=2.0.0", "vls": "*", "vti": "*", - "vue-tsc": "*" + "vue-tsc": ">=1.3.9" }, "peerDependenciesMeta": { "eslint": { @@ -10027,62 +10567,91 @@ } } }, - "node_modules/vitest": { - "version": "0.30.1", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.30.1.tgz", - "integrity": "sha512-y35WTrSTlTxfMLttgQk4rHcaDkbHQwDP++SNwPb+7H8yb13Q3cu2EixrtHzF27iZ8v0XCciSsLg00RkPAzB/aA==", + "node_modules/vite-plugin-checker/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "dev": true, "dependencies": { - "@types/chai": "^4.3.4", - "@types/chai-subset": "^1.3.3", - "@types/node": "*", - "@vitest/expect": "0.30.1", - "@vitest/runner": "0.30.1", - "@vitest/snapshot": "0.30.1", - "@vitest/spy": "0.30.1", - "@vitest/utils": "0.30.1", - "acorn": "^8.8.2", - "acorn-walk": "^8.2.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/vite-plugin-checker/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/vite-plugin-checker/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/vitest": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.2.2.tgz", + "integrity": "sha512-d5Ouvrnms3GD9USIK36KG8OZ5bEvKEkITFtnGv56HFaSlbItJuYr7hv2Lkn903+AvRAgSixiamozUVfORUekjw==", + "dev": true, + "dependencies": { + "@vitest/expect": "1.2.2", + "@vitest/runner": "1.2.2", + "@vitest/snapshot": "1.2.2", + "@vitest/spy": "1.2.2", + "@vitest/utils": "1.2.2", + "acorn-walk": "^8.3.2", "cac": "^6.7.14", - "chai": "^4.3.7", - "concordance": "^5.0.4", + "chai": "^4.3.10", "debug": "^4.3.4", - "local-pkg": "^0.4.3", - "magic-string": "^0.30.0", - "pathe": "^1.1.0", + "execa": "^8.0.1", + "local-pkg": "^0.5.0", + "magic-string": "^0.30.5", + "pathe": "^1.1.1", "picocolors": "^1.0.0", - "source-map": "^0.6.1", - "std-env": "^3.3.2", - "strip-literal": "^1.0.1", - "tinybench": "^2.4.0", - "tinypool": "^0.4.0", - "vite": "^3.0.0 || ^4.0.0", - "vite-node": "0.30.1", + "std-env": "^3.5.0", + "strip-literal": "^1.3.0", + "tinybench": "^2.5.1", + "tinypool": "^0.8.2", + "vite": "^5.0.0", + "vite-node": "1.2.2", "why-is-node-running": "^2.2.2" }, "bin": { "vitest": "vitest.mjs" }, "engines": { - "node": ">=v14.18.0" + "node": "^18.0.0 || >=20.0.0" }, "funding": { - "url": "https://github.com/sponsors/antfu" + "url": "https://opencollective.com/vitest" }, "peerDependencies": { "@edge-runtime/vm": "*", - "@vitest/browser": "*", - "@vitest/ui": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "^1.0.0", + "@vitest/ui": "^1.0.0", "happy-dom": "*", - "jsdom": "*", - "playwright": "*", - "safaridriver": "*", - "webdriverio": "*" + "jsdom": "*" }, "peerDependenciesMeta": { "@edge-runtime/vm": { "optional": true }, + "@types/node": { + "optional": true + }, "@vitest/browser": { "optional": true }, @@ -10094,25 +10663,141 @@ }, "jsdom": { "optional": true - }, - "playwright": { - "optional": true - }, - "safaridriver": { - "optional": true - }, - "webdriverio": { - "optional": true } } }, - "node_modules/vitest/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "node_modules/vitest/node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/vitest/node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", "dev": true, "engines": { - "node": ">=0.10.0" + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/vitest/node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/vitest/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/vitest/node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/vitest/node_modules/npm-run-path": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.2.0.tgz", + "integrity": "sha512-W4/tgAXFqFA0iL7fk0+uQ3g7wkL8xJmx3XdK0VGb4cHW//eZTtKGvFBBoRKVTpY7n6ze4NL9ly7rgXcHufqXKg==", + "dev": true, + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/vitest/node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/vitest/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/vitest/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/vitest/node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/vscode-jsonrpc": { @@ -10151,9 +10836,9 @@ } }, "node_modules/vscode-languageclient/node_modules/semver": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz", - "integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -10194,9 +10879,9 @@ } }, "node_modules/vscode-languageserver-textdocument": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.8.tgz", - "integrity": "sha512-1bonkGqQs5/fxGT5UchTgjGVnfysL0O8v1AYMBjqTbWQTFn721zaPGDYFkOKtfDgFiSgXM3KwaG3FMGfW4Ed9Q==", + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.11.tgz", + "integrity": "sha512-X+8T3GoiwTVlJbicx/sIAF+yuJAqz8VvwJyoMVhwEMoEKE/fkDmrqUgDMyBECcM2A2frVZIUj5HI/ErRXCfOeA==", "dev": true }, "node_modules/vscode-languageserver-types": { @@ -10206,21 +10891,21 @@ "dev": true }, "node_modules/vscode-uri": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.7.tgz", - "integrity": "sha512-eOpPHogvorZRobNqJGhapa0JdwaxpjVvyBp0QIUMRMSf8ZAlqOdEquKuRmw9Qwu0qXtJIWqFtMkmvJjUZmMjVA==", + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz", + "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==", "dev": true }, "node_modules/w3c-xmlserializer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", - "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", "dev": true, "dependencies": { - "xml-name-validator": "^4.0.0" + "xml-name-validator": "^5.0.0" }, "engines": { - "node": ">=14" + "node": ">=18" } }, "node_modules/webidl-conversions": { @@ -10232,47 +10917,38 @@ "node": ">=12" } }, - "node_modules/well-known-symbols": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/well-known-symbols/-/well-known-symbols-2.0.0.tgz", - "integrity": "sha512-ZMjC3ho+KXo0BfJb7JgtQ5IBuvnShdlACNkKkdsqBmYw3bPAaJfPeYUo6tLUaT5tG/Gkh7xkpBhKRQ9e7pyg9Q==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/whatwg-encoding": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", - "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", "dev": true, "dependencies": { "iconv-lite": "0.6.3" }, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/whatwg-mimetype": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", - "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", "dev": true, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/whatwg-url": { - "version": "12.0.1", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-12.0.1.tgz", - "integrity": "sha512-Ed/LrqB8EPlGxjS+TrsXcpUond1mhccS3pchLhzSgPCnTimUCKj3IZE75pAs5m6heB2U2TMerKFUXheyHY+VDQ==", + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.0.0.tgz", + "integrity": "sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==", "dev": true, "dependencies": { - "tr46": "^4.1.1", + "tr46": "^5.0.0", "webidl-conversions": "^7.0.0" }, "engines": { - "node": ">=14" + "node": ">=18" } }, "node_modules/which": { @@ -10306,6 +10982,32 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/which-builtin-type": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.1.3.tgz", + "integrity": "sha512-YmjsSMDBYsM1CaFiayOVT06+KJeXf0o5M/CAd4o1lTadFAtacTUM49zoYxr/oroopFDfhvN6iEcBxUyc3gvKmw==", + "dev": true, + "dependencies": { + "function.prototype.name": "^1.1.5", + "has-tostringtag": "^1.0.0", + "is-async-function": "^2.0.0", + "is-date-object": "^1.0.5", + "is-finalizationregistry": "^1.0.2", + "is-generator-function": "^1.0.10", + "is-regex": "^1.1.4", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/which-collection": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz", @@ -10322,17 +11024,16 @@ } }, "node_modules/which-typed-array": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz", - "integrity": "sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz", + "integrity": "sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==", "dev": true, "dependencies": { "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", + "call-bind": "^1.0.4", "for-each": "^0.3.3", "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0", - "is-typed-array": "^1.1.10" + "has-tostringtag": "^1.0.0" }, "engines": { "node": ">= 0.4" @@ -10357,41 +11058,15 @@ "node": ">=8" } }, - "node_modules/word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "node_modules/ws": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", - "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", + "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", "dev": true, "engines": { "node": ">=10.0.0" @@ -10410,12 +11085,12 @@ } }, "node_modules/xml-name-validator": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", - "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", "dev": true, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/xmlchars": { @@ -10432,15 +11107,6 @@ "node": ">=0.4.0" } }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, - "engines": { - "node": ">=10" - } - }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -10448,41 +11114,14 @@ "dev": true }, "node_modules/yaml": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.1.tgz", - "integrity": "sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==", + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", + "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==", "dev": true, "engines": { "node": ">= 14" } }, - "node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "dev": true, - "dependencies": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "dev": true, - "engines": { - "node": ">=10" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 2f5089f01..73edda3f6 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,56 +14,57 @@ "private": true, "dependencies": { "@mantine/core": "^6.0.0", - "@mantine/dropzone": "^6.0.0", - "@mantine/form": "^6.0.0", + "@mantine/dropzone": "^6.0.21", + "@mantine/form": "^6.0.21", "@mantine/hooks": "^6.0.0", - "@mantine/modals": "^6.0.0", - "@mantine/notifications": "^6.0.0", - "axios": "^0.27.2", + "@mantine/modals": "^6.0.21", + "@mantine/notifications": "^6.0.21", + "axios": "^1.6.7", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-query": "^3.39.2", - "react-router-dom": "~6.10.0", - "socket.io-client": "^4.5.3" + "react-query": "^3.39.3", + "react-router-dom": "^6.21.3", + "socket.io-client": "^4.7.4" }, "devDependencies": { - "@fontsource/roboto": "^4.5.0", - "@fortawesome/fontawesome-svg-core": "^6.2.0", - "@fortawesome/free-brands-svg-icons": "^6.2.0", - "@fortawesome/free-regular-svg-icons": "^6.2.0", - "@fortawesome/free-solid-svg-icons": "^6.2.0", + "@fontsource/roboto": "^5.0.8", + "@fortawesome/fontawesome-svg-core": "^6.5.1", + "@fortawesome/free-brands-svg-icons": "^6.5.1", + "@fortawesome/free-regular-svg-icons": "^6.5.1", + "@fortawesome/free-solid-svg-icons": "^6.5.1", "@fortawesome/react-fontawesome": "^0.2.0", - "@testing-library/jest-dom": "^5.16.5", - "@testing-library/react": "^14.0.0", - "@testing-library/user-event": "^14.4.3", - "@types/lodash": "^4.14.0", - "@types/node": "^18.16.0", - "@types/react": "^18.2.0", - "@types/react-dom": "^18.2.0", - "@types/react-table": "^7.7.0", - "@vitejs/plugin-react": "^4.0.0", - "@vitest/coverage-c8": "^0.30.0", - "@vitest/ui": "^0.30.0", - "clsx": "^1.2.0", - "eslint": "^8.39.0", + "@testing-library/jest-dom": "^6.3.0", + "@testing-library/react": "^14.1.2", + "@testing-library/user-event": "^14.5.2", + "@types/jest": "^29.5.11", + "@types/lodash": "^4.14.202", + "@types/node": "^20.11.7", + "@types/react": "^18.2.48", + "@types/react-dom": "^18.2.18", + "@types/react-table": "^7.7.19", + "@vitejs/plugin-react": "^4.2.1", + "@vitest/coverage-v8": "^1.2.2", + "@vitest/ui": "^1.2.2", + "clsx": "^2.1.0", + "eslint": "^8.56.0", "eslint-config-react-app": "^7.0.1", "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-testing-library": "^5.9.0", - "husky": "^8.0.2", - "jsdom": "^21.0.0", - "lodash": "^4.17.0", - "moment": "^2.29", - "prettier": "^2.8.0", - "prettier-plugin-organize-imports": "^3.1.0", - "pretty-quick": "^3.1.0", + "eslint-plugin-testing-library": "^6.2.0", + "husky": "^9.0.6", + "jsdom": "^24.0.0", + "lodash": "^4.17.21", + "moment": "^2.30.1", + "prettier": "^3.2.4", + "prettier-plugin-organize-imports": "^3.2.4", + "pretty-quick": "^4.0.0", "react-table": "^7.8.0", - "recharts": "~2.5.0", - "sass": "^1.62.0", - "typescript": "^5", - "vite": "^4.3.0", - "vite-plugin-checker": "^0.5.5", - "vitest": "^0.30.1", - "yaml": "^2.3.1" + "recharts": "^2.10.4", + "sass": "^1.70.0", + "typescript": "^5.3.3", + "vite": "^5.0.12", + "vite-plugin-checker": "^0.6.2", + "vitest": "^1.2.2", + "yaml": "^2.3.4" }, "scripts": { "start": "vite", diff --git a/frontend/src/App/Navbar.tsx b/frontend/src/App/Navbar.tsx index af64a6b35..c626dc257 100644 --- a/frontend/src/App/Navbar.tsx +++ b/frontend/src/App/Navbar.tsx @@ -84,7 +84,7 @@ function useIsActive(parent: string, route: RouteObject) { const paths = useMemo( () => [root, ...(children?.map((v) => pathJoin(root, v.path ?? "")) ?? [])], - [root, children] + [root, children], ); const selection = useSelection().selection; @@ -92,7 +92,7 @@ function useIsActive(parent: string, route: RouteObject) { () => selection?.includes(root) || paths.some((path) => matchPath(path, pathname)), - [pathname, paths, root, selection] + [pathname, paths, root, selection], ); } @@ -338,7 +338,7 @@ const NavbarItem: FunctionComponent = ({ clsx(classes.anchor, { [classes.active]: isActive, [classes.hover]: hovered, - }) + }), ) } > diff --git a/frontend/src/App/index.tsx b/frontend/src/App/index.tsx index 87236d6c6..4e09a97da 100644 --- a/frontend/src/App/index.tsx +++ b/frontend/src/App/index.tsx @@ -40,8 +40,8 @@ const App: FunctionComponent = () => { showNotification( notification.info( "Update available", - "A new version of Bazarr is ready, restart is required" - ) + "A new version of Bazarr is ready, restart is required", + ), ); } }, []); diff --git a/frontend/src/Router/index.tsx b/frontend/src/Router/index.tsx index 8d925e524..335cf2d75 100644 --- a/frontend/src/Router/index.tsx +++ b/frontend/src/Router/index.tsx @@ -309,7 +309,7 @@ function useRoutes(): CustomRouteObject[] { data?.announcements, radarr, sonarr, - ] + ], ); } @@ -321,7 +321,7 @@ export const Router: FunctionComponent = () => { // TODO: Move this outside the function component scope const router = useMemo( () => createBrowserRouter(routes, { basename: Environment.baseUrl }), - [routes] + [routes], ); return ( diff --git a/frontend/src/apis/hooks/episodes.ts b/frontend/src/apis/hooks/episodes.ts index e063eb22c..d182dc7e6 100644 --- a/frontend/src/apis/hooks/episodes.ts +++ b/frontend/src/apis/hooks/episodes.ts @@ -19,7 +19,7 @@ const cacheEpisodes = (client: QueryClient, episodes: Item.Episode[]) => { QueryKeys.Episodes, item.sonarrEpisodeId, ], - item + item, ); }); }; @@ -33,7 +33,7 @@ export function useEpisodesByIds(ids: number[]) { onSuccess: (data) => { cacheEpisodes(client, data); }, - } + }, ); } @@ -46,20 +46,20 @@ export function useEpisodesBySeriesId(id: number) { onSuccess: (data) => { cacheEpisodes(client, data); }, - } + }, ); } export function useEpisodeWantedPagination() { return usePaginationQuery([QueryKeys.Series, QueryKeys.Wanted], (param) => - api.episodes.wanted(param) + api.episodes.wanted(param), ); } export function useEpisodeBlacklist() { return useQuery( [QueryKeys.Series, QueryKeys.Episodes, QueryKeys.Blacklist], - () => api.episodes.blacklist() + () => api.episodes.blacklist(), ); } @@ -84,7 +84,7 @@ export function useEpisodeAddBlacklist() { ]); client.invalidateQueries([QueryKeys.Series, seriesId]); }, - } + }, ); } @@ -102,7 +102,7 @@ export function useEpisodeDeleteBlacklist() { QueryKeys.Blacklist, ]); }, - } + }, ); } @@ -110,7 +110,7 @@ export function useEpisodeHistoryPagination() { return usePaginationQuery( [QueryKeys.Series, QueryKeys.Episodes, QueryKeys.History], (param) => api.episodes.history(param), - false + false, ); } @@ -121,6 +121,6 @@ export function useEpisodeHistory(episodeId?: number) { if (episodeId) { return api.episodes.historyBy(episodeId); } - } + }, ); } diff --git a/frontend/src/apis/hooks/histories.ts b/frontend/src/apis/hooks/histories.ts index 53a7340ba..d8fc0676f 100644 --- a/frontend/src/apis/hooks/histories.ts +++ b/frontend/src/apis/hooks/histories.ts @@ -6,7 +6,7 @@ export function useHistoryStats( time: History.TimeFrameOptions, action: History.ActionOptions | null, provider: System.Provider | null, - language: Language.Info | null + language: Language.Info | null, ) { return useQuery( [QueryKeys.System, QueryKeys.History, { time, action, provider, language }], @@ -15,7 +15,7 @@ export function useHistoryStats( time, action ?? undefined, provider?.name, - language?.code2 - ) + language?.code2, + ), ); } diff --git a/frontend/src/apis/hooks/languages.ts b/frontend/src/apis/hooks/languages.ts index d26c46f87..149d80716 100644 --- a/frontend/src/apis/hooks/languages.ts +++ b/frontend/src/apis/hooks/languages.ts @@ -8,7 +8,7 @@ export function useLanguages(history?: boolean) { () => api.system.languages(history), { staleTime: Infinity, - } + }, ); } @@ -18,6 +18,6 @@ export function useLanguageProfiles() { () => api.system.languagesProfileList(), { staleTime: Infinity, - } + }, ); } diff --git a/frontend/src/apis/hooks/movies.ts b/frontend/src/apis/hooks/movies.ts index 904f6557d..ee8fe1100 100644 --- a/frontend/src/apis/hooks/movies.ts +++ b/frontend/src/apis/hooks/movies.ts @@ -39,13 +39,13 @@ export function useMovies() { onSuccess: (data) => { cacheMovies(client, data); }, - } + }, ); } export function useMoviesPagination() { return usePaginationQuery([QueryKeys.Movies], (param) => - api.movies.moviesBy(param) + api.movies.moviesBy(param), ); } @@ -62,7 +62,7 @@ export function useMovieModification() { // TODO: query less client.invalidateQueries([QueryKeys.Movies]); }, - } + }, ); } @@ -75,19 +75,19 @@ export function useMovieAction() { onSuccess: () => { client.invalidateQueries([QueryKeys.Movies]); }, - } + }, ); } export function useMovieWantedPagination() { return usePaginationQuery([QueryKeys.Movies, QueryKeys.Wanted], (param) => - api.movies.wanted(param) + api.movies.wanted(param), ); } export function useMovieBlacklist() { return useQuery([QueryKeys.Movies, QueryKeys.Blacklist], () => - api.movies.blacklist() + api.movies.blacklist(), ); } @@ -104,7 +104,7 @@ export function useMovieAddBlacklist() { client.invalidateQueries([QueryKeys.Movies, QueryKeys.Blacklist]); client.invalidateQueries([QueryKeys.Movies, id]); }, - } + }, ); } @@ -118,7 +118,7 @@ export function useMovieDeleteBlacklist() { onSuccess: (_, param) => { client.invalidateQueries([QueryKeys.Movies, QueryKeys.Blacklist]); }, - } + }, ); } @@ -126,7 +126,7 @@ export function useMovieHistoryPagination() { return usePaginationQuery( [QueryKeys.Movies, QueryKeys.History], (param) => api.movies.history(param), - false + false, ); } diff --git a/frontend/src/apis/hooks/providers.ts b/frontend/src/apis/hooks/providers.ts index c804a9a8e..5e4fed602 100644 --- a/frontend/src/apis/hooks/providers.ts +++ b/frontend/src/apis/hooks/providers.ts @@ -5,7 +5,7 @@ import api from "../raw"; export function useSystemProviders(history?: boolean) { return useQuery( [QueryKeys.System, QueryKeys.Providers, history ?? false], - () => api.providers.providers(history) + () => api.providers.providers(history), ); } @@ -19,7 +19,7 @@ export function useMoviesProvider(radarrId?: number) { }, { staleTime: 0, - } + }, ); } @@ -33,7 +33,7 @@ export function useEpisodesProvider(episodeId?: number) { }, { staleTime: 0, - } + }, ); } @@ -46,7 +46,7 @@ export function useResetProvider() { onSuccess: () => { client.invalidateQueries([QueryKeys.System, QueryKeys.Providers]); }, - } + }, ); } @@ -68,13 +68,13 @@ export function useDownloadEpisodeSubtitles() { api.providers.downloadEpisodeSubtitle( param.seriesId, param.episodeId, - param.form + param.form, ), { onSuccess: (_, param) => { client.invalidateQueries([QueryKeys.Series, param.seriesId]); }, - } + }, ); } @@ -94,6 +94,6 @@ export function useDownloadMovieSubtitles() { onSuccess: (_, param) => { client.invalidateQueries([QueryKeys.Movies, param.radarrId]); }, - } + }, ); } diff --git a/frontend/src/apis/hooks/series.ts b/frontend/src/apis/hooks/series.ts index 558d5be23..1e395a6e5 100644 --- a/frontend/src/apis/hooks/series.ts +++ b/frontend/src/apis/hooks/series.ts @@ -39,13 +39,13 @@ export function useSeries() { onSuccess: (data) => { cacheSeries(client, data); }, - } + }, ); } export function useSeriesPagination() { return usePaginationQuery([QueryKeys.Series], (param) => - api.series.seriesBy(param) + api.series.seriesBy(param), ); } @@ -61,7 +61,7 @@ export function useSeriesModification() { }); client.invalidateQueries([QueryKeys.Series]); }, - } + }, ); } @@ -74,6 +74,6 @@ export function useSeriesAction() { onSuccess: () => { client.invalidateQueries([QueryKeys.Series]); }, - } + }, ); } diff --git a/frontend/src/apis/hooks/subtitles.ts b/frontend/src/apis/hooks/subtitles.ts index 0a4417257..7864cdcbb 100644 --- a/frontend/src/apis/hooks/subtitles.ts +++ b/frontend/src/apis/hooks/subtitles.ts @@ -23,7 +23,7 @@ export function useSubtitleAction() { client.invalidateQueries([QueryKeys.Movies, id]); } }, - } + }, ); } @@ -42,13 +42,13 @@ export function useEpisodeSubtitleModification() { api.episodes.downloadSubtitles( param.seriesId, param.episodeId, - param.form + param.form, ), { onSuccess: (_, param) => { client.invalidateQueries([QueryKeys.Series, param.seriesId]); }, - } + }, ); const remove = useMutation( @@ -59,7 +59,7 @@ export function useEpisodeSubtitleModification() { onSuccess: (_, param) => { client.invalidateQueries([QueryKeys.Series, param.seriesId]); }, - } + }, ); const upload = useMutation( @@ -70,7 +70,7 @@ export function useEpisodeSubtitleModification() { onSuccess: (_, { seriesId }) => { client.invalidateQueries([QueryKeys.Series, seriesId]); }, - } + }, ); return { download, remove, upload }; @@ -92,7 +92,7 @@ export function useMovieSubtitleModification() { onSuccess: (_, param) => { client.invalidateQueries([QueryKeys.Movies, param.radarrId]); }, - } + }, ); const remove = useMutation( @@ -103,7 +103,7 @@ export function useMovieSubtitleModification() { onSuccess: (_, param) => { client.invalidateQueries([QueryKeys.Movies, param.radarrId]); }, - } + }, ); const upload = useMutation( @@ -114,7 +114,7 @@ export function useMovieSubtitleModification() { onSuccess: (_, { radarrId }) => { client.invalidateQueries([QueryKeys.Movies, radarrId]); }, - } + }, ); return { download, remove, upload }; @@ -122,30 +122,30 @@ export function useMovieSubtitleModification() { export function useSubtitleInfos(names: string[]) { return useQuery([QueryKeys.Subtitles, QueryKeys.Infos, names], () => - api.subtitles.info(names) + api.subtitles.info(names), ); } export function useRefTracksByEpisodeId( subtitlesPath: string, sonarrEpisodeId: number, - isEpisode: boolean + isEpisode: boolean, ) { return useQuery( [QueryKeys.Episodes, sonarrEpisodeId, QueryKeys.Subtitles, subtitlesPath], () => api.subtitles.getRefTracksByEpisodeId(subtitlesPath, sonarrEpisodeId), - { enabled: isEpisode } + { enabled: isEpisode }, ); } export function useRefTracksByMovieId( subtitlesPath: string, radarrMovieId: number, - isMovie: boolean + isMovie: boolean, ) { return useQuery( [QueryKeys.Movies, radarrMovieId, QueryKeys.Subtitles, subtitlesPath], () => api.subtitles.getRefTracksByMovieId(subtitlesPath, radarrMovieId), - { enabled: isMovie } + { enabled: isMovie }, ); } diff --git a/frontend/src/apis/hooks/system.ts b/frontend/src/apis/hooks/system.ts index 29e379a20..26946e910 100644 --- a/frontend/src/apis/hooks/system.ts +++ b/frontend/src/apis/hooks/system.ts @@ -13,14 +13,14 @@ export function useBadges() { refetchOnWindowFocus: "always", refetchInterval: 1000 * 60, staleTime: 1000 * 10, - } + }, ); } export function useFileSystem( type: "bazarr" | "sonarr" | "radarr", path: string, - enabled: boolean + enabled: boolean, ) { return useQuery( [QueryKeys.FileSystem, type, path], @@ -35,7 +35,7 @@ export function useFileSystem( }, { enabled, - } + }, ); } @@ -45,7 +45,7 @@ export function useSystemSettings() { () => api.system.settings(), { staleTime: Infinity, - } + }, ); } @@ -63,7 +63,7 @@ export function useSettingsMutation() { client.invalidateQueries([QueryKeys.Wanted]); client.invalidateQueries([QueryKeys.Badges]); }, - } + }, ); } @@ -73,7 +73,7 @@ export function useServerSearch(query: string, enabled: boolean) { () => api.system.search(query), { enabled, - } + }, ); } @@ -94,7 +94,7 @@ export function useDeleteLogs() { onSuccess: () => { client.invalidateQueries([QueryKeys.System, QueryKeys.Logs]); }, - } + }, ); } @@ -106,7 +106,7 @@ export function useSystemAnnouncements() { refetchOnWindowFocus: "always", refetchInterval: 1000 * 60, staleTime: 1000 * 10, - } + }, ); } @@ -123,7 +123,7 @@ export function useSystemAnnouncementsAddDismiss() { client.invalidateQueries([QueryKeys.System, QueryKeys.Announcements]); client.invalidateQueries([QueryKeys.System, QueryKeys.Badges]); }, - } + }, ); } @@ -135,7 +135,7 @@ export function useSystemTasks() { refetchOnWindowFocus: "always", refetchInterval: 1000 * 60, staleTime: 1000 * 10, - } + }, ); } @@ -149,7 +149,7 @@ export function useRunTask() { client.invalidateQueries([QueryKeys.System, QueryKeys.Tasks]); client.invalidateQueries([QueryKeys.System, QueryKeys.Backups]); }, - } + }, ); } @@ -166,7 +166,7 @@ export function useCreateBackups() { onSuccess: () => { client.invalidateQueries([QueryKeys.System, QueryKeys.Backups]); }, - } + }, ); } @@ -179,7 +179,7 @@ export function useRestoreBackups() { onSuccess: () => { client.invalidateQueries([QueryKeys.System, QueryKeys.Backups]); }, - } + }, ); } @@ -192,7 +192,7 @@ export function useDeleteBackups() { onSuccess: () => { client.invalidateQueries([QueryKeys.System, QueryKeys.Backups]); }, - } + }, ); } @@ -218,7 +218,7 @@ export function useSystem() { setAuthenticated(false); client.clear(); }, - } + }, ); const { mutate: login, isLoading: isLoggingIn } = useMutation( @@ -230,7 +230,7 @@ export function useSystem() { // TODO: Hard-coded value window.location.replace(Environment.baseUrl); }, - } + }, ); const { mutate: shutdown, isLoading: isShuttingDown } = useMutation( @@ -240,7 +240,7 @@ export function useSystem() { onSuccess: () => { client.clear(); }, - } + }, ); const { mutate: restart, isLoading: isRestarting } = useMutation( @@ -250,7 +250,7 @@ export function useSystem() { onSuccess: () => { client.clear(); }, - } + }, ); return useMemo( @@ -270,6 +270,6 @@ export function useSystem() { logout, restart, shutdown, - ] + ], ); } diff --git a/frontend/src/apis/queries/hooks.ts b/frontend/src/apis/queries/hooks.ts index 507cc2120..5f9bf63d3 100644 --- a/frontend/src/apis/queries/hooks.ts +++ b/frontend/src/apis/queries/hooks.ts @@ -26,11 +26,11 @@ export type UsePaginationQueryResult = UseQueryResult< export function usePaginationQuery< TObject extends object = object, - TQueryKey extends QueryKey = QueryKey + TQueryKey extends QueryKey = QueryKey, >( queryKey: TQueryKey, queryFn: RangeQuery, - cacheIndividual = true + cacheIndividual = true, ): UsePaginationQueryResult { const client = useQueryClient(); @@ -59,7 +59,7 @@ export function usePaginationQuery< }); } }, - } + }, ); const { data } = results; @@ -73,7 +73,7 @@ export function usePaginationQuery< setIndex(idx); } }, - [pageCount] + [pageCount], ); const [isPageLoading, setIsPageLoading] = useState(false); diff --git a/frontend/src/apis/raw/base.ts b/frontend/src/apis/raw/base.ts index 7c523e36e..5f4744411 100644 --- a/frontend/src/apis/raw/base.ts +++ b/frontend/src/apis/raw/base.ts @@ -38,16 +38,19 @@ class BaseApi { protected post( path: string, formdata?: LooseObject, - params?: LooseObject + params?: LooseObject, ): Promise> { const form = this.createFormdata(formdata); - return client.axios.post(this.prefix + path, form, { params }); + return client.axios.post(this.prefix + path, form, { + params, + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + }); } protected patch( path: string, formdata?: LooseObject, - params?: LooseObject + params?: LooseObject, ): Promise> { const form = this.createFormdata(formdata); return client.axios.patch(this.prefix + path, form, { params }); @@ -56,7 +59,7 @@ class BaseApi { protected delete( path: string, formdata?: LooseObject, - params?: LooseObject + params?: LooseObject, ): Promise> { const form = this.createFormdata(formdata); return client.axios.delete(this.prefix + path, { params, data: form }); diff --git a/frontend/src/apis/raw/client.ts b/frontend/src/apis/raw/client.ts index 406f4c1af..d77b81205 100644 --- a/frontend/src/apis/raw/client.ts +++ b/frontend/src/apis/raw/client.ts @@ -64,7 +64,7 @@ class BazarrClient { (error: AxiosError) => { const message = GetErrorMessage( error.response?.data, - "You have disconnected from the server" + "You have disconnected from the server", ); const backendError: BackendError = { @@ -76,7 +76,7 @@ class BazarrClient { this.handleError(backendError); return Promise.reject(error); - } + }, ); } diff --git a/frontend/src/apis/raw/episodes.ts b/frontend/src/apis/raw/episodes.ts index d831fb235..9df7f2bbc 100644 --- a/frontend/src/apis/raw/episodes.ts +++ b/frontend/src/apis/raw/episodes.ts @@ -22,7 +22,7 @@ class EpisodeApi extends BaseApi { async wanted(params: Parameter.Range) { const response = await this.get>( "/wanted", - params + params, ); return response; } @@ -30,7 +30,7 @@ class EpisodeApi extends BaseApi { async wantedBy(episodeid: number[]) { const response = await this.get>( "/wanted", - { episodeid } + { episodeid }, ); return response; } @@ -38,7 +38,7 @@ class EpisodeApi extends BaseApi { async history(params: Parameter.Range) { const response = await this.get>( "/history", - params + params, ); return response; } @@ -46,7 +46,7 @@ class EpisodeApi extends BaseApi { async historyBy(episodeid: number) { const response = await this.get>( "/history", - { episodeid } + { episodeid }, ); return response.data; } @@ -54,7 +54,7 @@ class EpisodeApi extends BaseApi { async downloadSubtitles( seriesid: number, episodeid: number, - form: FormType.Subtitle + form: FormType.Subtitle, ) { await this.patch("/subtitles", form, { seriesid, episodeid }); } @@ -62,7 +62,7 @@ class EpisodeApi extends BaseApi { async uploadSubtitles( seriesid: number, episodeid: number, - form: FormType.UploadSubtitle + form: FormType.UploadSubtitle, ) { await this.post("/subtitles", form, { seriesid, episodeid }); } @@ -70,22 +70,21 @@ class EpisodeApi extends BaseApi { async deleteSubtitles( seriesid: number, episodeid: number, - form: FormType.DeleteSubtitle + form: FormType.DeleteSubtitle, ) { await this.delete("/subtitles", form, { seriesid, episodeid }); } async blacklist() { - const response = await this.get>( - "/blacklist" - ); + const response = + await this.get>("/blacklist"); return response.data; } async addBlacklist( seriesid: number, episodeid: number, - form: FormType.AddBlacklist + form: FormType.AddBlacklist, ) { await this.post("/blacklist", form, { seriesid, episodeid }); } diff --git a/frontend/src/apis/raw/history.ts b/frontend/src/apis/raw/history.ts index 6a9609100..bfdcde502 100644 --- a/frontend/src/apis/raw/history.ts +++ b/frontend/src/apis/raw/history.ts @@ -9,7 +9,7 @@ class HistoryApi extends BaseApi { timeFrame?: History.TimeFrameOptions, action?: History.ActionOptions, provider?: string, - language?: Language.CodeType + language?: Language.CodeType, ) { const response = await this.get("/stats", { timeFrame, diff --git a/frontend/src/apis/raw/movies.ts b/frontend/src/apis/raw/movies.ts index 2123ec016..33fb61452 100644 --- a/frontend/src/apis/raw/movies.ts +++ b/frontend/src/apis/raw/movies.ts @@ -6,9 +6,8 @@ class MovieApi extends BaseApi { } async blacklist() { - const response = await this.get>( - "/blacklist" - ); + const response = + await this.get>("/blacklist"); return response.data; } @@ -30,7 +29,7 @@ class MovieApi extends BaseApi { async moviesBy(params: Parameter.Range) { const response = await this.get>( "", - params + params, ); return response; } @@ -42,7 +41,7 @@ class MovieApi extends BaseApi { async wanted(params: Parameter.Range) { const response = await this.get>( "/wanted", - params + params, ); return response; } @@ -52,7 +51,7 @@ class MovieApi extends BaseApi { "/wanted", { radarrid, - } + }, ); return response; } @@ -60,7 +59,7 @@ class MovieApi extends BaseApi { async history(params: Parameter.Range) { const response = await this.get>( "/history", - params + params, ); return response; } @@ -68,7 +67,7 @@ class MovieApi extends BaseApi { async historyBy(radarrid: number) { const response = await this.get>( "/history", - { radarrid } + { radarrid }, ); return response.data; } diff --git a/frontend/src/apis/raw/providers.ts b/frontend/src/apis/raw/providers.ts index 2722460cc..d3e81e3cc 100644 --- a/frontend/src/apis/raw/providers.ts +++ b/frontend/src/apis/raw/providers.ts @@ -19,7 +19,7 @@ class ProviderApi extends BaseApi { async movies(id: number) { const response = await this.get>( "/movies", - { radarrid: id } + { radarrid: id }, ); return response.data; } @@ -33,7 +33,7 @@ class ProviderApi extends BaseApi { "/episodes", { episodeid, - } + }, ); return response.data; } @@ -41,7 +41,7 @@ class ProviderApi extends BaseApi { async downloadEpisodeSubtitle( seriesid: number, episodeid: number, - form: FormType.ManualDownload + form: FormType.ManualDownload, ) { await this.post("/episodes", form, { seriesid, episodeid }); } diff --git a/frontend/src/apis/raw/series.ts b/frontend/src/apis/raw/series.ts index df8696cbc..23994b2bd 100644 --- a/frontend/src/apis/raw/series.ts +++ b/frontend/src/apis/raw/series.ts @@ -15,7 +15,7 @@ class SeriesApi extends BaseApi { async seriesBy(params: Parameter.Range) { const response = await this.get>( "", - params + params, ); return response; } diff --git a/frontend/src/apis/raw/subtitles.ts b/frontend/src/apis/raw/subtitles.ts index b3d75eb70..dfc7f5064 100644 --- a/frontend/src/apis/raw/subtitles.ts +++ b/frontend/src/apis/raw/subtitles.ts @@ -7,7 +7,7 @@ class SubtitlesApi extends BaseApi { async getRefTracksByEpisodeId( subtitlesPath: string, - sonarrEpisodeId: number + sonarrEpisodeId: number, ) { const response = await this.get>("", { subtitlesPath, @@ -18,7 +18,7 @@ class SubtitlesApi extends BaseApi { async getRefTracksByMovieId( subtitlesPath: string, - radarrMovieId?: number | undefined + radarrMovieId?: number | undefined, ) { const response = await this.get>("", { subtitlesPath, diff --git a/frontend/src/apis/raw/system.ts b/frontend/src/apis/raw/system.ts index 3d8791a90..628494c01 100644 --- a/frontend/src/apis/raw/system.ts +++ b/frontend/src/apis/raw/system.ts @@ -88,9 +88,8 @@ class SystemApi extends BaseApi { } async announcements() { - const response = await this.get>( - "/announcements" - ); + const response = + await this.get>("/announcements"); return response.data; } diff --git a/frontend/src/apis/raw/utils.ts b/frontend/src/apis/raw/utils.ts index 4dae38a0f..2da7014d3 100644 --- a/frontend/src/apis/raw/utils.ts +++ b/frontend/src/apis/raw/utils.ts @@ -15,7 +15,7 @@ class RequestUtils { try { const result = await client.axios.get( `../test/${protocol}/${url}api/system/status`, - { params } + { params }, ); const { data } = result; if (data.status && data.version) { @@ -26,7 +26,7 @@ class RequestUtils { } catch (e) { const result = await client.axios.get( `../test/${protocol}/${url}api/v3/system/status`, - { params } + { params }, ); return result.data; } diff --git a/frontend/src/components/Search.tsx b/frontend/src/components/Search.tsx index e4124d09f..bc4a9f8d3 100644 --- a/frontend/src/components/Search.tsx +++ b/frontend/src/components/Search.tsx @@ -37,7 +37,7 @@ function useSearch(query: string) { link, }; }) ?? [], - [data] + [data], ); } @@ -69,7 +69,7 @@ const ResultComponent = forwardRef( {value} ); - } + }, ); const Search: FunctionComponent = () => { diff --git a/frontend/src/components/SubtitleToolsMenu.tsx b/frontend/src/components/SubtitleToolsMenu.tsx index ba44e94aa..50509c3c2 100644 --- a/frontend/src/components/SubtitleToolsMenu.tsx +++ b/frontend/src/components/SubtitleToolsMenu.tsx @@ -100,7 +100,7 @@ export function useTools() { modal: TranslationModal, }, ], - [] + [], ); } @@ -131,7 +131,7 @@ const SubtitleToolsMenu: FunctionComponent = ({ task.create(s.path, name, mutateAsync, { action, form }); }); }, - [mutateAsync, selections] + [mutateAsync, selections], ); const tools = useTools(); diff --git a/frontend/src/components/bazarr/Language.test.tsx b/frontend/src/components/bazarr/Language.test.tsx index 8d328753e..9e0e0fab8 100644 --- a/frontend/src/components/bazarr/Language.test.tsx +++ b/frontend/src/components/bazarr/Language.test.tsx @@ -53,7 +53,7 @@ describe("Language text", () => { it("should show long text with Forced", () => { rawRender( - + , ); const expectedText = `${testLanguageWithHi.name} Forced`; diff --git a/frontend/src/components/bazarr/LanguageProfile.tsx b/frontend/src/components/bazarr/LanguageProfile.tsx index 03ca6255a..75b7b73ca 100644 --- a/frontend/src/components/bazarr/LanguageProfile.tsx +++ b/frontend/src/components/bazarr/LanguageProfile.tsx @@ -14,7 +14,7 @@ const LanguageProfileName: FunctionComponent = ({ const name = useMemo( () => data?.find((v) => v.profileId === index)?.name ?? empty, - [data, empty, index] + [data, empty, index], ); return <>{name}; diff --git a/frontend/src/components/bazarr/LanguageSelector.tsx b/frontend/src/components/bazarr/LanguageSelector.tsx index 84ce363d5..c2219ca7c 100644 --- a/frontend/src/components/bazarr/LanguageSelector.tsx +++ b/frontend/src/components/bazarr/LanguageSelector.tsx @@ -25,7 +25,7 @@ const LanguageSelector: FunctionComponent = ({ const options = useSelectorOptions( filteredData ?? [], (value) => value.name, - (value) => value.code3 + (value) => value.code3, ); return ; diff --git a/frontend/src/components/forms/ColorToolForm.tsx b/frontend/src/components/forms/ColorToolForm.tsx index af4d2b6e5..a37819bee 100644 --- a/frontend/src/components/forms/ColorToolForm.tsx +++ b/frontend/src/components/forms/ColorToolForm.tsx @@ -96,7 +96,7 @@ const ColorToolForm: FunctionComponent = ({ selections, onSubmit }) => { validate: { color: FormUtils.validation( (value) => colorOptions.find((op) => op.value === value) !== undefined, - "Must select a color" + "Must select a color", ), }, }); @@ -110,7 +110,7 @@ const ColorToolForm: FunctionComponent = ({ selections, onSubmit }) => { task.create(s.path, TaskName, mutateAsync, { action, form: s, - }) + }), ); onSubmit?.(); diff --git a/frontend/src/components/forms/FrameRateForm.tsx b/frontend/src/components/forms/FrameRateForm.tsx index c8694ae37..7e7eca24c 100644 --- a/frontend/src/components/forms/FrameRateForm.tsx +++ b/frontend/src/components/forms/FrameRateForm.tsx @@ -29,11 +29,11 @@ const FrameRateForm: FunctionComponent = ({ selections, onSubmit }) => { validate: { from: FormUtils.validation( (value) => value > 0, - "The From value must be larger than 0" + "The From value must be larger than 0", ), to: FormUtils.validation( (value) => value > 0, - "The To value must be larger than 0" + "The To value must be larger than 0", ), }, }); @@ -47,7 +47,7 @@ const FrameRateForm: FunctionComponent = ({ selections, onSubmit }) => { task.create(s.path, TaskName, mutateAsync, { action, form: s, - }) + }), ); onSubmit?.(); diff --git a/frontend/src/components/forms/ItemEditForm.tsx b/frontend/src/components/forms/ItemEditForm.tsx index 31d00bfec..a51d63f54 100644 --- a/frontend/src/components/forms/ItemEditForm.tsx +++ b/frontend/src/components/forms/ItemEditForm.tsx @@ -27,12 +27,12 @@ const ItemEditForm: FunctionComponent = ({ const profileOptions = useSelectorOptions( data ?? [], (v) => v.name ?? "Unknown", - (v) => v.profileId.toString() ?? "-1" + (v) => v.profileId.toString() ?? "-1", ); const profile = useMemo( () => data?.find((v) => v.profileId === item?.profileId) ?? null, - [data, item?.profileId] + [data, item?.profileId], ); const form = useForm({ @@ -44,7 +44,7 @@ const ItemEditForm: FunctionComponent = ({ const options = useSelectorOptions( item?.audio_language ?? [], (v) => v.name, - (v) => v.code2 + (v) => v.code2, ); const isOverlayVisible = isLoading || isFetching || item === null; diff --git a/frontend/src/components/forms/MovieUploadForm.tsx b/frontend/src/components/forms/MovieUploadForm.tsx index 46b592081..fbcb0b04f 100644 --- a/frontend/src/components/forms/MovieUploadForm.tsx +++ b/frontend/src/components/forms/MovieUploadForm.tsx @@ -47,7 +47,7 @@ type SubtitleValidateResult = { const validator = ( movie: Item.Movie, - file: SubtitleFile + file: SubtitleFile, ): SubtitleValidateResult => { if (file.language === null) { return { @@ -57,7 +57,7 @@ const validator = ( } else { const { subtitles } = movie; const existing = subtitles.find( - (v) => v.code2 === file.language?.code2 && isString(v.path) + (v) => v.code2 === file.language?.code2 && isString(v.path), ); if (existing !== undefined) { return { @@ -91,12 +91,12 @@ const MovieUploadForm: FunctionComponent = ({ const languageOptions = useSelectorOptions( languages, (v) => v.name, - (v) => v.code2 + (v) => v.code2, ); const defaultLanguage = useMemo( () => (languages.length > 0 ? languages[0] : null), - [languages] + [languages], ); const form = useForm({ @@ -120,7 +120,7 @@ const MovieUploadForm: FunctionComponent = ({ (v) => v.language === null || v.validateResult === undefined || - v.validateResult.state === "error" + v.validateResult.state === "error", ) === undefined ); }, "Some files cannot be uploaded, please check"), @@ -254,7 +254,7 @@ const MovieUploadForm: FunctionComponent = ({ }, }, ], - [action, languageOptions] + [action, languageOptions], ); const { upload } = useMovieSubtitleModification(); @@ -294,7 +294,7 @@ export const MovieUploadModal = withModal( { title: "Upload Subtitles", size: "xl", - } + }, ); export default MovieUploadForm; diff --git a/frontend/src/components/forms/ProfileEditForm.tsx b/frontend/src/components/forms/ProfileEditForm.tsx index d31a0e338..874f5b8a6 100644 --- a/frontend/src/components/forms/ProfileEditForm.tsx +++ b/frontend/src/components/forms/ProfileEditForm.tsx @@ -69,11 +69,11 @@ const ProfileEditForm: FunctionComponent = ({ validate: { name: FormUtils.validation( (value) => value.length > 0, - "Must have a name" + "Must have a name", ), items: FormUtils.validation( (value) => value.length > 0, - "Must contain at lease 1 language" + "Must contain at lease 1 language", ), }, }); @@ -83,7 +83,7 @@ const ProfileEditForm: FunctionComponent = ({ const itemCutoffOptions = useSelectorOptions( form.values.items, (v) => v.language, - (v) => String(v.id) + (v) => String(v.id), ); const cutoffOptions = useMemo( @@ -91,24 +91,24 @@ const ProfileEditForm: FunctionComponent = ({ ...itemCutoffOptions, options: [...itemCutoffOptions.options, ...defaultCutoffOptions], }), - [itemCutoffOptions] + [itemCutoffOptions], ); const selectedCutoff = useMemo( () => cutoffOptions.options.find((v) => v.value.id === form.values.cutoff) ?.value ?? null, - [cutoffOptions, form.values.cutoff] + [cutoffOptions, form.values.cutoff], ); const mustContainOptions = useSelectorOptions( form.values.mustContain, - (v) => v + (v) => v, ); const mustNotContainOptions = useSelectorOptions( form.values.mustNotContain, - (v) => v + (v) => v, ); const action = useArrayAction((fn) => { @@ -120,7 +120,7 @@ const ProfileEditForm: FunctionComponent = ({ 1 + form.values.items.reduce( (val, item) => Math.max(item.id, val), - 0 + 0, ); if (languages.length > 0) { @@ -154,7 +154,7 @@ const ProfileEditForm: FunctionComponent = ({ () => languageOptions.options.find((l) => l.value.code2 === code) ?.value ?? null, - [code] + [code], ); const { classes } = useTableStyles(); @@ -238,7 +238,7 @@ const ProfileEditForm: FunctionComponent = ({ }, }, ], - [action, languageOptions] + [action, languageOptions], ); return ( @@ -332,5 +332,5 @@ export const ProfileEditModal = withModal( { title: "Edit Languages Profile", size: "xl", - } + }, ); diff --git a/frontend/src/components/forms/SeriesUploadForm.tsx b/frontend/src/components/forms/SeriesUploadForm.tsx index 3c8b036a4..784baf8a5 100644 --- a/frontend/src/components/forms/SeriesUploadForm.tsx +++ b/frontend/src/components/forms/SeriesUploadForm.tsx @@ -64,7 +64,7 @@ const validator = (file: SubtitleFile): SubtitleValidateResult => { } else { const { subtitles } = file.episode; const existing = subtitles.find( - (v) => v.code2 === file.language?.code2 && isString(v.path) + (v) => v.code2 === file.language?.code2 && isString(v.path), ); if (existing !== undefined) { return { @@ -95,7 +95,7 @@ const SeriesUploadForm: FunctionComponent = ({ const episodeOptions = useSelectorOptions( episodes.data ?? [], (v) => `(${v.season}x${v.episode}) ${v.title}`, - (v) => v.sonarrEpisodeId.toString() + (v) => v.sonarrEpisodeId.toString(), ); const profile = useLanguageProfileBy(series.profileId); @@ -103,12 +103,12 @@ const SeriesUploadForm: FunctionComponent = ({ const languageOptions = useSelectorOptions( languages, (v) => v.name, - (v) => v.code2 + (v) => v.code2, ); const defaultLanguage = useMemo( () => (languages.length > 0 ? languages[0] : null), - [languages] + [languages], ); const form = useForm({ @@ -134,9 +134,9 @@ const SeriesUploadForm: FunctionComponent = ({ v.language === null || v.episode === null || v.validateResult === undefined || - v.validateResult.state === "error" + v.validateResult.state === "error", ) === undefined, - "Some files cannot be uploaded, please check" + "Some files cannot be uploaded, please check", ), }, }); @@ -162,7 +162,7 @@ const SeriesUploadForm: FunctionComponent = ({ if (info) { item.episode = episodes.data?.find( - (v) => v.season === info.season && v.episode === info.episode + (v) => v.season === info.season && v.episode === info.episode, ) ?? item.episode; } return item; @@ -320,7 +320,7 @@ const SeriesUploadForm: FunctionComponent = ({ }, }, ], - [action, episodeOptions, languageOptions] + [action, episodeOptions, languageOptions], ); const { upload } = useEpisodeSubtitleModification(); @@ -335,7 +335,7 @@ const SeriesUploadForm: FunctionComponent = ({ if (language === null || episode === null) { throw new Error( - "Invalid language or episode. This shouldn't happen, please report this bug." + "Invalid language or episode. This shouldn't happen, please report this bug.", ); } @@ -370,7 +370,7 @@ const SeriesUploadForm: FunctionComponent = ({ export const SeriesUploadModal = withModal( SeriesUploadForm, "upload-series-subtitles", - { title: "Upload Subtitles", size: "xl" } + { title: "Upload Subtitles", size: "xl" }, ); export default SeriesUploadForm; diff --git a/frontend/src/components/forms/SyncSubtitleForm.tsx b/frontend/src/components/forms/SyncSubtitleForm.tsx index 349058f63..b5136fc85 100644 --- a/frontend/src/components/forms/SyncSubtitleForm.tsx +++ b/frontend/src/components/forms/SyncSubtitleForm.tsx @@ -21,18 +21,18 @@ const TaskName = "Syncing Subtitle"; function useReferencedSubtitles( mediaType: "episode" | "movie", mediaId: number, - subtitlesPath: string + subtitlesPath: string, ) { // We cannot call hooks conditionally, we rely on useQuery "enabled" option to do only the required API call const episodeData = useRefTracksByEpisodeId( subtitlesPath, mediaId, - mediaType === "episode" + mediaType === "episode", ); const movieData = useRefTracksByMovieId( subtitlesPath, mediaId, - mediaType === "movie" + mediaType === "movie", ); const mediaData = mediaType === "episode" ? episodeData : movieData; @@ -108,7 +108,7 @@ const SyncSubtitleForm: FunctionComponent = ({ const subtitles: SelectorOption[] = useReferencedSubtitles( mediaType, mediaId, - subtitlesPath + subtitlesPath, ); const form = useForm({ diff --git a/frontend/src/components/forms/TimeOffsetForm.tsx b/frontend/src/components/forms/TimeOffsetForm.tsx index 6d213b359..2792d64d8 100644 --- a/frontend/src/components/forms/TimeOffsetForm.tsx +++ b/frontend/src/components/forms/TimeOffsetForm.tsx @@ -37,7 +37,7 @@ const TimeOffsetForm: FunctionComponent = ({ selections, onSubmit }) => { sec: FormUtils.validation((v) => v >= 0, "Second must be larger than 0"), ms: FormUtils.validation( (v) => v >= 0, - "Millisecond must be larger than 0" + "Millisecond must be larger than 0", ), }, }); @@ -62,7 +62,7 @@ const TimeOffsetForm: FunctionComponent = ({ selections, onSubmit }) => { task.create(s.path, TaskName, mutateAsync, { action, form: s, - }) + }), ); onSubmit?.(); diff --git a/frontend/src/components/forms/TranslationForm.tsx b/frontend/src/components/forms/TranslationForm.tsx index 260d9b198..976b2f72f 100644 --- a/frontend/src/components/forms/TranslationForm.tsx +++ b/frontend/src/components/forms/TranslationForm.tsx @@ -146,13 +146,13 @@ const TranslationForm: FunctionComponent = ({ const available = useMemo( () => languages.filter((v) => v.code2 in translations), - [languages] + [languages], ); const options = useSelectorOptions( available, (v) => v.name, - (v) => v.code2 + (v) => v.code2, ); return ( @@ -166,7 +166,7 @@ const TranslationForm: FunctionComponent = ({ ...s, language: language.code2, }, - }) + }), ); onSubmit?.(); diff --git a/frontend/src/components/inputs/Action.test.tsx b/frontend/src/components/inputs/Action.test.tsx index 05086e71c..189aca076 100644 --- a/frontend/src/components/inputs/Action.test.tsx +++ b/frontend/src/components/inputs/Action.test.tsx @@ -28,7 +28,7 @@ describe("Action button", () => { it("should call on-click event when clicked", async () => { const onClickFn = vitest.fn(); rawRender( - + , ); await userEvent.click(screen.getByRole("button", { name: testLabel })); diff --git a/frontend/src/components/inputs/Action.tsx b/frontend/src/components/inputs/Action.tsx index d85239404..236baf112 100644 --- a/frontend/src/components/inputs/Action.tsx +++ b/frontend/src/components/inputs/Action.tsx @@ -27,7 +27,7 @@ const Action = forwardRef( ); - } + }, ); export default Action; diff --git a/frontend/src/components/inputs/ChipInput.test.tsx b/frontend/src/components/inputs/ChipInput.test.tsx index 3e30490d2..cb52ee30c 100644 --- a/frontend/src/components/inputs/ChipInput.test.tsx +++ b/frontend/src/components/inputs/ChipInput.test.tsx @@ -30,7 +30,7 @@ describe("ChipInput", () => { }); rawRender( - + , ); const element = screen.getByRole("searchbox"); diff --git a/frontend/src/components/inputs/FileBrowser.tsx b/frontend/src/components/inputs/FileBrowser.tsx index 38c8a6776..ce57a4938 100644 --- a/frontend/src/components/inputs/FileBrowser.tsx +++ b/frontend/src/components/inputs/FileBrowser.tsx @@ -53,7 +53,7 @@ export const FileBrowser: FunctionComponent = ({ item: v, })) ?? []), ], - [tree] + [tree], ); const parent = useMemo(() => { diff --git a/frontend/src/components/inputs/Selector.test.tsx b/frontend/src/components/inputs/Selector.test.tsx index 96382c0b2..a7b6cfb85 100644 --- a/frontend/src/components/inputs/Selector.test.tsx +++ b/frontend/src/components/inputs/Selector.test.tsx @@ -19,7 +19,7 @@ describe("Selector", () => { describe("options", () => { it("should work with the SelectorOption", () => { rawRender( - + , ); // TODO: selectorName @@ -28,7 +28,7 @@ describe("Selector", () => { it("should display when clicked", async () => { rawRender( - + , ); const element = screen.getByRole("searchbox"); @@ -49,7 +49,7 @@ describe("Selector", () => { name={selectorName} options={testOptions} defaultValue={option.value} - > + >, ); expect(screen.getByDisplayValue(option.label)).toBeDefined(); @@ -62,7 +62,7 @@ describe("Selector", () => { name={selectorName} options={testOptions} value={option.value} - > + >, ); expect(screen.getByDisplayValue(option.label)).toBeDefined(); @@ -80,7 +80,7 @@ describe("Selector", () => { name={selectorName} options={testOptions} onChange={mockedFn} - > + >, ); const element = screen.getByRole("searchbox"); @@ -121,7 +121,7 @@ describe("Selector", () => { options={objectOptions} onChange={mockedFn} getkey={(v) => v.name} - > + >, ); const element = screen.getByRole("searchbox"); @@ -142,7 +142,7 @@ describe("Selector", () => { name={selectorName} options={testOptions} placeholder={placeholder} - > + >, ); expect(screen.getByPlaceholderText(placeholder)).toBeDefined(); diff --git a/frontend/src/components/inputs/Selector.tsx b/frontend/src/components/inputs/Selector.tsx index 79592b758..0af276fc4 100644 --- a/frontend/src/components/inputs/Selector.tsx +++ b/frontend/src/components/inputs/Selector.tsx @@ -29,7 +29,7 @@ function DefaultKeyBuilder(value: T) { } else { LOG("error", "Unknown value type", value); throw new Error( - `Invalid type (${typeof value}) in the SelectorOption, please provide a label builder` + `Invalid type (${typeof value}) in the SelectorOption, please provide a label builder`, ); } } @@ -64,7 +64,7 @@ export function Selector({ payload: value, ...option, })), - [keyRef, options] + [keyRef, options], ); const wrappedValue = useMemo(() => { @@ -88,7 +88,7 @@ export function Selector({ const payload = data.find((v) => v.value === value)?.payload ?? null; onChange?.(payload); }, - [data, onChange] + [data, onChange], ); return ( @@ -137,16 +137,16 @@ export function MultiSelector({ payload: value, ...option, })), - [options] + [options], ); const wrappedValue = useMemo( () => value && value.map(labelRef.current), - [value] + [value], ); const wrappedDefaultValue = useMemo( () => defaultValue && defaultValue.map(labelRef.current), - [defaultValue] + [defaultValue], ); const wrappedOnChange = useCallback( @@ -162,7 +162,7 @@ export function MultiSelector({ } onChange?.(payloads); }, - [data, onChange] + [data, onChange], ); return ( diff --git a/frontend/src/components/modals/HistoryModal.tsx b/frontend/src/components/modals/HistoryModal.tsx index 0f39e0ee0..cc4197c44 100644 --- a/frontend/src/components/modals/HistoryModal.tsx +++ b/frontend/src/components/modals/HistoryModal.tsx @@ -123,7 +123,7 @@ const MovieHistoryView: FunctionComponent = ({ }, }, ], - [] + [], ); return ( @@ -263,7 +263,7 @@ const EpisodeHistoryView: FunctionComponent = ({ }, }, ], - [] + [], ); return ( @@ -280,5 +280,5 @@ const EpisodeHistoryView: FunctionComponent = ({ export const EpisodeHistoryModal = withModal( EpisodeHistoryView, "episode-history", - { size: "xl" } + { size: "xl" }, ); diff --git a/frontend/src/components/modals/ManualSearchModal.tsx b/frontend/src/components/modals/ManualSearchModal.tsx index 56fd0fb4b..0dc0f1347 100644 --- a/frontend/src/components/modals/ManualSearchModal.tsx +++ b/frontend/src/components/modals/ManualSearchModal.tsx @@ -32,7 +32,7 @@ type SupportType = Item.Movie | Item.Episode; interface Props { download: (item: T, result: SearchResultType) => Promise; query: ( - id?: number + id?: number, ) => UseQueryResult; item: T; } @@ -111,7 +111,7 @@ function ManualSearchView(props: Props) { const items = useMemo( () => value.slice(1).map((v, idx) => {v}), - [value] + [value], ); if (value.length === 0) { @@ -176,7 +176,7 @@ function ManualSearchView(props: Props) { TaskGroup.DownloadSubtitle, download, item, - result + result, ); }} > @@ -184,7 +184,7 @@ function ManualSearchView(props: Props) { }, }, ], - [download, item] + [download, item], ); const bSceneNameAvailable = @@ -228,10 +228,10 @@ function ManualSearchView(props: Props) { export const MovieSearchModal = withModal>( ManualSearchView, "movie-manual-search", - { title: "Search Subtitles", size: "calc(100vw - 4rem)" } + { title: "Search Subtitles", size: "calc(100vw - 4rem)" }, ); export const EpisodeSearchModal = withModal>( ManualSearchView, "episode-manual-search", - { title: "Search Subtitles", size: "calc(100vw - 4rem)" } + { title: "Search Subtitles", size: "calc(100vw - 4rem)" }, ); diff --git a/frontend/src/components/modals/SubtitleToolsModal.tsx b/frontend/src/components/modals/SubtitleToolsModal.tsx index d780504c3..2ba99ec73 100644 --- a/frontend/src/components/modals/SubtitleToolsModal.tsx +++ b/frontend/src/components/modals/SubtitleToolsModal.tsx @@ -67,7 +67,7 @@ const SubtitleToolView: FunctionComponent = ({ }, }, ], - [] + [], ); const data = useMemo( @@ -91,7 +91,7 @@ const SubtitleToolView: FunctionComponent = ({ } }); }), - [payload] + [payload], ); const plugins = [useRowSelect, useCustomSelection]; diff --git a/frontend/src/components/tables/BaseTable.tsx b/frontend/src/components/tables/BaseTable.tsx index b13b1629a..6ec49e61a 100644 --- a/frontend/src/components/tables/BaseTable.tsx +++ b/frontend/src/components/tables/BaseTable.tsx @@ -33,7 +33,7 @@ const useStyles = createStyles((theme) => { }); function DefaultHeaderRenderer( - headers: HeaderGroup[] + headers: HeaderGroup[], ): JSX.Element[] { return headers.map((col) => ( @@ -71,7 +71,7 @@ export default function BaseTable(props: BaseTableProps) { const colCount = useMemo(() => { return headerGroups.reduce( (prev, curr) => (curr.headers.length > prev ? curr.headers.length : prev), - 0 + 0, ); }, [headerGroups]); diff --git a/frontend/src/components/tables/GroupTable.tsx b/frontend/src/components/tables/GroupTable.tsx index 3d31bfcf2..3a8be3d1b 100644 --- a/frontend/src/components/tables/GroupTable.tsx +++ b/frontend/src/components/tables/GroupTable.tsx @@ -60,7 +60,7 @@ function renderRow(row: Row) { } function renderHeaders( - headers: HeaderGroup[] + headers: HeaderGroup[], ): JSX.Element[] { return headers .filter((col) => !col.isGrouped) diff --git a/frontend/src/components/tables/PageTable.tsx b/frontend/src/components/tables/PageTable.tsx index d84940857..4f64fe7b8 100644 --- a/frontend/src/components/tables/PageTable.tsx +++ b/frontend/src/components/tables/PageTable.tsx @@ -20,7 +20,7 @@ export default function PageTable(props: Props) { options, useDefaultSettings, ...tablePlugins, - ...(plugins ?? []) + ...(plugins ?? []), ); // use page size as specified in UI settings diff --git a/frontend/src/components/tables/SimpleTable.tsx b/frontend/src/components/tables/SimpleTable.tsx index d2beb9630..90f76c7f2 100644 --- a/frontend/src/components/tables/SimpleTable.tsx +++ b/frontend/src/components/tables/SimpleTable.tsx @@ -9,7 +9,7 @@ export type SimpleTableProps = TableOptions & { }; export default function SimpleTable( - props: SimpleTableProps + props: SimpleTableProps, ) { const { plugins, instanceRef, tableStyles, ...options } = props; diff --git a/frontend/src/components/tables/plugins/useCustomSelection.tsx b/frontend/src/components/tables/plugins/useCustomSelection.tsx index f3d357378..d6ea82de4 100644 --- a/frontend/src/components/tables/plugins/useCustomSelection.tsx +++ b/frontend/src/components/tables/plugins/useCustomSelection.tsx @@ -71,7 +71,7 @@ function useInstance(instance: TableInstance) { useEffect(() => { // Performance let items = Object.keys(selectedRowIds).flatMap( - (v) => rows.find((n) => n.id === v)?.original ?? [] + (v) => rows.find((n) => n.id === v)?.original ?? [], ); if (canSelect) { @@ -84,7 +84,7 @@ function useInstance(instance: TableInstance) { function visibleColumns( columns: ColumnInstance[], - meta: MetaBase + meta: MetaBase, ): Column[] { const { instance } = meta; const checkbox: Column = { diff --git a/frontend/src/components/toolbox/Button.tsx b/frontend/src/components/toolbox/Button.tsx index 95502726c..735ef3ca1 100644 --- a/frontend/src/components/toolbox/Button.tsx +++ b/frontend/src/components/toolbox/Button.tsx @@ -38,7 +38,7 @@ type ToolboxMutateButtonProps Promise> = { } & Omit; export function ToolboxMutateButton Promise>( - props: PropsWithChildren> + props: PropsWithChildren>, ): JSX.Element { const { promise, onSuccess, ...button } = props; diff --git a/frontend/src/dom.tsx b/frontend/src/dom.tsx index e00b3e089..07b2078a8 100644 --- a/frontend/src/dom.tsx +++ b/frontend/src/dom.tsx @@ -14,6 +14,6 @@ if (container === null) { - + , ); } diff --git a/frontend/src/modules/modals/ModalsProvider.tsx b/frontend/src/modules/modals/ModalsProvider.tsx index d76aa763f..c704a7ae5 100644 --- a/frontend/src/modules/modals/ModalsProvider.tsx +++ b/frontend/src/modules/modals/ModalsProvider.tsx @@ -21,7 +21,7 @@ const ModalsProvider: FunctionComponent = ({ children }) => { prev[curr.modalKey] = curr; return prev; }, {}), - [] + [], ); return ( diff --git a/frontend/src/modules/modals/WithModal.tsx b/frontend/src/modules/modals/WithModal.tsx index c8de266f9..eadd6b932 100644 --- a/frontend/src/modules/modals/WithModal.tsx +++ b/frontend/src/modules/modals/WithModal.tsx @@ -17,7 +17,7 @@ export const ModalIdContext = createContext(null); export default function withModal( Content: FunctionComponent, key: string, - defaultSettings?: ModalSettings + defaultSettings?: ModalSettings, ) { const Comp: ModalComponent = (props) => { const { id, innerProps } = props; diff --git a/frontend/src/modules/modals/hooks.ts b/frontend/src/modules/modals/hooks.ts index 25399f414..40e891b8e 100644 --- a/frontend/src/modules/modals/hooks.ts +++ b/frontend/src/modules/modals/hooks.ts @@ -15,7 +15,7 @@ export function useModals() { ( modal: ModalComponent, props: ARGS, - settings?: ModalSettings + settings?: ModalSettings, ) => { openMantineContextModal(modal.modalKey, { ...modal.settings, @@ -23,14 +23,14 @@ export function useModals() { innerProps: props, }); }, - [openMantineContextModal] + [openMantineContextModal], ); const closeContextModal = useCallback( (modal: ModalComponent) => { rest.closeModal(modal.modalKey); }, - [rest] + [rest], ); const id = useContext(ModalIdContext); @@ -44,6 +44,6 @@ export function useModals() { // TODO: Performance return useMemo( () => ({ openContextModal, closeContextModal, closeSelf, ...rest }), - [closeContextModal, closeSelf, openContextModal, rest] + [closeContextModal, closeSelf, openContextModal, rest], ); } diff --git a/frontend/src/modules/socketio/socket.d.ts b/frontend/src/modules/socketio/socket.d.ts index f1b213a66..3fd5f7db3 100644 --- a/frontend/src/modules/socketio/socket.d.ts +++ b/frontend/src/modules/socketio/socket.d.ts @@ -32,7 +32,7 @@ declare namespace SocketIO { type ReducerGroup< E extends EventType, U extends PayloadType | undefined, - D = U + D = U, > = ValueOf<{ [P in E]: { key: P; diff --git a/frontend/src/modules/task/index.ts b/frontend/src/modules/task/index.ts index eb39e9e87..f4dc956cd 100644 --- a/frontend/src/modules/task/index.ts +++ b/frontend/src/modules/task/index.ts @@ -59,7 +59,7 @@ class TaskDispatcher { group, task.description, index, - tasks.length + tasks.length, ); updateNotification(notifyInProgress); @@ -120,8 +120,8 @@ class TaskDispatcher { item.header, item.name, item.value, - item.count - ) + item.count, + ), ); } else if (item.value > 1 && this.progress[item.id] === undefined) { showNotification(notification.progress.pending(item.id, item.header)); @@ -134,7 +134,7 @@ class TaskDispatcher { public removeProgress(ids: string[]) { setTimeout( () => ids.forEach(hideNotification), - notification.PROGRESS_TIMEOUT + notification.PROGRESS_TIMEOUT, ); } } diff --git a/frontend/src/modules/task/notification.ts b/frontend/src/modules/task/notification.ts index 1bd1b3de5..bb796b213 100644 --- a/frontend/src/modules/task/notification.ts +++ b/frontend/src/modules/task/notification.ts @@ -32,7 +32,7 @@ export const notification = { progress: { pending: ( id: string, - header: string + header: string, ): NotificationProps & { id: string } => { return { id, @@ -47,7 +47,7 @@ export const notification = { header: string, body: string, current: number, - total: number + total: number, ): NotificationProps & { id: string } => { return { id, diff --git a/frontend/src/pages/Blacklist/Movies/table.tsx b/frontend/src/pages/Blacklist/Movies/table.tsx index 02e63588d..9ab06f2ba 100644 --- a/frontend/src/pages/Blacklist/Movies/table.tsx +++ b/frontend/src/pages/Blacklist/Movies/table.tsx @@ -84,7 +84,7 @@ const Table: FunctionComponent = ({ blacklist }) => { }, }, ], - [] + [], ); return ( = ({ blacklist }) => { }, }, ], - [] + [], ); return ( = ({ hi: subtitle.hi, forced: subtitle.forced, }, - } + }, ); } else if (action === "delete" && subtitle.path) { task.create( @@ -97,7 +97,7 @@ export const Subtitle: FunctionComponent = ({ forced: subtitle.forced, path: subtitle.path, }, - } + }, ); } }} diff --git a/frontend/src/pages/Episodes/index.tsx b/frontend/src/pages/Episodes/index.tsx index 7a50f40c6..28e375744 100644 --- a/frontend/src/pages/Episodes/index.tsx +++ b/frontend/src/pages/Episodes/index.tsx @@ -72,7 +72,7 @@ const SeriesEpisodesView: FunctionComponent = () => { text: series?.seriesType ?? "", }, ], - [series] + [series], ); const modals = useModals(); @@ -92,12 +92,12 @@ const SeriesEpisodesView: FunctionComponent = () => { showNotification( notification.warn( "Cannot Upload Files", - "series or language profile is not ready" - ) + "series or language profile is not ready", + ), ); } }, - [modals, profile, series] + [modals, profile, series], ); useDocumentTitle(`${series?.title ?? "Unknown Series"} - Bazarr (Series)`); @@ -202,7 +202,7 @@ const SeriesEpisodesView: FunctionComponent = () => { item: series, mutation, }, - { title: series.title } + { title: series.title }, ); } }} diff --git a/frontend/src/pages/Episodes/table.tsx b/frontend/src/pages/Episodes/table.tsx index 77f503483..8cf02672e 100644 --- a/frontend/src/pages/Episodes/table.tsx +++ b/frontend/src/pages/Episodes/table.tsx @@ -73,7 +73,7 @@ const Table: FunctionComponent = ({ }, }); }, - [mutateAsync] + [mutateAsync], ); const columns: Column[] = useMemo[]>( @@ -194,7 +194,7 @@ const Table: FunctionComponent = ({ }, { title: `History - ${row.original.title}`, - } + }, ); }} icon={faHistory} @@ -204,16 +204,16 @@ const Table: FunctionComponent = ({ }, }, ], - [onlyDesired, profileItems, disabled, download] + [onlyDesired, profileItems, disabled, download], ); const maxSeason = useMemo( () => episodes?.reduce( (prev, curr) => Math.max(prev, curr.season), - 0 + 0, ) ?? 0, - [episodes] + [episodes], ); const instance = useRef | null>(null); diff --git a/frontend/src/pages/History/Movies/index.tsx b/frontend/src/pages/History/Movies/index.tsx index 80f05cae8..f82863cfc 100644 --- a/frontend/src/pages/History/Movies/index.tsx +++ b/frontend/src/pages/History/Movies/index.tsx @@ -144,7 +144,7 @@ const MoviesHistoryView: FunctionComponent = () => { }, }, ], - [] + [], ); const query = useMovieHistoryPagination(); diff --git a/frontend/src/pages/History/Series/index.tsx b/frontend/src/pages/History/Series/index.tsx index b52c070b8..ceea28299 100644 --- a/frontend/src/pages/History/Series/index.tsx +++ b/frontend/src/pages/History/Series/index.tsx @@ -167,7 +167,7 @@ const SeriesHistoryView: FunctionComponent = () => { }, }, ], - [] + [], ); const query = useEpisodeHistoryPagination(); diff --git a/frontend/src/pages/History/Statistics/index.tsx b/frontend/src/pages/History/Statistics/index.tsx index df03a0a65..243225538 100644 --- a/frontend/src/pages/History/Statistics/index.tsx +++ b/frontend/src/pages/History/Statistics/index.tsx @@ -50,7 +50,7 @@ const HistoryStats: FunctionComponent = () => { const languageOptions = useSelectorOptions( historyLanguages ?? [], - (value) => value.name + (value) => value.name, ); const [timeFrame, setTimeFrame] = useState("month"); diff --git a/frontend/src/pages/Movies/Details/index.tsx b/frontend/src/pages/Movies/Details/index.tsx index 488850cb4..a6b4b0aa8 100644 --- a/frontend/src/pages/Movies/Details/index.tsx +++ b/frontend/src/pages/Movies/Details/index.tsx @@ -78,7 +78,7 @@ const MovieDetailView: FunctionComponent = () => { }, }); }, - [downloadAsync] + [downloadAsync], ); const onDrop = useCallback( @@ -92,12 +92,12 @@ const MovieDetailView: FunctionComponent = () => { showNotification( notification.warn( "Cannot Upload Files", - "movie or language profile is not ready" - ) + "movie or language profile is not ready", + ), ); } }, - [modals, movie, profile] + [modals, movie, profile], ); const hasTask = useIsMovieActionRunning(); @@ -187,7 +187,7 @@ const MovieDetailView: FunctionComponent = () => { item: movie, mutation, }, - { title: movie.title } + { title: movie.title }, ); } }} diff --git a/frontend/src/pages/Movies/Details/table.tsx b/frontend/src/pages/Movies/Details/table.tsx index 9f4e5c948..0a327b745 100644 --- a/frontend/src/pages/Movies/Details/table.tsx +++ b/frontend/src/pages/Movies/Details/table.tsx @@ -126,7 +126,7 @@ const Table: FunctionComponent = ({ movie, profile, disabled }) => { forced, hi, }, - } + }, ); }} > @@ -150,11 +150,11 @@ const Table: FunctionComponent = ({ movie, profile, disabled }) => { hi, path, }, - } + }, ); } else if (action === "search") { throw new Error( - "This shouldn't happen, please report the bug" + "This shouldn't happen, please report the bug", ); } }} @@ -170,7 +170,7 @@ const Table: FunctionComponent = ({ movie, profile, disabled }) => { }, }, ], - [movie, disabled] + [movie, disabled], ); const data: Subtitle[] = useMemo(() => { diff --git a/frontend/src/pages/Movies/Editor.tsx b/frontend/src/pages/Movies/Editor.tsx index 4a86c6c45..a196f9deb 100644 --- a/frontend/src/pages/Movies/Editor.tsx +++ b/frontend/src/pages/Movies/Editor.tsx @@ -32,7 +32,7 @@ const MovieMassEditor: FunctionComponent = () => { }, }, ], - [] + [], ); useDocumentTitle("Movies - Bazarr (Mass Editor)"); diff --git a/frontend/src/pages/Movies/index.tsx b/frontend/src/pages/Movies/index.tsx index 479ed50ee..dd9f531e1 100644 --- a/frontend/src/pages/Movies/index.tsx +++ b/frontend/src/pages/Movies/index.tsx @@ -99,7 +99,7 @@ const MovieView: FunctionComponent = () => { }, { title: row.original.title, - } + }, ) } icon={faWrench} @@ -108,7 +108,7 @@ const MovieView: FunctionComponent = () => { }, }, ], - [] + [], ); useDocumentTitle("Movies - Bazarr"); diff --git a/frontend/src/pages/Series/Editor.tsx b/frontend/src/pages/Series/Editor.tsx index cd6c1722e..4db9a4c1d 100644 --- a/frontend/src/pages/Series/Editor.tsx +++ b/frontend/src/pages/Series/Editor.tsx @@ -32,7 +32,7 @@ const SeriesMassEditor: FunctionComponent = () => { }, }, ], - [] + [], ); useDocumentTitle("Series - Bazarr (Mass Editor)"); diff --git a/frontend/src/pages/Series/index.tsx b/frontend/src/pages/Series/index.tsx index 6dfe07230..139ad3a46 100644 --- a/frontend/src/pages/Series/index.tsx +++ b/frontend/src/pages/Series/index.tsx @@ -106,7 +106,7 @@ const SeriesView: FunctionComponent = () => { }, { title: original.title, - } + }, ) } icon={faWrench} @@ -115,7 +115,7 @@ const SeriesView: FunctionComponent = () => { }, }, ], - [mutation] + [mutation], ); useDocumentTitle("Series - Bazarr"); diff --git a/frontend/src/pages/Settings/Languages/components.tsx b/frontend/src/pages/Settings/Languages/components.tsx index 0fa3b0f57..de3e89c3e 100644 --- a/frontend/src/pages/Settings/Languages/components.tsx +++ b/frontend/src/pages/Settings/Languages/components.tsx @@ -35,7 +35,7 @@ export const LanguageSelector: FunctionComponent< searchable onChange={(val) => { setValue(val, settingKey, (value: Language.Info[]) => - value.map((v) => v.code2) + value.map((v) => v.code2), ); }} > @@ -53,7 +53,7 @@ export const ProfileSelector: FunctionComponent< profiles.map((v) => { return { label: v.name, value: v.profileId }; }), - [profiles] + [profiles], ); return ( diff --git a/frontend/src/pages/Settings/Languages/equals.test.ts b/frontend/src/pages/Settings/Languages/equals.test.ts index 19c641fcb..ead613946 100644 --- a/frontend/src/pages/Settings/Languages/equals.test.ts +++ b/frontend/src/pages/Settings/Languages/equals.test.ts @@ -15,7 +15,7 @@ describe("Equals Parser", () => { function testParsedResult( text: string, - expected: LanguageEqualImmediateData + expected: LanguageEqualImmediateData, ) { const result = decodeEqualData(text); @@ -26,7 +26,7 @@ describe("Equals Parser", () => { expect( result, - `${text} does not match with the expected equal data` + `${text} does not match with the expected equal data`, ).toStrictEqual(expected); } @@ -187,7 +187,7 @@ describe("Equals Parser", () => { expect( encoded, - `Encoded result '${encoded}' is not matched to '${expected}'` + `Encoded result '${encoded}' is not matched to '${expected}'`, ).toEqual(expected); } diff --git a/frontend/src/pages/Settings/Languages/equals.tsx b/frontend/src/pages/Settings/Languages/equals.tsx index a958237df..a4fe95eee 100644 --- a/frontend/src/pages/Settings/Languages/equals.tsx +++ b/frontend/src/pages/Settings/Languages/equals.tsx @@ -28,7 +28,7 @@ export type LanguageEqualImmediateData = export type LanguageEqualData = LanguageEqualGenericData; function decodeEqualTarget( - text: string + text: string, ): GenericEqualTarget | undefined { const [code, decoration] = text.split("@"); @@ -47,7 +47,7 @@ function decodeEqualTarget( } export function decodeEqualData( - text: string + text: string, ): LanguageEqualImmediateData | undefined { const [first, second] = text.split(":"); @@ -97,10 +97,10 @@ export function useLatestLanguageEquals(): LanguageEqualData[] { } const source = data?.find( - (value) => value.code3 === parsed.source.content + (value) => value.code3 === parsed.source.content, ); const target = data?.find( - (value) => value.code3 === parsed.target.content + (value) => value.code3 === parsed.target.content, ); if (source === undefined || target === undefined) { @@ -113,7 +113,7 @@ export function useLatestLanguageEquals(): LanguageEqualData[] { }; }) .filter((v): v is LanguageEqualData => v !== undefined) ?? [], - [data, latest] + [data, latest], ); } @@ -134,7 +134,7 @@ const EqualsTable: FunctionComponent = () => { LOG("info", "updating language equals data", values); setValue(encodedValues, languageEqualsKey); }, - [setValue] + [setValue], ); const add = useCallback(() => { @@ -178,7 +178,7 @@ const EqualsTable: FunctionComponent = () => { newValue[index] = { ...value }; setEquals(newValue); }, - [equals, setEquals] + [equals, setEquals], ); const remove = useCallback( @@ -193,7 +193,7 @@ const EqualsTable: FunctionComponent = () => { setEquals(newValue); }, - [equals, setEquals] + [equals, setEquals], ); const columns = useMemo[]>( @@ -349,7 +349,7 @@ const EqualsTable: FunctionComponent = () => { }, }, ], - [remove, update] + [remove, update], ); return ( diff --git a/frontend/src/pages/Settings/Languages/table.tsx b/frontend/src/pages/Settings/Languages/table.tsx index ac8da306c..a1ee217e8 100644 --- a/frontend/src/pages/Settings/Languages/table.tsx +++ b/frontend/src/pages/Settings/Languages/table.tsx @@ -23,7 +23,7 @@ const Table: FunctionComponent = () => { () => 1 + profiles.reduce((val, prof) => Math.max(prof.profileId, val), 0), - [profiles] + [profiles], ); const { setValue } = useFormActions(); @@ -34,7 +34,7 @@ const Table: FunctionComponent = () => { (list: Language.Profile[]) => { setValue(list, languageProfileKey, (value) => JSON.stringify(value)); }, - [setValue] + [setValue], ); const updateProfile = useCallback( @@ -49,7 +49,7 @@ const Table: FunctionComponent = () => { } submitProfiles(list); }, - [profiles, submitProfiles] + [profiles, submitProfiles], ); const action = useArrayAction((fn) => { @@ -152,7 +152,7 @@ const Table: FunctionComponent = () => { }, ], // TODO: Optimize this - [action, languages, modals, updateProfile] + [action, languages, modals, updateProfile], ); const canAdd = languages.length !== 0; diff --git a/frontend/src/pages/Settings/Notifications/components.tsx b/frontend/src/pages/Settings/Notifications/components.tsx index 11ff4db0c..1a2b20f65 100644 --- a/frontend/src/pages/Settings/Notifications/components.tsx +++ b/frontend/src/pages/Settings/Notifications/components.tsx @@ -37,7 +37,7 @@ const NotificationForm: FunctionComponent = ({ }) => { const availableSelections = useMemo( () => selections.filter((v) => !v.enabled || v.name === payload?.name), - [payload?.name, selections] + [payload?.name, selections], ); const options = useSelectorOptions(availableSelections, (v) => v.name); @@ -51,11 +51,11 @@ const NotificationForm: FunctionComponent = ({ validate: { selection: FormUtils.validation( isObject, - "Please select a notification provider" + "Please select a notification provider", ), url: FormUtils.validation( (value) => value.trim().length !== 0, - "URL must not be empty" + "URL must not be empty", ), }, }); @@ -123,20 +123,20 @@ export const NotificationView: FunctionComponent = () => { { onLoaded: (settings) => settings.notifications.providers, onSubmit: (value) => value.map((v) => JSON.stringify(v)), - } + }, ); const update = useUpdateArray( notificationsKey, notifications ?? [], - "name" + "name", ); const updateWrapper = useCallback( (info: Settings.NotificationInfo) => { update(info, notificationHook); }, - [update] + [update], ); const modals = useModals(); diff --git a/frontend/src/pages/Settings/Providers/components.tsx b/frontend/src/pages/Settings/Providers/components.tsx index 46252a03d..5897e19a3 100644 --- a/frontend/src/pages/Settings/Providers/components.tsx +++ b/frontend/src/pages/Settings/Providers/components.tsx @@ -63,7 +63,7 @@ export const ProviderView: FunctionComponent = () => { }); } }, - [modals, providers, settings, staged, update] + [modals, providers, settings, staged, update], ); const cards = useMemo(() => { @@ -171,7 +171,7 @@ const ProviderTool: FunctionComponent = ({ modals.closeAll(); } }, - [info, enabledProviders, modals] + [info, enabledProviders, modals], ); const canSave = info !== null; @@ -192,14 +192,14 @@ const ProviderTool: FunctionComponent = ({ ProviderList.filter( (v) => enabledProviders?.find((p) => p === v.key && p !== info?.key) === - undefined + undefined, ), - [info?.key, enabledProviders] + [info?.key, enabledProviders], ); const options = useSelectorOptions( availableOptions, - (v) => v.name ?? capitalize(v.key) + (v) => v.name ?? capitalize(v.key), ); const inputs = useMemo(() => { @@ -223,7 +223,7 @@ const ProviderTool: FunctionComponent = ({ key={BuildKey(itemKey, key)} label={label} settingKey={`settings-${itemKey}-${key}`} - > + >, ); return; case "password": @@ -232,7 +232,7 @@ const ProviderTool: FunctionComponent = ({ key={BuildKey(itemKey, key)} label={label} settingKey={`settings-${itemKey}-${key}`} - > + >, ); return; case "switch": @@ -242,7 +242,7 @@ const ProviderTool: FunctionComponent = ({ inline label={label} settingKey={`settings-${itemKey}-${key}`} - > + >, ); return; case "select": @@ -252,7 +252,7 @@ const ProviderTool: FunctionComponent = ({ label={label} settingKey={`settings-${itemKey}-${key}`} options={options} - > + >, ); return; case "chips": @@ -261,7 +261,7 @@ const ProviderTool: FunctionComponent = ({ key={key} label={label} settingKey={`settings-${itemKey}-${key}`} - > + >, ); return; default: diff --git a/frontend/src/pages/Settings/Subtitles/options.ts b/frontend/src/pages/Settings/Subtitles/options.ts index 0af2f0fbb..d38133133 100644 --- a/frontend/src/pages/Settings/Subtitles/options.ts +++ b/frontend/src/pages/Settings/Subtitles/options.ts @@ -160,7 +160,7 @@ export const providerOptions: SelectorOption[] = ProviderList.map( (v) => ({ label: v.key, value: v.key, - }) + }), ); export const syncMaxOffsetSecondsOptions: SelectorOption[] = [ diff --git a/frontend/src/pages/Settings/components/Layout.test.tsx b/frontend/src/pages/Settings/components/Layout.test.tsx index 0cec7c9cd..512d0310c 100644 --- a/frontend/src/pages/Settings/components/Layout.test.tsx +++ b/frontend/src/pages/Settings/components/Layout.test.tsx @@ -8,7 +8,7 @@ describe("Settings layout", () => { render( Value - + , ); }); @@ -16,9 +16,9 @@ describe("Settings layout", () => { render( Value - + , ); - expect(screen.getByRole("button", { name: "Save" })).toBeDisabled(); + expect(screen.getAllByRole("button", { name: "Save" })[0]).toBeDisabled(); }); }); diff --git a/frontend/src/pages/Settings/components/Layout.tsx b/frontend/src/pages/Settings/components/Layout.tsx index a0922cd95..b20c8092b 100644 --- a/frontend/src/pages/Settings/components/Layout.tsx +++ b/frontend/src/pages/Settings/components/Layout.tsx @@ -47,7 +47,7 @@ const Layout: FunctionComponent = (props) => { mutate(settingsToSubmit); } }, - [mutate] + [mutate], ); const totalStagedCount = useMemo(() => { @@ -56,7 +56,7 @@ const Layout: FunctionComponent = (props) => { usePrompt( totalStagedCount > 0, - `You have ${totalStagedCount} unsaved changes, are you sure you want to leave?` + `You have ${totalStagedCount} unsaved changes, are you sure you want to leave?`, ); useDocumentTitle(`${name} - Bazarr (Settings)`); diff --git a/frontend/src/pages/Settings/components/LayoutModal.tsx b/frontend/src/pages/Settings/components/LayoutModal.tsx index 3ec074e09..cb4d5a1b5 100644 --- a/frontend/src/pages/Settings/components/LayoutModal.tsx +++ b/frontend/src/pages/Settings/components/LayoutModal.tsx @@ -54,7 +54,7 @@ const LayoutModal: FunctionComponent = (props) => { }, 500); } }, - [mutate, callbackModal] + [mutate, callbackModal], ); const totalStagedCount = useMemo(() => { diff --git a/frontend/src/pages/Settings/components/Section.test.tsx b/frontend/src/pages/Settings/components/Section.test.tsx index 716b95e40..e7f270e0d 100644 --- a/frontend/src/pages/Settings/components/Section.test.tsx +++ b/frontend/src/pages/Settings/components/Section.test.tsx @@ -17,7 +17,7 @@ describe("Settings section", () => { rawRender(
{text} -
+ , ); expect(screen.getByText(header)).toBeDefined(); @@ -29,7 +29,7 @@ describe("Settings section", () => { rawRender( + , ); expect(screen.getByText(header)).not.toBeVisible(); diff --git a/frontend/src/pages/Settings/components/forms.test.tsx b/frontend/src/pages/Settings/components/forms.test.tsx index 78ec5341e..19c66ade0 100644 --- a/frontend/src/pages/Settings/components/forms.test.tsx +++ b/frontend/src/pages/Settings/components/forms.test.tsx @@ -17,7 +17,7 @@ const FormSupport: FunctionComponent = ({ children }) => { const formRender = ( ui: ReactElement, - options?: Omit + options?: Omit, ) => rawRender(ui, { wrapper: FormSupport, ...options }); describe("Settings form", () => { diff --git a/frontend/src/pages/Settings/components/forms.tsx b/frontend/src/pages/Settings/components/forms.tsx index d0efa8e91..3e1d3f12f 100644 --- a/frontend/src/pages/Settings/components/forms.tsx +++ b/frontend/src/pages/Settings/components/forms.tsx @@ -115,7 +115,7 @@ export type MultiSelectorProps = BaseInput & GlobalMultiSelectorProps; export function MultiSelector( - props: MultiSelectorProps + props: MultiSelectorProps, ) { const { value, update, rest } = useBaseInput(props); @@ -187,7 +187,7 @@ export const Action: FunctionComponent< interface FileProps extends BaseInput {} export const File: FunctionComponent> = ( - props + props, ) => { const { value, update, rest } = useBaseInput(props); diff --git a/frontend/src/pages/Settings/components/pathMapper.tsx b/frontend/src/pages/Settings/components/pathMapper.tsx index a7e76594b..8bb3514b7 100644 --- a/frontend/src/pages/Settings/components/pathMapper.tsx +++ b/frontend/src/pages/Settings/components/pathMapper.tsx @@ -56,10 +56,10 @@ export const PathMappingTable: FunctionComponent = ({ type }) => { (newItems: PathMappingItem[]) => { setValue( newItems.map((v) => [v.from, v.to]), - key + key, ); }, - [key, setValue] + [key, setValue], ); const addRow = useCallback(() => { @@ -71,7 +71,7 @@ export const PathMappingTable: FunctionComponent = ({ type }) => { const data = useMemo( () => items?.map((v) => ({ from: v[0], to: v[1] })) ?? [], - [items] + [items], ); const action = useArrayAction((fn) => { @@ -130,7 +130,7 @@ export const PathMappingTable: FunctionComponent = ({ type }) => { }, }, ], - [action, type] + [action, type], ); if (enabled) { diff --git a/frontend/src/pages/Settings/utilities/FormValues.ts b/frontend/src/pages/Settings/utilities/FormValues.ts index 2fbe54b93..d5d1774f5 100644 --- a/frontend/src/pages/Settings/utilities/FormValues.ts +++ b/frontend/src/pages/Settings/utilities/FormValues.ts @@ -3,7 +3,7 @@ import type { UseFormReturnType } from "@mantine/form"; import { createContext, useCallback, useContext, useRef } from "react"; export const FormContext = createContext | null>( - null + null, ); export function useFormValues() { @@ -44,7 +44,7 @@ export function useFormActions() { if (hook) { LOG( "info", - `Adding submit hook ${key}, will be executed before submitting` + `Adding submit hook ${key}, will be executed before submitting`, ); hooks[key] = hook; } @@ -72,7 +72,7 @@ export type FormValues = { export function runHooks( hooks: FormValues["hooks"], - settings: FormValues["settings"] + settings: FormValues["settings"], ) { for (const key in settings) { if (key in hooks) { diff --git a/frontend/src/pages/Settings/utilities/hooks.ts b/frontend/src/pages/Settings/utilities/hooks.ts index 3de556c71..da874314e 100644 --- a/frontend/src/pages/Settings/utilities/hooks.ts +++ b/frontend/src/pages/Settings/utilities/hooks.ts @@ -36,7 +36,7 @@ export function useBaseInput(props: T & BaseInput) { setValue(moddedValue, settingKey, settingOptions?.onSubmit); }, - [settingOptions, setValue, settingKey] + [settingOptions, setValue, settingKey], ); return { value, update, rest }; @@ -44,7 +44,7 @@ export function useBaseInput(props: T & BaseInput) { export function useSettingValue( key: string, - options?: SettingValueOptions + options?: SettingValueOptions, ): Readonly> { const settings = useSettings(); @@ -85,7 +85,7 @@ export function useSettingValue( export function useUpdateArray( key: string, current: Readonly, - compare: keyof T + compare: keyof T, ) { const { setValue } = useFormActions(); const stagedValue = useStagedValues(); @@ -106,6 +106,6 @@ export function useUpdateArray( const newArray = uniqBy([v, ...staged], compareRef.current); setValue(newArray, key, hook); }, - [staged, setValue, key] + [staged, setValue, key], ); } diff --git a/frontend/src/pages/System/Announcements/table.tsx b/frontend/src/pages/System/Announcements/table.tsx index 97f8cbe3e..2d9037b20 100644 --- a/frontend/src/pages/System/Announcements/table.tsx +++ b/frontend/src/pages/System/Announcements/table.tsx @@ -62,7 +62,7 @@ const Table: FunctionComponent = ({ announcements }) => { }, }, ], - [] + [], ); return ( diff --git a/frontend/src/pages/System/Backups/table.tsx b/frontend/src/pages/System/Backups/table.tsx index 1a2801b85..d0570da40 100644 --- a/frontend/src/pages/System/Backups/table.tsx +++ b/frontend/src/pages/System/Backups/table.tsx @@ -100,7 +100,7 @@ const Table: FunctionComponent = ({ backups }) => { }, }, ], - [] + [], ); return ; diff --git a/frontend/src/pages/System/Logs/modal.tsx b/frontend/src/pages/System/Logs/modal.tsx index 0ecceb742..297909757 100644 --- a/frontend/src/pages/System/Logs/modal.tsx +++ b/frontend/src/pages/System/Logs/modal.tsx @@ -14,7 +14,7 @@ const SystemLogModal: FunctionComponent = ({ stack }) => { {v} )), - [stack] + [stack], ); return {result}; diff --git a/frontend/src/pages/System/Logs/table.tsx b/frontend/src/pages/System/Logs/table.tsx index c6419cd4c..5a36f0f2b 100644 --- a/frontend/src/pages/System/Logs/table.tsx +++ b/frontend/src/pages/System/Logs/table.tsx @@ -70,7 +70,7 @@ const Table: FunctionComponent = ({ logs }) => { }, }, ], - [] + [], ); return ( diff --git a/frontend/src/pages/System/Providers/table.tsx b/frontend/src/pages/System/Providers/table.tsx index 47991b3c5..961da65fb 100644 --- a/frontend/src/pages/System/Providers/table.tsx +++ b/frontend/src/pages/System/Providers/table.tsx @@ -22,7 +22,7 @@ const Table: FunctionComponent = (props) => { accessor: "retry", }, ], - [] + [], ); return ; diff --git a/frontend/src/pages/System/Releases/index.tsx b/frontend/src/pages/System/Releases/index.tsx index 63e8d0314..f205da086 100644 --- a/frontend/src/pages/System/Releases/index.tsx +++ b/frontend/src/pages/System/Releases/index.tsx @@ -42,7 +42,7 @@ const ReleaseCard: FunctionComponent = ({ }) => { const infos = useMemo( () => body.map((v) => v.replace(/(\s\[.*?\])\(.*?\)/, "")), - [body] + [body], ); return ( diff --git a/frontend/src/pages/System/Status/index.tsx b/frontend/src/pages/System/Status/index.tsx index 2fca8d26d..ebb5d03d8 100644 --- a/frontend/src/pages/System/Status/index.tsx +++ b/frontend/src/pages/System/Status/index.tsx @@ -86,7 +86,7 @@ const SystemStatusView: FunctionComponent = () => { if (startTime) { const duration = moment.duration( moment().utc().unix() - startTime, - "seconds" + "seconds", ), days = duration.days(), hours = duration.hours().toString().padStart(2, "0"), diff --git a/frontend/src/pages/System/Status/table.tsx b/frontend/src/pages/System/Status/table.tsx index c570fbabf..3b8a87e8a 100644 --- a/frontend/src/pages/System/Status/table.tsx +++ b/frontend/src/pages/System/Status/table.tsx @@ -28,7 +28,7 @@ const Table: FunctionComponent = ({ health }) => { }, }, ], - [] + [], ); return ( diff --git a/frontend/src/pages/System/Tasks/table.tsx b/frontend/src/pages/System/Tasks/table.tsx index ac87c7c54..38ea5b5e0 100644 --- a/frontend/src/pages/System/Tasks/table.tsx +++ b/frontend/src/pages/System/Tasks/table.tsx @@ -52,7 +52,7 @@ const Table: FunctionComponent = ({ tasks }) => { }, }, ], - [] + [], ); return ( diff --git a/frontend/src/pages/Wanted/Movies/index.tsx b/frontend/src/pages/Wanted/Movies/index.tsx index 663a2fa57..57fb6d6ed 100644 --- a/frontend/src/pages/Wanted/Movies/index.tsx +++ b/frontend/src/pages/Wanted/Movies/index.tsx @@ -58,7 +58,7 @@ const WantedMoviesView: FunctionComponent = () => { hi: item.hi, forced: item.forced, }, - } + }, ); }} > @@ -70,7 +70,7 @@ const WantedMoviesView: FunctionComponent = () => { }, }, ], - [] + [], ); const { mutateAsync } = useMovieAction(); diff --git a/frontend/src/pages/Wanted/Series/index.tsx b/frontend/src/pages/Wanted/Series/index.tsx index 863f17dfd..58e9f525c 100644 --- a/frontend/src/pages/Wanted/Series/index.tsx +++ b/frontend/src/pages/Wanted/Series/index.tsx @@ -74,7 +74,7 @@ const WantedSeriesView: FunctionComponent = () => { hi: item.hi, forced: item.forced, }, - } + }, ); }} > @@ -86,7 +86,7 @@ const WantedSeriesView: FunctionComponent = () => { }, }, ], - [] + [], ); const { mutateAsync } = useSeriesAction(); diff --git a/frontend/src/pages/views/ItemOverview.tsx b/frontend/src/pages/views/ItemOverview.tsx index 0f1b536b3..d95944db3 100644 --- a/frontend/src/pages/views/ItemOverview.tsx +++ b/frontend/src/pages/views/ItemOverview.tsx @@ -67,7 +67,7 @@ const ItemOverview: FunctionComponent = (props) => { badges.push( {item.path} - + , ); badges.push( @@ -75,14 +75,14 @@ const ItemOverview: FunctionComponent = (props) => { {val.text} - )) ?? []) + )) ?? []), ); if (item.tags.length > 0) { badges.push( {item.tags.join("|")} - + , ); } } @@ -101,7 +101,7 @@ const ItemOverview: FunctionComponent = (props) => { {v.name} )) ?? [], - [item?.audio_language] + [item?.audio_language], ); const profile = useLanguageProfileBy(item?.profileId); @@ -118,7 +118,7 @@ const ItemOverview: FunctionComponent = (props) => { title="Languages Profile" > {profile.name} - + , ); badges.push( @@ -130,7 +130,7 @@ const ItemOverview: FunctionComponent = (props) => { > - )) + )), ); } diff --git a/frontend/src/pages/views/MassEditor.tsx b/frontend/src/pages/views/MassEditor.tsx index 02668302d..b15a55e83 100644 --- a/frontend/src/pages/views/MassEditor.tsx +++ b/frontend/src/pages/views/MassEditor.tsx @@ -31,7 +31,7 @@ function MassEditor(props: MassEditorProps) { const data = useMemo( () => uniqBy([...dirties, ...(raw ?? [])], GetItemId), - [dirties, raw] + [dirties, raw], ); const profileOptions = useSelectorOptions(profiles ?? [], (v) => v.name); @@ -43,7 +43,7 @@ function MassEditor(props: MassEditorProps) { { label: "Clear", value: null, group: "Action" }, ...profileOptions.options, ], - [profileOptions.options] + [profileOptions.options], ); const getKey = useCallback((value: Language.Profile | null) => { @@ -80,7 +80,7 @@ function MassEditor(props: MassEditorProps) { return uniqBy([...newItems, ...dirty], GetItemId); }); }, - [selections] + [selections], ); return ( diff --git a/frontend/src/tests/index.tsx b/frontend/src/tests/index.tsx index b627626fd..d18df7227 100644 --- a/frontend/src/tests/index.tsx +++ b/frontend/src/tests/index.tsx @@ -34,7 +34,7 @@ const AllProvidersWithStrictMode: FunctionComponent = ({ const customRender = ( ui: ReactElement, - options?: Omit + options?: Omit, ) => render(ui, { wrapper: AllProvidersWithStrictMode, ...options }); // re-export everything diff --git a/frontend/src/types/basic.d.ts b/frontend/src/types/basic.d.ts index 7ed30808c..4a48fb094 100644 --- a/frontend/src/types/basic.d.ts +++ b/frontend/src/types/basic.d.ts @@ -13,7 +13,7 @@ type StorageType = string | null; type SimpleStateType = [ T, - ((item: T) => void) | ((fn: (item: T) => T) => void) + ((item: T) => void) | ((fn: (item: T) => T) => void), ]; type Factory = () => T; diff --git a/frontend/src/types/function.d.ts b/frontend/src/types/function.d.ts index 9efa69169..e0b54e115 100644 --- a/frontend/src/types/function.d.ts +++ b/frontend/src/types/function.d.ts @@ -1,3 +1,3 @@ type RangeQuery = ( - param: Parameter.Range + param: Parameter.Range, ) => Promise>; diff --git a/frontend/src/types/react-table.d.ts b/frontend/src/types/react-table.d.ts index e1acd4230..6795b50a3 100644 --- a/frontend/src/types/react-table.d.ts +++ b/frontend/src/types/react-table.d.ts @@ -60,14 +60,14 @@ declare module "react-table" { } export interface Hooks< - D extends Record = Record + D extends Record = Record, > extends UseExpandedHooks, UseGroupByHooks, UseRowSelectHooks, UseSortByHooks {} export interface TableInstance< - D extends Record = Record + D extends Record = Record, > extends UseColumnOrderInstanceProps, UseExpandedInstanceProps, // UseFiltersInstanceProps, @@ -80,7 +80,7 @@ declare module "react-table" { CustomTableProps {} export interface TableState< - D extends Record = Record + D extends Record = Record, > extends UseColumnOrderState, UseExpandedState, // UseFiltersState, @@ -93,7 +93,7 @@ declare module "react-table" { UseSortByState {} export interface ColumnInterface< - D extends Record = Record + D extends Record = Record, > extends UseFiltersColumnOptions, // UseGlobalFiltersColumnOptions, UseGroupByColumnOptions, @@ -101,7 +101,7 @@ declare module "react-table" { UseSortByColumnOptions {} export interface ColumnInstance< - D extends Record = Record + D extends Record = Record, > extends UseFiltersColumnProps, UseGroupByColumnProps, // UseResizeColumnsColumnProps, @@ -109,11 +109,11 @@ declare module "react-table" { // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface Cell< - D extends Record = Record + D extends Record = Record, > extends UseGroupByCellProps {} export interface Row< - D extends Record = Record + D extends Record = Record, > extends UseExpandedRowProps, UseGroupByRowProps, UseRowSelectRowProps {} diff --git a/frontend/src/utilities/console.ts b/frontend/src/utilities/console.ts index ac474fec1..64da1330c 100644 --- a/frontend/src/utilities/console.ts +++ b/frontend/src/utilities/console.ts @@ -25,7 +25,7 @@ export function ENSURE(condition: boolean, msg: string, ...payload: any[]) { export function GROUP( header: string, - content: (logger: typeof console.log) => void + content: (logger: typeof console.log) => void, ) { if (!isProdEnv) { console.group(header); diff --git a/frontend/src/utilities/event.ts b/frontend/src/utilities/event.ts index 60fbd63e1..1a6e45342 100644 --- a/frontend/src/utilities/event.ts +++ b/frontend/src/utilities/event.ts @@ -2,7 +2,7 @@ type CustomEventDetail = T extends CustomEvent ? D : never; function createEvent< K extends keyof WindowEventMap, - P extends CustomEventDetail + P extends CustomEventDetail, >(event: K, payload: P) { return new CustomEvent

(event, { bubbles: true, detail: payload }); } diff --git a/frontend/src/utilities/hooks.ts b/frontend/src/utilities/hooks.ts index dd485ebdd..4f703877c 100644 --- a/frontend/src/utilities/hooks.ts +++ b/frontend/src/utilities/hooks.ts @@ -18,7 +18,7 @@ export function useGotoHomepage() { export function useSelectorOptions( options: readonly T[], label: (value: T) => string, - key?: (value: T) => string + key?: (value: T) => string, ): Pick, "options" | "getkey"> { const labelRef = useRef(label); labelRef.current = label; @@ -32,7 +32,7 @@ export function useSelectorOptions( value, label: labelRef.current(value), })), - [options] + [options], ); return useMemo( @@ -40,7 +40,7 @@ export function useSelectorOptions( options: wrappedOptions, getkey: keyRef.current ?? labelRef.current, }), - [wrappedOptions] + [wrappedOptions], ); } @@ -51,7 +51,7 @@ export function useSliderMarks(values: number[]): SliderProps["marks"] { value: value, label: value.toString(), })), - [values] + [values], ); } @@ -103,7 +103,7 @@ export function useArrayAction(setData: Dispatch<(prev: T[]) => T[]>) { remove, update, }), - [add, mutate, remove, update] + [add, mutate, remove, update], ); } @@ -121,7 +121,7 @@ export function useThrottle(fn: F, ms: number) { } timer.current = window.setTimeout(() => fnRef.current(...args), ms); }, - [ms] + [ms], ); } diff --git a/frontend/src/utilities/index.ts b/frontend/src/utilities/index.ts index 549660722..b0555ad31 100644 --- a/frontend/src/utilities/index.ts +++ b/frontend/src/utilities/index.ts @@ -5,7 +5,7 @@ import { isEpisode, isMovie, isSeries } from "./validate"; export function toggleState( dispatch: Dispatch, wait: number, - start = false + start = false, ) { dispatch(!start); setTimeout(() => dispatch(start), wait); @@ -43,7 +43,7 @@ export function pathJoin(...parts: string[]) { export function filterSubtitleBy( subtitles: Subtitle[], - languages: Language.Info[] + languages: Language.Info[], ): Subtitle[] { if (languages.length === 0) { return subtitles.filter((subtitle) => { @@ -53,7 +53,7 @@ export function filterSubtitleBy( const result = differenceWith( subtitles, languages, - (a, b) => a.code2 === b.code2 || a.path !== null || a.code2 === undefined + (a, b) => a.code2 === b.code2 || a.path !== null || a.code2 === undefined, ); return difference(subtitles, result); } diff --git a/frontend/src/utilities/languages.ts b/frontend/src/utilities/languages.ts index 282db22a9..ca0c5c51a 100644 --- a/frontend/src/utilities/languages.ts +++ b/frontend/src/utilities/languages.ts @@ -39,7 +39,7 @@ export function useProfileItemsToLanguages(profile?: Language.Profile) { name, }; }) ?? [], - [data, profile?.items] + [data, profile?.items], ); } @@ -48,6 +48,6 @@ export function useLanguageFromCode3(code3: string) { return useMemo( () => data?.find((value) => value.code3 === code3), - [data, code3] + [data, code3], ); } diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index 390e328e3..7815fa95f 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -19,8 +19,8 @@ "incremental": true, "noFallthroughCasesInSwitch": true, "paths": { - "@/*": ["./src/*"] - } + "@/*": ["./src/*"], + }, }, - "include": ["./src", "./test"] + "include": ["./src", "./test"], } diff --git a/libs/APScheduler-3.10.4.dist-info/INSTALLER b/libs/APScheduler-3.10.4.dist-info/INSTALLER new file mode 100644 index 000000000..a1b589e38 --- /dev/null +++ b/libs/APScheduler-3.10.4.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/libs/APScheduler-3.10.4.dist-info/LICENSE.txt b/libs/APScheduler-3.10.4.dist-info/LICENSE.txt new file mode 100644 index 000000000..07806f8af --- /dev/null +++ b/libs/APScheduler-3.10.4.dist-info/LICENSE.txt @@ -0,0 +1,19 @@ +This is the MIT license: http://www.opensource.org/licenses/mit-license.php + +Copyright (c) Alex Grönholm + +Permission is hereby granted, free of charge, to any person obtaining a copy of this +software and associated documentation files (the "Software"), to deal in the Software +without restriction, including without limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or +substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/libs/APScheduler-3.10.4.dist-info/METADATA b/libs/APScheduler-3.10.4.dist-info/METADATA new file mode 100644 index 000000000..62df97e73 --- /dev/null +++ b/libs/APScheduler-3.10.4.dist-info/METADATA @@ -0,0 +1,138 @@ +Metadata-Version: 2.1 +Name: APScheduler +Version: 3.10.4 +Summary: In-process task scheduler with Cron-like capabilities +Home-page: https://github.com/agronholm/apscheduler +Author: Alex Grönholm +Author-email: apscheduler@nextday.fi +License: MIT +Keywords: scheduling cron +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: MIT License +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Requires-Python: >=3.6 +License-File: LICENSE.txt +Requires-Dist: six >=1.4.0 +Requires-Dist: pytz +Requires-Dist: tzlocal !=3.*,>=2.0 +Requires-Dist: importlib-metadata >=3.6.0 ; python_version < "3.8" +Provides-Extra: doc +Requires-Dist: sphinx ; extra == 'doc' +Requires-Dist: sphinx-rtd-theme ; extra == 'doc' +Provides-Extra: gevent +Requires-Dist: gevent ; extra == 'gevent' +Provides-Extra: mongodb +Requires-Dist: pymongo >=3.0 ; extra == 'mongodb' +Provides-Extra: redis +Requires-Dist: redis >=3.0 ; extra == 'redis' +Provides-Extra: rethinkdb +Requires-Dist: rethinkdb >=2.4.0 ; extra == 'rethinkdb' +Provides-Extra: sqlalchemy +Requires-Dist: sqlalchemy >=1.4 ; extra == 'sqlalchemy' +Provides-Extra: testing +Requires-Dist: pytest ; extra == 'testing' +Requires-Dist: pytest-asyncio ; extra == 'testing' +Requires-Dist: pytest-cov ; extra == 'testing' +Requires-Dist: pytest-tornado5 ; extra == 'testing' +Provides-Extra: tornado +Requires-Dist: tornado >=4.3 ; extra == 'tornado' +Provides-Extra: twisted +Requires-Dist: twisted ; extra == 'twisted' +Provides-Extra: zookeeper +Requires-Dist: kazoo ; extra == 'zookeeper' + +.. image:: https://github.com/agronholm/apscheduler/workflows/Python%20codeqa/test/badge.svg?branch=3.x + :target: https://github.com/agronholm/apscheduler/actions?query=workflow%3A%22Python+codeqa%2Ftest%22+branch%3A3.x + :alt: Build Status +.. image:: https://coveralls.io/repos/github/agronholm/apscheduler/badge.svg?branch=3.x + :target: https://coveralls.io/github/agronholm/apscheduler?branch=3.x + :alt: Code Coverage +.. image:: https://readthedocs.org/projects/apscheduler/badge/?version=3.x + :target: https://apscheduler.readthedocs.io/en/master/?badge=3.x + :alt: Documentation + +Advanced Python Scheduler (APScheduler) is a Python library that lets you schedule your Python code +to be executed later, either just once or periodically. You can add new jobs or remove old ones on +the fly as you please. If you store your jobs in a database, they will also survive scheduler +restarts and maintain their state. When the scheduler is restarted, it will then run all the jobs +it should have run while it was offline [#f1]_. + +Among other things, APScheduler can be used as a cross-platform, application specific replacement +to platform specific schedulers, such as the cron daemon or the Windows task scheduler. Please +note, however, that APScheduler is **not** a daemon or service itself, nor does it come with any +command line tools. It is primarily meant to be run inside existing applications. That said, +APScheduler does provide some building blocks for you to build a scheduler service or to run a +dedicated scheduler process. + +APScheduler has three built-in scheduling systems you can use: + +* Cron-style scheduling (with optional start/end times) +* Interval-based execution (runs jobs on even intervals, with optional start/end times) +* One-off delayed execution (runs jobs once, on a set date/time) + +You can mix and match scheduling systems and the backends where the jobs are stored any way you +like. Supported backends for storing jobs include: + +* Memory +* `SQLAlchemy `_ (any RDBMS supported by SQLAlchemy works) +* `MongoDB `_ +* `Redis `_ +* `RethinkDB `_ +* `ZooKeeper `_ + +APScheduler also integrates with several common Python frameworks, like: + +* `asyncio `_ (:pep:`3156`) +* `gevent `_ +* `Tornado `_ +* `Twisted `_ +* `Qt `_ (using either + `PyQt `_ , + `PySide6 `_ , + `PySide2 `_ or + `PySide `_) + +There are third party solutions for integrating APScheduler with other frameworks: + +* `Django `_ +* `Flask `_ + + +.. [#f1] The cutoff period for this is also configurable. + + +Documentation +------------- + +Documentation can be found `here `_. + + +Source +------ + +The source can be browsed at `Github `_. + + +Reporting bugs +-------------- + +A `bug tracker `_ is provided by Github. + + +Getting help +------------ + +If you have problems or other questions, you can either: + +* Ask in the `apscheduler `_ room on Gitter +* Ask on the `APScheduler GitHub discussion forum `_, or +* Ask on `StackOverflow `_ and tag your + question with the ``apscheduler`` tag diff --git a/libs/APScheduler-3.10.4.dist-info/RECORD b/libs/APScheduler-3.10.4.dist-info/RECORD new file mode 100644 index 000000000..6a44be923 --- /dev/null +++ b/libs/APScheduler-3.10.4.dist-info/RECORD @@ -0,0 +1,46 @@ +APScheduler-3.10.4.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +APScheduler-3.10.4.dist-info/LICENSE.txt,sha256=YWP3mH37ONa8MgzitwsvArhivEESZRbVUu8c1DJH51g,1130 +APScheduler-3.10.4.dist-info/METADATA,sha256=ITYjDYv8SBO2ynuPiXmySCDJPjfvrFElLJoKQr58h8U,5695 +APScheduler-3.10.4.dist-info/RECORD,, +APScheduler-3.10.4.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +APScheduler-3.10.4.dist-info/WHEEL,sha256=Xo9-1PvkuimrydujYJAjF7pCkriuXBpUPEjma1nZyJ0,92 +APScheduler-3.10.4.dist-info/entry_points.txt,sha256=KMxTUp2QykDNL6w-WBU5xrk8ebroCPEBN0eZtyL3x2w,1147 +APScheduler-3.10.4.dist-info/top_level.txt,sha256=O3oMCWxG-AHkecUoO6Ze7-yYjWrttL95uHO8-RFdYvE,12 +apscheduler/__init__.py,sha256=c_KXMg1QziacYqUpDuzLY5g1mcEZvBLq1dJY7NjLoKc,452 +apscheduler/events.py,sha256=KRMTDQUS6d2uVnrQvPoz3ZPV5V9XKsCAZLsgx913FFo,3593 +apscheduler/executors/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +apscheduler/executors/asyncio.py,sha256=9m4wvRHSSYplllxAQyxWkPVcFdyFG5aZbHt5nfWKIAc,1859 +apscheduler/executors/base.py,sha256=hogiMc_t-huw6BMod0HEeY2FhRNmAAUyNNuBHvIX31M,5336 +apscheduler/executors/base_py3.py,sha256=8WOpTeX1NA-spdbEQ1oJMh5T2O_t2UdsaSnAh-iEWe0,1831 +apscheduler/executors/debug.py,sha256=15_ogSBzl8RRCfBYDnkIV2uMH8cLk1KImYmBa_NVGpc,573 +apscheduler/executors/gevent.py,sha256=aulrNmoefyBgrOkH9awRhFiXIDnSCnZ4U0o0_JXIXgc,777 +apscheduler/executors/pool.py,sha256=h4cYgKMRhjpNHmkhlogHLbmT4O_q6HePXVLmiJIHC3c,2484 +apscheduler/executors/tornado.py,sha256=DU75VaQ9R6nBuy8lbPUvDKUgsuJcZqwAvURC5vg3r6w,1780 +apscheduler/executors/twisted.py,sha256=bRoU0C4BoVcS6_BjKD5wfUs0IJpGkmLsRAcMH2rJJss,778 +apscheduler/job.py,sha256=JCRERBpfWLuomPiNNHX-jrluEwfHkdscEmz4i0Y8rao,11216 +apscheduler/jobstores/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +apscheduler/jobstores/base.py,sha256=DXzSW9XscueHZHMvy1qFiG-vYqUl_MMv0n0uBSZWXGo,4523 +apscheduler/jobstores/memory.py,sha256=ZxWiKsqfsCHFvac-6X9BztuhnuSxlOYi1dhT6g-pjQo,3655 +apscheduler/jobstores/mongodb.py,sha256=r9t2neNuzfPuf_omDm0KdkLGPZXLksiH-U3j13MIBlM,5347 +apscheduler/jobstores/redis.py,sha256=kjQDIzPXz-Yq976U9HK3aMkcCI_QRLKgTADQWKewtik,5483 +apscheduler/jobstores/rethinkdb.py,sha256=k1rSLYJqejuhQxJY3pXwHAQYcpZ1QFJsoQ8n0oEu5MM,5863 +apscheduler/jobstores/sqlalchemy.py,sha256=LIA9iSGMvuPTVqGHdztgQs4YFmYN1xqXvpJauYNK470,6529 +apscheduler/jobstores/zookeeper.py,sha256=avGLXaJGjHD0F7uG6rLJ2gg_TXNqXDEM4PqOu56f-Xg,6363 +apscheduler/schedulers/__init__.py,sha256=jM63xA_K7GSToBenhsz-SCcqfhk1pdEVb6ajwoO5Kqg,406 +apscheduler/schedulers/asyncio.py,sha256=iJO6QUo1oW16giOU_nW8WMu2b9NTWT4Tg2gY586G08w,1994 +apscheduler/schedulers/background.py,sha256=751p-f5Di6pY4x6UXlZggpxQ5k2ObJ_Q5wSeWmKHS8o,1566 +apscheduler/schedulers/base.py,sha256=hCchDyhEXCoVmCfGgD3QMrKumYYLAUwY4456tQrukAY,43780 +apscheduler/schedulers/blocking.py,sha256=8nubfJ4PoUnAkEY6WRQG4COzG4SxGyW9PjuVPhDAbsk,985 +apscheduler/schedulers/gevent.py,sha256=csPBvV75FGcboXXsdex6fCD7J54QgBddYNdWj62ZO9g,1031 +apscheduler/schedulers/qt.py,sha256=jy58cP5roWOv68ytg8fiwtxMVnZKw7a8tkCHbLWeUs8,1329 +apscheduler/schedulers/tornado.py,sha256=D9Vaq3Ee9EFiXa1jDy9tedI048gR_YT_LAFUWqO_uEw,1926 +apscheduler/schedulers/twisted.py,sha256=D5EBjjMRtMBxy0_aAURcULAI8Ky2IvCTr9tK9sO1rYk,1844 +apscheduler/triggers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +apscheduler/triggers/base.py,sha256=BvBJdOnIeVClXPXeInzYK25cN64jAc4a9IiEQucSiVk,1355 +apscheduler/triggers/combining.py,sha256=klaSoBp1kyrPX5D3gBpNTlsGKjks5QeKPW5JN_MVs30,3449 +apscheduler/triggers/cron/__init__.py,sha256=D39BQ63qWyk6XZcSuWth46ELQ3VIFpYjUHh7Kj65Z9M,9251 +apscheduler/triggers/cron/expressions.py,sha256=hu1kq0mKvivIw7U0D0Nnrbuk3q01dCuhZ7SHRPw6qhI,9184 +apscheduler/triggers/cron/fields.py,sha256=NWPClh1NgSOpTlJ3sm1TXM_ViC2qJGKWkd_vg0xsw7o,3510 +apscheduler/triggers/date.py,sha256=RrfB1PNO9G9e91p1BOf-y_TseVHQQR-KJPhNdPpAHcU,1705 +apscheduler/triggers/interval.py,sha256=ABjcZFaGYAAgdAaUQIuLr9_dLszIifu88qaXrJmdxQ4,4377 +apscheduler/util.py,sha256=aCLu_v8-c7rpY6sD7EKgxH2zYjZARiBdqKFZktaxO68,13260 diff --git a/libs/dynaconf/vendor/ruamel/yaml/py.typed b/libs/APScheduler-3.10.4.dist-info/REQUESTED similarity index 100% rename from libs/dynaconf/vendor/ruamel/yaml/py.typed rename to libs/APScheduler-3.10.4.dist-info/REQUESTED diff --git a/libs/APScheduler-3.10.4.dist-info/WHEEL b/libs/APScheduler-3.10.4.dist-info/WHEEL new file mode 100644 index 000000000..ba48cbcf9 --- /dev/null +++ b/libs/APScheduler-3.10.4.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.41.3) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/libs/APScheduler-3.10.4.dist-info/entry_points.txt b/libs/APScheduler-3.10.4.dist-info/entry_points.txt new file mode 100644 index 000000000..0adfe3ead --- /dev/null +++ b/libs/APScheduler-3.10.4.dist-info/entry_points.txt @@ -0,0 +1,23 @@ +[apscheduler.executors] +asyncio = apscheduler.executors.asyncio:AsyncIOExecutor [asyncio] +debug = apscheduler.executors.debug:DebugExecutor +gevent = apscheduler.executors.gevent:GeventExecutor [gevent] +processpool = apscheduler.executors.pool:ProcessPoolExecutor +threadpool = apscheduler.executors.pool:ThreadPoolExecutor +tornado = apscheduler.executors.tornado:TornadoExecutor [tornado] +twisted = apscheduler.executors.twisted:TwistedExecutor [twisted] + +[apscheduler.jobstores] +memory = apscheduler.jobstores.memory:MemoryJobStore +mongodb = apscheduler.jobstores.mongodb:MongoDBJobStore [mongodb] +redis = apscheduler.jobstores.redis:RedisJobStore [redis] +rethinkdb = apscheduler.jobstores.rethinkdb:RethinkDBJobStore [rethinkdb] +sqlalchemy = apscheduler.jobstores.sqlalchemy:SQLAlchemyJobStore [sqlalchemy] +zookeeper = apscheduler.jobstores.zookeeper:ZooKeeperJobStore [zookeeper] + +[apscheduler.triggers] +and = apscheduler.triggers.combining:AndTrigger +cron = apscheduler.triggers.cron:CronTrigger +date = apscheduler.triggers.date:DateTrigger +interval = apscheduler.triggers.interval:IntervalTrigger +or = apscheduler.triggers.combining:OrTrigger diff --git a/libs/APScheduler-3.10.4.dist-info/top_level.txt b/libs/APScheduler-3.10.4.dist-info/top_level.txt new file mode 100644 index 000000000..d31d10dda --- /dev/null +++ b/libs/APScheduler-3.10.4.dist-info/top_level.txt @@ -0,0 +1 @@ +apscheduler diff --git a/libs/Flask_Cors-4.0.0.dist-info/INSTALLER b/libs/Flask_Cors-4.0.0.dist-info/INSTALLER new file mode 100644 index 000000000..a1b589e38 --- /dev/null +++ b/libs/Flask_Cors-4.0.0.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/libs/Flask_Cors-4.0.0.dist-info/LICENSE b/libs/Flask_Cors-4.0.0.dist-info/LICENSE new file mode 100644 index 000000000..46d932f8d --- /dev/null +++ b/libs/Flask_Cors-4.0.0.dist-info/LICENSE @@ -0,0 +1,7 @@ +Copyright (C) 2016 Cory Dolphin, Olin College + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/libs/Flask_Cors-4.0.0.dist-info/METADATA b/libs/Flask_Cors-4.0.0.dist-info/METADATA new file mode 100644 index 000000000..c6e2af695 --- /dev/null +++ b/libs/Flask_Cors-4.0.0.dist-info/METADATA @@ -0,0 +1,147 @@ +Metadata-Version: 2.1 +Name: Flask-Cors +Version: 4.0.0 +Summary: A Flask extension adding a decorator for CORS support +Home-page: https://github.com/corydolphin/flask-cors +Author: Cory Dolphin +Author-email: corydolphin@gmail.com +License: MIT +Platform: any +Classifier: Environment :: Web Environment +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: MIT License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: PyPy +Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content +Classifier: Topic :: Software Development :: Libraries :: Python Modules +License-File: LICENSE +Requires-Dist: Flask >=0.9 + +Flask-CORS +========== + +|Build Status| |Latest Version| |Supported Python versions| +|License| + +A Flask extension for handling Cross Origin Resource Sharing (CORS), making cross-origin AJAX possible. + +This package has a simple philosophy: when you want to enable CORS, you wish to enable it for all use cases on a domain. +This means no mucking around with different allowed headers, methods, etc. + +By default, submission of cookies across domains is disabled due to the security implications. +Please see the documentation for how to enable credential'ed requests, and please make sure you add some sort of `CSRF `__ protection before doing so! + +Installation +------------ + +Install the extension with using pip, or easy\_install. + +.. code:: bash + + $ pip install -U flask-cors + +Usage +----- + +This package exposes a Flask extension which by default enables CORS support on all routes, for all origins and methods. +It allows parameterization of all CORS headers on a per-resource level. +The package also contains a decorator, for those who prefer this approach. + +Simple Usage +~~~~~~~~~~~~ + +In the simplest case, initialize the Flask-Cors extension with default arguments in order to allow CORS for all domains on all routes. +See the full list of options in the `documentation `__. + +.. code:: python + + + from flask import Flask + from flask_cors import CORS + + app = Flask(__name__) + CORS(app) + + @app.route("/") + def helloWorld(): + return "Hello, cross-origin-world!" + +Resource specific CORS +^^^^^^^^^^^^^^^^^^^^^^ + +Alternatively, you can specify CORS options on a resource and origin level of granularity by passing a dictionary as the `resources` option, mapping paths to a set of options. +See the full list of options in the `documentation `__. + +.. code:: python + + app = Flask(__name__) + cors = CORS(app, resources={r"/api/*": {"origins": "*"}}) + + @app.route("/api/v1/users") + def list_users(): + return "user example" + +Route specific CORS via decorator +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +This extension also exposes a simple decorator to decorate flask routes with. +Simply add ``@cross_origin()`` below a call to Flask's ``@app.route(..)`` to allow CORS on a given route. +See the full list of options in the `decorator documentation `__. + +.. code:: python + + @app.route("/") + @cross_origin() + def helloWorld(): + return "Hello, cross-origin-world!" + +Documentation +------------- + +For a full list of options, please see the full `documentation `__ + +Troubleshooting +--------------- + +If things aren't working as you expect, enable logging to help understand what is going on under the hood, and why. + +.. code:: python + + logging.getLogger('flask_cors').level = logging.DEBUG + + +Tests +----- + +A simple set of tests is included in ``test/``. +To run, install nose, and simply invoke ``nosetests`` or ``python setup.py test`` to exercise the tests. + +If nosetests does not work for you, due to it no longer working with newer python versions. +You can use pytest to run the tests instead. + +Contributing +------------ + +Questions, comments or improvements? +Please create an issue on `Github `__, tweet at `@corydolphin `__ or send me an email. +I do my best to include every contribution proposed in any way that I can. + +Credits +------- + +This Flask extension is based upon the `Decorator for the HTTP Access Control `__ written by Armin Ronacher. + +.. |Build Status| image:: https://api.travis-ci.org/corydolphin/flask-cors.svg?branch=master + :target: https://travis-ci.org/corydolphin/flask-cors +.. |Latest Version| image:: https://img.shields.io/pypi/v/Flask-Cors.svg + :target: https://pypi.python.org/pypi/Flask-Cors/ +.. |Supported Python versions| image:: https://img.shields.io/pypi/pyversions/Flask-Cors.svg + :target: https://img.shields.io/pypi/pyversions/Flask-Cors.svg +.. |License| image:: http://img.shields.io/:license-mit-blue.svg + :target: https://pypi.python.org/pypi/Flask-Cors/ diff --git a/libs/Flask_Cors-4.0.0.dist-info/RECORD b/libs/Flask_Cors-4.0.0.dist-info/RECORD new file mode 100644 index 000000000..eae9c8673 --- /dev/null +++ b/libs/Flask_Cors-4.0.0.dist-info/RECORD @@ -0,0 +1,12 @@ +Flask_Cors-4.0.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +Flask_Cors-4.0.0.dist-info/LICENSE,sha256=bhob3FSDTB4HQMvOXV9vLK4chG_Sp_SCsRZJWU-vvV0,1069 +Flask_Cors-4.0.0.dist-info/METADATA,sha256=gH5CIZManWT1lJuvM-YzOlSGJNdzB03QkylAtEc24tY,5417 +Flask_Cors-4.0.0.dist-info/RECORD,, +Flask_Cors-4.0.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +Flask_Cors-4.0.0.dist-info/WHEEL,sha256=P2T-6epvtXQ2cBOE_U1K4_noqlJFN3tj15djMgEu4NM,110 +Flask_Cors-4.0.0.dist-info/top_level.txt,sha256=aWye_0QNZPp_QtPF4ZluLHqnyVLT9CPJsfiGhwqkWuo,11 +flask_cors/__init__.py,sha256=wZDCvPTHspA2g1VV7KyKN7R-uCdBnirTlsCzgPDcQtI,792 +flask_cors/core.py,sha256=e1u_o5SOcS_gMWGjcQrkyk91uPICnzZ3AXZvy5jQ_FE,14063 +flask_cors/decorator.py,sha256=BeJsyX1wYhVKWN04FAhb6z8YqffiRr7wKqwzHPap4bw,5009 +flask_cors/extension.py,sha256=nP4Zq_BhgDVWwPdIl_f-uucNxD38pXUo-dkL-voXc58,7832 +flask_cors/version.py,sha256=61rJjfThnbRdElpSP2tm31hPmFnHJmcwoPhtqA0Bi_Q,22 diff --git a/libs/importlib_resources/tests/zipdata02/__init__.py b/libs/Flask_Cors-4.0.0.dist-info/REQUESTED similarity index 100% rename from libs/importlib_resources/tests/zipdata02/__init__.py rename to libs/Flask_Cors-4.0.0.dist-info/REQUESTED diff --git a/libs/Flask_Cors-4.0.0.dist-info/WHEEL b/libs/Flask_Cors-4.0.0.dist-info/WHEEL new file mode 100644 index 000000000..f31e450fd --- /dev/null +++ b/libs/Flask_Cors-4.0.0.dist-info/WHEEL @@ -0,0 +1,6 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.41.3) +Root-Is-Purelib: true +Tag: py2-none-any +Tag: py3-none-any + diff --git a/libs/Flask_Cors-4.0.0.dist-info/top_level.txt b/libs/Flask_Cors-4.0.0.dist-info/top_level.txt new file mode 100644 index 000000000..27af98818 --- /dev/null +++ b/libs/Flask_Cors-4.0.0.dist-info/top_level.txt @@ -0,0 +1 @@ +flask_cors diff --git a/libs/Flask_Migrate-4.0.5.dist-info/INSTALLER b/libs/Flask_Migrate-4.0.5.dist-info/INSTALLER new file mode 100644 index 000000000..a1b589e38 --- /dev/null +++ b/libs/Flask_Migrate-4.0.5.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/libs/Flask_Migrate-4.0.5.dist-info/LICENSE b/libs/Flask_Migrate-4.0.5.dist-info/LICENSE new file mode 100644 index 000000000..2448fd266 --- /dev/null +++ b/libs/Flask_Migrate-4.0.5.dist-info/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2013 Miguel Grinberg + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/libs/Flask_Migrate-4.0.5.dist-info/METADATA b/libs/Flask_Migrate-4.0.5.dist-info/METADATA new file mode 100644 index 000000000..0590a4cf5 --- /dev/null +++ b/libs/Flask_Migrate-4.0.5.dist-info/METADATA @@ -0,0 +1,86 @@ +Metadata-Version: 2.1 +Name: Flask-Migrate +Version: 4.0.5 +Summary: SQLAlchemy database migrations for Flask applications using Alembic. +Home-page: https://github.com/miguelgrinberg/flask-migrate +Author: Miguel Grinberg +Author-email: miguel.grinberg@gmail.com +License: MIT +Project-URL: Bug Tracker, https://github.com/miguelgrinberg/flask-migrate/issues +Classifier: Environment :: Web Environment +Classifier: Intended Audience :: Developers +Classifier: Programming Language :: Python :: 3 +Classifier: License :: OSI Approved :: MIT License +Classifier: Operating System :: OS Independent +Requires-Python: >=3.6 +Description-Content-Type: text/markdown +License-File: LICENSE +Requires-Dist: Flask >=0.9 +Requires-Dist: Flask-SQLAlchemy >=1.0 +Requires-Dist: alembic >=1.9.0 + +Flask-Migrate +============= + +[![Build status](https://github.com/miguelgrinberg/flask-migrate/workflows/build/badge.svg)](https://github.com/miguelgrinberg/flask-migrate/actions) + +Flask-Migrate is an extension that handles SQLAlchemy database migrations for Flask applications using Alembic. The database operations are provided as command-line arguments under the `flask db` command. + +Installation +------------ + +Install Flask-Migrate with `pip`: + + pip install Flask-Migrate + +Example +------- + +This is an example application that handles database migrations through Flask-Migrate: + +```python +from flask import Flask +from flask_sqlalchemy import SQLAlchemy +from flask_migrate import Migrate + +app = Flask(__name__) +app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///app.db' + +db = SQLAlchemy(app) +migrate = Migrate(app, db) + +class User(db.Model): + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(128)) +``` + +With the above application you can create the database or enable migrations if the database already exists with the following command: + + $ flask db init + +Note that the `FLASK_APP` environment variable must be set according to the Flask documentation for this command to work. This will add a `migrations` folder to your application. The contents of this folder need to be added to version control along with your other source files. + +You can then generate an initial migration: + + $ flask db migrate + +The migration script needs to be reviewed and edited, as Alembic currently does not detect every change you make to your models. In particular, Alembic is currently unable to detect indexes. Once finalized, the migration script also needs to be added to version control. + +Then you can apply the migration to the database: + + $ flask db upgrade + +Then each time the database models change repeat the `migrate` and `upgrade` commands. + +To sync the database in another system just refresh the `migrations` folder from source control and run the `upgrade` command. + +To see all the commands that are available run this command: + + $ flask db --help + +Resources +--------- + +- [Documentation](http://flask-migrate.readthedocs.io/en/latest/) +- [pypi](https://pypi.python.org/pypi/Flask-Migrate) +- [Change Log](https://github.com/miguelgrinberg/Flask-Migrate/blob/master/CHANGES.md) diff --git a/libs/Flask_Migrate-4.0.5.dist-info/RECORD b/libs/Flask_Migrate-4.0.5.dist-info/RECORD new file mode 100644 index 000000000..740ada03e --- /dev/null +++ b/libs/Flask_Migrate-4.0.5.dist-info/RECORD @@ -0,0 +1,25 @@ +Flask_Migrate-4.0.5.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +Flask_Migrate-4.0.5.dist-info/LICENSE,sha256=kfkXGlJQvKy3Y__6tAJ8ynIp1HQfeROXhL8jZU1d-DI,1082 +Flask_Migrate-4.0.5.dist-info/METADATA,sha256=d-EcnhZa_vyVAph2u84OpGIteJaBmqLQxO5Rf6wUI7Y,3095 +Flask_Migrate-4.0.5.dist-info/RECORD,, +Flask_Migrate-4.0.5.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +Flask_Migrate-4.0.5.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92 +Flask_Migrate-4.0.5.dist-info/top_level.txt,sha256=jLoPgiMG6oR4ugNteXn3IHskVVIyIXVStZOVq-AWLdU,14 +flask_migrate/__init__.py,sha256=-JFdExGtr7UrwCpmjYvTfzFHqMjE7AmP0Rr3T53tBNU,10037 +flask_migrate/cli.py,sha256=H-N4NNS5HyEB61HpUADLU8pW3naejyDPgeEbzEqG5-w,10298 +flask_migrate/templates/aioflask-multidb/README,sha256=Ek4cJqTaxneVjtkue--BXMlfpfp3MmJRjqoZvnSizww,43 +flask_migrate/templates/aioflask-multidb/alembic.ini.mako,sha256=SjYEmJKzz6K8QfuZWtLJAJWcCKOdRbfUhsVlpgv8ock,857 +flask_migrate/templates/aioflask-multidb/env.py,sha256=UcjeqkAbyUjTkuQFmCFPG7QOvqhco8-uGp8QEbto0T8,6573 +flask_migrate/templates/aioflask-multidb/script.py.mako,sha256=198VPxVEN3NZ3vHcRuCxSoI4XnOYirGWt01qkbPKoJw,1246 +flask_migrate/templates/aioflask/README,sha256=KKqWGl4YC2RqdOdq-y6quTDW0b7D_UZNHuM8glM1L-c,44 +flask_migrate/templates/aioflask/alembic.ini.mako,sha256=SjYEmJKzz6K8QfuZWtLJAJWcCKOdRbfUhsVlpgv8ock,857 +flask_migrate/templates/aioflask/env.py,sha256=m6ZtBhdpwuq89vVeLTWmNT-1NfJZqarC_hsquCdR9bw,3478 +flask_migrate/templates/aioflask/script.py.mako,sha256=8_xgA-gm_OhehnO7CiIijWgnm00ZlszEHtIHrAYFJl0,494 +flask_migrate/templates/flask-multidb/README,sha256=AfiP5foaV2odZxXxuUuSIS6YhkIpR7CsOo2mpuxwHdc,40 +flask_migrate/templates/flask-multidb/alembic.ini.mako,sha256=SjYEmJKzz6K8QfuZWtLJAJWcCKOdRbfUhsVlpgv8ock,857 +flask_migrate/templates/flask-multidb/env.py,sha256=F44iqsAxLTVBN_zD8CMUkdE7Aub4niHMmo5wl9mY4Uw,6190 +flask_migrate/templates/flask-multidb/script.py.mako,sha256=198VPxVEN3NZ3vHcRuCxSoI4XnOYirGWt01qkbPKoJw,1246 +flask_migrate/templates/flask/README,sha256=JL0NrjOrscPcKgRmQh1R3hlv1_rohDot0TvpmdM27Jk,41 +flask_migrate/templates/flask/alembic.ini.mako,sha256=SjYEmJKzz6K8QfuZWtLJAJWcCKOdRbfUhsVlpgv8ock,857 +flask_migrate/templates/flask/env.py,sha256=ibK1hsdOsOBzXNU2yQoAIza7f_EFzaVSWwON_NSpNzQ,3344 +flask_migrate/templates/flask/script.py.mako,sha256=8_xgA-gm_OhehnO7CiIijWgnm00ZlszEHtIHrAYFJl0,494 diff --git a/libs/js2py/host/dom/__init__.py b/libs/Flask_Migrate-4.0.5.dist-info/REQUESTED similarity index 100% rename from libs/js2py/host/dom/__init__.py rename to libs/Flask_Migrate-4.0.5.dist-info/REQUESTED diff --git a/libs/Flask_Migrate-4.0.5.dist-info/WHEEL b/libs/Flask_Migrate-4.0.5.dist-info/WHEEL new file mode 100644 index 000000000..98c0d20b7 --- /dev/null +++ b/libs/Flask_Migrate-4.0.5.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.42.0) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/libs/Flask_Migrate-4.0.5.dist-info/top_level.txt b/libs/Flask_Migrate-4.0.5.dist-info/top_level.txt new file mode 100644 index 000000000..0652762c7 --- /dev/null +++ b/libs/Flask_Migrate-4.0.5.dist-info/top_level.txt @@ -0,0 +1 @@ +flask_migrate diff --git a/libs/Flask_SocketIO-5.3.6.dist-info/INSTALLER b/libs/Flask_SocketIO-5.3.6.dist-info/INSTALLER new file mode 100644 index 000000000..a1b589e38 --- /dev/null +++ b/libs/Flask_SocketIO-5.3.6.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/libs/Flask_SocketIO-5.3.6.dist-info/LICENSE b/libs/Flask_SocketIO-5.3.6.dist-info/LICENSE new file mode 100644 index 000000000..f5c10ab28 --- /dev/null +++ b/libs/Flask_SocketIO-5.3.6.dist-info/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2014 Miguel Grinberg + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/libs/Flask_SocketIO-5.3.6.dist-info/METADATA b/libs/Flask_SocketIO-5.3.6.dist-info/METADATA new file mode 100644 index 000000000..ddec6b09a --- /dev/null +++ b/libs/Flask_SocketIO-5.3.6.dist-info/METADATA @@ -0,0 +1,77 @@ +Metadata-Version: 2.1 +Name: Flask-SocketIO +Version: 5.3.6 +Summary: Socket.IO integration for Flask applications +Home-page: https://github.com/miguelgrinberg/flask-socketio +Author: Miguel Grinberg +Author-email: miguel.grinberg@gmail.com +Project-URL: Bug Tracker, https://github.com/miguelgrinberg/flask-socketio/issues +Classifier: Environment :: Web Environment +Classifier: Intended Audience :: Developers +Classifier: Programming Language :: Python :: 3 +Classifier: License :: OSI Approved :: MIT License +Classifier: Operating System :: OS Independent +Requires-Python: >=3.6 +Description-Content-Type: text/markdown +License-File: LICENSE +Requires-Dist: Flask >=0.9 +Requires-Dist: python-socketio >=5.0.2 +Provides-Extra: docs +Requires-Dist: sphinx ; extra == 'docs' + +Flask-SocketIO +============== + +[![Build status](https://github.com/miguelgrinberg/flask-socketio/workflows/build/badge.svg)](https://github.com/miguelgrinberg/Flask-SocketIO/actions) [![codecov](https://codecov.io/gh/miguelgrinberg/flask-socketio/branch/main/graph/badge.svg)](https://codecov.io/gh/miguelgrinberg/flask-socketio) + +Socket.IO integration for Flask applications. + +Sponsors +-------- + +The following organizations are funding this project: + +![Socket.IO](https://images.opencollective.com/socketio/050e5eb/logo/64.png)
[Socket.IO](https://socket.io) | [Add your company here!](https://github.com/sponsors/miguelgrinberg)| +-|- + +Many individual sponsors also support this project through small ongoing contributions. Why not [join them](https://github.com/sponsors/miguelgrinberg)? + +Installation +------------ + +You can install this package as usual with pip: + + pip install flask-socketio + +Example +------- + +```py +from flask import Flask, render_template +from flask_socketio import SocketIO, emit + +app = Flask(__name__) +app.config['SECRET_KEY'] = 'secret!' +socketio = SocketIO(app) + +@app.route('/') +def index(): + return render_template('index.html') + +@socketio.event +def my_event(message): + emit('my response', {'data': 'got it!'}) + +if __name__ == '__main__': + socketio.run(app) +``` + +Resources +--------- + +- [Tutorial](http://blog.miguelgrinberg.com/post/easy-websockets-with-flask-and-gevent) +- [Documentation](http://flask-socketio.readthedocs.io/en/latest/) +- [PyPI](https://pypi.python.org/pypi/Flask-SocketIO) +- [Change Log](https://github.com/miguelgrinberg/Flask-SocketIO/blob/main/CHANGES.md) +- Questions? See the [questions](https://stackoverflow.com/questions/tagged/flask-socketio) others have asked on Stack Overflow, or [ask](https://stackoverflow.com/questions/ask?tags=python+flask-socketio+python-socketio) your own question. + diff --git a/libs/Flask_SocketIO-5.3.6.dist-info/RECORD b/libs/Flask_SocketIO-5.3.6.dist-info/RECORD new file mode 100644 index 000000000..4a6082118 --- /dev/null +++ b/libs/Flask_SocketIO-5.3.6.dist-info/RECORD @@ -0,0 +1,10 @@ +Flask_SocketIO-5.3.6.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +Flask_SocketIO-5.3.6.dist-info/LICENSE,sha256=aNCWbkgKjS_T1cJtACyZbvCM36KxWnfQ0LWTuavuYKQ,1082 +Flask_SocketIO-5.3.6.dist-info/METADATA,sha256=vmIOzjkNLXRjmocRXtso6hLV27aiJgH7_A55TVJyD4k,2631 +Flask_SocketIO-5.3.6.dist-info/RECORD,, +Flask_SocketIO-5.3.6.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +Flask_SocketIO-5.3.6.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92 +Flask_SocketIO-5.3.6.dist-info/top_level.txt,sha256=C1ugzQBJ3HHUJsWGzyt70XRVOX-y4CUAR8MWKjwJOQ8,15 +flask_socketio/__init__.py,sha256=ea3QXRYKBje4JQGcNSEOmj42qlf2peRNbCzZZWfD9DE,54731 +flask_socketio/namespace.py,sha256=b3oyXEemu2po-wpoy4ILTHQMVuVQqicogCDxfymfz_w,2020 +flask_socketio/test_client.py,sha256=9_R1y_vP8yr8wzimQUEMAUyVqX12FMXurLj8t1ecDdc,11034 diff --git a/libs/subliminal/converters/__init__.py b/libs/Flask_SocketIO-5.3.6.dist-info/REQUESTED similarity index 100% rename from libs/subliminal/converters/__init__.py rename to libs/Flask_SocketIO-5.3.6.dist-info/REQUESTED diff --git a/libs/Flask_SocketIO-5.3.6.dist-info/WHEEL b/libs/Flask_SocketIO-5.3.6.dist-info/WHEEL new file mode 100644 index 000000000..98c0d20b7 --- /dev/null +++ b/libs/Flask_SocketIO-5.3.6.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.42.0) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/libs/Flask_SocketIO-5.3.6.dist-info/top_level.txt b/libs/Flask_SocketIO-5.3.6.dist-info/top_level.txt new file mode 100644 index 000000000..ba82ec385 --- /dev/null +++ b/libs/Flask_SocketIO-5.3.6.dist-info/top_level.txt @@ -0,0 +1 @@ +flask_socketio diff --git a/libs/Jinja2-3.1.3.dist-info/INSTALLER b/libs/Jinja2-3.1.3.dist-info/INSTALLER new file mode 100644 index 000000000..a1b589e38 --- /dev/null +++ b/libs/Jinja2-3.1.3.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/libs/Jinja2-3.1.3.dist-info/LICENSE.rst b/libs/Jinja2-3.1.3.dist-info/LICENSE.rst new file mode 100644 index 000000000..c37cae49e --- /dev/null +++ b/libs/Jinja2-3.1.3.dist-info/LICENSE.rst @@ -0,0 +1,28 @@ +Copyright 2007 Pallets + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/libs/Jinja2-3.1.3.dist-info/METADATA b/libs/Jinja2-3.1.3.dist-info/METADATA new file mode 100644 index 000000000..56e942902 --- /dev/null +++ b/libs/Jinja2-3.1.3.dist-info/METADATA @@ -0,0 +1,105 @@ +Metadata-Version: 2.1 +Name: Jinja2 +Version: 3.1.3 +Summary: A very fast and expressive template engine. +Home-page: https://palletsprojects.com/p/jinja/ +Maintainer: Pallets +Maintainer-email: contact@palletsprojects.com +License: BSD-3-Clause +Project-URL: Donate, https://palletsprojects.com/donate +Project-URL: Documentation, https://jinja.palletsprojects.com/ +Project-URL: Changes, https://jinja.palletsprojects.com/changes/ +Project-URL: Source Code, https://github.com/pallets/jinja/ +Project-URL: Issue Tracker, https://github.com/pallets/jinja/issues/ +Project-URL: Chat, https://discord.gg/pallets +Classifier: Development Status :: 5 - Production/Stable +Classifier: Environment :: Web Environment +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: BSD License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content +Classifier: Topic :: Text Processing :: Markup :: HTML +Requires-Python: >=3.7 +Description-Content-Type: text/x-rst +License-File: LICENSE.rst +Requires-Dist: MarkupSafe >=2.0 +Provides-Extra: i18n +Requires-Dist: Babel >=2.7 ; extra == 'i18n' + +Jinja +===== + +Jinja is a fast, expressive, extensible templating engine. Special +placeholders in the template allow writing code similar to Python +syntax. Then the template is passed data to render the final document. + +It includes: + +- Template inheritance and inclusion. +- Define and import macros within templates. +- HTML templates can use autoescaping to prevent XSS from untrusted + user input. +- A sandboxed environment can safely render untrusted templates. +- AsyncIO support for generating templates and calling async + functions. +- I18N support with Babel. +- Templates are compiled to optimized Python code just-in-time and + cached, or can be compiled ahead-of-time. +- Exceptions point to the correct line in templates to make debugging + easier. +- Extensible filters, tests, functions, and even syntax. + +Jinja's philosophy is that while application logic belongs in Python if +possible, it shouldn't make the template designer's job difficult by +restricting functionality too much. + + +Installing +---------- + +Install and update using `pip`_: + +.. code-block:: text + + $ pip install -U Jinja2 + +.. _pip: https://pip.pypa.io/en/stable/getting-started/ + + +In A Nutshell +------------- + +.. code-block:: jinja + + {% extends "base.html" %} + {% block title %}Members{% endblock %} + {% block content %} +

+ {% endblock %} + + +Donate +------ + +The Pallets organization develops and supports Jinja and other popular +packages. In order to grow the community of contributors and users, and +allow the maintainers to devote more time to the projects, `please +donate today`_. + +.. _please donate today: https://palletsprojects.com/donate + + +Links +----- + +- Documentation: https://jinja.palletsprojects.com/ +- Changes: https://jinja.palletsprojects.com/changes/ +- PyPI Releases: https://pypi.org/project/Jinja2/ +- Source Code: https://github.com/pallets/jinja/ +- Issue Tracker: https://github.com/pallets/jinja/issues/ +- Chat: https://discord.gg/pallets diff --git a/libs/Jinja2-3.1.3.dist-info/RECORD b/libs/Jinja2-3.1.3.dist-info/RECORD new file mode 100644 index 000000000..e80904634 --- /dev/null +++ b/libs/Jinja2-3.1.3.dist-info/RECORD @@ -0,0 +1,34 @@ +Jinja2-3.1.3.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +Jinja2-3.1.3.dist-info/LICENSE.rst,sha256=O0nc7kEF6ze6wQ-vG-JgQI_oXSUrjp3y4JefweCUQ3s,1475 +Jinja2-3.1.3.dist-info/METADATA,sha256=0cLNbRCI91jytc7Bzv3XAQfZzFDF2gxkJuH46eF5vew,3301 +Jinja2-3.1.3.dist-info/RECORD,, +Jinja2-3.1.3.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +Jinja2-3.1.3.dist-info/WHEEL,sha256=Xo9-1PvkuimrydujYJAjF7pCkriuXBpUPEjma1nZyJ0,92 +Jinja2-3.1.3.dist-info/entry_points.txt,sha256=zRd62fbqIyfUpsRtU7EVIFyiu1tPwfgO7EvPErnxgTE,59 +Jinja2-3.1.3.dist-info/top_level.txt,sha256=PkeVWtLb3-CqjWi1fO29OCbj55EhX_chhKrCdrVe_zs,7 +jinja2/__init__.py,sha256=NTBwMwsECrdHmxeXF7seusHLzrh6Ldn1A9qhS5cDuf0,1927 +jinja2/_identifier.py,sha256=_zYctNKzRqlk_murTNlzrju1FFJL7Va_Ijqqd7ii2lU,1958 +jinja2/async_utils.py,sha256=dFcmh6lMNfbh7eLKrBio8JqAKLHdZbpCuurFN4OERtY,2447 +jinja2/bccache.py,sha256=mhz5xtLxCcHRAa56azOhphIAe19u1we0ojifNMClDio,14061 +jinja2/compiler.py,sha256=PJzYdRLStlEOqmnQs1YxlizPrJoj3jTZuUleREn6AIQ,72199 +jinja2/constants.py,sha256=GMoFydBF_kdpaRKPoM5cl5MviquVRLVyZtfp5-16jg0,1433 +jinja2/debug.py,sha256=iWJ432RadxJNnaMOPrjIDInz50UEgni3_HKuFXi2vuQ,6299 +jinja2/defaults.py,sha256=boBcSw78h-lp20YbaXSJsqkAI2uN_mD_TtCydpeq5wU,1267 +jinja2/environment.py,sha256=0qldX3VQKZcm6lgn7zHz94oRFow7YPYERiqkquomNjU,61253 +jinja2/exceptions.py,sha256=ioHeHrWwCWNaXX1inHmHVblvc4haO7AXsjCp3GfWvx0,5071 +jinja2/ext.py,sha256=5fnMpllaXkfm2P_93RIvi-OnK7Tk8mCW8Du-GcD12Hc,31844 +jinja2/filters.py,sha256=vYjKb2zaPShvYtn_LpSmqfS8SScbrA_KOanNibsMDIE,53862 +jinja2/idtracking.py,sha256=GfNmadir4oDALVxzn3DL9YInhJDr69ebXeA2ygfuCGA,10704 +jinja2/lexer.py,sha256=DW2nX9zk-6MWp65YR2bqqj0xqCvLtD-u9NWT8AnFRxQ,29726 +jinja2/loaders.py,sha256=ayAwxfrA1SAffQta0nwSDm3TDT4KYiIGN_D9Z45B310,23085 +jinja2/meta.py,sha256=GNPEvifmSaU3CMxlbheBOZjeZ277HThOPUTf1RkppKQ,4396 +jinja2/nativetypes.py,sha256=7GIGALVJgdyL80oZJdQUaUfwSt5q2lSSZbXt0dNf_M4,4210 +jinja2/nodes.py,sha256=i34GPRAZexXMT6bwuf5SEyvdmS-bRCy9KMjwN5O6pjk,34550 +jinja2/optimizer.py,sha256=tHkMwXxfZkbfA1KmLcqmBMSaz7RLIvvItrJcPoXTyD8,1650 +jinja2/parser.py,sha256=Y199wPL-G67gJoi5G_5sHuu9uEP1PJkjjLEW_xTH8-k,39736 +jinja2/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +jinja2/runtime.py,sha256=_6LkKIWFJjQdqlrgA3K39zBFQ-7Orm3wGDm96RwxQoE,33406 +jinja2/sandbox.py,sha256=Y0xZeXQnH6EX5VjaV2YixESxoepnRbW_3UeQosaBU3M,14584 +jinja2/tests.py,sha256=Am5Z6Lmfr2XaH_npIfJJ8MdXtWsbLjMULZJulTAj30E,5905 +jinja2/utils.py,sha256=IMwRIcN1SsTw2-jdQtlH2KzNABsXZBW_-tnFXafQBvY,23933 +jinja2/visitor.py,sha256=MH14C6yq24G_KVtWzjwaI7Wg14PCJIYlWW1kpkxYak0,3568 diff --git a/libs/subliminal_patch/converters/__init__.py b/libs/Jinja2-3.1.3.dist-info/REQUESTED similarity index 100% rename from libs/subliminal_patch/converters/__init__.py rename to libs/Jinja2-3.1.3.dist-info/REQUESTED diff --git a/libs/Jinja2-3.1.3.dist-info/WHEEL b/libs/Jinja2-3.1.3.dist-info/WHEEL new file mode 100644 index 000000000..ba48cbcf9 --- /dev/null +++ b/libs/Jinja2-3.1.3.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.41.3) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/libs/Jinja2-3.1.3.dist-info/entry_points.txt b/libs/Jinja2-3.1.3.dist-info/entry_points.txt new file mode 100644 index 000000000..7b9666c8e --- /dev/null +++ b/libs/Jinja2-3.1.3.dist-info/entry_points.txt @@ -0,0 +1,2 @@ +[babel.extractors] +jinja2 = jinja2.ext:babel_extract[i18n] diff --git a/libs/Jinja2-3.1.3.dist-info/top_level.txt b/libs/Jinja2-3.1.3.dist-info/top_level.txt new file mode 100644 index 000000000..7f7afbf3b --- /dev/null +++ b/libs/Jinja2-3.1.3.dist-info/top_level.txt @@ -0,0 +1 @@ +jinja2 diff --git a/libs/Js2Py-0.74.dist-info/INSTALLER b/libs/Js2Py-0.74.dist-info/INSTALLER new file mode 100644 index 000000000..a1b589e38 --- /dev/null +++ b/libs/Js2Py-0.74.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/libs/Js2Py-0.74.dist-info/LICENSE.md b/libs/Js2Py-0.74.dist-info/LICENSE.md new file mode 100644 index 000000000..059be8d60 --- /dev/null +++ b/libs/Js2Py-0.74.dist-info/LICENSE.md @@ -0,0 +1,19 @@ +The MIT License + +Copyright © 2014, 2015 Piotr Dabkowski + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the “Software”), +to deal in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, subject +to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or +substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE + OR THE USE OR OTHER DEALINGS IN THE SOFTWARE \ No newline at end of file diff --git a/libs/Js2Py-0.74.dist-info/METADATA b/libs/Js2Py-0.74.dist-info/METADATA new file mode 100644 index 000000000..4063e4ba9 --- /dev/null +++ b/libs/Js2Py-0.74.dist-info/METADATA @@ -0,0 +1,29 @@ +Metadata-Version: 2.1 +Name: Js2Py +Version: 0.74 +Summary: JavaScript to Python Translator & JavaScript interpreter written in 100% pure Python. +Home-page: https://github.com/PiotrDabkowski/Js2Py +Author: Piotr Dabkowski +Author-email: piodrus@gmail.com +License: MIT +License-File: LICENSE.md +Requires-Dist: tzlocal >=1.2 +Requires-Dist: six >=1.10 +Requires-Dist: pyjsparser >=2.5.1 + +Translates JavaScript to Python code. Js2Py is able to translate and execute virtually any JavaScript code. + +Js2Py is written in pure python and does not have any dependencies. Basically an implementation of JavaScript core in pure python. + + + import js2py + + f = js2py.eval_js( "function $(name) {return name.length}" ) + + f("Hello world") + + # returns 11 + +Now also supports ECMA 6 through js2py.eval_js6(js6_code)! + +More examples at: https://github.com/PiotrDabkowski/Js2Py diff --git a/libs/Js2Py-0.74.dist-info/RECORD b/libs/Js2Py-0.74.dist-info/RECORD new file mode 100644 index 000000000..d14c3b06c --- /dev/null +++ b/libs/Js2Py-0.74.dist-info/RECORD @@ -0,0 +1,101 @@ +Js2Py-0.74.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +Js2Py-0.74.dist-info/LICENSE.md,sha256=5HcnGlDEDJrDrVbQLnDH19l_ZscybPUk0TsS-IQsxOk,1088 +Js2Py-0.74.dist-info/METADATA,sha256=shZhquhN4nVuaurudCERGEnLk38BjT7grtZLNg4MABc,862 +Js2Py-0.74.dist-info/RECORD,, +Js2Py-0.74.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +Js2Py-0.74.dist-info/WHEEL,sha256=Xo9-1PvkuimrydujYJAjF7pCkriuXBpUPEjma1nZyJ0,92 +Js2Py-0.74.dist-info/top_level.txt,sha256=Me1vDvBnqRgA6Jf96euhHjsa-dYkaXpr3Sm0RGPoGn8,6 +js2py/__init__.py,sha256=VlWswk9Sf3qBwxaJ1E9STDxUdTYh3PLKp6Kn1ws0STE,2886 +js2py/base.py,sha256=_h7HbsB30cybzGAU7XIX5tawMA4C7IHFsRi_ESRqzZc,115810 +js2py/constructors/__init__.py,sha256=isKbPQhm2gf7O6f4aW0F9J0yBlds8jYDq5xA4u08xrE,30 +js2py/constructors/jsarray.py,sha256=_uwE632g_crmwQeBTdi5U0RztCeTOofjm2eoStiMFlI,969 +js2py/constructors/jsarraybuffer.py,sha256=jD3kFsBykePAjsSitg3qki4Dhc1fGvOM1hxomtxPPEI,871 +js2py/constructors/jsboolean.py,sha256=1LneDP_41oaQ6twN8qTDcu_gHPPPE0lcpUKegWPXGbA,350 +js2py/constructors/jsdate.py,sha256=8Qs-e8u0SuYzPsuL7TRabf2oMKcwAWKNu1UXl9bu__o,16407 +js2py/constructors/jsfloat32array.py,sha256=NElqLQu99NnJDKhYab6LUg-UP6kNQ9E1TAJOmPpMpzs,2811 +js2py/constructors/jsfloat64array.py,sha256=V-lurvILkLevzDywcO6UBZgLQsZ7wEYEeV0r-zUUcT0,2811 +js2py/constructors/jsfunction.py,sha256=QKsFNBO4JJjtWX809G4xuKvXm1HCf3a_Klq8TQ0ChRw,1399 +js2py/constructors/jsint16array.py,sha256=S8lJSOSHQzrrF2RPhhYA24YR68HWcJZ_ofrsVg-JMBw,2779 +js2py/constructors/jsint32array.py,sha256=OIopIBcREY8Xd-Gw-nNMFg5wrhmzD6o7euiefO0rpvw,2779 +js2py/constructors/jsint8array.py,sha256=71jSjflD8FAXhD0NMXJMFIA3l-dBZgeGsHqnVtDwxCk,2422 +js2py/constructors/jsmath.py,sha256=AksaLOtJRNBXP7SvYGHh2pI-e9PM8nhfxtLZxPqyKic,3692 +js2py/constructors/jsnumber.py,sha256=_0Aa-sk5URPu-SnZ6g5Hi1OshPn5G4TkASciHGOJeXA,491 +js2py/constructors/jsobject.py,sha256=AgGDGiYBQAQ5lWtwSXf3O85zSGMrQxquunODOVHqYck,6916 +js2py/constructors/jsregexp.py,sha256=Dk4yjWOBlCxOnFmrFKoj6IEKBWjpuxBRxAcSfgFV4ts,346 +js2py/constructors/jsstring.py,sha256=_q2sUc3q_zMkzUFo4N0ymsgnve-Pe6jvhzn5Q6oU-jY,775 +js2py/constructors/jsuint16array.py,sha256=h7XTf3db433tFN6uXEDdkhThXk0p_w3d6S8fCo4KIN8,2795 +js2py/constructors/jsuint32array.py,sha256=Haa0wtsu6krz4I51bLSKGBzVxCZLGuwOJCImFcfxg3g,3041 +js2py/constructors/jsuint8array.py,sha256=DJyP2x45ww7tLaVEYAVA2bkmqvAAIdRL9yDLVYzD67E,2436 +js2py/constructors/jsuint8clampedarray.py,sha256=Imk2Sijw-PKFTQoZ9d5hsCpONnq0yjUGc1Q3s73K-5c,2569 +js2py/constructors/time_helpers.py,sha256=AVgKArQqglPzVgm2ojMJZIpLPOu__s1hYq9hZPcfGGo,4392 +js2py/es6/__init__.py,sha256=1wsp7UrLjeluUuj8DT3PynY1PItrRZ7LDoIzuLMKR9E,1346 +js2py/es6/babel.py,sha256=WaOzA-YAo3KuBejnQJHl_CeKE_G7mfj5ywtz2QRfK6A,3846971 +js2py/evaljs.py,sha256=52H7wu72rsg-2FoOQ8R1w0Qd3QEXCT-XHTbjqytQs60,9435 +js2py/host/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +js2py/host/console.py,sha256=7kNYyF5aOlhUXG6kcVnvA1eBe6yBaNOLhOA6Y-UfMds,263 +js2py/host/jseval.py,sha256=A9efTYN58c3XaHk2codGCfV4qt7odoP_OIv9l1koOuo,1570 +js2py/host/jsfunctions.py,sha256=NCndE01xYPxiNZ0y39YIYIpmltFLndWxlSXWItN_QGE,3038 +js2py/internals/__init__.py,sha256=JbzdjbrSn4FHH_6eAEZHoNRzZmJc09GHXrE7QnrlhoU,29 +js2py/internals/base.py,sha256=p1ThnCKaClCfK0PlLfk5V-mXt6WTXUvu9r0OF99RebM,33137 +js2py/internals/byte_trans.py,sha256=eMcDUGNgT37sK3O36HwdOO37014XoBwAF65Qv8bqRTM,26157 +js2py/internals/code.py,sha256=zTEZyucV-cQLM6ftMqTUWo_FoBqMN-E2k5fAPNGKqlE,8877 +js2py/internals/constructors/__init__.py,sha256=isKbPQhm2gf7O6f4aW0F9J0yBlds8jYDq5xA4u08xrE,30 +js2py/internals/constructors/jsarray.py,sha256=mYtyK3OGFBlS7FFwUdeYgK8zDuuy67Gk0SwVRjBzpOE,725 +js2py/internals/constructors/jsboolean.py,sha256=YDYyR4l9rbxYenmPaI2IoFGq6_1f_tVXXmQs5-1QCQU,323 +js2py/internals/constructors/jsconsole.py,sha256=GKp-m6mQ7w_cfYAXBFVa--RfRgf1Kyf79H1qFmCPHFQ,264 +js2py/internals/constructors/jsdate.py,sha256=eFdrKx8eBZyDJ2xLmeQkye_jEsshe-GheQlShs9t-H8,15954 +js2py/internals/constructors/jsfunction.py,sha256=L2J4jCye3uPmjHm1LsIXs248BTV9g7RnWkLCCCNEvXw,2700 +js2py/internals/constructors/jsmath.py,sha256=7_Jwne8-oYrwPDbIy1f1yU-BYaZ7RJRb4q-jo5YNA-A,3743 +js2py/internals/constructors/jsnumber.py,sha256=3urg8xZT0lnJekhTKeID3FHPJy--GgmXotQD_ItREtY,591 +js2py/internals/constructors/jsobject.py,sha256=QZziWhJyEUkr5lzuM8JgtTTZxuGp_fY2gppuLxBsjAE,7304 +js2py/internals/constructors/jsregexp.py,sha256=cxJf0EW-amFQGsgMQYZWn7SgLgTnVQipz-9_H6VBhl0,1243 +js2py/internals/constructors/jsstring.py,sha256=Te0BVkhfUQmhC5LyWKccL6GvwEM10TLbMI49zfqaW80,514 +js2py/internals/constructors/time_helpers.py,sha256=ajXM8YbzIhKP_Bftq8DjCyHC4Hih7rAbNysjFzatC6g,4433 +js2py/internals/conversions.py,sha256=5_O67_HPuK1H2EsVu8TtaedjgROXfe3XlL6eGaQL_cQ,4159 +js2py/internals/desc.py,sha256=wpZvelGC3sLOoYxRTZMe2Y_f_uZkJn-O1BME6-r1n6c,2861 +js2py/internals/fill_space.py,sha256=hinxkuzAQEbYGcaE4k_710mLXsFsMS81q2H13PYTpfM,9023 +js2py/internals/func_utils.py,sha256=zFX9OxGLv-VzvJuWhlR3H3VmgY_fFjLKI0vE1MHAvLI,1927 +js2py/internals/gen.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +js2py/internals/opcodes.py,sha256=Mrx_0fYyjxriZuJS5LiO2GWuOClVGaRZpUXL0SOYrFk,21830 +js2py/internals/operations.py,sha256=Eq9HKy-16xG3ytMIwI3R2W8H03YdfAyVlA-Wx83e1ac,7756 +js2py/internals/prototypes/__init__.py,sha256=vToaNN4iVR00Jnk-mwC6gsNQptrsGHWD_a3cL2w45yg,31 +js2py/internals/prototypes/jsarray.py,sha256=blCDuSVhsKW-pUGHFFOWCJeXtQ7LLzgkjj0SSr06QU8,16374 +js2py/internals/prototypes/jsboolean.py,sha256=UK2MLfLEuwDZPmQ2AMGgIHLhrlv9CcEe7ZVEghjaZAE,670 +js2py/internals/prototypes/jserror.py,sha256=0G1GqkBf6JZFtxofqoNFsIDrDVMpFS5wrDoxaaQAPN4,545 +js2py/internals/prototypes/jsfunction.py,sha256=Bae5hCPSQ7C1pR5M795J0p0ldxEzD_mQ1aRnA1JJaYc,2009 +js2py/internals/prototypes/jsjson.py,sha256=2G-jpFBnyQCF1vkC5UURdtWVKfIR15SvZBYrOXVeeNE,6347 +js2py/internals/prototypes/jsnumber.py,sha256=RdLgKR-JFRK1VwoiPGo4B_B1IKGQYLJKfr2sVtvJBHM,4419 +js2py/internals/prototypes/jsobject.py,sha256=iC9Zjln76ouLwwfUovCKtQuP7d_OORqzXPcsgYg-3Fs,1665 +js2py/internals/prototypes/jsregexp.py,sha256=9ZzdAGEbNat_HLRtgKXNa86AbB09XEvaOmB8VwVt5sQ,1627 +js2py/internals/prototypes/jsstring.py,sha256=i541J5lk1qfbdGVe_mZS6p6dTDJhsR3EKkMFq7Q3_4g,10804 +js2py/internals/prototypes/jsutils.py,sha256=QQPrU63TkAiZQFqy799_0FQUbwEQCu4iJ2AWmo3WVUY,2730 +js2py/internals/seval.py,sha256=Yt-sET-BQpr8aPwPdeF4qJRsrnEZ3Wp15hh0ngWm_Hc,751 +js2py/internals/simplex.py,sha256=O7ucCFjiLsX8D87HfA5n78fcMKOwHEs-PJP4KRw7FoU,4132 +js2py/internals/space.py,sha256=Y5NNHy4TF7Ojn6QPwWlwmaV0FD8Dc83yoRLYo6FbwIA,2669 +js2py/internals/speed.py,sha256=FCvaUwjafvSB_wWzW4Yu64UXNNw3sIwH_sKFosWh4iw,974 +js2py/internals/trans_utils.py,sha256=k7cXh-zRpr-Rn-iL9yIPgrlrNXj5LNGFyiVMeTiH5aM,956 +js2py/node_import.py,sha256=xPeGi7BEk3heZwsRHsXy2MMvD4h-X2YWp0M5DkcSrYQ,7224 +js2py/prototypes/__init__.py,sha256=vToaNN4iVR00Jnk-mwC6gsNQptrsGHWD_a3cL2w45yg,31 +js2py/prototypes/jsarray.py,sha256=NjbL_zWtITrJKgLBOhkDVZlLc_ss6nUM-6P8MO8hggE,15458 +js2py/prototypes/jsarraybuffer.py,sha256=v1_XhH4NdmM6CoM6IxMTZsLMxZyJFZSblW4cLsHBos0,278 +js2py/prototypes/jsboolean.py,sha256=yj3oR9dA-fCpJUh-gAgwZ7K1oHjwYYSoCRGvLiZrjaQ,337 +js2py/prototypes/jserror.py,sha256=eNbCFQWOUWxPP7bYwjIAVrjpppRN7BHscF-lTaIXs-8,446 +js2py/prototypes/jsfunction.py,sha256=BxdpfEBdf_1h8D6FOX7QegIxxjxQRSU2ntX_bKg25fk,1497 +js2py/prototypes/jsjson.py,sha256=iNfpz0QS4lLdxKesJLYTrGo902A-8FAKQExKDgp3w3Y,6544 +js2py/prototypes/jsnumber.py,sha256=tx7yKbgsZ_p66BPE4Ax2PVy3bjHf09xlHYQHXDYuw2I,4016 +js2py/prototypes/jsobject.py,sha256=B2SfLQf_69CsdWm9G6qe3XGIHdXcMRLNi9fjxF-QtTE,855 +js2py/prototypes/jsregexp.py,sha256=5ceKcSGBoGs73NLQz1tpxwp3sWgT1E3nf6rRQVVcxlc,1379 +js2py/prototypes/jsstring.py,sha256=b7wbqtmuhr88mjFah-gUuzJ1ppiwYLBQZiidFYGx6G4,10059 +js2py/prototypes/jstypedarray.py,sha256=KK1cW-sRojT1iTNEdXYfbKtEPSiQ5HwbcEfJhvaeF2c,11070 +js2py/py_node_modules/__init__.py,sha256=lQz6EnruY5a4_aaPK1j9izKeF1ADaeit0ngviwZBDKI,83 +js2py/py_node_modules/crypto_js.py,sha256=100zdrTH8qL1Tt9-XmM93yBlErs2MtITS2EAWBy1H48,1207658 +js2py/py_node_modules/escodegen.py,sha256=99so-iaKhYzGfSpYzJQZrrUUM7kocUy8DLS3DFYKDUY,1255227 +js2py/py_node_modules/esprima.py,sha256=h1lRQWkvXo0L5Y4kJtp5FpKkHBRRErfVnvRZuLHFWQ0,1064590 +js2py/pyjs.py,sha256=Y3wBk8oOa5bPJmMhSvgAeMCc-7WKBM0BuYng_oKovck,2904 +js2py/translators/__init__.py,sha256=qcFRnTsPT7rSTkPWG0l0VUTQs-y23aCxZx3-3gAEAU4,1719 +js2py/translators/friendly_nodes.py,sha256=guMKmKsuNj6iCzngs40OZS2cizOdteKzx4Er8APjYtQ,8761 +js2py/translators/jsregexps.py,sha256=HDxWj-_zrnGiRztTn2aTXcqR_tgLf9TUR7TK2kKK0iw,6763 +js2py/translators/translating_nodes.py,sha256=-w18ybCM18YxWxnBmphltXbLhzlvKmSg_Ox6HIxm3xo,24712 +js2py/translators/translator.py,sha256=NGzNkN3fRrChdzmsu6fvhKhgxcdV-hVoOlNRA9gbWtk,6523 +js2py/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +js2py/utils/injector.py,sha256=gdTCsBT3KnMRjgZI7V37pIs1YjrPgMuexEU5ATSZSgc,12882 diff --git a/libs/subscene_api/__init__.py b/libs/Js2Py-0.74.dist-info/REQUESTED similarity index 100% rename from libs/subscene_api/__init__.py rename to libs/Js2Py-0.74.dist-info/REQUESTED diff --git a/libs/Js2Py-0.74.dist-info/WHEEL b/libs/Js2Py-0.74.dist-info/WHEEL new file mode 100644 index 000000000..ba48cbcf9 --- /dev/null +++ b/libs/Js2Py-0.74.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.41.3) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/libs/Js2Py-0.74.dist-info/top_level.txt b/libs/Js2Py-0.74.dist-info/top_level.txt new file mode 100644 index 000000000..c5a949ec9 --- /dev/null +++ b/libs/Js2Py-0.74.dist-info/top_level.txt @@ -0,0 +1 @@ +js2py diff --git a/libs/Mako-1.3.2.dist-info/INSTALLER b/libs/Mako-1.3.2.dist-info/INSTALLER new file mode 100644 index 000000000..a1b589e38 --- /dev/null +++ b/libs/Mako-1.3.2.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/libs/Mako-1.3.2.dist-info/LICENSE b/libs/Mako-1.3.2.dist-info/LICENSE new file mode 100644 index 000000000..7cf3d4337 --- /dev/null +++ b/libs/Mako-1.3.2.dist-info/LICENSE @@ -0,0 +1,19 @@ +Copyright 2006-2024 the Mako authors and contributors . + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/libs/Mako-1.3.2.dist-info/METADATA b/libs/Mako-1.3.2.dist-info/METADATA new file mode 100644 index 000000000..4558ed984 --- /dev/null +++ b/libs/Mako-1.3.2.dist-info/METADATA @@ -0,0 +1,87 @@ +Metadata-Version: 2.1 +Name: Mako +Version: 1.3.2 +Summary: A super-fast templating language that borrows the best ideas from the existing templating languages. +Home-page: https://www.makotemplates.org/ +Author: Mike Bayer +Author-email: mike@zzzcomputing.com +License: MIT +Project-URL: Documentation, https://docs.makotemplates.org +Project-URL: Issue Tracker, https://github.com/sqlalchemy/mako +Classifier: Development Status :: 5 - Production/Stable +Classifier: License :: OSI Approved :: MIT License +Classifier: Environment :: Web Environment +Classifier: Intended Audience :: Developers +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: PyPy +Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content +Requires-Python: >=3.8 +Description-Content-Type: text/x-rst +License-File: LICENSE +Requires-Dist: MarkupSafe >=0.9.2 +Provides-Extra: babel +Requires-Dist: Babel ; extra == 'babel' +Provides-Extra: lingua +Requires-Dist: lingua ; extra == 'lingua' +Provides-Extra: testing +Requires-Dist: pytest ; extra == 'testing' + +========================= +Mako Templates for Python +========================= + +Mako is a template library written in Python. It provides a familiar, non-XML +syntax which compiles into Python modules for maximum performance. Mako's +syntax and API borrows from the best ideas of many others, including Django +templates, Cheetah, Myghty, and Genshi. Conceptually, Mako is an embedded +Python (i.e. Python Server Page) language, which refines the familiar ideas +of componentized layout and inheritance to produce one of the most +straightforward and flexible models available, while also maintaining close +ties to Python calling and scoping semantics. + +Nutshell +======== + +:: + + <%inherit file="base.html"/> + <% + rows = [[v for v in range(0,10)] for row in range(0,10)] + %> + + % for row in rows: + ${makerow(row)} + % endfor +
+ + <%def name="makerow(row)"> + + % for name in row: + ${name}\ + % endfor + + + +Philosophy +=========== + +Python is a great scripting language. Don't reinvent the wheel...your templates can handle it ! + +Documentation +============== + +See documentation for Mako at https://docs.makotemplates.org/en/latest/ + +License +======== + +Mako is licensed under an MIT-style license (see LICENSE). +Other incorporated projects may be licensed under different licenses. +All licenses allow for non-commercial and commercial use. diff --git a/libs/Mako-1.3.2.dist-info/RECORD b/libs/Mako-1.3.2.dist-info/RECORD new file mode 100644 index 000000000..fed8f2c56 --- /dev/null +++ b/libs/Mako-1.3.2.dist-info/RECORD @@ -0,0 +1,42 @@ +../../bin/mako-render,sha256=NK39DgCmw8pz5T7ALDcW2MB6hFGNVOpWXAHq3-GKyss,236 +Mako-1.3.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +Mako-1.3.2.dist-info/LICENSE,sha256=FWJ7NrONBynN1obfmr9gZQPZnWJLL17FyyVKddWvqJE,1098 +Mako-1.3.2.dist-info/METADATA,sha256=G3lsPTYAPanaYdk-_e8yek5DZpuOjT3Qcf-RMLrlMU0,2900 +Mako-1.3.2.dist-info/RECORD,, +Mako-1.3.2.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +Mako-1.3.2.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92 +Mako-1.3.2.dist-info/entry_points.txt,sha256=LsKkUsOsJQYbJ2M72hZCm968wi5K8Ywb5uFxCuN8Obk,512 +Mako-1.3.2.dist-info/top_level.txt,sha256=LItdH8cDPetpUu8rUyBG3DObS6h9Gcpr9j_WLj2S-R0,5 +mako/__init__.py,sha256=brA7o1ju8zST1YWfFRXDxaKmMlKiRHVqefjxeUN6YjI,242 +mako/_ast_util.py,sha256=CenxCrdES1irHDhOQU6Ldta4rdsytfYaMkN6s0TlveM,20247 +mako/ast.py,sha256=pY7MH-5cLnUuVz5YAwoGhWgWfgoVvLQkRDtc_s9qqw0,6642 +mako/cache.py,sha256=5DBBorj1NqiWDqNhN3ZJ8tMCm-h6Mew541276kdsxAU,7680 +mako/cmd.py,sha256=vP5M5g9yc5sjAT5owVTQu056YwyS-YkpulFSDb0IMGw,2813 +mako/codegen.py,sha256=UgB8K6BMNiBTUGucR4UYkqFqlbNUJfErKI9A-n4Wteg,47307 +mako/compat.py,sha256=wjVMf7uMg0TlC_aI5hdwWizza99nqJuGNdrnTNrZbt0,1820 +mako/exceptions.py,sha256=pfdd5-1lCZ--I2YqQ_oHODZLmo62bn_lO5Kz_1__72w,12530 +mako/ext/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +mako/ext/autohandler.py,sha256=Tyz1CLRlG_C0EnKgjuHzqS4BBnpeA49O05x8rriNtTY,1885 +mako/ext/babelplugin.py,sha256=v10o5XQdgXwbr1bo0aL8VV3THm_84C_eeq6BmAGd3uA,2091 +mako/ext/beaker_cache.py,sha256=aAs9ELzO2WaiO5OppV4KDT6f7yNyn1BF1XHDQZwf0-E,2578 +mako/ext/extract.py,sha256=c3YuIN3Z5ZgS-xzX_gjKrEVQaptK3liXkm5j-Vq8yEM,4659 +mako/ext/linguaplugin.py,sha256=pzHlHC3-KlFeVAR4r8S1--_dfE5DcYmjLXtr0genBYU,1935 +mako/ext/preprocessors.py,sha256=zKQy42Ce6dOmU0Yk_rUVDAAn38-RUUfQolVKTJjLotA,576 +mako/ext/pygmentplugin.py,sha256=qBdsAhKktlQX7d5Yv1sAXufUNOZqcnJmKuC7V4D_srM,4753 +mako/ext/turbogears.py,sha256=0emY1WiMnuY8Pf6ARv5JBArKtouUdmuTljI-w6rE3J4,2141 +mako/filters.py,sha256=F7aDIKTUxnT-Og4rgboQtnML7Q87DJTHQyhi_dY_Ih4,4658 +mako/lexer.py,sha256=dtCZU1eoF3ymEdiCwCzEIw5SH0lgJkDsHJy9VHI_2XY,16324 +mako/lookup.py,sha256=rkMvT5T7EOS5KRvPtgYii-sjh1nWWyKok_mEk-cEzrM,12428 +mako/parsetree.py,sha256=7RNVRTsKcsMt8vU4NQi5C7e4vhdUyA9tqyd1yIkvAAQ,19007 +mako/pygen.py,sha256=d4f_ugRACCXuV9hJgEk6Ncoj38EaRHA3RTxkr_tK7UQ,10416 +mako/pyparser.py,sha256=81rIcSn4PoALpZF0WO6D5rB65TvF8R9Qn_hBSfTGS5Q,7029 +mako/runtime.py,sha256=ZsUEN22nX3d3dECQujF69mBKDQS6yVv2nvz_0eTvFGg,27804 +mako/template.py,sha256=4xQzwruZd5XzPw7iONZMZJj4SdFsctYYg4PfBYs2PLk,23857 +mako/testing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +mako/testing/_config.py,sha256=k-qpnsnbXUoN-ykMN5BRpg84i1x0p6UsAddKQnrIytU,3566 +mako/testing/assertions.py,sha256=pfbGl84QlW7QWGg3_lo3wP8XnBAVo9AjzNp2ajmn7FA,5161 +mako/testing/config.py,sha256=wmYVZfzGvOK3mJUZpzmgO8-iIgvaCH41Woi4yDpxq6E,323 +mako/testing/exclusions.py,sha256=_t6ADKdatk3f18tOfHV_ZY6u_ZwQsKphZ2MXJVSAOcI,1553 +mako/testing/fixtures.py,sha256=nEp7wTusf7E0n3Q-BHJW2s_t1vx0KB9poadQ1BmIJzE,3044 +mako/testing/helpers.py,sha256=z4HAactwlht4ut1cbvxKt1QLb3yLPk1U7cnh5BwVUlc,1623 +mako/util.py,sha256=dIFuchHfiNtRJJ99kEIRdHBkCZ3UmEvNO6l2ZQSCdVU,10638 diff --git a/libs/urllib3/contrib/_securetransport/__init__.py b/libs/Mako-1.3.2.dist-info/REQUESTED similarity index 100% rename from libs/urllib3/contrib/_securetransport/__init__.py rename to libs/Mako-1.3.2.dist-info/REQUESTED diff --git a/libs/Mako-1.3.2.dist-info/WHEEL b/libs/Mako-1.3.2.dist-info/WHEEL new file mode 100644 index 000000000..98c0d20b7 --- /dev/null +++ b/libs/Mako-1.3.2.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.42.0) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/libs/Mako-1.3.2.dist-info/entry_points.txt b/libs/Mako-1.3.2.dist-info/entry_points.txt new file mode 100644 index 000000000..30f31b2bf --- /dev/null +++ b/libs/Mako-1.3.2.dist-info/entry_points.txt @@ -0,0 +1,18 @@ +[babel.extractors] +mako = mako.ext.babelplugin:extract [babel] + +[console_scripts] +mako-render = mako.cmd:cmdline + +[lingua.extractors] +mako = mako.ext.linguaplugin:LinguaMakoExtractor [lingua] + +[pygments.lexers] +css+mako = mako.ext.pygmentplugin:MakoCssLexer +html+mako = mako.ext.pygmentplugin:MakoHtmlLexer +js+mako = mako.ext.pygmentplugin:MakoJavascriptLexer +mako = mako.ext.pygmentplugin:MakoLexer +xml+mako = mako.ext.pygmentplugin:MakoXmlLexer + +[python.templating.engines] +mako = mako.ext.turbogears:TGPlugin diff --git a/libs/Mako-1.3.2.dist-info/top_level.txt b/libs/Mako-1.3.2.dist-info/top_level.txt new file mode 100644 index 000000000..2951cdd49 --- /dev/null +++ b/libs/Mako-1.3.2.dist-info/top_level.txt @@ -0,0 +1 @@ +mako diff --git a/libs/Markdown-3.5.2.dist-info/INSTALLER b/libs/Markdown-3.5.2.dist-info/INSTALLER new file mode 100644 index 000000000..a1b589e38 --- /dev/null +++ b/libs/Markdown-3.5.2.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/libs/Markdown-3.5.2.dist-info/LICENSE.md b/libs/Markdown-3.5.2.dist-info/LICENSE.md new file mode 100644 index 000000000..2652d97ad --- /dev/null +++ b/libs/Markdown-3.5.2.dist-info/LICENSE.md @@ -0,0 +1,29 @@ +Copyright 2007, 2008 The Python Markdown Project (v. 1.7 and later) +Copyright 2004, 2005, 2006 Yuri Takhteyev (v. 0.2-1.6b) +Copyright 2004 Manfred Stienstra (the original version) + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +* Neither the name of the Python Markdown Project nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE PYTHON MARKDOWN PROJECT ''AS IS'' AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL ANY CONTRIBUTORS TO THE PYTHON MARKDOWN PROJECT +BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/libs/Markdown-3.5.2.dist-info/METADATA b/libs/Markdown-3.5.2.dist-info/METADATA new file mode 100644 index 000000000..866453f0b --- /dev/null +++ b/libs/Markdown-3.5.2.dist-info/METADATA @@ -0,0 +1,145 @@ +Metadata-Version: 2.1 +Name: Markdown +Version: 3.5.2 +Summary: Python implementation of John Gruber's Markdown. +Author: Manfred Stienstra, Yuri Takhteyev +Author-email: Waylan limberg +Maintainer: Isaac Muse +Maintainer-email: Waylan Limberg +License: Copyright 2007, 2008 The Python Markdown Project (v. 1.7 and later) + Copyright 2004, 2005, 2006 Yuri Takhteyev (v. 0.2-1.6b) + Copyright 2004 Manfred Stienstra (the original version) + + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the Python Markdown Project nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE PYTHON MARKDOWN PROJECT ''AS IS'' AND ANY + EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL ANY CONTRIBUTORS TO THE PYTHON MARKDOWN PROJECT + BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. + +Project-URL: Homepage, https://Python-Markdown.github.io/ +Project-URL: Documentation, https://Python-Markdown.github.io/ +Project-URL: Repository, https://github.com/Python-Markdown/markdown +Project-URL: Issue Tracker, https://github.com/Python-Markdown/markdown/issues +Project-URL: Changelog, https://python-markdown.github.io/changelog/ +Keywords: markdown,markdown-parser,python-markdown,markdown-to-html +Classifier: Development Status :: 5 - Production/Stable +Classifier: License :: OSI Approved :: BSD License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3 :: Only +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: PyPy +Classifier: Topic :: Communications :: Email :: Filters +Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content :: CGI Tools/Libraries +Classifier: Topic :: Internet :: WWW/HTTP :: Site Management +Classifier: Topic :: Software Development :: Documentation +Classifier: Topic :: Software Development :: Libraries :: Python Modules +Classifier: Topic :: Text Processing :: Filters +Classifier: Topic :: Text Processing :: Markup :: HTML +Classifier: Topic :: Text Processing :: Markup :: Markdown +Requires-Python: >=3.8 +Description-Content-Type: text/markdown +License-File: LICENSE.md +Requires-Dist: importlib-metadata >=4.4 ; python_version < "3.10" +Provides-Extra: docs +Requires-Dist: mkdocs >=1.5 ; extra == 'docs' +Requires-Dist: mkdocs-nature >=0.6 ; extra == 'docs' +Requires-Dist: mdx-gh-links >=0.2 ; extra == 'docs' +Requires-Dist: mkdocstrings[python] ; extra == 'docs' +Requires-Dist: mkdocs-gen-files ; extra == 'docs' +Requires-Dist: mkdocs-section-index ; extra == 'docs' +Requires-Dist: mkdocs-literate-nav ; extra == 'docs' +Provides-Extra: testing +Requires-Dist: coverage ; extra == 'testing' +Requires-Dist: pyyaml ; extra == 'testing' + +[Python-Markdown][] +=================== + +[![Build Status][build-button]][build] +[![Coverage Status][codecov-button]][codecov] +[![Latest Version][mdversion-button]][md-pypi] +[![Python Versions][pyversion-button]][md-pypi] +[![BSD License][bsdlicense-button]][bsdlicense] +[![Code of Conduct][codeofconduct-button]][Code of Conduct] + +[build-button]: https://github.com/Python-Markdown/markdown/workflows/CI/badge.svg?event=push +[build]: https://github.com/Python-Markdown/markdown/actions?query=workflow%3ACI+event%3Apush +[codecov-button]: https://codecov.io/gh/Python-Markdown/markdown/branch/master/graph/badge.svg +[codecov]: https://codecov.io/gh/Python-Markdown/markdown +[mdversion-button]: https://img.shields.io/pypi/v/Markdown.svg +[md-pypi]: https://pypi.org/project/Markdown/ +[pyversion-button]: https://img.shields.io/pypi/pyversions/Markdown.svg +[bsdlicense-button]: https://img.shields.io/badge/license-BSD-yellow.svg +[bsdlicense]: https://opensource.org/licenses/BSD-3-Clause +[codeofconduct-button]: https://img.shields.io/badge/code%20of%20conduct-contributor%20covenant-green.svg?style=flat-square +[Code of Conduct]: https://github.com/Python-Markdown/markdown/blob/master/CODE_OF_CONDUCT.md + +This is a Python implementation of John Gruber's [Markdown][]. +It is almost completely compliant with the reference implementation, +though there are a few known issues. See [Features][] for information +on what exactly is supported and what is not. Additional features are +supported by the [Available Extensions][]. + +[Python-Markdown]: https://Python-Markdown.github.io/ +[Markdown]: https://daringfireball.net/projects/markdown/ +[Features]: https://Python-Markdown.github.io#Features +[Available Extensions]: https://Python-Markdown.github.io/extensions + +Documentation +------------- + +```bash +pip install markdown +``` +```python +import markdown +html = markdown.markdown(your_text_string) +``` + +For more advanced [installation] and [usage] documentation, see the `docs/` directory +of the distribution or the project website at . + +[installation]: https://python-markdown.github.io/install/ +[usage]: https://python-markdown.github.io/reference/ + +See the change log at . + +Support +------- + +You may report bugs, ask for help, and discuss various other issues on the [bug tracker][]. + +[bug tracker]: https://github.com/Python-Markdown/markdown/issues + +Code of Conduct +--------------- + +Everyone interacting in the Python-Markdown project's code bases, issue trackers, +and mailing lists is expected to follow the [Code of Conduct]. diff --git a/libs/Markdown-3.5.2.dist-info/RECORD b/libs/Markdown-3.5.2.dist-info/RECORD new file mode 100644 index 000000000..a3e6a9479 --- /dev/null +++ b/libs/Markdown-3.5.2.dist-info/RECORD @@ -0,0 +1,42 @@ +../../bin/markdown_py,sha256=a0a3HrUHepb4z4hcrRdCfAEQ8SiB-QoWxf9g1e-KLv8,237 +Markdown-3.5.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +Markdown-3.5.2.dist-info/LICENSE.md,sha256=bxGTy2NHGOZcOlN9biXr1hSCDsDvaTz8EiSBEmONZNo,1645 +Markdown-3.5.2.dist-info/METADATA,sha256=9pbPWhPBzgBE-uAwHn0vC0CGT-mw0KC8-SanIkqAFUo,7029 +Markdown-3.5.2.dist-info/RECORD,, +Markdown-3.5.2.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +Markdown-3.5.2.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92 +Markdown-3.5.2.dist-info/entry_points.txt,sha256=lMEyiiA_ZZyfPCBlDviBl-SiU0cfoeuEKpwxw361sKQ,1102 +Markdown-3.5.2.dist-info/top_level.txt,sha256=IAxs8x618RXoH1uCqeLLxXsDefJvE_mIibr_M4sOlyk,9 +markdown/__init__.py,sha256=dfzwwdpG9L8QLEPBpLFPIHx_BN056aZXp9xZifTxYIU,1777 +markdown/__main__.py,sha256=innFBxRqwPBNxG1zhKktJji4bnRKtVyYYd30ID13Tcw,5859 +markdown/__meta__.py,sha256=K-Yr6cieMQWMYt9f7Rx8PGkX2PYqz4aVJdupZf_CE2Q,1712 +markdown/blockparser.py,sha256=j4CQImVpiq7g9pz8wCxvzT61X_T2iSAjXupHJk8P3eA,5728 +markdown/blockprocessors.py,sha256=dZFoOXABuOs5AFNpWrrsVEmPe1i8_JVe4rOusdRRPAA,26648 +markdown/core.py,sha256=DyyzDsmd-KcuEp8ZWUKJAeUCt7B7G3J3NeqZqp3LphI,21335 +markdown/extensions/__init__.py,sha256=9z1khsdKCVrmrJ_2GfxtPAdjD3FyMe5vhC7wmM4O9m0,4822 +markdown/extensions/abbr.py,sha256=J27cKf_vKY5wdKA_Bunwk83c3RpxwfgDeGarFF0FCuk,3540 +markdown/extensions/admonition.py,sha256=Hqcn3I8JG0i-OPWdoqI189TmlQRgH6bs5PmpCANyLlg,6547 +markdown/extensions/attr_list.py,sha256=6PzqkH7N_U5lks7PGH7dSGmEGVuMCYR9MaR3g8c9Spw,6584 +markdown/extensions/codehilite.py,sha256=ChlmpM6S--j-UK7t82859UpYjm8EftdiLqmgDnknyes,13503 +markdown/extensions/def_list.py,sha256=J3NVa6CllfZPsboJCEycPyRhtjBHnOn8ET6omEvVlDo,4029 +markdown/extensions/extra.py,sha256=1vleT284kued4HQBtF83IjSumJVo0q3ng6MjTkVNfNQ,2163 +markdown/extensions/fenced_code.py,sha256=Xy4sQDjEsSJuShiAf9bwpv8Khtyf7Y6QDjNlH7QVM-Q,7817 +markdown/extensions/footnotes.py,sha256=bRFlmIBOKDI5efG1jZfDkMoV2osfqWip1rN1j2P-mMg,16710 +markdown/extensions/legacy_attrs.py,sha256=oWcyNrfP0F6zsBoBOaD5NiwrJyy4kCpgQLl12HA7JGU,2788 +markdown/extensions/legacy_em.py,sha256=-Z_w4PEGSS-Xg-2-BtGAnXwwy5g5GDgv2tngASnPgxg,1693 +markdown/extensions/md_in_html.py,sha256=y4HEWEnkvfih22fojcaJeAmjx1AtF8N-a_jb6IDFfts,16546 +markdown/extensions/meta.py,sha256=v_4Uq7nbcQ76V1YAvqVPiNLbRLIQHJsnfsk-tN70RmY,2600 +markdown/extensions/nl2br.py,sha256=9KKcrPs62c3ENNnmOJZs0rrXXqUtTCfd43j1_OPpmgU,1090 +markdown/extensions/sane_lists.py,sha256=ogAKcm7gEpcXV7fSTf8JZH5YdKAssPCEOUzdGM3C9Tw,2150 +markdown/extensions/smarty.py,sha256=DLmH22prpdZLDkV7GOCC1OTlCbTknKPHT9UNPs5-TwQ,11048 +markdown/extensions/tables.py,sha256=oTDvGD1qp9xjVWPGYNgDBWe9NqsX5gS6UU5wUsQ1bC8,8741 +markdown/extensions/toc.py,sha256=Vo2PFW4I0-ixOxTXzhoMhUTIAGiDeLPT7l0LNc9ZcBI,15293 +markdown/extensions/wikilinks.py,sha256=j7D2sozica6sqXOUa_GuAXqIzxp-7Hi60bfXymiuma8,3285 +markdown/htmlparser.py,sha256=dEr6IE7i9b6Tc1gdCLZGeWw6g6-E-jK1Z4KPj8yGk8Q,14332 +markdown/inlinepatterns.py,sha256=7_HF5nTOyQag_CyBgU4wwmuI6aMjtadvGadyS9IP21w,38256 +markdown/postprocessors.py,sha256=eYi6eW0mGudmWpmsW45hduLwX66Zr8Bf44WyU9vKp-I,4807 +markdown/preprocessors.py,sha256=pq5NnHKkOSVQeIo-ajC-Yt44kvyMV97D04FBOQXctJM,3224 +markdown/serializers.py,sha256=YtAFYQoOdp_TAmYGow6nBo0eB6I-Sl4PTLdLDfQJHwQ,7174 +markdown/test_tools.py,sha256=MtN4cf3ZPDtb83wXLTol-3q3aIGRIkJ2zWr6fd-RgVE,8662 +markdown/treeprocessors.py,sha256=o4dnoZZsIeVV8qR45Njr8XgwKleWYDS5pv8dKQhJvv8,17651 +markdown/util.py,sha256=vJ1E0xjMzDAlTqLUSJWgdEvxdQfLXDEYUssOQMw9kPQ,13929 diff --git a/libs/urllib3/packages/__init__.py b/libs/Markdown-3.5.2.dist-info/REQUESTED similarity index 100% rename from libs/urllib3/packages/__init__.py rename to libs/Markdown-3.5.2.dist-info/REQUESTED diff --git a/libs/Markdown-3.5.2.dist-info/WHEEL b/libs/Markdown-3.5.2.dist-info/WHEEL new file mode 100644 index 000000000..98c0d20b7 --- /dev/null +++ b/libs/Markdown-3.5.2.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.42.0) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/libs/Markdown-3.5.2.dist-info/entry_points.txt b/libs/Markdown-3.5.2.dist-info/entry_points.txt new file mode 100644 index 000000000..be3bd8ff2 --- /dev/null +++ b/libs/Markdown-3.5.2.dist-info/entry_points.txt @@ -0,0 +1,22 @@ +[console_scripts] +markdown_py = markdown.__main__:run + +[markdown.extensions] +abbr = markdown.extensions.abbr:AbbrExtension +admonition = markdown.extensions.admonition:AdmonitionExtension +attr_list = markdown.extensions.attr_list:AttrListExtension +codehilite = markdown.extensions.codehilite:CodeHiliteExtension +def_list = markdown.extensions.def_list:DefListExtension +extra = markdown.extensions.extra:ExtraExtension +fenced_code = markdown.extensions.fenced_code:FencedCodeExtension +footnotes = markdown.extensions.footnotes:FootnoteExtension +legacy_attrs = markdown.extensions.legacy_attrs:LegacyAttrExtension +legacy_em = markdown.extensions.legacy_em:LegacyEmExtension +md_in_html = markdown.extensions.md_in_html:MarkdownInHtmlExtension +meta = markdown.extensions.meta:MetaExtension +nl2br = markdown.extensions.nl2br:Nl2BrExtension +sane_lists = markdown.extensions.sane_lists:SaneListExtension +smarty = markdown.extensions.smarty:SmartyExtension +tables = markdown.extensions.tables:TableExtension +toc = markdown.extensions.toc:TocExtension +wikilinks = markdown.extensions.wikilinks:WikiLinkExtension diff --git a/libs/Markdown-3.5.2.dist-info/top_level.txt b/libs/Markdown-3.5.2.dist-info/top_level.txt new file mode 100644 index 000000000..0918c9768 --- /dev/null +++ b/libs/Markdown-3.5.2.dist-info/top_level.txt @@ -0,0 +1 @@ +markdown diff --git a/libs/MarkupSafe-2.1.5.dist-info/INSTALLER b/libs/MarkupSafe-2.1.5.dist-info/INSTALLER new file mode 100644 index 000000000..a1b589e38 --- /dev/null +++ b/libs/MarkupSafe-2.1.5.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/libs/MarkupSafe-2.1.5.dist-info/LICENSE.rst b/libs/MarkupSafe-2.1.5.dist-info/LICENSE.rst new file mode 100644 index 000000000..9d227a0cc --- /dev/null +++ b/libs/MarkupSafe-2.1.5.dist-info/LICENSE.rst @@ -0,0 +1,28 @@ +Copyright 2010 Pallets + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/libs/MarkupSafe-2.1.5.dist-info/METADATA b/libs/MarkupSafe-2.1.5.dist-info/METADATA new file mode 100644 index 000000000..dfe37d52d --- /dev/null +++ b/libs/MarkupSafe-2.1.5.dist-info/METADATA @@ -0,0 +1,93 @@ +Metadata-Version: 2.1 +Name: MarkupSafe +Version: 2.1.5 +Summary: Safely add untrusted strings to HTML/XML markup. +Home-page: https://palletsprojects.com/p/markupsafe/ +Maintainer: Pallets +Maintainer-email: contact@palletsprojects.com +License: BSD-3-Clause +Project-URL: Donate, https://palletsprojects.com/donate +Project-URL: Documentation, https://markupsafe.palletsprojects.com/ +Project-URL: Changes, https://markupsafe.palletsprojects.com/changes/ +Project-URL: Source Code, https://github.com/pallets/markupsafe/ +Project-URL: Issue Tracker, https://github.com/pallets/markupsafe/issues/ +Project-URL: Chat, https://discord.gg/pallets +Classifier: Development Status :: 5 - Production/Stable +Classifier: Environment :: Web Environment +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: BSD License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content +Classifier: Topic :: Text Processing :: Markup :: HTML +Requires-Python: >=3.7 +Description-Content-Type: text/x-rst +License-File: LICENSE.rst + +MarkupSafe +========== + +MarkupSafe implements a text object that escapes characters so it is +safe to use in HTML and XML. Characters that have special meanings are +replaced so that they display as the actual characters. This mitigates +injection attacks, meaning untrusted user input can safely be displayed +on a page. + + +Installing +---------- + +Install and update using `pip`_: + +.. code-block:: text + + pip install -U MarkupSafe + +.. _pip: https://pip.pypa.io/en/stable/getting-started/ + + +Examples +-------- + +.. code-block:: pycon + + >>> from markupsafe import Markup, escape + + >>> # escape replaces special characters and wraps in Markup + >>> escape("") + Markup('<script>alert(document.cookie);</script>') + + >>> # wrap in Markup to mark text "safe" and prevent escaping + >>> Markup("Hello") + Markup('hello') + + >>> escape(Markup("Hello")) + Markup('hello') + + >>> # Markup is a str subclass + >>> # methods and operators escape their arguments + >>> template = Markup("Hello {name}") + >>> template.format(name='"World"') + Markup('Hello "World"') + + +Donate +------ + +The Pallets organization develops and supports MarkupSafe and other +popular packages. In order to grow the community of contributors and +users, and allow the maintainers to devote more time to the projects, +`please donate today`_. + +.. _please donate today: https://palletsprojects.com/donate + + +Links +----- + +- Documentation: https://markupsafe.palletsprojects.com/ +- Changes: https://markupsafe.palletsprojects.com/changes/ +- PyPI Releases: https://pypi.org/project/MarkupSafe/ +- Source Code: https://github.com/pallets/markupsafe/ +- Issue Tracker: https://github.com/pallets/markupsafe/issues/ +- Chat: https://discord.gg/pallets diff --git a/libs/MarkupSafe-2.1.5.dist-info/RECORD b/libs/MarkupSafe-2.1.5.dist-info/RECORD new file mode 100644 index 000000000..2b3c4338b --- /dev/null +++ b/libs/MarkupSafe-2.1.5.dist-info/RECORD @@ -0,0 +1,13 @@ +MarkupSafe-2.1.5.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +MarkupSafe-2.1.5.dist-info/LICENSE.rst,sha256=SJqOEQhQntmKN7uYPhHg9-HTHwvY-Zp5yESOf_N9B-o,1475 +MarkupSafe-2.1.5.dist-info/METADATA,sha256=2dRDPam6OZLfpX0wg1JN5P3u9arqACxVSfdGmsJU7o8,3003 +MarkupSafe-2.1.5.dist-info/RECORD,, +MarkupSafe-2.1.5.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +MarkupSafe-2.1.5.dist-info/WHEEL,sha256=EO1EUWjlSI9vqFKe-qOLBJFxSac53mP8l62vW3JFDec,109 +MarkupSafe-2.1.5.dist-info/top_level.txt,sha256=qy0Plje5IJuvsCBjejJyhDCjEAdcDLK_2agVcex8Z6U,11 +markupsafe/__init__.py,sha256=r7VOTjUq7EMQ4v3p4R1LoVOGJg6ysfYRncLr34laRBs,10958 +markupsafe/_native.py,sha256=GR86Qvo_GcgKmKreA1WmYN9ud17OFwkww8E-fiW-57s,1713 +markupsafe/_speedups.c,sha256=X2XvQVtIdcK4Usz70BvkzoOfjTCmQlDkkjYSn-swE0g,7083 +markupsafe/_speedups.cpython-38-darwin.so,sha256=_9uBZXDBin5PdKhWn3XnAUpjp031_nC6MFOMuSDD-3g,51480 +markupsafe/_speedups.pyi,sha256=vfMCsOgbAXRNLUXkyuyonG8uEWKYU4PDqNuMaDELAYw,229 +markupsafe/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 diff --git a/libs/urllib3/packages/backports/__init__.py b/libs/MarkupSafe-2.1.5.dist-info/REQUESTED similarity index 100% rename from libs/urllib3/packages/backports/__init__.py rename to libs/MarkupSafe-2.1.5.dist-info/REQUESTED diff --git a/libs/MarkupSafe-2.1.5.dist-info/WHEEL b/libs/MarkupSafe-2.1.5.dist-info/WHEEL new file mode 100644 index 000000000..9fd57fe24 --- /dev/null +++ b/libs/MarkupSafe-2.1.5.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.41.3) +Root-Is-Purelib: false +Tag: cp38-cp38-macosx_12_0_x86_64 + diff --git a/libs/MarkupSafe-2.1.5.dist-info/top_level.txt b/libs/MarkupSafe-2.1.5.dist-info/top_level.txt new file mode 100644 index 000000000..75bf72925 --- /dev/null +++ b/libs/MarkupSafe-2.1.5.dist-info/top_level.txt @@ -0,0 +1 @@ +markupsafe diff --git a/libs/PySocks-1.7.1.dist-info/INSTALLER b/libs/PySocks-1.7.1.dist-info/INSTALLER new file mode 100644 index 000000000..a1b589e38 --- /dev/null +++ b/libs/PySocks-1.7.1.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/libs/PySocks-1.7.1.dist-info/LICENSE b/libs/PySocks-1.7.1.dist-info/LICENSE new file mode 100644 index 000000000..04b6b1f37 --- /dev/null +++ b/libs/PySocks-1.7.1.dist-info/LICENSE @@ -0,0 +1,22 @@ +Copyright 2006 Dan-Haim. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. +3. Neither the name of Dan Haim nor the names of his contributors may be used + to endorse or promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY DAN HAIM "AS IS" AND ANY EXPRESS OR IMPLIED +WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO +EVENT SHALL DAN HAIM OR HIS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA +OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMANGE. diff --git a/libs/PySocks-1.7.1.dist-info/METADATA b/libs/PySocks-1.7.1.dist-info/METADATA new file mode 100644 index 000000000..ea990785e --- /dev/null +++ b/libs/PySocks-1.7.1.dist-info/METADATA @@ -0,0 +1,319 @@ +Metadata-Version: 2.1 +Name: PySocks +Version: 1.7.1 +Summary: A Python SOCKS client module. See https://github.com/Anorov/PySocks for more information. +Home-page: https://github.com/Anorov/PySocks +Author: Anorov +Author-email: anorov.vorona@gmail.com +License: BSD +Keywords: socks,proxy +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.4 +Classifier: Programming Language :: Python :: 3.5 +Classifier: Programming Language :: Python :: 3.6 +Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.* +Description-Content-Type: text/markdown +License-File: LICENSE + +PySocks +======= + +PySocks lets you send traffic through SOCKS and HTTP proxy servers. It is a modern fork of [SocksiPy](http://socksipy.sourceforge.net/) with bug fixes and extra features. + +Acts as a drop-in replacement to the socket module. Seamlessly configure SOCKS proxies for any socket object by calling `socket_object.set_proxy()`. + +---------------- + +Features +======== + +* SOCKS proxy client for Python 2.7 and 3.4+ +* TCP supported +* UDP mostly supported (issues may occur in some edge cases) +* HTTP proxy client included but not supported or recommended (you should use urllib2's or requests' own HTTP proxy interface) +* urllib2 handler included. `pip install` / `setup.py install` will automatically install the `sockshandler` module. + +Installation +============ + + pip install PySocks + +Or download the tarball / `git clone` and... + + python setup.py install + +These will install both the `socks` and `sockshandler` modules. + +Alternatively, include just `socks.py` in your project. + +-------------------------------------------- + +*Warning:* PySocks/SocksiPy only supports HTTP proxies that use CONNECT tunneling. Certain HTTP proxies may not work with this library. If you wish to use HTTP (not SOCKS) proxies, it is recommended that you rely on your HTTP client's native proxy support (`proxies` dict for `requests`, or `urllib2.ProxyHandler` for `urllib2`) instead. + +-------------------------------------------- + +Usage +===== + +## socks.socksocket ## + + import socks + + s = socks.socksocket() # Same API as socket.socket in the standard lib + + s.set_proxy(socks.SOCKS5, "localhost") # SOCKS4 and SOCKS5 use port 1080 by default + # Or + s.set_proxy(socks.SOCKS4, "localhost", 4444) + # Or + s.set_proxy(socks.HTTP, "5.5.5.5", 8888) + + # Can be treated identical to a regular socket object + s.connect(("www.somesite.com", 80)) + s.sendall("GET / HTTP/1.1 ...") + print s.recv(4096) + +## Monkeypatching ## + +To monkeypatch the entire standard library with a single default proxy: + + import urllib2 + import socket + import socks + + socks.set_default_proxy(socks.SOCKS5, "localhost") + socket.socket = socks.socksocket + + urllib2.urlopen("http://www.somesite.com/") # All requests will pass through the SOCKS proxy + +Note that monkeypatching may not work for all standard modules or for all third party modules, and generally isn't recommended. Monkeypatching is usually an anti-pattern in Python. + +## urllib2 Handler ## + +Example use case with the `sockshandler` urllib2 handler. Note that you must import both `socks` and `sockshandler`, as the handler is its own module separate from PySocks. The module is included in the PyPI package. + + import urllib2 + import socks + from sockshandler import SocksiPyHandler + + opener = urllib2.build_opener(SocksiPyHandler(socks.SOCKS5, "127.0.0.1", 9050)) + print opener.open("http://www.somesite.com/") # All requests made by the opener will pass through the SOCKS proxy + +-------------------------------------------- + +Original SocksiPy README attached below, amended to reflect API changes. + +-------------------------------------------- + +SocksiPy + +A Python SOCKS module. + +(C) 2006 Dan-Haim. All rights reserved. + +See LICENSE file for details. + + +*WHAT IS A SOCKS PROXY?* + +A SOCKS proxy is a proxy server at the TCP level. In other words, it acts as +a tunnel, relaying all traffic going through it without modifying it. +SOCKS proxies can be used to relay traffic using any network protocol that +uses TCP. + +*WHAT IS SOCKSIPY?* + +This Python module allows you to create TCP connections through a SOCKS +proxy without any special effort. +It also supports relaying UDP packets with a SOCKS5 proxy. + +*PROXY COMPATIBILITY* + +SocksiPy is compatible with three different types of proxies: + +1. SOCKS Version 4 (SOCKS4), including the SOCKS4a extension. +2. SOCKS Version 5 (SOCKS5). +3. HTTP Proxies which support tunneling using the CONNECT method. + +*SYSTEM REQUIREMENTS* + +Being written in Python, SocksiPy can run on any platform that has a Python +interpreter and TCP/IP support. +This module has been tested with Python 2.3 and should work with greater versions +just as well. + + +INSTALLATION +------------- + +Simply copy the file "socks.py" to your Python's `lib/site-packages` directory, +and you're ready to go. [Editor's note: it is better to use `python setup.py install` for PySocks] + + +USAGE +------ + +First load the socks module with the command: + + >>> import socks + >>> + +The socks module provides a class called `socksocket`, which is the base to all of the module's functionality. + +The `socksocket` object has the same initialization parameters as the normal socket +object to ensure maximal compatibility, however it should be noted that `socksocket` will only function with family being `AF_INET` and +type being either `SOCK_STREAM` or `SOCK_DGRAM`. +Generally, it is best to initialize the `socksocket` object with no parameters + + >>> s = socks.socksocket() + >>> + +The `socksocket` object has an interface which is very similiar to socket's (in fact +the `socksocket` class is derived from socket) with a few extra methods. +To select the proxy server you would like to use, use the `set_proxy` method, whose +syntax is: + + set_proxy(proxy_type, addr[, port[, rdns[, username[, password]]]]) + +Explanation of the parameters: + +`proxy_type` - The type of the proxy server. This can be one of three possible +choices: `PROXY_TYPE_SOCKS4`, `PROXY_TYPE_SOCKS5` and `PROXY_TYPE_HTTP` for SOCKS4, +SOCKS5 and HTTP servers respectively. `SOCKS4`, `SOCKS5`, and `HTTP` are all aliases, respectively. + +`addr` - The IP address or DNS name of the proxy server. + +`port` - The port of the proxy server. Defaults to 1080 for socks and 8080 for http. + +`rdns` - This is a boolean flag than modifies the behavior regarding DNS resolving. +If it is set to True, DNS resolving will be preformed remotely, on the server. +If it is set to False, DNS resolving will be preformed locally. Please note that +setting this to True with SOCKS4 servers actually use an extension to the protocol, +called SOCKS4a, which may not be supported on all servers (SOCKS5 and http servers +always support DNS). The default is True. + +`username` - For SOCKS5 servers, this allows simple username / password authentication +with the server. For SOCKS4 servers, this parameter will be sent as the userid. +This parameter is ignored if an HTTP server is being used. If it is not provided, +authentication will not be used (servers may accept unauthenticated requests). + +`password` - This parameter is valid only for SOCKS5 servers and specifies the +respective password for the username provided. + +Example of usage: + + >>> s.set_proxy(socks.SOCKS5, "socks.example.com") # uses default port 1080 + >>> s.set_proxy(socks.SOCKS4, "socks.test.com", 1081) + +After the set_proxy method has been called, simply call the connect method with the +traditional parameters to establish a connection through the proxy: + + >>> s.connect(("www.sourceforge.net", 80)) + >>> + +Connection will take a bit longer to allow negotiation with the proxy server. +Please note that calling connect without calling `set_proxy` earlier will connect +without a proxy (just like a regular socket). + +Errors: Any errors in the connection process will trigger exceptions. The exception +may either be generated by the underlying socket layer or may be custom module +exceptions, whose details follow: + +class `ProxyError` - This is a base exception class. It is not raised directly but +rather all other exception classes raised by this module are derived from it. +This allows an easy way to catch all proxy-related errors. It descends from `IOError`. + +All `ProxyError` exceptions have an attribute `socket_err`, which will contain either a +caught `socket.error` exception, or `None` if there wasn't any. + +class `GeneralProxyError` - When thrown, it indicates a problem which does not fall +into another category. + +* `Sent invalid data` - This error means that unexpected data has been received from +the server. The most common reason is that the server specified as the proxy is +not really a SOCKS4/SOCKS5/HTTP proxy, or maybe the proxy type specified is wrong. + +* `Connection closed unexpectedly` - The proxy server unexpectedly closed the connection. +This may indicate that the proxy server is experiencing network or software problems. + +* `Bad proxy type` - This will be raised if the type of the proxy supplied to the +set_proxy function was not one of `SOCKS4`/`SOCKS5`/`HTTP`. + +* `Bad input` - This will be raised if the `connect()` method is called with bad input +parameters. + +class `SOCKS5AuthError` - This indicates that the connection through a SOCKS5 server +failed due to an authentication problem. + +* `Authentication is required` - This will happen if you use a SOCKS5 server which +requires authentication without providing a username / password at all. + +* `All offered authentication methods were rejected` - This will happen if the proxy +requires a special authentication method which is not supported by this module. + +* `Unknown username or invalid password` - Self descriptive. + +class `SOCKS5Error` - This will be raised for SOCKS5 errors which are not related to +authentication. +The parameter is a tuple containing a code, as given by the server, +and a description of the +error. The possible errors, according to the RFC, are: + +* `0x01` - General SOCKS server failure - If for any reason the proxy server is unable to +fulfill your request (internal server error). +* `0x02` - connection not allowed by ruleset - If the address you're trying to connect to +is blacklisted on the server or requires authentication. +* `0x03` - Network unreachable - The target could not be contacted. A router on the network +had replied with a destination net unreachable error. +* `0x04` - Host unreachable - The target could not be contacted. A router on the network +had replied with a destination host unreachable error. +* `0x05` - Connection refused - The target server has actively refused the connection +(the requested port is closed). +* `0x06` - TTL expired - The TTL value of the SYN packet from the proxy to the target server +has expired. This usually means that there are network problems causing the packet +to be caught in a router-to-router "ping-pong". +* `0x07` - Command not supported - For instance if the server does not support UDP. +* `0x08` - Address type not supported - The client has provided an invalid address type. +When using this module, this error should not occur. + +class `SOCKS4Error` - This will be raised for SOCKS4 errors. The parameter is a tuple +containing a code and a description of the error, as given by the server. The +possible error, according to the specification are: + +* `0x5B` - Request rejected or failed - Will be raised in the event of an failure for any +reason other then the two mentioned next. +* `0x5C` - request rejected because SOCKS server cannot connect to identd on the client - +The Socks server had tried an ident lookup on your computer and has failed. In this +case you should run an identd server and/or configure your firewall to allow incoming +connections to local port 113 from the remote server. +* `0x5D` - request rejected because the client program and identd report different user-ids - +The Socks server had performed an ident lookup on your computer and has received a +different userid than the one you have provided. Change your userid (through the +username parameter of the set_proxy method) to match and try again. + +class `HTTPError` - This will be raised for HTTP errors. The message will contain +the HTTP status code and provided error message. + +After establishing the connection, the object behaves like a standard socket. + +Methods like `makefile()` and `settimeout()` should behave just like regular sockets. +Call the `close()` method to close the connection. + +In addition to the `socksocket` class, an additional function worth mentioning is the +`set_default_proxy` function. The parameters are the same as the `set_proxy` method. +This function will set default proxy settings for newly created `socksocket` objects, +in which the proxy settings haven't been changed via the `set_proxy` method. +This is quite useful if you wish to force 3rd party modules to use a SOCKS proxy, +by overriding the socket object. +For example: + + >>> socks.set_default_proxy(socks.SOCKS5, "socks.example.com") + >>> socket.socket = socks.socksocket + >>> urllib.urlopen("http://www.sourceforge.net/") + + +PROBLEMS +--------- + +Please open a GitHub issue at https://github.com/Anorov/PySocks diff --git a/libs/PySocks-1.7.1.dist-info/RECORD b/libs/PySocks-1.7.1.dist-info/RECORD new file mode 100644 index 000000000..3b5bbf3d1 --- /dev/null +++ b/libs/PySocks-1.7.1.dist-info/RECORD @@ -0,0 +1,9 @@ +PySocks-1.7.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +PySocks-1.7.1.dist-info/LICENSE,sha256=cCfiFOAU63i3rcwc7aWspxOnn8T2oMUsnaWz5wfm_-k,1401 +PySocks-1.7.1.dist-info/METADATA,sha256=RThVWnkrwm4fr1ITwGmvqqDXAYxHZG_WIoyRdQTBk4g,13237 +PySocks-1.7.1.dist-info/RECORD,, +PySocks-1.7.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +PySocks-1.7.1.dist-info/WHEEL,sha256=Xo9-1PvkuimrydujYJAjF7pCkriuXBpUPEjma1nZyJ0,92 +PySocks-1.7.1.dist-info/top_level.txt,sha256=TKSOIfCFBoK9EY8FBYbYqC3PWd3--G15ph9n8-QHPDk,19 +socks.py,sha256=xOYn27t9IGrbTBzWsUUuPa0YBuplgiUykzkOB5V5iFY,31086 +sockshandler.py,sha256=2SYGj-pwt1kjgLoZAmyeaEXCeZDWRmfVS_QG6kErGtY,3966 diff --git a/libs/PySocks-1.7.1.dist-info/REQUESTED b/libs/PySocks-1.7.1.dist-info/REQUESTED new file mode 100644 index 000000000..e69de29bb diff --git a/libs/PySocks-1.7.1.dist-info/WHEEL b/libs/PySocks-1.7.1.dist-info/WHEEL new file mode 100644 index 000000000..ba48cbcf9 --- /dev/null +++ b/libs/PySocks-1.7.1.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.41.3) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/libs/PySocks-1.7.1.dist-info/top_level.txt b/libs/PySocks-1.7.1.dist-info/top_level.txt new file mode 100644 index 000000000..9476163ae --- /dev/null +++ b/libs/PySocks-1.7.1.dist-info/top_level.txt @@ -0,0 +1,2 @@ +socks +sockshandler diff --git a/libs/PyYAML-6.0.1.dist-info/INSTALLER b/libs/PyYAML-6.0.1.dist-info/INSTALLER new file mode 100644 index 000000000..a1b589e38 --- /dev/null +++ b/libs/PyYAML-6.0.1.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/libs/PyYAML-6.0.1.dist-info/LICENSE b/libs/PyYAML-6.0.1.dist-info/LICENSE new file mode 100644 index 000000000..2f1b8e15e --- /dev/null +++ b/libs/PyYAML-6.0.1.dist-info/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2017-2021 Ingy döt Net +Copyright (c) 2006-2016 Kirill Simonov + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/libs/PyYAML-6.0.1.dist-info/METADATA b/libs/PyYAML-6.0.1.dist-info/METADATA new file mode 100644 index 000000000..c8905983e --- /dev/null +++ b/libs/PyYAML-6.0.1.dist-info/METADATA @@ -0,0 +1,46 @@ +Metadata-Version: 2.1 +Name: PyYAML +Version: 6.0.1 +Summary: YAML parser and emitter for Python +Home-page: https://pyyaml.org/ +Download-URL: https://pypi.org/project/PyYAML/ +Author: Kirill Simonov +Author-email: xi@resolvent.net +License: MIT +Project-URL: Bug Tracker, https://github.com/yaml/pyyaml/issues +Project-URL: CI, https://github.com/yaml/pyyaml/actions +Project-URL: Documentation, https://pyyaml.org/wiki/PyYAMLDocumentation +Project-URL: Mailing lists, http://lists.sourceforge.net/lists/listinfo/yaml-core +Project-URL: Source Code, https://github.com/yaml/pyyaml +Platform: Any +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: MIT License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Cython +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: PyPy +Classifier: Topic :: Software Development :: Libraries :: Python Modules +Classifier: Topic :: Text Processing :: Markup +Requires-Python: >=3.6 +License-File: LICENSE + +YAML is a data serialization format designed for human readability +and interaction with scripting languages. PyYAML is a YAML parser +and emitter for Python. + +PyYAML features a complete YAML 1.1 parser, Unicode support, pickle +support, capable extension API, and sensible error messages. PyYAML +supports standard YAML tags and provides Python-specific tags that +allow to represent an arbitrary Python object. + +PyYAML is applicable for a broad range of tasks from complex +configuration files to object serialization and persistence. diff --git a/libs/PyYAML-6.0.1.dist-info/RECORD b/libs/PyYAML-6.0.1.dist-info/RECORD new file mode 100644 index 000000000..079577cf1 --- /dev/null +++ b/libs/PyYAML-6.0.1.dist-info/RECORD @@ -0,0 +1,25 @@ +PyYAML-6.0.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +PyYAML-6.0.1.dist-info/LICENSE,sha256=jTko-dxEkP1jVwfLiOsmvXZBAqcoKVQwfT5RZ6V36KQ,1101 +PyYAML-6.0.1.dist-info/METADATA,sha256=UNNF8-SzzwOKXVo-kV5lXUGH2_wDWMBmGxqISpp5HQk,2058 +PyYAML-6.0.1.dist-info/RECORD,, +PyYAML-6.0.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +PyYAML-6.0.1.dist-info/WHEEL,sha256=UkJU7GAEhyIKVwGX1j1L5OGjUFT--MirwyCseqfCpZU,109 +PyYAML-6.0.1.dist-info/top_level.txt,sha256=rpj0IVMTisAjh_1vG3Ccf9v5jpCQwAz6cD1IVU5ZdhQ,11 +_yaml/__init__.py,sha256=04Ae_5osxahpJHa3XBZUAf4wi6XX32gR8D6X6p64GEA,1402 +yaml/__init__.py,sha256=bhl05qSeO-1ZxlSRjGrvl2m9nrXb1n9-GQatTN0Mrqc,12311 +yaml/composer.py,sha256=_Ko30Wr6eDWUeUpauUGT3Lcg9QPBnOPVlTnIMRGJ9FM,4883 +yaml/constructor.py,sha256=kNgkfaeLUkwQYY_Q6Ff1Tz2XVw_pG1xVE9Ak7z-viLA,28639 +yaml/cyaml.py,sha256=6ZrAG9fAYvdVe2FK_w0hmXoG7ZYsoYUwapG8CiC72H0,3851 +yaml/dumper.py,sha256=PLctZlYwZLp7XmeUdwRuv4nYOZ2UBnDIUy8-lKfLF-o,2837 +yaml/emitter.py,sha256=jghtaU7eFwg31bG0B7RZea_29Adi9CKmXq_QjgQpCkQ,43006 +yaml/error.py,sha256=Ah9z-toHJUbE9j-M8YpxgSRM5CgLCcwVzJgLLRF2Fxo,2533 +yaml/events.py,sha256=50_TksgQiE4up-lKo_V-nBy-tAIxkIPQxY5qDhKCeHw,2445 +yaml/loader.py,sha256=UVa-zIqmkFSCIYq_PgSGm4NSJttHY2Rf_zQ4_b1fHN0,2061 +yaml/nodes.py,sha256=gPKNj8pKCdh2d4gr3gIYINnPOaOxGhJAUiYhGRnPE84,1440 +yaml/parser.py,sha256=ilWp5vvgoHFGzvOZDItFoGjD6D42nhlZrZyjAwa0oJo,25495 +yaml/reader.py,sha256=0dmzirOiDG4Xo41RnuQS7K9rkY3xjHiVasfDMNTqCNw,6794 +yaml/representer.py,sha256=IuWP-cAW9sHKEnS0gCqSa894k1Bg4cgTxaDwIcbRQ-Y,14190 +yaml/resolver.py,sha256=9L-VYfm4mWHxUD1Vg4X7rjDRK_7VZd6b92wzq7Y2IKY,9004 +yaml/scanner.py,sha256=YEM3iLZSaQwXcQRg2l2R4MdT0zGP2F9eHkKGKnHyWQY,51279 +yaml/serializer.py,sha256=ChuFgmhU01hj4xgI8GaKv6vfM2Bujwa9i7d2FAHj7cA,4165 +yaml/tokens.py,sha256=lTQIzSVw8Mg9wv459-TjiOQe6wVziqaRlqX2_89rp54,2573 diff --git a/libs/PyYAML-6.0.1.dist-info/REQUESTED b/libs/PyYAML-6.0.1.dist-info/REQUESTED new file mode 100644 index 000000000..e69de29bb diff --git a/libs/PyYAML-6.0.1.dist-info/WHEEL b/libs/PyYAML-6.0.1.dist-info/WHEEL new file mode 100644 index 000000000..844cf17ca --- /dev/null +++ b/libs/PyYAML-6.0.1.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.42.0) +Root-Is-Purelib: false +Tag: cp38-cp38-macosx_12_0_x86_64 + diff --git a/libs/PyYAML-6.0.1.dist-info/top_level.txt b/libs/PyYAML-6.0.1.dist-info/top_level.txt new file mode 100644 index 000000000..e6475e911 --- /dev/null +++ b/libs/PyYAML-6.0.1.dist-info/top_level.txt @@ -0,0 +1,2 @@ +_yaml +yaml diff --git a/libs/SQLAlchemy-2.0.27.dist-info/INSTALLER b/libs/SQLAlchemy-2.0.27.dist-info/INSTALLER new file mode 100644 index 000000000..a1b589e38 --- /dev/null +++ b/libs/SQLAlchemy-2.0.27.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/libs/SQLAlchemy-2.0.27.dist-info/LICENSE b/libs/SQLAlchemy-2.0.27.dist-info/LICENSE new file mode 100644 index 000000000..967cdc5dc --- /dev/null +++ b/libs/SQLAlchemy-2.0.27.dist-info/LICENSE @@ -0,0 +1,19 @@ +Copyright 2005-2024 SQLAlchemy authors and contributors . + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/libs/SQLAlchemy-2.0.27.dist-info/METADATA b/libs/SQLAlchemy-2.0.27.dist-info/METADATA new file mode 100644 index 000000000..e43a4599d --- /dev/null +++ b/libs/SQLAlchemy-2.0.27.dist-info/METADATA @@ -0,0 +1,242 @@ +Metadata-Version: 2.1 +Name: SQLAlchemy +Version: 2.0.27 +Summary: Database Abstraction Library +Home-page: https://www.sqlalchemy.org +Author: Mike Bayer +Author-email: mike_mp@zzzcomputing.com +License: MIT +Project-URL: Documentation, https://docs.sqlalchemy.org +Project-URL: Issue Tracker, https://github.com/sqlalchemy/sqlalchemy/ +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: MIT License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: PyPy +Classifier: Topic :: Database :: Front-Ends +Requires-Python: >=3.7 +Description-Content-Type: text/x-rst +License-File: LICENSE +Requires-Dist: typing-extensions >=4.6.0 +Requires-Dist: greenlet !=0.4.17 ; platform_machine == "aarch64" or (platform_machine == "ppc64le" or (platform_machine == "x86_64" or (platform_machine == "amd64" or (platform_machine == "AMD64" or (platform_machine == "win32" or platform_machine == "WIN32"))))) +Requires-Dist: importlib-metadata ; python_version < "3.8" +Provides-Extra: aiomysql +Requires-Dist: greenlet !=0.4.17 ; extra == 'aiomysql' +Requires-Dist: aiomysql >=0.2.0 ; extra == 'aiomysql' +Provides-Extra: aioodbc +Requires-Dist: greenlet !=0.4.17 ; extra == 'aioodbc' +Requires-Dist: aioodbc ; extra == 'aioodbc' +Provides-Extra: aiosqlite +Requires-Dist: greenlet !=0.4.17 ; extra == 'aiosqlite' +Requires-Dist: aiosqlite ; extra == 'aiosqlite' +Requires-Dist: typing-extensions !=3.10.0.1 ; extra == 'aiosqlite' +Provides-Extra: asyncio +Requires-Dist: greenlet !=0.4.17 ; extra == 'asyncio' +Provides-Extra: asyncmy +Requires-Dist: greenlet !=0.4.17 ; extra == 'asyncmy' +Requires-Dist: asyncmy !=0.2.4,!=0.2.6,>=0.2.3 ; extra == 'asyncmy' +Provides-Extra: mariadb_connector +Requires-Dist: mariadb !=1.1.2,!=1.1.5,>=1.0.1 ; extra == 'mariadb_connector' +Provides-Extra: mssql +Requires-Dist: pyodbc ; extra == 'mssql' +Provides-Extra: mssql_pymssql +Requires-Dist: pymssql ; extra == 'mssql_pymssql' +Provides-Extra: mssql_pyodbc +Requires-Dist: pyodbc ; extra == 'mssql_pyodbc' +Provides-Extra: mypy +Requires-Dist: mypy >=0.910 ; extra == 'mypy' +Provides-Extra: mysql +Requires-Dist: mysqlclient >=1.4.0 ; extra == 'mysql' +Provides-Extra: mysql_connector +Requires-Dist: mysql-connector-python ; extra == 'mysql_connector' +Provides-Extra: oracle +Requires-Dist: cx-oracle >=8 ; extra == 'oracle' +Provides-Extra: oracle_oracledb +Requires-Dist: oracledb >=1.0.1 ; extra == 'oracle_oracledb' +Provides-Extra: postgresql +Requires-Dist: psycopg2 >=2.7 ; extra == 'postgresql' +Provides-Extra: postgresql_asyncpg +Requires-Dist: greenlet !=0.4.17 ; extra == 'postgresql_asyncpg' +Requires-Dist: asyncpg ; extra == 'postgresql_asyncpg' +Provides-Extra: postgresql_pg8000 +Requires-Dist: pg8000 >=1.29.1 ; extra == 'postgresql_pg8000' +Provides-Extra: postgresql_psycopg +Requires-Dist: psycopg >=3.0.7 ; extra == 'postgresql_psycopg' +Provides-Extra: postgresql_psycopg2binary +Requires-Dist: psycopg2-binary ; extra == 'postgresql_psycopg2binary' +Provides-Extra: postgresql_psycopg2cffi +Requires-Dist: psycopg2cffi ; extra == 'postgresql_psycopg2cffi' +Provides-Extra: postgresql_psycopgbinary +Requires-Dist: psycopg[binary] >=3.0.7 ; extra == 'postgresql_psycopgbinary' +Provides-Extra: pymysql +Requires-Dist: pymysql ; extra == 'pymysql' +Provides-Extra: sqlcipher +Requires-Dist: sqlcipher3-binary ; extra == 'sqlcipher' + +SQLAlchemy +========== + +|PyPI| |Python| |Downloads| + +.. |PyPI| image:: https://img.shields.io/pypi/v/sqlalchemy + :target: https://pypi.org/project/sqlalchemy + :alt: PyPI + +.. |Python| image:: https://img.shields.io/pypi/pyversions/sqlalchemy + :target: https://pypi.org/project/sqlalchemy + :alt: PyPI - Python Version + +.. |Downloads| image:: https://static.pepy.tech/badge/sqlalchemy/month + :target: https://pepy.tech/project/sqlalchemy + :alt: PyPI - Downloads + + +The Python SQL Toolkit and Object Relational Mapper + +Introduction +------------- + +SQLAlchemy is the Python SQL toolkit and Object Relational Mapper +that gives application developers the full power and +flexibility of SQL. SQLAlchemy provides a full suite +of well known enterprise-level persistence patterns, +designed for efficient and high-performing database +access, adapted into a simple and Pythonic domain +language. + +Major SQLAlchemy features include: + +* An industrial strength ORM, built + from the core on the identity map, unit of work, + and data mapper patterns. These patterns + allow transparent persistence of objects + using a declarative configuration system. + Domain models + can be constructed and manipulated naturally, + and changes are synchronized with the + current transaction automatically. +* A relationally-oriented query system, exposing + the full range of SQL's capabilities + explicitly, including joins, subqueries, + correlation, and most everything else, + in terms of the object model. + Writing queries with the ORM uses the same + techniques of relational composition you use + when writing SQL. While you can drop into + literal SQL at any time, it's virtually never + needed. +* A comprehensive and flexible system + of eager loading for related collections and objects. + Collections are cached within a session, + and can be loaded on individual access, all + at once using joins, or by query per collection + across the full result set. +* A Core SQL construction system and DBAPI + interaction layer. The SQLAlchemy Core is + separate from the ORM and is a full database + abstraction layer in its own right, and includes + an extensible Python-based SQL expression + language, schema metadata, connection pooling, + type coercion, and custom types. +* All primary and foreign key constraints are + assumed to be composite and natural. Surrogate + integer primary keys are of course still the + norm, but SQLAlchemy never assumes or hardcodes + to this model. +* Database introspection and generation. Database + schemas can be "reflected" in one step into + Python structures representing database metadata; + those same structures can then generate + CREATE statements right back out - all within + the Core, independent of the ORM. + +SQLAlchemy's philosophy: + +* SQL databases behave less and less like object + collections the more size and performance start to + matter; object collections behave less and less like + tables and rows the more abstraction starts to matter. + SQLAlchemy aims to accommodate both of these + principles. +* An ORM doesn't need to hide the "R". A relational + database provides rich, set-based functionality + that should be fully exposed. SQLAlchemy's + ORM provides an open-ended set of patterns + that allow a developer to construct a custom + mediation layer between a domain model and + a relational schema, turning the so-called + "object relational impedance" issue into + a distant memory. +* The developer, in all cases, makes all decisions + regarding the design, structure, and naming conventions + of both the object model as well as the relational + schema. SQLAlchemy only provides the means + to automate the execution of these decisions. +* With SQLAlchemy, there's no such thing as + "the ORM generated a bad query" - you + retain full control over the structure of + queries, including how joins are organized, + how subqueries and correlation is used, what + columns are requested. Everything SQLAlchemy + does is ultimately the result of a developer-initiated + decision. +* Don't use an ORM if the problem doesn't need one. + SQLAlchemy consists of a Core and separate ORM + component. The Core offers a full SQL expression + language that allows Pythonic construction + of SQL constructs that render directly to SQL + strings for a target database, returning + result sets that are essentially enhanced DBAPI + cursors. +* Transactions should be the norm. With SQLAlchemy's + ORM, nothing goes to permanent storage until + commit() is called. SQLAlchemy encourages applications + to create a consistent means of delineating + the start and end of a series of operations. +* Never render a literal value in a SQL statement. + Bound parameters are used to the greatest degree + possible, allowing query optimizers to cache + query plans effectively and making SQL injection + attacks a non-issue. + +Documentation +------------- + +Latest documentation is at: + +https://www.sqlalchemy.org/docs/ + +Installation / Requirements +--------------------------- + +Full documentation for installation is at +`Installation `_. + +Getting Help / Development / Bug reporting +------------------------------------------ + +Please refer to the `SQLAlchemy Community Guide `_. + +Code of Conduct +--------------- + +Above all, SQLAlchemy places great emphasis on polite, thoughtful, and +constructive communication between users and developers. +Please see our current Code of Conduct at +`Code of Conduct `_. + +License +------- + +SQLAlchemy is distributed under the `MIT license +`_. + diff --git a/libs/SQLAlchemy-2.0.27.dist-info/RECORD b/libs/SQLAlchemy-2.0.27.dist-info/RECORD new file mode 100644 index 000000000..1563d2d52 --- /dev/null +++ b/libs/SQLAlchemy-2.0.27.dist-info/RECORD @@ -0,0 +1,276 @@ +SQLAlchemy-2.0.27.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +SQLAlchemy-2.0.27.dist-info/LICENSE,sha256=PA9Zq4h9BB3mpOUv_j6e212VIt6Qn66abNettue-MpM,1100 +SQLAlchemy-2.0.27.dist-info/METADATA,sha256=fZGrNxgSqoY_vLjP6pXy7Ax_9Fvpy6P0SN_Qmjpaf8M,9602 +SQLAlchemy-2.0.27.dist-info/RECORD,, +SQLAlchemy-2.0.27.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +SQLAlchemy-2.0.27.dist-info/WHEEL,sha256=UkJU7GAEhyIKVwGX1j1L5OGjUFT--MirwyCseqfCpZU,109 +SQLAlchemy-2.0.27.dist-info/top_level.txt,sha256=rp-ZgB7D8G11ivXON5VGPjupT1voYmWqkciDt5Uaw_Q,11 +sqlalchemy/__init__.py,sha256=s94qQVe-QqqRL9xhlih382KZikm_5rCLehCmTVeMkxo,13033 +sqlalchemy/connectors/__init__.py,sha256=PzXPqZqi3BzEnrs1eW0DcsR4lyknAzhhN9rWcQ97hb4,476 +sqlalchemy/connectors/aioodbc.py,sha256=GSTiNMO9h0qjPxgqaxDwWZ8HvhWMFNVR6MJQnN1oc40,5288 +sqlalchemy/connectors/asyncio.py,sha256=6s4hDYfuMjJ9KbJ4s7bF1fp5DmcgV77ozgZ5-bwZ0wc,5955 +sqlalchemy/connectors/pyodbc.py,sha256=t7AjyxIOnaWg3CrlUEpBs4Y5l0HFdNt3P_cSSKhbi0Y,8501 +sqlalchemy/cyextension/.gitignore,sha256=_4eLZEBj5Ht5WoM-WvbiDSycdViEzM62ucOZYPQWw9c,64 +sqlalchemy/cyextension/__init__.py,sha256=GzhhN8cjMnDTE0qerlUlpbrNmFPHQWCZ4Gk74OAxl04,244 +sqlalchemy/cyextension/collections.cpython-38-darwin.so,sha256=T-PB8ecKVTCk2cZ2puq_f5XZg9d7gJEfgWTUEVD6sEo,258336 +sqlalchemy/cyextension/collections.pyx,sha256=L7DZ3DGKpgw2MT2ZZRRxCnrcyE5pU1NAFowWgAzQPEc,12571 +sqlalchemy/cyextension/immutabledict.cpython-38-darwin.so,sha256=hNhW_0eX4BbJKF-YaZoWSh8nFzoh-iWjs5pzdakZr5A,127440 +sqlalchemy/cyextension/immutabledict.pxd,sha256=3x3-rXG5eRQ7bBnktZ-OJ9-6ft8zToPmTDOd92iXpB0,291 +sqlalchemy/cyextension/immutabledict.pyx,sha256=KfDTYbTfebstE8xuqAtuXsHNAK0_b5q_ymUiinUe_xs,3535 +sqlalchemy/cyextension/processors.cpython-38-darwin.so,sha256=NF8ouwV4ffK6AnAUb4ms9VaXC_5rc32Q16_cIy3HKKg,104648 +sqlalchemy/cyextension/processors.pyx,sha256=R1rHsGLEaGeBq5VeCydjClzYlivERIJ9B-XLOJlf2MQ,1792 +sqlalchemy/cyextension/resultproxy.cpython-38-darwin.so,sha256=4UKCNsTKsvERmKn5kSDC3cT06p0t1kwjtJ1FrSslVcA,106752 +sqlalchemy/cyextension/resultproxy.pyx,sha256=eWLdyBXiBy_CLQrF5ScfWJm7X0NeelscSXedtj1zv9Q,2725 +sqlalchemy/cyextension/util.cpython-38-darwin.so,sha256=i39G3r6E1dWN3wVt0n-j_uQE074_rcD9uFxOMs5vFYY,125832 +sqlalchemy/cyextension/util.pyx,sha256=B85orxa9LddLuQEaDoVSq1XmAXIbLKxrxpvuB8ogV_o,2530 +sqlalchemy/dialects/__init__.py,sha256=Kos9Gf5JZg1Vg6GWaCqEbD6e0r1jCwCmcnJIfcxDdcY,1770 +sqlalchemy/dialects/_typing.py,sha256=hyv0nKucX2gI8ispB1IsvaUgrEPn9zEcq9hS7kfstEw,888 +sqlalchemy/dialects/mssql/__init__.py,sha256=r5t8wFRNtBQoiUWh0WfIEWzXZW6f3D0uDt6NZTW_7Cc,1880 +sqlalchemy/dialects/mssql/aioodbc.py,sha256=UQd9ecSMIML713TDnLAviuBVJle7P7i1FtqGZZePk2Y,2022 +sqlalchemy/dialects/mssql/base.py,sha256=2Tx9sC5bOd0JbweaMaqnjGOgESur7ZaaN1S66KMTwHk,133298 +sqlalchemy/dialects/mssql/information_schema.py,sha256=HswjDc6y0mPXCf_x6VyylHlBdBa4PSY6Evxmmlch700,8084 +sqlalchemy/dialects/mssql/json.py,sha256=evUACW2O62TAPq8B7QIPagz7jfc664ql9ms68JqiYzg,4816 +sqlalchemy/dialects/mssql/provision.py,sha256=RTVbgYLFAHzEnpVQDJroU8ji_10MqBTiZfyP9_-QNT4,5362 +sqlalchemy/dialects/mssql/pymssql.py,sha256=eZRLz7HGt3SdoZUjFBmA9BS43N7AhIASw7VPBPEJuG0,4038 +sqlalchemy/dialects/mssql/pyodbc.py,sha256=vwM-vBlmRwrqxOc73P0sFOrBSwn24wzc5IkEOpalbXQ,27056 +sqlalchemy/dialects/mysql/__init__.py,sha256=bxbi4hkysUK2OOVvr1F49akUj1cky27kKb07tgFzI9U,2153 +sqlalchemy/dialects/mysql/aiomysql.py,sha256=67JrSUD1BmN88k_ASk6GvrttZFQiFjDY0wBiwdllxMk,9964 +sqlalchemy/dialects/mysql/asyncmy.py,sha256=CGILIRKf_2Ut9Ng2yBlmdg62laL-ockEm6GMuN7xlKE,10033 +sqlalchemy/dialects/mysql/base.py,sha256=KA7tvRxKUw0KwHwMth2rz-NWV0xMkVbYvPoBM9wrAFw,120850 +sqlalchemy/dialects/mysql/cymysql.py,sha256=eXT1ry0w_qRxjiO24M980c-8PZ9qSsbhqBHntjEiKB0,2300 +sqlalchemy/dialects/mysql/dml.py,sha256=HXJMAvimJsqvhj3UZO4vW_6LkF5RqaKbHvklAjor7yU,7645 +sqlalchemy/dialects/mysql/enumerated.py,sha256=ipEPPQqoXfFwcywNdcLlZCEzHBtnitHRah1Gn6nItcg,8448 +sqlalchemy/dialects/mysql/expression.py,sha256=lsmQCHKwfPezUnt27d2kR6ohk4IRFCA64KBS16kx5dc,4097 +sqlalchemy/dialects/mysql/json.py,sha256=l6MEZ0qp8FgiRrIQvOMhyEJq0q6OqiEnvDTx5Cbt9uQ,2269 +sqlalchemy/dialects/mysql/mariadb.py,sha256=kTfBLioLKk4JFFst4TY_iWqPtnvvQXFHknLfm89H2N8,853 +sqlalchemy/dialects/mysql/mariadbconnector.py,sha256=VVRwKLb6GzDmitOM4wLNvmZw6RdhnIwkLl7IZfAmUy8,8734 +sqlalchemy/dialects/mysql/mysqlconnector.py,sha256=qiQdfLPze3QHuASAZ9iqRzD0hDW8FbKoQnfAEQCF7tM,5675 +sqlalchemy/dialects/mysql/mysqldb.py,sha256=9x_JiY4hj4tykG1ckuEGPyH4jCtsh4fgBhNukVnjUos,9658 +sqlalchemy/dialects/mysql/provision.py,sha256=4oGkClQ8jC3YLPF54sB4kCjFc8HRTwf5zl5zftAAXGo,3474 +sqlalchemy/dialects/mysql/pymysql.py,sha256=GUnSHd2M2uKjmN46Hheymtm26g7phEgwYOXrX0zLY8M,4083 +sqlalchemy/dialects/mysql/pyodbc.py,sha256=072crI4qVyPhajYvHnsfFeSrNjLFVPIjBQKo5uyz5yk,4297 +sqlalchemy/dialects/mysql/reflection.py,sha256=XXM8AGpaRTqDvuObg89Bzn_4h2ETG03viYBpWZJM3vc,22822 +sqlalchemy/dialects/mysql/reserved_words.py,sha256=Dm7FINIAkrKLoXmdu26SpE6V8LDCGyp734nmHV2tMd0,9154 +sqlalchemy/dialects/mysql/types.py,sha256=aPzx7hqqZ21aGwByEC-yWZUl6OpMvkbxwTqdN3OUGGI,24267 +sqlalchemy/dialects/oracle/__init__.py,sha256=p4-2gw7TT0bX_MoJXTGD4i8WHctYsK9kCRbkpzykBrc,1493 +sqlalchemy/dialects/oracle/base.py,sha256=-7b5iubFPxJyDRoLXlxj8rk8eBRN2_IdZlB2zzzrrbw,118246 +sqlalchemy/dialects/oracle/cx_oracle.py,sha256=t5yH4svVz7xoDSITF958blgZ01hbCUEWUKrAXwiCiAE,55566 +sqlalchemy/dialects/oracle/dictionary.py,sha256=7WMrbPkqo8ZdGjaEZyQr-5f2pajSOF1OTGb8P97z8-g,19519 +sqlalchemy/dialects/oracle/oracledb.py,sha256=UFcZwrrk0pWfAp_SKJZ1B5rIQHtNhOvuu73_JaSnTbI,9487 +sqlalchemy/dialects/oracle/provision.py,sha256=O9ZpF4OG6Cx4mMzLRfZwhs8dZjrJETWR402n9c7726A,8304 +sqlalchemy/dialects/oracle/types.py,sha256=QK3hJvWzKnnCe3oD3rItwEEIwcoBze8qGg7VFOvVlIk,8231 +sqlalchemy/dialects/postgresql/__init__.py,sha256=kwgzMhtZKDHD12HMGo5MtdKCnDdy6wLezDGZPOEoU3Q,3895 +sqlalchemy/dialects/postgresql/_psycopg_common.py,sha256=7TudtgsPiSB8O5kX8W8KxcNYR8t5h_UHb86b_ChL0P8,5696 +sqlalchemy/dialects/postgresql/array.py,sha256=9dJ_1WjWSBX1-MGDZtJACJ38vtRO3da7d4UId79WsnQ,13713 +sqlalchemy/dialects/postgresql/asyncpg.py,sha256=12DN8hlK-Na_bEFmQ5kXK7MRqu87ze2IMX8aDyiSddU,40183 +sqlalchemy/dialects/postgresql/base.py,sha256=ogY8rcQvT9jjYdtStxZ_nhTl8LmhB_zeJyhZIaUyMLk,176486 +sqlalchemy/dialects/postgresql/dml.py,sha256=Pc69Le6qzmUHHb1FT5zeUSD31dWm6SBgdCAGW89cs3s,11212 +sqlalchemy/dialects/postgresql/ext.py,sha256=1bZ--iNh2O9ym7l2gXZX48yP3yMO4dqb9RpYro2Mj2Q,16262 +sqlalchemy/dialects/postgresql/hstore.py,sha256=otAx-RTDfpi_tcXkMuQV0JOIXtYgevgnsikLKKOkI6U,11541 +sqlalchemy/dialects/postgresql/json.py,sha256=-ffnp85fQBOyt0Bjb7XAupmOxloUdzFZZgixUG3Wj5w,11212 +sqlalchemy/dialects/postgresql/named_types.py,sha256=SFhs9_l108errKNuAMPl761RQ2imTO9PbUAnSv-WtRg,17100 +sqlalchemy/dialects/postgresql/operators.py,sha256=NsAaWun_tL3d_be0fs9YL6T4LPKK6crnmFxxIJHgyeY,2808 +sqlalchemy/dialects/postgresql/pg8000.py,sha256=3yoekiWSF-xnaWMqG76XrYPMqerg-42TdmfsW_ivK9E,18640 +sqlalchemy/dialects/postgresql/pg_catalog.py,sha256=nAKavWTE_4cqxiDKDTdo-ivkCxxRIlzD5GO9Wl1yrG4,8884 +sqlalchemy/dialects/postgresql/provision.py,sha256=yqyx-aDFO9l2YcL9f4T5HBP_Lnt5dHsMjpuXUG8mi7A,5762 +sqlalchemy/dialects/postgresql/psycopg.py,sha256=TF53axr1EkTBAZD85JCq6wA7XTcJTzXueSz26txDbgc,22364 +sqlalchemy/dialects/postgresql/psycopg2.py,sha256=gAP3poHDUxEB6iut6sxe9PhBiOrV_iIMvnP0NUlC-Rw,31607 +sqlalchemy/dialects/postgresql/psycopg2cffi.py,sha256=M7wAYSL6Pvt-4nbfacAHGyyw4XMKJ_bQZ1tc1pBtIdg,1756 +sqlalchemy/dialects/postgresql/ranges.py,sha256=6CgV7qkxEMJ9AQsiibo_XBLJYzGh-2ZxpG83sRaesVY,32949 +sqlalchemy/dialects/postgresql/types.py,sha256=Jfxqw9JaKNOq29JRWBublywgb3lLMyzx8YZI7CXpS2s,7300 +sqlalchemy/dialects/sqlite/__init__.py,sha256=lp9DIggNn349M-7IYhUA8et8--e8FRExWD2V_r1LJk4,1182 +sqlalchemy/dialects/sqlite/aiosqlite.py,sha256=OMvxP2eWyqk5beF-sHhzxRmjzO4VCQp55q7NH2XPVTE,12305 +sqlalchemy/dialects/sqlite/base.py,sha256=lUtigjn7NdPBq831zQsLcBwdwRJqdgKM_tUaDrMElOE,96794 +sqlalchemy/dialects/sqlite/dml.py,sha256=9GE55WvwoktKy2fHeT-Wbc9xPHgsbh5oBfd_fckMH5Q,8443 +sqlalchemy/dialects/sqlite/json.py,sha256=Eoplbb_4dYlfrtmQaI8Xddd2suAIHA-IdbDQYM-LIhs,2777 +sqlalchemy/dialects/sqlite/provision.py,sha256=UCpmwxf4IWlrpb2eLHGbPTpCFVbdI_KAh2mKtjiLYao,5632 +sqlalchemy/dialects/sqlite/pysqlcipher.py,sha256=OL2S_05DK9kllZj6DOz7QtEl7jI7syxjW6woS725ii4,5356 +sqlalchemy/dialects/sqlite/pysqlite.py,sha256=TAOqsHIjhbUZOF_Qk7UooiekkVZNhYJNduxlGQjokeA,27900 +sqlalchemy/dialects/type_migration_guidelines.txt,sha256=-uHNdmYFGB7bzUNT6i8M5nb4j6j9YUKAtW4lcBZqsMg,8239 +sqlalchemy/engine/__init__.py,sha256=Stb2oV6l8w65JvqEo6J4qtKoApcmOpXy3AAxQud4C1o,2818 +sqlalchemy/engine/_py_processors.py,sha256=j9i_lcYYQOYJMcsDerPxI0sVFBIlX5sqoYMdMJlgWPI,3744 +sqlalchemy/engine/_py_row.py,sha256=wSqoUFzLOJ1f89kgDb6sJm9LUrF5LMFpXPcK1vUsKcs,3787 +sqlalchemy/engine/_py_util.py,sha256=f2DI3AN1kv6EplelowesCVpwS8hSXNufRkZoQmJtSH8,2484 +sqlalchemy/engine/base.py,sha256=NGD1iokXsJBw_6sBOpX4STo_05fQFd52qUl1YiJZsdU,122038 +sqlalchemy/engine/characteristics.py,sha256=Qbvt4CPrggJ3GfxHl0hOAxopjnCQy-W_pjtwLIe-Q1g,2590 +sqlalchemy/engine/create.py,sha256=5Me7rgLvmZVJM6QzoH8aBHz0lIratA2vXN8cW6kUgdY,32872 +sqlalchemy/engine/cursor.py,sha256=jSjpGM5DiwX1pwEHGx3wyqgHrgj8rwU5ZpVvMv5GaJs,74443 +sqlalchemy/engine/default.py,sha256=VSqSm-juosz-5WqZPWjgDQf8Fra27M-YsrVVcs7RwPU,84672 +sqlalchemy/engine/events.py,sha256=c0unNFFiHzTAvkUtXoJaxzMFMDwurBkHiiUhuN8qluc,37381 +sqlalchemy/engine/interfaces.py,sha256=gktNzgLjNK-KrYMU__Lk0h85SXQI8LCjDakkuLxagNE,112688 +sqlalchemy/engine/mock.py,sha256=yvpxgFmRw5G4QsHeF-ZwQGHKES-HqQOucTxFtN1uzdk,4179 +sqlalchemy/engine/processors.py,sha256=XyfINKbo-2fjN-mW55YybvFyQMOil50_kVqsunahkNs,2379 +sqlalchemy/engine/reflection.py,sha256=FlT5kPpKm7Lah50GNt5XcnlJWojTL3LD_x0SoCF9kfY,75127 +sqlalchemy/engine/result.py,sha256=j6BI4Wj2bziQNQG5OlG_Cm4KcNWY9AoYvTXVlJUU-D8,77603 +sqlalchemy/engine/row.py,sha256=9AAQo9zYDL88GcZ3bjcQTwMT-YIcuGTSMAyTfmBJ_yM,12032 +sqlalchemy/engine/strategies.py,sha256=DqFSWaXJPL-29Omot9O0aOcuGL8KmCGyOvnPGDkAJoE,442 +sqlalchemy/engine/url.py,sha256=8eWkUaIUyDExOcJ2D4xJXRcn4OY1GQJ3Q2duSX6UGAg,30784 +sqlalchemy/engine/util.py,sha256=hkEql1t19WHl6uzR55-F-Fs_VMCJ7p02KKQVNUDSXTk,5667 +sqlalchemy/event/__init__.py,sha256=KBrp622xojnC3FFquxa2JsMamwAbfkvzfv6Op0NKiYc,997 +sqlalchemy/event/api.py,sha256=BUTAZjSlzvq4Hn2v2pihP_P1yo3lvCVDczK8lV_XJ80,8227 +sqlalchemy/event/attr.py,sha256=X8QeHGK4ioSYht1vkhc11f606_mq_t91jMNIT314ubs,20751 +sqlalchemy/event/base.py,sha256=3n9FmUkcXYHHyGzfpjKDsrIUVCNST_hq4zOtrNm0_a4,14954 +sqlalchemy/event/legacy.py,sha256=teMPs00fO-4g8a_z2omcVKkYce5wj_1uvJO2n2MIeuo,8227 +sqlalchemy/event/registry.py,sha256=nfTSSyhjZZXc5wseWB4sXn-YibSc0LKX8mg17XlWmAo,10835 +sqlalchemy/events.py,sha256=k-ZD38aSPD29LYhED7CBqttp5MDVVx_YSaWC2-cu9ec,525 +sqlalchemy/exc.py,sha256=M_8-O1hd8i6gbyx-TapV400p_Lxq2QqTGMXUAO-YgCc,23976 +sqlalchemy/ext/__init__.py,sha256=S1fGKAbycnQDV01gs-JWGaFQ9GCD4QHwKcU2wnugg_o,322 +sqlalchemy/ext/associationproxy.py,sha256=5O5ANHARO8jytvqBQmOu-QjNVE4Hh3tfYquqKAj5ajs,65771 +sqlalchemy/ext/asyncio/__init__.py,sha256=1OqSxEyIUn7RWLGyO12F-jAUIvk1I6DXlVy80-Gvkds,1317 +sqlalchemy/ext/asyncio/base.py,sha256=fl7wxZD9KjgFiCtG3WXrYjHEvanamcsodCqq9pH9lOk,8905 +sqlalchemy/ext/asyncio/engine.py,sha256=vQRdpBnGuyzyG48ZssDZvFlcS6Y6ZXUYI0GEOQqdDxk,47941 +sqlalchemy/ext/asyncio/exc.py,sha256=8sII7VMXzs2TrhizhFQMzSfcroRtiesq8o3UwLfXSgQ,639 +sqlalchemy/ext/asyncio/result.py,sha256=ID2eh-NHW-lnNFTxbKhje8fr-tnsucUsiw_jcpGcSPc,30409 +sqlalchemy/ext/asyncio/scoping.py,sha256=BmE1UbFV_C4BXB4WngJc523DeMH-nTchNb8ORiSPYfE,52597 +sqlalchemy/ext/asyncio/session.py,sha256=Zhkrwwc4rqZJntUpzbgruQNgpuOwaRmjrBQb8ol19z0,62894 +sqlalchemy/ext/automap.py,sha256=hBlKAfZn2fgAAQh7vh4f2kClbb5ryOgV59tzVHEObQM,61389 +sqlalchemy/ext/baked.py,sha256=H6T1il7GY84BhzPFj49UECSpZh_eBuiHomA-QIsYOYQ,17807 +sqlalchemy/ext/compiler.py,sha256=ONPoxoKD2yUS9R2-oOhmPsA7efm-Bs0BXo7HE1dGlsU,20391 +sqlalchemy/ext/declarative/__init__.py,sha256=20psLdFQbbOWfpdXHZ0CTY6I1k4UqXvKemNVu1LvPOI,1818 +sqlalchemy/ext/declarative/extensions.py,sha256=uCjN1GisQt54AjqYnKYzJdUjnGd2pZBW47WWdPlS7FE,19547 +sqlalchemy/ext/horizontal_shard.py,sha256=wuwAPnHymln0unSBnyx-cpX0AfESKSsypaSQTYCvzDk,16750 +sqlalchemy/ext/hybrid.py,sha256=LXph2NOtBQj6rZMi5ar-WCxkY7qaFp-o-UFIvCy-ep0,52432 +sqlalchemy/ext/indexable.py,sha256=UkTelbydKCdKelzbv3HWFFavoET9WocKaGRPGEOVfN8,11032 +sqlalchemy/ext/instrumentation.py,sha256=sg8ghDjdHSODFXh_jAmpgemnNX1rxCeeXEG3-PMdrNk,15707 +sqlalchemy/ext/mutable.py,sha256=L5ZkHBGYhMaqO75Xtyrk2DBR44RDk0g6Rz2HzHH0F8Q,37355 +sqlalchemy/ext/mypy/__init__.py,sha256=0WebDIZmqBD0OTq5JLtd_PmfF9JGxe4d4Qv3Ml3PKUg,241 +sqlalchemy/ext/mypy/apply.py,sha256=Aek_-XA1eXihT4attxhfE43yBKtCgsxBSb--qgZKUqc,10550 +sqlalchemy/ext/mypy/decl_class.py,sha256=1vVJRII2apnLTUbc5HkJS6Z2GueaUv_eKvhbqh7Wik4,17384 +sqlalchemy/ext/mypy/infer.py,sha256=KVnmLFEVS33Al8pUKI7MJbJQu3KeveBUMl78EluBORw,19369 +sqlalchemy/ext/mypy/names.py,sha256=IQ16GLZFqKxfYxIZxkbTurBqOUYbUV-64V_DSRns1tc,10630 +sqlalchemy/ext/mypy/plugin.py,sha256=74ML8LI9xar0V86oCxnPFv5FQGEEfUzK64vOay4BKFs,9750 +sqlalchemy/ext/mypy/util.py,sha256=1zuDQG8ezmF-XhJmAQU_lcBHiD--sL-lq20clg8t4lE,9448 +sqlalchemy/ext/orderinglist.py,sha256=TGYbsGH72wEZcFNQDYDsZg9OSPuzf__P8YX8_2HtYUo,14384 +sqlalchemy/ext/serializer.py,sha256=YemanWdeMVUDweHCnQc-iMO6mVVXNo2qQ5NK0Eb2_Es,6178 +sqlalchemy/future/__init__.py,sha256=q2mw-gxk_xoxJLEvRoyMha3vO1xSRHrslcExOHZwmPA,512 +sqlalchemy/future/engine.py,sha256=AgIw6vMsef8W6tynOTkxsjd6o_OQDwGjLdbpoMD8ue8,495 +sqlalchemy/inspection.py,sha256=MF-LE358wZDUEl1IH8-Uwt2HI65EsQpQW5o5udHkZwA,5063 +sqlalchemy/log.py,sha256=8x9UR3nj0uFm6or6bQF-JWb4fYv2zOeQjG_w-0wOJFA,8607 +sqlalchemy/orm/__init__.py,sha256=ZYys5nL3RFUDCMOLFDBrRI52F6er3S1U1OY9TeORuKs,8463 +sqlalchemy/orm/_orm_constructors.py,sha256=VWY_MotbcQlECGx2uwEu3IcRkZ4RgLM_ufPad3IA9ZM,99354 +sqlalchemy/orm/_typing.py,sha256=DVBfpHmDVK4x1zxaGJPY2GoTrAsyR6uexv20Lzf1afc,4973 +sqlalchemy/orm/attributes.py,sha256=-IGg2RFjOPwAEr-boohvlZfitz7OtaXz1v8-7uG8ekw,92520 +sqlalchemy/orm/base.py,sha256=HhuarpRU-BpKSHE1LeeBaG8CpdkwwLrvTkUVRK-ofjg,27424 +sqlalchemy/orm/bulk_persistence.py,sha256=SSSR0Omv8A8BzpsOdSo4x75XICoqGpO1sUkyEWUVGso,70022 +sqlalchemy/orm/clsregistry.py,sha256=29LyYiuj0qbebOpgW6DbBPNB2ikTweFQar1byCst7I0,17958 +sqlalchemy/orm/collections.py,sha256=jpMsJGVixmrW9kfT8wevm9kpatKRqyDLcqWd7CjKPxE,52179 +sqlalchemy/orm/context.py,sha256=Wjx0d1Rkxd-wsX1mP2V2_4VbOxdNY6S_HijdXJ-TtKg,112001 +sqlalchemy/orm/decl_api.py,sha256=0gCZWM2sOXb_4OzUXfevVUisZWOUrErQTAHyaSQQL5k,63674 +sqlalchemy/orm/decl_base.py,sha256=Tq6I3Jm3bkM01mmoiHfdFXLE94YDk1ik2u2dXL1RxLc,81601 +sqlalchemy/orm/dependency.py,sha256=hgjksUWhgbmgHK5GdJdiDCBgDAIGQXIrY-Tj79tbL2k,47631 +sqlalchemy/orm/descriptor_props.py,sha256=pKtpP7H1LB_YuHRVrEYpfFZybEnUUdPwQXxduYFe2hA,37180 +sqlalchemy/orm/dynamic.py,sha256=jksBDCOsm6EBMVParcNGuMeaAv12hX4IzouKspC-HPA,9786 +sqlalchemy/orm/evaluator.py,sha256=q292K5vdpP69G7Z9y1RqI5GFAk2diUPwnsXE8De_Wgw,11925 +sqlalchemy/orm/events.py,sha256=_Ttun_bCSGgvsfg-VzAeEcsGpacf8p4c5z12JkSQkjM,127697 +sqlalchemy/orm/exc.py,sha256=w7MZkJMGGlu5J6jOFSmi9XXzc02ctnTv34jrEWpI-eM,7356 +sqlalchemy/orm/identity.py,sha256=jHdCxCpCyda_8mFOfGmN_Pr0XZdKiU-2hFZshlNxbHs,9249 +sqlalchemy/orm/instrumentation.py,sha256=M-kZmkUvHUxtf-0mCA8RIM5QmMH1hWlYR_pKMwaidjA,24321 +sqlalchemy/orm/interfaces.py,sha256=1yyppjMHcP5NPXjxfOlSeFNmc-3_T_o2upeF3KFZtc0,48378 +sqlalchemy/orm/loading.py,sha256=JN2zLnPjNnk7K9DERbyerxESCXin7m7X1XP0gfdWLOE,57537 +sqlalchemy/orm/mapped_collection.py,sha256=3cneB1dfPTLrsTvKoo9_oCY2xtq4UAHfe5WSXPyqIS4,19690 +sqlalchemy/orm/mapper.py,sha256=8SVHr7tO-DDNpNGi68usc7PLQ7mTwzkZNEJu1aMb6dQ,171059 +sqlalchemy/orm/path_registry.py,sha256=bIXllBRevK7Ic5irajYnZgl2iazJ0rKNRkhXJSlfxjY,25850 +sqlalchemy/orm/persistence.py,sha256=dzyB2JOXNwQgaCbN8kh0sEz00WFePr48qf8NWVCUZH8,61701 +sqlalchemy/orm/properties.py,sha256=81I-PIF7f7bB0qdH4BCYMWzCzRpe57yUiEIPv2tzBoA,29127 +sqlalchemy/orm/query.py,sha256=UVNWn_Rq4a8agh5UUWNeu0DJQtPceCWVpXx1-uW7A4E,117555 +sqlalchemy/orm/relationships.py,sha256=DqD3WBKpeVQ59ldh6eCxar_sIToA3tc2-bJPtp3zfpM,127709 +sqlalchemy/orm/scoping.py,sha256=gFYywLeMmd5qjFdVPzeuCX727mTaChrCv8aqn4wPke0,78677 +sqlalchemy/orm/session.py,sha256=yiKyoJBARQj4I1ZBjsIxc6ecCpt2Upjvlxruo2A5HRc,193181 +sqlalchemy/orm/state.py,sha256=mW2f1hMSNeTJ89foutOE1EqLLD6DZkrSeO-pgagZweg,37520 +sqlalchemy/orm/state_changes.py,sha256=qKYg7NxwrDkuUY3EPygAztym6oAVUFcP2wXn7QD3Mz4,6815 +sqlalchemy/orm/strategies.py,sha256=OtmMtWpCDk4ZiaM_ipzGn80sPOi6Opwj3Co4lUHpd_w,114206 +sqlalchemy/orm/strategy_options.py,sha256=RbFl-79Lrh8XIVUnZFmQ5GVvR586SG_szs3prw5DZLQ,84204 +sqlalchemy/orm/sync.py,sha256=g7iZfSge1HgxMk9SKRgUgtHEbpbZ1kP_CBqOIdTOXqc,5779 +sqlalchemy/orm/unitofwork.py,sha256=fiVaqcymbDDHRa1NjS90N9Z466nd5pkJOEi1dHO6QLY,27033 +sqlalchemy/orm/util.py,sha256=MSLKAWZNw3FzFTH094xpfrhoA2Ov5pJQinK_dU_M0zo,80351 +sqlalchemy/orm/writeonly.py,sha256=SYu2sAaHZONk2pW4PmtE871LG-O0P_bjidvKzY1H_zI,22305 +sqlalchemy/pool/__init__.py,sha256=qiDdq4r4FFAoDrK6ncugF_i6usi_X1LeJt-CuBHey0s,1804 +sqlalchemy/pool/base.py,sha256=WF4az4ZKuzQGuKeSJeyexaYjmWZUvYdC6KIi8zTGodw,52236 +sqlalchemy/pool/events.py,sha256=xGjkIUZl490ZDtCHqnQF9ZCwe2Jv93eGXmnQxftB11E,13147 +sqlalchemy/pool/impl.py,sha256=2k2YMY9AepEoqGD_ClP_sUodSoa6atkt3GLPPWI49i4,17717 +sqlalchemy/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sqlalchemy/schema.py,sha256=dKiWmgHYjcKQ4TiiD6vD0UMmIsD8u0Fsor1M9AAeGUs,3194 +sqlalchemy/sql/__init__.py,sha256=UNa9EUiYWoPayf-FzNcwVgQvpsBdInPZfpJesAStN9o,5820 +sqlalchemy/sql/_dml_constructors.py,sha256=YdBJex0MCVACv4q2nl_ii3uhxzwU6aDB8zAsratX5UQ,3867 +sqlalchemy/sql/_elements_constructors.py,sha256=rrxq5Nzyo7GbLaGxb8VRZxko2nW0z7SYEli8ArfMIXI,62550 +sqlalchemy/sql/_orm_types.py,sha256=T-vjcry4C1y0GToFKVxQCnmly_-Zsq4IO4SHN6bvUF4,625 +sqlalchemy/sql/_py_util.py,sha256=hiM9ePbRSGs60bAMxPFuJCIC_p9SQ1VzqXGiPchiYwE,2173 +sqlalchemy/sql/_selectable_constructors.py,sha256=wjE6HrLm9cR7bxvZXT8sFLUqT6t_J9G1XyQCnYmBDl0,18780 +sqlalchemy/sql/_typing.py,sha256=Z3tBzapYRP0sKL7IwqnzPE9b8Bbq9vQtc4iV9tvxDoU,12494 +sqlalchemy/sql/annotation.py,sha256=aqbbVz9kfbCT3_66CZ9GEirVN197Cukoqt8rq48FgkQ,18245 +sqlalchemy/sql/base.py,sha256=2MVQnIL0b8xuzp1Fcv0NAye6h_OcgYpsUgLB4sy8Ahk,73756 +sqlalchemy/sql/cache_key.py,sha256=sJhAA-2jovIteeIU7mw9hSL1liPP9Ez89CZZJFC3h6E,33548 +sqlalchemy/sql/coercions.py,sha256=1xzN_9U5BCJGgokdc5iYj5o2cMAfEEZkr1Oa9Q-JYj8,40493 +sqlalchemy/sql/compiler.py,sha256=aDD100xmz8WpBq8oe7PJ5ar8lk9emd54gEE5K2Hr76g,271187 +sqlalchemy/sql/crud.py,sha256=g9xcol2KRGjZi1qsb2-bVz8zgVy_53gfMtJcnNO2vyQ,56521 +sqlalchemy/sql/ddl.py,sha256=CIqMilCKfuQnF0lrZsQdTxgrbXqcTauKr0Ojzj77PFQ,45602 +sqlalchemy/sql/default_comparator.py,sha256=utXWsZVGEjflhFfCT4ywa6RnhORc1Rryo87Hga71Rps,16707 +sqlalchemy/sql/dml.py,sha256=pn0Lm1ofC5qVZzwGWFW73lPCiNba8OsTeemurJgwRyg,65614 +sqlalchemy/sql/elements.py,sha256=kGRUilpx-rr6TTZgZpC5b71OnxxmCgDJRF2fYjDtxh8,172025 +sqlalchemy/sql/events.py,sha256=iC_Q1Htm1Aobt5tOYxWfHHqNpoytrULORmUKcusH_-E,18290 +sqlalchemy/sql/expression.py,sha256=VMX-dLpsZYnVRJpYNDozDUgaj7iQ0HuewUKVefD57PE,7586 +sqlalchemy/sql/functions.py,sha256=MjXK0IVv45Y4n96_TMDZGJ7fwAhGHPRbFP8hOJgaplQ,63689 +sqlalchemy/sql/lambdas.py,sha256=6P__bsWsFnrD7M18FPiBXI0L0OdWZOEV25FAijT4bwo,49289 +sqlalchemy/sql/naming.py,sha256=ZHs1qSV3ou8TYmZ92uvU3sfdklUQlIz4uhe330n05SU,6858 +sqlalchemy/sql/operators.py,sha256=r4oQp4h5zTMFFOpiFNV56joIK-QIjJCobatsmaZ-724,75935 +sqlalchemy/sql/roles.py,sha256=pOsVn_OZD7mF2gJByHf24Rjopt0_Hu3dUCEOK5t4KS8,7662 +sqlalchemy/sql/schema.py,sha256=WOIBaDVdg-zahrP95CPYgY4--3OQN56DH6xm28JDF-Y,228262 +sqlalchemy/sql/selectable.py,sha256=7lxe79hZvnHyzHe1DobodI1lZ1eo8quSLZ6phw10Zj4,232848 +sqlalchemy/sql/sqltypes.py,sha256=UV46KTkgxSin48oPckPOqk3Gx0tZT1l60qXwk7SbKlo,127101 +sqlalchemy/sql/traversals.py,sha256=NFgJrVJzInO3HrnG90CklxrDXhFydZohPs2vRJkh3Bo,33589 +sqlalchemy/sql/type_api.py,sha256=5DzdVquCJomFfpfMyLYbCb66PWxjxbSRdjh6UYB1Yv4,83841 +sqlalchemy/sql/util.py,sha256=qGHQF-tPCj-m1FBerzT7weCanGcXU7dK5m-W7NHio-4,48077 +sqlalchemy/sql/visitors.py,sha256=71wdVvhhZL4nJvVwFAs6ssaW-qZgNRSmKjpAcOzF_TA,36317 +sqlalchemy/testing/__init__.py,sha256=VsrEHrORpAF5n7Vfl43YQgABh6EP1xBx_gHxs7pSXeE,3126 +sqlalchemy/testing/assertions.py,sha256=gL0rA7CCZJbcVgvWOPV91tTZTRwQc1_Ta0-ykBn83Ew,31439 +sqlalchemy/testing/assertsql.py,sha256=IgQG7l94WaiRP8nTbilJh1ZHZl125g7GPq-S5kmQZN0,16817 +sqlalchemy/testing/asyncio.py,sha256=fkdRz-E37d5OrQKw5hdjmglOTJyXGnJzaJpvNXOBLxg,3728 +sqlalchemy/testing/config.py,sha256=AqyH1qub_gDqX0BvlL-JBQe7N-t2wo8655FtwblUNOY,12090 +sqlalchemy/testing/engines.py,sha256=UnH-8--3zLlYz4IbbCPwC375Za_DC61Spz-oKulbs9Q,13347 +sqlalchemy/testing/entities.py,sha256=IphFegPKbff3Un47jY6bi7_MQXy6qkx_50jX2tHZJR4,3354 +sqlalchemy/testing/exclusions.py,sha256=T8B01hmm8WVs-EKcUOQRzabahPqblWJfOidi6bHJ6GA,12460 +sqlalchemy/testing/fixtures/__init__.py,sha256=dMClrIoxqlYIFpk2ia4RZpkbfxsS_3EBigr9QsPJ66g,1198 +sqlalchemy/testing/fixtures/base.py,sha256=9r_J2ksiTzClpUxW0TczICHrWR7Ny8PV8IsBz6TsGFI,12256 +sqlalchemy/testing/fixtures/mypy.py,sha256=gdxiwNFIzDlNGSOdvM3gbwDceVCC9t8oM5kKbwyhGBk,11973 +sqlalchemy/testing/fixtures/orm.py,sha256=8EFbnaBbXX_Bf4FcCzBUaAHgyVpsLGBHX16SGLqE3Fg,6095 +sqlalchemy/testing/fixtures/sql.py,sha256=MFOuYBUyPIpHJzjRCHL9vU-IT4bD6LXGGMvsp0v1FY8,15704 +sqlalchemy/testing/pickleable.py,sha256=U9mIqk-zaxq9Xfy7HErP7UrKgTov-A3QFnhZh-NiOjI,2833 +sqlalchemy/testing/plugin/__init__.py,sha256=79F--BIY_NTBzVRIlJGgAY5LNJJ3cD19XvrAo4X0W9A,247 +sqlalchemy/testing/plugin/bootstrap.py,sha256=oYScMbEW4pCnWlPEAq1insFruCXFQeEVBwo__i4McpU,1685 +sqlalchemy/testing/plugin/plugin_base.py,sha256=BgNzWNEmgpK4CwhyblQQKnH-7FDKVi_Uul5vw8fFjBU,21578 +sqlalchemy/testing/plugin/pytestplugin.py,sha256=Jtj073ArTcAmetv81sHmrUhlH0SblcSK4wyN8S4hmvo,27554 +sqlalchemy/testing/profiling.py,sha256=PbuPhRFbauFilUONeY3tV_Y_5lBkD7iCa8VVyH2Sk9Y,10148 +sqlalchemy/testing/provision.py,sha256=zXsw2D2Xpmw_chmYLsE1GXQqKQ-so3V8xU_joTcKan0,14619 +sqlalchemy/testing/requirements.py,sha256=N9pSj7z2wVMkBif-DQfPVa_cl9k6p9g_J5FY1OsWtrY,51817 +sqlalchemy/testing/schema.py,sha256=lr4GkGrGwagaHMuSGzWdzkMaj3HnS7dgfLLWfxt__-U,6513 +sqlalchemy/testing/suite/__init__.py,sha256=Y5DRNG0Yl1u3ypt9zVF0Z9suPZeuO_UQGLl-wRgvTjU,722 +sqlalchemy/testing/suite/test_cte.py,sha256=6zBC3W2OwX1Xs-HedzchcKN2S7EaLNkgkvV_JSZ_Pq0,6451 +sqlalchemy/testing/suite/test_ddl.py,sha256=1Npkf0C_4UNxphthAGjG078n0vPEgnSIHpDu5MfokxQ,12031 +sqlalchemy/testing/suite/test_deprecations.py,sha256=BcJxZTcjYqeOAENVElCg3hVvU6fkGEW3KGBMfnW8bng,5337 +sqlalchemy/testing/suite/test_dialect.py,sha256=EH4ZQWbnGdtjmx5amZtTyhYmrkXJCvW1SQoLahoE7uk,22923 +sqlalchemy/testing/suite/test_insert.py,sha256=9azifj6-OCD7s8h_tAO1uPw100ibQv8YoKc_VA3hn3c,18824 +sqlalchemy/testing/suite/test_reflection.py,sha256=tJSbJFg5fw0sSUv3I_FPmhN7rWWeJtq3YyxmylWJUlM,106466 +sqlalchemy/testing/suite/test_results.py,sha256=NQ23m8FDVd0ub751jN4PswGoAhk5nrqvjHvpYULZXnc,15937 +sqlalchemy/testing/suite/test_rowcount.py,sha256=3KDTlRgjpQ1OVfp__1cv8Hvq4CsDKzmrhJQ_WIJWoJg,7900 +sqlalchemy/testing/suite/test_select.py,sha256=FvMFYQW9IJpDWGYZiJk46is6YrtmdSghBdTjZCG8T0Y,58574 +sqlalchemy/testing/suite/test_sequence.py,sha256=66bCoy4xo99GBSaX6Hxb88foANAykLGRz1YEKbvpfuA,9923 +sqlalchemy/testing/suite/test_types.py,sha256=rFmTOg6XuMch9L2-XthfLJRCTTwpZbMfrNss2g09gmc,65677 +sqlalchemy/testing/suite/test_unicode_ddl.py,sha256=c3_eIxLyORuSOhNDP0jWKxPyUf3SwMFpdalxtquwqlM,6141 +sqlalchemy/testing/suite/test_update_delete.py,sha256=yTiM2unnfOK9rK8ZkqeTTU_MkT-RsKFLmdYliniZfAY,3994 +sqlalchemy/testing/util.py,sha256=BFiSp3CEX95Dr-vv4l_7ZRu5vjZi9hjjnp-JKNfuS5E,14080 +sqlalchemy/testing/warnings.py,sha256=fJ-QJUY2zY2PPxZJKv9medW-BKKbCNbA4Ns_V3YwFXM,1546 +sqlalchemy/types.py,sha256=cQFM-hFRmaf1GErun1qqgEs6QxufvzMuwKqj9tuMPpE,3168 +sqlalchemy/util/__init__.py,sha256=B3bedg-LSQEscwqgmYYU-VENUX8_zAE3q9vb7tkfJNY,8277 +sqlalchemy/util/_collections.py,sha256=NE9dGJo8UNXIMbY3l3k8AO9BdPW04DlKTYraKCinchI,20063 +sqlalchemy/util/_concurrency_py3k.py,sha256=v8VVoBfFvFHe4j8mMkVLfdUrTbV897p8RWGAm73Ue9U,8574 +sqlalchemy/util/_has_cy.py,sha256=wCQmeSjT3jaH_oxfCEtGk-1g0gbSpt5MCK5UcWdMWqk,1247 +sqlalchemy/util/_py_collections.py,sha256=U6L5AoyLdgSv7cdqB4xxQbw1rpeJjyOZVXffgxgga8I,16714 +sqlalchemy/util/compat.py,sha256=R6bpBydldtbr6h7oJePihQxFb7jKiI-YDsK465MSOzk,8714 +sqlalchemy/util/concurrency.py,sha256=mhwHm0utriD14DRqxTBWgIW7QuwdSEiLgLiJdUjiR3w,2427 +sqlalchemy/util/deprecations.py,sha256=YBwvvYhSB8LhasIZRKvg_-WNoVhPUcaYI1ZrnjDn868,11971 +sqlalchemy/util/langhelpers.py,sha256=khoFN05HjHiWY9ddeehCYxYG2u8LDzuiIKLOGLSAihU,64905 +sqlalchemy/util/preloaded.py,sha256=az7NmLJLsqs0mtM9uBkIu10-841RYDq8wOyqJ7xXvqE,5904 +sqlalchemy/util/queue.py,sha256=CaeSEaYZ57YwtmLdNdOIjT5PK_LCuwMFiO0mpp39ybM,10185 +sqlalchemy/util/tool_support.py,sha256=9braZyidaiNrZVsWtGmkSmus50-byhuYrlAqvhjcmnA,6135 +sqlalchemy/util/topological.py,sha256=N3M3Le7KzGHCmqPGg0ZBqixTDGwmFLhOZvBtc4rHL_g,3458 +sqlalchemy/util/typing.py,sha256=FqH6WjV3p-8rz68YaXktpiZrPu3kmug2-gktJxBQSnI,16641 diff --git a/libs/SQLAlchemy-2.0.27.dist-info/REQUESTED b/libs/SQLAlchemy-2.0.27.dist-info/REQUESTED new file mode 100644 index 000000000..e69de29bb diff --git a/libs/SQLAlchemy-2.0.27.dist-info/WHEEL b/libs/SQLAlchemy-2.0.27.dist-info/WHEEL new file mode 100644 index 000000000..844cf17ca --- /dev/null +++ b/libs/SQLAlchemy-2.0.27.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.42.0) +Root-Is-Purelib: false +Tag: cp38-cp38-macosx_12_0_x86_64 + diff --git a/libs/SQLAlchemy-2.0.27.dist-info/top_level.txt b/libs/SQLAlchemy-2.0.27.dist-info/top_level.txt new file mode 100644 index 000000000..39fb2befb --- /dev/null +++ b/libs/SQLAlchemy-2.0.27.dist-info/top_level.txt @@ -0,0 +1 @@ +sqlalchemy diff --git a/libs/Unidecode-1.3.8.dist-info/INSTALLER b/libs/Unidecode-1.3.8.dist-info/INSTALLER new file mode 100644 index 000000000..a1b589e38 --- /dev/null +++ b/libs/Unidecode-1.3.8.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/libs/Unidecode-1.3.8.dist-info/LICENSE b/libs/Unidecode-1.3.8.dist-info/LICENSE new file mode 100644 index 000000000..d159169d1 --- /dev/null +++ b/libs/Unidecode-1.3.8.dist-info/LICENSE @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/libs/Unidecode-1.3.8.dist-info/METADATA b/libs/Unidecode-1.3.8.dist-info/METADATA new file mode 100644 index 000000000..5c7086b9e --- /dev/null +++ b/libs/Unidecode-1.3.8.dist-info/METADATA @@ -0,0 +1,310 @@ +Metadata-Version: 2.1 +Name: Unidecode +Version: 1.3.8 +Summary: ASCII transliterations of Unicode text +Author: Tomaz Solc +Author-email: tomaz.solc@tablix.org +License: GPL +Classifier: License :: OSI Approved :: GNU General Public License v2 or later (GPLv2+) +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.5 +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: PyPy +Classifier: Topic :: Text Processing +Classifier: Topic :: Text Processing :: Filters +Requires-Python: >=3.5 +License-File: LICENSE + +Unidecode, lossy ASCII transliterations of Unicode text +======================================================= + +It often happens that you have text data in Unicode, but you need to +represent it in ASCII. For example when integrating with legacy code that +doesn't support Unicode, or for ease of entry of non-Roman names on a US +keyboard, or when constructing ASCII machine identifiers from human-readable +Unicode strings that should still be somewhat intelligible. A popular example +of this is when making an URL slug from an article title. + +**Unidecode is not a replacement for fully supporting Unicode for strings in +your program. There are a number of caveats that come with its use, +especially when its output is directly visible to users. Please read the rest +of this README before using Unidecode in your project.** + +In most of examples listed above you could represent Unicode characters as +``???`` or ``\\15BA\\15A0\\1610``, to mention two extreme cases. But that's +nearly useless to someone who actually wants to read what the text says. + +What Unidecode provides is a middle road: the function ``unidecode()`` takes +Unicode data and tries to represent it in ASCII characters (i.e., the +universally displayable characters between 0x00 and 0x7F), where the +compromises taken when mapping between two character sets are chosen to be +near what a human with a US keyboard would choose. + +The quality of resulting ASCII representation varies. For languages of +western origin it should be between perfect and good. On the other hand +transliteration (i.e., conveying, in Roman letters, the pronunciation +expressed by the text in some other writing system) of languages like +Chinese, Japanese or Korean is a very complex issue and this library does +not even attempt to address it. It draws the line at context-free +character-by-character mapping. So a good rule of thumb is that the further +the script you are transliterating is from Latin alphabet, the worse the +transliteration will be. + +Generally Unidecode produces better results than simply stripping accents from +characters (which can be done in Python with built-in functions). It is based +on hand-tuned character mappings that for example also contain ASCII +approximations for symbols and non-Latin alphabets. + +**Note that some people might find certain transliterations offending.** Most +common examples include characters that are used in multiple languages. A user +expects a character to be transliterated in their language but Unidecode uses a +transliteration for a different language. It's best to not use Unidecode for +strings that are directly visible to users of your application. See also the +*Frequently Asked Questions* section for more info on common problems. + +This is a Python port of ``Text::Unidecode`` Perl module by Sean M. Burke +. + + +Module content +-------------- + +This library contains a function that takes a string object, possibly +containing non-ASCII characters, and returns a string that can be safely +encoded to ASCII:: + + >>> from unidecode import unidecode + >>> unidecode('kožušček') + 'kozuscek' + >>> unidecode('30 \U0001d5c4\U0001d5c6/\U0001d5c1') + '30 km/h' + >>> unidecode('\u5317\u4EB0') + 'Bei Jing ' + +You can also specify an *errors* argument to ``unidecode()`` that determines +what Unidecode does with characters that are not present in its transliteration +tables. The default is ``'ignore'`` meaning that Unidecode will ignore those +characters (replace them with an empty string). ``'strict'`` will raise a +``UnidecodeError``. The exception object will contain an *index* attribute that +can be used to find the offending character. ``'replace'`` will replace them +with ``'?'`` (or another string, specified in the *replace_str* argument). +``'preserve'`` will keep the original, non-ASCII character in the string. Note +that if ``'preserve'`` is used the string returned by ``unidecode()`` will not +be ASCII-encodable!:: + + >>> unidecode('\ue000') # unidecode does not have replacements for Private Use Area characters + '' + >>> unidecode('\ue000', errors='strict') + Traceback (most recent call last): + ... + unidecode.UnidecodeError: no replacement found for character '\ue000' in position 0 + +A utility is also included that allows you to transliterate text from the +command line in several ways. Reading from standard input:: + + $ echo hello | unidecode + hello + +from a command line argument:: + + $ unidecode -c hello + hello + +or from a file:: + + $ unidecode hello.txt + hello + +The default encoding used by the utility depends on your system locale. You can +specify another encoding with the ``-e`` argument. See ``unidecode --help`` for +a full list of available options. + +Requirements +------------ + +Nothing except Python itself. Unidecode supports Python 3.5 or later. + +You need a Python build with "wide" Unicode characters (also called "UCS-4 +build") in order for Unidecode to work correctly with characters outside of +Basic Multilingual Plane (BMP). Common characters outside BMP are bold, italic, +script, etc. variants of the Latin alphabet intended for mathematical notation. +Surrogate pair encoding of "narrow" builds is not supported in Unidecode. + +If your Python build supports "wide" Unicode the following expression will +return True:: + + >>> import sys + >>> sys.maxunicode > 0xffff + True + +See `PEP 261 `_ for details +regarding support for "wide" Unicode characters in Python. + + +Installation +------------ + +To install the latest version of Unidecode from the Python package index, use +these commands:: + + $ pip install unidecode + +To install Unidecode from the source distribution and run unit tests, use:: + + $ python setup.py install + $ python setup.py test + +Frequently asked questions +-------------------------- + +German umlauts are transliterated incorrectly + Latin letters "a", "o" and "u" with diaeresis are transliterated by + Unidecode as "a", "o", "u", *not* according to German rules "ae", "oe", + "ue". This is intentional and will not be changed. Rationale is that these + letters are used in languages other than German (for example, Finnish and + Turkish). German text transliterated without the extra "e" is much more + readable than other languages transliterated using German rules. A + workaround is to do your own replacements of these characters before + passing the string to ``unidecode()``. + +Japanese Kanji is transliterated as Chinese + Same as with Latin letters with accents discussed in the answer above, the + Unicode standard encodes letters, not letters in a certain language or + their meaning. With Japanese and Chinese this is even more evident because + the same letter can have very different transliterations depending on the + language it is used in. Since Unidecode does not do language-specific + transliteration (see next question), it must decide on one. For certain + characters that are used in both Japanese and Chinese the decision was to + use Chinese transliterations. If you intend to transliterate Japanese, + Chinese or Korean text please consider using other libraries which do + language-specific transliteration, such as `Unihandecode + `_. + +Unidecode should support localization (e.g. a language or country parameter, inspecting system locale, etc.) + Language-specific transliteration is a complicated problem and beyond the + scope of this library. Changes related to this will not be accepted. Please + consider using other libraries which do provide this capability, such as + `Unihandecode `_. + +Unidecode should automatically detect the language of the text being transliterated + Language detection is a completely separate problem and beyond the scope of + this library. + +Unidecode should use a permissive license such as MIT or the BSD license. + The maintainer of Unidecode believes that providing access to source code + on redistribution is a fair and reasonable request when basing products on + voluntary work of many contributors. If the license is not suitable for + you, please consider using other libraries, such as `text-unidecode + `_. + +Unidecode produces completely wrong results (e.g. "u" with diaeresis transliterating as "A 1/4 ") + The strings you are passing to Unidecode have been wrongly decoded + somewhere in your program. For example, you might be decoding utf-8 encoded + strings as latin1. With a misconfigured terminal, locale and/or a text + editor this might not be immediately apparent. Inspect your strings with + ``repr()`` and consult the + `Unicode HOWTO `_. + +Why does Unidecode not replace \\u and \\U backslash escapes in my strings? + Unidecode knows nothing about escape sequences. Interpreting these sequences + and replacing them with actual Unicode characters in string literals is the + task of the Python interpreter. If you are asking this question you are + very likely misunderstanding the purpose of this library. Consult the + `Unicode HOWTO `_ and possibly + the ``unicode_escape`` encoding in the standard library. + +I've upgraded Unidecode and now some URLs on my website return 404 Not Found. + This is an issue with the software that is running your website, not + Unidecode. Occasionally, new versions of Unidecode library are released + which contain improvements to the transliteration tables. This means that + you cannot rely that ``unidecode()`` output will not change across + different versions of Unidecode library. If you use ``unidecode()`` to + generate URLs for your website, either generate the URL slug once and store + it in the database or lock your dependency of Unidecode to one specific + version. + +Some of the issues in this section are discussed in more detail in `this blog +post `_. + + +Performance notes +----------------- + +By default, ``unidecode()`` optimizes for the use case where most of the strings +passed to it are already ASCII-only and no transliteration is necessary (this +default might change in future versions). + +For performance critical applications, two additional functions are exposed: + +``unidecode_expect_ascii()`` is optimized for ASCII-only inputs (approximately +5 times faster than ``unidecode_expect_nonascii()`` on 10 character strings, +more on longer strings), but slightly slower for non-ASCII inputs. + +``unidecode_expect_nonascii()`` takes approximately the same amount of time on +ASCII and non-ASCII inputs, but is slightly faster for non-ASCII inputs than +``unidecode_expect_ascii()``. + +Apart from differences in run time, both functions produce identical results. +For most users of Unidecode, the difference in performance should be +negligible. + + +Source +------ + +You can get the latest development version of Unidecode with:: + + $ git clone https://www.tablix.org/~avian/git/unidecode.git + +There is also an official mirror of this repository on GitHub at +https://github.com/avian2/unidecode + + +Contact +------- + +Please make sure to read the `Frequently asked questions`_ section above before +contacting the maintainer. + +Bug reports, patches and suggestions for Unidecode can be sent to +tomaz.solc@tablix.org. + +Alternatively, you can also open a ticket or pull request at +https://github.com/avian2/unidecode + + +Copyright +--------- + +Original character transliteration tables: + +Copyright 2001, Sean M. Burke , all rights reserved. + +Python code and later additions: + +Copyright 2024, Tomaž Šolc + +This program is free software; you can redistribute it and/or modify it +under the terms of the GNU General Public License as published by the Free +Software Foundation; either version 2 of the License, or (at your option) +any later version. + +This program is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +more details. + +You should have received a copy of the GNU General Public License along +with this program; if not, write to the Free Software Foundation, Inc., 51 +Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. The programs and +documentation in this dist are distributed in the hope that they will be +useful, but without any warranty; without even the implied warranty of +merchantability or fitness for a particular purpose. + +.. + vim: set filetype=rst: diff --git a/libs/Unidecode-1.3.8.dist-info/RECORD b/libs/Unidecode-1.3.8.dist-info/RECORD new file mode 100644 index 000000000..92b5732ca --- /dev/null +++ b/libs/Unidecode-1.3.8.dist-info/RECORD @@ -0,0 +1,203 @@ +../../bin/unidecode,sha256=NbqWJOWfXecMdJfbAD2hnDahz-mDBHIWjTSidYHnxSA,236 +Unidecode-1.3.8.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +Unidecode-1.3.8.dist-info/LICENSE,sha256=gXf5dRMhNSbfLPYYTY_5hsZ1r7UU1OaKQEAQUhuIBkM,18092 +Unidecode-1.3.8.dist-info/METADATA,sha256=TjOEznFzIHnDfx8CRJjrHfMWiIOOa6drPp6zqa0Obc4,13615 +Unidecode-1.3.8.dist-info/RECORD,, +Unidecode-1.3.8.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +Unidecode-1.3.8.dist-info/WHEEL,sha256=Xo9-1PvkuimrydujYJAjF7pCkriuXBpUPEjma1nZyJ0,92 +Unidecode-1.3.8.dist-info/entry_points.txt,sha256=zjje8BrCWj_5MDf7wASbnNdeWYxxdt5BuTQI9x8c_24,50 +Unidecode-1.3.8.dist-info/top_level.txt,sha256=4uYNG2l04s0dm0mEQmPLo2zrjLbhLPKUesLr2dOTdpo,10 +unidecode/__init__.py,sha256=uUP370Iden1EsQtgglNd57DMKOG5mXh9UxIMm8yhDfQ,4230 +unidecode/__main__.py,sha256=VWYWCclyJsdhtNMQtryMFbgsCZtNUsWcEuS7ZOlH1Jc,40 +unidecode/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +unidecode/util.py,sha256=ZxssZFzbZlAf6oiDIu2HZjrAQckbOD2VPD9uy-wZgCI,1652 +unidecode/x000.py,sha256=DaoVzSCvFzhzHbFtzFOE8uS9CgWD7K3JuhmACpFbivY,3038 +unidecode/x001.py,sha256=ylHh3UVaPtibVuUEEWvdSeDFK0OXrWt4-LnxAgYD6qo,3891 +unidecode/x002.py,sha256=NWord8myi2jYV4YwlNZFbKu6HgbbolWLNCOlseR3WsY,3871 +unidecode/x003.py,sha256=5gZS5aXbQ4Z8aH08EehKx4SqAgUNBcTz_x-I3o5qvVg,3825 +unidecode/x004.py,sha256=KAeJjKgkdzMU1MK9J9JqmPeKBDgjhG5UcfyAa594Hk8,4054 +unidecode/x005.py,sha256=7ezPyF52iKiK5LPf6TA5zVUZ7RbIjz7EVLS42aXG9ug,3920 +unidecode/x006.py,sha256=Jye83eXYQqtpowxsQ01jQSDlhAjWbmGNFRdmbojvgyE,3912 +unidecode/x007.py,sha256=6lnnnArEmvi3XeZLFwrCZGStdDKDAHt7alIpdo8S7rk,3987 +unidecode/x009.py,sha256=xNz8qrO1PDseMjOwA0rjsiAhNZTO_uFgjpmbp7qcH_c,4013 +unidecode/x00a.py,sha256=2xksKrrMWF9xLbs8OPfTxT7g86ciwdK9QZ8AQeecmus,4019 +unidecode/x00b.py,sha256=Y7GlfYE2v-D3BkZd3ctfo6L21VG-aR2OFESRb8_WRH4,4019 +unidecode/x00c.py,sha256=jOGpNU7vxghp3jwUuUATiSrDwvgZuOe8nlkcjJYTHco,4007 +unidecode/x00d.py,sha256=lkFf8d_oXN8IZop6CFlYpKdWuJqWGnH0WQMwir4_WgI,4025 +unidecode/x00e.py,sha256=ARKK__sIXUXL4h2Egac2f9ng2Z_YCGD5kYP2oj-ptlI,3989 +unidecode/x00f.py,sha256=TdSmr755Jw1TRtxk5Z4UPZIp1CVhXii8S0zSAcQ2vWk,3998 +unidecode/x010.py,sha256=YhXX8s1dP7YJMzaaV9CMBCOraExb6QrQQWbkFT3d2Jo,4011 +unidecode/x011.py,sha256=bc5lAse0haio2pceaADqkjzTh8MdgNTwTh04W2FJO-Q,4120 +unidecode/x012.py,sha256=XoiRFvNtHV29Q76KcpPBSrC4sLd6faTz4tKZEMIQ45M,4293 +unidecode/x013.py,sha256=UkxSb2Q4xq7dydCZNg_f0Nu90slVSmAckq-btDZ7uAA,4190 +unidecode/x014.py,sha256=4R3w_Dgg9yCw-9KkpqHfWFzyQZZfdb444fMIh240l-Q,4298 +unidecode/x015.py,sha256=TB6O4l2qPxbmF2dejlxXLqX5tTfjl95cMYx1770GHs0,4329 +unidecode/x016.py,sha256=Tx3P-DjDqCLuKbmiG-0cMzw2xFVuojQg3o5yyt4506E,4114 +unidecode/x017.py,sha256=Ks_t-4BgOrTqmqYC6BpqXePI-YyStE7p3P27lzBefSA,4038 +unidecode/x018.py,sha256=C1jpnsK3YO27xpiWJ2DXSAkV9dsPUwKqWtkgePtzp3g,3998 +unidecode/x01d.py,sha256=EwAYkMVHAFvbKRzsQ-e4cRcvS_eia3kYCM2GcaqkBWY,3701 +unidecode/x01e.py,sha256=rG1jtL0dpL-RNsvG-AxX1izkyWkbgwe0BWhATDJtmgg,3845 +unidecode/x01f.py,sha256=NUC2rlFE9YpODdDn4e5uzV7uIqEBNvKw486nOD7UQpQ,3877 +unidecode/x020.py,sha256=lXj8wkWMbD2Iuw3OCrEqZofJjJccnvY3ro5SpyotCq8,4080 +unidecode/x021.py,sha256=QQGWXFmQhQ9ei6rVCx2y-pbx_-7n8bv9DGJpdK_Q8jc,3987 +unidecode/x022.py,sha256=wX6BUR7yKGgSICIzY_B15mqgnjvRbSlepM6aqb2tnGY,4085 +unidecode/x023.py,sha256=weebXXqY3E8OhqS0ziAKHo58lCl3dkkyD0w2aKHqv7Q,4089 +unidecode/x024.py,sha256=JmCTFnYtmMHQvfYP-4f5uDiCxlwhNk7LZLyxLWWGjK8,4003 +unidecode/x025.py,sha256=DAMdCakIv0m21AWcRUNK9QWReCYXPSwVDmbFdriM4qc,3854 +unidecode/x026.py,sha256=TKU0cwRXL8vLAmZ26R8E2dpkmXmRKx4wTU0VEbuTAnM,3874 +unidecode/x027.py,sha256=qZacxfhS5nWgBhbrIT6-wm9yGP_OlAVRJ-GcmUhPl14,3718 +unidecode/x028.py,sha256=FZPCZ9w3N3WOI42h2gHEQgVOAlLBNTZjMu_KQQkIMdk,5069 +unidecode/x029.py,sha256=b8afmG-DjZmHHy0XdjcZlSXtlnwjScIcPBGbMv_YSUQ,4090 +unidecode/x02a.py,sha256=QHAyHnegV0OVOTQ5OnfJKzkaHQIFbWmmMjiFcHGUZi0,4093 +unidecode/x02c.py,sha256=ZkmMztaYT7d81E9qtUU9ayG9hBi5XqWY_ta-X5Hsaqc,4076 +unidecode/x02e.py,sha256=VCGlK7123S2wDzfkggEARyGZKi-0ElepSYECGGluf7E,4072 +unidecode/x02f.py,sha256=hcUTlkw_6Hjnxsk0e28RTd-HWpSK0IGq5hkrwA1fJFk,4091 +unidecode/x030.py,sha256=wdodiC_N7bMsh8vSmVF0STHGZnOAsZnVN-_RPiqupRA,4028 +unidecode/x031.py,sha256=jed0xoqQmUnnOqATVe7z9F2zigAZVAJX6BrWtXFPWbs,4044 +unidecode/x032.py,sha256=lj4IwokKA0IdIJiJJTfmBUGVYmWvLowFtPLwLzhfokU,4466 +unidecode/x033.py,sha256=ImTd4BRRPgCqWmrvJPoikoL0dJMKH8eQgd48vksi60A,4513 +unidecode/x04d.py,sha256=hcUTlkw_6Hjnxsk0e28RTd-HWpSK0IGq5hkrwA1fJFk,4091 +unidecode/x04e.py,sha256=X-Pzl5_QGkYexzNTY04C_tq3RvbyAUYemf0C4mIl5-U,4630 +unidecode/x04f.py,sha256=BM29-2OTb6aR7CN7NMN3nnC9BGxgediLEHGMcIB5ENU,4597 +unidecode/x050.py,sha256=SPmkA-PD39V8eO4DByxVa8HyqanGcw54xW51kLnaieY,4676 +unidecode/x051.py,sha256=GGJT-fiYxTk_FAAW6eTobT3pOGI-Qq1M3eCxN7c7f5E,4681 +unidecode/x052.py,sha256=a09eo_5pL6jpU9TW-zG2w2iXTYp6awtQ4OxGnLdcwKg,4654 +unidecode/x053.py,sha256=4x8X4Hrf56DOAINYi8JxStXW4m7FGJNiH-51JzCxE64,4608 +unidecode/x054.py,sha256=N8hO8YrlNoepnrYLUZ_EcTVRqI1lekqq3h-i-UNlTJw,4577 +unidecode/x055.py,sha256=_PK65HLpk7puojAFGeOm5Cdk-PDevHHI6NR8sHuo0Ko,4595 +unidecode/x056.py,sha256=mlNNouWFIjpZdjuBWhxFGSB_UDh0OItlsShjHQRjhxc,4607 +unidecode/x057.py,sha256=uivN7P3d-kkonqBATLKOM0ni4jVvsSzA9SOEFhbOuP4,4627 +unidecode/x058.py,sha256=lPNpdrFLFfaBoQz8Cwm2Ess8m4m_45ylIHspOUpDrLk,4664 +unidecode/x059.py,sha256=BdA_NFQqr-aGpuyo9he6uxDwm9facV-ql5axiKqgByk,4640 +unidecode/x05a.py,sha256=9UFNWH8FpkHUArS2-Td3VYOo21VQkoqYW7A0Slk0YhQ,4632 +unidecode/x05b.py,sha256=yfWnRe6mtnqY3b3Ac2_IJBA5vBYb64PYF9XM4HSZygU,4666 +unidecode/x05c.py,sha256=6iZj6HHnJ4lF3k1i68-9Dgge2H3KAlyZtNxW0BIu66o,4602 +unidecode/x05d.py,sha256=Wudbb7xOtWry4Xu5xm9j80vFkigCedGq5uHcYAYl0o8,4660 +unidecode/x05e.py,sha256=wKqvr0lkEy1yfXbYj2OtXHBxw5FxVz_MzJULXWrGvA0,4662 +unidecode/x05f.py,sha256=NnSIJOl_9CC4IRwBIQ6CEhTfvvzZ2PXhZSLJuC6sgHY,4656 +unidecode/x060.py,sha256=-Ajr6Q7RP_fdetvZ2hWflxNiaOokB3q5oeRCt7CqcDc,4640 +unidecode/x061.py,sha256=aqOY7Jt--4JhdktU2RB1bf5J0fH27fRDLhV55aR3gO0,4656 +unidecode/x062.py,sha256=wxQkvAGrppx4Y5E-hAVCps0I9bz_fbG1YSqs1E8k9sU,4616 +unidecode/x063.py,sha256=wAcyLr9CJ35G4sNTfvYb7DtFjeRlyo585JC2_-aBuQM,4648 +unidecode/x064.py,sha256=8e775dKt12GedypWT9jPXeqWLkW5-AsVG106FlfiTvA,4651 +unidecode/x065.py,sha256=fPak6ADqEOBFPfP2u7pAIZ_ObbgtdGFa4enmjVBpsVE,4634 +unidecode/x066.py,sha256=K6g6XTVEFEAppiln64vxgA2V1FMWl0YdbhDJgihQsTA,4675 +unidecode/x067.py,sha256=5d8zLxoh2vS76uBWQckXGbeyjzEUJ5aJMAMvNA-YxLs,4627 +unidecode/x068.py,sha256=-UhVYRQGQtxQJbgwyHAox-JHizu_RvO7Lb5I1F9mpvY,4670 +unidecode/x069.py,sha256=cRQZP6ZGJQsx5l2qSfpe9XmiDfxlGh7rEh30_u9oTSo,4665 +unidecode/x06a.py,sha256=iXZkuxRRsgUuNlVlNliR7gio4M4WUN0JNCPdINrzYlY,4662 +unidecode/x06b.py,sha256=5GRxv36m9zR163UNrGb_c64-uueKrpqyeeRWG9ZDme0,4600 +unidecode/x06c.py,sha256=RNKzdImtimBIuLtvbsUAzYSV7iZmVvPWyV8dj91KJlw,4637 +unidecode/x06d.py,sha256=jFvmxRU4VHSeoahyFtHIHqpvfqvJbNzvsrDn4Kd7WAQ,4647 +unidecode/x06e.py,sha256=1esJUSaQ4QotdjhxG6vtvC3CDWjY2rTr4EVLD4if8CU,4630 +unidecode/x06f.py,sha256=s7JTw6eW_6pqjCc1DEMDQ178vtllhHiejtvb360vDVc,4638 +unidecode/x070.py,sha256=oLeIanQmBbyz8OU_l5VQ-POF8mY5XbLL3rfEjr3XkUw,4677 +unidecode/x071.py,sha256=v1S9E-H06WC0rr10gP27Dqev2nxRlymECJ681BSs9Y4,4644 +unidecode/x072.py,sha256=veZOktQoJQ2wmKKLjq17UM5hAa3xo3nRLdFgSHjv8rI,4645 +unidecode/x073.py,sha256=NWkyVIbNgSu_U9katu1LRaLkL7iHx4bSuRtfsqRG4yk,4642 +unidecode/x074.py,sha256=AocniPNZMcBTeiDWA6OLzQilcWMc_ZHh7pCGXTzqMSg,4686 +unidecode/x075.py,sha256=P3SrhI5BQ5sJ66hyu_LWDONpuzLZJBKsl7f-A37sJXc,4675 +unidecode/x076.py,sha256=9rwfe41pej250BneHHO663PU9vVWyrnHRnP11VUqxEc,4635 +unidecode/x077.py,sha256=ugbmqiry2-tBstXW0Q9o7XEZQimpagZK1EttvBCK1sE,4673 +unidecode/x078.py,sha256=NxeTS_dXa6jmc7iDVUve6_SqO4AhjULng_Gei7pqbRE,4630 +unidecode/x079.py,sha256=ucPPGrgm-AnnWdVFd__unqiSMtdEpZQF6E8ta6IzdiQ,4590 +unidecode/x07a.py,sha256=fjyeO--0F5Kd80F0yOvFIIuiDW7lFKWaVIUqQRIwR9k,4659 +unidecode/x07b.py,sha256=3g39Yw2ZMs7_tcC3OT2e4nGxaWMY6V8iJ2Z6PsnhPS4,4667 +unidecode/x07c.py,sha256=Cbs98r7vdJD_YxpXgAAYoPdA7RDYR82MXN44TQJxoN8,4647 +unidecode/x07d.py,sha256=EKFrTQTNFLGnsm3qI76ALxrxGCcDuyEbapi9j9jy1B4,4678 +unidecode/x07e.py,sha256=r96YBkHoCO8GAvO0j3BdY45RdlNkqpiFWl-Q6mieVcc,4680 +unidecode/x07f.py,sha256=MNRU4aNOE2dKl4p0_WPy-oga_cx7wZ6w4Mlk-RN3PeU,4658 +unidecode/x080.py,sha256=9feIVoCdOFolKgZfRCpdL80l9kRvjbl0z9sV4FAk2Qg,4643 +unidecode/x081.py,sha256=ffvplClKTCDre83MhO7-X3tgdUWfjvkUMxQCPEnRj_U,4671 +unidecode/x082.py,sha256=XTFSjZO8LO3SFcYh9h-Oqby6a67hFDx4B-AQRyptlJU,4641 +unidecode/x083.py,sha256=wXP1lZZAravJZm1f1bCT1cumocFGRG0ZQmgFMVCOSDQ,4635 +unidecode/x084.py,sha256=inA5ODar8zAherLeTyX9-KtCUOrTigxDwb3ei2Kl1CE,4630 +unidecode/x085.py,sha256=QDKK-wbb04nCFc91pSGhyHsxcS_MhdeQLsRqqXhV9h8,4628 +unidecode/x086.py,sha256=DcXhJemXKgrGwPBRFCbINxfxatqjpy7jFgM9jbN8eEk,4608 +unidecode/x087.py,sha256=nddqMqheth-n7kHCyjRNvVPO82UI_PdOic1kQer_iF0,4641 +unidecode/x088.py,sha256=0PVL160fpQ-Kkw29X-bLviyfs4TKIAwp_-SwEWsvemM,4639 +unidecode/x089.py,sha256=RrIGIX6dojryaYh6Da4ysaM_-yREbNZ-HasFX2O_SQc,4624 +unidecode/x08a.py,sha256=NjMp9ck824PXG2gcJXfi_9oQCFgXhhiallO3bYCtXCE,4647 +unidecode/x08b.py,sha256=vUwkG_IOBIhB8gQAaVbgD5EAIA1wY4BBPk5kXwAcPg0,4639 +unidecode/x08c.py,sha256=0sHcCXB9YzXE9oJcwzPtPUltCn6Oo-itdY5vk6MbtdA,4628 +unidecode/x08d.py,sha256=SWD7xSIR-1P30S5-yuNDHpVjWlpfxmUxuJr7f178WsA,4630 +unidecode/x08e.py,sha256=Ym0RQUdsgZJdVmOI56nzSmzfxuKjuS5MUbPSOeyv2Ws,4655 +unidecode/x08f.py,sha256=tNFpnEzNLIY4xHbcR0rZqaoNUKinj-XO2XfSnh6c4u4,4649 +unidecode/x090.py,sha256=XGomJNriNZsHQRUDy3vKwFc4W38uxeqWpn5SHM4G4j8,4627 +unidecode/x091.py,sha256=u8tRZhaVNa2mbsDSYIKqRZ3u4Npj-kiz55rC9izadnM,4653 +unidecode/x092.py,sha256=NvNce8y3YFlPI20pN1a4LY68sid5ApetXs9bo9cxb7w,4644 +unidecode/x093.py,sha256=O2e1p58RB1TS2Au-JSjft3FgPBx1YRAGxnviqSsfnYE,4646 +unidecode/x094.py,sha256=k8ZwNc9qCSzU2b8wMrWUeGSg39tPMiwiKHCiKw6zteM,4653 +unidecode/x095.py,sha256=H2O3xJDE3cAOecyYMUTl6fLs9shETPFwZshtIIK5V3E,4667 +unidecode/x096.py,sha256=sev3zRm46EBQgEtkR4T-Ah0cHYEM-9CM2pFCCc21BFI,4608 +unidecode/x097.py,sha256=S1nZBdt-MHySCAgV9MDdNSQTCSaD62iAhz8EjikfS5A,4633 +unidecode/x098.py,sha256=w0KMxUF7XyG9gdfTJylYsG_clvm3RD_LIM5XHR0xsdY,4643 +unidecode/x099.py,sha256=nlaWb2nRTSnFfDjseDRJ1a3Y0okOHbNA1-htKo9SkAM,4627 +unidecode/x09a.py,sha256=Z8pQsTc62CWgm0JPnj3kokKKf9_qfzRpo0u5iH61CaE,4623 +unidecode/x09b.py,sha256=njA75MlCgC-5UuS1Hvm-mdgsVwW9r801odfBTJg-BFE,4653 +unidecode/x09c.py,sha256=NveMhN85_Cm4H1cnfHDTcnSj675MOVBq9Lkjpw3YxA0,4659 +unidecode/x09d.py,sha256=_0fAaUhK3axhhWLA4QPNJf_J9YSs1MCKx2xR-vl5QYI,4630 +unidecode/x09e.py,sha256=wreETFCeKf9bVvLc3E7MUAvlu2CN5xKeebf3ESuV13s,4613 +unidecode/x09f.py,sha256=pNAdX7-9yMEPXtozjCuXxzc74eCVft9meOTxCtU7vJw,4420 +unidecode/x0a0.py,sha256=EpopPuuocybgCcpX19Ii-udqsPXJjSces3360lqJ8vs,4428 +unidecode/x0a1.py,sha256=0hvF77d5E640SujjdHVqy5gMUH85gEdOv80eRvCEAGM,4469 +unidecode/x0a2.py,sha256=9Icpfk_ElebYd_xN09OMziFrpAGPXEUNVmawpnhbBaQ,4503 +unidecode/x0a3.py,sha256=G1lPrnCqYz0s4wsSa1qM0WgrZBWO_beRk3AgK0iVZLA,4521 +unidecode/x0a4.py,sha256=nWPXzCragW0rsDQPa6ooo9KOc-SOjVCP8KIOuGc7WpU,4373 +unidecode/x0ac.py,sha256=wj7hl88VlCdc_eGpOL4m4CBJILyQqd9atObC5Xvd0aA,4709 +unidecode/x0ad.py,sha256=Rz5rn0fM-CqRjaN4TvSq_1StAQdyAF2WX3cUvcQHaWU,4766 +unidecode/x0ae.py,sha256=jNIBVB-Pw2ZNihAeyWbDIEq9Yt9zlhdfGylfvAaxUks,4875 +unidecode/x0af.py,sha256=Am5YC8Zfrun5NUKxU6LrU2-d5GgkGSBs7fZt2rqSi74,5012 +unidecode/x0b0.py,sha256=1bgHerCDAqIcJHYeGddJjJfRWiHCKtU2B0J-XGvcbbc,4853 +unidecode/x0b1.py,sha256=Six-lzGdvgJx4YsIa0lTusnBEV1zbCKQCquq17TDJoQ,4746 +unidecode/x0b2.py,sha256=HQDbmglNi4QfiRSGucUclgq_4FGpRjbJkWU1JTLAFGc,4680 +unidecode/x0b3.py,sha256=1lqxghVZiiStOAx1IG_vc1zZTXrAa7Z__QY6ZWvo2aA,4741 +unidecode/x0b4.py,sha256=V6BNSTxpyP8VuqF7x5z7bpF3MQAkwZfKtEu6NFr_vSg,4762 +unidecode/x0b5.py,sha256=9NVd2hNLyRlLceVlznba1dreqBGeKU_0gzmkgAw0gyg,4919 +unidecode/x0b6.py,sha256=V_vRsB0GICu9hqhO4pnbPWreDSevJ3bbmLRJkuQUxnE,4996 +unidecode/x0b7.py,sha256=CwBaCBICyVagnFjUpkwabuDvBJw7gAeqkSRpfBAVv8s,4833 +unidecode/x0b8.py,sha256=xYp-xy2LIwq95OWyS9vYMc_Z5od9dud0W1dxeg4P_Jk,4714 +unidecode/x0b9.py,sha256=z3hKNzBq_MeK9V3AyQzaY58cgi0-VGOsLk3-UFmszLQ,4704 +unidecode/x0ba.py,sha256=4gubifoBeJUUrwXEI4litJygekufEycmWDLrJ-Qvs14,4765 +unidecode/x0bb.py,sha256=bsCTABUdC6yTn8_0vhYe5jRP1z_BoAdificB8Y1c1hA,4730 +unidecode/x0bc.py,sha256=AhQvAz7yHlbQ_4c2KOIisq07eZJ5JQn6cV8I31oT9kg,4707 +unidecode/x0bd.py,sha256=IGtyVxIUr1mU3hokn6iUDJhXZezQozVvfWOyf4Pa5dI,4752 +unidecode/x0be.py,sha256=1D-hXu3p3wvOnGVMjEqVsrltYe7UuSwit2yqN5eFizc,4849 +unidecode/x0bf.py,sha256=NkEXqr2ER3BNFkTasDV9CHnkRBuX_Ao5OHGv_NgKAew,5010 +unidecode/x0c0.py,sha256=zDlHpyM0omza5TqGLb8Rhl7Wd-LlV1AjvH_xdnEnNFw,4856 +unidecode/x0c1.py,sha256=AC6xJyx9UblKAGNqGN7AH2Idb3_3vbc-I5U0Myig5fA,4765 +unidecode/x0c2.py,sha256=siRYLA8Cv9Z8XsRp3WQOBdRrPkjJOuEh8z1-3SMXOzQ,4710 +unidecode/x0c3.py,sha256=hlAFe6lsz0aLMixlpeFjV4I-WTIiA3B2BU58yGlTwRg,4975 +unidecode/x0c4.py,sha256=z3xZwSkf5ru1FCdBMHOr5fyglzVdyPhQVtWjq9xInsQ,5024 +unidecode/x0c5.py,sha256=F-DR0eVMRkemOnNXOtDjI5i6gW9136XLmWM_yMVvc84,4581 +unidecode/x0c6.py,sha256=7p_jMrHf3WUa_zANms-RGVN1bAeshgWLkC16_VcSawA,4490 +unidecode/x0c7.py,sha256=5eOAq4jFsPZ-zKO7lHzAGj_EvXdaMC4Kud7gvE-B7Tg,4564 +unidecode/x0c8.py,sha256=wltKvhBgn51jULzwUnEbmyDuK9JvQpQee0uTKK42-20,4733 +unidecode/x0c9.py,sha256=GoARON07wCoHN2wRHb5fvzqE9L3Yme2hKeciynUIAIk,4722 +unidecode/x0ca.py,sha256=BsBZTNj3npIkdo3L9pSEX7XvDT68KV7wFtOOwyEb2So,5007 +unidecode/x0cb.py,sha256=8T7vnJMRmYGyySYthMWz0bgN-MremktGImjejodFeMo,5012 +unidecode/x0cc.py,sha256=GKoHN-4vL4Y3EL42G0xbN74Tgspew1oMvxQtsIa3ess,4749 +unidecode/x0cd.py,sha256=7sZ05OjugbaombMRDYOVxgstZbXMcuX5kHFheKv4W2E,4738 +unidecode/x0ce.py,sha256=mOEHFrsAwIvcTnh7OKVK5qbuXUXHfJOR7D4FtXsQmao,4708 +unidecode/x0cf.py,sha256=H9PeYcbOG68F_yc7zsELUuN05ANfFNOUX-e3-gzx7Ow,4713 +unidecode/x0d0.py,sha256=eULqcGHPmaoEdl0EwRB5wWSu8M43bp4HoFo5gGljacg,4706 +unidecode/x0d1.py,sha256=BClLDAjPgsAX6MJCsuHfmfuhH9qfzUy_vb-d9zBs3Oc,4767 +unidecode/x0d2.py,sha256=e74nqGo4E4sF1sy8qBFu2ecWoRfJdoXI1xRFRPqYEz8,4724 +unidecode/x0d3.py,sha256=8-UmvJ3-ILXo9d3GA-ReOE4OfUenL3tVUJYldZ9gHu0,4705 +unidecode/x0d4.py,sha256=fwUmzksoddTKB8fH2rZMxRK3pJtLrxhcrYpHfBauAwE,4758 +unidecode/x0d5.py,sha256=rANSL5ndzLgSgYJQNEw57AfXpicRe7pvHRlKTPb4-QQ,4680 +unidecode/x0d6.py,sha256=fT8_cRzp7y60IIhn87kM9lLehKGAg5wYmfFOwgGp6e0,4765 +unidecode/x0d7.py,sha256=40-m7uKNvylWCcVBuTXrbiP6Lrj_4d4PWgLcX8670Kk,4468 +unidecode/x0f9.py,sha256=2PD0_fpDnaFO9ftICjYSOhnjAfBppjsj1TcLIuYjnCI,4567 +unidecode/x0fa.py,sha256=XHxCfXOhHDqzjG0Nw6n1sT5Q_MKLCovPFe-22IQxVXU,4172 +unidecode/x0fb.py,sha256=n_5urRXj6Ecf0MKMnuwNY0UK6TJtUW2hKcNLQqa2Gf8,3787 +unidecode/x0fc.py,sha256=KcyQnyv7gxNeVcAnRwQrm4NlabZE3CrnmtLqXj_7te8,3595 +unidecode/x0fd.py,sha256=mVHMrX8AhRzwCkMNA4sJkhwirK3BqmNv6YZfyCpE9Is,3703 +unidecode/x0fe.py,sha256=CrdwUOf0sl8yUfOFnXOXFZ8U662dQThpGMwGBkY8cJ4,3769 +unidecode/x0ff.py,sha256=Ijfv5VVDCTWRzRqwMYSp0fSycy176gn7P8ut8x3bv-w,3957 +unidecode/x1d4.py,sha256=xzL0OicR95IWq6LiApIPEgPoST8dyVgYuIUGxkz1b28,3863 +unidecode/x1d5.py,sha256=bmTSTgWnsLP7yUDZq2Irtz84Zm7bmLzYzurY0eI0uIU,3863 +unidecode/x1d6.py,sha256=8H0RmEfbY82X1iQwr0vcsgQGCvGKv19_773K_T2NI2A,4052 +unidecode/x1d7.py,sha256=yyHV2dCo1p_m_QVgz1H9S6XqeiN9GpGxB-ZqAW7l5ts,4057 +unidecode/x1f1.py,sha256=URX9F6UPgUo4-tpr7bhPm4G5ruFDoScW5bZLwzR88Yg,4308 +unidecode/x1f6.py,sha256=Ji4t-EFmJmo3CDeZ0yD7pX58hj5fQQc99TOrD-yad9k,4103 diff --git a/libs/Unidecode-1.3.8.dist-info/REQUESTED b/libs/Unidecode-1.3.8.dist-info/REQUESTED new file mode 100644 index 000000000..e69de29bb diff --git a/libs/Unidecode-1.3.8.dist-info/WHEEL b/libs/Unidecode-1.3.8.dist-info/WHEEL new file mode 100644 index 000000000..ba48cbcf9 --- /dev/null +++ b/libs/Unidecode-1.3.8.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.41.3) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/libs/Unidecode-1.3.8.dist-info/entry_points.txt b/libs/Unidecode-1.3.8.dist-info/entry_points.txt new file mode 100644 index 000000000..fd31232a1 --- /dev/null +++ b/libs/Unidecode-1.3.8.dist-info/entry_points.txt @@ -0,0 +1,2 @@ +[console_scripts] +unidecode = unidecode.util:main diff --git a/libs/Unidecode-1.3.8.dist-info/top_level.txt b/libs/Unidecode-1.3.8.dist-info/top_level.txt new file mode 100644 index 000000000..051b14ceb --- /dev/null +++ b/libs/Unidecode-1.3.8.dist-info/top_level.txt @@ -0,0 +1 @@ +unidecode diff --git a/libs/_pyrsistent_version.py b/libs/_pyrsistent_version.py new file mode 100644 index 000000000..2f15b8cd3 --- /dev/null +++ b/libs/_pyrsistent_version.py @@ -0,0 +1 @@ +__version__ = '0.20.0' diff --git a/libs/alembic-1.13.1.dist-info/INSTALLER b/libs/alembic-1.13.1.dist-info/INSTALLER new file mode 100644 index 000000000..a1b589e38 --- /dev/null +++ b/libs/alembic-1.13.1.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/libs/alembic-1.13.1.dist-info/LICENSE b/libs/alembic-1.13.1.dist-info/LICENSE new file mode 100644 index 000000000..74b9ce342 --- /dev/null +++ b/libs/alembic-1.13.1.dist-info/LICENSE @@ -0,0 +1,19 @@ +Copyright 2009-2023 Michael Bayer. + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/libs/alembic-1.13.1.dist-info/METADATA b/libs/alembic-1.13.1.dist-info/METADATA new file mode 100644 index 000000000..7a6884d93 --- /dev/null +++ b/libs/alembic-1.13.1.dist-info/METADATA @@ -0,0 +1,142 @@ +Metadata-Version: 2.1 +Name: alembic +Version: 1.13.1 +Summary: A database migration tool for SQLAlchemy. +Home-page: https://alembic.sqlalchemy.org +Author: Mike Bayer +Author-email: mike_mp@zzzcomputing.com +License: MIT +Project-URL: Documentation, https://alembic.sqlalchemy.org/en/latest/ +Project-URL: Changelog, https://alembic.sqlalchemy.org/en/latest/changelog.html +Project-URL: Source, https://github.com/sqlalchemy/alembic/ +Project-URL: Issue Tracker, https://github.com/sqlalchemy/alembic/issues/ +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: Environment :: Console +Classifier: License :: OSI Approved :: MIT License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: PyPy +Classifier: Topic :: Database :: Front-Ends +Requires-Python: >=3.8 +Description-Content-Type: text/x-rst +License-File: LICENSE +Requires-Dist: SQLAlchemy >=1.3.0 +Requires-Dist: Mako +Requires-Dist: typing-extensions >=4 +Requires-Dist: importlib-metadata ; python_version < "3.9" +Requires-Dist: importlib-resources ; python_version < "3.9" +Provides-Extra: tz +Requires-Dist: backports.zoneinfo ; (python_version < "3.9") and extra == 'tz' + +Alembic is a database migrations tool written by the author +of `SQLAlchemy `_. A migrations tool +offers the following functionality: + +* Can emit ALTER statements to a database in order to change + the structure of tables and other constructs +* Provides a system whereby "migration scripts" may be constructed; + each script indicates a particular series of steps that can "upgrade" a + target database to a new version, and optionally a series of steps that can + "downgrade" similarly, doing the same steps in reverse. +* Allows the scripts to execute in some sequential manner. + +The goals of Alembic are: + +* Very open ended and transparent configuration and operation. A new + Alembic environment is generated from a set of templates which is selected + among a set of options when setup first occurs. The templates then deposit a + series of scripts that define fully how database connectivity is established + and how migration scripts are invoked; the migration scripts themselves are + generated from a template within that series of scripts. The scripts can + then be further customized to define exactly how databases will be + interacted with and what structure new migration files should take. +* Full support for transactional DDL. The default scripts ensure that all + migrations occur within a transaction - for those databases which support + this (Postgresql, Microsoft SQL Server), migrations can be tested with no + need to manually undo changes upon failure. +* Minimalist script construction. Basic operations like renaming + tables/columns, adding/removing columns, changing column attributes can be + performed through one line commands like alter_column(), rename_table(), + add_constraint(). There is no need to recreate full SQLAlchemy Table + structures for simple operations like these - the functions themselves + generate minimalist schema structures behind the scenes to achieve the given + DDL sequence. +* "auto generation" of migrations. While real world migrations are far more + complex than what can be automatically determined, Alembic can still + eliminate the initial grunt work in generating new migration directives + from an altered schema. The ``--autogenerate`` feature will inspect the + current status of a database using SQLAlchemy's schema inspection + capabilities, compare it to the current state of the database model as + specified in Python, and generate a series of "candidate" migrations, + rendering them into a new migration script as Python directives. The + developer then edits the new file, adding additional directives and data + migrations as needed, to produce a finished migration. Table and column + level changes can be detected, with constraints and indexes to follow as + well. +* Full support for migrations generated as SQL scripts. Those of us who + work in corporate environments know that direct access to DDL commands on a + production database is a rare privilege, and DBAs want textual SQL scripts. + Alembic's usage model and commands are oriented towards being able to run a + series of migrations into a textual output file as easily as it runs them + directly to a database. Care must be taken in this mode to not invoke other + operations that rely upon in-memory SELECTs of rows - Alembic tries to + provide helper constructs like bulk_insert() to help with data-oriented + operations that are compatible with script-based DDL. +* Non-linear, dependency-graph versioning. Scripts are given UUID + identifiers similarly to a DVCS, and the linkage of one script to the next + is achieved via human-editable markers within the scripts themselves. + The structure of a set of migration files is considered as a + directed-acyclic graph, meaning any migration file can be dependent + on any other arbitrary set of migration files, or none at + all. Through this open-ended system, migration files can be organized + into branches, multiple roots, and mergepoints, without restriction. + Commands are provided to produce new branches, roots, and merges of + branches automatically. +* Provide a library of ALTER constructs that can be used by any SQLAlchemy + application. The DDL constructs build upon SQLAlchemy's own DDLElement base + and can be used standalone by any application or script. +* At long last, bring SQLite and its inability to ALTER things into the fold, + but in such a way that SQLite's very special workflow needs are accommodated + in an explicit way that makes the most of a bad situation, through the + concept of a "batch" migration, where multiple changes to a table can + be batched together to form a series of instructions for a single, subsequent + "move-and-copy" workflow. You can even use "move-and-copy" workflow for + other databases, if you want to recreate a table in the background + on a busy system. + +Documentation and status of Alembic is at https://alembic.sqlalchemy.org/ + +The SQLAlchemy Project +====================== + +Alembic is part of the `SQLAlchemy Project `_ and +adheres to the same standards and conventions as the core project. + +Development / Bug reporting / Pull requests +___________________________________________ + +Please refer to the +`SQLAlchemy Community Guide `_ for +guidelines on coding and participating in this project. + +Code of Conduct +_______________ + +Above all, SQLAlchemy places great emphasis on polite, thoughtful, and +constructive communication between users and developers. +Please see our current Code of Conduct at +`Code of Conduct `_. + +License +======= + +Alembic is distributed under the `MIT license +`_. diff --git a/libs/alembic-1.13.1.dist-info/RECORD b/libs/alembic-1.13.1.dist-info/RECORD new file mode 100644 index 000000000..f4dee8948 --- /dev/null +++ b/libs/alembic-1.13.1.dist-info/RECORD @@ -0,0 +1,86 @@ +../../bin/alembic,sha256=xqPGhIsDow0IG3BUa3a_VtCtKJgqxLpVJuFe1PQcGoA,236 +alembic-1.13.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +alembic-1.13.1.dist-info/LICENSE,sha256=soUmiob0QW6vTQWyrjiAwVb3xZqPk1pAK8BW6vszrwg,1058 +alembic-1.13.1.dist-info/METADATA,sha256=W1F2NBRkhqW55HiGmEIpdmiRt2skU5wwJd21UHFbSdQ,7390 +alembic-1.13.1.dist-info/RECORD,, +alembic-1.13.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +alembic-1.13.1.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92 +alembic-1.13.1.dist-info/entry_points.txt,sha256=aykM30soxwGN0pB7etLc1q0cHJbL9dy46RnK9VX4LLw,48 +alembic-1.13.1.dist-info/top_level.txt,sha256=FwKWd5VsPFC8iQjpu1u9Cn-JnK3-V1RhUCmWqz1cl-s,8 +alembic/__init__.py,sha256=PMiQT_1tHyC_Od3GDBArBk7r14v8F62_xkzliPq9tLU,63 +alembic/__main__.py,sha256=373m7-TBh72JqrSMYviGrxCHZo-cnweM8AGF8A22PmY,78 +alembic/autogenerate/__init__.py,sha256=ntmUTXhjLm4_zmqIwyVaECdpPDn6_u1yM9vYk6-553E,543 +alembic/autogenerate/api.py,sha256=Oc7MRtDhkSICsQ82fYP9bBMYaAjzzW2X_izM3AQU-OY,22171 +alembic/autogenerate/compare.py,sha256=3QLK2yCDW37bXbAIXcHGz4YOFlOW8bSfLHJ8bmzgG1M,44938 +alembic/autogenerate/render.py,sha256=uSbCpkh72mo00xGZ8CJa3teM_gqulCoNtxH0Ey8Ny1k,34939 +alembic/autogenerate/rewriter.py,sha256=uZWRkTYJoncoEJ5WY1QBRiozjyChqZDJPy4LtcRibjM,7846 +alembic/command.py,sha256=jWFNS-wPWA-Klfm0GsPfWh_8sPj4n7rKROJ0zrwhoR0,21712 +alembic/config.py,sha256=I12lm4V-AXSt-7nvub-Vtx5Zfa68pYP5xSrFQQd45rQ,22256 +alembic/context.py,sha256=hK1AJOQXJ29Bhn276GYcosxeG7pC5aZRT5E8c4bMJ4Q,195 +alembic/context.pyi,sha256=hUHbSnbSeEEMVkk0gDSXOq4_9edSjYzsjmmf-mL9Iao,31737 +alembic/ddl/__init__.py,sha256=Df8fy4Vn_abP8B7q3x8gyFwEwnLw6hs2Ljt_bV3EZWE,152 +alembic/ddl/_autogen.py,sha256=0no9ywWP8gjvO57Ozc2naab4qNusVNn2fiJekjc275g,9179 +alembic/ddl/base.py,sha256=Jd7oPoAOGjOMcdMUIzSKnTjd8NKnTd7IjBXXyVpDCkU,9955 +alembic/ddl/impl.py,sha256=vkhkXFpLPJBG9jW2kv_sR5CC5czNd1ERLjLtfLuOFP0,28778 +alembic/ddl/mssql.py,sha256=ydvgBSaftKYjaBaMyqius66Ta4CICQSj79Og3Ed2atY,14219 +alembic/ddl/mysql.py,sha256=am221U_UK3wX33tNcXNiOObZV-Pa4CXuv7vN-epF9IU,16788 +alembic/ddl/oracle.py,sha256=TmoCq_FlbfyWAAk3e_q6mMQU0YmlfIcgKHpRfNMmgr0,6211 +alembic/ddl/postgresql.py,sha256=dcWLdDSqivzizVCce_H6RnOVAayPXDFfns-NC4-UaA8,29842 +alembic/ddl/sqlite.py,sha256=wLXhb8bJWRspKQTb-iVfepR4LXYgOuEbUWKX5qwDhIQ,7570 +alembic/environment.py,sha256=MM5lPayGT04H3aeng1H7GQ8HEAs3VGX5yy6mDLCPLT4,43 +alembic/migration.py,sha256=MV6Fju6rZtn2fTREKzXrCZM6aIBGII4OMZFix0X-GLs,41 +alembic/op.py,sha256=flHtcsVqOD-ZgZKK2pv-CJ5Cwh-KJ7puMUNXzishxLw,167 +alembic/op.pyi,sha256=8R6SJr1dQharU0blmMJJj23XFO_hk9ZmAQBJBQOeXRU,49816 +alembic/operations/__init__.py,sha256=e0KQSZAgLpTWvyvreB7DWg7RJV_MWSOPVDgCqsd2FzY,318 +alembic/operations/base.py,sha256=LCx4NH5NA2NLWQFaZTqE_p2KgLtqJ76oVcp1Grj-zFM,74004 +alembic/operations/batch.py,sha256=YqtD4hJ3_RkFxvI7zbmBwxcLEyLHYyWQpsz4l5L85yI,26943 +alembic/operations/ops.py,sha256=2vtYFhYFDEVq158HwORfGTsobDM7c-t0lewuR7JKw7g,94353 +alembic/operations/schemaobj.py,sha256=vjoD57QvjbzzA-jyPIXulbOmb5_bGPtxoq58KsKI4Y0,9424 +alembic/operations/toimpl.py,sha256=SoNY2_gZX2baXTD-pNjpCWnON8D2QBSYQBIjo13-WKA,7115 +alembic/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +alembic/runtime/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +alembic/runtime/environment.py,sha256=9wSJaePNAXBXvirif_85ql7dSq4bXM1E6pSb2k-6uGI,41508 +alembic/runtime/migration.py,sha256=Yfv2fa11wiQ0WgoZaFldlWxCPq4dVDOCEOxST_-1VB0,50066 +alembic/script/__init__.py,sha256=lSj06O391Iy5avWAiq8SPs6N8RBgxkSPjP8wpXcNDGg,100 +alembic/script/base.py,sha256=4gkppn2FKCYDoBgzGkTslcwdyxSabmHvGq0uGKulwbI,37586 +alembic/script/revision.py,sha256=sfnXQw2UwiXs0E6gEPHBKWuSsB5KyuxZPTrFn__hIEk,62060 +alembic/script/write_hooks.py,sha256=NGB6NGgfdf7HK6XNNpSKqUCfzxazj-NRUePgFx7MJSM,5036 +alembic/templates/async/README,sha256=ISVtAOvqvKk_5ThM5ioJE-lMkvf9IbknFUFVU_vPma4,58 +alembic/templates/async/alembic.ini.mako,sha256=uuhJETLWQuiYcs_jAOXHEjshEJ7VslEc1q4RRj0HWbE,3525 +alembic/templates/async/env.py,sha256=zbOCf3Y7w2lg92hxSwmG1MM_7y56i_oRH4AKp0pQBYo,2389 +alembic/templates/async/script.py.mako,sha256=MEqL-2qATlST9TAOeYgscMn1uy6HUS9NFvDgl93dMj8,635 +alembic/templates/generic/README,sha256=MVlc9TYmr57RbhXET6QxgyCcwWP7w-vLkEsirENqiIQ,38 +alembic/templates/generic/alembic.ini.mako,sha256=sT7F852yN3c8X1-GKFlhuWExXxw9hY1eb1ZZ9flFSzc,3634 +alembic/templates/generic/env.py,sha256=TLRWOVW3Xpt_Tpf8JFzlnoPn_qoUu8UV77Y4o9XD6yI,2103 +alembic/templates/generic/script.py.mako,sha256=MEqL-2qATlST9TAOeYgscMn1uy6HUS9NFvDgl93dMj8,635 +alembic/templates/multidb/README,sha256=dWLDhnBgphA4Nzb7sNlMfCS3_06YqVbHhz-9O5JNqyI,606 +alembic/templates/multidb/alembic.ini.mako,sha256=mPh8JFJfWiGs6tMtL8_HAQ-Dz1QOoJgE5Vm76nIMqgU,3728 +alembic/templates/multidb/env.py,sha256=6zNjnW8mXGUk7erTsAvrfhvqoczJ-gagjVq1Ypg2YIQ,4230 +alembic/templates/multidb/script.py.mako,sha256=N06nMtNSwHkgl0EBXDyMt8njp9tlOesR583gfq21nbY,1090 +alembic/testing/__init__.py,sha256=kOxOh5nwmui9d-_CCq9WA4Udwy7ITjm453w74CTLZDo,1159 +alembic/testing/assertions.py,sha256=1CbJk8c8-WO9eJ0XJ0jJvMsNRLUrXV41NOeIJUAlOBk,5015 +alembic/testing/env.py,sha256=zJacVb_z6uLs2U1TtkmnFH9P3_F-3IfYbVv4UEPOvfo,10754 +alembic/testing/fixtures.py,sha256=NyP4wE_dFN9ZzSGiBagRu1cdzkka03nwJYJYHYrrkSY,9112 +alembic/testing/plugin/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +alembic/testing/plugin/bootstrap.py,sha256=9C6wtjGrIVztZ928w27hsQE0KcjDLIUtUN3dvZKsMVk,50 +alembic/testing/requirements.py,sha256=dKeAO1l5TwBqXarJN-IPORlCqCJv-41Dj6oXoEikxHQ,5133 +alembic/testing/schemacompare.py,sha256=N5UqSNCOJetIKC4vKhpYzQEpj08XkdgIoqBmEPQ3tlc,4838 +alembic/testing/suite/__init__.py,sha256=MvE7-hwbaVN1q3NM-ztGxORU9dnIelUCINKqNxewn7Y,288 +alembic/testing/suite/_autogen_fixtures.py,sha256=cDq1pmzHe15S6dZPGNC6sqFaCQ3hLT_oPV2IDigUGQ0,9880 +alembic/testing/suite/test_autogen_comments.py,sha256=aEGqKUDw4kHjnDk298aoGcQvXJWmZXcIX_2FxH4cJK8,6283 +alembic/testing/suite/test_autogen_computed.py,sha256=qJeBpc8urnwTFvbwWrSTIbHVkRUuCXP-dKaNbUK2U2U,6077 +alembic/testing/suite/test_autogen_diffs.py,sha256=T4SR1n_kmcOKYhR4W1-dA0e5sddJ69DSVL2HW96kAkE,8394 +alembic/testing/suite/test_autogen_fks.py,sha256=AqFmb26Buex167HYa9dZWOk8x-JlB1OK3bwcvvjDFaU,32927 +alembic/testing/suite/test_autogen_identity.py,sha256=kcuqngG7qXAKPJDX4U8sRzPKHEJECHuZ0DtuaS6tVkk,5824 +alembic/testing/suite/test_environment.py,sha256=w9F0xnLEbALeR8k6_-Tz6JHvy91IqiTSypNasVzXfZQ,11877 +alembic/testing/suite/test_op.py,sha256=2XQCdm_NmnPxHGuGj7hmxMzIhKxXNotUsKdACXzE1mM,1343 +alembic/testing/util.py,sha256=CQrcQDA8fs_7ME85z5ydb-Bt70soIIID-qNY1vbR2dg,3350 +alembic/testing/warnings.py,sha256=RxA7x_8GseANgw07Us8JN_1iGbANxaw6_VitX2ZGQH4,1078 +alembic/util/__init__.py,sha256=KSZ7UT2YzH6CietgUtljVoE3QnGjoFKOi7RL5sgUxrk,1688 +alembic/util/compat.py,sha256=RjHdQa1NomU3Zlvgfvza0OMiSRQSLRL3xVl3OdUy2UE,2594 +alembic/util/editor.py,sha256=JIz6_BdgV8_oKtnheR6DZoB7qnrHrlRgWjx09AsTsUw,2546 +alembic/util/exc.py,sha256=KQTru4zcgAmN4IxLMwLFS56XToUewaXB7oOLcPNjPwg,98 +alembic/util/langhelpers.py,sha256=KYyOjFjJ26evPmrwhdTvLQNXN0bK7AIy5sRdKD91Fvg,10038 +alembic/util/messaging.py,sha256=BM5OCZ6qmLftFRw5yPSxj539_QmfVwNYoU8qYsDqoJY,3132 +alembic/util/pyfiles.py,sha256=zltVdcwEJJCPS2gHsQvkHkQakuF6wXiZ6zfwHbGNT0g,3489 +alembic/util/sqla_compat.py,sha256=toD1S63PgZ6iEteP9bwIf5E7DIUdQPo0UQ_Fn18qWnI,19536 diff --git a/libs/alembic-1.13.1.dist-info/REQUESTED b/libs/alembic-1.13.1.dist-info/REQUESTED new file mode 100644 index 000000000..e69de29bb diff --git a/libs/alembic-1.13.1.dist-info/WHEEL b/libs/alembic-1.13.1.dist-info/WHEEL new file mode 100644 index 000000000..98c0d20b7 --- /dev/null +++ b/libs/alembic-1.13.1.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.42.0) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/libs/alembic-1.13.1.dist-info/entry_points.txt b/libs/alembic-1.13.1.dist-info/entry_points.txt new file mode 100644 index 000000000..594526817 --- /dev/null +++ b/libs/alembic-1.13.1.dist-info/entry_points.txt @@ -0,0 +1,2 @@ +[console_scripts] +alembic = alembic.config:main diff --git a/libs/alembic-1.13.1.dist-info/top_level.txt b/libs/alembic-1.13.1.dist-info/top_level.txt new file mode 100644 index 000000000..b5bd98d32 --- /dev/null +++ b/libs/alembic-1.13.1.dist-info/top_level.txt @@ -0,0 +1 @@ +alembic diff --git a/libs/alembic/__init__.py b/libs/alembic/__init__.py index 72201f70f..c153c8aaf 100644 --- a/libs/alembic/__init__.py +++ b/libs/alembic/__init__.py @@ -1,6 +1,4 @@ -import sys - from . import context from . import op -__version__ = "1.10.3" +__version__ = "1.13.1" diff --git a/libs/alembic/autogenerate/__init__.py b/libs/alembic/autogenerate/__init__.py index cd2ed1c15..445ddb251 100644 --- a/libs/alembic/autogenerate/__init__.py +++ b/libs/alembic/autogenerate/__init__.py @@ -1,10 +1,10 @@ -from .api import _render_migration_diffs -from .api import compare_metadata -from .api import produce_migrations -from .api import render_python_code -from .api import RevisionContext -from .compare import _produce_net_changes -from .compare import comparators -from .render import render_op_text -from .render import renderers -from .rewriter import Rewriter +from .api import _render_migration_diffs as _render_migration_diffs +from .api import compare_metadata as compare_metadata +from .api import produce_migrations as produce_migrations +from .api import render_python_code as render_python_code +from .api import RevisionContext as RevisionContext +from .compare import _produce_net_changes as _produce_net_changes +from .compare import comparators as comparators +from .render import render_op_text as render_op_text +from .render import renderers as renderers +from .rewriter import Rewriter as Rewriter diff --git a/libs/alembic/autogenerate/api.py b/libs/alembic/autogenerate/api.py index d7a0913d6..aa8f32f65 100644 --- a/libs/alembic/autogenerate/api.py +++ b/libs/alembic/autogenerate/api.py @@ -2,12 +2,12 @@ from __future__ import annotations import contextlib from typing import Any -from typing import Callable from typing import Dict from typing import Iterator +from typing import List from typing import Optional +from typing import Sequence from typing import Set -from typing import Tuple from typing import TYPE_CHECKING from typing import Union @@ -17,6 +17,7 @@ from . import compare from . import render from .. import util from ..operations import ops +from ..util import sqla_compat """Provide the 'autogenerate' feature which can produce migration operations automatically.""" @@ -25,19 +26,22 @@ if TYPE_CHECKING: from sqlalchemy.engine import Connection from sqlalchemy.engine import Dialect from sqlalchemy.engine import Inspector - from sqlalchemy.sql.schema import Column - from sqlalchemy.sql.schema import ForeignKeyConstraint - from sqlalchemy.sql.schema import Index from sqlalchemy.sql.schema import MetaData + from sqlalchemy.sql.schema import SchemaItem from sqlalchemy.sql.schema import Table - from sqlalchemy.sql.schema import UniqueConstraint - from alembic.config import Config - from alembic.operations.ops import MigrationScript - from alembic.operations.ops import UpgradeOps - from alembic.runtime.migration import MigrationContext - from alembic.script.base import Script - from alembic.script.base import ScriptDirectory + from ..config import Config + from ..operations.ops import DowngradeOps + from ..operations.ops import MigrationScript + from ..operations.ops import UpgradeOps + from ..runtime.environment import NameFilterParentNames + from ..runtime.environment import NameFilterType + from ..runtime.environment import ProcessRevisionDirectiveFn + from ..runtime.environment import RenderItemFn + from ..runtime.migration import MigrationContext + from ..script.base import Script + from ..script.base import ScriptDirectory + from ..script.revision import _GetRevArg def compare_metadata(context: MigrationContext, metadata: MetaData) -> Any: @@ -57,36 +61,42 @@ def compare_metadata(context: MigrationContext, metadata: MetaData) -> Any: from alembic.migration import MigrationContext from alembic.autogenerate import compare_metadata - from sqlalchemy.schema import SchemaItem - from sqlalchemy.types import TypeEngine - from sqlalchemy import (create_engine, MetaData, Column, - Integer, String, Table, text) + from sqlalchemy import ( + create_engine, + MetaData, + Column, + Integer, + String, + Table, + text, + ) import pprint engine = create_engine("sqlite://") with engine.begin() as conn: - conn.execute(text(''' - create table foo ( - id integer not null primary key, - old_data varchar, - x integer - )''')) - - conn.execute(text(''' - create table bar ( - data varchar - )''')) + conn.execute( + text( + ''' + create table foo ( + id integer not null primary key, + old_data varchar, + x integer + ) + ''' + ) + ) + conn.execute(text("create table bar (data varchar)")) metadata = MetaData() - Table('foo', metadata, - Column('id', Integer, primary_key=True), - Column('data', Integer), - Column('x', Integer, nullable=False) - ) - Table('bat', metadata, - Column('info', String) + Table( + "foo", + metadata, + Column("id", Integer, primary_key=True), + Column("data", Integer), + Column("x", Integer, nullable=False), ) + Table("bat", metadata, Column("info", String)) mc = MigrationContext.configure(engine.connect()) @@ -95,29 +105,53 @@ def compare_metadata(context: MigrationContext, metadata: MetaData) -> Any: Output:: - [ ( 'add_table', - Table('bat', MetaData(bind=None), - Column('info', String(), table=), schema=None)), - ( 'remove_table', - Table(u'bar', MetaData(bind=None), - Column(u'data', VARCHAR(), table=), schema=None)), - ( 'add_column', - None, - 'foo', - Column('data', Integer(), table=)), - ( 'remove_column', - None, - 'foo', - Column(u'old_data', VARCHAR(), table=None)), - [ ( 'modify_nullable', - None, - 'foo', - u'x', - { 'existing_server_default': None, - 'existing_type': INTEGER()}, - True, - False)]] - + [ + ( + "add_table", + Table( + "bat", + MetaData(), + Column("info", String(), table=), + schema=None, + ), + ), + ( + "remove_table", + Table( + "bar", + MetaData(), + Column("data", VARCHAR(), table=), + schema=None, + ), + ), + ( + "add_column", + None, + "foo", + Column("data", Integer(), table=), + ), + [ + ( + "modify_nullable", + None, + "foo", + "x", + { + "existing_comment": None, + "existing_server_default": False, + "existing_type": INTEGER(), + }, + True, + False, + ) + ], + ( + "remove_column", + None, + "foo", + Column("old_data", VARCHAR(), table=), + ), + ] :param context: a :class:`.MigrationContext` instance. @@ -132,6 +166,7 @@ def compare_metadata(context: MigrationContext, metadata: MetaData) -> Any: """ migration_script = produce_migrations(context, metadata) + assert migration_script.upgrade_ops is not None return migration_script.upgrade_ops.as_diffs() @@ -167,13 +202,14 @@ def produce_migrations( def render_python_code( - up_or_down_op: UpgradeOps, + up_or_down_op: Union[UpgradeOps, DowngradeOps], sqlalchemy_module_prefix: str = "sa.", alembic_module_prefix: str = "op.", render_as_batch: bool = False, - imports: Tuple[str, ...] = (), - render_item: None = None, + imports: Sequence[str] = (), + render_item: Optional[RenderItemFn] = None, migration_context: Optional[MigrationContext] = None, + user_module_prefix: Optional[str] = None, ) -> str: """Render Python code given an :class:`.UpgradeOps` or :class:`.DowngradeOps` object. @@ -181,12 +217,24 @@ def render_python_code( This is a convenience function that can be used to test the autogenerate output of a user-defined :class:`.MigrationScript` structure. + :param up_or_down_op: :class:`.UpgradeOps` or :class:`.DowngradeOps` object + :param sqlalchemy_module_prefix: module prefix for SQLAlchemy objects + :param alembic_module_prefix: module prefix for Alembic constructs + :param render_as_batch: use "batch operations" style for rendering + :param imports: sequence of import symbols to add + :param render_item: callable to render items + :param migration_context: optional :class:`.MigrationContext` + :param user_module_prefix: optional string prefix for user-defined types + + .. versionadded:: 1.11.0 + """ opts = { "sqlalchemy_module_prefix": sqlalchemy_module_prefix, "alembic_module_prefix": alembic_module_prefix, "render_item": render_item, "render_as_batch": render_as_batch, + "user_module_prefix": user_module_prefix, } if migration_context is None: @@ -285,10 +333,9 @@ class AutogenContext: self, migration_context: MigrationContext, metadata: Optional[MetaData] = None, - opts: Optional[dict] = None, + opts: Optional[Dict[str, Any]] = None, autogenerate: bool = True, ) -> None: - if ( autogenerate and migration_context is not None @@ -359,8 +406,8 @@ class AutogenContext: def run_name_filters( self, name: Optional[str], - type_: str, - parent_names: Dict[str, Optional[str]], + type_: NameFilterType, + parent_names: NameFilterParentNames, ) -> bool: """Run the context's name filters and return True if the targets should be part of the autogenerate operation. @@ -388,7 +435,6 @@ class AutogenContext: parent_names["schema_qualified_table_name"] = table_name for fn in self._name_filters: - if not fn(name, type_, parent_names): return False else: @@ -396,17 +442,11 @@ class AutogenContext: def run_object_filters( self, - object_: Union[ - Table, - Index, - Column, - UniqueConstraint, - ForeignKeyConstraint, - ], - name: Optional[str], - type_: str, + object_: SchemaItem, + name: sqla_compat._ConstraintName, + type_: NameFilterType, reflected: bool, - compare_to: Optional[Union[Table, Index, Column, UniqueConstraint]], + compare_to: Optional[SchemaItem], ) -> bool: """Run the context's object filters and return True if the targets should be part of the autogenerate operation. @@ -427,7 +467,7 @@ class AutogenContext: run_filters = run_object_filters @util.memoized_property - def sorted_tables(self): + def sorted_tables(self) -> List[Table]: """Return an aggregate of the :attr:`.MetaData.sorted_tables` collection(s). @@ -443,7 +483,7 @@ class AutogenContext: return result @util.memoized_property - def table_key_to_table(self): + def table_key_to_table(self) -> Dict[str, Table]: """Return an aggregate of the :attr:`.MetaData.tables` dictionaries. The :attr:`.MetaData.tables` collection is a dictionary of table key @@ -454,7 +494,7 @@ class AutogenContext: objects contain the same table key, an exception is raised. """ - result = {} + result: Dict[str, Table] = {} for m in util.to_list(self.metadata): intersect = set(result).intersection(set(m.tables)) if intersect: @@ -472,12 +512,17 @@ class RevisionContext: """Maintains configuration and state that's specific to a revision file generation operation.""" + generated_revisions: List[MigrationScript] + process_revision_directives: Optional[ProcessRevisionDirectiveFn] + def __init__( self, config: Config, script_directory: ScriptDirectory, command_args: Dict[str, Any], - process_revision_directives: Optional[Callable] = None, + process_revision_directives: Optional[ + ProcessRevisionDirectiveFn + ] = None, ) -> None: self.config = config self.script_directory = script_directory @@ -520,18 +565,18 @@ class RevisionContext: ) def run_autogenerate( - self, rev: tuple, migration_context: MigrationContext + self, rev: _GetRevArg, migration_context: MigrationContext ) -> None: self._run_environment(rev, migration_context, True) def run_no_autogenerate( - self, rev: tuple, migration_context: MigrationContext + self, rev: _GetRevArg, migration_context: MigrationContext ) -> None: self._run_environment(rev, migration_context, False) def _run_environment( self, - rev: tuple, + rev: _GetRevArg, migration_context: MigrationContext, autogenerate: bool, ) -> None: diff --git a/libs/alembic/autogenerate/compare.py b/libs/alembic/autogenerate/compare.py index 4f5126f53..fcef531a5 100644 --- a/libs/alembic/autogenerate/compare.py +++ b/libs/alembic/autogenerate/compare.py @@ -1,3 +1,6 @@ +# mypy: allow-untyped-defs, allow-incomplete-defs, allow-untyped-calls +# mypy: no-warn-return-any, allow-any-generics + from __future__ import annotations import contextlib @@ -7,11 +10,12 @@ from typing import Any from typing import cast from typing import Dict from typing import Iterator -from typing import List +from typing import Mapping from typing import Optional from typing import Set from typing import Tuple from typing import TYPE_CHECKING +from typing import TypeVar from typing import Union from sqlalchemy import event @@ -19,10 +23,15 @@ from sqlalchemy import inspect from sqlalchemy import schema as sa_schema from sqlalchemy import text from sqlalchemy import types as sqltypes +from sqlalchemy.sql import expression +from sqlalchemy.sql.schema import ForeignKeyConstraint +from sqlalchemy.sql.schema import Index +from sqlalchemy.sql.schema import UniqueConstraint from sqlalchemy.util import OrderedSet -from alembic.ddl.base import _fk_spec from .. import util +from ..ddl._autogen import is_index_sig +from ..ddl._autogen import is_uq_sig from ..operations import ops from ..util import sqla_compat @@ -33,10 +42,7 @@ if TYPE_CHECKING: from sqlalchemy.sql.elements import quoted_name from sqlalchemy.sql.elements import TextClause from sqlalchemy.sql.schema import Column - from sqlalchemy.sql.schema import ForeignKeyConstraint - from sqlalchemy.sql.schema import Index from sqlalchemy.sql.schema import Table - from sqlalchemy.sql.schema import UniqueConstraint from alembic.autogenerate.api import AutogenContext from alembic.ddl.impl import DefaultImpl @@ -44,6 +50,8 @@ if TYPE_CHECKING: from alembic.operations.ops import MigrationScript from alembic.operations.ops import ModifyTableOps from alembic.operations.ops import UpgradeOps + from ..ddl._autogen import _constraint_sig + log = logging.getLogger(__name__) @@ -64,7 +72,6 @@ comparators = util.Dispatcher(uselist=True) def _produce_net_changes( autogen_context: AutogenContext, upgrade_ops: UpgradeOps ) -> None: - connection = autogen_context.connection assert connection is not None include_schemas = autogen_context.opts.get("include_schemas", False) @@ -143,7 +150,6 @@ def _compare_tables( upgrade_ops: UpgradeOps, autogen_context: AutogenContext, ) -> None: - default_schema = inspector.bind.dialect.default_schema_name # tables coming from the connection will not have "schema" @@ -210,9 +216,8 @@ def _compare_tables( (inspector), # fmt: on ) - sqla_compat._reflect_table(inspector, t, None) + sqla_compat._reflect_table(inspector, t) if autogen_context.run_object_filters(t, tname, "table", True, None): - modify_table_ops = ops.ModifyTableOps(tname, [], schema=s) comparators.dispatch("table")( @@ -241,7 +246,7 @@ def _compare_tables( _compat_autogen_column_reflect(inspector), # fmt: on ) - sqla_compat._reflect_table(inspector, t, None) + sqla_compat._reflect_table(inspector, t) conn_column_info[(s, tname)] = t for s, tname in sorted(existing_tables, key=lambda x: (x[0] or "", x[1])): @@ -253,7 +258,6 @@ def _compare_tables( if autogen_context.run_object_filters( metadata_table, tname, "table", False, conn_table ): - modify_table_ops = ops.ModifyTableOps(tname, [], schema=s) with _compare_columns( s, @@ -264,7 +268,6 @@ def _compare_tables( autogen_context, inspector, ): - comparators.dispatch("table")( autogen_context, modify_table_ops, @@ -278,18 +281,44 @@ def _compare_tables( upgrade_ops.ops.append(modify_table_ops) -def _make_index(params: Dict[str, Any], conn_table: Table) -> Optional[Index]: +_IndexColumnSortingOps: Mapping[str, Any] = util.immutabledict( + { + "asc": expression.asc, + "desc": expression.desc, + "nulls_first": expression.nullsfirst, + "nulls_last": expression.nullslast, + "nullsfirst": expression.nullsfirst, # 1_3 name + "nullslast": expression.nullslast, # 1_3 name + } +) + + +def _make_index( + impl: DefaultImpl, params: Dict[str, Any], conn_table: Table +) -> Optional[Index]: exprs: list[Union[Column[Any], TextClause]] = [] + sorting = params.get("column_sorting") + for num, col_name in enumerate(params["column_names"]): item: Union[Column[Any], TextClause] if col_name is None: assert "expressions" in params - item = text(params["expressions"][num]) + name = params["expressions"][num] + item = text(name) else: + name = col_name item = conn_table.c[col_name] + if sorting and name in sorting: + for operator in sorting[name]: + if operator in _IndexColumnSortingOps: + item = _IndexColumnSortingOps[operator](item) exprs.append(item) ix = sa_schema.Index( - params["name"], *exprs, unique=params["unique"], _table=conn_table + params["name"], + *exprs, + unique=params["unique"], + _table=conn_table, + **impl.adjust_reflected_dialect_options(params, "index"), ) if "duplicates_constraint" in params: ix.info["duplicates_constraint"] = params["duplicates_constraint"] @@ -297,11 +326,12 @@ def _make_index(params: Dict[str, Any], conn_table: Table) -> Optional[Index]: def _make_unique_constraint( - params: Dict[str, Any], conn_table: Table + impl: DefaultImpl, params: Dict[str, Any], conn_table: Table ) -> UniqueConstraint: uq = sa_schema.UniqueConstraint( *[conn_table.c[cname] for cname in params["column_names"]], name=params["name"], + **impl.adjust_reflected_dialect_options(params, "unique_constraint"), ) if "duplicates_index" in params: uq.info["duplicates_index"] = params["duplicates_index"] @@ -405,100 +435,7 @@ def _compare_columns( log.info("Detected removed column '%s.%s'", name, cname) -class _constraint_sig: - const: Union[UniqueConstraint, ForeignKeyConstraint, Index] - - def md_name_to_sql_name(self, context: AutogenContext) -> Optional[str]: - return sqla_compat._get_constraint_final_name( - self.const, context.dialect - ) - - def __eq__(self, other): - return self.const == other.const - - def __ne__(self, other): - return self.const != other.const - - def __hash__(self) -> int: - return hash(self.const) - - -class _uq_constraint_sig(_constraint_sig): - is_index = False - is_unique = True - - def __init__(self, const: UniqueConstraint) -> None: - self.const = const - self.name = const.name - self.sig = tuple(sorted([col.name for col in const.columns])) - - @property - def column_names(self) -> List[str]: - return [col.name for col in self.const.columns] - - -class _ix_constraint_sig(_constraint_sig): - is_index = True - - def __init__(self, const: Index, impl: DefaultImpl) -> None: - self.const = const - self.name = const.name - self.sig = impl.create_index_sig(const) - self.is_unique = bool(const.unique) - - def md_name_to_sql_name(self, context: AutogenContext) -> Optional[str]: - return sqla_compat._get_constraint_final_name( - self.const, context.dialect - ) - - @property - def column_names(self) -> Union[List[quoted_name], List[None]]: - return sqla_compat._get_index_column_names(self.const) - - -class _fk_constraint_sig(_constraint_sig): - def __init__( - self, const: ForeignKeyConstraint, include_options: bool = False - ) -> None: - self.const = const - self.name = const.name - - ( - self.source_schema, - self.source_table, - self.source_columns, - self.target_schema, - self.target_table, - self.target_columns, - onupdate, - ondelete, - deferrable, - initially, - ) = _fk_spec(const) - - self.sig: Tuple[Any, ...] = ( - self.source_schema, - self.source_table, - tuple(self.source_columns), - self.target_schema, - self.target_table, - tuple(self.target_columns), - ) - if include_options: - self.sig += ( - (None if onupdate.lower() == "no action" else onupdate.lower()) - if onupdate - else None, - (None if ondelete.lower() == "no action" else ondelete.lower()) - if ondelete - else None, - # convert initially + deferrable into one three-state value - "initially_deferrable" - if initially and initially.lower() == "deferred" - else "deferrable" - if deferrable - else "not deferrable", - ) +_C = TypeVar("_C", bound=Union[UniqueConstraint, ForeignKeyConstraint, Index]) @comparators.dispatch_for("table") @@ -510,10 +447,10 @@ def _compare_indexes_and_uniques( conn_table: Optional[Table], metadata_table: Optional[Table], ) -> None: - inspector = autogen_context.inspector is_create_table = conn_table is None is_drop_table = metadata_table is None + impl = autogen_context.migration_context.impl # 1a. get raw indexes and unique constraints from metadata ... if metadata_table is not None: @@ -535,32 +472,31 @@ def _compare_indexes_and_uniques( if conn_table is not None: # 1b. ... and from connection, if the table exists - if hasattr(inspector, "get_unique_constraints"): - try: - conn_uniques = inspector.get_unique_constraints( # type:ignore[assignment] # noqa - tname, schema=schema + try: + conn_uniques = inspector.get_unique_constraints( # type:ignore[assignment] # noqa + tname, schema=schema + ) + supports_unique_constraints = True + except NotImplementedError: + pass + except TypeError: + # number of arguments is off for the base + # method in SQLAlchemy due to the cache decorator + # not being present + pass + else: + conn_uniques = [ # type:ignore[assignment] + uq + for uq in conn_uniques + if autogen_context.run_name_filters( + uq["name"], + "unique_constraint", + {"table_name": tname, "schema_name": schema}, ) - supports_unique_constraints = True - except NotImplementedError: - pass - except TypeError: - # number of arguments is off for the base - # method in SQLAlchemy due to the cache decorator - # not being present - pass - else: - conn_uniques = [ # type:ignore[assignment] - uq - for uq in conn_uniques - if autogen_context.run_name_filters( - uq["name"], - "unique_constraint", - {"table_name": tname, "schema_name": schema}, - ) - ] - for uq in conn_uniques: - if uq.get("duplicates_index"): - unique_constraints_duplicate_unique_indexes = True + ] + for uq in conn_uniques: + if uq.get("duplicates_index"): + unique_constraints_duplicate_unique_indexes = True try: conn_indexes = inspector.get_indexes( # type:ignore[assignment] tname, schema=schema @@ -585,13 +521,15 @@ def _compare_indexes_and_uniques( conn_uniques = set() # type:ignore[assignment] else: conn_uniques = { # type:ignore[assignment] - _make_unique_constraint(uq_def, conn_table) + _make_unique_constraint(impl, uq_def, conn_table) for uq_def in conn_uniques } conn_indexes = { # type:ignore[assignment] index - for index in (_make_index(ix, conn_table) for ix in conn_indexes) + for index in ( + _make_index(impl, ix, conn_table) for ix in conn_indexes + ) if index is not None } @@ -605,12 +543,13 @@ def _compare_indexes_and_uniques( metadata_unique_constraints, metadata_indexes, autogen_context.dialect, + impl, ) # 3. give the dialect a chance to omit indexes and constraints that # we know are either added implicitly by the DB or that the DB # can't accurately report on - autogen_context.migration_context.impl.correct_for_autogen_constraints( + impl.correct_for_autogen_constraints( conn_uniques, # type: ignore[arg-type] conn_indexes, # type: ignore[arg-type] metadata_unique_constraints, @@ -622,30 +561,31 @@ def _compare_indexes_and_uniques( # Index and UniqueConstraint so we can easily work with them # interchangeably metadata_unique_constraints_sig = { - _uq_constraint_sig(uq) for uq in metadata_unique_constraints + impl._create_metadata_constraint_sig(uq) + for uq in metadata_unique_constraints } - impl = autogen_context.migration_context.impl metadata_indexes_sig = { - _ix_constraint_sig(ix, impl) for ix in metadata_indexes + impl._create_metadata_constraint_sig(ix) for ix in metadata_indexes } - conn_unique_constraints = {_uq_constraint_sig(uq) for uq in conn_uniques} + conn_unique_constraints = { + impl._create_reflected_constraint_sig(uq) for uq in conn_uniques + } - conn_indexes_sig = {_ix_constraint_sig(ix, impl) for ix in conn_indexes} + conn_indexes_sig = { + impl._create_reflected_constraint_sig(ix) for ix in conn_indexes + } # 5. index things by name, for those objects that have names metadata_names = { cast(str, c.md_name_to_sql_name(autogen_context)): c - for c in metadata_unique_constraints_sig.union( - metadata_indexes_sig # type:ignore[arg-type] - ) - if isinstance(c, _ix_constraint_sig) - or sqla_compat._constraint_is_named(c.const, autogen_context.dialect) + for c in metadata_unique_constraints_sig.union(metadata_indexes_sig) + if c.is_named } - conn_uniques_by_name: Dict[sqla_compat._ConstraintName, _uq_constraint_sig] - conn_indexes_by_name: Dict[sqla_compat._ConstraintName, _ix_constraint_sig] + conn_uniques_by_name: Dict[sqla_compat._ConstraintName, _constraint_sig] + conn_indexes_by_name: Dict[sqla_compat._ConstraintName, _constraint_sig] conn_uniques_by_name = {c.name: c for c in conn_unique_constraints} conn_indexes_by_name = {c.name: c for c in conn_indexes_sig} @@ -664,13 +604,12 @@ def _compare_indexes_and_uniques( # 6. index things by "column signature", to help with unnamed unique # constraints. - conn_uniques_by_sig = {uq.sig: uq for uq in conn_unique_constraints} + conn_uniques_by_sig = {uq.unnamed: uq for uq in conn_unique_constraints} metadata_uniques_by_sig = { - uq.sig: uq for uq in metadata_unique_constraints_sig + uq.unnamed: uq for uq in metadata_unique_constraints_sig } - metadata_indexes_by_sig = {ix.sig: ix for ix in metadata_indexes_sig} unnamed_metadata_uniques = { - uq.sig: uq + uq.unnamed: uq for uq in metadata_unique_constraints_sig if not sqla_compat._constraint_is_named( uq.const, autogen_context.dialect @@ -685,18 +624,18 @@ def _compare_indexes_and_uniques( # 4. The backend may double up indexes as unique constraints and # vice versa (e.g. MySQL, Postgresql) - def obj_added(obj): - if obj.is_index: + def obj_added(obj: _constraint_sig): + if is_index_sig(obj): if autogen_context.run_object_filters( obj.const, obj.name, "index", False, None ): modify_ops.ops.append(ops.CreateIndexOp.from_index(obj.const)) log.info( - "Detected added index '%s' on %s", + "Detected added index '%r' on '%s'", obj.name, - ", ".join(["'%s'" % obj.column_names]), + obj.column_names, ) - else: + elif is_uq_sig(obj): if not supports_unique_constraints: # can't report unique indexes as added if we don't # detect them @@ -711,13 +650,15 @@ def _compare_indexes_and_uniques( ops.AddConstraintOp.from_constraint(obj.const) ) log.info( - "Detected added unique constraint '%s' on %s", + "Detected added unique constraint %r on '%s'", obj.name, - ", ".join(["'%s'" % obj.column_names]), + obj.column_names, ) + else: + assert False - def obj_removed(obj): - if obj.is_index: + def obj_removed(obj: _constraint_sig): + if is_index_sig(obj): if obj.is_unique and not supports_unique_constraints: # many databases double up unique constraints # as unique indexes. without that list we can't @@ -728,10 +669,8 @@ def _compare_indexes_and_uniques( obj.const, obj.name, "index", True, None ): modify_ops.ops.append(ops.DropIndexOp.from_index(obj.const)) - log.info( - "Detected removed index '%s' on '%s'", obj.name, tname - ) - else: + log.info("Detected removed index %r on %r", obj.name, tname) + elif is_uq_sig(obj): if is_create_table or is_drop_table: # if the whole table is being dropped, we don't need to # consider unique constraint separately @@ -743,33 +682,40 @@ def _compare_indexes_and_uniques( ops.DropConstraintOp.from_constraint(obj.const) ) log.info( - "Detected removed unique constraint '%s' on '%s'", + "Detected removed unique constraint %r on %r", obj.name, tname, ) + else: + assert False + + def obj_changed( + old: _constraint_sig, + new: _constraint_sig, + msg: str, + ): + if is_index_sig(old): + assert is_index_sig(new) - def obj_changed(old, new, msg): - if old.is_index: if autogen_context.run_object_filters( new.const, new.name, "index", False, old.const ): log.info( - "Detected changed index '%s' on '%s':%s", - old.name, - tname, - ", ".join(msg), + "Detected changed index %r on %r: %s", old.name, tname, msg ) modify_ops.ops.append(ops.DropIndexOp.from_index(old.const)) modify_ops.ops.append(ops.CreateIndexOp.from_index(new.const)) - else: + elif is_uq_sig(old): + assert is_uq_sig(new) + if autogen_context.run_object_filters( new.const, new.name, "unique_constraint", False, old.const ): log.info( - "Detected changed unique constraint '%s' on '%s':%s", + "Detected changed unique constraint %r on %r: %s", old.name, tname, - ", ".join(msg), + msg, ) modify_ops.ops.append( ops.DropConstraintOp.from_constraint(old.const) @@ -777,19 +723,25 @@ def _compare_indexes_and_uniques( modify_ops.ops.append( ops.AddConstraintOp.from_constraint(new.const) ) + else: + assert False for removed_name in sorted(set(conn_names).difference(metadata_names)): - conn_obj: Union[_ix_constraint_sig, _uq_constraint_sig] = conn_names[ - removed_name - ] - if not conn_obj.is_index and conn_obj.sig in unnamed_metadata_uniques: + conn_obj = conn_names[removed_name] + if ( + is_uq_sig(conn_obj) + and conn_obj.unnamed in unnamed_metadata_uniques + ): continue elif removed_name in doubled_constraints: + conn_uq, conn_idx = doubled_constraints[removed_name] if ( - conn_obj.sig not in metadata_indexes_by_sig - and conn_obj.sig not in metadata_uniques_by_sig + all( + conn_idx.unnamed != meta_idx.unnamed + for meta_idx in metadata_indexes_sig + ) + and conn_uq.unnamed not in metadata_uniques_by_sig ): - conn_uq, conn_idx = doubled_constraints[removed_name] obj_removed(conn_uq) obj_removed(conn_idx) else: @@ -800,30 +752,36 @@ def _compare_indexes_and_uniques( if existing_name in doubled_constraints: conn_uq, conn_idx = doubled_constraints[existing_name] - if metadata_obj.is_index: + if is_index_sig(metadata_obj): conn_obj = conn_idx else: conn_obj = conn_uq else: conn_obj = conn_names[existing_name] - if conn_obj.is_index != metadata_obj.is_index: + if type(conn_obj) != type(metadata_obj): obj_removed(conn_obj) obj_added(metadata_obj) else: - msg = [] - if conn_obj.is_unique != metadata_obj.is_unique: - msg.append( - " unique=%r to unique=%r" - % (conn_obj.is_unique, metadata_obj.is_unique) - ) - if conn_obj.sig != metadata_obj.sig: - msg.append( - " expression %r to %r" % (conn_obj.sig, metadata_obj.sig) - ) + comparison = metadata_obj.compare_to_reflected(conn_obj) - if msg: - obj_changed(conn_obj, metadata_obj, msg) + if comparison.is_different: + # constraint are different + obj_changed(conn_obj, metadata_obj, comparison.message) + elif comparison.is_skip: + # constraint cannot be compared, skip them + thing = ( + "index" if is_index_sig(conn_obj) else "unique constraint" + ) + log.info( + "Cannot compare %s %r, assuming equal and skipping. %s", + thing, + conn_obj.name, + comparison.message, + ) + else: + # constraint are equal + assert comparison.is_equal for added_name in sorted(set(metadata_names).difference(conn_names)): obj = metadata_names[added_name] @@ -840,6 +798,7 @@ def _correct_for_uq_duplicates_uix( metadata_unique_constraints, metadata_indexes, dialect, + impl, ): # dedupe unique indexes vs. constraints, since MySQL / Oracle # doesn't really have unique constraints as a separate construct. @@ -862,7 +821,7 @@ def _correct_for_uq_duplicates_uix( } unnamed_metadata_uqs = { - _uq_constraint_sig(cons).sig + impl._create_metadata_constraint_sig(cons).unnamed for name, cons in metadata_cons_names if name is None } @@ -886,10 +845,11 @@ def _correct_for_uq_duplicates_uix( for overlap in uqs_dupe_indexes: if overlap not in metadata_uq_names: if ( - _uq_constraint_sig(uqs_dupe_indexes[overlap]).sig + impl._create_reflected_constraint_sig( + uqs_dupe_indexes[overlap] + ).unnamed not in unnamed_metadata_uqs ): - conn_unique_constraints.discard(uqs_dupe_indexes[overlap]) elif overlap not in metadata_ix_names: conn_indexes.discard(conn_ix_names[overlap]) @@ -902,10 +862,9 @@ def _compare_nullable( schema: Optional[str], tname: Union[quoted_name, str], cname: Union[quoted_name, str], - conn_col: Column, - metadata_col: Column, + conn_col: Column[Any], + metadata_col: Column[Any], ) -> None: - metadata_col_nullable = metadata_col.nullable conn_col_nullable = conn_col.nullable alter_column_op.existing_nullable = conn_col_nullable @@ -944,10 +903,9 @@ def _setup_autoincrement( schema: Optional[str], tname: Union[quoted_name, str], cname: quoted_name, - conn_col: Column, - metadata_col: Column, + conn_col: Column[Any], + metadata_col: Column[Any], ) -> None: - if metadata_col.table._autoincrement_column is metadata_col: alter_column_op.kw["autoincrement"] = True elif metadata_col.autoincrement is True: @@ -963,10 +921,9 @@ def _compare_type( schema: Optional[str], tname: Union[quoted_name, str], cname: Union[quoted_name, str], - conn_col: Column, - metadata_col: Column, + conn_col: Column[Any], + metadata_col: Column[Any], ) -> None: - conn_type = conn_col.type alter_column_op.existing_type = conn_type metadata_type = metadata_col.type @@ -1001,11 +958,8 @@ def _compare_type( def _render_server_default_for_compare( - metadata_default: Optional[Any], - metadata_col: Column, - autogen_context: AutogenContext, + metadata_default: Optional[Any], autogen_context: AutogenContext ) -> Optional[str]: - if isinstance(metadata_default, sa_schema.DefaultClause): if isinstance(metadata_default.arg, str): metadata_default = metadata_default.arg @@ -1017,11 +971,7 @@ def _render_server_default_for_compare( ) ) if isinstance(metadata_default, str): - if metadata_col.type._type_affinity is sqltypes.String: - metadata_default = re.sub(r"^'|'$", "", metadata_default) - return f"'{metadata_default}'" - else: - return metadata_default + return metadata_default else: return None @@ -1042,8 +992,8 @@ def _compare_computed_default( schema: Optional[str], tname: str, cname: str, - conn_col: Column, - metadata_col: Column, + conn_col: Column[Any], + metadata_col: Column[Any], ) -> None: rendered_metadata_default = str( cast(sa_schema.Computed, metadata_col.server_default).sqltext.compile( @@ -1108,10 +1058,9 @@ def _compare_server_default( schema: Optional[str], tname: Union[quoted_name, str], cname: Union[quoted_name, str], - conn_col: Column, - metadata_col: Column, + conn_col: Column[Any], + metadata_col: Column[Any], ) -> Optional[bool]: - metadata_default = metadata_col.server_default conn_col_default = conn_col.server_default if conn_col_default is None and metadata_default is None: @@ -1168,7 +1117,7 @@ def _compare_server_default( ) else: rendered_metadata_default = _render_server_default_for_compare( - metadata_default, metadata_col, autogen_context + metadata_default, autogen_context ) rendered_conn_default = ( @@ -1197,10 +1146,9 @@ def _compare_column_comment( schema: Optional[str], tname: Union[quoted_name, str], cname: quoted_name, - conn_col: Column, - metadata_col: Column, + conn_col: Column[Any], + metadata_col: Column[Any], ) -> Optional[Literal[False]]: - assert autogen_context.dialect is not None if not autogen_context.dialect.supports_comments: return None @@ -1225,10 +1173,9 @@ def _compare_foreign_keys( modify_table_ops: ModifyTableOps, schema: Optional[str], tname: Union[quoted_name, str], - conn_table: Optional[Table], - metadata_table: Optional[Table], + conn_table: Table, + metadata_table: Table, ) -> None: - # if we're doing CREATE TABLE, all FKs are created # inline within the table def if conn_table is None or metadata_table is None: @@ -1251,15 +1198,13 @@ def _compare_foreign_keys( ) ] - backend_reflects_fk_options = bool( - conn_fks_list and "options" in conn_fks_list[0] - ) - conn_fks = { _make_foreign_key(const, conn_table) # type: ignore[arg-type] for const in conn_fks_list } + impl = autogen_context.migration_context.impl + # give the dialect a chance to correct the FKs to match more # closely autogen_context.migration_context.impl.correct_for_autogen_foreignkeys( @@ -1267,17 +1212,24 @@ def _compare_foreign_keys( ) metadata_fks_sig = { - _fk_constraint_sig(fk, include_options=backend_reflects_fk_options) - for fk in metadata_fks + impl._create_metadata_constraint_sig(fk) for fk in metadata_fks } conn_fks_sig = { - _fk_constraint_sig(fk, include_options=backend_reflects_fk_options) - for fk in conn_fks + impl._create_reflected_constraint_sig(fk) for fk in conn_fks } - conn_fks_by_sig = {c.sig: c for c in conn_fks_sig} - metadata_fks_by_sig = {c.sig: c for c in metadata_fks_sig} + # check if reflected FKs include options, indicating the backend + # can reflect FK options + if conn_fks_list and "options" in conn_fks_list[0]: + conn_fks_by_sig = {c.unnamed: c for c in conn_fks_sig} + metadata_fks_by_sig = {c.unnamed: c for c in metadata_fks_sig} + else: + # otherwise compare by sig without options added + conn_fks_by_sig = {c.unnamed_no_options: c for c in conn_fks_sig} + metadata_fks_by_sig = { + c.unnamed_no_options: c for c in metadata_fks_sig + } metadata_fks_by_name = { c.name: c for c in metadata_fks_sig if c.name is not None @@ -1289,7 +1241,7 @@ def _compare_foreign_keys( obj.const, obj.name, "foreign_key_constraint", False, compare_to ): modify_table_ops.ops.append( - ops.CreateForeignKeyOp.from_constraint(const.const) + ops.CreateForeignKeyOp.from_constraint(const.const) # type: ignore[has-type] # noqa: E501 ) log.info( @@ -1348,7 +1300,6 @@ def _compare_table_comment( conn_table: Optional[Table], metadata_table: Optional[Table], ) -> None: - assert autogen_context.dialect is not None if not autogen_context.dialect.supports_comments: return diff --git a/libs/alembic/autogenerate/render.py b/libs/alembic/autogenerate/render.py index dc841f83e..317a6dbed 100644 --- a/libs/alembic/autogenerate/render.py +++ b/libs/alembic/autogenerate/render.py @@ -1,6 +1,8 @@ +# mypy: allow-untyped-defs, allow-incomplete-defs, allow-untyped-calls +# mypy: no-warn-return-any, allow-any-generics + from __future__ import annotations -from collections import OrderedDict from io import StringIO import re from typing import Any @@ -26,6 +28,7 @@ from ..util import sqla_compat if TYPE_CHECKING: from typing import Literal + from sqlalchemy.sql.base import DialectKWArgs from sqlalchemy.sql.elements import ColumnElement from sqlalchemy.sql.elements import TextClause from sqlalchemy.sql.schema import CheckConstraint @@ -94,7 +97,6 @@ def _render_cmd_body( op_container: ops.OpContainer, autogen_context: AutogenContext, ) -> str: - buf = StringIO() printer = PythonPrinter(buf) @@ -165,15 +167,22 @@ def _render_modify_table( def _render_create_table_comment( autogen_context: AutogenContext, op: ops.CreateTableCommentOp ) -> str: - - templ = ( - "{prefix}create_table_comment(\n" - "{indent}'{tname}',\n" - "{indent}{comment},\n" - "{indent}existing_comment={existing},\n" - "{indent}schema={schema}\n" - ")" - ) + if autogen_context._has_batch: + templ = ( + "{prefix}create_table_comment(\n" + "{indent}{comment},\n" + "{indent}existing_comment={existing}\n" + ")" + ) + else: + templ = ( + "{prefix}create_table_comment(\n" + "{indent}'{tname}',\n" + "{indent}{comment},\n" + "{indent}existing_comment={existing},\n" + "{indent}schema={schema}\n" + ")" + ) return templ.format( prefix=_alembic_autogenerate_prefix(autogen_context), tname=op.table_name, @@ -190,14 +199,20 @@ def _render_create_table_comment( def _render_drop_table_comment( autogen_context: AutogenContext, op: ops.DropTableCommentOp ) -> str: - - templ = ( - "{prefix}drop_table_comment(\n" - "{indent}'{tname}',\n" - "{indent}existing_comment={existing},\n" - "{indent}schema={schema}\n" - ")" - ) + if autogen_context._has_batch: + templ = ( + "{prefix}drop_table_comment(\n" + "{indent}existing_comment={existing}\n" + ")" + ) + else: + templ = ( + "{prefix}drop_table_comment(\n" + "{indent}'{tname}',\n" + "{indent}existing_comment={existing},\n" + "{indent}schema={schema}\n" + ")" + ) return templ.format( prefix=_alembic_autogenerate_prefix(autogen_context), tname=op.table_name, @@ -248,6 +263,11 @@ def _add_table(autogen_context: AutogenContext, op: ops.CreateTableOp) -> str: comment = table.comment if comment: text += ",\ncomment=%r" % _ident(comment) + + info = table.info + if info: + text += f",\ninfo={info!r}" + for k in sorted(op.kw): text += ",\n%s=%r" % (k.replace(" ", "_"), op.kw[k]) @@ -271,6 +291,15 @@ def _drop_table(autogen_context: AutogenContext, op: ops.DropTableOp) -> str: return text +def _render_dialect_kwargs_items( + autogen_context: AutogenContext, item: DialectKWArgs +) -> list[str]: + return [ + f"{key}={_render_potential_expr(val, autogen_context)}" + for key, val in item.dialect_kwargs.items() + ] + + @renderers.dispatch_for(ops.CreateIndexOp) def _add_index(autogen_context: AutogenContext, op: ops.CreateIndexOp) -> str: index = op.to_index() @@ -289,6 +318,8 @@ def _add_index(autogen_context: AutogenContext, op: ops.CreateIndexOp) -> str: ) assert index.table is not None + + opts = _render_dialect_kwargs_items(autogen_context, index) text = tmpl % { "prefix": _alembic_autogenerate_prefix(autogen_context), "name": _render_gen_name(autogen_context, index.name), @@ -300,18 +331,7 @@ def _add_index(autogen_context: AutogenContext, op: ops.CreateIndexOp) -> str: "schema": (", schema=%r" % _ident(index.table.schema)) if index.table.schema else "", - "kwargs": ( - ", " - + ", ".join( - [ - "%s=%s" - % (key, _render_potential_expr(val, autogen_context)) - for key, val in index.kwargs.items() - ] - ) - ) - if len(index.kwargs) - else "", + "kwargs": ", " + ", ".join(opts) if opts else "", } return text @@ -329,24 +349,13 @@ def _drop_index(autogen_context: AutogenContext, op: ops.DropIndexOp) -> str: "%(prefix)sdrop_index(%(name)r, " "table_name=%(table_name)r%(schema)s%(kwargs)s)" ) - + opts = _render_dialect_kwargs_items(autogen_context, index) text = tmpl % { "prefix": _alembic_autogenerate_prefix(autogen_context), "name": _render_gen_name(autogen_context, op.index_name), "table_name": _ident(op.table_name), "schema": ((", schema=%r" % _ident(op.schema)) if op.schema else ""), - "kwargs": ( - ", " - + ", ".join( - [ - "%s=%s" - % (key, _render_potential_expr(val, autogen_context)) - for key, val in index.kwargs.items() - ] - ) - ) - if len(index.kwargs) - else "", + "kwargs": ", " + ", ".join(opts) if opts else "", } return text @@ -362,7 +371,6 @@ def _add_unique_constraint( def _add_fk_constraint( autogen_context: AutogenContext, op: ops.CreateForeignKeyOp ) -> str: - args = [repr(_render_gen_name(autogen_context, op.constraint_name))] if not autogen_context._has_batch: args.append(repr(_ident(op.source_table))) @@ -381,6 +389,7 @@ def _add_fk_constraint( "initially", "deferrable", "use_alter", + "match", ] if not autogen_context._has_batch: kwargs.insert(0, "source_schema") @@ -411,28 +420,25 @@ def _add_check_constraint(constraint, autogen_context): def _drop_constraint( autogen_context: AutogenContext, op: ops.DropConstraintOp ) -> str: + prefix = _alembic_autogenerate_prefix(autogen_context) + name = _render_gen_name(autogen_context, op.constraint_name) + schema = _ident(op.schema) if op.schema else None + type_ = _ident(op.constraint_type) if op.constraint_type else None - if autogen_context._has_batch: - template = "%(prefix)sdrop_constraint" "(%(name)r, type_=%(type)r)" - else: - template = ( - "%(prefix)sdrop_constraint" - "(%(name)r, '%(table_name)s'%(schema)s, type_=%(type)r)" - ) + params_strs = [] + params_strs.append(repr(name)) + if not autogen_context._has_batch: + params_strs.append(repr(_ident(op.table_name))) + if schema is not None: + params_strs.append(f"schema={schema!r}") + if type_ is not None: + params_strs.append(f"type_={type_!r}") - text = template % { - "prefix": _alembic_autogenerate_prefix(autogen_context), - "name": _render_gen_name(autogen_context, op.constraint_name), - "table_name": _ident(op.table_name), - "type": op.constraint_type, - "schema": (", schema=%r" % _ident(op.schema)) if op.schema else "", - } - return text + return f"{prefix}drop_constraint({', '.join(params_strs)})" @renderers.dispatch_for(ops.AddColumnOp) def _add_column(autogen_context: AutogenContext, op: ops.AddColumnOp) -> str: - schema, tname, column = op.schema, op.table_name, op.column if autogen_context._has_batch: template = "%(prefix)sadd_column(%(column)s)" @@ -452,7 +458,6 @@ def _add_column(autogen_context: AutogenContext, op: ops.AddColumnOp) -> str: @renderers.dispatch_for(ops.DropColumnOp) def _drop_column(autogen_context: AutogenContext, op: ops.DropColumnOp) -> str: - schema, tname, column_name = op.schema, op.table_name, op.column_name if autogen_context._has_batch: @@ -476,7 +481,6 @@ def _drop_column(autogen_context: AutogenContext, op: ops.DropColumnOp) -> str: def _alter_column( autogen_context: AutogenContext, op: ops.AlterColumnOp ) -> str: - tname = op.table_name cname = op.column_name server_default = op.modify_server_default @@ -562,11 +566,12 @@ def _ident(name: Optional[Union[quoted_name, str]]) -> Optional[str]: def _render_potential_expr( value: Any, autogen_context: AutogenContext, + *, wrap_in_text: bool = True, is_server_default: bool = False, + is_index: bool = False, ) -> str: if isinstance(value, sql.ClauseElement): - if wrap_in_text: template = "%(prefix)stext(%(sql)r)" else: @@ -575,7 +580,7 @@ def _render_potential_expr( return template % { "prefix": _sqlalchemy_autogenerate_prefix(autogen_context), "sql": autogen_context.migration_context.impl.render_ddl_sql_expr( - value, is_server_default=is_server_default + value, is_server_default=is_server_default, is_index=is_index ), } @@ -589,7 +594,7 @@ def _get_index_rendered_expressions( return [ repr(_ident(getattr(exp, "name", None))) if isinstance(exp, sa_schema.Column) - else _render_potential_expr(exp, autogen_context) + else _render_potential_expr(exp, autogen_context, is_index=True) for exp in idx.expressions ] @@ -613,6 +618,7 @@ def _uq_constraint( opts.append( ("name", _render_gen_name(autogen_context, constraint.name)) ) + dialect_options = _render_dialect_kwargs_items(autogen_context, constraint) if alter: args = [repr(_render_gen_name(autogen_context, constraint.name))] @@ -620,6 +626,7 @@ def _uq_constraint( args += [repr(_ident(constraint.table.name))] args.append(repr([_ident(col.name) for col in constraint.columns])) args.extend(["%s=%r" % (k, v) for k, v in opts]) + args.extend(dialect_options) return "%(prefix)screate_unique_constraint(%(args)s)" % { "prefix": _alembic_autogenerate_prefix(autogen_context), "args": ", ".join(args), @@ -627,6 +634,7 @@ def _uq_constraint( else: args = [repr(_ident(col.name)) for col in constraint.columns] args.extend(["%s=%r" % (k, v) for k, v in opts]) + args.extend(dialect_options) return "%(prefix)sUniqueConstraint(%(args)s)" % { "prefix": _sqlalchemy_autogenerate_prefix(autogen_context), "args": ", ".join(args), @@ -664,7 +672,9 @@ def _user_defined_render( return False -def _render_column(column: Column, autogen_context: AutogenContext) -> str: +def _render_column( + column: Column[Any], autogen_context: AutogenContext +) -> str: rendered = _user_defined_render("column", column, autogen_context) if rendered is not False: return rendered @@ -673,7 +683,6 @@ def _render_column(column: Column, autogen_context: AutogenContext) -> str: opts: List[Tuple[str, Any]] = [] if column.server_default: - rendered = _render_server_default( # type:ignore[assignment] column.server_default, autogen_context ) @@ -727,7 +736,9 @@ def _should_render_server_default_positionally(server_default: Any) -> bool: def _render_server_default( - default: Optional[Union[FetchedValue, str, TextClause, ColumnElement]], + default: Optional[ + Union[FetchedValue, str, TextClause, ColumnElement[Any]] + ], autogen_context: AutogenContext, repr_: bool = True, ) -> Optional[str]: @@ -773,11 +784,9 @@ def _render_computed( def _render_identity( identity: Identity, autogen_context: AutogenContext ) -> str: - # always=None means something different than always=False - kwargs = OrderedDict(always=identity.always) - if identity.on_null is not None: - kwargs["on_null"] = identity.on_null - kwargs.update(_get_identity_options(identity)) + kwargs = sqla_compat._get_identity_options_dict( + identity, dialect_kwargs=True + ) return "%(prefix)sIdentity(%(kwargs)s)" % { "prefix": _sqlalchemy_autogenerate_prefix(autogen_context), @@ -785,15 +794,6 @@ def _render_identity( } -def _get_identity_options(identity_options: Identity) -> OrderedDict: - kwargs = OrderedDict() - for attr in sqla_compat._identity_options_attrs: - value = getattr(identity_options, attr, None) - if value is not None: - kwargs[attr] = value - return kwargs - - def _repr_type( type_: TypeEngine, autogen_context: AutogenContext, @@ -852,7 +852,7 @@ def _render_Variant_type( ) -> str: base_type, variant_mapping = sqla_compat._get_variant_mapping(type_) base = _repr_type(base_type, autogen_context, _skip_variants=True) - assert base is not None and base is not False + assert base is not None and base is not False # type: ignore[comparison-overlap] # noqa:E501 for dialect in sorted(variant_mapping): typ = variant_mapping[dialect] base += ".with_variant(%s, %r)" % ( @@ -949,7 +949,7 @@ def _fk_colspec( won't fail if the remote table can't be resolved. """ - colspec = fk._get_colspec() # type:ignore[attr-defined] + colspec = fk._get_colspec() tokens = colspec.split(".") tname, colname = tokens[-2:] @@ -980,7 +980,6 @@ def _fk_colspec( def _populate_render_fk_opts( constraint: ForeignKeyConstraint, opts: List[Tuple[str, str]] ) -> None: - if constraint.onupdate: opts.append(("onupdate", repr(constraint.onupdate))) if constraint.ondelete: @@ -991,6 +990,8 @@ def _populate_render_fk_opts( opts.append(("deferrable", repr(constraint.deferrable))) if constraint.use_alter: opts.append(("use_alter", repr(constraint.use_alter))) + if constraint.match: + opts.append(("match", repr(constraint.match))) @_constraint_renderers.dispatch_for(sa_schema.ForeignKeyConstraint) @@ -1018,8 +1019,7 @@ def _render_foreign_key( % { "prefix": _sqlalchemy_autogenerate_prefix(autogen_context), "cols": ", ".join( - "%r" % _ident(cast("Column", f.parent).name) - for f in constraint.elements + repr(_ident(f.parent.name)) for f in constraint.elements ), "refcols": ", ".join( repr(_fk_colspec(f, apply_metadata_schema, namespace_metadata)) @@ -1060,12 +1060,10 @@ def _render_check_constraint( # ideally SQLAlchemy would give us more of a first class # way to detect this. if ( - constraint._create_rule # type:ignore[attr-defined] - and hasattr( - constraint._create_rule, "target" # type:ignore[attr-defined] - ) + constraint._create_rule + and hasattr(constraint._create_rule, "target") and isinstance( - constraint._create_rule.target, # type:ignore[attr-defined] + constraint._create_rule.target, sqltypes.TypeEngine, ) ): diff --git a/libs/alembic/autogenerate/rewriter.py b/libs/alembic/autogenerate/rewriter.py index 1a29b963e..8994dcf82 100644 --- a/libs/alembic/autogenerate/rewriter.py +++ b/libs/alembic/autogenerate/rewriter.py @@ -4,24 +4,30 @@ from typing import Any from typing import Callable from typing import Iterator from typing import List -from typing import Optional +from typing import Tuple from typing import Type from typing import TYPE_CHECKING from typing import Union -from alembic import util -from alembic.operations import ops +from .. import util +from ..operations import ops if TYPE_CHECKING: - from alembic.operations.ops import AddColumnOp - from alembic.operations.ops import AlterColumnOp - from alembic.operations.ops import CreateTableOp - from alembic.operations.ops import MigrateOperation - from alembic.operations.ops import MigrationScript - from alembic.operations.ops import ModifyTableOps - from alembic.operations.ops import OpContainer - from alembic.runtime.migration import MigrationContext - from alembic.script.revision import Revision + from ..operations.ops import AddColumnOp + from ..operations.ops import AlterColumnOp + from ..operations.ops import CreateTableOp + from ..operations.ops import DowngradeOps + from ..operations.ops import MigrateOperation + from ..operations.ops import MigrationScript + from ..operations.ops import ModifyTableOps + from ..operations.ops import OpContainer + from ..operations.ops import UpgradeOps + from ..runtime.migration import MigrationContext + from ..script.revision import _GetRevArg + +ProcessRevisionDirectiveFn = Callable[ + ["MigrationContext", "_GetRevArg", List["MigrationScript"]], None +] class Rewriter: @@ -52,33 +58,39 @@ class Rewriter: _traverse = util.Dispatcher() - _chained: Optional[Rewriter] = None + _chained: Tuple[Union[ProcessRevisionDirectiveFn, Rewriter], ...] = () def __init__(self) -> None: self.dispatch = util.Dispatcher() - def chain(self, other: Rewriter) -> Rewriter: + def chain( + self, + other: Union[ + ProcessRevisionDirectiveFn, + Rewriter, + ], + ) -> Rewriter: """Produce a "chain" of this :class:`.Rewriter` to another. - This allows two rewriters to operate serially on a stream, + This allows two or more rewriters to operate serially on a stream, e.g.:: writer1 = autogenerate.Rewriter() writer2 = autogenerate.Rewriter() + @writer1.rewrites(ops.AddColumnOp) def add_column_nullable(context, revision, op): op.column.nullable = True return op + @writer2.rewrites(ops.AddColumnOp) def add_column_idx(context, revision, op): idx_op = ops.CreateIndexOp( - 'ixc', op.table_name, [op.column.name]) - return [ - op, - idx_op - ] + "ixc", op.table_name, [op.column.name] + ) + return [op, idx_op] writer = writer1.chain(writer2) @@ -89,7 +101,7 @@ class Rewriter: """ wr = self.__class__.__new__(self.__class__) wr.__dict__.update(self.__dict__) - wr._chained = other + wr._chained += (other,) return wr def rewrites( @@ -101,7 +113,7 @@ class Rewriter: Type[CreateTableOp], Type[ModifyTableOps], ], - ) -> Callable: + ) -> Callable[..., Any]: """Register a function as rewriter for a given type. The function should receive three arguments, which are @@ -119,7 +131,7 @@ class Rewriter: def _rewrite( self, context: MigrationContext, - revision: Revision, + revision: _GetRevArg, directive: MigrateOperation, ) -> Iterator[MigrateOperation]: try: @@ -142,21 +154,21 @@ class Rewriter: def __call__( self, context: MigrationContext, - revision: Revision, + revision: _GetRevArg, directives: List[MigrationScript], ) -> None: self.process_revision_directives(context, revision, directives) - if self._chained: - self._chained(context, revision, directives) + for process_revision_directives in self._chained: + process_revision_directives(context, revision, directives) @_traverse.dispatch_for(ops.MigrationScript) def _traverse_script( self, context: MigrationContext, - revision: Revision, + revision: _GetRevArg, directive: MigrationScript, ) -> None: - upgrade_ops_list = [] + upgrade_ops_list: List[UpgradeOps] = [] for upgrade_ops in directive.upgrade_ops_list: ret = self._traverse_for(context, revision, upgrade_ops) if len(ret) != 1: @@ -164,9 +176,10 @@ class Rewriter: "Can only return single object for UpgradeOps traverse" ) upgrade_ops_list.append(ret[0]) - directive.upgrade_ops = upgrade_ops_list - downgrade_ops_list = [] + directive.upgrade_ops = upgrade_ops_list # type: ignore + + downgrade_ops_list: List[DowngradeOps] = [] for downgrade_ops in directive.downgrade_ops_list: ret = self._traverse_for(context, revision, downgrade_ops) if len(ret) != 1: @@ -174,13 +187,13 @@ class Rewriter: "Can only return single object for DowngradeOps traverse" ) downgrade_ops_list.append(ret[0]) - directive.downgrade_ops = downgrade_ops_list + directive.downgrade_ops = downgrade_ops_list # type: ignore @_traverse.dispatch_for(ops.OpContainer) def _traverse_op_container( self, context: MigrationContext, - revision: Revision, + revision: _GetRevArg, directive: OpContainer, ) -> None: self._traverse_list(context, revision, directive.ops) @@ -189,7 +202,7 @@ class Rewriter: def _traverse_any_directive( self, context: MigrationContext, - revision: Revision, + revision: _GetRevArg, directive: MigrateOperation, ) -> None: pass @@ -197,7 +210,7 @@ class Rewriter: def _traverse_for( self, context: MigrationContext, - revision: Revision, + revision: _GetRevArg, directive: MigrateOperation, ) -> Any: directives = list(self._rewrite(context, revision, directive)) @@ -209,7 +222,7 @@ class Rewriter: def _traverse_list( self, context: MigrationContext, - revision: Revision, + revision: _GetRevArg, directives: Any, ) -> None: dest = [] @@ -221,7 +234,7 @@ class Rewriter: def process_revision_directives( self, context: MigrationContext, - revision: Revision, + revision: _GetRevArg, directives: List[MigrationScript], ) -> None: self._traverse_list(context, revision, directives) diff --git a/libs/alembic/command.py b/libs/alembic/command.py index a8e56571f..37aa6e67e 100644 --- a/libs/alembic/command.py +++ b/libs/alembic/command.py @@ -1,3 +1,5 @@ +# mypy: allow-untyped-defs, allow-untyped-calls + from __future__ import annotations import os @@ -14,10 +16,11 @@ from .script import ScriptDirectory if TYPE_CHECKING: from alembic.config import Config from alembic.script.base import Script + from alembic.script.revision import _RevIdType from .runtime.environment import ProcessRevisionDirectiveFn -def list_templates(config): +def list_templates(config: Config) -> None: """List available templates. :param config: a :class:`.Config` object. @@ -54,9 +57,6 @@ def init( :param package: when True, write ``__init__.py`` files into the environment location as well as the versions/ location. - .. versionadded:: 1.2 - - """ if os.access(directory, os.F_OK) and os.listdir(directory): @@ -69,28 +69,32 @@ def init( raise util.CommandError("No such template %r" % template) if not os.access(directory, os.F_OK): - util.status( - "Creating directory %s" % os.path.abspath(directory), - os.makedirs, - directory, - ) + with util.status( + f"Creating directory {os.path.abspath(directory)!r}", + **config.messaging_opts, + ): + os.makedirs(directory) versions = os.path.join(directory, "versions") - util.status( - "Creating directory %s" % os.path.abspath(versions), - os.makedirs, - versions, - ) + with util.status( + f"Creating directory {os.path.abspath(versions)!r}", + **config.messaging_opts, + ): + os.makedirs(versions) script = ScriptDirectory(directory) + config_file: str | None = None for file_ in os.listdir(template_dir): file_path = os.path.join(template_dir, file_) if file_ == "alembic.ini.mako": assert config.config_file_name is not None config_file = os.path.abspath(config.config_file_name) if os.access(config_file, os.F_OK): - util.msg("File %s already exists, skipping" % config_file) + util.msg( + f"File {config_file!r} already exists, skipping", + **config.messaging_opts, + ) else: script._generate_template( file_path, config_file, script_location=directory @@ -104,12 +108,15 @@ def init( os.path.join(os.path.abspath(directory), "__init__.py"), os.path.join(os.path.abspath(versions), "__init__.py"), ]: - file_ = util.status("Adding %s" % path, open, path, "w") - file_.close() # type:ignore[attr-defined] + with util.status(f"Adding {path!r}", **config.messaging_opts): + with open(path, "w"): + pass + assert config_file is not None util.msg( "Please edit configuration/connection/logging " - "settings in %r before proceeding." % config_file + f"settings in {config_file!r} before proceeding.", + **config.messaging_opts, ) @@ -120,7 +127,7 @@ def revision( sql: bool = False, head: str = "head", splice: bool = False, - branch_label: Optional[str] = None, + branch_label: Optional[_RevIdType] = None, version_path: Optional[str] = None, rev_id: Optional[str] = None, depends_on: Optional[str] = None, @@ -240,9 +247,7 @@ def revision( return scripts -def check( - config: "Config", -) -> None: +def check(config: "Config") -> None: """Check if revision command with autogenerate has pending upgrade ops. :param config: a :class:`.Config` object. @@ -287,7 +292,10 @@ def check( # the revision_context now has MigrationScript structure(s) present. migration_script = revision_context.generated_revisions[-1] - diffs = migration_script.upgrade_ops.as_diffs() + diffs = [] + for upgrade_ops in migration_script.upgrade_ops_list: + diffs.extend(upgrade_ops.as_diffs()) + if diffs: raise util.AutogenerateDiffsDetected( f"New upgrade operations detected: {diffs}" @@ -298,9 +306,9 @@ def check( def merge( config: Config, - revisions: str, + revisions: _RevIdType, message: Optional[str] = None, - branch_label: Optional[str] = None, + branch_label: Optional[_RevIdType] = None, rev_id: Optional[str] = None, ) -> Optional[Script]: """Merge two revisions together. Creates a new migration file. @@ -325,6 +333,23 @@ def merge( "config": config # Let templates use config for # e.g. multiple databases } + + environment = util.asbool(config.get_main_option("revision_environment")) + + if environment: + + def nothing(rev, context): + return [] + + with EnvironmentContext( + config, + script, + fn=nothing, + as_sql=False, + template_args=template_args, + ): + script.run_env() + return script.generate_revision( rev_id or util.rev_id(), message, @@ -487,7 +512,6 @@ def history( for sc in script.walk_revisions( base=base or "base", head=head or "heads" ): - if indicate_current: sc._db_current_indicator = sc.revision in currents @@ -603,7 +627,7 @@ def current(config: Config, verbose: bool = False) -> None: def stamp( config: Config, - revision: str, + revision: _RevIdType, sql: bool = False, tag: Optional[str] = None, purge: bool = False, @@ -619,9 +643,6 @@ def stamp( .. note:: this parameter is called "revisions" in the command line interface. - .. versionchanged:: 1.2 The revision may be a single revision or - list of revisions when stamping multiple branch heads. - :param sql: use ``--sql`` mode :param tag: an arbitrary "tag" that can be intercepted by custom @@ -630,8 +651,6 @@ def stamp( :param purge: delete all entries in the version table before stamping. - .. versionadded:: 1.2 - """ script = ScriptDirectory.from_config(config) diff --git a/libs/alembic/config.py b/libs/alembic/config.py index 338769b29..4b2263fdd 100644 --- a/libs/alembic/config.py +++ b/libs/alembic/config.py @@ -6,12 +6,18 @@ from configparser import ConfigParser import inspect import os import sys +from typing import Any +from typing import cast from typing import Dict +from typing import Mapping from typing import Optional from typing import overload +from typing import Sequence from typing import TextIO from typing import Union +from typing_extensions import TypedDict + from . import __version__ from . import command from . import util @@ -19,7 +25,6 @@ from .util import compat class Config: - r"""Represent an Alembic configuration. Within an ``env.py`` script, this is available @@ -30,7 +35,7 @@ class Config: some_param = context.config.get_main_option("my option") - When invoking Alembic programatically, a new + When invoking Alembic programmatically, a new :class:`.Config` can be created by passing the name of an .ini file to the constructor:: @@ -99,8 +104,8 @@ class Config: output_buffer: Optional[TextIO] = None, stdout: TextIO = sys.stdout, cmd_opts: Optional[Namespace] = None, - config_args: util.immutabledict = util.immutabledict(), - attributes: Optional[dict] = None, + config_args: Mapping[str, Any] = util.immutabledict(), + attributes: Optional[Dict[str, Any]] = None, ) -> None: """Construct a new :class:`.Config`""" self.config_file_name = file_ @@ -136,7 +141,7 @@ class Config: """ @util.memoized_property - def attributes(self): + def attributes(self) -> Dict[str, Any]: """A Python dictionary for storage of additional state. @@ -155,13 +160,15 @@ class Config: """ return {} - def print_stdout(self, text: str, *arg) -> None: + def print_stdout(self, text: str, *arg: Any) -> None: """Render a message to standard out. When :meth:`.Config.print_stdout` is called with additional args those arguments will formatted against the provided text, otherwise we simply output the provided text verbatim. + This is a no-op when the``quiet`` messaging option is enabled. + e.g.:: >>> config.print_stdout('Some text %s', 'arg') @@ -174,10 +181,10 @@ class Config: else: output = str(text) - util.write_outstream(self.stdout, output, "\n") + util.write_outstream(self.stdout, output, "\n", **self.messaging_opts) @util.memoized_property - def file_config(self): + def file_config(self) -> ConfigParser: """Return the underlying ``ConfigParser`` object. Direct access to the .ini file is available here, @@ -194,7 +201,7 @@ class Config: self.config_args["here"] = here file_config = ConfigParser(self.config_args) if self.config_file_name: - file_config.read([self.config_file_name]) + compat.read_config_parser(file_config, [self.config_file_name]) else: file_config.add_section(self.config_ini_section) return file_config @@ -211,6 +218,15 @@ class Config: package_dir = os.path.abspath(os.path.dirname(alembic.__file__)) return os.path.join(package_dir, "templates") + @overload + def get_section( + self, name: str, default: None = ... + ) -> Optional[Dict[str, str]]: + ... + + # "default" here could also be a TypeVar + # _MT = TypeVar("_MT", bound=Mapping[str, str]), + # however mypy wasn't handling that correctly (pyright was) @overload def get_section( self, name: str, default: Dict[str, str] @@ -219,14 +235,19 @@ class Config: @overload def get_section( - self, name: str, default: Optional[Dict[str, str]] = ... - ) -> Optional[Dict[str, str]]: + self, name: str, default: Mapping[str, str] + ) -> Union[Dict[str, str], Mapping[str, str]]: ... - def get_section(self, name: str, default=None): + def get_section( + self, name: str, default: Optional[Mapping[str, str]] = None + ) -> Optional[Mapping[str, str]]: """Return all the configuration options from a given .ini file section as a dictionary. + If the given section does not exist, the value of ``default`` + is returned, which is expected to be a dictionary or other mapping. + """ if not self.file_config.has_section(name): return default @@ -301,7 +322,9 @@ class Config: ) -> Optional[str]: ... - def get_main_option(self, name, default=None): + def get_main_option( + self, name: str, default: Optional[str] = None + ) -> Optional[str]: """Return an option from the 'main' section of the .ini file. This defaults to being a key from the ``[alembic]`` @@ -311,13 +334,29 @@ class Config: """ return self.get_section_option(self.config_ini_section, name, default) + @util.memoized_property + def messaging_opts(self) -> MessagingOptions: + """The messaging options.""" + return cast( + MessagingOptions, + util.immutabledict( + {"quiet": getattr(self.cmd_opts, "quiet", False)} + ), + ) + + +class MessagingOptions(TypedDict, total=False): + quiet: bool + class CommandLine: def __init__(self, prog: Optional[str] = None) -> None: self._generate_args(prog) def _generate_args(self, prog: Optional[str]) -> None: - def add_options(fn, parser, positional, kwargs): + def add_options( + fn: Any, parser: Any, positional: Any, kwargs: Any + ) -> None: kwargs_opts = { "template": ( "-t", @@ -512,9 +551,17 @@ class CommandLine: action="store_true", help="Raise a full stack trace on error", ) + parser.add_argument( + "-q", + "--quiet", + action="store_true", + help="Do not log to std output.", + ) subparsers = parser.add_subparsers() - positional_translations = {command.stamp: {"revision": "revisions"}} + positional_translations: Dict[Any, Any] = { + command.stamp: {"revision": "revisions"} + } for fn in [getattr(command, n) for n in dir(command)]: if ( @@ -522,7 +569,6 @@ class CommandLine: and fn.__name__[0] != "_" and fn.__module__ == "alembic.command" ): - spec = compat.inspect_getfullargspec(fn) if spec[3] is not None: positional = spec[0][1 : -len(spec[3])] @@ -568,9 +614,9 @@ class CommandLine: if options.raiseerr: raise else: - util.err(str(e)) + util.err(str(e), **config.messaging_opts) - def main(self, argv=None): + def main(self, argv: Optional[Sequence[str]] = None) -> None: options = self.parser.parse_args(argv) if not hasattr(options, "cmd"): # see http://bugs.python.org/issue9253, argparse @@ -585,7 +631,11 @@ class CommandLine: self.run_cmd(cfg, options) -def main(argv=None, prog=None, **kwargs): +def main( + argv: Optional[Sequence[str]] = None, + prog: Optional[str] = None, + **kwargs: Any, +) -> None: """The console runner function for Alembic.""" CommandLine(prog=prog).main(argv=argv) diff --git a/libs/alembic/context.pyi b/libs/alembic/context.pyi index a262638b2..80619fb24 100644 --- a/libs/alembic/context.pyi +++ b/libs/alembic/context.pyi @@ -4,12 +4,17 @@ from __future__ import annotations from typing import Any from typing import Callable +from typing import Collection from typing import ContextManager from typing import Dict +from typing import Iterable from typing import List from typing import Literal +from typing import Mapping +from typing import MutableMapping from typing import Optional from typing import overload +from typing import Sequence from typing import TextIO from typing import Tuple from typing import TYPE_CHECKING @@ -18,18 +23,25 @@ from typing import Union if TYPE_CHECKING: from sqlalchemy.engine.base import Connection from sqlalchemy.engine.url import URL - from sqlalchemy.sql.elements import ClauseElement + from sqlalchemy.sql import Executable + from sqlalchemy.sql.schema import Column + from sqlalchemy.sql.schema import FetchedValue from sqlalchemy.sql.schema import MetaData + from sqlalchemy.sql.schema import SchemaItem + from sqlalchemy.sql.type_api import TypeEngine + from .autogenerate.api import AutogenContext from .config import Config - from .operations import MigrateOperation + from .operations.ops import MigrationScript from .runtime.migration import _ProxyTransaction from .runtime.migration import MigrationContext + from .runtime.migration import MigrationInfo from .script import ScriptDirectory + ### end imports ### def begin_transaction() -> Union[_ProxyTransaction, ContextManager[None]]: - r"""Return a context manager that will + """Return a context manager that will enclose an operation within a "transaction", as defined by the environment's offline and transactional DDL settings. @@ -86,28 +98,111 @@ def configure( tag: Optional[str] = None, template_args: Optional[Dict[str, Any]] = None, render_as_batch: bool = False, - target_metadata: Optional[MetaData] = None, - include_name: Optional[Callable[..., bool]] = None, - include_object: Optional[Callable[..., bool]] = None, + target_metadata: Union[MetaData, Sequence[MetaData], None] = None, + include_name: Optional[ + Callable[ + [ + Optional[str], + Literal[ + "schema", + "table", + "column", + "index", + "unique_constraint", + "foreign_key_constraint", + ], + MutableMapping[ + Literal[ + "schema_name", + "table_name", + "schema_qualified_table_name", + ], + Optional[str], + ], + ], + bool, + ] + ] = None, + include_object: Optional[ + Callable[ + [ + SchemaItem, + Optional[str], + Literal[ + "schema", + "table", + "column", + "index", + "unique_constraint", + "foreign_key_constraint", + ], + bool, + Optional[SchemaItem], + ], + bool, + ] + ] = None, include_schemas: bool = False, process_revision_directives: Optional[ Callable[ - [MigrationContext, Tuple[str, str], List[MigrateOperation]], None + [ + MigrationContext, + Union[str, Iterable[Optional[str]], Iterable[str]], + List[MigrationScript], + ], + None, ] ] = None, - compare_type: bool = False, - compare_server_default: bool = False, - render_item: Optional[Callable[..., bool]] = None, + compare_type: Union[ + bool, + Callable[ + [ + MigrationContext, + Column[Any], + Column[Any], + TypeEngine[Any], + TypeEngine[Any], + ], + Optional[bool], + ], + ] = True, + compare_server_default: Union[ + bool, + Callable[ + [ + MigrationContext, + Column[Any], + Column[Any], + Optional[str], + Optional[FetchedValue], + Optional[str], + ], + Optional[bool], + ], + ] = False, + render_item: Optional[ + Callable[[str, Any, AutogenContext], Union[str, Literal[False]]] + ] = None, literal_binds: bool = False, upgrade_token: str = "upgrades", downgrade_token: str = "downgrades", alembic_module_prefix: str = "op.", sqlalchemy_module_prefix: str = "sa.", user_module_prefix: Optional[str] = None, - on_version_apply: Optional[Callable[..., None]] = None, + on_version_apply: Optional[ + Callable[ + [ + MigrationContext, + MigrationInfo, + Collection[Any], + Mapping[str, Any], + ], + None, + ] + ] = None, **kw: Any, ) -> None: - r"""Configure a :class:`.MigrationContext` within this + """Configure a :class:`.MigrationContext` within this :class:`.EnvironmentContext` which will provide database connectivity and other configuration to a series of migration scripts. @@ -151,9 +246,6 @@ def configure( ``connection`` and ``url`` are not passed. :param dialect_opts: dictionary of options to be passed to dialect constructor. - - .. versionadded:: 1.0.12 - :param transactional_ddl: Force the usage of "transactional" DDL on or off; this otherwise defaults to whether or not the dialect in @@ -236,12 +328,16 @@ def configure( to produce candidate upgrade/downgrade operations. :param compare_type: Indicates type comparison behavior during an autogenerate - operation. Defaults to ``False`` which disables type - comparison. Set to - ``True`` to turn on default type comparison, which has varied - accuracy depending on backend. See :ref:`compare_types` + operation. Defaults to ``True`` turning on type comparison, which + has good accuracy on most backends. See :ref:`compare_types` for an example as well as information on other type - comparison options. + comparison options. Set to ``False`` which disables type + comparison. A callable can also be passed to provide custom type + comparison, see :ref:`compare_types` for additional details. + + .. versionchanged:: 1.12.0 The default value of + :paramref:`.EnvironmentContext.configure.compare_type` has been + changed to ``True``. .. seealso:: @@ -308,7 +404,8 @@ def configure( ``"unique_constraint"``, or ``"foreign_key_constraint"`` * ``parent_names``: a dictionary of "parent" object names, that are relative to the name being given. Keys in this dictionary may - include: ``"schema_name"``, ``"table_name"``. + include: ``"schema_name"``, ``"table_name"`` or + ``"schema_qualified_table_name"``. E.g.:: @@ -324,8 +421,6 @@ def configure( include_name = include_name ) - .. versionadded:: 1.5 - .. seealso:: :ref:`autogenerate_include_hooks` @@ -541,9 +636,10 @@ def configure( """ def execute( - sql: Union[ClauseElement, str], execution_options: Optional[dict] = None + sql: Union[Executable, str], + execution_options: Optional[Dict[str, Any]] = None, ) -> None: - r"""Execute the given SQL using the current change context. + """Execute the given SQL using the current change context. The behavior of :meth:`.execute` is the same as that of :meth:`.Operations.execute`. Please see that @@ -556,7 +652,7 @@ def execute( """ def get_bind() -> Connection: - r"""Return the current 'bind'. + """Return the current 'bind'. In "online" mode, this is the :class:`sqlalchemy.engine.Connection` currently being used @@ -568,7 +664,7 @@ def get_bind() -> Connection: """ def get_context() -> MigrationContext: - r"""Return the current :class:`.MigrationContext` object. + """Return the current :class:`.MigrationContext` object. If :meth:`.EnvironmentContext.configure` has not been called yet, raises an exception. @@ -576,7 +672,7 @@ def get_context() -> MigrationContext: """ def get_head_revision() -> Union[str, Tuple[str, ...], None]: - r"""Return the hex identifier of the 'head' script revision. + """Return the hex identifier of the 'head' script revision. If the script directory has multiple heads, this method raises a :class:`.CommandError`; @@ -590,7 +686,7 @@ def get_head_revision() -> Union[str, Tuple[str, ...], None]: """ def get_head_revisions() -> Union[str, Tuple[str, ...], None]: - r"""Return the hex identifier of the 'heads' script revision(s). + """Return the hex identifier of the 'heads' script revision(s). This returns a tuple containing the version number of all heads in the script directory. @@ -601,7 +697,7 @@ def get_head_revisions() -> Union[str, Tuple[str, ...], None]: """ def get_revision_argument() -> Union[str, Tuple[str, ...], None]: - r"""Get the 'destination' revision argument. + """Get the 'destination' revision argument. This is typically the argument passed to the ``upgrade`` or ``downgrade`` command. @@ -616,7 +712,7 @@ def get_revision_argument() -> Union[str, Tuple[str, ...], None]: """ def get_starting_revision_argument() -> Union[str, Tuple[str, ...], None]: - r"""Return the 'starting revision' argument, + """Return the 'starting revision' argument, if the revision was passed using ``start:end``. This is only meaningful in "offline" mode. @@ -629,7 +725,7 @@ def get_starting_revision_argument() -> Union[str, Tuple[str, ...], None]: """ def get_tag_argument() -> Optional[str]: - r"""Return the value passed for the ``--tag`` argument, if any. + """Return the value passed for the ``--tag`` argument, if any. The ``--tag`` argument is not used directly by Alembic, but is available for custom ``env.py`` configurations that @@ -655,7 +751,7 @@ def get_x_argument(as_dictionary: Literal[True]) -> Dict[str, str]: ... def get_x_argument( as_dictionary: bool = ..., ) -> Union[List[str], Dict[str, str]]: - r"""Return the value(s) passed for the ``-x`` argument, if any. + """Return the value(s) passed for the ``-x`` argument, if any. The ``-x`` argument is an open ended flag that allows any user-defined value or values to be passed on the command line, then available @@ -664,7 +760,11 @@ def get_x_argument( The return value is a list, returned directly from the ``argparse`` structure. If ``as_dictionary=True`` is passed, the ``x`` arguments are parsed using ``key=value`` format into a dictionary that is - then returned. + then returned. If there is no ``=`` in the argument, value is an empty + string. + + .. versionchanged:: 1.13.1 Support ``as_dictionary=True`` when + arguments are passed without the ``=`` symbol. For example, to support passing a database URL on the command line, the standard ``env.py`` script can be modified like this:: @@ -695,7 +795,7 @@ def get_x_argument( """ def is_offline_mode() -> bool: - r"""Return True if the current migrations environment + """Return True if the current migrations environment is running in "offline mode". This is ``True`` or ``False`` depending @@ -706,8 +806,8 @@ def is_offline_mode() -> bool: """ -def is_transactional_ddl(): - r"""Return True if the context is configured to expect a +def is_transactional_ddl() -> bool: + """Return True if the context is configured to expect a transactional DDL capable backend. This defaults to the type of database in use, and @@ -720,7 +820,7 @@ def is_transactional_ddl(): """ def run_migrations(**kw: Any) -> None: - r"""Run migrations as determined by the current command line + """Run migrations as determined by the current command line configuration as well as versioning information present (or not) in the current database connection (if one is present). @@ -743,7 +843,7 @@ def run_migrations(**kw: Any) -> None: script: ScriptDirectory def static_output(text: str) -> None: - r"""Emit text directly to the "offline" SQL stream. + """Emit text directly to the "offline" SQL stream. Typically this is for emitting comments that start with --. The statement is not treated diff --git a/libs/alembic/ddl/__init__.py b/libs/alembic/ddl/__init__.py index cfcc47e02..f2f72b3dd 100644 --- a/libs/alembic/ddl/__init__.py +++ b/libs/alembic/ddl/__init__.py @@ -3,4 +3,4 @@ from . import mysql from . import oracle from . import postgresql from . import sqlite -from .impl import DefaultImpl +from .impl import DefaultImpl as DefaultImpl diff --git a/libs/alembic/ddl/_autogen.py b/libs/alembic/ddl/_autogen.py new file mode 100644 index 000000000..e22153c49 --- /dev/null +++ b/libs/alembic/ddl/_autogen.py @@ -0,0 +1,325 @@ +# mypy: allow-untyped-defs, allow-incomplete-defs, allow-untyped-calls +# mypy: no-warn-return-any, allow-any-generics + +from __future__ import annotations + +from typing import Any +from typing import ClassVar +from typing import Dict +from typing import Generic +from typing import NamedTuple +from typing import Optional +from typing import Sequence +from typing import Tuple +from typing import Type +from typing import TYPE_CHECKING +from typing import TypeVar +from typing import Union + +from sqlalchemy.sql.schema import Constraint +from sqlalchemy.sql.schema import ForeignKeyConstraint +from sqlalchemy.sql.schema import Index +from sqlalchemy.sql.schema import UniqueConstraint +from typing_extensions import TypeGuard + +from .. import util +from ..util import sqla_compat + +if TYPE_CHECKING: + from typing import Literal + + from alembic.autogenerate.api import AutogenContext + from alembic.ddl.impl import DefaultImpl + +CompareConstraintType = Union[Constraint, Index] + +_C = TypeVar("_C", bound=CompareConstraintType) + +_clsreg: Dict[str, Type[_constraint_sig]] = {} + + +class ComparisonResult(NamedTuple): + status: Literal["equal", "different", "skip"] + message: str + + @property + def is_equal(self) -> bool: + return self.status == "equal" + + @property + def is_different(self) -> bool: + return self.status == "different" + + @property + def is_skip(self) -> bool: + return self.status == "skip" + + @classmethod + def Equal(cls) -> ComparisonResult: + """the constraints are equal.""" + return cls("equal", "The two constraints are equal") + + @classmethod + def Different(cls, reason: Union[str, Sequence[str]]) -> ComparisonResult: + """the constraints are different for the provided reason(s).""" + return cls("different", ", ".join(util.to_list(reason))) + + @classmethod + def Skip(cls, reason: Union[str, Sequence[str]]) -> ComparisonResult: + """the constraint cannot be compared for the provided reason(s). + + The message is logged, but the constraints will be otherwise + considered equal, meaning that no migration command will be + generated. + """ + return cls("skip", ", ".join(util.to_list(reason))) + + +class _constraint_sig(Generic[_C]): + const: _C + + _sig: Tuple[Any, ...] + name: Optional[sqla_compat._ConstraintNameDefined] + + impl: DefaultImpl + + _is_index: ClassVar[bool] = False + _is_fk: ClassVar[bool] = False + _is_uq: ClassVar[bool] = False + + _is_metadata: bool + + def __init_subclass__(cls) -> None: + cls._register() + + @classmethod + def _register(cls): + raise NotImplementedError() + + def __init__( + self, is_metadata: bool, impl: DefaultImpl, const: _C + ) -> None: + raise NotImplementedError() + + def compare_to_reflected( + self, other: _constraint_sig[Any] + ) -> ComparisonResult: + assert self.impl is other.impl + assert self._is_metadata + assert not other._is_metadata + + return self._compare_to_reflected(other) + + def _compare_to_reflected( + self, other: _constraint_sig[_C] + ) -> ComparisonResult: + raise NotImplementedError() + + @classmethod + def from_constraint( + cls, is_metadata: bool, impl: DefaultImpl, constraint: _C + ) -> _constraint_sig[_C]: + # these could be cached by constraint/impl, however, if the + # constraint is modified in place, then the sig is wrong. the mysql + # impl currently does this, and if we fixed that we can't be sure + # someone else might do it too, so play it safe. + sig = _clsreg[constraint.__visit_name__](is_metadata, impl, constraint) + return sig + + def md_name_to_sql_name(self, context: AutogenContext) -> Optional[str]: + return sqla_compat._get_constraint_final_name( + self.const, context.dialect + ) + + @util.memoized_property + def is_named(self): + return sqla_compat._constraint_is_named(self.const, self.impl.dialect) + + @util.memoized_property + def unnamed(self) -> Tuple[Any, ...]: + return self._sig + + @util.memoized_property + def unnamed_no_options(self) -> Tuple[Any, ...]: + raise NotImplementedError() + + @util.memoized_property + def _full_sig(self) -> Tuple[Any, ...]: + return (self.name,) + self.unnamed + + def __eq__(self, other) -> bool: + return self._full_sig == other._full_sig + + def __ne__(self, other) -> bool: + return self._full_sig != other._full_sig + + def __hash__(self) -> int: + return hash(self._full_sig) + + +class _uq_constraint_sig(_constraint_sig[UniqueConstraint]): + _is_uq = True + + @classmethod + def _register(cls) -> None: + _clsreg["unique_constraint"] = cls + + is_unique = True + + def __init__( + self, + is_metadata: bool, + impl: DefaultImpl, + const: UniqueConstraint, + ) -> None: + self.impl = impl + self.const = const + self.name = sqla_compat.constraint_name_or_none(const.name) + self._sig = tuple(sorted([col.name for col in const.columns])) + self._is_metadata = is_metadata + + @property + def column_names(self) -> Tuple[str, ...]: + return tuple([col.name for col in self.const.columns]) + + def _compare_to_reflected( + self, other: _constraint_sig[_C] + ) -> ComparisonResult: + assert self._is_metadata + metadata_obj = self + conn_obj = other + + assert is_uq_sig(conn_obj) + return self.impl.compare_unique_constraint( + metadata_obj.const, conn_obj.const + ) + + +class _ix_constraint_sig(_constraint_sig[Index]): + _is_index = True + + name: sqla_compat._ConstraintName + + @classmethod + def _register(cls) -> None: + _clsreg["index"] = cls + + def __init__( + self, is_metadata: bool, impl: DefaultImpl, const: Index + ) -> None: + self.impl = impl + self.const = const + self.name = const.name + self.is_unique = bool(const.unique) + self._is_metadata = is_metadata + + def _compare_to_reflected( + self, other: _constraint_sig[_C] + ) -> ComparisonResult: + assert self._is_metadata + metadata_obj = self + conn_obj = other + + assert is_index_sig(conn_obj) + return self.impl.compare_indexes(metadata_obj.const, conn_obj.const) + + @util.memoized_property + def has_expressions(self): + return sqla_compat.is_expression_index(self.const) + + @util.memoized_property + def column_names(self) -> Tuple[str, ...]: + return tuple([col.name for col in self.const.columns]) + + @util.memoized_property + def column_names_optional(self) -> Tuple[Optional[str], ...]: + return tuple( + [getattr(col, "name", None) for col in self.const.expressions] + ) + + @util.memoized_property + def is_named(self): + return True + + @util.memoized_property + def unnamed(self): + return (self.is_unique,) + self.column_names_optional + + +class _fk_constraint_sig(_constraint_sig[ForeignKeyConstraint]): + _is_fk = True + + @classmethod + def _register(cls) -> None: + _clsreg["foreign_key_constraint"] = cls + + def __init__( + self, + is_metadata: bool, + impl: DefaultImpl, + const: ForeignKeyConstraint, + ) -> None: + self._is_metadata = is_metadata + + self.impl = impl + self.const = const + + self.name = sqla_compat.constraint_name_or_none(const.name) + + ( + self.source_schema, + self.source_table, + self.source_columns, + self.target_schema, + self.target_table, + self.target_columns, + onupdate, + ondelete, + deferrable, + initially, + ) = sqla_compat._fk_spec(const) + + self._sig: Tuple[Any, ...] = ( + self.source_schema, + self.source_table, + tuple(self.source_columns), + self.target_schema, + self.target_table, + tuple(self.target_columns), + ) + ( + (None if onupdate.lower() == "no action" else onupdate.lower()) + if onupdate + else None, + (None if ondelete.lower() == "no action" else ondelete.lower()) + if ondelete + else None, + # convert initially + deferrable into one three-state value + "initially_deferrable" + if initially and initially.lower() == "deferred" + else "deferrable" + if deferrable + else "not deferrable", + ) + + @util.memoized_property + def unnamed_no_options(self): + return ( + self.source_schema, + self.source_table, + tuple(self.source_columns), + self.target_schema, + self.target_table, + tuple(self.target_columns), + ) + + +def is_index_sig(sig: _constraint_sig) -> TypeGuard[_ix_constraint_sig]: + return sig._is_index + + +def is_uq_sig(sig: _constraint_sig) -> TypeGuard[_uq_constraint_sig]: + return sig._is_uq + + +def is_fk_sig(sig: _constraint_sig) -> TypeGuard[_fk_constraint_sig]: + return sig._is_fk diff --git a/libs/alembic/ddl/base.py b/libs/alembic/ddl/base.py index 65da32f40..7a85a5c19 100644 --- a/libs/alembic/ddl/base.py +++ b/libs/alembic/ddl/base.py @@ -1,3 +1,6 @@ +# mypy: allow-untyped-defs, allow-incomplete-defs, allow-untyped-calls +# mypy: no-warn-return-any, allow-any-generics + from __future__ import annotations import functools @@ -150,7 +153,7 @@ class AddColumn(AlterTable): def __init__( self, name: str, - column: Column, + column: Column[Any], schema: Optional[Union[quoted_name, str]] = None, ) -> None: super().__init__(name, schema=schema) @@ -159,7 +162,7 @@ class AddColumn(AlterTable): class DropColumn(AlterTable): def __init__( - self, name: str, column: Column, schema: Optional[str] = None + self, name: str, column: Column[Any], schema: Optional[str] = None ) -> None: super().__init__(name, schema=schema) self.column = column @@ -173,7 +176,7 @@ class ColumnComment(AlterColumn): self.comment = comment -@compiles(RenameTable) +@compiles(RenameTable) # type: ignore[misc] def visit_rename_table( element: RenameTable, compiler: DDLCompiler, **kw ) -> str: @@ -183,7 +186,7 @@ def visit_rename_table( ) -@compiles(AddColumn) +@compiles(AddColumn) # type: ignore[misc] def visit_add_column(element: AddColumn, compiler: DDLCompiler, **kw) -> str: return "%s %s" % ( alter_table(compiler, element.table_name, element.schema), @@ -191,7 +194,7 @@ def visit_add_column(element: AddColumn, compiler: DDLCompiler, **kw) -> str: ) -@compiles(DropColumn) +@compiles(DropColumn) # type: ignore[misc] def visit_drop_column(element: DropColumn, compiler: DDLCompiler, **kw) -> str: return "%s %s" % ( alter_table(compiler, element.table_name, element.schema), @@ -199,7 +202,7 @@ def visit_drop_column(element: DropColumn, compiler: DDLCompiler, **kw) -> str: ) -@compiles(ColumnNullable) +@compiles(ColumnNullable) # type: ignore[misc] def visit_column_nullable( element: ColumnNullable, compiler: DDLCompiler, **kw ) -> str: @@ -210,7 +213,7 @@ def visit_column_nullable( ) -@compiles(ColumnType) +@compiles(ColumnType) # type: ignore[misc] def visit_column_type(element: ColumnType, compiler: DDLCompiler, **kw) -> str: return "%s %s %s" % ( alter_table(compiler, element.table_name, element.schema), @@ -219,7 +222,7 @@ def visit_column_type(element: ColumnType, compiler: DDLCompiler, **kw) -> str: ) -@compiles(ColumnName) +@compiles(ColumnName) # type: ignore[misc] def visit_column_name(element: ColumnName, compiler: DDLCompiler, **kw) -> str: return "%s RENAME %s TO %s" % ( alter_table(compiler, element.table_name, element.schema), @@ -228,7 +231,7 @@ def visit_column_name(element: ColumnName, compiler: DDLCompiler, **kw) -> str: ) -@compiles(ColumnDefault) +@compiles(ColumnDefault) # type: ignore[misc] def visit_column_default( element: ColumnDefault, compiler: DDLCompiler, **kw ) -> str: @@ -241,7 +244,7 @@ def visit_column_default( ) -@compiles(ComputedColumnDefault) +@compiles(ComputedColumnDefault) # type: ignore[misc] def visit_computed_column( element: ComputedColumnDefault, compiler: DDLCompiler, **kw ): @@ -251,7 +254,7 @@ def visit_computed_column( ) -@compiles(IdentityColumnDefault) +@compiles(IdentityColumnDefault) # type: ignore[misc] def visit_identity_column( element: IdentityColumnDefault, compiler: DDLCompiler, **kw ): @@ -320,7 +323,7 @@ def alter_column(compiler: DDLCompiler, name: str) -> str: return "ALTER COLUMN %s" % format_column_name(compiler, name) -def add_column(compiler: DDLCompiler, column: Column, **kw) -> str: +def add_column(compiler: DDLCompiler, column: Column[Any], **kw) -> str: text = "ADD COLUMN %s" % compiler.get_column_specification(column, **kw) const = " ".join( diff --git a/libs/alembic/ddl/impl.py b/libs/alembic/ddl/impl.py index f11d1edc1..2e4f1ae94 100644 --- a/libs/alembic/ddl/impl.py +++ b/libs/alembic/ddl/impl.py @@ -1,11 +1,17 @@ +# mypy: allow-untyped-defs, allow-incomplete-defs, allow-untyped-calls +# mypy: no-warn-return-any, allow-any-generics + from __future__ import annotations -from collections import namedtuple +import logging import re from typing import Any from typing import Callable from typing import Dict +from typing import Iterable from typing import List +from typing import Mapping +from typing import NamedTuple from typing import Optional from typing import Sequence from typing import Set @@ -18,7 +24,10 @@ from sqlalchemy import cast from sqlalchemy import schema from sqlalchemy import text +from . import _autogen from . import base +from ._autogen import _constraint_sig as _constraint_sig +from ._autogen import ComparisonResult as ComparisonResult from .. import util from ..util import sqla_compat @@ -30,7 +39,8 @@ if TYPE_CHECKING: from sqlalchemy.engine import Dialect from sqlalchemy.engine.cursor import CursorResult from sqlalchemy.engine.reflection import Inspector - from sqlalchemy.sql.elements import ClauseElement + from sqlalchemy.sql import ClauseElement + from sqlalchemy.sql import Executable from sqlalchemy.sql.elements import ColumnElement from sqlalchemy.sql.elements import quoted_name from sqlalchemy.sql.schema import Column @@ -47,6 +57,8 @@ if TYPE_CHECKING: from ..operations.batch import ApplyBatchImpl from ..operations.batch import BatchOperationsImpl +log = logging.getLogger(__name__) + class ImplMeta(type): def __init__( @@ -63,8 +75,6 @@ class ImplMeta(type): _impls: Dict[str, Type[DefaultImpl]] = {} -Params = namedtuple("Params", ["token0", "tokens", "args", "kwargs"]) - class DefaultImpl(metaclass=ImplMeta): @@ -86,8 +96,11 @@ class DefaultImpl(metaclass=ImplMeta): command_terminator = ";" type_synonyms: Tuple[Set[str], ...] = ({"NUMERIC", "DECIMAL"},) type_arg_extract: Sequence[str] = () - # on_null is known to be supported only by oracle - identity_attrs_ignore: Tuple[str, ...] = ("on_null",) + # These attributes are deprecated in SQLAlchemy via #10247. They need to + # be ignored to support older version that did not use dialect kwargs. + # They only apply to Oracle and are replaced by oracle_order, + # oracle_on_null + identity_attrs_ignore: Tuple[str, ...] = ("order", "on_null") def __init__( self, @@ -154,10 +167,10 @@ class DefaultImpl(metaclass=ImplMeta): def _exec( self, - construct: Union[ClauseElement, str], - execution_options: Optional[dict] = None, + construct: Union[Executable, str], + execution_options: Optional[dict[str, Any]] = None, multiparams: Sequence[dict] = (), - params: Dict[str, int] = util.immutabledict(), + params: Dict[str, Any] = util.immutabledict(), ) -> Optional[CursorResult]: if isinstance(construct, str): construct = text(construct) @@ -166,6 +179,7 @@ class DefaultImpl(metaclass=ImplMeta): # TODO: coverage raise Exception("Execution arguments not allowed with as_sql") + compile_kw: dict[str, Any] if self.literal_binds and not isinstance( construct, schema.DDLElement ): @@ -173,9 +187,9 @@ class DefaultImpl(metaclass=ImplMeta): else: compile_kw = {} - compiled = construct.compile( - dialect=self.dialect, **compile_kw # type: ignore[arg-type] - ) + if TYPE_CHECKING: + assert isinstance(construct, ClauseElement) + compiled = construct.compile(dialect=self.dialect, **compile_kw) self.static_output( str(compiled).replace("\t", " ").strip() + self.command_terminator @@ -190,14 +204,12 @@ class DefaultImpl(metaclass=ImplMeta): assert isinstance(multiparams, tuple) multiparams += (params,) - return conn.execute( # type: ignore[call-overload] - construct, multiparams - ) + return conn.execute(construct, multiparams) def execute( self, - sql: Union[ClauseElement, str], - execution_options: None = None, + sql: Union[Executable, str], + execution_options: Optional[dict[str, Any]] = None, ) -> None: self._exec(sql, execution_options) @@ -316,7 +328,7 @@ class DefaultImpl(metaclass=ImplMeta): def add_column( self, table_name: str, - column: Column, + column: Column[Any], schema: Optional[Union[str, quoted_name]] = None, ) -> None: self._exec(base.AddColumn(table_name, column, schema=schema)) @@ -324,7 +336,7 @@ class DefaultImpl(metaclass=ImplMeta): def drop_column( self, table_name: str, - column: Column, + column: Column[Any], schema: Optional[str] = None, **kw, ) -> None: @@ -379,8 +391,8 @@ class DefaultImpl(metaclass=ImplMeta): table, self.connection, checkfirst=False, _ddl_runner=self ) - def create_index(self, index: Index) -> None: - self._exec(schema.CreateIndex(index)) + def create_index(self, index: Index, **kw: Any) -> None: + self._exec(schema.CreateIndex(index, **kw)) def create_table_comment(self, table: Table) -> None: self._exec(schema.SetTableComment(table)) @@ -388,11 +400,11 @@ class DefaultImpl(metaclass=ImplMeta): def drop_table_comment(self, table: Table) -> None: self._exec(schema.DropTableComment(table)) - def create_column_comment(self, column: ColumnElement) -> None: + def create_column_comment(self, column: ColumnElement[Any]) -> None: self._exec(schema.SetColumnComment(column)) - def drop_index(self, index: Index) -> None: - self._exec(schema.DropIndex(index)) + def drop_index(self, index: Index, **kw: Any) -> None: + self._exec(schema.DropIndex(index, **kw)) def bulk_insert( self, @@ -433,6 +445,7 @@ class DefaultImpl(metaclass=ImplMeta): ) def _tokenize_column_type(self, column: Column) -> Params: + definition: str definition = self.dialect.type_compiler.process(column.type).lower() # tokenize the SQLAlchemy-generated version of a type, so that @@ -447,9 +460,9 @@ class DefaultImpl(metaclass=ImplMeta): # varchar character set utf8 # - tokens = re.findall(r"[\w\-_]+|\(.+?\)", definition) + tokens: List[str] = re.findall(r"[\w\-_]+|\(.+?\)", definition) - term_tokens = [] + term_tokens: List[str] = [] paren_term = None for token in tokens: @@ -461,6 +474,7 @@ class DefaultImpl(metaclass=ImplMeta): params = Params(term_tokens[0], term_tokens[1:], [], {}) if paren_term: + term: str for term in re.findall("[^(),]+", paren_term): if "=" in term: key, val = term.split("=") @@ -526,7 +540,7 @@ class DefaultImpl(metaclass=ImplMeta): return True def compare_type( - self, inspector_column: Column, metadata_column: Column + self, inspector_column: Column[Any], metadata_column: Column ) -> bool: """Returns True if there ARE differences between the types of the two columns. Takes impl.type_synonyms into account between retrospected @@ -571,17 +585,12 @@ class DefaultImpl(metaclass=ImplMeta): """Render a SQL expression that is typically a server default, index expression, etc. - .. versionadded:: 1.0.11 - """ - compile_kw = { - "compile_kwargs": {"literal_binds": True, "include_table": False} - } + compile_kw = {"literal_binds": True, "include_table": False} + return str( - expr.compile( - dialect=self.dialect, **compile_kw # type: ignore[arg-type] - ) + expr.compile(dialect=self.dialect, compile_kwargs=compile_kw) ) def _compat_autogen_column_reflect(self, inspector: Inspector) -> Callable: @@ -637,14 +646,13 @@ class DefaultImpl(metaclass=ImplMeta): return False def _compare_identity_default(self, metadata_identity, inspector_identity): - # ignored contains the attributes that were not considered # because assumed to their default values in the db. diff, ignored = _compare_identity_options( - sqla_compat._identity_attrs, metadata_identity, inspector_identity, sqla_compat.Identity(), + skip={"always"}, ) meta_always = getattr(metadata_identity, "always", None) @@ -665,9 +673,96 @@ class DefaultImpl(metaclass=ImplMeta): bool(diff) or bool(metadata_identity) != bool(inspector_identity), ) - def create_index_sig(self, index: Index) -> Tuple[Any, ...]: - # order of col matters in an index - return tuple(col.name for col in index.columns) + def _compare_index_unique( + self, metadata_index: Index, reflected_index: Index + ) -> Optional[str]: + conn_unique = bool(reflected_index.unique) + meta_unique = bool(metadata_index.unique) + if conn_unique != meta_unique: + return f"unique={conn_unique} to unique={meta_unique}" + else: + return None + + def _create_metadata_constraint_sig( + self, constraint: _autogen._C, **opts: Any + ) -> _constraint_sig[_autogen._C]: + return _constraint_sig.from_constraint(True, self, constraint, **opts) + + def _create_reflected_constraint_sig( + self, constraint: _autogen._C, **opts: Any + ) -> _constraint_sig[_autogen._C]: + return _constraint_sig.from_constraint(False, self, constraint, **opts) + + def compare_indexes( + self, + metadata_index: Index, + reflected_index: Index, + ) -> ComparisonResult: + """Compare two indexes by comparing the signature generated by + ``create_index_sig``. + + This method returns a ``ComparisonResult``. + """ + msg: List[str] = [] + unique_msg = self._compare_index_unique( + metadata_index, reflected_index + ) + if unique_msg: + msg.append(unique_msg) + m_sig = self._create_metadata_constraint_sig(metadata_index) + r_sig = self._create_reflected_constraint_sig(reflected_index) + + assert _autogen.is_index_sig(m_sig) + assert _autogen.is_index_sig(r_sig) + + # The assumption is that the index have no expression + for sig in m_sig, r_sig: + if sig.has_expressions: + log.warning( + "Generating approximate signature for index %s. " + "The dialect " + "implementation should either skip expression indexes " + "or provide a custom implementation.", + sig.const, + ) + + if m_sig.column_names != r_sig.column_names: + msg.append( + f"expression {r_sig.column_names} to {m_sig.column_names}" + ) + + if msg: + return ComparisonResult.Different(msg) + else: + return ComparisonResult.Equal() + + def compare_unique_constraint( + self, + metadata_constraint: UniqueConstraint, + reflected_constraint: UniqueConstraint, + ) -> ComparisonResult: + """Compare two unique constraints by comparing the two signatures. + + The arguments are two tuples that contain the unique constraint and + the signatures generated by ``create_unique_constraint_sig``. + + This method returns a ``ComparisonResult``. + """ + metadata_tup = self._create_metadata_constraint_sig( + metadata_constraint + ) + reflected_tup = self._create_reflected_constraint_sig( + reflected_constraint + ) + + meta_sig = metadata_tup.unnamed + conn_sig = reflected_tup.unnamed + if conn_sig != meta_sig: + return ComparisonResult.Different( + f"expression {conn_sig} to {meta_sig}" + ) + else: + return ComparisonResult.Equal() def _skip_functional_indexes(self, metadata_indexes, conn_indexes): conn_indexes_by_name = {c.name: c for c in conn_indexes} @@ -686,22 +781,64 @@ class DefaultImpl(metaclass=ImplMeta): ) metadata_indexes.discard(idx) + def adjust_reflected_dialect_options( + self, reflected_object: Dict[str, Any], kind: str + ) -> Dict[str, Any]: + return reflected_object.get("dialect_options", {}) + + +class Params(NamedTuple): + token0: str + tokens: List[str] + args: List[str] + kwargs: Dict[str, str] + def _compare_identity_options( - attributes, metadata_io, inspector_io, default_io + metadata_io: Union[schema.Identity, schema.Sequence, None], + inspector_io: Union[schema.Identity, schema.Sequence, None], + default_io: Union[schema.Identity, schema.Sequence], + skip: Set[str], ): # this can be used for identity or sequence compare. # default_io is an instance of IdentityOption with all attributes to the # default value. + meta_d = sqla_compat._get_identity_options_dict(metadata_io) + insp_d = sqla_compat._get_identity_options_dict(inspector_io) + diff = set() ignored_attr = set() - for attr in attributes: - meta_value = getattr(metadata_io, attr, None) - default_value = getattr(default_io, attr, None) - conn_value = getattr(inspector_io, attr, None) - if conn_value != meta_value: - if meta_value == default_value: - ignored_attr.add(attr) - else: - diff.add(attr) + + def check_dicts( + meta_dict: Mapping[str, Any], + insp_dict: Mapping[str, Any], + default_dict: Mapping[str, Any], + attrs: Iterable[str], + ): + for attr in set(attrs).difference(skip): + meta_value = meta_dict.get(attr) + insp_value = insp_dict.get(attr) + if insp_value != meta_value: + default_value = default_dict.get(attr) + if meta_value == default_value: + ignored_attr.add(attr) + else: + diff.add(attr) + + check_dicts( + meta_d, + insp_d, + sqla_compat._get_identity_options_dict(default_io), + set(meta_d).union(insp_d), + ) + if sqla_compat.identity_has_dialect_kwargs: + # use only the dialect kwargs in inspector_io since metadata_io + # can have options for many backends + check_dicts( + getattr(metadata_io, "dialect_kwargs", {}), + getattr(inspector_io, "dialect_kwargs", {}), + default_io.dialect_kwargs, # type: ignore[union-attr] + getattr(inspector_io, "dialect_kwargs", {}), + ) + return diff, ignored_attr diff --git a/libs/alembic/ddl/mssql.py b/libs/alembic/ddl/mssql.py index ebf4db19a..baa43d5e7 100644 --- a/libs/alembic/ddl/mssql.py +++ b/libs/alembic/ddl/mssql.py @@ -1,14 +1,17 @@ +# mypy: allow-untyped-defs, allow-incomplete-defs, allow-untyped-calls +# mypy: no-warn-return-any, allow-any-generics + from __future__ import annotations import re from typing import Any +from typing import Dict from typing import List from typing import Optional from typing import TYPE_CHECKING from typing import Union from sqlalchemy import types as sqltypes -from sqlalchemy.ext.compiler import compiles from sqlalchemy.schema import Column from sqlalchemy.schema import CreateIndex from sqlalchemy.sql.base import Executable @@ -29,6 +32,7 @@ from .base import RenameTable from .impl import DefaultImpl from .. import util from ..util import sqla_compat +from ..util.sqla_compat import compiles if TYPE_CHECKING: from typing import Literal @@ -50,16 +54,13 @@ class MSSQLImpl(DefaultImpl): batch_separator = "GO" type_synonyms = DefaultImpl.type_synonyms + ({"VARCHAR", "NVARCHAR"},) - identity_attrs_ignore = ( + identity_attrs_ignore = DefaultImpl.identity_attrs_ignore + ( "minvalue", "maxvalue", "nominvalue", "nomaxvalue", "cycle", "cache", - "order", - "on_null", - "order", ) def __init__(self, *arg, **kw) -> None: @@ -98,7 +99,6 @@ class MSSQLImpl(DefaultImpl): existing_nullable: Optional[bool] = None, **kw: Any, ) -> None: - if nullable is not None: if type_ is not None: # the NULL/NOT NULL alter will handle @@ -171,7 +171,7 @@ class MSSQLImpl(DefaultImpl): table_name, column_name, schema=schema, name=name ) - def create_index(self, index: Index) -> None: + def create_index(self, index: Index, **kw: Any) -> None: # this likely defaults to None if not present, so get() # should normally not return the default value. being # defensive in any case @@ -180,7 +180,7 @@ class MSSQLImpl(DefaultImpl): for col in mssql_include: if col not in index.table.c: index.table.append_column(Column(col, sqltypes.NullType)) - self._exec(CreateIndex(index)) + self._exec(CreateIndex(index, **kw)) def bulk_insert( # type:ignore[override] self, table: Union[TableClause, Table], rows: List[dict], **kw: Any @@ -201,7 +201,7 @@ class MSSQLImpl(DefaultImpl): def drop_column( self, table_name: str, - column: Column, + column: Column[Any], schema: Optional[str] = None, **kw, ) -> None: @@ -231,9 +231,7 @@ class MSSQLImpl(DefaultImpl): rendered_metadata_default, rendered_inspector_default, ): - if rendered_metadata_default is not None: - rendered_metadata_default = re.sub( r"[\(\) \"\']", "", rendered_metadata_default ) @@ -266,6 +264,17 @@ class MSSQLImpl(DefaultImpl): return diff, ignored, is_alter + def adjust_reflected_dialect_options( + self, reflected_object: Dict[str, Any], kind: str + ) -> Dict[str, Any]: + options: Dict[str, Any] + options = reflected_object.get("dialect_options", {}).copy() + if not options.get("mssql_include"): + options.pop("mssql_include", None) + if not options.get("mssql_clustered"): + options.pop("mssql_clustered", None) + return options + class _ExecDropConstraint(Executable, ClauseElement): inherit_cache = False @@ -273,7 +282,7 @@ class _ExecDropConstraint(Executable, ClauseElement): def __init__( self, tname: str, - colname: Union[Column, str], + colname: Union[Column[Any], str], type_: str, schema: Optional[str], ) -> None: @@ -287,7 +296,7 @@ class _ExecDropFKConstraint(Executable, ClauseElement): inherit_cache = False def __init__( - self, tname: str, colname: Column, schema: Optional[str] + self, tname: str, colname: Column[Any], schema: Optional[str] ) -> None: self.tname = tname self.colname = colname @@ -347,7 +356,9 @@ def visit_add_column(element: AddColumn, compiler: MSDDLCompiler, **kw) -> str: ) -def mssql_add_column(compiler: MSDDLCompiler, column: Column, **kw) -> str: +def mssql_add_column( + compiler: MSDDLCompiler, column: Column[Any], **kw +) -> str: return "ADD %s" % compiler.get_column_specification(column, **kw) diff --git a/libs/alembic/ddl/mysql.py b/libs/alembic/ddl/mysql.py index a45276022..f312173e9 100644 --- a/libs/alembic/ddl/mysql.py +++ b/libs/alembic/ddl/mysql.py @@ -1,3 +1,6 @@ +# mypy: allow-untyped-defs, allow-incomplete-defs, allow-untyped-calls +# mypy: no-warn-return-any, allow-any-generics + from __future__ import annotations import re @@ -8,7 +11,6 @@ from typing import Union from sqlalchemy import schema from sqlalchemy import types as sqltypes -from sqlalchemy.ext.compiler import compiles from .base import alter_table from .base import AlterColumn @@ -20,10 +22,10 @@ from .base import format_column_name from .base import format_server_default from .impl import DefaultImpl from .. import util -from ..autogenerate import compare from ..util import sqla_compat from ..util.sqla_compat import _is_mariadb from ..util.sqla_compat import _is_type_bound +from ..util.sqla_compat import compiles if TYPE_CHECKING: from typing import Literal @@ -161,8 +163,7 @@ class MySQLImpl(DefaultImpl): ) -> bool: return ( type_ is not None - and type_._type_affinity # type:ignore[attr-defined] - is sqltypes.DateTime + and type_._type_affinity is sqltypes.DateTime and server_default is not None ) @@ -185,13 +186,22 @@ class MySQLImpl(DefaultImpl): and rendered_inspector_default == "'0'" ): return False - elif inspector_column.type._type_affinity is sqltypes.Integer: + elif ( + rendered_inspector_default + and inspector_column.type._type_affinity is sqltypes.Integer + ): rendered_inspector_default = ( re.sub(r"^'|'$", "", rendered_inspector_default) if rendered_inspector_default is not None else None ) return rendered_inspector_default != rendered_metadata_default + elif ( + rendered_metadata_default + and metadata_column.type._type_affinity is sqltypes.String + ): + metadata_default = re.sub(r"^'|'$", "", rendered_metadata_default) + return rendered_inspector_default != f"'{metadata_default}'" elif rendered_inspector_default and rendered_metadata_default: # adjust for "function()" vs. "FUNCTION" as can occur particularly # for the CURRENT_TIMESTAMP function on newer MariaDB versions @@ -231,7 +241,6 @@ class MySQLImpl(DefaultImpl): metadata_unique_constraints, metadata_indexes, ): - # TODO: if SQLA 1.0, make use of "duplicates_index" # metadata removed = set() @@ -264,10 +273,12 @@ class MySQLImpl(DefaultImpl): def correct_for_autogen_foreignkeys(self, conn_fks, metadata_fks): conn_fk_by_sig = { - compare._fk_constraint_sig(fk).sig: fk for fk in conn_fks + self._create_reflected_constraint_sig(fk).unnamed_no_options: fk + for fk in conn_fks } metadata_fk_by_sig = { - compare._fk_constraint_sig(fk).sig: fk for fk in metadata_fks + self._create_metadata_constraint_sig(fk).unnamed_no_options: fk + for fk in metadata_fks } for sig in set(conn_fk_by_sig).intersection(metadata_fk_by_sig): diff --git a/libs/alembic/ddl/oracle.py b/libs/alembic/ddl/oracle.py index 9715c1e81..540117407 100644 --- a/libs/alembic/ddl/oracle.py +++ b/libs/alembic/ddl/oracle.py @@ -1,3 +1,6 @@ +# mypy: allow-untyped-defs, allow-incomplete-defs, allow-untyped-calls +# mypy: no-warn-return-any, allow-any-generics + from __future__ import annotations import re @@ -5,7 +8,6 @@ from typing import Any from typing import Optional from typing import TYPE_CHECKING -from sqlalchemy.ext.compiler import compiles from sqlalchemy.sql import sqltypes from .base import AddColumn @@ -22,6 +24,7 @@ from .base import format_type from .base import IdentityColumnDefault from .base import RenameTable from .impl import DefaultImpl +from ..util.sqla_compat import compiles if TYPE_CHECKING: from sqlalchemy.dialects.oracle.base import OracleDDLCompiler @@ -176,7 +179,7 @@ def alter_column(compiler: OracleDDLCompiler, name: str) -> str: return "MODIFY %s" % format_column_name(compiler, name) -def add_column(compiler: OracleDDLCompiler, column: Column, **kw) -> str: +def add_column(compiler: OracleDDLCompiler, column: Column[Any], **kw) -> str: return "ADD %s" % compiler.get_column_specification(column, **kw) diff --git a/libs/alembic/ddl/postgresql.py b/libs/alembic/ddl/postgresql.py index 4ffc2eb99..6507fcbdd 100644 --- a/libs/alembic/ddl/postgresql.py +++ b/libs/alembic/ddl/postgresql.py @@ -1,9 +1,13 @@ +# mypy: allow-untyped-defs, allow-incomplete-defs, allow-untyped-calls +# mypy: no-warn-return-any, allow-any-generics + from __future__ import annotations import logging import re from typing import Any from typing import cast +from typing import Dict from typing import List from typing import Optional from typing import Sequence @@ -12,7 +16,6 @@ from typing import TYPE_CHECKING from typing import Union from sqlalchemy import Column -from sqlalchemy import Index from sqlalchemy import literal_column from sqlalchemy import Numeric from sqlalchemy import text @@ -23,18 +26,19 @@ from sqlalchemy.dialects.postgresql import INTEGER from sqlalchemy.schema import CreateIndex from sqlalchemy.sql.elements import ColumnClause from sqlalchemy.sql.elements import TextClause +from sqlalchemy.sql.functions import FunctionElement from sqlalchemy.types import NULLTYPE from .base import alter_column from .base import alter_table from .base import AlterColumn from .base import ColumnComment -from .base import compiles from .base import format_column_name from .base import format_table_name from .base import format_type from .base import IdentityColumnDefault from .base import RenameTable +from .impl import ComparisonResult from .impl import DefaultImpl from .. import util from ..autogenerate import render @@ -43,16 +47,20 @@ from ..operations import schemaobj from ..operations.base import BatchOperations from ..operations.base import Operations from ..util import sqla_compat +from ..util.sqla_compat import compiles if TYPE_CHECKING: from typing import Literal + from sqlalchemy import Index + from sqlalchemy import UniqueConstraint from sqlalchemy.dialects.postgresql.array import ARRAY from sqlalchemy.dialects.postgresql.base import PGDDLCompiler from sqlalchemy.dialects.postgresql.hstore import HSTORE from sqlalchemy.dialects.postgresql.json import JSON from sqlalchemy.dialects.postgresql.json import JSONB - from sqlalchemy.sql.elements import BinaryExpression + from sqlalchemy.sql.elements import ClauseElement + from sqlalchemy.sql.elements import ColumnElement from sqlalchemy.sql.elements import quoted_name from sqlalchemy.sql.schema import MetaData from sqlalchemy.sql.schema import Table @@ -73,20 +81,20 @@ class PostgresqlImpl(DefaultImpl): type_synonyms = DefaultImpl.type_synonyms + ( {"FLOAT", "DOUBLE PRECISION"}, ) - identity_attrs_ignore = ("on_null", "order") - def create_index(self, index): + def create_index(self, index: Index, **kw: Any) -> None: # this likely defaults to None if not present, so get() # should normally not return the default value. being # defensive in any case postgresql_include = index.kwargs.get("postgresql_include", None) or () for col in postgresql_include: - if col not in index.table.c: - index.table.append_column(Column(col, sqltypes.NullType)) - self._exec(CreateIndex(index)) + if col not in index.table.c: # type: ignore[union-attr] + index.table.append_column( # type: ignore[union-attr] + Column(col, sqltypes.NullType) + ) + self._exec(CreateIndex(index, **kw)) def prep_table_for_batch(self, batch_impl, table): - for constraint in table.constraints: if ( constraint.name is not None @@ -131,7 +139,9 @@ class PostgresqlImpl(DefaultImpl): metadata_default = literal_column(metadata_default) # run a real compare against the server - return not self.connection.scalar( + conn = self.connection + assert conn is not None + return not conn.scalar( sqla_compat._select( literal_column(conn_col_default) == metadata_default ) @@ -153,7 +163,6 @@ class PostgresqlImpl(DefaultImpl): existing_autoincrement: Optional[bool] = None, **kw: Any, ) -> None: - using = kw.pop("postgresql_using", None) if using is not None and type_ is None: @@ -235,7 +244,6 @@ class PostgresqlImpl(DefaultImpl): metadata_unique_constraints, metadata_indexes, ): - doubled_constraints = { index for index in conn_indexes @@ -248,38 +256,195 @@ class PostgresqlImpl(DefaultImpl): if not sqla_compat.sqla_2: self._skip_functional_indexes(metadata_indexes, conn_indexes) + # pg behavior regarding modifiers + # | # | compiled sql | returned sql | regexp. group is removed | + # | - | ---------------- | -----------------| ------------------------ | + # | 1 | nulls first | nulls first | - | + # | 2 | nulls last | | (? str: - # start = expr - expr = expr.lower() - expr = expr.replace('"', "") + expr = expr.lower().replace('"', "").replace("'", "") if index.table is not None: + # should not be needed, since include_table=False is in compile expr = expr.replace(f"{index.table.name.lower()}.", "") - while expr and expr[0] == "(" and expr[-1] == ")": - expr = expr[1:-1] if "::" in expr: # strip :: cast. types can have spaces in them expr = re.sub(r"(::[\w ]+\w)", "", expr) - # print(f"START: {start} END: {expr}") + while expr and expr[0] == "(" and expr[-1] == ")": + expr = expr[1:-1] + + # NOTE: when parsing the connection expression this cleanup could + # be skipped + for rs in self._default_modifiers_re: + if match := rs.search(expr): + start, end = match.span(1) + expr = expr[:start] + expr[end:] + break + + while expr and expr[0] == "(" and expr[-1] == ")": + expr = expr[1:-1] + + # strip casts + cast_re = re.compile(r"cast\s*\(") + if cast_re.match(expr): + expr = cast_re.sub("", expr) + # remove the as type + expr = re.sub(r"as\s+[^)]+\)", "", expr) + # remove spaces + expr = expr.replace(" ", "") return expr - def create_index_sig(self, index: Index) -> Tuple[Any, ...]: - if sqla_compat.is_expression_index(index): - return tuple( - self._cleanup_index_expr( - index, - e - if isinstance(e, str) - else e.compile( - dialect=self.dialect, - compile_kwargs={"literal_binds": True}, - ).string, + def _dialect_options( + self, item: Union[Index, UniqueConstraint] + ) -> Tuple[Any, ...]: + # only the positive case is returned by sqlalchemy reflection so + # None and False are threated the same + if item.dialect_kwargs.get("postgresql_nulls_not_distinct"): + return ("nulls_not_distinct",) + return () + + def compare_indexes( + self, + metadata_index: Index, + reflected_index: Index, + ) -> ComparisonResult: + msg = [] + unique_msg = self._compare_index_unique( + metadata_index, reflected_index + ) + if unique_msg: + msg.append(unique_msg) + m_exprs = metadata_index.expressions + r_exprs = reflected_index.expressions + if len(m_exprs) != len(r_exprs): + msg.append(f"expression number {len(r_exprs)} to {len(m_exprs)}") + if msg: + # no point going further, return early + return ComparisonResult.Different(msg) + skip = [] + for pos, (m_e, r_e) in enumerate(zip(m_exprs, r_exprs), 1): + m_compile = self._compile_element(m_e) + m_text = self._cleanup_index_expr(metadata_index, m_compile) + # print(f"META ORIG: {m_compile!r} CLEANUP: {m_text!r}") + r_compile = self._compile_element(r_e) + r_text = self._cleanup_index_expr(metadata_index, r_compile) + # print(f"CONN ORIG: {r_compile!r} CLEANUP: {r_text!r}") + if m_text == r_text: + continue # expressions these are equal + elif m_compile.strip().endswith("_ops") and ( + " " in m_compile or ")" in m_compile # is an expression + ): + skip.append( + f"expression #{pos} {m_compile!r} detected " + "as including operator clause." ) - for e in index.expressions - ) + util.warn( + f"Expression #{pos} {m_compile!r} in index " + f"{reflected_index.name!r} detected to include " + "an operator clause. Expression compare cannot proceed. " + "Please move the operator clause to the " + "``postgresql_ops`` dict to enable proper compare " + "of the index expressions: " + "https://docs.sqlalchemy.org/en/latest/dialects/postgresql.html#operator-classes", # noqa: E501 + ) + else: + msg.append(f"expression #{pos} {r_compile!r} to {m_compile!r}") + + m_options = self._dialect_options(metadata_index) + r_options = self._dialect_options(reflected_index) + if m_options != r_options: + msg.extend(f"options {r_options} to {m_options}") + + if msg: + return ComparisonResult.Different(msg) + elif skip: + # if there are other changes detected don't skip the index + return ComparisonResult.Skip(skip) else: - return super().create_index_sig(index) + return ComparisonResult.Equal() + + def compare_unique_constraint( + self, + metadata_constraint: UniqueConstraint, + reflected_constraint: UniqueConstraint, + ) -> ComparisonResult: + metadata_tup = self._create_metadata_constraint_sig( + metadata_constraint + ) + reflected_tup = self._create_reflected_constraint_sig( + reflected_constraint + ) + + meta_sig = metadata_tup.unnamed + conn_sig = reflected_tup.unnamed + if conn_sig != meta_sig: + return ComparisonResult.Different( + f"expression {conn_sig} to {meta_sig}" + ) + + metadata_do = self._dialect_options(metadata_tup.const) + conn_do = self._dialect_options(reflected_tup.const) + if metadata_do != conn_do: + return ComparisonResult.Different( + f"expression {conn_do} to {metadata_do}" + ) + + return ComparisonResult.Equal() + + def adjust_reflected_dialect_options( + self, reflected_options: Dict[str, Any], kind: str + ) -> Dict[str, Any]: + options: Dict[str, Any] + options = reflected_options.get("dialect_options", {}).copy() + if not options.get("postgresql_include"): + options.pop("postgresql_include", None) + return options + + def _compile_element(self, element: Union[ClauseElement, str]) -> str: + if isinstance(element, str): + return element + return element.compile( + dialect=self.dialect, + compile_kwargs={"literal_binds": True, "include_table": False}, + ).string + + def render_ddl_sql_expr( + self, + expr: ClauseElement, + is_server_default: bool = False, + is_index: bool = False, + **kw: Any, + ) -> str: + """Render a SQL expression that is typically a server default, + index expression, etc. + + """ + + # apply self_group to index expressions; + # see https://github.com/sqlalchemy/sqlalchemy/blob/ + # 82fa95cfce070fab401d020c6e6e4a6a96cc2578/ + # lib/sqlalchemy/dialects/postgresql/base.py#L2261 + if is_index and not isinstance(expr, ColumnClause): + expr = expr.self_group() + + return super().render_ddl_sql_expr( + expr, is_server_default=is_server_default, is_index=is_index, **kw + ) def render_type( self, type_: TypeEngine, autogen_context: AutogenContext @@ -440,9 +605,9 @@ class CreateExcludeConstraintOp(ops.AddConstraintOp): table_name: Union[str, quoted_name], elements: Union[ Sequence[Tuple[str, str]], - Sequence[Tuple[ColumnClause, str]], + Sequence[Tuple[ColumnClause[Any], str]], ], - where: Optional[Union[BinaryExpression, str]] = None, + where: Optional[Union[ColumnElement[bool], str]] = None, schema: Optional[str] = None, _orig_constraint: Optional[ExcludeConstraint] = None, **kw, @@ -463,13 +628,10 @@ class CreateExcludeConstraintOp(ops.AddConstraintOp): return cls( constraint.name, constraint_table.name, - [ - (expr, op) - for expr, name, op in constraint._render_exprs # type:ignore[attr-defined] # noqa + [ # type: ignore + (expr, op) for expr, name, op in constraint._render_exprs ], - where=cast( - "Optional[Union[BinaryExpression, str]]", constraint.where - ), + where=cast("ColumnElement[bool] | None", constraint.where), schema=constraint_table.schema, _orig_constraint=constraint, deferrable=constraint.deferrable, @@ -494,7 +656,7 @@ class CreateExcludeConstraintOp(ops.AddConstraintOp): expr, name, oper, - ) in excl._render_exprs: # type:ignore[attr-defined] + ) in excl._render_exprs: t.append_column(Column(name, NULLTYPE)) t.append_constraint(excl) return excl @@ -521,11 +683,9 @@ class CreateExcludeConstraintOp(ops.AddConstraintOp): op.create_exclude_constraint( "user_excl", "user", - - ("period", '&&'), - ("group", '='), - where=("group != 'some group'") - + ("period", "&&"), + ("group", "="), + where=("group != 'some group'"), ) Note that the expressions work the same way as that of @@ -549,8 +709,12 @@ class CreateExcludeConstraintOp(ops.AddConstraintOp): @classmethod def batch_create_exclude_constraint( - cls, operations, constraint_name, *elements, **kw - ): + cls, + operations: BatchOperations, + constraint_name: str, + *elements: Any, + **kw: Any, + ) -> Optional[Table]: """Issue a "create exclude constraint" instruction using the current batch migration context. @@ -590,7 +754,6 @@ def _render_inline_exclude_constraint( def _postgresql_autogenerate_prefix(autogen_context: AutogenContext) -> str: - imports = autogen_context.imports if imports is not None: imports.add("from sqlalchemy.dialects import postgresql") @@ -619,38 +782,16 @@ def _exclude_constraint( ("name", render._render_gen_name(autogen_context, constraint.name)) ) - if alter: - args = [ - repr(render._render_gen_name(autogen_context, constraint.name)) - ] - if not has_batch: - args += [repr(render._ident(constraint.table.name))] - args.extend( - [ - "(%s, %r)" - % ( - _render_potential_column(sqltext, autogen_context), - opstring, - ) - for sqltext, name, opstring in constraint._render_exprs # type:ignore[attr-defined] # noqa - ] - ) - if constraint.where is not None: - args.append( - "where=%s" - % render._render_potential_expr( - constraint.where, autogen_context - ) - ) - args.extend(["%s=%r" % (k, v) for k, v in opts]) - return "%(prefix)screate_exclude_constraint(%(args)s)" % { - "prefix": render._alembic_autogenerate_prefix(autogen_context), - "args": ", ".join(args), - } - else: + def do_expr_where_opts(): args = [ "(%s, %r)" - % (_render_potential_column(sqltext, autogen_context), opstring) + % ( + _render_potential_column( + sqltext, # type:ignore[arg-type] + autogen_context, + ), + opstring, + ) for sqltext, name, opstring in constraint._render_exprs ] if constraint.where is not None: @@ -661,6 +802,21 @@ def _exclude_constraint( ) ) args.extend(["%s=%r" % (k, v) for k, v in opts]) + return args + + if alter: + args = [ + repr(render._render_gen_name(autogen_context, constraint.name)) + ] + if not has_batch: + args += [repr(render._ident(constraint.table.name))] + args.extend(do_expr_where_opts()) + return "%(prefix)screate_exclude_constraint(%(args)s)" % { + "prefix": render._alembic_autogenerate_prefix(autogen_context), + "args": ", ".join(args), + } + else: + args = do_expr_where_opts() return "%(prefix)sExcludeConstraint(%(args)s)" % { "prefix": _postgresql_autogenerate_prefix(autogen_context), "args": ", ".join(args), @@ -668,7 +824,9 @@ def _exclude_constraint( def _render_potential_column( - value: Union[ColumnClause, Column, TextClause], + value: Union[ + ColumnClause[Any], Column[Any], TextClause, FunctionElement[Any] + ], autogen_context: AutogenContext, ) -> str: if isinstance(value, ColumnClause): @@ -684,5 +842,7 @@ def _render_potential_column( } else: return render._render_potential_expr( - value, autogen_context, wrap_in_text=isinstance(value, TextClause) + value, + autogen_context, + wrap_in_text=isinstance(value, (TextClause, FunctionElement)), ) diff --git a/libs/alembic/ddl/sqlite.py b/libs/alembic/ddl/sqlite.py index 302a87752..762e8ca19 100644 --- a/libs/alembic/ddl/sqlite.py +++ b/libs/alembic/ddl/sqlite.py @@ -1,3 +1,6 @@ +# mypy: allow-untyped-defs, allow-incomplete-defs, allow-untyped-calls +# mypy: no-warn-return-any, allow-any-generics + from __future__ import annotations import re @@ -11,13 +14,13 @@ from sqlalchemy import cast from sqlalchemy import JSON from sqlalchemy import schema from sqlalchemy import sql -from sqlalchemy.ext.compiler import compiles from .base import alter_table from .base import format_table_name from .base import RenameTable from .impl import DefaultImpl from .. import util +from ..util.sqla_compat import compiles if TYPE_CHECKING: from sqlalchemy.engine.reflection import Inspector @@ -71,13 +74,13 @@ class SQLiteImpl(DefaultImpl): def add_constraint(self, const: Constraint): # attempt to distinguish between an # auto-gen constraint and an explicit one - if const._create_rule is None: # type:ignore[attr-defined] + if const._create_rule is None: raise NotImplementedError( "No support for ALTER of constraints in SQLite dialect. " "Please refer to the batch mode feature which allows for " "SQLite migrations using a copy-and-move strategy." ) - elif const._create_rule(self): # type:ignore[attr-defined] + elif const._create_rule(self): util.warn( "Skipping unsupported ALTER for " "creation of implicit constraint. " @@ -86,7 +89,7 @@ class SQLiteImpl(DefaultImpl): ) def drop_constraint(self, const: Constraint): - if const._create_rule is None: # type:ignore[attr-defined] + if const._create_rule is None: raise NotImplementedError( "No support for ALTER of constraints in SQLite dialect. " "Please refer to the batch mode feature which allows for " @@ -95,12 +98,11 @@ class SQLiteImpl(DefaultImpl): def compare_server_default( self, - inspector_column: Column, - metadata_column: Column, + inspector_column: Column[Any], + metadata_column: Column[Any], rendered_metadata_default: Optional[str], rendered_inspector_default: Optional[str], ) -> bool: - if rendered_metadata_default is not None: rendered_metadata_default = re.sub( r"^\((.+)\)$", r"\1", rendered_metadata_default @@ -173,13 +175,12 @@ class SQLiteImpl(DefaultImpl): def cast_for_batch_migrate( self, - existing: Column, + existing: Column[Any], existing_transfer: Dict[str, Union[TypeEngine, Cast]], new_type: TypeEngine, ) -> None: if ( - existing.type._type_affinity # type:ignore[attr-defined] - is not new_type._type_affinity # type:ignore[attr-defined] + existing.type._type_affinity is not new_type._type_affinity and not isinstance(new_type, JSON) ): existing_transfer["expr"] = cast( @@ -193,7 +194,6 @@ class SQLiteImpl(DefaultImpl): metadata_unique_constraints, metadata_indexes, ): - self._skip_functional_indexes(metadata_indexes, conn_indexes) diff --git a/libs/alembic/op.pyi b/libs/alembic/op.pyi index 2f92dc340..83deac1eb 100644 --- a/libs/alembic/op.pyi +++ b/libs/alembic/op.pyi @@ -1,7 +1,10 @@ # ### this file stubs are generated by tools/write_pyi.py - do not edit ### # ### imports are manually managed +from __future__ import annotations + from contextlib import contextmanager from typing import Any +from typing import Awaitable from typing import Callable from typing import Dict from typing import Iterator @@ -9,21 +12,21 @@ from typing import List from typing import Literal from typing import Mapping from typing import Optional +from typing import overload from typing import Sequence from typing import Tuple from typing import Type from typing import TYPE_CHECKING +from typing import TypeVar from typing import Union -from sqlalchemy.sql.expression import TableClause -from sqlalchemy.sql.expression import Update - if TYPE_CHECKING: - from sqlalchemy.engine import Connection - from sqlalchemy.sql.elements import BinaryExpression + from sqlalchemy.sql import Executable + from sqlalchemy.sql.elements import ColumnElement from sqlalchemy.sql.elements import conv from sqlalchemy.sql.elements import TextClause + from sqlalchemy.sql.expression import TableClause from sqlalchemy.sql.functions import Function from sqlalchemy.sql.schema import Column from sqlalchemy.sql.schema import Computed @@ -33,16 +36,34 @@ if TYPE_CHECKING: from sqlalchemy.sql.type_api import TypeEngine from sqlalchemy.util import immutabledict - from .operations.ops import BatchOperations + from .operations.base import BatchOperations + from .operations.ops import AddColumnOp + from .operations.ops import AddConstraintOp + from .operations.ops import AlterColumnOp + from .operations.ops import AlterTableOp + from .operations.ops import BulkInsertOp + from .operations.ops import CreateIndexOp + from .operations.ops import CreateTableCommentOp + from .operations.ops import CreateTableOp + from .operations.ops import DropColumnOp + from .operations.ops import DropConstraintOp + from .operations.ops import DropIndexOp + from .operations.ops import DropTableCommentOp + from .operations.ops import DropTableOp + from .operations.ops import ExecuteSQLOp from .operations.ops import MigrateOperation from .runtime.migration import MigrationContext from .util.sqla_compat import _literal_bindparam + +_T = TypeVar("_T") +_C = TypeVar("_C", bound=Callable[..., Any]) + ### end imports ### def add_column( - table_name: str, column: Column, schema: Optional[str] = None -) -> Optional[Table]: - r"""Issue an "add column" instruction using the current + table_name: str, column: Column[Any], *, schema: Optional[str] = None +) -> None: + """Issue an "add column" instruction using the current migration context. e.g.:: @@ -50,35 +71,64 @@ def add_column( from alembic import op from sqlalchemy import Column, String - op.add_column('organization', - Column('name', String()) - ) + op.add_column("organization", Column("name", String())) - The provided :class:`~sqlalchemy.schema.Column` object can also - specify a :class:`~sqlalchemy.schema.ForeignKey`, referencing - a remote table name. Alembic will automatically generate a stub - "referenced" table and emit a second ALTER statement in order - to add the constraint separately:: + The :meth:`.Operations.add_column` method typically corresponds + to the SQL command "ALTER TABLE... ADD COLUMN". Within the scope + of this command, the column's name, datatype, nullability, + and optional server-generated defaults may be indicated. + + .. note:: + + With the exception of NOT NULL constraints or single-column FOREIGN + KEY constraints, other kinds of constraints such as PRIMARY KEY, + UNIQUE or CHECK constraints **cannot** be generated using this + method; for these constraints, refer to operations such as + :meth:`.Operations.create_primary_key` and + :meth:`.Operations.create_check_constraint`. In particular, the + following :class:`~sqlalchemy.schema.Column` parameters are + **ignored**: + + * :paramref:`~sqlalchemy.schema.Column.primary_key` - SQL databases + typically do not support an ALTER operation that can add + individual columns one at a time to an existing primary key + constraint, therefore it's less ambiguous to use the + :meth:`.Operations.create_primary_key` method, which assumes no + existing primary key constraint is present. + * :paramref:`~sqlalchemy.schema.Column.unique` - use the + :meth:`.Operations.create_unique_constraint` method + * :paramref:`~sqlalchemy.schema.Column.index` - use the + :meth:`.Operations.create_index` method + + + The provided :class:`~sqlalchemy.schema.Column` object may include a + :class:`~sqlalchemy.schema.ForeignKey` constraint directive, + referencing a remote table name. For this specific type of constraint, + Alembic will automatically emit a second ALTER statement in order to + add the single-column FOREIGN KEY constraint separately:: from alembic import op from sqlalchemy import Column, INTEGER, ForeignKey - op.add_column('organization', - Column('account_id', INTEGER, ForeignKey('accounts.id')) + op.add_column( + "organization", + Column("account_id", INTEGER, ForeignKey("accounts.id")), ) - Note that this statement uses the :class:`~sqlalchemy.schema.Column` - construct as is from the SQLAlchemy library. In particular, - default values to be created on the database side are - specified using the ``server_default`` parameter, and not - ``default`` which only specifies Python-side defaults:: + The column argument passed to :meth:`.Operations.add_column` is a + :class:`~sqlalchemy.schema.Column` construct, used in the same way it's + used in SQLAlchemy. In particular, values or functions to be indicated + as producing the column's default value on the database side are + specified using the ``server_default`` parameter, and not ``default`` + which only specifies Python-side defaults:: from alembic import op from sqlalchemy import Column, TIMESTAMP, func # specify "DEFAULT NOW" along with the column add - op.add_column('account', - Column('timestamp', TIMESTAMP, server_default=func.now()) + op.add_column( + "account", + Column("timestamp", TIMESTAMP, server_default=func.now()), ) :param table_name: String name of the parent table. @@ -94,20 +144,21 @@ def add_column( def alter_column( table_name: str, column_name: str, + *, nullable: Optional[bool] = None, comment: Union[str, Literal[False], None] = False, server_default: Any = False, new_column_name: Optional[str] = None, - type_: Union[TypeEngine, Type[TypeEngine], None] = None, - existing_type: Union[TypeEngine, Type[TypeEngine], None] = None, + type_: Union[TypeEngine[Any], Type[TypeEngine[Any]], None] = None, + existing_type: Union[TypeEngine[Any], Type[TypeEngine[Any]], None] = None, existing_server_default: Union[ str, bool, Identity, Computed, None ] = False, existing_nullable: Optional[bool] = None, existing_comment: Optional[str] = None, schema: Optional[str] = None, - **kw: Any -) -> Optional[Table]: + **kw: Any, +) -> None: r"""Issue an "alter column" instruction using the current migration context. @@ -146,9 +197,6 @@ def alter_column( Set to ``None`` to have the default removed. :param comment: optional string text of a new comment to add to the column. - - .. versionadded:: 1.0.6 - :param new_column_name: Optional; specify a string name here to indicate the new name within a column rename operation. :param type\_: Optional; a :class:`~sqlalchemy.types.TypeEngine` @@ -165,7 +213,7 @@ def alter_column( don't otherwise specify a new type, as well as for when nullability is being changed on a SQL Server column. It is also used if the type is a so-called - SQLlchemy "schema" type which may define a constraint (i.e. + SQLAlchemy "schema" type which may define a constraint (i.e. :class:`~sqlalchemy.types.Boolean`, :class:`~sqlalchemy.types.Enum`), so that the constraint can be dropped. @@ -182,9 +230,6 @@ def alter_column( :param existing_comment: string text of the existing comment on the column to be maintained. Required on MySQL if the existing comment on the column is not being changed. - - .. versionadded:: 1.0.6 - :param schema: Optional schema name to operate within. To control quoting of the schema outside of the default behavior, use the SQLAlchemy construct @@ -202,7 +247,7 @@ def batch_alter_table( table_name: str, schema: Optional[str] = None, recreate: Literal["auto", "always", "never"] = "auto", - partial_reordering: Optional[tuple] = None, + partial_reordering: Optional[Tuple[Any, ...]] = None, copy_from: Optional[Table] = None, table_args: Tuple[Any, ...] = (), table_kwargs: Mapping[str, Any] = immutabledict({}), @@ -210,7 +255,7 @@ def batch_alter_table( reflect_kwargs: Mapping[str, Any] = immutabledict({}), naming_convention: Optional[Dict[str, str]] = None, ) -> Iterator[BatchOperations]: - r"""Invoke a series of per-table migrations in batch. + """Invoke a series of per-table migrations in batch. Batch mode allows a series of operations specific to a table to be syntactically grouped together, and allows for alternate @@ -240,8 +285,8 @@ def batch_alter_table( are omitted. E.g.:: with op.batch_alter_table("some_table") as batch_op: - batch_op.add_column(Column('foo', Integer)) - batch_op.drop_column('bar') + batch_op.add_column(Column("foo", Integer)) + batch_op.drop_column("bar") The operations within the context manager are invoked at once when the context is ended. When run against SQLite, if the @@ -320,16 +365,18 @@ def batch_alter_table( Specify the order of all columns:: with op.batch_alter_table( - "some_table", recreate="always", - partial_reordering=[("c", "d", "a", "b")] + "some_table", + recreate="always", + partial_reordering=[("c", "d", "a", "b")], ) as batch_op: pass Ensure "d" appears before "c", and "b", appears before "a":: with op.batch_alter_table( - "some_table", recreate="always", - partial_reordering=[("d", "c"), ("b", "a")] + "some_table", + recreate="always", + partial_reordering=[("d", "c"), ("b", "a")], ) as batch_op: pass @@ -337,8 +384,6 @@ def batch_alter_table( set is undefined. Therefore it is best to specify the complete ordering of all columns for best results. - .. versionadded:: 1.4.0 - .. note:: batch mode requires SQLAlchemy 0.8 or above. .. seealso:: @@ -349,10 +394,11 @@ def batch_alter_table( def bulk_insert( table: Union[Table, TableClause], - rows: List[dict], + rows: List[Dict[str, Any]], + *, multiinsert: bool = True, ) -> None: - r"""Issue a "bulk insert" operation using the current + """Issue a "bulk insert" operation using the current migration context. This provides a means of representing an INSERT of multiple rows @@ -369,37 +415,58 @@ def bulk_insert( from sqlalchemy import String, Integer, Date # Create an ad-hoc table to use for the insert statement. - accounts_table = table('account', - column('id', Integer), - column('name', String), - column('create_date', Date) + accounts_table = table( + "account", + column("id", Integer), + column("name", String), + column("create_date", Date), ) - op.bulk_insert(accounts_table, + op.bulk_insert( + accounts_table, [ - {'id':1, 'name':'John Smith', - 'create_date':date(2010, 10, 5)}, - {'id':2, 'name':'Ed Williams', - 'create_date':date(2007, 5, 27)}, - {'id':3, 'name':'Wendy Jones', - 'create_date':date(2008, 8, 15)}, - ] + { + "id": 1, + "name": "John Smith", + "create_date": date(2010, 10, 5), + }, + { + "id": 2, + "name": "Ed Williams", + "create_date": date(2007, 5, 27), + }, + { + "id": 3, + "name": "Wendy Jones", + "create_date": date(2008, 8, 15), + }, + ], ) When using --sql mode, some datatypes may not render inline automatically, such as dates and other special types. When this issue is present, :meth:`.Operations.inline_literal` may be used:: - op.bulk_insert(accounts_table, + op.bulk_insert( + accounts_table, [ - {'id':1, 'name':'John Smith', - 'create_date':op.inline_literal("2010-10-05")}, - {'id':2, 'name':'Ed Williams', - 'create_date':op.inline_literal("2007-05-27")}, - {'id':3, 'name':'Wendy Jones', - 'create_date':op.inline_literal("2008-08-15")}, + { + "id": 1, + "name": "John Smith", + "create_date": op.inline_literal("2010-10-05"), + }, + { + "id": 2, + "name": "Ed Williams", + "create_date": op.inline_literal("2007-05-27"), + }, + { + "id": 3, + "name": "Wendy Jones", + "create_date": op.inline_literal("2008-08-15"), + }, ], - multiinsert=False + multiinsert=False, ) When using :meth:`.Operations.inline_literal` in conjunction with @@ -430,11 +497,12 @@ def bulk_insert( def create_check_constraint( constraint_name: Optional[str], table_name: str, - condition: Union[str, BinaryExpression], + condition: Union[str, ColumnElement[bool], TextClause], + *, schema: Optional[str] = None, - **kw: Any -) -> Optional[Table]: - r"""Issue a "create check constraint" instruction using the + **kw: Any, +) -> None: + """Issue a "create check constraint" instruction using the current migration context. e.g.:: @@ -445,7 +513,7 @@ def create_check_constraint( op.create_check_constraint( "ck_user_name_len", "user", - func.len(column('name')) > 5 + func.len(column("name")) > 5, ) CHECK constraints are usually against a SQL expression, so ad-hoc @@ -478,7 +546,7 @@ def create_check_constraint( def create_exclude_constraint( constraint_name: str, table_name: str, *elements: Any, **kw: Any ) -> Optional[Table]: - r"""Issue an alter to create an EXCLUDE constraint using the + """Issue an alter to create an EXCLUDE constraint using the current migration context. .. note:: This method is Postgresql specific, and additionally @@ -491,11 +559,9 @@ def create_exclude_constraint( op.create_exclude_constraint( "user_excl", "user", - - ("period", '&&'), - ("group", '='), - where=("group != 'some group'") - + ("period", "&&"), + ("group", "="), + where=("group != 'some group'"), ) Note that the expressions work the same way as that of @@ -521,6 +587,7 @@ def create_foreign_key( referent_table: str, local_cols: List[str], remote_cols: List[str], + *, onupdate: Optional[str] = None, ondelete: Optional[str] = None, deferrable: Optional[bool] = None, @@ -528,17 +595,22 @@ def create_foreign_key( match: Optional[str] = None, source_schema: Optional[str] = None, referent_schema: Optional[str] = None, - **dialect_kw: Any -) -> Optional[Table]: - r"""Issue a "create foreign key" instruction using the + **dialect_kw: Any, +) -> None: + """Issue a "create foreign key" instruction using the current migration context. e.g.:: from alembic import op + op.create_foreign_key( - "fk_user_address", "address", - "user", ["user_id"], ["id"]) + "fk_user_address", + "address", + "user", + ["user_id"], + ["id"], + ) This internally generates a :class:`~sqlalchemy.schema.Table` object containing the necessary columns, then generates a new @@ -579,24 +651,28 @@ def create_index( index_name: Optional[str], table_name: str, columns: Sequence[Union[str, TextClause, Function[Any]]], + *, schema: Optional[str] = None, unique: bool = False, - **kw: Any -) -> Optional[Table]: + if_not_exists: Optional[bool] = None, + **kw: Any, +) -> None: r"""Issue a "create index" instruction using the current migration context. e.g.:: from alembic import op - op.create_index('ik_test', 't1', ['foo', 'bar']) + + op.create_index("ik_test", "t1", ["foo", "bar"]) Functional indexes can be produced by using the :func:`sqlalchemy.sql.expression.text` construct:: from alembic import op from sqlalchemy import text - op.create_index('ik_test', 't1', [text('lower(foo)')]) + + op.create_index("ik_test", "t1", [text("lower(foo)")]) :param index_name: name of the index. :param table_name: name of the owning table. @@ -608,20 +684,24 @@ def create_index( :class:`~sqlalchemy.sql.elements.quoted_name`. :param unique: If True, create a unique index. - :param quote: - Force quoting of this column's name on or off, corresponding - to ``True`` or ``False``. When left at its default - of ``None``, the column identifier will be quoted according to - whether the name is case sensitive (identifiers with at least one - upper case character are treated as case sensitive), or if it's a - reserved word. This flag is only needed to force quoting of a - reserved word which is not known by the SQLAlchemy dialect. + :param quote: Force quoting of this column's name on or off, + corresponding to ``True`` or ``False``. When left at its default + of ``None``, the column identifier will be quoted according to + whether the name is case sensitive (identifiers with at least one + upper case character are treated as case sensitive), or if it's a + reserved word. This flag is only needed to force quoting of a + reserved word which is not known by the SQLAlchemy dialect. + + :param if_not_exists: If True, adds IF NOT EXISTS operator when + creating the new index. + + .. versionadded:: 1.12.0 :param \**kw: Additional keyword arguments not mentioned above are - dialect specific, and passed in the form - ``_``. - See the documentation regarding an individual dialect at - :ref:`dialect_toplevel` for detail on documented arguments. + dialect specific, and passed in the form + ``_``. + See the documentation regarding an individual dialect at + :ref:`dialect_toplevel` for detail on documented arguments. """ @@ -629,18 +709,17 @@ def create_primary_key( constraint_name: Optional[str], table_name: str, columns: List[str], + *, schema: Optional[str] = None, -) -> Optional[Table]: - r"""Issue a "create primary key" instruction using the current +) -> None: + """Issue a "create primary key" instruction using the current migration context. e.g.:: from alembic import op - op.create_primary_key( - "pk_my_table", "my_table", - ["id", "version"] - ) + + op.create_primary_key("pk_my_table", "my_table", ["id", "version"]) This internally generates a :class:`~sqlalchemy.schema.Table` object containing the necessary columns, then generates a new @@ -668,9 +747,7 @@ def create_primary_key( """ -def create_table( - table_name: str, *columns: SchemaItem, **kw: Any -) -> Optional[Table]: +def create_table(table_name: str, *columns: SchemaItem, **kw: Any) -> Table: r"""Issue a "create table" instruction using the current migration context. @@ -682,11 +759,11 @@ def create_table( from alembic import op op.create_table( - 'account', - Column('id', INTEGER, primary_key=True), - Column('name', VARCHAR(50), nullable=False), - Column('description', NVARCHAR(200)), - Column('timestamp', TIMESTAMP, server_default=func.now()) + "account", + Column("id", INTEGER, primary_key=True), + Column("name", VARCHAR(50), nullable=False), + Column("description", NVARCHAR(200)), + Column("timestamp", TIMESTAMP, server_default=func.now()), ) Note that :meth:`.create_table` accepts @@ -700,9 +777,10 @@ def create_table( from sqlalchemy import Column, TIMESTAMP, func # specify "DEFAULT NOW" along with the "timestamp" column - op.create_table('account', - Column('id', INTEGER, primary_key=True), - Column('timestamp', TIMESTAMP, server_default=func.now()) + op.create_table( + "account", + Column("id", INTEGER, primary_key=True), + Column("timestamp", TIMESTAMP, server_default=func.now()), ) The function also returns a newly created @@ -715,11 +793,11 @@ def create_table( from alembic import op account_table = op.create_table( - 'account', - Column('id', INTEGER, primary_key=True), - Column('name', VARCHAR(50), nullable=False), - Column('description', NVARCHAR(200)), - Column('timestamp', TIMESTAMP, server_default=func.now()) + "account", + Column("id", INTEGER, primary_key=True), + Column("name", VARCHAR(50), nullable=False), + Column("description", NVARCHAR(200)), + Column("timestamp", TIMESTAMP, server_default=func.now()), ) op.bulk_insert( @@ -727,7 +805,7 @@ def create_table( [ {"name": "A1", "description": "account 1"}, {"name": "A2", "description": "account 2"}, - ] + ], ) :param table_name: Name of the table @@ -751,12 +829,11 @@ def create_table( def create_table_comment( table_name: str, comment: Optional[str], - existing_comment: None = None, + *, + existing_comment: Optional[str] = None, schema: Optional[str] = None, -) -> Optional[Table]: - r"""Emit a COMMENT ON operation to set the comment for a table. - - .. versionadded:: 1.0.6 +) -> None: + """Emit a COMMENT ON operation to set the comment for a table. :param table_name: string name of the target table. :param comment: string value of the comment being registered against @@ -778,10 +855,11 @@ def create_unique_constraint( constraint_name: Optional[str], table_name: str, columns: Sequence[str], + *, schema: Optional[str] = None, - **kw: Any + **kw: Any, ) -> Any: - r"""Issue a "create unique constraint" instruction using the + """Issue a "create unique constraint" instruction using the current migration context. e.g.:: @@ -820,14 +898,18 @@ def create_unique_constraint( """ def drop_column( - table_name: str, column_name: str, schema: Optional[str] = None, **kw: Any -) -> Optional[Table]: - r"""Issue a "drop column" instruction using the current + table_name: str, + column_name: str, + *, + schema: Optional[str] = None, + **kw: Any, +) -> None: + """Issue a "drop column" instruction using the current migration context. e.g.:: - drop_column('organization', 'account_id') + drop_column("organization", "account_id") :param table_name: name of table :param column_name: name of column @@ -863,8 +945,9 @@ def drop_constraint( constraint_name: str, table_name: str, type_: Optional[str] = None, + *, schema: Optional[str] = None, -) -> Optional[Table]: +) -> None: r"""Drop a constraint of the given name, typically via DROP CONSTRAINT. :param constraint_name: name of the constraint. @@ -881,9 +964,11 @@ def drop_constraint( def drop_index( index_name: str, table_name: Optional[str] = None, + *, schema: Optional[str] = None, - **kw: Any -) -> Optional[Table]: + if_exists: Optional[bool] = None, + **kw: Any, +) -> None: r"""Issue a "drop index" instruction using the current migration context. @@ -898,16 +983,22 @@ def drop_index( quoting of the schema outside of the default behavior, use the SQLAlchemy construct :class:`~sqlalchemy.sql.elements.quoted_name`. + + :param if_exists: If True, adds IF EXISTS operator when + dropping the index. + + .. versionadded:: 1.12.0 + :param \**kw: Additional keyword arguments not mentioned above are - dialect specific, and passed in the form - ``_``. - See the documentation regarding an individual dialect at - :ref:`dialect_toplevel` for detail on documented arguments. + dialect specific, and passed in the form + ``_``. + See the documentation regarding an individual dialect at + :ref:`dialect_toplevel` for detail on documented arguments. """ def drop_table( - table_name: str, schema: Optional[str] = None, **kw: Any + table_name: str, *, schema: Optional[str] = None, **kw: Any ) -> None: r"""Issue a "drop table" instruction using the current migration context. @@ -929,14 +1020,13 @@ def drop_table( def drop_table_comment( table_name: str, + *, existing_comment: Optional[str] = None, schema: Optional[str] = None, -) -> Optional[Table]: - r"""Issue a "drop table comment" operation to +) -> None: + """Issue a "drop table comment" operation to remove an existing comment set on a table. - .. versionadded:: 1.0.6 - :param table_name: string name of the target table. :param existing_comment: An optional string value of a comment already registered on the specified table. @@ -950,8 +1040,10 @@ def drop_table_comment( """ def execute( - sqltext: Union[str, TextClause, Update], execution_options: None = None -) -> Optional[Table]: + sqltext: Union[Executable, str], + *, + execution_options: Optional[dict[str, Any]] = None, +) -> None: r"""Execute the given SQL using the current migration context. The given SQL can be a plain string, e.g.:: @@ -965,14 +1057,12 @@ def execute( from sqlalchemy import String from alembic import op - account = table('account', - column('name', String) - ) + account = table("account", column("name", String)) op.execute( - account.update().\\ - where(account.c.name==op.inline_literal('account 1')).\\ - values({'name':op.inline_literal('account 2')}) - ) + account.update() + .where(account.c.name == op.inline_literal("account 1")) + .values({"name": op.inline_literal("account 2")}) + ) Above, we made use of the SQLAlchemy :func:`sqlalchemy.sql.expression.table` and @@ -996,15 +1086,17 @@ def execute( also be used normally, use the "bind" available from the context:: from alembic import op + connection = op.get_bind() connection.execute( - account.update().where(account.c.name=='account 1'). - values({"name": "account 2"}) + account.update() + .where(account.c.name == "account 1") + .values({"name": "account 2"}) ) Additionally, when passing the statement as a plain string, it is first - coerceed into a :func:`sqlalchemy.sql.expression.text` construct + coerced into a :func:`sqlalchemy.sql.expression.text` construct before being passed along. In the less likely case that the literal SQL string contains a colon, it must be escaped with a backslash, as:: @@ -1017,11 +1109,10 @@ def execute( * a string * a :func:`sqlalchemy.sql.expression.text` construct. * a :func:`sqlalchemy.sql.expression.insert` construct. - * a :func:`sqlalchemy.sql.expression.update`, - :func:`sqlalchemy.sql.expression.insert`, - or :func:`sqlalchemy.sql.expression.delete` construct. - * Pretty much anything that's "executable" as described - in :ref:`sqlexpression_toplevel`. + * a :func:`sqlalchemy.sql.expression.update` construct. + * a :func:`sqlalchemy.sql.expression.delete` construct. + * Any "executable" described in SQLAlchemy Core documentation, + noting that no result set is returned. .. note:: when passing a plain string, the statement is coerced into a :func:`sqlalchemy.sql.expression.text` construct. This construct @@ -1035,7 +1126,7 @@ def execute( """ def f(name: str) -> conv: - r"""Indicate a string name that has already had a naming convention + """Indicate a string name that has already had a naming convention applied to it. This feature combines with the SQLAlchemy ``naming_convention`` feature @@ -1048,7 +1139,7 @@ def f(name: str) -> conv: If the :meth:`.Operations.f` is used on a constraint, the naming convention will not take effect:: - op.add_column('t', 'x', Boolean(name=op.f('ck_bool_t_x'))) + op.add_column("t", "x", Boolean(name=op.f("ck_bool_t_x"))) Above, the CHECK constraint generated will have the name ``ck_bool_t_x`` regardless of whether or not a naming convention is @@ -1060,7 +1151,7 @@ def f(name: str) -> conv: ``{"ck": "ck_bool_%(table_name)s_%(constraint_name)s"}``, then the output of the following: - op.add_column('t', 'x', Boolean(name='x')) + op.add_column("t", "x", Boolean(name="x")) will be:: @@ -1072,7 +1163,7 @@ def f(name: str) -> conv: """ def get_bind() -> Connection: - r"""Return the current 'bind'. + """Return the current 'bind'. Under normal circumstances, this is the :class:`~sqlalchemy.engine.Connection` currently being used @@ -1083,13 +1174,13 @@ def get_bind() -> Connection: """ def get_context() -> MigrationContext: - r"""Return the :class:`.MigrationContext` object that's + """Return the :class:`.MigrationContext` object that's currently in use. """ -def implementation_for(op_cls: Any) -> Callable[..., Any]: - r"""Register an implementation for a given :class:`.MigrateOperation`. +def implementation_for(op_cls: Any) -> Callable[[_C], _C]: + """Register an implementation for a given :class:`.MigrateOperation`. This is part of the operation extensibility API. @@ -1100,7 +1191,7 @@ def implementation_for(op_cls: Any) -> Callable[..., Any]: """ def inline_literal( - value: Union[str, int], type_: None = None + value: Union[str, int], type_: Optional[TypeEngine[Any]] = None ) -> _literal_bindparam: r"""Produce an 'inline literal' expression, suitable for using in an INSERT, UPDATE, or DELETE statement. @@ -1119,8 +1210,8 @@ def inline_literal( advanced types like dates may not be supported directly by SQLAlchemy. - See :meth:`.execute` for an example usage of - :meth:`.inline_literal`. + See :meth:`.Operations.execute` for an example usage of + :meth:`.Operations.inline_literal`. The environment can also be configured to attempt to render "literal" values inline automatically, for those simple types @@ -1144,16 +1235,37 @@ def inline_literal( """ +@overload +def invoke(operation: CreateTableOp) -> Table: ... +@overload +def invoke( + operation: Union[ + AddConstraintOp, + DropConstraintOp, + CreateIndexOp, + DropIndexOp, + AddColumnOp, + AlterColumnOp, + AlterTableOp, + CreateTableCommentOp, + DropTableCommentOp, + DropColumnOp, + BulkInsertOp, + DropTableOp, + ExecuteSQLOp, + ] +) -> None: ... +@overload def invoke(operation: MigrateOperation) -> Any: - r"""Given a :class:`.MigrateOperation`, invoke it in terms of + """Given a :class:`.MigrateOperation`, invoke it in terms of this :class:`.Operations` instance. """ def register_operation( name: str, sourcename: Optional[str] = None -) -> Callable[..., Any]: - r"""Register a new operation for this class. +) -> Callable[[Type[_T]], Type[_T]]: + """Register a new operation for this class. This method is normally used to add new operations to the :class:`.Operations` class, and possibly the @@ -1170,9 +1282,9 @@ def register_operation( """ def rename_table( - old_table_name: str, new_table_name: str, schema: Optional[str] = None -) -> Optional[Table]: - r"""Emit an ALTER TABLE to rename a table. + old_table_name: str, new_table_name: str, *, schema: Optional[str] = None +) -> None: + """Emit an ALTER TABLE to rename a table. :param old_table_name: old name. :param new_table_name: new name. @@ -1182,3 +1294,28 @@ def rename_table( :class:`~sqlalchemy.sql.elements.quoted_name`. """ + +def run_async( + async_function: Callable[..., Awaitable[_T]], *args: Any, **kw_args: Any +) -> _T: + """Invoke the given asynchronous callable, passing an asynchronous + :class:`~sqlalchemy.ext.asyncio.AsyncConnection` as the first + argument. + + This method allows calling async functions from within the + synchronous ``upgrade()`` or ``downgrade()`` alembic migration + method. + + The async connection passed to the callable shares the same + transaction as the connection running in the migration context. + + Any additional arg or kw_arg passed to this function are passed + to the provided async function. + + .. versionadded: 1.11 + + .. note:: + + This method can be called only when alembic is called using + an async dialect. + """ diff --git a/libs/alembic/operations/__init__.py b/libs/alembic/operations/__init__.py index 9527620de..26197cbe8 100644 --- a/libs/alembic/operations/__init__.py +++ b/libs/alembic/operations/__init__.py @@ -1,7 +1,15 @@ from . import toimpl +from .base import AbstractOperations from .base import BatchOperations from .base import Operations from .ops import MigrateOperation +from .ops import MigrationScript -__all__ = ["Operations", "BatchOperations", "MigrateOperation"] +__all__ = [ + "AbstractOperations", + "Operations", + "BatchOperations", + "MigrateOperation", + "MigrationScript", +] diff --git a/libs/alembic/operations/base.py b/libs/alembic/operations/base.py index 04b66b55d..bafe441a6 100644 --- a/libs/alembic/operations/base.py +++ b/libs/alembic/operations/base.py @@ -1,19 +1,25 @@ +# mypy: allow-untyped-calls + from __future__ import annotations from contextlib import contextmanager import re import textwrap from typing import Any +from typing import Awaitable from typing import Callable from typing import Dict from typing import Iterator from typing import List # noqa from typing import Mapping +from typing import NoReturn from typing import Optional +from typing import overload from typing import Sequence # noqa from typing import Tuple from typing import Type # noqa from typing import TYPE_CHECKING +from typing import TypeVar from typing import Union from sqlalchemy.sql.elements import conv @@ -28,50 +34,50 @@ from ..util.compat import inspect_getfullargspec from ..util.sqla_compat import _literal_bindparam -NoneType = type(None) - if TYPE_CHECKING: from typing import Literal - from sqlalchemy import Table # noqa + from sqlalchemy import Table from sqlalchemy.engine import Connection + from sqlalchemy.sql import Executable + from sqlalchemy.sql.expression import ColumnElement + from sqlalchemy.sql.expression import TableClause + from sqlalchemy.sql.expression import TextClause + from sqlalchemy.sql.functions import Function + from sqlalchemy.sql.schema import Column + from sqlalchemy.sql.schema import Computed + from sqlalchemy.sql.schema import Identity + from sqlalchemy.sql.schema import SchemaItem + from sqlalchemy.types import TypeEngine from .batch import BatchOperationsImpl + from .ops import AddColumnOp + from .ops import AddConstraintOp + from .ops import AlterColumnOp + from .ops import AlterTableOp + from .ops import BulkInsertOp + from .ops import CreateIndexOp + from .ops import CreateTableCommentOp + from .ops import CreateTableOp + from .ops import DropColumnOp + from .ops import DropConstraintOp + from .ops import DropIndexOp + from .ops import DropTableCommentOp + from .ops import DropTableOp + from .ops import ExecuteSQLOp from .ops import MigrateOperation from ..ddl import DefaultImpl from ..runtime.migration import MigrationContext - __all__ = ("Operations", "BatchOperations") +_T = TypeVar("_T") + +_C = TypeVar("_C", bound=Callable[..., Any]) -class Operations(util.ModuleClsProxy): +class AbstractOperations(util.ModuleClsProxy): + """Base class for Operations and BatchOperations. - """Define high level migration operations. - - Each operation corresponds to some schema migration operation, - executed against a particular :class:`.MigrationContext` - which in turn represents connectivity to a database, - or a file output stream. - - While :class:`.Operations` is normally configured as - part of the :meth:`.EnvironmentContext.run_migrations` - method called from an ``env.py`` script, a standalone - :class:`.Operations` instance can be - made for use cases external to regular Alembic - migrations by passing in a :class:`.MigrationContext`:: - - from alembic.migration import MigrationContext - from alembic.operations import Operations - - conn = myengine.connect() - ctx = MigrationContext.configure(conn) - op = Operations(ctx) - - op.alter_column("t", "c", nullable=True) - - Note that as of 0.8, most of the methods on this class are produced - dynamically using the :meth:`.Operations.register_operation` - method. + .. versionadded:: 1.11.0 """ @@ -100,7 +106,7 @@ class Operations(util.ModuleClsProxy): @classmethod def register_operation( cls, name: str, sourcename: Optional[str] = None - ) -> Callable[..., Any]: + ) -> Callable[[Type[_T]], Type[_T]]: """Register a new operation for this class. This method is normally used to add new operations @@ -117,7 +123,7 @@ class Operations(util.ModuleClsProxy): """ - def register(op_cls): + def register(op_cls: Type[_T]) -> Type[_T]: if sourcename is None: fn = getattr(op_cls, name) source_name = fn.__name__ @@ -136,13 +142,21 @@ class Operations(util.ModuleClsProxy): *spec, formatannotation=formatannotation_fwdref ) num_defaults = len(spec[3]) if spec[3] else 0 + + defaulted_vals: Tuple[Any, ...] + if num_defaults: - defaulted_vals = name_args[0 - num_defaults :] + defaulted_vals = tuple(name_args[0 - num_defaults :]) else: defaulted_vals = () + defaulted_vals += tuple(spec[4]) + # here, we are using formatargspec in a different way in order + # to get a string that will re-apply incoming arguments to a new + # function call + apply_kw = inspect_formatargspec( - name_args, + name_args + spec[4], spec[1], spec[2], defaulted_vals, @@ -170,9 +184,10 @@ class Operations(util.ModuleClsProxy): "doc": fn.__doc__, } ) + globals_ = dict(globals()) globals_.update({"op_cls": op_cls}) - lcl = {} + lcl: Dict[str, Any] = {} exec(func_text, globals_, lcl) setattr(cls, name, lcl[name]) @@ -188,7 +203,7 @@ class Operations(util.ModuleClsProxy): return register @classmethod - def implementation_for(cls, op_cls: Any) -> Callable[..., Any]: + def implementation_for(cls, op_cls: Any) -> Callable[[_C], _C]: """Register an implementation for a given :class:`.MigrateOperation`. This is part of the operation extensibility API. @@ -199,7 +214,7 @@ class Operations(util.ModuleClsProxy): """ - def decorate(fn): + def decorate(fn: _C) -> _C: cls._to_impl.dispatch_for(op_cls)(fn) return fn @@ -221,7 +236,7 @@ class Operations(util.ModuleClsProxy): table_name: str, schema: Optional[str] = None, recreate: Literal["auto", "always", "never"] = "auto", - partial_reordering: Optional[tuple] = None, + partial_reordering: Optional[Tuple[Any, ...]] = None, copy_from: Optional[Table] = None, table_args: Tuple[Any, ...] = (), table_kwargs: Mapping[str, Any] = util.immutabledict(), @@ -259,8 +274,8 @@ class Operations(util.ModuleClsProxy): are omitted. E.g.:: with op.batch_alter_table("some_table") as batch_op: - batch_op.add_column(Column('foo', Integer)) - batch_op.drop_column('bar') + batch_op.add_column(Column("foo", Integer)) + batch_op.drop_column("bar") The operations within the context manager are invoked at once when the context is ended. When run against SQLite, if the @@ -339,16 +354,18 @@ class Operations(util.ModuleClsProxy): Specify the order of all columns:: with op.batch_alter_table( - "some_table", recreate="always", - partial_reordering=[("c", "d", "a", "b")] + "some_table", + recreate="always", + partial_reordering=[("c", "d", "a", "b")], ) as batch_op: pass Ensure "d" appears before "c", and "b", appears before "a":: with op.batch_alter_table( - "some_table", recreate="always", - partial_reordering=[("d", "c"), ("b", "a")] + "some_table", + recreate="always", + partial_reordering=[("d", "c"), ("b", "a")], ) as batch_op: pass @@ -356,8 +373,6 @@ class Operations(util.ModuleClsProxy): set is undefined. Therefore it is best to specify the complete ordering of all columns for best results. - .. versionadded:: 1.4.0 - .. note:: batch mode requires SQLAlchemy 0.8 or above. .. seealso:: @@ -390,6 +405,35 @@ class Operations(util.ModuleClsProxy): return self.migration_context + @overload + def invoke(self, operation: CreateTableOp) -> Table: + ... + + @overload + def invoke( + self, + operation: Union[ + AddConstraintOp, + DropConstraintOp, + CreateIndexOp, + DropIndexOp, + AddColumnOp, + AlterColumnOp, + AlterTableOp, + CreateTableCommentOp, + DropTableCommentOp, + DropColumnOp, + BulkInsertOp, + DropTableOp, + ExecuteSQLOp, + ], + ) -> None: + ... + + @overload + def invoke(self, operation: MigrateOperation) -> Any: + ... + def invoke(self, operation: MigrateOperation) -> Any: """Given a :class:`.MigrateOperation`, invoke it in terms of this :class:`.Operations` instance. @@ -414,7 +458,7 @@ class Operations(util.ModuleClsProxy): If the :meth:`.Operations.f` is used on a constraint, the naming convention will not take effect:: - op.add_column('t', 'x', Boolean(name=op.f('ck_bool_t_x'))) + op.add_column("t", "x", Boolean(name=op.f("ck_bool_t_x"))) Above, the CHECK constraint generated will have the name ``ck_bool_t_x`` regardless of whether or not a naming convention is @@ -426,7 +470,7 @@ class Operations(util.ModuleClsProxy): ``{"ck": "ck_bool_%(table_name)s_%(constraint_name)s"}``, then the output of the following: - op.add_column('t', 'x', Boolean(name='x')) + op.add_column("t", "x", Boolean(name="x")) will be:: @@ -439,7 +483,7 @@ class Operations(util.ModuleClsProxy): return conv(name) def inline_literal( - self, value: Union[str, int], type_: None = None + self, value: Union[str, int], type_: Optional[TypeEngine[Any]] = None ) -> _literal_bindparam: r"""Produce an 'inline literal' expression, suitable for using in an INSERT, UPDATE, or DELETE statement. @@ -458,8 +502,8 @@ class Operations(util.ModuleClsProxy): advanced types like dates may not be supported directly by SQLAlchemy. - See :meth:`.execute` for an example usage of - :meth:`.inline_literal`. + See :meth:`.Operations.execute` for an example usage of + :meth:`.Operations.inline_literal`. The environment can also be configured to attempt to render "literal" values inline automatically, for those simple types @@ -496,8 +540,1062 @@ class Operations(util.ModuleClsProxy): """ return self.migration_context.impl.bind # type: ignore[return-value] + def run_async( + self, + async_function: Callable[..., Awaitable[_T]], + *args: Any, + **kw_args: Any, + ) -> _T: + """Invoke the given asynchronous callable, passing an asynchronous + :class:`~sqlalchemy.ext.asyncio.AsyncConnection` as the first + argument. -class BatchOperations(Operations): + This method allows calling async functions from within the + synchronous ``upgrade()`` or ``downgrade()`` alembic migration + method. + + The async connection passed to the callable shares the same + transaction as the connection running in the migration context. + + Any additional arg or kw_arg passed to this function are passed + to the provided async function. + + .. versionadded: 1.11 + + .. note:: + + This method can be called only when alembic is called using + an async dialect. + """ + if not sqla_compat.sqla_14_18: + raise NotImplementedError("SQLAlchemy 1.4.18+ required") + sync_conn = self.get_bind() + if sync_conn is None: + raise NotImplementedError("Cannot call run_async in SQL mode") + if not sync_conn.dialect.is_async: + raise ValueError("Cannot call run_async with a sync engine") + from sqlalchemy.ext.asyncio import AsyncConnection + from sqlalchemy.util import await_only + + async_conn = AsyncConnection._retrieve_proxy_for_target(sync_conn) + return await_only(async_function(async_conn, *args, **kw_args)) + + +class Operations(AbstractOperations): + """Define high level migration operations. + + Each operation corresponds to some schema migration operation, + executed against a particular :class:`.MigrationContext` + which in turn represents connectivity to a database, + or a file output stream. + + While :class:`.Operations` is normally configured as + part of the :meth:`.EnvironmentContext.run_migrations` + method called from an ``env.py`` script, a standalone + :class:`.Operations` instance can be + made for use cases external to regular Alembic + migrations by passing in a :class:`.MigrationContext`:: + + from alembic.migration import MigrationContext + from alembic.operations import Operations + + conn = myengine.connect() + ctx = MigrationContext.configure(conn) + op = Operations(ctx) + + op.alter_column("t", "c", nullable=True) + + Note that as of 0.8, most of the methods on this class are produced + dynamically using the :meth:`.Operations.register_operation` + method. + + """ + + if TYPE_CHECKING: + # START STUB FUNCTIONS: op_cls + # ### the following stubs are generated by tools/write_pyi.py ### + # ### do not edit ### + + def add_column( + self, + table_name: str, + column: Column[Any], + *, + schema: Optional[str] = None, + ) -> None: + """Issue an "add column" instruction using the current + migration context. + + e.g.:: + + from alembic import op + from sqlalchemy import Column, String + + op.add_column("organization", Column("name", String())) + + The :meth:`.Operations.add_column` method typically corresponds + to the SQL command "ALTER TABLE... ADD COLUMN". Within the scope + of this command, the column's name, datatype, nullability, + and optional server-generated defaults may be indicated. + + .. note:: + + With the exception of NOT NULL constraints or single-column FOREIGN + KEY constraints, other kinds of constraints such as PRIMARY KEY, + UNIQUE or CHECK constraints **cannot** be generated using this + method; for these constraints, refer to operations such as + :meth:`.Operations.create_primary_key` and + :meth:`.Operations.create_check_constraint`. In particular, the + following :class:`~sqlalchemy.schema.Column` parameters are + **ignored**: + + * :paramref:`~sqlalchemy.schema.Column.primary_key` - SQL databases + typically do not support an ALTER operation that can add + individual columns one at a time to an existing primary key + constraint, therefore it's less ambiguous to use the + :meth:`.Operations.create_primary_key` method, which assumes no + existing primary key constraint is present. + * :paramref:`~sqlalchemy.schema.Column.unique` - use the + :meth:`.Operations.create_unique_constraint` method + * :paramref:`~sqlalchemy.schema.Column.index` - use the + :meth:`.Operations.create_index` method + + + The provided :class:`~sqlalchemy.schema.Column` object may include a + :class:`~sqlalchemy.schema.ForeignKey` constraint directive, + referencing a remote table name. For this specific type of constraint, + Alembic will automatically emit a second ALTER statement in order to + add the single-column FOREIGN KEY constraint separately:: + + from alembic import op + from sqlalchemy import Column, INTEGER, ForeignKey + + op.add_column( + "organization", + Column("account_id", INTEGER, ForeignKey("accounts.id")), + ) + + The column argument passed to :meth:`.Operations.add_column` is a + :class:`~sqlalchemy.schema.Column` construct, used in the same way it's + used in SQLAlchemy. In particular, values or functions to be indicated + as producing the column's default value on the database side are + specified using the ``server_default`` parameter, and not ``default`` + which only specifies Python-side defaults:: + + from alembic import op + from sqlalchemy import Column, TIMESTAMP, func + + # specify "DEFAULT NOW" along with the column add + op.add_column( + "account", + Column("timestamp", TIMESTAMP, server_default=func.now()), + ) + + :param table_name: String name of the parent table. + :param column: a :class:`sqlalchemy.schema.Column` object + representing the new column. + :param schema: Optional schema name to operate within. To control + quoting of the schema outside of the default behavior, use + the SQLAlchemy construct + :class:`~sqlalchemy.sql.elements.quoted_name`. + + """ # noqa: E501 + ... + + def alter_column( + self, + table_name: str, + column_name: str, + *, + nullable: Optional[bool] = None, + comment: Union[str, Literal[False], None] = False, + server_default: Any = False, + new_column_name: Optional[str] = None, + type_: Union[TypeEngine[Any], Type[TypeEngine[Any]], None] = None, + existing_type: Union[ + TypeEngine[Any], Type[TypeEngine[Any]], None + ] = None, + existing_server_default: Union[ + str, bool, Identity, Computed, None + ] = False, + existing_nullable: Optional[bool] = None, + existing_comment: Optional[str] = None, + schema: Optional[str] = None, + **kw: Any, + ) -> None: + r"""Issue an "alter column" instruction using the + current migration context. + + Generally, only that aspect of the column which + is being changed, i.e. name, type, nullability, + default, needs to be specified. Multiple changes + can also be specified at once and the backend should + "do the right thing", emitting each change either + separately or together as the backend allows. + + MySQL has special requirements here, since MySQL + cannot ALTER a column without a full specification. + When producing MySQL-compatible migration files, + it is recommended that the ``existing_type``, + ``existing_server_default``, and ``existing_nullable`` + parameters be present, if not being altered. + + Type changes which are against the SQLAlchemy + "schema" types :class:`~sqlalchemy.types.Boolean` + and :class:`~sqlalchemy.types.Enum` may also + add or drop constraints which accompany those + types on backends that don't support them natively. + The ``existing_type`` argument is + used in this case to identify and remove a previous + constraint that was bound to the type object. + + :param table_name: string name of the target table. + :param column_name: string name of the target column, + as it exists before the operation begins. + :param nullable: Optional; specify ``True`` or ``False`` + to alter the column's nullability. + :param server_default: Optional; specify a string + SQL expression, :func:`~sqlalchemy.sql.expression.text`, + or :class:`~sqlalchemy.schema.DefaultClause` to indicate + an alteration to the column's default value. + Set to ``None`` to have the default removed. + :param comment: optional string text of a new comment to add to the + column. + :param new_column_name: Optional; specify a string name here to + indicate the new name within a column rename operation. + :param type\_: Optional; a :class:`~sqlalchemy.types.TypeEngine` + type object to specify a change to the column's type. + For SQLAlchemy types that also indicate a constraint (i.e. + :class:`~sqlalchemy.types.Boolean`, :class:`~sqlalchemy.types.Enum`), + the constraint is also generated. + :param autoincrement: set the ``AUTO_INCREMENT`` flag of the column; + currently understood by the MySQL dialect. + :param existing_type: Optional; a + :class:`~sqlalchemy.types.TypeEngine` + type object to specify the previous type. This + is required for all MySQL column alter operations that + don't otherwise specify a new type, as well as for + when nullability is being changed on a SQL Server + column. It is also used if the type is a so-called + SQLAlchemy "schema" type which may define a constraint (i.e. + :class:`~sqlalchemy.types.Boolean`, + :class:`~sqlalchemy.types.Enum`), + so that the constraint can be dropped. + :param existing_server_default: Optional; The existing + default value of the column. Required on MySQL if + an existing default is not being changed; else MySQL + removes the default. + :param existing_nullable: Optional; the existing nullability + of the column. Required on MySQL if the existing nullability + is not being changed; else MySQL sets this to NULL. + :param existing_autoincrement: Optional; the existing autoincrement + of the column. Used for MySQL's system of altering a column + that specifies ``AUTO_INCREMENT``. + :param existing_comment: string text of the existing comment on the + column to be maintained. Required on MySQL if the existing comment + on the column is not being changed. + :param schema: Optional schema name to operate within. To control + quoting of the schema outside of the default behavior, use + the SQLAlchemy construct + :class:`~sqlalchemy.sql.elements.quoted_name`. + :param postgresql_using: String argument which will indicate a + SQL expression to render within the Postgresql-specific USING clause + within ALTER COLUMN. This string is taken directly as raw SQL which + must explicitly include any necessary quoting or escaping of tokens + within the expression. + + """ # noqa: E501 + ... + + def bulk_insert( + self, + table: Union[Table, TableClause], + rows: List[Dict[str, Any]], + *, + multiinsert: bool = True, + ) -> None: + """Issue a "bulk insert" operation using the current + migration context. + + This provides a means of representing an INSERT of multiple rows + which works equally well in the context of executing on a live + connection as well as that of generating a SQL script. In the + case of a SQL script, the values are rendered inline into the + statement. + + e.g.:: + + from alembic import op + from datetime import date + from sqlalchemy.sql import table, column + from sqlalchemy import String, Integer, Date + + # Create an ad-hoc table to use for the insert statement. + accounts_table = table( + "account", + column("id", Integer), + column("name", String), + column("create_date", Date), + ) + + op.bulk_insert( + accounts_table, + [ + { + "id": 1, + "name": "John Smith", + "create_date": date(2010, 10, 5), + }, + { + "id": 2, + "name": "Ed Williams", + "create_date": date(2007, 5, 27), + }, + { + "id": 3, + "name": "Wendy Jones", + "create_date": date(2008, 8, 15), + }, + ], + ) + + When using --sql mode, some datatypes may not render inline + automatically, such as dates and other special types. When this + issue is present, :meth:`.Operations.inline_literal` may be used:: + + op.bulk_insert( + accounts_table, + [ + { + "id": 1, + "name": "John Smith", + "create_date": op.inline_literal("2010-10-05"), + }, + { + "id": 2, + "name": "Ed Williams", + "create_date": op.inline_literal("2007-05-27"), + }, + { + "id": 3, + "name": "Wendy Jones", + "create_date": op.inline_literal("2008-08-15"), + }, + ], + multiinsert=False, + ) + + When using :meth:`.Operations.inline_literal` in conjunction with + :meth:`.Operations.bulk_insert`, in order for the statement to work + in "online" (e.g. non --sql) mode, the + :paramref:`~.Operations.bulk_insert.multiinsert` + flag should be set to ``False``, which will have the effect of + individual INSERT statements being emitted to the database, each + with a distinct VALUES clause, so that the "inline" values can + still be rendered, rather than attempting to pass the values + as bound parameters. + + :param table: a table object which represents the target of the INSERT. + + :param rows: a list of dictionaries indicating rows. + + :param multiinsert: when at its default of True and --sql mode is not + enabled, the INSERT statement will be executed using + "executemany()" style, where all elements in the list of + dictionaries are passed as bound parameters in a single + list. Setting this to False results in individual INSERT + statements being emitted per parameter set, and is needed + in those cases where non-literal values are present in the + parameter sets. + + """ # noqa: E501 + ... + + def create_check_constraint( + self, + constraint_name: Optional[str], + table_name: str, + condition: Union[str, ColumnElement[bool], TextClause], + *, + schema: Optional[str] = None, + **kw: Any, + ) -> None: + """Issue a "create check constraint" instruction using the + current migration context. + + e.g.:: + + from alembic import op + from sqlalchemy.sql import column, func + + op.create_check_constraint( + "ck_user_name_len", + "user", + func.len(column("name")) > 5, + ) + + CHECK constraints are usually against a SQL expression, so ad-hoc + table metadata is usually needed. The function will convert the given + arguments into a :class:`sqlalchemy.schema.CheckConstraint` bound + to an anonymous table in order to emit the CREATE statement. + + :param name: Name of the check constraint. The name is necessary + so that an ALTER statement can be emitted. For setups that + use an automated naming scheme such as that described at + :ref:`sqla:constraint_naming_conventions`, + ``name`` here can be ``None``, as the event listener will + apply the name to the constraint object when it is associated + with the table. + :param table_name: String name of the source table. + :param condition: SQL expression that's the condition of the + constraint. Can be a string or SQLAlchemy expression language + structure. + :param deferrable: optional bool. If set, emit DEFERRABLE or + NOT DEFERRABLE when issuing DDL for this constraint. + :param initially: optional string. If set, emit INITIALLY + when issuing DDL for this constraint. + :param schema: Optional schema name to operate within. To control + quoting of the schema outside of the default behavior, use + the SQLAlchemy construct + :class:`~sqlalchemy.sql.elements.quoted_name`. + + """ # noqa: E501 + ... + + def create_exclude_constraint( + self, + constraint_name: str, + table_name: str, + *elements: Any, + **kw: Any, + ) -> Optional[Table]: + """Issue an alter to create an EXCLUDE constraint using the + current migration context. + + .. note:: This method is Postgresql specific, and additionally + requires at least SQLAlchemy 1.0. + + e.g.:: + + from alembic import op + + op.create_exclude_constraint( + "user_excl", + "user", + ("period", "&&"), + ("group", "="), + where=("group != 'some group'"), + ) + + Note that the expressions work the same way as that of + the ``ExcludeConstraint`` object itself; if plain strings are + passed, quoting rules must be applied manually. + + :param name: Name of the constraint. + :param table_name: String name of the source table. + :param elements: exclude conditions. + :param where: SQL expression or SQL string with optional WHERE + clause. + :param deferrable: optional bool. If set, emit DEFERRABLE or + NOT DEFERRABLE when issuing DDL for this constraint. + :param initially: optional string. If set, emit INITIALLY + when issuing DDL for this constraint. + :param schema: Optional schema name to operate within. + + """ # noqa: E501 + ... + + def create_foreign_key( + self, + constraint_name: Optional[str], + source_table: str, + referent_table: str, + local_cols: List[str], + remote_cols: List[str], + *, + onupdate: Optional[str] = None, + ondelete: Optional[str] = None, + deferrable: Optional[bool] = None, + initially: Optional[str] = None, + match: Optional[str] = None, + source_schema: Optional[str] = None, + referent_schema: Optional[str] = None, + **dialect_kw: Any, + ) -> None: + """Issue a "create foreign key" instruction using the + current migration context. + + e.g.:: + + from alembic import op + + op.create_foreign_key( + "fk_user_address", + "address", + "user", + ["user_id"], + ["id"], + ) + + This internally generates a :class:`~sqlalchemy.schema.Table` object + containing the necessary columns, then generates a new + :class:`~sqlalchemy.schema.ForeignKeyConstraint` + object which it then associates with the + :class:`~sqlalchemy.schema.Table`. + Any event listeners associated with this action will be fired + off normally. The :class:`~sqlalchemy.schema.AddConstraint` + construct is ultimately used to generate the ALTER statement. + + :param constraint_name: Name of the foreign key constraint. The name + is necessary so that an ALTER statement can be emitted. For setups + that use an automated naming scheme such as that described at + :ref:`sqla:constraint_naming_conventions`, + ``name`` here can be ``None``, as the event listener will + apply the name to the constraint object when it is associated + with the table. + :param source_table: String name of the source table. + :param referent_table: String name of the destination table. + :param local_cols: a list of string column names in the + source table. + :param remote_cols: a list of string column names in the + remote table. + :param onupdate: Optional string. If set, emit ON UPDATE when + issuing DDL for this constraint. Typical values include CASCADE, + DELETE and RESTRICT. + :param ondelete: Optional string. If set, emit ON DELETE when + issuing DDL for this constraint. Typical values include CASCADE, + DELETE and RESTRICT. + :param deferrable: optional bool. If set, emit DEFERRABLE or NOT + DEFERRABLE when issuing DDL for this constraint. + :param source_schema: Optional schema name of the source table. + :param referent_schema: Optional schema name of the destination table. + + """ # noqa: E501 + ... + + def create_index( + self, + index_name: Optional[str], + table_name: str, + columns: Sequence[Union[str, TextClause, Function[Any]]], + *, + schema: Optional[str] = None, + unique: bool = False, + if_not_exists: Optional[bool] = None, + **kw: Any, + ) -> None: + r"""Issue a "create index" instruction using the current + migration context. + + e.g.:: + + from alembic import op + + op.create_index("ik_test", "t1", ["foo", "bar"]) + + Functional indexes can be produced by using the + :func:`sqlalchemy.sql.expression.text` construct:: + + from alembic import op + from sqlalchemy import text + + op.create_index("ik_test", "t1", [text("lower(foo)")]) + + :param index_name: name of the index. + :param table_name: name of the owning table. + :param columns: a list consisting of string column names and/or + :func:`~sqlalchemy.sql.expression.text` constructs. + :param schema: Optional schema name to operate within. To control + quoting of the schema outside of the default behavior, use + the SQLAlchemy construct + :class:`~sqlalchemy.sql.elements.quoted_name`. + :param unique: If True, create a unique index. + + :param quote: Force quoting of this column's name on or off, + corresponding to ``True`` or ``False``. When left at its default + of ``None``, the column identifier will be quoted according to + whether the name is case sensitive (identifiers with at least one + upper case character are treated as case sensitive), or if it's a + reserved word. This flag is only needed to force quoting of a + reserved word which is not known by the SQLAlchemy dialect. + + :param if_not_exists: If True, adds IF NOT EXISTS operator when + creating the new index. + + .. versionadded:: 1.12.0 + + :param \**kw: Additional keyword arguments not mentioned above are + dialect specific, and passed in the form + ``_``. + See the documentation regarding an individual dialect at + :ref:`dialect_toplevel` for detail on documented arguments. + + """ # noqa: E501 + ... + + def create_primary_key( + self, + constraint_name: Optional[str], + table_name: str, + columns: List[str], + *, + schema: Optional[str] = None, + ) -> None: + """Issue a "create primary key" instruction using the current + migration context. + + e.g.:: + + from alembic import op + + op.create_primary_key("pk_my_table", "my_table", ["id", "version"]) + + This internally generates a :class:`~sqlalchemy.schema.Table` object + containing the necessary columns, then generates a new + :class:`~sqlalchemy.schema.PrimaryKeyConstraint` + object which it then associates with the + :class:`~sqlalchemy.schema.Table`. + Any event listeners associated with this action will be fired + off normally. The :class:`~sqlalchemy.schema.AddConstraint` + construct is ultimately used to generate the ALTER statement. + + :param constraint_name: Name of the primary key constraint. The name + is necessary so that an ALTER statement can be emitted. For setups + that use an automated naming scheme such as that described at + :ref:`sqla:constraint_naming_conventions` + ``name`` here can be ``None``, as the event listener will + apply the name to the constraint object when it is associated + with the table. + :param table_name: String name of the target table. + :param columns: a list of string column names to be applied to the + primary key constraint. + :param schema: Optional schema name to operate within. To control + quoting of the schema outside of the default behavior, use + the SQLAlchemy construct + :class:`~sqlalchemy.sql.elements.quoted_name`. + + """ # noqa: E501 + ... + + def create_table( + self, table_name: str, *columns: SchemaItem, **kw: Any + ) -> Table: + r"""Issue a "create table" instruction using the current migration + context. + + This directive receives an argument list similar to that of the + traditional :class:`sqlalchemy.schema.Table` construct, but without the + metadata:: + + from sqlalchemy import INTEGER, VARCHAR, NVARCHAR, Column + from alembic import op + + op.create_table( + "account", + Column("id", INTEGER, primary_key=True), + Column("name", VARCHAR(50), nullable=False), + Column("description", NVARCHAR(200)), + Column("timestamp", TIMESTAMP, server_default=func.now()), + ) + + Note that :meth:`.create_table` accepts + :class:`~sqlalchemy.schema.Column` + constructs directly from the SQLAlchemy library. In particular, + default values to be created on the database side are + specified using the ``server_default`` parameter, and not + ``default`` which only specifies Python-side defaults:: + + from alembic import op + from sqlalchemy import Column, TIMESTAMP, func + + # specify "DEFAULT NOW" along with the "timestamp" column + op.create_table( + "account", + Column("id", INTEGER, primary_key=True), + Column("timestamp", TIMESTAMP, server_default=func.now()), + ) + + The function also returns a newly created + :class:`~sqlalchemy.schema.Table` object, corresponding to the table + specification given, which is suitable for + immediate SQL operations, in particular + :meth:`.Operations.bulk_insert`:: + + from sqlalchemy import INTEGER, VARCHAR, NVARCHAR, Column + from alembic import op + + account_table = op.create_table( + "account", + Column("id", INTEGER, primary_key=True), + Column("name", VARCHAR(50), nullable=False), + Column("description", NVARCHAR(200)), + Column("timestamp", TIMESTAMP, server_default=func.now()), + ) + + op.bulk_insert( + account_table, + [ + {"name": "A1", "description": "account 1"}, + {"name": "A2", "description": "account 2"}, + ], + ) + + :param table_name: Name of the table + :param \*columns: collection of :class:`~sqlalchemy.schema.Column` + objects within + the table, as well as optional :class:`~sqlalchemy.schema.Constraint` + objects + and :class:`~.sqlalchemy.schema.Index` objects. + :param schema: Optional schema name to operate within. To control + quoting of the schema outside of the default behavior, use + the SQLAlchemy construct + :class:`~sqlalchemy.sql.elements.quoted_name`. + :param \**kw: Other keyword arguments are passed to the underlying + :class:`sqlalchemy.schema.Table` object created for the command. + + :return: the :class:`~sqlalchemy.schema.Table` object corresponding + to the parameters given. + + """ # noqa: E501 + ... + + def create_table_comment( + self, + table_name: str, + comment: Optional[str], + *, + existing_comment: Optional[str] = None, + schema: Optional[str] = None, + ) -> None: + """Emit a COMMENT ON operation to set the comment for a table. + + :param table_name: string name of the target table. + :param comment: string value of the comment being registered against + the specified table. + :param existing_comment: String value of a comment + already registered on the specified table, used within autogenerate + so that the operation is reversible, but not required for direct + use. + + .. seealso:: + + :meth:`.Operations.drop_table_comment` + + :paramref:`.Operations.alter_column.comment` + + """ # noqa: E501 + ... + + def create_unique_constraint( + self, + constraint_name: Optional[str], + table_name: str, + columns: Sequence[str], + *, + schema: Optional[str] = None, + **kw: Any, + ) -> Any: + """Issue a "create unique constraint" instruction using the + current migration context. + + e.g.:: + + from alembic import op + op.create_unique_constraint("uq_user_name", "user", ["name"]) + + This internally generates a :class:`~sqlalchemy.schema.Table` object + containing the necessary columns, then generates a new + :class:`~sqlalchemy.schema.UniqueConstraint` + object which it then associates with the + :class:`~sqlalchemy.schema.Table`. + Any event listeners associated with this action will be fired + off normally. The :class:`~sqlalchemy.schema.AddConstraint` + construct is ultimately used to generate the ALTER statement. + + :param name: Name of the unique constraint. The name is necessary + so that an ALTER statement can be emitted. For setups that + use an automated naming scheme such as that described at + :ref:`sqla:constraint_naming_conventions`, + ``name`` here can be ``None``, as the event listener will + apply the name to the constraint object when it is associated + with the table. + :param table_name: String name of the source table. + :param columns: a list of string column names in the + source table. + :param deferrable: optional bool. If set, emit DEFERRABLE or + NOT DEFERRABLE when issuing DDL for this constraint. + :param initially: optional string. If set, emit INITIALLY + when issuing DDL for this constraint. + :param schema: Optional schema name to operate within. To control + quoting of the schema outside of the default behavior, use + the SQLAlchemy construct + :class:`~sqlalchemy.sql.elements.quoted_name`. + + """ # noqa: E501 + ... + + def drop_column( + self, + table_name: str, + column_name: str, + *, + schema: Optional[str] = None, + **kw: Any, + ) -> None: + """Issue a "drop column" instruction using the current + migration context. + + e.g.:: + + drop_column("organization", "account_id") + + :param table_name: name of table + :param column_name: name of column + :param schema: Optional schema name to operate within. To control + quoting of the schema outside of the default behavior, use + the SQLAlchemy construct + :class:`~sqlalchemy.sql.elements.quoted_name`. + :param mssql_drop_check: Optional boolean. When ``True``, on + Microsoft SQL Server only, first + drop the CHECK constraint on the column using a + SQL-script-compatible + block that selects into a @variable from sys.check_constraints, + then exec's a separate DROP CONSTRAINT for that constraint. + :param mssql_drop_default: Optional boolean. When ``True``, on + Microsoft SQL Server only, first + drop the DEFAULT constraint on the column using a + SQL-script-compatible + block that selects into a @variable from sys.default_constraints, + then exec's a separate DROP CONSTRAINT for that default. + :param mssql_drop_foreign_key: Optional boolean. When ``True``, on + Microsoft SQL Server only, first + drop a single FOREIGN KEY constraint on the column using a + SQL-script-compatible + block that selects into a @variable from + sys.foreign_keys/sys.foreign_key_columns, + then exec's a separate DROP CONSTRAINT for that default. Only + works if the column has exactly one FK constraint which refers to + it, at the moment. + + """ # noqa: E501 + ... + + def drop_constraint( + self, + constraint_name: str, + table_name: str, + type_: Optional[str] = None, + *, + schema: Optional[str] = None, + ) -> None: + r"""Drop a constraint of the given name, typically via DROP CONSTRAINT. + + :param constraint_name: name of the constraint. + :param table_name: table name. + :param type\_: optional, required on MySQL. can be + 'foreignkey', 'primary', 'unique', or 'check'. + :param schema: Optional schema name to operate within. To control + quoting of the schema outside of the default behavior, use + the SQLAlchemy construct + :class:`~sqlalchemy.sql.elements.quoted_name`. + + """ # noqa: E501 + ... + + def drop_index( + self, + index_name: str, + table_name: Optional[str] = None, + *, + schema: Optional[str] = None, + if_exists: Optional[bool] = None, + **kw: Any, + ) -> None: + r"""Issue a "drop index" instruction using the current + migration context. + + e.g.:: + + drop_index("accounts") + + :param index_name: name of the index. + :param table_name: name of the owning table. Some + backends such as Microsoft SQL Server require this. + :param schema: Optional schema name to operate within. To control + quoting of the schema outside of the default behavior, use + the SQLAlchemy construct + :class:`~sqlalchemy.sql.elements.quoted_name`. + + :param if_exists: If True, adds IF EXISTS operator when + dropping the index. + + .. versionadded:: 1.12.0 + + :param \**kw: Additional keyword arguments not mentioned above are + dialect specific, and passed in the form + ``_``. + See the documentation regarding an individual dialect at + :ref:`dialect_toplevel` for detail on documented arguments. + + """ # noqa: E501 + ... + + def drop_table( + self, table_name: str, *, schema: Optional[str] = None, **kw: Any + ) -> None: + r"""Issue a "drop table" instruction using the current + migration context. + + + e.g.:: + + drop_table("accounts") + + :param table_name: Name of the table + :param schema: Optional schema name to operate within. To control + quoting of the schema outside of the default behavior, use + the SQLAlchemy construct + :class:`~sqlalchemy.sql.elements.quoted_name`. + :param \**kw: Other keyword arguments are passed to the underlying + :class:`sqlalchemy.schema.Table` object created for the command. + + """ # noqa: E501 + ... + + def drop_table_comment( + self, + table_name: str, + *, + existing_comment: Optional[str] = None, + schema: Optional[str] = None, + ) -> None: + """Issue a "drop table comment" operation to + remove an existing comment set on a table. + + :param table_name: string name of the target table. + :param existing_comment: An optional string value of a comment already + registered on the specified table. + + .. seealso:: + + :meth:`.Operations.create_table_comment` + + :paramref:`.Operations.alter_column.comment` + + """ # noqa: E501 + ... + + def execute( + self, + sqltext: Union[Executable, str], + *, + execution_options: Optional[dict[str, Any]] = None, + ) -> None: + r"""Execute the given SQL using the current migration context. + + The given SQL can be a plain string, e.g.:: + + op.execute("INSERT INTO table (foo) VALUES ('some value')") + + Or it can be any kind of Core SQL Expression construct, such as + below where we use an update construct:: + + from sqlalchemy.sql import table, column + from sqlalchemy import String + from alembic import op + + account = table("account", column("name", String)) + op.execute( + account.update() + .where(account.c.name == op.inline_literal("account 1")) + .values({"name": op.inline_literal("account 2")}) + ) + + Above, we made use of the SQLAlchemy + :func:`sqlalchemy.sql.expression.table` and + :func:`sqlalchemy.sql.expression.column` constructs to make a brief, + ad-hoc table construct just for our UPDATE statement. A full + :class:`~sqlalchemy.schema.Table` construct of course works perfectly + fine as well, though note it's a recommended practice to at least + ensure the definition of a table is self-contained within the migration + script, rather than imported from a module that may break compatibility + with older migrations. + + In a SQL script context, the statement is emitted directly to the + output stream. There is *no* return result, however, as this + function is oriented towards generating a change script + that can run in "offline" mode. Additionally, parameterized + statements are discouraged here, as they *will not work* in offline + mode. Above, we use :meth:`.inline_literal` where parameters are + to be used. + + For full interaction with a connected database where parameters can + also be used normally, use the "bind" available from the context:: + + from alembic import op + + connection = op.get_bind() + + connection.execute( + account.update() + .where(account.c.name == "account 1") + .values({"name": "account 2"}) + ) + + Additionally, when passing the statement as a plain string, it is first + coerced into a :func:`sqlalchemy.sql.expression.text` construct + before being passed along. In the less likely case that the + literal SQL string contains a colon, it must be escaped with a + backslash, as:: + + op.execute(r"INSERT INTO table (foo) VALUES ('\:colon_value')") + + + :param sqltext: Any legal SQLAlchemy expression, including: + + * a string + * a :func:`sqlalchemy.sql.expression.text` construct. + * a :func:`sqlalchemy.sql.expression.insert` construct. + * a :func:`sqlalchemy.sql.expression.update` construct. + * a :func:`sqlalchemy.sql.expression.delete` construct. + * Any "executable" described in SQLAlchemy Core documentation, + noting that no result set is returned. + + .. note:: when passing a plain string, the statement is coerced into + a :func:`sqlalchemy.sql.expression.text` construct. This construct + considers symbols with colons, e.g. ``:foo`` to be bound parameters. + To avoid this, ensure that colon symbols are escaped, e.g. + ``\:foo``. + + :param execution_options: Optional dictionary of + execution options, will be passed to + :meth:`sqlalchemy.engine.Connection.execution_options`. + """ # noqa: E501 + ... + + def rename_table( + self, + old_table_name: str, + new_table_name: str, + *, + schema: Optional[str] = None, + ) -> None: + """Emit an ALTER TABLE to rename a table. + + :param old_table_name: old name. + :param new_table_name: new name. + :param schema: Optional schema name to operate within. To control + quoting of the schema outside of the default behavior, use + the SQLAlchemy construct + :class:`~sqlalchemy.sql.elements.quoted_name`. + + """ # noqa: E501 + ... + + # END STUB FUNCTIONS: op_cls + + +class BatchOperations(AbstractOperations): """Modifies the interface :class:`.Operations` for batch mode. This basically omits the ``table_name`` and ``schema`` parameters @@ -516,8 +1614,280 @@ class BatchOperations(Operations): impl: BatchOperationsImpl - def _noop(self, operation): + def _noop(self, operation: Any) -> NoReturn: raise NotImplementedError( "The %s method does not apply to a batch table alter operation." % operation ) + + if TYPE_CHECKING: + # START STUB FUNCTIONS: batch_op + # ### the following stubs are generated by tools/write_pyi.py ### + # ### do not edit ### + + def add_column( + self, + column: Column[Any], + *, + insert_before: Optional[str] = None, + insert_after: Optional[str] = None, + ) -> None: + """Issue an "add column" instruction using the current + batch migration context. + + .. seealso:: + + :meth:`.Operations.add_column` + + """ # noqa: E501 + ... + + def alter_column( + self, + column_name: str, + *, + nullable: Optional[bool] = None, + comment: Union[str, Literal[False], None] = False, + server_default: Any = False, + new_column_name: Optional[str] = None, + type_: Union[TypeEngine[Any], Type[TypeEngine[Any]], None] = None, + existing_type: Union[ + TypeEngine[Any], Type[TypeEngine[Any]], None + ] = None, + existing_server_default: Union[ + str, bool, Identity, Computed, None + ] = False, + existing_nullable: Optional[bool] = None, + existing_comment: Optional[str] = None, + insert_before: Optional[str] = None, + insert_after: Optional[str] = None, + **kw: Any, + ) -> None: + """Issue an "alter column" instruction using the current + batch migration context. + + Parameters are the same as that of :meth:`.Operations.alter_column`, + as well as the following option(s): + + :param insert_before: String name of an existing column which this + column should be placed before, when creating the new table. + + :param insert_after: String name of an existing column which this + column should be placed after, when creating the new table. If + both :paramref:`.BatchOperations.alter_column.insert_before` + and :paramref:`.BatchOperations.alter_column.insert_after` are + omitted, the column is inserted after the last existing column + in the table. + + .. seealso:: + + :meth:`.Operations.alter_column` + + + """ # noqa: E501 + ... + + def create_check_constraint( + self, + constraint_name: str, + condition: Union[str, ColumnElement[bool], TextClause], + **kw: Any, + ) -> None: + """Issue a "create check constraint" instruction using the + current batch migration context. + + The batch form of this call omits the ``source`` and ``schema`` + arguments from the call. + + .. seealso:: + + :meth:`.Operations.create_check_constraint` + + """ # noqa: E501 + ... + + def create_exclude_constraint( + self, constraint_name: str, *elements: Any, **kw: Any + ) -> Optional[Table]: + """Issue a "create exclude constraint" instruction using the + current batch migration context. + + .. note:: This method is Postgresql specific, and additionally + requires at least SQLAlchemy 1.0. + + .. seealso:: + + :meth:`.Operations.create_exclude_constraint` + + """ # noqa: E501 + ... + + def create_foreign_key( + self, + constraint_name: str, + referent_table: str, + local_cols: List[str], + remote_cols: List[str], + *, + referent_schema: Optional[str] = None, + onupdate: Optional[str] = None, + ondelete: Optional[str] = None, + deferrable: Optional[bool] = None, + initially: Optional[str] = None, + match: Optional[str] = None, + **dialect_kw: Any, + ) -> None: + """Issue a "create foreign key" instruction using the + current batch migration context. + + The batch form of this call omits the ``source`` and ``source_schema`` + arguments from the call. + + e.g.:: + + with batch_alter_table("address") as batch_op: + batch_op.create_foreign_key( + "fk_user_address", + "user", + ["user_id"], + ["id"], + ) + + .. seealso:: + + :meth:`.Operations.create_foreign_key` + + """ # noqa: E501 + ... + + def create_index( + self, index_name: str, columns: List[str], **kw: Any + ) -> None: + """Issue a "create index" instruction using the + current batch migration context. + + .. seealso:: + + :meth:`.Operations.create_index` + + """ # noqa: E501 + ... + + def create_primary_key( + self, constraint_name: str, columns: List[str] + ) -> None: + """Issue a "create primary key" instruction using the + current batch migration context. + + The batch form of this call omits the ``table_name`` and ``schema`` + arguments from the call. + + .. seealso:: + + :meth:`.Operations.create_primary_key` + + """ # noqa: E501 + ... + + def create_table_comment( + self, + comment: Optional[str], + *, + existing_comment: Optional[str] = None, + ) -> None: + """Emit a COMMENT ON operation to set the comment for a table + using the current batch migration context. + + :param comment: string value of the comment being registered against + the specified table. + :param existing_comment: String value of a comment + already registered on the specified table, used within autogenerate + so that the operation is reversible, but not required for direct + use. + + """ # noqa: E501 + ... + + def create_unique_constraint( + self, constraint_name: str, columns: Sequence[str], **kw: Any + ) -> Any: + """Issue a "create unique constraint" instruction using the + current batch migration context. + + The batch form of this call omits the ``source`` and ``schema`` + arguments from the call. + + .. seealso:: + + :meth:`.Operations.create_unique_constraint` + + """ # noqa: E501 + ... + + def drop_column(self, column_name: str, **kw: Any) -> None: + """Issue a "drop column" instruction using the current + batch migration context. + + .. seealso:: + + :meth:`.Operations.drop_column` + + """ # noqa: E501 + ... + + def drop_constraint( + self, constraint_name: str, type_: Optional[str] = None + ) -> None: + """Issue a "drop constraint" instruction using the + current batch migration context. + + The batch form of this call omits the ``table_name`` and ``schema`` + arguments from the call. + + .. seealso:: + + :meth:`.Operations.drop_constraint` + + """ # noqa: E501 + ... + + def drop_index(self, index_name: str, **kw: Any) -> None: + """Issue a "drop index" instruction using the + current batch migration context. + + .. seealso:: + + :meth:`.Operations.drop_index` + + """ # noqa: E501 + ... + + def drop_table_comment( + self, *, existing_comment: Optional[str] = None + ) -> None: + """Issue a "drop table comment" operation to + remove an existing comment set on a table using the current + batch operations context. + + :param existing_comment: An optional string value of a comment already + registered on the specified table. + + """ # noqa: E501 + ... + + def execute( + self, + sqltext: Union[Executable, str], + *, + execution_options: Optional[dict[str, Any]] = None, + ) -> None: + """Execute the given SQL using the current migration context. + + .. seealso:: + + :meth:`.Operations.execute` + + """ # noqa: E501 + ... + + # END STUB FUNCTIONS: batch_op diff --git a/libs/alembic/operations/batch.py b/libs/alembic/operations/batch.py index 00f13a1be..fd7ab9903 100644 --- a/libs/alembic/operations/batch.py +++ b/libs/alembic/operations/batch.py @@ -1,3 +1,6 @@ +# mypy: allow-untyped-defs, allow-incomplete-defs, allow-untyped-calls +# mypy: no-warn-return-any, allow-any-generics + from __future__ import annotations from typing import Any @@ -17,7 +20,7 @@ from sqlalchemy import PrimaryKeyConstraint from sqlalchemy import schema as sql_schema from sqlalchemy import Table from sqlalchemy import types as sqltypes -from sqlalchemy.events import SchemaEventTarget +from sqlalchemy.sql.schema import SchemaEventTarget from sqlalchemy.util import OrderedDict from sqlalchemy.util import topological @@ -185,11 +188,11 @@ class BatchOperationsImpl: def rename_table(self, *arg, **kw): self.batch.append(("rename_table", arg, kw)) - def create_index(self, idx: Index) -> None: - self.batch.append(("create_index", (idx,), {})) + def create_index(self, idx: Index, **kw: Any) -> None: + self.batch.append(("create_index", (idx,), kw)) - def drop_index(self, idx: Index) -> None: - self.batch.append(("drop_index", (idx,), {})) + def drop_index(self, idx: Index, **kw: Any) -> None: + self.batch.append(("drop_index", (idx,), kw)) def create_table_comment(self, table): self.batch.append(("create_table_comment", (table,), {})) @@ -243,7 +246,7 @@ class ApplyBatchImpl: def _grab_table_elements(self) -> None: schema = self.table.schema - self.columns: Dict[str, Column] = OrderedDict() + self.columns: Dict[str, Column[Any]] = OrderedDict() for c in self.table.c: c_copy = _copy(c, schema=schema) c_copy.unique = c_copy.index = False @@ -337,7 +340,6 @@ class ApplyBatchImpl: for const in ( list(self.named_constraints.values()) + self.unnamed_constraints ): - const_columns = {c.key for c in _columns_for_constraint(const)} if not const_columns.issubset(self.column_transfers): @@ -375,7 +377,7 @@ class ApplyBatchImpl: for idx_existing in self.indexes.values(): # this is a lift-and-move from Table.to_metadata - if idx_existing._column_flag: # type: ignore + if idx_existing._column_flag: continue idx_copy = Index( @@ -404,9 +406,7 @@ class ApplyBatchImpl: def _setup_referent( self, metadata: MetaData, constraint: ForeignKeyConstraint ) -> None: - spec = constraint.elements[ - 0 - ]._get_colspec() # type:ignore[attr-defined] + spec = constraint.elements[0]._get_colspec() parts = spec.split(".") tname = parts[-2] if len(parts) == 3: @@ -487,7 +487,7 @@ class ApplyBatchImpl: server_default: Optional[Union[Function[Any], str, bool]] = False, name: Optional[str] = None, type_: Optional[TypeEngine] = None, - autoincrement: None = None, + autoincrement: Optional[Union[bool, Literal["auto"]]] = None, comment: Union[str, Literal[False]] = False, **kw, ) -> None: @@ -547,9 +547,7 @@ class ApplyBatchImpl: else: sql_schema.DefaultClause( server_default # type: ignore[arg-type] - )._set_parent( # type:ignore[attr-defined] - existing - ) + )._set_parent(existing) if autoincrement is not None: existing.autoincrement = bool(autoincrement) @@ -607,7 +605,7 @@ class ApplyBatchImpl: def add_column( self, table_name: str, - column: Column, + column: Column[Any], insert_before: Optional[str] = None, insert_after: Optional[str] = None, **kw, @@ -621,7 +619,10 @@ class ApplyBatchImpl: self.column_transfers[column.name] = {} def drop_column( - self, table_name: str, column: Union[ColumnClause, Column], **kw + self, + table_name: str, + column: Union[ColumnClause[Any], Column[Any]], + **kw, ) -> None: if column.name in self.table.primary_key.columns: _remove_column_from_collection( diff --git a/libs/alembic/operations/ops.py b/libs/alembic/operations/ops.py index a40704fdc..7b65191cf 100644 --- a/libs/alembic/operations/ops.py +++ b/libs/alembic/operations/ops.py @@ -5,6 +5,7 @@ import re from typing import Any from typing import Callable from typing import cast +from typing import Dict from typing import FrozenSet from typing import Iterator from typing import List @@ -15,6 +16,7 @@ from typing import Set from typing import Tuple from typing import Type from typing import TYPE_CHECKING +from typing import TypeVar from typing import Union from sqlalchemy.types import NULLTYPE @@ -28,9 +30,7 @@ from ..util import sqla_compat if TYPE_CHECKING: from typing import Literal - from sqlalchemy.sql.dml import Insert - from sqlalchemy.sql.dml import Update - from sqlalchemy.sql.elements import BinaryExpression + from sqlalchemy.sql import Executable from sqlalchemy.sql.elements import ColumnElement from sqlalchemy.sql.elements import conv from sqlalchemy.sql.elements import quoted_name @@ -53,6 +53,10 @@ if TYPE_CHECKING: from ..autogenerate.rewriter import Rewriter from ..runtime.migration import MigrationContext + from ..script.revision import _RevIdType + +_T = TypeVar("_T", bound=Any) +_AC = TypeVar("_AC", bound="AddConstraintOp") class MigrateOperation: @@ -71,7 +75,7 @@ class MigrateOperation: """ @util.memoized_property - def info(self): + def info(self) -> Dict[Any, Any]: """A dictionary that may be used to store arbitrary information along with this :class:`.MigrateOperation` object. @@ -93,12 +97,14 @@ class AddConstraintOp(MigrateOperation): add_constraint_ops = util.Dispatcher() @property - def constraint_type(self): + def constraint_type(self) -> str: raise NotImplementedError() @classmethod - def register_add_constraint(cls, type_: str) -> Callable: - def go(klass): + def register_add_constraint( + cls, type_: str + ) -> Callable[[Type[_AC]], Type[_AC]]: + def go(klass: Type[_AC]) -> Type[_AC]: cls.add_constraint_ops.dispatch_for(type_)(klass.from_constraint) return klass @@ -106,7 +112,7 @@ class AddConstraintOp(MigrateOperation): @classmethod def from_constraint(cls, constraint: Constraint) -> AddConstraintOp: - return cls.add_constraint_ops.dispatch(constraint.__visit_name__)( + return cls.add_constraint_ops.dispatch(constraint.__visit_name__)( # type: ignore[no-any-return] # noqa: E501 constraint ) @@ -133,6 +139,7 @@ class DropConstraintOp(MigrateOperation): constraint_name: Optional[sqla_compat._ConstraintNameDefined], table_name: str, type_: Optional[str] = None, + *, schema: Optional[str] = None, _reverse: Optional[AddConstraintOp] = None, ) -> None: @@ -169,14 +176,11 @@ class DropConstraintOp(MigrateOperation): sqla_compat.constraint_name_or_none(constraint.name), constraint_table.name, schema=constraint_table.schema, - type_=types[constraint.__visit_name__], + type_=types.get(constraint.__visit_name__), _reverse=AddConstraintOp.from_constraint(constraint), ) - def to_constraint( - self, - ) -> Constraint: - + def to_constraint(self) -> Constraint: if self._reverse is not None: constraint = self._reverse.to_constraint() constraint.name = self.constraint_name @@ -198,8 +202,9 @@ class DropConstraintOp(MigrateOperation): constraint_name: str, table_name: str, type_: Optional[str] = None, + *, schema: Optional[str] = None, - ) -> Optional[Table]: + ) -> None: r"""Drop a constraint of the given name, typically via DROP CONSTRAINT. :param constraint_name: name of the constraint. @@ -258,6 +263,7 @@ class CreatePrimaryKeyOp(AddConstraintOp): constraint_name: Optional[sqla_compat._ConstraintNameDefined], table_name: str, columns: Sequence[str], + *, schema: Optional[str] = None, **kw: Any, ) -> None: @@ -299,18 +305,17 @@ class CreatePrimaryKeyOp(AddConstraintOp): constraint_name: Optional[str], table_name: str, columns: List[str], + *, schema: Optional[str] = None, - ) -> Optional[Table]: + ) -> None: """Issue a "create primary key" instruction using the current migration context. e.g.:: from alembic import op - op.create_primary_key( - "pk_my_table", "my_table", - ["id", "version"] - ) + + op.create_primary_key("pk_my_table", "my_table", ["id", "version"]) This internally generates a :class:`~sqlalchemy.schema.Table` object containing the necessary columns, then generates a new @@ -337,7 +342,7 @@ class CreatePrimaryKeyOp(AddConstraintOp): :class:`~sqlalchemy.sql.elements.quoted_name`. """ - op = cls(constraint_name, table_name, columns, schema) + op = cls(constraint_name, table_name, columns, schema=schema) return operations.invoke(op) @classmethod @@ -382,6 +387,7 @@ class CreateUniqueConstraintOp(AddConstraintOp): constraint_name: Optional[sqla_compat._ConstraintNameDefined], table_name: str, columns: Sequence[str], + *, schema: Optional[str] = None, **kw: Any, ) -> None: @@ -395,12 +401,11 @@ class CreateUniqueConstraintOp(AddConstraintOp): def from_constraint( cls, constraint: Constraint ) -> CreateUniqueConstraintOp: - constraint_table = sqla_compat._table_for_constraint(constraint) uq_constraint = cast("UniqueConstraint", constraint) - kw: dict = {} + kw: Dict[str, Any] = {} if uq_constraint.deferrable: kw["deferrable"] = uq_constraint.deferrable if uq_constraint.initially: @@ -433,6 +438,7 @@ class CreateUniqueConstraintOp(AddConstraintOp): constraint_name: Optional[str], table_name: str, columns: Sequence[str], + *, schema: Optional[str] = None, **kw: Any, ) -> Any: @@ -532,9 +538,8 @@ class CreateForeignKeyOp(AddConstraintOp): @classmethod def from_constraint(cls, constraint: Constraint) -> CreateForeignKeyOp: - fk_constraint = cast("ForeignKeyConstraint", constraint) - kw: dict = {} + kw: Dict[str, Any] = {} if fk_constraint.onupdate: kw["onupdate"] = fk_constraint.onupdate if fk_constraint.ondelete: @@ -545,6 +550,8 @@ class CreateForeignKeyOp(AddConstraintOp): kw["deferrable"] = fk_constraint.deferrable if fk_constraint.use_alter: kw["use_alter"] = fk_constraint.use_alter + if fk_constraint.match: + kw["match"] = fk_constraint.match ( source_schema, @@ -593,6 +600,7 @@ class CreateForeignKeyOp(AddConstraintOp): referent_table: str, local_cols: List[str], remote_cols: List[str], + *, onupdate: Optional[str] = None, ondelete: Optional[str] = None, deferrable: Optional[bool] = None, @@ -601,16 +609,21 @@ class CreateForeignKeyOp(AddConstraintOp): source_schema: Optional[str] = None, referent_schema: Optional[str] = None, **dialect_kw: Any, - ) -> Optional[Table]: + ) -> None: """Issue a "create foreign key" instruction using the current migration context. e.g.:: from alembic import op + op.create_foreign_key( - "fk_user_address", "address", - "user", ["user_id"], ["id"]) + "fk_user_address", + "address", + "user", + ["user_id"], + ["id"], + ) This internally generates a :class:`~sqlalchemy.schema.Table` object containing the necessary columns, then generates a new @@ -672,12 +685,13 @@ class CreateForeignKeyOp(AddConstraintOp): referent_table: str, local_cols: List[str], remote_cols: List[str], + *, referent_schema: Optional[str] = None, - onupdate: None = None, - ondelete: None = None, - deferrable: None = None, - initially: None = None, - match: None = None, + onupdate: Optional[str] = None, + ondelete: Optional[str] = None, + deferrable: Optional[bool] = None, + initially: Optional[str] = None, + match: Optional[str] = None, **dialect_kw: Any, ) -> None: """Issue a "create foreign key" instruction using the @@ -690,8 +704,11 @@ class CreateForeignKeyOp(AddConstraintOp): with batch_alter_table("address") as batch_op: batch_op.create_foreign_key( - "fk_user_address", - "user", ["user_id"], ["id"]) + "fk_user_address", + "user", + ["user_id"], + ["id"], + ) .. seealso:: @@ -733,6 +750,7 @@ class CreateCheckConstraintOp(AddConstraintOp): constraint_name: Optional[sqla_compat._ConstraintNameDefined], table_name: str, condition: Union[str, TextClause, ColumnElement[Any]], + *, schema: Optional[str] = None, **kw: Any, ) -> None: @@ -775,10 +793,11 @@ class CreateCheckConstraintOp(AddConstraintOp): operations: Operations, constraint_name: Optional[str], table_name: str, - condition: Union[str, BinaryExpression], + condition: Union[str, ColumnElement[bool], TextClause], + *, schema: Optional[str] = None, **kw: Any, - ) -> Optional[Table]: + ) -> None: """Issue a "create check constraint" instruction using the current migration context. @@ -790,7 +809,7 @@ class CreateCheckConstraintOp(AddConstraintOp): op.create_check_constraint( "ck_user_name_len", "user", - func.len(column('name')) > 5 + func.len(column("name")) > 5, ) CHECK constraints are usually against a SQL expression, so ad-hoc @@ -827,9 +846,9 @@ class CreateCheckConstraintOp(AddConstraintOp): cls, operations: BatchOperations, constraint_name: str, - condition: TextClause, + condition: Union[str, ColumnElement[bool], TextClause], **kw: Any, - ) -> Optional[Table]: + ) -> None: """Issue a "create check constraint" instruction using the current batch migration context. @@ -861,8 +880,10 @@ class CreateIndexOp(MigrateOperation): index_name: Optional[str], table_name: str, columns: Sequence[Union[str, TextClause, ColumnElement[Any]]], + *, schema: Optional[str] = None, unique: bool = False, + if_not_exists: Optional[bool] = None, **kw: Any, ) -> None: self.index_name = index_name @@ -870,6 +891,7 @@ class CreateIndexOp(MigrateOperation): self.columns = columns self.schema = schema self.unique = unique + self.if_not_exists = if_not_exists self.kw = kw def reverse(self) -> DropIndexOp: @@ -882,9 +904,9 @@ class CreateIndexOp(MigrateOperation): def from_index(cls, index: Index) -> CreateIndexOp: assert index.table is not None return cls( - index.name, # type: ignore[arg-type] + index.name, index.table.name, - sqla_compat._get_index_expressions(index), + index.expressions, schema=index.table.schema, unique=index.unique, **index.kwargs, @@ -912,24 +934,28 @@ class CreateIndexOp(MigrateOperation): index_name: Optional[str], table_name: str, columns: Sequence[Union[str, TextClause, Function[Any]]], + *, schema: Optional[str] = None, unique: bool = False, + if_not_exists: Optional[bool] = None, **kw: Any, - ) -> Optional[Table]: + ) -> None: r"""Issue a "create index" instruction using the current migration context. e.g.:: from alembic import op - op.create_index('ik_test', 't1', ['foo', 'bar']) + + op.create_index("ik_test", "t1", ["foo", "bar"]) Functional indexes can be produced by using the :func:`sqlalchemy.sql.expression.text` construct:: from alembic import op from sqlalchemy import text - op.create_index('ik_test', 't1', [text('lower(foo)')]) + + op.create_index("ik_test", "t1", [text("lower(foo)")]) :param index_name: name of the index. :param table_name: name of the owning table. @@ -941,24 +967,34 @@ class CreateIndexOp(MigrateOperation): :class:`~sqlalchemy.sql.elements.quoted_name`. :param unique: If True, create a unique index. - :param quote: - Force quoting of this column's name on or off, corresponding - to ``True`` or ``False``. When left at its default - of ``None``, the column identifier will be quoted according to - whether the name is case sensitive (identifiers with at least one - upper case character are treated as case sensitive), or if it's a - reserved word. This flag is only needed to force quoting of a - reserved word which is not known by the SQLAlchemy dialect. + :param quote: Force quoting of this column's name on or off, + corresponding to ``True`` or ``False``. When left at its default + of ``None``, the column identifier will be quoted according to + whether the name is case sensitive (identifiers with at least one + upper case character are treated as case sensitive), or if it's a + reserved word. This flag is only needed to force quoting of a + reserved word which is not known by the SQLAlchemy dialect. + + :param if_not_exists: If True, adds IF NOT EXISTS operator when + creating the new index. + + .. versionadded:: 1.12.0 :param \**kw: Additional keyword arguments not mentioned above are - dialect specific, and passed in the form - ``_``. - See the documentation regarding an individual dialect at - :ref:`dialect_toplevel` for detail on documented arguments. + dialect specific, and passed in the form + ``_``. + See the documentation regarding an individual dialect at + :ref:`dialect_toplevel` for detail on documented arguments. """ op = cls( - index_name, table_name, columns, schema=schema, unique=unique, **kw + index_name, + table_name, + columns, + schema=schema, + unique=unique, + if_not_exists=if_not_exists, + **kw, ) return operations.invoke(op) @@ -969,7 +1005,7 @@ class CreateIndexOp(MigrateOperation): index_name: str, columns: List[str], **kw: Any, - ) -> Optional[Table]: + ) -> None: """Issue a "create index" instruction using the current batch migration context. @@ -998,13 +1034,16 @@ class DropIndexOp(MigrateOperation): self, index_name: Union[quoted_name, str, conv], table_name: Optional[str] = None, + *, schema: Optional[str] = None, + if_exists: Optional[bool] = None, _reverse: Optional[CreateIndexOp] = None, **kw: Any, ) -> None: self.index_name = index_name self.table_name = table_name self.schema = schema + self.if_exists = if_exists self._reverse = _reverse self.kw = kw @@ -1019,9 +1058,10 @@ class DropIndexOp(MigrateOperation): assert index.table is not None return cls( index.name, # type: ignore[arg-type] - index.table.name, + table_name=index.table.name, schema=index.table.schema, _reverse=CreateIndexOp.from_index(index), + unique=index.unique, **index.kwargs, ) @@ -1046,9 +1086,11 @@ class DropIndexOp(MigrateOperation): operations: Operations, index_name: str, table_name: Optional[str] = None, + *, schema: Optional[str] = None, + if_exists: Optional[bool] = None, **kw: Any, - ) -> Optional[Table]: + ) -> None: r"""Issue a "drop index" instruction using the current migration context. @@ -1063,20 +1105,32 @@ class DropIndexOp(MigrateOperation): quoting of the schema outside of the default behavior, use the SQLAlchemy construct :class:`~sqlalchemy.sql.elements.quoted_name`. + + :param if_exists: If True, adds IF EXISTS operator when + dropping the index. + + .. versionadded:: 1.12.0 + :param \**kw: Additional keyword arguments not mentioned above are - dialect specific, and passed in the form - ``_``. - See the documentation regarding an individual dialect at - :ref:`dialect_toplevel` for detail on documented arguments. + dialect specific, and passed in the form + ``_``. + See the documentation regarding an individual dialect at + :ref:`dialect_toplevel` for detail on documented arguments. """ - op = cls(index_name, table_name=table_name, schema=schema, **kw) + op = cls( + index_name, + table_name=table_name, + schema=schema, + if_exists=if_exists, + **kw, + ) return operations.invoke(op) @classmethod def batch_drop_index( cls, operations: BatchOperations, index_name: str, **kw: Any - ) -> Optional[Table]: + ) -> None: """Issue a "drop index" instruction using the current batch migration context. @@ -1103,6 +1157,7 @@ class CreateTableOp(MigrateOperation): self, table_name: str, columns: Sequence[SchemaItem], + *, schema: Optional[str] = None, _namespace_metadata: Optional[MetaData] = None, _constraints_included: bool = False, @@ -1128,14 +1183,14 @@ class CreateTableOp(MigrateOperation): @classmethod def from_table( - cls, table: Table, _namespace_metadata: Optional[MetaData] = None + cls, table: Table, *, _namespace_metadata: Optional[MetaData] = None ) -> CreateTableOp: if _namespace_metadata is None: _namespace_metadata = table.metadata return cls( table.name, - list(table.c) + list(table.constraints), # type:ignore[arg-type] + list(table.c) + list(table.constraints), schema=table.schema, _namespace_metadata=_namespace_metadata, # given a Table() object, this Table will contain full Index() @@ -1174,7 +1229,7 @@ class CreateTableOp(MigrateOperation): table_name: str, *columns: SchemaItem, **kw: Any, - ) -> Optional[Table]: + ) -> Table: r"""Issue a "create table" instruction using the current migration context. @@ -1186,11 +1241,11 @@ class CreateTableOp(MigrateOperation): from alembic import op op.create_table( - 'account', - Column('id', INTEGER, primary_key=True), - Column('name', VARCHAR(50), nullable=False), - Column('description', NVARCHAR(200)), - Column('timestamp', TIMESTAMP, server_default=func.now()) + "account", + Column("id", INTEGER, primary_key=True), + Column("name", VARCHAR(50), nullable=False), + Column("description", NVARCHAR(200)), + Column("timestamp", TIMESTAMP, server_default=func.now()), ) Note that :meth:`.create_table` accepts @@ -1204,9 +1259,10 @@ class CreateTableOp(MigrateOperation): from sqlalchemy import Column, TIMESTAMP, func # specify "DEFAULT NOW" along with the "timestamp" column - op.create_table('account', - Column('id', INTEGER, primary_key=True), - Column('timestamp', TIMESTAMP, server_default=func.now()) + op.create_table( + "account", + Column("id", INTEGER, primary_key=True), + Column("timestamp", TIMESTAMP, server_default=func.now()), ) The function also returns a newly created @@ -1219,11 +1275,11 @@ class CreateTableOp(MigrateOperation): from alembic import op account_table = op.create_table( - 'account', - Column('id', INTEGER, primary_key=True), - Column('name', VARCHAR(50), nullable=False), - Column('description', NVARCHAR(200)), - Column('timestamp', TIMESTAMP, server_default=func.now()) + "account", + Column("id", INTEGER, primary_key=True), + Column("name", VARCHAR(50), nullable=False), + Column("description", NVARCHAR(200)), + Column("timestamp", TIMESTAMP, server_default=func.now()), ) op.bulk_insert( @@ -1231,7 +1287,7 @@ class CreateTableOp(MigrateOperation): [ {"name": "A1", "description": "account 1"}, {"name": "A2", "description": "account 2"}, - ] + ], ) :param table_name: Name of the table @@ -1262,6 +1318,7 @@ class DropTableOp(MigrateOperation): def __init__( self, table_name: str, + *, schema: Optional[str] = None, table_kw: Optional[MutableMapping[Any, Any]] = None, _reverse: Optional[CreateTableOp] = None, @@ -1282,7 +1339,7 @@ class DropTableOp(MigrateOperation): @classmethod def from_table( - cls, table: Table, _namespace_metadata: Optional[MetaData] = None + cls, table: Table, *, _namespace_metadata: Optional[MetaData] = None ) -> DropTableOp: return cls( table.name, @@ -1326,6 +1383,7 @@ class DropTableOp(MigrateOperation): cls, operations: Operations, table_name: str, + *, schema: Optional[str] = None, **kw: Any, ) -> None: @@ -1356,6 +1414,7 @@ class AlterTableOp(MigrateOperation): def __init__( self, table_name: str, + *, schema: Optional[str] = None, ) -> None: self.table_name = table_name @@ -1370,6 +1429,7 @@ class RenameTableOp(AlterTableOp): self, old_table_name: str, new_table_name: str, + *, schema: Optional[str] = None, ) -> None: super().__init__(old_table_name, schema=schema) @@ -1381,8 +1441,9 @@ class RenameTableOp(AlterTableOp): operations: Operations, old_table_name: str, new_table_name: str, + *, schema: Optional[str] = None, - ) -> Optional[Table]: + ) -> None: """Emit an ALTER TABLE to rename a table. :param old_table_name: old name. @@ -1408,6 +1469,7 @@ class CreateTableCommentOp(AlterTableOp): self, table_name: str, comment: Optional[str], + *, schema: Optional[str] = None, existing_comment: Optional[str] = None, ) -> None: @@ -1422,13 +1484,12 @@ class CreateTableCommentOp(AlterTableOp): operations: Operations, table_name: str, comment: Optional[str], - existing_comment: None = None, + *, + existing_comment: Optional[str] = None, schema: Optional[str] = None, - ) -> Optional[Table]: + ) -> None: """Emit a COMMENT ON operation to set the comment for a table. - .. versionadded:: 1.0.6 - :param table_name: string name of the target table. :param comment: string value of the comment being registered against the specified table. @@ -1456,15 +1517,14 @@ class CreateTableCommentOp(AlterTableOp): @classmethod def batch_create_table_comment( cls, - operations, - comment, - existing_comment=None, - ): + operations: BatchOperations, + comment: Optional[str], + *, + existing_comment: Optional[str] = None, + ) -> None: """Emit a COMMENT ON operation to set the comment for a table using the current batch migration context. - .. versionadded:: 1.6.0 - :param comment: string value of the comment being registered against the specified table. :param existing_comment: String value of a comment @@ -1482,7 +1542,7 @@ class CreateTableCommentOp(AlterTableOp): ) return operations.invoke(op) - def reverse(self): + def reverse(self) -> Union[CreateTableCommentOp, DropTableCommentOp]: """Reverses the COMMENT ON operation against a table.""" if self.existing_comment is None: return DropTableCommentOp( @@ -1498,14 +1558,16 @@ class CreateTableCommentOp(AlterTableOp): schema=self.schema, ) - def to_table(self, migration_context=None): + def to_table( + self, migration_context: Optional[MigrationContext] = None + ) -> Table: schema_obj = schemaobj.SchemaObjects(migration_context) return schema_obj.table( self.table_name, schema=self.schema, comment=self.comment ) - def to_diff_tuple(self): + def to_diff_tuple(self) -> Tuple[Any, ...]: return ("add_table_comment", self.to_table(), self.existing_comment) @@ -1519,6 +1581,7 @@ class DropTableCommentOp(AlterTableOp): def __init__( self, table_name: str, + *, schema: Optional[str] = None, existing_comment: Optional[str] = None, ) -> None: @@ -1531,14 +1594,13 @@ class DropTableCommentOp(AlterTableOp): cls, operations: Operations, table_name: str, + *, existing_comment: Optional[str] = None, schema: Optional[str] = None, - ) -> Optional[Table]: + ) -> None: """Issue a "drop table comment" operation to remove an existing comment set on a table. - .. versionadded:: 1.0.6 - :param table_name: string name of the target table. :param existing_comment: An optional string value of a comment already registered on the specified table. @@ -1555,13 +1617,16 @@ class DropTableCommentOp(AlterTableOp): return operations.invoke(op) @classmethod - def batch_drop_table_comment(cls, operations, existing_comment=None): + def batch_drop_table_comment( + cls, + operations: BatchOperations, + *, + existing_comment: Optional[str] = None, + ) -> None: """Issue a "drop table comment" operation to remove an existing comment set on a table using the current batch operations context. - .. versionadded:: 1.6.0 - :param existing_comment: An optional string value of a comment already registered on the specified table. @@ -1574,18 +1639,20 @@ class DropTableCommentOp(AlterTableOp): ) return operations.invoke(op) - def reverse(self): + def reverse(self) -> CreateTableCommentOp: """Reverses the COMMENT ON operation against a table.""" return CreateTableCommentOp( self.table_name, self.existing_comment, schema=self.schema ) - def to_table(self, migration_context=None): + def to_table( + self, migration_context: Optional[MigrationContext] = None + ) -> Table: schema_obj = schemaobj.SchemaObjects(migration_context) return schema_obj.table(self.table_name, schema=self.schema) - def to_diff_tuple(self): + def to_diff_tuple(self) -> Tuple[Any, ...]: return ("remove_table_comment", self.to_table()) @@ -1598,6 +1665,7 @@ class AlterColumnOp(AlterTableOp): self, table_name: str, column_name: str, + *, schema: Optional[str] = None, existing_type: Optional[Any] = None, existing_server_default: Any = False, @@ -1719,7 +1787,6 @@ class AlterColumnOp(AlterTableOp): return False def reverse(self) -> AlterColumnOp: - kw = self.kw.copy() kw["existing_type"] = self.existing_type kw["existing_nullable"] = self.existing_nullable @@ -1757,12 +1824,15 @@ class AlterColumnOp(AlterTableOp): operations: Operations, table_name: str, column_name: str, + *, nullable: Optional[bool] = None, comment: Optional[Union[str, Literal[False]]] = False, server_default: Any = False, new_column_name: Optional[str] = None, - type_: Optional[Union[TypeEngine, Type[TypeEngine]]] = None, - existing_type: Optional[Union[TypeEngine, Type[TypeEngine]]] = None, + type_: Optional[Union[TypeEngine[Any], Type[TypeEngine[Any]]]] = None, + existing_type: Optional[ + Union[TypeEngine[Any], Type[TypeEngine[Any]]] + ] = None, existing_server_default: Optional[ Union[str, bool, Identity, Computed] ] = False, @@ -1770,7 +1840,7 @@ class AlterColumnOp(AlterTableOp): existing_comment: Optional[str] = None, schema: Optional[str] = None, **kw: Any, - ) -> Optional[Table]: + ) -> None: r"""Issue an "alter column" instruction using the current migration context. @@ -1809,9 +1879,6 @@ class AlterColumnOp(AlterTableOp): Set to ``None`` to have the default removed. :param comment: optional string text of a new comment to add to the column. - - .. versionadded:: 1.0.6 - :param new_column_name: Optional; specify a string name here to indicate the new name within a column rename operation. :param type\_: Optional; a :class:`~sqlalchemy.types.TypeEngine` @@ -1828,7 +1895,7 @@ class AlterColumnOp(AlterTableOp): don't otherwise specify a new type, as well as for when nullability is being changed on a SQL Server column. It is also used if the type is a so-called - SQLlchemy "schema" type which may define a constraint (i.e. + SQLAlchemy "schema" type which may define a constraint (i.e. :class:`~sqlalchemy.types.Boolean`, :class:`~sqlalchemy.types.Enum`), so that the constraint can be dropped. @@ -1845,9 +1912,6 @@ class AlterColumnOp(AlterTableOp): :param existing_comment: string text of the existing comment on the column to be maintained. Required on MySQL if the existing comment on the column is not being changed. - - .. versionadded:: 1.0.6 - :param schema: Optional schema name to operate within. To control quoting of the schema outside of the default behavior, use the SQLAlchemy construct @@ -1883,19 +1947,24 @@ class AlterColumnOp(AlterTableOp): cls, operations: BatchOperations, column_name: str, + *, nullable: Optional[bool] = None, - comment: Union[str, Literal[False]] = False, - server_default: Union[Function[Any], bool] = False, + comment: Optional[Union[str, Literal[False]]] = False, + server_default: Any = False, new_column_name: Optional[str] = None, - type_: Optional[Union[TypeEngine, Type[TypeEngine]]] = None, - existing_type: Optional[Union[TypeEngine, Type[TypeEngine]]] = None, - existing_server_default: bool = False, - existing_nullable: None = None, - existing_comment: None = None, - insert_before: None = None, - insert_after: None = None, + type_: Optional[Union[TypeEngine[Any], Type[TypeEngine[Any]]]] = None, + existing_type: Optional[ + Union[TypeEngine[Any], Type[TypeEngine[Any]]] + ] = None, + existing_server_default: Optional[ + Union[str, bool, Identity, Computed] + ] = False, + existing_nullable: Optional[bool] = None, + existing_comment: Optional[str] = None, + insert_before: Optional[str] = None, + insert_after: Optional[str] = None, **kw: Any, - ) -> Optional[Table]: + ) -> None: """Issue an "alter column" instruction using the current batch migration context. @@ -1905,8 +1974,6 @@ class AlterColumnOp(AlterTableOp): :param insert_before: String name of an existing column which this column should be placed before, when creating the new table. - .. versionadded:: 1.4.0 - :param insert_after: String name of an existing column which this column should be placed after, when creating the new table. If both :paramref:`.BatchOperations.alter_column.insert_before` @@ -1914,8 +1981,6 @@ class AlterColumnOp(AlterTableOp): omitted, the column is inserted after the last existing column in the table. - .. versionadded:: 1.4.0 - .. seealso:: :meth:`.Operations.alter_column` @@ -1935,6 +2000,8 @@ class AlterColumnOp(AlterTableOp): modify_server_default=server_default, modify_nullable=nullable, modify_comment=comment, + insert_before=insert_before, + insert_after=insert_after, **kw, ) @@ -1949,7 +2016,8 @@ class AddColumnOp(AlterTableOp): def __init__( self, table_name: str, - column: Column, + column: Column[Any], + *, schema: Optional[str] = None, **kw: Any, ) -> None: @@ -1964,14 +2032,14 @@ class AddColumnOp(AlterTableOp): def to_diff_tuple( self, - ) -> Tuple[str, Optional[str], str, Column]: + ) -> Tuple[str, Optional[str], str, Column[Any]]: return ("add_column", self.schema, self.table_name, self.column) - def to_column(self) -> Column: + def to_column(self) -> Column[Any]: return self.column @classmethod - def from_column(cls, col: Column) -> AddColumnOp: + def from_column(cls, col: Column[Any]) -> AddColumnOp: return cls(col.table.name, col, schema=col.table.schema) @classmethod @@ -1979,7 +2047,7 @@ class AddColumnOp(AlterTableOp): cls, schema: Optional[str], tname: str, - col: Column, + col: Column[Any], ) -> AddColumnOp: return cls(tname, col, schema=schema) @@ -1988,9 +2056,10 @@ class AddColumnOp(AlterTableOp): cls, operations: Operations, table_name: str, - column: Column, + column: Column[Any], + *, schema: Optional[str] = None, - ) -> Optional[Table]: + ) -> None: """Issue an "add column" instruction using the current migration context. @@ -1999,35 +2068,64 @@ class AddColumnOp(AlterTableOp): from alembic import op from sqlalchemy import Column, String - op.add_column('organization', - Column('name', String()) - ) + op.add_column("organization", Column("name", String())) - The provided :class:`~sqlalchemy.schema.Column` object can also - specify a :class:`~sqlalchemy.schema.ForeignKey`, referencing - a remote table name. Alembic will automatically generate a stub - "referenced" table and emit a second ALTER statement in order - to add the constraint separately:: + The :meth:`.Operations.add_column` method typically corresponds + to the SQL command "ALTER TABLE... ADD COLUMN". Within the scope + of this command, the column's name, datatype, nullability, + and optional server-generated defaults may be indicated. + + .. note:: + + With the exception of NOT NULL constraints or single-column FOREIGN + KEY constraints, other kinds of constraints such as PRIMARY KEY, + UNIQUE or CHECK constraints **cannot** be generated using this + method; for these constraints, refer to operations such as + :meth:`.Operations.create_primary_key` and + :meth:`.Operations.create_check_constraint`. In particular, the + following :class:`~sqlalchemy.schema.Column` parameters are + **ignored**: + + * :paramref:`~sqlalchemy.schema.Column.primary_key` - SQL databases + typically do not support an ALTER operation that can add + individual columns one at a time to an existing primary key + constraint, therefore it's less ambiguous to use the + :meth:`.Operations.create_primary_key` method, which assumes no + existing primary key constraint is present. + * :paramref:`~sqlalchemy.schema.Column.unique` - use the + :meth:`.Operations.create_unique_constraint` method + * :paramref:`~sqlalchemy.schema.Column.index` - use the + :meth:`.Operations.create_index` method + + + The provided :class:`~sqlalchemy.schema.Column` object may include a + :class:`~sqlalchemy.schema.ForeignKey` constraint directive, + referencing a remote table name. For this specific type of constraint, + Alembic will automatically emit a second ALTER statement in order to + add the single-column FOREIGN KEY constraint separately:: from alembic import op from sqlalchemy import Column, INTEGER, ForeignKey - op.add_column('organization', - Column('account_id', INTEGER, ForeignKey('accounts.id')) + op.add_column( + "organization", + Column("account_id", INTEGER, ForeignKey("accounts.id")), ) - Note that this statement uses the :class:`~sqlalchemy.schema.Column` - construct as is from the SQLAlchemy library. In particular, - default values to be created on the database side are - specified using the ``server_default`` parameter, and not - ``default`` which only specifies Python-side defaults:: + The column argument passed to :meth:`.Operations.add_column` is a + :class:`~sqlalchemy.schema.Column` construct, used in the same way it's + used in SQLAlchemy. In particular, values or functions to be indicated + as producing the column's default value on the database side are + specified using the ``server_default`` parameter, and not ``default`` + which only specifies Python-side defaults:: from alembic import op from sqlalchemy import Column, TIMESTAMP, func # specify "DEFAULT NOW" along with the column add - op.add_column('account', - Column('timestamp', TIMESTAMP, server_default=func.now()) + op.add_column( + "account", + Column("timestamp", TIMESTAMP, server_default=func.now()), ) :param table_name: String name of the parent table. @@ -2047,10 +2145,11 @@ class AddColumnOp(AlterTableOp): def batch_add_column( cls, operations: BatchOperations, - column: Column, + column: Column[Any], + *, insert_before: Optional[str] = None, insert_after: Optional[str] = None, - ) -> Optional[Table]: + ) -> None: """Issue an "add column" instruction using the current batch migration context. @@ -2084,6 +2183,7 @@ class DropColumnOp(AlterTableOp): self, table_name: str, column_name: str, + *, schema: Optional[str] = None, _reverse: Optional[AddColumnOp] = None, **kw: Any, @@ -2095,7 +2195,7 @@ class DropColumnOp(AlterTableOp): def to_diff_tuple( self, - ) -> Tuple[str, Optional[str], str, Column]: + ) -> Tuple[str, Optional[str], str, Column[Any]]: return ( "remove_column", self.schema, @@ -2119,7 +2219,7 @@ class DropColumnOp(AlterTableOp): cls, schema: Optional[str], tname: str, - col: Column, + col: Column[Any], ) -> DropColumnOp: return cls( tname, @@ -2130,7 +2230,7 @@ class DropColumnOp(AlterTableOp): def to_column( self, migration_context: Optional[MigrationContext] = None - ) -> Column: + ) -> Column[Any]: if self._reverse is not None: return self._reverse.column schema_obj = schemaobj.SchemaObjects(migration_context) @@ -2142,15 +2242,16 @@ class DropColumnOp(AlterTableOp): operations: Operations, table_name: str, column_name: str, + *, schema: Optional[str] = None, **kw: Any, - ) -> Optional[Table]: + ) -> None: """Issue a "drop column" instruction using the current migration context. e.g.:: - drop_column('organization', 'account_id') + drop_column("organization", "account_id") :param table_name: name of table :param column_name: name of column @@ -2188,7 +2289,7 @@ class DropColumnOp(AlterTableOp): @classmethod def batch_drop_column( cls, operations: BatchOperations, column_name: str, **kw: Any - ) -> Optional[Table]: + ) -> None: """Issue a "drop column" instruction using the current batch migration context. @@ -2213,7 +2314,8 @@ class BulkInsertOp(MigrateOperation): def __init__( self, table: Union[Table, TableClause], - rows: List[dict], + rows: List[Dict[str, Any]], + *, multiinsert: bool = True, ) -> None: self.table = table @@ -2225,7 +2327,8 @@ class BulkInsertOp(MigrateOperation): cls, operations: Operations, table: Union[Table, TableClause], - rows: List[dict], + rows: List[Dict[str, Any]], + *, multiinsert: bool = True, ) -> None: """Issue a "bulk insert" operation using the current @@ -2245,37 +2348,58 @@ class BulkInsertOp(MigrateOperation): from sqlalchemy import String, Integer, Date # Create an ad-hoc table to use for the insert statement. - accounts_table = table('account', - column('id', Integer), - column('name', String), - column('create_date', Date) + accounts_table = table( + "account", + column("id", Integer), + column("name", String), + column("create_date", Date), ) - op.bulk_insert(accounts_table, + op.bulk_insert( + accounts_table, [ - {'id':1, 'name':'John Smith', - 'create_date':date(2010, 10, 5)}, - {'id':2, 'name':'Ed Williams', - 'create_date':date(2007, 5, 27)}, - {'id':3, 'name':'Wendy Jones', - 'create_date':date(2008, 8, 15)}, - ] + { + "id": 1, + "name": "John Smith", + "create_date": date(2010, 10, 5), + }, + { + "id": 2, + "name": "Ed Williams", + "create_date": date(2007, 5, 27), + }, + { + "id": 3, + "name": "Wendy Jones", + "create_date": date(2008, 8, 15), + }, + ], ) When using --sql mode, some datatypes may not render inline automatically, such as dates and other special types. When this issue is present, :meth:`.Operations.inline_literal` may be used:: - op.bulk_insert(accounts_table, + op.bulk_insert( + accounts_table, [ - {'id':1, 'name':'John Smith', - 'create_date':op.inline_literal("2010-10-05")}, - {'id':2, 'name':'Ed Williams', - 'create_date':op.inline_literal("2007-05-27")}, - {'id':3, 'name':'Wendy Jones', - 'create_date':op.inline_literal("2008-08-15")}, + { + "id": 1, + "name": "John Smith", + "create_date": op.inline_literal("2010-10-05"), + }, + { + "id": 2, + "name": "Ed Williams", + "create_date": op.inline_literal("2007-05-27"), + }, + { + "id": 3, + "name": "Wendy Jones", + "create_date": op.inline_literal("2008-08-15"), + }, ], - multiinsert=False + multiinsert=False, ) When using :meth:`.Operations.inline_literal` in conjunction with @@ -2308,13 +2432,15 @@ class BulkInsertOp(MigrateOperation): @Operations.register_operation("execute") +@BatchOperations.register_operation("execute", "batch_execute") class ExecuteSQLOp(MigrateOperation): """Represent an execute SQL operation.""" def __init__( self, - sqltext: Union[Update, str, Insert, TextClause], - execution_options: None = None, + sqltext: Union[Executable, str], + *, + execution_options: Optional[dict[str, Any]] = None, ) -> None: self.sqltext = sqltext self.execution_options = execution_options @@ -2323,9 +2449,10 @@ class ExecuteSQLOp(MigrateOperation): def execute( cls, operations: Operations, - sqltext: Union[str, TextClause, Update], - execution_options: None = None, - ) -> Optional[Table]: + sqltext: Union[Executable, str], + *, + execution_options: Optional[dict[str, Any]] = None, + ) -> None: r"""Execute the given SQL using the current migration context. The given SQL can be a plain string, e.g.:: @@ -2339,14 +2466,12 @@ class ExecuteSQLOp(MigrateOperation): from sqlalchemy import String from alembic import op - account = table('account', - column('name', String) - ) + account = table("account", column("name", String)) op.execute( - account.update().\\ - where(account.c.name==op.inline_literal('account 1')).\\ - values({'name':op.inline_literal('account 2')}) - ) + account.update() + .where(account.c.name == op.inline_literal("account 1")) + .values({"name": op.inline_literal("account 2")}) + ) Above, we made use of the SQLAlchemy :func:`sqlalchemy.sql.expression.table` and @@ -2370,15 +2495,17 @@ class ExecuteSQLOp(MigrateOperation): also be used normally, use the "bind" available from the context:: from alembic import op + connection = op.get_bind() connection.execute( - account.update().where(account.c.name=='account 1'). - values({"name": "account 2"}) + account.update() + .where(account.c.name == "account 1") + .values({"name": "account 2"}) ) Additionally, when passing the statement as a plain string, it is first - coerceed into a :func:`sqlalchemy.sql.expression.text` construct + coerced into a :func:`sqlalchemy.sql.expression.text` construct before being passed along. In the less likely case that the literal SQL string contains a colon, it must be escaped with a backslash, as:: @@ -2391,11 +2518,10 @@ class ExecuteSQLOp(MigrateOperation): * a string * a :func:`sqlalchemy.sql.expression.text` construct. * a :func:`sqlalchemy.sql.expression.insert` construct. - * a :func:`sqlalchemy.sql.expression.update`, - :func:`sqlalchemy.sql.expression.insert`, - or :func:`sqlalchemy.sql.expression.delete` construct. - * Pretty much anything that's "executable" as described - in :ref:`sqlexpression_toplevel`. + * a :func:`sqlalchemy.sql.expression.update` construct. + * a :func:`sqlalchemy.sql.expression.delete` construct. + * Any "executable" described in SQLAlchemy Core documentation, + noting that no result set is returned. .. note:: when passing a plain string, the statement is coerced into a :func:`sqlalchemy.sql.expression.text` construct. This construct @@ -2410,6 +2536,28 @@ class ExecuteSQLOp(MigrateOperation): op = cls(sqltext, execution_options=execution_options) return operations.invoke(op) + @classmethod + def batch_execute( + cls, + operations: Operations, + sqltext: Union[Executable, str], + *, + execution_options: Optional[dict[str, Any]] = None, + ) -> None: + """Execute the given SQL using the current migration context. + + .. seealso:: + + :meth:`.Operations.execute` + + """ + return cls.execute( + operations, sqltext, execution_options=execution_options + ) + + def to_diff_tuple(self) -> Tuple[str, Union[Executable, str]]: + return ("execute", self.sqltext) + class OpContainer(MigrateOperation): """Represent a sequence of operations operation.""" @@ -2441,6 +2589,7 @@ class ModifyTableOps(OpContainer): self, table_name: str, ops: Sequence[MigrateOperation], + *, schema: Optional[str] = None, ) -> None: super().__init__(ops) @@ -2474,7 +2623,7 @@ class UpgradeOps(OpContainer): self.upgrade_token = upgrade_token def reverse_into(self, downgrade_ops: DowngradeOps) -> DowngradeOps: - downgrade_ops.ops[:] = list( # type:ignore[index] + downgrade_ops.ops[:] = list( reversed([op.reverse() for op in self.ops]) ) return downgrade_ops @@ -2501,7 +2650,7 @@ class DowngradeOps(OpContainer): super().__init__(ops=ops) self.downgrade_token = downgrade_token - def reverse(self): + def reverse(self) -> UpgradeOps: return UpgradeOps( ops=list(reversed([op.reverse() for op in self.ops])) ) @@ -2532,19 +2681,22 @@ class MigrationScript(MigrateOperation): """ _needs_render: Optional[bool] + _upgrade_ops: List[UpgradeOps] + _downgrade_ops: List[DowngradeOps] def __init__( self, rev_id: Optional[str], upgrade_ops: UpgradeOps, downgrade_ops: DowngradeOps, + *, message: Optional[str] = None, imports: Set[str] = set(), head: Optional[str] = None, splice: Optional[bool] = None, - branch_label: Optional[str] = None, + branch_label: Optional[_RevIdType] = None, version_path: Optional[str] = None, - depends_on: Optional[Union[str, Sequence[str]]] = None, + depends_on: Optional[_RevIdType] = None, ) -> None: self.rev_id = rev_id self.message = message @@ -2558,7 +2710,7 @@ class MigrationScript(MigrateOperation): self.downgrade_ops = downgrade_ops @property - def upgrade_ops(self): + def upgrade_ops(self) -> Optional[UpgradeOps]: """An instance of :class:`.UpgradeOps`. .. seealso:: @@ -2577,13 +2729,15 @@ class MigrationScript(MigrateOperation): return self._upgrade_ops[0] @upgrade_ops.setter - def upgrade_ops(self, upgrade_ops): + def upgrade_ops( + self, upgrade_ops: Union[UpgradeOps, List[UpgradeOps]] + ) -> None: self._upgrade_ops = util.to_list(upgrade_ops) for elem in self._upgrade_ops: assert isinstance(elem, UpgradeOps) @property - def downgrade_ops(self): + def downgrade_ops(self) -> Optional[DowngradeOps]: """An instance of :class:`.DowngradeOps`. .. seealso:: @@ -2602,7 +2756,9 @@ class MigrationScript(MigrateOperation): return self._downgrade_ops[0] @downgrade_ops.setter - def downgrade_ops(self, downgrade_ops): + def downgrade_ops( + self, downgrade_ops: Union[DowngradeOps, List[DowngradeOps]] + ) -> None: self._downgrade_ops = util.to_list(downgrade_ops) for elem in self._downgrade_ops: assert isinstance(elem, DowngradeOps) diff --git a/libs/alembic/operations/schemaobj.py b/libs/alembic/operations/schemaobj.py index 0568471a7..32b26e9b9 100644 --- a/libs/alembic/operations/schemaobj.py +++ b/libs/alembic/operations/schemaobj.py @@ -1,3 +1,6 @@ +# mypy: allow-untyped-defs, allow-incomplete-defs, allow-untyped-calls +# mypy: no-warn-return-any, allow-any-generics + from __future__ import annotations from typing import Any @@ -89,7 +92,10 @@ class SchemaObjects: t1 = sa_schema.Table( source, m, - *[sa_schema.Column(n, NULLTYPE) for n in t1_cols], + *[ + sa_schema.Column(n, NULLTYPE) + for n in util.unique_list(t1_cols) + ], schema=source_schema, ) @@ -271,10 +277,8 @@ class SchemaObjects: ForeignKey. """ - if isinstance(fk._colspec, str): # type:ignore[attr-defined] - table_key, cname = fk._colspec.rsplit( # type:ignore[attr-defined] - ".", 1 - ) + if isinstance(fk._colspec, str): + table_key, cname = fk._colspec.rsplit(".", 1) sname, tname = self._parse_table_key(table_key) if table_key not in metadata.tables: rel_t = sa_schema.Table(tname, metadata, schema=sname) diff --git a/libs/alembic/operations/toimpl.py b/libs/alembic/operations/toimpl.py index add142de3..4759f7fd2 100644 --- a/libs/alembic/operations/toimpl.py +++ b/libs/alembic/operations/toimpl.py @@ -1,3 +1,6 @@ +# mypy: allow-untyped-defs, allow-incomplete-defs, allow-untyped-calls +# mypy: no-warn-return-any, allow-any-generics + from typing import TYPE_CHECKING from sqlalchemy import schema as sa_schema @@ -5,6 +8,7 @@ from sqlalchemy import schema as sa_schema from . import ops from .base import Operations from ..util.sqla_compat import _copy +from ..util.sqla_compat import sqla_14 if TYPE_CHECKING: from sqlalchemy.sql.schema import Table @@ -14,7 +18,6 @@ if TYPE_CHECKING: def alter_column( operations: "Operations", operation: "ops.AlterColumnOp" ) -> None: - compiler = operations.impl.dialect.statement_compiler( operations.impl.dialect, None ) @@ -60,7 +63,7 @@ def alter_column( existing_nullable=existing_nullable, comment=comment, existing_comment=existing_comment, - **operation.kw + **operation.kw, ) if type_: @@ -96,13 +99,27 @@ def create_index( operations: "Operations", operation: "ops.CreateIndexOp" ) -> None: idx = operation.to_index(operations.migration_context) - operations.impl.create_index(idx) + kw = {} + if operation.if_not_exists is not None: + if not sqla_14: + raise NotImplementedError("SQLAlchemy 1.4+ required") + + kw["if_not_exists"] = operation.if_not_exists + operations.impl.create_index(idx, **kw) @Operations.implementation_for(ops.DropIndexOp) def drop_index(operations: "Operations", operation: "ops.DropIndexOp") -> None: + kw = {} + if operation.if_exists is not None: + if not sqla_14: + raise NotImplementedError("SQLAlchemy 1.4+ required") + + kw["if_exists"] = operation.if_exists + operations.impl.drop_index( - operation.to_index(operations.migration_context) + operation.to_index(operations.migration_context), + **kw, ) diff --git a/libs/alembic/runtime/environment.py b/libs/alembic/runtime/environment.py index c2fa11adb..d64b2adc2 100644 --- a/libs/alembic/runtime/environment.py +++ b/libs/alembic/runtime/environment.py @@ -2,38 +2,108 @@ from __future__ import annotations from typing import Any from typing import Callable +from typing import Collection from typing import ContextManager from typing import Dict from typing import List +from typing import Mapping +from typing import MutableMapping from typing import Optional from typing import overload +from typing import Sequence from typing import TextIO from typing import Tuple from typing import TYPE_CHECKING from typing import Union +from sqlalchemy.sql.schema import Column +from sqlalchemy.sql.schema import FetchedValue +from typing_extensions import Literal + from .migration import _ProxyTransaction from .migration import MigrationContext from .. import util from ..operations import Operations +from ..script.revision import _GetRevArg if TYPE_CHECKING: - from typing import Literal - from sqlalchemy.engine import URL from sqlalchemy.engine.base import Connection - from sqlalchemy.sql.elements import ClauseElement + from sqlalchemy.sql import Executable from sqlalchemy.sql.schema import MetaData + from sqlalchemy.sql.schema import SchemaItem + from sqlalchemy.sql.type_api import TypeEngine + from .migration import MigrationInfo + from ..autogenerate.api import AutogenContext from ..config import Config from ..ddl import DefaultImpl - from ..operations.ops import MigrateOperation + from ..operations.ops import MigrationScript from ..script.base import ScriptDirectory _RevNumber = Optional[Union[str, Tuple[str, ...]]] ProcessRevisionDirectiveFn = Callable[ - [MigrationContext, Tuple[str, str], List["MigrateOperation"]], None + [MigrationContext, _GetRevArg, List["MigrationScript"]], None +] + +RenderItemFn = Callable[ + [str, Any, "AutogenContext"], Union[str, Literal[False]] +] + +NameFilterType = Literal[ + "schema", + "table", + "column", + "index", + "unique_constraint", + "foreign_key_constraint", +] +NameFilterParentNames = MutableMapping[ + Literal["schema_name", "table_name", "schema_qualified_table_name"], + Optional[str], +] +IncludeNameFn = Callable[ + [Optional[str], NameFilterType, NameFilterParentNames], bool +] + +IncludeObjectFn = Callable[ + [ + "SchemaItem", + Optional[str], + NameFilterType, + bool, + Optional["SchemaItem"], + ], + bool, +] + +OnVersionApplyFn = Callable[ + [MigrationContext, "MigrationInfo", Collection[Any], Mapping[str, Any]], + None, +] + +CompareServerDefault = Callable[ + [ + MigrationContext, + "Column[Any]", + "Column[Any]", + Optional[str], + Optional[FetchedValue], + Optional[str], + ], + Optional[bool], +] + +CompareType = Callable[ + [ + MigrationContext, + "Column[Any]", + "Column[Any]", + "TypeEngine[Any]", + "TypeEngine[Any]", + ], + Optional[bool], ] @@ -65,20 +135,22 @@ class EnvironmentContext(util.ModuleClsProxy): config.set_main_option("script_location", "myapp:migrations") script = ScriptDirectory.from_config(config) + def my_function(rev, context): '''do something with revision "rev", which will be the current database revision, and "context", which is the MigrationContext that the env.py will create''' + with EnvironmentContext( config, script, - fn = my_function, - as_sql = False, - starting_rev = 'base', - destination_rev = 'head', - tag = "sometag" + fn=my_function, + as_sql=False, + starting_rev="base", + destination_rev="head", + tag="sometag", ): script.run_env() @@ -156,9 +228,9 @@ class EnvironmentContext(util.ModuleClsProxy): has been configured. """ - return self.context_opts.get("as_sql", False) + return self.context_opts.get("as_sql", False) # type: ignore[no-any-return] # noqa: E501 - def is_transactional_ddl(self): + def is_transactional_ddl(self) -> bool: """Return True if the context is configured to expect a transactional DDL capable backend. @@ -267,7 +339,7 @@ class EnvironmentContext(util.ModuleClsProxy): line. """ - return self.context_opts.get("tag", None) + return self.context_opts.get("tag", None) # type: ignore[no-any-return] # noqa: E501 @overload def get_x_argument(self, as_dictionary: Literal[False]) -> List[str]: @@ -295,7 +367,11 @@ class EnvironmentContext(util.ModuleClsProxy): The return value is a list, returned directly from the ``argparse`` structure. If ``as_dictionary=True`` is passed, the ``x`` arguments are parsed using ``key=value`` format into a dictionary that is - then returned. + then returned. If there is no ``=`` in the argument, value is an empty + string. + + .. versionchanged:: 1.13.1 Support ``as_dictionary=True`` when + arguments are passed without the ``=`` symbol. For example, to support passing a database URL on the command line, the standard ``env.py`` script can be modified like this:: @@ -329,7 +405,12 @@ class EnvironmentContext(util.ModuleClsProxy): else: value = [] if as_dictionary: - value = dict(arg.split("=", 1) for arg in value) + dict_value = {} + for arg in value: + x_key, _, x_value = arg.partition("=") + dict_value[x_key] = x_value + value = dict_value + return value def configure( @@ -345,23 +426,23 @@ class EnvironmentContext(util.ModuleClsProxy): tag: Optional[str] = None, template_args: Optional[Dict[str, Any]] = None, render_as_batch: bool = False, - target_metadata: Optional[MetaData] = None, - include_name: Optional[Callable[..., bool]] = None, - include_object: Optional[Callable[..., bool]] = None, + target_metadata: Union[MetaData, Sequence[MetaData], None] = None, + include_name: Optional[IncludeNameFn] = None, + include_object: Optional[IncludeObjectFn] = None, include_schemas: bool = False, process_revision_directives: Optional[ ProcessRevisionDirectiveFn ] = None, - compare_type: bool = False, - compare_server_default: bool = False, - render_item: Optional[Callable[..., bool]] = None, + compare_type: Union[bool, CompareType] = True, + compare_server_default: Union[bool, CompareServerDefault] = False, + render_item: Optional[RenderItemFn] = None, literal_binds: bool = False, upgrade_token: str = "upgrades", downgrade_token: str = "downgrades", alembic_module_prefix: str = "op.", sqlalchemy_module_prefix: str = "sa.", user_module_prefix: Optional[str] = None, - on_version_apply: Optional[Callable[..., None]] = None, + on_version_apply: Optional[OnVersionApplyFn] = None, **kw: Any, ) -> None: """Configure a :class:`.MigrationContext` within this @@ -408,9 +489,6 @@ class EnvironmentContext(util.ModuleClsProxy): ``connection`` and ``url`` are not passed. :param dialect_opts: dictionary of options to be passed to dialect constructor. - - .. versionadded:: 1.0.12 - :param transactional_ddl: Force the usage of "transactional" DDL on or off; this otherwise defaults to whether or not the dialect in @@ -493,12 +571,16 @@ class EnvironmentContext(util.ModuleClsProxy): to produce candidate upgrade/downgrade operations. :param compare_type: Indicates type comparison behavior during an autogenerate - operation. Defaults to ``False`` which disables type - comparison. Set to - ``True`` to turn on default type comparison, which has varied - accuracy depending on backend. See :ref:`compare_types` + operation. Defaults to ``True`` turning on type comparison, which + has good accuracy on most backends. See :ref:`compare_types` for an example as well as information on other type - comparison options. + comparison options. Set to ``False`` which disables type + comparison. A callable can also be passed to provide custom type + comparison, see :ref:`compare_types` for additional details. + + .. versionchanged:: 1.12.0 The default value of + :paramref:`.EnvironmentContext.configure.compare_type` has been + changed to ``True``. .. seealso:: @@ -565,7 +647,8 @@ class EnvironmentContext(util.ModuleClsProxy): ``"unique_constraint"``, or ``"foreign_key_constraint"`` * ``parent_names``: a dictionary of "parent" object names, that are relative to the name being given. Keys in this dictionary may - include: ``"schema_name"``, ``"table_name"``. + include: ``"schema_name"``, ``"table_name"`` or + ``"schema_qualified_table_name"``. E.g.:: @@ -581,8 +664,6 @@ class EnvironmentContext(util.ModuleClsProxy): include_name = include_name ) - .. versionadded:: 1.5 - .. seealso:: :ref:`autogenerate_include_hooks` @@ -826,8 +907,7 @@ class EnvironmentContext(util.ModuleClsProxy): if render_item is not None: opts["render_item"] = render_item - if compare_type is not None: - opts["compare_type"] = compare_type + opts["compare_type"] = compare_type if compare_server_default is not None: opts["compare_server_default"] = compare_server_default opts["script"] = self.script @@ -869,8 +949,8 @@ class EnvironmentContext(util.ModuleClsProxy): def execute( self, - sql: Union[ClauseElement, str], - execution_options: Optional[dict] = None, + sql: Union[Executable, str], + execution_options: Optional[Dict[str, Any]] = None, ) -> None: """Execute the given SQL using the current change context. diff --git a/libs/alembic/runtime/migration.py b/libs/alembic/runtime/migration.py index 4e2d06251..95c69bc69 100644 --- a/libs/alembic/runtime/migration.py +++ b/libs/alembic/runtime/migration.py @@ -1,3 +1,6 @@ +# mypy: allow-untyped-defs, allow-incomplete-defs, allow-untyped-calls +# mypy: no-warn-return-any, allow-any-generics + from __future__ import annotations from contextlib import contextmanager @@ -5,10 +8,12 @@ from contextlib import nullcontext import logging import sys from typing import Any +from typing import Callable from typing import cast from typing import Collection from typing import ContextManager from typing import Dict +from typing import Iterable from typing import Iterator from typing import List from typing import Optional @@ -38,7 +43,7 @@ if TYPE_CHECKING: from sqlalchemy.engine.base import Connection from sqlalchemy.engine.base import Transaction from sqlalchemy.engine.mock import MockConnection - from sqlalchemy.sql.elements import ClauseElement + from sqlalchemy.sql import Executable from .environment import EnvironmentContext from ..config import Config @@ -74,7 +79,7 @@ class _ProxyTransaction: def __enter__(self) -> _ProxyTransaction: return self - def __exit__(self, type_: None, value: None, traceback: None) -> None: + def __exit__(self, type_: Any, value: Any, traceback: Any) -> None: if self._proxied_transaction is not None: self._proxied_transaction.__exit__(type_, value, traceback) self.migration_context._transaction = None @@ -97,6 +102,7 @@ class MigrationContext: # from within env.py script from alembic import context + migration_context = context.get_context() For usage outside of an ``env.py`` script, such as for @@ -122,6 +128,7 @@ class MigrationContext: # in any application, outside of the normal Alembic environment from alembic.operations import Operations + op = Operations(context) op.alter_column("mytable", "somecolumn", nullable=True) @@ -158,7 +165,9 @@ class MigrationContext: sqla_compat._get_connection_in_transaction(connection) ) - self._migrations_fn = opts.get("fn") + self._migrations_fn: Optional[ + Callable[..., Iterable[RevisionStep]] + ] = opts.get("fn") self.as_sql = as_sql self.purge = opts.get("purge", False) @@ -172,7 +181,7 @@ class MigrationContext: else: self.output_buffer = opts.get("output_buffer", sys.stdout) - self._user_compare_type = opts.get("compare_type", False) + self._user_compare_type = opts.get("compare_type", True) self._user_compare_server_default = opts.get( "compare_server_default", False ) @@ -314,8 +323,6 @@ class MigrationContext: migrations whether or not one of them has an autocommit block. - .. versionadded:: 1.2.0 - """ _in_connection_transaction = self._in_connection_transaction() @@ -516,9 +523,8 @@ class MigrationContext: if start_from_rev == "base": start_from_rev = None elif start_from_rev is not None and self.script: - start_from_rev = [ - cast("Script", self.script.get_revision(sfr)).revision + self.script.get_revision(sfr).revision for sfr in util.to_list(start_from_rev) if sfr not in (None, "base") ] @@ -608,7 +614,6 @@ class MigrationContext: assert self._migrations_fn is not None for step in self._migrations_fn(heads, self): with self.begin_transaction(_per_migration=True): - if self.as_sql and not head_maintainer.heads: # for offline mode, include a CREATE TABLE from # the base @@ -649,8 +654,8 @@ class MigrationContext: def execute( self, - sql: Union[ClauseElement, str], - execution_options: Optional[dict] = None, + sql: Union[Executable, str], + execution_options: Optional[Dict[str, Any]] = None, ) -> None: """Execute a SQL construct or string statement. @@ -677,7 +682,7 @@ class MigrationContext: In online mode, this is an instance of :class:`sqlalchemy.engine.Connection`, and is suitable for ad-hoc execution of any kind of usage described - in :ref:`sqlexpression_toplevel` as well as + in SQLAlchemy Core documentation as well as for usage with the :meth:`sqlalchemy.schema.Table.create` and :meth:`sqlalchemy.schema.MetaData.create_all` methods of :class:`~sqlalchemy.schema.Table`, @@ -702,7 +707,7 @@ class MigrationContext: return None def _compare_type( - self, inspector_column: Column, metadata_column: Column + self, inspector_column: Column[Any], metadata_column: Column ) -> bool: if self._user_compare_type is False: return False @@ -722,12 +727,11 @@ class MigrationContext: def _compare_server_default( self, - inspector_column: Column, - metadata_column: Column, + inspector_column: Column[Any], + metadata_column: Column[Any], rendered_metadata_default: Optional[str], rendered_column_default: Optional[str], ) -> bool: - if self._user_compare_server_default is False: return False @@ -994,12 +998,17 @@ class MigrationInfo: class MigrationStep: - from_revisions_no_deps: Tuple[str, ...] to_revisions_no_deps: Tuple[str, ...] is_upgrade: bool migration_fn: Any + if TYPE_CHECKING: + + @property + def doc(self) -> Optional[str]: + ... + @property def name(self) -> str: return self.migration_fn.__name__ @@ -1048,13 +1057,9 @@ class RevisionStep(MigrationStep): self.revision = revision self.is_upgrade = is_upgrade if is_upgrade: - self.migration_fn = ( - revision.module.upgrade # type:ignore[attr-defined] - ) + self.migration_fn = revision.module.upgrade else: - self.migration_fn = ( - revision.module.downgrade # type:ignore[attr-defined] - ) + self.migration_fn = revision.module.downgrade def __repr__(self): return "RevisionStep(%r, is_upgrade=%r)" % ( @@ -1070,7 +1075,7 @@ class RevisionStep(MigrationStep): ) @property - def doc(self) -> str: + def doc(self) -> Optional[str]: return self.revision.doc @property @@ -1157,7 +1162,7 @@ class RevisionStep(MigrationStep): self.to_revisions[0], ) - def _unmerge_to_revisions(self, heads: Collection[str]) -> Tuple[str, ...]: + def _unmerge_to_revisions(self, heads: Set[str]) -> Tuple[str, ...]: other_heads = set(heads).difference([self.revision.revision]) if other_heads: ancestors = { @@ -1168,10 +1173,21 @@ class RevisionStep(MigrationStep): } return tuple(set(self.to_revisions).difference(ancestors)) else: - return self.to_revisions + # for each revision we plan to return, compute its ancestors + # (excluding self), and remove those from the final output since + # they are already accounted for. + ancestors = { + r.revision + for to_revision in self.to_revisions + for r in self.revision_map._get_ancestor_nodes( + self.revision_map.get_revisions(to_revision), check=False + ) + if r.revision != to_revision + } + return tuple(set(self.to_revisions).difference(ancestors)) def unmerge_branch_idents( - self, heads: Collection[str] + self, heads: Set[str] ) -> Tuple[str, str, Tuple[str, ...]]: to_revisions = self._unmerge_to_revisions(heads) @@ -1275,7 +1291,7 @@ class StampStep(MigrationStep): self.migration_fn = self.stamp_revision self.revision_map = revision_map - doc: None = None + doc: Optional[str] = None def stamp_revision(self, **kw: Any) -> None: return None @@ -1283,7 +1299,7 @@ class StampStep(MigrationStep): def __eq__(self, other): return ( isinstance(other, StampStep) - and other.from_revisions == self.revisions + and other.from_revisions == self.from_revisions and other.to_revisions == self.to_revisions and other.branch_move == self.branch_move and self.is_upgrade == other.is_upgrade diff --git a/libs/alembic/script/base.py b/libs/alembic/script/base.py index b6858b59a..5945ca591 100644 --- a/libs/alembic/script/base.py +++ b/libs/alembic/script/base.py @@ -9,9 +9,9 @@ import sys from types import ModuleType from typing import Any from typing import cast -from typing import Dict from typing import Iterator from typing import List +from typing import Mapping from typing import Optional from typing import Sequence from typing import Set @@ -23,25 +23,31 @@ from . import revision from . import write_hooks from .. import util from ..runtime import migration +from ..util import compat from ..util import not_none if TYPE_CHECKING: + from .revision import _GetRevArg + from .revision import _RevIdType + from .revision import Revision from ..config import Config + from ..config import MessagingOptions from ..runtime.migration import RevisionStep from ..runtime.migration import StampStep - from ..script.revision import Revision try: - from dateutil import tz + if compat.py39: + from zoneinfo import ZoneInfo + from zoneinfo import ZoneInfoNotFoundError + else: + from backports.zoneinfo import ZoneInfo # type: ignore[import-not-found,no-redef] # noqa: E501 + from backports.zoneinfo import ZoneInfoNotFoundError # type: ignore[no-redef] # noqa: E501 except ImportError: - tz = None # type: ignore[assignment] - -_RevIdType = Union[str, Sequence[str]] + ZoneInfo = None # type: ignore[assignment, misc] _sourceless_rev_file = re.compile(r"(?!\.\#|__init__)(.*\.py)(c|o)?$") _only_source_rev_file = re.compile(r"(?!\.\#|__init__)(.*\.py)$") _legacy_rev = re.compile(r"([a-f0-9]+)\.py$") -_mod_def_re = re.compile(r"(upgrade|downgrade)_([a-z0-9]+)") _slug_re = re.compile(r"\w+") _default_file_template = "%(rev)s_%(slug)s" _split_on_space_comma = re.compile(r", *|(?: +)") @@ -79,8 +85,11 @@ class ScriptDirectory: sourceless: bool = False, output_encoding: str = "utf-8", timezone: Optional[str] = None, - hook_config: Optional[Dict[str, str]] = None, + hook_config: Optional[Mapping[str, str]] = None, recursive_version_locations: bool = False, + messaging_opts: MessagingOptions = cast( + "MessagingOptions", util.EMPTY_DICT + ), ) -> None: self.dir = dir self.file_template = file_template @@ -92,6 +101,7 @@ class ScriptDirectory: self.timezone = timezone self.hook_config = hook_config self.recursive_version_locations = recursive_version_locations + self.messaging_opts = messaging_opts if not os.access(dir, os.F_OK): raise util.CommandError( @@ -109,7 +119,7 @@ class ScriptDirectory: return loc[0] @util.memoized_property - def _version_locations(self): + def _version_locations(self) -> Sequence[str]: if self.version_locations: return [ os.path.abspath(util.coerce_resource_to_filename(location)) @@ -225,6 +235,7 @@ class ScriptDirectory: timezone=config.get_main_option("timezone"), hook_config=config.get_section("post_write_hooks", {}), recursive_version_locations=rvl, + messaging_opts=config.messaging_opts, ) @contextmanager @@ -292,24 +303,22 @@ class ScriptDirectory: ): yield cast(Script, rev) - def get_revisions(self, id_: _RevIdType) -> Tuple[Optional[Script], ...]: + def get_revisions(self, id_: _GetRevArg) -> Tuple[Script, ...]: """Return the :class:`.Script` instance with the given rev identifier, symbolic name, or sequence of identifiers. """ with self._catch_revision_errors(): return cast( - Tuple[Optional[Script], ...], + Tuple[Script, ...], self.revision_map.get_revisions(id_), ) - def get_all_current(self, id_: Tuple[str, ...]) -> Set[Optional[Script]]: + def get_all_current(self, id_: Tuple[str, ...]) -> Set[Script]: with self._catch_revision_errors(): - return cast( - Set[Optional[Script]], self.revision_map._get_all_current(id_) - ) + return cast(Set[Script], self.revision_map._get_all_current(id_)) - def get_revision(self, id_: str) -> Optional[Script]: + def get_revision(self, id_: str) -> Script: """Return the :class:`.Script` instance with the given rev id. .. seealso:: @@ -319,7 +328,7 @@ class ScriptDirectory: """ with self._catch_revision_errors(): - return cast(Optional[Script], self.revision_map.get_revision(id_)) + return cast(Script, self.revision_map.get_revision(id_)) def as_revision_number( self, id_: Optional[str] @@ -475,7 +484,6 @@ class ScriptDirectory: multiple_heads="Multiple heads are present; please specify a " "single target revision" ): - heads_revs = self.get_revisions(heads) steps = [] @@ -498,7 +506,6 @@ class ScriptDirectory: dests = self.get_revisions(revision) or [None] for dest in dests: - if dest is None: # dest is 'base'. Return a "delete branch" migration # for all applicable heads. @@ -576,48 +583,51 @@ class ScriptDirectory: util.load_python_file(self.dir, "env.py") @property - def env_py_location(self): + def env_py_location(self) -> str: return os.path.abspath(os.path.join(self.dir, "env.py")) def _generate_template(self, src: str, dest: str, **kw: Any) -> None: - util.status( - "Generating %s" % os.path.abspath(dest), - util.template_to_file, - src, - dest, - self.output_encoding, - **kw, - ) + with util.status( + f"Generating {os.path.abspath(dest)}", **self.messaging_opts + ): + util.template_to_file(src, dest, self.output_encoding, **kw) def _copy_file(self, src: str, dest: str) -> None: - util.status( - "Generating %s" % os.path.abspath(dest), shutil.copy, src, dest - ) + with util.status( + f"Generating {os.path.abspath(dest)}", **self.messaging_opts + ): + shutil.copy(src, dest) def _ensure_directory(self, path: str) -> None: path = os.path.abspath(path) if not os.path.exists(path): - util.status("Creating directory %s" % path, os.makedirs, path) + with util.status( + f"Creating directory {path}", **self.messaging_opts + ): + os.makedirs(path) def _generate_create_date(self) -> datetime.datetime: if self.timezone is not None: - if tz is None: + if ZoneInfo is None: raise util.CommandError( - "The library 'python-dateutil' is required " - "for timezone support" + "Python >= 3.9 is required for timezone support or" + "the 'backports.zoneinfo' package must be installed." ) # First, assume correct capitalization - tzinfo = tz.gettz(self.timezone) + try: + tzinfo = ZoneInfo(self.timezone) + except ZoneInfoNotFoundError: + tzinfo = None if tzinfo is None: - # Fall back to uppercase - tzinfo = tz.gettz(self.timezone.upper()) - if tzinfo is None: - raise util.CommandError( - "Can't locate timezone: %s" % self.timezone - ) + try: + tzinfo = ZoneInfo(self.timezone.upper()) + except ZoneInfoNotFoundError: + raise util.CommandError( + "Can't locate timezone: %s" % self.timezone + ) from None create_date = ( datetime.datetime.utcnow() - .replace(tzinfo=tz.tzutc()) + .replace(tzinfo=datetime.timezone.utc) .astimezone(tzinfo) ) else: @@ -628,10 +638,9 @@ class ScriptDirectory: self, revid: str, message: Optional[str], - head: Optional[str] = None, - refresh: bool = False, + head: Optional[_RevIdType] = None, splice: Optional[bool] = False, - branch_labels: Optional[str] = None, + branch_labels: Optional[_RevIdType] = None, version_path: Optional[str] = None, depends_on: Optional[_RevIdType] = None, **kw: Any, @@ -651,7 +660,6 @@ class ScriptDirectory: :param splice: if True, allow the "head" version to not be an actual head; otherwise, the selected head must be a head (e.g. endpoint) revision. - :param refresh: deprecated. """ if head is None: @@ -674,7 +682,7 @@ class ScriptDirectory: self.revision_map.get_revisions(head), ) for h in heads: - assert h != "base" + assert h != "base" # type: ignore[comparison-overlap] if len(set(heads)) != len(heads): raise util.CommandError("Duplicate head revisions specified") @@ -813,7 +821,7 @@ class Script(revision.Revision): self.path = path super().__init__( rev_id, - module.down_revision, # type: ignore[attr-defined] + module.down_revision, branch_labels=util.to_tuple( getattr(module, "branch_labels", None), default=() ), @@ -846,7 +854,7 @@ class Script(revision.Revision): if doc: if hasattr(self.module, "_alembic_source_encoding"): doc = doc.decode( # type: ignore[attr-defined] - self.module._alembic_source_encoding # type: ignore[attr-defined] # noqa + self.module._alembic_source_encoding ) return doc.strip() # type: ignore[union-attr] else: @@ -888,7 +896,7 @@ class Script(revision.Revision): ) return entry - def __str__(self): + def __str__(self) -> str: return "%s -> %s%s%s%s, %s" % ( self._format_down_revision(), self.revision, diff --git a/libs/alembic/script/revision.py b/libs/alembic/script/revision.py index 39152969f..77a802cdc 100644 --- a/libs/alembic/script/revision.py +++ b/libs/alembic/script/revision.py @@ -14,6 +14,7 @@ from typing import Iterator from typing import List from typing import Optional from typing import overload +from typing import Protocol from typing import Sequence from typing import Set from typing import Tuple @@ -29,18 +30,36 @@ from ..util import not_none if TYPE_CHECKING: from typing import Literal -_RevIdType = Union[str, Sequence[str]] +_RevIdType = Union[str, List[str], Tuple[str, ...]] +_GetRevArg = Union[ + str, + Iterable[Optional[str]], + Iterable[str], +] _RevisionIdentifierType = Union[str, Tuple[str, ...], None] _RevisionOrStr = Union["Revision", str] _RevisionOrBase = Union["Revision", "Literal['base']"] _InterimRevisionMapType = Dict[str, "Revision"] _RevisionMapType = Dict[Union[None, str, Tuple[()]], Optional["Revision"]] -_T = TypeVar("_T", bound=Union[str, "Revision"]) +_T = TypeVar("_T") +_TR = TypeVar("_TR", bound=Optional[_RevisionOrStr]) _relative_destination = re.compile(r"(?:(.+?)@)?(\w+)?((?:\+|-)\d+)") _revision_illegal_chars = ["@", "-", "+"] +class _CollectRevisionsProtocol(Protocol): + def __call__( + self, + upper: _RevisionIdentifierType, + lower: _RevisionIdentifierType, + inclusive: bool, + implicit_base: bool, + assert_relative_length: bool, + ) -> Tuple[Set[Revision], Tuple[Optional[_RevisionOrBase], ...]]: + ... + + class RevisionError(Exception): pass @@ -336,7 +355,6 @@ class RevisionMap: and not parent._is_real_branch_point and not parent.is_merge_point ): - parent.branch_labels.update(revision.branch_labels) if parent.down_revision: parent = map_[parent.down_revision] @@ -391,7 +409,7 @@ class RevisionMap: for rev in self._get_ancestor_nodes( [revision], include_dependencies=False, - map_=cast(_RevisionMapType, map_), + map_=map_, ): if rev is revision: continue @@ -502,7 +520,7 @@ class RevisionMap: return self.filter_for_lineage(self.bases, identifier) def get_revisions( - self, id_: Union[str, Collection[Optional[str]], None] + self, id_: Optional[_GetRevArg] ) -> Tuple[Optional[_RevisionOrBase], ...]: """Return the :class:`.Revision` instances with the given rev id or identifiers. @@ -524,9 +542,7 @@ class RevisionMap: if isinstance(id_, (list, tuple, set, frozenset)): return sum([self.get_revisions(id_elem) for id_elem in id_], ()) else: - resolved_id, branch_label = self._resolve_revision_number( - id_ # type:ignore [arg-type] - ) + resolved_id, branch_label = self._resolve_revision_number(id_) if len(resolved_id) == 1: try: rint = int(resolved_id[0]) @@ -591,7 +607,7 @@ class RevisionMap: def _revision_for_ident( self, - resolved_id: Union[str, Tuple[()]], + resolved_id: Union[str, Tuple[()], None], check_branch: Optional[str] = None, ) -> Optional[Revision]: branch_rev: Optional[Revision] @@ -670,10 +686,10 @@ class RevisionMap: def filter_for_lineage( self, - targets: Iterable[_T], + targets: Iterable[_TR], check_against: Optional[str], include_dependencies: bool = False, - ) -> Tuple[_T, ...]: + ) -> Tuple[_TR, ...]: id_, branch_label = self._resolve_revision_number(check_against) shares = [] @@ -692,7 +708,7 @@ class RevisionMap: def _shares_lineage( self, - target: _RevisionOrStr, + target: Optional[_RevisionOrStr], test_against_revs: Sequence[_RevisionOrStr], include_dependencies: bool = False, ) -> bool: @@ -729,7 +745,7 @@ class RevisionMap: ) def _resolve_revision_number( - self, id_: Optional[str] + self, id_: Optional[_GetRevArg] ) -> Tuple[Tuple[str, ...], Optional[str]]: branch_label: Optional[str] if isinstance(id_, str) and "@" in id_: @@ -788,7 +804,7 @@ class RevisionMap: The iterator yields :class:`.Revision` objects. """ - fn: Callable + fn: _CollectRevisionsProtocol if select_for_downgrade: fn = self._collect_downgrade_revisions else: @@ -813,10 +829,9 @@ class RevisionMap: omit_immediate_dependencies: bool = False, include_dependencies: bool = True, ) -> Iterator[Any]: - if omit_immediate_dependencies: - def fn(rev): + def fn(rev: Revision) -> Iterable[str]: if rev not in targets: return rev._all_nextrev else: @@ -824,12 +839,12 @@ class RevisionMap: elif include_dependencies: - def fn(rev): + def fn(rev: Revision) -> Iterable[str]: return rev._all_nextrev else: - def fn(rev): + def fn(rev: Revision) -> Iterable[str]: return rev.nextrev return self._iterate_related_revisions( @@ -843,15 +858,14 @@ class RevisionMap: check: bool = False, include_dependencies: bool = True, ) -> Iterator[Revision]: - if include_dependencies: - def fn(rev): + def fn(rev: Revision) -> Iterable[str]: return rev._normalized_down_revisions else: - def fn(rev): + def fn(rev: Revision) -> Iterable[str]: return rev._versioned_down_revisions return self._iterate_related_revisions( @@ -860,7 +874,7 @@ class RevisionMap: def _iterate_related_revisions( self, - fn: Callable, + fn: Callable[[Revision], Iterable[str]], targets: Collection[Optional[_RevisionOrBase]], map_: Optional[_RevisionMapType], check: bool = False, @@ -922,7 +936,7 @@ class RevisionMap: id_to_rev = self._revision_map - def get_ancestors(rev_id): + def get_ancestors(rev_id: str) -> Set[str]: return { r.revision for r in self._get_ancestor_nodes([id_to_rev[rev_id]]) @@ -945,7 +959,6 @@ class RevisionMap: current_candidate_idx = 0 while current_heads: - candidate = current_heads[current_candidate_idx] for check_head_index, ancestors in enumerate(ancestors_by_idx): @@ -1041,7 +1054,7 @@ class RevisionMap: children: Sequence[Optional[_RevisionOrBase]] for _ in range(abs(steps)): if steps > 0: - assert initial != "base" + assert initial != "base" # type: ignore[comparison-overlap] # Walk up walk_up = [ is_revision(rev) @@ -1055,7 +1068,7 @@ class RevisionMap: children = walk_up else: # Walk down - if initial == "base": + if initial == "base": # type: ignore[comparison-overlap] children = () else: children = self.get_revisions( @@ -1084,13 +1097,13 @@ class RevisionMap: ) -> Tuple[Optional[str], Optional[_RevisionOrBase]]: """ Parse downgrade command syntax :target to retrieve the target revision - and branch label (if any) given the :current_revisons stamp of the + and branch label (if any) given the :current_revisions stamp of the database. Returns a tuple (branch_label, target_revision) where branch_label is a string from the command specifying the branch to consider (or None if no branch given), and target_revision is a Revision object - which the command refers to. target_revsions is None if the command + which the command refers to. target_revisions is None if the command refers to 'base'. The target may be specified in absolute form, or relative to :current_revisions. """ @@ -1133,7 +1146,7 @@ class RevisionMap: if not symbol_list: # check the case where there are multiple branches # but there is currently a single heads, since all - # other branch heads are dependant of the current + # other branch heads are dependent of the current # single heads. all_current = cast( Set[Revision], self._get_all_current(cr_tuple) @@ -1189,7 +1202,7 @@ class RevisionMap: # No relative destination given, revision specified is absolute. branch_label, _, symbol = target.rpartition("@") if not branch_label: - branch_label = None # type:ignore[assignment] + branch_label = None return branch_label, self.get_revision(symbol) def _parse_upgrade_target( @@ -1200,7 +1213,7 @@ class RevisionMap: ) -> Tuple[Optional[_RevisionOrBase], ...]: """ Parse upgrade command syntax :target to retrieve the target revision - and given the :current_revisons stamp of the database. + and given the :current_revisions stamp of the database. Returns a tuple of Revision objects which should be iterated/upgraded to. The target may be specified in absolute form, or relative to @@ -1215,7 +1228,7 @@ class RevisionMap: # No relative destination, target is absolute. return self.get_revisions(target) - current_revisions_tup: Union[str, Collection[Optional[str]], None] + current_revisions_tup: Union[str, Tuple[Optional[str], ...], None] current_revisions_tup = util.to_tuple(current_revisions) branch_label, symbol, relative_str = match.groups() @@ -1228,7 +1241,8 @@ class RevisionMap: start_revs = current_revisions_tup if branch_label: start_revs = self.filter_for_lineage( - self.get_revisions(current_revisions_tup), branch_label + self.get_revisions(current_revisions_tup), # type: ignore[arg-type] # noqa: E501 + branch_label, ) if not start_revs: # The requested branch is not a head, so we need to @@ -1300,11 +1314,11 @@ class RevisionMap: def _collect_downgrade_revisions( self, upper: _RevisionIdentifierType, - target: _RevisionIdentifierType, + lower: _RevisionIdentifierType, inclusive: bool, implicit_base: bool, assert_relative_length: bool, - ) -> Any: + ) -> Tuple[Set[Revision], Tuple[Optional[_RevisionOrBase], ...]]: """ Compute the set of current revisions specified by :upper, and the downgrade target specified by :target. Return all dependents of target @@ -1315,7 +1329,7 @@ class RevisionMap: branch_label, target_revision = self._parse_downgrade_target( current_revisions=upper, - target=target, + target=lower, assert_relative_length=assert_relative_length, ) if target_revision == "base": @@ -1407,7 +1421,7 @@ class RevisionMap: inclusive: bool, implicit_base: bool, assert_relative_length: bool, - ) -> Tuple[Set[Revision], Tuple[Optional[_RevisionOrBase]]]: + ) -> Tuple[Set[Revision], Tuple[Revision, ...]]: """ Compute the set of required revisions specified by :upper, and the current set of active revisions specified by :lower. Find the @@ -1499,7 +1513,7 @@ class RevisionMap: ) needs.intersection_update(lower_descendents) - return needs, tuple(targets) # type:ignore[return-value] + return needs, tuple(targets) def _get_all_current( self, id_: Tuple[str, ...] @@ -1581,8 +1595,8 @@ class Revision: self.verify_rev_id(revision) self.revision = revision - self.down_revision = tuple_rev_as_scalar(down_revision) - self.dependencies = tuple_rev_as_scalar(dependencies) + self.down_revision = tuple_rev_as_scalar(util.to_tuple(down_revision)) + self.dependencies = tuple_rev_as_scalar(util.to_tuple(dependencies)) self._orig_branch_labels = util.to_tuple(branch_labels, default=()) self.branch_labels = set(self._orig_branch_labels) @@ -1680,20 +1694,20 @@ class Revision: @overload -def tuple_rev_as_scalar( - rev: Optional[Sequence[str]], -) -> Optional[Union[str, Sequence[str]]]: +def tuple_rev_as_scalar(rev: None) -> None: ... @overload def tuple_rev_as_scalar( - rev: Optional[Sequence[Optional[str]]], -) -> Optional[Union[Optional[str], Sequence[Optional[str]]]]: + rev: Union[Tuple[_T, ...], List[_T]] +) -> Union[_T, Tuple[_T, ...], List[_T]]: ... -def tuple_rev_as_scalar(rev): +def tuple_rev_as_scalar( + rev: Optional[Sequence[_T]], +) -> Union[_T, Sequence[_T], None]: if not rev: return None elif len(rev) == 1: diff --git a/libs/alembic/script/write_hooks.py b/libs/alembic/script/write_hooks.py index 8bc7ac1c1..997714792 100644 --- a/libs/alembic/script/write_hooks.py +++ b/libs/alembic/script/write_hooks.py @@ -1,3 +1,6 @@ +# mypy: allow-untyped-defs, allow-incomplete-defs, allow-untyped-calls +# mypy: no-warn-return-any, allow-any-generics + from __future__ import annotations import shlex @@ -7,6 +10,7 @@ from typing import Any from typing import Callable from typing import Dict from typing import List +from typing import Mapping from typing import Optional from typing import Union @@ -24,8 +28,6 @@ def register(name: str) -> Callable: See the documentation linked below for an example. - .. versionadded:: 1.2.0 - .. seealso:: :ref:`post_write_hooks_custom` @@ -41,7 +43,7 @@ def register(name: str) -> Callable: def _invoke( - name: str, revision: str, options: Dict[str, Union[str, int]] + name: str, revision: str, options: Mapping[str, Union[str, int]] ) -> Any: """Invokes the formatter registered for the given name. @@ -55,13 +57,13 @@ def _invoke( hook = _registry[name] except KeyError as ke: raise util.CommandError( - "No formatter with name '%s' registered" % name + f"No formatter with name '{name}' registered" ) from ke else: return hook(revision, options) -def _run_hooks(path: str, hook_config: Dict[str, str]) -> None: +def _run_hooks(path: str, hook_config: Mapping[str, str]) -> None: """Invoke hooks for a generated revision.""" from .base import _split_on_space_comma @@ -81,17 +83,13 @@ def _run_hooks(path: str, hook_config: Dict[str, str]) -> None: type_ = opts["type"] except KeyError as ke: raise util.CommandError( - "Key %s.type is required for post write hook %r" % (name, name) + f"Key {name}.type is required for post write hook {name!r}" ) from ke else: - util.status( - 'Running post write hook "%s"' % name, - _invoke, - type_, - path, - opts, - newline=True, - ) + with util.status( + f"Running post write hook {name!r}", newline=True + ): + _invoke(type_, path, opts) def _parse_cmdline_options(cmdline_options_str: str, path: str) -> List[str]: @@ -119,13 +117,12 @@ def _parse_cmdline_options(cmdline_options_str: str, path: str) -> List[str]: def console_scripts( path: str, options: dict, ignore_output: bool = False ) -> None: - try: entrypoint_name = options["entrypoint"] except KeyError as ke: raise util.CommandError( - "Key %s.entrypoint is required for post write hook %r" - % (options["_hook_name"], options["_hook_name"]) + f"Key {options['_hook_name']}.entrypoint is required for post " + f"write hook {options['_hook_name']!r}" ) from ke for entry in compat.importlib_metadata_get("console_scripts"): if entry.name == entrypoint_name: @@ -147,9 +144,36 @@ def console_scripts( [ sys.executable, "-c", - "import %s; %s.%s()" % (impl.module, impl.module, impl.attr), + f"import {impl.module}; {impl.module}.{impl.attr}()", ] + cmdline_options_list, cwd=cwd, **kw, ) + + +@register("exec") +def exec_(path: str, options: dict, ignore_output: bool = False) -> None: + try: + executable = options["executable"] + except KeyError as ke: + raise util.CommandError( + f"Key {options['_hook_name']}.executable is required for post " + f"write hook {options['_hook_name']!r}" + ) from ke + cwd: Optional[str] = options.get("cwd", None) + cmdline_options_str = options.get("options", "") + cmdline_options_list = _parse_cmdline_options(cmdline_options_str, path) + + kw: Dict[str, Any] = {} + if ignore_output: + kw["stdout"] = kw["stderr"] = subprocess.DEVNULL + + subprocess.run( + [ + executable, + *cmdline_options_list, + ], + cwd=cwd, + **kw, + ) diff --git a/libs/alembic/templates/async/alembic.ini.mako b/libs/alembic/templates/async/alembic.ini.mako index 64c7b6b97..0e5f43fde 100644 --- a/libs/alembic/templates/async/alembic.ini.mako +++ b/libs/alembic/templates/async/alembic.ini.mako @@ -14,9 +14,9 @@ prepend_sys_path = . # timezone to use when rendering the date within the migration file # as well as the filename. -# If specified, requires the python-dateutil library that can be -# installed by adding `alembic[tz]` to the pip requirements -# string value is passed to dateutil.tz.gettz() +# If specified, requires the python>=3.9 or backports.zoneinfo library. +# Any required deps can installed by adding `alembic[tz]` to the pip requirements +# string value is passed to ZoneInfo() # leave blank for localtime # timezone = @@ -72,6 +72,12 @@ sqlalchemy.url = driver://user:pass@localhost/dbname # black.entrypoint = black # black.options = -l 79 REVISION_SCRIPT_FILENAME +# lint with attempts to fix using "ruff" - use the exec runner, execute a binary +# hooks = ruff +# ruff.type = exec +# ruff.executable = %(here)s/.venv/bin/ruff +# ruff.options = --fix REVISION_SCRIPT_FILENAME + # Logging configuration [loggers] keys = root,sqlalchemy,alembic diff --git a/libs/alembic/templates/async/script.py.mako b/libs/alembic/templates/async/script.py.mako index 55df2863d..fbc4b07dc 100644 --- a/libs/alembic/templates/async/script.py.mako +++ b/libs/alembic/templates/async/script.py.mako @@ -5,15 +5,17 @@ Revises: ${down_revision | comma,n} Create Date: ${create_date} """ +from typing import Sequence, Union + from alembic import op import sqlalchemy as sa ${imports if imports else ""} # revision identifiers, used by Alembic. -revision = ${repr(up_revision)} -down_revision = ${repr(down_revision)} -branch_labels = ${repr(branch_labels)} -depends_on = ${repr(depends_on)} +revision: str = ${repr(up_revision)} +down_revision: Union[str, None] = ${repr(down_revision)} +branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} +depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} def upgrade() -> None: diff --git a/libs/alembic/templates/generic/alembic.ini.mako b/libs/alembic/templates/generic/alembic.ini.mako index f541b179a..29245dd3f 100644 --- a/libs/alembic/templates/generic/alembic.ini.mako +++ b/libs/alembic/templates/generic/alembic.ini.mako @@ -16,9 +16,9 @@ prepend_sys_path = . # timezone to use when rendering the date within the migration file # as well as the filename. -# If specified, requires the python-dateutil library that can be -# installed by adding `alembic[tz]` to the pip requirements -# string value is passed to dateutil.tz.gettz() +# If specified, requires the python>=3.9 or backports.zoneinfo library. +# Any required deps can installed by adding `alembic[tz]` to the pip requirements +# string value is passed to ZoneInfo() # leave blank for localtime # timezone = @@ -74,6 +74,12 @@ sqlalchemy.url = driver://user:pass@localhost/dbname # black.entrypoint = black # black.options = -l 79 REVISION_SCRIPT_FILENAME +# lint with attempts to fix using "ruff" - use the exec runner, execute a binary +# hooks = ruff +# ruff.type = exec +# ruff.executable = %(here)s/.venv/bin/ruff +# ruff.options = --fix REVISION_SCRIPT_FILENAME + # Logging configuration [loggers] keys = root,sqlalchemy,alembic diff --git a/libs/alembic/templates/generic/script.py.mako b/libs/alembic/templates/generic/script.py.mako index 55df2863d..fbc4b07dc 100644 --- a/libs/alembic/templates/generic/script.py.mako +++ b/libs/alembic/templates/generic/script.py.mako @@ -5,15 +5,17 @@ Revises: ${down_revision | comma,n} Create Date: ${create_date} """ +from typing import Sequence, Union + from alembic import op import sqlalchemy as sa ${imports if imports else ""} # revision identifiers, used by Alembic. -revision = ${repr(up_revision)} -down_revision = ${repr(down_revision)} -branch_labels = ${repr(branch_labels)} -depends_on = ${repr(depends_on)} +revision: str = ${repr(up_revision)} +down_revision: Union[str, None] = ${repr(down_revision)} +branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} +depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} def upgrade() -> None: diff --git a/libs/alembic/templates/multidb/alembic.ini.mako b/libs/alembic/templates/multidb/alembic.ini.mako index 4230fe135..c7fbe4822 100644 --- a/libs/alembic/templates/multidb/alembic.ini.mako +++ b/libs/alembic/templates/multidb/alembic.ini.mako @@ -16,9 +16,9 @@ prepend_sys_path = . # timezone to use when rendering the date within the migration file # as well as the filename. -# If specified, requires the python-dateutil library that can be -# installed by adding `alembic[tz]` to the pip requirements -# string value is passed to dateutil.tz.gettz() +# If specified, requires the python>=3.9 or backports.zoneinfo library. +# Any required deps can installed by adding `alembic[tz]` to the pip requirements +# string value is passed to ZoneInfo() # leave blank for localtime # timezone = @@ -79,6 +79,12 @@ sqlalchemy.url = driver://user:pass@localhost/dbname2 # black.entrypoint = black # black.options = -l 79 REVISION_SCRIPT_FILENAME +# lint with attempts to fix using "ruff" - use the exec runner, execute a binary +# hooks = ruff +# ruff.type = exec +# ruff.executable = %(here)s/.venv/bin/ruff +# ruff.options = --fix REVISION_SCRIPT_FILENAME + # Logging configuration [loggers] keys = root,sqlalchemy,alembic diff --git a/libs/alembic/templates/multidb/script.py.mako b/libs/alembic/templates/multidb/script.py.mako index 946ab6a97..6108b8a0d 100644 --- a/libs/alembic/templates/multidb/script.py.mako +++ b/libs/alembic/templates/multidb/script.py.mako @@ -8,15 +8,17 @@ Revises: ${down_revision | comma,n} Create Date: ${create_date} """ +from typing import Sequence, Union + from alembic import op import sqlalchemy as sa ${imports if imports else ""} # revision identifiers, used by Alembic. -revision = ${repr(up_revision)} -down_revision = ${repr(down_revision)} -branch_labels = ${repr(branch_labels)} -depends_on = ${repr(depends_on)} +revision: str = ${repr(up_revision)} +down_revision: Union[str, None] = ${repr(down_revision)} +branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} +depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} def upgrade(engine_name: str) -> None: diff --git a/libs/alembic/testing/assertions.py b/libs/alembic/testing/assertions.py index 1c24066b8..ec9593b71 100644 --- a/libs/alembic/testing/assertions.py +++ b/libs/alembic/testing/assertions.py @@ -64,7 +64,6 @@ def assert_raises_message_context_ok( def _assert_raises( except_cls, callable_, args, kwargs, msg=None, check_context=False ): - with _expect_raises(except_cls, msg, check_context) as ec: callable_(*args, **kwargs) return ec.error @@ -104,7 +103,6 @@ def expect_raises_message(except_cls, msg, check_context=True): def eq_ignore_whitespace(a, b, msg=None): - a = re.sub(r"^\s+?|\n", "", a) a = re.sub(r" {2,}", " ", a) b = re.sub(r"^\s+?|\n", "", b) @@ -120,7 +118,6 @@ def _get_dialect(name): if name is None or name == "default": return default.DefaultDialect() else: - d = sqla_compat._create_url(name).get_dialect()() if name == "postgresql": diff --git a/libs/alembic/testing/env.py b/libs/alembic/testing/env.py index 79a4980f4..5df7ef822 100644 --- a/libs/alembic/testing/env.py +++ b/libs/alembic/testing/env.py @@ -22,10 +22,8 @@ def _get_staging_directory(): def staging_env(create=True, template="generic", sourceless=False): - cfg = _testing_config() if create: - path = os.path.join(_get_staging_directory(), "scripts") assert not os.path.exists(path), ( "staging directory %s already exists; poor cleanup?" % path @@ -284,7 +282,6 @@ def write_script( def make_sourceless(path, style): - import py_compile py_compile.compile(path) diff --git a/libs/alembic/testing/fixtures.py b/libs/alembic/testing/fixtures.py index ef1c3bbaf..4b83a745f 100644 --- a/libs/alembic/testing/fixtures.py +++ b/libs/alembic/testing/fixtures.py @@ -28,7 +28,7 @@ from ..operations import Operations from ..util import sqla_compat from ..util.sqla_compat import create_mock_engine from ..util.sqla_compat import sqla_14 -from ..util.sqla_compat import sqla_1x +from ..util.sqla_compat import sqla_2 testing_config = configparser.ConfigParser() @@ -36,10 +36,7 @@ testing_config.read(["test.cfg"]) class TestBase(SQLAlchemyTestBase): - if sqla_1x: - is_sqlalchemy_future = False - else: - is_sqlalchemy_future = True + is_sqlalchemy_future = sqla_2 @testing.fixture() def ops_context(self, migration_context): @@ -137,7 +134,6 @@ def op_fixture( literal_binds=False, native_boolean=None, ): - opts = {} if naming_convention: opts["target_metadata"] = MetaData(naming_convention=naming_convention) @@ -220,7 +216,6 @@ def op_fixture( class AlterColRoundTripFixture: - # since these tests are about syntax, use more recent SQLAlchemy as some of # the type / server default compare logic might not work on older # SQLAlchemy versions as seems to be the case for SQLAlchemy 1.1 on Oracle diff --git a/libs/alembic/testing/requirements.py b/libs/alembic/testing/requirements.py index a4a6045e8..6e07e28ea 100644 --- a/libs/alembic/testing/requirements.py +++ b/libs/alembic/testing/requirements.py @@ -84,7 +84,7 @@ class SuiteRequirements(Requirements): @property def sqlalchemy_1x(self): return exclusions.skip_if( - lambda config: not util.sqla_1x, + lambda config: util.sqla_2, "SQLAlchemy 1.x test", ) @@ -95,6 +95,18 @@ class SuiteRequirements(Requirements): "SQLAlchemy 2.x test", ) + @property + def asyncio(self): + def go(config): + try: + import greenlet # noqa: F401 + except ImportError: + return False + else: + return True + + return self.sqlalchemy_14 + exclusions.only_if(go) + @property def comments(self): return exclusions.only_if( @@ -196,7 +208,3 @@ class SuiteRequirements(Requirements): return exclusions.only_if( exclusions.BooleanPredicate(sqla_compat.has_identity) ) - - @property - def supports_identity_on_null(self): - return exclusions.closed() diff --git a/libs/alembic/testing/schemacompare.py b/libs/alembic/testing/schemacompare.py index c06349957..204cc4ddc 100644 --- a/libs/alembic/testing/schemacompare.py +++ b/libs/alembic/testing/schemacompare.py @@ -1,6 +1,7 @@ from itertools import zip_longest from sqlalchemy import schema +from sqlalchemy.sql.elements import ClauseList class CompareTable: @@ -60,6 +61,14 @@ class CompareIndex: def __ne__(self, other): return not self.__eq__(other) + def __repr__(self): + expr = ClauseList(*self.index.expressions) + try: + expr_str = expr.compile().string + except Exception: + expr_str = str(expr) + return f"" + class CompareCheckConstraint: def __init__(self, constraint): diff --git a/libs/alembic/testing/suite/_autogen_fixtures.py b/libs/alembic/testing/suite/_autogen_fixtures.py index e09fbfe58..d838ebef1 100644 --- a/libs/alembic/testing/suite/_autogen_fixtures.py +++ b/libs/alembic/testing/suite/_autogen_fixtures.py @@ -279,7 +279,6 @@ class AutogenFixtureTest(_ComparesFKs): return_ops=False, max_identifier_length=None, ): - if max_identifier_length: dialect = self.bind.dialect existing_length = dialect.max_identifier_length diff --git a/libs/alembic/testing/suite/test_autogen_identity.py b/libs/alembic/testing/suite/test_autogen_identity.py index 9aedf9e9f..3dee9fc99 100644 --- a/libs/alembic/testing/suite/test_autogen_identity.py +++ b/libs/alembic/testing/suite/test_autogen_identity.py @@ -4,6 +4,7 @@ from sqlalchemy import Integer from sqlalchemy import MetaData from sqlalchemy import Table +from alembic.util import sqla_compat from ._autogen_fixtures import AutogenFixtureTest from ... import testing from ...testing import config @@ -78,16 +79,33 @@ class AutogenerateIdentityTest(AutogenFixtureTest, TestBase): m2 = MetaData() for m in (m1, m2): - Table( - "user", - m, - Column("id", Integer, sa.Identity(start=2)), - ) + id_ = sa.Identity(start=2) + Table("user", m, Column("id", Integer, id_)) diffs = self._fixture(m1, m2) eq_(diffs, []) + def test_dialect_kwargs_changes(self): + m1 = MetaData() + m2 = MetaData() + + if sqla_compat.identity_has_dialect_kwargs: + args = {"oracle_on_null": True, "oracle_order": True} + else: + args = {"on_null": True, "order": True} + + Table("user", m1, Column("id", Integer, sa.Identity(start=2))) + id_ = sa.Identity(start=2, **args) + Table("user", m2, Column("id", Integer, id_)) + + diffs = self._fixture(m1, m2) + if config.db.name == "oracle": + is_true(len(diffs), 1) + eq_(diffs[0][0][0], "modify_default") + else: + eq_(diffs, []) + @testing.combinations( (None, dict(start=2)), (dict(start=2), None), @@ -206,36 +224,3 @@ class AutogenerateIdentityTest(AutogenFixtureTest, TestBase): removed = diffs[5] is_true(isinstance(removed, sa.Identity)) - - def test_identity_on_null(self): - m1 = MetaData() - m2 = MetaData() - - Table( - "user", - m1, - Column("id", Integer, sa.Identity(start=2, on_null=True)), - Column("other", sa.Text), - ) - - Table( - "user", - m2, - Column("id", Integer, sa.Identity(start=2, on_null=False)), - Column("other", sa.Text), - ) - - diffs = self._fixture(m1, m2) - if not config.requirements.supports_identity_on_null.enabled: - eq_(diffs, []) - else: - eq_(len(diffs[0]), 1) - diffs = diffs[0][0] - eq_(diffs[0], "modify_default") - eq_(diffs[2], "user") - eq_(diffs[3], "id") - old = diffs[5] - new = diffs[6] - - is_true(isinstance(old, sa.Identity)) - is_true(isinstance(new, sa.Identity)) diff --git a/libs/alembic/testing/util.py b/libs/alembic/testing/util.py index e65597d9a..4517a69f6 100644 --- a/libs/alembic/testing/util.py +++ b/libs/alembic/testing/util.py @@ -6,12 +6,13 @@ # the MIT License: http://www.opensource.org/licenses/mit-license.php from __future__ import annotations -import re import types from typing import Union from sqlalchemy.util import inspect_getfullargspec +from ..util import sqla_2 + def flag_combinations(*combinations): """A facade around @testing.combinations() oriented towards boolean @@ -114,17 +115,11 @@ def _safe_int(value: str) -> Union[int, str]: def testing_engine(url=None, options=None, future=False): from sqlalchemy.testing import config from sqlalchemy.testing.engines import testing_engine - from sqlalchemy import __version__ - - _vers = tuple( - [_safe_int(x) for x in re.findall(r"(\d+|[abc]\d)", __version__)] - ) - sqla_1x = _vers < (2,) if not future: future = getattr(config._current.options, "future_engine", False) - if sqla_1x: + if not sqla_2: kw = {"future": future} if future else {} else: kw = {} diff --git a/libs/alembic/util/__init__.py b/libs/alembic/util/__init__.py index c81d4317a..4724e1f08 100644 --- a/libs/alembic/util/__init__.py +++ b/libs/alembic/util/__init__.py @@ -1,34 +1,34 @@ -from .editor import open_in_editor -from .exc import AutogenerateDiffsDetected -from .exc import CommandError -from .langhelpers import _with_legacy_names -from .langhelpers import asbool -from .langhelpers import dedupe_tuple -from .langhelpers import Dispatcher -from .langhelpers import immutabledict -from .langhelpers import memoized_property -from .langhelpers import ModuleClsProxy -from .langhelpers import not_none -from .langhelpers import rev_id -from .langhelpers import to_list -from .langhelpers import to_tuple -from .langhelpers import unique_list -from .messaging import err -from .messaging import format_as_comma -from .messaging import msg -from .messaging import obfuscate_url_pw -from .messaging import status -from .messaging import warn -from .messaging import write_outstream -from .pyfiles import coerce_resource_to_filename -from .pyfiles import load_python_file -from .pyfiles import pyc_file_from_path -from .pyfiles import template_to_file -from .sqla_compat import has_computed -from .sqla_compat import sqla_13 -from .sqla_compat import sqla_14 -from .sqla_compat import sqla_1x -from .sqla_compat import sqla_2 +from .editor import open_in_editor as open_in_editor +from .exc import AutogenerateDiffsDetected as AutogenerateDiffsDetected +from .exc import CommandError as CommandError +from .langhelpers import _with_legacy_names as _with_legacy_names +from .langhelpers import asbool as asbool +from .langhelpers import dedupe_tuple as dedupe_tuple +from .langhelpers import Dispatcher as Dispatcher +from .langhelpers import EMPTY_DICT as EMPTY_DICT +from .langhelpers import immutabledict as immutabledict +from .langhelpers import memoized_property as memoized_property +from .langhelpers import ModuleClsProxy as ModuleClsProxy +from .langhelpers import not_none as not_none +from .langhelpers import rev_id as rev_id +from .langhelpers import to_list as to_list +from .langhelpers import to_tuple as to_tuple +from .langhelpers import unique_list as unique_list +from .messaging import err as err +from .messaging import format_as_comma as format_as_comma +from .messaging import msg as msg +from .messaging import obfuscate_url_pw as obfuscate_url_pw +from .messaging import status as status +from .messaging import warn as warn +from .messaging import write_outstream as write_outstream +from .pyfiles import coerce_resource_to_filename as coerce_resource_to_filename +from .pyfiles import load_python_file as load_python_file +from .pyfiles import pyc_file_from_path as pyc_file_from_path +from .pyfiles import template_to_file as template_to_file +from .sqla_compat import has_computed as has_computed +from .sqla_compat import sqla_13 as sqla_13 +from .sqla_compat import sqla_14 as sqla_14 +from .sqla_compat import sqla_2 as sqla_2 if not sqla_13: diff --git a/libs/alembic/util/compat.py b/libs/alembic/util/compat.py index 2fe49573d..e185cc417 100644 --- a/libs/alembic/util/compat.py +++ b/libs/alembic/util/compat.py @@ -1,18 +1,32 @@ +# mypy: no-warn-unused-ignores + from __future__ import annotations +from configparser import ConfigParser import io import os import sys +import typing +from typing import Any +from typing import List +from typing import Optional from typing import Sequence +from typing import Union -from sqlalchemy.util import inspect_getfullargspec # noqa -from sqlalchemy.util.compat import inspect_formatargspec # noqa +if True: + # zimports hack for too-long names + from sqlalchemy.util import ( # noqa: F401 + inspect_getfullargspec as inspect_getfullargspec, + ) + from sqlalchemy.util.compat import ( # noqa: F401 + inspect_formatargspec as inspect_formatargspec, + ) is_posix = os.name == "posix" py311 = sys.version_info >= (3, 11) +py310 = sys.version_info >= (3, 10) py39 = sys.version_info >= (3, 9) -py38 = sys.version_info >= (3, 8) # produce a wrapper that allows encoded text to stream @@ -24,9 +38,13 @@ class EncodedIO(io.TextIOWrapper): if py39: - from importlib import resources as importlib_resources - from importlib import metadata as importlib_metadata - from importlib.metadata import EntryPoint + from importlib import resources as _resources + + importlib_resources = _resources + from importlib import metadata as _metadata + + importlib_metadata = _metadata + from importlib.metadata import EntryPoint as EntryPoint else: import importlib_resources # type:ignore # noqa import importlib_metadata # type:ignore # noqa @@ -36,19 +54,36 @@ else: def importlib_metadata_get(group: str) -> Sequence[EntryPoint]: ep = importlib_metadata.entry_points() if hasattr(ep, "select"): - return ep.select(group=group) # type: ignore + return ep.select(group=group) else: return ep.get(group, ()) # type: ignore -def formatannotation_fwdref(annotation, base_module=None): - """the python 3.7 _formatannotation with an extra repr() for 3rd party - modules""" +def formatannotation_fwdref( + annotation: Any, base_module: Optional[Any] = None +) -> str: + """vendored from python 3.7""" + # copied over _formatannotation from sqlalchemy 2.0 + + if isinstance(annotation, str): + return annotation if getattr(annotation, "__module__", None) == "typing": - return repr(annotation).replace("typing.", "") + return repr(annotation).replace("typing.", "").replace("~", "") if isinstance(annotation, type): if annotation.__module__ in ("builtins", base_module): - return annotation.__qualname__ - return repr(annotation.__module__ + "." + annotation.__qualname__) - return repr(annotation) + return repr(annotation.__qualname__) + return annotation.__module__ + "." + annotation.__qualname__ + elif isinstance(annotation, typing.TypeVar): + return repr(annotation).replace("~", "") + return repr(annotation).replace("~", "") + + +def read_config_parser( + file_config: ConfigParser, + file_argument: Sequence[Union[str, os.PathLike[str]]], +) -> List[str]: + if py310: + return file_config.read(file_argument, encoding="locale") + else: + return file_config.read(file_argument) diff --git a/libs/alembic/util/langhelpers.py b/libs/alembic/util/langhelpers.py index 8203358e9..4a5bf09a9 100644 --- a/libs/alembic/util/langhelpers.py +++ b/libs/alembic/util/langhelpers.py @@ -5,31 +5,46 @@ from collections.abc import Iterable import textwrap from typing import Any from typing import Callable +from typing import cast from typing import Dict from typing import List +from typing import Mapping +from typing import MutableMapping +from typing import NoReturn from typing import Optional from typing import overload from typing import Sequence +from typing import Set from typing import Tuple +from typing import Type +from typing import TYPE_CHECKING from typing import TypeVar from typing import Union import uuid import warnings -from sqlalchemy.util import asbool # noqa -from sqlalchemy.util import immutabledict # noqa -from sqlalchemy.util import memoized_property # noqa -from sqlalchemy.util import to_list # noqa -from sqlalchemy.util import unique_list # noqa +from sqlalchemy.util import asbool as asbool # noqa: F401 +from sqlalchemy.util import immutabledict as immutabledict # noqa: F401 +from sqlalchemy.util import to_list as to_list # noqa: F401 +from sqlalchemy.util import unique_list as unique_list from .compat import inspect_getfullargspec +if True: + # zimports workaround :( + from sqlalchemy.util import ( # noqa: F401 + memoized_property as memoized_property, + ) -_T = TypeVar("_T") + +EMPTY_DICT: Mapping[Any, Any] = immutabledict() +_T = TypeVar("_T", bound=Any) + +_C = TypeVar("_C", bound=Callable[..., Any]) class _ModuleClsMeta(type): - def __setattr__(cls, key: str, value: Callable) -> None: + def __setattr__(cls, key: str, value: Callable[..., Any]) -> None: super().__setattr__(key, value) cls._update_module_proxies(key) # type: ignore @@ -43,9 +58,13 @@ class ModuleClsProxy(metaclass=_ModuleClsMeta): """ - _setups: Dict[type, Tuple[set, list]] = collections.defaultdict( - lambda: (set(), []) - ) + _setups: Dict[ + Type[Any], + Tuple[ + Set[str], + List[Tuple[MutableMapping[str, Any], MutableMapping[str, Any]]], + ], + ] = collections.defaultdict(lambda: (set(), [])) @classmethod def _update_module_proxies(cls, name: str) -> None: @@ -68,18 +87,33 @@ class ModuleClsProxy(metaclass=_ModuleClsMeta): del globals_[attr_name] @classmethod - def create_module_class_proxy(cls, globals_, locals_): + def create_module_class_proxy( + cls, + globals_: MutableMapping[str, Any], + locals_: MutableMapping[str, Any], + ) -> None: attr_names, modules = cls._setups[cls] modules.append((globals_, locals_)) cls._setup_proxy(globals_, locals_, attr_names) @classmethod - def _setup_proxy(cls, globals_, locals_, attr_names): + def _setup_proxy( + cls, + globals_: MutableMapping[str, Any], + locals_: MutableMapping[str, Any], + attr_names: Set[str], + ) -> None: for methname in dir(cls): cls._add_proxied_attribute(methname, globals_, locals_, attr_names) @classmethod - def _add_proxied_attribute(cls, methname, globals_, locals_, attr_names): + def _add_proxied_attribute( + cls, + methname: str, + globals_: MutableMapping[str, Any], + locals_: MutableMapping[str, Any], + attr_names: Set[str], + ) -> None: if not methname.startswith("_"): meth = getattr(cls, methname) if callable(meth): @@ -90,10 +124,15 @@ class ModuleClsProxy(metaclass=_ModuleClsMeta): attr_names.add(methname) @classmethod - def _create_method_proxy(cls, name, globals_, locals_): + def _create_method_proxy( + cls, + name: str, + globals_: MutableMapping[str, Any], + locals_: MutableMapping[str, Any], + ) -> Callable[..., Any]: fn = getattr(cls, name) - def _name_error(name, from_): + def _name_error(name: str, from_: Exception) -> NoReturn: raise NameError( "Can't invoke function '%s', as the proxy object has " "not yet been " @@ -117,7 +156,9 @@ class ModuleClsProxy(metaclass=_ModuleClsMeta): translations, ) - def translate(fn_name, spec, translations, args, kw): + def translate( + fn_name: str, spec: Any, translations: Any, args: Any, kw: Any + ) -> Any: return_kw = {} return_args = [] @@ -174,15 +215,15 @@ class ModuleClsProxy(metaclass=_ModuleClsMeta): "doc": fn.__doc__, } ) - lcl = {} + lcl: MutableMapping[str, Any] = {} - exec(func_text, globals_, lcl) - return lcl[name] + exec(func_text, cast("Dict[str, Any]", globals_), lcl) + return cast("Callable[..., Any]", lcl[name]) -def _with_legacy_names(translations): - def decorate(fn): - fn._legacy_translations = translations +def _with_legacy_names(translations: Any) -> Any: + def decorate(fn: _C) -> _C: + fn._legacy_translations = translations # type: ignore[attr-defined] return fn return decorate @@ -193,21 +234,25 @@ def rev_id() -> str: @overload -def to_tuple(x: Any, default: tuple) -> tuple: +def to_tuple(x: Any, default: Tuple[Any, ...]) -> Tuple[Any, ...]: ... @overload -def to_tuple(x: None, default: Optional[_T] = None) -> _T: +def to_tuple(x: None, default: Optional[_T] = ...) -> _T: ... @overload -def to_tuple(x: Any, default: Optional[tuple] = None) -> tuple: +def to_tuple( + x: Any, default: Optional[Tuple[Any, ...]] = None +) -> Tuple[Any, ...]: ... -def to_tuple(x, default=None): +def to_tuple( + x: Any, default: Optional[Tuple[Any, ...]] = None +) -> Optional[Tuple[Any, ...]]: if x is None: return default elif isinstance(x, str): @@ -224,13 +269,13 @@ def dedupe_tuple(tup: Tuple[str, ...]) -> Tuple[str, ...]: class Dispatcher: def __init__(self, uselist: bool = False) -> None: - self._registry: Dict[tuple, Any] = {} + self._registry: Dict[Tuple[Any, ...], Any] = {} self.uselist = uselist def dispatch_for( self, target: Any, qualifier: str = "default" - ) -> Callable: - def decorate(fn): + ) -> Callable[[_C], _C]: + def decorate(fn: _C) -> _C: if self.uselist: self._registry.setdefault((target, qualifier), []).append(fn) else: @@ -241,9 +286,8 @@ class Dispatcher: return decorate def dispatch(self, obj: Any, qualifier: str = "default") -> Any: - if isinstance(obj, str): - targets: Sequence = [obj] + targets: Sequence[Any] = [obj] elif isinstance(obj, type): targets = obj.__mro__ else: @@ -258,11 +302,13 @@ class Dispatcher: raise ValueError("no dispatch function for object: %s" % obj) def _fn_or_list( - self, fn_or_list: Union[List[Callable], Callable] - ) -> Callable: + self, fn_or_list: Union[List[Callable[..., Any]], Callable[..., Any]] + ) -> Callable[..., Any]: if self.uselist: - def go(*arg, **kw): + def go(*arg: Any, **kw: Any) -> None: + if TYPE_CHECKING: + assert isinstance(fn_or_list, Sequence) for fn in fn_or_list: fn(*arg, **kw) diff --git a/libs/alembic/util/messaging.py b/libs/alembic/util/messaging.py index 7d9d090a7..5f14d5975 100644 --- a/libs/alembic/util/messaging.py +++ b/libs/alembic/util/messaging.py @@ -1,11 +1,11 @@ from __future__ import annotations from collections.abc import Iterable +from contextlib import contextmanager import logging import sys import textwrap -from typing import Any -from typing import Callable +from typing import Iterator from typing import Optional from typing import TextIO from typing import Union @@ -34,7 +34,11 @@ except (ImportError, OSError): TERMWIDTH = None -def write_outstream(stream: TextIO, *text) -> None: +def write_outstream( + stream: TextIO, *text: Union[str, bytes], quiet: bool = False +) -> None: + if quiet: + return encoding = getattr(stream, "encoding", "ascii") or "ascii" for t in text: if not isinstance(t, bytes): @@ -49,34 +53,42 @@ def write_outstream(stream: TextIO, *text) -> None: break -def status(_statmsg: str, fn: Callable, *arg, **kw) -> Any: - newline = kw.pop("newline", False) - msg(_statmsg + " ...", newline, True) +@contextmanager +def status( + status_msg: str, newline: bool = False, quiet: bool = False +) -> Iterator[None]: + msg(status_msg + " ...", newline, flush=True, quiet=quiet) try: - ret = fn(*arg, **kw) - write_outstream(sys.stdout, " done\n") - return ret + yield except: - write_outstream(sys.stdout, " FAILED\n") + if not quiet: + write_outstream(sys.stdout, " FAILED\n") raise + else: + if not quiet: + write_outstream(sys.stdout, " done\n") -def err(message: str): +def err(message: str, quiet: bool = False) -> None: log.error(message) - msg("FAILED: %s" % message) + msg(f"FAILED: {message}", quiet=quiet) sys.exit(-1) def obfuscate_url_pw(input_url: str) -> str: u = url.make_url(input_url) - return sqla_compat.url_render_as_string(u, hide_password=True) + return sqla_compat.url_render_as_string(u, hide_password=True) # type: ignore # noqa: E501 def warn(msg: str, stacklevel: int = 2) -> None: warnings.warn(msg, UserWarning, stacklevel=stacklevel) -def msg(msg: str, newline: bool = True, flush: bool = False) -> None: +def msg( + msg: str, newline: bool = True, flush: bool = False, quiet: bool = False +) -> None: + if quiet: + return if TERMWIDTH is None: write_outstream(sys.stdout, msg) if newline: diff --git a/libs/alembic/util/pyfiles.py b/libs/alembic/util/pyfiles.py index 753500476..973bd458e 100644 --- a/libs/alembic/util/pyfiles.py +++ b/libs/alembic/util/pyfiles.py @@ -8,6 +8,8 @@ import importlib.util import os import re import tempfile +from types import ModuleType +from typing import Any from typing import Optional from mako import exceptions @@ -18,7 +20,7 @@ from .exc import CommandError def template_to_file( - template_file: str, dest: str, output_encoding: str, **kw + template_file: str, dest: str, output_encoding: str, **kw: Any ) -> None: template = Template(filename=template_file) try: @@ -49,7 +51,6 @@ def coerce_resource_to_filename(fname: str) -> str: """ if not os.path.isabs(fname) and ":" in fname: - tokens = fname.split(":") # from https://importlib-resources.readthedocs.io/en/latest/migration.html#pkg-resources-resource-filename # noqa E501 @@ -83,7 +84,7 @@ def pyc_file_from_path(path: str) -> Optional[str]: return None -def load_python_file(dir_: str, filename: str): +def load_python_file(dir_: str, filename: str) -> ModuleType: """Load a file from the given path as a Python module.""" module_id = re.sub(r"\W", "_", filename) @@ -100,10 +101,12 @@ def load_python_file(dir_: str, filename: str): module = load_module_py(module_id, pyc_path) elif ext in (".pyc", ".pyo"): module = load_module_py(module_id, path) + else: + assert False return module -def load_module_py(module_id: str, path: str): +def load_module_py(module_id: str, path: str) -> ModuleType: spec = importlib.util.spec_from_file_location(module_id, path) assert spec module = importlib.util.module_from_spec(spec) diff --git a/libs/alembic/util/sqla_compat.py b/libs/alembic/util/sqla_compat.py index cab99494b..8489c19fa 100644 --- a/libs/alembic/util/sqla_compat.py +++ b/libs/alembic/util/sqla_compat.py @@ -1,12 +1,20 @@ +# mypy: allow-untyped-defs, allow-incomplete-defs, allow-untyped-calls +# mypy: no-warn-return-any, allow-any-generics + from __future__ import annotations import contextlib import re from typing import Any +from typing import Callable +from typing import Dict from typing import Iterable from typing import Iterator from typing import Mapping from typing import Optional +from typing import Protocol +from typing import Set +from typing import Type from typing import TYPE_CHECKING from typing import TypeVar from typing import Union @@ -17,11 +25,11 @@ from sqlalchemy import schema from sqlalchemy import sql from sqlalchemy import types as sqltypes from sqlalchemy.engine import url -from sqlalchemy.ext.compiler import compiles from sqlalchemy.schema import CheckConstraint from sqlalchemy.schema import Column from sqlalchemy.schema import ForeignKeyConstraint from sqlalchemy.sql import visitors +from sqlalchemy.sql.base import DialectKWArgs from sqlalchemy.sql.elements import BindParameter from sqlalchemy.sql.elements import ColumnClause from sqlalchemy.sql.elements import quoted_name @@ -31,6 +39,7 @@ from sqlalchemy.sql.visitors import traverse from typing_extensions import TypeGuard if TYPE_CHECKING: + from sqlalchemy import ClauseElement from sqlalchemy import Index from sqlalchemy import Table from sqlalchemy.engine import Connection @@ -46,7 +55,12 @@ if TYPE_CHECKING: from sqlalchemy.sql.selectable import Select from sqlalchemy.sql.selectable import TableClause -_CE = TypeVar("_CE", bound=Union["ColumnElement", "SchemaItem"]) +_CE = TypeVar("_CE", bound=Union["ColumnElement[Any]", "SchemaItem"]) + + +class _CompilerProtocol(Protocol): + def __call__(self, element: Any, compiler: Any, **kw: Any) -> str: + ... def _safe_int(value: str) -> Union[int, str]: @@ -61,28 +75,40 @@ _vers = tuple( ) sqla_13 = _vers >= (1, 3) sqla_14 = _vers >= (1, 4) +# https://docs.sqlalchemy.org/en/latest/changelog/changelog_14.html#change-0c6e0cc67dfe6fac5164720e57ef307d +sqla_14_18 = _vers >= (1, 4, 18) sqla_14_26 = _vers >= (1, 4, 26) sqla_2 = _vers >= (2,) sqlalchemy_version = __version__ try: - from sqlalchemy.sql.naming import _NONE_NAME as _NONE_NAME + from sqlalchemy.sql.naming import _NONE_NAME as _NONE_NAME # type: ignore[attr-defined] # noqa: E501 except ImportError: from sqlalchemy.sql.elements import _NONE_NAME as _NONE_NAME # type: ignore # noqa: E501 -if sqla_14: - # when future engine merges, this can be again based on version string - from sqlalchemy.engine import Connection as legacy_connection +class _Unsupported: + "Placeholder for unsupported SQLAlchemy classes" + + +if TYPE_CHECKING: + + def compiles( + element: Type[ClauseElement], *dialects: str + ) -> Callable[[_CompilerProtocol], _CompilerProtocol]: + ... - sqla_1x = not hasattr(legacy_connection, "commit") else: - sqla_1x = True + from sqlalchemy.ext.compiler import compiles try: - from sqlalchemy import Computed # noqa + from sqlalchemy import Computed as Computed except ImportError: - Computed = type(None) # type: ignore + if not TYPE_CHECKING: + + class Computed(_Unsupported): + pass + has_computed = False has_computed_reflection = False else: @@ -90,25 +116,56 @@ else: has_computed_reflection = _vers >= (1, 3, 16) try: - from sqlalchemy import Identity # noqa + from sqlalchemy import Identity as Identity except ImportError: - Identity = type(None) # type: ignore + if not TYPE_CHECKING: + + class Identity(_Unsupported): + pass + has_identity = False else: - # attributes common to Indentity and Sequence - _identity_options_attrs = ( - "start", - "increment", - "minvalue", - "maxvalue", - "nominvalue", - "nomaxvalue", - "cycle", - "cache", - "order", - ) - # attributes of Indentity - _identity_attrs = _identity_options_attrs + ("on_null",) + identity_has_dialect_kwargs = issubclass(Identity, DialectKWArgs) + + def _get_identity_options_dict( + identity: Union[Identity, schema.Sequence, None], + dialect_kwargs: bool = False, + ) -> Dict[str, Any]: + if identity is None: + return {} + elif identity_has_dialect_kwargs: + as_dict = identity._as_dict() # type: ignore + if dialect_kwargs: + assert isinstance(identity, DialectKWArgs) + as_dict.update(identity.dialect_kwargs) + else: + as_dict = {} + if isinstance(identity, Identity): + # always=None means something different than always=False + as_dict["always"] = identity.always + if identity.on_null is not None: + as_dict["on_null"] = identity.on_null + # attributes common to Identity and Sequence + attrs = ( + "start", + "increment", + "minvalue", + "maxvalue", + "nominvalue", + "nomaxvalue", + "cycle", + "cache", + "order", + ) + as_dict.update( + { + key: getattr(identity, key, None) + for key in attrs + if getattr(identity, key, None) is not None + } + ) + return as_dict + has_identity = True if sqla_2: @@ -215,7 +272,7 @@ def _idx_table_bound_expressions(idx: Index) -> Iterable[ColumnElement[Any]]: def _copy(schema_item: _CE, **kw) -> _CE: if hasattr(schema_item, "_copy"): - return schema_item._copy(**kw) # type: ignore[union-attr] + return schema_item._copy(**kw) else: return schema_item.copy(**kw) # type: ignore[union-attr] @@ -299,9 +356,7 @@ def _columns_for_constraint(constraint): return list(constraint.columns) -def _reflect_table( - inspector: Inspector, table: Table, include_cols: None -) -> None: +def _reflect_table(inspector: Inspector, table: Table) -> None: if sqla_14: return inspector.reflect_table(table, None) else: @@ -335,7 +390,12 @@ else: return type_.impl, type_.mapping -def _fk_spec(constraint): +def _fk_spec(constraint: ForeignKeyConstraint) -> Any: + if TYPE_CHECKING: + assert constraint.columns is not None + assert constraint.elements is not None + assert isinstance(constraint.parent, Table) + source_columns = [ constraint.columns[key].name for key in constraint.column_keys ] @@ -364,7 +424,7 @@ def _fk_spec(constraint): def _fk_is_self_referential(constraint: ForeignKeyConstraint) -> bool: - spec = constraint.elements[0]._get_colspec() # type: ignore[attr-defined] + spec = constraint.elements[0]._get_colspec() tokens = spec.split(".") tokens.pop(-1) # colname tablekey = ".".join(tokens) @@ -376,19 +436,19 @@ def _is_type_bound(constraint: Constraint) -> bool: # this deals with SQLAlchemy #3260, don't copy CHECK constraints # that will be generated by the type. # new feature added for #3260 - return constraint._type_bound # type: ignore[attr-defined] + return constraint._type_bound def _find_columns(clause): """locate Column objects within the given expression.""" - cols = set() + cols: Set[ColumnElement[Any]] = set() traverse(clause, {}, {"column": cols.add}) return cols def _remove_column_from_collection( - collection: ColumnCollection, column: Union[Column, ColumnClause] + collection: ColumnCollection, column: Union[Column[Any], ColumnClause[Any]] ) -> None: """remove a column from a ColumnCollection.""" @@ -406,8 +466,8 @@ def _remove_column_from_collection( def _textual_index_column( - table: Table, text_: Union[str, TextClause, ColumnElement] -) -> Union[ColumnElement, Column]: + table: Table, text_: Union[str, TextClause, ColumnElement[Any]] +) -> Union[ColumnElement[Any], Column[Any]]: """a workaround for the Index construct's severe lack of flexibility""" if isinstance(text_, str): c = Column(text_, sqltypes.NULLTYPE) @@ -491,14 +551,6 @@ def _render_literal_bindparam( return compiler.render_literal_bindparam(element, **kw) -def _get_index_expressions(idx): - return list(idx.expressions) - - -def _get_index_column_names(idx): - return [getattr(exp, "name", None) for exp in _get_index_expressions(idx)] - - def _column_kwargs(col: Column) -> Mapping: if sqla_13: return col.kwargs @@ -522,7 +574,6 @@ def _get_constraint_final_name( constraint, _alembic_quote=False ) else: - # prior to SQLAlchemy 1.4, work around quoting logic to get at the # final compiled name without quotes. if hasattr(constraint.name, "quote"): @@ -538,9 +589,7 @@ def _get_constraint_final_name( if isinstance(constraint, schema.Index): # name should not be quoted. d = dialect.ddl_compiler(dialect, None) # type: ignore[arg-type] - return d._prepared_index_name( # type: ignore[attr-defined] - constraint - ) + return d._prepared_index_name(constraint) else: # name should not be quoted. return dialect.identifier_preparer.format_constraint(constraint) @@ -584,7 +633,11 @@ def _insert_inline(table: Union[TableClause, Table]) -> Insert: if sqla_14: from sqlalchemy import create_mock_engine - from sqlalchemy import select as _select + + # weird mypy workaround + from sqlalchemy import select as _sa_select + + _select = _sa_select else: from sqlalchemy import create_engine @@ -593,15 +646,20 @@ else: "postgresql://", strategy="mock", executor=executor ) - def _select(*columns, **kw) -> Select: # type: ignore[no-redef] + def _select(*columns, **kw) -> Select: return sql.select(list(columns), **kw) # type: ignore[call-overload] def is_expression_index(index: Index) -> bool: - expr: Any for expr in index.expressions: - while isinstance(expr, UnaryExpression): - expr = expr.element - if not isinstance(expr, ColumnClause) or expr.is_literal: + if is_expression(expr): return True return False + + +def is_expression(expr: Any) -> bool: + while isinstance(expr, UnaryExpression): + expr = expr.element + if not isinstance(expr, ColumnClause) or expr.is_literal: + return True + return False diff --git a/libs/aniso8601-9.0.1.dist-info/INSTALLER b/libs/aniso8601-9.0.1.dist-info/INSTALLER new file mode 100644 index 000000000..a1b589e38 --- /dev/null +++ b/libs/aniso8601-9.0.1.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/libs/aniso8601-9.0.1.dist-info/LICENSE b/libs/aniso8601-9.0.1.dist-info/LICENSE new file mode 100644 index 000000000..40b077445 --- /dev/null +++ b/libs/aniso8601-9.0.1.dist-info/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2021, Brandon Nielsen +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/libs/aniso8601-9.0.1.dist-info/METADATA b/libs/aniso8601-9.0.1.dist-info/METADATA new file mode 100644 index 000000000..a926a412b --- /dev/null +++ b/libs/aniso8601-9.0.1.dist-info/METADATA @@ -0,0 +1,510 @@ +Metadata-Version: 2.1 +Name: aniso8601 +Version: 9.0.1 +Summary: A library for parsing ISO 8601 strings. +Home-page: https://bitbucket.org/nielsenb/aniso8601 +Author: Brandon Nielsen +Author-email: nielsenb@jetfuse.net +Project-URL: Documentation, https://aniso8601.readthedocs.io/ +Project-URL: Source, https://bitbucket.org/nielsenb/aniso8601 +Project-URL: Tracker, https://bitbucket.org/nielsenb/aniso8601/issues +Keywords: iso8601 parser +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: BSD License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.4 +Classifier: Programming Language :: Python :: 3.5 +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Topic :: Software Development :: Libraries :: Python Modules +Description-Content-Type: text/x-rst +License-File: LICENSE +Provides-Extra: dev +Requires-Dist: black ; extra == 'dev' +Requires-Dist: coverage ; extra == 'dev' +Requires-Dist: isort ; extra == 'dev' +Requires-Dist: pre-commit ; extra == 'dev' +Requires-Dist: pyenchant ; extra == 'dev' +Requires-Dist: pylint ; extra == 'dev' + +aniso8601 +========= + +Another ISO 8601 parser for Python +---------------------------------- + +Features +======== +* Pure Python implementation +* Logical behavior + + - Parse a time, get a `datetime.time `_ + - Parse a date, get a `datetime.date `_ + - Parse a datetime, get a `datetime.datetime `_ + - Parse a duration, get a `datetime.timedelta `_ + - Parse an interval, get a tuple of dates or datetimes + - Parse a repeating interval, get a date or datetime `generator `_ + +* UTC offset represented as fixed-offset tzinfo +* Parser separate from representation, allowing parsing to different datetime representations (see `Builders`_) +* No regular expressions + +Installation +============ + +The recommended installation method is to use pip:: + + $ pip install aniso8601 + +Alternatively, you can download the source (git repository hosted at `Bitbucket `_) and install directly:: + + $ python setup.py install + +Use +=== + +Parsing datetimes +----------------- + +*Consider* `datetime.datetime.fromisoformat `_ *for basic ISO 8601 datetime parsing* + +To parse a typical ISO 8601 datetime string:: + + >>> import aniso8601 + >>> aniso8601.parse_datetime('1977-06-10T12:00:00Z') + datetime.datetime(1977, 6, 10, 12, 0, tzinfo=+0:00:00 UTC) + +Alternative delimiters can be specified, for example, a space:: + + >>> aniso8601.parse_datetime('1977-06-10 12:00:00Z', delimiter=' ') + datetime.datetime(1977, 6, 10, 12, 0, tzinfo=+0:00:00 UTC) + +UTC offsets are supported:: + + >>> aniso8601.parse_datetime('1979-06-05T08:00:00-08:00') + datetime.datetime(1979, 6, 5, 8, 0, tzinfo=-8:00:00 UTC) + +If a UTC offset is not specified, the returned datetime will be naive:: + + >>> aniso8601.parse_datetime('1983-01-22T08:00:00') + datetime.datetime(1983, 1, 22, 8, 0) + +Leap seconds are currently not supported and attempting to parse one raises a :code:`LeapSecondError`:: + + >>> aniso8601.parse_datetime('2018-03-06T23:59:60') + Traceback (most recent call last): + File "", line 1, in + File "/home/nielsenb/Jetfuse/aniso8601/aniso8601/aniso8601/time.py", line 196, in parse_datetime + return builder.build_datetime(datepart, timepart) + File "/home/nielsenb/Jetfuse/aniso8601/aniso8601/aniso8601/builders/python.py", line 237, in build_datetime + cls._build_object(time)) + File "/home/nielsenb/Jetfuse/aniso8601/aniso8601/aniso8601/builders/__init__.py", line 336, in _build_object + return cls.build_time(hh=parsetuple.hh, mm=parsetuple.mm, + File "/home/nielsenb/Jetfuse/aniso8601/aniso8601/aniso8601/builders/python.py", line 191, in build_time + hh, mm, ss, tz = cls.range_check_time(hh, mm, ss, tz) + File "/home/nielsenb/Jetfuse/aniso8601/aniso8601/aniso8601/builders/__init__.py", line 266, in range_check_time + raise LeapSecondError('Leap seconds are not supported.') + aniso8601.exceptions.LeapSecondError: Leap seconds are not supported. + +To get the resolution of an ISO 8601 datetime string:: + + >>> aniso8601.get_datetime_resolution('1977-06-10T12:00:00Z') == aniso8601.resolution.TimeResolution.Seconds + True + >>> aniso8601.get_datetime_resolution('1977-06-10T12:00') == aniso8601.resolution.TimeResolution.Minutes + True + >>> aniso8601.get_datetime_resolution('1977-06-10T12') == aniso8601.resolution.TimeResolution.Hours + True + +Note that datetime resolutions map to :code:`TimeResolution` as a valid datetime must have at least one time member so the resolution mapping is equivalent. + +Parsing dates +------------- + +*Consider* `datetime.date.fromisoformat `_ *for basic ISO 8601 date parsing* + +To parse a date represented in an ISO 8601 string:: + + >>> import aniso8601 + >>> aniso8601.parse_date('1984-04-23') + datetime.date(1984, 4, 23) + +Basic format is supported as well:: + + >>> aniso8601.parse_date('19840423') + datetime.date(1984, 4, 23) + +To parse a date using the ISO 8601 week date format:: + + >>> aniso8601.parse_date('1986-W38-1') + datetime.date(1986, 9, 15) + +To parse an ISO 8601 ordinal date:: + + >>> aniso8601.parse_date('1988-132') + datetime.date(1988, 5, 11) + +To get the resolution of an ISO 8601 date string:: + + >>> aniso8601.get_date_resolution('1981-04-05') == aniso8601.resolution.DateResolution.Day + True + >>> aniso8601.get_date_resolution('1981-04') == aniso8601.resolution.DateResolution.Month + True + >>> aniso8601.get_date_resolution('1981') == aniso8601.resolution.DateResolution.Year + True + +Parsing times +------------- + +*Consider* `datetime.time.fromisoformat `_ *for basic ISO 8601 time parsing* + +To parse a time formatted as an ISO 8601 string:: + + >>> import aniso8601 + >>> aniso8601.parse_time('11:31:14') + datetime.time(11, 31, 14) + +As with all of the above, basic format is supported:: + + >>> aniso8601.parse_time('113114') + datetime.time(11, 31, 14) + +A UTC offset can be specified for times:: + + >>> aniso8601.parse_time('17:18:19-02:30') + datetime.time(17, 18, 19, tzinfo=-2:30:00 UTC) + >>> aniso8601.parse_time('171819Z') + datetime.time(17, 18, 19, tzinfo=+0:00:00 UTC) + +Reduced accuracy is supported:: + + >>> aniso8601.parse_time('21:42') + datetime.time(21, 42) + >>> aniso8601.parse_time('22') + datetime.time(22, 0) + +A decimal fraction is always allowed on the lowest order element of an ISO 8601 formatted time:: + + >>> aniso8601.parse_time('22:33.5') + datetime.time(22, 33, 30) + >>> aniso8601.parse_time('23.75') + datetime.time(23, 45) + +The decimal fraction can be specified with a comma instead of a full-stop:: + + >>> aniso8601.parse_time('22:33,5') + datetime.time(22, 33, 30) + >>> aniso8601.parse_time('23,75') + datetime.time(23, 45) + +Leap seconds are currently not supported and attempting to parse one raises a :code:`LeapSecondError`:: + + >>> aniso8601.parse_time('23:59:60') + Traceback (most recent call last): + File "", line 1, in + File "/home/nielsenb/Jetfuse/aniso8601/aniso8601/aniso8601/time.py", line 174, in parse_time + return builder.build_time(hh=hourstr, mm=minutestr, ss=secondstr, tz=tz) + File "/home/nielsenb/Jetfuse/aniso8601/aniso8601/aniso8601/builders/python.py", line 191, in build_time + hh, mm, ss, tz = cls.range_check_time(hh, mm, ss, tz) + File "/home/nielsenb/Jetfuse/aniso8601/aniso8601/aniso8601/builders/__init__.py", line 266, in range_check_time + raise LeapSecondError('Leap seconds are not supported.') + aniso8601.exceptions.LeapSecondError: Leap seconds are not supported. + +To get the resolution of an ISO 8601 time string:: + + >>> aniso8601.get_time_resolution('11:31:14') == aniso8601.resolution.TimeResolution.Seconds + True + >>> aniso8601.get_time_resolution('11:31') == aniso8601.resolution.TimeResolution.Minutes + True + >>> aniso8601.get_time_resolution('11') == aniso8601.resolution.TimeResolution.Hours + True + +Parsing durations +----------------- + +To parse a duration formatted as an ISO 8601 string:: + + >>> import aniso8601 + >>> aniso8601.parse_duration('P1Y2M3DT4H54M6S') + datetime.timedelta(428, 17646) + +Reduced accuracy is supported:: + + >>> aniso8601.parse_duration('P1Y') + datetime.timedelta(365) + +A decimal fraction is allowed on the lowest order element:: + + >>> aniso8601.parse_duration('P1YT3.5M') + datetime.timedelta(365, 210) + +The decimal fraction can be specified with a comma instead of a full-stop:: + + >>> aniso8601.parse_duration('P1YT3,5M') + datetime.timedelta(365, 210) + +Parsing a duration from a combined date and time is supported as well:: + + >>> aniso8601.parse_duration('P0001-01-02T01:30:05') + datetime.timedelta(397, 5405) + +To get the resolution of an ISO 8601 duration string:: + + >>> aniso8601.get_duration_resolution('P1Y2M3DT4H54M6S') == aniso8601.resolution.DurationResolution.Seconds + True + >>> aniso8601.get_duration_resolution('P1Y2M3DT4H54M') == aniso8601.resolution.DurationResolution.Minutes + True + >>> aniso8601.get_duration_resolution('P1Y2M3DT4H') == aniso8601.resolution.DurationResolution.Hours + True + >>> aniso8601.get_duration_resolution('P1Y2M3D') == aniso8601.resolution.DurationResolution.Days + True + >>> aniso8601.get_duration_resolution('P1Y2M') == aniso8601.resolution.DurationResolution.Months + True + >>> aniso8601.get_duration_resolution('P1Y') == aniso8601.resolution.DurationResolution.Years + True + +The default :code:`PythonTimeBuilder` assumes years are 365 days, and months are 30 days. Where calendar level accuracy is required, a `RelativeTimeBuilder `_ can be used, see also `Builders`_. + +Parsing intervals +----------------- + +To parse an interval specified by a start and end:: + + >>> import aniso8601 + >>> aniso8601.parse_interval('2007-03-01T13:00:00/2008-05-11T15:30:00') + (datetime.datetime(2007, 3, 1, 13, 0), datetime.datetime(2008, 5, 11, 15, 30)) + +Intervals specified by a start time and a duration are supported:: + + >>> aniso8601.parse_interval('2007-03-01T13:00:00Z/P1Y2M10DT2H30M') + (datetime.datetime(2007, 3, 1, 13, 0, tzinfo=+0:00:00 UTC), datetime.datetime(2008, 5, 9, 15, 30, tzinfo=+0:00:00 UTC)) + +A duration can also be specified by a duration and end time:: + + >>> aniso8601.parse_interval('P1M/1981-04-05') + (datetime.date(1981, 4, 5), datetime.date(1981, 3, 6)) + +Notice that the result of the above parse is not in order from earliest to latest. If sorted intervals are required, simply use the :code:`sorted` keyword as shown below:: + + >>> sorted(aniso8601.parse_interval('P1M/1981-04-05')) + [datetime.date(1981, 3, 6), datetime.date(1981, 4, 5)] + +The end of an interval is returned as a datetime when required to maintain the resolution specified by a duration, even if the duration start is given as a date:: + + >>> aniso8601.parse_interval('2014-11-12/PT4H54M6.5S') + (datetime.date(2014, 11, 12), datetime.datetime(2014, 11, 12, 4, 54, 6, 500000)) + >>> aniso8601.parse_interval('2007-03-01/P1.5D') + (datetime.date(2007, 3, 1), datetime.datetime(2007, 3, 2, 12, 0)) + +Concise representations are supported:: + + >>> aniso8601.parse_interval('2020-01-01/02') + (datetime.date(2020, 1, 1), datetime.date(2020, 1, 2)) + >>> aniso8601.parse_interval('2007-12-14T13:30/15:30') + (datetime.datetime(2007, 12, 14, 13, 30), datetime.datetime(2007, 12, 14, 15, 30)) + >>> aniso8601.parse_interval('2008-02-15/03-14') + (datetime.date(2008, 2, 15), datetime.date(2008, 3, 14)) + >>> aniso8601.parse_interval('2007-11-13T09:00/15T17:00') + (datetime.datetime(2007, 11, 13, 9, 0), datetime.datetime(2007, 11, 15, 17, 0)) + +Repeating intervals are supported as well, and return a `generator `_:: + + >>> aniso8601.parse_repeating_interval('R3/1981-04-05/P1D') + + >>> list(aniso8601.parse_repeating_interval('R3/1981-04-05/P1D')) + [datetime.date(1981, 4, 5), datetime.date(1981, 4, 6), datetime.date(1981, 4, 7)] + +Repeating intervals are allowed to go in the reverse direction:: + + >>> list(aniso8601.parse_repeating_interval('R2/PT1H2M/1980-03-05T01:01:00')) + [datetime.datetime(1980, 3, 5, 1, 1), datetime.datetime(1980, 3, 4, 23, 59)] + +Unbounded intervals are also allowed (Python 2):: + + >>> result = aniso8601.parse_repeating_interval('R/PT1H2M/1980-03-05T01:01:00') + >>> result.next() + datetime.datetime(1980, 3, 5, 1, 1) + >>> result.next() + datetime.datetime(1980, 3, 4, 23, 59) + +or for Python 3:: + + >>> result = aniso8601.parse_repeating_interval('R/PT1H2M/1980-03-05T01:01:00') + >>> next(result) + datetime.datetime(1980, 3, 5, 1, 1) + >>> next(result) + datetime.datetime(1980, 3, 4, 23, 59) + +Note that you should never try to convert a generator produced by an unbounded interval to a list:: + + >>> list(aniso8601.parse_repeating_interval('R/PT1H2M/1980-03-05T01:01:00')) + Traceback (most recent call last): + File "", line 1, in + File "/home/nielsenb/Jetfuse/aniso8601/aniso8601/aniso8601/builders/python.py", line 560, in _date_generator_unbounded + currentdate += timedelta + OverflowError: date value out of range + +To get the resolution of an ISO 8601 interval string:: + + >>> aniso8601.get_interval_resolution('2007-03-01T13:00:00/2008-05-11T15:30:00') == aniso8601.resolution.IntervalResolution.Seconds + True + >>> aniso8601.get_interval_resolution('2007-03-01T13:00/2008-05-11T15:30') == aniso8601.resolution.IntervalResolution.Minutes + True + >>> aniso8601.get_interval_resolution('2007-03-01T13/2008-05-11T15') == aniso8601.resolution.IntervalResolution.Hours + True + >>> aniso8601.get_interval_resolution('2007-03-01/2008-05-11') == aniso8601.resolution.IntervalResolution.Day + True + >>> aniso8601.get_interval_resolution('2007-03/P1Y') == aniso8601.resolution.IntervalResolution.Month + True + >>> aniso8601.get_interval_resolution('2007/P1Y') == aniso8601.resolution.IntervalResolution.Year + True + +And for repeating ISO 8601 interval strings:: + + >>> aniso8601.get_repeating_interval_resolution('R3/1981-04-05/P1D') == aniso8601.resolution.IntervalResolution.Day + True + >>> aniso8601.get_repeating_interval_resolution('R/PT1H2M/1980-03-05T01:01:00') == aniso8601.resolution.IntervalResolution.Seconds + True + +Builders +======== + +Builders can be used to change the output format of a parse operation. All parse functions have a :code:`builder` keyword argument which accepts a builder class. + +Two builders are included. The :code:`PythonTimeBuilder` (the default) in the :code:`aniso8601.builders.python` module, and the :code:`TupleBuilder` which returns the parse result as a corresponding named tuple and is located in the :code:`aniso8601.builders` module. + +Information on writing a builder can be found in `BUILDERS `_. + +The following builders are available as separate projects: + +* `RelativeTimeBuilder `_ supports parsing to `datetutil relativedelta types `_ for calendar level accuracy +* `AttoTimeBuilder `_ supports parsing directly to `attotime attodatetime and attotimedelta types `_ which support sub-nanosecond precision +* `NumPyTimeBuilder `_ supports parsing directly to `NumPy datetime64 and timedelta64 types `_ + +TupleBuilder +------------ + +The :code:`TupleBuilder` returns parse results as `named tuples `_. It is located in the :code:`aniso8601.builders` module. + +Datetimes +^^^^^^^^^ + +Parsing a datetime returns a :code:`DatetimeTuple` containing :code:`Date` and :code:`Time` tuples . The date tuple contains the following parse components: :code:`YYYY`, :code:`MM`, :code:`DD`, :code:`Www`, :code:`D`, :code:`DDD`. The time tuple contains the following parse components :code:`hh`, :code:`mm`, :code:`ss`, :code:`tz`, where :code:`tz` itself is a tuple with the following components :code:`negative`, :code:`Z`, :code:`hh`, :code:`mm`, :code:`name` with :code:`negative` and :code:`Z` being booleans:: + + >>> import aniso8601 + >>> from aniso8601.builders import TupleBuilder + >>> aniso8601.parse_datetime('1977-06-10T12:00:00', builder=TupleBuilder) + Datetime(date=Date(YYYY='1977', MM='06', DD='10', Www=None, D=None, DDD=None), time=Time(hh='12', mm='00', ss='00', tz=None)) + >>> aniso8601.parse_datetime('1979-06-05T08:00:00-08:00', builder=TupleBuilder) + Datetime(date=Date(YYYY='1979', MM='06', DD='05', Www=None, D=None, DDD=None), time=Time(hh='08', mm='00', ss='00', tz=Timezone(negative=True, Z=None, hh='08', mm='00', name='-08:00'))) + +Dates +^^^^^ + +Parsing a date returns a :code:`DateTuple` containing the following parse components: :code:`YYYY`, :code:`MM`, :code:`DD`, :code:`Www`, :code:`D`, :code:`DDD`:: + + >>> import aniso8601 + >>> from aniso8601.builders import TupleBuilder + >>> aniso8601.parse_date('1984-04-23', builder=TupleBuilder) + Date(YYYY='1984', MM='04', DD='23', Www=None, D=None, DDD=None) + >>> aniso8601.parse_date('1986-W38-1', builder=TupleBuilder) + Date(YYYY='1986', MM=None, DD=None, Www='38', D='1', DDD=None) + >>> aniso8601.parse_date('1988-132', builder=TupleBuilder) + Date(YYYY='1988', MM=None, DD=None, Www=None, D=None, DDD='132') + +Times +^^^^^ + +Parsing a time returns a :code:`TimeTuple` containing following parse components: :code:`hh`, :code:`mm`, :code:`ss`, :code:`tz`, where :code:`tz` is a :code:`TimezoneTuple` with the following components :code:`negative`, :code:`Z`, :code:`hh`, :code:`mm`, :code:`name`, with :code:`negative` and :code:`Z` being booleans:: + + >>> import aniso8601 + >>> from aniso8601.builders import TupleBuilder + >>> aniso8601.parse_time('11:31:14', builder=TupleBuilder) + Time(hh='11', mm='31', ss='14', tz=None) + >>> aniso8601.parse_time('171819Z', builder=TupleBuilder) + Time(hh='17', mm='18', ss='19', tz=Timezone(negative=False, Z=True, hh=None, mm=None, name='Z')) + >>> aniso8601.parse_time('17:18:19-02:30', builder=TupleBuilder) + Time(hh='17', mm='18', ss='19', tz=Timezone(negative=True, Z=None, hh='02', mm='30', name='-02:30')) + +Durations +^^^^^^^^^ + +Parsing a duration returns a :code:`DurationTuple` containing the following parse components: :code:`PnY`, :code:`PnM`, :code:`PnW`, :code:`PnD`, :code:`TnH`, :code:`TnM`, :code:`TnS`:: + + >>> import aniso8601 + >>> from aniso8601.builders import TupleBuilder + >>> aniso8601.parse_duration('P1Y2M3DT4H54M6S', builder=TupleBuilder) + Duration(PnY='1', PnM='2', PnW=None, PnD='3', TnH='4', TnM='54', TnS='6') + >>> aniso8601.parse_duration('P7W', builder=TupleBuilder) + Duration(PnY=None, PnM=None, PnW='7', PnD=None, TnH=None, TnM=None, TnS=None) + +Intervals +^^^^^^^^^ + +Parsing an interval returns an :code:`IntervalTuple` containing the following parse components: :code:`start`, :code:`end`, :code:`duration`, :code:`start` and :code:`end` may both be datetime or date tuples, :code:`duration` is a duration tuple:: + + >>> import aniso8601 + >>> from aniso8601.builders import TupleBuilder + >>> aniso8601.parse_interval('2007-03-01T13:00:00/2008-05-11T15:30:00', builder=TupleBuilder) + Interval(start=Datetime(date=Date(YYYY='2007', MM='03', DD='01', Www=None, D=None, DDD=None), time=Time(hh='13', mm='00', ss='00', tz=None)), end=Datetime(date=Date(YYYY='2008', MM='05', DD='11', Www=None, D=None, DDD=None), time=Time(hh='15', mm='30', ss='00', tz=None)), duration=None) + >>> aniso8601.parse_interval('2007-03-01T13:00:00Z/P1Y2M10DT2H30M', builder=TupleBuilder) + Interval(start=Datetime(date=Date(YYYY='2007', MM='03', DD='01', Www=None, D=None, DDD=None), time=Time(hh='13', mm='00', ss='00', tz=Timezone(negative=False, Z=True, hh=None, mm=None, name='Z'))), end=None, duration=Duration(PnY='1', PnM='2', PnW=None, PnD='10', TnH='2', TnM='30', TnS=None)) + >>> aniso8601.parse_interval('P1M/1981-04-05', builder=TupleBuilder) + Interval(start=None, end=Date(YYYY='1981', MM='04', DD='05', Www=None, D=None, DDD=None), duration=Duration(PnY=None, PnM='1', PnW=None, PnD=None, TnH=None, TnM=None, TnS=None)) + +A repeating interval returns a :code:`RepeatingIntervalTuple` containing the following parse components: :code:`R`, :code:`Rnn`, :code:`interval`, where :code:`R` is a boolean, :code:`True` for an unbounded interval, :code:`False` otherwise.:: + + >>> aniso8601.parse_repeating_interval('R3/1981-04-05/P1D', builder=TupleBuilder) + RepeatingInterval(R=False, Rnn='3', interval=Interval(start=Date(YYYY='1981', MM='04', DD='05', Www=None, D=None, DDD=None), end=None, duration=Duration(PnY=None, PnM=None, PnW=None, PnD='1', TnH=None, TnM=None, TnS=None))) + >>> aniso8601.parse_repeating_interval('R/PT1H2M/1980-03-05T01:01:00', builder=TupleBuilder) + RepeatingInterval(R=True, Rnn=None, interval=Interval(start=None, end=Datetime(date=Date(YYYY='1980', MM='03', DD='05', Www=None, D=None, DDD=None), time=Time(hh='01', mm='01', ss='00', tz=None)), duration=Duration(PnY=None, PnM=None, PnW=None, PnD=None, TnH='1', TnM='2', TnS=None))) + +Development +=========== + +Setup +----- + +It is recommended to develop using a `virtualenv `_. + +Inside a virtualenv, development dependencies can be installed automatically:: + + $ pip install -e .[dev] + +`pre-commit `_ is used for managing pre-commit hooks:: + + $ pre-commit install + +To run the pre-commit hooks manually:: + + $ pre-commit run --all-files + +Tests +----- + +Tests can be run using the `unittest testing framework `_:: + + $ python -m unittest discover aniso8601 + +Contributing +============ + +aniso8601 is an open source project hosted on `Bitbucket `_. + +Any and all bugs are welcome on our `issue tracker `_. +Of particular interest are valid ISO 8601 strings that don't parse, or invalid ones that do. At a minimum, +bug reports should include an example of the misbehaving string, as well as the expected result. Of course +patches containing unit tests (or fixed bugs) are welcome! + +References +========== + +* `ISO 8601:2004(E) `_ (Caution, PDF link) +* `Wikipedia article on ISO 8601 `_ +* `Discussion on alternative ISO 8601 parsers for Python `_ diff --git a/libs/aniso8601-9.0.1.dist-info/RECORD b/libs/aniso8601-9.0.1.dist-info/RECORD new file mode 100644 index 000000000..7a3fac144 --- /dev/null +++ b/libs/aniso8601-9.0.1.dist-info/RECORD @@ -0,0 +1,34 @@ +aniso8601-9.0.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +aniso8601-9.0.1.dist-info/LICENSE,sha256=Z_-MGC_A4Nc1cViNi8B5tOSmJKknTE4mqSPeIxDTvSk,1501 +aniso8601-9.0.1.dist-info/METADATA,sha256=2Esk-53vD4ruSRkC_RMTnKscxUh9nWSEj9MeS6lTlDU,23416 +aniso8601-9.0.1.dist-info/RECORD,, +aniso8601-9.0.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +aniso8601-9.0.1.dist-info/WHEEL,sha256=P2T-6epvtXQ2cBOE_U1K4_noqlJFN3tj15djMgEu4NM,110 +aniso8601-9.0.1.dist-info/top_level.txt,sha256=MVQomyeED8nGIH7PUQdMzxgLppIB48oYHtcmL17ETB0,10 +aniso8601/__init__.py,sha256=tHN7Nq-3I79PLzKkBuWun_UKgolnDrn7ISO8s1HlMdo,704 +aniso8601/builders/__init__.py,sha256=sJanTP5Lo0lRxpLa5VKVBS9u6ZP8R1VRgozx5uSUMUU,17958 +aniso8601/builders/python.py,sha256=I0RhPY2syncaMwYRVJxM6ct-O_5MHnNFY3dcF6wvy0Y,22122 +aniso8601/builders/tests/__init__.py,sha256=XWM00Wzg9EZkSKyy3IW18Z8TiXfCbJS-XJNFVuylvuU,209 +aniso8601/builders/tests/test_init.py,sha256=wnDhjyb5iBt9l_zTXT96uqXus-igSqn5Kn_rqX_NSHA,29997 +aniso8601/builders/tests/test_python.py,sha256=pNr3lwfBKVSUQKc5BPmwCiCTpP_063WpOM-canDz4J8,61593 +aniso8601/compat.py,sha256=2exJsHW2DAxt_D2_mGj5mv0HCSMFeAAkPyFAM-ZFrA0,571 +aniso8601/date.py,sha256=IDn_kqeZshllwr4pICUNZhjbqSVVlYTyHmBOgp2MlNE,4475 +aniso8601/decimalfraction.py,sha256=EtwqSZJTtsQlu05m2guolhii5N1yN4dVv0v1zCZhiyk,333 +aniso8601/duration.py,sha256=6AAl9A-WM2Io898peIz9xbwOvxcLc6WYGUdkYuQlTU8,9583 +aniso8601/exceptions.py,sha256=-zrdcKocZhzhl71HhgVKXWF481XDWO3UhinbcycCzPU,1313 +aniso8601/interval.py,sha256=7e5wICHdF2gTeFluPxBrzaA4-_5b78QzXC62DSnNzlM,10763 +aniso8601/resolution.py,sha256=ee7GxL865D0dJL70TsXScz4Kzo_dwMORNvfuyCXdsgI,684 +aniso8601/tests/__init__.py,sha256=XWM00Wzg9EZkSKyy3IW18Z8TiXfCbJS-XJNFVuylvuU,209 +aniso8601/tests/compat.py,sha256=9HJqKvl0PIFBjePUgT-1eMGkA9tlESX0wNDkPvV7GOk,346 +aniso8601/tests/test_compat.py,sha256=2oFOFLKTfOJIMbLjkeVhrkxSDMjE0wM-NB86SJ6st5g,763 +aniso8601/tests/test_date.py,sha256=3AWmIHTS2sxm9_ZUYcI2w9ALJOYnHkkYEwlD1VW90iQ,8960 +aniso8601/tests/test_decimalfraction.py,sha256=T4R_SY24DW30YuQkyofxvAmngTuXtsmwd77pF25QAlc,578 +aniso8601/tests/test_duration.py,sha256=ZqUxodLrDBZ1GZWutFXjktAFHYS1hidxLclIGZP7aSA,44952 +aniso8601/tests/test_init.py,sha256=GazCeGTv-OFocCx9Cck04b-c1cWiiRnqhGwoGgm4Y1Q,1689 +aniso8601/tests/test_interval.py,sha256=lTg-E1vW1xmgwiWfHHwJDJ25AogSR-1p-0L4O2gQKQw,60457 +aniso8601/tests/test_time.py,sha256=HLutGVdg2_HHU51U2eEEZ9UNwljrQBPU_PtX8JrdVV0,19147 +aniso8601/tests/test_timezone.py,sha256=Shw7-fcUJZAbH7diCx37iXZ4VZEH45lqIgMJvoQQhtQ,4649 +aniso8601/tests/test_utcoffset.py,sha256=fRNuiz3WPMrHtrdMGK3HOuZRYd68hR-VNldbwVG-cDA,1926 +aniso8601/time.py,sha256=9IRsCERfEl_SnBBUIOR8E43XFD7Y2EqhowjiCcfinb0,5688 +aniso8601/timezone.py,sha256=5_LRd_pYd08i2hmXsn_1tTUxKOI4caSvxci-VByHCWU,2134 +aniso8601/utcoffset.py,sha256=8Gh8WNk_q9ELLEFZLMPbMESH-yqcoNFjul7VcpHq_1Q,2423 diff --git a/libs/aniso8601-9.0.1.dist-info/REQUESTED b/libs/aniso8601-9.0.1.dist-info/REQUESTED new file mode 100644 index 000000000..e69de29bb diff --git a/libs/aniso8601-9.0.1.dist-info/WHEEL b/libs/aniso8601-9.0.1.dist-info/WHEEL new file mode 100644 index 000000000..f31e450fd --- /dev/null +++ b/libs/aniso8601-9.0.1.dist-info/WHEEL @@ -0,0 +1,6 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.41.3) +Root-Is-Purelib: true +Tag: py2-none-any +Tag: py3-none-any + diff --git a/libs/aniso8601-9.0.1.dist-info/top_level.txt b/libs/aniso8601-9.0.1.dist-info/top_level.txt new file mode 100644 index 000000000..166ae78c5 --- /dev/null +++ b/libs/aniso8601-9.0.1.dist-info/top_level.txt @@ -0,0 +1 @@ +aniso8601 diff --git a/libs/appdirs-1.4.4.dist-info/INSTALLER b/libs/appdirs-1.4.4.dist-info/INSTALLER new file mode 100644 index 000000000..a1b589e38 --- /dev/null +++ b/libs/appdirs-1.4.4.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/libs/appdirs-1.4.4.dist-info/LICENSE.txt b/libs/appdirs-1.4.4.dist-info/LICENSE.txt new file mode 100644 index 000000000..107c61405 --- /dev/null +++ b/libs/appdirs-1.4.4.dist-info/LICENSE.txt @@ -0,0 +1,23 @@ +# This is the MIT license + +Copyright (c) 2010 ActiveState Software Inc. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/libs/appdirs-1.4.4.dist-info/METADATA b/libs/appdirs-1.4.4.dist-info/METADATA new file mode 100644 index 000000000..26a62703f --- /dev/null +++ b/libs/appdirs-1.4.4.dist-info/METADATA @@ -0,0 +1,262 @@ +Metadata-Version: 2.1 +Name: appdirs +Version: 1.4.4 +Summary: A small Python module for determining appropriate platform-specific dirs, e.g. a "user data dir". +Home-page: http://github.com/ActiveState/appdirs +Author: Trent Mick +Author-email: trentm@gmail.com +Maintainer: Jeff Rouse +Maintainer-email: jr@its.to +License: MIT +Keywords: application directory log cache user +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: MIT License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.4 +Classifier: Programming Language :: Python :: 3.5 +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: Implementation :: PyPy +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Topic :: Software Development :: Libraries :: Python Modules +License-File: LICENSE.txt + + +.. image:: https://secure.travis-ci.org/ActiveState/appdirs.png + :target: http://travis-ci.org/ActiveState/appdirs + +the problem +=========== + +What directory should your app use for storing user data? If running on Mac OS X, you +should use:: + + ~/Library/Application Support/ + +If on Windows (at least English Win XP) that should be:: + + C:\Documents and Settings\\Application Data\Local Settings\\ + +or possibly:: + + C:\Documents and Settings\\Application Data\\ + +for `roaming profiles `_ but that is another story. + +On Linux (and other Unices) the dir, according to the `XDG +spec `_, is:: + + ~/.local/share/ + + +``appdirs`` to the rescue +========================= + +This kind of thing is what the ``appdirs`` module is for. ``appdirs`` will +help you choose an appropriate: + +- user data dir (``user_data_dir``) +- user config dir (``user_config_dir``) +- user cache dir (``user_cache_dir``) +- site data dir (``site_data_dir``) +- site config dir (``site_config_dir``) +- user log dir (``user_log_dir``) + +and also: + +- is a single module so other Python packages can include their own private copy +- is slightly opinionated on the directory names used. Look for "OPINION" in + documentation and code for when an opinion is being applied. + + +some example output +=================== + +On Mac OS X:: + + >>> from appdirs import * + >>> appname = "SuperApp" + >>> appauthor = "Acme" + >>> user_data_dir(appname, appauthor) + '/Users/trentm/Library/Application Support/SuperApp' + >>> site_data_dir(appname, appauthor) + '/Library/Application Support/SuperApp' + >>> user_cache_dir(appname, appauthor) + '/Users/trentm/Library/Caches/SuperApp' + >>> user_log_dir(appname, appauthor) + '/Users/trentm/Library/Logs/SuperApp' + +On Windows 7:: + + >>> from appdirs import * + >>> appname = "SuperApp" + >>> appauthor = "Acme" + >>> user_data_dir(appname, appauthor) + 'C:\\Users\\trentm\\AppData\\Local\\Acme\\SuperApp' + >>> user_data_dir(appname, appauthor, roaming=True) + 'C:\\Users\\trentm\\AppData\\Roaming\\Acme\\SuperApp' + >>> user_cache_dir(appname, appauthor) + 'C:\\Users\\trentm\\AppData\\Local\\Acme\\SuperApp\\Cache' + >>> user_log_dir(appname, appauthor) + 'C:\\Users\\trentm\\AppData\\Local\\Acme\\SuperApp\\Logs' + +On Linux:: + + >>> from appdirs import * + >>> appname = "SuperApp" + >>> appauthor = "Acme" + >>> user_data_dir(appname, appauthor) + '/home/trentm/.local/share/SuperApp + >>> site_data_dir(appname, appauthor) + '/usr/local/share/SuperApp' + >>> site_data_dir(appname, appauthor, multipath=True) + '/usr/local/share/SuperApp:/usr/share/SuperApp' + >>> user_cache_dir(appname, appauthor) + '/home/trentm/.cache/SuperApp' + >>> user_log_dir(appname, appauthor) + '/home/trentm/.cache/SuperApp/log' + >>> user_config_dir(appname) + '/home/trentm/.config/SuperApp' + >>> site_config_dir(appname) + '/etc/xdg/SuperApp' + >>> os.environ['XDG_CONFIG_DIRS'] = '/etc:/usr/local/etc' + >>> site_config_dir(appname, multipath=True) + '/etc/SuperApp:/usr/local/etc/SuperApp' + + +``AppDirs`` for convenience +=========================== + +:: + + >>> from appdirs import AppDirs + >>> dirs = AppDirs("SuperApp", "Acme") + >>> dirs.user_data_dir + '/Users/trentm/Library/Application Support/SuperApp' + >>> dirs.site_data_dir + '/Library/Application Support/SuperApp' + >>> dirs.user_cache_dir + '/Users/trentm/Library/Caches/SuperApp' + >>> dirs.user_log_dir + '/Users/trentm/Library/Logs/SuperApp' + + + +Per-version isolation +===================== + +If you have multiple versions of your app in use that you want to be +able to run side-by-side, then you may want version-isolation for these +dirs:: + + >>> from appdirs import AppDirs + >>> dirs = AppDirs("SuperApp", "Acme", version="1.0") + >>> dirs.user_data_dir + '/Users/trentm/Library/Application Support/SuperApp/1.0' + >>> dirs.site_data_dir + '/Library/Application Support/SuperApp/1.0' + >>> dirs.user_cache_dir + '/Users/trentm/Library/Caches/SuperApp/1.0' + >>> dirs.user_log_dir + '/Users/trentm/Library/Logs/SuperApp/1.0' + + + +appdirs Changelog +================= + +appdirs 1.4.4 +------------- +- [PR #92] Don't import appdirs from setup.py + +Project officially classified as Stable which is important +for inclusion in other distros such as ActivePython. + +First of several incremental releases to catch up on maintenance. + +appdirs 1.4.3 +------------- +- [PR #76] Python 3.6 invalid escape sequence deprecation fixes +- Fix for Python 3.6 support + +appdirs 1.4.2 +------------- +- [PR #84] Allow installing without setuptools +- [PR #86] Fix string delimiters in setup.py description +- Add Python 3.6 support + +appdirs 1.4.1 +------------- +- [issue #38] Fix _winreg import on Windows Py3 +- [issue #55] Make appname optional + +appdirs 1.4.0 +------------- +- [PR #42] AppAuthor is now optional on Windows +- [issue 41] Support Jython on Windows, Mac, and Unix-like platforms. Windows + support requires `JNA `_. +- [PR #44] Fix incorrect behaviour of the site_config_dir method + +appdirs 1.3.0 +------------- +- [Unix, issue 16] Conform to XDG standard, instead of breaking it for + everybody +- [Unix] Removes gratuitous case mangling of the case, since \*nix-es are + usually case sensitive, so mangling is not wise +- [Unix] Fixes the utterly wrong behaviour in ``site_data_dir``, return result + based on XDG_DATA_DIRS and make room for respecting the standard which + specifies XDG_DATA_DIRS is a multiple-value variable +- [Issue 6] Add ``*_config_dir`` which are distinct on nix-es, according to + XDG specs; on Windows and Mac return the corresponding ``*_data_dir`` + +appdirs 1.2.0 +------------- + +- [Unix] Put ``user_log_dir`` under the *cache* dir on Unix. Seems to be more + typical. +- [issue 9] Make ``unicode`` work on py3k. + +appdirs 1.1.0 +------------- + +- [issue 4] Add ``AppDirs.user_log_dir``. +- [Unix, issue 2, issue 7] appdirs now conforms to `XDG base directory spec + `_. +- [Mac, issue 5] Fix ``site_data_dir()`` on Mac. +- [Mac] Drop use of 'Carbon' module in favour of hardcoded paths; supports + Python3 now. +- [Windows] Append "Cache" to ``user_cache_dir`` on Windows by default. Use + ``opinion=False`` option to disable this. +- Add ``appdirs.AppDirs`` convenience class. Usage: + + >>> dirs = AppDirs("SuperApp", "Acme", version="1.0") + >>> dirs.user_data_dir + '/Users/trentm/Library/Application Support/SuperApp/1.0' + +- [Windows] Cherry-pick Komodo's change to downgrade paths to the Windows short + paths if there are high bit chars. +- [Linux] Change default ``user_cache_dir()`` on Linux to be singular, e.g. + "~/.superapp/cache". +- [Windows] Add ``roaming`` option to ``user_data_dir()`` (for use on Windows only) + and change the default ``user_data_dir`` behaviour to use a *non*-roaming + profile dir (``CSIDL_LOCAL_APPDATA`` instead of ``CSIDL_APPDATA``). Why? Because + a large roaming profile can cause login speed issues. The "only syncs on + logout" behaviour can cause surprises in appdata info. + + +appdirs 1.0.1 (never released) +------------------------------ + +Started this changelog 27 July 2010. Before that this module originated in the +`Komodo `_ product as ``applib.py`` and then +as `applib/location.py +`_ (used by +`PyPM `_ in `ActivePython +`_). This is basically a fork of +applib.py 1.0.1 and applib/location.py 1.0.1. + diff --git a/libs/appdirs-1.4.4.dist-info/RECORD b/libs/appdirs-1.4.4.dist-info/RECORD new file mode 100644 index 000000000..439f34a9a --- /dev/null +++ b/libs/appdirs-1.4.4.dist-info/RECORD @@ -0,0 +1,8 @@ +appdirs-1.4.4.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +appdirs-1.4.4.dist-info/LICENSE.txt,sha256=Nt200KdFqTqyAyA9cZCBSxuJcn0lTK_0jHp6-71HAAs,1097 +appdirs-1.4.4.dist-info/METADATA,sha256=2kntSNh0a5RTW7_nUoezsk50bHfrXp7iQSNNEWM8HGk,8991 +appdirs-1.4.4.dist-info/RECORD,, +appdirs-1.4.4.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +appdirs-1.4.4.dist-info/WHEEL,sha256=P2T-6epvtXQ2cBOE_U1K4_noqlJFN3tj15djMgEu4NM,110 +appdirs-1.4.4.dist-info/top_level.txt,sha256=nKncE8CUqZERJ6VuQWL4_bkunSPDNfn7KZqb4Tr5YEM,8 +appdirs.py,sha256=g99s2sXhnvTEm79oj4bWI0Toapc-_SmKKNXvOXHkVic,24720 diff --git a/libs/appdirs-1.4.4.dist-info/REQUESTED b/libs/appdirs-1.4.4.dist-info/REQUESTED new file mode 100644 index 000000000..e69de29bb diff --git a/libs/appdirs-1.4.4.dist-info/WHEEL b/libs/appdirs-1.4.4.dist-info/WHEEL new file mode 100644 index 000000000..f31e450fd --- /dev/null +++ b/libs/appdirs-1.4.4.dist-info/WHEEL @@ -0,0 +1,6 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.41.3) +Root-Is-Purelib: true +Tag: py2-none-any +Tag: py3-none-any + diff --git a/libs/appdirs-1.4.4.dist-info/top_level.txt b/libs/appdirs-1.4.4.dist-info/top_level.txt new file mode 100644 index 000000000..d64bc321a --- /dev/null +++ b/libs/appdirs-1.4.4.dist-info/top_level.txt @@ -0,0 +1 @@ +appdirs diff --git a/libs/apprise-1.7.2.dist-info/INSTALLER b/libs/apprise-1.7.2.dist-info/INSTALLER new file mode 100644 index 000000000..a1b589e38 --- /dev/null +++ b/libs/apprise-1.7.2.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/libs/apprise-1.7.2.dist-info/LICENSE b/libs/apprise-1.7.2.dist-info/LICENSE new file mode 100644 index 000000000..f9154fefe --- /dev/null +++ b/libs/apprise-1.7.2.dist-info/LICENSE @@ -0,0 +1,25 @@ +BSD 2-Clause License + +Copyright (c) 2024, Chris Caron +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/libs/apprise-1.7.2.dist-info/METADATA b/libs/apprise-1.7.2.dist-info/METADATA new file mode 100644 index 000000000..9d4e2f391 --- /dev/null +++ b/libs/apprise-1.7.2.dist-info/METADATA @@ -0,0 +1,580 @@ +Metadata-Version: 2.1 +Name: apprise +Version: 1.7.2 +Summary: Push Notifications that work with just about every platform! +Home-page: https://github.com/caronc/apprise +Author: Chris Caron +Author-email: lead2gold@gmail.com +License: BSD +Keywords: Alerts Apprise API Automated Packet Reporting System AWS Boxcar BulkSMS BulkVS Burst SMS Chat CLI ClickSend D7Networks Dapnet DBus DingTalk Discord Email Emby Enigma2 Faast FCM Flock Form Gnome Google Chat Gotify Growl Guilded Home Assistant httpSMS IFTTT Join JSON Kavenegar KODI Kumulos LaMetric Line MacOSX Mailgun Mastodon Matrix Mattermost MessageBird Microsoft Misskey MQTT MSG91 MSTeams Nextcloud NextcloudTalk Notica Notifiarr Notifico Ntfy Office365 OneSignal Opsgenie PagerDuty PagerTree ParsePlatform PopcornNotify Prowl PushBullet Pushed Pushjet PushMe Push Notifications Pushover PushSafer Pushy PushDeer Reddit Rocket.Chat RSyslog Ryver SendGrid ServerChan SES Signal SimplePush Sinch Slack SMSEagle SMS Manager SMTP2Go SNS SparkPost Streamlabs Stride Synology Chat Syslog Techulus Telegram Threema Gateway Twilio Twist Twitter Voipms Vonage Webex WeCom Bot WhatsApp Windows XBMC XML Zulip +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: Intended Audience :: System Administrators +Classifier: Operating System :: OS Independent +Classifier: Natural Language :: English +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: PyPy +Classifier: License :: OSI Approved :: BSD License +Classifier: Topic :: Software Development :: Libraries :: Python Modules +Classifier: Topic :: Software Development :: Libraries :: Application Frameworks +Requires-Python: >=3.6 +Description-Content-Type: text/markdown +License-File: LICENSE +Requires-Dist: certifi +Requires-Dist: requests +Requires-Dist: requests-oauthlib +Requires-Dist: click >=5.0 +Requires-Dist: markdown +Requires-Dist: PyYAML +Requires-Dist: dataclasses ; python_version < "3.7" + +![Apprise Logo](https://raw.githubusercontent.com/caronc/apprise/master/apprise/assets/themes/default/apprise-logo.png) + +
+ +**ap·prise** / *verb*
+To inform or tell (someone). To make one aware of something. +
+ +*Apprise* allows you to send a notification to *almost* all of the most popular *notification* services available to us today such as: Telegram, Discord, Slack, Amazon SNS, Gotify, etc. + +* One notification library to rule them all. +* A common and intuitive notification syntax. +* Supports the handling of images and attachments (_to the notification services that will accept them_). +* It's incredibly lightweight. +* Amazing response times because all messages sent asynchronously. + +Developers who wish to provide a notification service no longer need to research each and every one out there. They no longer need to try to adapt to the new ones that comeout thereafter. They just need to include this one library and then they can immediately gain access to almost all of the notifications services available to us today. + +System Administrators and DevOps who wish to send a notification now no longer need to find the right tool for the job. Everything is already wrapped and supported within the `apprise` command line tool (CLI) that ships with this product. + +[![Paypal](https://img.shields.io/badge/paypal-donate-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=MHANV39UZNQ5E) +[![Follow](https://img.shields.io/twitter/follow/l2gnux)](https://twitter.com/l2gnux/)
+[![Discord](https://img.shields.io/discord/558793703356104724.svg?colorB=7289DA&label=Discord&logo=Discord&logoColor=7289DA&style=flat-square)](https://discord.gg/MMPeN2D) +[![Python](https://img.shields.io/pypi/pyversions/apprise.svg?style=flat-square)](https://pypi.org/project/apprise/) +[![Build Status](https://github.com/caronc/apprise/actions/workflows/tests.yml/badge.svg)](https://github.com/caronc/apprise/actions/workflows/tests.yml) +[![CodeCov Status](https://codecov.io/github/caronc/apprise/branch/master/graph/badge.svg)](https://codecov.io/github/caronc/apprise) +[![PyPi](https://img.shields.io/pypi/dm/apprise.svg?style=flat-square)](https://pypi.org/project/apprise/) + +# Table of Contents + +* [Supported Notifications](#supported-notifications) + * [Productivity Based Notifications](#productivity-based-notifications) + * [SMS Notifications](#sms-notifications) + * [Desktop Notifications](#desktop-notifications) + * [Email Notifications](#email-notifications) + * [Custom Notifications](#custom-notifications) +* [Installation](#installation) +* [Command Line Usage](#command-line-usage) + * [Configuration Files](#cli-configuration-files) + * [File Attachments](#cli-file-attachments) + * [Loading Custom Notifications/Hooks](#cli-loading-custom-notificationshooks) +* [Developer API Usage](#developer-api-usage) + * [Configuration Files](#api-configuration-files) + * [File Attachments](#api-file-attachments) + * [Loading Custom Notifications/Hooks](#api-loading-custom-notificationshooks) +* [More Supported Links and Documentation](#want-to-learn-more) + + +# Supported Notifications + +The section identifies all of the services supported by this library. [Check out the wiki for more information on the supported modules here](https://github.com/caronc/apprise/wiki). + +## Productivity Based Notifications + +The table below identifies the services this tool supports and some example service urls you need to use in order to take advantage of it. Click on any of the services listed below to get more details on how you can configure Apprise to access them. + +| Notification Service | Service ID | Default Port | Example Syntax | +| -------------------- | ---------- | ------------ | -------------- | +| [Apprise API](https://github.com/caronc/apprise/wiki/Notify_apprise_api) | apprise:// or apprises:// | (TCP) 80 or 443 | apprise://hostname/Token +| [AWS SES](https://github.com/caronc/apprise/wiki/Notify_ses) | ses:// | (TCP) 443 | ses://user@domain/AccessKeyID/AccessSecretKey/RegionName
ses://user@domain/AccessKeyID/AccessSecretKey/RegionName/email1/email2/emailN +| [Bark](https://github.com/caronc/apprise/wiki/Notify_bark) | bark:// | (TCP) 80 or 443 | bark://hostname
bark://hostname/device_key
bark://hostname/device_key1/device_key2/device_keyN
barks://hostname
barks://hostname/device_key
barks://hostname/device_key1/device_key2/device_keyN +| [Boxcar](https://github.com/caronc/apprise/wiki/Notify_boxcar) | boxcar:// | (TCP) 443 | boxcar://hostname
boxcar://hostname/@tag
boxcar://hostname/device_token
boxcar://hostname/device_token1/device_token2/device_tokenN
boxcar://hostname/@tag/@tag2/device_token +| [Discord](https://github.com/caronc/apprise/wiki/Notify_discord) | discord:// | (TCP) 443 | discord://webhook_id/webhook_token
discord://avatar@webhook_id/webhook_token +| [Emby](https://github.com/caronc/apprise/wiki/Notify_emby) | emby:// or embys:// | (TCP) 8096 | emby://user@hostname/
emby://user:password@hostname +| [Enigma2](https://github.com/caronc/apprise/wiki/Notify_enigma2) | enigma2:// or enigma2s:// | (TCP) 80 or 443 | enigma2://hostname +| [Faast](https://github.com/caronc/apprise/wiki/Notify_faast) | faast:// | (TCP) 443 | faast://authorizationtoken +| [FCM](https://github.com/caronc/apprise/wiki/Notify_fcm) | fcm:// | (TCP) 443 | fcm://project@apikey/DEVICE_ID
fcm://project@apikey/#TOPIC
fcm://project@apikey/DEVICE_ID1/#topic1/#topic2/DEVICE_ID2/ +| [Flock](https://github.com/caronc/apprise/wiki/Notify_flock) | flock:// | (TCP) 443 | flock://token
flock://botname@token
flock://app_token/u:userid
flock://app_token/g:channel_id
flock://app_token/u:userid/g:channel_id +| [Google Chat](https://github.com/caronc/apprise/wiki/Notify_googlechat) | gchat:// | (TCP) 443 | gchat://workspace/key/token +| [Gotify](https://github.com/caronc/apprise/wiki/Notify_gotify) | gotify:// or gotifys:// | (TCP) 80 or 443 | gotify://hostname/token
gotifys://hostname/token?priority=high +| [Growl](https://github.com/caronc/apprise/wiki/Notify_growl) | growl:// | (UDP) 23053 | growl://hostname
growl://hostname:portno
growl://password@hostname
growl://password@hostname:port
**Note**: you can also use the get parameter _version_ which can allow the growl request to behave using the older v1.x protocol. An example would look like: growl://hostname?version=1 +| [Guilded](https://github.com/caronc/apprise/wiki/Notify_guilded) | guilded:// | (TCP) 443 | guilded://webhook_id/webhook_token
guilded://avatar@webhook_id/webhook_token +| [Home Assistant](https://github.com/caronc/apprise/wiki/Notify_homeassistant) | hassio:// or hassios:// | (TCP) 8123 or 443 | hassio://hostname/accesstoken
hassio://user@hostname/accesstoken
hassio://user:password@hostname:port/accesstoken
hassio://hostname/optional/path/accesstoken +| [IFTTT](https://github.com/caronc/apprise/wiki/Notify_ifttt) | ifttt:// | (TCP) 443 | ifttt://webhooksID/Event
ifttt://webhooksID/Event1/Event2/EventN
ifttt://webhooksID/Event1/?+Key=Value
ifttt://webhooksID/Event1/?-Key=value1 +| [Join](https://github.com/caronc/apprise/wiki/Notify_join) | join:// | (TCP) 443 | join://apikey/device
join://apikey/device1/device2/deviceN/
join://apikey/group
join://apikey/groupA/groupB/groupN
join://apikey/DeviceA/groupA/groupN/DeviceN/ +| [KODI](https://github.com/caronc/apprise/wiki/Notify_kodi) | kodi:// or kodis:// | (TCP) 8080 or 443 | kodi://hostname
kodi://user@hostname
kodi://user:password@hostname:port +| [Kumulos](https://github.com/caronc/apprise/wiki/Notify_kumulos) | kumulos:// | (TCP) 443 | kumulos://apikey/serverkey +| [LaMetric Time](https://github.com/caronc/apprise/wiki/Notify_lametric) | lametric:// | (TCP) 443 | lametric://apikey@device_ipaddr
lametric://apikey@hostname:port
lametric://client_id@client_secret +| [Line](https://github.com/caronc/apprise/wiki/Notify_line) | line:// | (TCP) 443 | line://Token@User
line://Token/User1/User2/UserN +| [Mailgun](https://github.com/caronc/apprise/wiki/Notify_mailgun) | mailgun:// | (TCP) 443 | mailgun://user@hostname/apikey
mailgun://user@hostname/apikey/email
mailgun://user@hostname/apikey/email1/email2/emailN
mailgun://user@hostname/apikey/?name="From%20User" +| [Mastodon](https://github.com/caronc/apprise/wiki/Notify_mastodon) | mastodon:// or mastodons://| (TCP) 80 or 443 | mastodon://access_key@hostname
mastodon://access_key@hostname/@user
mastodon://access_key@hostname/@user1/@user2/@userN +| [Matrix](https://github.com/caronc/apprise/wiki/Notify_matrix) | matrix:// or matrixs:// | (TCP) 80 or 443 | matrix://hostname
matrix://user@hostname
matrixs://user:pass@hostname:port/#room_alias
matrixs://user:pass@hostname:port/!room_id
matrixs://user:pass@hostname:port/#room_alias/!room_id/#room2
matrixs://token@hostname:port/?webhook=matrix
matrix://user:token@hostname/?webhook=slack&format=markdown +| [Mattermost](https://github.com/caronc/apprise/wiki/Notify_mattermost) | mmost:// or mmosts:// | (TCP) 8065 | mmost://hostname/authkey
mmost://hostname:80/authkey
mmost://user@hostname:80/authkey
mmost://hostname/authkey?channel=channel
mmosts://hostname/authkey
mmosts://user@hostname/authkey
+| [Microsoft Teams](https://github.com/caronc/apprise/wiki/Notify_msteams) | msteams:// | (TCP) 443 | msteams://TokenA/TokenB/TokenC/ +| [Misskey](https://github.com/caronc/apprise/wiki/Notify_misskey) | misskey:// or misskeys://| (TCP) 80 or 443 | misskey://access_token@hostname +| [MQTT](https://github.com/caronc/apprise/wiki/Notify_mqtt) | mqtt:// or mqtts:// | (TCP) 1883 or 8883 | mqtt://hostname/topic
mqtt://user@hostname/topic
mqtts://user:pass@hostname:9883/topic +| [Nextcloud](https://github.com/caronc/apprise/wiki/Notify_nextcloud) | ncloud:// or nclouds:// | (TCP) 80 or 443 | ncloud://adminuser:pass@host/User
nclouds://adminuser:pass@host/User1/User2/UserN +| [NextcloudTalk](https://github.com/caronc/apprise/wiki/Notify_nextcloudtalk) | nctalk:// or nctalks:// | (TCP) 80 or 443 | nctalk://user:pass@host/RoomId
nctalks://user:pass@host/RoomId1/RoomId2/RoomIdN +| [Notica](https://github.com/caronc/apprise/wiki/Notify_notica) | notica:// | (TCP) 443 | notica://Token/ +| [Notifiarr](https://github.com/caronc/apprise/wiki/Notify_notifiarr) | notifiarr:// | (TCP) 443 | notifiarr://apikey/#channel
notifiarr://apikey/#channel1/#channel2/#channeln +| [Notifico](https://github.com/caronc/apprise/wiki/Notify_notifico) | notifico:// | (TCP) 443 | notifico://ProjectID/MessageHook/ +| [ntfy](https://github.com/caronc/apprise/wiki/Notify_ntfy) | ntfy:// | (TCP) 80 or 443 | ntfy://topic/
ntfys://topic/ +| [Office 365](https://github.com/caronc/apprise/wiki/Notify_office365) | o365:// | (TCP) 443 | o365://TenantID:AccountEmail/ClientID/ClientSecret
o365://TenantID:AccountEmail/ClientID/ClientSecret/TargetEmail
o365://TenantID:AccountEmail/ClientID/ClientSecret/TargetEmail1/TargetEmail2/TargetEmailN +| [OneSignal](https://github.com/caronc/apprise/wiki/Notify_onesignal) | onesignal:// | (TCP) 443 | onesignal://AppID@APIKey/PlayerID
onesignal://TemplateID:AppID@APIKey/UserID
onesignal://AppID@APIKey/#IncludeSegment
onesignal://AppID@APIKey/Email +| [Opsgenie](https://github.com/caronc/apprise/wiki/Notify_opsgenie) | opsgenie:// | (TCP) 443 | opsgenie://APIKey
opsgenie://APIKey/UserID
opsgenie://APIKey/#Team
opsgenie://APIKey/\*Schedule
opsgenie://APIKey/^Escalation +| [PagerDuty](https://github.com/caronc/apprise/wiki/Notify_pagerduty) | pagerduty:// | (TCP) 443 | pagerduty://IntegrationKey@ApiKey
pagerduty://IntegrationKey@ApiKey/Source/Component +| [PagerTree](https://github.com/caronc/apprise/wiki/Notify_pagertree) | pagertree:// | (TCP) 443 | pagertree://integration_id +| [ParsePlatform](https://github.com/caronc/apprise/wiki/Notify_parseplatform) | parsep:// or parseps:// | (TCP) 80 or 443 | parsep://AppID:MasterKey@Hostname
parseps://AppID:MasterKey@Hostname +| [PopcornNotify](https://github.com/caronc/apprise/wiki/Notify_popcornnotify) | popcorn:// | (TCP) 443 | popcorn://ApiKey/ToPhoneNo
popcorn://ApiKey/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN/
popcorn://ApiKey/ToEmail
popcorn://ApiKey/ToEmail1/ToEmail2/ToEmailN/
popcorn://ApiKey/ToPhoneNo1/ToEmail1/ToPhoneNoN/ToEmailN +| [Prowl](https://github.com/caronc/apprise/wiki/Notify_prowl) | prowl:// | (TCP) 443 | prowl://apikey
prowl://apikey/providerkey +| [PushBullet](https://github.com/caronc/apprise/wiki/Notify_pushbullet) | pbul:// | (TCP) 443 | pbul://accesstoken
pbul://accesstoken/#channel
pbul://accesstoken/A_DEVICE_ID
pbul://accesstoken/email@address.com
pbul://accesstoken/#channel/#channel2/email@address.net/DEVICE +| [Pushjet](https://github.com/caronc/apprise/wiki/Notify_pushjet) | pjet:// or pjets:// | (TCP) 80 or 443 | pjet://hostname/secret
pjet://hostname:port/secret
pjets://secret@hostname/secret
pjets://hostname:port/secret +| [Push (Techulus)](https://github.com/caronc/apprise/wiki/Notify_techulus) | push:// | (TCP) 443 | push://apikey/ +| [Pushed](https://github.com/caronc/apprise/wiki/Notify_pushed) | pushed:// | (TCP) 443 | pushed://appkey/appsecret/
pushed://appkey/appsecret/#ChannelAlias
pushed://appkey/appsecret/#ChannelAlias1/#ChannelAlias2/#ChannelAliasN
pushed://appkey/appsecret/@UserPushedID
pushed://appkey/appsecret/@UserPushedID1/@UserPushedID2/@UserPushedIDN +| [PushMe](https://github.com/caronc/apprise/wiki/Notify_pushme) | pushme:// | (TCP) 443 | pushme://Token/ +| [Pushover](https://github.com/caronc/apprise/wiki/Notify_pushover) | pover:// | (TCP) 443 | pover://user@token
pover://user@token/DEVICE
pover://user@token/DEVICE1/DEVICE2/DEVICEN
**Note**: you must specify both your user_id and token +| [PushSafer](https://github.com/caronc/apprise/wiki/Notify_pushsafer) | psafer:// or psafers:// | (TCP) 80 or 443 | psafer://privatekey
psafers://privatekey/DEVICE
psafer://privatekey/DEVICE1/DEVICE2/DEVICEN +| [Pushy](https://github.com/caronc/apprise/wiki/Notify_pushy) | pushy:// | (TCP) 443 | pushy://apikey/DEVICE
pushy://apikey/DEVICE1/DEVICE2/DEVICEN
pushy://apikey/TOPIC
pushy://apikey/TOPIC1/TOPIC2/TOPICN +| [PushDeer](https://github.com/caronc/apprise/wiki/Notify_pushdeer) | pushdeer:// or pushdeers:// | (TCP) 80 or 443 | pushdeer://pushKey
pushdeer://hostname/pushKey
pushdeer://hostname:port/pushKey +| [Reddit](https://github.com/caronc/apprise/wiki/Notify_reddit) | reddit:// | (TCP) 443 | reddit://user:password@app_id/app_secret/subreddit
reddit://user:password@app_id/app_secret/sub1/sub2/subN +| [Rocket.Chat](https://github.com/caronc/apprise/wiki/Notify_rocketchat) | rocket:// or rockets:// | (TCP) 80 or 443 | rocket://user:password@hostname/RoomID/Channel
rockets://user:password@hostname:443/#Channel1/#Channel1/RoomID
rocket://user:password@hostname/#Channel
rocket://webhook@hostname
rockets://webhook@hostname/@User/#Channel +| [RSyslog](https://github.com/caronc/apprise/wiki/Notify_rsyslog) | rsyslog:// | (UDP) 514 | rsyslog://hostname
rsyslog://hostname/Facility +| [Ryver](https://github.com/caronc/apprise/wiki/Notify_ryver) | ryver:// | (TCP) 443 | ryver://Organization/Token
ryver://botname@Organization/Token +| [SendGrid](https://github.com/caronc/apprise/wiki/Notify_sendgrid) | sendgrid:// | (TCP) 443 | sendgrid://APIToken:FromEmail/
sendgrid://APIToken:FromEmail/ToEmail
sendgrid://APIToken:FromEmail/ToEmail1/ToEmail2/ToEmailN/ +| [ServerChan](https://github.com/caronc/apprise/wiki/Notify_serverchan) | schan:// | (TCP) 443 | schan://sendkey/ +| [Signal API](https://github.com/caronc/apprise/wiki/Notify_signal) | signal:// or signals:// | (TCP) 80 or 443 | signal://hostname:port/FromPhoneNo
signal://hostname:port/FromPhoneNo/ToPhoneNo
signal://hostname:port/FromPhoneNo/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN/ +| [SimplePush](https://github.com/caronc/apprise/wiki/Notify_simplepush) | spush:// | (TCP) 443 | spush://apikey
spush://salt:password@apikey
spush://apikey?event=Apprise +| [Slack](https://github.com/caronc/apprise/wiki/Notify_slack) | slack:// | (TCP) 443 | slack://TokenA/TokenB/TokenC/
slack://TokenA/TokenB/TokenC/Channel
slack://botname@TokenA/TokenB/TokenC/Channel
slack://user@TokenA/TokenB/TokenC/Channel1/Channel2/ChannelN +| [SMTP2Go](https://github.com/caronc/apprise/wiki/Notify_smtp2go) | smtp2go:// | (TCP) 443 | smtp2go://user@hostname/apikey
smtp2go://user@hostname/apikey/email
smtp2go://user@hostname/apikey/email1/email2/emailN
smtp2go://user@hostname/apikey/?name="From%20User" +| [Streamlabs](https://github.com/caronc/apprise/wiki/Notify_streamlabs) | strmlabs:// | (TCP) 443 | strmlabs://AccessToken/
strmlabs://AccessToken/?name=name&identifier=identifier&amount=0¤cy=USD +| [SparkPost](https://github.com/caronc/apprise/wiki/Notify_sparkpost) | sparkpost:// | (TCP) 443 | sparkpost://user@hostname/apikey
sparkpost://user@hostname/apikey/email
sparkpost://user@hostname/apikey/email1/email2/emailN
sparkpost://user@hostname/apikey/?name="From%20User" +| [Synology Chat](https://github.com/caronc/apprise/wiki/Notify_synology_chat) | synology:// or synologys:// | (TCP) 80 or 443 | synology://hostname/token
synology://hostname:port/token +| [Syslog](https://github.com/caronc/apprise/wiki/Notify_syslog) | syslog:// | n/a | syslog://
syslog://Facility +| [Telegram](https://github.com/caronc/apprise/wiki/Notify_telegram) | tgram:// | (TCP) 443 | tgram://bottoken/ChatID
tgram://bottoken/ChatID1/ChatID2/ChatIDN +| [Twitter](https://github.com/caronc/apprise/wiki/Notify_twitter) | twitter:// | (TCP) 443 | twitter://CKey/CSecret/AKey/ASecret
twitter://user@CKey/CSecret/AKey/ASecret
twitter://CKey/CSecret/AKey/ASecret/User1/User2/User2
twitter://CKey/CSecret/AKey/ASecret?mode=tweet +| [Twist](https://github.com/caronc/apprise/wiki/Notify_twist) | twist:// | (TCP) 443 | twist://pasword:login
twist://password:login/#channel
twist://password:login/#team:channel
twist://password:login/#team:channel1/channel2/#team3:channel +| [XBMC](https://github.com/caronc/apprise/wiki/Notify_xbmc) | xbmc:// or xbmcs:// | (TCP) 8080 or 443 | xbmc://hostname
xbmc://user@hostname
xbmc://user:password@hostname:port +| [Webex Teams (Cisco)](https://github.com/caronc/apprise/wiki/Notify_wxteams) | wxteams:// | (TCP) 443 | wxteams://Token +| [WeCom Bot](https://github.com/caronc/apprise/wiki/Notify_wecombot) | wecombot:// | (TCP) 443 | wecombot://BotKey +| [WhatsApp](https://github.com/caronc/apprise/wiki/Notify_whatsapp) | whatsapp:// | (TCP) 443 | whatsapp://AccessToken@FromPhoneID/ToPhoneNo
whatsapp://Template:AccessToken@FromPhoneID/ToPhoneNo +| [Zulip Chat](https://github.com/caronc/apprise/wiki/Notify_zulip) | zulip:// | (TCP) 443 | zulip://botname@Organization/Token
zulip://botname@Organization/Token/Stream
zulip://botname@Organization/Token/Email + +## SMS Notifications + +| Notification Service | Service ID | Default Port | Example Syntax | +| -------------------- | ---------- | ------------ | -------------- | +| [Automated Packet Reporting System (ARPS)](https://github.com/caronc/apprise/wiki/Notify_aprs) | aprs:// | (TCP) 10152 | aprs://user:pass@callsign
aprs://user:pass@callsign1/callsign2/callsignN +| [AWS SNS](https://github.com/caronc/apprise/wiki/Notify_sns) | sns:// | (TCP) 443 | sns://AccessKeyID/AccessSecretKey/RegionName/+PhoneNo
sns://AccessKeyID/AccessSecretKey/RegionName/+PhoneNo1/+PhoneNo2/+PhoneNoN
sns://AccessKeyID/AccessSecretKey/RegionName/Topic
sns://AccessKeyID/AccessSecretKey/RegionName/Topic1/Topic2/TopicN +| [BulkSMS](https://github.com/caronc/apprise/wiki/Notify_bulksms) | bulksms:// | (TCP) 443 | bulksms://user:password@ToPhoneNo
bulksms://User:Password@ToPhoneNo1/ToPhoneNo2/ToPhoneNoN/ +| [BulkVS](https://github.com/caronc/apprise/wiki/Notify_bulkvs) | bulkvs:// | (TCP) 443 | bulkvs://user:password@FromPhoneNo
bulkvs://user:password@FromPhoneNo/ToPhoneNo
bulkvs://user:password@FromPhoneNo/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN/ +| [Burst SMS](https://github.com/caronc/apprise/wiki/Notify_burst_sms) | burstsms:// | (TCP) 443 | burstsms://ApiKey:ApiSecret@FromPhoneNo/ToPhoneNo
burstsms://ApiKey:ApiSecret@FromPhoneNo/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN/ +| [Burst SMS](https://github.com/caronc/apprise/wiki/Notify_burst_sms) | burstsms:// | (TCP) 443 | burstsms://ApiKey:ApiSecret@FromPhoneNo/ToPhoneNo
burstsms://ApiKey:ApiSecret@FromPhoneNo/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN/ +| [ClickSend](https://github.com/caronc/apprise/wiki/Notify_clicksend) | clicksend:// | (TCP) 443 | clicksend://user:pass@PhoneNo
clicksend://user:pass@ToPhoneNo1/ToPhoneNo2/ToPhoneNoN +| [DAPNET](https://github.com/caronc/apprise/wiki/Notify_dapnet) | dapnet:// | (TCP) 80 | dapnet://user:pass@callsign
dapnet://user:pass@callsign1/callsign2/callsignN +| [D7 Networks](https://github.com/caronc/apprise/wiki/Notify_d7networks) | d7sms:// | (TCP) 443 | d7sms://token@PhoneNo
d7sms://token@ToPhoneNo1/ToPhoneNo2/ToPhoneNoN +| [DingTalk](https://github.com/caronc/apprise/wiki/Notify_dingtalk) | dingtalk:// | (TCP) 443 | dingtalk://token/
dingtalk://token/ToPhoneNo
dingtalk://token/ToPhoneNo1/ToPhoneNo2/ToPhoneNo1/ + [httpSMS](https://github.com/caronc/apprise/wiki/Notify_httpsms) | httpsms:// | (TCP) 443 | httpsms://ApiKey@FromPhoneNo
httpsms://ApiKey@FromPhoneNo/ToPhoneNo
httpsms://ApiKey@FromPhoneNo/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN/ +| [Kavenegar](https://github.com/caronc/apprise/wiki/Notify_kavenegar) | kavenegar:// | (TCP) 443 | kavenegar://ApiKey/ToPhoneNo
kavenegar://FromPhoneNo@ApiKey/ToPhoneNo
kavenegar://ApiKey/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN +| [MessageBird](https://github.com/caronc/apprise/wiki/Notify_messagebird) | msgbird:// | (TCP) 443 | msgbird://ApiKey/FromPhoneNo
msgbird://ApiKey/FromPhoneNo/ToPhoneNo
msgbird://ApiKey/FromPhoneNo/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN/ +| [MSG91](https://github.com/caronc/apprise/wiki/Notify_msg91) | msg91:// | (TCP) 443 | msg91://TemplateID@AuthKey/ToPhoneNo
msg91://TemplateID@AuthKey/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN/ +| [Signal API](https://github.com/caronc/apprise/wiki/Notify_signal) | signal:// or signals:// | (TCP) 80 or 443 | signal://hostname:port/FromPhoneNo
signal://hostname:port/FromPhoneNo/ToPhoneNo
signal://hostname:port/FromPhoneNo/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN/ +| [Sinch](https://github.com/caronc/apprise/wiki/Notify_sinch) | sinch:// | (TCP) 443 | sinch://ServicePlanId:ApiToken@FromPhoneNo
sinch://ServicePlanId:ApiToken@FromPhoneNo/ToPhoneNo
sinch://ServicePlanId:ApiToken@FromPhoneNo/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN/
sinch://ServicePlanId:ApiToken@ShortCode/ToPhoneNo
sinch://ServicePlanId:ApiToken@ShortCode/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN/ +| [SMSEagle](https://github.com/caronc/apprise/wiki/Notify_smseagle) | smseagle:// or smseagles:// | (TCP) 80 or 443 | smseagles://hostname:port/ToPhoneNo
smseagles://hostname:port/@ToContact
smseagles://hostname:port/#ToGroup
smseagles://hostname:port/ToPhoneNo1/#ToGroup/@ToContact/ + [SMS Manager](https://github.com/caronc/apprise/wiki/Notify_sms_manager) | smsmgr:// | (TCP) 443 | smsmgr://ApiKey@ToPhoneNo
smsmgr://ApiKey@ToPhoneNo1/ToPhoneNo2/ToPhoneNoN/ +| [Threema Gateway](https://github.com/caronc/apprise/wiki/Notify_threema) | threema:// | (TCP) 443 | threema://GatewayID@secret/ToPhoneNo
threema://GatewayID@secret/ToEmail
threema://GatewayID@secret/ToThreemaID/
threema://GatewayID@secret/ToEmail/ToThreemaID/ToPhoneNo/... +| [Twilio](https://github.com/caronc/apprise/wiki/Notify_twilio) | twilio:// | (TCP) 443 | twilio://AccountSid:AuthToken@FromPhoneNo
twilio://AccountSid:AuthToken@FromPhoneNo/ToPhoneNo
twilio://AccountSid:AuthToken@FromPhoneNo/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN/
twilio://AccountSid:AuthToken@FromPhoneNo/ToPhoneNo?apikey=Key
twilio://AccountSid:AuthToken@ShortCode/ToPhoneNo
twilio://AccountSid:AuthToken@ShortCode/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN/ +| [Voipms](https://github.com/caronc/apprise/wiki/Notify_voipms) | voipms:// | (TCP) 443 | voipms://password:email/FromPhoneNo
voipms://password:email/FromPhoneNo/ToPhoneNo
voipms://password:email/FromPhoneNo/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN/ +| [Vonage](https://github.com/caronc/apprise/wiki/Notify_nexmo) (formerly Nexmo) | nexmo:// | (TCP) 443 | nexmo://ApiKey:ApiSecret@FromPhoneNo
nexmo://ApiKey:ApiSecret@FromPhoneNo/ToPhoneNo
nexmo://ApiKey:ApiSecret@FromPhoneNo/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN/ + +## Desktop Notifications + +| Notification Service | Service ID | Default Port | Example Syntax | +| -------------------- | ---------- | ------------ | -------------- | +| [Linux DBus Notifications](https://github.com/caronc/apprise/wiki/Notify_dbus) | dbus://
qt://
glib://
kde:// | n/a | dbus://
qt://
glib://
kde:// +| [Linux Gnome Notifications](https://github.com/caronc/apprise/wiki/Notify_gnome) | gnome:// | n/a | gnome:// +| [MacOS X Notifications](https://github.com/caronc/apprise/wiki/Notify_macosx) | macosx:// | n/a | macosx:// +| [Windows Notifications](https://github.com/caronc/apprise/wiki/Notify_windows) | windows:// | n/a | windows:// + +## Email Notifications + +| Service ID | Default Port | Example Syntax | +| ---------- | ------------ | -------------- | +| [mailto://](https://github.com/caronc/apprise/wiki/Notify_email) | (TCP) 25 | mailto://userid:pass@domain.com
mailto://domain.com?user=userid&pass=password
mailto://domain.com:2525?user=userid&pass=password
mailto://user@gmail.com&pass=password
mailto://mySendingUsername:mySendingPassword@example.com?to=receivingAddress@example.com
mailto://userid:password@example.com?smtp=mail.example.com&from=noreply@example.com&name=no%20reply +| [mailtos://](https://github.com/caronc/apprise/wiki/Notify_email) | (TCP) 587 | mailtos://userid:pass@domain.com
mailtos://domain.com?user=userid&pass=password
mailtos://domain.com:465?user=userid&pass=password
mailtos://user@hotmail.com&pass=password
mailtos://mySendingUsername:mySendingPassword@example.com?to=receivingAddress@example.com
mailtos://userid:password@example.com?smtp=mail.example.com&from=noreply@example.com&name=no%20reply + +Apprise have some email services built right into it (such as yahoo, fastmail, hotmail, gmail, etc) that greatly simplify the mailto:// service. See more details [here](https://github.com/caronc/apprise/wiki/Notify_email). + +## Custom Notifications + +| Post Method | Service ID | Default Port | Example Syntax | +| -------------------- | ---------- | ------------ | -------------- | +| [Form](https://github.com/caronc/apprise/wiki/Notify_Custom_Form) | form:// or forms:// | (TCP) 80 or 443 | form://hostname
form://user@hostname
form://user:password@hostname:port
form://hostname/a/path/to/post/to +| [JSON](https://github.com/caronc/apprise/wiki/Notify_Custom_JSON) | json:// or jsons:// | (TCP) 80 or 443 | json://hostname
json://user@hostname
json://user:password@hostname:port
json://hostname/a/path/to/post/to +| [XML](https://github.com/caronc/apprise/wiki/Notify_Custom_XML) | xml:// or xmls:// | (TCP) 80 or 443 | xml://hostname
xml://user@hostname
xml://user:password@hostname:port
xml://hostname/a/path/to/post/to + +# Installation + +The easiest way is to install this package is from pypi: +```bash +pip install apprise +``` + +Apprise is also packaged as an RPM and available through [EPEL](https://docs.fedoraproject.org/en-US/epel/) supporting CentOS, Redhat, Rocky, Oracle Linux, etc. +```bash +# Follow instructions on https://docs.fedoraproject.org/en-US/epel +# to get your system connected up to EPEL and then: +# Redhat/CentOS 7.x users +yum install apprise + +# Redhat/CentOS 8.x+ and/or Fedora Users +dnf install apprise +``` + +You can also check out the [Graphical version of Apprise](https://github.com/caronc/apprise-api) to centralize your configuration and notifications through a managable webpage. + +# Command Line Usage + +A small command line interface (CLI) tool is also provided with this package called *apprise*. If you know the server urls you wish to notify, you can simply provide them all on the command line and send your notifications that way: +```bash +# Send a notification to as many servers as you want +# as you can easily chain one after another (the -vv provides some +# additional verbosity to help let you know what is going on): +apprise -vv -t 'my title' -b 'my notification body' \ + 'mailto://myemail:mypass@gmail.com' \ + 'pbul://o.gn5kj6nfhv736I7jC3cj3QLRiyhgl98b' + +# If you don't specify a --body (-b) then stdin is used allowing +# you to use the tool as part of your every day administration: +cat /proc/cpuinfo | apprise -vv -t 'cpu info' \ + 'mailto://myemail:mypass@gmail.com' + +# The title field is totally optional +uptime | apprise -vv \ + 'discord:///4174216298/JHMHI8qBe7bk2ZwO5U711o3dV_js' +``` + +## CLI Configuration Files + +No one wants to put their credentials out for everyone to see on the command line. No problem *apprise* also supports configuration files. It can handle both a specific [YAML format](https://github.com/caronc/apprise/wiki/config_yaml) or a very simple [TEXT format](https://github.com/caronc/apprise/wiki/config_text). You can also pull these configuration files via an HTTP query too! You can read more about the expected structure of the configuration files [here](https://github.com/caronc/apprise/wiki/config). +```bash +# By default if no url or configuration is specified apprise will attempt to load +# configuration files (if present) from: +# ~/.apprise +# ~/.apprise.yml +# ~/.config/apprise +# ~/.config/apprise.yml +# /etc/apprise +# /etc/apprise.yml + +# Also a subdirectory handling allows you to leverage plugins +# ~/.apprise/apprise +# ~/.apprise/apprise.yml +# ~/.config/apprise/apprise +# ~/.config/apprise/apprise.yml +# /etc/apprise/apprise +# /etc/apprise/apprise.yml + +# Windows users can store their default configuration files here: +# %APPDATA%/Apprise/apprise +# %APPDATA%/Apprise/apprise.yml +# %LOCALAPPDATA%/Apprise/apprise +# %LOCALAPPDATA%/Apprise/apprise.yml +# %ALLUSERSPROFILE%\Apprise\apprise +# %ALLUSERSPROFILE%\Apprise\apprise.yml +# %PROGRAMFILES%\Apprise\apprise +# %PROGRAMFILES%\Apprise\apprise.yml +# %COMMONPROGRAMFILES%\Apprise\apprise +# %COMMONPROGRAMFILES%\Apprise\apprise.yml + +# If you loaded one of those files, your command line gets really easy: +apprise -vv -t 'my title' -b 'my notification body' + +# If you want to deviate from the default paths or specify more than one, +# just specify them using the --config switch: +apprise -vv -t 'my title' -b 'my notification body' \ + --config=/path/to/my/config.yml + +# Got lots of configuration locations? No problem, you can specify them all: +# Apprise can even fetch the configuration from over a network! +apprise -vv -t 'my title' -b 'my notification body' \ + --config=/path/to/my/config.yml \ + --config=https://localhost/my/apprise/config +``` + +## CLI File Attachments + +Apprise also supports file attachments too! Specify as many attachments to a notification as you want. +```bash +# Send a funny image you found on the internet to a colleague: +apprise -vv --title 'Agile Joke' \ + --body 'Did you see this one yet?' \ + --attach https://i.redd.it/my2t4d2fx0u31.jpg \ + 'mailto://myemail:mypass@gmail.com' + +# Easily send an update from a critical server to your dev team +apprise -vv --title 'system crash' \ + --body 'I do not think Jim fixed the bug; see attached...' \ + --attach /var/log/myprogram.log \ + --attach /var/debug/core.2345 \ + --tag devteam +``` + +## CLI Loading Custom Notifications/Hooks + +To create your own custom `schema://` hook so that you can trigger your own custom code, +simply include the `@notify` decorator to wrap your function. +```python +from apprise.decorators import notify +# +# The below assumes you want to catch foobar:// calls: +# +@notify(on="foobar", name="My Custom Foobar Plugin") +def my_custom_notification_wrapper(body, title, notify_type, *args, **kwargs): + """My custom notification function that triggers on all foobar:// calls + """ + # Write all of your code here... as an example... + print("{}: {} - {}".format(notify_type.upper(), title, body)) + + # Returning True/False is a way to relay your status back to Apprise. + # Returning nothing (None by default) is always interpreted as a Success +``` + +Once you've defined your custom hook, you just need to tell Apprise where it is at runtime. +```bash +# By default if no plugin path is specified apprise will attempt to load +# all plugin files (if present) from the following directory paths: +# ~/.apprise/plugins +# ~/.config/apprise/plugins +# /var/lib/apprise/plugins + +# Windows users can store their default plugin files in these directories: +# %APPDATA%/Apprise/plugins +# %LOCALAPPDATA%/Apprise/plugins +# %ALLUSERSPROFILE%\Apprise\plugins +# %PROGRAMFILES%\Apprise\plugins +# %COMMONPROGRAMFILES%\Apprise\plugins + +# If you placed your plugin file within one of the directories already defined +# above, then your call simply needs to look like: +apprise -vv --title 'custom override' \ + --body 'the body of my message' \ + foobar:\\ + +# However you can over-ride the path like so +apprise -vv --title 'custom override' \ + --body 'the body of my message' \ + --plugin-path /path/to/my/plugin.py \ + foobar:\\ +``` + +You can read more about creating your own custom notifications and/or hooks [here](https://github.com/caronc/apprise/wiki/decorator_notify). + +# Developer API Usage + +To send a notification from within your python application, just do the following: +```python +import apprise + +# Create an Apprise instance +apobj = apprise.Apprise() + +# Add all of the notification services by their server url. +# A sample email notification: +apobj.add('mailto://myuserid:mypass@gmail.com') + +# A sample pushbullet notification +apobj.add('pbul://o.gn5kj6nfhv736I7jC3cj3QLRiyhgl98b') + +# Then notify these services any time you desire. The below would +# notify all of the services loaded into our Apprise object. +apobj.notify( + body='what a great notification service!', + title='my notification title', +) +``` + +## API Configuration Files + +Developers need access to configuration files too. The good news is their use just involves declaring another object (called *AppriseConfig*) that the *Apprise* object can ingest. You can also freely mix and match config and notification entries as often as you wish! You can read more about the expected structure of the configuration files [here](https://github.com/caronc/apprise/wiki/config). +```python +import apprise + +# Create an Apprise instance +apobj = apprise.Apprise() + +# Create an Config instance +config = apprise.AppriseConfig() + +# Add a configuration source: +config.add('/path/to/my/config.yml') + +# Add another... +config.add('https://myserver:8080/path/to/config') + +# Make sure to add our config into our apprise object +apobj.add(config) + +# You can mix and match; add an entry directly if you want too +# In this entry we associate the 'admin' tag with our notification +apobj.add('mailto://myuser:mypass@hotmail.com', tag='admin') + +# Then notify these services any time you desire. The below would +# notify all of the services that have not been bound to any specific +# tag. +apobj.notify( + body='what a great notification service!', + title='my notification title', +) + +# Tagging allows you to specifically target only specific notification +# services you've loaded: +apobj.notify( + body='send a notification to our admin group', + title='Attention Admins', + # notify any services tagged with the 'admin' tag + tag='admin', +) + +# If you want to notify absolutely everything (regardless of whether +# it's been tagged or not), just use the reserved tag of 'all': +apobj.notify( + body='send a notification to our admin group', + title='Attention Admins', + # notify absolutely everything loaded, regardless on wether + # it has a tag associated with it or not: + tag='all', +) +``` + +## API File Attachments + +Attachments are very easy to send using the Apprise API: +```python +import apprise + +# Create an Apprise instance +apobj = apprise.Apprise() + +# Add at least one service you want to notify +apobj.add('mailto://myuser:mypass@hotmail.com') + +# Then send your attachment. +apobj.notify( + title='A great photo of our family', + body='The flash caused Jane to close her eyes! hah! :)', + attach='/local/path/to/my/DSC_003.jpg', +) + +# Send a web based attachment too! In the below example, we connect to a home +# security camera and send a live image to an email. By default remote web +# content is cached, but for a security camera we might want to call notify +# again later in our code, so we want our last image retrieved to expire(in +# this case after 3 seconds). +apobj.notify( + title='Latest security image', + attach='http://admin:password@hikvision-cam01/ISAPI/Streaming/channels/101/picture?cache=3' +) +``` + +To send more than one attachment, just use a list, set, or tuple instead: +```python +import apprise + +# Create an Apprise instance +apobj = apprise.Apprise() + +# Add at least one service you want to notify +apobj.add('mailto://myuser:mypass@hotmail.com') + +# Now add all of the entries we're interested in: +attach = ( + # ?name= allows us to rename the actual jpeg as found on the site + # to be another name when sent to our receipient(s) + 'https://i.redd.it/my2t4d2fx0u31.jpg?name=FlyingToMars.jpg', + + # Now add another: + '/path/to/funny/joke.gif', +) + +# Send your multiple attachments with a single notify call: +apobj.notify( + title='Some good jokes.', + body='Hey guys, check out these!', + attach=attach, +) +``` + +## API Loading Custom Notifications/Hooks + +By default, no custom plugins are loaded at all for those building from within the Apprise API. +It's at the developers discretion to load custom modules. But should you choose to do so, it's as easy +as including the path reference in the `AppriseAsset()` object prior to the initialization of your `Apprise()` +instance. + +For example: +```python +from apprise import Apprise +from apprise import AppriseAsset + +# Prepare your Asset object so that you can enable the custom plugins to +# be loaded for your instance of Apprise... +asset = AppriseAsset(plugin_paths="/path/to/scan") + +# OR You can also generate scan more then one file too: +asset = AppriseAsset( + plugin_paths=[ + # Iterate over all python libraries found in the root of the + # specified path. This is NOT a recursive (directory) scan; only + # the first level is parsed. HOWEVER, if a directory containing + # an __init__.py is found, it will be included in the load. + "/dir/containing/many/python/libraries", + + # An absolute path to a plugin.py to exclusively load + "/path/to/plugin.py", + + # if you point to a directory that has an __init__.py file found in + # it, then only that file is loaded (it's similar to point to a + # absolute .py file. Hence, there is no (level 1) scanning at all + # within the directory specified. + "/path/to/dir/library" + ] +) + +# Now that we've got our asset, we just work with our Apprise object as we +# normally do +aobj = Apprise(asset=asset) + +# If our new custom `foobar://` library was loaded (presuming we prepared +# one like in the examples above). then you would be able to safely add it +# into Apprise at this point +aobj.add('foobar://') + +# Send our notification out through our foobar:// +aobj.notify("test") +``` + +You can read more about creating your own custom notifications and/or hooks [here](https://github.com/caronc/apprise/wiki/decorator_notify). + +# Want To Learn More? + +If you're interested in reading more about this and other methods on how to customize your own notifications, please check out the following links: +* 📣 [Using the CLI](https://github.com/caronc/apprise/wiki/CLI_Usage) +* 🛠️ [Development API](https://github.com/caronc/apprise/wiki/Development_API) +* 🔧 [Troubleshooting](https://github.com/caronc/apprise/wiki/Troubleshooting) +* ⚙️ [Configuration File Help](https://github.com/caronc/apprise/wiki/config) +* ⚡ [Create Your Own Custom Notifications](https://github.com/caronc/apprise/wiki/decorator_notify) +* 🌎 [Apprise API/Web Interface](https://github.com/caronc/apprise-api) +* 🎉 [Showcase](https://github.com/caronc/apprise/wiki/showcase) + +Want to help make Apprise better? +* 💡 [Contribute to the Apprise Code Base](https://github.com/caronc/apprise/wiki/Development_Contribution) +* ❤️ [Sponsorship and Donations](https://github.com/caronc/apprise/wiki/Sponsors) diff --git a/libs/apprise-1.7.2.dist-info/RECORD b/libs/apprise-1.7.2.dist-info/RECORD new file mode 100644 index 000000000..335494660 --- /dev/null +++ b/libs/apprise-1.7.2.dist-info/RECORD @@ -0,0 +1,179 @@ +../../bin/apprise,sha256=ZJ-e4qqxNLtdW_DAvpuPPX5iROIiQd8I6nvg7vtAv-g,233 +apprise-1.7.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +apprise-1.7.2.dist-info/LICENSE,sha256=gt7qKBxRhVcdmXCYVtrWP6DtYjD0DzONet600dkU994,1343 +apprise-1.7.2.dist-info/METADATA,sha256=lNkOI_XF6axOtqkZLFfmVDiDGew_HtM2pfFDZyG62ME,43818 +apprise-1.7.2.dist-info/RECORD,, +apprise-1.7.2.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +apprise-1.7.2.dist-info/WHEEL,sha256=Xo9-1PvkuimrydujYJAjF7pCkriuXBpUPEjma1nZyJ0,92 +apprise-1.7.2.dist-info/entry_points.txt,sha256=71YypBuNdjAKiaLsiMG40HEfLHxkU4Mi7o_S0s0d8wI,45 +apprise-1.7.2.dist-info/top_level.txt,sha256=JrCRn-_rXw5LMKXkIgMSE4E0t1Ks9TYrBH54Pflwjkk,8 +apprise/Apprise.py,sha256=Stm2NhJprWRaMwQfTiIQG_nR1bLpHi_zcdwEcsCpa-A,32865 +apprise/Apprise.pyi,sha256=_4TBKvT-QVj3s6PuTh3YX-BbQMeJTdBGdVpubLMY4_k,2203 +apprise/AppriseAsset.py,sha256=jRW8Y1EcAvjVA9h_mINmsjO4DM3S0aDl6INIFVMcUCs,11647 +apprise/AppriseAsset.pyi,sha256=NYLXXYbScgRkspP27XGpRRM_uliPu1OCdWdZBPPvLng,979 +apprise/AppriseAttachment.py,sha256=vhrktSrp8GLr32aK4KqV6BX83IpI1lxZe-pGo1wiSFM,12540 +apprise/AppriseAttachment.pyi,sha256=R9-0dVqWpeaFrVpcREwPhGy3qHWztG5jEjYIOsbE5dM,1145 +apprise/AppriseConfig.py,sha256=wfuR6Mb3ZLHvjvqWdFp9lVmjjDRWs65unY9qa92RkCg,16909 +apprise/AppriseConfig.pyi,sha256=_mUlCnncqAq8sL01WxQTgZjnb2ic9kZXvtqZmVl-fc8,1568 +apprise/AppriseLocale.py,sha256=ISth7xC7M1WhsSNXdGZFouaA4bi07KP35m9RX-ExG48,8852 +apprise/AttachmentManager.py,sha256=EwlnjuKn3fv_pioWcmMCkyDTsO178t6vkEOD8AjAPsw,2053 +apprise/ConfigurationManager.py,sha256=MUmGajxjgnr6FGN7xb3q0nD0VVgdTdvapBBR7CsI-rc,2058 +apprise/NotificationManager.py,sha256=ZJgkiCgcJ7Bz_6bwQ47flrcxvLMbA4Vbw0HG_yTsGdE,2041 +apprise/URLBase.py,sha256=HgRiGXOCb4ZhTXmRved9VxfcX-eec3pII3Eb0zRh8Aw,28389 +apprise/URLBase.pyi,sha256=WLaRREH7FzZ5x3-qkDkupojWGFC4uFwJ1EDt02lVs8c,520 +apprise/__init__.py,sha256=cQvk-yABi1MGIYCxa9di1DYMMAl6IuI5BhbzfOt6NSY,3368 +apprise/assets/NotifyXML-1.0.xsd,sha256=292qQ_IUl5EWDhPyzm9UTT0C2rVvJkyGar8jiODkJs8,986 +apprise/assets/NotifyXML-1.1.xsd,sha256=bjR3CGG4AEXoJjYkGCbDttKHSkPP1FlIWO02E7G59g4,1758 +apprise/assets/themes/default/apprise-failure-128x128.ico,sha256=Mt0ptfHJaN3Wsv5UCNDn9_3lyEDHxVDv1JdaDEI_xCA,67646 +apprise/assets/themes/default/apprise-failure-128x128.png,sha256=66ps8TDPxVH3g9PlObJqF-0x952CjnqQyN3zvpRcOT8,16135 +apprise/assets/themes/default/apprise-failure-256x256.png,sha256=bQBsKKCsKfR9EqgYOZrcVcVa5y8qG58PN2mEqO5eNRI,41931 +apprise/assets/themes/default/apprise-failure-32x32.png,sha256=vH0pZffIDCvkejpr3fJHGXW__8Yc3R_p0bacX6t6l18,2437 +apprise/assets/themes/default/apprise-failure-72x72.png,sha256=EP5A8DHRDr9srgupFSwOoyQ308bNJ8aL192J_L4K-ec,7600 +apprise/assets/themes/default/apprise-info-128x128.ico,sha256=F5_CirmXueRCRI5Z_Crf6TS6jVIXTJlRD83zw1oJ66g,67646 +apprise/assets/themes/default/apprise-info-128x128.png,sha256=bBqRZAgQey-gkmJrnFhPbzjILSrljE59mRkgj3raMQo,16671 +apprise/assets/themes/default/apprise-info-256x256.png,sha256=B5r_O4d9MHCmSWZwfbqQgZSp-ZetTdiBSwKcMTF1aFA,43331 +apprise/assets/themes/default/apprise-info-32x32.png,sha256=lt3NZ95TzkiCNVNlurrB2fE2nriMa1wftl7nrNXmb6c,2485 +apprise/assets/themes/default/apprise-info-72x72.png,sha256=kDnsZpqNUZGqs9t1ECUup7FOfXUIL-rupnQCYJp9So4,7875 +apprise/assets/themes/default/apprise-logo.png,sha256=85ttALudKkLmiqilJT7mUQLUXRFmM1AK89rnwLm313s,160907 +apprise/assets/themes/default/apprise-success-128x128.ico,sha256=uCopPwdQjxgfohKazHaDzYs9y4oiaOpL048PYC6WRlg,67646 +apprise/assets/themes/default/apprise-success-128x128.png,sha256=nvDuU_QqhGlw6cMtdj7Mv-gPgqCEx-0DaaXn1KBLVYg,17446 +apprise/assets/themes/default/apprise-success-256x256.png,sha256=vXfKuxY3n0eeXHKdb9hTxICxOEn7HjAQ4IZpX0HSLzc,48729 +apprise/assets/themes/default/apprise-success-32x32.png,sha256=Jg9pFJh3YPI-LiPBebyJ7Z4Vt7BRecaE8AsRjQVIkME,2471 +apprise/assets/themes/default/apprise-success-72x72.png,sha256=FQbgvIhqKOhEK0yvrhaSpai0R7hrkTt_-GaC2KUgCCk,7858 +apprise/assets/themes/default/apprise-warning-128x128.ico,sha256=6XaQPOx0oWK_xbhr4Yhb7qNazCWwSs9lk2SYR2MHTrQ,67646 +apprise/assets/themes/default/apprise-warning-128x128.png,sha256=pf5c4Ph7jWH7gf39dJoieSj8TzAsY3TXI-sGISGVIW4,16784 +apprise/assets/themes/default/apprise-warning-256x256.png,sha256=SY-xlaiXaj420iEYKC2_fJxU-yj2SuaQg6xfPNi83bw,43708 +apprise/assets/themes/default/apprise-warning-32x32.png,sha256=97R2ywNvcwczhBoWEIgajVtWjgT8fLs4FCCz4wu0dwc,2472 +apprise/assets/themes/default/apprise-warning-72x72.png,sha256=L8moEInkO_OLxoOcuvN7rmrGZo64iJeH20o-24MQghE,7913 +apprise/attachment/AttachBase.py,sha256=ik3hRFnr8Z9bXt69P9Ej1VST4gQbnE0C_9WQvEE-72A,13592 +apprise/attachment/AttachBase.pyi,sha256=w0XG_QKauiMLJ7eQ4S57IiLIURZHm_Snw7l6-ih9GP8,961 +apprise/attachment/AttachFile.py,sha256=MbHY_av0GeM_AIBKV02Hq7SHiZ9eCr1yTfvDMUgi2I4,4765 +apprise/attachment/AttachHTTP.py,sha256=dyDy3U47cI28ENhaw1r5nQlGh8FWHZlHI8n9__k8wcY,11995 +apprise/attachment/__init__.py,sha256=xabgXpvV05X-YRuqIt3uGYMXwYNXjHyF6Dwd8HfZCFE,1658 +apprise/cli.py,sha256=fa-3beNKx3ZC3KkNwgJMMfs1LDI2Hjyol_bXp6WhK4s,19739 +apprise/common.py,sha256=I6wfrndggCL7l7KAl7Cm4uwAX9n0l3SN4-BVvTE0L0M,5593 +apprise/common.pyi,sha256=luF3QRiClDCk8Z23rI6FCGYsVmodOt_JYfYyzGogdNM,447 +apprise/config/ConfigBase.py,sha256=A4p_N9vSxOK37x9kuYeZFzHhAeEt-TCe2oweNi2KGg4,53062 +apprise/config/ConfigBase.pyi,sha256=cngfobwH6v2vxYbQrObDi5Z-t5wcquWF-wR0kBCr3Eg,54 +apprise/config/ConfigFile.py,sha256=u_SDaN3OHMyaAq2X7k_T4_PRKkVsDwleqBz9YIN5lbA,6138 +apprise/config/ConfigHTTP.py,sha256=Iy6Ji8_nX3xDjFgJGLrz4ftrMlMiyKiFGzYGJ7rMSMQ,9457 +apprise/config/ConfigMemory.py,sha256=epEAgNy-eJVWoQaUOvjivMWxXTofy6wAQ-NbCqYmuyE,2829 +apprise/config/__init__.py,sha256=lbsxrUpB1IYM2q7kjYhsXQGgPF-yZXJrKFE361tdIPY,1663 +apprise/conversion.py,sha256=bvTu-3TU2CPEhdroLRtd_XpDzzXqe_wyUql089IpYxs,6197 +apprise/decorators/CustomNotifyPlugin.py,sha256=F49vOM2EVy43Pn3j8z7tgTacweMUxGhw0UX-1n2Y3c8,7836 +apprise/decorators/__init__.py,sha256=e_PDAm0kQNzwDPx-NJZLPfLMd2VAABvNZtxx_iDviRM,1487 +apprise/decorators/notify.py,sha256=a2WupErNw1_SMAld7jPC273bskiChMpYy95BOog5A9w,5111 +apprise/emojis.py,sha256=ONF0t8dY9f2XlEkLUG79-ybKVAj2GqbPj2-Be97vAoI,87738 +apprise/i18n/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +apprise/i18n/en/LC_MESSAGES/apprise.mo,sha256=oUTuHREmLEYN07oqYqRMJ_kU71-o5o37NsF4RXlC5AU,3959 +apprise/logger.py,sha256=131hqhed8cUj9x_mfXDEvwA2YbcYDFAYiWVK1HgxRVY,6921 +apprise/manager.py,sha256=sJUNy6IttMVVS3D8Nqzab96dogmKJpNVOBVx93HrX7c,25526 +apprise/plugins/NotifyAppriseAPI.py,sha256=ISBE0brD3eQdyw3XrGXd4Uc4kSYvIuI3SSUVCt-bkdo,16654 +apprise/plugins/NotifyAprs.py,sha256=IS1uxIl391L3i2LOK6x8xmlOG1W58k4o793Oq2W5Wao,24220 +apprise/plugins/NotifyBark.py,sha256=bsDvKooRy4k1Gg7tvBjv3DIx7-WZiV_mbTrkTwMtd9Q,15698 +apprise/plugins/NotifyBase.py,sha256=9MB2uv4Rv8BnoXjU52k5Mv4YQppkNPv4Y_iPwauKxKQ,29716 +apprise/plugins/NotifyBase.pyi,sha256=aKlZXRYUgG8lz_ZgGkYYJ_GKhuf18youTmMU-FlG7z8,21 +apprise/plugins/NotifyBoxcar.py,sha256=vR00-WggHa1nHYWyb-f5P2V-G4f683fU_-GBlIeJvD0,12867 +apprise/plugins/NotifyBulkSMS.py,sha256=stPWAFCfhBP617zYK9Dgk6pNJBN_WcyJtODzo0jR1QQ,16005 +apprise/plugins/NotifyBulkVS.py,sha256=viLGeyUDiirRRM7CgRqqElHSLYFnMugDtWE6Ytjqfaw,13290 +apprise/plugins/NotifyBurstSMS.py,sha256=cN2kRETKIK5LhwpQEA8C68LKv8KEUPmXYe-nTSegGls,15550 +apprise/plugins/NotifyClickSend.py,sha256=UfOJqsas6WLjQskojuJE7I_-lrb5QrkMiBZv-po_Q9c,11229 +apprise/plugins/NotifyD7Networks.py,sha256=4E6Fh0kQoDlMMwgZJDOXky7c7KrdMMvqprcfm29scWU,15043 +apprise/plugins/NotifyDBus.py,sha256=1eVJHIL3XkFjDePMqfcll35Ie1vxggJ1iBsVFAIaF00,14379 +apprise/plugins/NotifyDapnet.py,sha256=KuXjBU0ZrIYtoDei85NeLZ-IP810T4w5oFXH9sWiSh0,13624 +apprise/plugins/NotifyDingTalk.py,sha256=NJyETgN6QjtRqtxQjfBLFVuFpURyWykRftm6WpQJVbY,12009 +apprise/plugins/NotifyDiscord.py,sha256=M_qmTzB7NNL5_agjYDX38KBN1jRzDBp2EMSNwEF_9Tw,26072 +apprise/plugins/NotifyEmail.py,sha256=q75KtPsvLIaa_0gH4-0ASV4KbE9VKDo3ssj_j7Z-fdk,38284 +apprise/plugins/NotifyEmby.py,sha256=OMVO8XsVl_XCBYNNNQi8ni2lS4voLfU8Puk1xJOAvHs,24039 +apprise/plugins/NotifyEnigma2.py,sha256=Hj0Q9YOeljSwbfiuMKLqXTVX_1g_mjNUGEts7wfrwno,11498 +apprise/plugins/NotifyFCM/__init__.py,sha256=mBFtIgIJuLIFnMB5ndx5Makjs9orVMc2oLoD7LaVT48,21669 +apprise/plugins/NotifyFCM/color.py,sha256=8iqDtadloQh2TMxkFmIFwenHqKp1pHHn1bwyWOzZ6TY,4592 +apprise/plugins/NotifyFCM/common.py,sha256=978uBUoNdtopCtylipGiKQdsQ8FTONxkFBp7uJMZHc8,1718 +apprise/plugins/NotifyFCM/oauth.py,sha256=Vvbd0-rd5BPIjAneG3rILU153JIzfSZ0kaDov6hm96M,11197 +apprise/plugins/NotifyFCM/priority.py,sha256=0WuRW1y1HVnybgjlTeCZPHzt7j8SwWnC7faNcjioAOc,8163 +apprise/plugins/NotifyFaast.py,sha256=_F1633tQhk8gCfaNpZZm808f2G0S6fP0OOEetSiv0h8,6972 +apprise/plugins/NotifyFlock.py,sha256=0rUIa9nToGsO8BTUgixh8Z_qdVixJeH479UNYjcE4EM,12748 +apprise/plugins/NotifyForm.py,sha256=38nL-2m1cf4gEQFQ4NpvA4j9i5_nNUgelReWFSjyV5U,17905 +apprise/plugins/NotifyGnome.py,sha256=8MXTa8gZg1wTgNJfLlmq7_fl3WaYK-SX6VR91u308C4,9059 +apprise/plugins/NotifyGoogleChat.py,sha256=lnoN17m6lZANaXcElDTP8lcuVWjIZEK8C6_iqJNAnw4,12622 +apprise/plugins/NotifyGotify.py,sha256=DNlOIHyuYitO5use9oa_REPm2Fant7y9QSaatrZFNI0,10551 +apprise/plugins/NotifyGrowl.py,sha256=M6ViUz967VhEHtXrE7lbCKF3aB4pIXNEzJLjjGAmvhM,14023 +apprise/plugins/NotifyGuilded.py,sha256=eCMCoFFuE0XNY8HlLM21zoxgBNgqEKQ8dwYj8LihfRU,3641 +apprise/plugins/NotifyHomeAssistant.py,sha256=zqWu7TtdXhTbGNuflC8WfydbHsCLiEBw4uBUcF7YZtw,10739 +apprise/plugins/NotifyHttpSMS.py,sha256=pDEUHCCB18IhOgDcVK3_FFDJdAcrdTIfPzj0jNnZZBo,11136 +apprise/plugins/NotifyIFTTT.py,sha256=oMvTQ0bEu2eJQgw9BwxAwTNOtbZ_ER-zleJvWpWTj7w,13425 +apprise/plugins/NotifyJSON.py,sha256=70ctjmArGzuvM1gHNt1bCiQVWE7Fp9vd2nWhSXwFvw0,13851 +apprise/plugins/NotifyJoin.py,sha256=B8FHp7cblZBkxTgfrka6mNnf6oQVBXVuGISgSau00z0,13581 +apprise/plugins/NotifyKavenegar.py,sha256=F5xTUdebM1lK6yGFbZJQB9Zgw2LTI0angeA-3Nu-89w,12620 +apprise/plugins/NotifyKumulos.py,sha256=eCEW2ZverZqETOLHVWMC4E8Ll6rEhhEWOSD73RD80SM,8214 +apprise/plugins/NotifyLametric.py,sha256=h8vZoX-Ll5NBZRprBlxTO2H9w0lOiMxglGvUgJtK4_8,37534 +apprise/plugins/NotifyLine.py,sha256=OVI0ozMJcq_-dI8dodVX52dzUzgENlAbOik-Kw4l-rI,10676 +apprise/plugins/NotifyMQTT.py,sha256=PFLwESgR8dMZvVFHxmOZ8xfy-YqyX5b2kl_e8Z1lo-0,19537 +apprise/plugins/NotifyMSG91.py,sha256=P7JPyT1xmucnaEeCZPf_6aJfe1gS_STYYwEM7hJ7QBw,12677 +apprise/plugins/NotifyMSTeams.py,sha256=dFH575hoLL3zRddbBKfozlYjxvPJGbj3BKvfJSIkvD0,22976 +apprise/plugins/NotifyMacOSX.py,sha256=1LlSjTxkm27btdXCE-rDn2FMGiyZmqlR5-HoXLxK7jM,8227 +apprise/plugins/NotifyMailgun.py,sha256=FNS_QLOQWMo62yVO-mMZkpiXudUtSdbHOjfSrLC4oIo,25409 +apprise/plugins/NotifyMastodon.py,sha256=2ovjQIOOITHH8lOinC8QCFCJN2QA8foIM2pjdknbblc,35277 +apprise/plugins/NotifyMatrix.py,sha256=I8kdaZUZS-drew0JExBbChQVe7Ib4EwAjQd0xE30XT0,50049 +apprise/plugins/NotifyMattermost.py,sha256=JgEc-wC-43FBMItezDJ62zv1Nc9ROFjDiwD_8bt8rgM,12722 +apprise/plugins/NotifyMessageBird.py,sha256=EUPwhs1PHiPZpluIrLiNKQMUPcdlKnx1sdnllCtN_Ns,12248 +apprise/plugins/NotifyMisskey.py,sha256=zYZkBKv0p3jJpm_HLDBugUgKeGb0qpLoPqy0ffwwxVg,9600 +apprise/plugins/NotifyNextcloud.py,sha256=M3EyvUzBMHbTKU3gxW_7fPA6vmQUF5x8GTMZQ78sWCA,12759 +apprise/plugins/NotifyNextcloudTalk.py,sha256=dLl_g7Knq5PVcadbzDuQsxbGHTZlC4r-pQC8wzYnmAo,11011 +apprise/plugins/NotifyNotica.py,sha256=yHmk8HiNFjzoI4Gewo_nBRrx9liEmhT95k1d10wqhYg,12990 +apprise/plugins/NotifyNotifiarr.py,sha256=ADwLJO9eenfLkNa09tXMGSBTM4c3zTY0SEePvyB8WYA,15857 +apprise/plugins/NotifyNotifico.py,sha256=Qe9jMN_M3GL4XlYIWkAf-w_Hf65g9Hde4bVuytGhUW4,12035 +apprise/plugins/NotifyNtfy.py,sha256=EiG7-z84XibAcWd0iANsU7nZofWEa9xQ6X1z8oc1ZGE,27789 +apprise/plugins/NotifyOffice365.py,sha256=8TxsVsdbUghmNj0kceMlmoZzTOKQTgn3priI8JuRuHE,25190 +apprise/plugins/NotifyOneSignal.py,sha256=gsw7ckW7xLiJDRUb7eJHNe_4bvdBXmt6_YsB1u_ghjw,18153 +apprise/plugins/NotifyOpsgenie.py,sha256=zJWpknjoHq35Iv9w88ucR62odaeIN3nrGFPtYnhDdjA,20515 +apprise/plugins/NotifyPagerDuty.py,sha256=lu6oNdygrs6UezYm6xgiQxQDeDz8EVUtfP-xsArRvyw,17874 +apprise/plugins/NotifyPagerTree.py,sha256=mPl6ejdelNlWUWGVs46kZT0VV4uFZoeCdcv4VJ_f_XQ,13849 +apprise/plugins/NotifyParsePlatform.py,sha256=6oFOTpu-HMhesaYXRBvu5oaESYlFrKBNYTHE-ItCBRk,10291 +apprise/plugins/NotifyPopcornNotify.py,sha256=kRstzG0tWBdxSRfn2RN2J7FhvIj2qYWlwUyLxxZCbPc,10587 +apprise/plugins/NotifyProwl.py,sha256=EGOdmiZq8CFbjxTtWWKLQEdYiSvr4czZfE_8aCMEokw,9782 +apprise/plugins/NotifyPushBullet.py,sha256=JVd2GQH-DWmPaKjuGBpsE6DXNCcZEUDH7tA5zbM1qEU,15372 +apprise/plugins/NotifyPushDeer.py,sha256=cG1UFG06PfzbmI1RxtrMqmfaHK_Ojk_W-QMEdtkEuUI,6922 +apprise/plugins/NotifyPushMe.py,sha256=ioRzeXbd2X5miTd3h3m7AwCqkIIfbXNm4PjYk0OOXZ0,7134 +apprise/plugins/NotifyPushSafer.py,sha256=hIcYHwUZapmC-VDvaO_UkDY9RSPTxHgF7m2FL-6JBZw,26756 +apprise/plugins/NotifyPushed.py,sha256=NqLMXD9gvihXLfLUtCcMfz5oUAhPM7sKXECqKgD0v-U,12270 +apprise/plugins/NotifyPushjet.py,sha256=8qWpIqM4dKWjO-BjOrRJXZYtvtJBt_mikdBWRxfibnE,8952 +apprise/plugins/NotifyPushover.py,sha256=MJDquV4zl1cNrGZOC55hLlt6lOb6625WeUcgS5ceCbk,21213 +apprise/plugins/NotifyPushy.py,sha256=mmWcnu905Fvc8ihYXvZ7lVYErGZH5Q-GbBNS20v5r48,12496 +apprise/plugins/NotifyRSyslog.py,sha256=W42LT90X65-pNoU7KdhdX1PBcmsz9RyV376CDa_H3CI,11982 +apprise/plugins/NotifyReddit.py,sha256=E78OSyDQfUalBEcg71sdMsNBOwdj7cVBnELrhrZEAXY,25785 +apprise/plugins/NotifyRocketChat.py,sha256=GTEfT-upQ56tJgE0kuc59l4uQGySj_d15wjdcARR9Ko,24624 +apprise/plugins/NotifyRyver.py,sha256=yhHPMLGeJtcHwBKSPPk0OBfp59DgTvXio1R59JhrJu4,11823 +apprise/plugins/NotifySES.py,sha256=wtRmpAZkS5mQma6sdiaPT6U1xcgoj77CB9mNFvSEAw8,33545 +apprise/plugins/NotifySMSEagle.py,sha256=voFNqOewD9OC1eRctD0YdUB_ZSWsb06rjUwBfCcxPYA,24161 +apprise/plugins/NotifySMSManager.py,sha256=DbVc35qLfYkNL7eq43_rPD6k-PELL9apf3S09S6qvDA,14125 +apprise/plugins/NotifySMTP2Go.py,sha256=foQ7aMMmNc5Oree8YwrxZJgMnF6yVMFAfqShm_nLbx0,19711 +apprise/plugins/NotifySNS.py,sha256=ZEBWf0ZJ9w_ftzUikKEvQWJ2fkxrUbrLhPmTRD2DvRQ,24159 +apprise/plugins/NotifySendGrid.py,sha256=IBdYmZcthkvGCz1N_Fs8vDnImtHug6LpuKv1mWT_Cdo,16213 +apprise/plugins/NotifyServerChan.py,sha256=WsrClO9f0xi-KpnLZGTUHV7PxeU3l1D875gvMaZRG_M,5779 +apprise/plugins/NotifySignalAPI.py,sha256=OwJ7qjJ-ZJyS8GS-dBWAtgizHMnGegg76GuwFobyWkw,16733 +apprise/plugins/NotifySimplePush.py,sha256=dUC6O8IGuUIAz5z6_H7A7jdv5Gj1plytNm5QyKnHAYg,10876 +apprise/plugins/NotifySinch.py,sha256=tmHLwQa9lWHEI3EcRfigl4i7JU46A6gKAi_GbY0PrX4,16813 +apprise/plugins/NotifySlack.py,sha256=3VdjruU5FPr3jT_s3axwRJKMcBYXP0lvJnyuKedIlcE,42521 +apprise/plugins/NotifySparkPost.py,sha256=6dRTwnYU50Lvmp6AlwCyePe0TMbVEXaSwNeGkg__EYo,27878 +apprise/plugins/NotifyStreamlabs.py,sha256=lx3N8T2ufUWFYIZ-kU_rOv50YyGWBqLSCKk7xim2_Io,16023 +apprise/plugins/NotifySynology.py,sha256=_jTqfgWeOuSi_I8geMOraHBVFtDkvm9mempzymrmeAo,11105 +apprise/plugins/NotifySyslog.py,sha256=J9Kain2bb-PDNiG5Ydb0q678cYjNE_NjZFqMG9oEXM0,10617 +apprise/plugins/NotifyTechulusPush.py,sha256=m43_Qj1scPcgCRX5Dr2Ul7nxMbaiVxNzm_HRuNmfgoA,7253 +apprise/plugins/NotifyTelegram.py,sha256=km4Izpx0SIP4f__R9_rVjdgUpJCXmM8KX8Tvl3FMqms,35630 +apprise/plugins/NotifyThreema.py,sha256=C_C3j0fJWgeF2uB7ceJFXOdC6Lt0TFBInFMs5Xlg04M,11885 +apprise/plugins/NotifyTwilio.py,sha256=WCo8eTI9OF1rtg3ueHHRDXt4Lp45eZ6h3IdTZVf5HM8,15976 +apprise/plugins/NotifyTwist.py,sha256=nZA73CYVe-p0tkVMy5q3vFRyflLM4yjUo9LECvkUwgc,28841 +apprise/plugins/NotifyTwitter.py,sha256=qML0jlBkLZMHrkKRxBpVUnBwAz8MWGYyI3cvwi-hrgM,30152 +apprise/plugins/NotifyVoipms.py,sha256=msy_D32YhP8OP4_Mj_L3OYd4iablqQETN-DvilGZeVQ,12552 +apprise/plugins/NotifyVonage.py,sha256=xmzZgobFaGA_whpQ5fDuG2poUrK9W4T77yP7dusHcSo,13431 +apprise/plugins/NotifyWeComBot.py,sha256=5lkhXDgyJ1edzknemKsO1sJVv7miR9F_7xI40Ag7ICI,8789 +apprise/plugins/NotifyWebexTeams.py,sha256=gbbRlHiPuOvUIZexE5m2QNd1dN_5_x0OdT5m6NSrcso,9164 +apprise/plugins/NotifyWhatsApp.py,sha256=PtzW0ue3d2wZ8Pva_LG29jUcpRRP03TFxO5SME_8Juo,19924 +apprise/plugins/NotifyWindows.py,sha256=QgWJfJF8AE6RWr-L81YYVZNWrnImK9Qr3B991HWanqU,8563 +apprise/plugins/NotifyXBMC.py,sha256=5hDuOTP3Kwtp4NEMaokNjWyEKEkQcN_fSx-cUPJvhaU,12096 +apprise/plugins/NotifyXML.py,sha256=WJnmdvXseuTRgioVMRqpR8a09cDfTpPTfuFlTnT_TfI,16973 +apprise/plugins/NotifyZulip.py,sha256=mbZoPiQXFbcaJ5UYDbkX4HJPAvRzPEAB-rsOlF9SD4o,13755 +apprise/plugins/__init__.py,sha256=jTfLmW47kZC_Wf5eFFta2NoD2J-7_E7JaPrrVMIECkU,18725 +apprise/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +apprise/utils.py,sha256=SjRU2tb1UsVnTCTXPUyXVz3WpRbDWwAHH-d3ll38EHY,53185 diff --git a/libs/apprise-1.7.2.dist-info/REQUESTED b/libs/apprise-1.7.2.dist-info/REQUESTED new file mode 100644 index 000000000..e69de29bb diff --git a/libs/apprise-1.7.2.dist-info/WHEEL b/libs/apprise-1.7.2.dist-info/WHEEL new file mode 100644 index 000000000..ba48cbcf9 --- /dev/null +++ b/libs/apprise-1.7.2.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.41.3) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/libs/apprise-1.7.2.dist-info/entry_points.txt b/libs/apprise-1.7.2.dist-info/entry_points.txt new file mode 100644 index 000000000..7f20ac9a3 --- /dev/null +++ b/libs/apprise-1.7.2.dist-info/entry_points.txt @@ -0,0 +1,2 @@ +[console_scripts] +apprise = apprise.cli:main diff --git a/libs/apprise-1.7.2.dist-info/top_level.txt b/libs/apprise-1.7.2.dist-info/top_level.txt new file mode 100644 index 000000000..9f8c12a76 --- /dev/null +++ b/libs/apprise-1.7.2.dist-info/top_level.txt @@ -0,0 +1 @@ +apprise diff --git a/libs/apprise/Apprise.py b/libs/apprise/Apprise.py index 4c83c481f..9a3e8dfc7 100644 --- a/libs/apprise/Apprise.py +++ b/libs/apprise/Apprise.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: @@ -33,9 +33,11 @@ from itertools import chain from . import common from .conversion import convert_between from .utils import is_exclusive_match +from .NotificationManager import NotificationManager from .utils import parse_list from .utils import parse_urls from .utils import cwe312_url +from .emojis import apply_emojis from .logger import logger from .AppriseAsset import AppriseAsset from .AppriseConfig import AppriseConfig @@ -44,10 +46,12 @@ from .AppriseLocale import AppriseLocale from .config.ConfigBase import ConfigBase from .plugins.NotifyBase import NotifyBase - from . import plugins from . import __version__ +# Grant access to our Notification Manager Singleton +N_MGR = NotificationManager() + class Apprise: """ @@ -137,7 +141,7 @@ class Apprise: # We already have our result set results = url - if results.get('schema') not in common.NOTIFY_SCHEMA_MAP: + if results.get('schema') not in N_MGR: # schema is a mandatory dictionary item as it is the only way # we can index into our loaded plugins logger.error('Dictionary does not include a "schema" entry.') @@ -160,7 +164,7 @@ class Apprise: type(url)) return None - if not common.NOTIFY_SCHEMA_MAP[results['schema']].enabled: + if not N_MGR[results['schema']].enabled: # # First Plugin Enable Check (Pre Initialization) # @@ -180,13 +184,12 @@ class Apprise: try: # Attempt to create an instance of our plugin using the parsed # URL information - plugin = common.NOTIFY_SCHEMA_MAP[results['schema']](**results) + plugin = N_MGR[results['schema']](**results) # Create log entry of loaded URL logger.debug( 'Loaded {} URL: {}'.format( - common. - NOTIFY_SCHEMA_MAP[results['schema']].service_name, + N_MGR[results['schema']].service_name, plugin.url(privacy=asset.secure_logging))) except Exception: @@ -197,15 +200,14 @@ class Apprise: # the arguments are invalid or can not be used. logger.error( 'Could not load {} URL: {}'.format( - common. - NOTIFY_SCHEMA_MAP[results['schema']].service_name, + N_MGR[results['schema']].service_name, loggable_url)) return None else: # Attempt to create an instance of our plugin using the parsed # URL information but don't wrap it in a try catch - plugin = common.NOTIFY_SCHEMA_MAP[results['schema']](**results) + plugin = N_MGR[results['schema']](**results) if not plugin.enabled: # @@ -376,7 +378,7 @@ class Apprise: body, title, notify_type=notify_type, body_format=body_format, tag=tag, match_always=match_always, attach=attach, - interpret_escapes=interpret_escapes + interpret_escapes=interpret_escapes, ) except TypeError: @@ -501,6 +503,11 @@ class Apprise: key = server.notify_format if server.title_maxlen > 0\ else f'_{server.notify_format}' + if server.interpret_emojis: + # alter our key slightly to handle emojis since their value is + # pulled out of the notification + key += "-emojis" + if key not in conversion_title_map: # Prepare our title @@ -542,6 +549,16 @@ class Apprise: logger.error(msg) raise TypeError(msg) + if server.interpret_emojis: + # + # Convert our :emoji: definitions + # + + conversion_body_map[key] = \ + apply_emojis(conversion_body_map[key]) + conversion_title_map[key] = \ + apply_emojis(conversion_title_map[key]) + kwargs = dict( body=conversion_body_map[key], title=conversion_title_map[key], @@ -674,7 +691,7 @@ class Apprise: 'asset': self.asset.details(), } - for plugin in set(common.NOTIFY_SCHEMA_MAP.values()): + for plugin in N_MGR.plugins(): # Iterate over our hashed plugins and dynamically build details on # their status: diff --git a/libs/apprise/AppriseAsset.py b/libs/apprise/AppriseAsset.py index 835c3b6ad..97a7bccfb 100644 --- a/libs/apprise/AppriseAsset.py +++ b/libs/apprise/AppriseAsset.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: @@ -33,7 +33,11 @@ from os.path import dirname from os.path import isfile from os.path import abspath from .common import NotifyType -from .utils import module_detection +from .NotificationManager import NotificationManager + + +# Grant access to our Notification Manager Singleton +N_MGR = NotificationManager() class AppriseAsset: @@ -121,6 +125,12 @@ class AppriseAsset: # notifications are sent sequentially (one after another) async_mode = True + # Support :smile:, and other alike keywords swapping them for their + # unicode value. A value of None leaves the interpretation up to the + # end user to control (allowing them to specify emojis=yes on the + # URL) + interpret_emojis = None + # Whether or not to interpret escapes found within the input text prior # to passing it upstream. Such as converting \t to an actual tab and \n # to a new line. @@ -174,7 +184,7 @@ class AppriseAsset: if plugin_paths: # Load any decorated modules if defined - module_detection(plugin_paths) + N_MGR.module_detection(plugin_paths) def color(self, notify_type, color_type=None): """ diff --git a/libs/apprise/AppriseAttachment.py b/libs/apprise/AppriseAttachment.py index e00645d2d..fcfed3af6 100644 --- a/libs/apprise/AppriseAttachment.py +++ b/libs/apprise/AppriseAttachment.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: @@ -26,15 +26,18 @@ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. -from . import attachment from . import URLBase +from .attachment.AttachBase import AttachBase from .AppriseAsset import AppriseAsset +from .AttachmentManager import AttachmentManager from .logger import logger from .common import ContentLocation from .common import CONTENT_LOCATIONS -from .common import ATTACHMENT_SCHEMA_MAP from .utils import GET_SCHEMA_RE +# Grant access to our Notification Manager Singleton +A_MGR = AttachmentManager() + class AppriseAttachment: """ @@ -139,7 +142,7 @@ class AppriseAttachment: # prepare default asset asset = self.asset - if isinstance(attachments, attachment.AttachBase): + if isinstance(attachments, AttachBase): # Go ahead and just add our attachments into our list self.attachments.append(attachments) return True @@ -169,7 +172,7 @@ class AppriseAttachment: # returns None if it fails instance = AppriseAttachment.instantiate( _attachment, asset=asset, cache=cache) - if not isinstance(instance, attachment.AttachBase): + if not isinstance(instance, AttachBase): return_status = False continue @@ -178,7 +181,7 @@ class AppriseAttachment: # append our content together instance = _attachment.attachments - elif not isinstance(_attachment, attachment.AttachBase): + elif not isinstance(_attachment, AttachBase): logger.warning( "An invalid attachment (type={}) was specified.".format( type(_attachment))) @@ -228,7 +231,7 @@ class AppriseAttachment: schema = GET_SCHEMA_RE.match(url) if schema is None: # Plan B is to assume we're dealing with a file - schema = attachment.AttachFile.protocol + schema = 'file' url = '{}://{}'.format(schema, URLBase.quote(url)) else: @@ -236,13 +239,13 @@ class AppriseAttachment: schema = schema.group('schema').lower() # Some basic validation - if schema not in ATTACHMENT_SCHEMA_MAP: + if schema not in A_MGR: logger.warning('Unsupported schema {}.'.format(schema)) return None # Parse our url details of the server object as dictionary containing # all of the information parsed from our URL - results = ATTACHMENT_SCHEMA_MAP[schema].parse_url(url) + results = A_MGR[schema].parse_url(url) if not results: # Failed to parse the server URL @@ -261,8 +264,7 @@ class AppriseAttachment: try: # Attempt to create an instance of our plugin using the parsed # URL information - attach_plugin = \ - ATTACHMENT_SCHEMA_MAP[results['schema']](**results) + attach_plugin = A_MGR[results['schema']](**results) except Exception: # the arguments are invalid or can not be used. @@ -272,7 +274,7 @@ class AppriseAttachment: else: # Attempt to create an instance of our plugin using the parsed # URL information but don't wrap it in a try catch - attach_plugin = ATTACHMENT_SCHEMA_MAP[results['schema']](**results) + attach_plugin = A_MGR[results['schema']](**results) return attach_plugin diff --git a/libs/apprise/AppriseConfig.py b/libs/apprise/AppriseConfig.py index 07e7b48ed..7e5a9126f 100644 --- a/libs/apprise/AppriseConfig.py +++ b/libs/apprise/AppriseConfig.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: @@ -26,9 +26,9 @@ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. -from . import config from . import ConfigBase from . import CONFIG_FORMATS +from .ConfigurationManager import ConfigurationManager from . import URLBase from .AppriseAsset import AppriseAsset from . import common @@ -37,6 +37,9 @@ from .utils import parse_list from .utils import is_exclusive_match from .logger import logger +# Grant access to our Configuration Manager Singleton +C_MGR = ConfigurationManager() + class AppriseConfig: """ @@ -251,7 +254,7 @@ class AppriseConfig: logger.debug("Loading raw configuration: {}".format(content)) # Create ourselves a ConfigMemory Object to store our configuration - instance = config.ConfigMemory( + instance = C_MGR['memory']( content=content, format=format, asset=asset, tag=tag, recursion=recursion, insecure_includes=insecure_includes) @@ -326,7 +329,7 @@ class AppriseConfig: schema = GET_SCHEMA_RE.match(url) if schema is None: # Plan B is to assume we're dealing with a file - schema = config.ConfigFile.protocol + schema = 'file' url = '{}://{}'.format(schema, URLBase.quote(url)) else: @@ -334,13 +337,13 @@ class AppriseConfig: schema = schema.group('schema').lower() # Some basic validation - if schema not in common.CONFIG_SCHEMA_MAP: + if schema not in C_MGR: logger.warning('Unsupported schema {}.'.format(schema)) return None # Parse our url details of the server object as dictionary containing # all of the information parsed from our URL - results = common.CONFIG_SCHEMA_MAP[schema].parse_url(url) + results = C_MGR[schema].parse_url(url) if not results: # Failed to parse the server URL @@ -368,8 +371,7 @@ class AppriseConfig: try: # Attempt to create an instance of our plugin using the parsed # URL information - cfg_plugin = \ - common.CONFIG_SCHEMA_MAP[results['schema']](**results) + cfg_plugin = C_MGR[results['schema']](**results) except Exception: # the arguments are invalid or can not be used. @@ -379,7 +381,7 @@ class AppriseConfig: else: # Attempt to create an instance of our plugin using the parsed # URL information but don't wrap it in a try catch - cfg_plugin = common.CONFIG_SCHEMA_MAP[results['schema']](**results) + cfg_plugin = C_MGR[results['schema']](**results) return cfg_plugin diff --git a/libs/apprise/AppriseLocale.py b/libs/apprise/AppriseLocale.py index c80afae27..cb9512e5e 100644 --- a/libs/apprise/AppriseLocale.py +++ b/libs/apprise/AppriseLocale.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/AttachmentManager.py b/libs/apprise/AttachmentManager.py new file mode 100644 index 000000000..d296a4996 --- /dev/null +++ b/libs/apprise/AttachmentManager.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +# BSD 2-Clause License +# +# Apprise - Push Notification Library. +# Copyright (c) 2024, Chris Caron +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +from os.path import dirname +from os.path import abspath +from os.path import join +from .manager import PluginManager + + +class AttachmentManager(PluginManager): + """ + Designed to be a singleton object to maintain all initialized + attachment plugins/modules in memory. + """ + + # Description (used for logging) + name = 'Attachment Plugin' + + # Filename Prefix to filter on + fname_prefix = 'Attach' + + # Memory Space + _id = 'attachment' + + # Our Module Python path name + module_name_prefix = f'apprise.{_id}' + + # The module path to scan + module_path = join(abspath(dirname(__file__)), _id) diff --git a/libs/apprise/ConfigurationManager.py b/libs/apprise/ConfigurationManager.py new file mode 100644 index 000000000..6696895b9 --- /dev/null +++ b/libs/apprise/ConfigurationManager.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +# BSD 2-Clause License +# +# Apprise - Push Notification Library. +# Copyright (c) 2024, Chris Caron +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +from os.path import dirname +from os.path import abspath +from os.path import join +from .manager import PluginManager + + +class ConfigurationManager(PluginManager): + """ + Designed to be a singleton object to maintain all initialized + configuration plugins/modules in memory. + """ + + # Description (used for logging) + name = 'Configuration Plugin' + + # Filename Prefix to filter on + fname_prefix = 'Config' + + # Memory Space + _id = 'config' + + # Our Module Python path name + module_name_prefix = f'apprise.{_id}' + + # The module path to scan + module_path = join(abspath(dirname(__file__)), _id) diff --git a/libs/apprise/NotificationManager.py b/libs/apprise/NotificationManager.py new file mode 100644 index 000000000..abbbdd203 --- /dev/null +++ b/libs/apprise/NotificationManager.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +# BSD 2-Clause License +# +# Apprise - Push Notification Library. +# Copyright (c) 2024, Chris Caron +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +from os.path import dirname +from os.path import abspath +from os.path import join +from .manager import PluginManager + + +class NotificationManager(PluginManager): + """ + Designed to be a singleton object to maintain all initialized notifications + in memory. + """ + + # Description (used for logging) + name = 'Notification Plugin' + + # Filename Prefix to filter on + fname_prefix = 'Notify' + + # Memory Space + _id = 'plugins' + + # Our Module Python path name + module_name_prefix = f'apprise.{_id}' + + # The module path to scan + module_path = join(abspath(dirname(__file__)), _id) diff --git a/libs/apprise/URLBase.py b/libs/apprise/URLBase.py index 1cea66d15..c9f5877f6 100644 --- a/libs/apprise/URLBase.py +++ b/libs/apprise/URLBase.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: @@ -550,7 +550,7 @@ class URLBase: return paths @staticmethod - def parse_list(content, unquote=True): + def parse_list(content, allow_whitespace=True, unquote=True): """A wrapper to utils.parse_list() with unquoting support Parses a specified set of data and breaks it into a list. @@ -559,6 +559,9 @@ class URLBase: content (str): The path to split up into a list. If a list is provided, then it's individual entries are processed. + allow_whitespace (:obj:`bool`, optional): whitespace is to be + treated as a delimiter + unquote (:obj:`bool`, optional): call unquote on each element added to the returned list. @@ -566,7 +569,7 @@ class URLBase: list: A unique list containing all of the elements in the path """ - content = parse_list(content) + content = parse_list(content, allow_whitespace=allow_whitespace) if unquote: content = \ [URLBase.unquote(x) for x in filter(bool, content)] diff --git a/libs/apprise/__init__.py b/libs/apprise/__init__.py index f8bb5c752..6b9f0394b 100644 --- a/libs/apprise/__init__.py +++ b/libs/apprise/__init__.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: @@ -27,10 +27,10 @@ # POSSIBILITY OF SUCH DAMAGE. __title__ = 'Apprise' -__version__ = '1.6.0' +__version__ = '1.7.2' __author__ = 'Chris Caron' __license__ = 'BSD' -__copywrite__ = 'Copyright (C) 2023 Chris Caron ' +__copywrite__ = 'Copyright (C) 2024 Chris Caron ' __email__ = 'lead2gold@gmail.com' __status__ = 'Production' diff --git a/libs/apprise/attachment/AttachBase.py b/libs/apprise/attachment/AttachBase.py index c1cadbf91..062e553d7 100644 --- a/libs/apprise/attachment/AttachBase.py +++ b/libs/apprise/attachment/AttachBase.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/attachment/AttachFile.py b/libs/apprise/attachment/AttachFile.py index d30855553..4c9c8f136 100644 --- a/libs/apprise/attachment/AttachFile.py +++ b/libs/apprise/attachment/AttachFile.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/attachment/AttachHTTP.py b/libs/apprise/attachment/AttachHTTP.py index 0c859477e..719ebc625 100644 --- a/libs/apprise/attachment/AttachHTTP.py +++ b/libs/apprise/attachment/AttachHTTP.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/attachment/__init__.py b/libs/apprise/attachment/__init__.py index ba7620a45..0a88313d6 100644 --- a/libs/apprise/attachment/__init__.py +++ b/libs/apprise/attachment/__init__.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: @@ -26,93 +26,14 @@ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. -import re -from os import listdir -from os.path import dirname -from os.path import abspath -from ..common import ATTACHMENT_SCHEMA_MAP +# Used for testing +from .AttachBase import AttachBase +from ..AttachmentManager import AttachmentManager -__all__ = [] +# Initalize our Attachment Manager Singleton +A_MGR = AttachmentManager() - -# Load our Lookup Matrix -def __load_matrix(path=abspath(dirname(__file__)), name='apprise.attachment'): - """ - Dynamically load our schema map; this allows us to gracefully - skip over modules we simply don't have the dependencies for. - - """ - # Used for the detection of additional Attachment Services objects - # The .py extension is optional as we support loading directories too - module_re = re.compile(r'^(?PAttach[a-z0-9]+)(\.py)?$', re.I) - - for f in listdir(path): - match = module_re.match(f) - if not match: - # keep going - continue - - # Store our notification/plugin name: - plugin_name = match.group('name') - try: - module = __import__( - '{}.{}'.format(name, plugin_name), - globals(), locals(), - fromlist=[plugin_name]) - - except ImportError: - # No problem, we can't use this object - continue - - if not hasattr(module, plugin_name): - # Not a library we can load as it doesn't follow the simple rule - # that the class must bear the same name as the notification - # file itself. - continue - - # Get our plugin - plugin = getattr(module, plugin_name) - if not hasattr(plugin, 'app_id'): - # Filter out non-notification modules - continue - - elif plugin_name in __all__: - # we're already handling this object - continue - - # Add our module name to our __all__ - __all__.append(plugin_name) - - # Ensure we provide the class as the reference to this directory and - # not the module: - globals()[plugin_name] = plugin - - # Load protocol(s) if defined - proto = getattr(plugin, 'protocol', None) - if isinstance(proto, str): - if proto not in ATTACHMENT_SCHEMA_MAP: - ATTACHMENT_SCHEMA_MAP[proto] = plugin - - elif isinstance(proto, (set, list, tuple)): - # Support iterables list types - for p in proto: - if p not in ATTACHMENT_SCHEMA_MAP: - ATTACHMENT_SCHEMA_MAP[p] = plugin - - # Load secure protocol(s) if defined - protos = getattr(plugin, 'secure_protocol', None) - if isinstance(protos, str): - if protos not in ATTACHMENT_SCHEMA_MAP: - ATTACHMENT_SCHEMA_MAP[protos] = plugin - - if isinstance(protos, (set, list, tuple)): - # Support iterables list types - for p in protos: - if p not in ATTACHMENT_SCHEMA_MAP: - ATTACHMENT_SCHEMA_MAP[p] = plugin - - return ATTACHMENT_SCHEMA_MAP - - -# Dynamically build our schema base -__load_matrix() +__all__ = [ + # Reference + 'AttachBase', +] diff --git a/libs/apprise/cli.py b/libs/apprise/cli.py index 130351802..0e16b99f4 100644 --- a/libs/apprise/cli.py +++ b/libs/apprise/cli.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: @@ -213,6 +213,8 @@ def print_version_msg(): 'increase the verbosity. I.e.: -vvvv') @click.option('--interpret-escapes', '-e', is_flag=True, help='Enable interpretation of backslash escapes') +@click.option('--interpret-emojis', '-j', is_flag=True, + help='Enable interpretation of :emoji: definitions') @click.option('--debug', '-D', is_flag=True, help='Debug mode') @click.option('--version', '-V', is_flag=True, help='Display the apprise version and exit.') @@ -220,7 +222,8 @@ def print_version_msg(): metavar='SERVER_URL [SERVER_URL2 [SERVER_URL3]]',) def main(body, title, config, attach, urls, notification_type, theme, tag, input_format, dry_run, recursion_depth, verbose, disable_async, - details, interpret_escapes, plugin_path, debug, version): + details, interpret_escapes, interpret_emojis, plugin_path, debug, + version): """ Send a notification to all of the specified servers identified by their URLs the content provided within the title, body and notification-type. @@ -308,6 +311,9 @@ def main(body, title, config, attach, urls, notification_type, theme, tag, # Interpret Escapes interpret_escapes=interpret_escapes, + # Interpret Emojis + interpret_emojis=None if not interpret_emojis else True, + # Set the theme theme=theme, diff --git a/libs/apprise/common.py b/libs/apprise/common.py index aaf746eaa..d6fe2cd0d 100644 --- a/libs/apprise/common.py +++ b/libs/apprise/common.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: @@ -26,50 +26,6 @@ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. -# we mirror our base purely for the ability to reset everything; this -# is generally only used in testing and should not be used by developers -# It is also used as a means of preventing a module from being reloaded -# in the event it already exists -NOTIFY_MODULE_MAP = {} - -# Maintains a mapping of all of the Notification services -NOTIFY_SCHEMA_MAP = {} - -# This contains a mapping of all plugins dynamicaly loaded at runtime from -# external modules such as the @notify decorator -# -# The elements here will be additionally added to the NOTIFY_SCHEMA_MAP if -# there is no conflict otherwise. -# The structure looks like the following: -# Module path, e.g. /usr/share/apprise/plugins/my_notify_hook.py -# { -# 'path': path, -# -# 'notify': { -# 'schema': { -# 'name': 'Custom schema name', -# 'fn_name': 'name_of_function_decorator_was_found_on', -# 'url': 'schema://any/additional/info/found/on/url' -# 'plugin': -# }, -# 'schema2': { -# 'name': 'Custom schema name', -# 'fn_name': 'name_of_function_decorator_was_found_on', -# 'url': 'schema://any/additional/info/found/on/url' -# 'plugin': -# } -# } -# -# Note: that the inherits from -# NotifyBase -NOTIFY_CUSTOM_MODULE_MAP = {} - -# Maintains a mapping of all configuration schema's supported -CONFIG_SCHEMA_MAP = {} - -# Maintains a mapping of all attachment schema's supported -ATTACHMENT_SCHEMA_MAP = {} - class NotifyType: """ diff --git a/libs/apprise/config/ConfigBase.py b/libs/apprise/config/ConfigBase.py index adddc4f56..731945256 100644 --- a/libs/apprise/config/ConfigBase.py +++ b/libs/apprise/config/ConfigBase.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: @@ -35,16 +35,24 @@ from .. import plugins from .. import common from ..AppriseAsset import AppriseAsset from ..URLBase import URLBase +from ..ConfigurationManager import ConfigurationManager from ..utils import GET_SCHEMA_RE from ..utils import parse_list from ..utils import parse_bool from ..utils import parse_urls from ..utils import cwe312_url +from ..NotificationManager import NotificationManager # Test whether token is valid or not VALID_TOKEN = re.compile( r'(?P[a-z0-9][a-z0-9_]+)', re.I) +# Grant access to our Notification Manager Singleton +N_MGR = NotificationManager() + +# Grant access to our Configuration Manager Singleton +C_MGR = ConfigurationManager() + class ConfigBase(URLBase): """ @@ -229,7 +237,7 @@ class ConfigBase(URLBase): schema = schema.group('schema').lower() # Some basic validation - if schema not in common.CONFIG_SCHEMA_MAP: + if schema not in C_MGR: ConfigBase.logger.warning( 'Unsupported include schema {}.'.format(schema)) continue @@ -240,7 +248,7 @@ class ConfigBase(URLBase): # Parse our url details of the server object as dictionary # containing all of the information parsed from our URL - results = common.CONFIG_SCHEMA_MAP[schema].parse_url(url) + results = C_MGR[schema].parse_url(url) if not results: # Failed to parse the server URL self.logger.warning( @@ -248,11 +256,10 @@ class ConfigBase(URLBase): continue # Handle cross inclusion based on allow_cross_includes rules - if (common.CONFIG_SCHEMA_MAP[schema].allow_cross_includes == + if (C_MGR[schema].allow_cross_includes == common.ContentIncludeMode.STRICT and schema not in self.schemas() - and not self.insecure_includes) or \ - common.CONFIG_SCHEMA_MAP[schema] \ + and not self.insecure_includes) or C_MGR[schema] \ .allow_cross_includes == \ common.ContentIncludeMode.NEVER: @@ -280,8 +287,7 @@ class ConfigBase(URLBase): try: # Attempt to create an instance of our plugin using the # parsed URL information - cfg_plugin = \ - common.CONFIG_SCHEMA_MAP[results['schema']](**results) + cfg_plugin = C_MGR[results['schema']](**results) except Exception as e: # the arguments are invalid or can not be used. @@ -393,7 +399,11 @@ class ConfigBase(URLBase): # Track our groups groups.add(tag) - # Store what we know is worth keping + # Store what we know is worth keeping + if tag not in group_tags: # pragma: no cover + # handle cases where the tag doesn't exist + group_tags[tag] = set() + results |= group_tags[tag] - tag_groups # Get simple tag assignments @@ -761,8 +771,7 @@ class ConfigBase(URLBase): try: # Attempt to create an instance of our plugin using the # parsed URL information - plugin = common.NOTIFY_SCHEMA_MAP[ - results['schema']](**results) + plugin = N_MGR[results['schema']](**results) # Create log entry of loaded URL ConfigBase.logger.debug( @@ -898,19 +907,11 @@ class ConfigBase(URLBase): # groups root directive # groups = result.get('groups', None) - if not isinstance(groups, (list, tuple)): - # Not a problem; we simply have no group entry - groups = list() - - # Iterate over each group defined and store it - for no, entry in enumerate(groups): - if not isinstance(entry, dict): - ConfigBase.logger.warning( - 'No assignment for group {}, entry #{}'.format( - entry, no + 1)) - continue - - for _groups, tags in entry.items(): + if isinstance(groups, dict): + # + # Dictionary + # + for _groups, tags in groups.items(): for group in parse_list(_groups, cast=str): if isinstance(tags, (list, tuple)): _tags = set() @@ -932,7 +933,41 @@ class ConfigBase(URLBase): else: group_tags[group] |= tags - # + elif isinstance(groups, (list, tuple)): + # + # List of Dictionaries + # + + # Iterate over each group defined and store it + for no, entry in enumerate(groups): + if not isinstance(entry, dict): + ConfigBase.logger.warning( + 'No assignment for group {}, entry #{}'.format( + entry, no + 1)) + continue + + for _groups, tags in entry.items(): + for group in parse_list(_groups, cast=str): + if isinstance(tags, (list, tuple)): + _tags = set() + for e in tags: + if isinstance(e, dict): + _tags |= set(e.keys()) + else: + _tags |= set(parse_list(e, cast=str)) + + # Final assignment + tags = _tags + + else: + tags = set(parse_list(tags, cast=str)) + + if group not in group_tags: + group_tags[group] = tags + + else: + group_tags[group] |= tags + # include root directive # includes = result.get('include', None) @@ -1064,7 +1099,7 @@ class ConfigBase(URLBase): del entries['schema'] # support our special tokens (if they're present) - if schema in common.NOTIFY_SCHEMA_MAP: + if schema in N_MGR: entries = ConfigBase._special_token_handler( schema, entries) @@ -1076,7 +1111,7 @@ class ConfigBase(URLBase): elif isinstance(tokens, dict): # support our special tokens (if they're present) - if schema in common.NOTIFY_SCHEMA_MAP: + if schema in N_MGR: tokens = ConfigBase._special_token_handler( schema, tokens) @@ -1110,7 +1145,7 @@ class ConfigBase(URLBase): # Grab our first item _results = results.pop(0) - if _results['schema'] not in common.NOTIFY_SCHEMA_MAP: + if _results['schema'] not in N_MGR: # the arguments are invalid or can not be used. ConfigBase.logger.warning( 'An invalid Apprise schema ({}) in YAML configuration ' @@ -1183,8 +1218,7 @@ class ConfigBase(URLBase): try: # Attempt to create an instance of our plugin using the # parsed URL information - plugin = common.\ - NOTIFY_SCHEMA_MAP[results['schema']](**results) + plugin = N_MGR[results['schema']](**results) # Create log entry of loaded URL ConfigBase.logger.debug( @@ -1237,8 +1271,7 @@ class ConfigBase(URLBase): # Create a copy of our dictionary tokens = tokens.copy() - for kw, meta in common.NOTIFY_SCHEMA_MAP[schema]\ - .template_kwargs.items(): + for kw, meta in N_MGR[schema].template_kwargs.items(): # Determine our prefix: prefix = meta.get('prefix', '+') @@ -1281,8 +1314,7 @@ class ConfigBase(URLBase): # # This function here allows these mappings to take place within the # YAML file as independant arguments. - class_templates = \ - plugins.details(common.NOTIFY_SCHEMA_MAP[schema]) + class_templates = plugins.details(N_MGR[schema]) for key in list(tokens.keys()): diff --git a/libs/apprise/config/ConfigFile.py b/libs/apprise/config/ConfigFile.py index 719355130..172d699f8 100644 --- a/libs/apprise/config/ConfigFile.py +++ b/libs/apprise/config/ConfigFile.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/config/ConfigHTTP.py b/libs/apprise/config/ConfigHTTP.py index 8e8677c24..f6faba8d4 100644 --- a/libs/apprise/config/ConfigHTTP.py +++ b/libs/apprise/config/ConfigHTTP.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/config/ConfigMemory.py b/libs/apprise/config/ConfigMemory.py index 110e04a3c..413956dfc 100644 --- a/libs/apprise/config/ConfigMemory.py +++ b/libs/apprise/config/ConfigMemory.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/config/__init__.py b/libs/apprise/config/__init__.py index 4b7e3fd78..efbace687 100644 --- a/libs/apprise/config/__init__.py +++ b/libs/apprise/config/__init__.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: @@ -26,84 +26,14 @@ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. -import re -from os import listdir -from os.path import dirname -from os.path import abspath -from ..logger import logger -from ..common import CONFIG_SCHEMA_MAP +# Used for testing +from .ConfigBase import ConfigBase +from ..ConfigurationManager import ConfigurationManager -__all__ = [] +# Initalize our Config Manager Singleton +C_MGR = ConfigurationManager() - -# Load our Lookup Matrix -def __load_matrix(path=abspath(dirname(__file__)), name='apprise.config'): - """ - Dynamically load our schema map; this allows us to gracefully - skip over modules we simply don't have the dependencies for. - - """ - # Used for the detection of additional Configuration Services objects - # The .py extension is optional as we support loading directories too - module_re = re.compile(r'^(?PConfig[a-z0-9]+)(\.py)?$', re.I) - - for f in listdir(path): - match = module_re.match(f) - if not match: - # keep going - continue - - # Store our notification/plugin name: - plugin_name = match.group('name') - try: - module = __import__( - '{}.{}'.format(name, plugin_name), - globals(), locals(), - fromlist=[plugin_name]) - - except ImportError: - # No problem, we can't use this object - continue - - if not hasattr(module, plugin_name): - # Not a library we can load as it doesn't follow the simple rule - # that the class must bear the same name as the notification - # file itself. - continue - - # Get our plugin - plugin = getattr(module, plugin_name) - if not hasattr(plugin, 'app_id'): - # Filter out non-notification modules - continue - - elif plugin_name in __all__: - # we're already handling this object - continue - - # Add our module name to our __all__ - __all__.append(plugin_name) - - # Ensure we provide the class as the reference to this directory and - # not the module: - globals()[plugin_name] = plugin - - fn = getattr(plugin, 'schemas', None) - schemas = set([]) if not callable(fn) else fn(plugin) - - # map our schema to our plugin - for schema in schemas: - if schema in CONFIG_SCHEMA_MAP: - logger.error( - "Config schema ({}) mismatch detected - {} to {}" - .format(schema, CONFIG_SCHEMA_MAP[schema], plugin)) - continue - - # Assign plugin - CONFIG_SCHEMA_MAP[schema] = plugin - - return CONFIG_SCHEMA_MAP - - -# Dynamically build our schema base -__load_matrix() +__all__ = [ + # Reference + 'ConfigBase', +] diff --git a/libs/apprise/conversion.py b/libs/apprise/conversion.py index d3781f606..86e967bc4 100644 --- a/libs/apprise/conversion.py +++ b/libs/apprise/conversion.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/decorators/CustomNotifyPlugin.py b/libs/apprise/decorators/CustomNotifyPlugin.py index 5ccfded55..4c93ef1bd 100644 --- a/libs/apprise/decorators/CustomNotifyPlugin.py +++ b/libs/apprise/decorators/CustomNotifyPlugin.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: @@ -28,6 +28,7 @@ # POSSIBILITY OF SUCH DAMAGE. from ..plugins.NotifyBase import NotifyBase +from ..NotificationManager import NotificationManager from ..utils import URL_DETAILS_RE from ..utils import parse_url from ..utils import url_assembly @@ -36,6 +37,9 @@ from .. import common from ..logger import logger import inspect +# Grant access to our Notification Manager Singleton +N_MGR = NotificationManager() + class CustomNotifyPlugin(NotifyBase): """ @@ -91,17 +95,17 @@ class CustomNotifyPlugin(NotifyBase): logger.warning(msg) return None - # Acquire our plugin name - plugin_name = re_match.group('schema').lower() + # Acquire our schema + schema = re_match.group('schema').lower() if not re_match.group('base'): - url = '{}://'.format(plugin_name) + url = '{}://'.format(schema) # Keep a default set of arguments to apply to all called references base_args = parse_url( - url, default_schema=plugin_name, verify_host=False, simple=True) + url, default_schema=schema, verify_host=False, simple=True) - if plugin_name in common.NOTIFY_SCHEMA_MAP: + if schema in N_MGR: # we're already handling this object msg = 'The schema ({}) is already defined and could not be ' \ 'loaded from custom notify function {}.' \ @@ -117,10 +121,10 @@ class CustomNotifyPlugin(NotifyBase): # Our Service Name service_name = name if isinstance(name, str) \ - and name else 'Custom - {}'.format(plugin_name) + and name else 'Custom - {}'.format(schema) # Store our matched schema - secure_protocol = plugin_name + secure_protocol = schema requirements = { # Define our required packaging in order to work @@ -181,51 +185,26 @@ class CustomNotifyPlugin(NotifyBase): # Unhandled Exception self.logger.warning( 'An exception occured sending a %s notification.', - common. - NOTIFY_SCHEMA_MAP[self.secure_protocol].service_name) + N_MGR[self.secure_protocol].service_name) self.logger.debug( '%s Exception: %s', - common.NOTIFY_SCHEMA_MAP[self.secure_protocol], str(e)) + N_MGR[self.secure_protocol], str(e)) return False if response: self.logger.info( 'Sent %s notification.', - common. - NOTIFY_SCHEMA_MAP[self.secure_protocol].service_name) + N_MGR[self.secure_protocol].service_name) else: self.logger.warning( 'Failed to send %s notification.', - common. - NOTIFY_SCHEMA_MAP[self.secure_protocol].service_name) + N_MGR[self.secure_protocol].service_name) return response # Store our plugin into our core map file - common.NOTIFY_SCHEMA_MAP[plugin_name] = CustomNotifyPluginWrapper - - # Update our custom plugin map - module_pyname = str(send_func.__module__) - if module_pyname not in common.NOTIFY_CUSTOM_MODULE_MAP: - # Support non-dynamic includes as well... - common.NOTIFY_CUSTOM_MODULE_MAP[module_pyname] = { - 'path': inspect.getfile(send_func), - - # Initialize our template - 'notify': {}, - } - - common.\ - NOTIFY_CUSTOM_MODULE_MAP[module_pyname]['notify'][plugin_name] = { - # Our Serivice Description (for API and CLI --details view) - 'name': CustomNotifyPluginWrapper.service_name, - # The name of the send function the @notify decorator wrapped - 'fn_name': send_func.__name__, - # The URL that was provided in the @notify decorator call - # associated with the 'on=' - 'url': url, - # The Initialized Plugin that was generated based on the above - # parameters - 'plugin': CustomNotifyPluginWrapper} - - # return our plugin - return common.NOTIFY_SCHEMA_MAP[plugin_name] + return N_MGR.add( + plugin=CustomNotifyPluginWrapper, + schemas=schema, + send_func=send_func, + url=url, + ) diff --git a/libs/apprise/decorators/__init__.py b/libs/apprise/decorators/__init__.py index 5b089bbf5..db9a15a01 100644 --- a/libs/apprise/decorators/__init__.py +++ b/libs/apprise/decorators/__init__.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/decorators/notify.py b/libs/apprise/decorators/notify.py index 07b4ceb1e..2dd5f5099 100644 --- a/libs/apprise/decorators/notify.py +++ b/libs/apprise/decorators/notify.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/emojis.py b/libs/apprise/emojis.py new file mode 100644 index 000000000..d8a824813 --- /dev/null +++ b/libs/apprise/emojis.py @@ -0,0 +1,2273 @@ +# -*- coding: utf-8 -*- +# BSD 2-Clause License +# +# Apprise - Push Notification Library. +# Copyright (c) 2024, Chris Caron +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +import re +import time +from .logger import logger + +# All Emoji's are wrapped in this character +DELIM = ':' + +# the map simply contains the emoji that should be mapped to the regular +# expression it should be swapped on. +# This list was based on: https://github.com/ikatyang/emoji-cheat-sheet +EMOJI_MAP = { + # + # Face Smiling + # + DELIM + r'grinning' + DELIM: '😄', + DELIM + r'smile' + DELIM: '😄', + DELIM + r'(laughing|satisfied)' + DELIM: '😆', + DELIM + r'rofl' + DELIM: '🤣', + DELIM + r'slightly_smiling_face' + DELIM: '🙂', + DELIM + r'wink' + DELIM: '😉', + DELIM + r'innocent' + DELIM: '😇', + DELIM + r'smiley' + DELIM: '😃', + DELIM + r'grin' + DELIM: '😃', + DELIM + r'sweat_smile' + DELIM: '😅', + DELIM + r'joy' + DELIM: '😂', + DELIM + r'upside_down_face' + DELIM: '🙃', + DELIM + r'blush' + DELIM: '😊', + + # + # Face Affection + # + DELIM + r'smiling_face_with_three_hearts' + DELIM: '🥰', + DELIM + r'star_struck' + DELIM: '🤩', + DELIM + r'kissing' + DELIM: '😗', + DELIM + r'kissing_closed_eyes' + DELIM: '😚', + DELIM + r'smiling_face_with_tear' + DELIM: '🥲', + DELIM + r'heart_eyes' + DELIM: '😍', + DELIM + r'kissing_heart' + DELIM: '😘', + DELIM + r'relaxed' + DELIM: '☺️', + DELIM + r'kissing_smiling_eyes' + DELIM: '😙', + + # + # Face Tongue + # + DELIM + r'yum' + DELIM: '😋', + DELIM + r'stuck_out_tongue_winking_eye' + DELIM: '😜', + DELIM + r'stuck_out_tongue_closed_eyes' + DELIM: '😝', + DELIM + r'stuck_out_tongue' + DELIM: '😛', + DELIM + r'zany_face' + DELIM: '🤪', + DELIM + r'money_mouth_face' + DELIM: '🤑', + + # + # Face Hand + # + DELIM + r'hugs' + DELIM: '🤗', + DELIM + r'shushing_face' + DELIM: '🤫', + DELIM + r'hand_over_mouth' + DELIM: '🤭', + DELIM + r'thinking' + DELIM: '🤔', + + # + # Face Neutral Skeptical + # + DELIM + r'zipper_mouth_face' + DELIM: '🤐', + DELIM + r'neutral_face' + DELIM: '😐', + DELIM + r'no_mouth' + DELIM: '😶', + DELIM + r'smirk' + DELIM: '😏', + DELIM + r'roll_eyes' + DELIM: '🙄', + DELIM + r'face_exhaling' + DELIM: '😮‍💨', + DELIM + r'raised_eyebrow' + DELIM: '🤨', + DELIM + r'expressionless' + DELIM: '😑', + DELIM + r'face_in_clouds' + DELIM: '😶‍🌫️', + DELIM + r'unamused' + DELIM: '😒', + DELIM + r'grimacing' + DELIM: '😬', + DELIM + r'lying_face' + DELIM: '🤥', + + # + # Face Sleepy + # + DELIM + r'relieved' + DELIM: '😌', + DELIM + r'sleepy' + DELIM: '😪', + DELIM + r'sleeping' + DELIM: '😴', + DELIM + r'pensive' + DELIM: '😔', + DELIM + r'drooling_face' + DELIM: '🤤', + + # + # Face Unwell + # + DELIM + r'mask' + DELIM: '😷', + DELIM + r'face_with_head_bandage' + DELIM: '🤕', + DELIM + r'vomiting_face' + DELIM: '🤮', + DELIM + r'hot_face' + DELIM: '🥵', + DELIM + r'woozy_face' + DELIM: '🥴', + DELIM + r'face_with_spiral_eyes' + DELIM: '😵‍💫', + DELIM + r'face_with_thermometer' + DELIM: '🤒', + DELIM + r'nauseated_face' + DELIM: '🤢', + DELIM + r'sneezing_face' + DELIM: '🤧', + DELIM + r'cold_face' + DELIM: '🥶', + DELIM + r'dizzy_face' + DELIM: '😵', + DELIM + r'exploding_head' + DELIM: '🤯', + + # + # Face Hat + # + DELIM + r'cowboy_hat_face' + DELIM: '🤠', + DELIM + r'disguised_face' + DELIM: '🥸', + DELIM + r'partying_face' + DELIM: '🥳', + + # + # Face Glasses + # + DELIM + r'sunglasses' + DELIM: '😎', + DELIM + r'monocle_face' + DELIM: '🧐', + DELIM + r'nerd_face' + DELIM: '🤓', + + # + # Face Concerned + # + DELIM + r'confused' + DELIM: '😕', + DELIM + r'slightly_frowning_face' + DELIM: '🙁', + DELIM + r'open_mouth' + DELIM: '😮', + DELIM + r'astonished' + DELIM: '😲', + DELIM + r'pleading_face' + DELIM: '🥺', + DELIM + r'anguished' + DELIM: '😧', + DELIM + r'cold_sweat' + DELIM: '😰', + DELIM + r'cry' + DELIM: '😢', + DELIM + r'scream' + DELIM: '😱', + DELIM + r'persevere' + DELIM: '😣', + DELIM + r'sweat' + DELIM: '😓', + DELIM + r'tired_face' + DELIM: '😫', + DELIM + r'worried' + DELIM: '😟', + DELIM + r'frowning_face' + DELIM: '☹️', + DELIM + r'hushed' + DELIM: '😯', + DELIM + r'flushed' + DELIM: '😳', + DELIM + r'frowning' + DELIM: '😦', + DELIM + r'fearful' + DELIM: '😨', + DELIM + r'disappointed_relieved' + DELIM: '😥', + DELIM + r'sob' + DELIM: '😭', + DELIM + r'confounded' + DELIM: '😖', + DELIM + r'disappointed' + DELIM: '😞', + DELIM + r'weary' + DELIM: '😩', + DELIM + r'yawning_face' + DELIM: '🥱', + + # + # Face Negative + # + DELIM + r'triumph' + DELIM: '😤', + DELIM + r'angry' + DELIM: '😠', + DELIM + r'smiling_imp' + DELIM: '😈', + DELIM + r'skull' + DELIM: '💀', + DELIM + r'(pout|rage)' + DELIM: '😡', + DELIM + r'cursing_face' + DELIM: '🤬', + DELIM + r'imp' + DELIM: '👿', + DELIM + r'skull_and_crossbones' + DELIM: '☠️', + + # + # Face Costume + # + DELIM + r'(hankey|poop|shit)' + DELIM: '💩', + DELIM + r'japanese_ogre' + DELIM: '👹', + DELIM + r'ghost' + DELIM: '👻', + DELIM + r'space_invader' + DELIM: '👾', + DELIM + r'clown_face' + DELIM: '🤡', + DELIM + r'japanese_goblin' + DELIM: '👺', + DELIM + r'alien' + DELIM: '👽', + DELIM + r'robot' + DELIM: '🤖', + + # + # Cat Face + # + DELIM + r'smiley_cat' + DELIM: '😺', + DELIM + r'joy_cat' + DELIM: '😹', + DELIM + r'smirk_cat' + DELIM: '😼', + DELIM + r'scream_cat' + DELIM: '🙀', + DELIM + r'pouting_cat' + DELIM: '😾', + DELIM + r'smile_cat' + DELIM: '😸', + DELIM + r'heart_eyes_cat' + DELIM: '😻', + DELIM + r'kissing_cat' + DELIM: '😽', + DELIM + r'crying_cat_face' + DELIM: '😿', + + # + # Monkey Face + # + DELIM + r'see_no_evil' + DELIM: '🙈', + DELIM + r'speak_no_evil' + DELIM: '🙊', + DELIM + r'hear_no_evil' + DELIM: '🙉', + + # + # Heart + # + DELIM + r'love_letter' + DELIM: '💌', + DELIM + r'gift_heart' + DELIM: '💝', + DELIM + r'heartpulse' + DELIM: '💗', + DELIM + r'revolving_hearts' + DELIM: '💞', + DELIM + r'heart_decoration' + DELIM: '💟', + DELIM + r'broken_heart' + DELIM: '💔', + DELIM + r'mending_heart' + DELIM: '❤️‍🩹', + DELIM + r'orange_heart' + DELIM: '🧡', + DELIM + r'green_heart' + DELIM: '💚', + DELIM + r'purple_heart' + DELIM: '💜', + DELIM + r'black_heart' + DELIM: '🖤', + DELIM + r'cupid' + DELIM: '💘', + DELIM + r'sparkling_heart' + DELIM: '💖', + DELIM + r'heartbeat' + DELIM: '💓', + DELIM + r'two_hearts' + DELIM: '💕', + DELIM + r'heavy_heart_exclamation' + DELIM: '❣️', + DELIM + r'heart_on_fire' + DELIM: '❤️‍🔥', + DELIM + r'heart' + DELIM: '❤️', + DELIM + r'yellow_heart' + DELIM: '💛', + DELIM + r'blue_heart' + DELIM: '💙', + DELIM + r'brown_heart' + DELIM: '🤎', + DELIM + r'white_heart' + DELIM: '🤍', + + # + # Emotion + # + DELIM + r'kiss' + DELIM: '💋', + DELIM + r'anger' + DELIM: '💢', + DELIM + r'dizzy' + DELIM: '💫', + DELIM + r'dash' + DELIM: '💨', + DELIM + r'speech_balloon' + DELIM: '💬', + DELIM + r'left_speech_bubble' + DELIM: '🗨️', + DELIM + r'thought_balloon' + DELIM: '💭', + DELIM + r'100' + DELIM: '💯', + DELIM + r'(boom|collision)' + DELIM: '💥', + DELIM + r'sweat_drops' + DELIM: '💦', + DELIM + r'hole' + DELIM: '🕳️', + DELIM + r'eye_speech_bubble' + DELIM: '👁️‍🗨️', + DELIM + r'right_anger_bubble' + DELIM: '🗯️', + DELIM + r'zzz' + DELIM: '💤', + + # + # Hand Fingers Open + # + DELIM + r'wave' + DELIM: '👋', + DELIM + r'raised_hand_with_fingers_splayed' + DELIM: '🖐️', + DELIM + r'vulcan_salute' + DELIM: '🖖', + DELIM + r'raised_back_of_hand' + DELIM: '🤚', + DELIM + r'(raised_)?hand' + DELIM: '✋', + + # + # Hand Fingers Partial + # + DELIM + r'ok_hand' + DELIM: '👌', + DELIM + r'pinched_fingers' + DELIM: '🤌', + DELIM + r'pinching_hand' + DELIM: '🤏', + DELIM + r'v' + DELIM: '✌️', + DELIM + r'crossed_fingers' + DELIM: '🤞', + DELIM + r'love_you_gesture' + DELIM: '🤟', + DELIM + r'metal' + DELIM: '🤘', + DELIM + r'call_me_hand' + DELIM: '🤙', + + # + # Hand Single Finger + # + DELIM + r'point_left' + DELIM: '👈', + DELIM + r'point_right' + DELIM: '👉', + DELIM + r'point_up_2' + DELIM: '👆', + DELIM + r'(fu|middle_finger)' + DELIM: '🖕', + DELIM + r'point_down' + DELIM: '👇', + DELIM + r'point_up' + DELIM: '☝️', + + # + # Hand Fingers Closed + # + DELIM + r'(\+1|thumbsup)' + DELIM: '👍', + DELIM + r'(-1|thumbsdown)' + DELIM: '👎', + DELIM + r'fist' + DELIM: '✊', + DELIM + r'(fist_(raised|oncoming)|(face)?punch)' + DELIM: '👊', + DELIM + r'fist_left' + DELIM: '🤛', + DELIM + r'fist_right' + DELIM: '🤜', + + # + # Hands + # + DELIM + r'clap' + DELIM: '👏', + DELIM + r'raised_hands' + DELIM: '🙌', + DELIM + r'open_hands' + DELIM: '👐', + DELIM + r'palms_up_together' + DELIM: '🤲', + DELIM + r'handshake' + DELIM: '🤝', + DELIM + r'pray' + DELIM: '🙏', + + # + # Hand Prop + # + DELIM + r'writing_hand' + DELIM: '✍️', + DELIM + r'nail_care' + DELIM: '💅', + DELIM + r'selfie' + DELIM: '🤳', + + # + # Body Parts + # + DELIM + r'muscle' + DELIM: '💪', + DELIM + r'mechanical_arm' + DELIM: '🦾', + DELIM + r'mechanical_leg' + DELIM: '🦿', + DELIM + r'leg' + DELIM: '🦵', + DELIM + r'foot' + DELIM: '🦶', + DELIM + r'ear' + DELIM: '👂', + DELIM + r'ear_with_hearing_aid' + DELIM: '🦻', + DELIM + r'nose' + DELIM: '👃', + DELIM + r'brain' + DELIM: '🧠', + DELIM + r'anatomical_heart' + DELIM: '🫀', + DELIM + r'lungs' + DELIM: '🫁', + DELIM + r'tooth' + DELIM: '🦷', + DELIM + r'bone' + DELIM: '🦴', + DELIM + r'eyes' + DELIM: '👀', + DELIM + r'eye' + DELIM: '👁️', + DELIM + r'tongue' + DELIM: '👅', + DELIM + r'lips' + DELIM: '👄', + + # + # Person + # + DELIM + r'baby' + DELIM: '👶', + DELIM + r'child' + DELIM: '🧒', + DELIM + r'boy' + DELIM: '👦', + DELIM + r'girl' + DELIM: '👧', + DELIM + r'adult' + DELIM: '🧑', + DELIM + r'blond_haired_person' + DELIM: '👱', + DELIM + r'man' + DELIM: '👨', + DELIM + r'bearded_person' + DELIM: '🧔', + DELIM + r'man_beard' + DELIM: '🧔‍♂️', + DELIM + r'woman_beard' + DELIM: '🧔‍♀️', + DELIM + r'red_haired_man' + DELIM: '👨‍🦰', + DELIM + r'curly_haired_man' + DELIM: '👨‍🦱', + DELIM + r'white_haired_man' + DELIM: '👨‍🦳', + DELIM + r'bald_man' + DELIM: '👨‍🦲', + DELIM + r'woman' + DELIM: '👩', + DELIM + r'red_haired_woman' + DELIM: '👩‍🦰', + DELIM + r'person_red_hair' + DELIM: '🧑‍🦰', + DELIM + r'curly_haired_woman' + DELIM: '👩‍🦱', + DELIM + r'person_curly_hair' + DELIM: '🧑‍🦱', + DELIM + r'white_haired_woman' + DELIM: '👩‍🦳', + DELIM + r'person_white_hair' + DELIM: '🧑‍🦳', + DELIM + r'bald_woman' + DELIM: '👩‍🦲', + DELIM + r'person_bald' + DELIM: '🧑‍🦲', + DELIM + r'blond_(haired_)?woman' + DELIM: '👱‍♀️', + DELIM + r'blond_haired_man' + DELIM: '👱‍♂️', + DELIM + r'older_adult' + DELIM: '🧓', + DELIM + r'older_man' + DELIM: '👴', + DELIM + r'older_woman' + DELIM: '👵', + + # + # Person Gesture + # + DELIM + r'frowning_person' + DELIM: '🙍', + DELIM + r'frowning_man' + DELIM: '🙍‍♂️', + DELIM + r'frowning_woman' + DELIM: '🙍‍♀️', + DELIM + r'pouting_face' + DELIM: '🙎', + DELIM + r'pouting_man' + DELIM: '🙎‍♂️', + DELIM + r'pouting_woman' + DELIM: '🙎‍♀️', + DELIM + r'no_good' + DELIM: '🙅', + DELIM + r'(ng|no_good)_man' + DELIM: '🙅‍♂️', + DELIM + r'(ng_woman|no_good_woman)' + DELIM: '🙅‍♀️', + DELIM + r'ok_person' + DELIM: '🙆', + DELIM + r'ok_man' + DELIM: '🙆‍♂️', + DELIM + r'ok_woman' + DELIM: '🙆‍♀️', + DELIM + r'(information_desk|tipping_hand_)person' + DELIM: '💁', + DELIM + r'(sassy_man|tipping_hand_man)' + DELIM: '💁‍♂️', + DELIM + r'(sassy_woman|tipping_hand_woman)' + DELIM: '💁‍♀️', + DELIM + r'raising_hand' + DELIM: '🙋', + DELIM + r'raising_hand_man' + DELIM: '🙋‍♂️', + DELIM + r'raising_hand_woman' + DELIM: '🙋‍♀️', + DELIM + r'deaf_person' + DELIM: '🧏', + DELIM + r'deaf_man' + DELIM: '🧏‍♂️', + DELIM + r'deaf_woman' + DELIM: '🧏‍♀️', + DELIM + r'bow' + DELIM: '🙇', + DELIM + r'bowing_man' + DELIM: '🙇‍♂️', + DELIM + r'bowing_woman' + DELIM: '🙇‍♀️', + DELIM + r'facepalm' + DELIM: '🤦', + DELIM + r'man_facepalming' + DELIM: '🤦‍♂️', + DELIM + r'woman_facepalming' + DELIM: '🤦‍♀️', + DELIM + r'shrug' + DELIM: '🤷', + DELIM + r'man_shrugging' + DELIM: '🤷‍♂️', + DELIM + r'woman_shrugging' + DELIM: '🤷‍♀️', + + # + # Person Role + # + DELIM + r'health_worker' + DELIM: '🧑‍⚕️', + DELIM + r'man_health_worker' + DELIM: '👨‍⚕️', + DELIM + r'woman_health_worker' + DELIM: '👩‍⚕️', + DELIM + r'student' + DELIM: '🧑‍🎓', + DELIM + r'man_student' + DELIM: '👨‍🎓', + DELIM + r'woman_student' + DELIM: '👩‍🎓', + DELIM + r'teacher' + DELIM: '🧑‍🏫', + DELIM + r'man_teacher' + DELIM: '👨‍🏫', + DELIM + r'woman_teacher' + DELIM: '👩‍🏫', + DELIM + r'judge' + DELIM: '🧑‍⚖️', + DELIM + r'man_judge' + DELIM: '👨‍⚖️', + DELIM + r'woman_judge' + DELIM: '👩‍⚖️', + DELIM + r'farmer' + DELIM: '🧑‍🌾', + DELIM + r'man_farmer' + DELIM: '👨‍🌾', + DELIM + r'woman_farmer' + DELIM: '👩‍🌾', + DELIM + r'cook' + DELIM: '🧑‍🍳', + DELIM + r'man_cook' + DELIM: '👨‍🍳', + DELIM + r'woman_cook' + DELIM: '👩‍🍳', + DELIM + r'mechanic' + DELIM: '🧑‍🔧', + DELIM + r'man_mechanic' + DELIM: '👨‍🔧', + DELIM + r'woman_mechanic' + DELIM: '👩‍🔧', + DELIM + r'factory_worker' + DELIM: '🧑‍🏭', + DELIM + r'man_factory_worker' + DELIM: '👨‍🏭', + DELIM + r'woman_factory_worker' + DELIM: '👩‍🏭', + DELIM + r'office_worker' + DELIM: '🧑‍💼', + DELIM + r'man_office_worker' + DELIM: '👨‍💼', + DELIM + r'woman_office_worker' + DELIM: '👩‍💼', + DELIM + r'scientist' + DELIM: '🧑‍🔬', + DELIM + r'man_scientist' + DELIM: '👨‍🔬', + DELIM + r'woman_scientist' + DELIM: '👩‍🔬', + DELIM + r'technologist' + DELIM: '🧑‍💻', + DELIM + r'man_technologist' + DELIM: '👨‍💻', + DELIM + r'woman_technologist' + DELIM: '👩‍💻', + DELIM + r'singer' + DELIM: '🧑‍🎤', + DELIM + r'man_singer' + DELIM: '👨‍🎤', + DELIM + r'woman_singer' + DELIM: '👩‍🎤', + DELIM + r'artist' + DELIM: '🧑‍🎨', + DELIM + r'man_artist' + DELIM: '👨‍🎨', + DELIM + r'woman_artist' + DELIM: '👩‍🎨', + DELIM + r'pilot' + DELIM: '🧑‍✈️', + DELIM + r'man_pilot' + DELIM: '👨‍✈️', + DELIM + r'woman_pilot' + DELIM: '👩‍✈️', + DELIM + r'astronaut' + DELIM: '🧑‍🚀', + DELIM + r'man_astronaut' + DELIM: '👨‍🚀', + DELIM + r'woman_astronaut' + DELIM: '👩‍🚀', + DELIM + r'firefighter' + DELIM: '🧑‍🚒', + DELIM + r'man_firefighter' + DELIM: '👨‍🚒', + DELIM + r'woman_firefighter' + DELIM: '👩‍🚒', + DELIM + r'cop' + DELIM: '👮', + DELIM + r'police(_officer|man)' + DELIM: '👮‍♂️', + DELIM + r'policewoman' + DELIM: '👮‍♀️', + DELIM + r'detective' + DELIM: '🕵️', + DELIM + r'male_detective' + DELIM: '🕵️‍♂️', + DELIM + r'female_detective' + DELIM: '🕵️‍♀️', + DELIM + r'guard' + DELIM: '💂', + DELIM + r'guardsman' + DELIM: '💂‍♂️', + DELIM + r'guardswoman' + DELIM: '💂‍♀️', + DELIM + r'ninja' + DELIM: '🥷', + DELIM + r'construction_worker' + DELIM: '👷', + DELIM + r'construction_worker_man' + DELIM: '👷‍♂️', + DELIM + r'construction_worker_woman' + DELIM: '👷‍♀️', + DELIM + r'prince' + DELIM: '🤴', + DELIM + r'princess' + DELIM: '👸', + DELIM + r'person_with_turban' + DELIM: '👳', + DELIM + r'man_with_turban' + DELIM: '👳‍♂️', + DELIM + r'woman_with_turban' + DELIM: '👳‍♀️', + DELIM + r'man_with_gua_pi_mao' + DELIM: '👲', + DELIM + r'woman_with_headscarf' + DELIM: '🧕', + DELIM + r'person_in_tuxedo' + DELIM: '🤵', + DELIM + r'man_in_tuxedo' + DELIM: '🤵‍♂️', + DELIM + r'woman_in_tuxedo' + DELIM: '🤵‍♀️', + DELIM + r'person_with_veil' + DELIM: '👰', + DELIM + r'man_with_veil' + DELIM: '👰‍♂️', + DELIM + r'(bride|woman)_with_veil' + DELIM: '👰‍♀️', + DELIM + r'pregnant_woman' + DELIM: '🤰', + DELIM + r'breast_feeding' + DELIM: '🤱', + DELIM + r'woman_feeding_baby' + DELIM: '👩‍🍼', + DELIM + r'man_feeding_baby' + DELIM: '👨‍🍼', + DELIM + r'person_feeding_baby' + DELIM: '🧑‍🍼', + + # + # Person Fantasy + # + DELIM + r'angel' + DELIM: '👼', + DELIM + r'santa' + DELIM: '🎅', + DELIM + r'mrs_claus' + DELIM: '🤶', + DELIM + r'mx_claus' + DELIM: '🧑‍🎄', + DELIM + r'superhero' + DELIM: '🦸', + DELIM + r'superhero_man' + DELIM: '🦸‍♂️', + DELIM + r'superhero_woman' + DELIM: '🦸‍♀️', + DELIM + r'supervillain' + DELIM: '🦹', + DELIM + r'supervillain_man' + DELIM: '🦹‍♂️', + DELIM + r'supervillain_woman' + DELIM: '🦹‍♀️', + DELIM + r'mage' + DELIM: '🧙', + DELIM + r'mage_man' + DELIM: '🧙‍♂️', + DELIM + r'mage_woman' + DELIM: '🧙‍♀️', + DELIM + r'fairy' + DELIM: '🧚', + DELIM + r'fairy_man' + DELIM: '🧚‍♂️', + DELIM + r'fairy_woman' + DELIM: '🧚‍♀️', + DELIM + r'vampire' + DELIM: '🧛', + DELIM + r'vampire_man' + DELIM: '🧛‍♂️', + DELIM + r'vampire_woman' + DELIM: '🧛‍♀️', + DELIM + r'merperson' + DELIM: '🧜', + DELIM + r'merman' + DELIM: '🧜‍♂️', + DELIM + r'mermaid' + DELIM: '🧜‍♀️', + DELIM + r'elf' + DELIM: '🧝', + DELIM + r'elf_man' + DELIM: '🧝‍♂️', + DELIM + r'elf_woman' + DELIM: '🧝‍♀️', + DELIM + r'genie' + DELIM: '🧞', + DELIM + r'genie_man' + DELIM: '🧞‍♂️', + DELIM + r'genie_woman' + DELIM: '🧞‍♀️', + DELIM + r'zombie' + DELIM: '🧟', + DELIM + r'zombie_man' + DELIM: '🧟‍♂️', + DELIM + r'zombie_woman' + DELIM: '🧟‍♀️', + + # + # Person Activity + # + DELIM + r'massage' + DELIM: '💆', + DELIM + r'massage_man' + DELIM: '💆‍♂️', + DELIM + r'massage_woman' + DELIM: '💆‍♀️', + DELIM + r'haircut' + DELIM: '💇', + DELIM + r'haircut_man' + DELIM: '💇‍♂️', + DELIM + r'haircut_woman' + DELIM: '💇‍♀️', + DELIM + r'walking' + DELIM: '🚶', + DELIM + r'walking_man' + DELIM: '🚶‍♂️', + DELIM + r'walking_woman' + DELIM: '🚶‍♀️', + DELIM + r'standing_person' + DELIM: '🧍', + DELIM + r'standing_man' + DELIM: '🧍‍♂️', + DELIM + r'standing_woman' + DELIM: '🧍‍♀️', + DELIM + r'kneeling_person' + DELIM: '🧎', + DELIM + r'kneeling_man' + DELIM: '🧎‍♂️', + DELIM + r'kneeling_woman' + DELIM: '🧎‍♀️', + DELIM + r'person_with_probing_cane' + DELIM: '🧑‍🦯', + DELIM + r'man_with_probing_cane' + DELIM: '👨‍🦯', + DELIM + r'woman_with_probing_cane' + DELIM: '👩‍🦯', + DELIM + r'person_in_motorized_wheelchair' + DELIM: '🧑‍🦼', + DELIM + r'man_in_motorized_wheelchair' + DELIM: '👨‍🦼', + DELIM + r'woman_in_motorized_wheelchair' + DELIM: '👩‍🦼', + DELIM + r'person_in_manual_wheelchair' + DELIM: '🧑‍🦽', + DELIM + r'man_in_manual_wheelchair' + DELIM: '👨‍🦽', + DELIM + r'woman_in_manual_wheelchair' + DELIM: '👩‍🦽', + DELIM + r'runn(er|ing)' + DELIM: '🏃', + DELIM + r'running_man' + DELIM: '🏃‍♂️', + DELIM + r'running_woman' + DELIM: '🏃‍♀️', + DELIM + r'(dancer|woman_dancing)' + DELIM: '💃', + DELIM + r'man_dancing' + DELIM: '🕺', + DELIM + r'business_suit_levitating' + DELIM: '🕴️', + DELIM + r'dancers' + DELIM: '👯', + DELIM + r'dancing_men' + DELIM: '👯‍♂️', + DELIM + r'dancing_women' + DELIM: '👯‍♀️', + DELIM + r'sauna_person' + DELIM: '🧖', + DELIM + r'sauna_man' + DELIM: '🧖‍♂️', + DELIM + r'sauna_woman' + DELIM: '🧖‍♀️', + DELIM + r'climbing' + DELIM: '🧗', + DELIM + r'climbing_man' + DELIM: '🧗‍♂️', + DELIM + r'climbing_woman' + DELIM: '🧗‍♀️', + + # + # Person Sport + # + DELIM + r'person_fencing' + DELIM: '🤺', + DELIM + r'horse_racing' + DELIM: '🏇', + DELIM + r'skier' + DELIM: '⛷️', + DELIM + r'snowboarder' + DELIM: '🏂', + DELIM + r'golfing' + DELIM: '🏌️', + DELIM + r'golfing_man' + DELIM: '🏌️‍♂️', + DELIM + r'golfing_woman' + DELIM: '🏌️‍♀️', + DELIM + r'surfer' + DELIM: '🏄', + DELIM + r'surfing_man' + DELIM: '🏄‍♂️', + DELIM + r'surfing_woman' + DELIM: '🏄‍♀️', + DELIM + r'rowboat' + DELIM: '🚣', + DELIM + r'rowing_man' + DELIM: '🚣‍♂️', + DELIM + r'rowing_woman' + DELIM: '🚣‍♀️', + DELIM + r'swimmer' + DELIM: '🏊', + DELIM + r'swimming_man' + DELIM: '🏊‍♂️', + DELIM + r'swimming_woman' + DELIM: '🏊‍♀️', + DELIM + r'bouncing_ball_person' + DELIM: '⛹️', + DELIM + r'(basketball|bouncing_ball)_man' + DELIM: '⛹️‍♂️', + DELIM + r'(basketball|bouncing_ball)_woman' + DELIM: '⛹️‍♀️', + DELIM + r'weight_lifting' + DELIM: '🏋️', + DELIM + r'weight_lifting_man' + DELIM: '🏋️‍♂️', + DELIM + r'weight_lifting_woman' + DELIM: '🏋️‍♀️', + DELIM + r'bicyclist' + DELIM: '🚴', + DELIM + r'biking_man' + DELIM: '🚴‍♂️', + DELIM + r'biking_woman' + DELIM: '🚴‍♀️', + DELIM + r'mountain_bicyclist' + DELIM: '🚵', + DELIM + r'mountain_biking_man' + DELIM: '🚵‍♂️', + DELIM + r'mountain_biking_woman' + DELIM: '🚵‍♀️', + DELIM + r'cartwheeling' + DELIM: '🤸', + DELIM + r'man_cartwheeling' + DELIM: '🤸‍♂️', + DELIM + r'woman_cartwheeling' + DELIM: '🤸‍♀️', + DELIM + r'wrestling' + DELIM: '🤼', + DELIM + r'men_wrestling' + DELIM: '🤼‍♂️', + DELIM + r'women_wrestling' + DELIM: '🤼‍♀️', + DELIM + r'water_polo' + DELIM: '🤽', + DELIM + r'man_playing_water_polo' + DELIM: '🤽‍♂️', + DELIM + r'woman_playing_water_polo' + DELIM: '🤽‍♀️', + DELIM + r'handball_person' + DELIM: '🤾', + DELIM + r'man_playing_handball' + DELIM: '🤾‍♂️', + DELIM + r'woman_playing_handball' + DELIM: '🤾‍♀️', + DELIM + r'juggling_person' + DELIM: '🤹', + DELIM + r'man_juggling' + DELIM: '🤹‍♂️', + DELIM + r'woman_juggling' + DELIM: '🤹‍♀️', + + # + # Person Resting + # + DELIM + r'lotus_position' + DELIM: '🧘', + DELIM + r'lotus_position_man' + DELIM: '🧘‍♂️', + DELIM + r'lotus_position_woman' + DELIM: '🧘‍♀️', + DELIM + r'bath' + DELIM: '🛀', + DELIM + r'sleeping_bed' + DELIM: '🛌', + + # + # Family + # + DELIM + r'people_holding_hands' + DELIM: '🧑‍🤝‍🧑', + DELIM + r'two_women_holding_hands' + DELIM: '👭', + DELIM + r'couple' + DELIM: '👫', + DELIM + r'two_men_holding_hands' + DELIM: '👬', + DELIM + r'couplekiss' + DELIM: '💏', + DELIM + r'couplekiss_man_woman' + DELIM: '👩‍❤️‍💋‍👨', + DELIM + r'couplekiss_man_man' + DELIM: '👨‍❤️‍💋‍👨', + DELIM + r'couplekiss_woman_woman' + DELIM: '👩‍❤️‍💋‍👩', + DELIM + r'couple_with_heart' + DELIM: '💑', + DELIM + r'couple_with_heart_woman_man' + DELIM: '👩‍❤️‍👨', + DELIM + r'couple_with_heart_man_man' + DELIM: '👨‍❤️‍👨', + DELIM + r'couple_with_heart_woman_woman' + DELIM: '👩‍❤️‍👩', + DELIM + r'family_man_woman_boy' + DELIM: '👨‍👩‍👦', + DELIM + r'family_man_woman_girl' + DELIM: '👨‍👩‍👧', + DELIM + r'family_man_woman_girl_boy' + DELIM: '👨‍👩‍👧‍👦', + DELIM + r'family_man_woman_boy_boy' + DELIM: '👨‍👩‍👦‍👦', + DELIM + r'family_man_woman_girl_girl' + DELIM: '👨‍👩‍👧‍👧', + DELIM + r'family_man_man_boy' + DELIM: '👨‍👨‍👦', + DELIM + r'family_man_man_girl' + DELIM: '👨‍👨‍👧', + DELIM + r'family_man_man_girl_boy' + DELIM: '👨‍👨‍👧‍👦', + DELIM + r'family_man_man_boy_boy' + DELIM: '👨‍👨‍👦‍👦', + DELIM + r'family_man_man_girl_girl' + DELIM: '👨‍👨‍👧‍👧', + DELIM + r'family_woman_woman_boy' + DELIM: '👩‍👩‍👦', + DELIM + r'family_woman_woman_girl' + DELIM: '👩‍👩‍👧', + DELIM + r'family_woman_woman_girl_boy' + DELIM: '👩‍👩‍👧‍👦', + DELIM + r'family_woman_woman_boy_boy' + DELIM: '👩‍👩‍👦‍👦', + DELIM + r'family_woman_woman_girl_girl' + DELIM: '👩‍👩‍👧‍👧', + DELIM + r'family_man_boy' + DELIM: '👨‍👦', + DELIM + r'family_man_boy_boy' + DELIM: '👨‍👦‍👦', + DELIM + r'family_man_girl' + DELIM: '👨‍👧', + DELIM + r'family_man_girl_boy' + DELIM: '👨‍👧‍👦', + DELIM + r'family_man_girl_girl' + DELIM: '👨‍👧‍👧', + DELIM + r'family_woman_boy' + DELIM: '👩‍👦', + DELIM + r'family_woman_boy_boy' + DELIM: '👩‍👦‍👦', + DELIM + r'family_woman_girl' + DELIM: '👩‍👧', + DELIM + r'family_woman_girl_boy' + DELIM: '👩‍👧‍👦', + DELIM + r'family_woman_girl_girl' + DELIM: '👩‍👧‍👧', + + # + # Person Symbol + # + DELIM + r'speaking_head' + DELIM: '🗣️', + DELIM + r'bust_in_silhouette' + DELIM: '👤', + DELIM + r'busts_in_silhouette' + DELIM: '👥', + DELIM + r'people_hugging' + DELIM: '🫂', + DELIM + r'family' + DELIM: '👪', + DELIM + r'footprints' + DELIM: '👣', + + # + # Animal Mammal + # + DELIM + r'monkey_face' + DELIM: '🐵', + DELIM + r'monkey' + DELIM: '🐒', + DELIM + r'gorilla' + DELIM: '🦍', + DELIM + r'orangutan' + DELIM: '🦧', + DELIM + r'dog' + DELIM: '🐶', + DELIM + r'dog2' + DELIM: '🐕', + DELIM + r'guide_dog' + DELIM: '🦮', + DELIM + r'service_dog' + DELIM: '🐕‍🦺', + DELIM + r'poodle' + DELIM: '🐩', + DELIM + r'wolf' + DELIM: '🐺', + DELIM + r'fox_face' + DELIM: '🦊', + DELIM + r'raccoon' + DELIM: '🦝', + DELIM + r'cat' + DELIM: '🐱', + DELIM + r'cat2' + DELIM: '🐈', + DELIM + r'black_cat' + DELIM: '🐈‍⬛', + DELIM + r'lion' + DELIM: '🦁', + DELIM + r'tiger' + DELIM: '🐯', + DELIM + r'tiger2' + DELIM: '🐅', + DELIM + r'leopard' + DELIM: '🐆', + DELIM + r'horse' + DELIM: '🐴', + DELIM + r'racehorse' + DELIM: '🐎', + DELIM + r'unicorn' + DELIM: '🦄', + DELIM + r'zebra' + DELIM: '🦓', + DELIM + r'deer' + DELIM: '🦌', + DELIM + r'bison' + DELIM: '🦬', + DELIM + r'cow' + DELIM: '🐮', + DELIM + r'ox' + DELIM: '🐂', + DELIM + r'water_buffalo' + DELIM: '🐃', + DELIM + r'cow2' + DELIM: '🐄', + DELIM + r'pig' + DELIM: '🐷', + DELIM + r'pig2' + DELIM: '🐖', + DELIM + r'boar' + DELIM: '🐗', + DELIM + r'pig_nose' + DELIM: '🐽', + DELIM + r'ram' + DELIM: '🐏', + DELIM + r'sheep' + DELIM: '🐑', + DELIM + r'goat' + DELIM: '🐐', + DELIM + r'dromedary_camel' + DELIM: '🐪', + DELIM + r'camel' + DELIM: '🐫', + DELIM + r'llama' + DELIM: '🦙', + DELIM + r'giraffe' + DELIM: '🦒', + DELIM + r'elephant' + DELIM: '🐘', + DELIM + r'mammoth' + DELIM: '🦣', + DELIM + r'rhinoceros' + DELIM: '🦏', + DELIM + r'hippopotamus' + DELIM: '🦛', + DELIM + r'mouse' + DELIM: '🐭', + DELIM + r'mouse2' + DELIM: '🐁', + DELIM + r'rat' + DELIM: '🐀', + DELIM + r'hamster' + DELIM: '🐹', + DELIM + r'rabbit' + DELIM: '🐰', + DELIM + r'rabbit2' + DELIM: '🐇', + DELIM + r'chipmunk' + DELIM: '🐿️', + DELIM + r'beaver' + DELIM: '🦫', + DELIM + r'hedgehog' + DELIM: '🦔', + DELIM + r'bat' + DELIM: '🦇', + DELIM + r'bear' + DELIM: '🐻', + DELIM + r'polar_bear' + DELIM: '🐻‍❄️', + DELIM + r'koala' + DELIM: '🐨', + DELIM + r'panda_face' + DELIM: '🐼', + DELIM + r'sloth' + DELIM: '🦥', + DELIM + r'otter' + DELIM: '🦦', + DELIM + r'skunk' + DELIM: '🦨', + DELIM + r'kangaroo' + DELIM: '🦘', + DELIM + r'badger' + DELIM: '🦡', + DELIM + r'(feet|paw_prints)' + DELIM: '🐾', + + # + # Animal Bird + # + DELIM + r'turkey' + DELIM: '🦃', + DELIM + r'chicken' + DELIM: '🐔', + DELIM + r'rooster' + DELIM: '🐓', + DELIM + r'hatching_chick' + DELIM: '🐣', + DELIM + r'baby_chick' + DELIM: '🐤', + DELIM + r'hatched_chick' + DELIM: '🐥', + DELIM + r'bird' + DELIM: '🐦', + DELIM + r'penguin' + DELIM: '🐧', + DELIM + r'dove' + DELIM: '🕊️', + DELIM + r'eagle' + DELIM: '🦅', + DELIM + r'duck' + DELIM: '🦆', + DELIM + r'swan' + DELIM: '🦢', + DELIM + r'owl' + DELIM: '🦉', + DELIM + r'dodo' + DELIM: '🦤', + DELIM + r'feather' + DELIM: '🪶', + DELIM + r'flamingo' + DELIM: '🦩', + DELIM + r'peacock' + DELIM: '🦚', + DELIM + r'parrot' + DELIM: '🦜', + + # + # Animal Amphibian + # + DELIM + r'frog' + DELIM: '🐸', + + # + # Animal Reptile + # + DELIM + r'crocodile' + DELIM: '🐊', + DELIM + r'turtle' + DELIM: '🐢', + DELIM + r'lizard' + DELIM: '🦎', + DELIM + r'snake' + DELIM: '🐍', + DELIM + r'dragon_face' + DELIM: '🐲', + DELIM + r'dragon' + DELIM: '🐉', + DELIM + r'sauropod' + DELIM: '🦕', + DELIM + r't-rex' + DELIM: '🦖', + + # + # Animal Marine + # + DELIM + r'whale' + DELIM: '🐳', + DELIM + r'whale2' + DELIM: '🐋', + DELIM + r'dolphin' + DELIM: '🐬', + DELIM + r'(seal|flipper)' + DELIM: '🦭', + DELIM + r'fish' + DELIM: '🐟', + DELIM + r'tropical_fish' + DELIM: '🐠', + DELIM + r'blowfish' + DELIM: '🐡', + DELIM + r'shark' + DELIM: '🦈', + DELIM + r'octopus' + DELIM: '🐙', + DELIM + r'shell' + DELIM: '🐚', + + # + # Animal Bug + # + DELIM + r'snail' + DELIM: '🐌', + DELIM + r'butterfly' + DELIM: '🦋', + DELIM + r'bug' + DELIM: '🐛', + DELIM + r'ant' + DELIM: '🐜', + DELIM + r'bee' + DELIM: '🐝', + DELIM + r'honeybee' + DELIM: '🪲', + DELIM + r'(lady_)?beetle' + DELIM: '🐞', + DELIM + r'cricket' + DELIM: '🦗', + DELIM + r'cockroach' + DELIM: '🪳', + DELIM + r'spider' + DELIM: '🕷️', + DELIM + r'spider_web' + DELIM: '🕸️', + DELIM + r'scorpion' + DELIM: '🦂', + DELIM + r'mosquito' + DELIM: '🦟', + DELIM + r'fly' + DELIM: '🪰', + DELIM + r'worm' + DELIM: '🪱', + DELIM + r'microbe' + DELIM: '🦠', + + # + # Plant Flower + # + DELIM + r'bouquet' + DELIM: '💐', + DELIM + r'cherry_blossom' + DELIM: '🌸', + DELIM + r'white_flower' + DELIM: '💮', + DELIM + r'rosette' + DELIM: '🏵️', + DELIM + r'rose' + DELIM: '🌹', + DELIM + r'wilted_flower' + DELIM: '🥀', + DELIM + r'hibiscus' + DELIM: '🌺', + DELIM + r'sunflower' + DELIM: '🌻', + DELIM + r'blossom' + DELIM: '🌼', + DELIM + r'tulip' + DELIM: '🌷', + + # + # Plant Other + # + DELIM + r'seedling' + DELIM: '🌱', + DELIM + r'potted_plant' + DELIM: '🪴', + DELIM + r'evergreen_tree' + DELIM: '🌲', + DELIM + r'deciduous_tree' + DELIM: '🌳', + DELIM + r'palm_tree' + DELIM: '🌴', + DELIM + r'cactus' + DELIM: '🌵', + DELIM + r'ear_of_rice' + DELIM: '🌾', + DELIM + r'herb' + DELIM: '🌿', + DELIM + r'shamrock' + DELIM: '☘️', + DELIM + r'four_leaf_clover' + DELIM: '🍀', + DELIM + r'maple_leaf' + DELIM: '🍁', + DELIM + r'fallen_leaf' + DELIM: '🍂', + DELIM + r'leaves' + DELIM: '🍃', + DELIM + r'mushroom' + DELIM: '🍄', + + # + # Food Fruit + # + DELIM + r'grapes' + DELIM: '🍇', + DELIM + r'melon' + DELIM: '🍈', + DELIM + r'watermelon' + DELIM: '🍉', + DELIM + r'(orange|mandarin|tangerine)' + DELIM: '🍊', + DELIM + r'lemon' + DELIM: '🍋', + DELIM + r'banana' + DELIM: '🍌', + DELIM + r'pineapple' + DELIM: '🍍', + DELIM + r'mango' + DELIM: '🥭', + DELIM + r'apple' + DELIM: '🍎', + DELIM + r'green_apple' + DELIM: '🍏', + DELIM + r'pear' + DELIM: '🍐', + DELIM + r'peach' + DELIM: '🍑', + DELIM + r'cherries' + DELIM: '🍒', + DELIM + r'strawberry' + DELIM: '🍓', + DELIM + r'blueberries' + DELIM: '🫐', + DELIM + r'kiwi_fruit' + DELIM: '🥝', + DELIM + r'tomato' + DELIM: '🍅', + DELIM + r'olive' + DELIM: '🫒', + DELIM + r'coconut' + DELIM: '🥥', + + # + # Food Vegetable + # + DELIM + r'avocado' + DELIM: '🥑', + DELIM + r'eggplant' + DELIM: '🍆', + DELIM + r'potato' + DELIM: '🥔', + DELIM + r'carrot' + DELIM: '🥕', + DELIM + r'corn' + DELIM: '🌽', + DELIM + r'hot_pepper' + DELIM: '🌶️', + DELIM + r'bell_pepper' + DELIM: '🫑', + DELIM + r'cucumber' + DELIM: '🥒', + DELIM + r'leafy_green' + DELIM: '🥬', + DELIM + r'broccoli' + DELIM: '🥦', + DELIM + r'garlic' + DELIM: '🧄', + DELIM + r'onion' + DELIM: '🧅', + DELIM + r'peanuts' + DELIM: '🥜', + DELIM + r'chestnut' + DELIM: '🌰', + + # + # Food Prepared + # + DELIM + r'bread' + DELIM: '🍞', + DELIM + r'croissant' + DELIM: '🥐', + DELIM + r'baguette_bread' + DELIM: '🥖', + DELIM + r'flatbread' + DELIM: '🫓', + DELIM + r'pretzel' + DELIM: '🥨', + DELIM + r'bagel' + DELIM: '🥯', + DELIM + r'pancakes' + DELIM: '🥞', + DELIM + r'waffle' + DELIM: '🧇', + DELIM + r'cheese' + DELIM: '🧀', + DELIM + r'meat_on_bone' + DELIM: '🍖', + DELIM + r'poultry_leg' + DELIM: '🍗', + DELIM + r'cut_of_meat' + DELIM: '🥩', + DELIM + r'bacon' + DELIM: '🥓', + DELIM + r'hamburger' + DELIM: '🍔', + DELIM + r'fries' + DELIM: '🍟', + DELIM + r'pizza' + DELIM: '🍕', + DELIM + r'hotdog' + DELIM: '🌭', + DELIM + r'sandwich' + DELIM: '🥪', + DELIM + r'taco' + DELIM: '🌮', + DELIM + r'burrito' + DELIM: '🌯', + DELIM + r'tamale' + DELIM: '🫔', + DELIM + r'stuffed_flatbread' + DELIM: '🥙', + DELIM + r'falafel' + DELIM: '🧆', + DELIM + r'egg' + DELIM: '🥚', + DELIM + r'fried_egg' + DELIM: '🍳', + DELIM + r'shallow_pan_of_food' + DELIM: '🥘', + DELIM + r'stew' + DELIM: '🍲', + DELIM + r'fondue' + DELIM: '🫕', + DELIM + r'bowl_with_spoon' + DELIM: '🥣', + DELIM + r'green_salad' + DELIM: '🥗', + DELIM + r'popcorn' + DELIM: '🍿', + DELIM + r'butter' + DELIM: '🧈', + DELIM + r'salt' + DELIM: '🧂', + DELIM + r'canned_food' + DELIM: '🥫', + + # + # Food Asian + # + DELIM + r'bento' + DELIM: '🍱', + DELIM + r'rice_cracker' + DELIM: '🍘', + DELIM + r'rice_ball' + DELIM: '🍙', + DELIM + r'rice' + DELIM: '🍚', + DELIM + r'curry' + DELIM: '🍛', + DELIM + r'ramen' + DELIM: '🍜', + DELIM + r'spaghetti' + DELIM: '🍝', + DELIM + r'sweet_potato' + DELIM: '🍠', + DELIM + r'oden' + DELIM: '🍢', + DELIM + r'sushi' + DELIM: '🍣', + DELIM + r'fried_shrimp' + DELIM: '🍤', + DELIM + r'fish_cake' + DELIM: '🍥', + DELIM + r'moon_cake' + DELIM: '🥮', + DELIM + r'dango' + DELIM: '🍡', + DELIM + r'dumpling' + DELIM: '🥟', + DELIM + r'fortune_cookie' + DELIM: '🥠', + DELIM + r'takeout_box' + DELIM: '🥡', + + # + # Food Marine + # + DELIM + r'crab' + DELIM: '🦀', + DELIM + r'lobster' + DELIM: '🦞', + DELIM + r'shrimp' + DELIM: '🦐', + DELIM + r'squid' + DELIM: '🦑', + DELIM + r'oyster' + DELIM: '🦪', + + # + # Food Sweet + # + DELIM + r'icecream' + DELIM: '🍦', + DELIM + r'shaved_ice' + DELIM: '🍧', + DELIM + r'ice_cream' + DELIM: '🍨', + DELIM + r'doughnut' + DELIM: '🍩', + DELIM + r'cookie' + DELIM: '🍪', + DELIM + r'birthday' + DELIM: '🎂', + DELIM + r'cake' + DELIM: '🍰', + DELIM + r'cupcake' + DELIM: '🧁', + DELIM + r'pie' + DELIM: '🥧', + DELIM + r'chocolate_bar' + DELIM: '🍫', + DELIM + r'candy' + DELIM: '🍬', + DELIM + r'lollipop' + DELIM: '🍭', + DELIM + r'custard' + DELIM: '🍮', + DELIM + r'honey_pot' + DELIM: '🍯', + + # + # Drink + # + DELIM + r'baby_bottle' + DELIM: '🍼', + DELIM + r'milk_glass' + DELIM: '🥛', + DELIM + r'coffee' + DELIM: '☕', + DELIM + r'teapot' + DELIM: '🫖', + DELIM + r'tea' + DELIM: '🍵', + DELIM + r'sake' + DELIM: '🍶', + DELIM + r'champagne' + DELIM: '🍾', + DELIM + r'wine_glass' + DELIM: '🍷', + DELIM + r'cocktail' + DELIM: '🍸', + DELIM + r'tropical_drink' + DELIM: '🍹', + DELIM + r'beer' + DELIM: '🍺', + DELIM + r'beers' + DELIM: '🍻', + DELIM + r'clinking_glasses' + DELIM: '🥂', + DELIM + r'tumbler_glass' + DELIM: '🥃', + DELIM + r'cup_with_straw' + DELIM: '🥤', + DELIM + r'bubble_tea' + DELIM: '🧋', + DELIM + r'beverage_box' + DELIM: '🧃', + DELIM + r'mate' + DELIM: '🧉', + DELIM + r'ice_cube' + DELIM: '🧊', + + # + # Dishware + # + DELIM + r'chopsticks' + DELIM: '🥢', + DELIM + r'plate_with_cutlery' + DELIM: '🍽️', + DELIM + r'fork_and_knife' + DELIM: '🍴', + DELIM + r'spoon' + DELIM: '🥄', + DELIM + r'(hocho|knife)' + DELIM: '🔪', + DELIM + r'amphora' + DELIM: '🏺', + + # + # Place Map + # + DELIM + r'earth_africa' + DELIM: '🌍', + DELIM + r'earth_americas' + DELIM: '🌎', + DELIM + r'earth_asia' + DELIM: '🌏', + DELIM + r'globe_with_meridians' + DELIM: '🌐', + DELIM + r'world_map' + DELIM: '🗺️', + DELIM + r'japan' + DELIM: '🗾', + DELIM + r'compass' + DELIM: '🧭', + + # + # Place Geographic + # + DELIM + r'mountain_snow' + DELIM: '🏔️', + DELIM + r'mountain' + DELIM: '⛰️', + DELIM + r'volcano' + DELIM: '🌋', + DELIM + r'mount_fuji' + DELIM: '🗻', + DELIM + r'camping' + DELIM: '🏕️', + DELIM + r'beach_umbrella' + DELIM: '🏖️', + DELIM + r'desert' + DELIM: '🏜️', + DELIM + r'desert_island' + DELIM: '🏝️', + DELIM + r'national_park' + DELIM: '🏞️', + + # + # Place Building + # + DELIM + r'stadium' + DELIM: '🏟️', + DELIM + r'classical_building' + DELIM: '🏛️', + DELIM + r'building_construction' + DELIM: '🏗️', + DELIM + r'bricks' + DELIM: '🧱', + DELIM + r'rock' + DELIM: '🪨', + DELIM + r'wood' + DELIM: '🪵', + DELIM + r'hut' + DELIM: '🛖', + DELIM + r'houses' + DELIM: '🏘️', + DELIM + r'derelict_house' + DELIM: '🏚️', + DELIM + r'house' + DELIM: '🏠', + DELIM + r'house_with_garden' + DELIM: '🏡', + DELIM + r'office' + DELIM: '🏢', + DELIM + r'post_office' + DELIM: '🏣', + DELIM + r'european_post_office' + DELIM: '🏤', + DELIM + r'hospital' + DELIM: '🏥', + DELIM + r'bank' + DELIM: '🏦', + DELIM + r'hotel' + DELIM: '🏨', + DELIM + r'love_hotel' + DELIM: '🏩', + DELIM + r'convenience_store' + DELIM: '🏪', + DELIM + r'school' + DELIM: '🏫', + DELIM + r'department_store' + DELIM: '🏬', + DELIM + r'factory' + DELIM: '🏭', + DELIM + r'japanese_castle' + DELIM: '🏯', + DELIM + r'european_castle' + DELIM: '🏰', + DELIM + r'wedding' + DELIM: '💒', + DELIM + r'tokyo_tower' + DELIM: '🗼', + DELIM + r'statue_of_liberty' + DELIM: '🗽', + + # + # Place Religious + # + DELIM + r'church' + DELIM: '⛪', + DELIM + r'mosque' + DELIM: '🕌', + DELIM + r'hindu_temple' + DELIM: '🛕', + DELIM + r'synagogue' + DELIM: '🕍', + DELIM + r'shinto_shrine' + DELIM: '⛩️', + DELIM + r'kaaba' + DELIM: '🕋', + + # + # Place Other + # + DELIM + r'fountain' + DELIM: '⛲', + DELIM + r'tent' + DELIM: '⛺', + DELIM + r'foggy' + DELIM: '🌁', + DELIM + r'night_with_stars' + DELIM: '🌃', + DELIM + r'cityscape' + DELIM: '🏙️', + DELIM + r'sunrise_over_mountains' + DELIM: '🌄', + DELIM + r'sunrise' + DELIM: '🌅', + DELIM + r'city_sunset' + DELIM: '🌆', + DELIM + r'city_sunrise' + DELIM: '🌇', + DELIM + r'bridge_at_night' + DELIM: '🌉', + DELIM + r'hotsprings' + DELIM: '♨️', + DELIM + r'carousel_horse' + DELIM: '🎠', + DELIM + r'ferris_wheel' + DELIM: '🎡', + DELIM + r'roller_coaster' + DELIM: '🎢', + DELIM + r'barber' + DELIM: '💈', + DELIM + r'circus_tent' + DELIM: '🎪', + + # + # Transport Ground + # + DELIM + r'steam_locomotive' + DELIM: '🚂', + DELIM + r'railway_car' + DELIM: '🚃', + DELIM + r'bullettrain_side' + DELIM: '🚄', + DELIM + r'bullettrain_front' + DELIM: '🚅', + DELIM + r'train2' + DELIM: '🚆', + DELIM + r'metro' + DELIM: '🚇', + DELIM + r'light_rail' + DELIM: '🚈', + DELIM + r'station' + DELIM: '🚉', + DELIM + r'tram' + DELIM: '🚊', + DELIM + r'monorail' + DELIM: '🚝', + DELIM + r'mountain_railway' + DELIM: '🚞', + DELIM + r'train' + DELIM: '🚋', + DELIM + r'bus' + DELIM: '🚌', + DELIM + r'oncoming_bus' + DELIM: '🚍', + DELIM + r'trolleybus' + DELIM: '🚎', + DELIM + r'minibus' + DELIM: '🚐', + DELIM + r'ambulance' + DELIM: '🚑', + DELIM + r'fire_engine' + DELIM: '🚒', + DELIM + r'police_car' + DELIM: '🚓', + DELIM + r'oncoming_police_car' + DELIM: '🚔', + DELIM + r'taxi' + DELIM: '🚕', + DELIM + r'oncoming_taxi' + DELIM: '🚖', + DELIM + r'car' + DELIM: '🚗', + DELIM + r'(red_car|oncoming_automobile)' + DELIM: '🚘', + DELIM + r'blue_car' + DELIM: '🚙', + DELIM + r'pickup_truck' + DELIM: '🛻', + DELIM + r'truck' + DELIM: '🚚', + DELIM + r'articulated_lorry' + DELIM: '🚛', + DELIM + r'tractor' + DELIM: '🚜', + DELIM + r'racing_car' + DELIM: '🏎️', + DELIM + r'motorcycle' + DELIM: '🏍️', + DELIM + r'motor_scooter' + DELIM: '🛵', + DELIM + r'manual_wheelchair' + DELIM: '🦽', + DELIM + r'motorized_wheelchair' + DELIM: '🦼', + DELIM + r'auto_rickshaw' + DELIM: '🛺', + DELIM + r'bike' + DELIM: '🚲', + DELIM + r'kick_scooter' + DELIM: '🛴', + DELIM + r'skateboard' + DELIM: '🛹', + DELIM + r'roller_skate' + DELIM: '🛼', + DELIM + r'busstop' + DELIM: '🚏', + DELIM + r'motorway' + DELIM: '🛣️', + DELIM + r'railway_track' + DELIM: '🛤️', + DELIM + r'oil_drum' + DELIM: '🛢️', + DELIM + r'fuelpump' + DELIM: '⛽', + DELIM + r'rotating_light' + DELIM: '🚨', + DELIM + r'traffic_light' + DELIM: '🚥', + DELIM + r'vertical_traffic_light' + DELIM: '🚦', + DELIM + r'stop_sign' + DELIM: '🛑', + DELIM + r'construction' + DELIM: '🚧', + + # + # Transport Water + # + DELIM + r'anchor' + DELIM: '⚓', + DELIM + r'(sailboat|boat)' + DELIM: '⛵', + DELIM + r'canoe' + DELIM: '🛶', + DELIM + r'speedboat' + DELIM: '🚤', + DELIM + r'passenger_ship' + DELIM: '🛳️', + DELIM + r'ferry' + DELIM: '⛴️', + DELIM + r'motor_boat' + DELIM: '🛥️', + DELIM + r'ship' + DELIM: '🚢', + + # + # Transport Air + # + DELIM + r'airplane' + DELIM: '✈️', + DELIM + r'small_airplane' + DELIM: '🛩️', + DELIM + r'flight_departure' + DELIM: '🛫', + DELIM + r'flight_arrival' + DELIM: '🛬', + DELIM + r'parachute' + DELIM: '🪂', + DELIM + r'seat' + DELIM: '💺', + DELIM + r'helicopter' + DELIM: '🚁', + DELIM + r'suspension_railway' + DELIM: '🚟', + DELIM + r'mountain_cableway' + DELIM: '🚠', + DELIM + r'aerial_tramway' + DELIM: '🚡', + DELIM + r'artificial_satellite' + DELIM: '🛰️', + DELIM + r'rocket' + DELIM: '🚀', + DELIM + r'flying_saucer' + DELIM: '🛸', + + # + # Hotel + # + DELIM + r'bellhop_bell' + DELIM: '🛎️', + DELIM + r'luggage' + DELIM: '🧳', + + # + # Time + # + DELIM + r'hourglass' + DELIM: '⌛', + DELIM + r'hourglass_flowing_sand' + DELIM: '⏳', + DELIM + r'watch' + DELIM: '⌚', + DELIM + r'alarm_clock' + DELIM: '⏰', + DELIM + r'stopwatch' + DELIM: '⏱️', + DELIM + r'timer_clock' + DELIM: '⏲️', + DELIM + r'mantelpiece_clock' + DELIM: '🕰️', + DELIM + r'clock12' + DELIM: '🕛', + DELIM + r'clock1230' + DELIM: '🕧', + DELIM + r'clock1' + DELIM: '🕐', + DELIM + r'clock130' + DELIM: '🕜', + DELIM + r'clock2' + DELIM: '🕑', + DELIM + r'clock230' + DELIM: '🕝', + DELIM + r'clock3' + DELIM: '🕒', + DELIM + r'clock330' + DELIM: '🕞', + DELIM + r'clock4' + DELIM: '🕓', + DELIM + r'clock430' + DELIM: '🕟', + DELIM + r'clock5' + DELIM: '🕔', + DELIM + r'clock530' + DELIM: '🕠', + DELIM + r'clock6' + DELIM: '🕕', + DELIM + r'clock630' + DELIM: '🕡', + DELIM + r'clock7' + DELIM: '🕖', + DELIM + r'clock730' + DELIM: '🕢', + DELIM + r'clock8' + DELIM: '🕗', + DELIM + r'clock830' + DELIM: '🕣', + DELIM + r'clock9' + DELIM: '🕘', + DELIM + r'clock930' + DELIM: '🕤', + DELIM + r'clock10' + DELIM: '🕙', + DELIM + r'clock1030' + DELIM: '🕥', + DELIM + r'clock11' + DELIM: '🕚', + DELIM + r'clock1130' + DELIM: '🕦', + + # Sky & Weather + DELIM + r'new_moon' + DELIM: '🌑', + DELIM + r'waxing_crescent_moon' + DELIM: '🌒', + DELIM + r'first_quarter_moon' + DELIM: '🌓', + DELIM + r'moon' + DELIM: '🌔', + DELIM + r'(waxing_gibbous_moon|full_moon)' + DELIM: '🌕', + DELIM + r'waning_gibbous_moon' + DELIM: '🌖', + DELIM + r'last_quarter_moon' + DELIM: '🌗', + DELIM + r'waning_crescent_moon' + DELIM: '🌘', + DELIM + r'crescent_moon' + DELIM: '🌙', + DELIM + r'new_moon_with_face' + DELIM: '🌚', + DELIM + r'first_quarter_moon_with_face' + DELIM: '🌛', + DELIM + r'last_quarter_moon_with_face' + DELIM: '🌜', + DELIM + r'thermometer' + DELIM: '🌡️', + DELIM + r'sunny' + DELIM: '☀️', + DELIM + r'full_moon_with_face' + DELIM: '🌝', + DELIM + r'sun_with_face' + DELIM: '🌞', + DELIM + r'ringed_planet' + DELIM: '🪐', + DELIM + r'star' + DELIM: '⭐', + DELIM + r'star2' + DELIM: '🌟', + DELIM + r'stars' + DELIM: '🌠', + DELIM + r'milky_way' + DELIM: '🌌', + DELIM + r'cloud' + DELIM: '☁️', + DELIM + r'partly_sunny' + DELIM: '⛅', + DELIM + r'cloud_with_lightning_and_rain' + DELIM: '⛈️', + DELIM + r'sun_behind_small_cloud' + DELIM: '🌤️', + DELIM + r'sun_behind_large_cloud' + DELIM: '🌥️', + DELIM + r'sun_behind_rain_cloud' + DELIM: '🌦️', + DELIM + r'cloud_with_rain' + DELIM: '🌧️', + DELIM + r'cloud_with_snow' + DELIM: '🌨️', + DELIM + r'cloud_with_lightning' + DELIM: '🌩️', + DELIM + r'tornado' + DELIM: '🌪️', + DELIM + r'fog' + DELIM: '🌫️', + DELIM + r'wind_face' + DELIM: '🌬️', + DELIM + r'cyclone' + DELIM: '🌀', + DELIM + r'rainbow' + DELIM: '🌈', + DELIM + r'closed_umbrella' + DELIM: '🌂', + DELIM + r'open_umbrella' + DELIM: '☂️', + DELIM + r'umbrella' + DELIM: '☔', + DELIM + r'parasol_on_ground' + DELIM: '⛱️', + DELIM + r'zap' + DELIM: '⚡', + DELIM + r'snowflake' + DELIM: '❄️', + DELIM + r'snowman_with_snow' + DELIM: '☃️', + DELIM + r'snowman' + DELIM: '⛄', + DELIM + r'comet' + DELIM: '☄️', + DELIM + r'fire' + DELIM: '🔥', + DELIM + r'droplet' + DELIM: '💧', + DELIM + r'ocean' + DELIM: '🌊', + + # + # Event + # + DELIM + r'jack_o_lantern' + DELIM: '🎃', + DELIM + r'christmas_tree' + DELIM: '🎄', + DELIM + r'fireworks' + DELIM: '🎆', + DELIM + r'sparkler' + DELIM: '🎇', + DELIM + r'firecracker' + DELIM: '🧨', + DELIM + r'sparkles' + DELIM: '✨', + DELIM + r'balloon' + DELIM: '🎈', + DELIM + r'tada' + DELIM: '🎉', + DELIM + r'confetti_ball' + DELIM: '🎊', + DELIM + r'tanabata_tree' + DELIM: '🎋', + DELIM + r'bamboo' + DELIM: '🎍', + DELIM + r'dolls' + DELIM: '🎎', + DELIM + r'flags' + DELIM: '🎏', + DELIM + r'wind_chime' + DELIM: '🎐', + DELIM + r'rice_scene' + DELIM: '🎑', + DELIM + r'red_envelope' + DELIM: '🧧', + DELIM + r'ribbon' + DELIM: '🎀', + DELIM + r'gift' + DELIM: '🎁', + DELIM + r'reminder_ribbon' + DELIM: '🎗️', + DELIM + r'tickets' + DELIM: '🎟️', + DELIM + r'ticket' + DELIM: '🎫', + + # + # Award Medal + # + DELIM + r'medal_military' + DELIM: '🎖️', + DELIM + r'trophy' + DELIM: '🏆', + DELIM + r'medal_sports' + DELIM: '🏅', + DELIM + r'1st_place_medal' + DELIM: '🥇', + DELIM + r'2nd_place_medal' + DELIM: '🥈', + DELIM + r'3rd_place_medal' + DELIM: '🥉', + + # + # Sport + # + DELIM + r'soccer' + DELIM: '⚽', + DELIM + r'baseball' + DELIM: '⚾', + DELIM + r'softball' + DELIM: '🥎', + DELIM + r'basketball' + DELIM: '🏀', + DELIM + r'volleyball' + DELIM: '🏐', + DELIM + r'football' + DELIM: '🏈', + DELIM + r'rugby_football' + DELIM: '🏉', + DELIM + r'tennis' + DELIM: '🎾', + DELIM + r'flying_disc' + DELIM: '🥏', + DELIM + r'bowling' + DELIM: '🎳', + DELIM + r'cricket_game' + DELIM: '🏏', + DELIM + r'field_hockey' + DELIM: '🏑', + DELIM + r'ice_hockey' + DELIM: '🏒', + DELIM + r'lacrosse' + DELIM: '🥍', + DELIM + r'ping_pong' + DELIM: '🏓', + DELIM + r'badminton' + DELIM: '🏸', + DELIM + r'boxing_glove' + DELIM: '🥊', + DELIM + r'martial_arts_uniform' + DELIM: '🥋', + DELIM + r'goal_net' + DELIM: '🥅', + DELIM + r'golf' + DELIM: '⛳', + DELIM + r'ice_skate' + DELIM: '⛸️', + DELIM + r'fishing_pole_and_fish' + DELIM: '🎣', + DELIM + r'diving_mask' + DELIM: '🤿', + DELIM + r'running_shirt_with_sash' + DELIM: '🎽', + DELIM + r'ski' + DELIM: '🎿', + DELIM + r'sled' + DELIM: '🛷', + DELIM + r'curling_stone' + DELIM: '🥌', + + # + # Game + # + DELIM + r'dart' + DELIM: '🎯', + DELIM + r'yo_yo' + DELIM: '🪀', + DELIM + r'kite' + DELIM: '🪁', + DELIM + r'gun' + DELIM: '🔫', + DELIM + r'8ball' + DELIM: '🎱', + DELIM + r'crystal_ball' + DELIM: '🔮', + DELIM + r'magic_wand' + DELIM: '🪄', + DELIM + r'video_game' + DELIM: '🎮', + DELIM + r'joystick' + DELIM: '🕹️', + DELIM + r'slot_machine' + DELIM: '🎰', + DELIM + r'game_die' + DELIM: '🎲', + DELIM + r'jigsaw' + DELIM: '🧩', + DELIM + r'teddy_bear' + DELIM: '🧸', + DELIM + r'pinata' + DELIM: '🪅', + DELIM + r'nesting_dolls' + DELIM: '🪆', + DELIM + r'spades' + DELIM: '♠️', + DELIM + r'hearts' + DELIM: '♥️', + DELIM + r'diamonds' + DELIM: '♦️', + DELIM + r'clubs' + DELIM: '♣️', + DELIM + r'chess_pawn' + DELIM: '♟️', + DELIM + r'black_joker' + DELIM: '🃏', + DELIM + r'mahjong' + DELIM: '🀄', + DELIM + r'flower_playing_cards' + DELIM: '🎴', + + # + # Arts & Crafts + # + DELIM + r'performing_arts' + DELIM: '🎭', + DELIM + r'framed_picture' + DELIM: '🖼️', + DELIM + r'art' + DELIM: '🎨', + DELIM + r'thread' + DELIM: '🧵', + DELIM + r'sewing_needle' + DELIM: '🪡', + DELIM + r'yarn' + DELIM: '🧶', + DELIM + r'knot' + DELIM: '🪢', + + # + # Clothing + # + DELIM + r'eyeglasses' + DELIM: '👓', + DELIM + r'dark_sunglasses' + DELIM: '🕶️', + DELIM + r'goggles' + DELIM: '🥽', + DELIM + r'lab_coat' + DELIM: '🥼', + DELIM + r'safety_vest' + DELIM: '🦺', + DELIM + r'necktie' + DELIM: '👔', + DELIM + r't?shirt' + DELIM: '👕', + DELIM + r'jeans' + DELIM: '👖', + DELIM + r'scarf' + DELIM: '🧣', + DELIM + r'gloves' + DELIM: '🧤', + DELIM + r'coat' + DELIM: '🧥', + DELIM + r'socks' + DELIM: '🧦', + DELIM + r'dress' + DELIM: '👗', + DELIM + r'kimono' + DELIM: '👘', + DELIM + r'sari' + DELIM: '🥻', + DELIM + r'one_piece_swimsuit' + DELIM: '🩱', + DELIM + r'swim_brief' + DELIM: '🩲', + DELIM + r'shorts' + DELIM: '🩳', + DELIM + r'bikini' + DELIM: '👙', + DELIM + r'womans_clothes' + DELIM: '👚', + DELIM + r'purse' + DELIM: '👛', + DELIM + r'handbag' + DELIM: '👜', + DELIM + r'pouch' + DELIM: '👝', + DELIM + r'shopping' + DELIM: '🛍️', + DELIM + r'school_satchel' + DELIM: '🎒', + DELIM + r'thong_sandal' + DELIM: '🩴', + DELIM + r'(mans_)?shoe' + DELIM: '👞', + DELIM + r'athletic_shoe' + DELIM: '👟', + DELIM + r'hiking_boot' + DELIM: '🥾', + DELIM + r'flat_shoe' + DELIM: '🥿', + DELIM + r'high_heel' + DELIM: '👠', + DELIM + r'sandal' + DELIM: '👡', + DELIM + r'ballet_shoes' + DELIM: '🩰', + DELIM + r'boot' + DELIM: '👢', + DELIM + r'crown' + DELIM: '👑', + DELIM + r'womans_hat' + DELIM: '👒', + DELIM + r'tophat' + DELIM: '🎩', + DELIM + r'mortar_board' + DELIM: '🎓', + DELIM + r'billed_cap' + DELIM: '🧢', + DELIM + r'military_helmet' + DELIM: '🪖', + DELIM + r'rescue_worker_helmet' + DELIM: '⛑️', + DELIM + r'prayer_beads' + DELIM: '📿', + DELIM + r'lipstick' + DELIM: '💄', + DELIM + r'ring' + DELIM: '💍', + DELIM + r'gem' + DELIM: '💎', + + # + # Sound + # + DELIM + r'mute' + DELIM: '🔇', + DELIM + r'speaker' + DELIM: '🔈', + DELIM + r'sound' + DELIM: '🔉', + DELIM + r'loud_sound' + DELIM: '🔊', + DELIM + r'loudspeaker' + DELIM: '📢', + DELIM + r'mega' + DELIM: '📣', + DELIM + r'postal_horn' + DELIM: '📯', + DELIM + r'bell' + DELIM: '🔔', + DELIM + r'no_bell' + DELIM: '🔕', + + # + # Music + # + DELIM + r'musical_score' + DELIM: '🎼', + DELIM + r'musical_note' + DELIM: '🎵', + DELIM + r'notes' + DELIM: '🎶', + DELIM + r'studio_microphone' + DELIM: '🎙️', + DELIM + r'level_slider' + DELIM: '🎚️', + DELIM + r'control_knobs' + DELIM: '🎛️', + DELIM + r'microphone' + DELIM: '🎤', + DELIM + r'headphones' + DELIM: '🎧', + DELIM + r'radio' + DELIM: '📻', + + # + # Musical Instrument + # + DELIM + r'saxophone' + DELIM: '🎷', + DELIM + r'accordion' + DELIM: '🪗', + DELIM + r'guitar' + DELIM: '🎸', + DELIM + r'musical_keyboard' + DELIM: '🎹', + DELIM + r'trumpet' + DELIM: '🎺', + DELIM + r'violin' + DELIM: '🎻', + DELIM + r'banjo' + DELIM: '🪕', + DELIM + r'drum' + DELIM: '🥁', + DELIM + r'long_drum' + DELIM: '🪘', + + # + # Phone + # + DELIM + r'iphone' + DELIM: '📱', + DELIM + r'calling' + DELIM: '📲', + DELIM + r'phone' + DELIM: '☎️', + DELIM + r'telephone(_receiver)?' + DELIM: '📞', + DELIM + r'pager' + DELIM: '📟', + DELIM + r'fax' + DELIM: '📠', + + # + # Computer + # + DELIM + r'battery' + DELIM: '🔋', + DELIM + r'electric_plug' + DELIM: '🔌', + DELIM + r'computer' + DELIM: '💻', + DELIM + r'desktop_computer' + DELIM: '🖥️', + DELIM + r'printer' + DELIM: '🖨️', + DELIM + r'keyboard' + DELIM: '⌨️', + DELIM + r'computer_mouse' + DELIM: '🖱️', + DELIM + r'trackball' + DELIM: '🖲️', + DELIM + r'minidisc' + DELIM: '💽', + DELIM + r'floppy_disk' + DELIM: '💾', + DELIM + r'cd' + DELIM: '💿', + DELIM + r'dvd' + DELIM: '📀', + DELIM + r'abacus' + DELIM: '🧮', + + # + # Light & Video + # + DELIM + r'movie_camera' + DELIM: '🎥', + DELIM + r'film_strip' + DELIM: '🎞️', + DELIM + r'film_projector' + DELIM: '📽️', + DELIM + r'clapper' + DELIM: '🎬', + DELIM + r'tv' + DELIM: '📺', + DELIM + r'camera' + DELIM: '📷', + DELIM + r'camera_flash' + DELIM: '📸', + DELIM + r'video_camera' + DELIM: '📹', + DELIM + r'vhs' + DELIM: '📼', + DELIM + r'mag' + DELIM: '🔍', + DELIM + r'mag_right' + DELIM: '🔎', + DELIM + r'candle' + DELIM: '🕯️', + DELIM + r'bulb' + DELIM: '💡', + DELIM + r'flashlight' + DELIM: '🔦', + DELIM + r'(izakaya_)?lantern' + DELIM: '🏮', + DELIM + r'diya_lamp' + DELIM: '🪔', + + # + # Book Paper + # + DELIM + r'notebook_with_decorative_cover' + DELIM: '📔', + DELIM + r'closed_book' + DELIM: '📕', + DELIM + r'(open_)?book' + DELIM: '📖', + DELIM + r'green_book' + DELIM: '📗', + DELIM + r'blue_book' + DELIM: '📘', + DELIM + r'orange_book' + DELIM: '📙', + DELIM + r'books' + DELIM: '📚', + DELIM + r'notebook' + DELIM: '📓', + DELIM + r'ledger' + DELIM: '📒', + DELIM + r'page_with_curl' + DELIM: '📃', + DELIM + r'scroll' + DELIM: '📜', + DELIM + r'page_facing_up' + DELIM: '📄', + DELIM + r'newspaper' + DELIM: '📰', + DELIM + r'newspaper_roll' + DELIM: '🗞️', + DELIM + r'bookmark_tabs' + DELIM: '📑', + DELIM + r'bookmark' + DELIM: '🔖', + DELIM + r'label' + DELIM: '🏷️', + + # + # Money + # + DELIM + r'moneybag' + DELIM: '💰', + DELIM + r'coin' + DELIM: '🪙', + DELIM + r'yen' + DELIM: '💴', + DELIM + r'dollar' + DELIM: '💵', + DELIM + r'euro' + DELIM: '💶', + DELIM + r'pound' + DELIM: '💷', + DELIM + r'money_with_wings' + DELIM: '💸', + DELIM + r'credit_card' + DELIM: '💳', + DELIM + r'receipt' + DELIM: '🧾', + DELIM + r'chart' + DELIM: '💹', + + # + # Mail + # + DELIM + r'envelope' + DELIM: '✉️', + DELIM + r'e-?mail' + DELIM: '📧', + DELIM + r'incoming_envelope' + DELIM: '📨', + DELIM + r'envelope_with_arrow' + DELIM: '📩', + DELIM + r'outbox_tray' + DELIM: '📤', + DELIM + r'inbox_tray' + DELIM: '📥', + DELIM + r'package' + DELIM: '📦', + DELIM + r'mailbox' + DELIM: '📫', + DELIM + r'mailbox_closed' + DELIM: '📪', + DELIM + r'mailbox_with_mail' + DELIM: '📬', + DELIM + r'mailbox_with_no_mail' + DELIM: '📭', + DELIM + r'postbox' + DELIM: '📮', + DELIM + r'ballot_box' + DELIM: '🗳️', + + # + # Writing + # + DELIM + r'pencil2' + DELIM: '✏️', + DELIM + r'black_nib' + DELIM: '✒️', + DELIM + r'fountain_pen' + DELIM: '🖋️', + DELIM + r'pen' + DELIM: '🖊️', + DELIM + r'paintbrush' + DELIM: '🖌️', + DELIM + r'crayon' + DELIM: '🖍️', + DELIM + r'(memo|pencil)' + DELIM: '📝', + + # + # Office + # + DELIM + r'briefcase' + DELIM: '💼', + DELIM + r'file_folder' + DELIM: '📁', + DELIM + r'open_file_folder' + DELIM: '📂', + DELIM + r'card_index_dividers' + DELIM: '🗂️', + DELIM + r'date' + DELIM: '📅', + DELIM + r'calendar' + DELIM: '📆', + DELIM + r'spiral_notepad' + DELIM: '🗒️', + DELIM + r'spiral_calendar' + DELIM: '🗓️', + DELIM + r'card_index' + DELIM: '📇', + DELIM + r'chart_with_upwards_trend' + DELIM: '📈', + DELIM + r'chart_with_downwards_trend' + DELIM: '📉', + DELIM + r'bar_chart' + DELIM: '📊', + DELIM + r'clipboard' + DELIM: '📋', + DELIM + r'pushpin' + DELIM: '📌', + DELIM + r'round_pushpin' + DELIM: '📍', + DELIM + r'paperclip' + DELIM: '📎', + DELIM + r'paperclips' + DELIM: '🖇️', + DELIM + r'straight_ruler' + DELIM: '📏', + DELIM + r'triangular_ruler' + DELIM: '📐', + DELIM + r'scissors' + DELIM: '✂️', + DELIM + r'card_file_box' + DELIM: '🗃️', + DELIM + r'file_cabinet' + DELIM: '🗄️', + DELIM + r'wastebasket' + DELIM: '🗑️', + + # + # Lock + # + DELIM + r'lock' + DELIM: '🔒', + DELIM + r'unlock' + DELIM: '🔓', + DELIM + r'lock_with_ink_pen' + DELIM: '🔏', + DELIM + r'closed_lock_with_key' + DELIM: '🔐', + DELIM + r'key' + DELIM: '🔑', + DELIM + r'old_key' + DELIM: '🗝️', + + # + # Tool + # + DELIM + r'hammer' + DELIM: '🔨', + DELIM + r'axe' + DELIM: '🪓', + DELIM + r'pick' + DELIM: '⛏️', + DELIM + r'hammer_and_pick' + DELIM: '⚒️', + DELIM + r'hammer_and_wrench' + DELIM: '🛠️', + DELIM + r'dagger' + DELIM: '🗡️', + DELIM + r'crossed_swords' + DELIM: '⚔️', + DELIM + r'bomb' + DELIM: '💣', + DELIM + r'boomerang' + DELIM: '🪃', + DELIM + r'bow_and_arrow' + DELIM: '🏹', + DELIM + r'shield' + DELIM: '🛡️', + DELIM + r'carpentry_saw' + DELIM: '🪚', + DELIM + r'wrench' + DELIM: '🔧', + DELIM + r'screwdriver' + DELIM: '🪛', + DELIM + r'nut_and_bolt' + DELIM: '🔩', + DELIM + r'gear' + DELIM: '⚙️', + DELIM + r'clamp' + DELIM: '🗜️', + DELIM + r'balance_scale' + DELIM: '⚖️', + DELIM + r'probing_cane' + DELIM: '🦯', + DELIM + r'link' + DELIM: '🔗', + DELIM + r'chains' + DELIM: '⛓️', + DELIM + r'hook' + DELIM: '🪝', + DELIM + r'toolbox' + DELIM: '🧰', + DELIM + r'magnet' + DELIM: '🧲', + DELIM + r'ladder' + DELIM: '🪜', + + # + # Science + # + DELIM + r'alembic' + DELIM: '⚗️', + DELIM + r'test_tube' + DELIM: '🧪', + DELIM + r'petri_dish' + DELIM: '🧫', + DELIM + r'dna' + DELIM: '🧬', + DELIM + r'microscope' + DELIM: '🔬', + DELIM + r'telescope' + DELIM: '🔭', + DELIM + r'satellite' + DELIM: '📡', + + # + # Medical + # + DELIM + r'syringe' + DELIM: '💉', + DELIM + r'drop_of_blood' + DELIM: '🩸', + DELIM + r'pill' + DELIM: '💊', + DELIM + r'adhesive_bandage' + DELIM: '🩹', + DELIM + r'stethoscope' + DELIM: '🩺', + + # + # Household + # + DELIM + r'door' + DELIM: '🚪', + DELIM + r'elevator' + DELIM: '🛗', + DELIM + r'mirror' + DELIM: '🪞', + DELIM + r'window' + DELIM: '🪟', + DELIM + r'bed' + DELIM: '🛏️', + DELIM + r'couch_and_lamp' + DELIM: '🛋️', + DELIM + r'chair' + DELIM: '🪑', + DELIM + r'toilet' + DELIM: '🚽', + DELIM + r'plunger' + DELIM: '🪠', + DELIM + r'shower' + DELIM: '🚿', + DELIM + r'bathtub' + DELIM: '🛁', + DELIM + r'mouse_trap' + DELIM: '🪤', + DELIM + r'razor' + DELIM: '🪒', + DELIM + r'lotion_bottle' + DELIM: '🧴', + DELIM + r'safety_pin' + DELIM: '🧷', + DELIM + r'broom' + DELIM: '🧹', + DELIM + r'basket' + DELIM: '🧺', + DELIM + r'roll_of_paper' + DELIM: '🧻', + DELIM + r'bucket' + DELIM: '🪣', + DELIM + r'soap' + DELIM: '🧼', + DELIM + r'toothbrush' + DELIM: '🪥', + DELIM + r'sponge' + DELIM: '🧽', + DELIM + r'fire_extinguisher' + DELIM: '🧯', + DELIM + r'shopping_cart' + DELIM: '🛒', + + # + # Other Object + # + DELIM + r'smoking' + DELIM: '🚬', + DELIM + r'coffin' + DELIM: '⚰️', + DELIM + r'headstone' + DELIM: '🪦', + DELIM + r'funeral_urn' + DELIM: '⚱️', + DELIM + r'nazar_amulet' + DELIM: '🧿', + DELIM + r'moyai' + DELIM: '🗿', + DELIM + r'placard' + DELIM: '🪧', + + # + # Transport Sign + # + DELIM + r'atm' + DELIM: '🏧', + DELIM + r'put_litter_in_its_place' + DELIM: '🚮', + DELIM + r'potable_water' + DELIM: '🚰', + DELIM + r'wheelchair' + DELIM: '♿', + DELIM + r'mens' + DELIM: '🚹', + DELIM + r'womens' + DELIM: '🚺', + DELIM + r'restroom' + DELIM: '🚻', + DELIM + r'baby_symbol' + DELIM: '🚼', + DELIM + r'wc' + DELIM: '🚾', + DELIM + r'passport_control' + DELIM: '🛂', + DELIM + r'customs' + DELIM: '🛃', + DELIM + r'baggage_claim' + DELIM: '🛄', + DELIM + r'left_luggage' + DELIM: '🛅', + + # + # Warning + # + DELIM + r'warning' + DELIM: '⚠️', + DELIM + r'children_crossing' + DELIM: '🚸', + DELIM + r'no_entry' + DELIM: '⛔', + DELIM + r'no_entry_sign' + DELIM: '🚫', + DELIM + r'no_bicycles' + DELIM: '🚳', + DELIM + r'no_smoking' + DELIM: '🚭', + DELIM + r'do_not_litter' + DELIM: '🚯', + DELIM + r'non-potable_water' + DELIM: '🚱', + DELIM + r'no_pedestrians' + DELIM: '🚷', + DELIM + r'no_mobile_phones' + DELIM: '📵', + DELIM + r'underage' + DELIM: '🔞', + DELIM + r'radioactive' + DELIM: '☢️', + DELIM + r'biohazard' + DELIM: '☣️', + + # + # Arrow + # + DELIM + r'arrow_up' + DELIM: '⬆️', + DELIM + r'arrow_upper_right' + DELIM: '↗️', + DELIM + r'arrow_right' + DELIM: '➡️', + DELIM + r'arrow_lower_right' + DELIM: '↘️', + DELIM + r'arrow_down' + DELIM: '⬇️', + DELIM + r'arrow_lower_left' + DELIM: '↙️', + DELIM + r'arrow_left' + DELIM: '⬅️', + DELIM + r'arrow_upper_left' + DELIM: '↖️', + DELIM + r'arrow_up_down' + DELIM: '↕️', + DELIM + r'left_right_arrow' + DELIM: '↔️', + DELIM + r'leftwards_arrow_with_hook' + DELIM: '↩️', + DELIM + r'arrow_right_hook' + DELIM: '↪️', + DELIM + r'arrow_heading_up' + DELIM: '⤴️', + DELIM + r'arrow_heading_down' + DELIM: '⤵️', + DELIM + r'arrows_clockwise' + DELIM: '🔃', + DELIM + r'arrows_counterclockwise' + DELIM: '🔄', + DELIM + r'back' + DELIM: '🔙', + DELIM + r'end' + DELIM: '🔚', + DELIM + r'on' + DELIM: '🔛', + DELIM + r'soon' + DELIM: '🔜', + DELIM + r'top' + DELIM: '🔝', + + # + # Religion + # + DELIM + r'place_of_worship' + DELIM: '🛐', + DELIM + r'atom_symbol' + DELIM: '⚛️', + DELIM + r'om' + DELIM: '🕉️', + DELIM + r'star_of_david' + DELIM: '✡️', + DELIM + r'wheel_of_dharma' + DELIM: '☸️', + DELIM + r'yin_yang' + DELIM: '☯️', + DELIM + r'latin_cross' + DELIM: '✝️', + DELIM + r'orthodox_cross' + DELIM: '☦️', + DELIM + r'star_and_crescent' + DELIM: '☪️', + DELIM + r'peace_symbol' + DELIM: '☮️', + DELIM + r'menorah' + DELIM: '🕎', + DELIM + r'six_pointed_star' + DELIM: '🔯', + + # + # Zodiac + # + DELIM + r'aries' + DELIM: '♈', + DELIM + r'taurus' + DELIM: '♉', + DELIM + r'gemini' + DELIM: '♊', + DELIM + r'cancer' + DELIM: '♋', + DELIM + r'leo' + DELIM: '♌', + DELIM + r'virgo' + DELIM: '♍', + DELIM + r'libra' + DELIM: '♎', + DELIM + r'scorpius' + DELIM: '♏', + DELIM + r'sagittarius' + DELIM: '♐', + DELIM + r'capricorn' + DELIM: '♑', + DELIM + r'aquarius' + DELIM: '♒', + DELIM + r'pisces' + DELIM: '♓', + DELIM + r'ophiuchus' + DELIM: '⛎', + + # + # Av Symbol + # + DELIM + r'twisted_rightwards_arrows' + DELIM: '🔀', + DELIM + r'repeat' + DELIM: '🔁', + DELIM + r'repeat_one' + DELIM: '🔂', + DELIM + r'arrow_forward' + DELIM: '▶️', + DELIM + r'fast_forward' + DELIM: '⏩', + DELIM + r'next_track_button' + DELIM: '⏭️', + DELIM + r'play_or_pause_button' + DELIM: '⏯️', + DELIM + r'arrow_backward' + DELIM: '◀️', + DELIM + r'rewind' + DELIM: '⏪', + DELIM + r'previous_track_button' + DELIM: '⏮️', + DELIM + r'arrow_up_small' + DELIM: '🔼', + DELIM + r'arrow_double_up' + DELIM: '⏫', + DELIM + r'arrow_down_small' + DELIM: '🔽', + DELIM + r'arrow_double_down' + DELIM: '⏬', + DELIM + r'pause_button' + DELIM: '⏸️', + DELIM + r'stop_button' + DELIM: '⏹️', + DELIM + r'record_button' + DELIM: '⏺️', + DELIM + r'eject_button' + DELIM: '⏏️', + DELIM + r'cinema' + DELIM: '🎦', + DELIM + r'low_brightness' + DELIM: '🔅', + DELIM + r'high_brightness' + DELIM: '🔆', + DELIM + r'signal_strength' + DELIM: '📶', + DELIM + r'vibration_mode' + DELIM: '📳', + DELIM + r'mobile_phone_off' + DELIM: '📴', + + # + # Gender + # + DELIM + r'female_sign' + DELIM: '♀️', + DELIM + r'male_sign' + DELIM: '♂️', + DELIM + r'transgender_symbol' + DELIM: '⚧️', + + # + # Math + # + DELIM + r'heavy_multiplication_x' + DELIM: '✖️', + DELIM + r'heavy_plus_sign' + DELIM: '➕', + DELIM + r'heavy_minus_sign' + DELIM: '➖', + DELIM + r'heavy_division_sign' + DELIM: '➗', + DELIM + r'infinity' + DELIM: '♾️', + + # + # Punctuation + # + DELIM + r'bangbang' + DELIM: '‼️', + DELIM + r'interrobang' + DELIM: '⁉️', + DELIM + r'question' + DELIM: '❓', + DELIM + r'grey_question' + DELIM: '❔', + DELIM + r'grey_exclamation' + DELIM: '❕', + DELIM + r'(heavy_exclamation_mark|exclamation)' + DELIM: '❗', + DELIM + r'wavy_dash' + DELIM: '〰️', + + # + # Currency + # + DELIM + r'currency_exchange' + DELIM: '💱', + DELIM + r'heavy_dollar_sign' + DELIM: '💲', + + # + # Other Symbol + # + DELIM + r'medical_symbol' + DELIM: '⚕️', + DELIM + r'recycle' + DELIM: '♻️', + DELIM + r'fleur_de_lis' + DELIM: '⚜️', + DELIM + r'trident' + DELIM: '🔱', + DELIM + r'name_badge' + DELIM: '📛', + DELIM + r'beginner' + DELIM: '🔰', + DELIM + r'o' + DELIM: '⭕', + DELIM + r'white_check_mark' + DELIM: '✅', + DELIM + r'ballot_box_with_check' + DELIM: '☑️', + DELIM + r'heavy_check_mark' + DELIM: '✔️', + DELIM + r'x' + DELIM: '❌', + DELIM + r'negative_squared_cross_mark' + DELIM: '❎', + DELIM + r'curly_loop' + DELIM: '➰', + DELIM + r'loop' + DELIM: '➿', + DELIM + r'part_alternation_mark' + DELIM: '〽️', + DELIM + r'eight_spoked_asterisk' + DELIM: '✳️', + DELIM + r'eight_pointed_black_star' + DELIM: '✴️', + DELIM + r'sparkle' + DELIM: '❇️', + DELIM + r'copyright' + DELIM: '©️', + DELIM + r'registered' + DELIM: '®️', + DELIM + r'tm' + DELIM: '™️', + + # + # Keycap + # + DELIM + r'hash' + DELIM: '#️⃣', + DELIM + r'asterisk' + DELIM: '*️⃣', + DELIM + r'zero' + DELIM: '0️⃣', + DELIM + r'one' + DELIM: '1️⃣', + DELIM + r'two' + DELIM: '2️⃣', + DELIM + r'three' + DELIM: '3️⃣', + DELIM + r'four' + DELIM: '4️⃣', + DELIM + r'five' + DELIM: '5️⃣', + DELIM + r'six' + DELIM: '6️⃣', + DELIM + r'seven' + DELIM: '7️⃣', + DELIM + r'eight' + DELIM: '8️⃣', + DELIM + r'nine' + DELIM: '9️⃣', + DELIM + r'keycap_ten' + DELIM: '🔟', + + # + # Alphanum + # + DELIM + r'capital_abcd' + DELIM: '🔠', + DELIM + r'abcd' + DELIM: '🔡', + DELIM + r'1234' + DELIM: '🔢', + DELIM + r'symbols' + DELIM: '🔣', + DELIM + r'abc' + DELIM: '🔤', + DELIM + r'a' + DELIM: '🅰️', + DELIM + r'ab' + DELIM: '🆎', + DELIM + r'b' + DELIM: '🅱️', + DELIM + r'cl' + DELIM: '🆑', + DELIM + r'cool' + DELIM: '🆒', + DELIM + r'free' + DELIM: '🆓', + DELIM + r'information_source' + DELIM: 'ℹ️', + DELIM + r'id' + DELIM: '🆔', + DELIM + r'm' + DELIM: 'Ⓜ️', + DELIM + r'new' + DELIM: '🆕', + DELIM + r'ng' + DELIM: '🆖', + DELIM + r'o2' + DELIM: '🅾️', + DELIM + r'ok' + DELIM: '🆗', + DELIM + r'parking' + DELIM: '🅿️', + DELIM + r'sos' + DELIM: '🆘', + DELIM + r'up' + DELIM: '🆙', + DELIM + r'vs' + DELIM: '🆚', + DELIM + r'koko' + DELIM: '🈁', + DELIM + r'sa' + DELIM: '🈂️', + DELIM + r'u6708' + DELIM: '🈷️', + DELIM + r'u6709' + DELIM: '🈶', + DELIM + r'u6307' + DELIM: '🈯', + DELIM + r'ideograph_advantage' + DELIM: '🉐', + DELIM + r'u5272' + DELIM: '🈹', + DELIM + r'u7121' + DELIM: '🈚', + DELIM + r'u7981' + DELIM: '🈲', + DELIM + r'accept' + DELIM: '🉑', + DELIM + r'u7533' + DELIM: '🈸', + DELIM + r'u5408' + DELIM: '🈴', + DELIM + r'u7a7a' + DELIM: '🈳', + DELIM + r'congratulations' + DELIM: '㊗️', + DELIM + r'secret' + DELIM: '㊙️', + DELIM + r'u55b6' + DELIM: '🈺', + DELIM + r'u6e80' + DELIM: '🈵', + + # + # Geometric + # + DELIM + r'red_circle' + DELIM: '🔴', + DELIM + r'orange_circle' + DELIM: '🟠', + DELIM + r'yellow_circle' + DELIM: '🟡', + DELIM + r'green_circle' + DELIM: '🟢', + DELIM + r'large_blue_circle' + DELIM: '🔵', + DELIM + r'purple_circle' + DELIM: '🟣', + DELIM + r'brown_circle' + DELIM: '🟤', + DELIM + r'black_circle' + DELIM: '⚫', + DELIM + r'white_circle' + DELIM: '⚪', + DELIM + r'red_square' + DELIM: '🟥', + DELIM + r'orange_square' + DELIM: '🟧', + DELIM + r'yellow_square' + DELIM: '🟨', + DELIM + r'green_square' + DELIM: '🟩', + DELIM + r'blue_square' + DELIM: '🟦', + DELIM + r'purple_square' + DELIM: '🟪', + DELIM + r'brown_square' + DELIM: '🟫', + DELIM + r'black_large_square' + DELIM: '⬛', + DELIM + r'white_large_square' + DELIM: '⬜', + DELIM + r'black_medium_square' + DELIM: '◼️', + DELIM + r'white_medium_square' + DELIM: '◻️', + DELIM + r'black_medium_small_square' + DELIM: '◾', + DELIM + r'white_medium_small_square' + DELIM: '◽', + DELIM + r'black_small_square' + DELIM: '▪️', + DELIM + r'white_small_square' + DELIM: '▫️', + DELIM + r'large_orange_diamond' + DELIM: '🔶', + DELIM + r'large_blue_diamond' + DELIM: '🔷', + DELIM + r'small_orange_diamond' + DELIM: '🔸', + DELIM + r'small_blue_diamond' + DELIM: '🔹', + DELIM + r'small_red_triangle' + DELIM: '🔺', + DELIM + r'small_red_triangle_down' + DELIM: '🔻', + DELIM + r'diamond_shape_with_a_dot_inside' + DELIM: '💠', + DELIM + r'radio_button' + DELIM: '🔘', + DELIM + r'white_square_button' + DELIM: '🔳', + DELIM + r'black_square_button' + DELIM: '🔲', + + # + # Flag + # + DELIM + r'checkered_flag' + DELIM: '🏁', + DELIM + r'triangular_flag_on_post' + DELIM: '🚩', + DELIM + r'crossed_flags' + DELIM: '🎌', + DELIM + r'black_flag' + DELIM: '🏴', + DELIM + r'white_flag' + DELIM: '🏳️', + DELIM + r'rainbow_flag' + DELIM: '🏳️‍🌈', + DELIM + r'transgender_flag' + DELIM: '🏳️‍⚧️', + DELIM + r'pirate_flag' + DELIM: '🏴‍☠️', + + # + # Country Flag + # + DELIM + r'ascension_island' + DELIM: '🇦🇨', + DELIM + r'andorra' + DELIM: '🇦🇩', + DELIM + r'united_arab_emirates' + DELIM: '🇦🇪', + DELIM + r'afghanistan' + DELIM: '🇦🇫', + DELIM + r'antigua_barbuda' + DELIM: '🇦🇬', + DELIM + r'anguilla' + DELIM: '🇦🇮', + DELIM + r'albania' + DELIM: '🇦🇱', + DELIM + r'armenia' + DELIM: '🇦🇲', + DELIM + r'angola' + DELIM: '🇦🇴', + DELIM + r'antarctica' + DELIM: '🇦🇶', + DELIM + r'argentina' + DELIM: '🇦🇷', + DELIM + r'american_samoa' + DELIM: '🇦🇸', + DELIM + r'austria' + DELIM: '🇦🇹', + DELIM + r'australia' + DELIM: '🇦🇺', + DELIM + r'aruba' + DELIM: '🇦🇼', + DELIM + r'aland_islands' + DELIM: '🇦🇽', + DELIM + r'azerbaijan' + DELIM: '🇦🇿', + DELIM + r'bosnia_herzegovina' + DELIM: '🇧🇦', + DELIM + r'barbados' + DELIM: '🇧🇧', + DELIM + r'bangladesh' + DELIM: '🇧🇩', + DELIM + r'belgium' + DELIM: '🇧🇪', + DELIM + r'burkina_faso' + DELIM: '🇧🇫', + DELIM + r'bulgaria' + DELIM: '🇧🇬', + DELIM + r'bahrain' + DELIM: '🇧🇭', + DELIM + r'burundi' + DELIM: '🇧🇮', + DELIM + r'benin' + DELIM: '🇧🇯', + DELIM + r'st_barthelemy' + DELIM: '🇧🇱', + DELIM + r'bermuda' + DELIM: '🇧🇲', + DELIM + r'brunei' + DELIM: '🇧🇳', + DELIM + r'bolivia' + DELIM: '🇧🇴', + DELIM + r'caribbean_netherlands' + DELIM: '🇧🇶', + DELIM + r'brazil' + DELIM: '🇧🇷', + DELIM + r'bahamas' + DELIM: '🇧🇸', + DELIM + r'bhutan' + DELIM: '🇧🇹', + DELIM + r'bouvet_island' + DELIM: '🇧🇻', + DELIM + r'botswana' + DELIM: '🇧🇼', + DELIM + r'belarus' + DELIM: '🇧🇾', + DELIM + r'belize' + DELIM: '🇧🇿', + DELIM + r'canada' + DELIM: '🇨🇦', + DELIM + r'cocos_islands' + DELIM: '🇨🇨', + DELIM + r'congo_kinshasa' + DELIM: '🇨🇩', + DELIM + r'central_african_republic' + DELIM: '🇨🇫', + DELIM + r'congo_brazzaville' + DELIM: '🇨🇬', + DELIM + r'switzerland' + DELIM: '🇨🇭', + DELIM + r'cote_divoire' + DELIM: '🇨🇮', + DELIM + r'cook_islands' + DELIM: '🇨🇰', + DELIM + r'chile' + DELIM: '🇨🇱', + DELIM + r'cameroon' + DELIM: '🇨🇲', + DELIM + r'cn' + DELIM: '🇨🇳', + DELIM + r'colombia' + DELIM: '🇨🇴', + DELIM + r'clipperton_island' + DELIM: '🇨🇵', + DELIM + r'costa_rica' + DELIM: '🇨🇷', + DELIM + r'cuba' + DELIM: '🇨🇺', + DELIM + r'cape_verde' + DELIM: '🇨🇻', + DELIM + r'curacao' + DELIM: '🇨🇼', + DELIM + r'christmas_island' + DELIM: '🇨🇽', + DELIM + r'cyprus' + DELIM: '🇨🇾', + DELIM + r'czech_republic' + DELIM: '🇨🇿', + DELIM + r'de' + DELIM: '🇩🇪', + DELIM + r'diego_garcia' + DELIM: '🇩🇬', + DELIM + r'djibouti' + DELIM: '🇩🇯', + DELIM + r'denmark' + DELIM: '🇩🇰', + DELIM + r'dominica' + DELIM: '🇩🇲', + DELIM + r'dominican_republic' + DELIM: '🇩🇴', + DELIM + r'algeria' + DELIM: '🇩🇿', + DELIM + r'ceuta_melilla' + DELIM: '🇪🇦', + DELIM + r'ecuador' + DELIM: '🇪🇨', + DELIM + r'estonia' + DELIM: '🇪🇪', + DELIM + r'egypt' + DELIM: '🇪🇬', + DELIM + r'western_sahara' + DELIM: '🇪🇭', + DELIM + r'eritrea' + DELIM: '🇪🇷', + DELIM + r'es' + DELIM: '🇪🇸', + DELIM + r'ethiopia' + DELIM: '🇪🇹', + DELIM + r'(eu|european_union)' + DELIM: '🇪🇺', + DELIM + r'finland' + DELIM: '🇫🇮', + DELIM + r'fiji' + DELIM: '🇫🇯', + DELIM + r'falkland_islands' + DELIM: '🇫🇰', + DELIM + r'micronesia' + DELIM: '🇫🇲', + DELIM + r'faroe_islands' + DELIM: '🇫🇴', + DELIM + r'fr' + DELIM: '🇫🇷', + DELIM + r'gabon' + DELIM: '🇬🇦', + DELIM + r'(uk|gb)' + DELIM: '🇬🇧', + DELIM + r'grenada' + DELIM: '🇬🇩', + DELIM + r'georgia' + DELIM: '🇬🇪', + DELIM + r'french_guiana' + DELIM: '🇬🇫', + DELIM + r'guernsey' + DELIM: '🇬🇬', + DELIM + r'ghana' + DELIM: '🇬🇭', + DELIM + r'gibraltar' + DELIM: '🇬🇮', + DELIM + r'greenland' + DELIM: '🇬🇱', + DELIM + r'gambia' + DELIM: '🇬🇲', + DELIM + r'guinea' + DELIM: '🇬🇳', + DELIM + r'guadeloupe' + DELIM: '🇬🇵', + DELIM + r'equatorial_guinea' + DELIM: '🇬🇶', + DELIM + r'greece' + DELIM: '🇬🇷', + DELIM + r'south_georgia_south_sandwich_islands' + DELIM: '🇬🇸', + DELIM + r'guatemala' + DELIM: '🇬🇹', + DELIM + r'guam' + DELIM: '🇬🇺', + DELIM + r'guinea_bissau' + DELIM: '🇬🇼', + DELIM + r'guyana' + DELIM: '🇬🇾', + DELIM + r'hong_kong' + DELIM: '🇭🇰', + DELIM + r'heard_mcdonald_islands' + DELIM: '🇭🇲', + DELIM + r'honduras' + DELIM: '🇭🇳', + DELIM + r'croatia' + DELIM: '🇭🇷', + DELIM + r'haiti' + DELIM: '🇭🇹', + DELIM + r'hungary' + DELIM: '🇭🇺', + DELIM + r'canary_islands' + DELIM: '🇮🇨', + DELIM + r'indonesia' + DELIM: '🇮🇩', + DELIM + r'ireland' + DELIM: '🇮🇪', + DELIM + r'israel' + DELIM: '🇮🇱', + DELIM + r'isle_of_man' + DELIM: '🇮🇲', + DELIM + r'india' + DELIM: '🇮🇳', + DELIM + r'british_indian_ocean_territory' + DELIM: '🇮🇴', + DELIM + r'iraq' + DELIM: '🇮🇶', + DELIM + r'iran' + DELIM: '🇮🇷', + DELIM + r'iceland' + DELIM: '🇮🇸', + DELIM + r'it' + DELIM: '🇮🇹', + DELIM + r'jersey' + DELIM: '🇯🇪', + DELIM + r'jamaica' + DELIM: '🇯🇲', + DELIM + r'jordan' + DELIM: '🇯🇴', + DELIM + r'jp' + DELIM: '🇯🇵', + DELIM + r'kenya' + DELIM: '🇰🇪', + DELIM + r'kyrgyzstan' + DELIM: '🇰🇬', + DELIM + r'cambodia' + DELIM: '🇰🇭', + DELIM + r'kiribati' + DELIM: '🇰🇮', + DELIM + r'comoros' + DELIM: '🇰🇲', + DELIM + r'st_kitts_nevis' + DELIM: '🇰🇳', + DELIM + r'north_korea' + DELIM: '🇰🇵', + DELIM + r'kr' + DELIM: '🇰🇷', + DELIM + r'kuwait' + DELIM: '🇰🇼', + DELIM + r'cayman_islands' + DELIM: '🇰🇾', + DELIM + r'kazakhstan' + DELIM: '🇰🇿', + DELIM + r'laos' + DELIM: '🇱🇦', + DELIM + r'lebanon' + DELIM: '🇱🇧', + DELIM + r'st_lucia' + DELIM: '🇱🇨', + DELIM + r'liechtenstein' + DELIM: '🇱🇮', + DELIM + r'sri_lanka' + DELIM: '🇱🇰', + DELIM + r'liberia' + DELIM: '🇱🇷', + DELIM + r'lesotho' + DELIM: '🇱🇸', + DELIM + r'lithuania' + DELIM: '🇱🇹', + DELIM + r'luxembourg' + DELIM: '🇱🇺', + DELIM + r'latvia' + DELIM: '🇱🇻', + DELIM + r'libya' + DELIM: '🇱🇾', + DELIM + r'morocco' + DELIM: '🇲🇦', + DELIM + r'monaco' + DELIM: '🇲🇨', + DELIM + r'moldova' + DELIM: '🇲🇩', + DELIM + r'montenegro' + DELIM: '🇲🇪', + DELIM + r'st_martin' + DELIM: '🇲🇫', + DELIM + r'madagascar' + DELIM: '🇲🇬', + DELIM + r'marshall_islands' + DELIM: '🇲🇭', + DELIM + r'macedonia' + DELIM: '🇲🇰', + DELIM + r'mali' + DELIM: '🇲🇱', + DELIM + r'myanmar' + DELIM: '🇲🇲', + DELIM + r'mongolia' + DELIM: '🇲🇳', + DELIM + r'macau' + DELIM: '🇲🇴', + DELIM + r'northern_mariana_islands' + DELIM: '🇲🇵', + DELIM + r'martinique' + DELIM: '🇲🇶', + DELIM + r'mauritania' + DELIM: '🇲🇷', + DELIM + r'montserrat' + DELIM: '🇲🇸', + DELIM + r'malta' + DELIM: '🇲🇹', + DELIM + r'mauritius' + DELIM: '🇲🇺', + DELIM + r'maldives' + DELIM: '🇲🇻', + DELIM + r'malawi' + DELIM: '🇲🇼', + DELIM + r'mexico' + DELIM: '🇲🇽', + DELIM + r'malaysia' + DELIM: '🇲🇾', + DELIM + r'mozambique' + DELIM: '🇲🇿', + DELIM + r'namibia' + DELIM: '🇳🇦', + DELIM + r'new_caledonia' + DELIM: '🇳🇨', + DELIM + r'niger' + DELIM: '🇳🇪', + DELIM + r'norfolk_island' + DELIM: '🇳🇫', + DELIM + r'nigeria' + DELIM: '🇳🇬', + DELIM + r'nicaragua' + DELIM: '🇳🇮', + DELIM + r'netherlands' + DELIM: '🇳🇱', + DELIM + r'norway' + DELIM: '🇳🇴', + DELIM + r'nepal' + DELIM: '🇳🇵', + DELIM + r'nauru' + DELIM: '🇳🇷', + DELIM + r'niue' + DELIM: '🇳🇺', + DELIM + r'new_zealand' + DELIM: '🇳🇿', + DELIM + r'oman' + DELIM: '🇴🇲', + DELIM + r'panama' + DELIM: '🇵🇦', + DELIM + r'peru' + DELIM: '🇵🇪', + DELIM + r'french_polynesia' + DELIM: '🇵🇫', + DELIM + r'papua_new_guinea' + DELIM: '🇵🇬', + DELIM + r'philippines' + DELIM: '🇵🇭', + DELIM + r'pakistan' + DELIM: '🇵🇰', + DELIM + r'poland' + DELIM: '🇵🇱', + DELIM + r'st_pierre_miquelon' + DELIM: '🇵🇲', + DELIM + r'pitcairn_islands' + DELIM: '🇵🇳', + DELIM + r'puerto_rico' + DELIM: '🇵🇷', + DELIM + r'palestinian_territories' + DELIM: '🇵🇸', + DELIM + r'portugal' + DELIM: '🇵🇹', + DELIM + r'palau' + DELIM: '🇵🇼', + DELIM + r'paraguay' + DELIM: '🇵🇾', + DELIM + r'qatar' + DELIM: '🇶🇦', + DELIM + r'reunion' + DELIM: '🇷🇪', + DELIM + r'romania' + DELIM: '🇷🇴', + DELIM + r'serbia' + DELIM: '🇷🇸', + DELIM + r'ru' + DELIM: '🇷🇺', + DELIM + r'rwanda' + DELIM: '🇷🇼', + DELIM + r'saudi_arabia' + DELIM: '🇸🇦', + DELIM + r'solomon_islands' + DELIM: '🇸🇧', + DELIM + r'seychelles' + DELIM: '🇸🇨', + DELIM + r'sudan' + DELIM: '🇸🇩', + DELIM + r'sweden' + DELIM: '🇸🇪', + DELIM + r'singapore' + DELIM: '🇸🇬', + DELIM + r'st_helena' + DELIM: '🇸🇭', + DELIM + r'slovenia' + DELIM: '🇸🇮', + DELIM + r'svalbard_jan_mayen' + DELIM: '🇸🇯', + DELIM + r'slovakia' + DELIM: '🇸🇰', + DELIM + r'sierra_leone' + DELIM: '🇸🇱', + DELIM + r'san_marino' + DELIM: '🇸🇲', + DELIM + r'senegal' + DELIM: '🇸🇳', + DELIM + r'somalia' + DELIM: '🇸🇴', + DELIM + r'suriname' + DELIM: '🇸🇷', + DELIM + r'south_sudan' + DELIM: '🇸🇸', + DELIM + r'sao_tome_principe' + DELIM: '🇸🇹', + DELIM + r'el_salvador' + DELIM: '🇸🇻', + DELIM + r'sint_maarten' + DELIM: '🇸🇽', + DELIM + r'syria' + DELIM: '🇸🇾', + DELIM + r'swaziland' + DELIM: '🇸🇿', + DELIM + r'tristan_da_cunha' + DELIM: '🇹🇦', + DELIM + r'turks_caicos_islands' + DELIM: '🇹🇨', + DELIM + r'chad' + DELIM: '🇹🇩', + DELIM + r'french_southern_territories' + DELIM: '🇹🇫', + DELIM + r'togo' + DELIM: '🇹🇬', + DELIM + r'thailand' + DELIM: '🇹🇭', + DELIM + r'tajikistan' + DELIM: '🇹🇯', + DELIM + r'tokelau' + DELIM: '🇹🇰', + DELIM + r'timor_leste' + DELIM: '🇹🇱', + DELIM + r'turkmenistan' + DELIM: '🇹🇲', + DELIM + r'tunisia' + DELIM: '🇹🇳', + DELIM + r'tonga' + DELIM: '🇹🇴', + DELIM + r'tr' + DELIM: '🇹🇷', + DELIM + r'trinidad_tobago' + DELIM: '🇹🇹', + DELIM + r'tuvalu' + DELIM: '🇹🇻', + DELIM + r'taiwan' + DELIM: '🇹🇼', + DELIM + r'tanzania' + DELIM: '🇹🇿', + DELIM + r'ukraine' + DELIM: '🇺🇦', + DELIM + r'uganda' + DELIM: '🇺🇬', + DELIM + r'us_outlying_islands' + DELIM: '🇺🇲', + DELIM + r'united_nations' + DELIM: '🇺🇳', + DELIM + r'us' + DELIM: '🇺🇸', + DELIM + r'uruguay' + DELIM: '🇺🇾', + DELIM + r'uzbekistan' + DELIM: '🇺🇿', + DELIM + r'vatican_city' + DELIM: '🇻🇦', + DELIM + r'st_vincent_grenadines' + DELIM: '🇻🇨', + DELIM + r'venezuela' + DELIM: '🇻🇪', + DELIM + r'british_virgin_islands' + DELIM: '🇻🇬', + DELIM + r'us_virgin_islands' + DELIM: '🇻🇮', + DELIM + r'vietnam' + DELIM: '🇻🇳', + DELIM + r'vanuatu' + DELIM: '🇻🇺', + DELIM + r'wallis_futuna' + DELIM: '🇼🇫', + DELIM + r'samoa' + DELIM: '🇼🇸', + DELIM + r'kosovo' + DELIM: '🇽🇰', + DELIM + r'yemen' + DELIM: '🇾🇪', + DELIM + r'mayotte' + DELIM: '🇾🇹', + DELIM + r'south_africa' + DELIM: '🇿🇦', + DELIM + r'zambia' + DELIM: '🇿🇲', + DELIM + r'zimbabwe' + DELIM: '🇿🇼', + + # + # Subdivision Flag + # + DELIM + r'england' + DELIM: '🏴󠁧󠁢󠁥󠁮󠁧󠁿', + DELIM + r'scotland' + DELIM: '🏴󠁧󠁢󠁳󠁣󠁴󠁿', + DELIM + r'wales' + DELIM: '🏴󠁧󠁢󠁷󠁬󠁳󠁿', +} + +# Define our singlton +EMOJI_COMPILED_MAP = None + + +def apply_emojis(content): + """ + Takes the content and swaps any matched emoji's found with their + utf-8 encoded mapping + """ + + global EMOJI_COMPILED_MAP + + if EMOJI_COMPILED_MAP is None: + t_start = time.time() + # Perform our compilation + EMOJI_COMPILED_MAP = re.compile( + r'(' + '|'.join(EMOJI_MAP.keys()) + r')', + re.IGNORECASE) + logger.trace( + 'Emoji engine loaded in {:.4f}s'.format((time.time() - t_start))) + + try: + return EMOJI_COMPILED_MAP.sub(lambda x: EMOJI_MAP[x.group()], content) + + except TypeError: + # No change; but force string return + return '' diff --git a/libs/apprise/i18n/en/LC_MESSAGES/apprise.mo b/libs/apprise/i18n/en/LC_MESSAGES/apprise.mo index 0236722fd..1d22b89a6 100644 Binary files a/libs/apprise/i18n/en/LC_MESSAGES/apprise.mo and b/libs/apprise/i18n/en/LC_MESSAGES/apprise.mo differ diff --git a/libs/apprise/logger.py b/libs/apprise/logger.py index 6a594ec60..d9efe47c9 100644 --- a/libs/apprise/logger.py +++ b/libs/apprise/logger.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/manager.py b/libs/apprise/manager.py new file mode 100644 index 000000000..3d964af28 --- /dev/null +++ b/libs/apprise/manager.py @@ -0,0 +1,717 @@ +# -*- coding: utf-8 -*- +# BSD 2-Clause License +# +# Apprise - Push Notification Library. +# Copyright (c) 2024, Chris Caron +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +import os +import re +import sys +import time +import hashlib +import inspect +from .utils import import_module +from .utils import Singleton +from .utils import parse_list +from os.path import dirname +from os.path import abspath +from os.path import join + +from .logger import logger + + +class PluginManager(metaclass=Singleton): + """ + Designed to be a singleton object to maintain all initialized loading + of modules in memory. + """ + + # Description (used for logging) + name = 'Singleton Plugin' + + # Memory Space + _id = 'undefined' + + # Our Module Python path name + module_name_prefix = f'apprise.{_id}' + + # The module path to scan + module_path = join(abspath(dirname(__file__)), _id) + + def __init__(self, *args, **kwargs): + """ + Over-ride our class instantiation to provide a singleton + """ + + self._module_map = None + self._schema_map = None + + # This contains a mapping of all plugins dynamicaly loaded at runtime + # from external modules such as the @notify decorator + # + # The elements here will be additionally added to the _schema_map if + # there is no conflict otherwise. + # The structure looks like the following: + # Module path, e.g. /usr/share/apprise/plugins/my_notify_hook.py + # { + # 'path': path, + # + # 'notify': { + # 'schema': { + # 'name': 'Custom schema name', + # 'fn_name': 'name_of_function_decorator_was_found_on', + # 'url': 'schema://any/additional/info/found/on/url' + # 'plugin': + # }, + # 'schema2': { + # 'name': 'Custom schema name', + # 'fn_name': 'name_of_function_decorator_was_found_on', + # 'url': 'schema://any/additional/info/found/on/url' + # 'plugin': + # } + # } + # Note: that the inherits from + # NotifyBase + self._custom_module_map = {} + + # Track manually disabled modules (by their schema) + self._disabled = set() + + # Hash of all paths previously scanned so we don't waste + # effort/overhead doing it again + self._paths_previously_scanned = set() + + def unload_modules(self, disable_native=False): + """ + Reset our object and unload all modules + """ + + if self._custom_module_map: + # Handle Custom Module Assignments + for meta in self._custom_module_map.values(): + if meta['name'] not in self._module_map: + # Nothing to remove + continue + + # For the purpose of tidying up un-used modules in memory + loaded = [m for m in sys.modules.keys() + if m.startswith( + self._module_map[meta['name']]['path'])] + + for module_path in loaded: + del sys.modules[module_path] + + # Reset disabled plugins (if any) + for schema in self._disabled: + self._schema_map[schema].enabled = True + self._disabled.clear() + + # Reset our variables + self._module_map = None if not disable_native else {} + self._schema_map = {} + self._custom_module_map = {} + + # Reset our path cache + self._paths_previously_scanned = set() + + def load_modules(self, path=None, name=None): + """ + Load our modules into memory + """ + + # Default value + module_name_prefix = self.module_name_prefix if name is None else name + module_path = self.module_path if path is None else path + + if not self: + # Initialize our maps + self._module_map = {} + self._schema_map = {} + self._custom_module_map = {} + + # Used for the detection of additional Notify Services objects + # The .py extension is optional as we support loading directories too + module_re = re.compile( + r'^(?P' + self.fname_prefix + r'[a-z0-9]+)(\.py)?$', re.I) + + t_start = time.time() + for f in os.listdir(module_path): + tl_start = time.time() + match = module_re.match(f) + if not match: + # keep going + continue + + elif match.group('name') == f'{self.fname_prefix}Base': + # keep going + continue + + # Store our notification/plugin name: + module_name = match.group('name') + module_pyname = '{}.{}'.format(module_name_prefix, module_name) + + if module_name in self._module_map: + logger.warning( + "%s(s) (%s) already loaded; ignoring %s", + self.name, module_name, os.path.join(module_path, f)) + continue + + try: + module = __import__( + module_pyname, + globals(), locals(), + fromlist=[module_name]) + + except ImportError: + # No problem, we can try again another way... + module = import_module( + os.path.join(module_path, f), module_pyname) + if not module: + # logging found in import_module and not needed here + continue + + if not hasattr(module, module_name): + # Not a library we can load as it doesn't follow the simple + # rule that the class must bear the same name as the + # notification file itself. + logger.trace( + "%s (%s) import failed; no filename/Class " + "match found in %s", + self.name, module_name, os.path.join(module_path, f)) + continue + + # Get our plugin + plugin = getattr(module, module_name) + if not hasattr(plugin, 'app_id'): + # Filter out non-notification modules + logger.trace( + "(%s) import failed; no app_id defined in %s", + self.name, module_name, os.path.join(module_path, f)) + continue + + # Add our plugin name to our module map + self._module_map[module_name] = { + 'plugin': set([plugin]), + 'module': module, + 'path': '{}.{}'.format(module_name_prefix, module_name), + 'native': True, + } + + fn = getattr(plugin, 'schemas', None) + schemas = set([]) if not callable(fn) else fn(plugin) + + # map our schema to our plugin + for schema in schemas: + if schema in self._schema_map: + logger.error( + "{} schema ({}) mismatch detected - {} to {}" + .format(self.name, schema, self._schema_map, plugin)) + continue + + # Assign plugin + self._schema_map[schema] = plugin + + logger.trace( + '{} {} loaded in {:.6f}s'.format( + self.name, module_name, (time.time() - tl_start))) + logger.debug( + '{} {}(s) and {} Schema(s) loaded in {:.4f}s' + .format( + self.name, len(self._module_map), len(self._schema_map), + (time.time() - t_start))) + + def module_detection(self, paths, cache=True): + """ + Leverage the @notify decorator and load all objects found matching + this. + """ + # A simple restriction that we don't allow periods in the filename at + # all so it can't be hidden (Linux OS's) and it won't conflict with + # Python path naming. This also prevents us from loading any python + # file that starts with an underscore or dash + # We allow for __init__.py as well + module_re = re.compile( + r'^(?P[_a-z0-9][a-z0-9._-]+)?(\.py)?$', re.I) + + # Validate if we're a loadable Python file or not + valid_python_file_re = re.compile(r'.+\.py(o|c)?$', re.IGNORECASE) + + if isinstance(paths, str): + paths = [paths, ] + + if not paths or not isinstance(paths, (tuple, list)): + # We're done + return + + def _import_module(path): + # Since our plugin name can conflict (as a module) with another + # we want to generate random strings to avoid steping on + # another's namespace + if not (path and valid_python_file_re.match(path)): + # Ignore file/module type + logger.trace('Plugin Scan: Skipping %s', path) + return + + t_start = time.time() + module_name = hashlib.sha1(path.encode('utf-8')).hexdigest() + module_pyname = "{prefix}.{name}".format( + prefix='apprise.custom.module', name=module_name) + + if module_pyname in self._custom_module_map: + # First clear out existing entries + for schema in \ + self._custom_module_map[module_pyname]['notify']\ + .keys(): + + # Remove any mapped modules to this file + del self._schema_map[schema] + + # Reset + del self._custom_module_map[module_pyname] + + # Load our module + module = import_module(path, module_pyname) + if not module: + # No problem, we can't use this object + logger.warning('Failed to load custom module: %s', _path) + return + + # Print our loaded modules if any + if module_pyname in self._custom_module_map: + logger.debug( + 'Custom module %s - %d schema(s) (name=%s) ' + 'loaded in %.6fs', _path, + len(self._custom_module_map[module_pyname]['notify']), + module_name, (time.time() - t_start)) + + # Add our plugin name to our module map + self._module_map[module_name] = { + 'plugin': set(), + 'module': module, + 'path': module_pyname, + 'native': False, + } + + for schema, meta in\ + self._custom_module_map[module_pyname]['notify']\ + .items(): + + # For mapping purposes; map our element in our main list + self._module_map[module_name]['plugin'].add( + self._schema_map[schema]) + + # Log our success + logger.info('Loaded custom notification: %s://', schema) + else: + # The code reaches here if we successfully loaded the Python + # module but no hooks/triggers were found. So we can safely + # just remove/ignore this entry + del sys.modules[module_pyname] + return + + # end of _import_module() + return + + for _path in paths: + path = os.path.abspath(os.path.expanduser(_path)) + if (cache and path in self._paths_previously_scanned) \ + or not os.path.exists(path): + # We're done as we've already scanned this + continue + + # Store our path as a way of hashing it has been handled + self._paths_previously_scanned.add(path) + + if os.path.isdir(path) and not \ + os.path.isfile(os.path.join(path, '__init__.py')): + + logger.debug('Scanning for custom plugins in: %s', path) + for entry in os.listdir(path): + re_match = module_re.match(entry) + if not re_match: + # keep going + logger.trace('Plugin Scan: Ignoring %s', entry) + continue + + new_path = os.path.join(path, entry) + if os.path.isdir(new_path): + # Update our path + new_path = os.path.join(path, entry, '__init__.py') + if not os.path.isfile(new_path): + logger.trace( + 'Plugin Scan: Ignoring %s', + os.path.join(path, entry)) + continue + + if not cache or \ + (cache and + new_path not in self._paths_previously_scanned): + # Load our module + _import_module(new_path) + + # Add our subdir path + self._paths_previously_scanned.add(new_path) + else: + if os.path.isdir(path): + # This logic is safe to apply because we already validated + # the directories state above; update our path + path = os.path.join(path, '__init__.py') + if cache and path in self._paths_previously_scanned: + continue + + self._paths_previously_scanned.add(path) + + # directly load as is + re_match = module_re.match(os.path.basename(path)) + # must be a match and must have a .py extension + if not re_match or not re_match.group(1): + # keep going + logger.trace('Plugin Scan: Ignoring %s', path) + continue + + # Load our module + _import_module(path) + + return None + + def add(self, plugin, schemas=None, url=None, send_func=None): + """ + Ability to manually add Notification services to our stack + """ + + if not self: + # Lazy load + self.load_modules() + + # Acquire a list of schemas + p_schemas = parse_list(plugin.secure_protocol, plugin.protocol) + if isinstance(schemas, str): + schemas = [schemas, ] + + elif schemas is None: + # Default + schemas = p_schemas + + if not schemas or not isinstance(schemas, (set, tuple, list)): + # We're done + logger.error( + 'The schemas provided (type %s) is unsupported; ' + 'loaded from %s.', + type(schemas), + send_func.__name__ if send_func else plugin.__class__.__name__) + return False + + # Convert our schemas into a set + schemas = set([s.lower() for s in schemas]) | set(p_schemas) + + # Valdation + conflict = [s for s in schemas if s in self] + if conflict: + # we're already handling this schema + logger.warning( + 'The schema(s) (%s) are already defined and could not be ' + 'loaded from %s%s.', + ', '.join(conflict), + 'custom notify function ' if send_func else '', + send_func.__name__ if send_func else plugin.__class__.__name__) + return False + + if send_func: + # Acquire the function name + fn_name = send_func.__name__ + + # Acquire the python filename path + path = inspect.getfile(send_func) + + # Acquire our path to our module + module_name = str(send_func.__module__) + + if module_name not in self._custom_module_map: + # Support non-dynamic includes as well... + self._custom_module_map[module_name] = { + # Name can be useful for indexing back into the + # _module_map object; this is the key to do it with: + 'name': module_name.split('.')[-1], + + # The path to the module loaded + 'path': path, + + # Initialize our template + 'notify': {}, + } + + for schema in schemas: + self._custom_module_map[module_name]['notify'][schema] = { + # The name of the send function the @notify decorator + # wrapped + 'fn_name': fn_name, + # The URL that was provided in the @notify decorator call + # associated with the 'on=' + 'url': url, + } + + else: + module_name = hashlib.sha1( + ''.join(schemas).encode('utf-8')).hexdigest() + module_pyname = "{prefix}.{name}".format( + prefix='apprise.adhoc.module', name=module_name) + + # Add our plugin name to our module map + self._module_map[module_name] = { + 'plugin': set([plugin]), + 'module': None, + 'path': module_pyname, + 'native': False, + } + + for schema in schemas: + # Assign our mapping + self._schema_map[schema] = plugin + + return True + + def remove(self, *schemas): + """ + Removes a loaded element (if defined) + """ + if not self: + # Lazy load + self.load_modules() + + for schema in schemas: + try: + del self[schema] + + except KeyError: + pass + + def plugins(self, include_disabled=True): + """ + Return all of our loaded plugins + """ + if not self: + # Lazy load + self.load_modules() + + for module in self._module_map.values(): + for plugin in module['plugin']: + if not include_disabled and not plugin.enabled: + continue + yield plugin + + def schemas(self, include_disabled=True): + """ + Return all of our loaded schemas + + if include_disabled == True, then even disabled notifications are + returned + """ + if not self: + # Lazy load + self.load_modules() + + # Return our list + return list(self._schema_map.keys()) if include_disabled else \ + [s for s in self._schema_map.keys() if self._schema_map[s].enabled] + + def disable(self, *schemas): + """ + Disables the modules associated with the specified schemas + """ + if not self: + # Lazy load + self.load_modules() + + for schema in schemas: + if schema not in self._schema_map: + continue + + if not self._schema_map[schema].enabled: + continue + + # Disable + self._schema_map[schema].enabled = False + self._disabled.add(schema) + + def enable_only(self, *schemas): + """ + Disables the modules associated with the specified schemas + """ + if not self: + # Lazy load + self.load_modules() + + # convert to set for faster indexing + schemas = set(schemas) + + for plugin in self.plugins(): + # Get our plugin's schema list + p_schemas = set( + parse_list(plugin.secure_protocol, plugin.protocol)) + + if not schemas & p_schemas: + if plugin.enabled: + # Disable it (only if previously enabled); this prevents us + # from adjusting schemas that were disabled due to missing + # libraries or other environment reasons + plugin.enabled = False + self._disabled |= p_schemas + continue + + # If we reach here, our schema was flagged to be enabled + if p_schemas & self._disabled: + # Previously disabled; no worries, let's clear this up + self._disabled -= p_schemas + plugin.enabled = True + + def __contains__(self, schema): + """ + Checks if a schema exists + """ + if not self: + # Lazy load + self.load_modules() + + return schema in self._schema_map + + def __delitem__(self, schema): + if not self: + # Lazy load + self.load_modules() + + # Get our plugin (otherwise we throw a KeyError) which is + # intended on del action that doesn't align + plugin = self._schema_map[schema] + + # Our list of all schema entries + p_schemas = set([schema]) + + for key in list(self._module_map.keys()): + if plugin in self._module_map[key]['plugin']: + # Remove our plugin + self._module_map[key]['plugin'].remove(plugin) + + # Custom Plugin Entry; Clean up cross reference + module_pyname = self._module_map[key]['path'] + if not self._module_map[key]['native'] and \ + module_pyname in self._custom_module_map: + + del self.\ + _custom_module_map[module_pyname]['notify'][schema] + + if not self._custom_module_map[module_pyname]['notify']: + # + # Last custom loaded element + # + + # Free up custom object entry + del self._custom_module_map[module_pyname] + + if not self._module_map[key]['plugin']: + # + # Last element + # + if self._module_map[key]['native']: + # Get our plugin's schema list + p_schemas = \ + set([s for s in parse_list( + plugin.secure_protocol, plugin.protocol) + if s in self._schema_map]) + + # free system memory + if self._module_map[key]['module']: + del sys.modules[self._module_map[key]['path']] + + # free last remaining pointer in module map + del self._module_map[key] + + for schema in p_schemas: + # Final Tidy + del self._schema_map[schema] + + def __setitem__(self, schema, plugin): + """ + Support fast assigning of Plugin/Notification Objects + """ + if not self: + # Lazy load + self.load_modules() + + # Set default values if not otherwise set + if not plugin.service_name: + # Assign service name if one doesn't exist + plugin.service_name = f'{schema}://' + + p_schemas = set( + parse_list(plugin.secure_protocol, plugin.protocol)) + if not p_schemas: + # Assign our protocol + plugin.secure_protocol = schema + p_schemas.add(schema) + + elif schema not in p_schemas: + # Add our others (if defined) + plugin.secure_protocol = \ + set([schema] + parse_list(plugin.secure_protocol)) + p_schemas.add(schema) + + if not self.add(plugin, schemas=p_schemas): + raise KeyError('Conflicting Assignment') + + def __getitem__(self, schema): + """ + Returns the indexed plugin identified by the schema specified + """ + if not self: + # Lazy load + self.load_modules() + + return self._schema_map[schema] + + def __iter__(self): + """ + Returns an iterator so we can iterate over our loaded modules + """ + if not self: + # Lazy load + self.load_modules() + + return iter(self._module_map.values()) + + def __len__(self): + """ + Returns the number of modules/plugins loaded + """ + if not self: + # Lazy load + self.load_modules() + + return len(self._module_map) + + def __bool__(self): + """ + Determines if object has loaded or not + """ + return True if self._module_map is not None else False diff --git a/libs/apprise/plugins/NotifyAppriseAPI.py b/libs/apprise/plugins/NotifyAppriseAPI.py index 3c85b8ac6..34c34a6d4 100644 --- a/libs/apprise/plugins/NotifyAppriseAPI.py +++ b/libs/apprise/plugins/NotifyAppriseAPI.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifyAprs.py b/libs/apprise/plugins/NotifyAprs.py new file mode 100644 index 000000000..c56982a70 --- /dev/null +++ b/libs/apprise/plugins/NotifyAprs.py @@ -0,0 +1,741 @@ +# -*- coding: utf-8 -*- +# BSD 2-Clause License +# +# Apprise - Push Notification Library. +# Copyright (c) 2024, Chris Caron +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# +# To use this plugin, you need to be a licensed ham radio operator +# +# Plugin constraints: +# +# - message length = 67 chars max. +# - message content = ASCII 7 bit +# - APRS messages will be sent without msg ID, meaning that +# ham radio operators cannot acknowledge them +# - Bring your own APRS-IS passcode. If you don't know what +# this is or how to get it, then this plugin is not for you +# - Do NOT change the Device/ToCall ID setting UNLESS this +# module is used outside of Apprise. This identifier helps +# the ham radio community with determining the software behind +# a given APRS message. +# - With great (ham radio) power comes great responsibility; do +# not use this plugin for spamming other ham radio operators + +# +# In order to digest text input which is not in plain English, +# users can install the optional 'unidecode' package as part +# of their venv environment. Details: see plugin description +# + +# +# You're done at this point, you only need to know your user/pass that +# you signed up with. + +# The following URLs would be accepted by Apprise: +# - aprs://{user}:{password}@{callsign} +# - aprs://{user}:{password}@{callsign1}/{callsign2} + +# Optional parameters: +# - locale --> APRS-IS target server to connect with +# Default: EURO --> 'euro.aprs2.net' +# Details: https://www.aprs2.net/ + +# +# APRS message format specification: +# http://www.aprs.org/doc/APRS101.PDF +# + +import socket +import sys +from itertools import chain +from .NotifyBase import NotifyBase +from ..AppriseLocale import gettext_lazy as _ +from ..URLBase import PrivacyMode +from ..common import NotifyType +from ..utils import is_call_sign +from ..utils import parse_call_sign +from .. import __version__ +import re + +# Fixed APRS-IS server locales +# Default is 'EURO' +# See https://www.aprs2.net/ for details +# Select the rotating server in case you +# don"t care about a specific locale +APRS_LOCALES = { + "NOAM": "noam.aprs2.net", + "SOAM": "soam.aprs2.net", + "EURO": "euro.aprs2.net", + "ASIA": "asia.aprs2.net", + "AUNZ": "aunz.aprs2.net", + "ROTA": "rotate.aprs2.net", +} + +# Identify all unsupported characters +APRS_BAD_CHARMAP = { + r"Ä": "Ae", + r"Ö": "Oe", + r"Ü": "Ue", + r"ä": "ae", + r"ö": "oe", + r"ü": "ue", + r"ß": "ss", +} + +# Our compiled mapping of bad characters +APRS_COMPILED_MAP = re.compile( + r'(' + '|'.join(APRS_BAD_CHARMAP.keys()) + r')') + + +class NotifyAprs(NotifyBase): + """ + A wrapper for APRS Notifications via APRS-IS + """ + + # The default descriptive name associated with the Notification + service_name = "Aprs" + + # The services URL + service_url = "https://www.aprs2.net/" + + # The default secure protocol + secure_protocol = "aprs" + + # A URL that takes you to the setup/help of the specific protocol + setup_url = "https://github.com/caronc/apprise/wiki/Notify_aprs" + + # APRS default port, supported by all core servers + # Details: https://www.aprs-is.net/Connecting.aspx + notify_port = 10152 + + # The maximum length of the APRS message body + body_maxlen = 67 + + # Apprise APRS Device ID / TOCALL ID + # This is a FIXED value which is associated with this plugin. + # Its value MUST NOT be changed. If you use this APRS plugin + # code OUTSIDE of Apprise, please request your own TOCALL ID. + # Details: see https://github.com/aprsorg/aprs-deviceid + # + # Do NOT use the generic "APRS" TOCALL ID !!!!! + # + device_id = "APPRIS" + + # A title can not be used for APRS Messages. Setting this to zero will + # cause any title (if defined) to get placed into the message body. + title_maxlen = 0 + + # Helps to reduce the number of login-related errors where the + # APRS-IS server "isn't ready yet". If we try to receive the rx buffer + # without this grace perid in place, we may receive "incomplete" responses + # where the login response lacks information. In case you receive too many + # "Rx: APRS-IS msg is too short - needs to have at least two lines" error + # messages, you might want to increase this value to a larger time span + # Per previous experience, do not use values lower than 0.5 (seconds) + request_rate_per_sec = 0.8 + + # Encoding of retrieved content + aprs_encoding = 'latin-1' + + # Define object templates + templates = ("{schema}://{user}:{password}@{targets}",) + + # Define our template tokens + template_tokens = dict( + NotifyBase.template_tokens, + **{ + "user": { + "name": _("User Name"), + "type": "string", + "required": True, + }, + "password": { + "name": _("Password"), + "type": "string", + "private": True, + "required": True, + }, + "target_callsign": { + "name": _("Target Callsign"), + "type": "string", + "regex": ( + r"^[a-z0-9]{2,5}(-[a-z0-9]{1,2})?$", + "i", + ), + "map_to": "targets", + }, + "targets": { + "name": _("Targets"), + "type": "list:string", + "required": True, + }, + } + ) + + # Define our template arguments + template_args = dict( + NotifyBase.template_args, + **{ + "to": { + "name": _("Target Callsign"), + "type": "string", + "map_to": "targets", + }, + "locale": { + "name": _("Locale"), + "type": "choice:string", + "values": APRS_LOCALES, + "default": "EURO", + }, + } + ) + + def __init__(self, targets=None, locale=None, **kwargs): + """ + Initialize APRS Object + """ + super().__init__(**kwargs) + + # Our (future) socket sobject + self.sock = None + + # Parse our targets + self.targets = list() + + """ + Check if the user has provided credentials + """ + if not (self.user and self.password): + msg = "An APRS user/pass was not provided." + self.logger.warning(msg) + raise TypeError(msg) + + """ + Check if the user tries to use a read-only access + to APRS-IS. We need to send content, meaning that + read-only access will not work + """ + if self.password == "-1": + msg = "APRS read-only passwords are not supported." + self.logger.warning(msg) + raise TypeError(msg) + + """ + Check if the password is numeric + """ + if not self.password.isnumeric(): + msg = "Invalid APRS-IS password" + self.logger.warning(msg) + raise TypeError(msg) + + """ + Convert given user name (FROM callsign) and + device ID to to uppercase + """ + self.user = self.user.upper() + self.device_id = self.device_id.upper() + + """ + Check if the user has provided a locale for the + APRS-IS-server and validate it, if necessary + """ + if locale: + if locale.upper() not in APRS_LOCALES: + msg = ( + "Unsupported APRS-IS server locale. " + "Received: {}. Valid: {}".format( + locale, ", ".join(str(x) for x in APRS_LOCALES.keys()) + ) + ) + self.logger.warning(msg) + raise TypeError(msg) + + # Set the transmitter group + self.locale = \ + NotifyAprs.template_args["locale"]["default"] \ + if not locale else locale.upper() + + # Used for URL generation afterwards only + self.invalid_targets = list() + + for target in parse_call_sign(targets): + # Validate targets and drop bad ones + # We just need to know if the call sign (including SSID, if + # provided) is valid and can then process the input as is + result = is_call_sign(target) + if not result: + self.logger.warning( + "Dropping invalid Amateur radio call sign ({}).".format( + target + ), + ) + self.invalid_targets.append(target.upper()) + continue + + # Store entry + self.targets.append(target.upper()) + + return + + def socket_close(self): + """ + Closes the socket connection whereas present + """ + if self.sock: + try: + self.sock.close() + + except Exception: + # No worries if socket exception thrown on close() + pass + + self.sock = None + + def socket_open(self): + """ + Establishes the connection to the APRS-IS + socket server + """ + self.logger.debug( + "Creating socket connection with APRS-IS {}:{}".format( + APRS_LOCALES[self.locale], self.notify_port + ) + ) + + try: + self.sock = socket.create_connection( + (APRS_LOCALES[self.locale], self.notify_port), + self.socket_connect_timeout, + ) + + except ConnectionError as e: + self.logger.debug("Socket Exception socket_open: %s", str(e)) + self.sock = None + return False + + except socket.gaierror as e: + self.logger.debug("Socket Exception socket_open: %s", str(e)) + self.sock = None + return False + + except socket.timeout as e: + self.logger.debug( + "Socket Timeout Exception socket_open: %s", str(e)) + self.sock = None + return False + + except Exception as e: + self.logger.debug("General Exception socket_open: %s", str(e)) + self.sock = None + return False + + # We are connected. + # getpeername() is not supported by every OS. Therefore, + # we MAY receive an exception even though we are + # connected successfully. + try: + # Get the physical host/port of the server + host, port = self.sock.getpeername() + # and create debug info + self.logger.debug("Connected to {}:{}".format(host, port)) + + except ValueError: + # Seens as if we are running on an operating + # system that does not support getpeername() + # Create a minimal log file entry + self.logger.debug("Connected to APRS-IS") + + # Return success + return True + + def aprsis_login(self): + """ + Generate the APRS-IS login string, send it to the server + and parse the response + + Returns True/False wrt whether the login was successful + """ + self.logger.debug("socket_login: init") + + # Check if we are connected + if not self.sock: + self.logger.warning("socket_login: Not connected to APRS-IS") + return False + + # APRS-IS login string, see https://www.aprs-is.net/Connecting.aspx + login_str = "user {0} pass {1} vers apprise {2}\r\n".format( + self.user, self.password, __version__ + ) + + # Send the data & abort in case of error + if not self.socket_send(login_str): + self.logger.warning( + "socket_login: Login to APRS-IS unsuccessful," + " exception occurred" + ) + self.socket_close() + return False + + rx_buf = self.socket_receive(len(login_str) + 100) + # Abort the remaining process in case an error has occurred + if not rx_buf: + self.logger.warning( + "socket_login: Login to APRS-IS " + "unsuccessful, exception occurred" + ) + self.socket_close() + return False + + # APRS-IS sends at least two lines of data + # The data that we need is in line #2 so + # let's split the content and see what we have + rx_lines = rx_buf.splitlines() + if len(rx_lines) < 2: + self.logger.warning( + "socket_login: APRS-IS msg is too short" + " - needs to have at least two lines" + ) + self.socket_close() + return False + + # Now split the 2nd line's content and extract + # both call sign and login status + try: + _, _, callsign, status, _ = rx_lines[1].split(" ", 4) + + except ValueError: + # ValueError is returned if there were not enough elements to + # populate the response + self.logger.warning( + "socket_login: " "received invalid response from APRS-IS" + ) + self.socket_close() + return False + + if callsign != self.user: + self.logger.warning( + "socket_login: " "call signs differ: %s" % callsign + ) + self.socket_close() + return False + + if status.startswith("unverified"): + self.logger.warning( + "socket_login: " + "invalid APRS-IS password for given call sign" + ) + self.socket_close() + return False + + # all validations are successful; we are connected + return True + + def socket_send(self, tx_data): + """ + Generic "Send data to a socket" + """ + self.logger.debug("socket_send: init") + + # Check if we are connected + if not self.sock: + self.logger.warning("socket_send: Not connected to APRS-IS") + return False + + # Encode our data if we are on Python3 or later + payload = ( + tx_data.encode("utf-8") if sys.version_info[0] >= 3 else tx_data + ) + + # Always call throttle before any remote server i/o is made + self.throttle() + + # Try to open the socket + # Send the content to APRS-IS + try: + self.sock.setblocking(True) + self.sock.settimeout(self.socket_connect_timeout) + self.sock.sendall(payload) + + except socket.gaierror as e: + self.logger.warning("Socket Exception socket_send: %s" % str(e)) + self.sock = None + return False + + except socket.timeout as e: + self.logger.warning( + "Socket Timeout Exception " "socket_send: %s" % str(e) + ) + self.sock = None + return False + + except Exception as e: + self.logger.warning( + "General Exception " "socket_send: %s" % str(e) + ) + self.sock = None + return False + + self.logger.debug("socket_send: successful") + + # mandatory on several APRS-IS servers + # helps to reduce the number of errors where + # the server only returns an abbreviated message + return True + + def socket_reset(self): + """ + Resets the socket's buffer + """ + self.logger.debug("socket_reset: init") + _ = self.socket_receive(0) + self.logger.debug("socket_reset: successful") + return True + + def socket_receive(self, rx_len): + """ + Generic "Receive data from a socket" + """ + self.logger.debug("socket_receive: init") + + # Check if we are connected + if not self.sock: + self.logger.warning("socket_receive: not connected to APRS-IS") + return False + + # len is zero in case we intend to + # reset the socket + if rx_len > 0: + self.logger.debug("socket_receive: Receiving data from APRS-IS") + + # Receive content from the socket + try: + self.sock.setblocking(False) + self.sock.settimeout(self.socket_connect_timeout) + rx_buf = self.sock.recv(rx_len) + + except socket.gaierror as e: + self.logger.warning( + "Socket Exception socket_receive: %s" % str(e) + ) + self.sock = None + return False + + except socket.timeout as e: + self.logger.warning( + "Socket Timeout Exception " "socket_receive: %s" % str(e) + ) + self.sock = None + return False + + except Exception as e: + self.logger.warning( + "General Exception " "socket_receive: %s" % str(e) + ) + self.sock = None + return False + + rx_buf = ( + rx_buf.decode(self.aprs_encoding) + if sys.version_info[0] >= 3 else rx_buf + ) + + # There will be no data in case we reset the socket + if rx_len > 0: + self.logger.debug("Received content: {}".format(rx_buf)) + + self.logger.debug("socket_receive: successful") + + return rx_buf.rstrip() + + def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs): + """ + Perform APRS Notification + """ + + if not self.targets: + # There is no one to notify; we're done + self.logger.warning( + "There are no amateur radio call signs to notify" + ) + return False + + # prepare payload + payload = body + + # sock object is "None" if we were unable to establish a connection + # In case of errors, the error message has already been sent + # to the logger object + if not self.socket_open(): + return False + + # We have established a successful connection + # to the socket server. Now send the login information + if not self.aprsis_login(): + return False + + # Login & authorization confirmed + # reset what is in our buffer + self.socket_reset() + + # error tracking (used for function return) + has_error = False + + # Create a copy of the targets list + targets = list(self.targets) + + self.logger.debug("Starting Payload setup") + + # Prepare the outgoing message + # Due to APRS's contraints, we need to do + # a lot of filtering before we can send + # the actual message + # + # First remove all characters from the + # payload that would break APRS + # see https://www.aprs.org/doc/APRS101.PDF pg. 71 + payload = re.sub("[{}|~]+", "", payload) + + payload = ( # pragma: no branch + APRS_COMPILED_MAP.sub( + lambda x: APRS_BAD_CHARMAP[x.group()], payload) + ) + + # Finally, constrain output string to 67 characters as + # APRS messages are limited in length + payload = payload[:67] + + # Our outgoing message MUST end with a CRLF so + # let's amend our payload respectively + payload = payload.rstrip("\r\n") + "\r\n" + + self.logger.debug("Payload setup complete: {}".format(payload)) + + # send the message to our target call sign(s) + for index in range(0, len(targets)): + # prepare the output string + # Format: + # Device ID/TOCALL - our call sign - target call sign - body + buffer = "{}>{}::{:9}:{}".format( + self.user, self.device_id, targets[index], payload + ) + + # and send the content to the socket + # Note that there will be no response from APRS and + # that all exceptions are handled within the 'send' method + self.logger.debug("Sending APRS message: {}".format(buffer)) + + # send the content + if not self.socket_send(buffer): + has_error = True + break + + # Finally, reset our socket buffer + # we DO NOT read from the socket as we + # would simply listen to the default APRS-IS stream + self.socket_reset() + + self.logger.debug("Closing socket.") + self.socket_close() + self.logger.info( + "Sent %d/%d APRS-IS notification(s)", index + 1, len(targets)) + return not has_error + + def url(self, privacy=False, *args, **kwargs): + """ + Returns the URL built dynamically based on specified arguments. + """ + + # Define any URL parameters + params = {} + + if self.locale != NotifyAprs.template_args["locale"]["default"]: + # Store our locale if not default + params['locale'] = self.locale + + # Extend our parameters + params.update(self.url_parameters(privacy=privacy, *args, **kwargs)) + + # Setup Authentication + auth = "{user}:{password}@".format( + user=NotifyAprs.quote(self.user, safe=""), + password=self.pprint( + self.password, privacy, mode=PrivacyMode.Secret, safe="" + ), + ) + + return "{schema}://{auth}{targets}?{params}".format( + schema=self.secure_protocol, + auth=auth, + targets="/".join(chain( + [self.pprint(x, privacy, safe="") for x in self.targets], + [self.pprint(x, privacy, safe="") + for x in self.invalid_targets], + )), + params=NotifyAprs.urlencode(params), + ) + + def __len__(self): + """ + Returns the number of targets associated with this notification + """ + targets = len(self.targets) + return targets if targets > 0 else 1 + + def __del__(self): + """ + Ensure we close any lingering connections + """ + self.socket_close() + + @staticmethod + def parse_url(url): + """ + Parses the URL and returns enough arguments that can allow + us to re-instantiate this object. + + """ + results = NotifyBase.parse_url(url, verify_host=False) + if not results: + # We're done early as we couldn't load the results + return results + + # All elements are targets + results["targets"] = [NotifyAprs.unquote(results["host"])] + + # All entries after the hostname are additional targets + results["targets"].extend(NotifyAprs.split_path(results["fullpath"])) + + # Support the 'to' variable so that we can support rooms this way too + # The 'to' makes it easier to use yaml configuration + if "to" in results["qsd"] and len(results["qsd"]["to"]): + results["targets"] += NotifyAprs.parse_list(results["qsd"]["to"]) + + # Set our APRS-IS server locale's key value and convert it to uppercase + if "locale" in results["qsd"] and len(results["qsd"]["locale"]): + results["locale"] = NotifyAprs.unquote( + results["qsd"]["locale"] + ).upper() + + return results diff --git a/libs/apprise/plugins/NotifyBark.py b/libs/apprise/plugins/NotifyBark.py index edef82bd8..781a1515e 100644 --- a/libs/apprise/plugins/NotifyBark.py +++ b/libs/apprise/plugins/NotifyBark.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifyBase.py b/libs/apprise/plugins/NotifyBase.py index 5138c15c8..6daa8aa1d 100644 --- a/libs/apprise/plugins/NotifyBase.py +++ b/libs/apprise/plugins/NotifyBase.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: @@ -32,6 +32,7 @@ from functools import partial from ..URLBase import URLBase from ..common import NotifyType +from ..utils import parse_bool from ..common import NOTIFY_TYPES from ..common import NotifyFormat from ..common import NOTIFY_FORMATS @@ -135,6 +136,9 @@ class NotifyBase(URLBase): # Default Overflow Mode overflow_mode = OverflowMode.UPSTREAM + # Default Emoji Interpretation + interpret_emojis = False + # Support Attachments; this defaults to being disabled. # Since apprise allows you to send attachments without a body or title # defined, by letting Apprise know the plugin won't support attachments @@ -183,8 +187,66 @@ class NotifyBase(URLBase): # runtime. '_lookup_default': 'notify_format', }, + 'emojis': { + 'name': _('Interpret Emojis'), + # SSL Certificate Authority Verification + 'type': 'bool', + # Provide a default + 'default': interpret_emojis, + # look up default using the following parent class value at + # runtime. + '_lookup_default': 'interpret_emojis', + }, }) + # + # Overflow Defaults / Configuration applicable to SPLIT mode only + # + + # Display Count [X/X] + # ^^^^^^ + # \\\\\\ + # 6 characters (space + count) + # Display Count [XX/XX] + # ^^^^^^^^ + # \\\\\\\\ + # 8 characters (space + count) + # Display Count [XXX/XXX] + # ^^^^^^^^^^ + # \\\\\\\\\\ + # 10 characters (space + count) + # Display Count [XXXX/XXXX] + # ^^^^^^^^^^^^ + # \\\\\\\\\\\\ + # 12 characters (space + count) + # + # Given the above + some buffer we come up with the following: + # If this value is exceeded, display counts automatically shut off + overflow_max_display_count_width = 12 + + # The number of characters to reserver for whitespace buffering + # This is detected automatically, but you can enforce a value if + # you desire: + overflow_buffer = 0 + + # the min accepted length of a title to allow for a counter display + overflow_display_count_threshold = 130 + + # Whether or not when over-flow occurs, if the title should be repeated + # each time the message is split up + # - None: Detect + # - True: Always display title once + # - False: Display the title for each occurance + overflow_display_title_once = None + + # If this is set to to True: + # The title_maxlen should be considered as a subset of the body_maxlen + # Hence: len(title) + len(body) should never be greater then body_maxlen + # + # If set to False, then there is no corrorlation between title_maxlen + # restrictions and that of body_maxlen + overflow_amalgamate_title = False + def __init__(self, **kwargs): """ Initialize some general configuration that will keep things consistent @@ -194,6 +256,29 @@ class NotifyBase(URLBase): super().__init__(**kwargs) + # Store our interpret_emoji's setting + # If asset emoji value is set to a default of True and the user + # specifies it to be false, this is accepted and False over-rides. + # + # If asset emoji value is set to a default of None, a user may + # optionally over-ride this and set it to True from the Apprise + # URL. ?emojis=yes + # + # If asset emoji value is set to a default of False, then all emoji's + # are turned off (no user over-rides allowed) + # + + # Take a default + self.interpret_emojis = self.asset.interpret_emojis + if 'emojis' in kwargs: + # possibly over-ride default + self.interpret_emojis = True if self.interpret_emojis \ + in (None, True) and \ + parse_bool( + kwargs.get('emojis', False), + default=NotifyBase.template_args['emojis']['default']) \ + else False + if 'format' in kwargs: # Store the specified format if specified notify_format = kwargs.get('format', '') @@ -417,7 +502,6 @@ class NotifyBase(URLBase): overflow = self.overflow_mode if self.title_maxlen <= 0 and len(title) > 0: - if self.notify_format == NotifyFormat.HTML: # Content is appended to body as html body = '<{open_tag}>{title}' \ @@ -453,29 +537,148 @@ class NotifyBase(URLBase): response.append({'body': body, 'title': title}) return response - elif len(title) > self.title_maxlen: - # Truncate our Title - title = title[:self.title_maxlen] + # a value of '2' allows for the \r\n that is applied when + # amalgamating the title + overflow_buffer = max(2, self.overflow_buffer) \ + if (self.title_maxlen == 0 and len(title)) \ + else self.overflow_buffer - if self.body_maxlen > 0 and len(body) <= self.body_maxlen: + # + # If we reach here in our code, then we're using TRUNCATE, or SPLIT + # actions which require some math to handle the data + # + + # Handle situations where our body and title are amalamated into one + # calculation + title_maxlen = self.title_maxlen \ + if not self.overflow_amalgamate_title \ + else min(len(title) + self.overflow_max_display_count_width, + self.title_maxlen, self.body_maxlen) + + if len(title) > title_maxlen: + # Truncate our Title + title = title[:title_maxlen].rstrip() + + if self.overflow_amalgamate_title and ( + self.body_maxlen - overflow_buffer) >= title_maxlen: + body_maxlen = (self.body_maxlen if not title else ( + self.body_maxlen - title_maxlen)) - overflow_buffer + else: + # status quo + body_maxlen = self.body_maxlen \ + if not self.overflow_amalgamate_title else \ + (self.body_maxlen - overflow_buffer) + + if body_maxlen > 0 and len(body) <= body_maxlen: response.append({'body': body, 'title': title}) return response if overflow == OverflowMode.TRUNCATE: # Truncate our body and return response.append({ - 'body': body[:self.body_maxlen], + 'body': body[:body_maxlen].lstrip('\r\n\x0b\x0c').rstrip(), 'title': title, }) # For truncate mode, we're done now return response + if self.overflow_display_title_once is None: + # Detect if we only display our title once or not: + overflow_display_title_once = \ + True if self.overflow_amalgamate_title and \ + body_maxlen < self.overflow_display_count_threshold \ + else False + else: + # Take on defined value + + overflow_display_title_once = self.overflow_display_title_once + # If we reach here, then we are in SPLIT mode. # For here, we want to split the message as many times as we have to # in order to fit it within the designated limits. - response = [{ - 'body': body[i: i + self.body_maxlen], - 'title': title} for i in range(0, len(body), self.body_maxlen)] + if not overflow_display_title_once and not ( + # edge case that can occur when overflow_display_title_once is + # forced off, but no body exists + self.overflow_amalgamate_title and body_maxlen <= 0): + + show_counter = title and len(body) > body_maxlen and \ + ((self.overflow_amalgamate_title and + body_maxlen >= self.overflow_display_count_threshold) or + (not self.overflow_amalgamate_title and + title_maxlen > self.overflow_display_count_threshold)) and ( + title_maxlen > (self.overflow_max_display_count_width + + overflow_buffer) and + self.title_maxlen >= self.overflow_display_count_threshold) + + count = 0 + template = '' + if show_counter: + # introduce padding + body_maxlen -= overflow_buffer + + count = int(len(body) / body_maxlen) \ + + (1 if len(body) % body_maxlen else 0) + + # Detect padding and prepare template + digits = len(str(count)) + template = ' [{:0%d}/{:0%d}]' % (digits, digits) + + # Update our counter + overflow_display_count_width = 4 + (digits * 2) + if overflow_display_count_width <= \ + self.overflow_max_display_count_width: + if len(title) > \ + title_maxlen - overflow_display_count_width: + # Truncate our title further + title = title[:title_maxlen - + overflow_display_count_width] + + else: # Way to many messages to display + show_counter = False + + response = [{ + 'body': body[i: i + body_maxlen] + .lstrip('\r\n\x0b\x0c').rstrip(), + 'title': title + ( + '' if not show_counter else + template.format(idx, count))} for idx, i in + enumerate(range(0, len(body), body_maxlen), start=1)] + + else: # Display title once and move on + response = [] + try: + i = range(0, len(body), body_maxlen)[0] + + response.append({ + 'body': body[i: i + body_maxlen] + .lstrip('\r\n\x0b\x0c').rstrip(), + 'title': title, + }) + + except (ValueError, IndexError): + # IndexError: + # - This happens if there simply was no body to display + + # ValueError: + # - This happens when body_maxlen < 0 (due to title being + # so large) + + # No worries; send title along + response.append({ + 'body': '', + 'title': title, + }) + + # Ensure our start is set properly + body_maxlen = 0 + + # Now re-calculate based on the increased length + for i in range(body_maxlen, len(body), self.body_maxlen): + response.append({ + 'body': body[i: i + self.body_maxlen] + .lstrip('\r\n\x0b\x0c').rstrip(), + 'title': '', + }) return response @@ -548,6 +751,10 @@ class NotifyBase(URLBase): results['overflow'])) del results['overflow'] + # Allow emoji's override + if 'emojis' in results['qsd']: + results['emojis'] = parse_bool(results['qsd'].get('emojis')) + return results @staticmethod diff --git a/libs/apprise/plugins/NotifyBoxcar.py b/libs/apprise/plugins/NotifyBoxcar.py index 9d3be6aec..808920ed5 100644 --- a/libs/apprise/plugins/NotifyBoxcar.py +++ b/libs/apprise/plugins/NotifyBoxcar.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: @@ -33,11 +33,7 @@ from json import dumps from time import time from hashlib import sha1 from itertools import chain -try: - from urlparse import urlparse - -except ImportError: - from urllib.parse import urlparse +from urllib.parse import urlparse from .NotifyBase import NotifyBase from ..URLBase import PrivacyMode diff --git a/libs/apprise/plugins/NotifyBulkSMS.py b/libs/apprise/plugins/NotifyBulkSMS.py index cf82a87a4..33664fb00 100644 --- a/libs/apprise/plugins/NotifyBulkSMS.py +++ b/libs/apprise/plugins/NotifyBulkSMS.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: @@ -248,7 +248,7 @@ class NotifyBulkSMS(NotifyBase): if not (self.targets or self.groups): # We have nothing to notify - self.logger.warning('There are no Twist targets to notify') + self.logger.warning('There are no BulkSMS targets to notify') return False # Send in batches if identified to do so diff --git a/libs/apprise/plugins/NotifyBulkVS.py b/libs/apprise/plugins/NotifyBulkVS.py new file mode 100644 index 000000000..e912dff25 --- /dev/null +++ b/libs/apprise/plugins/NotifyBulkVS.py @@ -0,0 +1,394 @@ +# -*- coding: utf-8 -*- +# BSD 2-Clause License +# +# Apprise - Push Notification Library. +# Copyright (c) 2024, Chris Caron +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +# To use this service you will need a BulkVS account +# You will need credits (new accounts start with a few) +# https://www.bulkvs.com/ + +# API is documented here: +# - https://portal.bulkvs.com/api/v1.0/documentation#/\ +# Messaging/post_messageSend +import requests +import json +from .NotifyBase import NotifyBase +from ..URLBase import PrivacyMode +from ..common import NotifyType +from ..utils import is_phone_no +from ..utils import parse_phone_no +from ..utils import parse_bool +from ..AppriseLocale import gettext_lazy as _ + + +class NotifyBulkVS(NotifyBase): + """ + A wrapper for BulkVS Notifications + """ + + # The default descriptive name associated with the Notification + service_name = 'BulkVS' + + # The services URL + service_url = 'https://www.bulkvs.com/' + + # All notification requests are secure + secure_protocol = 'bulkvs' + + # A URL that takes you to the setup/help of the specific protocol + setup_url = 'https://github.com/caronc/apprise/wiki/Notify_bulkvs' + + # BulkVS uses the http protocol with JSON requests + notify_url = 'https://portal.bulkvs.com/api/v1.0/messageSend' + + # The maximum length of the body + body_maxlen = 160 + + # The maximum amount of texts that can go out in one batch + default_batch_size = 4000 + + # A title can not be used for SMS Messages. Setting this to zero will + # cause any title (if defined) to get placed into the message body. + title_maxlen = 0 + + # Define object templates + templates = ( + '{schema}://{user}:{password}@{from_phone}/{targets}', + '{schema}://{user}:{password}@{from_phone}', + ) + + # Define our template tokens + template_tokens = dict(NotifyBase.template_tokens, **{ + 'user': { + 'name': _('User Name'), + 'type': 'string', + 'required': True, + }, + 'password': { + 'name': _('Password'), + 'type': 'string', + 'private': True, + 'required': True, + }, + 'from_phone': { + 'name': _('From Phone No'), + 'type': 'string', + 'regex': (r'^\+?[0-9\s)(+-]+$', 'i'), + 'map_to': 'source', + 'required': True, + }, + 'target_phone': { + 'name': _('Target Phone No'), + 'type': 'string', + 'prefix': '+', + 'regex': (r'^[0-9\s)(+-]+$', 'i'), + 'map_to': 'targets', + }, + 'targets': { + 'name': _('Targets'), + 'type': 'list:string', + 'required': True, + }, + }) + + # Define our template arguments + template_args = dict(NotifyBase.template_args, **{ + 'to': { + 'alias_of': 'targets', + }, + 'from': { + 'name': _('From Phone No'), + 'type': 'string', + 'regex': (r'^\+?[0-9\s)(+-]+$', 'i'), + 'map_to': 'source', + }, + 'batch': { + 'name': _('Batch Mode'), + 'type': 'bool', + 'default': False, + }, + }) + + def __init__(self, source=None, targets=None, batch=None, **kwargs): + """ + Initialize BulkVS Object + """ + super(NotifyBulkVS, self).__init__(**kwargs) + + if not (self.user and self.password): + msg = 'A BulkVS user/pass was not provided.' + self.logger.warning(msg) + raise TypeError(msg) + + result = is_phone_no(source) + if not result: + msg = 'The Account (From) Phone # specified ' \ + '({}) is invalid.'.format(source) + self.logger.warning(msg) + raise TypeError(msg) + + # Tidy source + self.source = result['full'] + + # Define whether or not we should operate in a batch mode + self.batch = self.template_args['batch']['default'] \ + if batch is None else bool(batch) + + # Parse our targets + self.targets = list() + + has_error = False + for target in parse_phone_no(targets): + # Parse each phone number we found + result = is_phone_no(target) + if result: + self.targets.append(result['full']) + continue + + has_error = True + self.logger.warning( + 'Dropped invalid phone # ({}) specified.'.format(target), + ) + + if not targets and not has_error: + # Default the SMS Message to ourselves + self.targets.append(self.source) + + return + + def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): + """ + Perform BulkVS Notification + """ + + if not self.targets: + # We have nothing to notify + self.logger.warning('There are no BulkVS targets to notify') + return False + + # Send in batches if identified to do so + batch_size = 1 if not self.batch else self.default_batch_size + + # error tracking (used for function return) + has_error = False + + # Prepare our headers + headers = { + 'User-Agent': self.app_id, + 'Accept': 'application/json', + 'Content-Type': 'application/json', + } + + # Prepare our payload + payload = { + # The To gets populated in the loop below + 'From': self.source, + 'To': None, + 'Message': body, + } + + # Authentication + auth = (self.user, self.password) + + # Prepare our targets + targets = list(self.targets) if batch_size == 1 else \ + [self.targets[index:index + batch_size] + for index in range(0, len(self.targets), batch_size)] + + while len(targets): + # Get our target to notify + target = targets.pop(0) + + # Prepare our user + payload['To'] = target + + # Printable reference + if isinstance(target, list): + p_target = '{} targets'.format(len(target)) + + else: + p_target = target + + # Some Debug Logging + self.logger.debug('BulkVS POST URL: {} (cert_verify={})'.format( + self.notify_url, self.verify_certificate)) + self.logger.debug('BulkVS Payload: {}' .format(payload)) + + # Always call throttle before any remote server i/o is made + self.throttle() + try: + r = requests.post( + self.notify_url, + data=json.dumps(payload), + headers=headers, + auth=auth, + verify=self.verify_certificate, + timeout=self.request_timeout, + ) + + # A Response may look like: + # { + # "RefId": "5a66dee6-ff7a-40ee-8218-5805c074dc01", + # "From": "13109060901", + # "MessageType": "SMS|MMS", + # "Results": [ + # { + # "To": "13105551212", + # "Status": "SUCCESS" + # }, + # { + # "To": "13105551213", + # "Status": "SUCCESS" + # } + # ] + # } + if r.status_code != requests.codes.ok: + # We had a problem + status_str = \ + NotifyBase.http_response_code_lookup(r.status_code) + + # set up our status code to use + status_code = r.status_code + + self.logger.warning( + 'Failed to send BulkVS notification to {}: ' + '{}{}error={}.'.format( + p_target, + status_str, + ', ' if status_str else '', + status_code)) + + self.logger.debug( + 'Response Details:\r\n{}'.format(r.content)) + + # Mark our failure + has_error = True + continue + + else: + self.logger.info( + 'Sent BulkVS notification to {}.'.format(p_target)) + + except requests.RequestException as e: + self.logger.warning( + 'A Connection error occurred sending BulkVS: to %s ', + p_target) + self.logger.debug('Socket Exception: %s' % str(e)) + + # Mark our failure + has_error = True + continue + + return not has_error + + def url(self, privacy=False, *args, **kwargs): + """ + Returns the URL built dynamically based on specified arguments. + """ + + # Define any URL parameters + params = { + 'batch': 'yes' if self.batch else 'no', + } + + # Extend our parameters + params.update(self.url_parameters(privacy=privacy, *args, **kwargs)) + + # A nice way of cleaning up the URL length a bit + targets = [] if len(self.targets) == 1 \ + and self.targets[0] == self.source else self.targets + + return '{schema}://{user}:{password}@{source}/{targets}' \ + '?{params}'.format( + schema=self.secure_protocol, + source=self.source, + user=self.pprint(self.user, privacy, safe=''), + password=self.pprint( + self.password, privacy, mode=PrivacyMode.Secret, safe=''), + targets='/'.join([ + NotifyBulkVS.quote('{}'.format(x), safe='+') + for x in targets]), + params=NotifyBulkVS.urlencode(params)) + + def __len__(self): + """ + Returns the number of targets associated with this notification + """ + + # + # Factor batch into calculation + # + batch_size = 1 if not self.batch else self.default_batch_size + targets = len(self.targets) if self.targets else 1 + if batch_size > 1: + targets = int(targets / batch_size) + \ + (1 if targets % batch_size else 0) + + return targets + + @staticmethod + def parse_url(url): + """ + Parses the URL and returns enough arguments that can allow + us to re-instantiate this object. + + """ + results = NotifyBase.parse_url(url, verify_host=False) + if not results: + # We're done early as we couldn't load the results + return results + + # Support the 'from' and 'source' variable so that we can support + # targets this way too. + # The 'from' makes it easier to use yaml configuration + if 'from' in results['qsd'] and len(results['qsd']['from']): + results['source'] = \ + NotifyBulkVS.unquote(results['qsd']['from']) + + # hostname will also be a target in this case + results['targets'] = [ + *NotifyBulkVS.parse_phone_no(results['host']), + *NotifyBulkVS.split_path(results['fullpath'])] + + else: + # store our source + results['source'] = NotifyBulkVS.unquote(results['host']) + + # store targets + results['targets'] = NotifyBulkVS.split_path(results['fullpath']) + + # Support the 'to' variable so that we can support targets this way too + # The 'to' makes it easier to use yaml configuration + if 'to' in results['qsd'] and len(results['qsd']['to']): + results['targets'] += \ + NotifyBulkVS.parse_phone_no(results['qsd']['to']) + + # Get Batch Mode Flag + results['batch'] = \ + parse_bool(results['qsd'].get( + 'batch', NotifyBulkVS.template_args['batch']['default'])) + + return results diff --git a/libs/apprise/plugins/NotifyBurstSMS.py b/libs/apprise/plugins/NotifyBurstSMS.py index 59219b3d1..39606abba 100644 --- a/libs/apprise/plugins/NotifyBurstSMS.py +++ b/libs/apprise/plugins/NotifyBurstSMS.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifyClickSend.py b/libs/apprise/plugins/NotifyClickSend.py index 670e74e80..5e345fe10 100644 --- a/libs/apprise/plugins/NotifyClickSend.py +++ b/libs/apprise/plugins/NotifyClickSend.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifyD7Networks.py b/libs/apprise/plugins/NotifyD7Networks.py index 3e7787da8..906ec2fb9 100644 --- a/libs/apprise/plugins/NotifyD7Networks.py +++ b/libs/apprise/plugins/NotifyD7Networks.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifyDBus.py b/libs/apprise/plugins/NotifyDBus.py index 7d357aa75..52e119813 100644 --- a/libs/apprise/plugins/NotifyDBus.py +++ b/libs/apprise/plugins/NotifyDBus.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: @@ -26,9 +26,6 @@ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. -from __future__ import absolute_import -from __future__ import print_function - import sys from .NotifyBase import NotifyBase from ..common import NotifyImageSize diff --git a/libs/apprise/plugins/NotifyDapnet.py b/libs/apprise/plugins/NotifyDapnet.py index 5848b6886..ae7199c94 100644 --- a/libs/apprise/plugins/NotifyDapnet.py +++ b/libs/apprise/plugins/NotifyDapnet.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifyDingTalk.py b/libs/apprise/plugins/NotifyDingTalk.py index 91bfcd6fb..d4a492fc7 100644 --- a/libs/apprise/plugins/NotifyDingTalk.py +++ b/libs/apprise/plugins/NotifyDingTalk.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifyDiscord.py b/libs/apprise/plugins/NotifyDiscord.py index f87b66944..82d764f50 100644 --- a/libs/apprise/plugins/NotifyDiscord.py +++ b/libs/apprise/plugins/NotifyDiscord.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: @@ -60,6 +60,11 @@ from ..AppriseLocale import gettext_lazy as _ from ..attachment.AttachBase import AttachBase +# Used to detect user/role IDs +USER_ROLE_DETECTION_RE = re.compile( + r'\s*(?:<@(?P&?)(?P[0-9]+)>|@(?P[a-z0-9]+))', re.I) + + class NotifyDiscord(NotifyBase): """ A wrapper to Discord Notifications @@ -100,6 +105,10 @@ class NotifyDiscord(NotifyBase): # The maximum allowable characters allowed in the body per message body_maxlen = 2000 + # The 2000 characters above defined by the body_maxlen include that of the + # title. Setting this to True ensures overflow options behave properly + overflow_amalgamate_title = True + # Discord has a limit of the number of fields you can include in an # embeds message. This value allows the discord message to safely # break into multiple messages to handle these cases. @@ -336,6 +345,33 @@ class NotifyDiscord(NotifyBase): payload['content'] = \ body if not title else "{}\r\n{}".format(title, body) + # parse for user id's <@123> and role IDs <@&456> + results = USER_ROLE_DETECTION_RE.findall(body) + if results: + payload['allow_mentions'] = { + 'parse': [], + 'users': [], + 'roles': [], + } + + _content = [] + for (is_role, no, value) in results: + if value: + payload['allow_mentions']['parse'].append(value) + _content.append(f'@{value}') + + elif is_role: + payload['allow_mentions']['roles'].append(no) + _content.append(f'<@&{no}>') + + else: # is_user + payload['allow_mentions']['users'].append(no) + _content.append(f'<@{no}>') + + if self.notify_format == NotifyFormat.MARKDOWN: + # Add pingable elements to content field + payload['content'] = '👉 ' + ' '.join(_content) + if not self._send(payload, params=params): # We failed to post our message return False @@ -360,16 +396,21 @@ class NotifyDiscord(NotifyBase): 'wait': True, }) + # # Remove our text/title based content for attachment use + # if 'embeds' in payload: - # Markdown del payload['embeds'] if 'content' in payload: - # Markdown del payload['content'] + if 'allow_mentions' in payload: + del payload['allow_mentions'] + + # # Send our attachments + # for attachment in attach: self.logger.info( 'Posting Discord Attachment {}'.format(attachment.name)) diff --git a/libs/apprise/plugins/NotifyEmail.py b/libs/apprise/plugins/NotifyEmail.py index db70c8ef6..5ca422009 100644 --- a/libs/apprise/plugins/NotifyEmail.py +++ b/libs/apprise/plugins/NotifyEmail.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifyEmby.py b/libs/apprise/plugins/NotifyEmby.py index 99f3a9ab1..ce96553a2 100644 --- a/libs/apprise/plugins/NotifyEmby.py +++ b/libs/apprise/plugins/NotifyEmby.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifyEnigma2.py b/libs/apprise/plugins/NotifyEnigma2.py index 054726469..313149993 100644 --- a/libs/apprise/plugins/NotifyEnigma2.py +++ b/libs/apprise/plugins/NotifyEnigma2.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifyFCM/__init__.py b/libs/apprise/plugins/NotifyFCM/__init__.py index 57b03499b..54b6c9cc7 100644 --- a/libs/apprise/plugins/NotifyFCM/__init__.py +++ b/libs/apprise/plugins/NotifyFCM/__init__.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifyFCM/color.py b/libs/apprise/plugins/NotifyFCM/color.py index 69474a30c..c7da209a7 100644 --- a/libs/apprise/plugins/NotifyFCM/color.py +++ b/libs/apprise/plugins/NotifyFCM/color.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifyFCM/common.py b/libs/apprise/plugins/NotifyFCM/common.py index af71f8817..9f139226a 100644 --- a/libs/apprise/plugins/NotifyFCM/common.py +++ b/libs/apprise/plugins/NotifyFCM/common.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifyFCM/oauth.py b/libs/apprise/plugins/NotifyFCM/oauth.py index f0961039d..fbde3ccf7 100644 --- a/libs/apprise/plugins/NotifyFCM/oauth.py +++ b/libs/apprise/plugins/NotifyFCM/oauth.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifyFCM/priority.py b/libs/apprise/plugins/NotifyFCM/priority.py index 966a0e149..8564d3460 100644 --- a/libs/apprise/plugins/NotifyFCM/priority.py +++ b/libs/apprise/plugins/NotifyFCM/priority.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifyFaast.py b/libs/apprise/plugins/NotifyFaast.py index be3eff28d..f82c44020 100644 --- a/libs/apprise/plugins/NotifyFaast.py +++ b/libs/apprise/plugins/NotifyFaast.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifyFlock.py b/libs/apprise/plugins/NotifyFlock.py index 71a15da53..f1d12067e 100644 --- a/libs/apprise/plugins/NotifyFlock.py +++ b/libs/apprise/plugins/NotifyFlock.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifyForm.py b/libs/apprise/plugins/NotifyForm.py index 066f299b2..9690cd4f5 100644 --- a/libs/apprise/plugins/NotifyForm.py +++ b/libs/apprise/plugins/NotifyForm.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifyGnome.py b/libs/apprise/plugins/NotifyGnome.py index f27c286cb..67129216d 100644 --- a/libs/apprise/plugins/NotifyGnome.py +++ b/libs/apprise/plugins/NotifyGnome.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: @@ -26,9 +26,6 @@ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. -from __future__ import absolute_import -from __future__ import print_function - from .NotifyBase import NotifyBase from ..common import NotifyImageSize from ..common import NotifyType diff --git a/libs/apprise/plugins/NotifyGoogleChat.py b/libs/apprise/plugins/NotifyGoogleChat.py index 7119e7429..d2a6cc8a8 100644 --- a/libs/apprise/plugins/NotifyGoogleChat.py +++ b/libs/apprise/plugins/NotifyGoogleChat.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifyGotify.py b/libs/apprise/plugins/NotifyGotify.py index e20aa03da..3f4ce132d 100644 --- a/libs/apprise/plugins/NotifyGotify.py +++ b/libs/apprise/plugins/NotifyGotify.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifyGrowl.py b/libs/apprise/plugins/NotifyGrowl.py index 790945f00..0b42e3bec 100644 --- a/libs/apprise/plugins/NotifyGrowl.py +++ b/libs/apprise/plugins/NotifyGrowl.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifyGuilded.py b/libs/apprise/plugins/NotifyGuilded.py index 066cddee8..0ea36d9f8 100644 --- a/libs/apprise/plugins/NotifyGuilded.py +++ b/libs/apprise/plugins/NotifyGuilded.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifyHomeAssistant.py b/libs/apprise/plugins/NotifyHomeAssistant.py index 25d8f5fb4..0829381b9 100644 --- a/libs/apprise/plugins/NotifyHomeAssistant.py +++ b/libs/apprise/plugins/NotifyHomeAssistant.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifyHttpSMS.py b/libs/apprise/plugins/NotifyHttpSMS.py new file mode 100644 index 000000000..647100949 --- /dev/null +++ b/libs/apprise/plugins/NotifyHttpSMS.py @@ -0,0 +1,330 @@ +# -*- coding: utf-8 -*- +# BSD 2-Clause License +# +# Apprise - Push Notification Library. +# Copyright (c) 2024, Chris Caron +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +# To use this service you will need a httpSMS account +# You will need credits (new accounts start with a few) +# https://httpsms.com +import requests +import json +from .NotifyBase import NotifyBase +from ..common import NotifyType +from ..utils import is_phone_no +from ..utils import parse_phone_no +from ..utils import validate_regex +from ..AppriseLocale import gettext_lazy as _ + + +class NotifyHttpSMS(NotifyBase): + """ + A wrapper for HttpSMS Notifications + """ + + # The default descriptive name associated with the Notification + service_name = 'httpSMS' + + # The services URL + service_url = 'https://httpsms.com' + + # All notification requests are secure + secure_protocol = 'httpsms' + + # A URL that takes you to the setup/help of the specific protocol + setup_url = 'https://github.com/caronc/apprise/wiki/Notify_httpsms' + + # HttpSMS uses the http protocol with JSON requests + notify_url = 'https://api.httpsms.com/v1/messages/send' + + # The maximum length of the body + body_maxlen = 160 + + # A title can not be used for SMS Messages. Setting this to zero will + # cause any title (if defined) to get placed into the message body. + title_maxlen = 0 + + # Define object templates + templates = ( + '{schema}://{apikey}@{from_phone}', + '{schema}://{apikey}@{from_phone}/{targets}', + ) + + # Define our template tokens + template_tokens = dict(NotifyBase.template_tokens, **{ + 'apikey': { + 'name': _('API Key'), + 'type': 'string', + 'private': True, + 'required': True, + }, + 'from_phone': { + 'name': _('From Phone No'), + 'type': 'string', + 'regex': (r'^\+?[0-9\s)(+-]+$', 'i'), + 'map_to': 'source', + 'required': True, + }, + 'target_phone': { + 'name': _('Target Phone No'), + 'type': 'string', + 'prefix': '+', + 'regex': (r'^[0-9\s)(+-]+$', 'i'), + 'map_to': 'targets', + }, + 'targets': { + 'name': _('Targets'), + 'type': 'list:string', + 'required': True, + }, + }) + + # Define our template arguments + template_args = dict(NotifyBase.template_args, **{ + 'key': { + 'alias_of': 'apikey', + }, + 'to': { + 'alias_of': 'targets', + }, + 'from': { + 'name': _('From Phone No'), + 'type': 'string', + 'regex': (r'^\+?[0-9\s)(+-]+$', 'i'), + 'map_to': 'source', + }, + }) + + def __init__(self, apikey=None, source=None, targets=None, **kwargs): + """ + Initialize HttpSMS Object + """ + super(NotifyHttpSMS, self).__init__(**kwargs) + + self.apikey = validate_regex(apikey) + if not self.apikey: + msg = 'An invalid API Key ({}) was specified.'.format(apikey) + self.logger.warning(msg) + raise TypeError(msg) + + result = is_phone_no(source) + if not result: + msg = 'The Account (From) Phone # specified ' \ + '({}) is invalid.'.format(source) + self.logger.warning(msg) + raise TypeError(msg) + + # Tidy source + self.source = result['full'] + + # Parse our targets + self.targets = list() + + has_error = False + for target in parse_phone_no(targets): + # Parse each phone number we found + result = is_phone_no(target) + if result: + self.targets.append(result['full']) + continue + + has_error = True + self.logger.warning( + 'Dropped invalid phone # ({}) specified.'.format(target), + ) + + if not targets and not has_error: + # Default the SMS Message to ourselves + self.targets.append(self.source) + + return + + def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): + """ + Perform HttpSMS Notification + """ + + if not self.targets: + # We have nothing to notify + self.logger.warning('There are no HttpSMS targets to notify') + return False + + # error tracking (used for function return) + has_error = False + + # Prepare our headers + headers = { + 'User-Agent': self.app_id, + 'x-api-key': self.apikey, + 'Content-Type': 'application/json', + } + + # Prepare our payload + payload = { + # The To gets populated in the loop below + 'from': '+' + self.source, + 'to': None, + 'content': body, + } + + # Prepare our targets + targets = list(self.targets) + while len(targets): + # Get our target to notify + target = targets.pop(0) + + # Prepare our user + payload['to'] = '+' + target + + # Some Debug Logging + self.logger.debug('HttpSMS POST URL: {} (cert_verify={})'.format( + self.notify_url, self.verify_certificate)) + self.logger.debug('HttpSMS Payload: {}' .format(payload)) + + # Always call throttle before any remote server i/o is made + self.throttle() + try: + r = requests.post( + self.notify_url, + data=json.dumps(payload), + headers=headers, + verify=self.verify_certificate, + timeout=self.request_timeout, + ) + + if r.status_code != requests.codes.ok: + # We had a problem + status_str = \ + NotifyBase.http_response_code_lookup(r.status_code) + + # set up our status code to use + status_code = r.status_code + + self.logger.warning( + 'Failed to send HttpSMS notification to {}: ' + '{}{}error={}.'.format( + target, + status_str, + ', ' if status_str else '', + status_code)) + + self.logger.debug( + 'Response Details:\r\n{}'.format(r.content)) + + # Mark our failure + has_error = True + continue + + else: + self.logger.info( + 'Sent HttpSMS notification to {}.'.format(target)) + + except requests.RequestException as e: + self.logger.warning( + 'A Connection error occurred sending HttpSMS: to %s ', + target) + self.logger.debug('Socket Exception: %s' % str(e)) + + # Mark our failure + has_error = True + continue + + return not has_error + + def url(self, privacy=False, *args, **kwargs): + """ + Returns the URL built dynamically based on specified arguments. + """ + + # Prepare our parameters + params = self.url_parameters(privacy=privacy, *args, **kwargs) + + # A nice way of cleaning up the URL length a bit + targets = [] if len(self.targets) == 1 \ + and self.targets[0] == self.source else self.targets + + return '{schema}://{apikey}@{source}/{targets}' \ + '?{params}'.format( + schema=self.secure_protocol, + source=self.source, + apikey=self.pprint(self.apikey, privacy, safe=''), + targets='/'.join([ + NotifyHttpSMS.quote('{}'.format(x), safe='+') + for x in targets]), + params=NotifyHttpSMS.urlencode(params)) + + def __len__(self): + """ + Returns the number of targets associated with this notification + """ + + return len(self.targets) if self.targets else 1 + + @staticmethod + def parse_url(url): + """ + Parses the URL and returns enough arguments that can allow + us to re-instantiate this object. + + """ + + results = NotifyBase.parse_url(url, verify_host=False) + if not results: + # We're done early as we couldn't load the results + return results + + # Get our API Key + results['apikey'] = NotifyHttpSMS.unquote(results['user']) + + # Support the 'from' and 'source' variable so that we can support + # targets this way too. + # The 'from' makes it easier to use yaml configuration + if 'from' in results['qsd'] and len(results['qsd']['from']): + results['source'] = \ + NotifyHttpSMS.unquote(results['qsd']['from']) + + # hostname will also be a target in this case + results['targets'] = [ + *NotifyHttpSMS.parse_phone_no(results['host']), + *NotifyHttpSMS.split_path(results['fullpath'])] + + else: + # store our source + results['source'] = NotifyHttpSMS.unquote(results['host']) + + # store targets + results['targets'] = NotifyHttpSMS.split_path(results['fullpath']) + + # Support the 'to' variable so that we can support targets this way too + # The 'to' makes it easier to use yaml configuration + if 'to' in results['qsd'] and len(results['qsd']['to']): + results['targets'] += \ + NotifyHttpSMS.parse_phone_no(results['qsd']['to']) + + if 'key' in results['qsd'] and len(results['qsd']['key']): + results['apikey'] = \ + NotifyHttpSMS.unquote(results['qsd']['key']) + + return results diff --git a/libs/apprise/plugins/NotifyIFTTT.py b/libs/apprise/plugins/NotifyIFTTT.py index 2c386c6b6..9174640d0 100644 --- a/libs/apprise/plugins/NotifyIFTTT.py +++ b/libs/apprise/plugins/NotifyIFTTT.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifyJSON.py b/libs/apprise/plugins/NotifyJSON.py index a8ab7adc3..182ff77cf 100644 --- a/libs/apprise/plugins/NotifyJSON.py +++ b/libs/apprise/plugins/NotifyJSON.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifyJoin.py b/libs/apprise/plugins/NotifyJoin.py index 92af6c3f1..c6b0d91e9 100644 --- a/libs/apprise/plugins/NotifyJoin.py +++ b/libs/apprise/plugins/NotifyJoin.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifyKavenegar.py b/libs/apprise/plugins/NotifyKavenegar.py index d1df47c9e..2a9c169d7 100644 --- a/libs/apprise/plugins/NotifyKavenegar.py +++ b/libs/apprise/plugins/NotifyKavenegar.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifyKumulos.py b/libs/apprise/plugins/NotifyKumulos.py index 6072340f8..da372e773 100644 --- a/libs/apprise/plugins/NotifyKumulos.py +++ b/libs/apprise/plugins/NotifyKumulos.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifyLametric.py b/libs/apprise/plugins/NotifyLametric.py index 516ec27ca..5825d9176 100644 --- a/libs/apprise/plugins/NotifyLametric.py +++ b/libs/apprise/plugins/NotifyLametric.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifyLine.py b/libs/apprise/plugins/NotifyLine.py index 09d72fed8..61e4f3703 100644 --- a/libs/apprise/plugins/NotifyLine.py +++ b/libs/apprise/plugins/NotifyLine.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifyMQTT.py b/libs/apprise/plugins/NotifyMQTT.py index 2372c8b45..4e159b662 100644 --- a/libs/apprise/plugins/NotifyMQTT.py +++ b/libs/apprise/plugins/NotifyMQTT.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifyMSG91.py b/libs/apprise/plugins/NotifyMSG91.py index 225a2d3d9..a7bd9c473 100644 --- a/libs/apprise/plugins/NotifyMSG91.py +++ b/libs/apprise/plugins/NotifyMSG91.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifyMSTeams.py b/libs/apprise/plugins/NotifyMSTeams.py index e82fdb8ca..06572c3e6 100644 --- a/libs/apprise/plugins/NotifyMSTeams.py +++ b/libs/apprise/plugins/NotifyMSTeams.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifyMacOSX.py b/libs/apprise/plugins/NotifyMacOSX.py index ae08da112..971951259 100644 --- a/libs/apprise/plugins/NotifyMacOSX.py +++ b/libs/apprise/plugins/NotifyMacOSX.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: @@ -26,9 +26,6 @@ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. -from __future__ import absolute_import -from __future__ import print_function - import platform import subprocess import os @@ -43,7 +40,6 @@ from ..AppriseLocale import gettext_lazy as _ NOTIFY_MACOSX_SUPPORT_ENABLED = False -# TODO: The module will be easier to test without module-level code. if platform.system() == 'Darwin': # Check this is Mac OS X 10.8, or higher major, minor = platform.mac_ver()[0].split('.')[:2] diff --git a/libs/apprise/plugins/NotifyMailgun.py b/libs/apprise/plugins/NotifyMailgun.py index 5afebc52b..82cf970bf 100644 --- a/libs/apprise/plugins/NotifyMailgun.py +++ b/libs/apprise/plugins/NotifyMailgun.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifyMastodon.py b/libs/apprise/plugins/NotifyMastodon.py index 90c39e14b..0d2f27df3 100644 --- a/libs/apprise/plugins/NotifyMastodon.py +++ b/libs/apprise/plugins/NotifyMastodon.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifyMatrix.py b/libs/apprise/plugins/NotifyMatrix.py index 8f3e77ff9..594274761 100644 --- a/libs/apprise/plugins/NotifyMatrix.py +++ b/libs/apprise/plugins/NotifyMatrix.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: @@ -727,28 +727,38 @@ class NotifyMatrix(NotifyBase): # "content_uri": "mxc://example.com/a-unique-key" # } - if self.version == MatrixVersion.V3: - # Prepare our payload - payloads.append({ - "body": attachment.name, - "info": { - "mimetype": attachment.mimetype, - "size": len(attachment), - }, - "msgtype": "m.image", - "url": response.get('content_uri'), - }) + # FUTURE if self.version == MatrixVersion.V3: + # FUTURE # Prepare our payload + # FUTURE payloads.append({ + # FUTURE "body": attachment.name, + # FUTURE "info": { + # FUTURE "mimetype": attachment.mimetype, + # FUTURE "size": len(attachment), + # FUTURE }, + # FUTURE "msgtype": "m.image", + # FUTURE "url": response.get('content_uri'), + # FUTURE }) - else: - # Prepare our payload - payloads.append({ - "info": { - "mimetype": attachment.mimetype, - }, - "msgtype": "m.image", - "body": "tta.webp", - "url": response.get('content_uri'), - }) + # FUTURE else: + # FUTURE # Prepare our payload + # FUTURE payloads.append({ + # FUTURE "info": { + # FUTURE "mimetype": attachment.mimetype, + # FUTURE }, + # FUTURE "msgtype": "m.image", + # FUTURE "body": "tta.webp", + # FUTURE "url": response.get('content_uri'), + # FUTURE }) + + # Prepare our payload + payloads.append({ + "info": { + "mimetype": attachment.mimetype, + }, + "msgtype": "m.image", + "body": "tta.webp", + "url": response.get('content_uri'), + }) return payloads @@ -1131,11 +1141,12 @@ class NotifyMatrix(NotifyBase): or self.port == default_port else f':{self.port}') if path == '/upload': - if self.version == MatrixVersion.V3: - url += MATRIX_V3_MEDIA_PATH + path + # FUTURE if self.version == MatrixVersion.V3: + # FUTURE url += MATRIX_V3_MEDIA_PATH + path - else: - url += MATRIX_V2_MEDIA_PATH + path + # FUTURE else: + # FUTURE url += MATRIX_V2_MEDIA_PATH + path + url += MATRIX_V2_MEDIA_PATH + path params = {'filename': attachment.name} with open(attachment.path, 'rb') as fp: diff --git a/libs/apprise/plugins/NotifyMattermost.py b/libs/apprise/plugins/NotifyMattermost.py index 859fed311..dbb5f0dd3 100644 --- a/libs/apprise/plugins/NotifyMattermost.py +++ b/libs/apprise/plugins/NotifyMattermost.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifyMessageBird.py b/libs/apprise/plugins/NotifyMessageBird.py index 4cb9d7b56..42d880acd 100644 --- a/libs/apprise/plugins/NotifyMessageBird.py +++ b/libs/apprise/plugins/NotifyMessageBird.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifyMisskey.py b/libs/apprise/plugins/NotifyMisskey.py index 57633a515..8965a0f7b 100644 --- a/libs/apprise/plugins/NotifyMisskey.py +++ b/libs/apprise/plugins/NotifyMisskey.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifyNextcloud.py b/libs/apprise/plugins/NotifyNextcloud.py index b1d623d0b..fd471d9eb 100644 --- a/libs/apprise/plugins/NotifyNextcloud.py +++ b/libs/apprise/plugins/NotifyNextcloud.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifyNextcloudTalk.py b/libs/apprise/plugins/NotifyNextcloudTalk.py index 4f6dc0541..4bfced282 100644 --- a/libs/apprise/plugins/NotifyNextcloudTalk.py +++ b/libs/apprise/plugins/NotifyNextcloudTalk.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifyNotica.py b/libs/apprise/plugins/NotifyNotica.py index f95baba3f..33a94fc96 100644 --- a/libs/apprise/plugins/NotifyNotica.py +++ b/libs/apprise/plugins/NotifyNotica.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifyNotifiarr.py b/libs/apprise/plugins/NotifyNotifiarr.py index 748e3b7aa..e195cbd32 100644 --- a/libs/apprise/plugins/NotifyNotifiarr.py +++ b/libs/apprise/plugins/NotifyNotifiarr.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifyNotifico.py b/libs/apprise/plugins/NotifyNotifico.py index 8636e2e00..27ce29a6e 100644 --- a/libs/apprise/plugins/NotifyNotifico.py +++ b/libs/apprise/plugins/NotifyNotifico.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifyNtfy.py b/libs/apprise/plugins/NotifyNtfy.py index ceab5a2a3..cc705c6cc 100644 --- a/libs/apprise/plugins/NotifyNtfy.py +++ b/libs/apprise/plugins/NotifyNtfy.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifyOffice365.py b/libs/apprise/plugins/NotifyOffice365.py index f445bc49d..0c62279f9 100644 --- a/libs/apprise/plugins/NotifyOffice365.py +++ b/libs/apprise/plugins/NotifyOffice365.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifyOneSignal.py b/libs/apprise/plugins/NotifyOneSignal.py index 39dd7f206..eb1e10f7a 100644 --- a/libs/apprise/plugins/NotifyOneSignal.py +++ b/libs/apprise/plugins/NotifyOneSignal.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifyOpsgenie.py b/libs/apprise/plugins/NotifyOpsgenie.py index 29cd0a202..c2dfed232 100644 --- a/libs/apprise/plugins/NotifyOpsgenie.py +++ b/libs/apprise/plugins/NotifyOpsgenie.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifyPagerDuty.py b/libs/apprise/plugins/NotifyPagerDuty.py index 1592f93c9..0896b41b1 100644 --- a/libs/apprise/plugins/NotifyPagerDuty.py +++ b/libs/apprise/plugins/NotifyPagerDuty.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifyPagerTree.py b/libs/apprise/plugins/NotifyPagerTree.py index a1579c30c..c9290f2f7 100644 --- a/libs/apprise/plugins/NotifyPagerTree.py +++ b/libs/apprise/plugins/NotifyPagerTree.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifyParsePlatform.py b/libs/apprise/plugins/NotifyParsePlatform.py index f3d7d635e..2a182ed31 100644 --- a/libs/apprise/plugins/NotifyParsePlatform.py +++ b/libs/apprise/plugins/NotifyParsePlatform.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifyPopcornNotify.py b/libs/apprise/plugins/NotifyPopcornNotify.py index 47a296147..0ecd8af68 100644 --- a/libs/apprise/plugins/NotifyPopcornNotify.py +++ b/libs/apprise/plugins/NotifyPopcornNotify.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifyProwl.py b/libs/apprise/plugins/NotifyProwl.py index 80f0aca3a..e5c07bf4e 100644 --- a/libs/apprise/plugins/NotifyProwl.py +++ b/libs/apprise/plugins/NotifyProwl.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifyPushBullet.py b/libs/apprise/plugins/NotifyPushBullet.py index 61e8db2d5..5e9c43fbf 100644 --- a/libs/apprise/plugins/NotifyPushBullet.py +++ b/libs/apprise/plugins/NotifyPushBullet.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifyPushDeer.py b/libs/apprise/plugins/NotifyPushDeer.py index 76805c34b..6766dc7ff 100644 --- a/libs/apprise/plugins/NotifyPushDeer.py +++ b/libs/apprise/plugins/NotifyPushDeer.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifyPushMe.py b/libs/apprise/plugins/NotifyPushMe.py index 8ef3c79c5..30889f868 100644 --- a/libs/apprise/plugins/NotifyPushMe.py +++ b/libs/apprise/plugins/NotifyPushMe.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifyPushSafer.py b/libs/apprise/plugins/NotifyPushSafer.py index 9873bd8e1..32cfa82fe 100644 --- a/libs/apprise/plugins/NotifyPushSafer.py +++ b/libs/apprise/plugins/NotifyPushSafer.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifyPushed.py b/libs/apprise/plugins/NotifyPushed.py index 96e2e89d4..a50970f99 100644 --- a/libs/apprise/plugins/NotifyPushed.py +++ b/libs/apprise/plugins/NotifyPushed.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifyPushjet.py b/libs/apprise/plugins/NotifyPushjet.py index 50ee16e41..253ac6818 100644 --- a/libs/apprise/plugins/NotifyPushjet.py +++ b/libs/apprise/plugins/NotifyPushjet.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifyPushover.py b/libs/apprise/plugins/NotifyPushover.py index 4a76e7d54..be6ada289 100644 --- a/libs/apprise/plugins/NotifyPushover.py +++ b/libs/apprise/plugins/NotifyPushover.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifyPushy.py b/libs/apprise/plugins/NotifyPushy.py index 2a8a456b3..097017dac 100644 --- a/libs/apprise/plugins/NotifyPushy.py +++ b/libs/apprise/plugins/NotifyPushy.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifyRSyslog.py b/libs/apprise/plugins/NotifyRSyslog.py index 473e4c5cb..e39744627 100644 --- a/libs/apprise/plugins/NotifyRSyslog.py +++ b/libs/apprise/plugins/NotifyRSyslog.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifyReddit.py b/libs/apprise/plugins/NotifyReddit.py index b25e76d0b..022a0a50d 100644 --- a/libs/apprise/plugins/NotifyReddit.py +++ b/libs/apprise/plugins/NotifyReddit.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifyRocketChat.py b/libs/apprise/plugins/NotifyRocketChat.py index 6384386e7..8e6c0751c 100644 --- a/libs/apprise/plugins/NotifyRocketChat.py +++ b/libs/apprise/plugins/NotifyRocketChat.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifyRyver.py b/libs/apprise/plugins/NotifyRyver.py index 70f2fa436..0872f3e52 100644 --- a/libs/apprise/plugins/NotifyRyver.py +++ b/libs/apprise/plugins/NotifyRyver.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifySES.py b/libs/apprise/plugins/NotifySES.py index 37a0342ac..b580b14d6 100644 --- a/libs/apprise/plugins/NotifySES.py +++ b/libs/apprise/plugins/NotifySES.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifySMSEagle.py b/libs/apprise/plugins/NotifySMSEagle.py index 3db131fbc..33b4af752 100644 --- a/libs/apprise/plugins/NotifySMSEagle.py +++ b/libs/apprise/plugins/NotifySMSEagle.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifySMSManager.py b/libs/apprise/plugins/NotifySMSManager.py new file mode 100644 index 000000000..efc158b62 --- /dev/null +++ b/libs/apprise/plugins/NotifySMSManager.py @@ -0,0 +1,413 @@ +# -*- coding: utf-8 -*- +# BSD 2-Clause License +# +# Apprise - Push Notification Library. +# Copyright (c) 2024, Chris Caron +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +# API Reference: https://smsmanager.cz/api/http#send + +# To use this service you will need a SMS Manager account +# You will need credits (new accounts start with a few) +# https://smsmanager.cz +# 1. Sign up and get test credit +# 2. Generate an API key in web administration. + +import requests +from .NotifyBase import NotifyBase +from ..common import NotifyType +from ..utils import is_phone_no +from ..utils import parse_phone_no +from ..utils import parse_bool +from ..utils import validate_regex +from ..AppriseLocale import gettext_lazy as _ + + +class SMSManagerGateway(object): + """ + The different gateway values + """ + HIGH = "high" + ECONOMY = "economy" + LOW = "low" + DIRECT = "direct" + + +# Used for verification purposes +SMS_MANAGER_GATEWAYS = ( + SMSManagerGateway.HIGH, + SMSManagerGateway.ECONOMY, + SMSManagerGateway.LOW, + SMSManagerGateway.DIRECT, +) + + +class NotifySMSManager(NotifyBase): + """ + A wrapper for SMS Manager Notifications + """ + + # The default descriptive name associated with the Notification + service_name = 'SMS Manager' + + # The services URL + service_url = 'https://smsmanager.cz' + + # All notification requests are secure + secure_protocol = ('smsmgr', 'smsmanager',) + + # A URL that takes you to the setup/help of the specific protocol + setup_url = 'https://github.com/caronc/apprise/wiki/Notify_sms_manager' + + # SMS Manager uses the http protocol with JSON requests + notify_url = 'https://http-api.smsmanager.cz/Send' + + # The maximum amount of texts that can go out in one batch + default_batch_size = 4000 + + # The maximum length of the body + body_maxlen = 160 + + # A title can not be used for SMS Messages. Setting this to zero will + # cause any title (if defined) to get placed into the message body. + title_maxlen = 0 + + # Define object templates + templates = ( + '{schema}://{apikey}@{targets}', + ) + + # Define our template tokens + template_tokens = dict(NotifyBase.template_tokens, **{ + 'apikey': { + 'name': _('API Key'), + 'type': 'string', + 'private': True, + 'required': True, + }, + 'target_phone': { + 'name': _('Target Phone No'), + 'type': 'string', + 'prefix': '+', + 'regex': (r'^[0-9\s)(+-]+$', 'i'), + 'map_to': 'targets', + }, + 'targets': { + 'name': _('Targets'), + 'type': 'list:string', + 'required': True, + }, + }) + + # Define our template arguments + template_args = dict(NotifyBase.template_args, **{ + 'key': { + 'alias_of': 'apikey', + }, + 'to': { + 'alias_of': 'targets', + }, + 'from': { + 'name': _('From Phone No'), + 'type': 'string', + 'regex': (r'^\+?[0-9\s)(+-]+$', 'i'), + 'map_to': 'sender', + }, + 'sender': { + 'alias_of': 'from', + }, + 'gateway': { + 'name': _('Gateway'), + 'type': 'choice:string', + 'values': SMS_MANAGER_GATEWAYS, + 'default': SMS_MANAGER_GATEWAYS[0], + }, + 'batch': { + 'name': _('Batch Mode'), + 'type': 'bool', + 'default': False, + }, + }) + + def __init__(self, apikey=None, sender=None, targets=None, batch=None, + gateway=None, **kwargs): + """ + Initialize SMS Manager Object + """ + super(NotifySMSManager, self).__init__(**kwargs) + + self.apikey = validate_regex(apikey) + if not self.apikey: + msg = 'An invalid API Key ({}) was specified.'.format(apikey) + self.logger.warning(msg) + raise TypeError(msg) + + # Setup our gateway + self.gateway = self.template_args['gateway']['default'] \ + if not isinstance(gateway, str) else gateway.lower() + if self.gateway not in SMS_MANAGER_GATEWAYS: + msg = 'The Gateway specified ({}) is invalid.'.format(gateway) + self.logger.warning(msg) + raise TypeError(msg) + + # Define whether or not we should operate in a batch mode + self.batch = self.template_args['batch']['default'] \ + if batch is None else bool(batch) + + # Maximum 11 characters and must be approved by administrators of site + self.sender = sender[0:11] if isinstance(sender, str) else None + + # Parse our targets + self.targets = list() + + for target in parse_phone_no(targets): + # Parse each phone number we found + # It is documented that numbers with a length of 9 characters are + # supplemented by "420". + result = is_phone_no(target, min_len=9) + if result: + # Carry forward '+' if defined, otherwise do not... + self.targets.append( + ('+' + result['full']) + if target.lstrip()[0] == '+' else result['full']) + continue + + self.logger.warning( + 'Dropped invalid phone # ({}) specified.'.format(target), + ) + + return + + def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): + """ + Perform SMS Manager Notification + """ + + if not self.targets: + # We have nothing to notify + self.logger.warning('There are no SMS Manager targets to notify') + return False + + # error tracking (used for function return) + has_error = False + + # Send in batches if identified to do so + batch_size = 1 if not self.batch else self.default_batch_size + + # Prepare our headers + headers = { + 'User-Agent': self.app_id, + } + + # Prepare our targets + targets = list(self.targets) if batch_size == 1 else \ + [self.targets[index:index + batch_size] + for index in range(0, len(self.targets), batch_size)] + + while len(targets): + # Get our target to notify + target = targets.pop(0) + + # Prepare our payload + # Note: Payload is assembled inside of our while-loop due to + # mock testing issues (payload singleton isn't persistent + # when performing follow up checks on the params object. + payload = { + 'apikey': self.apikey, + 'gateway': self.gateway, + # The number gets populated in the loop below + 'number': None, + 'message': body, + } + + if self.sender: + # Sender is ony set if specified + payload['sender'] = self.sender + + # Printable target details + if isinstance(target, list): + p_target = '{} targets'.format(len(target)) + + # Prepare our target numbers + payload['number'] = ';'.join(target) + + else: + p_target = target + # Prepare our target numbers + payload['number'] = target + + # Some Debug Logging + self.logger.debug( + 'SMS Manager POST URL: {} (cert_verify={})'.format( + self.notify_url, self.verify_certificate)) + self.logger.debug('SMS Manager Payload: {}' .format(payload)) + + # Always call throttle before any remote server i/o is made + self.throttle() + try: + r = requests.get( + self.notify_url, + params=payload, + headers=headers, + verify=self.verify_certificate, + timeout=self.request_timeout, + ) + + if r.status_code != requests.codes.ok: + # We had a problem + status_str = \ + NotifyBase.http_response_code_lookup(r.status_code) + + # set up our status code to use + status_code = r.status_code + + self.logger.warning( + 'Failed to send SMS Manager notification to {}: ' + '{}{}error={}.'.format( + p_target, + status_str, + ', ' if status_str else '', + status_code)) + + self.logger.debug( + 'Response Details:\r\n{}'.format(r.content)) + + # Mark our failure + has_error = True + continue + + else: + self.logger.info( + 'Sent SMS Manager notification to {}.'.format( + p_target)) + + except requests.RequestException as e: + self.logger.warning( + 'A Connection error occurred sending SMS Manager: to %s ', + p_target) + self.logger.debug('Socket Exception: %s' % str(e)) + + # Mark our failure + has_error = True + continue + + return not has_error + + def url(self, privacy=False, *args, **kwargs): + """ + Returns the URL built dynamically based on specified arguments. + """ + + # Define any URL parameters + params = { + 'batch': 'yes' if self.batch else 'no', + 'gateway': self.gateway, + } + + if self.sender: + # Set our sender if it was set + params['sender'] = self.sender + + # Extend our parameters + params.update(self.url_parameters(privacy=privacy, *args, **kwargs)) + + return '{schema}://{apikey}@{targets}' \ + '?{params}'.format( + schema=self.secure_protocol[0], + apikey=self.pprint(self.apikey, privacy, safe=''), + targets='/'.join([ + NotifySMSManager.quote('{}'.format(x), safe='+') + for x in self.targets]), + params=NotifySMSManager.urlencode(params)) + + def __len__(self): + """ + Returns the number of targets associated with this notification + """ + + # + # Factor batch into calculation + # + # Note: Groups always require a separate request (and can not be + # included in batch calculations) + batch_size = 1 if not self.batch else self.default_batch_size + targets = len(self.targets) + if batch_size > 1: + targets = int(targets / batch_size) + \ + (1 if targets % batch_size else 0) + + return targets + + @staticmethod + def parse_url(url): + """ + Parses the URL and returns enough arguments that can allow + us to re-instantiate this object. + + """ + + results = NotifyBase.parse_url(url, verify_host=False) + if not results: + # We're done early as we couldn't load the results + return results + + # Get our API Key + results['apikey'] = NotifySMSManager.unquote(results['user']) + + # Store our targets + results['targets'] = [ + *NotifySMSManager.parse_phone_no(results['host']), + *NotifySMSManager.split_path(results['fullpath'])] + + # The 'from' makes it easier to use yaml configuration + if 'from' in results['qsd'] and len(results['qsd']['from']): + results['sender'] = \ + NotifySMSManager.unquote(results['qsd']['from']) + + elif 'sender' in results['qsd'] and len(results['qsd']['sender']): + # Support sender= value as well to align with SMS Manager API + results['sender'] = \ + NotifySMSManager.unquote(results['qsd']['sender']) + + # Support the 'to' variable so that we can support targets this way too + # The 'to' makes it easier to use yaml configuration + if 'to' in results['qsd'] and len(results['qsd']['to']): + results['targets'] += \ + NotifySMSManager.parse_phone_no(results['qsd']['to']) + + if 'key' in results['qsd'] and len(results['qsd']['key']): + results['apikey'] = \ + NotifySMSManager.unquote(results['qsd']['key']) + + # Get Batch Mode Flag + results['batch'] = \ + parse_bool(results['qsd'].get( + 'batch', NotifySMSManager.template_args['batch']['default'])) + + # Define our gateway + if 'gateway' in results['qsd'] and len(results['qsd']['gateway']): + results['gateway'] = \ + NotifySMSManager.unquote(results['qsd']['gateway']) + + return results diff --git a/libs/apprise/plugins/NotifySMTP2Go.py b/libs/apprise/plugins/NotifySMTP2Go.py index 45f6615cb..a34492d05 100644 --- a/libs/apprise/plugins/NotifySMTP2Go.py +++ b/libs/apprise/plugins/NotifySMTP2Go.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifySNS.py b/libs/apprise/plugins/NotifySNS.py index 5edac727c..5a287e37e 100644 --- a/libs/apprise/plugins/NotifySNS.py +++ b/libs/apprise/plugins/NotifySNS.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifySendGrid.py b/libs/apprise/plugins/NotifySendGrid.py index b7f4a8a6c..b82e3e60d 100644 --- a/libs/apprise/plugins/NotifySendGrid.py +++ b/libs/apprise/plugins/NotifySendGrid.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifyServerChan.py b/libs/apprise/plugins/NotifyServerChan.py index 87a294a39..cf250cf5b 100644 --- a/libs/apprise/plugins/NotifyServerChan.py +++ b/libs/apprise/plugins/NotifyServerChan.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifySignalAPI.py b/libs/apprise/plugins/NotifySignalAPI.py index a2a31de10..b35b4989e 100644 --- a/libs/apprise/plugins/NotifySignalAPI.py +++ b/libs/apprise/plugins/NotifySignalAPI.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifySimplePush.py b/libs/apprise/plugins/NotifySimplePush.py index d6bd2ab6b..3851e1e3c 100644 --- a/libs/apprise/plugins/NotifySimplePush.py +++ b/libs/apprise/plugins/NotifySimplePush.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifySinch.py b/libs/apprise/plugins/NotifySinch.py index b2c5683fa..74b3c452a 100644 --- a/libs/apprise/plugins/NotifySinch.py +++ b/libs/apprise/plugins/NotifySinch.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifySlack.py b/libs/apprise/plugins/NotifySlack.py index bbd2bf242..b66fe99f4 100644 --- a/libs/apprise/plugins/NotifySlack.py +++ b/libs/apprise/plugins/NotifySlack.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: @@ -96,6 +96,10 @@ SLACK_HTTP_ERROR_MAP = { # Used to break path apart into list of channels CHANNEL_LIST_DELIM = re.compile(r'[ \t\r\n,#\\/]+') +# Channel Regular Expression Parsing +CHANNEL_RE = re.compile( + r'^(?P[+#@]?[A-Z0-9_-]{1,32})(:(?P[0-9.]+))?$', re.I) + class SlackMode: """ @@ -547,39 +551,52 @@ class NotifySlack(NotifyBase): attach_channel_list = [] while len(channels): channel = channels.pop(0) - if channel is not None: - channel = validate_regex(channel, r'[+#@]?[A-Z0-9_]{1,32}') - if not channel: - # Channel over-ride was specified - self.logger.warning( - "The specified target {} is invalid;" - "skipping.".format(channel)) + # We'll perform a user lookup if we detect an email + email = is_email(channel) + if email: + payload['channel'] = \ + self.lookup_userid(email['full_email']) - # Mark our failure - has_error = True - continue + if not payload['channel']: + # Move along; any notifications/logging would have + # come from lookup_userid() + has_error = True + continue - if channel[0] == '+': - # Treat as encoded id if prefixed with a + - payload['channel'] = channel[1:] + else: # Channel + result = CHANNEL_RE.match(channel) - elif channel[0] == '@': - # Treat @ value 'as is' - payload['channel'] = channel + if not result: + # Channel over-ride was specified + self.logger.warning( + "The specified Slack target {} is invalid;" + "skipping.".format(channel)) - else: - # We'll perform a user lookup if we detect an email - email = is_email(channel) - if email: - payload['channel'] = \ - self.lookup_userid(email['full_email']) + # Mark our failure + has_error = True + continue + + # Store oure content + channel, thread_ts = \ + result.group('channel'), result.group('thread_ts') + if thread_ts: + payload['thread_ts'] = thread_ts + + elif 'thread_ts' in payload: + # Handle situations where one channel has a thread_id + # specified, and the next does not. We do not want to + # cary forward the last value specified + del payload['thread_ts'] + + if channel[0] == '+': + # Treat as encoded id if prefixed with a + + payload['channel'] = channel[1:] + + elif channel[0] == '@': + # Treat @ value 'as is' + payload['channel'] = channel - if not payload['channel']: - # Move along; any notifications/logging would have - # come from lookup_userid() - has_error = True - continue else: # Prefix with channel hash tag (if not already) payload['channel'] = \ @@ -795,7 +812,6 @@ class NotifySlack(NotifyBase): """ Wrapper to the requests (post) object """ - self.logger.debug('Slack POST URL: %s (cert_verify=%r)' % ( url, self.verify_certificate, )) diff --git a/libs/apprise/plugins/NotifySparkPost.py b/libs/apprise/plugins/NotifySparkPost.py index 282f55093..255db0709 100644 --- a/libs/apprise/plugins/NotifySparkPost.py +++ b/libs/apprise/plugins/NotifySparkPost.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifySpontit.py b/libs/apprise/plugins/NotifySpontit.py deleted file mode 100644 index 4705fc058..000000000 --- a/libs/apprise/plugins/NotifySpontit.py +++ /dev/null @@ -1,386 +0,0 @@ -# -*- coding: utf-8 -*- -# BSD 2-Clause License -# -# Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# 1. Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# -# 2. Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - -# To use this service you will need a Spontit account from their website -# at https://spontit.com/ -# -# After you have an account created: -# - Visit your profile at https://spontit.com/profile and take note of your -# {username}. It might look something like: user12345678901 -# - Next generate an API key at https://spontit.com/secret_keys. This will -# generate a very long alpha-numeric string we'll refer to as the -# {apikey} - -# The Spontit Syntax is as follows: -# spontit://{username}@{apikey} - -import re -import requests -from json import loads - -from .NotifyBase import NotifyBase -from ..common import NotifyType -from ..utils import parse_list -from ..utils import validate_regex -from ..AppriseLocale import gettext_lazy as _ - -# Syntax suggests you use a hashtag '#' to help distinguish we're dealing -# with a channel. -# Secondly we extract the user information only if it's -# specified. If not, we use the user of the person sending the notification -# Finally the channel identifier is detected -CHANNEL_REGEX = re.compile( - r'^\s*(\#|\%23)?((\@|\%40)?(?P[a-z0-9_]+)([/\\]|\%2F))?' - r'(?P[a-z0-9_-]+)\s*$', re.I) - - -class NotifySpontit(NotifyBase): - """ - A wrapper for Spontit Notifications - """ - - # The default descriptive name associated with the Notification - service_name = 'Spontit' - - # The services URL - service_url = 'https://spontit.com/' - - # All notification requests are secure - secure_protocol = 'spontit' - - # Allow 300 requests per minute. - # 60/300 = 0.2 - request_rate_per_sec = 0.20 - - # A URL that takes you to the setup/help of the specific protocol - setup_url = 'https://github.com/caronc/apprise/wiki/Notify_spontit' - - # Spontit single notification URL - notify_url = 'https://api.spontit.com/v3/push' - - # The maximum length of the body - body_maxlen = 5000 - - # The maximum length of the title - title_maxlen = 100 - - # If we don't have the specified min length, then we don't bother using - # the body directive - spontit_body_minlen = 100 - - # Subtitle support; this is the maximum allowed characters defined by - # the API page - spontit_subtitle_maxlen = 20 - - # Define object templates - templates = ( - '{schema}://{user}@{apikey}', - '{schema}://{user}@{apikey}/{targets}', - ) - - # Define our template tokens - template_tokens = dict(NotifyBase.template_tokens, **{ - 'user': { - 'name': _('User ID'), - 'type': 'string', - 'required': True, - 'regex': (r'^[a-z0-9_-]+$', 'i'), - }, - 'apikey': { - 'name': _('API Key'), - 'type': 'string', - 'required': True, - 'private': True, - 'regex': (r'^[a-z0-9]+$', 'i'), - }, - # Target Channel ID's - # If a slash is used; you must escape it - # If no slash is used; channel is presumed to be your own - 'target_channel': { - 'name': _('Target Channel ID'), - 'type': 'string', - 'prefix': '#', - 'regex': (r'^[0-9\s)(+-]+$', 'i'), - 'map_to': 'targets', - }, - 'targets': { - 'name': _('Targets'), - 'type': 'list:string', - }, - }) - - # Define our template arguments - template_args = dict(NotifyBase.template_args, **{ - 'to': { - 'alias_of': 'targets', - }, - 'subtitle': { - # Subtitle is available for MacOS users - 'name': _('Subtitle'), - 'type': 'string', - }, - }) - - def __init__(self, apikey, targets=None, subtitle=None, **kwargs): - """ - Initialize Spontit Object - """ - super().__init__(**kwargs) - - # User ID (associated with project) - user = validate_regex( - self.user, *self.template_tokens['user']['regex']) - if not user: - msg = 'An invalid Spontit User ID ' \ - '({}) was specified.'.format(self.user) - self.logger.warning(msg) - raise TypeError(msg) - # use cleaned up version - self.user = user - - # API Key (associated with project) - self.apikey = validate_regex( - apikey, *self.template_tokens['apikey']['regex']) - if not self.apikey: - msg = 'An invalid Spontit API Key ' \ - '({}) was specified.'.format(apikey) - self.logger.warning(msg) - raise TypeError(msg) - - # Save our subtitle information - self.subtitle = subtitle - - # Parse our targets - self.targets = list() - - for target in parse_list(targets): - # Validate targets and drop bad ones: - result = CHANNEL_REGEX.match(target) - if result: - # Just extract the channel - self.targets.append( - '{}'.format(result.group('channel'))) - continue - - self.logger.warning( - 'Dropped invalid channel/user ({}) specified.'.format(target)) - - return - - def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): - """ - Sends Message - """ - - # error tracking (used for function return) - has_error = False - - # Prepare our headers - headers = { - 'User-Agent': self.app_id, - 'Content-Type': 'application/json', - 'X-Authorization': self.apikey, - 'X-UserId': self.user, - } - - # use the list directly - targets = list(self.targets) - - if not len(targets): - # The user did not specify a channel and therefore wants to notify - # the main account only. We just set a substitute marker of - # None so that our while loop below can still process one iteration - targets = [None, ] - - while len(targets): - # Get our target(s) to notify - target = targets.pop(0) - - # Prepare our payload - payload = { - 'message': body, - } - - # Use our body directive if we exceed the minimum message - # limitation - if len(body) > self.spontit_body_minlen: - payload['message'] = '{}...'.format( - body[:self.spontit_body_minlen - 3]) - payload['body'] = body - - if self.subtitle: - # Set title if specified - payload['subtitle'] = \ - self.subtitle[:self.spontit_subtitle_maxlen] - - elif self.app_desc: - # fall back to app description - payload['subtitle'] = \ - self.app_desc[:self.spontit_subtitle_maxlen] - - elif self.app_id: - # fall back to app id - payload['subtitle'] = \ - self.app_id[:self.spontit_subtitle_maxlen] - - if title: - # Set title if specified - payload['pushTitle'] = title - - if target is not None: - payload['channelName'] = target - - # Some Debug Logging - self.logger.debug( - 'Spontit POST URL: {} (cert_verify={})'.format( - self.notify_url, self.verify_certificate)) - self.logger.debug('Spontit Payload: {}' .format(payload)) - - # Always call throttle before any remote server i/o is made - self.throttle() - try: - r = requests.post( - self.notify_url, - params=payload, - headers=headers, - verify=self.verify_certificate, - timeout=self.request_timeout, - ) - - if r.status_code not in ( - requests.codes.created, requests.codes.ok): - status_str = \ - NotifyBase.http_response_code_lookup( - r.status_code) - - try: - # Update our status response if we can - json_response = loads(r.content) - status_str = json_response.get('message', status_str) - - except (AttributeError, TypeError, ValueError): - # ValueError = r.content is Unparsable - # TypeError = r.content is None - # AttributeError = r is None - - # We could not parse JSON response. - # We will just use the status we already have. - pass - - self.logger.warning( - 'Failed to send Spontit notification to {}: ' - '{}{}error={}.'.format( - target, - status_str, - ', ' if status_str else '', - r.status_code)) - - self.logger.debug( - 'Response Details:\r\n{}'.format(r.content)) - - # Mark our failure - has_error = True - continue - - # If we reach here; the message was sent - self.logger.info( - 'Sent Spontit notification to {}.'.format(target)) - - self.logger.debug( - 'Response Details:\r\n{}'.format(r.content)) - - except requests.RequestException as e: - self.logger.warning( - 'A Connection error occurred sending Spontit:%s ' % ( - ', '.join(self.targets)) + 'notification.' - ) - self.logger.debug('Socket Exception: %s' % str(e)) - # Mark our failure - has_error = True - continue - - return not has_error - - def url(self, privacy=False, *args, **kwargs): - """ - Returns the URL built dynamically based on specified arguments. - """ - - # Our URL parameters - params = self.url_parameters(privacy=privacy, *args, **kwargs) - - if self.subtitle: - params['subtitle'] = self.subtitle - - return '{schema}://{userid}@{apikey}/{targets}?{params}'.format( - schema=self.secure_protocol, - userid=self.user, - apikey=self.pprint(self.apikey, privacy, safe=''), - targets='/'.join( - [NotifySpontit.quote(x, safe='') for x in self.targets]), - params=NotifySpontit.urlencode(params)) - - def __len__(self): - """ - Returns the number of targets associated with this notification - """ - targets = len(self.targets) - return targets if targets > 0 else 1 - - @staticmethod - def parse_url(url): - """ - Parses the URL and returns enough arguments that can allow - us to re-instantiate this object. - - """ - - results = NotifyBase.parse_url(url, verify_host=False) - if not results: - # We're done early as we couldn't load the results - return results - - # Get our entries; split_path() looks after unquoting content for us - # by default - results['targets'] = NotifySpontit.split_path(results['fullpath']) - - # The hostname is our authentication key - results['apikey'] = NotifySpontit.unquote(results['host']) - - # Support MacOS subtitle option - if 'subtitle' in results['qsd'] and len(results['qsd']['subtitle']): - results['subtitle'] = \ - NotifySpontit.unquote(results['qsd']['subtitle']) - - # Support the 'to' variable so that we can support targets this way too - # The 'to' makes it easier to use yaml configuration - if 'to' in results['qsd'] and len(results['qsd']['to']): - results['targets'] += \ - NotifySpontit.parse_list(results['qsd']['to']) - - return results diff --git a/libs/apprise/plugins/NotifyStreamlabs.py b/libs/apprise/plugins/NotifyStreamlabs.py index 56b577e49..d1e4186a6 100644 --- a/libs/apprise/plugins/NotifyStreamlabs.py +++ b/libs/apprise/plugins/NotifyStreamlabs.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifySynology.py b/libs/apprise/plugins/NotifySynology.py new file mode 100644 index 000000000..be58c0643 --- /dev/null +++ b/libs/apprise/plugins/NotifySynology.py @@ -0,0 +1,338 @@ +# -*- coding: utf-8 -*- +# BSD 2-Clause License +# +# Apprise - Push Notification Library. +# Copyright (c) 2024, Chris Caron +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +import requests +from json import dumps + +from .NotifyBase import NotifyBase +from ..URLBase import PrivacyMode +from ..common import NotifyType +from ..AppriseLocale import gettext_lazy as _ + +# For API Details see: +# https://kb.synology.com/en-au/DSM/help/Chat/chat_integration + + +class NotifySynology(NotifyBase): + """ + A wrapper for Synology Chat Notifications + """ + + # The default descriptive name associated with the Notification + service_name = 'Synology Chat' + + # The services URL + service_url = 'https://www.synology.com/' + + # The default protocol + protocol = 'synology' + + # The default secure protocol + secure_protocol = 'synologys' + + # A URL that takes you to the setup/help of the specific protocol + setup_url = 'https://github.com/caronc/apprise/wiki/Notify_synology_chat' + + # Title is to be part of body + title_maxlen = 0 + + # Disable throttle rate for Synology requests since they are normally + # local anyway + request_rate_per_sec = 0 + + # Define object templates + templates = ( + '{schema}://{host}/{token}', + '{schema}://{host}:{port}/{token}', + '{schema}://{user}@{host}/{token}', + '{schema}://{user}@{host}:{port}/{token}', + '{schema}://{user}:{password}@{host}/{token}', + '{schema}://{user}:{password}@{host}:{port}/{token}', + ) + + # Define our tokens; these are the minimum tokens required required to + # be passed into this function (as arguments). The syntax appends any + # previously defined in the base package and builds onto them + template_tokens = dict(NotifyBase.template_tokens, **{ + 'host': { + 'name': _('Hostname'), + 'type': 'string', + 'required': True, + }, + 'port': { + 'name': _('Port'), + 'type': 'int', + 'min': 1, + 'max': 65535, + }, + 'user': { + 'name': _('Username'), + 'type': 'string', + }, + 'password': { + 'name': _('Password'), + 'type': 'string', + 'private': True, + }, + 'token': { + 'name': _('Token'), + 'type': 'string', + 'required': True, + 'private': True, + }, + }) + + # Define our template arguments + template_args = dict(NotifyBase.template_args, **{ + 'file_url': { + 'name': _('Upload'), + 'type': 'string', + }, + 'token': { + 'alias_of': 'token', + }, + }) + + # Define any kwargs we're using + template_kwargs = { + 'headers': { + 'name': _('HTTP Header'), + 'prefix': '+', + }, + } + + def __init__(self, token=None, headers=None, file_url=None, **kwargs): + """ + Initialize Synology Chat Object + + headers can be a dictionary of key/value pairs that you want to + additionally include as part of the server headers to post with + + """ + super().__init__(**kwargs) + + self.token = token + if not self.token: + msg = 'An invalid Synology Token ' \ + '({}) was specified.'.format(token) + self.logger.warning(msg) + raise TypeError(msg) + + self.fullpath = kwargs.get('fullpath') + + # A URL to an attachment you want to upload (must be less then 32MB + # Acording to API details (at the time of writing plugin) + self.file_url = file_url + + self.headers = {} + if headers: + # Store our extra headers + self.headers.update(headers) + + return + + def url(self, privacy=False, *args, **kwargs): + """ + Returns the URL built dynamically based on specified arguments. + """ + + # Define any URL parameters + params = {} + + if self.file_url: + params['file_url'] = self.file_url + + # Extend our parameters + params.update(self.url_parameters(privacy=privacy, *args, **kwargs)) + + # Append our headers into our parameters + params.update({'+{}'.format(k): v for k, v in self.headers.items()}) + + # Determine Authentication + auth = '' + if self.user and self.password: + auth = '{user}:{password}@'.format( + user=NotifySynology.quote(self.user, safe=''), + password=self.pprint( + self.password, privacy, mode=PrivacyMode.Secret, safe=''), + ) + elif self.user: + auth = '{user}@'.format( + user=NotifySynology.quote(self.user, safe=''), + ) + + default_port = 443 if self.secure else 80 + + return '{schema}://{auth}{hostname}{port}/{token}' \ + '{fullpath}?{params}'.format( + schema=self.secure_protocol if self.secure else self.protocol, + auth=auth, + # never encode hostname since we're expecting it to be a valid + # one + hostname=self.host, + port='' if self.port is None or self.port == default_port + else ':{}'.format(self.port), + token=self.pprint(self.token, privacy, safe=''), + fullpath=NotifySynology.quote(self.fullpath, safe='/') + if self.fullpath else '/', + params=NotifySynology.urlencode(params), + ) + + def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): + """ + Perform Synology Chat Notification + """ + + # Prepare HTTP Headers + headers = { + 'User-Agent': self.app_id, + 'Content-Type': 'application/x-www-form-urlencoded', + 'Accept': '*/*', + } + + # Apply any/all header over-rides defined + headers.update(self.headers) + + # prepare Synology Object + payload = { + 'text': body, + } + + if self.file_url: + payload['file_url'] = self.file_url + + # Prepare our parameters + params = { + 'api': 'SYNO.Chat.External', + 'method': 'incoming', + 'version': 2, + 'token': self.token, + } + + auth = None + if self.user: + auth = (self.user, self.password) + + # Set our schema + schema = 'https' if self.secure else 'http' + + url = '%s://%s' % (schema, self.host) + if isinstance(self.port, int): + url += ':%d' % self.port + + # Prepare our Synology API URL + url += self.fullpath + '/webapi/entry.cgi' + + self.logger.debug('Synology Chat POST URL: %s (cert_verify=%r)' % ( + url, self.verify_certificate, + )) + self.logger.debug('Synology Chat Payload: %s' % str(payload)) + + # Always call throttle before any remote server i/o is made + self.throttle() + + try: + r = requests.post( + url, + data=f"payload={dumps(payload)}", + params=params, + headers=headers, + auth=auth, + verify=self.verify_certificate, + timeout=self.request_timeout, + ) + if r.status_code < 200 or r.status_code >= 300: + # We had a problem + status_str = \ + NotifySynology.http_response_code_lookup(r.status_code) + + self.logger.warning( + 'Failed to send Synology Chat %s notification: ' + '%serror=%s.', + status_str, + ', ' if status_str else '', + str(r.status_code)) + + self.logger.debug('Response Details:\r\n{}'.format(r.content)) + + # Return; we're done + return False + + else: + self.logger.info('Sent Synology Chat notification.') + + except requests.RequestException as e: + self.logger.warning( + 'A Connection error occurred sending Synology ' + 'Chat notification to %s.' % self.host) + self.logger.debug('Socket Exception: %s' % str(e)) + + # Return; we're done + return False + + return True + + @staticmethod + def parse_url(url): + """ + Parses the URL and returns enough arguments that can allow + us to re-instantiate this object. + + """ + results = NotifyBase.parse_url(url) + if not results: + # We're done early as we couldn't load the results + return results + + # Add our headers that the user can potentially over-ride if they wish + # to to our returned result set and tidy entries by unquoting them + results['headers'] = { + NotifySynology.unquote(x): NotifySynology.unquote(y) + for x, y in results['qsd+'].items()} + + # Set our token if found as an argument + if 'token' in results['qsd'] and len(results['qsd']['token']): + results['token'] = NotifySynology.unquote(results['qsd']['token']) + + else: + # Get unquoted entries + entries = NotifySynology.split_path(results['fullpath']) + if entries: + # Pop the first element + results['token'] = entries.pop(0) + + # Update our fullpath to not include our token + results['fullpath'] = \ + results['fullpath'][len(results['token']) + 1:] + + # Set upload/file_url if not otherwise set + if 'file_url' in results['qsd'] and len(results['qsd']['file_url']): + results['file_url'] = \ + NotifySynology.unquote(results['qsd']['file_url']) + + return results diff --git a/libs/apprise/plugins/NotifySyslog.py b/libs/apprise/plugins/NotifySyslog.py index 3ff1f2576..5540fc758 100644 --- a/libs/apprise/plugins/NotifySyslog.py +++ b/libs/apprise/plugins/NotifySyslog.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifyTechulusPush.py b/libs/apprise/plugins/NotifyTechulusPush.py index 3e2085c53..4d0b99348 100644 --- a/libs/apprise/plugins/NotifyTechulusPush.py +++ b/libs/apprise/plugins/NotifyTechulusPush.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifyTelegram.py b/libs/apprise/plugins/NotifyTelegram.py index 1727fe87d..03bc36c6f 100644 --- a/libs/apprise/plugins/NotifyTelegram.py +++ b/libs/apprise/plugins/NotifyTelegram.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: @@ -74,8 +74,10 @@ TELEGRAM_IMAGE_XY = NotifyImageSize.XY_256 # Chat ID is required # If the Chat ID is positive, then it's addressed to a single person # If the Chat ID is negative, then it's targeting a group +# We can support :topic (an integer) if specified as well IS_CHAT_ID_RE = re.compile( - r'^(@*(?P-?[0-9]{1,32})|(?P[a-z_-][a-z0-9_-]+))$', + r'^((?P-?[0-9]{1,32})|(@|%40)?(?P[a-z_-][a-z0-9_-]+))' + r'((:|%3A)(?P[0-9]+))?$', re.IGNORECASE, ) @@ -360,9 +362,6 @@ class NotifyTelegram(NotifyBase): self.logger.warning(err) raise TypeError(err) - # Parse our list - self.targets = parse_list(targets) - # Define whether or not we should make audible alarms self.silent = self.template_args['silent']['default'] \ if silent is None else bool(silent) @@ -403,15 +402,41 @@ class NotifyTelegram(NotifyBase): # URL later to directly include the user that we should message. self.detect_owner = detect_owner - if self.user: - # Treat this as a channel too - self.targets.append(self.user) + # Parse our list + self.targets = [] + for target in parse_list(targets): + results = IS_CHAT_ID_RE.match(target) + if not results: + self.logger.warning( + 'Dropped invalid Telegram chat/group ({}) specified.' + .format(target), + ) + + # Ensure we don't fall back to owner detection + self.detect_owner = False + continue + + try: + topic = int( + results.group('topic') + if results.group('topic') else self.topic) + + except TypeError: + # No worries + topic = None + + if results.group('name') is not None: + # Name + self.targets.append(('@%s' % results.group('name'), topic)) + + else: # ID + self.targets.append((int(results.group('idno')), topic)) # Track whether or not we want to send an image with our notification # or not. self.include_image = include_image - def send_media(self, chat_id, notify_type, attach=None): + def send_media(self, target, notify_type, attach=None): """ Sends a sticker based on the specified notify type @@ -470,9 +495,12 @@ class NotifyTelegram(NotifyBase): # content can arrive together. self.throttle() + # Extract our target + chat_id, topic = target + payload = {'chat_id': chat_id} - if self.topic: - payload['message_thread_id'] = self.topic + if topic: + payload['message_thread_id'] = topic try: with open(path, 'rb') as f: @@ -658,7 +686,7 @@ class NotifyTelegram(NotifyBase): _id = self.detect_bot_owner() if _id: # Permanently store our id in our target list for next time - self.targets.append(str(_id)) + self.targets.append((str(_id), None)) self.logger.info( 'Update your Telegram Apprise URL to read: ' '{}'.format(self.url(privacy=True))) @@ -681,26 +709,23 @@ class NotifyTelegram(NotifyBase): 'sendMessage' ) - payload = { + _payload = { # Notification Audible Control 'disable_notification': self.silent, # Display Web Page Preview (if possible) 'disable_web_page_preview': not self.preview, } - if self.topic: - payload['message_thread_id'] = self.topic - # Prepare Message Body if self.notify_format == NotifyFormat.MARKDOWN: - payload['parse_mode'] = 'MARKDOWN' + _payload['parse_mode'] = 'MARKDOWN' - payload['text'] = body + _payload['text'] = body else: # HTML # Use Telegram's HTML mode - payload['parse_mode'] = 'HTML' + _payload['parse_mode'] = 'HTML' for r, v, m in self.__telegram_escape_html_entries: if 'html' in m: @@ -712,7 +737,7 @@ class NotifyTelegram(NotifyBase): body = r.sub(v, body) # Prepare our payload based on HTML or TEXT - payload['text'] = body + _payload['text'] = body # Handle payloads without a body specified (but an attachment present) attach_content = \ @@ -721,41 +746,31 @@ class NotifyTelegram(NotifyBase): # Create a copy of the chat_ids list targets = list(self.targets) while len(targets): - chat_id = targets.pop(0) - chat_id = IS_CHAT_ID_RE.match(chat_id) - if not chat_id: - self.logger.warning( - "The specified chat_id '%s' is invalid; skipping." % ( - chat_id, - ) - ) + target = targets.pop(0) + chat_id, topic = target - # Flag our error - has_error = True - continue + # Printable chat_id details + pchat_id = f'{chat_id}' if not topic else f'{chat_id}:{topic}' - if chat_id.group('name') is not None: - # Name - payload['chat_id'] = '@%s' % chat_id.group('name') - - else: - # ID - payload['chat_id'] = int(chat_id.group('idno')) + payload = _payload.copy() + payload['chat_id'] = chat_id + if topic: + payload['message_thread_id'] = topic if self.include_image is True: # Define our path - if not self.send_media(payload['chat_id'], notify_type): + if not self.send_media(target, notify_type): # We failed to send the image associated with our notify_type self.logger.warning( 'Failed to send Telegram type image to {}.', - payload['chat_id']) + pchat_id) if attach and self.attachment_support and \ attach_content == TelegramContentPlacement.AFTER: # Send our attachments now (if specified and if it exists) if not self._send_attachments( - chat_id=payload['chat_id'], notify_type=notify_type, + target, notify_type=notify_type, attach=attach): has_error = True @@ -803,7 +818,7 @@ class NotifyTelegram(NotifyBase): self.logger.warning( 'Failed to send Telegram notification to {}: ' '{}, error={}.'.format( - payload['chat_id'], + pchat_id, error_msg if error_msg else status_str, r.status_code)) @@ -817,7 +832,7 @@ class NotifyTelegram(NotifyBase): except requests.RequestException as e: self.logger.warning( 'A connection error occurred sending Telegram:%s ' % ( - payload['chat_id']) + 'notification.' + pchat_id) + 'notification.' ) self.logger.debug('Socket Exception: %s' % str(e)) @@ -833,7 +848,7 @@ class NotifyTelegram(NotifyBase): # it was identified to send the content before the attachments # which is now done. if not self._send_attachments( - chat_id=payload['chat_id'], + target=target, notify_type=notify_type, attach=attach): @@ -842,14 +857,14 @@ class NotifyTelegram(NotifyBase): return not has_error - def _send_attachments(self, chat_id, notify_type, attach): + def _send_attachments(self, target, notify_type, attach): """ Sends our attachments """ has_error = False # Send our attachments now (if specified and if it exists) for attachment in attach: - if not self.send_media(chat_id, notify_type, attach=attachment): + if not self.send_media(target, notify_type, attach=attachment): # We failed; don't continue has_error = True @@ -880,13 +895,21 @@ class NotifyTelegram(NotifyBase): # Extend our parameters params.update(self.url_parameters(privacy=privacy, *args, **kwargs)) + targets = [] + for (chat_id, _topic) in self.targets: + topic = _topic if _topic else self.topic + + targets.append(''.join( + [NotifyTelegram.quote(f'{chat_id}', safe='@') + if isinstance(chat_id, str) else f'{chat_id}', + '' if not topic else f':{topic}'])) + # No need to check the user token because the user automatically gets # appended into the list of chat ids return '{schema}://{bot_token}/{targets}/?{params}'.format( schema=self.secure_protocol, bot_token=self.pprint(self.bot_token, privacy, safe=''), - targets='/'.join( - [NotifyTelegram.quote('@{}'.format(x)) for x in self.targets]), + targets='/'.join(targets), params=NotifyTelegram.urlencode(params)) def __len__(self): @@ -987,6 +1010,7 @@ class NotifyTelegram(NotifyBase): # Include images with our message results['detect_owner'] = \ - parse_bool(results['qsd'].get('detect', True)) + parse_bool( + results['qsd'].get('detect', not results['targets'])) return results diff --git a/libs/apprise/plugins/NotifyThreema.py b/libs/apprise/plugins/NotifyThreema.py new file mode 100644 index 000000000..c2ad82e2e --- /dev/null +++ b/libs/apprise/plugins/NotifyThreema.py @@ -0,0 +1,370 @@ +# -*- coding: utf-8 -*- +# BSD 2-Clause License +# +# Apprise - Push Notification Library. +# Copyright (c) 2024, Chris Caron +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +# Create an account https://gateway.threema.ch/en/ if you don't already have +# one +# +# Read more about Threema Gateway API here: +# - https://gateway.threema.ch/en/developer/api + +import requests +from itertools import chain + +from .NotifyBase import NotifyBase +from ..common import NotifyType +from ..utils import is_phone_no +from ..utils import validate_regex +from ..utils import is_email +from ..URLBase import PrivacyMode +from ..utils import parse_list +from ..AppriseLocale import gettext_lazy as _ + + +class ThreemaRecipientTypes: + """ + The supported recipient specifiers + """ + THREEMA_ID = 'to' + PHONE = 'phone' + EMAIL = 'email' + + +class NotifyThreema(NotifyBase): + """ + A wrapper for Threema Gateway Notifications + """ + + # The default descriptive name associated with the Notification + service_name = 'Threema Gateway' + + # The services URL + service_url = 'https://gateway.threema.ch/' + + # The default protocol + secure_protocol = 'threema' + + # A URL that takes you to the setup/help of the specific protocol + setup_url = 'https://github.com/caronc/apprise/wiki/Notify_threema' + + # Threema Gateway uses the http protocol with JSON requests + notify_url = 'https://msgapi.threema.ch/send_simple' + + # The maximum length of the body + body_maxlen = 3500 + + # No title support + title_maxlen = 0 + + # Define object templates + templates = ( + '{schema}://{gateway_id}@{secret}/{targets}', + ) + + # Define our template tokens + template_tokens = dict(NotifyBase.template_tokens, **{ + 'gateway_id': { + 'name': _('Gateway ID'), + 'type': 'string', + 'private': True, + 'required': True, + 'map_to': 'user', + }, + 'secret': { + 'name': _('API Secret'), + 'type': 'string', + 'private': True, + 'required': True, + }, + 'target_phone': { + 'name': _('Target Phone No'), + 'type': 'string', + 'prefix': '+', + 'regex': (r'^[0-9\s)(+-]+$', 'i'), + 'map_to': 'targets', + }, + 'target_email': { + 'name': _('Target Email'), + 'type': 'string', + 'map_to': 'targets', + }, + 'target_threema_id': { + 'name': _('Target Threema ID'), + 'type': 'string', + 'map_to': 'targets', + }, + 'targets': { + 'name': _('Targets'), + 'type': 'list:string', + 'required': True, + }, + }) + + # Define our template arguments + template_args = dict(NotifyBase.template_args, **{ + 'to': { + 'alias_of': 'targets', + }, + 'from': { + 'alias_of': 'gateway_id', + }, + 'gwid': { + 'alias_of': 'gateway_id', + }, + 'secret': { + 'alias_of': 'secret', + }, + }) + + def __init__(self, secret=None, targets=None, **kwargs): + """ + Initialize Threema Gateway Object + """ + super().__init__(**kwargs) + + # Validate our params here. + + if not self.user: + msg = 'Threema Gateway ID must be specified' + self.logger.warning(msg) + raise TypeError(msg) + + # Verify our Gateway ID + if len(self.user) != 8: + msg = 'Threema Gateway ID must be 8 characters in length' + self.logger.warning(msg) + raise TypeError(msg) + + # Verify our secret + self.secret = validate_regex(secret) + if not self.secret: + msg = \ + 'An invalid Threema API Secret ({}) was specified'.format( + secret) + self.logger.warning(msg) + raise TypeError(msg) + + # Parse our targets + self.targets = list() + + # Used for URL generation afterwards only + self.invalid_targets = list() + + for target in parse_list(targets, allow_whitespace=False): + if len(target) == 8: + # Store our user + self.targets.append( + (ThreemaRecipientTypes.THREEMA_ID, target)) + continue + + # Check if an email was defined + result = is_email(target) + if result: + # Store our user + self.targets.append( + (ThreemaRecipientTypes.EMAIL, result['full_email'])) + continue + + # Validate targets and drop bad ones: + result = is_phone_no(target) + if result: + # store valid phone number + self.targets.append(( + ThreemaRecipientTypes.PHONE, result['full'])) + continue + + self.logger.warning( + 'Dropped invalid user/email/phone ' + '({}) specified'.format(target), + ) + self.invalid_targets.append(target) + + return + + def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): + """ + Perform Threema Gateway Notification + """ + + if len(self.targets) == 0: + # There were no services to notify + self.logger.warning( + 'There were no Threema Gateway targets to notify') + return False + + # error tracking (used for function return) + has_error = False + + # Prepare our headers + headers = { + 'User-Agent': self.app_id, + 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8', + 'Accept': '*/*', + } + + # Prepare our payload + _payload = { + 'secret': self.secret, + 'from': self.user, + 'text': body.encode('utf-8'), + } + + # Create a copy of the targets list + targets = list(self.targets) + + while len(targets): + # Get our target to notify + key, target = targets.pop(0) + + # Prepare a payload object + payload = _payload.copy() + + # Set Target + payload[key] = target + + # Some Debug Logging + self.logger.debug( + 'Threema Gateway GET URL: {} (cert_verify={})'.format( + self.notify_url, self.verify_certificate)) + self.logger.debug('Threema Gateway Payload: {}' .format(payload)) + + # Always call throttle before any remote server i/o is made + self.throttle() + + try: + r = requests.post( + self.notify_url, + params=payload, + headers=headers, + verify=self.verify_certificate, + timeout=self.request_timeout, + ) + + if r.status_code != requests.codes.ok: + # We had a problem + status_str = \ + NotifyThreema.http_response_code_lookup( + r.status_code) + + self.logger.warning( + 'Failed to send Threema Gateway notification to {}: ' + '{}{}error={}'.format( + target, + status_str, + ', ' if status_str else '', + r.status_code)) + + self.logger.debug( + 'Response Details:\r\n{}'.format(r.content)) + + # Mark our failure + has_error = True + continue + + # We wee successful + self.logger.info( + 'Sent Threema Gateway notification to %s' % target) + + except requests.RequestException as e: + self.logger.warning( + 'A Connection error occurred sending Threema Gateway:%s ' + 'notification' % target + ) + self.logger.debug('Socket Exception: %s' % str(e)) + + # Mark our failure + has_error = True + continue + + return not has_error + + def url(self, privacy=False, *args, **kwargs): + """ + Returns the URL built dynamically based on specified arguments. + """ + + # Define any URL parameters + params = self.url_parameters(privacy=privacy, *args, **kwargs) + + schemaStr = \ + '{schema}://{gatewayid}@{secret}/{targets}?{params}' + return schemaStr.format( + schema=self.secure_protocol, + gatewayid=NotifyThreema.quote(self.user), + secret=self.pprint( + self.secret, privacy, mode=PrivacyMode.Secret, safe=''), + targets='/'.join(chain( + [NotifyThreema.quote(x[1], safe='@+') for x in self.targets], + [NotifyThreema.quote(x, safe='@+') + for x in self.invalid_targets])), + params=NotifyThreema.urlencode(params)) + + def __len__(self): + """ + Returns the number of targets associated with this notification + """ + targets = len(self.targets) + return targets if targets > 0 else 1 + + @staticmethod + def parse_url(url): + """ + Parses the URL and returns enough arguments that can allow + us to re-instantiate this object. + """ + + results = NotifyBase.parse_url(url, verify_host=False) + if not results: + # We're done early as we couldn't load the results + return results + + results['targets'] = list() + + if 'secret' in results['qsd'] and len(results['qsd']['secret']): + results['secret'] = \ + NotifyThreema.unquote(results['qsd']['secret']) + + else: + results['secret'] = NotifyThreema.unquote(results['host']) + + results['targets'] += \ + NotifyThreema.split_path(results['fullpath']) + + if 'from' in results['qsd'] and len(results['qsd']['from']): + results['user'] = \ + NotifyThreema.unquote(results['qsd']['from']) + + elif 'gwid' in results['qsd'] and len(results['qsd']['gwid']): + results['user'] = \ + NotifyThreema.unquote(results['qsd']['gwid']) + + if 'to' in results['qsd'] and len(results['qsd']['to']): + results['targets'] += \ + NotifyThreema.parse_list( + results['qsd']['to'], allow_whitespace=False) + + return results diff --git a/libs/apprise/plugins/NotifyTwilio.py b/libs/apprise/plugins/NotifyTwilio.py index ab4c88e32..863b09a94 100644 --- a/libs/apprise/plugins/NotifyTwilio.py +++ b/libs/apprise/plugins/NotifyTwilio.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifyTwist.py b/libs/apprise/plugins/NotifyTwist.py index 36a55313a..fa26feb84 100644 --- a/libs/apprise/plugins/NotifyTwist.py +++ b/libs/apprise/plugins/NotifyTwist.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifyTwitter.py b/libs/apprise/plugins/NotifyTwitter.py index 3647c8b39..7a0813c1f 100644 --- a/libs/apprise/plugins/NotifyTwitter.py +++ b/libs/apprise/plugins/NotifyTwitter.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifyVoipms.py b/libs/apprise/plugins/NotifyVoipms.py index c39da4dfe..a023589a7 100644 --- a/libs/apprise/plugins/NotifyVoipms.py +++ b/libs/apprise/plugins/NotifyVoipms.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifyVonage.py b/libs/apprise/plugins/NotifyVonage.py index 48d823195..e9b1422ad 100644 --- a/libs/apprise/plugins/NotifyVonage.py +++ b/libs/apprise/plugins/NotifyVonage.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifyWeComBot.py b/libs/apprise/plugins/NotifyWeComBot.py new file mode 100644 index 000000000..4289b39e9 --- /dev/null +++ b/libs/apprise/plugins/NotifyWeComBot.py @@ -0,0 +1,258 @@ +# -*- coding: utf-8 -*- +# BSD 2-Clause License +# +# Apprise - Push Notification Library. +# Copyright (c) 2024, Chris Caron +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +# WeCom for PC +# 1. On WeCom for PC, find the target WeCom group for receiving alarm +# notifications. +# 2. Right-click the WeCom group. In the window that appears, click +# "Add Group Bot". +# 3. In the window that appears, click Create a Bot. +# 4. In the window that appears, enter a custom bot name and click Add. +# 5. You will be provided a Webhook URL that looks like: +# https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=abcd +# +# WeCom for Web +# 1. On WebCom for Web, open the target WeCom group for receiving alarm +# notifications. +# 2. Click the group settings icon in the upper-right corner. +# 3. On the group settings page, choose Group Bots > Add a Bot. +# 4. On the management page for adding bots, enter a custom name for the new +# bot. +# 5. Click Add, copy the webhook address, and configure the API callback by +# following Step 2. + +# the URL will look something like this: +# https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=abcd +# ^ +# | +# webhook key +# +# This plugin also supports taking the URL (as identified above) directly +# as well. + +import re +import requests +from json import dumps + +from .NotifyBase import NotifyBase +from ..common import NotifyType +from ..utils import validate_regex +from ..AppriseLocale import gettext_lazy as _ + + +class NotifyWeComBot(NotifyBase): + """ + A wrapper for WeCom Bot Notifications + """ + + # The default descriptive name associated with the Notification + service_name = 'WeCom Bot' + + # The services URL + service_url = 'https://weixin.qq.com/' + + # The default secure protocol + secure_protocol = 'wecombot' + + # A URL that takes you to the setup/help of the specific protocol + setup_url = 'https://github.com/caronc/apprise/wiki/Notify_wecombot' + + # Plain Text Notification URL + notify_url = 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key={key}' + + # Define object templates + templates = ( + '{schema}://{key}', + ) + + # The title is not used + title_maxlen = 0 + + # Define our template arguments + template_tokens = dict(NotifyBase.template_tokens, **{ + # The Bot Key can be found at the end of the webhook provided (?key=) + 'key': { + 'name': _('Bot Webhook Key'), + 'type': 'string', + 'required': True, + 'private': True, + 'regex': (r'^[a-z0-9_-]+$', 'i'), + }, + }) + + # Define our template arguments + template_args = dict(NotifyBase.template_args, **{ + # You can optionally pass IRC colors into + 'key': { + 'alias_of': 'key', + }, + }) + + def __init__(self, key, **kwargs): + """ + Initialize WeCom Bot Object + """ + super().__init__(**kwargs) + + # Assign our bot webhook + self.key = validate_regex( + key, *self.template_tokens['key']['regex']) + if not self.key: + msg = 'An invalid WeCom Bot Webhook Key ' \ + '({}) was specified.'.format(key) + self.logger.warning(msg) + raise TypeError(msg) + + # Prepare our notification URL now: + self.api_url = self.notify_url.format( + key=self.key, + ) + return + + def url(self, privacy=False, *args, **kwargs): + """ + Returns the URL built dynamically based on specified arguments. + """ + + # Prepare our parameters + params = self.url_parameters(privacy=privacy, *args, **kwargs) + + return '{schema}://{key}/?{params}'.format( + schema=self.secure_protocol, + key=self.pprint(self.key, privacy, safe=''), + params=NotifyWeComBot.urlencode(params), + ) + + def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): + """ + wrapper to _send since we can alert more then one channel + """ + + # prepare our headers + headers = { + 'User-Agent': self.app_id, + 'Content-Type': 'application/json; charset=utf-8', + } + + # Prepare our payload + payload = { + 'msgtype': "text", + 'text': { + 'content': body, + } + } + + self.logger.debug('WeCom Bot GET URL: %s (cert_verify=%r)' % ( + self.api_url, self.verify_certificate)) + self.logger.debug('WeCom Bot Payload: %s' % str(payload)) + + # Always call throttle before any remote server i/o is made + self.throttle() + + try: + r = requests.post( + self.api_url, + data=dumps(payload).encode('utf-8'), + headers=headers, + verify=self.verify_certificate, + timeout=self.request_timeout, + ) + if r.status_code != requests.codes.ok: + # We had a problem + status_str = \ + NotifyWeComBot.http_response_code_lookup(r.status_code) + + self.logger.warning( + 'Failed to send WeCom Bot notification: ' + '{}{}error={}.'.format( + status_str, + ', ' if status_str else '', + r.status_code)) + + self.logger.debug('Response Details:\r\n{}'.format(r.content)) + + # Return; we're done + return False + + else: + self.logger.info('Sent WeCom Bot notification.') + + except requests.RequestException as e: + self.logger.warning( + 'A Connection error occurred sending WeCom Bot ' + 'notification.') + self.logger.debug('Socket Exception: %s' % str(e)) + + # Return; we're done + return False + + return True + + @staticmethod + def parse_url(url): + """ + Parses the URL and returns enough arguments that can allow + us to re-instantiate this object. + + """ + + results = NotifyBase.parse_url(url, verify_host=False) + if not results: + # We're done early as we couldn't load the results + return results + + # The first token is stored in the hostname + results['key'] = NotifyWeComBot.unquote(results['host']) + + # The 'key' makes it easier to use yaml configuration + if 'key' in results['qsd'] and len(results['qsd']['key']): + results['key'] = \ + NotifyWeComBot.unquote(results['qsd']['key']) + + return results + + @staticmethod + def parse_native_url(url): + """ + Support https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=BOTKEY + """ + + result = re.match( + r'^https?://qyapi\.weixin\.qq\.com/cgi-bin/webhook/send/?\?key=' + r'(?P[A-Z0-9_-]+)/?' + r'&?(?P.+)?$', url, re.I) + + if result: + return NotifyWeComBot.parse_url( + '{schema}://{key}{params}'.format( + schema=NotifyWeComBot.secure_protocol, + key=result.group('key'), + params='' if not result.group('params') + else '?' + result.group('params'))) + + return None diff --git a/libs/apprise/plugins/NotifyWebexTeams.py b/libs/apprise/plugins/NotifyWebexTeams.py index 67ed4e4b8..c91864bad 100644 --- a/libs/apprise/plugins/NotifyWebexTeams.py +++ b/libs/apprise/plugins/NotifyWebexTeams.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifyWhatsApp.py b/libs/apprise/plugins/NotifyWhatsApp.py index efa90f89b..4ccbcbdaf 100644 --- a/libs/apprise/plugins/NotifyWhatsApp.py +++ b/libs/apprise/plugins/NotifyWhatsApp.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifyWindows.py b/libs/apprise/plugins/NotifyWindows.py index 226cf92bf..207e0f221 100644 --- a/libs/apprise/plugins/NotifyWindows.py +++ b/libs/apprise/plugins/NotifyWindows.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: @@ -26,9 +26,6 @@ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. -from __future__ import absolute_import -from __future__ import print_function - from time import sleep from .NotifyBase import NotifyBase diff --git a/libs/apprise/plugins/NotifyXBMC.py b/libs/apprise/plugins/NotifyXBMC.py index a973989ac..7d4462e41 100644 --- a/libs/apprise/plugins/NotifyXBMC.py +++ b/libs/apprise/plugins/NotifyXBMC.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifyXML.py b/libs/apprise/plugins/NotifyXML.py index 20eeb114c..21ccb79d3 100644 --- a/libs/apprise/plugins/NotifyXML.py +++ b/libs/apprise/plugins/NotifyXML.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifyZulip.py b/libs/apprise/plugins/NotifyZulip.py index f0d0cd8d5..66ffb9d1d 100644 --- a/libs/apprise/plugins/NotifyZulip.py +++ b/libs/apprise/plugins/NotifyZulip.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/__init__.py b/libs/apprise/plugins/__init__.py index 27afef05c..72cb08fbf 100644 --- a/libs/apprise/plugins/__init__.py +++ b/libs/apprise/plugins/__init__.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: @@ -27,12 +27,8 @@ # POSSIBILITY OF SUCH DAMAGE. import os -import re import copy -from os.path import dirname -from os.path import abspath - # Used for testing from .NotifyBase import NotifyBase @@ -40,13 +36,17 @@ from ..common import NotifyImageSize from ..common import NOTIFY_IMAGE_SIZES from ..common import NotifyType from ..common import NOTIFY_TYPES -from .. import common from ..utils import parse_list from ..utils import cwe312_url from ..utils import GET_SCHEMA_RE from ..logger import logger from ..AppriseLocale import gettext_lazy as _ from ..AppriseLocale import LazyTranslation +from ..NotificationManager import NotificationManager + + +# Grant access to our Notification Manager Singleton +N_MGR = NotificationManager() __all__ = [ # Reference @@ -58,101 +58,6 @@ __all__ = [ ] -# Load our Lookup Matrix -def __load_matrix(path=abspath(dirname(__file__)), name='apprise.plugins'): - """ - Dynamically load our schema map; this allows us to gracefully - skip over modules we simply don't have the dependencies for. - - """ - # Used for the detection of additional Notify Services objects - # The .py extension is optional as we support loading directories too - module_re = re.compile(r'^(?PNotify[a-z0-9]+)(\.py)?$', re.I) - - for f in os.listdir(path): - match = module_re.match(f) - if not match: - # keep going - continue - - # Store our notification/plugin name: - plugin_name = match.group('name') - try: - module = __import__( - '{}.{}'.format(name, plugin_name), - globals(), locals(), - fromlist=[plugin_name]) - - except ImportError: - # No problem, we can't use this object - continue - - if not hasattr(module, plugin_name): - # Not a library we can load as it doesn't follow the simple rule - # that the class must bear the same name as the notification - # file itself. - continue - - # Get our plugin - plugin = getattr(module, plugin_name) - if not hasattr(plugin, 'app_id'): - # Filter out non-notification modules - continue - - elif plugin_name in common.NOTIFY_MODULE_MAP: - # we're already handling this object - continue - - # Add our plugin name to our module map - common.NOTIFY_MODULE_MAP[plugin_name] = { - 'plugin': plugin, - 'module': module, - } - - # Add our module name to our __all__ - __all__.append(plugin_name) - - fn = getattr(plugin, 'schemas', None) - schemas = set([]) if not callable(fn) else fn(plugin) - - # map our schema to our plugin - for schema in schemas: - if schema in common.NOTIFY_SCHEMA_MAP: - logger.error( - "Notification schema ({}) mismatch detected - {} to {}" - .format(schema, common.NOTIFY_SCHEMA_MAP[schema], plugin)) - continue - - # Assign plugin - common.NOTIFY_SCHEMA_MAP[schema] = plugin - - return common.NOTIFY_SCHEMA_MAP - - -# Reset our Lookup Matrix -def __reset_matrix(): - """ - Restores the Lookup matrix to it's base setting. This is only used through - testing and should not be directly called. - """ - - # Reset our schema map - common.NOTIFY_SCHEMA_MAP.clear() - - # Iterate over our module map so we can clear out our __all__ and globals - for plugin_name in common.NOTIFY_MODULE_MAP.keys(): - - # Remove element from plugins - __all__.remove(plugin_name) - - # Clear out our module map - common.NOTIFY_MODULE_MAP.clear() - - -# Dynamically build our schema base -__load_matrix() - - def _sanitize_token(tokens, default_delimiter): """ This is called by the details() function and santizes the output by @@ -176,6 +81,10 @@ def _sanitize_token(tokens, default_delimiter): # Do not touch this field continue + elif 'name' not in tokens[key]: + # Default to key + tokens[key]['name'] = key + if 'map_to' not in tokens[key]: # Default type to key tokens[key]['map_to'] = key @@ -538,16 +447,16 @@ def url_to_dict(url, secure_logging=True): # Ensure our schema is always in lower case schema = schema.group('schema').lower() - if schema not in common.NOTIFY_SCHEMA_MAP: + if schema not in N_MGR: # Give the user the benefit of the doubt that the user may be using # one of the URLs provided to them by their notification service. # Before we fail for good, just scan all the plugins that support the # native_url() parse function - results = \ - next((r['plugin'].parse_native_url(_url) - for r in common.NOTIFY_MODULE_MAP.values() - if r['plugin'].parse_native_url(_url) is not None), - None) + results = None + for plugin in N_MGR.plugins(): + results = plugin.parse_native_url(_url) + if results: + break if not results: logger.error('Unparseable URL {}'.format(loggable_url)) @@ -560,14 +469,14 @@ def url_to_dict(url, secure_logging=True): else: # Parse our url details of the server object as dictionary # containing all of the information parsed from our URL - results = common.NOTIFY_SCHEMA_MAP[schema].parse_url(_url) + results = N_MGR[schema].parse_url(_url) if not results: logger.error('Unparseable {} URL {}'.format( - common.NOTIFY_SCHEMA_MAP[schema].service_name, loggable_url)) + N_MGR[schema].service_name, loggable_url)) return None logger.trace('{} URL {} unpacked as:{}{}'.format( - common.NOTIFY_SCHEMA_MAP[schema].service_name, url, + N_MGR[schema].service_name, url, os.linesep, os.linesep.join( ['{}="{}"'.format(k, v) for k, v in results.items()]))) diff --git a/libs/apprise/utils.py b/libs/apprise/utils.py index 8d644ce90..e1881f314 100644 --- a/libs/apprise/utils.py +++ b/libs/apprise/utils.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron +# Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: @@ -31,14 +31,12 @@ import sys import json import contextlib import os -import hashlib import locale from itertools import chain from os.path import expanduser from functools import reduce from . import common from .logger import logger - from urllib.parse import unquote from urllib.parse import quote from urllib.parse import urlparse @@ -70,16 +68,12 @@ def import_module(path, name): module = None logger.debug( - 'Custom module exception raised from %s (name=%s) %s', + 'Module exception raised from %s (name=%s) %s', path, name, str(e)) return module -# Hash of all paths previously scanned so we don't waste effort/overhead doing -# it again -PATHS_PREVIOUSLY_SCANNED = set() - # URL Indexing Table for returns via parse_url() # The below accepts and scans for: # - schema:// @@ -96,6 +90,9 @@ VALID_QUERY_RE = re.compile(r'^(?P.*[/\\])(?P[^/\\]+)?$') # This is useful when turning a string into a list STRING_DELIMITERS = r'[\[\]\;,\s]+' +# String Delimiters without the whitespace +STRING_DELIMITERS_NO_WS = r'[\[\]\;,]+' + # Pre-Escape content since we reference it so much ESCAPED_PATH_SEPARATOR = re.escape('\\/') ESCAPED_WIN_PATH_SEPARATOR = re.escape('\\') @@ -211,6 +208,22 @@ VALID_PYTHON_FILE_RE = re.compile(r'.+\.py(o|c)?$', re.IGNORECASE) REGEX_VALIDATE_LOOKUP = {} +class Singleton(type): + """ + Our Singleton MetaClass + """ + _instances = {} + + def __call__(cls, *args, **kwargs): + """ + instantiate our singleton meta entry + """ + if cls not in cls._instances: + # we have not every built an instance before. Build one now. + cls._instances[cls] = super().__call__(*args, **kwargs) + return cls._instances[cls] + + class TemplateType: """ Defines the different template types we can perform parsing on @@ -1116,7 +1129,7 @@ def urlencode(query, doseq=False, safe='', encoding=None, errors=None): errors=errors) -def parse_list(*args, cast=None): +def parse_list(*args, cast=None, allow_whitespace=True): """ Take a string list and break it into a delimited list of arguments. This funciton also supports @@ -1143,10 +1156,12 @@ def parse_list(*args, cast=None): arg = cast(arg) if isinstance(arg, str): - result += re.split(STRING_DELIMITERS, arg) + result += re.split( + STRING_DELIMITERS if allow_whitespace + else STRING_DELIMITERS_NO_WS, arg) elif isinstance(arg, (set, list, tuple)): - result += parse_list(*arg) + result += parse_list(*arg, allow_whitespace=allow_whitespace) # # filter() eliminates any empty entries @@ -1154,7 +1169,9 @@ def parse_list(*args, cast=None): # Since Python v3 returns a filter (iterator) whereas Python v2 returned # a list, we need to change it into a list object to remain compatible with # both distribution types. - return sorted([x for x in filter(bool, list(set(result)))]) + return sorted([x for x in filter(bool, list(set(result)))]) \ + if allow_whitespace else sorted( + [x.strip() for x in filter(bool, list(set(result))) if x.strip()]) def is_exclusive_match(logic, data, match_all=common.MATCH_ALL_TAG, @@ -1554,142 +1571,6 @@ def remove_suffix(value, suffix): return value[:-len(suffix)] if value.endswith(suffix) else value -def module_detection(paths, cache=True): - """ - Iterates over a defined path for apprise decorators to load such as - @notify. - - """ - - # A simple restriction that we don't allow periods in the filename at all - # so it can't be hidden (Linux OS's) and it won't conflict with Python - # path naming. This also prevents us from loading any python file that - # starts with an underscore or dash - # We allow __init__.py as well - module_re = re.compile( - r'^(?P[_a-z0-9][a-z0-9._-]+)?(\.py)?$', re.I) - - if isinstance(paths, str): - paths = [paths, ] - - if not paths or not isinstance(paths, (tuple, list)): - # We're done - return None - - def _import_module(path): - # Since our plugin name can conflict (as a module) with another - # we want to generate random strings to avoid steping on - # another's namespace - if not (path and VALID_PYTHON_FILE_RE.match(path)): - # Ignore file/module type - logger.trace('Plugin Scan: Skipping %s', path) - return None - - module_name = hashlib.sha1(path.encode('utf-8')).hexdigest() - module_pyname = "{prefix}.{name}".format( - prefix='apprise.custom.module', name=module_name) - - if module_pyname in common.NOTIFY_CUSTOM_MODULE_MAP: - # First clear out existing entries - for schema in common.\ - NOTIFY_CUSTOM_MODULE_MAP[module_pyname]['notify'] \ - .keys(): - # Remove any mapped modules to this file - del common.NOTIFY_SCHEMA_MAP[schema] - - # Reset - del common.NOTIFY_CUSTOM_MODULE_MAP[module_pyname] - - # Load our module - module = import_module(path, module_pyname) - if not module: - # No problem, we can't use this object - logger.warning('Failed to load custom module: %s', _path) - return None - - # Print our loaded modules if any - if module_pyname in common.NOTIFY_CUSTOM_MODULE_MAP: - logger.debug( - 'Loaded custom module: %s (name=%s)', - _path, module_name) - - for schema, meta in common.\ - NOTIFY_CUSTOM_MODULE_MAP[module_pyname]['notify']\ - .items(): - - logger.info('Loaded custom notification: %s://', schema) - else: - # The code reaches here if we successfully loaded the Python - # module but no hooks/triggers were found. So we can safely - # just remove/ignore this entry - del sys.modules[module_pyname] - return None - - # end of _import_module() - return None - - for _path in paths: - path = os.path.abspath(os.path.expanduser(_path)) - if (cache and path in PATHS_PREVIOUSLY_SCANNED) \ - or not os.path.exists(path): - # We're done as we've already scanned this - continue - - # Store our path as a way of hashing it has been handled - PATHS_PREVIOUSLY_SCANNED.add(path) - - if os.path.isdir(path) and not \ - os.path.isfile(os.path.join(path, '__init__.py')): - - logger.debug('Scanning for custom plugins in: %s', path) - for entry in os.listdir(path): - re_match = module_re.match(entry) - if not re_match: - # keep going - logger.trace('Plugin Scan: Ignoring %s', entry) - continue - - new_path = os.path.join(path, entry) - if os.path.isdir(new_path): - # Update our path - new_path = os.path.join(path, entry, '__init__.py') - if not os.path.isfile(new_path): - logger.trace( - 'Plugin Scan: Ignoring %s', - os.path.join(path, entry)) - continue - - if not cache or \ - (cache and new_path not in PATHS_PREVIOUSLY_SCANNED): - # Load our module - _import_module(new_path) - - # Add our subdir path - PATHS_PREVIOUSLY_SCANNED.add(new_path) - else: - if os.path.isdir(path): - # This logic is safe to apply because we already validated - # the directories state above; update our path - path = os.path.join(path, '__init__.py') - if cache and path in PATHS_PREVIOUSLY_SCANNED: - continue - - PATHS_PREVIOUSLY_SCANNED.add(path) - - # directly load as is - re_match = module_re.match(os.path.basename(path)) - # must be a match and must have a .py extension - if not re_match or not re_match.group(1): - # keep going - logger.trace('Plugin Scan: Ignoring %s', path) - continue - - # Load our module - _import_module(path) - - return None - - def dict_full_update(dict1, dict2): """ Takes 2 dictionaries (dict1 and dict2) that contain sub-dictionaries and diff --git a/libs/apscheduler/__init__.py b/libs/apscheduler/__init__.py index 968169a9d..651aba499 100644 --- a/libs/apscheduler/__init__.py +++ b/libs/apscheduler/__init__.py @@ -1,10 +1,15 @@ -from pkg_resources import get_distribution, DistributionNotFound +import sys +if sys.version_info >= (3, 8): + import importlib.metadata as importlib_metadata +else: + import importlib_metadata + try: - release = get_distribution('APScheduler').version.split('-')[0] -except DistributionNotFound: + release = importlib_metadata.version('APScheduler').split('-')[0] +except importlib_metadata.PackageNotFoundError: release = '3.5.0' version_info = tuple(int(x) if x.isdigit() else x for x in release.split('.')) version = __version__ = '.'.join(str(x) for x in version_info[:3]) -del get_distribution, DistributionNotFound +del sys, importlib_metadata diff --git a/libs/apscheduler/executors/asyncio.py b/libs/apscheduler/executors/asyncio.py index 06fc7f968..7d45d6c14 100644 --- a/libs/apscheduler/executors/asyncio.py +++ b/libs/apscheduler/executors/asyncio.py @@ -3,13 +3,9 @@ from __future__ import absolute_import import sys from apscheduler.executors.base import BaseExecutor, run_job +from apscheduler.executors.base_py3 import run_coroutine_job from apscheduler.util import iscoroutinefunction_partial -try: - from apscheduler.executors.base_py3 import run_coroutine_job -except ImportError: - run_coroutine_job = None - class AsyncIOExecutor(BaseExecutor): """ @@ -46,11 +42,8 @@ class AsyncIOExecutor(BaseExecutor): self._run_job_success(job.id, events) if iscoroutinefunction_partial(job.func): - if run_coroutine_job is not None: - coro = run_coroutine_job(job, job._jobstore_alias, run_times, self._logger.name) - f = self._eventloop.create_task(coro) - else: - raise Exception('Executing coroutine based jobs is not supported with Trollius') + coro = run_coroutine_job(job, job._jobstore_alias, run_times, self._logger.name) + f = self._eventloop.create_task(coro) else: f = self._eventloop.run_in_executor(None, run_job, job, job._jobstore_alias, run_times, self._logger.name) diff --git a/libs/apscheduler/jobstores/sqlalchemy.py b/libs/apscheduler/jobstores/sqlalchemy.py index dcfd3e565..716549bc8 100644 --- a/libs/apscheduler/jobstores/sqlalchemy.py +++ b/libs/apscheduler/jobstores/sqlalchemy.py @@ -57,7 +57,7 @@ class SQLAlchemyJobStore(BaseJobStore): # 25 = precision that translates to an 8-byte float self.jobs_t = Table( tablename, metadata, - Column('id', Unicode(191, _warn_on_bytestring=False), primary_key=True), + Column('id', Unicode(191), primary_key=True), Column('next_run_time', Float(25), index=True), Column('job_state', LargeBinary, nullable=False), schema=tableschema @@ -68,20 +68,22 @@ class SQLAlchemyJobStore(BaseJobStore): self.jobs_t.create(self.engine, True) def lookup_job(self, job_id): - selectable = select([self.jobs_t.c.job_state]).where(self.jobs_t.c.id == job_id) - job_state = self.engine.execute(selectable).scalar() - return self._reconstitute_job(job_state) if job_state else None + selectable = select(self.jobs_t.c.job_state).where(self.jobs_t.c.id == job_id) + with self.engine.begin() as connection: + job_state = connection.execute(selectable).scalar() + return self._reconstitute_job(job_state) if job_state else None def get_due_jobs(self, now): timestamp = datetime_to_utc_timestamp(now) return self._get_jobs(self.jobs_t.c.next_run_time <= timestamp) def get_next_run_time(self): - selectable = select([self.jobs_t.c.next_run_time]).\ + selectable = select(self.jobs_t.c.next_run_time).\ where(self.jobs_t.c.next_run_time != null()).\ order_by(self.jobs_t.c.next_run_time).limit(1) - next_run_time = self.engine.execute(selectable).scalar() - return utc_timestamp_to_datetime(next_run_time) + with self.engine.begin() as connection: + next_run_time = connection.execute(selectable).scalar() + return utc_timestamp_to_datetime(next_run_time) def get_all_jobs(self): jobs = self._get_jobs() @@ -94,29 +96,33 @@ class SQLAlchemyJobStore(BaseJobStore): 'next_run_time': datetime_to_utc_timestamp(job.next_run_time), 'job_state': pickle.dumps(job.__getstate__(), self.pickle_protocol) }) - try: - self.engine.execute(insert) - except IntegrityError: - raise ConflictingIdError(job.id) + with self.engine.begin() as connection: + try: + connection.execute(insert) + except IntegrityError: + raise ConflictingIdError(job.id) def update_job(self, job): update = self.jobs_t.update().values(**{ 'next_run_time': datetime_to_utc_timestamp(job.next_run_time), 'job_state': pickle.dumps(job.__getstate__(), self.pickle_protocol) }).where(self.jobs_t.c.id == job.id) - result = self.engine.execute(update) - if result.rowcount == 0: - raise JobLookupError(job.id) + with self.engine.begin() as connection: + result = connection.execute(update) + if result.rowcount == 0: + raise JobLookupError(job.id) def remove_job(self, job_id): delete = self.jobs_t.delete().where(self.jobs_t.c.id == job_id) - result = self.engine.execute(delete) - if result.rowcount == 0: - raise JobLookupError(job_id) + with self.engine.begin() as connection: + result = connection.execute(delete) + if result.rowcount == 0: + raise JobLookupError(job_id) def remove_all_jobs(self): delete = self.jobs_t.delete() - self.engine.execute(delete) + with self.engine.begin() as connection: + connection.execute(delete) def shutdown(self): self.engine.dispose() @@ -132,21 +138,22 @@ class SQLAlchemyJobStore(BaseJobStore): def _get_jobs(self, *conditions): jobs = [] - selectable = select([self.jobs_t.c.id, self.jobs_t.c.job_state]).\ + selectable = select(self.jobs_t.c.id, self.jobs_t.c.job_state).\ order_by(self.jobs_t.c.next_run_time) selectable = selectable.where(and_(*conditions)) if conditions else selectable failed_job_ids = set() - for row in self.engine.execute(selectable): - try: - jobs.append(self._reconstitute_job(row.job_state)) - except BaseException: - self._logger.exception('Unable to restore job "%s" -- removing it', row.id) - failed_job_ids.add(row.id) + with self.engine.begin() as connection: + for row in connection.execute(selectable): + try: + jobs.append(self._reconstitute_job(row.job_state)) + except BaseException: + self._logger.exception('Unable to restore job "%s" -- removing it', row.id) + failed_job_ids.add(row.id) - # Remove all the jobs we failed to restore - if failed_job_ids: - delete = self.jobs_t.delete().where(self.jobs_t.c.id.in_(failed_job_ids)) - self.engine.execute(delete) + # Remove all the jobs we failed to restore + if failed_job_ids: + delete = self.jobs_t.delete().where(self.jobs_t.c.id.in_(failed_job_ids)) + connection.execute(delete) return jobs diff --git a/libs/apscheduler/schedulers/asyncio.py b/libs/apscheduler/schedulers/asyncio.py index 70ebedeb6..8bcdfdafa 100644 --- a/libs/apscheduler/schedulers/asyncio.py +++ b/libs/apscheduler/schedulers/asyncio.py @@ -1,18 +1,10 @@ from __future__ import absolute_import +import asyncio from functools import wraps, partial from apscheduler.schedulers.base import BaseScheduler from apscheduler.util import maybe_ref -try: - import asyncio -except ImportError: # pragma: nocover - try: - import trollius as asyncio - except ImportError: - raise ImportError( - 'AsyncIOScheduler requires either Python 3.4 or the asyncio package installed') - def run_in_event_loop(func): @wraps(func) diff --git a/libs/apscheduler/schedulers/base.py b/libs/apscheduler/schedulers/base.py index 444de8ef9..f751c9551 100644 --- a/libs/apscheduler/schedulers/base.py +++ b/libs/apscheduler/schedulers/base.py @@ -7,7 +7,6 @@ from logging import getLogger import warnings import sys -from pkg_resources import iter_entry_points from tzlocal import get_localzone import six @@ -31,6 +30,11 @@ try: except ImportError: from collections import MutableMapping +try: + from importlib.metadata import entry_points +except ModuleNotFoundError: + from importlib_metadata import entry_points + #: constant indicating a scheduler's stopped state STATE_STOPPED = 0 #: constant indicating a scheduler's running state (started and processing jobs) @@ -62,12 +66,18 @@ class BaseScheduler(six.with_metaclass(ABCMeta)): .. seealso:: :ref:`scheduler-config` """ + # The `group=...` API is only available in the backport, used in <=3.7, and in std>=3.10. + if (3, 8) <= sys.version_info < (3, 10): + _trigger_plugins = {ep.name: ep for ep in entry_points()['apscheduler.triggers']} + _executor_plugins = {ep.name: ep for ep in entry_points()['apscheduler.executors']} + _jobstore_plugins = {ep.name: ep for ep in entry_points()['apscheduler.jobstores']} + else: + _trigger_plugins = {ep.name: ep for ep in entry_points(group='apscheduler.triggers')} + _executor_plugins = {ep.name: ep for ep in entry_points(group='apscheduler.executors')} + _jobstore_plugins = {ep.name: ep for ep in entry_points(group='apscheduler.jobstores')} - _trigger_plugins = dict((ep.name, ep) for ep in iter_entry_points('apscheduler.triggers')) _trigger_classes = {} - _executor_plugins = dict((ep.name, ep) for ep in iter_entry_points('apscheduler.executors')) _executor_classes = {} - _jobstore_plugins = dict((ep.name, ep) for ep in iter_entry_points('apscheduler.jobstores')) _jobstore_classes = {} # @@ -1019,6 +1029,7 @@ class BaseScheduler(six.with_metaclass(ABCMeta)): wait_seconds = None self._logger.debug('No jobs; waiting until a job is added') else: + now = datetime.now(self.timezone) wait_seconds = min(max(timedelta_seconds(next_wakeup_time - now), 0), TIMEOUT_MAX) self._logger.debug('Next wakeup is due at %s (in %f seconds)', next_wakeup_time, wait_seconds) diff --git a/libs/apscheduler/schedulers/qt.py b/libs/apscheduler/schedulers/qt.py index 600f6e677..6762c5c39 100644 --- a/libs/apscheduler/schedulers/qt.py +++ b/libs/apscheduler/schedulers/qt.py @@ -1,24 +1,22 @@ from __future__ import absolute_import +from importlib import import_module +from itertools import product + from apscheduler.schedulers.base import BaseScheduler -try: - from PyQt5.QtCore import QObject, QTimer -except (ImportError, RuntimeError): # pragma: nocover +for version, pkgname in product(range(6, 1, -1), ("PySide", "PyQt")): try: - from PyQt4.QtCore import QObject, QTimer + qtcore = import_module(pkgname + str(version) + ".QtCore") except ImportError: - try: - from PySide6.QtCore import QObject, QTimer # noqa - except ImportError: - try: - from PySide2.QtCore import QObject, QTimer # noqa - except ImportError: - try: - from PySide.QtCore import QObject, QTimer # noqa - except ImportError: - raise ImportError('QtScheduler requires either PyQt5, PyQt4, PySide6, PySide2 ' - 'or PySide installed') + pass + else: + QTimer = qtcore.QTimer + break +else: + raise ImportError( + "QtScheduler requires either PySide/PyQt (v6 to v2) installed" + ) class QtScheduler(BaseScheduler): @@ -33,7 +31,7 @@ class QtScheduler(BaseScheduler): def _start_timer(self, wait_seconds): self._stop_timer() if wait_seconds is not None: - wait_time = min(wait_seconds * 1000, 2147483647) + wait_time = min(int(wait_seconds * 1000), 2147483647) self._timer = QTimer.singleShot(wait_time, self._process_jobs) def _stop_timer(self): diff --git a/libs/apscheduler/util.py b/libs/apscheduler/util.py index d929a4822..003b30f5a 100644 --- a/libs/apscheduler/util.py +++ b/libs/apscheduler/util.py @@ -2,10 +2,11 @@ from __future__ import division +from asyncio import iscoroutinefunction from datetime import date, datetime, time, timedelta, tzinfo from calendar import timegm from functools import partial -from inspect import isclass, ismethod +from inspect import isbuiltin, isclass, isfunction, ismethod import re import sys @@ -22,15 +23,6 @@ try: except ImportError: TIMEOUT_MAX = 4294967 # Maximum value accepted by Event.wait() on Windows -try: - from asyncio import iscoroutinefunction -except ImportError: - try: - from trollius import iscoroutinefunction - except ImportError: - def iscoroutinefunction(func): - return False - __all__ = ('asint', 'asbool', 'astimezone', 'convert_to_datetime', 'datetime_to_utc_timestamp', 'utc_timestamp_to_datetime', 'timedelta_seconds', 'datetime_ceil', 'get_callable_name', 'obj_to_ref', 'ref_to_obj', 'maybe_ref', 'repr_escape', 'check_callable_args', @@ -222,28 +214,15 @@ def get_callable_name(func): :rtype: str """ - # the easy case (on Python 3.3+) - if hasattr(func, '__qualname__'): + if ismethod(func): + self = func.__self__ + cls = self if isclass(self) else type(self) + return f"{cls.__qualname__}.{func.__name__}" + elif isclass(func) or isfunction(func) or isbuiltin(func): return func.__qualname__ - - # class methods, bound and unbound methods - f_self = getattr(func, '__self__', None) or getattr(func, 'im_self', None) - if f_self and hasattr(func, '__name__'): - f_class = f_self if isclass(f_self) else f_self.__class__ - else: - f_class = getattr(func, 'im_class', None) - - if f_class and hasattr(func, '__name__'): - return '%s.%s' % (f_class.__name__, func.__name__) - - # class or class instance - if hasattr(func, '__call__'): - # class - if hasattr(func, '__name__'): - return func.__name__ - + elif hasattr(func, '__call__') and callable(func.__call__): # instance of a class with a __call__ method - return func.__class__.__name__ + return type(func).__qualname__ raise TypeError('Unable to determine a name for %r -- maybe it is not a callable?' % func) @@ -268,16 +247,10 @@ def obj_to_ref(obj): raise ValueError('Cannot create a reference to a nested function') if ismethod(obj): - if hasattr(obj, 'im_self') and obj.im_self: - # bound method - module = obj.im_self.__module__ - elif hasattr(obj, 'im_class') and obj.im_class: - # unbound method - module = obj.im_class.__module__ - else: - module = obj.__module__ + module = obj.__self__.__module__ else: module = obj.__module__ + return '%s:%s' % (module, name) diff --git a/libs/argparse-1.4.0.dist-info/INSTALLER b/libs/argparse-1.4.0.dist-info/INSTALLER new file mode 100644 index 000000000..a1b589e38 --- /dev/null +++ b/libs/argparse-1.4.0.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/libs/argparse-1.4.0.dist-info/LICENSE.txt b/libs/argparse-1.4.0.dist-info/LICENSE.txt new file mode 100644 index 000000000..640bc7809 --- /dev/null +++ b/libs/argparse-1.4.0.dist-info/LICENSE.txt @@ -0,0 +1,20 @@ +argparse is (c) 2006-2009 Steven J. Bethard . + +The argparse module was contributed to Python as of Python 2.7 and thus +was licensed under the Python license. Same license applies to all files in +the argparse package project. + +For details about the Python License, please see doc/Python-License.txt. + +History +------- + +Before (and including) argparse 1.1, the argparse package was licensed under +Apache License v2.0. + +After argparse 1.1, all project files from the argparse project were deleted +due to license compatibility issues between Apache License 2.0 and GNU GPL v2. + +The project repository then had a clean start with some files taken from +Python 2.7.1, so definitely all files are under Python License now. + diff --git a/libs/argparse-1.4.0.dist-info/METADATA b/libs/argparse-1.4.0.dist-info/METADATA new file mode 100644 index 000000000..39d06b1a7 --- /dev/null +++ b/libs/argparse-1.4.0.dist-info/METADATA @@ -0,0 +1,84 @@ +Metadata-Version: 2.1 +Name: argparse +Version: 1.4.0 +Summary: Python command-line parsing library +Home-page: https://github.com/ThomasWaldmann/argparse/ +Author: Thomas Waldmann +Author-email: tw@waldmann-edv.de +License: Python Software Foundation License +Keywords: argparse command line parser parsing +Platform: any +Classifier: Development Status :: 5 - Production/Stable +Classifier: Environment :: Console +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: Python Software Foundation License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 2.3 +Classifier: Programming Language :: Python :: 2.4 +Classifier: Programming Language :: Python :: 2.5 +Classifier: Programming Language :: Python :: 2.6 +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3.0 +Classifier: Programming Language :: Python :: 3.1 +Classifier: Programming Language :: Python :: 3.2 +Classifier: Programming Language :: Python :: 3.3 +Classifier: Programming Language :: Python :: 3.4 +Classifier: Topic :: Software Development +License-File: LICENSE.txt + +The argparse module makes it easy to write user friendly command line +interfaces. + +The program defines what arguments it requires, and argparse will figure out +how to parse those out of sys.argv. The argparse module also automatically +generates help and usage messages and issues errors when users give the +program invalid arguments. + +As of Python >= 2.7 and >= 3.2, the argparse module is maintained within the +Python standard library. For users who still need to support Python < 2.7 or +< 3.2, it is also provided as a separate package, which tries to stay +compatible with the module in the standard library, but also supports older +Python versions. + +Also, we can fix bugs here for users who are stuck on some non-current python +version, like e.g. 3.2.3 (which has bugs that were fixed in a later 3.2.x +release). + +argparse is licensed under the Python license, for details see LICENSE.txt. + + +Compatibility +------------- + +argparse should work on Python >= 2.3, it was tested on: + +* 2.3, 2.4, 2.5, 2.6 and 2.7 +* 3.1, 3.2, 3.3, 3.4 + + +Installation +------------ + +Try one of these: + + python setup.py install + + easy_install argparse + + pip install argparse + + putting argparse.py in some directory listed in sys.path should also work + + +Bugs +---- + +If you find a bug in argparse (pypi), please try to reproduce it with latest +python 2.7 and 3.4 (and use argparse from stdlib). + +If it happens there also, please file a bug in the python.org issue tracker. +If it does not happen there, file a bug in the argparse package issue tracker. + diff --git a/libs/argparse-1.4.0.dist-info/RECORD b/libs/argparse-1.4.0.dist-info/RECORD new file mode 100644 index 000000000..8b6f6f6c9 --- /dev/null +++ b/libs/argparse-1.4.0.dist-info/RECORD @@ -0,0 +1,8 @@ +argparse-1.4.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +argparse-1.4.0.dist-info/LICENSE.txt,sha256=bVBNRcTRCfkl7wWJYLbRzicSu2tXk-kmv8FRcWrHQEg,741 +argparse-1.4.0.dist-info/METADATA,sha256=yZGPMA4uvkui2P7qaaiI89zqwjDbyFcehJG4j5Pk8Yk,2816 +argparse-1.4.0.dist-info/RECORD,, +argparse-1.4.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +argparse-1.4.0.dist-info/WHEEL,sha256=P2T-6epvtXQ2cBOE_U1K4_noqlJFN3tj15djMgEu4NM,110 +argparse-1.4.0.dist-info/top_level.txt,sha256=TgiWrQsF0mKWwqS2KHLORD0ZtqYHPRGdCAAzKwtVvJ4,9 +argparse.py,sha256=0ksYqisQDQvhoiuo19JERCSpg51tc641GFJIx7pTA0g,89214 diff --git a/libs/argparse-1.4.0.dist-info/REQUESTED b/libs/argparse-1.4.0.dist-info/REQUESTED new file mode 100644 index 000000000..e69de29bb diff --git a/libs/argparse-1.4.0.dist-info/WHEEL b/libs/argparse-1.4.0.dist-info/WHEEL new file mode 100644 index 000000000..f31e450fd --- /dev/null +++ b/libs/argparse-1.4.0.dist-info/WHEEL @@ -0,0 +1,6 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.41.3) +Root-Is-Purelib: true +Tag: py2-none-any +Tag: py3-none-any + diff --git a/libs/argparse-1.4.0.dist-info/top_level.txt b/libs/argparse-1.4.0.dist-info/top_level.txt new file mode 100644 index 000000000..1352d5e6f --- /dev/null +++ b/libs/argparse-1.4.0.dist-info/top_level.txt @@ -0,0 +1 @@ +argparse diff --git a/libs/attr/__init__.py b/libs/attr/__init__.py index 386305d62..9226258a2 100644 --- a/libs/attr/__init__.py +++ b/libs/attr/__init__.py @@ -1,12 +1,15 @@ # SPDX-License-Identifier: MIT - -import sys +""" +Classes Without Boilerplate +""" from functools import partial +from typing import Callable from . import converters, exceptions, filters, setters, validators from ._cmp import cmp_using +from ._compat import Protocol from ._config import get_run_validators, set_run_validators from ._funcs import asdict, assoc, astuple, evolve, has, resolve_types from ._make import ( @@ -20,31 +23,22 @@ from ._make import ( make_class, validate, ) +from ._next_gen import define, field, frozen, mutable from ._version_info import VersionInfo -__version__ = "22.1.0" -__version_info__ = VersionInfo._from_version_string(__version__) - -__title__ = "attrs" -__description__ = "Classes Without Boilerplate" -__url__ = "https://www.attrs.org/" -__uri__ = __url__ -__doc__ = __description__ + " <" + __uri__ + ">" - -__author__ = "Hynek Schlawack" -__email__ = "hs@ox.cx" - -__license__ = "MIT" -__copyright__ = "Copyright (c) 2015 Hynek Schlawack" - - s = attributes = attrs ib = attr = attrib dataclass = partial(attrs, auto_attribs=True) # happy Easter ;) + +class AttrsInstance(Protocol): + pass + + __all__ = [ "Attribute", + "AttrsInstance", "Factory", "NOTHING", "asdict", @@ -56,15 +50,19 @@ __all__ = [ "attrs", "cmp_using", "converters", + "define", "evolve", "exceptions", + "field", "fields", "fields_dict", "filters", + "frozen", "get_run_validators", "has", "ib", "make_class", + "mutable", "resolve_types", "s", "set_run_validators", @@ -73,7 +71,64 @@ __all__ = [ "validators", ] -if sys.version_info[:2] >= (3, 6): - from ._next_gen import define, field, frozen, mutable # noqa: F401 - __all__.extend(("define", "field", "frozen", "mutable")) +def _make_getattr(mod_name: str) -> Callable: + """ + Create a metadata proxy for packaging information that uses *mod_name* in + its warnings and errors. + """ + + def __getattr__(name: str) -> str: + dunder_to_metadata = { + "__title__": "Name", + "__copyright__": "", + "__version__": "version", + "__version_info__": "version", + "__description__": "summary", + "__uri__": "", + "__url__": "", + "__author__": "", + "__email__": "", + "__license__": "license", + } + if name not in dunder_to_metadata: + msg = f"module {mod_name} has no attribute {name}" + raise AttributeError(msg) + + import sys + import warnings + + if sys.version_info < (3, 8): + from importlib_metadata import metadata + else: + from importlib.metadata import metadata + + if name not in ("__version__", "__version_info__"): + warnings.warn( + f"Accessing {mod_name}.{name} is deprecated and will be " + "removed in a future release. Use importlib.metadata directly " + "to query for attrs's packaging metadata.", + DeprecationWarning, + stacklevel=2, + ) + + meta = metadata("attrs") + if name == "__license__": + return "MIT" + if name == "__copyright__": + return "Copyright (c) 2015 Hynek Schlawack" + if name in ("__uri__", "__url__"): + return meta["Project-URL"].split(" ", 1)[-1] + if name == "__version_info__": + return VersionInfo._from_version_string(meta["version"]) + if name == "__author__": + return meta["Author-email"].rsplit(" ", 1)[0] + if name == "__email__": + return meta["Author-email"].rsplit("<", 1)[1][:-1] + + return meta[dunder_to_metadata[name]] + + return __getattr__ + + +__getattr__ = _make_getattr(__name__) diff --git a/libs/attr/__init__.pyi b/libs/attr/__init__.pyi index 03cc4c82d..37a208732 100644 --- a/libs/attr/__init__.pyi +++ b/libs/attr/__init__.pyi @@ -1,9 +1,9 @@ +import enum import sys from typing import ( Any, Callable, - ClassVar, Dict, Generic, List, @@ -25,8 +25,19 @@ from . import filters as filters from . import setters as setters from . import validators as validators from ._cmp import cmp_using as cmp_using +from ._typing_compat import AttrsInstance_ from ._version_info import VersionInfo +if sys.version_info >= (3, 10): + from typing import TypeGuard +else: + from typing_extensions import TypeGuard + +if sys.version_info >= (3, 11): + from typing import dataclass_transform +else: + from typing_extensions import dataclass_transform + __version__: str __version_info__: VersionInfo __title__: str @@ -42,30 +53,33 @@ _T = TypeVar("_T") _C = TypeVar("_C", bound=type) _EqOrderType = Union[bool, Callable[[Any], Any]] -_ValidatorType = Callable[[Any, Attribute[_T], _T], Any] +_ValidatorType = Callable[[Any, "Attribute[_T]", _T], Any] _ConverterType = Callable[[Any], Any] -_FilterType = Callable[[Attribute[_T], _T], bool] +_FilterType = Callable[["Attribute[_T]", _T], bool] _ReprType = Callable[[Any], str] _ReprArgType = Union[bool, _ReprType] -_OnSetAttrType = Callable[[Any, Attribute[Any], Any], Any] +_OnSetAttrType = Callable[[Any, "Attribute[Any]", Any], Any] _OnSetAttrArgType = Union[ _OnSetAttrType, List[_OnSetAttrType], setters._NoOpType ] _FieldTransformer = Callable[ - [type, List[Attribute[Any]]], List[Attribute[Any]] + [type, List["Attribute[Any]"]], List["Attribute[Any]"] ] # FIXME: in reality, if multiple validators are passed they must be in a list # or tuple, but those are invariant and so would prevent subtypes of # _ValidatorType from working when passed in a list or tuple. _ValidatorArgType = Union[_ValidatorType[_T], Sequence[_ValidatorType[_T]]] -# A protocol to be able to statically accept an attrs class. -class AttrsInstance(Protocol): - __attrs_attrs__: ClassVar[Any] +# We subclass this here to keep the protocol's qualified name clean. +class AttrsInstance(AttrsInstance_, Protocol): + pass -# _make -- +_A = TypeVar("_A", bound=type[AttrsInstance]) -NOTHING: object +class _Nothing(enum.Enum): + NOTHING = enum.auto() + +NOTHING = _Nothing.NOTHING # NOTE: Factory lies about its return type to make this possible: # `x: List[int] # = Factory(list)` @@ -94,22 +108,6 @@ else: takes_self: bool = ..., ) -> _T: ... -# Static type inference support via __dataclass_transform__ implemented as per: -# https://github.com/microsoft/pyright/blob/1.1.135/specs/dataclass_transforms.md -# This annotation must be applied to all overloads of "define" and "attrs" -# -# NOTE: This is a typing construct and does not exist at runtime. Extensions -# wrapping attrs decorators should declare a separate __dataclass_transform__ -# signature in the extension module using the specification linked above to -# provide pyright support. -def __dataclass_transform__( - *, - eq_default: bool = True, - order_default: bool = False, - kw_only_default: bool = False, - field_descriptors: Tuple[Union[type, Callable[..., Any]], ...] = (()), -) -> Callable[[_T], _T]: ... - class Attribute(Generic[_T]): name: str default: Optional[_T] @@ -125,6 +123,8 @@ class Attribute(Generic[_T]): type: Optional[Type[_T]] kw_only: bool on_setattr: _OnSetAttrType + alias: Optional[str] + def evolve(self, **changes: Any) -> "Attribute[Any]": ... # NOTE: We had several choices for the annotation to use for type arg: @@ -167,6 +167,7 @@ def attrib( eq: Optional[_EqOrderType] = ..., order: Optional[_EqOrderType] = ..., on_setattr: Optional[_OnSetAttrArgType] = ..., + alias: Optional[str] = ..., ) -> Any: ... # This form catches an explicit None or no default and infers the type from the @@ -187,6 +188,7 @@ def attrib( eq: Optional[_EqOrderType] = ..., order: Optional[_EqOrderType] = ..., on_setattr: Optional[_OnSetAttrArgType] = ..., + alias: Optional[str] = ..., ) -> _T: ... # This form catches an explicit default argument. @@ -206,6 +208,7 @@ def attrib( eq: Optional[_EqOrderType] = ..., order: Optional[_EqOrderType] = ..., on_setattr: Optional[_OnSetAttrArgType] = ..., + alias: Optional[str] = ..., ) -> _T: ... # This form covers type=non-Type: e.g. forward references (str), Any @@ -225,6 +228,7 @@ def attrib( eq: Optional[_EqOrderType] = ..., order: Optional[_EqOrderType] = ..., on_setattr: Optional[_OnSetAttrArgType] = ..., + alias: Optional[str] = ..., ) -> Any: ... @overload def field( @@ -241,6 +245,8 @@ def field( eq: Optional[bool] = ..., order: Optional[bool] = ..., on_setattr: Optional[_OnSetAttrArgType] = ..., + alias: Optional[str] = ..., + type: Optional[type] = ..., ) -> Any: ... # This form catches an explicit None or no default and infers the type from the @@ -260,6 +266,8 @@ def field( eq: Optional[_EqOrderType] = ..., order: Optional[_EqOrderType] = ..., on_setattr: Optional[_OnSetAttrArgType] = ..., + alias: Optional[str] = ..., + type: Optional[type] = ..., ) -> _T: ... # This form catches an explicit default argument. @@ -278,6 +286,8 @@ def field( eq: Optional[_EqOrderType] = ..., order: Optional[_EqOrderType] = ..., on_setattr: Optional[_OnSetAttrArgType] = ..., + alias: Optional[str] = ..., + type: Optional[type] = ..., ) -> _T: ... # This form covers type=non-Type: e.g. forward references (str), Any @@ -296,9 +306,11 @@ def field( eq: Optional[_EqOrderType] = ..., order: Optional[_EqOrderType] = ..., on_setattr: Optional[_OnSetAttrArgType] = ..., + alias: Optional[str] = ..., + type: Optional[type] = ..., ) -> Any: ... @overload -@__dataclass_transform__(order_default=True, field_descriptors=(attrib, field)) +@dataclass_transform(order_default=True, field_specifiers=(attrib, field)) def attrs( maybe_cls: _C, these: Optional[Dict[str, Any]] = ..., @@ -323,9 +335,10 @@ def attrs( on_setattr: Optional[_OnSetAttrArgType] = ..., field_transformer: Optional[_FieldTransformer] = ..., match_args: bool = ..., + unsafe_hash: Optional[bool] = ..., ) -> _C: ... @overload -@__dataclass_transform__(order_default=True, field_descriptors=(attrib, field)) +@dataclass_transform(order_default=True, field_specifiers=(attrib, field)) def attrs( maybe_cls: None = ..., these: Optional[Dict[str, Any]] = ..., @@ -350,14 +363,16 @@ def attrs( on_setattr: Optional[_OnSetAttrArgType] = ..., field_transformer: Optional[_FieldTransformer] = ..., match_args: bool = ..., + unsafe_hash: Optional[bool] = ..., ) -> Callable[[_C], _C]: ... @overload -@__dataclass_transform__(field_descriptors=(attrib, field)) +@dataclass_transform(field_specifiers=(attrib, field)) def define( maybe_cls: _C, *, these: Optional[Dict[str, Any]] = ..., repr: bool = ..., + unsafe_hash: Optional[bool] = ..., hash: Optional[bool] = ..., init: bool = ..., slots: bool = ..., @@ -377,12 +392,13 @@ def define( match_args: bool = ..., ) -> _C: ... @overload -@__dataclass_transform__(field_descriptors=(attrib, field)) +@dataclass_transform(field_specifiers=(attrib, field)) def define( maybe_cls: None = ..., *, these: Optional[Dict[str, Any]] = ..., repr: bool = ..., + unsafe_hash: Optional[bool] = ..., hash: Optional[bool] = ..., init: bool = ..., slots: bool = ..., @@ -403,17 +419,69 @@ def define( ) -> Callable[[_C], _C]: ... mutable = define -frozen = define # they differ only in their defaults +@overload +@dataclass_transform(frozen_default=True, field_specifiers=(attrib, field)) +def frozen( + maybe_cls: _C, + *, + these: Optional[Dict[str, Any]] = ..., + repr: bool = ..., + unsafe_hash: Optional[bool] = ..., + hash: Optional[bool] = ..., + init: bool = ..., + slots: bool = ..., + frozen: bool = ..., + weakref_slot: bool = ..., + str: bool = ..., + auto_attribs: bool = ..., + kw_only: bool = ..., + cache_hash: bool = ..., + auto_exc: bool = ..., + eq: Optional[bool] = ..., + order: Optional[bool] = ..., + auto_detect: bool = ..., + getstate_setstate: Optional[bool] = ..., + on_setattr: Optional[_OnSetAttrArgType] = ..., + field_transformer: Optional[_FieldTransformer] = ..., + match_args: bool = ..., +) -> _C: ... +@overload +@dataclass_transform(frozen_default=True, field_specifiers=(attrib, field)) +def frozen( + maybe_cls: None = ..., + *, + these: Optional[Dict[str, Any]] = ..., + repr: bool = ..., + unsafe_hash: Optional[bool] = ..., + hash: Optional[bool] = ..., + init: bool = ..., + slots: bool = ..., + frozen: bool = ..., + weakref_slot: bool = ..., + str: bool = ..., + auto_attribs: bool = ..., + kw_only: bool = ..., + cache_hash: bool = ..., + auto_exc: bool = ..., + eq: Optional[bool] = ..., + order: Optional[bool] = ..., + auto_detect: bool = ..., + getstate_setstate: Optional[bool] = ..., + on_setattr: Optional[_OnSetAttrArgType] = ..., + field_transformer: Optional[_FieldTransformer] = ..., + match_args: bool = ..., +) -> Callable[[_C], _C]: ... def fields(cls: Type[AttrsInstance]) -> Any: ... def fields_dict(cls: Type[AttrsInstance]) -> Dict[str, Attribute[Any]]: ... def validate(inst: AttrsInstance) -> None: ... def resolve_types( - cls: _C, + cls: _A, globalns: Optional[Dict[str, Any]] = ..., localns: Optional[Dict[str, Any]] = ..., attribs: Optional[List[Attribute[Any]]] = ..., -) -> _C: ... + include_extras: bool = ..., +) -> _A: ... # TODO: add support for returning a proper attrs class from the mypy plugin # we use Any instead of _CountingAttr so that e.g. `make_class('Foo', @@ -422,6 +490,7 @@ def make_class( name: str, attrs: Union[List[str], Tuple[str, ...], Dict[str, Any]], bases: Tuple[type, ...] = ..., + class_body: Optional[Dict[str, Any]] = ..., repr_ns: Optional[str] = ..., repr: bool = ..., cmp: Optional[_EqOrderType] = ..., @@ -470,7 +539,7 @@ def astuple( tuple_factory: Type[Sequence[Any]] = ..., retain_collection_types: bool = ..., ) -> Tuple[Any, ...]: ... -def has(cls: type) -> bool: ... +def has(cls: type) -> TypeGuard[Type[AttrsInstance]]: ... def assoc(inst: _T, **changes: Any) -> _T: ... def evolve(inst: _T, **changes: Any) -> _T: ... diff --git a/libs/attr/_cmp.py b/libs/attr/_cmp.py index 81b99e4c3..a4a35e08f 100644 --- a/libs/attr/_cmp.py +++ b/libs/attr/_cmp.py @@ -20,22 +20,22 @@ def cmp_using( class_name="Comparable", ): """ - Create a class that can be passed into `attr.ib`'s ``eq``, ``order``, and - ``cmp`` arguments to customize field comparison. + Create a class that can be passed into `attrs.field`'s ``eq``, ``order``, + and ``cmp`` arguments to customize field comparison. - The resulting class will have a full set of ordering methods if - at least one of ``{lt, le, gt, ge}`` and ``eq`` are provided. + The resulting class will have a full set of ordering methods if at least + one of ``{lt, le, gt, ge}`` and ``eq`` are provided. - :param Optional[callable] eq: `callable` used to evaluate equality - of two objects. - :param Optional[callable] lt: `callable` used to evaluate whether - one object is less than another object. - :param Optional[callable] le: `callable` used to evaluate whether - one object is less than or equal to another object. - :param Optional[callable] gt: `callable` used to evaluate whether - one object is greater than another object. - :param Optional[callable] ge: `callable` used to evaluate whether - one object is greater than or equal to another object. + :param Optional[callable] eq: `callable` used to evaluate equality of two + objects. + :param Optional[callable] lt: `callable` used to evaluate whether one + object is less than another object. + :param Optional[callable] le: `callable` used to evaluate whether one + object is less than or equal to another object. + :param Optional[callable] gt: `callable` used to evaluate whether one + object is greater than another object. + :param Optional[callable] ge: `callable` used to evaluate whether one + object is greater than or equal to another object. :param bool require_same_type: When `True`, equality and ordering methods will return `NotImplemented` if objects are not of the same type. @@ -92,10 +92,8 @@ def cmp_using( if not has_eq_function: # functools.total_ordering requires __eq__ to be defined, # so raise early error here to keep a nice stack. - raise ValueError( - "eq must be define is order to complete ordering from " - "lt, le, gt, ge." - ) + msg = "eq must be define is order to complete ordering from lt, le, gt, ge." + raise ValueError(msg) type_ = functools.total_ordering(type_) return type_ @@ -130,9 +128,9 @@ def _make_operator(name, func): return result - method.__name__ = "__%s__" % (name,) - method.__doc__ = "Return a %s b. Computed by attrs." % ( - _operation_names[name], + method.__name__ = f"__{name}__" + method.__doc__ = ( + f"Return a {_operation_names[name]} b. Computed by attrs." ) return method @@ -142,10 +140,7 @@ def _is_comparable_to(self, other): """ Check whether `other` is comparable to `self`. """ - for func in self._requirements: - if not func(self, other): - return False - return True + return all(func(self, other) for func in self._requirements) def _check_same_type(self, other): diff --git a/libs/attr/_cmp.pyi b/libs/attr/_cmp.pyi index 35437eff6..f3dcdc1a7 100644 --- a/libs/attr/_cmp.pyi +++ b/libs/attr/_cmp.pyi @@ -3,11 +3,11 @@ from typing import Any, Callable, Optional, Type _CompareWithType = Callable[[Any, Any], bool] def cmp_using( - eq: Optional[_CompareWithType], - lt: Optional[_CompareWithType], - le: Optional[_CompareWithType], - gt: Optional[_CompareWithType], - ge: Optional[_CompareWithType], - require_same_type: bool, - class_name: str, + eq: Optional[_CompareWithType] = ..., + lt: Optional[_CompareWithType] = ..., + le: Optional[_CompareWithType] = ..., + gt: Optional[_CompareWithType] = ..., + ge: Optional[_CompareWithType] = ..., + require_same_type: bool = ..., + class_name: str = ..., ) -> Type: ... diff --git a/libs/attr/_compat.py b/libs/attr/_compat.py index 582649325..46b05ca45 100644 --- a/libs/attr/_compat.py +++ b/libs/attr/_compat.py @@ -1,38 +1,28 @@ # SPDX-License-Identifier: MIT - import inspect import platform import sys import threading -import types -import warnings -from collections.abc import Mapping, Sequence # noqa +from collections.abc import Mapping, Sequence # noqa: F401 +from typing import _GenericAlias PYPY = platform.python_implementation() == "PyPy" -PY36 = sys.version_info[:2] >= (3, 6) -HAS_F_STRINGS = PY36 +PY_3_8_PLUS = sys.version_info[:2] >= (3, 8) +PY_3_9_PLUS = sys.version_info[:2] >= (3, 9) PY310 = sys.version_info[:2] >= (3, 10) +PY_3_12_PLUS = sys.version_info[:2] >= (3, 12) -if PYPY or PY36: - ordered_dict = dict +if sys.version_info < (3, 8): + try: + from typing_extensions import Protocol + except ImportError: # pragma: no cover + Protocol = object else: - from collections import OrderedDict - - ordered_dict = OrderedDict - - -def just_warn(*args, **kw): - warnings.warn( - "Running interpreter doesn't sufficiently support code object " - "introspection. Some features like bare super() or accessing " - "__class__ will not work with slotted classes.", - RuntimeWarning, - stacklevel=2, - ) + from typing import Protocol # noqa: F401 class _AnnotationExtractor: @@ -75,101 +65,6 @@ class _AnnotationExtractor: return None -def make_set_closure_cell(): - """Return a function of two arguments (cell, value) which sets - the value stored in the closure cell `cell` to `value`. - """ - # pypy makes this easy. (It also supports the logic below, but - # why not do the easy/fast thing?) - if PYPY: - - def set_closure_cell(cell, value): - cell.__setstate__((value,)) - - return set_closure_cell - - # Otherwise gotta do it the hard way. - - # Create a function that will set its first cellvar to `value`. - def set_first_cellvar_to(value): - x = value - return - - # This function will be eliminated as dead code, but - # not before its reference to `x` forces `x` to be - # represented as a closure cell rather than a local. - def force_x_to_be_a_cell(): # pragma: no cover - return x - - try: - # Extract the code object and make sure our assumptions about - # the closure behavior are correct. - co = set_first_cellvar_to.__code__ - if co.co_cellvars != ("x",) or co.co_freevars != (): - raise AssertionError # pragma: no cover - - # Convert this code object to a code object that sets the - # function's first _freevar_ (not cellvar) to the argument. - if sys.version_info >= (3, 8): - - def set_closure_cell(cell, value): - cell.cell_contents = value - - else: - args = [co.co_argcount] - args.append(co.co_kwonlyargcount) - args.extend( - [ - co.co_nlocals, - co.co_stacksize, - co.co_flags, - co.co_code, - co.co_consts, - co.co_names, - co.co_varnames, - co.co_filename, - co.co_name, - co.co_firstlineno, - co.co_lnotab, - # These two arguments are reversed: - co.co_cellvars, - co.co_freevars, - ] - ) - set_first_freevar_code = types.CodeType(*args) - - def set_closure_cell(cell, value): - # Create a function using the set_first_freevar_code, - # whose first closure cell is `cell`. Calling it will - # change the value of that cell. - setter = types.FunctionType( - set_first_freevar_code, {}, "setter", (), (cell,) - ) - # And call it to set the cell. - setter(value) - - # Make sure it works on this interpreter: - def make_func_with_cell(): - x = None - - def func(): - return x # pragma: no cover - - return func - - cell = make_func_with_cell().__closure__[0] - set_closure_cell(cell, 100) - if cell.cell_contents != 100: - raise AssertionError # pragma: no cover - - except Exception: - return just_warn - else: - return set_closure_cell - - -set_closure_cell = make_set_closure_cell() - # Thread-local global to track attrs instances which are already being repr'd. # This is needed because there is no other (thread-safe) way to pass info # about the instances that are already being repr'd through the call stack @@ -183,3 +78,10 @@ set_closure_cell = make_set_closure_cell() # don't have a direct reference to the thread-local in their globals dict. # If they have such a reference, it breaks cloudpickle. repr_context = threading.local() + + +def get_generic_base(cl): + """If this is a generic class (A[str]), return the generic base for it.""" + if cl.__class__ is _GenericAlias: + return cl.__origin__ + return None diff --git a/libs/attr/_config.py b/libs/attr/_config.py index 96d420077..9c245b146 100644 --- a/libs/attr/_config.py +++ b/libs/attr/_config.py @@ -1,6 +1,5 @@ # SPDX-License-Identifier: MIT - __all__ = ["set_run_validators", "get_run_validators"] _run_validators = True @@ -15,7 +14,8 @@ def set_run_validators(run): instead. """ if not isinstance(run, bool): - raise TypeError("'run' must be bool.") + msg = "'run' must be bool." + raise TypeError(msg) global _run_validators _run_validators = run diff --git a/libs/attr/_funcs.py b/libs/attr/_funcs.py index a982d7cb5..a888991d9 100644 --- a/libs/attr/_funcs.py +++ b/libs/attr/_funcs.py @@ -3,6 +3,7 @@ import copy +from ._compat import PY_3_9_PLUS, get_generic_base from ._make import NOTHING, _obj_setattr, fields from .exceptions import AttrsAttributeNotFoundError @@ -16,13 +17,13 @@ def asdict( value_serializer=None, ): """ - Return the ``attrs`` attribute values of *inst* as a dict. + Return the *attrs* attribute values of *inst* as a dict. - Optionally recurse into other ``attrs``-decorated classes. + Optionally recurse into other *attrs*-decorated classes. - :param inst: Instance of an ``attrs``-decorated class. + :param inst: Instance of an *attrs*-decorated class. :param bool recurse: Recurse into classes that are also - ``attrs``-decorated. + *attrs*-decorated. :param callable filter: A callable whose return code determines whether an attribute or element is included (``True``) or dropped (``False``). Is called with the `attrs.Attribute` as the first argument and the @@ -40,7 +41,7 @@ def asdict( :rtype: return type of *dict_factory* - :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs`` + :raise attrs.exceptions.NotAnAttrsClassError: If *cls* is not an *attrs* class. .. versionadded:: 16.0.0 *dict_factory* @@ -71,19 +72,25 @@ def asdict( ) elif isinstance(v, (tuple, list, set, frozenset)): cf = v.__class__ if retain_collection_types is True else list - rv[a.name] = cf( - [ - _asdict_anything( - i, - is_key=False, - filter=filter, - dict_factory=dict_factory, - retain_collection_types=retain_collection_types, - value_serializer=value_serializer, - ) - for i in v - ] - ) + items = [ + _asdict_anything( + i, + is_key=False, + filter=filter, + dict_factory=dict_factory, + retain_collection_types=retain_collection_types, + value_serializer=value_serializer, + ) + for i in v + ] + try: + rv[a.name] = cf(items) + except TypeError: + if not issubclass(cf, tuple): + raise + # Workaround for TypeError: cf.__new__() missing 1 required + # positional argument (which appears, for a namedturle) + rv[a.name] = cf(*items) elif isinstance(v, dict): df = dict_factory rv[a.name] = df( @@ -195,13 +202,13 @@ def astuple( retain_collection_types=False, ): """ - Return the ``attrs`` attribute values of *inst* as a tuple. + Return the *attrs* attribute values of *inst* as a tuple. - Optionally recurse into other ``attrs``-decorated classes. + Optionally recurse into other *attrs*-decorated classes. - :param inst: Instance of an ``attrs``-decorated class. + :param inst: Instance of an *attrs*-decorated class. :param bool recurse: Recurse into classes that are also - ``attrs``-decorated. + *attrs*-decorated. :param callable filter: A callable whose return code determines whether an attribute or element is included (``True``) or dropped (``False``). Is called with the `attrs.Attribute` as the first argument and the @@ -215,7 +222,7 @@ def astuple( :rtype: return type of *tuple_factory* - :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs`` + :raise attrs.exceptions.NotAnAttrsClassError: If *cls* is not an *attrs* class. .. versionadded:: 16.2.0 @@ -240,22 +247,26 @@ def astuple( ) elif isinstance(v, (tuple, list, set, frozenset)): cf = v.__class__ if retain is True else list - rv.append( - cf( - [ - astuple( - j, - recurse=True, - filter=filter, - tuple_factory=tuple_factory, - retain_collection_types=retain, - ) - if has(j.__class__) - else j - for j in v - ] + items = [ + astuple( + j, + recurse=True, + filter=filter, + tuple_factory=tuple_factory, + retain_collection_types=retain, ) - ) + if has(j.__class__) + else j + for j in v + ] + try: + rv.append(cf(items)) + except TypeError: + if not issubclass(cf, tuple): + raise + # Workaround for TypeError: cf.__new__() missing 1 required + # positional argument (which appears, for a namedturle) + rv.append(cf(*items)) elif isinstance(v, dict): df = v.__class__ if retain is True else dict rv.append( @@ -289,28 +300,48 @@ def astuple( def has(cls): """ - Check whether *cls* is a class with ``attrs`` attributes. + Check whether *cls* is a class with *attrs* attributes. :param type cls: Class to introspect. :raise TypeError: If *cls* is not a class. :rtype: bool """ - return getattr(cls, "__attrs_attrs__", None) is not None + attrs = getattr(cls, "__attrs_attrs__", None) + if attrs is not None: + return True + + # No attrs, maybe it's a specialized generic (A[str])? + generic_base = get_generic_base(cls) + if generic_base is not None: + generic_attrs = getattr(generic_base, "__attrs_attrs__", None) + if generic_attrs is not None: + # Stick it on here for speed next time. + cls.__attrs_attrs__ = generic_attrs + return generic_attrs is not None + return False def assoc(inst, **changes): """ Copy *inst* and apply *changes*. - :param inst: Instance of a class with ``attrs`` attributes. + This is different from `evolve` that applies the changes to the arguments + that create the new instance. + + `evolve`'s behavior is preferable, but there are `edge cases`_ where it + doesn't work. Therefore `assoc` is deprecated, but will not be removed. + + .. _`edge cases`: https://github.com/python-attrs/attrs/issues/251 + + :param inst: Instance of a class with *attrs* attributes. :param changes: Keyword changes in the new copy. :return: A copy of inst with *changes* incorporated. - :raise attr.exceptions.AttrsAttributeNotFoundError: If *attr_name* couldn't - be found on *cls*. - :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs`` + :raise attrs.exceptions.AttrsAttributeNotFoundError: If *attr_name* + couldn't be found on *cls*. + :raise attrs.exceptions.NotAnAttrsClassError: If *cls* is not an *attrs* class. .. deprecated:: 17.1.0 @@ -318,57 +349,79 @@ def assoc(inst, **changes): This function will not be removed du to the slightly different approach compared to `attrs.evolve`. """ - import warnings - - warnings.warn( - "assoc is deprecated and will be removed after 2018/01.", - DeprecationWarning, - stacklevel=2, - ) new = copy.copy(inst) attrs = fields(inst.__class__) for k, v in changes.items(): a = getattr(attrs, k, NOTHING) if a is NOTHING: - raise AttrsAttributeNotFoundError( - "{k} is not an attrs attribute on {cl}.".format( - k=k, cl=new.__class__ - ) - ) + msg = f"{k} is not an attrs attribute on {new.__class__}." + raise AttrsAttributeNotFoundError(msg) _obj_setattr(new, k, v) return new -def evolve(inst, **changes): +def evolve(*args, **changes): """ - Create a new instance, based on *inst* with *changes* applied. + Create a new instance, based on the first positional argument with + *changes* applied. - :param inst: Instance of a class with ``attrs`` attributes. + :param inst: Instance of a class with *attrs* attributes. :param changes: Keyword changes in the new copy. :return: A copy of inst with *changes* incorporated. :raise TypeError: If *attr_name* couldn't be found in the class ``__init__``. - :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs`` + :raise attrs.exceptions.NotAnAttrsClassError: If *cls* is not an *attrs* class. - .. versionadded:: 17.1.0 + .. versionadded:: 17.1.0 + .. deprecated:: 23.1.0 + It is now deprecated to pass the instance using the keyword argument + *inst*. It will raise a warning until at least April 2024, after which + it will become an error. Always pass the instance as a positional + argument. """ + # Try to get instance by positional argument first. + # Use changes otherwise and warn it'll break. + if args: + try: + (inst,) = args + except ValueError: + msg = f"evolve() takes 1 positional argument, but {len(args)} were given" + raise TypeError(msg) from None + else: + try: + inst = changes.pop("inst") + except KeyError: + msg = "evolve() missing 1 required positional argument: 'inst'" + raise TypeError(msg) from None + + import warnings + + warnings.warn( + "Passing the instance per keyword argument is deprecated and " + "will stop working in, or after, April 2024.", + DeprecationWarning, + stacklevel=2, + ) + cls = inst.__class__ attrs = fields(cls) for a in attrs: if not a.init: continue attr_name = a.name # To deal with private attributes. - init_name = attr_name if attr_name[0] != "_" else attr_name[1:] + init_name = a.alias if init_name not in changes: changes[init_name] = getattr(inst, attr_name) return cls(**changes) -def resolve_types(cls, globalns=None, localns=None, attribs=None): +def resolve_types( + cls, globalns=None, localns=None, attribs=None, include_extras=True +): """ Resolve any strings and forward annotations in type annotations. @@ -387,10 +440,14 @@ def resolve_types(cls, globalns=None, localns=None, attribs=None): :param Optional[dict] localns: Dictionary containing local variables. :param Optional[list] attribs: List of attribs for the given class. This is necessary when calling from inside a ``field_transformer`` - since *cls* is not an ``attrs`` class yet. + since *cls* is not an *attrs* class yet. + :param bool include_extras: Resolve more accurately, if possible. + Pass ``include_extras`` to ``typing.get_hints``, if supported by the + typing module. On supported Python versions (3.9+), this resolves the + types more accurately. :raise TypeError: If *cls* is not a class. - :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs`` + :raise attrs.exceptions.NotAnAttrsClassError: If *cls* is not an *attrs* class and you didn't pass any attribs. :raise NameError: If types cannot be resolved because of missing variables. @@ -400,6 +457,7 @@ def resolve_types(cls, globalns=None, localns=None, attribs=None): .. versionadded:: 20.1.0 .. versionadded:: 21.1.0 *attribs* + .. versionadded:: 23.1.0 *include_extras* """ # Since calling get_type_hints is expensive we cache whether we've @@ -407,7 +465,12 @@ def resolve_types(cls, globalns=None, localns=None, attribs=None): if getattr(cls, "__attrs_types_resolved__", None) != cls: import typing - hints = typing.get_type_hints(cls, globalns=globalns, localns=localns) + kwargs = {"globalns": globalns, "localns": localns} + + if PY_3_9_PLUS: + kwargs["include_extras"] = include_extras + + hints = typing.get_type_hints(cls, **kwargs) for field in fields(cls) if attribs is None else attribs: if field.name in hints: # Since fields have been frozen we must work around it. diff --git a/libs/attr/_make.py b/libs/attr/_make.py index 4d1afe3fc..10b4eca77 100644 --- a/libs/attr/_make.py +++ b/libs/attr/_make.py @@ -1,6 +1,11 @@ # SPDX-License-Identifier: MIT +import contextlib import copy +import enum +import functools +import inspect +import itertools import linecache import sys import types @@ -12,12 +17,10 @@ from operator import itemgetter # having the thread-local in the globals here. from . import _compat, _config, setters from ._compat import ( - HAS_F_STRINGS, PY310, - PYPY, + PY_3_8_PLUS, _AnnotationExtractor, - ordered_dict, - set_closure_cell, + get_generic_base, ) from .exceptions import ( DefaultAlreadySetError, @@ -30,10 +33,7 @@ from .exceptions import ( # This is used at least twice, so cache it here. _obj_setattr = object.__setattr__ _init_converter_pat = "__attr_converter_%s" -_init_factory_pat = "__attr_factory_{}" -_tuple_property_pat = ( - " {attr_name} = _attrs_property(_attrs_itemgetter({index}))" -) +_init_factory_pat = "__attr_factory_%s" _classvar_prefixes = ( "typing.ClassVar", "t.ClassVar", @@ -53,21 +53,18 @@ _sentinel = object() _ng_default_on_setattr = setters.pipe(setters.convert, setters.validate) -class _Nothing: +class _Nothing(enum.Enum): """ - Sentinel class to indicate the lack of a value when ``None`` is ambiguous. + Sentinel to indicate the lack of a value when ``None`` is ambiguous. - ``_Nothing`` is a singleton. There is only ever one of it. + If extending attrs, you can use ``typing.Literal[NOTHING]`` to show + that a value may be ``NOTHING``. .. versionchanged:: 21.1.0 ``bool(NOTHING)`` is now False. + .. versionchanged:: 22.2.0 ``NOTHING`` is now an ``enum.Enum`` variant. """ - _singleton = None - - def __new__(cls): - if _Nothing._singleton is None: - _Nothing._singleton = super().__new__(cls) - return _Nothing._singleton + NOTHING = enum.auto() def __repr__(self): return "NOTHING" @@ -76,7 +73,7 @@ class _Nothing: return False -NOTHING = _Nothing() +NOTHING = _Nothing.NOTHING """ Sentinel to indicate the lack of a value when ``None`` is ambiguous. """ @@ -94,7 +91,7 @@ class _CacheHashWrapper(int): See GH #613 for more details. """ - def __reduce__(self, _none_constructor=type(None), _args=()): + def __reduce__(self, _none_constructor=type(None), _args=()): # noqa: B008 return _none_constructor, _args @@ -113,16 +110,20 @@ def attrib( eq=None, order=None, on_setattr=None, + alias=None, ): """ Create a new attribute on a class. .. warning:: - Does *not* do anything unless the class is also decorated with - `attr.s`! + Does *not* do anything unless the class is also decorated with `attr.s` + / `attrs.define` / and so on! - :param default: A value that is used if an ``attrs``-generated ``__init__`` + Please consider using `attrs.field` in new code (``attr.ib`` will *never* + go away, though). + + :param default: A value that is used if an *attrs*-generated ``__init__`` is used and no value is passed while instantiating or the attribute is excluded using ``init=False``. @@ -131,17 +132,17 @@ def attrib( or dicts). If a default is not set (or set manually to `attrs.NOTHING`), a value - *must* be supplied when instantiating; otherwise a `TypeError` - will be raised. + *must* be supplied when instantiating; otherwise a `TypeError` will be + raised. The default can also be set using decorator notation as shown below. - :type default: Any value + .. seealso:: `defaults` :param callable factory: Syntactic sugar for ``default=attr.Factory(factory)``. - :param validator: `callable` that is called by ``attrs``-generated + :param validator: `callable` that is called by *attrs*-generated ``__init__`` methods after the instance has been initialized. They receive the initialized instance, the :func:`~attrs.Attribute`, and the passed value. @@ -149,77 +150,90 @@ def attrib( The return value is *not* inspected so the validator has to throw an exception itself. - If a `list` is passed, its items are treated as validators and must - all pass. + If a `list` is passed, its items are treated as validators and must all + pass. Validators can be globally disabled and re-enabled using - `get_run_validators`. + `attrs.validators.get_disabled` / `attrs.validators.set_disabled`. The validator can also be set using decorator notation as shown below. + .. seealso:: :ref:`validators` + :type validator: `callable` or a `list` of `callable`\\ s. - :param repr: Include this attribute in the generated ``__repr__`` - method. If ``True``, include the attribute; if ``False``, omit it. By - default, the built-in ``repr()`` function is used. To override how the - attribute value is formatted, pass a ``callable`` that takes a single - value and returns a string. Note that the resulting string is used - as-is, i.e. it will be used directly *instead* of calling ``repr()`` - (the default). + :param repr: Include this attribute in the generated ``__repr__`` method. + If ``True``, include the attribute; if ``False``, omit it. By default, + the built-in ``repr()`` function is used. To override how the attribute + value is formatted, pass a ``callable`` that takes a single value and + returns a string. Note that the resulting string is used as-is, i.e. it + will be used directly *instead* of calling ``repr()`` (the default). :type repr: a `bool` or a `callable` to use a custom function. - :param eq: If ``True`` (default), include this attribute in the - generated ``__eq__`` and ``__ne__`` methods that check two instances - for equality. To override how the attribute value is compared, - pass a ``callable`` that takes a single value and returns the value - to be compared. + :param eq: If ``True`` (default), include this attribute in the generated + ``__eq__`` and ``__ne__`` methods that check two instances for + equality. To override how the attribute value is compared, pass a + ``callable`` that takes a single value and returns the value to be + compared. + + .. seealso:: `comparison` :type eq: a `bool` or a `callable`. :param order: If ``True`` (default), include this attributes in the - generated ``__lt__``, ``__le__``, ``__gt__`` and ``__ge__`` methods. - To override how the attribute value is ordered, - pass a ``callable`` that takes a single value and returns the value - to be ordered. + generated ``__lt__``, ``__le__``, ``__gt__`` and ``__ge__`` methods. To + override how the attribute value is ordered, pass a ``callable`` that + takes a single value and returns the value to be ordered. + + .. seealso:: `comparison` :type order: a `bool` or a `callable`. :param cmp: Setting *cmp* is equivalent to setting *eq* and *order* to the same value. Must not be mixed with *eq* or *order*. + + .. seealso:: `comparison` :type cmp: a `bool` or a `callable`. - :param Optional[bool] hash: Include this attribute in the generated + :param bool | None hash: Include this attribute in the generated ``__hash__`` method. If ``None`` (default), mirror *eq*'s value. This is the correct behavior according the Python spec. Setting this value to anything else than ``None`` is *discouraged*. + + .. seealso:: `hashing` :param bool init: Include this attribute in the generated ``__init__`` method. It is possible to set this to ``False`` and set a default value. In that case this attributed is unconditionally initialized with the specified default value or factory. - :param callable converter: `callable` that is called by - ``attrs``-generated ``__init__`` methods to convert attribute's value - to the desired format. It is given the passed-in value, and the - returned value will be used as the new value of the attribute. The - value is converted before being passed to the validator, if any. - :param metadata: An arbitrary mapping, to be used by third-party - components. See `extending_metadata`. - :param type: The type of the attribute. In Python 3.6 or greater, the - preferred method to specify the type is using a variable annotation - (see :pep:`526`). - This argument is provided for backward compatibility. - Regardless of the approach used, the type will be stored on - ``Attribute.type``. - Please note that ``attrs`` doesn't do anything with this metadata by - itself. You can use it as part of your own code or for - `static type checking `. - :param kw_only: Make this attribute keyword-only (Python 3+) - in the generated ``__init__`` (if ``init`` is ``False``, this - parameter is ignored). + .. seealso:: `init` + :param callable converter: `callable` that is called by *attrs*-generated + ``__init__`` methods to convert attribute's value to the desired + format. It is given the passed-in value, and the returned value will + be used as the new value of the attribute. The value is converted + before being passed to the validator, if any. + + .. seealso:: :ref:`converters` + :param dict | None metadata: An arbitrary mapping, to be used by + third-party components. See `extending-metadata`. + + :param type: The type of the attribute. Nowadays, the preferred method to + specify the type is using a variable annotation (see :pep:`526`). This + argument is provided for backward compatibility. Regardless of the + approach used, the type will be stored on ``Attribute.type``. + + Please note that *attrs* doesn't do anything with this metadata by + itself. You can use it as part of your own code or for `static type + checking `. + :param bool kw_only: Make this attribute keyword-only in the generated + ``__init__`` (if ``init`` is ``False``, this parameter is ignored). :param on_setattr: Allows to overwrite the *on_setattr* setting from `attr.s`. If left `None`, the *on_setattr* value from `attr.s` is used. Set to `attrs.setters.NO_OP` to run **no** `setattr` hooks for this attribute -- regardless of the setting in `attr.s`. :type on_setattr: `callable`, or a list of callables, or `None`, or `attrs.setters.NO_OP` + :param str | None alias: Override this attribute's parameter name in the + generated ``__init__`` method. If left `None`, default to ``name`` + stripped of leading underscores. See `private-attributes`. .. versionadded:: 15.2.0 *convert* .. versionadded:: 16.3.0 *metadata* @@ -242,24 +256,25 @@ def attrib( .. versionchanged:: 21.1.0 *eq*, *order*, and *cmp* also accept a custom callable .. versionchanged:: 21.1.0 *cmp* undeprecated + .. versionadded:: 22.2.0 *alias* """ eq, eq_key, order, order_key = _determine_attrib_eq_order( cmp, eq, order, True ) if hash is not None and hash is not True and hash is not False: - raise TypeError( - "Invalid value for hash. Must be True, False, or None." - ) + msg = "Invalid value for hash. Must be True, False, or None." + raise TypeError(msg) if factory is not None: if default is not NOTHING: - raise ValueError( - "The `default` and `factory` arguments are mutually " - "exclusive." + msg = ( + "The `default` and `factory` arguments are mutually exclusive." ) + raise ValueError(msg) if not callable(factory): - raise ValueError("The `factory` argument must be a callable.") + msg = "The `factory` argument must be a callable." + raise ValueError(msg) default = Factory(factory) if metadata is None: @@ -291,6 +306,7 @@ def attrib( order=order, order_key=order_key, on_setattr=on_setattr, + alias=alias, ) @@ -322,9 +338,9 @@ def _make_method(name, script, filename, globs): old_val = linecache.cache.setdefault(filename, linecache_tuple) if old_val == linecache_tuple: break - else: - filename = "{}-{}>".format(base_filename[:-1], count) - count += 1 + + filename = f"{base_filename[:-1]}-{count}>" + count += 1 _compile_and_eval(script, globs, locs, filename) @@ -341,15 +357,15 @@ def _make_attr_tuple_class(cls_name, attr_names): __slots__ = () x = property(itemgetter(0)) """ - attr_class_name = "{}Attributes".format(cls_name) + attr_class_name = f"{cls_name}Attributes" attr_class_template = [ - "class {}(tuple):".format(attr_class_name), + f"class {attr_class_name}(tuple):", " __slots__ = ()", ] if attr_names: for i, attr_name in enumerate(attr_names): attr_class_template.append( - _tuple_property_pat.format(index=i, attr_name=attr_name) + f" {attr_name} = _attrs_property(_attrs_itemgetter({i}))" ) else: attr_class_template.append(" pass") @@ -393,8 +409,6 @@ def _is_class_var(annot): def _has_own_attribute(cls, attrib_name): """ Check whether *cls* defines *attrib_name* (and doesn't just inherit it). - - Requires Python 3. """ attr = getattr(cls, attrib_name, _sentinel) if attr is _sentinel: @@ -418,13 +432,6 @@ def _get_annotations(cls): return {} -def _counter_getter(e): - """ - Key function for sorting to avoid re-creating a lambda for every class. - """ - return e[1].counter - - def _collect_base_attrs(cls, taken_attr_names): """ Collect attr.ibs from base classes of *cls*, except *taken_attr_names*. @@ -438,7 +445,7 @@ def _collect_base_attrs(cls, taken_attr_names): if a.inherited or a.name in taken_attr_names: continue - a = a.evolve(inherited=True) + a = a.evolve(inherited=True) # noqa: PLW2901 base_attrs.append(a) base_attr_map[a.name] = base_cls @@ -476,7 +483,7 @@ def _collect_base_attrs_broken(cls, taken_attr_names): if a.name in taken_attr_names: continue - a = a.evolve(inherited=True) + a = a.evolve(inherited=True) # noqa: PLW2901 taken_attr_names.add(a.name) base_attrs.append(a) base_attr_map[a.name] = base_cls @@ -501,10 +508,7 @@ def _transform_attrs( anns = _get_annotations(cls) if these is not None: - ca_list = [(name, ca) for name, ca in these.items()] - - if not isinstance(these, ordered_dict): - ca_list.sort(key=_counter_getter) + ca_list = list(these.items()) elif auto_attribs is True: ca_names = { name @@ -520,10 +524,7 @@ def _transform_attrs( a = cd.get(attr_name, NOTHING) if not isinstance(a, _CountingAttr): - if a is NOTHING: - a = attrib() - else: - a = attrib(default=a) + a = attrib() if a is NOTHING else attrib(default=a) ca_list.append((attr_name, a)) unannotated = ca_names - annot_names @@ -574,10 +575,8 @@ def _transform_attrs( had_default = False for a in (a for a in attrs if a.init is not False and a.kw_only is False): if had_default is True and a.default is NOTHING: - raise ValueError( - "No mandatory attributes allowed after an attribute with a " - "default value or factory. Attribute in question: %r" % (a,) - ) + msg = f"No mandatory attributes allowed after an attribute with a default value or factory. Attribute in question: {a!r}" + raise ValueError(msg) if had_default is False and a.default is not NOTHING: had_default = True @@ -585,6 +584,14 @@ def _transform_attrs( if field_transformer is not None: attrs = field_transformer(cls, attrs) + # Resolve default field alias after executing field_transformer. + # This allows field_transformer to differentiate between explicit vs + # default aliases and supply their own defaults. + attrs = [ + a.evolve(alias=_default_init_alias_for(a.name)) if not a.alias else a + for a in attrs + ] + # Create AttrsClass *after* applying the field_transformer since it may # add or remove attributes! attr_names = [a.name for a in attrs] @@ -593,28 +600,75 @@ def _transform_attrs( return _Attributes((AttrsClass(attrs), base_attrs, base_attr_map)) -if PYPY: +def _make_cached_property_getattr( + cached_properties, + original_getattr, + cls, +): + lines = [ + # Wrapped to get `__class__` into closure cell for super() + # (It will be replaced with the newly constructed class after construction). + "def wrapper():", + " __class__ = _cls", + " def __getattr__(self, item, cached_properties=cached_properties, original_getattr=original_getattr, _cached_setattr_get=_cached_setattr_get):", + " func = cached_properties.get(item)", + " if func is not None:", + " result = func(self)", + " _setter = _cached_setattr_get(self)", + " _setter(item, result)", + " return result", + ] + if original_getattr is not None: + lines.append( + " return original_getattr(self, item)", + ) + else: + lines.extend( + [ + " if hasattr(super(), '__getattr__'):", + " return super().__getattr__(item)", + " original_error = f\"'{self.__class__.__name__}' object has no attribute '{item}'\"", + " raise AttributeError(original_error)", + ] + ) - def _frozen_setattrs(self, name, value): - """ - Attached to frozen classes as __setattr__. - """ - if isinstance(self, BaseException) and name in ( - "__cause__", - "__context__", - ): - BaseException.__setattr__(self, name, value) - return + lines.extend( + [ + " return __getattr__", + "__getattr__ = wrapper()", + ] + ) - raise FrozenInstanceError() + unique_filename = _generate_unique_filename(cls, "getattr") -else: + glob = { + "cached_properties": cached_properties, + "_cached_setattr_get": _obj_setattr.__get__, + "_cls": cls, + "original_getattr": original_getattr, + } - def _frozen_setattrs(self, name, value): - """ - Attached to frozen classes as __setattr__. - """ - raise FrozenInstanceError() + return _make_method( + "__getattr__", + "\n".join(lines), + unique_filename, + glob, + ) + + +def _frozen_setattrs(self, name, value): + """ + Attached to frozen classes as __setattr__. + """ + if isinstance(self, BaseException) and name in ( + "__cause__", + "__context__", + "__traceback__", + ): + BaseException.__setattr__(self, name, value) + return + + raise FrozenInstanceError() def _frozen_delattrs(self, name): @@ -640,6 +694,7 @@ class _ClassBuilder: "_delete_attribs", "_frozen", "_has_pre_init", + "_pre_init_has_args", "_has_post_init", "_is_exc", "_on_setattr", @@ -686,6 +741,13 @@ class _ClassBuilder: self._weakref_slot = weakref_slot self._cache_hash = cache_hash self._has_pre_init = bool(getattr(cls, "__attrs_pre_init__", False)) + self._pre_init_has_args = False + if self._has_pre_init: + # Check if the pre init method has more arguments than just `self` + # We want to pass arguments if pre init expects arguments + pre_init_func = cls.__attrs_pre_init__ + pre_init_signature = inspect.signature(pre_init_func) + self._pre_init_has_args = len(pre_init_signature.parameters) > 1 self._has_post_init = bool(getattr(cls, "__attrs_post_init__", False)) self._delete_attribs = not bool(these) self._is_exc = is_exc @@ -735,17 +797,35 @@ class _ClassBuilder: ) = self._make_getstate_setstate() def __repr__(self): - return "<_ClassBuilder(cls={cls})>".format(cls=self._cls.__name__) + return f"<_ClassBuilder(cls={self._cls.__name__})>" - def build_class(self): - """ - Finalize class based on the accumulated configuration. + if PY310: + import abc + + def build_class(self): + """ + Finalize class based on the accumulated configuration. + + Builder cannot be used after calling this method. + """ + if self._slots is True: + return self._create_slots_class() + + return self.abc.update_abstractmethods( + self._patch_original_class() + ) + + else: + + def build_class(self): + """ + Finalize class based on the accumulated configuration. + + Builder cannot be used after calling this method. + """ + if self._slots is True: + return self._create_slots_class() - Builder cannot be used after calling this method. - """ - if self._slots is True: - return self._create_slots_class() - else: return self._patch_original_class() def _patch_original_class(self): @@ -762,13 +842,11 @@ class _ClassBuilder: name not in base_names and getattr(cls, name, _sentinel) is not _sentinel ): - try: + # An AttributeError can happen if a base class defines a + # class variable and we want to set an attribute with the + # same name by using only a type annotation. + with contextlib.suppress(AttributeError): delattr(cls, name) - except AttributeError: - # This can happen if a base class defines a class - # variable and we want to set an attribute with the - # same name by using only a type annotation. - pass # Attach our dunder methods. for name, value in self._cls_dict.items(): @@ -793,7 +871,7 @@ class _ClassBuilder: cd = { k: v for k, v in self._cls_dict.items() - if k not in tuple(self._attr_names) + ("__dict__", "__weakref__") + if k not in (*tuple(self._attr_names), "__dict__", "__weakref__") } # If our class doesn't have its own implementation of __setattr__ @@ -815,7 +893,7 @@ class _ClassBuilder: # Traverse the MRO to collect existing slots # and check for an existing __weakref__. - existing_slots = dict() + existing_slots = {} weakref_inherited = False for base_cls in self._cls.__mro__[1:-1]: if base_cls.__dict__.get("__weakref__", None) is not None: @@ -838,9 +916,46 @@ class _ClassBuilder: ): names += ("__weakref__",) + if PY_3_8_PLUS: + cached_properties = { + name: cached_property.func + for name, cached_property in cd.items() + if isinstance(cached_property, functools.cached_property) + } + else: + # `functools.cached_property` was introduced in 3.8. + # So can't be used before this. + cached_properties = {} + + # Collect methods with a `__class__` reference that are shadowed in the new class. + # To know to update them. + additional_closure_functions_to_update = [] + if cached_properties: + # Add cached properties to names for slotting. + names += tuple(cached_properties.keys()) + + for name in cached_properties: + # Clear out function from class to avoid clashing. + del cd[name] + + class_annotations = _get_annotations(self._cls) + for name, func in cached_properties.items(): + annotation = inspect.signature(func).return_annotation + if annotation is not inspect.Parameter.empty: + class_annotations[name] = annotation + + original_getattr = cd.get("__getattr__") + if original_getattr is not None: + additional_closure_functions_to_update.append(original_getattr) + + cd["__getattr__"] = _make_cached_property_getattr( + cached_properties, original_getattr, self._cls + ) + # We only add the names of attributes that aren't inherited. # Setting __slots__ to inherited attributes wastes memory. slot_names = [name for name in names if name not in base_names] + # There are slots for attributes from current class # that are defined in parent classes. # As their descriptors may be overridden by a child class, @@ -854,6 +969,7 @@ class _ClassBuilder: cd.update(reused_slots) if self._cache_hash: slot_names.append(_hash_cache_field) + cd["__slots__"] = tuple(slot_names) cd["__qualname__"] = self._cls.__qualname__ @@ -862,12 +978,14 @@ class _ClassBuilder: cls = type(self._cls)(self._cls.__name__, self._cls.__bases__, cd) # The following is a fix for - # . On Python 3, - # if a method mentions `__class__` or uses the no-arg super(), the + # . + # If a method mentions `__class__` or uses the no-arg super(), the # compiler will bake a reference to the class in the method itself # as `method.__closure__`. Since we replace the class with a # clone, we rewrite these references so it keeps working. - for item in cls.__dict__.values(): + for item in itertools.chain( + cls.__dict__.values(), additional_closure_functions_to_update + ): if isinstance(item, (classmethod, staticmethod)): # Class- and staticmethods hide their functions inside. # These might need to be rewritten as well. @@ -884,12 +1002,12 @@ class _ClassBuilder: for cell in closure_cells: try: match = cell.cell_contents is self._cls - except ValueError: # ValueError: Cell is empty + except ValueError: # noqa: PERF203 + # ValueError: Cell is empty pass else: if match: - set_closure_cell(cell, cls) - + cell.cell_contents = cls return cls def add_repr(self, ns): @@ -901,9 +1019,8 @@ class _ClassBuilder: def add_str(self): repr = self._cls_dict.get("__repr__") if repr is None: - raise ValueError( - "__str__ can only be generated if a __repr__ exists." - ) + msg = "__str__ can only be generated if a __repr__ exists." + raise ValueError(msg) def __str__(self): return self.__repr__() @@ -924,7 +1041,7 @@ class _ClassBuilder: """ Automatically created by attrs. """ - return tuple(getattr(self, name) for name in state_attr_names) + return {name: getattr(self, name) for name in state_attr_names} hash_caching_enabled = self._cache_hash @@ -932,9 +1049,16 @@ class _ClassBuilder: """ Automatically created by attrs. """ - __bound_setattr = _obj_setattr.__get__(self, Attribute) - for name, value in zip(state_attr_names, state): - __bound_setattr(name, value) + __bound_setattr = _obj_setattr.__get__(self) + if isinstance(state, tuple): + # Backward compatibility with attrs instances pickled with + # attrs versions before v22.2.0 which stored tuples. + for name, value in zip(state_attr_names, state): + __bound_setattr(name, value) + else: + for name in state_attr_names: + if name in state: + __bound_setattr(name, state[name]) # The hash code cache is not included when the object is # serialized, but it still needs to be initialized to None to @@ -967,6 +1091,7 @@ class _ClassBuilder: self._cls, self._attrs, self._has_pre_init, + self._pre_init_has_args, self._has_post_init, self._frozen, self._slots, @@ -993,6 +1118,7 @@ class _ClassBuilder: self._cls, self._attrs, self._has_pre_init, + self._pre_init_has_args, self._has_post_init, self._frozen, self._slots, @@ -1041,9 +1167,8 @@ class _ClassBuilder: if self._has_custom_setattr: # We need to write a __setattr__ but there already is one! - raise ValueError( - "Can't combine custom __setattr__ with on_setattr hooks." - ) + msg = "Can't combine custom __setattr__ with on_setattr hooks." + raise ValueError(msg) # docstring comes from _add_method_dunders def __setattr__(self, name, val): @@ -1066,24 +1191,17 @@ class _ClassBuilder: """ Add __module__ and __qualname__ to a *method* if possible. """ - try: + with contextlib.suppress(AttributeError): method.__module__ = self._cls.__module__ - except AttributeError: - pass - try: - method.__qualname__ = ".".join( - (self._cls.__qualname__, method.__name__) - ) - except AttributeError: - pass + with contextlib.suppress(AttributeError): + method.__qualname__ = f"{self._cls.__qualname__}.{method.__name__}" - try: - method.__doc__ = "Method generated by attrs for class %s." % ( - self._cls.__qualname__, + with contextlib.suppress(AttributeError): + method.__doc__ = ( + "Method generated by attrs for class " + f"{self._cls.__qualname__}." ) - except AttributeError: - pass return method @@ -1094,7 +1212,8 @@ def _determine_attrs_eq_order(cmp, eq, order, default_eq): values of eq and order. If *eq* is None, set it to *default_eq*. """ if cmp is not None and any((eq is not None, order is not None)): - raise ValueError("Don't mix `cmp` with `eq' and `order`.") + msg = "Don't mix `cmp` with `eq' and `order`." + raise ValueError(msg) # cmp takes precedence due to bw-compatibility. if cmp is not None: @@ -1109,7 +1228,8 @@ def _determine_attrs_eq_order(cmp, eq, order, default_eq): order = eq if eq is False and order is True: - raise ValueError("`order` can only be True if `eq` is True too.") + msg = "`order` can only be True if `eq` is True too." + raise ValueError(msg) return eq, order @@ -1120,7 +1240,8 @@ def _determine_attrib_eq_order(cmp, eq, order, default_eq): values of eq and order. If *eq* is None, set it to *default_eq*. """ if cmp is not None and any((eq is not None, order is not None)): - raise ValueError("Don't mix `cmp` with `eq' and `order`.") + msg = "Don't mix `cmp` with `eq' and `order`." + raise ValueError(msg) def decide_callable_or_boolean(value): """ @@ -1150,7 +1271,8 @@ def _determine_attrib_eq_order(cmp, eq, order, default_eq): order, order_key = decide_callable_or_boolean(order) if eq is False and order is True: - raise ValueError("`order` can only be True if `eq` is True too.") + msg = "`order` can only be True if `eq` is True too." + raise ValueError(msg) return eq, eq_key, order, order_key @@ -1205,24 +1327,24 @@ def attrs( on_setattr=None, field_transformer=None, match_args=True, + unsafe_hash=None, ): r""" - A class decorator that adds `dunder - `_\ -methods according to the + A class decorator that adds :term:`dunder methods` according to the specified attributes using `attr.ib` or the *these* argument. - :param these: A dictionary of name to `attr.ib` mappings. This is - useful to avoid the definition of your attributes within the class body + Please consider using `attrs.define` / `attrs.frozen` in new code + (``attr.s`` will *never* go away, though). + + :param these: A dictionary of name to `attr.ib` mappings. This is useful + to avoid the definition of your attributes within the class body because you can't (e.g. if you want to add ``__repr__`` methods to Django models) or don't want to. - If *these* is not ``None``, ``attrs`` will *not* search the class body + If *these* is not ``None``, *attrs* will *not* search the class body for attributes and will *not* remove any attributes from it. - If *these* is an ordered dict (`dict` on Python 3.6+, - `collections.OrderedDict` otherwise), the order is deduced from - the order of the attributes inside *these*. Otherwise the order - of the definition of the attributes is used. + The order is deduced from the order of the attributes inside *these*. :type these: `dict` of `str` to `attr.ib` @@ -1235,79 +1357,89 @@ def attrs( arguments is implemented in the *current* class (i.e. it is *not* inherited from some base class). - So for example by implementing ``__eq__`` on a class yourself, - ``attrs`` will deduce ``eq=False`` and will create *neither* - ``__eq__`` *nor* ``__ne__`` (but Python classes come with a sensible - ``__ne__`` by default, so it *should* be enough to only implement - ``__eq__`` in most cases). + So for example by implementing ``__eq__`` on a class yourself, *attrs* + will deduce ``eq=False`` and will create *neither* ``__eq__`` *nor* + ``__ne__`` (but Python classes come with a sensible ``__ne__`` by + default, so it *should* be enough to only implement ``__eq__`` in most + cases). .. warning:: - If you prevent ``attrs`` from creating the ordering methods for you + If you prevent *attrs* from creating the ordering methods for you (``order=False``, e.g. by implementing ``__le__``), it becomes *your* responsibility to make sure its ordering is sound. The best way is to use the `functools.total_ordering` decorator. - Passing ``True`` or ``False`` to *init*, *repr*, *eq*, *order*, - *cmp*, or *hash* overrides whatever *auto_detect* would determine. - - *auto_detect* requires Python 3. Setting it ``True`` on Python 2 raises - an `attrs.exceptions.PythonTooOldError`. + Passing ``True`` or ``False`` to *init*, *repr*, *eq*, *order*, *cmp*, + or *hash* overrides whatever *auto_detect* would determine. :param bool repr: Create a ``__repr__`` method with a human readable - representation of ``attrs`` attributes.. + representation of *attrs* attributes.. :param bool str: Create a ``__str__`` method that is identical to - ``__repr__``. This is usually not necessary except for - `Exception`\ s. - :param Optional[bool] eq: If ``True`` or ``None`` (default), add ``__eq__`` + ``__repr__``. This is usually not necessary except for `Exception`\ s. + :param bool | None eq: If ``True`` or ``None`` (default), add ``__eq__`` and ``__ne__`` methods that check two instances for equality. - They compare the instances as if they were tuples of their ``attrs`` + They compare the instances as if they were tuples of their *attrs* attributes if and only if the types of both classes are *identical*! - :param Optional[bool] order: If ``True``, add ``__lt__``, ``__le__``, + + .. seealso:: `comparison` + :param bool | None order: If ``True``, add ``__lt__``, ``__le__``, ``__gt__``, and ``__ge__`` methods that behave like *eq* above and allow instances to be ordered. If ``None`` (default) mirror value of *eq*. - :param Optional[bool] cmp: Setting *cmp* is equivalent to setting *eq* - and *order* to the same value. Must not be mixed with *eq* or *order*. - :param Optional[bool] hash: If ``None`` (default), the ``__hash__`` method - is generated according how *eq* and *frozen* are set. - 1. If *both* are True, ``attrs`` will generate a ``__hash__`` for you. + .. seealso:: `comparison` + :param bool | None cmp: Setting *cmp* is equivalent to setting *eq* and + *order* to the same value. Must not be mixed with *eq* or *order*. + + .. seealso:: `comparison` + :param bool | None unsafe_hash: If ``None`` (default), the ``__hash__`` + method is generated according how *eq* and *frozen* are set. + + 1. If *both* are True, *attrs* will generate a ``__hash__`` for you. 2. If *eq* is True and *frozen* is False, ``__hash__`` will be set to None, marking it unhashable (which it is). 3. If *eq* is False, ``__hash__`` will be left untouched meaning the ``__hash__`` method of the base class will be used (if base class is ``object``, this means it will fall back to id-based hashing.). - Although not recommended, you can decide for yourself and force - ``attrs`` to create one (e.g. if the class is immutable even though you - didn't freeze it programmatically) by passing ``True`` or not. Both of - these cases are rather special and should be used carefully. + Although not recommended, you can decide for yourself and force *attrs* + to create one (e.g. if the class is immutable even though you didn't + freeze it programmatically) by passing ``True`` or not. Both of these + cases are rather special and should be used carefully. - See our documentation on `hashing`, Python's documentation on - `object.__hash__`, and the `GitHub issue that led to the default \ - behavior `_ for more - details. - :param bool init: Create a ``__init__`` method that initializes the - ``attrs`` attributes. Leading underscores are stripped for the argument - name. If a ``__attrs_pre_init__`` method exists on the class, it will - be called before the class is initialized. If a ``__attrs_post_init__`` - method exists on the class, it will be called after the class is fully + .. seealso:: + + - Our documentation on `hashing`, + - Python's documentation on `object.__hash__`, + - and the `GitHub issue that led to the default \ + behavior `_ for + more details. + + :param bool | None hash: Alias for *unsafe_hash*. *unsafe_hash* takes + precedence. + :param bool init: Create a ``__init__`` method that initializes the *attrs* + attributes. Leading underscores are stripped for the argument name. If + a ``__attrs_pre_init__`` method exists on the class, it will be called + before the class is initialized. If a ``__attrs_post_init__`` method + exists on the class, it will be called after the class is fully initialized. - If ``init`` is ``False``, an ``__attrs_init__`` method will be - injected instead. This allows you to define a custom ``__init__`` - method that can do pre-init work such as ``super().__init__()``, - and then call ``__attrs_init__()`` and ``__attrs_post_init__()``. - :param bool slots: Create a `slotted class ` that's more - memory-efficient. Slotted classes are generally superior to the default - dict classes, but have some gotchas you should know about, so we - encourage you to read the `glossary entry `. + If ``init`` is ``False``, an ``__attrs_init__`` method will be injected + instead. This allows you to define a custom ``__init__`` method that + can do pre-init work such as ``super().__init__()``, and then call + ``__attrs_init__()`` and ``__attrs_post_init__()``. + + .. seealso:: `init` + :param bool slots: Create a :term:`slotted class ` that's + more memory-efficient. Slotted classes are generally superior to the + default dict classes, but have some gotchas you should know about, so + we encourage you to read the :term:`glossary entry `. :param bool frozen: Make instances immutable after initialization. If someone attempts to modify a frozen instance, - `attr.exceptions.FrozenInstanceError` is raised. + `attrs.exceptions.FrozenInstanceError` is raised. .. note:: @@ -1322,21 +1454,21 @@ def attrs( 4. If a class is frozen, you cannot modify ``self`` in ``__attrs_post_init__`` or a self-written ``__init__``. You can - circumvent that limitation by using - ``object.__setattr__(self, "attribute_name", value)``. + circumvent that limitation by using ``object.__setattr__(self, + "attribute_name", value)``. 5. Subclasses of a frozen class are frozen too. :param bool weakref_slot: Make instances weak-referenceable. This has no effect unless ``slots`` is also enabled. :param bool auto_attribs: If ``True``, collect :pep:`526`-annotated - attributes (Python 3.6 and later only) from the class body. + attributes from the class body. - In this case, you **must** annotate every field. If ``attrs`` - encounters a field that is set to an `attr.ib` but lacks a type - annotation, an `attr.exceptions.UnannotatedAttributeError` is - raised. Use ``field_name: typing.Any = attr.ib(...)`` if you don't - want to set a type. + In this case, you **must** annotate every field. If *attrs* encounters + a field that is set to an `attr.ib` but lacks a type annotation, an + `attr.exceptions.UnannotatedAttributeError` is raised. Use + ``field_name: typing.Any = attr.ib(...)`` if you don't want to set a + type. If you assign a value to those attributes (e.g. ``x: int = 42``), that value becomes the default value like if it were passed using @@ -1348,57 +1480,55 @@ def attrs( .. warning:: For features that use the attribute name to create decorators (e.g. - `validators `), you still *must* assign `attr.ib` to - them. Otherwise Python will either not find the name or try to use - the default value to call e.g. ``validator`` on it. + :ref:`validators `), you still *must* assign `attr.ib` + to them. Otherwise Python will either not find the name or try to + use the default value to call e.g. ``validator`` on it. These errors can be quite confusing and probably the most common bug report on our bug tracker. - :param bool kw_only: Make all attributes keyword-only (Python 3+) - in the generated ``__init__`` (if ``init`` is ``False``, this - parameter is ignored). - :param bool cache_hash: Ensure that the object's hash code is computed - only once and stored on the object. If this is set to ``True``, - hashing must be either explicitly or implicitly enabled for this - class. If the hash code is cached, avoid any reassignments of - fields involved in hash code computation or mutations of the objects - those fields point to after object creation. If such changes occur, - the behavior of the object's hash code is undefined. - :param bool auto_exc: If the class subclasses `BaseException` - (which implicitly includes any subclass of any exception), the - following happens to behave like a well-behaved Python exceptions - class: + :param bool kw_only: Make all attributes keyword-only in the generated + ``__init__`` (if ``init`` is ``False``, this parameter is ignored). + :param bool cache_hash: Ensure that the object's hash code is computed only + once and stored on the object. If this is set to ``True``, hashing + must be either explicitly or implicitly enabled for this class. If the + hash code is cached, avoid any reassignments of fields involved in hash + code computation or mutations of the objects those fields point to + after object creation. If such changes occur, the behavior of the + object's hash code is undefined. + :param bool auto_exc: If the class subclasses `BaseException` (which + implicitly includes any subclass of any exception), the following + happens to behave like a well-behaved Python exceptions class: - the values for *eq*, *order*, and *hash* are ignored and the - instances compare and hash by the instance's ids (N.B. ``attrs`` will + instances compare and hash by the instance's ids (N.B. *attrs* will *not* remove existing implementations of ``__hash__`` or the equality methods. It just won't add own ones.), - all attributes that are either passed into ``__init__`` or have a default value are additionally available as a tuple in the ``args`` attribute, - the value of *str* is ignored leaving ``__str__`` to base classes. - :param bool collect_by_mro: Setting this to `True` fixes the way ``attrs`` + :param bool collect_by_mro: Setting this to `True` fixes the way *attrs* collects attributes from base classes. The default behavior is incorrect in certain cases of multiple inheritance. It should be on by default but is kept off for backward-compatibility. - See issue `#428 `_ for - more details. + .. seealso:: + Issue `#428 `_ - :param Optional[bool] getstate_setstate: + :param bool | None getstate_setstate: .. note:: This is usually only interesting for slotted classes and you should probably just set *auto_detect* to `True`. - If `True`, ``__getstate__`` and - ``__setstate__`` are generated and attached to the class. This is - necessary for slotted classes to be pickleable. If left `None`, it's - `True` by default for slotted classes and ``False`` for dict classes. + If `True`, ``__getstate__`` and ``__setstate__`` are generated and + attached to the class. This is necessary for slotted classes to be + pickleable. If left `None`, it's `True` by default for slotted classes + and ``False`` for dict classes. - If *auto_detect* is `True`, and *getstate_setstate* is left `None`, - and **either** ``__getstate__`` or ``__setstate__`` is detected directly - on the class (i.e. not inherited), it is set to `False` (this is usually + If *auto_detect* is `True`, and *getstate_setstate* is left `None`, and + **either** ``__getstate__`` or ``__setstate__`` is detected directly on + the class (i.e. not inherited), it is set to `False` (this is usually what you want). :param on_setattr: A callable that is run whenever the user attempts to set @@ -1415,11 +1545,13 @@ def attrs( :type on_setattr: `callable`, or a list of callables, or `None`, or `attrs.setters.NO_OP` - :param Optional[callable] field_transformer: - A function that is called with the original class object and all - fields right before ``attrs`` finalizes the class. You can use - this, e.g., to automatically add converters or validators to - fields based on their types. See `transform-fields` for more details. + :param callable | None field_transformer: + A function that is called with the original class object and all fields + right before *attrs* finalizes the class. You can use this, e.g., to + automatically add converters or validators to fields based on their + types. + + .. seealso:: `transform-fields` :param bool match_args: If `True` (default), set ``__match_args__`` on the class to support @@ -1461,9 +1593,14 @@ def attrs( .. versionchanged:: 21.1.0 Support for ``__attrs_pre_init__`` .. versionchanged:: 21.1.0 *cmp* undeprecated .. versionadded:: 21.3.0 *match_args* + .. versionadded:: 22.2.0 + *unsafe_hash* as an alias for *hash* (for :pep:`681` compliance). """ eq_, order_ = _determine_attrs_eq_order(cmp, eq, order, None) - hash_ = hash # work around the lack of nonlocal + + # unsafe_hash takes precedence due to PEP 681. + if unsafe_hash is not None: + hash = unsafe_hash if isinstance(on_setattr, (list, tuple)): on_setattr = setters.pipe(*on_setattr) @@ -1476,7 +1613,8 @@ def attrs( ) if has_own_setattr and is_frozen: - raise ValueError("Can't freeze a class with a custom __setattr__.") + msg = "Can't freeze a class with a custom __setattr__." + raise ValueError(msg) builder = _ClassBuilder( cls, @@ -1519,28 +1657,25 @@ def attrs( builder.add_setattr() + nonlocal hash if ( - hash_ is None + hash is None and auto_detect is True and _has_own_attribute(cls, "__hash__") ): hash = False - else: - hash = hash_ + if hash is not True and hash is not False and hash is not None: # Can't use `hash in` because 1 == True for example. - raise TypeError( - "Invalid value for hash. Must be True, False, or None." - ) - elif hash is False or (hash is None and eq is False) or is_exc: + msg = "Invalid value for hash. Must be True, False, or None." + raise TypeError(msg) + + if hash is False or (hash is None and eq is False) or is_exc: # Don't do anything. Should fall back to __object__'s __hash__ # which is by id. if cache_hash: - raise TypeError( - "Invalid value for cache_hash. To use hash caching," - " hashing must be either explicitly or implicitly " - "enabled." - ) + msg = "Invalid value for cache_hash. To use hash caching, hashing must be either explicitly or implicitly enabled." + raise TypeError(msg) elif hash is True or ( hash is None and eq is True and is_frozen is True ): @@ -1549,11 +1684,8 @@ def attrs( else: # Raise TypeError on attempts to hash. if cache_hash: - raise TypeError( - "Invalid value for cache_hash. To use hash caching," - " hashing must be either explicitly or implicitly " - "enabled." - ) + msg = "Invalid value for cache_hash. To use hash caching, hashing must be either explicitly or implicitly enabled." + raise TypeError(msg) builder.make_unhashable() if _determine_whether_to_implement( @@ -1563,10 +1695,8 @@ def attrs( else: builder.add_attrs_init() if cache_hash: - raise TypeError( - "Invalid value for cache_hash. To use hash caching," - " init must be True." - ) + msg = "Invalid value for cache_hash. To use hash caching, init must be True." + raise TypeError(msg) if ( PY310 @@ -1581,8 +1711,8 @@ def attrs( # if it's used as `@attrs` but ``None`` if used as `@attrs()`. if maybe_cls is None: return wrap - else: - return wrap(maybe_cls) + + return wrap(maybe_cls) _attrs = attrs @@ -1604,12 +1734,10 @@ def _generate_unique_filename(cls, func_name): """ Create a "filename" suitable for a function being generated. """ - unique_filename = "".format( - func_name, - cls.__module__, - getattr(cls, "__qualname__", cls.__name__), + return ( + f"" ) - return unique_filename def _make_hash(cls, attrs, frozen, cache_hash): @@ -1632,10 +1760,7 @@ def _make_hash(cls, attrs, frozen, cache_hash): else: hash_def += ", *" - hash_def += ( - ", _cache_wrapper=" - + "__import__('attr._make')._make._CacheHashWrapper):" - ) + hash_def += ", _cache_wrapper=__import__('attr._make')._make._CacheHashWrapper):" hash_func = "_cache_wrapper(" + hash_func closing_braces += ")" @@ -1651,34 +1776,34 @@ def _make_hash(cls, attrs, frozen, cache_hash): method_lines.extend( [ indent + prefix + hash_func, - indent + " %d," % (type_hash,), + indent + f" {type_hash},", ] ) for a in attrs: if a.eq_key: - cmp_name = "_%s_key" % (a.name,) + cmp_name = f"_{a.name}_key" globs[cmp_name] = a.eq_key method_lines.append( - indent + " %s(self.%s)," % (cmp_name, a.name) + indent + f" {cmp_name}(self.{a.name})," ) else: - method_lines.append(indent + " self.%s," % a.name) + method_lines.append(indent + f" self.{a.name},") method_lines.append(indent + " " + closing_braces) if cache_hash: - method_lines.append(tab + "if self.%s is None:" % _hash_cache_field) + method_lines.append(tab + f"if self.{_hash_cache_field} is None:") if frozen: append_hash_computation_lines( - "object.__setattr__(self, '%s', " % _hash_cache_field, tab * 2 + f"object.__setattr__(self, '{_hash_cache_field}', ", tab * 2 ) method_lines.append(tab * 2 + ")") # close __setattr__ else: append_hash_computation_lines( - "self.%s = " % _hash_cache_field, tab * 2 + f"self.{_hash_cache_field} = ", tab * 2 ) - method_lines.append(tab + "return self.%s" % _hash_cache_field) + method_lines.append(tab + f"return self.{_hash_cache_field}") else: append_hash_computation_lines("return ", tab) @@ -1734,29 +1859,17 @@ def _make_eq(cls, attrs): others = [" ) == ("] for a in attrs: if a.eq_key: - cmp_name = "_%s_key" % (a.name,) + cmp_name = f"_{a.name}_key" # Add the key function to the global namespace # of the evaluated function. globs[cmp_name] = a.eq_key - lines.append( - " %s(self.%s)," - % ( - cmp_name, - a.name, - ) - ) - others.append( - " %s(other.%s)," - % ( - cmp_name, - a.name, - ) - ) + lines.append(f" {cmp_name}(self.{a.name}),") + others.append(f" {cmp_name}(other.{a.name}),") else: - lines.append(" self.%s," % (a.name,)) - others.append(" other.%s," % (a.name,)) + lines.append(f" self.{a.name},") + others.append(f" other.{a.name},") - lines += others + [" )"] + lines += [*others, " )"] else: lines.append(" return True") @@ -1834,126 +1947,61 @@ def _add_eq(cls, attrs=None): return cls -if HAS_F_STRINGS: - - def _make_repr(attrs, ns, cls): - unique_filename = _generate_unique_filename(cls, "repr") - # Figure out which attributes to include, and which function to use to - # format them. The a.repr value can be either bool or a custom - # callable. - attr_names_with_reprs = tuple( - (a.name, (repr if a.repr is True else a.repr), a.init) - for a in attrs - if a.repr is not False +def _make_repr(attrs, ns, cls): + unique_filename = _generate_unique_filename(cls, "repr") + # Figure out which attributes to include, and which function to use to + # format them. The a.repr value can be either bool or a custom + # callable. + attr_names_with_reprs = tuple( + (a.name, (repr if a.repr is True else a.repr), a.init) + for a in attrs + if a.repr is not False + ) + globs = { + name + "_repr": r for name, r, _ in attr_names_with_reprs if r != repr + } + globs["_compat"] = _compat + globs["AttributeError"] = AttributeError + globs["NOTHING"] = NOTHING + attribute_fragments = [] + for name, r, i in attr_names_with_reprs: + accessor = ( + "self." + name if i else 'getattr(self, "' + name + '", NOTHING)' ) - globs = { - name + "_repr": r - for name, r, _ in attr_names_with_reprs - if r != repr - } - globs["_compat"] = _compat - globs["AttributeError"] = AttributeError - globs["NOTHING"] = NOTHING - attribute_fragments = [] - for name, r, i in attr_names_with_reprs: - accessor = ( - "self." + name - if i - else 'getattr(self, "' + name + '", NOTHING)' - ) - fragment = ( - "%s={%s!r}" % (name, accessor) - if r == repr - else "%s={%s_repr(%s)}" % (name, name, accessor) - ) - attribute_fragments.append(fragment) - repr_fragment = ", ".join(attribute_fragments) - - if ns is None: - cls_name_fragment = ( - '{self.__class__.__qualname__.rsplit(">.", 1)[-1]}' - ) - else: - cls_name_fragment = ns + ".{self.__class__.__name__}" - - lines = [ - "def __repr__(self):", - " try:", - " already_repring = _compat.repr_context.already_repring", - " except AttributeError:", - " already_repring = {id(self),}", - " _compat.repr_context.already_repring = already_repring", - " else:", - " if id(self) in already_repring:", - " return '...'", - " else:", - " already_repring.add(id(self))", - " try:", - " return f'%s(%s)'" % (cls_name_fragment, repr_fragment), - " finally:", - " already_repring.remove(id(self))", - ] - - return _make_method( - "__repr__", "\n".join(lines), unique_filename, globs=globs + fragment = ( + "%s={%s!r}" % (name, accessor) + if r == repr + else "%s={%s_repr(%s)}" % (name, name, accessor) ) + attribute_fragments.append(fragment) + repr_fragment = ", ".join(attribute_fragments) -else: + if ns is None: + cls_name_fragment = '{self.__class__.__qualname__.rsplit(">.", 1)[-1]}' + else: + cls_name_fragment = ns + ".{self.__class__.__name__}" - def _make_repr(attrs, ns, _): - """ - Make a repr method that includes relevant *attrs*, adding *ns* to the - full name. - """ + lines = [ + "def __repr__(self):", + " try:", + " already_repring = _compat.repr_context.already_repring", + " except AttributeError:", + " already_repring = {id(self),}", + " _compat.repr_context.already_repring = already_repring", + " else:", + " if id(self) in already_repring:", + " return '...'", + " else:", + " already_repring.add(id(self))", + " try:", + f" return f'{cls_name_fragment}({repr_fragment})'", + " finally:", + " already_repring.remove(id(self))", + ] - # Figure out which attributes to include, and which function to use to - # format them. The a.repr value can be either bool or a custom - # callable. - attr_names_with_reprs = tuple( - (a.name, repr if a.repr is True else a.repr) - for a in attrs - if a.repr is not False - ) - - def __repr__(self): - """ - Automatically created by attrs. - """ - try: - already_repring = _compat.repr_context.already_repring - except AttributeError: - already_repring = set() - _compat.repr_context.already_repring = already_repring - - if id(self) in already_repring: - return "..." - real_cls = self.__class__ - if ns is None: - class_name = real_cls.__qualname__.rsplit(">.", 1)[-1] - else: - class_name = ns + "." + real_cls.__name__ - - # Since 'self' remains on the stack (i.e.: strongly referenced) - # for the duration of this call, it's safe to depend on id(...) - # stability, and not need to track the instance and therefore - # worry about properties like weakref- or hash-ability. - already_repring.add(id(self)) - try: - result = [class_name, "("] - first = True - for name, attr_repr in attr_names_with_reprs: - if first: - first = False - else: - result.append(", ") - result.extend( - (name, "=", attr_repr(getattr(self, name, NOTHING))) - ) - return "".join(result) + ")" - finally: - already_repring.remove(id(self)) - - return __repr__ + return _make_method( + "__repr__", "\n".join(lines), unique_filename, globs=globs + ) def _add_repr(cls, ns=None, attrs=None): @@ -1969,7 +2017,7 @@ def _add_repr(cls, ns=None, attrs=None): def fields(cls): """ - Return the tuple of ``attrs`` attributes for a class. + Return the tuple of *attrs* attributes for a class. The tuple also allows accessing the fields by their names (see below for examples). @@ -1977,50 +2025,61 @@ def fields(cls): :param type cls: Class to introspect. :raise TypeError: If *cls* is not a class. - :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs`` + :raise attrs.exceptions.NotAnAttrsClassError: If *cls* is not an *attrs* class. :rtype: tuple (with name accessors) of `attrs.Attribute` - .. versionchanged:: 16.2.0 Returned tuple allows accessing the fields - by name. + .. versionchanged:: 16.2.0 Returned tuple allows accessing the fields + by name. + .. versionchanged:: 23.1.0 Add support for generic classes. """ - if not isinstance(cls, type): - raise TypeError("Passed object must be a class.") + generic_base = get_generic_base(cls) + + if generic_base is None and not isinstance(cls, type): + msg = "Passed object must be a class." + raise TypeError(msg) + attrs = getattr(cls, "__attrs_attrs__", None) + if attrs is None: - raise NotAnAttrsClassError( - "{cls!r} is not an attrs-decorated class.".format(cls=cls) - ) + if generic_base is not None: + attrs = getattr(generic_base, "__attrs_attrs__", None) + if attrs is not None: + # Even though this is global state, stick it on here to speed + # it up. We rely on `cls` being cached for this to be + # efficient. + cls.__attrs_attrs__ = attrs + return attrs + msg = f"{cls!r} is not an attrs-decorated class." + raise NotAnAttrsClassError(msg) + return attrs def fields_dict(cls): """ - Return an ordered dictionary of ``attrs`` attributes for a class, whose + Return an ordered dictionary of *attrs* attributes for a class, whose keys are the attribute names. :param type cls: Class to introspect. :raise TypeError: If *cls* is not a class. - :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs`` + :raise attrs.exceptions.NotAnAttrsClassError: If *cls* is not an *attrs* class. - :rtype: an ordered dict where keys are attribute names and values are - `attrs.Attribute`\\ s. This will be a `dict` if it's - naturally ordered like on Python 3.6+ or an - :class:`~collections.OrderedDict` otherwise. + :rtype: dict .. versionadded:: 18.1.0 """ if not isinstance(cls, type): - raise TypeError("Passed object must be a class.") + msg = "Passed object must be a class." + raise TypeError(msg) attrs = getattr(cls, "__attrs_attrs__", None) if attrs is None: - raise NotAnAttrsClassError( - "{cls!r} is not an attrs-decorated class.".format(cls=cls) - ) - return ordered_dict((a.name, a) for a in attrs) + msg = f"{cls!r} is not an attrs-decorated class." + raise NotAnAttrsClassError(msg) + return {a.name: a for a in attrs} def validate(inst): @@ -2029,7 +2088,7 @@ def validate(inst): Leaves all exceptions through. - :param inst: Instance of a class with ``attrs`` attributes. + :param inst: Instance of a class with *attrs* attributes. """ if _config._run_validators is False: return @@ -2055,6 +2114,7 @@ def _make_init( cls, attrs, pre_init, + pre_init_has_args, post_init, frozen, slots, @@ -2069,7 +2129,8 @@ def _make_init( ) if frozen and has_cls_on_setattr: - raise ValueError("Frozen classes can't use on_setattr.") + msg = "Frozen classes can't use on_setattr." + raise ValueError(msg) needs_cached_setattr = cache_hash or frozen filtered_attrs = [] @@ -2083,7 +2144,8 @@ def _make_init( if a.on_setattr is not None: if frozen is True: - raise ValueError("Frozen classes can't use on_setattr.") + msg = "Frozen classes can't use on_setattr." + raise ValueError(msg) needs_cached_setattr = True elif has_cls_on_setattr and a.on_setattr is not setters.NO_OP: @@ -2096,10 +2158,12 @@ def _make_init( frozen, slots, pre_init, + pre_init_has_args, post_init, cache_hash, base_attr_map, is_exc, + needs_cached_setattr, has_cls_on_setattr, attrs_init, ) @@ -2112,7 +2176,7 @@ def _make_init( if needs_cached_setattr: # Save the lookup overhead in __init__ if we need to circumvent # setattr hooks. - globs["_setattr"] = _obj_setattr + globs["_cached_setattr_get"] = _obj_setattr.__get__ init = _make_method( "__attrs_init__" if attrs_init else "__init__", @@ -2129,7 +2193,7 @@ def _setattr(attr_name, value_var, has_on_setattr): """ Use the cached object.setattr to set *attr_name* to *value_var*. """ - return "_setattr(self, '%s', %s)" % (attr_name, value_var) + return f"_setattr('{attr_name}', {value_var})" def _setattr_with_converter(attr_name, value_var, has_on_setattr): @@ -2137,7 +2201,7 @@ def _setattr_with_converter(attr_name, value_var, has_on_setattr): Use the cached object.setattr to set *attr_name* to *value_var*, but run its converter first. """ - return "_setattr(self, '%s', %s(%s))" % ( + return "_setattr('%s', %s(%s))" % ( attr_name, _init_converter_pat % (attr_name,), value_var, @@ -2152,7 +2216,7 @@ def _assign(attr_name, value, has_on_setattr): if has_on_setattr: return _setattr(attr_name, value, True) - return "self.%s = %s" % (attr_name, value) + return f"self.{attr_name} = {value}" def _assign_with_converter(attr_name, value_var, has_on_setattr): @@ -2175,10 +2239,12 @@ def _attrs_to_init_script( frozen, slots, pre_init, + pre_init_has_args, post_init, cache_hash, base_attr_map, is_exc, + needs_cached_setattr, has_cls_on_setattr, attrs_init, ): @@ -2194,6 +2260,14 @@ def _attrs_to_init_script( if pre_init: lines.append("self.__attrs_pre_init__()") + if needs_cached_setattr: + lines.append( + # Circumvent the __setattr__ descriptor to save one lookup per + # assignment. + # Note _setattr will be used again below if cache_hash is True + "_setattr = _cached_setattr_get(self)" + ) + if frozen is True: if slots is True: fmt_setter = _setattr @@ -2209,7 +2283,7 @@ def _attrs_to_init_script( if _is_slot_attr(attr_name, base_attr_map): return _setattr(attr_name, value_var, has_on_setattr) - return "_inst_dict['%s'] = %s" % (attr_name, value_var) + return f"_inst_dict['{attr_name}'] = {value_var}" def fmt_setter_with_converter( attr_name, value_var, has_on_setattr @@ -2247,22 +2321,21 @@ def _attrs_to_init_script( has_on_setattr = a.on_setattr is not None or ( a.on_setattr is not setters.NO_OP and has_cls_on_setattr ) - arg_name = a.name.lstrip("_") + # a.alias is set to maybe-mangled attr_name in _ClassBuilder if not + # explicitly provided + arg_name = a.alias has_factory = isinstance(a.default, Factory) - if has_factory and a.default.takes_self: - maybe_self = "self" - else: - maybe_self = "" + maybe_self = "self" if has_factory and a.default.takes_self else "" if a.init is False: if has_factory: - init_factory_name = _init_factory_pat.format(a.name) + init_factory_name = _init_factory_pat % (a.name,) if a.converter is not None: lines.append( fmt_setter_with_converter( attr_name, - init_factory_name + "(%s)" % (maybe_self,), + init_factory_name + f"({maybe_self})", has_on_setattr, ) ) @@ -2272,32 +2345,31 @@ def _attrs_to_init_script( lines.append( fmt_setter( attr_name, - init_factory_name + "(%s)" % (maybe_self,), + init_factory_name + f"({maybe_self})", has_on_setattr, ) ) names_for_globals[init_factory_name] = a.default.factory + elif a.converter is not None: + lines.append( + fmt_setter_with_converter( + attr_name, + f"attr_dict['{attr_name}'].default", + has_on_setattr, + ) + ) + conv_name = _init_converter_pat % (a.name,) + names_for_globals[conv_name] = a.converter else: - if a.converter is not None: - lines.append( - fmt_setter_with_converter( - attr_name, - "attr_dict['%s'].default" % (attr_name,), - has_on_setattr, - ) - ) - conv_name = _init_converter_pat % (a.name,) - names_for_globals[conv_name] = a.converter - else: - lines.append( - fmt_setter( - attr_name, - "attr_dict['%s'].default" % (attr_name,), - has_on_setattr, - ) + lines.append( + fmt_setter( + attr_name, + f"attr_dict['{attr_name}'].default", + has_on_setattr, ) + ) elif a.default is not NOTHING and not has_factory: - arg = "%s=attr_dict['%s'].default" % (arg_name, attr_name) + arg = f"{arg_name}=attr_dict['{attr_name}'].default" if a.kw_only: kw_only_args.append(arg) else: @@ -2316,14 +2388,14 @@ def _attrs_to_init_script( lines.append(fmt_setter(attr_name, arg_name, has_on_setattr)) elif has_factory: - arg = "%s=NOTHING" % (arg_name,) + arg = f"{arg_name}=NOTHING" if a.kw_only: kw_only_args.append(arg) else: args.append(arg) - lines.append("if %s is not NOTHING:" % (arg_name,)) + lines.append(f"if {arg_name} is not NOTHING:") - init_factory_name = _init_factory_pat.format(a.name) + init_factory_name = _init_factory_pat % (a.name,) if a.converter is not None: lines.append( " " @@ -2390,9 +2462,7 @@ def _attrs_to_init_script( for a in attrs_to_validate: val_name = "__attr_validator_" + a.name attr_name = "__attr_" + a.name - lines.append( - " %s(self, %s, self.%s)" % (val_name, attr_name, a.name) - ) + lines.append(f" {val_name}(self, {attr_name}, self.{a.name})") names_for_globals[val_name] = a.validator names_for_globals[attr_name] = a @@ -2406,9 +2476,9 @@ def _attrs_to_init_script( # hash code would result in silent bugs. if cache_hash: if frozen: - if slots: + if slots: # noqa: SIM108 # if frozen and slots, then _setattr defined above - init_hash_cache = "_setattr(self, '%s', %s)" + init_hash_cache = "_setattr('%s', %s)" else: # if frozen and not slots, then _inst_dict defined above init_hash_cache = "_inst_dict['%s'] = %s" @@ -2419,39 +2489,67 @@ def _attrs_to_init_script( # For exceptions we rely on BaseException.__init__ for proper # initialization. if is_exc: - vals = ",".join("self." + a.name for a in attrs if a.init) + vals = ",".join(f"self.{a.name}" for a in attrs if a.init) - lines.append("BaseException.__init__(self, %s)" % (vals,)) + lines.append(f"BaseException.__init__(self, {vals})") args = ", ".join(args) + pre_init_args = args if kw_only_args: args += "%s*, %s" % ( ", " if args else "", # leading comma ", ".join(kw_only_args), # kw_only args ) + pre_init_kw_only_args = ", ".join( + ["%s=%s" % (kw_arg, kw_arg) for kw_arg in kw_only_args] + ) + pre_init_args += ( + ", " if pre_init_args else "" + ) # handle only kwargs and no regular args + pre_init_args += pre_init_kw_only_args + + if pre_init and pre_init_has_args: + # If pre init method has arguments, pass same arguments as `__init__` + lines[0] = "self.__attrs_pre_init__(%s)" % pre_init_args + return ( - """\ -def {init_name}(self, {args}): - {lines} -""".format( - init_name=("__attrs_init__" if attrs_init else "__init__"), - args=args, - lines="\n ".join(lines) if lines else "pass", + "def %s(self, %s):\n %s\n" + % ( + ("__attrs_init__" if attrs_init else "__init__"), + args, + "\n ".join(lines) if lines else "pass", ), names_for_globals, annotations, ) +def _default_init_alias_for(name: str) -> str: + """ + The default __init__ parameter name for a field. + + This performs private-name adjustment via leading-unscore stripping, + and is the default value of Attribute.alias if not provided. + """ + + return name.lstrip("_") + + class Attribute: """ *Read-only* representation of an attribute. + .. warning:: + + You should never instantiate this class yourself. + The class has *all* arguments of `attr.ib` (except for ``factory`` which is only syntactic sugar for ``default=Factory(...)`` plus the following: - ``name`` (`str`): The name of the attribute. + - ``alias`` (`str`): The __init__ parameter name of the attribute, after + any explicit overrides and default private-attribute-name handling. - ``inherited`` (`bool`): Whether or not that attribute has been inherited from a base class. - ``eq_key`` and ``order_key`` (`typing.Callable` or `None`): The callables @@ -2467,12 +2565,16 @@ class Attribute: - Validators get them passed as the first argument. - The :ref:`field transformer ` hook receives a list of them. + - The ``alias`` property exposes the __init__ parameter name of the field, + with any overrides and default private-attribute handling applied. + .. versionadded:: 20.1.0 *inherited* .. versionadded:: 20.1.0 *on_setattr* .. versionchanged:: 20.2.0 *inherited* is not taken into account for equality checks and hashing anymore. .. versionadded:: 21.1.0 *eq_key* and *order_key* + .. versionadded:: 22.2.0 *alias* For the full version history of the fields, see `attr.ib`. """ @@ -2494,6 +2596,7 @@ class Attribute: "kw_only", "inherited", "on_setattr", + "alias", ) def __init__( @@ -2515,13 +2618,14 @@ class Attribute: order=None, order_key=None, on_setattr=None, + alias=None, ): eq, eq_key, order, order_key = _determine_attrib_eq_order( cmp, eq_key or eq, order_key or order, True ) # Cache this descriptor here to speed things up later. - bound_setattr = _obj_setattr.__get__(self, Attribute) + bound_setattr = _obj_setattr.__get__(self) # Despite the big red warning, people *do* instantiate `Attribute` # themselves. @@ -2548,6 +2652,7 @@ class Attribute: bound_setattr("kw_only", kw_only) bound_setattr("inherited", inherited) bound_setattr("on_setattr", on_setattr) + bound_setattr("alias", alias) def __setattr__(self, name, value): raise FrozenInstanceError() @@ -2558,9 +2663,8 @@ class Attribute: if type is None: type = ca.type elif ca.type is not None: - raise ValueError( - "Type annotation and type argument cannot both be present" - ) + msg = "Type annotation and type argument cannot both be present" + raise ValueError(msg) inst_dict = { k: getattr(ca, k) for k in Attribute.__slots__ @@ -2580,16 +2684,16 @@ class Attribute: type=type, cmp=None, inherited=False, - **inst_dict + **inst_dict, ) - # Don't use attr.evolve since fields(Attribute) doesn't work + # Don't use attrs.evolve since fields(Attribute) doesn't work def evolve(self, **changes): """ Copy *self* and apply *changes*. - This works similarly to `attr.evolve` but that function does not work - with ``Attribute``. + This works similarly to `attrs.evolve` but that function does not work + with `Attribute`. It is mainly meant to be used for `transform-fields`. @@ -2618,7 +2722,7 @@ class Attribute: self._setattrs(zip(self.__slots__, state)) def _setattrs(self, name_values_pairs): - bound_setattr = _obj_setattr.__get__(self, Attribute) + bound_setattr = _obj_setattr.__get__(self) for name, value in name_values_pairs: if name != "metadata": bound_setattr(name, value) @@ -2643,6 +2747,7 @@ _a = [ hash=(name != "metadata"), init=True, inherited=False, + alias=_default_init_alias_for(name), ) for name in Attribute.__slots__ ] @@ -2681,37 +2786,42 @@ class _CountingAttr: "type", "kw_only", "on_setattr", + "alias", ) - __attrs_attrs__ = tuple( - Attribute( - name=name, - default=NOTHING, - validator=None, - repr=True, - cmp=None, - hash=True, - init=True, - kw_only=False, - eq=True, - eq_key=None, - order=False, - order_key=None, - inherited=False, - on_setattr=None, - ) - for name in ( - "counter", - "_default", - "repr", - "eq", - "order", - "hash", - "init", - "on_setattr", - ) - ) + ( + __attrs_attrs__ = ( + *tuple( + Attribute( + name=name, + alias=_default_init_alias_for(name), + default=NOTHING, + validator=None, + repr=True, + cmp=None, + hash=True, + init=True, + kw_only=False, + eq=True, + eq_key=None, + order=False, + order_key=None, + inherited=False, + on_setattr=None, + ) + for name in ( + "counter", + "_default", + "repr", + "eq", + "order", + "hash", + "init", + "on_setattr", + "alias", + ) + ), Attribute( name="metadata", + alias="metadata", default=None, validator=None, repr=True, @@ -2746,6 +2856,7 @@ class _CountingAttr: order, order_key, on_setattr, + alias, ): _CountingAttr.cls_counter += 1 self.counter = _CountingAttr.cls_counter @@ -2763,6 +2874,7 @@ class _CountingAttr: self.type = type self.kw_only = kw_only self.on_setattr = on_setattr + self.alias = alias def validator(self, meth): """ @@ -2817,10 +2929,6 @@ class Factory: __slots__ = ("factory", "takes_self") def __init__(self, factory, takes_self=False): - """ - `Factory` is part of the default machinery so if we want a default - value here, we have to implement it ourselves. - """ self.factory = factory self.takes_self = takes_self @@ -2857,23 +2965,26 @@ _f = [ Factory = _add_hash(_add_eq(_add_repr(Factory, attrs=_f), attrs=_f), attrs=_f) -def make_class(name, attrs, bases=(object,), **attributes_arguments): - """ +def make_class( + name, attrs, bases=(object,), class_body=None, **attributes_arguments +): + r""" A quick way to create a new class called *name* with *attrs*. :param str name: The name for the new class. :param attrs: A list of names or a dictionary of mappings of names to - attributes. + `attr.ib`\ s / `attrs.field`\ s. - If *attrs* is a list or an ordered dict (`dict` on Python 3.6+, - `collections.OrderedDict` otherwise), the order is deduced from - the order of the names or attributes inside *attrs*. Otherwise the - order of the definition of the attributes is used. + The order is deduced from the order of the names or attributes inside + *attrs*. Otherwise the order of the definition of the attributes is + used. :type attrs: `list` or `dict` :param tuple bases: Classes that the new class will subclass. + :param dict class_body: An optional dictionary of class attributes for the new class. + :param attributes_arguments: Passed unmodified to `attr.s`. :return: A new class with *attrs*. @@ -2881,19 +2992,23 @@ def make_class(name, attrs, bases=(object,), **attributes_arguments): .. versionadded:: 17.1.0 *bases* .. versionchanged:: 18.1.0 If *attrs* is ordered, the order is retained. + .. versionchanged:: 23.2.0 *class_body* """ if isinstance(attrs, dict): cls_dict = attrs elif isinstance(attrs, (list, tuple)): cls_dict = {a: attrib() for a in attrs} else: - raise TypeError("attrs argument must be a dict or a list.") + msg = "attrs argument must be a dict or a list." + raise TypeError(msg) pre_init = cls_dict.pop("__attrs_pre_init__", None) post_init = cls_dict.pop("__attrs_post_init__", None) user_init = cls_dict.pop("__init__", None) body = {} + if class_body is not None: + body.update(class_body) if pre_init is not None: body["__attrs_pre_init__"] = pre_init if post_init is not None: @@ -2907,12 +3022,10 @@ def make_class(name, attrs, bases=(object,), **attributes_arguments): # frame where the class is created. Bypass this step in environments where # sys._getframe is not defined (Jython for example) or sys._getframe is not # defined for arguments greater than 0 (IronPython). - try: + with contextlib.suppress(AttributeError, ValueError): type_.__module__ = sys._getframe(1).f_globals.get( "__name__", "__main__" ) - except (AttributeError, ValueError): - pass # We do it here for proper warnings with meaningful stacklevel. cmp = attributes_arguments.pop("cmp", None) diff --git a/libs/attr/_next_gen.py b/libs/attr/_next_gen.py index 5a06a7438..1fb9f259b 100644 --- a/libs/attr/_next_gen.py +++ b/libs/attr/_next_gen.py @@ -1,8 +1,8 @@ # SPDX-License-Identifier: MIT """ -These are Python 3.6+-only and keyword-only APIs that call `attr.s` and -`attr.ib` with different default values. +These are keyword-only APIs that call `attr.s` and `attr.ib` with different +default values. """ @@ -26,6 +26,7 @@ def define( *, these=None, repr=None, + unsafe_hash=None, hash=None, init=None, slots=True, @@ -45,20 +46,20 @@ def define( match_args=True, ): r""" - Define an ``attrs`` class. + Define an *attrs* class. Differences to the classic `attr.s` that it uses underneath: - Automatically detect whether or not *auto_attribs* should be `True` (c.f. *auto_attribs* parameter). - - If *frozen* is `False`, run converters and validators when setting an - attribute by default. + - Converters and validators run when attributes are set by default -- if + *frozen* is `False`. - *slots=True* .. caution:: Usually this has only upsides and few visible effects in everyday - programming. But it *can* lead to some suprising behaviors, so please + programming. But it *can* lead to some surprising behaviors, so please make sure to read :term:`slotted classes`. - *auto_exc=True* - *auto_detect=True* @@ -81,6 +82,8 @@ def define( .. versionadded:: 20.1.0 .. versionchanged:: 21.3.0 Converters are also run ``on_setattr``. + .. versionadded:: 22.2.0 + *unsafe_hash* as an alias for *hash* (for :pep:`681` compliance). """ def do_it(cls, auto_attribs): @@ -89,6 +92,7 @@ def define( these=these, repr=repr, hash=hash, + unsafe_hash=unsafe_hash, init=init, slots=slots, frozen=frozen, @@ -127,10 +131,8 @@ def define( for base_cls in cls.__bases__: if base_cls.__setattr__ is _frozen_setattrs: if had_on_setattr: - raise ValueError( - "Frozen classes can't use on_setattr " - "(frozen-ness was inherited)." - ) + msg = "Frozen classes can't use on_setattr (frozen-ness was inherited)." + raise ValueError(msg) on_setattr = setters.NO_OP break @@ -147,8 +149,8 @@ def define( # if it's used as `@attrs` but ``None`` if used as `@attrs()`. if maybe_cls is None: return wrap - else: - return wrap(maybe_cls) + + return wrap(maybe_cls) mutable = define @@ -163,17 +165,22 @@ def field( hash=None, init=True, metadata=None, + type=None, converter=None, factory=None, kw_only=False, eq=None, order=None, on_setattr=None, + alias=None, ): """ Identical to `attr.ib`, except keyword-only and with some arguments removed. + .. versionadded:: 23.1.0 + The *type* parameter has been re-added; mostly for `attrs.make_class`. + Please note that type checkers ignore this metadata. .. versionadded:: 20.1.0 """ return attrib( @@ -183,12 +190,14 @@ def field( hash=hash, init=init, metadata=metadata, + type=type, converter=converter, factory=factory, kw_only=kw_only, eq=eq, order=order, on_setattr=on_setattr, + alias=alias, ) diff --git a/libs/attr/_typing_compat.pyi b/libs/attr/_typing_compat.pyi new file mode 100644 index 000000000..ca7b71e90 --- /dev/null +++ b/libs/attr/_typing_compat.pyi @@ -0,0 +1,15 @@ +from typing import Any, ClassVar, Protocol + +# MYPY is a special constant in mypy which works the same way as `TYPE_CHECKING`. +MYPY = False + +if MYPY: + # A protocol to be able to statically accept an attrs class. + class AttrsInstance_(Protocol): + __attrs_attrs__: ClassVar[Any] + +else: + # For type checkers without plug-in support use an empty protocol that + # will (hopefully) be combined into a union. + class AttrsInstance_(Protocol): + pass diff --git a/libs/attr/converters.py b/libs/attr/converters.py index a73626c26..2bf4c902a 100644 --- a/libs/attr/converters.py +++ b/libs/attr/converters.py @@ -70,21 +70,20 @@ def default_if_none(default=NOTHING, factory=None): .. versionadded:: 18.2.0 """ if default is NOTHING and factory is None: - raise TypeError("Must pass either `default` or `factory`.") + msg = "Must pass either `default` or `factory`." + raise TypeError(msg) if default is not NOTHING and factory is not None: - raise TypeError( - "Must pass either `default` or `factory` but not both." - ) + msg = "Must pass either `default` or `factory` but not both." + raise TypeError(msg) if factory is not None: default = Factory(factory) if isinstance(default, Factory): if default.takes_self: - raise ValueError( - "`takes_self` is not supported by default_if_none." - ) + msg = "`takes_self` is not supported by default_if_none." + raise ValueError(msg) def default_if_none_converter(val): if val is not None: @@ -141,4 +140,5 @@ def to_bool(val): except TypeError: # Raised when "val" is not hashable (e.g., lists) pass - raise ValueError("Cannot convert value to bool: {}".format(val)) + msg = f"Cannot convert value to bool: {val}" + raise ValueError(msg) diff --git a/libs/attr/converters.pyi b/libs/attr/converters.pyi index 0f58088a3..5abb49f6d 100644 --- a/libs/attr/converters.pyi +++ b/libs/attr/converters.pyi @@ -1,4 +1,4 @@ -from typing import Callable, Optional, TypeVar, overload +from typing import Callable, TypeVar, overload from . import _ConverterType diff --git a/libs/attr/exceptions.py b/libs/attr/exceptions.py index 5dc51e0a8..3b7abb815 100644 --- a/libs/attr/exceptions.py +++ b/libs/attr/exceptions.py @@ -1,5 +1,9 @@ # SPDX-License-Identifier: MIT +from __future__ import annotations + +from typing import ClassVar + class FrozenError(AttributeError): """ @@ -13,7 +17,7 @@ class FrozenError(AttributeError): """ msg = "can't set attribute" - args = [msg] + args: ClassVar[tuple[str]] = [msg] class FrozenInstanceError(FrozenError): @@ -34,7 +38,7 @@ class FrozenAttributeError(FrozenError): class AttrsAttributeNotFoundError(ValueError): """ - An ``attrs`` function couldn't find an attribute that the user asked for. + An *attrs* function couldn't find an attribute that the user asked for. .. versionadded:: 16.2.0 """ @@ -42,7 +46,7 @@ class AttrsAttributeNotFoundError(ValueError): class NotAnAttrsClassError(ValueError): """ - A non-``attrs`` class has been passed into an ``attrs`` function. + A non-*attrs* class has been passed into an *attrs* function. .. versionadded:: 16.2.0 """ @@ -50,7 +54,7 @@ class NotAnAttrsClassError(ValueError): class DefaultAlreadySetError(RuntimeError): """ - A default has been set using ``attr.ib()`` and is attempted to be reset + A default has been set when defining the field and is attempted to be reset using the decorator. .. versionadded:: 17.1.0 @@ -59,8 +63,7 @@ class DefaultAlreadySetError(RuntimeError): class UnannotatedAttributeError(RuntimeError): """ - A class with ``auto_attribs=True`` has an ``attr.ib()`` without a type - annotation. + A class with ``auto_attribs=True`` has a field without a type annotation. .. versionadded:: 17.3.0 """ @@ -68,7 +71,7 @@ class UnannotatedAttributeError(RuntimeError): class PythonTooOldError(RuntimeError): """ - It was attempted to use an ``attrs`` feature that requires a newer Python + It was attempted to use an *attrs* feature that requires a newer Python version. .. versionadded:: 18.2.0 @@ -77,8 +80,8 @@ class PythonTooOldError(RuntimeError): class NotCallableError(TypeError): """ - A ``attr.ib()`` requiring a callable has been set with a value - that is not callable. + A field requiring a callable has been set with a value that is not + callable. .. versionadded:: 19.2.0 """ diff --git a/libs/attr/filters.py b/libs/attr/filters.py index baa25e946..a1e40c98d 100644 --- a/libs/attr/filters.py +++ b/libs/attr/filters.py @@ -13,6 +13,7 @@ def _split_what(what): """ return ( frozenset(cls for cls in what if isinstance(cls, type)), + frozenset(cls for cls in what if isinstance(cls, str)), frozenset(cls for cls in what if isinstance(cls, Attribute)), ) @@ -22,14 +23,21 @@ def include(*what): Include *what*. :param what: What to include. - :type what: `list` of `type` or `attrs.Attribute`\\ s + :type what: `list` of classes `type`, field names `str` or + `attrs.Attribute`\\ s :rtype: `callable` + + .. versionchanged:: 23.1.0 Accept strings with field names. """ - cls, attrs = _split_what(what) + cls, names, attrs = _split_what(what) def include_(attribute, value): - return value.__class__ in cls or attribute in attrs + return ( + value.__class__ in cls + or attribute.name in names + or attribute in attrs + ) return include_ @@ -39,13 +47,20 @@ def exclude(*what): Exclude *what*. :param what: What to exclude. - :type what: `list` of classes or `attrs.Attribute`\\ s. + :type what: `list` of classes `type`, field names `str` or + `attrs.Attribute`\\ s. :rtype: `callable` + + .. versionchanged:: 23.3.0 Accept field name string as input argument """ - cls, attrs = _split_what(what) + cls, names, attrs = _split_what(what) def exclude_(attribute, value): - return value.__class__ not in cls and attribute not in attrs + return not ( + value.__class__ in cls + or attribute.name in names + or attribute in attrs + ) return exclude_ diff --git a/libs/attr/filters.pyi b/libs/attr/filters.pyi index 993866865..8a02fa0fc 100644 --- a/libs/attr/filters.pyi +++ b/libs/attr/filters.pyi @@ -2,5 +2,5 @@ from typing import Any, Union from . import Attribute, _FilterType -def include(*what: Union[type, Attribute[Any]]) -> _FilterType[Any]: ... -def exclude(*what: Union[type, Attribute[Any]]) -> _FilterType[Any]: ... +def include(*what: Union[type, str, Attribute[Any]]) -> _FilterType[Any]: ... +def exclude(*what: Union[type, str, Attribute[Any]]) -> _FilterType[Any]: ... diff --git a/libs/attr/setters.pyi b/libs/attr/setters.pyi index 3f5603c2b..72f7ce476 100644 --- a/libs/attr/setters.pyi +++ b/libs/attr/setters.pyi @@ -1,4 +1,4 @@ -from typing import Any, NewType, NoReturn, TypeVar, cast +from typing import Any, NewType, NoReturn, TypeVar from . import Attribute, _OnSetAttrType diff --git a/libs/attr/validators.py b/libs/attr/validators.py index eece517da..34d6b761d 100644 --- a/libs/attr/validators.py +++ b/libs/attr/validators.py @@ -9,18 +9,14 @@ import operator import re from contextlib import contextmanager +from re import Pattern from ._config import get_run_validators, set_run_validators from ._make import _AndValidator, and_, attrib, attrs +from .converters import default_if_none from .exceptions import NotCallableError -try: - Pattern = re.Pattern -except AttributeError: # Python <3.7 lacks a Pattern type. - Pattern = type(re.compile("")) - - __all__ = [ "and_", "deep_iterable", @@ -37,6 +33,7 @@ __all__ = [ "matches_re", "max_len", "min_len", + "not_", "optional", "provides", "set_disabled", @@ -100,23 +97,21 @@ class _InstanceOfValidator: We use a callable class to be able to change the ``__repr__``. """ if not isinstance(value, self.type): + msg = "'{name}' must be {type!r} (got {value!r} that is a {actual!r}).".format( + name=attr.name, + type=self.type, + actual=value.__class__, + value=value, + ) raise TypeError( - "'{name}' must be {type!r} (got {value!r} that is a " - "{actual!r}).".format( - name=attr.name, - type=self.type, - actual=value.__class__, - value=value, - ), + msg, attr, self.type, value, ) def __repr__(self): - return "".format( - type=self.type - ) + return f"" def instance_of(type): @@ -126,7 +121,7 @@ def instance_of(type): `isinstance` therefore it's also valid to pass a tuple of types). :param type: The type to check for. - :type type: type or tuple of types + :type type: type or tuple of type :raises TypeError: With a human readable error message, the attribute (of type `attrs.Attribute`), the expected type, and the value it @@ -145,20 +140,18 @@ class _MatchesReValidator: We use a callable class to be able to change the ``__repr__``. """ if not self.match_func(value): + msg = "'{name}' must match regex {pattern!r} ({value!r} doesn't)".format( + name=attr.name, pattern=self.pattern.pattern, value=value + ) raise ValueError( - "'{name}' must match regex {pattern!r}" - " ({value!r} doesn't)".format( - name=attr.name, pattern=self.pattern.pattern, value=value - ), + msg, attr, self.pattern, value, ) def __repr__(self): - return "".format( - pattern=self.pattern - ) + return f"" def matches_re(regex, flags=0, func=None): @@ -179,22 +172,17 @@ def matches_re(regex, flags=0, func=None): """ valid_funcs = (re.fullmatch, None, re.search, re.match) if func not in valid_funcs: - raise ValueError( - "'func' must be one of {}.".format( - ", ".join( - sorted( - e and e.__name__ or "None" for e in set(valid_funcs) - ) - ) + msg = "'func' must be one of {}.".format( + ", ".join( + sorted(e and e.__name__ or "None" for e in set(valid_funcs)) ) ) + raise ValueError(msg) if isinstance(regex, Pattern): if flags: - raise TypeError( - "'flags' can only be used with a string pattern; " - "pass flags to re.compile() instead" - ) + msg = "'flags' can only be used with a string pattern; pass flags to re.compile() instead" + raise TypeError(msg) pattern = regex else: pattern = re.compile(regex, flags) @@ -218,20 +206,18 @@ class _ProvidesValidator: We use a callable class to be able to change the ``__repr__``. """ if not self.interface.providedBy(value): + msg = "'{name}' must provide {interface!r} which {value!r} doesn't.".format( + name=attr.name, interface=self.interface, value=value + ) raise TypeError( - "'{name}' must provide {interface!r} which {value!r} " - "doesn't.".format( - name=attr.name, interface=self.interface, value=value - ), + msg, attr, self.interface, value, ) def __repr__(self): - return "".format( - interface=self.interface - ) + return f"" def provides(interface): @@ -247,7 +233,17 @@ def provides(interface): :raises TypeError: With a human readable error message, the attribute (of type `attrs.Attribute`), the expected interface, and the value it got. + + .. deprecated:: 23.1.0 """ + import warnings + + warnings.warn( + "attrs's zope-interface support is deprecated and will be removed in, " + "or after, April 2024.", + DeprecationWarning, + stacklevel=2, + ) return _ProvidesValidator(interface) @@ -262,9 +258,7 @@ class _OptionalValidator: self.validator(inst, attr, value) def __repr__(self): - return "".format( - what=repr(self.validator) - ) + return f"" def optional(validator): @@ -273,15 +267,16 @@ def optional(validator): which can be set to ``None`` in addition to satisfying the requirements of the sub-validator. - :param validator: A validator (or a list of validators) that is used for - non-``None`` values. - :type validator: callable or `list` of callables. + :param Callable | tuple[Callable] | list[Callable] validator: A validator + (or validators) that is used for non-``None`` values. .. versionadded:: 15.1.0 .. versionchanged:: 17.1.0 *validator* can be a list of validators. + .. versionchanged:: 23.1.0 *validator* can also be a tuple of validators. """ - if isinstance(validator, list): + if isinstance(validator, (list, tuple)): return _OptionalValidator(_AndValidator(validator)) + return _OptionalValidator(validator) @@ -296,19 +291,16 @@ class _InValidator: in_options = False if not in_options: + msg = f"'{attr.name}' must be in {self.options!r} (got {value!r})" raise ValueError( - "'{name}' must be in {options!r} (got {value!r})".format( - name=attr.name, options=self.options, value=value - ), + msg, attr, self.options, value, ) def __repr__(self): - return "".format( - options=self.options - ) + return f"" def in_(options): @@ -357,13 +349,13 @@ class _IsCallableValidator: def is_callable(): """ - A validator that raises a `attr.exceptions.NotCallableError` if the + A validator that raises a `attrs.exceptions.NotCallableError` if the initializer is called with a value for this particular attribute that is not callable. .. versionadded:: 19.1.0 - :raises `attr.exceptions.NotCallableError`: With a human readable error + :raises attrs.exceptions.NotCallableError: With a human readable error message containing the attribute (`attrs.Attribute`) name, and the value it got. """ @@ -391,14 +383,11 @@ class _DeepIterable: iterable_identifier = ( "" if self.iterable_validator is None - else " {iterable!r}".format(iterable=self.iterable_validator) + else f" {self.iterable_validator!r}" ) return ( - "" - ).format( - iterable_identifier=iterable_identifier, - member=self.member_validator, + f"" ) @@ -469,19 +458,11 @@ class _NumberValidator: We use a callable class to be able to change the ``__repr__``. """ if not self.compare_func(value, self.bound): - raise ValueError( - "'{name}' must be {op} {bound}: {value}".format( - name=attr.name, - op=self.compare_op, - bound=self.bound, - value=value, - ) - ) + msg = f"'{attr.name}' must be {self.compare_op} {self.bound}: {value}" + raise ValueError(msg) def __repr__(self): - return "".format( - op=self.compare_op, bound=self.bound - ) + return f"" def lt(val): @@ -541,14 +522,11 @@ class _MaxLengthValidator: We use a callable class to be able to change the ``__repr__``. """ if len(value) > self.max_length: - raise ValueError( - "Length of '{name}' must be <= {max}: {len}".format( - name=attr.name, max=self.max_length, len=len(value) - ) - ) + msg = f"Length of '{attr.name}' must be <= {self.max_length}: {len(value)}" + raise ValueError(msg) def __repr__(self): - return "".format(max=self.max_length) + return f"" def max_len(length): @@ -572,14 +550,11 @@ class _MinLengthValidator: We use a callable class to be able to change the ``__repr__``. """ if len(value) < self.min_length: - raise ValueError( - "Length of '{name}' must be => {min}: {len}".format( - name=attr.name, min=self.min_length, len=len(value) - ) - ) + msg = f"Length of '{attr.name}' must be >= {self.min_length}: {len(value)}" + raise ValueError(msg) def __repr__(self): - return "".format(min=self.min_length) + return f"" def min_len(length): @@ -592,3 +567,115 @@ def min_len(length): .. versionadded:: 22.1.0 """ return _MinLengthValidator(length) + + +@attrs(repr=False, slots=True, hash=True) +class _SubclassOfValidator: + type = attrib() + + def __call__(self, inst, attr, value): + """ + We use a callable class to be able to change the ``__repr__``. + """ + if not issubclass(value, self.type): + msg = f"'{attr.name}' must be a subclass of {self.type!r} (got {value!r})." + raise TypeError( + msg, + attr, + self.type, + value, + ) + + def __repr__(self): + return f"" + + +def _subclass_of(type): + """ + A validator that raises a `TypeError` if the initializer is called + with a wrong type for this particular attribute (checks are performed using + `issubclass` therefore it's also valid to pass a tuple of types). + + :param type: The type to check for. + :type type: type or tuple of types + + :raises TypeError: With a human readable error message, the attribute + (of type `attrs.Attribute`), the expected type, and the value it + got. + """ + return _SubclassOfValidator(type) + + +@attrs(repr=False, slots=True, hash=True) +class _NotValidator: + validator = attrib() + msg = attrib( + converter=default_if_none( + "not_ validator child '{validator!r}' " + "did not raise a captured error" + ) + ) + exc_types = attrib( + validator=deep_iterable( + member_validator=_subclass_of(Exception), + iterable_validator=instance_of(tuple), + ), + ) + + def __call__(self, inst, attr, value): + try: + self.validator(inst, attr, value) + except self.exc_types: + pass # suppress error to invert validity + else: + raise ValueError( + self.msg.format( + validator=self.validator, + exc_types=self.exc_types, + ), + attr, + self.validator, + value, + self.exc_types, + ) + + def __repr__(self): + return ( + "" + ).format( + what=self.validator, + exc_types=self.exc_types, + ) + + +def not_(validator, *, msg=None, exc_types=(ValueError, TypeError)): + """ + A validator that wraps and logically 'inverts' the validator passed to it. + It will raise a `ValueError` if the provided validator *doesn't* raise a + `ValueError` or `TypeError` (by default), and will suppress the exception + if the provided validator *does*. + + Intended to be used with existing validators to compose logic without + needing to create inverted variants, for example, ``not_(in_(...))``. + + :param validator: A validator to be logically inverted. + :param msg: Message to raise if validator fails. + Formatted with keys ``exc_types`` and ``validator``. + :type msg: str + :param exc_types: Exception type(s) to capture. + Other types raised by child validators will not be intercepted and + pass through. + + :raises ValueError: With a human readable error message, + the attribute (of type `attrs.Attribute`), + the validator that failed to raise an exception, + the value it got, + and the expected exception types. + + .. versionadded:: 22.2.0 + """ + try: + exc_types = tuple(exc_types) + except TypeError: + exc_types = (exc_types,) + return _NotValidator(validator, msg, exc_types) diff --git a/libs/attr/validators.pyi b/libs/attr/validators.pyi index 54b9dba24..d194a75ab 100644 --- a/libs/attr/validators.pyi +++ b/libs/attr/validators.pyi @@ -51,7 +51,9 @@ def instance_of( def instance_of(type: Tuple[type, ...]) -> _ValidatorType[Any]: ... def provides(interface: Any) -> _ValidatorType[Any]: ... def optional( - validator: Union[_ValidatorType[_T], List[_ValidatorType[_T]]] + validator: Union[ + _ValidatorType[_T], List[_ValidatorType[_T]], Tuple[_ValidatorType[_T]] + ] ) -> _ValidatorType[Optional[_T]]: ... def in_(options: Container[_T]) -> _ValidatorType[_T]: ... def and_(*validators: _ValidatorType[_T]) -> _ValidatorType[_T]: ... @@ -78,3 +80,9 @@ def ge(val: _T) -> _ValidatorType[_T]: ... def gt(val: _T) -> _ValidatorType[_T]: ... def max_len(length: int) -> _ValidatorType[_T]: ... def min_len(length: int) -> _ValidatorType[_T]: ... +def not_( + validator: _ValidatorType[_T], + *, + msg: Optional[str] = None, + exc_types: Union[Type[Exception], Iterable[Type[Exception]]] = ..., +) -> _ValidatorType[_T]: ... diff --git a/libs/attrs-23.2.0.dist-info/INSTALLER b/libs/attrs-23.2.0.dist-info/INSTALLER new file mode 100644 index 000000000..a1b589e38 --- /dev/null +++ b/libs/attrs-23.2.0.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/libs/attrs-23.2.0.dist-info/METADATA b/libs/attrs-23.2.0.dist-info/METADATA new file mode 100644 index 000000000..c20be76c7 --- /dev/null +++ b/libs/attrs-23.2.0.dist-info/METADATA @@ -0,0 +1,202 @@ +Metadata-Version: 2.1 +Name: attrs +Version: 23.2.0 +Summary: Classes Without Boilerplate +Project-URL: Documentation, https://www.attrs.org/ +Project-URL: Changelog, https://www.attrs.org/en/stable/changelog.html +Project-URL: GitHub, https://github.com/python-attrs/attrs +Project-URL: Funding, https://github.com/sponsors/hynek +Project-URL: Tidelift, https://tidelift.com/subscription/pkg/pypi-attrs?utm_source=pypi-attrs&utm_medium=pypi +Author-email: Hynek Schlawack +License-Expression: MIT +License-File: LICENSE +Keywords: attribute,boilerplate,class +Classifier: Development Status :: 5 - Production/Stable +Classifier: License :: OSI Approved :: MIT License +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: PyPy +Classifier: Typing :: Typed +Requires-Python: >=3.7 +Requires-Dist: importlib-metadata; python_version < '3.8' +Provides-Extra: cov +Requires-Dist: attrs[tests]; extra == 'cov' +Requires-Dist: coverage[toml]>=5.3; extra == 'cov' +Provides-Extra: dev +Requires-Dist: attrs[tests]; extra == 'dev' +Requires-Dist: pre-commit; extra == 'dev' +Provides-Extra: docs +Requires-Dist: furo; extra == 'docs' +Requires-Dist: myst-parser; extra == 'docs' +Requires-Dist: sphinx; extra == 'docs' +Requires-Dist: sphinx-notfound-page; extra == 'docs' +Requires-Dist: sphinxcontrib-towncrier; extra == 'docs' +Requires-Dist: towncrier; extra == 'docs' +Requires-Dist: zope-interface; extra == 'docs' +Provides-Extra: tests +Requires-Dist: attrs[tests-no-zope]; extra == 'tests' +Requires-Dist: zope-interface; extra == 'tests' +Provides-Extra: tests-mypy +Requires-Dist: mypy>=1.6; (platform_python_implementation == 'CPython' and python_version >= '3.8') and extra == 'tests-mypy' +Requires-Dist: pytest-mypy-plugins; (platform_python_implementation == 'CPython' and python_version >= '3.8') and extra == 'tests-mypy' +Provides-Extra: tests-no-zope +Requires-Dist: attrs[tests-mypy]; extra == 'tests-no-zope' +Requires-Dist: cloudpickle; (platform_python_implementation == 'CPython') and extra == 'tests-no-zope' +Requires-Dist: hypothesis; extra == 'tests-no-zope' +Requires-Dist: pympler; extra == 'tests-no-zope' +Requires-Dist: pytest-xdist[psutil]; extra == 'tests-no-zope' +Requires-Dist: pytest>=4.3.0; extra == 'tests-no-zope' +Description-Content-Type: text/markdown + +

+ + attrs + +

+ + +*attrs* is the Python package that will bring back the **joy** of **writing classes** by relieving you from the drudgery of implementing object protocols (aka [dunder methods](https://www.attrs.org/en/latest/glossary.html#term-dunder-methods)). +[Trusted by NASA](https://docs.github.com/en/account-and-profile/setting-up-and-managing-your-github-profile/customizing-your-profile/personalizing-your-profile#list-of-qualifying-repositories-for-mars-2020-helicopter-contributor-achievement) for Mars missions since 2020! + +Its main goal is to help you to write **concise** and **correct** software without slowing down your code. + + +## Sponsors + +*attrs* would not be possible without our [amazing sponsors](https://github.com/sponsors/hynek). +Especially those generously supporting us at the *The Organization* tier and higher: + +

+ + + +

+ +

+ Please consider joining them to help make attrs’s maintenance more sustainable! +

+ + + +## Example + +*attrs* gives you a class decorator and a way to declaratively define the attributes on that class: + + + +```pycon +>>> from attrs import asdict, define, make_class, Factory + +>>> @define +... class SomeClass: +... a_number: int = 42 +... list_of_numbers: list[int] = Factory(list) +... +... def hard_math(self, another_number): +... return self.a_number + sum(self.list_of_numbers) * another_number + + +>>> sc = SomeClass(1, [1, 2, 3]) +>>> sc +SomeClass(a_number=1, list_of_numbers=[1, 2, 3]) + +>>> sc.hard_math(3) +19 +>>> sc == SomeClass(1, [1, 2, 3]) +True +>>> sc != SomeClass(2, [3, 2, 1]) +True + +>>> asdict(sc) +{'a_number': 1, 'list_of_numbers': [1, 2, 3]} + +>>> SomeClass() +SomeClass(a_number=42, list_of_numbers=[]) + +>>> C = make_class("C", ["a", "b"]) +>>> C("foo", "bar") +C(a='foo', b='bar') +``` + +After *declaring* your attributes, *attrs* gives you: + +- a concise and explicit overview of the class's attributes, +- a nice human-readable `__repr__`, +- equality-checking methods, +- an initializer, +- and much more, + +*without* writing dull boilerplate code again and again and *without* runtime performance penalties. + +**Hate type annotations**!? +No problem! +Types are entirely **optional** with *attrs*. +Simply assign `attrs.field()` to the attributes instead of annotating them with types. + +--- + +This example uses *attrs*'s modern APIs that have been introduced in version 20.1.0, and the *attrs* package import name that has been added in version 21.3.0. +The classic APIs (`@attr.s`, `attr.ib`, plus their serious-business aliases) and the `attr` package import name will remain **indefinitely**. + +Please check out [*On The Core API Names*](https://www.attrs.org/en/latest/names.html) for a more in-depth explanation. + + +## Data Classes + +On the tin, *attrs* might remind you of `dataclasses` (and indeed, `dataclasses` [are a descendant](https://hynek.me/articles/import-attrs/) of *attrs*). +In practice it does a lot more and is more flexible. +For instance it allows you to define [special handling of NumPy arrays for equality checks](https://www.attrs.org/en/stable/comparison.html#customization), allows more ways to [plug into the initialization process](https://www.attrs.org/en/stable/init.html#hooking-yourself-into-initialization), and allows for stepping through the generated methods using a debugger. + +For more details, please refer to our [comparison page](https://www.attrs.org/en/stable/why.html#data-classes). + + +## Project Information + +- [**Changelog**](https://www.attrs.org/en/stable/changelog.html) +- [**Documentation**](https://www.attrs.org/) +- [**PyPI**](https://pypi.org/project/attrs/) +- [**Source Code**](https://github.com/python-attrs/attrs) +- [**Contributing**](https://github.com/python-attrs/attrs/blob/main/.github/CONTRIBUTING.md) +- [**Third-party Extensions**](https://github.com/python-attrs/attrs/wiki/Extensions-to-attrs) +- **Get Help**: please use the `python-attrs` tag on [StackOverflow](https://stackoverflow.com/questions/tagged/python-attrs) + + +### *attrs* for Enterprise + +Available as part of the Tidelift Subscription. + +The maintainers of *attrs* and thousands of other packages are working with Tidelift to deliver commercial support and maintenance for the open source packages you use to build your applications. +Save time, reduce risk, and improve code health, while paying the maintainers of the exact packages you use. +[Learn more.](https://tidelift.com/subscription/pkg/pypi-attrs?utm_source=pypi-attrs&utm_medium=referral&utm_campaign=enterprise&utm_term=repo) + +## Release Information + +### Changes + +- The type annotation for `attrs.resolve_types()` is now correct. + [#1141](https://github.com/python-attrs/attrs/issues/1141) +- Type stubs now use `typing.dataclass_transform` to decorate dataclass-like decorators, instead of the non-standard `__dataclass_transform__` special form, which is only supported by Pyright. + [#1158](https://github.com/python-attrs/attrs/issues/1158) +- Fixed serialization of namedtuple fields using `attrs.asdict/astuple()` with `retain_collection_types=True`. + [#1165](https://github.com/python-attrs/attrs/issues/1165) +- `attrs.AttrsInstance` is now a `typing.Protocol` in both type hints and code. + This allows you to subclass it along with another `Protocol`. + [#1172](https://github.com/python-attrs/attrs/issues/1172) +- If *attrs* detects that `__attrs_pre_init__` accepts more than just `self`, it will call it with the same arguments as `__init__` was called. + This allows you to, for example, pass arguments to `super().__init__()`. + [#1187](https://github.com/python-attrs/attrs/issues/1187) +- Slotted classes now transform `functools.cached_property` decorated methods to support equivalent semantics. + [#1200](https://github.com/python-attrs/attrs/issues/1200) +- Added *class_body* argument to `attrs.make_class()` to provide additional attributes for newly created classes. + It is, for example, now possible to attach methods. + [#1203](https://github.com/python-attrs/attrs/issues/1203) + + +--- + +[Full changelog](https://www.attrs.org/en/stable/changelog.html) diff --git a/libs/attrs-23.2.0.dist-info/RECORD b/libs/attrs-23.2.0.dist-info/RECORD new file mode 100644 index 000000000..92edee9ee --- /dev/null +++ b/libs/attrs-23.2.0.dist-info/RECORD @@ -0,0 +1,37 @@ +attr/__init__.py,sha256=WlXJN6ICB0Y_HZ0lmuTUgia0kuSdn2p67d4N6cYxNZM,3307 +attr/__init__.pyi,sha256=u08EujYHy_rSyebNn-I9Xv2S_cXmtA9xWGc0cBsyl18,16976 +attr/_cmp.py,sha256=OQZlWdFX74z18adGEUp40Ojqm0NNu1Flqnv2JE8B2ng,4025 +attr/_cmp.pyi,sha256=sGQmOM0w3_K4-X8cTXR7g0Hqr290E8PTObA9JQxWQqc,399 +attr/_compat.py,sha256=QmRyxii295wcQfaugWqxuIumAPsNQ2-RUF82QZPqMKw,2540 +attr/_config.py,sha256=z81Vt-GeT_2taxs1XZfmHx9TWlSxjPb6eZH1LTGsS54,843 +attr/_funcs.py,sha256=VBTUFKLklsmqxys3qWSTK_Ac9Z4s0mAJWwgW9nA7Llk,17173 +attr/_make.py,sha256=LnVy2e0HygoqaZknhC19z7JmOt7qGkAadf2LZgWVJWI,101923 +attr/_next_gen.py,sha256=as1voi8siAI_o2OQG8YIiZvmn0G7-S3_j_774rnoZ_g,6203 +attr/_typing_compat.pyi,sha256=XDP54TUn-ZKhD62TOQebmzrwFyomhUCoGRpclb6alRA,469 +attr/_version_info.py,sha256=exSqb3b5E-fMSsgZAlEw9XcLpEgobPORCZpcaEglAM4,2121 +attr/_version_info.pyi,sha256=x_M3L3WuB7r_ULXAWjx959udKQ4HLB8l-hsc1FDGNvk,209 +attr/converters.py,sha256=Kyw5MY0yfnUR_RwN1Vydf0EiE---htDxOgSc_-NYL6A,3622 +attr/converters.pyi,sha256=jKlpHBEt6HVKJvgrMFJRrHq8p61GXg4-Nd5RZWKJX7M,406 +attr/exceptions.py,sha256=HRFq4iybmv7-DcZwyjl6M1euM2YeJVK_hFxuaBGAngI,1977 +attr/exceptions.pyi,sha256=zZq8bCUnKAy9mDtBEw42ZhPhAUIHoTKedDQInJD883M,539 +attr/filters.py,sha256=9pYvXqdg6mtLvKIIb56oALRMoHFnQTcGCO4EXTc1qyM,1470 +attr/filters.pyi,sha256=0mRCjLKxdcvAo0vD-Cr81HfRXXCp9j_cAXjOoAHtPGM,225 +attr/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +attr/setters.py,sha256=pbCZQ-pE6ZxjDqZfWWUhUFefXtpekIU4qS_YDMLPQ50,1400 +attr/setters.pyi,sha256=pyY8TVNBu8TWhOldv_RxHzmGvdgFQH981db70r0fn5I,567 +attr/validators.py,sha256=LGVpbiNg_KGzYrKUD5JPiZkx8TMfynDZGoQoLJNCIMo,19676 +attr/validators.pyi,sha256=167Dl9nt7NUhE9wht1I-buo039qyUT1nEUT_nKjSWr4,2580 +attrs-23.2.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +attrs-23.2.0.dist-info/METADATA,sha256=WwvG7OHyKjEPpyFUZCCYt1n0E_CcqdRb7bliGEdcm-A,9531 +attrs-23.2.0.dist-info/RECORD,, +attrs-23.2.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +attrs-23.2.0.dist-info/WHEEL,sha256=TJPnKdtrSue7xZ_AVGkp9YXcvDrobsjBds1du3Nx6dc,87 +attrs-23.2.0.dist-info/licenses/LICENSE,sha256=iCEVyV38KvHutnFPjsbVy8q_Znyv-HKfQkINpj9xTp8,1109 +attrs/__init__.py,sha256=9_5waVbFs7rLqtXZ73tNDrxhezyZ8VZeX4BbvQ3EeJw,1039 +attrs/__init__.pyi,sha256=s_ajQ_U14DOsOz0JbmAKDOi46B3v2PcdO0UAV1MY6Ek,2168 +attrs/converters.py,sha256=8kQljrVwfSTRu8INwEk8SI0eGrzmWftsT7rM0EqyohM,76 +attrs/exceptions.py,sha256=ACCCmg19-vDFaDPY9vFl199SPXCQMN_bENs4DALjzms,76 +attrs/filters.py,sha256=VOUMZug9uEU6dUuA0dF1jInUK0PL3fLgP0VBS5d-CDE,73 +attrs/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +attrs/setters.py,sha256=eL1YidYQV3T2h9_SYIZSZR1FAcHGb1TuCTy0E0Lv2SU,73 +attrs/validators.py,sha256=xcy6wD5TtTkdCG1f4XWbocPSO0faBjk5IfVJfP6SUj0,76 diff --git a/libs/attrs-23.2.0.dist-info/REQUESTED b/libs/attrs-23.2.0.dist-info/REQUESTED new file mode 100644 index 000000000..e69de29bb diff --git a/libs/attrs-23.2.0.dist-info/WHEEL b/libs/attrs-23.2.0.dist-info/WHEEL new file mode 100644 index 000000000..5998f3aab --- /dev/null +++ b/libs/attrs-23.2.0.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: hatchling 1.21.1 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/libs/attrs-23.2.0.dist-info/licenses/LICENSE b/libs/attrs-23.2.0.dist-info/licenses/LICENSE new file mode 100644 index 000000000..2bd6453d2 --- /dev/null +++ b/libs/attrs-23.2.0.dist-info/licenses/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 Hynek Schlawack and the attrs contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/libs/attrs/__init__.py b/libs/attrs/__init__.py index a704b8b56..0c2481561 100644 --- a/libs/attrs/__init__.py +++ b/libs/attrs/__init__.py @@ -3,17 +3,9 @@ from attr import ( NOTHING, Attribute, + AttrsInstance, Factory, - __author__, - __copyright__, - __description__, - __doc__, - __email__, - __license__, - __title__, - __url__, - __version__, - __version_info__, + _make_getattr, assoc, cmp_using, define, @@ -48,6 +40,7 @@ __all__ = [ "assoc", "astuple", "Attribute", + "AttrsInstance", "cmp_using", "converters", "define", @@ -68,3 +61,5 @@ __all__ = [ "validate", "validators", ] + +__getattr__ = _make_getattr(__name__) diff --git a/libs/attrs/__init__.pyi b/libs/attrs/__init__.pyi index fc44de46a..9372cfea1 100644 --- a/libs/attrs/__init__.pyi +++ b/libs/attrs/__init__.pyi @@ -23,6 +23,7 @@ from attr import __version_info__ as __version_info__ from attr import _FilterType from attr import assoc as assoc from attr import Attribute as Attribute +from attr import AttrsInstance as AttrsInstance from attr import cmp_using as cmp_using from attr import converters as converters from attr import define as define @@ -45,7 +46,7 @@ from attr import validators as validators # TODO: see definition of attr.asdict/astuple def asdict( - inst: Any, + inst: AttrsInstance, recurse: bool = ..., filter: Optional[_FilterType[Any]] = ..., dict_factory: Type[Mapping[Any, Any]] = ..., @@ -58,7 +59,7 @@ def asdict( # TODO: add support for returning NamedTuple from the mypy plugin def astuple( - inst: Any, + inst: AttrsInstance, recurse: bool = ..., filter: Optional[_FilterType[Any]] = ..., tuple_factory: Type[Sequence[Any]] = ..., diff --git a/libs/attrs/converters.py b/libs/attrs/converters.py index edfa8d3c1..7821f6c02 100644 --- a/libs/attrs/converters.py +++ b/libs/attrs/converters.py @@ -1,3 +1,3 @@ # SPDX-License-Identifier: MIT -from attr.converters import * # noqa +from attr.converters import * # noqa: F403 diff --git a/libs/attrs/exceptions.py b/libs/attrs/exceptions.py index bd9efed20..3323f9d21 100644 --- a/libs/attrs/exceptions.py +++ b/libs/attrs/exceptions.py @@ -1,3 +1,3 @@ # SPDX-License-Identifier: MIT -from attr.exceptions import * # noqa +from attr.exceptions import * # noqa: F403 diff --git a/libs/attrs/filters.py b/libs/attrs/filters.py index 52959005b..3080f4839 100644 --- a/libs/attrs/filters.py +++ b/libs/attrs/filters.py @@ -1,3 +1,3 @@ # SPDX-License-Identifier: MIT -from attr.filters import * # noqa +from attr.filters import * # noqa: F403 diff --git a/libs/attrs/setters.py b/libs/attrs/setters.py index 9b5077080..f3d73bb79 100644 --- a/libs/attrs/setters.py +++ b/libs/attrs/setters.py @@ -1,3 +1,3 @@ # SPDX-License-Identifier: MIT -from attr.setters import * # noqa +from attr.setters import * # noqa: F403 diff --git a/libs/attrs/validators.py b/libs/attrs/validators.py index ab2c9b302..037e124f2 100644 --- a/libs/attrs/validators.py +++ b/libs/attrs/validators.py @@ -1,3 +1,3 @@ # SPDX-License-Identifier: MIT -from attr.validators import * # noqa +from attr.validators import * # noqa: F403 diff --git a/libs/auditok-0.1.5.dist-info/INSTALLER b/libs/auditok-0.1.5.dist-info/INSTALLER new file mode 100644 index 000000000..a1b589e38 --- /dev/null +++ b/libs/auditok-0.1.5.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/libs/auditok-0.1.5.dist-info/LICENSE b/libs/auditok-0.1.5.dist-info/LICENSE new file mode 100644 index 000000000..94a9ed024 --- /dev/null +++ b/libs/auditok-0.1.5.dist-info/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/libs/auditok-0.1.5.dist-info/METADATA b/libs/auditok-0.1.5.dist-info/METADATA new file mode 100644 index 000000000..4192449d4 --- /dev/null +++ b/libs/auditok-0.1.5.dist-info/METADATA @@ -0,0 +1,103 @@ +Metadata-Version: 2.1 +Name: auditok +Version: 0.1.5 +Summary: A module for Audio/Acoustic Activity Detection +Home-page: http://github.com/amsehili/auditok/ +Author: Amine Sehili +Author-email: amine.sehili@gmail.com +License: GNU General Public License v3 (GPLv3) +Platform: ANY +Classifier: Development Status :: 3 - Alpha +Classifier: Environment :: Console +Classifier: Intended Audience :: Science/Research +Classifier: Intended Audience :: Developers +Classifier: Intended Audience :: Information Technology +Classifier: Intended Audience :: Telecommunications Industry +Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3) +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.2 +Classifier: Programming Language :: Python :: 3.3 +Classifier: Programming Language :: Python :: 3.4 +Classifier: Topic :: Multimedia :: Sound/Audio :: Analysis +Classifier: Topic :: Scientific/Engineering :: Information Analysis +Requires: PyAudio +Provides: auditok +License-File: LICENSE + +auditok, an AUDIo TOKenization tool +=================================== + +.. image:: https://travis-ci.org/amsehili/auditok.svg?branch=master + :target: https://travis-ci.org/amsehili/auditok + +.. image:: https://readthedocs.org/projects/auditok/badge/?version=latest + :target: http://auditok.readthedocs.org/en/latest/?badge=latest + :alt: Documentation Status + +`auditok` is an **Audio Activity Detection** tool that can process online data (read from an audio device or from standard input) as well as audio files. It can be used as a command line program and offers an easy to use API. + +The latest version of this documentation can be found at `Readthedocs `_. + +Requirements +------------ + +`auditok` can be used with standard Python! + +However, if you want more features, the following packages are needed: + +- `Pydub `_ : read audio files in popular audio formats (ogg, mp3, etc.) or extract audio from a video file. + +- `PyAudio `_ : read audio data from the microphone and play back detections. + +- `matplotlib `_ : plot audio signal and detections (see figures above). + +- `numpy `_ : required by matplotlib. Also used for math operations instead of standard python if available. + +- Optionally, you can use `sox` or `[p]arecord` for data acquisition and feed `auditok` using a pipe. + +Installation +------------ + +Install with pip: + +.. code:: bash + + sudo pip install auditok + +or install the latest version on Github: + +.. code:: bash + + git clone https://github.com/amsehili/auditok.git + cd auditok + sudo python setup.py install + +Getting started +--------------- + +.. toctree:: + :titlesonly: + :maxdepth: 2 + + Command-line Usage Guide + API Tutorial + +API Reference +------------- + +.. toctree:: + :maxdepth: 3 + + auditok.core + auditok.util + auditok.io + auditok.dataset + +Indices and tables +================== +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/libs/auditok-0.1.5.dist-info/RECORD b/libs/auditok-0.1.5.dist-info/RECORD new file mode 100644 index 000000000..b81262c5f --- /dev/null +++ b/libs/auditok-0.1.5.dist-info/RECORD @@ -0,0 +1,18 @@ +../../bin/auditok,sha256=san8L0JClGVSOLMSy84omHkcdNSCaX1CMO2_VYwB1Lw,237 +auditok-0.1.5.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +auditok-0.1.5.dist-info/LICENSE,sha256=jOtLnuWt7d5Hsx6XXB2QxzrSe2sWWh3NgMfFRetluQM,35147 +auditok-0.1.5.dist-info/METADATA,sha256=oU-1iYSzrQRSGOLnfzleHhDfwewUpMk5VsMMh6Bv4YA,3321 +auditok-0.1.5.dist-info/RECORD,, +auditok-0.1.5.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +auditok-0.1.5.dist-info/WHEEL,sha256=Xo9-1PvkuimrydujYJAjF7pCkriuXBpUPEjma1nZyJ0,92 +auditok-0.1.5.dist-info/entry_points.txt,sha256=jt34cdMDAqfsonQBYK0N9L7Kv4B7ypATasunykrWLaA,49 +auditok-0.1.5.dist-info/top_level.txt,sha256=LORjvmbbVaDSnZc-kJOP6jEludE4SoiY2rLmCJ-fFDg,8 +auditok/__init__.py,sha256=C8Umr2R2L5AogNuOaYyZsjg10NX8beuTReaB1aPf1RU,300 +auditok/cmdline.py,sha256=bYrGbnZyVPr5rVCGXKiUAq4Zw3rYgP8_6q-BnJs9rUU,32948 +auditok/core.py,sha256=1PbMaPHgZnDQPNCWJVeppxS4mIibcfh5r9lNtV1mzQw,17341 +auditok/data/1to6arabic_16000_mono_bc_noise.wav,sha256=XoLVWbNUWdxPG6YX9KMEYszmju1l7aVd0nUjhbz-aXQ,601256 +auditok/data/was_der_mensch_saet_das_wird_er_vielfach_ernten_44100Hz_mono_lead_trail_silence.wav,sha256=X2nH6tVQS4VCF7jXkIDD0CJLXp3InSBy0iiOCFh7a3E,1493036 +auditok/dataset.py,sha256=dzGwcDwxIu28La1fCiWa4tbDkPT3CWhydPfuEDfMMhM,774 +auditok/exceptions.py,sha256=5X7lBnNfpkJCYbRVzZEb670lM0qdHodLsEeQ6CKJRyY,117 +auditok/io.py,sha256=9dyApG9w7oqh5MM-gUKXVyvqPk25pIrKLKQCvaeG3Y8,14609 +auditok/util.py,sha256=WlErbyHGlEaTp4_jSoB2XLqGU_5i4qD-KCVTrlh_OGs,31816 diff --git a/libs/auditok-0.1.5.dist-info/REQUESTED b/libs/auditok-0.1.5.dist-info/REQUESTED new file mode 100644 index 000000000..e69de29bb diff --git a/libs/auditok-0.1.5.dist-info/WHEEL b/libs/auditok-0.1.5.dist-info/WHEEL new file mode 100644 index 000000000..ba48cbcf9 --- /dev/null +++ b/libs/auditok-0.1.5.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.41.3) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/libs/auditok-0.1.5.dist-info/entry_points.txt b/libs/auditok-0.1.5.dist-info/entry_points.txt new file mode 100644 index 000000000..73235b7f1 --- /dev/null +++ b/libs/auditok-0.1.5.dist-info/entry_points.txt @@ -0,0 +1,2 @@ +[console_scripts] +auditok = auditok.cmdline:main diff --git a/libs/auditok-0.1.5.dist-info/top_level.txt b/libs/auditok-0.1.5.dist-info/top_level.txt new file mode 100644 index 000000000..d5aa6315f --- /dev/null +++ b/libs/auditok-0.1.5.dist-info/top_level.txt @@ -0,0 +1 @@ +auditok diff --git a/libs/auditok/cmdline.py b/libs/auditok/cmdline.py old mode 100755 new mode 100644 diff --git a/libs/babelfish-0.6.0.dist-info/INSTALLER b/libs/babelfish-0.6.0.dist-info/INSTALLER new file mode 100644 index 000000000..a1b589e38 --- /dev/null +++ b/libs/babelfish-0.6.0.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/libs/babelfish-0.6.0.dist-info/LICENSE b/libs/babelfish-0.6.0.dist-info/LICENSE new file mode 100644 index 000000000..dcb883269 --- /dev/null +++ b/libs/babelfish-0.6.0.dist-info/LICENSE @@ -0,0 +1,25 @@ +Copyright (c) 2015, by the respective authors (see AUTHORS file). +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name of the BabelFish authors nor the names of its contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/libs/babelfish-0.6.0.dist-info/METADATA b/libs/babelfish-0.6.0.dist-info/METADATA new file mode 100644 index 000000000..5ed0d4c5c --- /dev/null +++ b/libs/babelfish-0.6.0.dist-info/METADATA @@ -0,0 +1,105 @@ +Metadata-Version: 2.1 +Name: babelfish +Version: 0.6.0 +Summary: A module to work with countries and languages +Home-page: https://github.com/Diaoul/babelfish +License: BSD-3-Clause +Keywords: language,country,locale +Author: Antoine Bertin +Author-email: ant.bertin@gmail.com +Requires-Python: >=3.6,<4.0 +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: BSD License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3 :: Only +Classifier: Topic :: Software Development :: Internationalization +Classifier: Topic :: Software Development :: Libraries :: Python Modules +Project-URL: Repository, https://github.com/Diaoul/babelfish +Description-Content-Type: text/markdown + +# BabelFish +BabelFish is a Python library to work with countries and languages. + +[![tests](https://github.com/Diaoul/babelfish/actions/workflows/test.yml/badge.svg)](https://github.com/Diaoul/babelfish/actions/workflows/test.yml) + +## Usage +BabelFish provides scripts, countries and languages from their respective ISO +standards and a handy way to manipulate them with converters. + +### Script +Script representation from 4-letter code (ISO-15924): +```python +>>> import babelfish +>>> script = babelfish.Script('Hira') +>>> script + for you +""" + + expect = """
+
some
+ for you 
+
+
+""" + soup = self.soup(markup) + assert expect == soup.div.prettify() def test_prettify_accepts_formatter_function(self): soup = BeautifulSoup("foo", 'html.parser') @@ -213,428 +249,6 @@ class TestFormatters(SoupTest): assert soup.contents[0].name == 'pre' -class TestCSSSelectors(SoupTest): - """Test basic CSS selector functionality. - - This functionality is implemented in soupsieve, which has a much - more comprehensive test suite, so this is basically an extra check - that soupsieve works as expected. - """ - - HTML = """ - - - -The title - - - -Hello there. -
-
-

An H1

-

Some text

-

Some more text

-

An H2

-

Another

-Bob -

Another H2

-me - -span1a1 -span1a2 test - -span2a1 - - - -
- -
- - - - - - - - -

English

-

English UK

-

English US

-

French

-
- - -""" - - def setup_method(self): - self.soup = BeautifulSoup(self.HTML, 'html.parser') - - def assert_selects(self, selector, expected_ids, **kwargs): - el_ids = [el['id'] for el in self.soup.select(selector, **kwargs)] - el_ids.sort() - expected_ids.sort() - assert expected_ids == el_ids, "Selector %s, expected [%s], got [%s]" % ( - selector, ', '.join(expected_ids), ', '.join(el_ids) - ) - - assertSelect = assert_selects - - def assert_select_multiple(self, *tests): - for selector, expected_ids in tests: - self.assert_selects(selector, expected_ids) - - def test_one_tag_one(self): - els = self.soup.select('title') - assert len(els) == 1 - assert els[0].name == 'title' - assert els[0].contents == ['The title'] - - def test_one_tag_many(self): - els = self.soup.select('div') - assert len(els) == 4 - for div in els: - assert div.name == 'div' - - el = self.soup.select_one('div') - assert 'main' == el['id'] - - def test_select_one_returns_none_if_no_match(self): - match = self.soup.select_one('nonexistenttag') - assert None == match - - - def test_tag_in_tag_one(self): - els = self.soup.select('div div') - self.assert_selects('div div', ['inner', 'data1']) - - def test_tag_in_tag_many(self): - for selector in ('html div', 'html body div', 'body div'): - self.assert_selects(selector, ['data1', 'main', 'inner', 'footer']) - - - def test_limit(self): - self.assert_selects('html div', ['main'], limit=1) - self.assert_selects('html body div', ['inner', 'main'], limit=2) - self.assert_selects('body div', ['data1', 'main', 'inner', 'footer'], - limit=10) - - def test_tag_no_match(self): - assert len(self.soup.select('del')) == 0 - - def test_invalid_tag(self): - with pytest.raises(SelectorSyntaxError): - self.soup.select('tag%t') - - def test_select_dashed_tag_ids(self): - self.assert_selects('custom-dashed-tag', ['dash1', 'dash2']) - - def test_select_dashed_by_id(self): - dashed = self.soup.select('custom-dashed-tag[id=\"dash2\"]') - assert dashed[0].name == 'custom-dashed-tag' - assert dashed[0]['id'] == 'dash2' - - def test_dashed_tag_text(self): - assert self.soup.select('body > custom-dashed-tag')[0].text == 'Hello there.' - - def test_select_dashed_matches_find_all(self): - assert self.soup.select('custom-dashed-tag') == self.soup.find_all('custom-dashed-tag') - - def test_header_tags(self): - self.assert_select_multiple( - ('h1', ['header1']), - ('h2', ['header2', 'header3']), - ) - - def test_class_one(self): - for selector in ('.onep', 'p.onep', 'html p.onep'): - els = self.soup.select(selector) - assert len(els) == 1 - assert els[0].name == 'p' - assert els[0]['class'] == ['onep'] - - def test_class_mismatched_tag(self): - els = self.soup.select('div.onep') - assert len(els) == 0 - - def test_one_id(self): - for selector in ('div#inner', '#inner', 'div div#inner'): - self.assert_selects(selector, ['inner']) - - def test_bad_id(self): - els = self.soup.select('#doesnotexist') - assert len(els) == 0 - - def test_items_in_id(self): - els = self.soup.select('div#inner p') - assert len(els) == 3 - for el in els: - assert el.name == 'p' - assert els[1]['class'] == ['onep'] - assert not els[0].has_attr('class') - - def test_a_bunch_of_emptys(self): - for selector in ('div#main del', 'div#main div.oops', 'div div#main'): - assert len(self.soup.select(selector)) == 0 - - def test_multi_class_support(self): - for selector in ('.class1', 'p.class1', '.class2', 'p.class2', - '.class3', 'p.class3', 'html p.class2', 'div#inner .class2'): - self.assert_selects(selector, ['pmulti']) - - def test_multi_class_selection(self): - for selector in ('.class1.class3', '.class3.class2', - '.class1.class2.class3'): - self.assert_selects(selector, ['pmulti']) - - def test_child_selector(self): - self.assert_selects('.s1 > a', ['s1a1', 's1a2']) - self.assert_selects('.s1 > a span', ['s1a2s1']) - - def test_child_selector_id(self): - self.assert_selects('.s1 > a#s1a2 span', ['s1a2s1']) - - def test_attribute_equals(self): - self.assert_select_multiple( - ('p[class="onep"]', ['p1']), - ('p[id="p1"]', ['p1']), - ('[class="onep"]', ['p1']), - ('[id="p1"]', ['p1']), - ('link[rel="stylesheet"]', ['l1']), - ('link[type="text/css"]', ['l1']), - ('link[href="blah.css"]', ['l1']), - ('link[href="no-blah.css"]', []), - ('[rel="stylesheet"]', ['l1']), - ('[type="text/css"]', ['l1']), - ('[href="blah.css"]', ['l1']), - ('[href="no-blah.css"]', []), - ('p[href="no-blah.css"]', []), - ('[href="no-blah.css"]', []), - ) - - def test_attribute_tilde(self): - self.assert_select_multiple( - ('p[class~="class1"]', ['pmulti']), - ('p[class~="class2"]', ['pmulti']), - ('p[class~="class3"]', ['pmulti']), - ('[class~="class1"]', ['pmulti']), - ('[class~="class2"]', ['pmulti']), - ('[class~="class3"]', ['pmulti']), - ('a[rel~="friend"]', ['bob']), - ('a[rel~="met"]', ['bob']), - ('[rel~="friend"]', ['bob']), - ('[rel~="met"]', ['bob']), - ) - - def test_attribute_startswith(self): - self.assert_select_multiple( - ('[rel^="style"]', ['l1']), - ('link[rel^="style"]', ['l1']), - ('notlink[rel^="notstyle"]', []), - ('[rel^="notstyle"]', []), - ('link[rel^="notstyle"]', []), - ('link[href^="bla"]', ['l1']), - ('a[href^="http://"]', ['bob', 'me']), - ('[href^="http://"]', ['bob', 'me']), - ('[id^="p"]', ['pmulti', 'p1']), - ('[id^="m"]', ['me', 'main']), - ('div[id^="m"]', ['main']), - ('a[id^="m"]', ['me']), - ('div[data-tag^="dashed"]', ['data1']) - ) - - def test_attribute_endswith(self): - self.assert_select_multiple( - ('[href$=".css"]', ['l1']), - ('link[href$=".css"]', ['l1']), - ('link[id$="1"]', ['l1']), - ('[id$="1"]', ['data1', 'l1', 'p1', 'header1', 's1a1', 's2a1', 's1a2s1', 'dash1']), - ('div[id$="1"]', ['data1']), - ('[id$="noending"]', []), - ) - - def test_attribute_contains(self): - self.assert_select_multiple( - # From test_attribute_startswith - ('[rel*="style"]', ['l1']), - ('link[rel*="style"]', ['l1']), - ('notlink[rel*="notstyle"]', []), - ('[rel*="notstyle"]', []), - ('link[rel*="notstyle"]', []), - ('link[href*="bla"]', ['l1']), - ('[href*="http://"]', ['bob', 'me']), - ('[id*="p"]', ['pmulti', 'p1']), - ('div[id*="m"]', ['main']), - ('a[id*="m"]', ['me']), - # From test_attribute_endswith - ('[href*=".css"]', ['l1']), - ('link[href*=".css"]', ['l1']), - ('link[id*="1"]', ['l1']), - ('[id*="1"]', ['data1', 'l1', 'p1', 'header1', 's1a1', 's1a2', 's2a1', 's1a2s1', 'dash1']), - ('div[id*="1"]', ['data1']), - ('[id*="noending"]', []), - # New for this test - ('[href*="."]', ['bob', 'me', 'l1']), - ('a[href*="."]', ['bob', 'me']), - ('link[href*="."]', ['l1']), - ('div[id*="n"]', ['main', 'inner']), - ('div[id*="nn"]', ['inner']), - ('div[data-tag*="edval"]', ['data1']) - ) - - def test_attribute_exact_or_hypen(self): - self.assert_select_multiple( - ('p[lang|="en"]', ['lang-en', 'lang-en-gb', 'lang-en-us']), - ('[lang|="en"]', ['lang-en', 'lang-en-gb', 'lang-en-us']), - ('p[lang|="fr"]', ['lang-fr']), - ('p[lang|="gb"]', []), - ) - - def test_attribute_exists(self): - self.assert_select_multiple( - ('[rel]', ['l1', 'bob', 'me']), - ('link[rel]', ['l1']), - ('a[rel]', ['bob', 'me']), - ('[lang]', ['lang-en', 'lang-en-gb', 'lang-en-us', 'lang-fr']), - ('p[class]', ['p1', 'pmulti']), - ('[blah]', []), - ('p[blah]', []), - ('div[data-tag]', ['data1']) - ) - - def test_quoted_space_in_selector_name(self): - html = """
nope
-
yes
- """ - soup = BeautifulSoup(html, 'html.parser') - [chosen] = soup.select('div[style="display: right"]') - assert "yes" == chosen.string - - def test_unsupported_pseudoclass(self): - with pytest.raises(NotImplementedError): - self.soup.select("a:no-such-pseudoclass") - - with pytest.raises(SelectorSyntaxError): - self.soup.select("a:nth-of-type(a)") - - def test_nth_of_type(self): - # Try to select first paragraph - els = self.soup.select('div#inner p:nth-of-type(1)') - assert len(els) == 1 - assert els[0].string == 'Some text' - - # Try to select third paragraph - els = self.soup.select('div#inner p:nth-of-type(3)') - assert len(els) == 1 - assert els[0].string == 'Another' - - # Try to select (non-existent!) fourth paragraph - els = self.soup.select('div#inner p:nth-of-type(4)') - assert len(els) == 0 - - # Zero will select no tags. - els = self.soup.select('div p:nth-of-type(0)') - assert len(els) == 0 - - def test_nth_of_type_direct_descendant(self): - els = self.soup.select('div#inner > p:nth-of-type(1)') - assert len(els) == 1 - assert els[0].string == 'Some text' - - def test_id_child_selector_nth_of_type(self): - self.assert_selects('#inner > p:nth-of-type(2)', ['p1']) - - def test_select_on_element(self): - # Other tests operate on the tree; this operates on an element - # within the tree. - inner = self.soup.find("div", id="main") - selected = inner.select("div") - # The
tag was selected. The