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)