diff --git a/dockerhub.py b/dockerhub.py new file mode 100644 index 0000000..208c663 --- /dev/null +++ b/dockerhub.py @@ -0,0 +1,164 @@ +import asyncio +import re +import sys +from logging import Logger +from multiprocessing import Process +from typing import Any + +import httpx +from httpx import AsyncHTTPTransport, AsyncClient +from packaging.version import parse as parse_version +from termcolor import colored + +SERVICES = { + 'nextcloud': '25.0.4', + 'gitea/gitea': '1.18.5', + 'caddy': '2.6.4', + 'mediawiki': '1.39.2', + 'bitwarden/server': '2023.2.0', + 'redis': '7.0.8', + 'nginx': '1.23.3', + 'mariadb': '10.11.2', + 'postgres': '15.2', + 'mysql': '8.0.32', + 'selenoid/firefox': '110.0', + 'python': '3.11.1', +} + + +def configure_logger() -> Logger: + try: + from loguru import logger as loguru_logger + + loguru_logger.remove() + loguru_logger.add( + sink=sys.stdout, + colorize=True, + level='DEBUG', + format='{time:DD.MM.YYYY HH:mm:ss} | {level} | {message}', + ) + return loguru_logger # type: ignore + except ImportError: + import logging + + logging_logger = logging.getLogger('main_logger') + formatter = logging.Formatter( + datefmt='%Y.%m.%d %H:%M:%S', + fmt='%(asctime)s | %(levelname)s | func name: %(funcName)s | message: %(message)s', + ) + handler = logging.StreamHandler(sys.stdout) + handler.setFormatter(formatter) + logging_logger.setLevel(logging.INFO) + logging_logger.addHandler(handler) + return logging_logger + + +logger = configure_logger() + + +class DockerHubScanner: + + # bitwarden/server + # https://hub.docker.com/v2/namespaces/bitwarden/repositories/server/tags?page=2 + + # caddy + # https://registry.hub.docker.com/v2/repositories/library/caddy/tags?page=1 + + DOCKERHUB_REGISTRY_API = 'https://registry.hub.docker.com/v2/repositories/library' + DOCKERHUB_API = 'https://hub.docker.com/v2/namespaces' + + def _docker_hub_api_url(self, service_name: str) -> str: + + if '/' in service_name: + namespace, name = service_name.split('/') + url = f'{self.DOCKERHUB_API}/{namespace}/repositories/{name}/tags' + else: + url = f'{self.DOCKERHUB_REGISTRY_API}/{service_name}/tags' + return url + + @staticmethod + async def _async_request(client: AsyncClient, url: str) -> dict[str, Any] | None: + + response = await client.get(url) + status = response.status_code + if status == httpx.codes.OK: + return response.json() + return None + + @staticmethod + def _get_next_page_and_tags_from_payload(payload: dict[str, Any]) -> tuple[str | None, list[str]]: + next_page = payload['next'] + names = [release['name'] for release in payload['results']] + return next_page, names + + async def get_tags(self, service_name: str) -> dict[str, list[str]]: + """ + To make method really async it should be rewritten on pages not by get next page each time. + Also, dockerhub protected from bruteforce requests. + Better with getting next page each time + """ + + tags = [] + url = self._docker_hub_api_url(service_name) + transport = AsyncHTTPTransport(retries=1) + async with AsyncClient(transport=transport) as client: + payload = await self._async_request(client=client, url=url) + + if not payload: + return {service_name: tags} + + next_page, names = self._get_next_page_and_tags_from_payload(payload) + + tags.extend(names) + + while SERVICES[service_name] not in tags: + payload = await self._async_request(client=client, url=next_page) + next_page, names = self._get_next_page_and_tags_from_payload(payload) + tags.extend(names) + + # filter tags contains versions 1.18.3 and not contains letters 1.18.3-fpm-alpine. Sort by version number + tags = sorted( + list(filter(lambda t: re.search(r'\d+\.\d', t) and not re.search(r'[a-z]', t), tags)), + reverse=True, + key=parse_version, + ) + + # Do not show older versions than current in tags + tags = tags[:tags.index(SERVICES[service_name]) + 1] + return {service_name: tags} + + def get_data(self, service_name: str) -> dict[str, list[str]]: + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + services_tags = loop.run_until_complete(self.get_tags(service_name)) + + return services_tags + + def print_data(self, service_name: str) -> None: + + data = self.get_data(service_name) + print( + f"Service: {colored(service_name, color='light_grey')}", + f"\nTags: {colored(str(data[service_name]), color='magenta')}", + f"\nCurrent version: {colored(SERVICES[service_name], color='cyan')}" + ) + + if data[service_name][0] > SERVICES[service_name]: + print(f"New version of {service_name}: {colored(data[service_name][0], color='yellow')}") + print() + + +if __name__ == '__main__': + + print('Services'.center(50, '-'), '\n') + + dockerhub_scanner = DockerHubScanner() + processes = [] + + for service in SERVICES: + process = Process(target=dockerhub_scanner.print_data, kwargs={'service_name': service}) + processes.append(process) + process.start() + + for process in processes: + process.join() diff --git a/get-gitlab-projects/get_project_core/__init__.py b/get-gitlab-projects/get_project_core/__init__.py deleted file mode 100644 index 62a2ee5..0000000 --- a/get-gitlab-projects/get_project_core/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -import requests -from requests.packages.urllib3.exceptions import InsecureRequestWarning - -requests.packages.urllib3.disable_warnings(InsecureRequestWarning) diff --git a/get-gitlab-projects/get_project_core/settings.py b/get-gitlab-projects/get_project_core/settings.py deleted file mode 100644 index dea5698..0000000 --- a/get-gitlab-projects/get_project_core/settings.py +++ /dev/null @@ -1,27 +0,0 @@ -import importlib.util -import logging -import sys -from pathlib import Path - - -current_dir = Path(__file__).parent.parent - -# use loguru if it is possible for color output -if importlib.util.find_spec('loguru') is not None: - from loguru import logger - logger.remove() - logger.add(sink=sys.stdout, colorize=True, level='DEBUG', - format="{time:DD.MM.YYYY HH:mm:ss} | {level} | " - "{message}") - -# use standard logging -else: - logger = logging.getLogger() - logger.setLevel(logging.INFO) - - console_handler = logging.StreamHandler() - console_handler.setLevel(logging.INFO) - log_formatter = logging.Formatter("%(asctime)s | %(levelname)s | %(message)s") - console_handler.setFormatter(log_formatter) - - logger.addHandler(console_handler) diff --git a/get-gitlab-projects/get_project_core/update-repos.sh b/get-gitlab-projects/get_project_core/update-repos.sh deleted file mode 100755 index 7598269..0000000 --- a/get-gitlab-projects/get_project_core/update-repos.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/bash - -git pull --all \ No newline at end of file diff --git a/get-gitlab-projects/get_projects.py b/get-gitlab-projects/get_projects.py deleted file mode 100644 index 6ba9e42..0000000 --- a/get-gitlab-projects/get_projects.py +++ /dev/null @@ -1,51 +0,0 @@ -import json # noqa # pylint: disable=unused-import -import subprocess -import sys -import time - -import requests - -from get_project_core.settings import current_dir, logger - -GITLAB_TOKEN = '' - -headers = {'PRIVATE-TOKEN': GITLAB_TOKEN} - - -def create_repositories(group_id: int): - """ - Create submodules from gitlab group - - :param group_id: Can be find under group name - """ - request = requests.get(f'https://scm.x5.ru/api/v4/groups/{group_id}/projects', headers=headers, verify=False) - # logger.info(f'{json.dumps(request.json(), indent=4, separators=(",", ":"))}') - - repos = request.json() - - for repo in repos: - name = str(repo.get("ssh_url_to_repo", None)).strip() - subprocess.Popen(['git', 'submodule', 'add', name]) - logger.info(f'Created: {name}') - time.sleep(15) - - -def update_submodules(): - """ - Update all submodules - - """ - subprocess.Popen(['git', 'submodule', 'foreach', f'{current_dir}/get-project-core/update-repos.sh']) - - -if __name__ == '__main__': - args = sys.argv[1:] - try: - group = args[0] - logger.info(group) - create_repositories(group_id=int(group)) - update_submodules() - except IndexError: - logger.error('Gitlab group id must be set') - except ValueError: - logger.error('Gitlab group id must be integer') diff --git a/get-gitlab-projects/requirements.txt b/get-gitlab-projects/requirements.txt deleted file mode 100644 index 663bd1f..0000000 --- a/get-gitlab-projects/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -requests \ No newline at end of file diff --git a/goodgame.py b/goodgame.py new file mode 100644 index 0000000..085b68d --- /dev/null +++ b/goodgame.py @@ -0,0 +1,200 @@ +import asyncio +import sys +import time +from logging import Logger +from multiprocessing import Process +from typing import Any + +import aiohttp +import requests + + +def configure_logger() -> Logger: + try: + from loguru import logger as loguru_logger + + loguru_logger.remove() + loguru_logger.add( + sink=sys.stdout, + colorize=True, + level='DEBUG', + format="{time:DD.MM.YYYY HH:mm:ss} | {level} | {message}", + ) + return loguru_logger # type: ignore + except ImportError: + import logging + + logging_logger = logging.getLogger('main_logger') + formatter = logging.Formatter( + datefmt="%Y.%m.%d %H:%M:%S", + fmt='%(asctime)s | %(levelname)s | func name: %(funcName)s | message: %(message)s', + ) + handler = logging.StreamHandler(sys.stdout) + handler.setFormatter(formatter) + logging_logger.setLevel(logging.INFO) + logging_logger.addHandler(handler) + return logging_logger + + +logger = configure_logger() + + +class GoodGame: + BASE_URL = 'https://goodgame.ru/api/4/streams' + PAGES_FOR_ASYNC_SCAN = 25 + CURRENT_WATCHERS_FILTER = 1 + + def __init__(self) -> None: + self.all_streams: dict[int, dict[str, Any]] = dict() + + @staticmethod + def _show_time_and_result(message: str) -> Any: + def wrapper(func: Any) -> Any: + def new_func(*args: Any, **kwargs: Any) -> None: + begin = time.time() + result = func(*args, **kwargs) + end = time.time() + logger.info(f'{message} execution time, sec: {round(end - begin, 2)}') + print(result) + + return new_func + + return wrapper + + def get_last_page_number(self) -> int: + """ + Deprecated + """ + last_page = 1 + for page in range(20, 0, -1): + response = requests.get(f'{self.BASE_URL}?page={page}') + if response.json()["streams"]: + last_page = page + break + return last_page + + def get_max_current_viewers_count(self) -> int | None: + """ + Deprecated + """ + response = requests.get(f'{self.BASE_URL}?page=1') + max_current_viewers = response.json()['streams'][0].get('viewers', None) + return max_current_viewers + + def _sort_trim_dict(self, data: dict[str, int]) -> dict[str, int]: + sorted_data = dict(sorted(data.items(), key=lambda x: x[1], reverse=True)) + new_data = { + stream: viewers_count + for stream, viewers_count in sorted_data.items() + if int(viewers_count) >= self.CURRENT_WATCHERS_FILTER + } + return new_data + + def __count_streams_with_watchers(self, current_watchers: list[int]) -> int: + return len( + list( + filter( + lambda stream: stream['viewers'] in current_watchers, + self.all_streams.values(), + ) + ) + ) + + def __prepare_result(self, max_current_viewers: int) -> str: + total_viewers: dict[str, int] = dict() + for stream in self.all_streams.values(): + if ( + max_current_viewers + and int(stream.get('viewers', 0)) <= max_current_viewers + ): + total_viewers[ + f'{stream["streamer"]["username"]} [{stream["game"]["url"]}]' + ] = int(stream['viewers']) + watchers_0 = self.__count_streams_with_watchers(current_watchers=[0]) + watchers_1 = self.__count_streams_with_watchers(current_watchers=[1]) + minimal_watchers = self.__count_streams_with_watchers(current_watchers=[0, 1]) + return ( + f'Total streams: {len(self.all_streams)} -> ' + f'with minimal watchers {round(minimal_watchers / len(self.all_streams) * 100)}%\n' + f'Total streams with 0 viewers: {watchers_0} -> {round(watchers_0/len(self.all_streams) * 100)}%\n' + f'Total streams with 1 viewer: {watchers_1} -> {round(watchers_1/len(self.all_streams) * 100)}%\n' + f'Total viewers: {sum(total_viewers.values())}\n' + f'Streams: {self._sort_trim_dict(total_viewers)}\n' + f'{"-"*76}' + ) + + async def _async_request(self, session: aiohttp.ClientSession, url: str) -> None: + async with asyncio.Semaphore(500): + counter = 0 + while True: + try: + counter += 1 + resp = await session.get(url) + async with resp: + if resp.status == 200: + data = await resp.json() + for stream in data['streams']: + self.all_streams.update({stream['id']: stream}) + return data['streams'] + except Exception as connection_error: + if counter < 5: + await asyncio.sleep(10) + else: + raise connection_error + + async def _async_data_scrapper(self) -> int: + async with aiohttp.ClientSession() as session: + + streams = await asyncio.gather( + *[ + self._async_request(session, f'{self.BASE_URL}?page={page}') + for page in range(1, self.PAGES_FOR_ASYNC_SCAN + 1) + ], + return_exceptions=True, + ) + max_current_viewers = streams[0][0]['viewers'] + return max_current_viewers + + @_show_time_and_result(message='Async counter') + def async_counter(self) -> str: + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + max_current_viewers = loop.run_until_complete(self._async_data_scrapper()) + return self.__prepare_result(max_current_viewers) + + @_show_time_and_result(message='Sync counter') + def sync_counter(self) -> str: + page = 1 + + resp = requests.get(f'{self.BASE_URL}?page={page}') + streams = resp.json()['streams'] + for stream in streams: + self.all_streams.update({stream['id']: stream}) + max_current_viewers = streams[0]['viewers'] + while streams: + page += 1 + resp = requests.get(f'{self.BASE_URL}?page={page}') + streams = resp.json()['streams'] + for stream in streams: + self.all_streams.update({stream['id']: stream}) + return self.__prepare_result(max_current_viewers) + + +if __name__ == '__main__': + print("-" * 76) + good_game = GoodGame() + start = time.time() + async_process = Process( + target=good_game.async_counter, args=(), kwargs={}, name='async_process' + ) + sync_process = Process( + target=good_game.sync_counter, args=(), kwargs={}, name='sync_process' + ) + + async_process.start() + sync_process.start() + + async_process.join() + sync_process.join() + stop = time.time() + logger.info(f'End all processes. Execution time: {round(stop-start, 2)} seconds') diff --git a/linked_list.py b/linked_list.py new file mode 100644 index 0000000..fa44791 --- /dev/null +++ b/linked_list.py @@ -0,0 +1,123 @@ +# Python3 program to merge sort of linked list + +# create Node using class Node. +class Node: + def __init__(self, data): + self.data = data + self.next = None + + def __repr__(self): + return f'{self.data}' + + +class LinkedList: + def __init__(self): + self.head = None + + # push new value to linked list + # using append method + def append(self, new_value): + + # Allocate new node + new_node = Node(new_value) + + # if head is None, initialize it to new node + if self.head is None: + self.head = new_node + return + curr_node = self.head + while curr_node.next is not None: + curr_node = curr_node.next + + # Append the new node at the end + # of the linked list + curr_node.next = new_node + + def sorted_merge(self, node_a, node_b): + + # Base cases + if node_a is None: + return node_b + if node_b is None: + return node_a + + # pick either a or b and recur.. + if node_a.data <= node_b.data: + result = node_a + result.next = self.sorted_merge(node_a.next, node_b) + else: + result = node_b + result.next = self.sorted_merge(node_a, node_b.next) + return result + + def merge_sort(self, head): + + # Base case if head is None + if head is None or head.next is None: + return head + + # get the middle of the list + middle = self.get_middle(head) + next_to_middle = middle.next + + # set the next of middle node to None + middle.next = None + + # Apply mergeSort on left list + left = self.merge_sort(head) + + # Apply mergeSort on right list + right = self.merge_sort(next_to_middle) + + # Merge the left and right lists + sorted_list = self.sorted_merge(left, right) + return sorted_list + + # Utility function to get the middle + # of the linked list + @staticmethod + def get_middle(head): + if head is None: + return head + + slow = head + fast = head + + while fast.next is not None and fast.next.next is not None: + slow = slow.next + fast = fast.next.next + + return slow + + def __repr__(self): + # Utility function to print the linked list + represent = '' + if self.head is None: + print(' ') + return + curr_node = self.head + while curr_node: + represent += f'{curr_node.data} -> ' + curr_node = curr_node.next + return represent[:-4] + + +# Driver Code +if __name__ == '__main__': + li = LinkedList() + + li.append(15) + li.append(10) + li.append(5) + li.append(20) + li.append(3) + li.append(2) + + print(li) + + # Apply merge Sort + li.head = li.merge_sort(li.head) + print("Sorted Linked List is:") + print(li) + + diff --git a/requiremetns.txt b/requiremetns.txt index eb140fd..cbff422 100644 --- a/requiremetns.txt +++ b/requiremetns.txt @@ -1,159 +1,177 @@ -aiohttp==3.8.1 -aiosignal==1.2.0 -alembic==1.7.6 -altgraph==0.17.2 -anyio==3.5.0 -arrow==1.2.2 -asgiref==3.5.0 -asttokens==2.0.5 +aiohttp==3.8.4 +aiosignal==1.3.1 +alembic==1.9.4 +altgraph==0.17.3 +amqp==5.1.1 +anyio==3.6.2 +arrow==1.2.3 +asgiref==3.6.0 +asttokens==2.2.1 async-generator==1.10 async-timeout==4.0.2 -attrs==21.4.0 -Babel==2.9.1 +attrs==22.2.0 backcall==0.2.0 -backports.entry-points-selectable==1.1.1 -bcrypt==3.2.0 -bidict==0.21.4 +bcrypt==4.0.1 +billiard==3.6.4.0 binaryornot==0.4.4 -black==22.1.0 -blinker==1.4 -Brotli==1.0.9 -CacheControl==0.12.10 +black==22.12.0 +CacheControl==0.12.11 cachy==0.3.0 -certifi==2021.10.8 -cffi==1.15.0 -chardet==4.0.0 -charset-normalizer==2.0.12 -cleo==0.8.1 -click==8.0.4 +celery==5.2.7 +certifi==2022.12.7 +cffi==1.15.1 +cfgv==3.3.1 +chardet==5.1.0 +charset-normalizer==3.0.1 +cleo==2.0.1 +click==8.1.3 +click-didyoumean==0.3.0 +click-plugins==1.1.1 +click-repl==0.2.0 clikit==0.6.2 -cookiecutter==1.7.3 -coverage==6.3.2 -crashtest==0.3.1 -cryptography==36.0.1 +cookiecutter==2.1.1 +coverage==7.2.1 +crashtest==0.4.1 +cryptography==39.0.0 +cyclonedx-python-lib==3.1.5 decorator==5.1.1 -distlib==0.3.4 -Django==4.0.3 -dnspython==2.2.0 -email-validator==1.1.3 -executing==0.8.3 -fastapi==0.74.1 -filelock==3.6.0 -Flask==2.0.3 -Flask-Login==0.5.0 -Flask-Principal==0.4.0 -Flask-SQLAlchemy==2.5.1 -Flask-WTF==1.0.0 -frozenlist==1.3.0 -greenlet==1.1.2 -h11==0.13.0 +distlib==0.3.6 +Django==4.1.7 +dparse==0.6.2 +dulwich==0.20.50 +executing==1.2.0 +factory-boy==3.2.1 +Faker==16.9.0 +fastapi==0.89.1 +filelock==3.9.0 +flake8==6.0.0 +frozenlist==1.3.3 +greenlet==2.0.2 +gunicorn==20.1.0 +h11==0.14.0 html5lib==1.1 -idna==3.3 -importlib-metadata==4.11.2 -iniconfig==1.1.1 -ipython==8.1.0 -itsdangerous==2.1.0 -jedi==0.18.1 -jeepney==0.7.1 -Jinja2==3.0.3 +httpcore==0.16.3 +httpx==0.23.3 +identify==2.5.18 +idna==3.4 +importlib-metadata==6.0.0 +iniconfig==2.0.0 +ipython==8.11.0 +jaraco.classes==3.2.3 +jedi==0.18.2 +jeepney==0.8.0 +Jinja2==3.1.2 jinja2-time==0.2.0 -keyring==23.5.0 +jsonschema==4.17.3 +keyring==23.13.1 +kombu==5.2.4 lockfile==0.12.2 loguru==0.6.0 -Mako==1.1.6 -MarkupSafe==2.1.0 -matplotlib-inline==0.1.3 -MouseInfo==0.1.3 -msgpack==1.0.3 -multidict==6.0.2 -mypy==0.931 -mypy-extensions==0.4.3 -outcome==1.1.0 +Mako==1.2.4 +markdown-it-py==2.1.0 +MarkupSafe==2.1.2 +matplotlib-inline==0.1.6 +mccabe==0.7.0 +mdurl==0.1.2 +more-itertools==9.0.0 +MouseInfo==0.1.0 +msgpack==1.0.4 +multidict==6.0.4 +mypy==0.991 +mypy-extensions==1.0.0 +nodeenv==1.7.0 +numpy==1.24.2 +orjson==3.8.7 +outcome==1.2.0 +packageurl-python==0.10.4 packaging==21.3 -paramiko==2.9.2 parso==0.8.3 -passlib==1.7.4 pastel==0.2.1 -pathspec==0.9.0 +pathspec==0.11.0 pexpect==4.8.0 pickleshare==0.7.5 -Pillow==9.0.1 -pkginfo==1.8.2 -platformdirs==2.5.1 +Pillow==9.4.0 +pip-api==0.0.30 +pip-requirements-parser==32.0.1 +pip_audit==2.4.14 +pkginfo==1.9.6 +platformdirs==3.0.0 pluggy==1.0.0 -poetry==1.1.13 -poetry-core==1.0.8 -poyo==0.5.0 -prompt-toolkit==3.0.28 -psycopg2-binary==2.9.3 +poetry==1.3.2 +poetry-core==1.4.0 +poetry-plugin-export==1.2.0 +pre-commit==2.21.0 +prompt-toolkit==3.0.38 +psycopg2-binary==2.9.5 ptyprocess==0.7.0 pure-eval==0.2.2 -py==1.11.0 -pyasn1==0.4.8 PyAutoGUI==0.9.53 +pycodestyle==2.10.0 pycparser==2.21 -pydantic==1.9.0 +pydantic==1.10.5 +pyflakes==3.0.1 PyGetWindow==0.0.9 -Pygments==2.11.2 -pyinstaller==4.9 -pyinstaller-hooks-contrib==2022.2 +Pygments==2.14.0 +pyinstaller==5.8.0 +pyinstaller-hooks-contrib==2023.0 pylev==1.4.0 PyMsgBox==1.0.9 -PyNaCl==1.5.0 -pyOpenSSL==22.0.0 -pyparsing==3.0.7 +pyparsing==3.0.9 pyperclip==1.8.2 -PyQt6==6.2.3 -PyQt6-Qt6==6.2.3 -PyQt6-sip==13.2.1 -PyRect==0.1.4 +PyQt6==6.4.2 +PyQt6-Qt6==6.4.2 +PyQt6-sip==13.4.1 +PyRect==0.2.0 +pyrsistent==0.19.3 PyScreeze==0.1.28 PySocks==1.7.1 -pytest==7.0.1 -pytest-cov==3.0.0 +pytest==7.2.1 +pytest-cov==4.0.0 python-dateutil==2.8.2 -python-decouple==3.6 -python-dotenv==0.19.2 -python-engineio==4.3.1 -python-slugify==6.1.1 -python-socketio==5.5.2 +python-decouple==3.8 +python-slugify==8.0.1 python3-xlib==0.15 pytweening==1.0.4 -pytz==2021.3 -qt6-applications==6.1.0.2.2 -qt6-tools==6.1.0.1.2 -requests==2.27.1 +pytz==2022.7.1 +PyYAML==6.0 +rapidfuzz==2.13.7 +redis==4.5.1 +requests==2.28.2 requests-toolbelt==0.9.1 -SecretStorage==3.3.1 -selenium==4.1.2 -shellingham==1.4.0 -simplejson==3.17.6 +resolvelib==0.9.0 +rfc3986==1.5.0 +rich==13.3.1 +ruamel.yaml==0.17.21 +safety==2.3.5 +SecretStorage==3.3.3 +selenium==4.8.2 +shellingham==1.5.0.post1 +simple-term-menu==1.6.1 six==1.16.0 -sniffio==1.2.0 +sniffio==1.3.0 sortedcontainers==2.4.0 -speaklater==1.3 -speaklater3==1.4 -SQLAlchemy==1.4.31 -sqlparse==0.4.2 -sshtunnel==0.4.0 -stack-data==0.2.0 -starlette==0.18.0 -style==1.1.6 +SQLAlchemy==1.4.46 +SQLAlchemy-Utils==0.38.3 +sqlparse==0.4.3 +stack-data==0.6.2 +starlette==0.22.0 +termcolor==2.2.0 text-unidecode==1.3 -tomli==2.0.1 -tomlkit==0.10.0 -traitlets==5.1.1 -trio==0.20.0 +toml==0.10.2 +tomlkit==0.11.6 +traitlets==5.9.0 +trio==0.22.0 trio-websocket==0.9.2 -typing_extensions==4.1.1 -ua-parser==0.10.0 -urllib3==1.26.8 -user-agents==2.2.0 -virtualenv==20.13.2 -wcwidth==0.2.5 +trove-classifiers==2023.1.20 +typing_extensions==4.5.0 +urllib3==1.26.14 +uvicorn==0.20.0 +validators==0.20.0 +vine==5.0.0 +virtualenv==20.20.0 +wcwidth==0.2.6 webencodings==0.5.1 -Werkzeug==2.0.3 -wsproto==1.1.0 -WTForms==3.0.1 -yarl==1.7.2 -zipp==3.7.0 +wget==3.2 +wsproto==1.2.0 +yarl==1.8.2 +zipp==3.15.0 \ No newline at end of file diff --git a/snake.py b/snake.py new file mode 100644 index 0000000..f14d5f2 --- /dev/null +++ b/snake.py @@ -0,0 +1,72 @@ +import math +from itertools import cycle + + +class Snake: + + def __init__(self): + self.x = 0 + self.y = 0 + self.move = self.move_right + + def move_right(self) -> None: + self.x += 1 + + def move_left(self) -> None: + self.x -= 1 + + def move_down(self) -> None: + self.y += 1 + + def move_up(self) -> None: + self.y -= 1 + + def move_direction(self) -> cycle: + return cycle([self.move_right, self.move_down, self.move_left, self.move_up]) + + def move_back(self) -> None: + match self.move: + case self.move_right: + self.x -= 1 + case self.move_left: + self.x += 1 + case self.move_down: + self.y -= 1 + case self.move_up: + self.y += 1 + + def get_current_element_or_none(self, board: dict[int, list[str]]) -> str | None: + try: + return board.get(self.y)[self.x] + except IndexError: + return None + except TypeError: + return None + + +def snake(n: int) -> None: + board: dict[int, list[str]] = {row: ['0' for column in range(n)] for row in range(n)} + + python = Snake() + + move_direction = python.move_direction() + next(move_direction) + python.move_back() # get on -1 position. And next move wil be on zero position + + for number in range(n ** 2): + python.move() + element = python.get_current_element_or_none(board) + + if not element or element != '0': + python.move_back() + python.move = next(move_direction) + python.move() + + board[python.y][python.x] = f'{number + 1}'.rjust(int(math.log10(n**2)) + 1, ' ') + + for line in board.values(): + print(*line) + + +if __name__ == '__main__': + snake(7) diff --git a/get-gitlab-projects/.gitignore b/sqlalchemy_study/.gitignore similarity index 55% rename from get-gitlab-projects/.gitignore rename to sqlalchemy_study/.gitignore index 5ccee39..4eb6c43 100644 --- a/get-gitlab-projects/.gitignore +++ b/sqlalchemy_study/.gitignore @@ -1,3 +1,7 @@ +### Python template + +.idea/ +.vscode/ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] @@ -20,6 +24,7 @@ parts/ sdist/ var/ wheels/ +share/python-wheels/ *.egg-info/ .installed.cfg *.egg @@ -38,14 +43,17 @@ pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ +.nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover +*.py,cover .hypothesis/ .pytest_cache/ +cover/ # Translations *.mo @@ -55,6 +63,8 @@ coverage.xml *.log local_settings.py db.sqlite3 +db.sqlite3-journal +*.db # Flask stuff: instance/ @@ -67,16 +77,34 @@ instance/ docs/_build/ # PyBuilder +.pybuilder/ target/ # Jupyter Notebook .ipynb_checkpoints -# pyenv -.python-version +# IPython +profile_default/ +ipython_config.py -# celery beat schedule file +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff celerybeat-schedule +celerybeat.pid # SageMath parsed files *.sage.py @@ -102,6 +130,18 @@ venv.bak/ # mypy .mypy_cache/ +.dmypy.json +dmypy.json -.idea/ -.vscode/ \ No newline at end of file +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# my staff +delete/ +delete.py diff --git a/sqlalchemy_study/README.md b/sqlalchemy_study/README.md new file mode 100644 index 0000000..6bd5908 --- /dev/null +++ b/sqlalchemy_study/README.md @@ -0,0 +1,103 @@ +# SQLALCHEMY STUDY + +--- + +*Note: MySQL will start on 3307 port* + +*Note: Postgres will start on 5433 port* + +--- + +## Create environment: + +```bash +cp ./src/config/.env.template ./src/config/.env +``` + +*Note: Change USE_DATABASE variable to 'mysql' for MySQL training or 'postgres' for Postgres use.* + +*Default is MySQL* + +## Run without app in docker: + +Requires python > 3.11 and poetry 1.3.1 + +- **install poetry dependencies:** +```bash +poetry install +poetry shell +``` + +- **run for mysql:** ```docker-compose -f docker-compose.mysql.yaml up``` + +- **run for postgres:** ```docker-compose -f docker-compose.postgres.yaml up``` + +- **run initial data:** ```python ./src/data/fill_data.py``` + +## Run all in docker: + +**run for mysql:** +```bash +docker-compose -f docker-compose.mysql.yaml -f docker-compose.docker.yaml up +``` +**run for postgres:** +```bash +docker-compose -f docker-compose.postgres.yaml -f docker-compose.docker.yaml up +``` +*Note: docker will start all migrations automatically. You don't need creation data step* + +## Help info: + +### Create alembic migrations: + +*Note: To generate migrations you should run:* +```bash +# For automatic change detection. +alembic revision --autogenerate -m "migration message" + +# For empty file generation. +alembic revision +``` + +*Note: If you want to migrate your database, you should run following commands:* +```bash +# To run all migrations untill the migration with revision_id. +alembic upgrade "" + +# To perform all pending migrations. +alembic upgrade "head" +``` + +### Reverting alembic migrations: + +*Note: If you want to revert migrations, you should run:* +```bash +# revert all migrations up to: revision_id. +alembic downgrade + +# Revert everything. +alembic downgrade base + +# Revert N revisions. +alembic downgrade -2 +``` + +### MySQL database access: + +Postgres: +```bash +docker exec -it sqlalchemy_study_db psql -d sqlalchemy_study -U balsh +``` + +- show help ```\?``` +- show all tables: ```\dt``` +- describe table ```\d {table name}``` + + + +## Clean database +```bash +docker-compose -f docker-compose.mysql.yaml down -v +``` + +## Known issues: diff --git a/sqlalchemy_study/docker-compose.docker.yaml b/sqlalchemy_study/docker-compose.docker.yaml new file mode 100644 index 0000000..c6f9d4d --- /dev/null +++ b/sqlalchemy_study/docker-compose.docker.yaml @@ -0,0 +1,39 @@ +version: '3.9' + +networks: + sqlalchemy_study_network: + name: "sqlalchemy_study_network" + ipam: + config: + - subnet: 200.20.0.0/24 + + +services: + db: + networks: + sqlalchemy_study_network: + ipv4_address: 200.20.0.12 + + app: + container_name: "sqlalchemy_study_app" + image: "sqlalchemy_study:latest" + build: + context: . + dockerfile: ./docker/Dockerfile + args: + USER: root + restart: unless-stopped + networks: + sqlalchemy_study_network: + ipv4_address: 200.20.0.10 + env_file: ./src/config/.env + environment: + DB_HOST: db + depends_on: + - db + command: > + bash -c "/app/scripts/docker-entrypoint.sh + && /app/scripts/alembic-init-migrate.sh && python data/fill_data.py + && sleep infinity" + volumes: + - ./src:/app/src/ \ No newline at end of file diff --git a/sqlalchemy_study/docker-compose.mysql.yaml b/sqlalchemy_study/docker-compose.mysql.yaml new file mode 100644 index 0000000..b44d975 --- /dev/null +++ b/sqlalchemy_study/docker-compose.mysql.yaml @@ -0,0 +1,29 @@ +version: '3.9' + + +volumes: + sqlalchemy_study_db_data: + name: "sqlalchemy_study_db_data" + +services: + + db: + image: mysql:8.0.31 + platform: linux/amd64 + container_name: "sqlalchemy_study_db" + hostname: 'db_host' + volumes: + - sqlalchemy_study_db_data:/var/lib/mysql + - /etc/localtime:/etc/localtime:ro + env_file: ./src/config/.env + environment: + MYSQL_TCP_PORT: 3307 + restart: unless-stopped + expose: + - '3307' + ports: + - '3307:3307' + security_opt: + - seccomp:unconfined + cap_add: + - SYS_NICE # CAP_SYS_NICE diff --git a/sqlalchemy_study/docker-compose.postgres.yaml b/sqlalchemy_study/docker-compose.postgres.yaml new file mode 100644 index 0000000..285b267 --- /dev/null +++ b/sqlalchemy_study/docker-compose.postgres.yaml @@ -0,0 +1,23 @@ +version: '3.9' + + +volumes: + sqlalchemy_study_db_data: + name: "sqlalchemy_study_db_data" + +services: + + db: + image: postgres:14.6 + container_name: "sqlalchemy_study_db" + hostname: 'db_host' + restart: unless-stopped + volumes: + - sqlalchemy_study_db_data:/var/lib/postgresql/data + - /etc/localtime:/etc/localtime:ro + env_file: ./src/config/.env + expose: + - '5433' + ports: + - '5433:5433' + command: -p 5433 diff --git a/sqlalchemy_study/docker/Dockerfile b/sqlalchemy_study/docker/Dockerfile new file mode 100644 index 0000000..205dc5b --- /dev/null +++ b/sqlalchemy_study/docker/Dockerfile @@ -0,0 +1,60 @@ + +FROM --platform=linux/amd64 python:3.11.1 + +ARG USER + +ENV SOURCE_DIR=/app/src/ + +ENV USER=${USER} \ + PYTHONFAULTHANDLER=1 \ + PYTHONUNBUFFERED=1 \ + PYTHONHASHSEED=random \ + PYTHONDONTWRITEBYTECODE=1 \ + PYTHONPATH="${PYTHONPATH}:${SOURCE_DIR}" \ + # pip: + PIP_NO_CACHE_DIR=off \ + PIP_DISABLE_PIP_VERSION_CHECK=on \ + PIP_DEFAULT_TIMEOUT=100 \ + POETRY_VIRTUALENVS_CREATE=false \ + POETRY_CACHE_DIR='/var/cache/pypoetry' \ + PATH="$PATH:/root/.poetry/bin" + +RUN printf "================\n\nStart build app. USER is: "${USER}"\n\n===============\n" \ + && apt-get update \ + && apt-get install --no-install-recommends -y \ + procps \ + bash \ + build-essential \ + curl \ + iputils-ping \ + gettext \ + git \ + libpq-dev \ + nano \ + sshpass \ + && pip install --upgrade pip \ + # Installing `poetry` package manager: + && pip install poetry \ + # Cleaning cache: + && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \ + && apt-get clean -y && rm -rf /var/lib/apt/lists/* + +WORKDIR ${SOURCE_DIR} + +RUN if [ "$USER" != "root" ]; then \ + groupadd -r "$USER" && useradd -d /home/"$USER" -r -g "$USER" "$USER" \ + && chown "$USER":"$USER" -R /home/"$USER"; \ + fi + +COPY --chown="$USER":"$USER" ./poetry.lock ./pyproject.toml ${SOURCE_DIR} + +# Installing requirements +RUN poetry install && rm -rf "$POETRY_CACHE_DIR" + +COPY ./docker/scripts/ /app/scripts/ +RUN chmod +x /app/scripts/docker-entrypoint.sh /app/scripts/alembic-init-migrate.sh + +USER "$USER" + +# Copying actuall application +COPY --chown="$USER":"$USER" . ${SOURCE_DIR} diff --git a/sqlalchemy_study/docker/scripts/alembic-init-migrate.sh b/sqlalchemy_study/docker/scripts/alembic-init-migrate.sh new file mode 100644 index 0000000..e91837f --- /dev/null +++ b/sqlalchemy_study/docker/scripts/alembic-init-migrate.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +alembic_init_migrations(){ + echo "Chosen database IS $USE_DATABASE" + if [ "$USE_DATABASE" = "mysql" ]; + then + echo "Start migrations for MySQL" + alembic upgrade mysql_init_migrations; + elif [ "$USE_DATABASE" = "postgres" ]; + then + echo "Start migrations for Postgres" + alembic upgrade postgres_init_migrations; + fi +} + +alembic_init_migrations \ No newline at end of file diff --git a/sqlalchemy_study/docker/scripts/docker-entrypoint.sh b/sqlalchemy_study/docker/scripts/docker-entrypoint.sh new file mode 100755 index 0000000..7ee4bed --- /dev/null +++ b/sqlalchemy_study/docker/scripts/docker-entrypoint.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +TIMEOUT=${TIMEOUT:-60} + +DATABASE_HOST=${DB_HOST:-db_host} + +POSTGRES_DATABASE_PORT=${POSTGRES_DB_PORT:-5432} +POSTGRES_DATABASE="$DATABASE_HOST:$POSTGRES_DATABASE_PORT" + +MYSQL_DATABASE_PORT=${MYSQL_DB_PORT:-3306} +MYSQL_DATABASE="$DATABASE_HOST:$MYSQL_DATABASE_PORT" + +wait_for_databases(){ + echo "Chosen database IS $USE_DATABASE" + if [ "$USE_DATABASE" = "mysql" ]; + then + echo "Waiting for DB on: $MYSQL_DATABASE" + /app/scripts/wait-for-it.sh -t $TIMEOUT -s $MYSQL_DATABASE -- echo 'MySQL database connected'; + elif [ "$USE_DATABASE" = "postgres" ]; + then + echo "Waiting for DB on: $POSTGRES_DATABASE" + /app/scripts/wait-for-it.sh -t $TIMEOUT -s $POSTGRES_DATABASE -- echo 'Postgres database connected'; + fi +} + +wait_for_databases \ No newline at end of file diff --git a/sqlalchemy_study/docker/scripts/wait-for-it.sh b/sqlalchemy_study/docker/scripts/wait-for-it.sh new file mode 100755 index 0000000..d990e0d --- /dev/null +++ b/sqlalchemy_study/docker/scripts/wait-for-it.sh @@ -0,0 +1,182 @@ +#!/usr/bin/env bash +# Use this script to test if a given TCP host/port are available + +WAITFORIT_cmdname=${0##*/} + +echoerr() { if [[ $WAITFORIT_QUIET -ne 1 ]]; then echo "$@" 1>&2; fi } + +usage() +{ + cat << USAGE >&2 +Usage: + $WAITFORIT_cmdname host:port [-s] [-t timeout] [-- command args] + -h HOST | --host=HOST Host or IP under test + -p PORT | --port=PORT TCP port under test + Alternatively, you specify the host and port as host:port + -s | --strict Only execute subcommand if the test succeeds + -q | --quiet Don't output any status messages + -t TIMEOUT | --timeout=TIMEOUT + Timeout in seconds, zero for no timeout + -- COMMAND ARGS Execute command with args after the test finishes +USAGE + exit 1 +} + +wait_for() +{ + if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then + echoerr "$WAITFORIT_cmdname: waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" + else + echoerr "$WAITFORIT_cmdname: waiting for $WAITFORIT_HOST:$WAITFORIT_PORT without a timeout" + fi + WAITFORIT_start_ts=$(date +%s) + while : + do + if [[ $WAITFORIT_ISBUSY -eq 1 ]]; then + nc -z $WAITFORIT_HOST $WAITFORIT_PORT + WAITFORIT_result=$? + else + (echo -n > /dev/tcp/$WAITFORIT_HOST/$WAITFORIT_PORT) >/dev/null 2>&1 + WAITFORIT_result=$? + fi + if [[ $WAITFORIT_result -eq 0 ]]; then + WAITFORIT_end_ts=$(date +%s) + echoerr "$WAITFORIT_cmdname: $WAITFORIT_HOST:$WAITFORIT_PORT is available after $((WAITFORIT_end_ts - WAITFORIT_start_ts)) seconds" + break + fi + sleep 1 + done + return $WAITFORIT_result +} + +wait_for_wrapper() +{ + # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692 + if [[ $WAITFORIT_QUIET -eq 1 ]]; then + timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --quiet --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & + else + timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & + fi + WAITFORIT_PID=$! + trap "kill -INT -$WAITFORIT_PID" INT + wait $WAITFORIT_PID + WAITFORIT_RESULT=$? + if [[ $WAITFORIT_RESULT -ne 0 ]]; then + echoerr "$WAITFORIT_cmdname: timeout occurred after waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" + fi + return $WAITFORIT_RESULT +} + +# process arguments +while [[ $# -gt 0 ]] +do + case "$1" in + *:* ) + WAITFORIT_hostport=(${1//:/ }) + WAITFORIT_HOST=${WAITFORIT_hostport[0]} + WAITFORIT_PORT=${WAITFORIT_hostport[1]} + shift 1 + ;; + --child) + WAITFORIT_CHILD=1 + shift 1 + ;; + -q | --quiet) + WAITFORIT_QUIET=1 + shift 1 + ;; + -s | --strict) + WAITFORIT_STRICT=1 + shift 1 + ;; + -h) + WAITFORIT_HOST="$2" + if [[ $WAITFORIT_HOST == "" ]]; then break; fi + shift 2 + ;; + --host=*) + WAITFORIT_HOST="${1#*=}" + shift 1 + ;; + -p) + WAITFORIT_PORT="$2" + if [[ $WAITFORIT_PORT == "" ]]; then break; fi + shift 2 + ;; + --port=*) + WAITFORIT_PORT="${1#*=}" + shift 1 + ;; + -t) + WAITFORIT_TIMEOUT="$2" + if [[ $WAITFORIT_TIMEOUT == "" ]]; then break; fi + shift 2 + ;; + --timeout=*) + WAITFORIT_TIMEOUT="${1#*=}" + shift 1 + ;; + --) + shift + WAITFORIT_CLI=("$@") + break + ;; + --help) + usage + ;; + *) + echoerr "Unknown argument: $1" + usage + ;; + esac +done + +if [[ "$WAITFORIT_HOST" == "" || "$WAITFORIT_PORT" == "" ]]; then + echoerr "Error: you need to provide a host and port to test." + usage +fi + +WAITFORIT_TIMEOUT=${WAITFORIT_TIMEOUT:-15} +WAITFORIT_STRICT=${WAITFORIT_STRICT:-0} +WAITFORIT_CHILD=${WAITFORIT_CHILD:-0} +WAITFORIT_QUIET=${WAITFORIT_QUIET:-0} + +# Check to see if timeout is from busybox? +WAITFORIT_TIMEOUT_PATH=$(type -p timeout) +WAITFORIT_TIMEOUT_PATH=$(realpath $WAITFORIT_TIMEOUT_PATH 2>/dev/null || readlink -f $WAITFORIT_TIMEOUT_PATH) + +WAITFORIT_BUSYTIMEFLAG="" +if [[ $WAITFORIT_TIMEOUT_PATH =~ "busybox" ]]; then + WAITFORIT_ISBUSY=1 + # Check if busybox timeout uses -t flag + # (recent Alpine versions don't support -t anymore) + if timeout &>/dev/stdout | grep -q -e '-t '; then + WAITFORIT_BUSYTIMEFLAG="-t" + fi +else + WAITFORIT_ISBUSY=0 +fi + +if [[ $WAITFORIT_CHILD -gt 0 ]]; then + wait_for + WAITFORIT_RESULT=$? + exit $WAITFORIT_RESULT +else + if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then + wait_for_wrapper + WAITFORIT_RESULT=$? + else + wait_for + WAITFORIT_RESULT=$? + fi +fi + +if [[ $WAITFORIT_CLI != "" ]]; then + if [[ $WAITFORIT_RESULT -ne 0 && $WAITFORIT_STRICT -eq 1 ]]; then + echoerr "$WAITFORIT_cmdname: strict mode, refusing to execute subprocess" + exit $WAITFORIT_RESULT + fi + exec "${WAITFORIT_CLI[@]}" +else + exit $WAITFORIT_RESULT +fi diff --git a/sqlalchemy_study/poetry.lock b/sqlalchemy_study/poetry.lock new file mode 100644 index 0000000..6afdfc3 --- /dev/null +++ b/sqlalchemy_study/poetry.lock @@ -0,0 +1,1104 @@ +# This file is automatically @generated by Poetry and should not be changed by hand. + +[[package]] +name = "alembic" +version = "1.9.2" +description = "A database migration tool for SQLAlchemy." +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "alembic-1.9.2-py3-none-any.whl", hash = "sha256:e8a6ff9f3b1887e1fed68bfb8fb9a000d8f61c21bdcc85b67bb9f87fcbc4fce3"}, + {file = "alembic-1.9.2.tar.gz", hash = "sha256:6880dec4f28dd7bd999d2ed13fbe7c9d4337700a44d11a524c0ce0c59aaf0dbd"}, +] + +[package.dependencies] +Mako = "*" +SQLAlchemy = ">=1.3.0" + +[package.extras] +tz = ["python-dateutil"] + +[[package]] +name = "appnope" +version = "0.1.3" +description = "Disable App Nap on macOS >= 10.9" +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "appnope-0.1.3-py2.py3-none-any.whl", hash = "sha256:265a455292d0bd8a72453494fa24df5a11eb18373a60c7c0430889f22548605e"}, + {file = "appnope-0.1.3.tar.gz", hash = "sha256:02bd91c4de869fbb1e1c50aafc4098827a7a54ab2f39d9dcba6c9547ed920e24"}, +] + +[[package]] +name = "asttokens" +version = "2.2.1" +description = "Annotate AST trees with source code positions" +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "asttokens-2.2.1-py2.py3-none-any.whl", hash = "sha256:6b0ac9e93fb0335014d382b8fa9b3afa7df546984258005da0b9e7095b3deb1c"}, + {file = "asttokens-2.2.1.tar.gz", hash = "sha256:4622110b2a6f30b77e1473affaa97e711bc2f07d3f10848420ff1898edbe94f3"}, +] + +[package.dependencies] +six = "*" + +[package.extras] +test = ["astroid", "pytest"] + +[[package]] +name = "asyncmy" +version = "0.2.5" +description = "A fast asyncio MySQL driver" +category = "main" +optional = false +python-versions = ">=3.7,<4.0" +files = [ + {file = "asyncmy-0.2.5-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:f176b55c82d3bdb10f0ecb3a518a54777f3d247e00f06add138f63df4edc0e3d"}, + {file = "asyncmy-0.2.5-cp310-cp310-manylinux_2_31_x86_64.whl", hash = "sha256:f404d351d4f9fc741cdb8b49da8278e63a8551be6ccd03b514c2c0828500633d"}, + {file = "asyncmy-0.2.5-cp310-cp310-win_amd64.whl", hash = "sha256:9824184d180753d23101b1d9fdc7955783b08fbc6f3cc88d6acce7ff019c0eaf"}, + {file = "asyncmy-0.2.5-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:02add8063bd00762a469dcb9915563f66520ffda598851a957443b1012182f43"}, + {file = "asyncmy-0.2.5-cp37-cp37m-manylinux_2_31_x86_64.whl", hash = "sha256:a341d8a22d9a1f3236a81f17836002291567aa7d878ab75b37ed9bf4ce401290"}, + {file = "asyncmy-0.2.5-cp37-cp37m-win_amd64.whl", hash = "sha256:2dc25f0bb723a71445d5c5b46ab2ae71c9dd3123fea9eae1bee4c36af5eda2bc"}, + {file = "asyncmy-0.2.5-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:e184e701fa153dd456f6b351136c0a7a7524b8f5959f7f4b47848efe434a9c7d"}, + {file = "asyncmy-0.2.5-cp38-cp38-manylinux_2_31_x86_64.whl", hash = "sha256:2275cc0036fa39f888c0770173ffb8ed16b89eecf2e380499275bfa6bf4ee088"}, + {file = "asyncmy-0.2.5-cp38-cp38-win_amd64.whl", hash = "sha256:527ccc24a9bd76d565f1024f4a50ee6fd92cac518e6d884281ad61ffc424e7cd"}, + {file = "asyncmy-0.2.5-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:6606e296dc161d71629f1163c3c82dda97431e25ee7240b6b7f139c2c35cff94"}, + {file = "asyncmy-0.2.5-cp39-cp39-manylinux_2_31_x86_64.whl", hash = "sha256:61ed1b9e69dafe476f02c20962b8ee4ec5ba130b760bdf504015fcb80bbe6787"}, + {file = "asyncmy-0.2.5-cp39-cp39-win_amd64.whl", hash = "sha256:f5dcc1a29d608cc5270f37328253032c554321b85bcf5653f1affa01f185c29c"}, + {file = "asyncmy-0.2.5.tar.gz", hash = "sha256:cf3ef3d1ada385d231f23fffcbf1fe040bae8575b187746633fbef6e7cff2844"}, +] + +[[package]] +name = "asyncpg" +version = "0.27.0" +description = "An asyncio PostgreSQL driver" +category = "main" +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "asyncpg-0.27.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fca608d199ffed4903dce1bcd97ad0fe8260f405c1c225bdf0002709132171c2"}, + {file = "asyncpg-0.27.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:20b596d8d074f6f695c13ffb8646d0b6bb1ab570ba7b0cfd349b921ff03cfc1e"}, + {file = "asyncpg-0.27.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a6206210c869ebd3f4eb9e89bea132aefb56ff3d1b7dd7e26b102b17e27bbb1"}, + {file = "asyncpg-0.27.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7a94c03386bb95456b12c66026b3a87d1b965f0f1e5733c36e7229f8f137747"}, + {file = "asyncpg-0.27.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bfc3980b4ba6f97138b04f0d32e8af21d6c9fa1f8e6e140c07d15690a0a99279"}, + {file = "asyncpg-0.27.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:9654085f2b22f66952124de13a8071b54453ff972c25c59b5ce1173a4283ffd9"}, + {file = "asyncpg-0.27.0-cp310-cp310-win32.whl", hash = "sha256:879c29a75969eb2722f94443752f4720d560d1e748474de54ae8dd230bc4956b"}, + {file = "asyncpg-0.27.0-cp310-cp310-win_amd64.whl", hash = "sha256:ab0f21c4818d46a60ca789ebc92327d6d874d3b7ccff3963f7af0a21dc6cff52"}, + {file = "asyncpg-0.27.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:18f77e8e71e826ba2d0c3ba6764930776719ae2b225ca07e014590545928b576"}, + {file = "asyncpg-0.27.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c2232d4625c558f2aa001942cac1d7952aa9f0dbfc212f63bc754277769e1ef2"}, + {file = "asyncpg-0.27.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9a3a4ff43702d39e3c97a8786314123d314e0f0e4dabc8367db5b665c93914de"}, + {file = "asyncpg-0.27.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccddb9419ab4e1c48742457d0c0362dbdaeb9b28e6875115abfe319b29ee225d"}, + {file = "asyncpg-0.27.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:768e0e7c2898d40b16d4ef7a0b44e8150db3dd8995b4652aa1fe2902e92c7df8"}, + {file = "asyncpg-0.27.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:609054a1f47292a905582a1cfcca51a6f3f30ab9d822448693e66fdddde27920"}, + {file = "asyncpg-0.27.0-cp311-cp311-win32.whl", hash = "sha256:8113e17cfe236dc2277ec844ba9b3d5312f61bd2fdae6d3ed1c1cdd75f6cf2d8"}, + {file = "asyncpg-0.27.0-cp311-cp311-win_amd64.whl", hash = "sha256:bb71211414dd1eeb8d31ec529fe77cff04bf53efc783a5f6f0a32d84923f45cf"}, + {file = "asyncpg-0.27.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4750f5cf49ed48a6e49c6e5aed390eee367694636c2dcfaf4a273ca832c5c43c"}, + {file = "asyncpg-0.27.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:eca01eb112a39d31cc4abb93a5aef2a81514c23f70956729f42fb83b11b3483f"}, + {file = "asyncpg-0.27.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:5710cb0937f696ce303f5eed6d272e3f057339bb4139378ccecafa9ee923a71c"}, + {file = "asyncpg-0.27.0-cp37-cp37m-win_amd64.whl", hash = "sha256:71cca80a056ebe19ec74b7117b09e650990c3ca535ac1c35234a96f65604192f"}, + {file = "asyncpg-0.27.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4bb366ae34af5b5cabc3ac6a5347dfb6013af38c68af8452f27968d49085ecc0"}, + {file = "asyncpg-0.27.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:16ba8ec2e85d586b4a12bcd03e8d29e3d99e832764d6a1d0b8c27dbbe4a2569d"}, + {file = "asyncpg-0.27.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d20dea7b83651d93b1eb2f353511fe7fd554752844523f17ad30115d8b9c8cd6"}, + {file = "asyncpg-0.27.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e56ac8a8237ad4adec97c0cd4728596885f908053ab725e22900b5902e7f8e69"}, + {file = "asyncpg-0.27.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bf21ebf023ec67335258e0f3d3ad7b91bb9507985ba2b2206346de488267cad0"}, + {file = "asyncpg-0.27.0-cp38-cp38-win32.whl", hash = "sha256:69aa1b443a182b13a17ff926ed6627af2d98f62f2fe5890583270cc4073f63bf"}, + {file = "asyncpg-0.27.0-cp38-cp38-win_amd64.whl", hash = "sha256:62932f29cf2433988fcd799770ec64b374a3691e7902ecf85da14d5e0854d1ea"}, + {file = "asyncpg-0.27.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fddcacf695581a8d856654bc4c8cfb73d5c9df26d5f55201722d3e6a699e9629"}, + {file = "asyncpg-0.27.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7d8585707ecc6661d07367d444bbaa846b4e095d84451340da8df55a3757e152"}, + {file = "asyncpg-0.27.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:975a320baf7020339a67315284a4d3bf7460e664e484672bd3e71dbd881bc692"}, + {file = "asyncpg-0.27.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2232ebae9796d4600a7819fc383da78ab51b32a092795f4555575fc934c1c89d"}, + {file = "asyncpg-0.27.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:88b62164738239f62f4af92567b846a8ef7cf8abf53eddd83650603de4d52163"}, + {file = "asyncpg-0.27.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:eb4b2fdf88af4fb1cc569781a8f933d2a73ee82cd720e0cb4edabbaecf2a905b"}, + {file = "asyncpg-0.27.0-cp39-cp39-win32.whl", hash = "sha256:8934577e1ed13f7d2d9cea3cc016cc6f95c19faedea2c2b56a6f94f257cea672"}, + {file = "asyncpg-0.27.0-cp39-cp39-win_amd64.whl", hash = "sha256:1b6499de06fe035cf2fa932ec5617ed3f37d4ebbf663b655922e105a484a6af9"}, + {file = "asyncpg-0.27.0.tar.gz", hash = "sha256:720986d9a4705dd8a40fdf172036f5ae787225036a7eb46e704c45aa8f62c054"}, +] + +[package.extras] +dev = ["Cython (>=0.29.24,<0.30.0)", "Sphinx (>=4.1.2,<4.2.0)", "flake8 (>=5.0.4,<5.1.0)", "pytest (>=6.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)", "uvloop (>=0.15.3)"] +docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"] +test = ["flake8 (>=5.0.4,<5.1.0)", "uvloop (>=0.15.3)"] + +[[package]] +name = "backcall" +version = "0.2.0" +description = "Specifications for callback functions passed in to an API" +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"}, + {file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"}, +] + +[[package]] +name = "cffi" +version = "1.15.1" +description = "Foreign Function Interface for Python calling C code." +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"}, + {file = "cffi-1.15.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2"}, + {file = "cffi-1.15.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914"}, + {file = "cffi-1.15.1-cp27-cp27m-win32.whl", hash = "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3"}, + {file = "cffi-1.15.1-cp27-cp27m-win_amd64.whl", hash = "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e"}, + {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162"}, + {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b"}, + {file = "cffi-1.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21"}, + {file = "cffi-1.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4"}, + {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01"}, + {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e"}, + {file = "cffi-1.15.1-cp310-cp310-win32.whl", hash = "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2"}, + {file = "cffi-1.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d"}, + {file = "cffi-1.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac"}, + {file = "cffi-1.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c"}, + {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef"}, + {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8"}, + {file = "cffi-1.15.1-cp311-cp311-win32.whl", hash = "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d"}, + {file = "cffi-1.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104"}, + {file = "cffi-1.15.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e"}, + {file = "cffi-1.15.1-cp36-cp36m-win32.whl", hash = "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf"}, + {file = "cffi-1.15.1-cp36-cp36m-win_amd64.whl", hash = "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497"}, + {file = "cffi-1.15.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426"}, + {file = "cffi-1.15.1-cp37-cp37m-win32.whl", hash = "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9"}, + {file = "cffi-1.15.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045"}, + {file = "cffi-1.15.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192"}, + {file = "cffi-1.15.1-cp38-cp38-win32.whl", hash = "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314"}, + {file = "cffi-1.15.1-cp38-cp38-win_amd64.whl", hash = "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5"}, + {file = "cffi-1.15.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585"}, + {file = "cffi-1.15.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27"}, + {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76"}, + {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3"}, + {file = "cffi-1.15.1-cp39-cp39-win32.whl", hash = "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee"}, + {file = "cffi-1.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c"}, + {file = "cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9"}, +] + +[package.dependencies] +pycparser = "*" + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "cryptography" +version = "37.0.4" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "cryptography-37.0.4-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:549153378611c0cca1042f20fd9c5030d37a72f634c9326e225c9f666d472884"}, + {file = "cryptography-37.0.4-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:a958c52505c8adf0d3822703078580d2c0456dd1d27fabfb6f76fe63d2971cd6"}, + {file = "cryptography-37.0.4-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f721d1885ecae9078c3f6bbe8a88bc0786b6e749bf32ccec1ef2b18929a05046"}, + {file = "cryptography-37.0.4-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:3d41b965b3380f10e4611dbae366f6dc3cefc7c9ac4e8842a806b9672ae9add5"}, + {file = "cryptography-37.0.4-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80f49023dd13ba35f7c34072fa17f604d2f19bf0989f292cedf7ab5770b87a0b"}, + {file = "cryptography-37.0.4-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2dcb0b3b63afb6df7fd94ec6fbddac81b5492513f7b0436210d390c14d46ee8"}, + {file = "cryptography-37.0.4-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:b7f8dd0d4c1f21759695c05a5ec8536c12f31611541f8904083f3dc582604280"}, + {file = "cryptography-37.0.4-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:30788e070800fec9bbcf9faa71ea6d8068f5136f60029759fd8c3efec3c9dcb3"}, + {file = "cryptography-37.0.4-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:190f82f3e87033821828f60787cfa42bff98404483577b591429ed99bed39d59"}, + {file = "cryptography-37.0.4-cp36-abi3-win32.whl", hash = "sha256:b62439d7cd1222f3da897e9a9fe53bbf5c104fff4d60893ad1355d4c14a24157"}, + {file = "cryptography-37.0.4-cp36-abi3-win_amd64.whl", hash = "sha256:f7a6de3e98771e183645181b3627e2563dcde3ce94a9e42a3f427d2255190327"}, + {file = "cryptography-37.0.4-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bc95ed67b6741b2607298f9ea4932ff157e570ef456ef7ff0ef4884a134cc4b"}, + {file = "cryptography-37.0.4-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:f8c0a6e9e1dd3eb0414ba320f85da6b0dcbd543126e30fcc546e7372a7fbf3b9"}, + {file = "cryptography-37.0.4-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:e007f052ed10cc316df59bc90fbb7ff7950d7e2919c9757fd42a2b8ecf8a5f67"}, + {file = "cryptography-37.0.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bc997818309f56c0038a33b8da5c0bfbb3f1f067f315f9abd6fc07ad359398d"}, + {file = "cryptography-37.0.4-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:d204833f3c8a33bbe11eda63a54b1aad7aa7456ed769a982f21ec599ba5fa282"}, + {file = "cryptography-37.0.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:75976c217f10d48a8b5a8de3d70c454c249e4b91851f6838a4e48b8f41eb71aa"}, + {file = "cryptography-37.0.4-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:7099a8d55cd49b737ffc99c17de504f2257e3787e02abe6d1a6d136574873441"}, + {file = "cryptography-37.0.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2be53f9f5505673eeda5f2736bea736c40f051a739bfae2f92d18aed1eb54596"}, + {file = "cryptography-37.0.4-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:91ce48d35f4e3d3f1d83e29ef4a9267246e6a3be51864a5b7d2247d5086fa99a"}, + {file = "cryptography-37.0.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:4c590ec31550a724ef893c50f9a97a0c14e9c851c85621c5650d699a7b88f7ab"}, + {file = "cryptography-37.0.4.tar.gz", hash = "sha256:63f9c17c0e2474ccbebc9302ce2f07b55b3b3fcb211ded18a42d5764f5c10a82"}, +] + +[package.dependencies] +cffi = ">=1.12" + +[package.extras] +docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"] +docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"] +pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] +sdist = ["setuptools-rust (>=0.11.4)"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["hypothesis (>=1.11.4,!=3.79.2)", "iso8601", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-subtests", "pytest-xdist", "pytz"] + +[[package]] +name = "decorator" +version = "5.1.1" +description = "Decorators for Humans" +category = "dev" +optional = false +python-versions = ">=3.5" +files = [ + {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"}, + {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, +] + +[[package]] +name = "dnspython" +version = "2.3.0" +description = "DNS toolkit" +category = "main" +optional = false +python-versions = ">=3.7,<4.0" +files = [ + {file = "dnspython-2.3.0-py3-none-any.whl", hash = "sha256:89141536394f909066cabd112e3e1a37e4e654db00a25308b0f130bc3152eb46"}, + {file = "dnspython-2.3.0.tar.gz", hash = "sha256:224e32b03eb46be70e12ef6d64e0be123a64e621ab4c0822ff6d450d52a540b9"}, +] + +[package.extras] +curio = ["curio (>=1.2,<2.0)", "sniffio (>=1.1,<2.0)"] +dnssec = ["cryptography (>=2.6,<40.0)"] +doh = ["h2 (>=4.1.0)", "httpx (>=0.21.1)", "requests (>=2.23.0,<3.0.0)", "requests-toolbelt (>=0.9.1,<0.11.0)"] +doq = ["aioquic (>=0.9.20)"] +idna = ["idna (>=2.1,<4.0)"] +trio = ["trio (>=0.14,<0.23)"] +wmi = ["wmi (>=1.5.1,<2.0.0)"] + +[[package]] +name = "email-validator" +version = "1.3.0" +description = "A robust email address syntax and deliverability validation library." +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +files = [ + {file = "email_validator-1.3.0-py2.py3-none-any.whl", hash = "sha256:816073f2a7cffef786b29928f58ec16cdac42710a53bb18aa94317e3e145ec5c"}, + {file = "email_validator-1.3.0.tar.gz", hash = "sha256:553a66f8be2ec2dea641ae1d3f29017ab89e9d603d4a25cdaac39eefa283d769"}, +] + +[package.dependencies] +dnspython = ">=1.15.0" +idna = ">=2.0.0" + +[[package]] +name = "executing" +version = "1.2.0" +description = "Get the currently executing AST node of a frame, and other information" +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "executing-1.2.0-py2.py3-none-any.whl", hash = "sha256:0314a69e37426e3608aada02473b4161d4caf5a4b244d1d0c48072b8fee7bacc"}, + {file = "executing-1.2.0.tar.gz", hash = "sha256:19da64c18d2d851112f09c287f8d3dbbdf725ab0e569077efb6cdcbd3497c107"}, +] + +[package.extras] +tests = ["asttokens", "littleutils", "pytest", "rich"] + +[[package]] +name = "factory-boy" +version = "3.2.1" +description = "A versatile test fixtures replacement based on thoughtbot's factory_bot for Ruby." +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "factory_boy-3.2.1-py2.py3-none-any.whl", hash = "sha256:eb02a7dd1b577ef606b75a253b9818e6f9eaf996d94449c9d5ebb124f90dc795"}, + {file = "factory_boy-3.2.1.tar.gz", hash = "sha256:a98d277b0c047c75eb6e4ab8508a7f81fb03d2cb21986f627913546ef7a2a55e"}, +] + +[package.dependencies] +Faker = ">=0.7.0" + +[package.extras] +dev = ["Django", "Pillow", "SQLAlchemy", "coverage", "flake8", "isort", "mongoengine", "tox", "wheel (>=0.32.0)", "zest.releaser[recommended]"] +doc = ["Sphinx", "sphinx-rtd-theme", "sphinxcontrib-spelling"] + +[[package]] +name = "faker" +version = "15.3.4" +description = "Faker is a Python package that generates fake data for you." +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "Faker-15.3.4-py3-none-any.whl", hash = "sha256:c2a2ff9dd8dfd991109b517ab98d5cb465e857acb45f6b643a0e284a9eb2cc76"}, + {file = "Faker-15.3.4.tar.gz", hash = "sha256:2d5443724f640ce07658ca8ca8bbd40d26b58914e63eec6549727869aa67e2cc"}, +] + +[package.dependencies] +python-dateutil = ">=2.4" + +[[package]] +name = "greenlet" +version = "2.0.1" +description = "Lightweight in-process concurrent programming" +category = "main" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" +files = [ + {file = "greenlet-2.0.1-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:9ed358312e63bf683b9ef22c8e442ef6c5c02973f0c2a939ec1d7b50c974015c"}, + {file = "greenlet-2.0.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:4f09b0010e55bec3239278f642a8a506b91034f03a4fb28289a7d448a67f1515"}, + {file = "greenlet-2.0.1-cp27-cp27m-win32.whl", hash = "sha256:1407fe45246632d0ffb7a3f4a520ba4e6051fc2cbd61ba1f806900c27f47706a"}, + {file = "greenlet-2.0.1-cp27-cp27m-win_amd64.whl", hash = "sha256:3001d00eba6bbf084ae60ec7f4bb8ed375748f53aeaefaf2a37d9f0370558524"}, + {file = "greenlet-2.0.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:d566b82e92ff2e09dd6342df7e0eb4ff6275a3f08db284888dcd98134dbd4243"}, + {file = "greenlet-2.0.1-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:0722c9be0797f544a3ed212569ca3fe3d9d1a1b13942d10dd6f0e8601e484d26"}, + {file = "greenlet-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d37990425b4687ade27810e3b1a1c37825d242ebc275066cfee8cb6b8829ccd"}, + {file = "greenlet-2.0.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be35822f35f99dcc48152c9839d0171a06186f2d71ef76dc57fa556cc9bf6b45"}, + {file = "greenlet-2.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c140e7eb5ce47249668056edf3b7e9900c6a2e22fb0eaf0513f18a1b2c14e1da"}, + {file = "greenlet-2.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d21681f09e297a5adaa73060737e3aa1279a13ecdcfcc6ef66c292cb25125b2d"}, + {file = "greenlet-2.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fb412b7db83fe56847df9c47b6fe3f13911b06339c2aa02dcc09dce8bbf582cd"}, + {file = "greenlet-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:c6a08799e9e88052221adca55741bf106ec7ea0710bca635c208b751f0d5b617"}, + {file = "greenlet-2.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9e112e03d37987d7b90c1e98ba5e1b59e1645226d78d73282f45b326f7bddcb9"}, + {file = "greenlet-2.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56961cfca7da2fdd178f95ca407fa330c64f33289e1804b592a77d5593d9bd94"}, + {file = "greenlet-2.0.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:13ba6e8e326e2116c954074c994da14954982ba2795aebb881c07ac5d093a58a"}, + {file = "greenlet-2.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bf633a50cc93ed17e494015897361010fc08700d92676c87931d3ea464123ce"}, + {file = "greenlet-2.0.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9f2c221eecb7ead00b8e3ddb913c67f75cba078fd1d326053225a3f59d850d72"}, + {file = "greenlet-2.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:13ebf93c343dd8bd010cd98e617cb4c1c1f352a0cf2524c82d3814154116aa82"}, + {file = "greenlet-2.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:6f61d71bbc9b4a3de768371b210d906726535d6ca43506737682caa754b956cd"}, + {file = "greenlet-2.0.1-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:2d0bac0385d2b43a7bd1d651621a4e0f1380abc63d6fb1012213a401cbd5bf8f"}, + {file = "greenlet-2.0.1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:f6327b6907b4cb72f650a5b7b1be23a2aab395017aa6f1adb13069d66360eb3f"}, + {file = "greenlet-2.0.1-cp35-cp35m-win32.whl", hash = "sha256:81b0ea3715bf6a848d6f7149d25bf018fd24554a4be01fcbbe3fdc78e890b955"}, + {file = "greenlet-2.0.1-cp35-cp35m-win_amd64.whl", hash = "sha256:38255a3f1e8942573b067510f9611fc9e38196077b0c8eb7a8c795e105f9ce77"}, + {file = "greenlet-2.0.1-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:04957dc96669be041e0c260964cfef4c77287f07c40452e61abe19d647505581"}, + {file = "greenlet-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:4aeaebcd91d9fee9aa768c1b39cb12214b30bf36d2b7370505a9f2165fedd8d9"}, + {file = "greenlet-2.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:974a39bdb8c90a85982cdb78a103a32e0b1be986d411303064b28a80611f6e51"}, + {file = "greenlet-2.0.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8dca09dedf1bd8684767bc736cc20c97c29bc0c04c413e3276e0962cd7aeb148"}, + {file = "greenlet-2.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4c0757db9bd08470ff8277791795e70d0bf035a011a528ee9a5ce9454b6cba2"}, + {file = "greenlet-2.0.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:5067920de254f1a2dee8d3d9d7e4e03718e8fd2d2d9db962c8c9fa781ae82a39"}, + {file = "greenlet-2.0.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:5a8e05057fab2a365c81abc696cb753da7549d20266e8511eb6c9d9f72fe3e92"}, + {file = "greenlet-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:3d75b8d013086b08e801fbbb896f7d5c9e6ccd44f13a9241d2bf7c0df9eda928"}, + {file = "greenlet-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:097e3dae69321e9100202fc62977f687454cd0ea147d0fd5a766e57450c569fd"}, + {file = "greenlet-2.0.1-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:cb242fc2cda5a307a7698c93173d3627a2a90d00507bccf5bc228851e8304963"}, + {file = "greenlet-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:72b00a8e7c25dcea5946692a2485b1a0c0661ed93ecfedfa9b6687bd89a24ef5"}, + {file = "greenlet-2.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5b0ff9878333823226d270417f24f4d06f235cb3e54d1103b71ea537a6a86ce"}, + {file = "greenlet-2.0.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be9e0fb2ada7e5124f5282d6381903183ecc73ea019568d6d63d33f25b2a9000"}, + {file = "greenlet-2.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b493db84d124805865adc587532ebad30efa68f79ad68f11b336e0a51ec86c2"}, + {file = "greenlet-2.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0459d94f73265744fee4c2d5ec44c6f34aa8a31017e6e9de770f7bcf29710be9"}, + {file = "greenlet-2.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a20d33124935d27b80e6fdacbd34205732660e0a1d35d8b10b3328179a2b51a1"}, + {file = "greenlet-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:ea688d11707d30e212e0110a1aac7f7f3f542a259235d396f88be68b649e47d1"}, + {file = "greenlet-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:afe07421c969e259e9403c3bb658968702bc3b78ec0b6fde3ae1e73440529c23"}, + {file = "greenlet-2.0.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:cd4ccc364cf75d1422e66e247e52a93da6a9b73cefa8cad696f3cbbb75af179d"}, + {file = "greenlet-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:4c8b1c43e75c42a6cafcc71defa9e01ead39ae80bd733a2608b297412beede68"}, + {file = "greenlet-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:659f167f419a4609bc0516fb18ea69ed39dbb25594934bd2dd4d0401660e8a1e"}, + {file = "greenlet-2.0.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:356e4519d4dfa766d50ecc498544b44c0249b6de66426041d7f8b751de4d6b48"}, + {file = "greenlet-2.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:811e1d37d60b47cb8126e0a929b58c046251f28117cb16fcd371eed61f66b764"}, + {file = "greenlet-2.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d38ffd0e81ba8ef347d2be0772e899c289b59ff150ebbbbe05dc61b1246eb4e0"}, + {file = "greenlet-2.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0109af1138afbfb8ae647e31a2b1ab030f58b21dd8528c27beaeb0093b7938a9"}, + {file = "greenlet-2.0.1-cp38-cp38-win32.whl", hash = "sha256:88c8d517e78acdf7df8a2134a3c4b964415b575d2840a2746ddb1cc6175f8608"}, + {file = "greenlet-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:d6ee1aa7ab36475035eb48c01efae87d37936a8173fc4d7b10bb02c2d75dd8f6"}, + {file = "greenlet-2.0.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:b1992ba9d4780d9af9726bbcef6a1db12d9ab1ccc35e5773685a24b7fb2758eb"}, + {file = "greenlet-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:b5e83e4de81dcc9425598d9469a624826a0b1211380ac444c7c791d4a2137c19"}, + {file = "greenlet-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:505138d4fa69462447a562a7c2ef723c6025ba12ac04478bc1ce2fcc279a2db5"}, + {file = "greenlet-2.0.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cce1e90dd302f45716a7715517c6aa0468af0bf38e814ad4eab58e88fc09f7f7"}, + {file = "greenlet-2.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e9744c657d896c7b580455e739899e492a4a452e2dd4d2b3e459f6b244a638d"}, + {file = "greenlet-2.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:662e8f7cad915ba75d8017b3e601afc01ef20deeeabf281bd00369de196d7726"}, + {file = "greenlet-2.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:41b825d65f31e394b523c84db84f9383a2f7eefc13d987f308f4663794d2687e"}, + {file = "greenlet-2.0.1-cp39-cp39-win32.whl", hash = "sha256:db38f80540083ea33bdab614a9d28bcec4b54daa5aff1668d7827a9fc769ae0a"}, + {file = "greenlet-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:b23d2a46d53210b498e5b701a1913697671988f4bf8e10f935433f6e7c332fb6"}, + {file = "greenlet-2.0.1.tar.gz", hash = "sha256:42e602564460da0e8ee67cb6d7236363ee5e131aa15943b6670e44e5c2ed0f67"}, +] + +[package.extras] +docs = ["Sphinx", "docutils (<0.18)"] +test = ["faulthandler", "objgraph", "psutil"] + +[[package]] +name = "idna" +version = "3.4" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" +optional = false +python-versions = ">=3.5" +files = [ + {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, + {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, +] + +[[package]] +name = "ipython" +version = "8.8.0" +description = "IPython: Productive Interactive Computing" +category = "dev" +optional = false +python-versions = ">=3.8" +files = [ + {file = "ipython-8.8.0-py3-none-any.whl", hash = "sha256:da01e6df1501e6e7c32b5084212ddadd4ee2471602e2cf3e0190f4de6b0ea481"}, + {file = "ipython-8.8.0.tar.gz", hash = "sha256:f3bf2c08505ad2c3f4ed5c46ae0331a8547d36bf4b21a451e8ae80c0791db95b"}, +] + +[package.dependencies] +appnope = {version = "*", markers = "sys_platform == \"darwin\""} +backcall = "*" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +decorator = "*" +jedi = ">=0.16" +matplotlib-inline = "*" +pexpect = {version = ">4.3", markers = "sys_platform != \"win32\""} +pickleshare = "*" +prompt-toolkit = ">=3.0.11,<3.1.0" +pygments = ">=2.4.0" +stack-data = "*" +traitlets = ">=5" + +[package.extras] +all = ["black", "curio", "docrepr", "ipykernel", "ipyparallel", "ipywidgets", "matplotlib", "matplotlib (!=3.2.0)", "nbconvert", "nbformat", "notebook", "numpy (>=1.20)", "pandas", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio", "qtconsole", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "trio", "typing-extensions"] +black = ["black"] +doc = ["docrepr", "ipykernel", "matplotlib", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "typing-extensions"] +kernel = ["ipykernel"] +nbconvert = ["nbconvert"] +nbformat = ["nbformat"] +notebook = ["ipywidgets", "notebook"] +parallel = ["ipyparallel"] +qtconsole = ["qtconsole"] +test = ["pytest (<7.1)", "pytest-asyncio", "testpath"] +test-extra = ["curio", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.20)", "pandas", "pytest (<7.1)", "pytest-asyncio", "testpath", "trio"] + +[[package]] +name = "jedi" +version = "0.18.2" +description = "An autocompletion tool for Python that can be used for text editors." +category = "dev" +optional = false +python-versions = ">=3.6" +files = [ + {file = "jedi-0.18.2-py2.py3-none-any.whl", hash = "sha256:203c1fd9d969ab8f2119ec0a3342e0b49910045abe6af0a3ae83a5764d54639e"}, + {file = "jedi-0.18.2.tar.gz", hash = "sha256:bae794c30d07f6d910d32a7048af09b5a39ed740918da923c6b780790ebac612"}, +] + +[package.dependencies] +parso = ">=0.8.0,<0.9.0" + +[package.extras] +docs = ["Jinja2 (==2.11.3)", "MarkupSafe (==1.1.1)", "Pygments (==2.8.1)", "alabaster (==0.7.12)", "babel (==2.9.1)", "chardet (==4.0.0)", "commonmark (==0.8.1)", "docutils (==0.17.1)", "future (==0.18.2)", "idna (==2.10)", "imagesize (==1.2.0)", "mock (==1.0.1)", "packaging (==20.9)", "pyparsing (==2.4.7)", "pytz (==2021.1)", "readthedocs-sphinx-ext (==2.1.4)", "recommonmark (==0.5.0)", "requests (==2.25.1)", "six (==1.15.0)", "snowballstemmer (==2.1.0)", "sphinx (==1.8.5)", "sphinx-rtd-theme (==0.4.3)", "sphinxcontrib-serializinghtml (==1.1.4)", "sphinxcontrib-websupport (==1.2.4)", "urllib3 (==1.26.4)"] +qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] +testing = ["Django (<3.1)", "attrs", "colorama", "docopt", "pytest (<7.0.0)"] + +[[package]] +name = "loguru" +version = "0.6.0" +description = "Python logging made (stupidly) simple" +category = "main" +optional = false +python-versions = ">=3.5" +files = [ + {file = "loguru-0.6.0-py3-none-any.whl", hash = "sha256:4e2414d534a2ab57573365b3e6d0234dfb1d84b68b7f3b948e6fb743860a77c3"}, + {file = "loguru-0.6.0.tar.gz", hash = "sha256:066bd06758d0a513e9836fd9c6b5a75bfb3fd36841f4b996bc60b547a309d41c"}, +] + +[package.dependencies] +colorama = {version = ">=0.3.4", markers = "sys_platform == \"win32\""} +win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""} + +[package.extras] +dev = ["Sphinx (>=4.1.1)", "black (>=19.10b0)", "colorama (>=0.3.4)", "docutils (==0.16)", "flake8 (>=3.7.7)", "isort (>=5.1.1)", "pytest (>=4.6.2)", "pytest-cov (>=2.7.1)", "sphinx-autobuild (>=0.7.1)", "sphinx-rtd-theme (>=0.4.3)", "tox (>=3.9.0)"] + +[[package]] +name = "mako" +version = "1.2.4" +description = "A super-fast templating language that borrows the best ideas from the existing templating languages." +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "Mako-1.2.4-py3-none-any.whl", hash = "sha256:c97c79c018b9165ac9922ae4f32da095ffd3c4e6872b45eded42926deea46818"}, + {file = "Mako-1.2.4.tar.gz", hash = "sha256:d60a3903dc3bb01a18ad6a89cdbe2e4eadc69c0bc8ef1e3773ba53d44c3f7a34"}, +] + +[package.dependencies] +MarkupSafe = ">=0.9.2" + +[package.extras] +babel = ["Babel"] +lingua = ["lingua"] +testing = ["pytest"] + +[[package]] +name = "markupsafe" +version = "2.1.1" +description = "Safely add untrusted strings to HTML/XML markup." +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-win32.whl", hash = "sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-win32.whl", hash = "sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-win32.whl", hash = "sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-win32.whl", hash = "sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247"}, + {file = "MarkupSafe-2.1.1.tar.gz", hash = "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b"}, +] + +[[package]] +name = "matplotlib-inline" +version = "0.1.6" +description = "Inline Matplotlib backend for Jupyter" +category = "dev" +optional = false +python-versions = ">=3.5" +files = [ + {file = "matplotlib-inline-0.1.6.tar.gz", hash = "sha256:f887e5f10ba98e8d2b150ddcf4702c1e5f8b3a20005eb0f74bfdbd360ee6f304"}, + {file = "matplotlib_inline-0.1.6-py3-none-any.whl", hash = "sha256:f1f41aab5328aa5aaea9b16d083b128102f8712542f819fe7e6a420ff581b311"}, +] + +[package.dependencies] +traitlets = "*" + +[[package]] +name = "parso" +version = "0.8.3" +description = "A Python Parser" +category = "dev" +optional = false +python-versions = ">=3.6" +files = [ + {file = "parso-0.8.3-py2.py3-none-any.whl", hash = "sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75"}, + {file = "parso-0.8.3.tar.gz", hash = "sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0"}, +] + +[package.extras] +qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] +testing = ["docopt", "pytest (<6.0.0)"] + +[[package]] +name = "pexpect" +version = "4.8.0" +description = "Pexpect allows easy control of interactive console applications." +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "pexpect-4.8.0-py2.py3-none-any.whl", hash = "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937"}, + {file = "pexpect-4.8.0.tar.gz", hash = "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"}, +] + +[package.dependencies] +ptyprocess = ">=0.5" + +[[package]] +name = "pickleshare" +version = "0.7.5" +description = "Tiny 'shelve'-like database with concurrency support" +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "pickleshare-0.7.5-py2.py3-none-any.whl", hash = "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"}, + {file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"}, +] + +[[package]] +name = "prompt-toolkit" +version = "3.0.36" +description = "Library for building powerful interactive command lines in Python" +category = "dev" +optional = false +python-versions = ">=3.6.2" +files = [ + {file = "prompt_toolkit-3.0.36-py3-none-any.whl", hash = "sha256:aa64ad242a462c5ff0363a7b9cfe696c20d55d9fc60c11fd8e632d064804d305"}, + {file = "prompt_toolkit-3.0.36.tar.gz", hash = "sha256:3e163f254bef5a03b146397d7c1963bd3e2812f0964bb9a24e6ec761fd28db63"}, +] + +[package.dependencies] +wcwidth = "*" + +[[package]] +name = "psycopg2-binary" +version = "2.9.5" +description = "psycopg2 - Python-PostgreSQL Database Adapter" +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "psycopg2-binary-2.9.5.tar.gz", hash = "sha256:33e632d0885b95a8b97165899006c40e9ecdc634a529dca7b991eb7de4ece41c"}, + {file = "psycopg2_binary-2.9.5-cp310-cp310-macosx_10_15_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:0775d6252ccb22b15da3b5d7adbbf8cfe284916b14b6dc0ff503a23edb01ee85"}, + {file = "psycopg2_binary-2.9.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2ec46ed947801652c9643e0b1dc334cfb2781232e375ba97312c2fc256597632"}, + {file = "psycopg2_binary-2.9.5-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3520d7af1ebc838cc6084a3281145d5cd5bdd43fdef139e6db5af01b92596cb7"}, + {file = "psycopg2_binary-2.9.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5cbc554ba47ecca8cd3396ddaca85e1ecfe3e48dd57dc5e415e59551affe568e"}, + {file = "psycopg2_binary-2.9.5-cp310-cp310-manylinux_2_24_aarch64.whl", hash = "sha256:5d28ecdf191db558d0c07d0f16524ee9d67896edf2b7990eea800abeb23ebd61"}, + {file = "psycopg2_binary-2.9.5-cp310-cp310-manylinux_2_24_ppc64le.whl", hash = "sha256:b9c33d4aef08dfecbd1736ceab8b7b3c4358bf10a0121483e5cd60d3d308cc64"}, + {file = "psycopg2_binary-2.9.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:05b3d479425e047c848b9782cd7aac9c6727ce23181eb9647baf64ffdfc3da41"}, + {file = "psycopg2_binary-2.9.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:1e491e6489a6cb1d079df8eaa15957c277fdedb102b6a68cfbf40c4994412fd0"}, + {file = "psycopg2_binary-2.9.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:9e32cedc389bcb76d9f24ea8a012b3cb8385ee362ea437e1d012ffaed106c17d"}, + {file = "psycopg2_binary-2.9.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:46850a640df62ae940e34a163f72e26aca1f88e2da79148e1862faaac985c302"}, + {file = "psycopg2_binary-2.9.5-cp310-cp310-win32.whl", hash = "sha256:3d790f84201c3698d1bfb404c917f36e40531577a6dda02e45ba29b64d539867"}, + {file = "psycopg2_binary-2.9.5-cp310-cp310-win_amd64.whl", hash = "sha256:1764546ffeaed4f9428707be61d68972eb5ede81239b46a45843e0071104d0dd"}, + {file = "psycopg2_binary-2.9.5-cp311-cp311-macosx_10_9_universal2.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:426c2ae999135d64e6a18849a7d1ad0e1bd007277e4a8f4752eaa40a96b550ff"}, + {file = "psycopg2_binary-2.9.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7cf1d44e710ca3a9ce952bda2855830fe9f9017ed6259e01fcd71ea6287565f5"}, + {file = "psycopg2_binary-2.9.5-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:024030b13bdcbd53d8a93891a2cf07719715724fc9fee40243f3bd78b4264b8f"}, + {file = "psycopg2_binary-2.9.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bcda1c84a1c533c528356da5490d464a139b6e84eb77cc0b432e38c5c6dd7882"}, + {file = "psycopg2_binary-2.9.5-cp311-cp311-manylinux_2_24_aarch64.whl", hash = "sha256:2ef892cabdccefe577088a79580301f09f2a713eb239f4f9f62b2b29cafb0577"}, + {file = "psycopg2_binary-2.9.5-cp311-cp311-manylinux_2_24_ppc64le.whl", hash = "sha256:af0516e1711995cb08dc19bbd05bec7dbdebf4185f68870595156718d237df3e"}, + {file = "psycopg2_binary-2.9.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e72c91bda9880f097c8aa3601a2c0de6c708763ba8128006151f496ca9065935"}, + {file = "psycopg2_binary-2.9.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:e67b3c26e9b6d37b370c83aa790bbc121775c57bfb096c2e77eacca25fd0233b"}, + {file = "psycopg2_binary-2.9.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:5fc447058d083b8c6ac076fc26b446d44f0145308465d745fba93a28c14c9e32"}, + {file = "psycopg2_binary-2.9.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d892bfa1d023c3781a3cab8dd5af76b626c483484d782e8bd047c180db590e4c"}, + {file = "psycopg2_binary-2.9.5-cp311-cp311-win32.whl", hash = "sha256:2abccab84d057723d2ca8f99ff7b619285d40da6814d50366f61f0fc385c3903"}, + {file = "psycopg2_binary-2.9.5-cp311-cp311-win_amd64.whl", hash = "sha256:bef7e3f9dc6f0c13afdd671008534be5744e0e682fb851584c8c3a025ec09720"}, + {file = "psycopg2_binary-2.9.5-cp36-cp36m-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:6e63814ec71db9bdb42905c925639f319c80e7909fb76c3b84edc79dadef8d60"}, + {file = "psycopg2_binary-2.9.5-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:212757ffcecb3e1a5338d4e6761bf9c04f750e7d027117e74aa3cd8a75bb6fbd"}, + {file = "psycopg2_binary-2.9.5-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f8a9bcab7b6db2e3dbf65b214dfc795b4c6b3bb3af922901b6a67f7cb47d5f8"}, + {file = "psycopg2_binary-2.9.5-cp36-cp36m-manylinux_2_24_aarch64.whl", hash = "sha256:56b2957a145f816726b109ee3d4e6822c23f919a7d91af5a94593723ed667835"}, + {file = "psycopg2_binary-2.9.5-cp36-cp36m-manylinux_2_24_ppc64le.whl", hash = "sha256:f95b8aca2703d6a30249f83f4fe6a9abf2e627aa892a5caaab2267d56be7ab69"}, + {file = "psycopg2_binary-2.9.5-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:70831e03bd53702c941da1a1ad36c17d825a24fbb26857b40913d58df82ec18b"}, + {file = "psycopg2_binary-2.9.5-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:dbc332beaf8492b5731229a881807cd7b91b50dbbbaf7fe2faf46942eda64a24"}, + {file = "psycopg2_binary-2.9.5-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:2d964eb24c8b021623df1c93c626671420c6efadbdb8655cb2bd5e0c6fa422ba"}, + {file = "psycopg2_binary-2.9.5-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:95076399ec3b27a8f7fa1cc9a83417b1c920d55cf7a97f718a94efbb96c7f503"}, + {file = "psycopg2_binary-2.9.5-cp36-cp36m-win32.whl", hash = "sha256:3fc33295cfccad697a97a76dec3f1e94ad848b7b163c3228c1636977966b51e2"}, + {file = "psycopg2_binary-2.9.5-cp36-cp36m-win_amd64.whl", hash = "sha256:02551647542f2bf89073d129c73c05a25c372fc0a49aa50e0de65c3c143d8bd0"}, + {file = "psycopg2_binary-2.9.5-cp37-cp37m-macosx_10_15_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:63e318dbe52709ed10d516a356f22a635e07a2e34c68145484ed96a19b0c4c68"}, + {file = "psycopg2_binary-2.9.5-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7e518a0911c50f60313cb9e74a169a65b5d293770db4770ebf004245f24b5c5"}, + {file = "psycopg2_binary-2.9.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9d38a4656e4e715d637abdf7296e98d6267df0cc0a8e9a016f8ba07e4aa3eeb"}, + {file = "psycopg2_binary-2.9.5-cp37-cp37m-manylinux_2_24_aarch64.whl", hash = "sha256:68d81a2fe184030aa0c5c11e518292e15d342a667184d91e30644c9d533e53e1"}, + {file = "psycopg2_binary-2.9.5-cp37-cp37m-manylinux_2_24_ppc64le.whl", hash = "sha256:7ee3095d02d6f38bd7d9a5358fcc9ea78fcdb7176921528dd709cc63f40184f5"}, + {file = "psycopg2_binary-2.9.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:46512486be6fbceef51d7660dec017394ba3e170299d1dc30928cbedebbf103a"}, + {file = "psycopg2_binary-2.9.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b911dfb727e247340d36ae20c4b9259e4a64013ab9888ccb3cbba69b77fd9636"}, + {file = "psycopg2_binary-2.9.5-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:422e3d43b47ac20141bc84b3d342eead8d8099a62881a501e97d15f6addabfe9"}, + {file = "psycopg2_binary-2.9.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c5682a45df7d9642eff590abc73157c887a68f016df0a8ad722dcc0f888f56d7"}, + {file = "psycopg2_binary-2.9.5-cp37-cp37m-win32.whl", hash = "sha256:b8104f709590fff72af801e916817560dbe1698028cd0afe5a52d75ceb1fce5f"}, + {file = "psycopg2_binary-2.9.5-cp37-cp37m-win_amd64.whl", hash = "sha256:7b3751857da3e224f5629400736a7b11e940b5da5f95fa631d86219a1beaafec"}, + {file = "psycopg2_binary-2.9.5-cp38-cp38-macosx_10_15_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:043a9fd45a03858ff72364b4b75090679bd875ee44df9c0613dc862ca6b98460"}, + {file = "psycopg2_binary-2.9.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9ffdc51001136b699f9563b1c74cc1f8c07f66ef7219beb6417a4c8aaa896c28"}, + {file = "psycopg2_binary-2.9.5-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c15ba5982c177bc4b23a7940c7e4394197e2d6a424a2d282e7c236b66da6d896"}, + {file = "psycopg2_binary-2.9.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc85b3777068ed30aff8242be2813038a929f2084f69e43ef869daddae50f6ee"}, + {file = "psycopg2_binary-2.9.5-cp38-cp38-manylinux_2_24_aarch64.whl", hash = "sha256:215d6bf7e66732a514f47614f828d8c0aaac9a648c46a831955cb103473c7147"}, + {file = "psycopg2_binary-2.9.5-cp38-cp38-manylinux_2_24_ppc64le.whl", hash = "sha256:7d07f552d1e412f4b4e64ce386d4c777a41da3b33f7098b6219012ba534fb2c2"}, + {file = "psycopg2_binary-2.9.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a0adef094c49f242122bb145c3c8af442070dc0e4312db17e49058c1702606d4"}, + {file = "psycopg2_binary-2.9.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:00475004e5ed3e3bf5e056d66e5dcdf41a0dc62efcd57997acd9135c40a08a50"}, + {file = "psycopg2_binary-2.9.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:7d88db096fa19d94f433420eaaf9f3c45382da2dd014b93e4bf3215639047c16"}, + {file = "psycopg2_binary-2.9.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:902844f9c4fb19b17dfa84d9e2ca053d4a4ba265723d62ea5c9c26b38e0aa1e6"}, + {file = "psycopg2_binary-2.9.5-cp38-cp38-win32.whl", hash = "sha256:4e7904d1920c0c89105c0517dc7e3f5c20fb4e56ba9cdef13048db76947f1d79"}, + {file = "psycopg2_binary-2.9.5-cp38-cp38-win_amd64.whl", hash = "sha256:a36a0e791805aa136e9cbd0ffa040d09adec8610453ee8a753f23481a0057af5"}, + {file = "psycopg2_binary-2.9.5-cp39-cp39-macosx_10_15_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:25382c7d174c679ce6927c16b6fbb68b10e56ee44b1acb40671e02d29f2fce7c"}, + {file = "psycopg2_binary-2.9.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9c38d3869238e9d3409239bc05bc27d6b7c99c2a460ea337d2814b35fb4fea1b"}, + {file = "psycopg2_binary-2.9.5-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5c6527c8efa5226a9e787507652dd5ba97b62d29b53c371a85cd13f957fe4d42"}, + {file = "psycopg2_binary-2.9.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e59137cdb970249ae60be2a49774c6dfb015bd0403f05af1fe61862e9626642d"}, + {file = "psycopg2_binary-2.9.5-cp39-cp39-manylinux_2_24_aarch64.whl", hash = "sha256:d4c7b3a31502184e856df1f7bbb2c3735a05a8ce0ade34c5277e1577738a5c91"}, + {file = "psycopg2_binary-2.9.5-cp39-cp39-manylinux_2_24_ppc64le.whl", hash = "sha256:b9a794cef1d9c1772b94a72eec6da144c18e18041d294a9ab47669bc77a80c1d"}, + {file = "psycopg2_binary-2.9.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c5254cbd4f4855e11cebf678c1a848a3042d455a22a4ce61349c36aafd4c2267"}, + {file = "psycopg2_binary-2.9.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c5e65c6ac0ae4bf5bef1667029f81010b6017795dcb817ba5c7b8a8d61fab76f"}, + {file = "psycopg2_binary-2.9.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:74eddec4537ab1f701a1647214734bc52cee2794df748f6ae5908e00771f180a"}, + {file = "psycopg2_binary-2.9.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:01ad49d68dd8c5362e4bfb4158f2896dc6e0c02e87b8a3770fc003459f1a4425"}, + {file = "psycopg2_binary-2.9.5-cp39-cp39-win32.whl", hash = "sha256:937880290775033a743f4836aa253087b85e62784b63fd099ee725d567a48aa1"}, + {file = "psycopg2_binary-2.9.5-cp39-cp39-win_amd64.whl", hash = "sha256:484405b883630f3e74ed32041a87456c5e0e63a8e3429aa93e8714c366d62bd1"}, +] + +[[package]] +name = "ptyprocess" +version = "0.7.0" +description = "Run a subprocess in a pseudo terminal" +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, + {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, +] + +[[package]] +name = "pure-eval" +version = "0.2.2" +description = "Safely evaluate AST nodes without side effects" +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "pure_eval-0.2.2-py3-none-any.whl", hash = "sha256:01eaab343580944bc56080ebe0a674b39ec44a945e6d09ba7db3cb8cec289350"}, + {file = "pure_eval-0.2.2.tar.gz", hash = "sha256:2b45320af6dfaa1750f543d714b6d1c520a1688dec6fd24d339063ce0aaa9ac3"}, +] + +[package.extras] +tests = ["pytest"] + +[[package]] +name = "pycparser" +version = "2.21" +description = "C parser in Python" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, + {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, +] + +[[package]] +name = "pydantic" +version = "1.10.4" +description = "Data validation and settings management using python type hints" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pydantic-1.10.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b5635de53e6686fe7a44b5cf25fcc419a0d5e5c1a1efe73d49d48fe7586db854"}, + {file = "pydantic-1.10.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6dc1cc241440ed7ca9ab59d9929075445da6b7c94ced281b3dd4cfe6c8cff817"}, + {file = "pydantic-1.10.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51bdeb10d2db0f288e71d49c9cefa609bca271720ecd0c58009bd7504a0c464c"}, + {file = "pydantic-1.10.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78cec42b95dbb500a1f7120bdf95c401f6abb616bbe8785ef09887306792e66e"}, + {file = "pydantic-1.10.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:8775d4ef5e7299a2f4699501077a0defdaac5b6c4321173bcb0f3c496fbadf85"}, + {file = "pydantic-1.10.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:572066051eeac73d23f95ba9a71349c42a3e05999d0ee1572b7860235b850cc6"}, + {file = "pydantic-1.10.4-cp310-cp310-win_amd64.whl", hash = "sha256:7feb6a2d401f4d6863050f58325b8d99c1e56f4512d98b11ac64ad1751dc647d"}, + {file = "pydantic-1.10.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:39f4a73e5342b25c2959529f07f026ef58147249f9b7431e1ba8414a36761f53"}, + {file = "pydantic-1.10.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:983e720704431a6573d626b00662eb78a07148c9115129f9b4351091ec95ecc3"}, + {file = "pydantic-1.10.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75d52162fe6b2b55964fbb0af2ee58e99791a3138588c482572bb6087953113a"}, + {file = "pydantic-1.10.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fdf8d759ef326962b4678d89e275ffc55b7ce59d917d9f72233762061fd04a2d"}, + {file = "pydantic-1.10.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:05a81b006be15655b2a1bae5faa4280cf7c81d0e09fcb49b342ebf826abe5a72"}, + {file = "pydantic-1.10.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d88c4c0e5c5dfd05092a4b271282ef0588e5f4aaf345778056fc5259ba098857"}, + {file = "pydantic-1.10.4-cp311-cp311-win_amd64.whl", hash = "sha256:6a05a9db1ef5be0fe63e988f9617ca2551013f55000289c671f71ec16f4985e3"}, + {file = "pydantic-1.10.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:887ca463c3bc47103c123bc06919c86720e80e1214aab79e9b779cda0ff92a00"}, + {file = "pydantic-1.10.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdf88ab63c3ee282c76d652fc86518aacb737ff35796023fae56a65ced1a5978"}, + {file = "pydantic-1.10.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a48f1953c4a1d9bd0b5167ac50da9a79f6072c63c4cef4cf2a3736994903583e"}, + {file = "pydantic-1.10.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:a9f2de23bec87ff306aef658384b02aa7c32389766af3c5dee9ce33e80222dfa"}, + {file = "pydantic-1.10.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:cd8702c5142afda03dc2b1ee6bc358b62b3735b2cce53fc77b31ca9f728e4bc8"}, + {file = "pydantic-1.10.4-cp37-cp37m-win_amd64.whl", hash = "sha256:6e7124d6855b2780611d9f5e1e145e86667eaa3bd9459192c8dc1a097f5e9903"}, + {file = "pydantic-1.10.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b53e1d41e97063d51a02821b80538053ee4608b9a181c1005441f1673c55423"}, + {file = "pydantic-1.10.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:55b1625899acd33229c4352ce0ae54038529b412bd51c4915349b49ca575258f"}, + {file = "pydantic-1.10.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:301d626a59edbe5dfb48fcae245896379a450d04baeed50ef40d8199f2733b06"}, + {file = "pydantic-1.10.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b6f9d649892a6f54a39ed56b8dfd5e08b5f3be5f893da430bed76975f3735d15"}, + {file = "pydantic-1.10.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d7b5a3821225f5c43496c324b0d6875fde910a1c2933d726a743ce328fbb2a8c"}, + {file = "pydantic-1.10.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f2f7eb6273dd12472d7f218e1fef6f7c7c2f00ac2e1ecde4db8824c457300416"}, + {file = "pydantic-1.10.4-cp38-cp38-win_amd64.whl", hash = "sha256:4b05697738e7d2040696b0a66d9f0a10bec0efa1883ca75ee9e55baf511909d6"}, + {file = "pydantic-1.10.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a9a6747cac06c2beb466064dda999a13176b23535e4c496c9d48e6406f92d42d"}, + {file = "pydantic-1.10.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:eb992a1ef739cc7b543576337bebfc62c0e6567434e522e97291b251a41dad7f"}, + {file = "pydantic-1.10.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:990406d226dea0e8f25f643b370224771878142155b879784ce89f633541a024"}, + {file = "pydantic-1.10.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e82a6d37a95e0b1b42b82ab340ada3963aea1317fd7f888bb6b9dfbf4fff57c"}, + {file = "pydantic-1.10.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9193d4f4ee8feca58bc56c8306bcb820f5c7905fd919e0750acdeeeef0615b28"}, + {file = "pydantic-1.10.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2b3ce5f16deb45c472dde1a0ee05619298c864a20cded09c4edd820e1454129f"}, + {file = "pydantic-1.10.4-cp39-cp39-win_amd64.whl", hash = "sha256:9cbdc268a62d9a98c56e2452d6c41c0263d64a2009aac69246486f01b4f594c4"}, + {file = "pydantic-1.10.4-py3-none-any.whl", hash = "sha256:4948f264678c703f3877d1c8877c4e3b2e12e549c57795107f08cf70c6ec7774"}, + {file = "pydantic-1.10.4.tar.gz", hash = "sha256:b9a3859f24eb4e097502a3be1fb4b2abb79b6103dd9e2e0edb70613a4459a648"}, +] + +[package.dependencies] +email-validator = {version = ">=1.0.3", optional = true, markers = "extra == \"email\""} +typing-extensions = ">=4.2.0" + +[package.extras] +dotenv = ["python-dotenv (>=0.10.4)"] +email = ["email-validator (>=1.0.3)"] + +[[package]] +name = "pygments" +version = "2.14.0" +description = "Pygments is a syntax highlighting package written in Python." +category = "dev" +optional = false +python-versions = ">=3.6" +files = [ + {file = "Pygments-2.14.0-py3-none-any.whl", hash = "sha256:fa7bd7bd2771287c0de303af8bfdfc731f51bd2c6a47ab69d117138893b82717"}, + {file = "Pygments-2.14.0.tar.gz", hash = "sha256:b3ed06a9e8ac9a9aae5a6f5dbe78a8a58655d17b43b93c078f094ddc476ae297"}, +] + +[package.extras] +plugins = ["importlib-metadata"] + +[[package]] +name = "pymysql" +version = "1.0.2" +description = "Pure Python MySQL Driver" +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "PyMySQL-1.0.2-py3-none-any.whl", hash = "sha256:41fc3a0c5013d5f039639442321185532e3e2c8924687abe6537de157d403641"}, + {file = "PyMySQL-1.0.2.tar.gz", hash = "sha256:816927a350f38d56072aeca5dfb10221fe1dc653745853d30a216637f5d7ad36"}, +] + +[package.extras] +ed25519 = ["PyNaCl (>=1.4.0)"] +rsa = ["cryptography"] + +[[package]] +name = "python-dateutil" +version = "2.8.2" +description = "Extensions to the standard Python datetime module" +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, + {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "python-dotenv" +version = "0.20.0" +description = "Read key-value pairs from a .env file and set them as environment variables" +category = "main" +optional = false +python-versions = ">=3.5" +files = [ + {file = "python-dotenv-0.20.0.tar.gz", hash = "sha256:b7e3b04a59693c42c36f9ab1cc2acc46fa5df8c78e178fc33a8d4cd05c8d498f"}, + {file = "python_dotenv-0.20.0-py3-none-any.whl", hash = "sha256:d92a187be61fe482e4fd675b6d52200e7be63a12b724abbf931a40ce4fa92938"}, +] + +[package.extras] +cli = ["click (>=5.0)"] + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + +[[package]] +name = "sqlalchemy" +version = "1.4.46" +description = "Database Abstraction Library" +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +files = [ + {file = "SQLAlchemy-1.4.46-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:7001f16a9a8e06488c3c7154827c48455d1c1507d7228d43e781afbc8ceccf6d"}, + {file = "SQLAlchemy-1.4.46-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:c7a46639ba058d320c9f53a81db38119a74b8a7a1884df44d09fbe807d028aaf"}, + {file = "SQLAlchemy-1.4.46-cp27-cp27m-win32.whl", hash = "sha256:c04144a24103135ea0315d459431ac196fe96f55d3213bfd6d39d0247775c854"}, + {file = "SQLAlchemy-1.4.46-cp27-cp27m-win_amd64.whl", hash = "sha256:7b81b1030c42b003fc10ddd17825571603117f848814a344d305262d370e7c34"}, + {file = "SQLAlchemy-1.4.46-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:939f9a018d2ad04036746e15d119c0428b1e557470361aa798e6e7d7f5875be0"}, + {file = "SQLAlchemy-1.4.46-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:b7f4b6aa6e87991ec7ce0e769689a977776db6704947e562102431474799a857"}, + {file = "SQLAlchemy-1.4.46-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dbf17ac9a61e7a3f1c7ca47237aac93cabd7f08ad92ac5b96d6f8dea4287fc1"}, + {file = "SQLAlchemy-1.4.46-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7f8267682eb41a0584cf66d8a697fef64b53281d01c93a503e1344197f2e01fe"}, + {file = "SQLAlchemy-1.4.46-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64cb0ad8a190bc22d2112001cfecdec45baffdf41871de777239da6a28ed74b6"}, + {file = "SQLAlchemy-1.4.46-cp310-cp310-win32.whl", hash = "sha256:5f752676fc126edc1c4af0ec2e4d2adca48ddfae5de46bb40adbd3f903eb2120"}, + {file = "SQLAlchemy-1.4.46-cp310-cp310-win_amd64.whl", hash = "sha256:31de1e2c45e67a5ec1ecca6ec26aefc299dd5151e355eb5199cd9516b57340be"}, + {file = "SQLAlchemy-1.4.46-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d68e1762997bfebf9e5cf2a9fd0bcf9ca2fdd8136ce7b24bbd3bbfa4328f3e4a"}, + {file = "SQLAlchemy-1.4.46-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d112b0f3c1bc5ff70554a97344625ef621c1bfe02a73c5d97cac91f8cd7a41e"}, + {file = "SQLAlchemy-1.4.46-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69fac0a7054d86b997af12dc23f581cf0b25fb1c7d1fed43257dee3af32d3d6d"}, + {file = "SQLAlchemy-1.4.46-cp311-cp311-win32.whl", hash = "sha256:887865924c3d6e9a473dc82b70977395301533b3030d0f020c38fd9eba5419f2"}, + {file = "SQLAlchemy-1.4.46-cp311-cp311-win_amd64.whl", hash = "sha256:984ee13543a346324319a1fb72b698e521506f6f22dc37d7752a329e9cd00a32"}, + {file = "SQLAlchemy-1.4.46-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:9167d4227b56591a4cc5524f1b79ccd7ea994f36e4c648ab42ca995d28ebbb96"}, + {file = "SQLAlchemy-1.4.46-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d61e9ecc849d8d44d7f80894ecff4abe347136e9d926560b818f6243409f3c86"}, + {file = "SQLAlchemy-1.4.46-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3ec187acf85984263299a3f15c34a6c0671f83565d86d10f43ace49881a82718"}, + {file = "SQLAlchemy-1.4.46-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9883f5fae4fd8e3f875adc2add69f8b945625811689a6c65866a35ee9c0aea23"}, + {file = "SQLAlchemy-1.4.46-cp36-cp36m-win32.whl", hash = "sha256:535377e9b10aff5a045e3d9ada8a62d02058b422c0504ebdcf07930599890eb0"}, + {file = "SQLAlchemy-1.4.46-cp36-cp36m-win_amd64.whl", hash = "sha256:18cafdb27834fa03569d29f571df7115812a0e59fd6a3a03ccb0d33678ec8420"}, + {file = "SQLAlchemy-1.4.46-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:a1ad90c97029cc3ab4ffd57443a20fac21d2ec3c89532b084b073b3feb5abff3"}, + {file = "SQLAlchemy-1.4.46-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4847f4b1d822754e35707db913396a29d874ee77b9c3c3ef3f04d5a9a6209618"}, + {file = "SQLAlchemy-1.4.46-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c5a99282848b6cae0056b85da17392a26b2d39178394fc25700bcf967e06e97a"}, + {file = "SQLAlchemy-1.4.46-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4b1cc7835b39835c75cf7c20c926b42e97d074147c902a9ebb7cf2c840dc4e2"}, + {file = "SQLAlchemy-1.4.46-cp37-cp37m-win32.whl", hash = "sha256:c522e496f9b9b70296a7675272ec21937ccfc15da664b74b9f58d98a641ce1b6"}, + {file = "SQLAlchemy-1.4.46-cp37-cp37m-win_amd64.whl", hash = "sha256:ae067ab639fa499f67ded52f5bc8e084f045d10b5ac7bb928ae4ca2b6c0429a5"}, + {file = "SQLAlchemy-1.4.46-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:e3c1808008124850115a3f7e793a975cfa5c8a26ceeeb9ff9cbb4485cac556df"}, + {file = "SQLAlchemy-1.4.46-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4d164df3d83d204c69f840da30b292ac7dc54285096c6171245b8d7807185aa"}, + {file = "SQLAlchemy-1.4.46-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b33ffbdbbf5446cf36cd4cc530c9d9905d3c2fe56ed09e25c22c850cdb9fac92"}, + {file = "SQLAlchemy-1.4.46-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d94682732d1a0def5672471ba42a29ff5e21bb0aae0afa00bb10796fc1e28dd"}, + {file = "SQLAlchemy-1.4.46-cp38-cp38-win32.whl", hash = "sha256:f8cb80fe8d14307e4124f6fad64dfd87ab749c9d275f82b8b4ec84c84ecebdbe"}, + {file = "SQLAlchemy-1.4.46-cp38-cp38-win_amd64.whl", hash = "sha256:07e48cbcdda6b8bc7a59d6728bd3f5f574ffe03f2c9fb384239f3789c2d95c2e"}, + {file = "SQLAlchemy-1.4.46-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:1b1e5e96e2789d89f023d080bee432e2fef64d95857969e70d3cadec80bd26f0"}, + {file = "SQLAlchemy-1.4.46-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3714e5b33226131ac0da60d18995a102a17dddd42368b7bdd206737297823ad"}, + {file = "SQLAlchemy-1.4.46-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:955162ad1a931fe416eded6bb144ba891ccbf9b2e49dc7ded39274dd9c5affc5"}, + {file = "SQLAlchemy-1.4.46-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6e4cb5c63f705c9d546a054c60d326cbde7421421e2d2565ce3e2eee4e1a01f"}, + {file = "SQLAlchemy-1.4.46-cp39-cp39-win32.whl", hash = "sha256:51e1ba2884c6a2b8e19109dc08c71c49530006c1084156ecadfaadf5f9b8b053"}, + {file = "SQLAlchemy-1.4.46-cp39-cp39-win_amd64.whl", hash = "sha256:315676344e3558f1f80d02535f410e80ea4e8fddba31ec78fe390eff5fb8f466"}, + {file = "SQLAlchemy-1.4.46.tar.gz", hash = "sha256:6913b8247d8a292ef8315162a51931e2b40ce91681f1b6f18f697045200c4a30"}, +] + +[package.dependencies] +greenlet = {version = "!=0.4.17", markers = "python_version >= \"3\" and (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\")"} + +[package.extras] +aiomysql = ["aiomysql", "greenlet (!=0.4.17)"] +aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing-extensions (!=3.10.0.1)"] +asyncio = ["greenlet (!=0.4.17)"] +asyncmy = ["asyncmy (>=0.2.3,!=0.2.4)", "greenlet (!=0.4.17)"] +mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2)"] +mssql = ["pyodbc"] +mssql-pymssql = ["pymssql"] +mssql-pyodbc = ["pyodbc"] +mypy = ["mypy (>=0.910)", "sqlalchemy2-stubs"] +mysql = ["mysqlclient (>=1.4.0)", "mysqlclient (>=1.4.0,<2)"] +mysql-connector = ["mysql-connector-python"] +oracle = ["cx-oracle (>=7)", "cx-oracle (>=7,<8)"] +postgresql = ["psycopg2 (>=2.7)"] +postgresql-asyncpg = ["asyncpg", "greenlet (!=0.4.17)"] +postgresql-pg8000 = ["pg8000 (>=1.16.6,!=1.29.0)"] +postgresql-psycopg2binary = ["psycopg2-binary"] +postgresql-psycopg2cffi = ["psycopg2cffi"] +pymysql = ["pymysql", "pymysql (<1)"] +sqlcipher = ["sqlcipher3-binary"] + +[[package]] +name = "sqlalchemy-utils" +version = "0.38.3" +description = "Various utility functions for SQLAlchemy." +category = "main" +optional = false +python-versions = "~=3.6" +files = [ + {file = "SQLAlchemy-Utils-0.38.3.tar.gz", hash = "sha256:9f9afba607a40455cf703adfa9846584bf26168a0c5a60a70063b70d65051f4d"}, + {file = "SQLAlchemy_Utils-0.38.3-py3-none-any.whl", hash = "sha256:5c13b5d08adfaa85f3d4e8ec09a75136216fad41346980d02974a70a77988bf9"}, +] + +[package.dependencies] +SQLAlchemy = ">=1.3" + +[package.extras] +arrow = ["arrow (>=0.3.4)"] +babel = ["Babel (>=1.3)"] +color = ["colour (>=0.0.4)"] +encrypted = ["cryptography (>=0.6)"] +intervals = ["intervals (>=0.7.1)"] +password = ["passlib (>=1.6,<2.0)"] +pendulum = ["pendulum (>=2.0.5)"] +phone = ["phonenumbers (>=5.9.2)"] +test = ["Jinja2 (>=2.3)", "Pygments (>=1.2)", "backports.zoneinfo", "docutils (>=0.10)", "flake8 (>=2.4.0)", "flexmock (>=0.9.7)", "isort (>=4.2.2)", "pg8000 (>=1.12.4)", "psycopg2 (>=2.5.1)", "psycopg2cffi (>=2.8.1)", "pymysql", "pyodbc", "pytest (>=2.7.1)", "python-dateutil (>=2.6)", "pytz (>=2014.2)"] +test-all = ["Babel (>=1.3)", "Jinja2 (>=2.3)", "Pygments (>=1.2)", "arrow (>=0.3.4)", "backports.zoneinfo", "colour (>=0.0.4)", "cryptography (>=0.6)", "docutils (>=0.10)", "flake8 (>=2.4.0)", "flexmock (>=0.9.7)", "furl (>=0.4.1)", "intervals (>=0.7.1)", "isort (>=4.2.2)", "passlib (>=1.6,<2.0)", "pendulum (>=2.0.5)", "pg8000 (>=1.12.4)", "phonenumbers (>=5.9.2)", "psycopg2 (>=2.5.1)", "psycopg2cffi (>=2.8.1)", "pymysql", "pyodbc", "pytest (>=2.7.1)", "python-dateutil", "python-dateutil (>=2.6)", "pytz (>=2014.2)"] +timezone = ["python-dateutil"] +url = ["furl (>=0.4.1)"] + +[[package]] +name = "stack-data" +version = "0.6.2" +description = "Extract data from python stack frames and tracebacks for informative displays" +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "stack_data-0.6.2-py3-none-any.whl", hash = "sha256:cbb2a53eb64e5785878201a97ed7c7b94883f48b87bfb0bbe8b623c74679e4a8"}, + {file = "stack_data-0.6.2.tar.gz", hash = "sha256:32d2dd0376772d01b6cb9fc996f3c8b57a357089dec328ed4b6553d037eaf815"}, +] + +[package.dependencies] +asttokens = ">=2.1.0" +executing = ">=1.2.0" +pure-eval = "*" + +[package.extras] +tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"] + +[[package]] +name = "traitlets" +version = "5.8.1" +description = "Traitlets Python configuration system" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "traitlets-5.8.1-py3-none-any.whl", hash = "sha256:a1ca5df6414f8b5760f7c5f256e326ee21b581742114545b462b35ffe3f04861"}, + {file = "traitlets-5.8.1.tar.gz", hash = "sha256:32500888f5ff7bbf3b9267ea31748fa657aaf34d56d85e60f91dda7dc7f5785b"}, +] + +[package.extras] +docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] +test = ["argcomplete (>=2.0)", "pre-commit", "pytest", "pytest-mock"] + +[[package]] +name = "typing-extensions" +version = "4.4.0" +description = "Backported and Experimental Type Hints for Python 3.7+" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"}, + {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"}, +] + +[[package]] +name = "wcwidth" +version = "0.2.6" +description = "Measures the displayed width of unicode strings in a terminal" +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "wcwidth-0.2.6-py2.py3-none-any.whl", hash = "sha256:795b138f6875577cd91bba52baf9e445cd5118fd32723b460e30a0af30ea230e"}, + {file = "wcwidth-0.2.6.tar.gz", hash = "sha256:a5220780a404dbe3353789870978e472cfe477761f06ee55077256e509b156d0"}, +] + +[[package]] +name = "win32-setctime" +version = "1.1.0" +description = "A small Python utility to set file creation time on Windows" +category = "main" +optional = false +python-versions = ">=3.5" +files = [ + {file = "win32_setctime-1.1.0-py3-none-any.whl", hash = "sha256:231db239e959c2fe7eb1d7dc129f11172354f98361c4fa2d6d2d7e278baa8aad"}, + {file = "win32_setctime-1.1.0.tar.gz", hash = "sha256:15cf5750465118d6929ae4de4eb46e8edae9a5634350c01ba582df868e932cb2"}, +] + +[package.extras] +dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"] + +[metadata] +lock-version = "2.0" +python-versions = "^3.11" +content-hash = "4baf7c974360f6d7d45a068aa18f6778f4665e68bdc327bfff04f3bd2fd9073a" diff --git a/sqlalchemy_study/pyproject.toml b/sqlalchemy_study/pyproject.toml new file mode 100644 index 0000000..43e667e --- /dev/null +++ b/sqlalchemy_study/pyproject.toml @@ -0,0 +1,28 @@ +[tool.poetry] +name = "sqlalchemy_study_project" +version = "1.0.1" +description = "for study sqlalchemy async models" +authors = ["Dmitry Afanasyev "] + +[tool.poetry.dependencies] +python = "^3.11" +SQLAlchemy = "^1.4" +SQLAlchemy-Utils = "^0.38.2" +pydantic = {version = "^1.9.1", extras = ["email"]} +factory-boy = "^3.2.1" +Faker = "^15.0.0" +loguru = "^0.6.0" +alembic = "^1.8.0" +python-dotenv = "^0.20.0" +asyncpg = "^0.27.0" +asyncmy = "^0.2.5" +PyMySQL = "^1.0.2" +cryptography = "^37.0.2" +psycopg2-binary = "^2.9.3" + +[tool.poetry.dev-dependencies] +ipython = "^8.4.0" + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/sqlalchemy.py b/sqlalchemy_study/sqlalchemy.py similarity index 100% rename from sqlalchemy.py rename to sqlalchemy_study/sqlalchemy.py diff --git a/sqlalchemy_study/src/alembic.ini b/sqlalchemy_study/src/alembic.ini new file mode 100644 index 0000000..eb01b53 --- /dev/null +++ b/sqlalchemy_study/src/alembic.ini @@ -0,0 +1,43 @@ +# A generic, single database configuration. + +[alembic] +# path to migration scripts +script_location = migrations +file_template = %%(year)d-%%(month).2d-%%(day).2d-%%(hour).2d-%%(minute).2d_%%(rev)s +prepend_sys_path = . +output_encoding = utf-8 + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/sqlalchemy_study/src/config/.env.template b/sqlalchemy_study/src/config/.env.template new file mode 100644 index 0000000..3778b39 --- /dev/null +++ b/sqlalchemy_study/src/config/.env.template @@ -0,0 +1,25 @@ +# --------------DATABASE------------- + +# ==== DB provider ====: 'mysql' -> MySQL use | 'postgres' -> Postgres use + +USE_DATABASE=mysql + +# ==== DB common ==== + +DB_HOST=localhost +DB_ECHO=True + +# ==== Postgres ==== + +POSTGRES_DB_PORT=5433 +POSTGRES_DB=sqlalchemy_study +POSTGRES_USER=user +POSTGRES_PASSWORD=postgrespwd + +# ==== MySQL ==== + +MYSQL_DB_PORT=3307 +MYSQL_ROOT_PASSWORD=mysqlpwd +MYSQL_PASSWORD=mysqlpwd +MYSQL_DATABASE=sqlalchemy_study +MYSQL_USER=user diff --git a/sqlalchemy_study/src/data/__init__.py b/sqlalchemy_study/src/data/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sqlalchemy_study/src/data/factories.py b/sqlalchemy_study/src/data/factories.py new file mode 100644 index 0000000..07947f4 --- /dev/null +++ b/sqlalchemy_study/src/data/factories.py @@ -0,0 +1,150 @@ +from datetime import datetime, timedelta +from typing import Optional + +import factory +from factory import fuzzy +from faker import Faker + +from db.dependencies import get_sync_db_session +from db.models.coin import Coin, CoinType +from db.models.department import Department, EmployeeDepartments +from db.models.skills import Skill, EmployeesSkills +from db.models.user import User, Employee + +faker = Faker('ru_RU') + + +Session = get_sync_db_session() + + +class BaseModelFactory(factory.alchemy.SQLAlchemyModelFactory): + class Meta: + abstract = True + sqlalchemy_session_persistence = 'commit' + sqlalchemy_session = Session + + +class UserFactory(BaseModelFactory): + + id = factory.Sequence(lambda n: n + 1) + username = faker.profile(fields=['username'])['username'] + email = factory.Faker('email') + hash_password = factory.Faker('password') + auth_token = factory.Faker('uuid4') + + class Meta: + model = User + sqlalchemy_get_or_create = ( + 'username', + ) + + +class CoinModelFactory(BaseModelFactory): + + id = factory.Sequence(lambda n: n + 1) + name = factory.Faker('cryptocurrency_name') + enabled = fuzzy.FuzzyChoice((0, 1)) + + class Meta: + model = Coin + sqlalchemy_get_or_create = ( + 'name', + ) + + @factory.post_generation + def coin_type(obj, create: bool, extracted: Optional[Coin], *args, **kwargs) -> None: + if create: + CoinTypeFactory.create_batch(faker.random_int(min=3, max=7), coin_id=obj.id) + + +class CoinTypeFactory(BaseModelFactory): + + id = factory.Sequence(lambda n: n + 1) + name = factory.Faker('cryptocurrency_code') + + class Meta: + model = CoinType + sqlalchemy_get_or_create = ('id', + ) + + +class SkillFactory(BaseModelFactory): + + id = factory.Sequence(lambda n: n + 1) + name = factory.Faker('job', locale='ru_ru') + description = factory.Faker('text', max_nb_chars=160, locale='ru_RU') + updated_at = factory.LazyFunction(datetime.now) + + class Meta: + model = Skill + sqlalchemy_get_or_create = ('name', + ) + + +class EmployeeFactory(BaseModelFactory): + + id = factory.Sequence(lambda n: n + 1) + first_name = factory.Faker('first_name', locale='ru_RU') + last_name = factory.Faker('last_name', locale='ru_RU') + phone = factory.Faker('phone_number') + description = factory.Faker('text', max_nb_chars=80, locale='ru_RU') + coin_id = factory.Faker('random_int') + + class Meta: + model = Employee + sqlalchemy_get_or_create = ('id', + ) + + +class EmployeesSkillsFactory(BaseModelFactory): + + id = factory.Sequence(lambda n: n + 1) + employee_id = factory.Faker('random_int') + skill_id = factory.Faker('random_int') + updated_at = factory.Faker( + 'date_time_between_dates', datetime_start=datetime.now() - timedelta(days=30), datetime_end=datetime.now() + ) + + class Meta: + model = EmployeesSkills + sqlalchemy_get_or_create = ( + 'id', + 'employee_id', + 'skill_id' + ) + + +class DepartmentFactory(BaseModelFactory): + + id = factory.Sequence(lambda n: n + 1) + name = factory.Faker('company') + description = factory.Faker('bs') + updated_at = factory.Faker( + 'date_time_between_dates', datetime_start=datetime.now() - timedelta(days=30), datetime_end=datetime.now() + ) + + class Meta: + model = Department + sqlalchemy_get_or_create = ( + 'id', + 'name', + ) + + +class EmployeeDepartmentFactory(BaseModelFactory): + + employee_id = factory.Faker('random_int') + department_id = factory.Faker('random_int') + created_at = factory.Faker( + 'date_time_between_dates', + datetime_start=datetime.now() - timedelta(days=30), + datetime_end=datetime.now() - timedelta(days=10) + ) + updated_at = factory.Faker( + 'date_time_between_dates', + datetime_start=datetime.now() - timedelta(days=10), + datetime_end=datetime.now() + ) + + class Meta: + model = EmployeeDepartments diff --git a/sqlalchemy_study/src/data/fill_data.py b/sqlalchemy_study/src/data/fill_data.py new file mode 100644 index 0000000..38906c6 --- /dev/null +++ b/sqlalchemy_study/src/data/fill_data.py @@ -0,0 +1,84 @@ +import asyncio +import random +import uuid + +from factory import fuzzy +from faker import Faker + +from data.factories import ( + UserFactory, + CoinModelFactory, + EmployeesSkillsFactory, + SkillFactory, + EmployeeFactory, + DepartmentFactory, + EmployeeDepartmentFactory +) +from db.dependencies import get_async_db_session +from db.models.user import User +from db.utils import drop_tables, run_migrations +from settings.logger import logger + +faker = Faker('ru_RU') + + +async def add_users_data() -> None: + + async with get_async_db_session() as session: + users = [] + for _ in range(10): + users.append(User(username=faker.profile(fields=['username'])['username'], + hash_password=faker.password(), + auth_token=str(uuid.uuid4()), + ) + ) + session.add_all(users) + + +def get_random_skill(skills: list[int]) -> list[int]: + random_skills = random.sample(skills, random.randint(2, 9)) + return random_skills + + +def fill_database() -> None: + + # async add faker data + asyncio.run(add_users_data()) + + # sync factory boy add data + coins = [coin.id for coin in CoinModelFactory.create_batch(42)] + + jonny = EmployeeFactory(first_name='Tony', last_name='Stark', coin_id=fuzzy.FuzzyChoice(coins)) + karl = EmployeeFactory(first_name='Karl', coin_id=fuzzy.FuzzyChoice(coins)) + employees = EmployeeFactory.create_batch(40, coin_id=fuzzy.FuzzyChoice(coins)) + + skills = [skill.id for skill in SkillFactory.create_batch(size=faker.random_int(min=20, max=42))] + + for skill in get_random_skill(skills): + EmployeesSkillsFactory(employee_id=jonny.id, skill_id=skill) + + for skill in get_random_skill(skills): + EmployeesSkillsFactory(employee_id=karl.id, skill_id=skill) + + for employee in employees: + for skill in get_random_skill(skills): + EmployeesSkillsFactory(employee_id=employee.id, skill_id=skill) + + # User data (first 20 rows if not exists) + for user_id in range(20, 30): + UserFactory(id=user_id, username=faker.profile(fields=['username'])['username']) + + # Department data + departments = DepartmentFactory.create_batch(5) + departments = [department.id for department in departments] + + for employee in [jonny, karl, *employees]: + EmployeeDepartmentFactory(employee_id=employee.id, department_id=fuzzy.FuzzyChoice(departments)) + + logger.info('All data has been created. You can run data/get_data.py script') + + +if __name__ == '__main__': + drop_tables() + run_migrations() + fill_database() diff --git a/sqlalchemy_study/src/data/get_data.py b/sqlalchemy_study/src/data/get_data.py new file mode 100644 index 0000000..c73f2db --- /dev/null +++ b/sqlalchemy_study/src/data/get_data.py @@ -0,0 +1,66 @@ +import asyncio + +from settings.logger import logger +from sqlalchemy_study.sqlalchemy import select +from sqlalchemy_study.sqlalchemy import load_only, contains_eager, joinedload + +from db.dependencies import get_async_db_session +from db.models.coin import Coin +from db.models.department import EmployeeDepartments, Department +from db.models.skills import Skill +from db.models.user import Employee, User + + +async def get_data() -> list[Employee]: + + query = ( + select(Employee) + .join(Employee.coin).options( + contains_eager(Employee.coin).options(load_only(Coin.name, + Coin.enabled))) + .join(Employee.skills).options( + contains_eager(Employee.skills).load_only(Skill.name) + ).options(load_only(Employee.id, + Employee.first_name, + Employee.phone, + ) + ) + .outerjoin(Employee.department).options( + contains_eager(Employee.department).options( + joinedload(EmployeeDepartments.department) + .options(load_only(Department.name, + Department.description, ) + ) + ) + ) + .outerjoin(Employee.user).options( + contains_eager(Employee.user).options(load_only(User.username, + ) + ) + ) + ).order_by(Employee.id, Skill.name) + + async with get_async_db_session() as session: + result = await session.execute(query) + data = result.unique().scalars().all() + return data + +employees = asyncio.run(get_data()) + + +for employee in employees: + print(''.center(40, '-'), '\nEmployee id: {0}\nFirst name: {1}\nPhone: {2}\nSkills: {3}\n' + 'Coin name: {4}\nCoin enabled: {5}\nDepartment: {6} -> {7}\nUsername: {8}' + .format(employee.id, + employee.first_name, + employee.phone, + ', '.join([skill.name for skill in employee.skills[:5]]), + employee.coin.name, + employee.coin.enabled, + employee.department.department.name, + employee.department.department.description, + employee.user.username if hasattr(employee.user, 'username') else None, + ) + ) + +logger.info(f'Total employees: {len(employees)}') diff --git a/sqlalchemy_study/src/db/base.py b/sqlalchemy_study/src/db/base.py new file mode 100644 index 0000000..6bf5098 --- /dev/null +++ b/sqlalchemy_study/src/db/base.py @@ -0,0 +1,31 @@ +from typing import Any, Tuple, Union, Type + +from sqlalchemy_study.sqlalchemy import Table, Column, Integer, DATETIME, TIMESTAMP, func +from sqlalchemy_study.sqlalchemy import as_declarative + +from db.meta import meta +from settings import settings + +DB_TIME_FORMAT: Type[Union[DATETIME, TIMESTAMP]] = DATETIME if settings.USE_DATABASE == 'mysql' else TIMESTAMP + + +@as_declarative(metadata=meta) +class BaseModel: + """ + BaseModel for all models. + + It has some type definitions to + enhance autocompletion. + """ + + __tablename__: str + __table__: Table + __table_args__: Tuple[Any, ...] + __abstract__ = True + + id = Column(Integer, nullable=False, unique=True, primary_key=True, autoincrement=True) + created_at = Column(DB_TIME_FORMAT, default=func.now(), index=True) + updated_at = Column(DB_TIME_FORMAT, nullable=True) + + def __repr__(self): + return f"<{self.__class__.__name__}(id={self.id!r})>" diff --git a/sqlalchemy_study/src/db/dependencies.py b/sqlalchemy_study/src/db/dependencies.py new file mode 100644 index 0000000..46a8cf9 --- /dev/null +++ b/sqlalchemy_study/src/db/dependencies.py @@ -0,0 +1,57 @@ +from asyncio import current_task +from contextlib import asynccontextmanager +from typing import AsyncGenerator + +from sqlalchemy_study.sqlalchemy import create_engine +from sqlalchemy_study.sqlalchemy import create_async_engine, AsyncSession, async_scoped_session, AsyncEngine +from sqlalchemy_study.sqlalchemy import sessionmaker, Session + +from settings import settings + +async_engine: AsyncEngine = create_async_engine(str(settings.async_db_url), echo=settings.DB_ECHO) +async_session_factory = async_scoped_session( + sessionmaker( + autocommit=False, + autoflush=False, + class_=AsyncSession, + expire_on_commit=False, + bind=async_engine, + ), + scopefunc=current_task, + ) + + +sync_engine = create_engine(settings.sync_db_url, echo=settings.DB_ECHO) +sync_session_factory = sessionmaker(sync_engine) + + +def get_sync_db_session() -> Session: + session: Session = sync_session_factory() + try: + return session + except Exception as err: + session.rollback() + raise err + finally: + session.commit() + session.close() + + +@asynccontextmanager +async def get_async_db_session() -> AsyncGenerator[AsyncSession, None]: + """ + Create and get database session. + + :param request: current request. + :yield: database session. + """ + session = async_session_factory() + try: + yield session + except Exception as err: + await session.rollback() + raise err + finally: + await session.commit() + await session.close() + await async_session_factory.remove() diff --git a/sqlalchemy_study/src/db/meta.py b/sqlalchemy_study/src/db/meta.py new file mode 100644 index 0000000..fd0aa2e --- /dev/null +++ b/sqlalchemy_study/src/db/meta.py @@ -0,0 +1,3 @@ +from sqlalchemy_study import sqlalchemy as sa + +meta = sa.MetaData() diff --git a/sqlalchemy_study/src/db/models/__init__.py b/sqlalchemy_study/src/db/models/__init__.py new file mode 100644 index 0000000..323baa4 --- /dev/null +++ b/sqlalchemy_study/src/db/models/__init__.py @@ -0,0 +1,13 @@ +import pkgutil +from pathlib import Path + + +def load_all_models() -> None: + """Load all models from this folder.""" + root_dir = Path(__file__).resolve().parent + modules = pkgutil.walk_packages( + path=[str(root_dir)], + prefix="db.models.", + ) + for module in modules: + __import__(module.name) diff --git a/sqlalchemy_study/src/db/models/cadre_movements.py b/sqlalchemy_study/src/db/models/cadre_movements.py new file mode 100755 index 0000000..f70db59 --- /dev/null +++ b/sqlalchemy_study/src/db/models/cadre_movements.py @@ -0,0 +1,16 @@ +from sqlalchemy_study.sqlalchemy import Column, Integer, ForeignKey, VARCHAR +from sqlalchemy_study.sqlalchemy import relation + +from db.base import BaseModel +from db.models.department import Department + + +class CadreMovement(BaseModel): + __tablename__ = 'cadre_movements' + + employee = Column(Integer, ForeignKey('employees.id', ondelete='CASCADE'), nullable=False, index=True) + old_department = Column(Integer, ForeignKey('departments.id', ondelete='CASCADE'), nullable=False, index=True) + new_department = Column(Integer, ForeignKey('departments.id', ondelete='CASCADE'), nullable=False, index=True) + reason = Column(VARCHAR(500), nullable=True) + + department = relation(Department, foreign_keys=new_department, lazy='select') diff --git a/sqlalchemy_study/src/db/models/coin.py b/sqlalchemy_study/src/db/models/coin.py new file mode 100644 index 0000000..5cc3def --- /dev/null +++ b/sqlalchemy_study/src/db/models/coin.py @@ -0,0 +1,35 @@ +from sqlalchemy_study.sqlalchemy import VARCHAR +from sqlalchemy_study.sqlalchemy import relationship +from sqlalchemy_study.sqlalchemy import Column +from sqlalchemy_study.sqlalchemy import ForeignKey +from sqlalchemy_study.sqlalchemy import Integer, BOOLEAN + +from db.base import BaseModel + + +class Coin(BaseModel): + """Model for coin.""" + + __tablename__ = "coins" + + name = Column('coin_name', VARCHAR(50), unique=True) + enabled = Column('enabled', BOOLEAN) + + coin_type_id = relationship("CoinType", + primaryjoin="Coin.id == CoinType.coin_id", + back_populates='coin', + uselist=False, + viewonly=True, + lazy="raise", + ) + employee = relationship('Employee', back_populates='coin') + + +class CoinType(BaseModel): + """Model for coin type.""" + + __tablename__ = "coin_types" + + name = Column('coin_name', VARCHAR(50)) + coin_id = Column(Integer, ForeignKey('coins.id', ondelete='CASCADE')) + coin = relationship(Coin, back_populates='coin_type_id') diff --git a/sqlalchemy_study/src/db/models/department.py b/sqlalchemy_study/src/db/models/department.py new file mode 100755 index 0000000..ed08a27 --- /dev/null +++ b/sqlalchemy_study/src/db/models/department.py @@ -0,0 +1,23 @@ +from sqlalchemy_study.sqlalchemy import Column, VARCHAR, Integer, ForeignKey +from sqlalchemy_study.sqlalchemy import relationship + +from db.base import BaseModel + + +class Department(BaseModel): + __tablename__ = 'departments' + + name = Column(VARCHAR(255), nullable=False) + description = Column(VARCHAR(255), nullable=False) + + +class EmployeeDepartments(BaseModel): + __tablename__ = 'employee_departments' + + employee_id = Column(Integer, ForeignKey('employees.id', ondelete='CASCADE'), nullable=False, index=True) + department_id = Column(Integer, ForeignKey('departments.id', ondelete='CASCADE'), nullable=False, index=True) + + department = relationship(Department, + lazy='noload', + backref='emp_depart', + ) diff --git a/sqlalchemy_study/src/db/models/skills.py b/sqlalchemy_study/src/db/models/skills.py new file mode 100644 index 0000000..316bb1a --- /dev/null +++ b/sqlalchemy_study/src/db/models/skills.py @@ -0,0 +1,19 @@ +from sqlalchemy_study.sqlalchemy import Column, ForeignKey, VARCHAR, Text, UniqueConstraint + +from db.base import BaseModel +from db.models.user import Employee + + +class Skill(BaseModel): + __tablename__ = 'skills' + + name = Column(VARCHAR(255), nullable=False, unique=True) + description = Column(Text, nullable=True) + + +class EmployeesSkills(BaseModel): + __tablename__ = 'employees_skills' + __table_args__ = (UniqueConstraint("employee_id", "skill_id"),) + + employee_id = Column(ForeignKey(Employee.id, ondelete='CASCADE'), nullable=False, index=True) + skill_id = Column(ForeignKey(Skill.id, ondelete='CASCADE'), nullable=False, index=True) diff --git a/sqlalchemy_study/src/db/models/user.py b/sqlalchemy_study/src/db/models/user.py new file mode 100644 index 0000000..02dc921 --- /dev/null +++ b/sqlalchemy_study/src/db/models/user.py @@ -0,0 +1,62 @@ +import datetime + +from sqlalchemy_study.sqlalchemy import Column, String, DateTime, ForeignKey +from sqlalchemy_study.sqlalchemy import VARCHAR +from sqlalchemy_study.sqlalchemy import relationship + +from db.base import BaseModel +from db.models.coin import Coin + + +class User(BaseModel): + __tablename__ = 'users' + + username: str = Column(String(255), unique=True) + email: str = Column(String(255), index=True, unique=True, nullable=True) + hash_password: str = Column(String(255)) + auth_token: str = Column(String(255)) + last_login: datetime.datetime = Column(DateTime, default=datetime.datetime.now, index=True) + + def __repr__(self): + return f'User: id:{self.id}, name: {self.username}' + + employee = relationship('Employee', + primaryjoin='foreign(User.id)==remote(Employee.id)', + lazy='noload', + backref='user_employee', + ) + + +class Employee(BaseModel): + __tablename__ = 'employees' + + first_name = Column(VARCHAR(128), nullable=False) + last_name = Column(VARCHAR(128), nullable=False) + phone = Column(VARCHAR(30), unique=True, nullable=True) + description = Column(VARCHAR(255), nullable=True) + coin_id = Column('coin_id', ForeignKey('coins.id', ondelete='SET NULL'), nullable=True) + + coin = relationship(Coin, + back_populates='employee', + primaryjoin='Employee.coin_id==Coin.id', + lazy='noload', + uselist=False, + ) + + skills = relationship('Skill', + secondary="employees_skills", + lazy='noload', + uselist=True, + ) + + department = relationship('EmployeeDepartments', + lazy='noload', + backref='employee', + uselist=False, + ) + + user = relationship('User', + primaryjoin='foreign(Employee.id)==remote(User.id)', + lazy='raise', + backref='user_employee', + ) diff --git a/sqlalchemy_study/src/db/utils.py b/sqlalchemy_study/src/db/utils.py new file mode 100644 index 0000000..c561e6b --- /dev/null +++ b/sqlalchemy_study/src/db/utils.py @@ -0,0 +1,56 @@ +from alembic import command, config as alembic_config +from sqlalchemy_study.sqlalchemy import MetaData, Table, ForeignKeyConstraint +from sqlalchemy_study.sqlalchemy import inspect +from sqlalchemy_study.sqlalchemy import NoSuchTableError +from sqlalchemy_study.sqlalchemy import DropConstraint + +from db.dependencies import sync_engine +from db.meta import meta +from db.models import load_all_models +from settings import settings +from settings.logger import logger + +alembic_cfg = alembic_config.Config("alembic.ini") + + +def remove_foreign_keys() -> None: + logger.info("Dropping all foreign key constraints from archive database") + + inspector = inspect(sync_engine) + fake_metadata = MetaData() + + fake_tables = [] + all_fks = [] + for table_name in meta.tables: + fks = [] + try: + for fk in inspector.get_foreign_keys(table_name): + if fk['name']: + fks.append(ForeignKeyConstraint((), (), name=fk['name'])) + except NoSuchTableError: + logger.error(f'Table {table_name} not exist') + t = Table(table_name, fake_metadata, *fks) + fake_tables.append(t) + all_fks.extend(fks) + connection = sync_engine.connect() + transaction = connection.begin() + for fkc in all_fks: + connection.execute(DropConstraint(fkc)) + transaction.commit() + + +def drop_tables() -> None: + load_all_models() + remove_foreign_keys() + meta.drop_all(bind=sync_engine, checkfirst=True) + sync_engine.execute('DROP TABLE IF EXISTS alembic_version') + sync_engine.dispose() + logger.info("All tables are dropped") + + +def run_migrations() -> None: + with sync_engine.begin() as connection: + alembic_cfg.attributes['connection'] = connection + migration_dialect = 'mysql_init_migrations' if settings.USE_DATABASE == 'mysql' else 'postgres_init_migrations' + command.upgrade(alembic_cfg, migration_dialect) + logger.info('Tables recreated') diff --git a/sqlalchemy_study/src/migrations/README b/sqlalchemy_study/src/migrations/README new file mode 100644 index 0000000..98e4f9c --- /dev/null +++ b/sqlalchemy_study/src/migrations/README @@ -0,0 +1 @@ +Generic single-database configuration. \ No newline at end of file diff --git a/sqlalchemy_study/src/migrations/__init__.py b/sqlalchemy_study/src/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sqlalchemy_study/src/migrations/env.py b/sqlalchemy_study/src/migrations/env.py new file mode 100644 index 0000000..d333b7a --- /dev/null +++ b/sqlalchemy_study/src/migrations/env.py @@ -0,0 +1,73 @@ +import asyncio +from logging.config import fileConfig + +from alembic import context +from sqlalchemy_study.sqlalchemy import create_async_engine +from sqlalchemy_study.sqlalchemy import Connection + +from db.base import BaseModel +from db.models import load_all_models +from settings import settings + +config = context.config + +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +target_metadata = BaseModel.metadata +load_all_models() + + +async def run_migrations_offline(): + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + + context.configure( + url=settings.async_db_url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + ) + + with context.begin_transaction(): + context.run_migrations() + + +def do_run_migrations(connection: Connection) -> None: + """ + Run actual sync migrations. + + :param connection: connection to the database. + """ + context.configure(connection=connection, target_metadata=target_metadata) + + with context.begin_transaction(): + context.run_migrations() + + +async def run_migrations_online(): + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + connectable = create_async_engine(settings.async_db_url) + + async with connectable.connect() as connection: + await connection.run_sync(do_run_migrations) + + +if context.is_offline_mode(): + asyncio.run(run_migrations_offline()) +else: + asyncio.run(run_migrations_online()) diff --git a/sqlalchemy_study/src/migrations/script.py.mako b/sqlalchemy_study/src/migrations/script.py.mako new file mode 100644 index 0000000..2c01563 --- /dev/null +++ b/sqlalchemy_study/src/migrations/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +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)} + + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} diff --git a/sqlalchemy_study/src/migrations/versions/mysql_init_migrations.py b/sqlalchemy_study/src/migrations/versions/mysql_init_migrations.py new file mode 100644 index 0000000..7f025d3 --- /dev/null +++ b/sqlalchemy_study/src/migrations/versions/mysql_init_migrations.py @@ -0,0 +1,174 @@ +"""mysql init models + +Revision ID: mysql_init_migrations +Revises: +Create Date: 2022-05-29 19:26:09.995005 + +""" +from alembic import op +from sqlalchemy_study import sqlalchemy as sa +from sqlalchemy_study.sqlalchemy import mysql + +# revision identifiers, used by Alembic. +revision = 'mysql_init_migrations' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('coins', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('created_at', sa.DATETIME(), nullable=True), + sa.Column('updated_at', sa.DATETIME(), nullable=True), + sa.Column('coin_name', sa.VARCHAR(length=50), nullable=True), + sa.Column('enabled', sa.BOOLEAN(), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('coin_name'), + sa.UniqueConstraint('id') + ) + op.create_index(op.f('ix_coins_created_at'), 'coins', ['created_at'], unique=False) + op.create_table('departments', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('created_at', sa.DATETIME(), nullable=True), + sa.Column('updated_at', sa.DATETIME(), nullable=True), + sa.Column('name', sa.VARCHAR(length=255), nullable=False), + sa.Column('description', sa.VARCHAR(length=255), nullable=False), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('id') + ) + op.create_index(op.f('ix_departments_created_at'), 'departments', ['created_at'], unique=False) + op.create_table('skills', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('created_at', sa.DATETIME(), nullable=True), + sa.Column('updated_at', sa.DATETIME(), nullable=True), + sa.Column('name', sa.VARCHAR(length=255), nullable=False), + sa.Column('description', sa.Text(), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('id'), + sa.UniqueConstraint('name') + ) + op.create_index(op.f('ix_skills_created_at'), 'skills', ['created_at'], unique=False) + op.create_table('users', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('created_at', sa.DATETIME(), nullable=True), + sa.Column('updated_at', sa.DATETIME(), nullable=True), + sa.Column('username', sa.String(length=255), nullable=True), + sa.Column('email', sa.String(length=255), nullable=True), + sa.Column('hash_password', sa.String(length=255), nullable=True), + sa.Column('auth_token', sa.String(length=255), nullable=True), + sa.Column('last_login', sa.DateTime(), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('id'), + sa.UniqueConstraint('username') + ) + op.create_index(op.f('ix_users_created_at'), 'users', ['created_at'], unique=False) + op.create_index(op.f('ix_users_email'), 'users', ['email'], unique=True) + op.create_index(op.f('ix_users_last_login'), 'users', ['last_login'], unique=False) + op.create_table('coin_types', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('created_at', sa.DATETIME(), nullable=True), + sa.Column('updated_at', sa.DATETIME(), nullable=True), + sa.Column('coin_name', sa.VARCHAR(length=50), nullable=True), + sa.Column('coin_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['coin_id'], ['coins.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('id') + ) + op.create_index(op.f('ix_coin_types_created_at'), 'coin_types', ['created_at'], unique=False) + op.create_table('employees', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('created_at', sa.DATETIME(), nullable=True), + sa.Column('updated_at', sa.DATETIME(), nullable=True), + sa.Column('first_name', mysql.VARCHAR(length=128), nullable=False), + sa.Column('last_name', mysql.VARCHAR(length=128), nullable=False), + sa.Column('phone', mysql.VARCHAR(length=30), nullable=True), + sa.Column('description', mysql.VARCHAR(length=255), nullable=True), + sa.Column('coin_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['coin_id'], ['coins.id'], ondelete='SET NULL'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('id'), + sa.UniqueConstraint('phone') + ) + op.create_index(op.f('ix_employees_created_at'), 'employees', ['created_at'], unique=False) + op.create_table('cadre_movements', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('created_at', sa.DATETIME(), nullable=True), + sa.Column('updated_at', sa.DATETIME(), nullable=True), + sa.Column('employee', sa.Integer(), nullable=False), + sa.Column('old_department', sa.Integer(), nullable=False), + sa.Column('new_department', sa.Integer(), nullable=False), + sa.Column('reason', sa.VARCHAR(length=500), nullable=True), + sa.ForeignKeyConstraint(['employee'], ['employees.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['new_department'], ['departments.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['old_department'], ['departments.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('id') + ) + op.create_index(op.f('ix_cadre_movements_created_at'), 'cadre_movements', ['created_at'], unique=False) + op.create_index(op.f('ix_cadre_movements_employee'), 'cadre_movements', ['employee'], unique=False) + op.create_index(op.f('ix_cadre_movements_new_department'), 'cadre_movements', ['new_department'], unique=False) + op.create_index(op.f('ix_cadre_movements_old_department'), 'cadre_movements', ['old_department'], unique=False) + op.create_table('employee_departments', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('created_at', sa.DATETIME(), nullable=True), + sa.Column('updated_at', sa.DATETIME(), nullable=True), + sa.Column('employee_id', sa.Integer(), nullable=False), + sa.Column('department_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['department_id'], ['departments.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['employee_id'], ['employees.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('id') + ) + op.create_index(op.f('ix_employee_departments_created_at'), 'employee_departments', ['created_at'], unique=False) + op.create_index(op.f('ix_employee_departments_department_id'), 'employee_departments', ['department_id'], unique=False) + op.create_index(op.f('ix_employee_departments_employee_id'), 'employee_departments', ['employee_id'], unique=False) + op.create_table('employees_skills', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('created_at', sa.DATETIME(), nullable=True), + sa.Column('updated_at', sa.DATETIME(), nullable=True), + sa.Column('employee_id', sa.Integer(), nullable=False), + sa.Column('skill_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['employee_id'], ['employees.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['skill_id'], ['skills.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('employee_id', 'skill_id'), + sa.UniqueConstraint('id') + ) + op.create_index(op.f('ix_employees_skills_created_at'), 'employees_skills', ['created_at'], unique=False) + op.create_index(op.f('ix_employees_skills_employee_id'), 'employees_skills', ['employee_id'], unique=False) + op.create_index(op.f('ix_employees_skills_skill_id'), 'employees_skills', ['skill_id'], unique=False) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f('ix_employees_skills_skill_id'), table_name='employees_skills') + op.drop_index(op.f('ix_employees_skills_employee_id'), table_name='employees_skills') + op.drop_index(op.f('ix_employees_skills_created_at'), table_name='employees_skills') + op.drop_table('employees_skills') + op.drop_index(op.f('ix_employee_departments_employee_id'), table_name='employee_departments') + op.drop_index(op.f('ix_employee_departments_department_id'), table_name='employee_departments') + op.drop_index(op.f('ix_employee_departments_created_at'), table_name='employee_departments') + op.drop_table('employee_departments') + op.drop_index(op.f('ix_cadre_movements_old_department'), table_name='cadre_movements') + op.drop_index(op.f('ix_cadre_movements_new_department'), table_name='cadre_movements') + op.drop_index(op.f('ix_cadre_movements_employee'), table_name='cadre_movements') + op.drop_index(op.f('ix_cadre_movements_created_at'), table_name='cadre_movements') + op.drop_table('cadre_movements') + op.drop_index(op.f('ix_employees_created_at'), table_name='employees') + op.drop_table('employees') + op.drop_index(op.f('ix_coin_types_created_at'), table_name='coin_types') + op.drop_table('coin_types') + op.drop_index(op.f('ix_users_last_login'), table_name='users') + op.drop_index(op.f('ix_users_email'), table_name='users') + op.drop_index(op.f('ix_users_created_at'), table_name='users') + op.drop_table('users') + op.drop_index(op.f('ix_skills_created_at'), table_name='skills') + op.drop_table('skills') + op.drop_index(op.f('ix_departments_created_at'), table_name='departments') + op.drop_table('departments') + op.drop_index(op.f('ix_coins_created_at'), table_name='coins') + op.drop_table('coins') + # ### end Alembic commands ### diff --git a/sqlalchemy_study/src/migrations/versions/postgres_init_migrations.py b/sqlalchemy_study/src/migrations/versions/postgres_init_migrations.py new file mode 100644 index 0000000..0952580 --- /dev/null +++ b/sqlalchemy_study/src/migrations/versions/postgres_init_migrations.py @@ -0,0 +1,174 @@ +"""postgres init migrations + +Revision ID: postgres_init_migrations +Revises: +Create Date: 2022-06-14 00:29:28.932954 + +""" +from alembic import op +from sqlalchemy_study import sqlalchemy as sa +from sqlalchemy_study.sqlalchemy import mysql + +# revision identifiers, used by Alembic. +revision = 'postgres_init_migrations' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('coins', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('created_at', sa.TIMESTAMP(), nullable=True), + sa.Column('updated_at', sa.TIMESTAMP(), nullable=True), + sa.Column('coin_name', sa.VARCHAR(length=50), nullable=True), + sa.Column('enabled', sa.BOOLEAN(), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('coin_name'), + sa.UniqueConstraint('id') + ) + op.create_index(op.f('ix_coins_created_at'), 'coins', ['created_at'], unique=False) + op.create_table('departments', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('created_at', sa.TIMESTAMP(), nullable=True), + sa.Column('updated_at', sa.TIMESTAMP(), nullable=True), + sa.Column('name', sa.VARCHAR(length=255), nullable=False), + sa.Column('description', sa.VARCHAR(length=255), nullable=False), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('id') + ) + op.create_index(op.f('ix_departments_created_at'), 'departments', ['created_at'], unique=False) + op.create_table('skills', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('created_at', sa.TIMESTAMP(), nullable=True), + sa.Column('updated_at', sa.TIMESTAMP(), nullable=True), + sa.Column('name', sa.VARCHAR(length=255), nullable=False), + sa.Column('description', sa.Text(), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('id'), + sa.UniqueConstraint('name') + ) + op.create_index(op.f('ix_skills_created_at'), 'skills', ['created_at'], unique=False) + op.create_table('users', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('created_at', sa.TIMESTAMP(), nullable=True), + sa.Column('updated_at', sa.TIMESTAMP(), nullable=True), + sa.Column('username', sa.String(length=255), nullable=True), + sa.Column('email', sa.String(length=255), nullable=True), + sa.Column('hash_password', sa.String(length=255), nullable=True), + sa.Column('auth_token', sa.String(length=255), nullable=True), + sa.Column('last_login', sa.DateTime(), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('id'), + sa.UniqueConstraint('username') + ) + op.create_index(op.f('ix_users_created_at'), 'users', ['created_at'], unique=False) + op.create_index(op.f('ix_users_email'), 'users', ['email'], unique=True) + op.create_index(op.f('ix_users_last_login'), 'users', ['last_login'], unique=False) + op.create_table('coin_types', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('created_at', sa.TIMESTAMP(), nullable=True), + sa.Column('updated_at', sa.TIMESTAMP(), nullable=True), + sa.Column('coin_name', sa.VARCHAR(length=50), nullable=True), + sa.Column('coin_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['coin_id'], ['coins.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('id') + ) + op.create_index(op.f('ix_coin_types_created_at'), 'coin_types', ['created_at'], unique=False) + op.create_table('employees', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('created_at', sa.TIMESTAMP(), nullable=True), + sa.Column('updated_at', sa.TIMESTAMP(), nullable=True), + sa.Column('first_name', mysql.VARCHAR(length=128), nullable=False), + sa.Column('last_name', mysql.VARCHAR(length=128), nullable=False), + sa.Column('phone', mysql.VARCHAR(length=30), nullable=True), + sa.Column('description', mysql.VARCHAR(length=255), nullable=True), + sa.Column('coin_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['coin_id'], ['coins.id'], ondelete='SET NULL'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('id'), + sa.UniqueConstraint('phone') + ) + op.create_index(op.f('ix_employees_created_at'), 'employees', ['created_at'], unique=False) + op.create_table('cadre_movements', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('created_at', sa.TIMESTAMP(), nullable=True), + sa.Column('updated_at', sa.TIMESTAMP(), nullable=True), + sa.Column('employee', sa.Integer(), nullable=False), + sa.Column('old_department', sa.Integer(), nullable=False), + sa.Column('new_department', sa.Integer(), nullable=False), + sa.Column('reason', sa.VARCHAR(length=500), nullable=True), + sa.ForeignKeyConstraint(['employee'], ['employees.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['new_department'], ['departments.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['old_department'], ['departments.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('id') + ) + op.create_index(op.f('ix_cadre_movements_created_at'), 'cadre_movements', ['created_at'], unique=False) + op.create_index(op.f('ix_cadre_movements_employee'), 'cadre_movements', ['employee'], unique=False) + op.create_index(op.f('ix_cadre_movements_new_department'), 'cadre_movements', ['new_department'], unique=False) + op.create_index(op.f('ix_cadre_movements_old_department'), 'cadre_movements', ['old_department'], unique=False) + op.create_table('employee_departments', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('created_at', sa.TIMESTAMP(), nullable=True), + sa.Column('updated_at', sa.TIMESTAMP(), nullable=True), + sa.Column('employee_id', sa.Integer(), nullable=False), + sa.Column('department_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['department_id'], ['departments.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['employee_id'], ['employees.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('id') + ) + op.create_index(op.f('ix_employee_departments_created_at'), 'employee_departments', ['created_at'], unique=False) + op.create_index(op.f('ix_employee_departments_department_id'), 'employee_departments', ['department_id'], unique=False) + op.create_index(op.f('ix_employee_departments_employee_id'), 'employee_departments', ['employee_id'], unique=False) + op.create_table('employees_skills', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('created_at', sa.TIMESTAMP(), nullable=True), + sa.Column('updated_at', sa.TIMESTAMP(), nullable=True), + sa.Column('employee_id', sa.Integer(), nullable=False), + sa.Column('skill_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['employee_id'], ['employees.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['skill_id'], ['skills.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('employee_id', 'skill_id'), + sa.UniqueConstraint('id') + ) + op.create_index(op.f('ix_employees_skills_created_at'), 'employees_skills', ['created_at'], unique=False) + op.create_index(op.f('ix_employees_skills_employee_id'), 'employees_skills', ['employee_id'], unique=False) + op.create_index(op.f('ix_employees_skills_skill_id'), 'employees_skills', ['skill_id'], unique=False) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f('ix_employees_skills_skill_id'), table_name='employees_skills') + op.drop_index(op.f('ix_employees_skills_employee_id'), table_name='employees_skills') + op.drop_index(op.f('ix_employees_skills_created_at'), table_name='employees_skills') + op.drop_table('employees_skills') + op.drop_index(op.f('ix_employee_departments_employee_id'), table_name='employee_departments') + op.drop_index(op.f('ix_employee_departments_department_id'), table_name='employee_departments') + op.drop_index(op.f('ix_employee_departments_created_at'), table_name='employee_departments') + op.drop_table('employee_departments') + op.drop_index(op.f('ix_cadre_movements_old_department'), table_name='cadre_movements') + op.drop_index(op.f('ix_cadre_movements_new_department'), table_name='cadre_movements') + op.drop_index(op.f('ix_cadre_movements_employee'), table_name='cadre_movements') + op.drop_index(op.f('ix_cadre_movements_created_at'), table_name='cadre_movements') + op.drop_table('cadre_movements') + op.drop_index(op.f('ix_employees_created_at'), table_name='employees') + op.drop_table('employees') + op.drop_index(op.f('ix_coin_types_created_at'), table_name='coin_types') + op.drop_table('coin_types') + op.drop_index(op.f('ix_users_last_login'), table_name='users') + op.drop_index(op.f('ix_users_email'), table_name='users') + op.drop_index(op.f('ix_users_created_at'), table_name='users') + op.drop_table('users') + op.drop_index(op.f('ix_skills_created_at'), table_name='skills') + op.drop_table('skills') + op.drop_index(op.f('ix_departments_created_at'), table_name='departments') + op.drop_table('departments') + op.drop_index(op.f('ix_coins_created_at'), table_name='coins') + op.drop_table('coins') + # ### end Alembic commands ### diff --git a/sqlalchemy_study/src/settings/__init__.py b/sqlalchemy_study/src/settings/__init__.py new file mode 100644 index 0000000..41b7675 --- /dev/null +++ b/sqlalchemy_study/src/settings/__init__.py @@ -0,0 +1,4 @@ +from settings.settings import Settings + + +settings = Settings() \ No newline at end of file diff --git a/sqlalchemy_study/src/settings/logger.py b/sqlalchemy_study/src/settings/logger.py new file mode 100644 index 0000000..d1116de --- /dev/null +++ b/sqlalchemy_study/src/settings/logger.py @@ -0,0 +1,11 @@ +import logging +import sys + +from loguru import logger + +logger.remove() + +formatter = "{time} | {level} | {message}" +sink = sys.stdout + +logger.add(sink=sink, colorize=True, level=logging.INFO, format=formatter) diff --git a/sqlalchemy_study/src/settings/settings.py b/sqlalchemy_study/src/settings/settings.py new file mode 100644 index 0000000..55b8a2d --- /dev/null +++ b/sqlalchemy_study/src/settings/settings.py @@ -0,0 +1,69 @@ +import os +from pathlib import Path + +from pydantic import BaseSettings + +BASE_DIR = Path(__file__).parent.parent + +SHARED_DIR = BASE_DIR.resolve().joinpath('shared') +SHARED_DIR.joinpath('logs').mkdir(exist_ok=True) +DIR_LOGS = SHARED_DIR.joinpath('logs') + + +class Settings(BaseSettings): + """Application settings.""" + + DB_HOST: str = 'db_host' + USE_DATABASE: str = 'mysql' + DB_ECHO: bool = False + + # Postgres + POSTGRES_DB_PORT: int + POSTGRES_DB: str + POSTGRES_USER: str + POSTGRES_PASSWORD: str + + MYSQL_DB_PORT: int + MYSQL_DATABASE: str + MYSQL_USER: str + MYSQL_PASSWORD: str + + @property + def async_db_url(self) -> str: + """ + Assemble database URL from settings. + + :return: database URL. + """ + async_postgres_url = (f'postgresql+asyncpg://{self.POSTGRES_USER}:{self.POSTGRES_PASSWORD}@' + f'{self.DB_HOST}:{self.POSTGRES_DB_PORT}/{self.POSTGRES_DB}' + ) + + async_mysql_url = (f'mysql+asyncmy://{self.MYSQL_USER}:{self.MYSQL_PASSWORD}@' + f'{self.DB_HOST}:{self.MYSQL_DB_PORT}/{self.MYSQL_DATABASE}' + ) + if os.environ.get('USE_DATABASE', self.USE_DATABASE).lower() == 'postgres': + return async_postgres_url + return async_mysql_url + + @property + def sync_db_url(self) -> str: + """ + Assemble database URL from settings. + + :return: database URL. + """ + sync_postgres_url = (f'postgresql://{self.POSTGRES_USER}:{self.POSTGRES_PASSWORD}@' + f'{self.DB_HOST}:{self.POSTGRES_DB_PORT}/{self.POSTGRES_DB}' + ) + + sync_mysql_url = (f'mysql+pymysql://{self.MYSQL_USER}:{self.MYSQL_PASSWORD}@' + f'{self.DB_HOST}:{self.MYSQL_DB_PORT}/{self.MYSQL_DATABASE}' + ) + if os.environ.get('USE_DATABASE', self.USE_DATABASE).lower() == 'postgres': + return sync_postgres_url + return sync_mysql_url + + class Config: + env_file = 'config/.env' + env_file_encoding = "utf-8" diff --git a/twitch_bonus.py b/twitch_bonus.py index 658325b..0eb4d73 100644 --- a/twitch_bonus.py +++ b/twitch_bonus.py @@ -1,13 +1,21 @@ +import argparse import atexit import os import sys +import tarfile import time +from pathlib import Path +from typing import Optional +import validators +import wget from loguru import logger from selenium import webdriver -from selenium.common.exceptions import NoSuchElementException +from selenium.common.exceptions import NoSuchElementException, ElementClickInterceptedException +from selenium.webdriver.common.keys import Keys from selenium.webdriver.firefox import options from selenium.webdriver.firefox.service import Service +from selenium.webdriver.firefox.webdriver import WebDriver from urllib3.exceptions import MaxRetryError logger.remove() @@ -15,10 +23,50 @@ logger.add(sink=sys.stdout, colorize=True, level='DEBUG', format="{time:DD.MM.YYYY HH:mm:ss} | {level} | " "{message}") -opt = options.Options() -opt.headless = False -service = Service(executable_path=r'./geckodriver') -driver = webdriver.Firefox(service=service, options=opt) + +GECKO_DRIVER_VERSION = '0.31.0' +BASE_DIR = Path(__file__).parent.resolve().as_posix() + +TWITCH_USERNAME = os.environ.get('TWITCH_USERNAME') +TWITCH_PASSWORD = os.environ.get('TWITCH_PASSWORD') +if not all([TWITCH_USERNAME, TWITCH_PASSWORD]): + raise Exception('Username and password must be set') + + +def download_gecko_driver(): + logger.info(f'Downloading gecodriver v {GECKO_DRIVER_VERSION}...') + + gecko_driver = f'https://github.com/mozilla/geckodriver/releases/download/v{GECKO_DRIVER_VERSION}/' \ + f'geckodriver-v{GECKO_DRIVER_VERSION}-linux64.tar.gz' + + geckodriver_file = wget.download(url=gecko_driver, out=BASE_DIR) + + with tarfile.open(geckodriver_file) as tar: + tar.extractall(BASE_DIR) + os.remove(f'{BASE_DIR}/geckodriver-v{GECKO_DRIVER_VERSION}-linux64.tar.gz') + print(f'\ngeckodriver has been downloaded to folder {BASE_DIR}') + + +def configure_firefox_driver(private_window: bool = False) -> WebDriver: + opt = options.Options() + opt.headless = False + opt.add_argument('-profile') + opt.add_argument(f'{Path.home()}/snap/firefox/common/.mozilla/firefox') + if private_window: + opt.set_preference("browser.privatebrowsing.autostart", True) + service = Service(executable_path=f'{BASE_DIR}/geckodriver') + firefox_driver = webdriver.Firefox(service=service, options=opt) + + return firefox_driver + + +def validate_stream_url(twitch_url: str) -> Optional[str]: + + twitch_url_valid = validators.url(twitch_url) + if twitch_url_valid is not True: + logger.error(f'Url {twitch_url} is invalid. Please provide correct one.') + sys.exit(1) + return twitch_url class UserExitException(Exception): @@ -29,20 +77,45 @@ def exit_log(message: str): try: logger.info(message) driver.close() + os.remove(f'{os.getcwd()}/geckodriver.log') sys.exit(0) except MaxRetryError: + pass except SystemExit: os.abort() -if __name__ == '__main__': +def main(twitch_url: str): try: try: - driver.get("https://www.twitch.tv/lol4to22") - logger.info('you have 60 seconds to login') - time.sleep(60) - logger.info('time for login is up') + driver.get(twitch_url) + time.sleep(4) + try: + elem = driver.find_element(by='css selector', value='[data-a-target="login-button"]') + elem.click() + logger.info('you have 60 seconds to login') + time.sleep(2) + login = driver.find_element(by='css selector', value='[aria-label="Enter your username"]') + login.clear() + login.send_keys(f'{TWITCH_USERNAME}') + password = driver.find_element(by='css selector', value='[aria-label="Enter your password"]') + password.clear() + password.send_keys(f'{TWITCH_PASSWORD}') + time.sleep(1) + password.send_keys(Keys.ENTER) + time.sleep(53) + logger.info('time for login is up') + except NoSuchElementException: + logger.info('Login button not found. Probably you are already logged in') + try: + security_button = driver.find_element( + by='css selector', + value='[data-a-target="account-checkup-generic-modal-secondary-button"]' + ) + security_button.click() + except NoSuchElementException: + logger.info('Security button not found, continue...') except Exception as e: logger.error(f'Open page exception: {e}') @@ -57,7 +130,32 @@ if __name__ == '__main__': time.sleep(60 * 15 - 2) except NoSuchElementException: time.sleep(1) + except ElementClickInterceptedException: + logger.error('Security button must be clicked') + time.sleep(15 * 60) except UserExitException: break + except KeyboardInterrupt as e: atexit.register(exit_log, 'Exit script') + + +if __name__ == '__main__': + + parser = argparse.ArgumentParser('Twitch clicker', add_help=True) + parser.add_argument('-u', '--twitch_url', required=False, default='https://www.twitch.tv/lol4to22', + help='Please provide twitch stream url') + + args = parser.parse_args(sys.argv[1:]) + + url = 'https://www.twitch.tv/lol4to22' + + stream_url = args.twitch_url + if stream_url: + url = validate_stream_url(stream_url) + logger.info(f'Stream url is: {url}') + + download_gecko_driver() + driver = configure_firefox_driver() + + main(url)