├── src ├── k8s │ ├── __init__.py │ ├── __pycache__ │ │ ├── node.cpython-36.pyc │ │ ├── __init__.cpython-36.pyc │ │ ├── my_kubernetes.cpython-36.pyc │ │ ├── node_controller.cpython-36.pyc │ │ └── kubernetes_controller.cpython-36.pyc │ ├── my_kubernetes.py │ ├── my_node.py │ └── my_pod.py ├── main.py ├── my_etcd.py └── controller.py ├── project.yml ├── README.md ├── Makefile ├── Pipfile ├── Dockerfile ├── docker-build.sh ├── LICENSE └── Pipfile.lock /src/k8s/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/k8s/__pycache__/node.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/latonaio/kube-etcd-sentinel/HEAD/src/k8s/__pycache__/node.cpython-36.pyc -------------------------------------------------------------------------------- /src/k8s/__pycache__/__init__.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/latonaio/kube-etcd-sentinel/HEAD/src/k8s/__pycache__/__init__.cpython-36.pyc -------------------------------------------------------------------------------- /src/k8s/__pycache__/my_kubernetes.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/latonaio/kube-etcd-sentinel/HEAD/src/k8s/__pycache__/my_kubernetes.cpython-36.pyc -------------------------------------------------------------------------------- /src/k8s/__pycache__/node_controller.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/latonaio/kube-etcd-sentinel/HEAD/src/k8s/__pycache__/node_controller.cpython-36.pyc -------------------------------------------------------------------------------- /src/k8s/__pycache__/kubernetes_controller.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/latonaio/kube-etcd-sentinel/HEAD/src/k8s/__pycache__/kubernetes_controller.cpython-36.pyc -------------------------------------------------------------------------------- /project.yml: -------------------------------------------------------------------------------- 1 | microservices: 2 | kube-etcd-sentinel: 3 | startup: yes 4 | always: yes 5 | withoutKanban: yes 6 | serviceAccount: controller-serviceaccount 7 | env: 8 | MY_NODE_NAME: 自身のデバイス名 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Dependencies 2 | 3 | - pipenv 4 | 5 | ## Setup 6 | 7 | 以下コマンドを実行してください 8 | 9 | ``` 10 | $ kubectl label no 自身のデバイス名 projectsymbol=PRJ 11 | $ kubectl label no 自身のデバイス名 devicename=自身のデバイス名 12 | ``` 13 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | docker-build: 2 | bash ./docker-build.sh 3 | 4 | docker-push: 5 | bash ./docker-build.sh push 6 | 7 | install: 8 | pipenv install --skip-lock 9 | pip install git+ssh://git@bitbucket.org/latonaio/python-base-images.git -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [scripts] 7 | list = "pip list" 8 | start = "python -u src/main.py" 9 | 10 | [dev-packages] 11 | flake8 = "*" 12 | pylint = "*" 13 | 14 | [packages] 15 | kubernetes = "==11.0" 16 | protobuf = "==3.12.2" 17 | etcd3 = "==0.12.0" 18 | 19 | [requires] 20 | python_version = "3.7" 21 | -------------------------------------------------------------------------------- /src/k8s/my_kubernetes.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from kubernetes import client 4 | 5 | 6 | class MyKubernetes(): 7 | project_symbol = os.environ.get("PROJECT_SYNMOL", "prj") 8 | 9 | def __init__(self, kube_client: client.CoreV1Api, node_name, datetime_fmt): 10 | self.kube_client = kube_client 11 | self.node_name = node_name 12 | self.datetime_fmt = datetime_fmt 13 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax = docker/dockerfile:experimental 2 | FROM python:3.7-buster as builder 3 | 4 | # Install PyPI packages 5 | COPY Pipfile Pipfile.lock ./ 6 | RUN pip install pipenv && \ 7 | pipenv install --system 8 | 9 | FROM latonaio/pylib-lite:latest as runner 10 | 11 | COPY --from=builder /usr/local/lib/python3.7/site-packages /usr/local/lib/python3.7/site-packages 12 | 13 | ADD src/ . 14 | 15 | CMD ["python3", "-u", "main.py"] 16 | -------------------------------------------------------------------------------- /src/main.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | 4 | from aion.logger import initialize_logger 5 | from controller import Controller 6 | 7 | SERVICE_NAME = os.environ.get("SERVICE_NAME") 8 | KUBERNETES_PKG = "kubernetes" 9 | 10 | 11 | def main(): 12 | initialize_logger(SERVICE_NAME) 13 | logging.getLogger(KUBERNETES_PKG).setLevel(logging.ERROR) 14 | 15 | controller = Controller() 16 | controller.start() 17 | 18 | 19 | if __name__ == "__main__": 20 | main() 21 | -------------------------------------------------------------------------------- /docker-build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | PUSH=$1 4 | DATE="$(date "+%Y%m%d%H%M")" 5 | REPOSITORY_PREFIX="latonaio" 6 | SERVICE_NAME="kube-etcd-sentinel" 7 | 8 | DOCKER_BUILDKIT=1 docker build --progress=plain -t ${REPOSITORY_PREFIX}/${SERVICE_NAME}:"${DATE}" . 9 | # tagging 10 | docker tag ${REPOSITORY_PREFIX}/${SERVICE_NAME}:"${DATE}" ${REPOSITORY_PREFIX}/${SERVICE_NAME}:latest 11 | 12 | if [[ $PUSH == "push" ]]; then 13 | docker push ${REPOSITORY_PREFIX}/${SERVICE_NAME}:"${DATE}" 14 | docker push ${REPOSITORY_PREFIX}/${SERVICE_NAME}:latest 15 | fi 16 | -------------------------------------------------------------------------------- /src/k8s/my_node.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime as dt 2 | from kubernetes import client 3 | from k8s.my_kubernetes import MyKubernetes 4 | 5 | 6 | class MyNode(MyKubernetes): 7 | def __init__(self, kube_client: client.CoreV1Api, node_name, datetime_fmt): 8 | super().__init__(kube_client, node_name, datetime_fmt) 9 | self._own = {} 10 | 11 | def fetch(self): 12 | my_node = self.kube_client.read_node(self.node_name) 13 | self.own = { 14 | "deviceIp": my_node.status.addresses[0].address, 15 | "deviceName": self.node_name, 16 | "projectSymbolFk": self.project_symbol, 17 | "os": my_node.status.node_info.os_image, 18 | "connectionStatus": "0", # only true. if host is down, we can't get the host info 19 | "updateAt": dt.now().strftime(self.datetime_fmt), 20 | } 21 | 22 | @property 23 | def own(self): 24 | return self._own 25 | 26 | @own.setter 27 | def own(self, own): 28 | self._own = own 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Latona, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/k8s/my_pod.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime as dt 2 | from kubernetes import client 3 | from k8s.my_kubernetes import MyKubernetes 4 | 5 | 6 | class MyPod(MyKubernetes): 7 | def __init__(self, kube_client: client.CoreV1Api, node_name, datetime_fmt): 8 | super().__init__(kube_client, node_name, datetime_fmt) 9 | self.pod_names = [] 10 | self.current_index = 0 11 | self._fetch_names() 12 | 13 | def __iter__(self): 14 | return self 15 | 16 | def __next__(self): 17 | if self.current_index >= len(self.pod_names): 18 | raise StopIteration 19 | 20 | self.name = self.pod_names[self.current_index] 21 | pod = self.kube_client.read_namespaced_pod(self.name, self.project_symbol) 22 | self.current_index = self.current_index + 1 23 | 24 | # remove additional string from docker registry 25 | image_name = pod.spec.containers[0].image.split('/')[-1].split(':')[0] 26 | 27 | return { 28 | "podName": self.name, 29 | "imageName": image_name, 30 | "deviceNameFk": self.node_name, 31 | "deployedAt": pod.status.start_time.strftime(self.datetime_fmt), 32 | "currentVersion": "1.00", 33 | "latestVersion": "2.00", 34 | "status": "0", # always true 35 | "updateAt": dt.now().strftime(self.datetime_fmt), 36 | } 37 | 38 | def _fetch_names(self): 39 | for pod in self.kube_client.list_namespaced_pod(self.project_symbol).items: 40 | self.pod_names.append(pod.metadata.name) 41 | -------------------------------------------------------------------------------- /src/my_etcd.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import etcd3 4 | 5 | from datetime import datetime as dt, timedelta 6 | from aion.logger import lprint 7 | 8 | 9 | class MyEtcd(): 10 | default_etcd_host = "titaniadb" 11 | default_etcd_port = "2379" 12 | alive_status_index = "0" 13 | dead_status_index = "1" 14 | 15 | def __init__(self): 16 | self.host = os.environ.get("ETCD_HOST", self.default_etcd_host) 17 | self.port = int(os.environ.get("ETCD_PORT", self.default_etcd_port)) 18 | self._connect() 19 | 20 | def get_alive_my_pods(self, device_name): 21 | prefix_key = "/Pod/" + device_name + "/" + self.alive_status_index 22 | return self.client.get_prefix(prefix_key) 23 | 24 | def get_disabled_resources(self): 25 | disabled_resources = [] 26 | for kv_value, kv_metadata in list(self.client.get_prefix("/")): 27 | key = kv_metadata.key.decode('utf-8') 28 | if "/" + self.dead_status_index in key: 29 | update_at = json.loads(kv_value.decode('utf-8'))["updateAt"] 30 | disabled_resources.append((key, update_at)) 31 | return disabled_resources 32 | 33 | def add_device(self, device_name, my_node): 34 | key = "/Device/" + device_name + "/" + self.alive_status_index 35 | if self._is_alive(key) is None: 36 | self.client.put(key, json.dumps(my_node)) 37 | lprint(f"success to add device: {key}") 38 | 39 | def add_pod(self, device_name, pod_name, my_pod): 40 | key = "/Pod/" + device_name + "/" + self.alive_status_index + "/" + pod_name 41 | if self._is_alive(key) is None: 42 | self.client.put(key, json.dumps(my_pod)) 43 | lprint(f"success to add pod: {key}") 44 | 45 | def disable_pod(self, device_name, pod_name, my_pod, datetime_fmt): 46 | before_key = "/Pod/" + device_name + "/" + self.alive_status_index + "/" + pod_name 47 | self.client.delete(before_key) 48 | 49 | # change status 0 -> 1 50 | my_pod["status"] = self.dead_status_index 51 | my_pod["updateAt"] = dt.now().strftime(datetime_fmt) 52 | after_key = "/Pod/" + device_name + "/" + self.dead_status_index + "/" + pod_name 53 | self.client.put(after_key, json.dumps(my_pod)) 54 | lprint(f"disabled status: {before_key} -> {after_key}") 55 | 56 | def delete_disabled_resource(self, update_at: timedelta, delete_moratorium: timedelta, key): 57 | if (dt.now() - update_at) > delete_moratorium: 58 | self.client.delete(key) 59 | lprint(f"success to delete {key}") 60 | 61 | def _connect(self): 62 | self.client = etcd3.client(host=self.host, port=self.port) 63 | 64 | def _is_alive(self, key): 65 | value, _ = self.client.get(key) 66 | return value 67 | -------------------------------------------------------------------------------- /src/controller.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import threading 4 | 5 | from time import sleep 6 | from datetime import datetime as dt, timedelta 7 | from kubernetes import client, config 8 | from aion.logger import lprint 9 | from k8s.my_node import MyNode 10 | from k8s.my_pod import MyPod 11 | from my_etcd import MyEtcd 12 | 13 | 14 | class Controller(): 15 | add_pod_interval = 5 16 | add_device_interval = 30 17 | disable_interval = 10 18 | delete_interval = 10 19 | datetime_fmt = "%Y-%m-%d %H:%M:%S" 20 | 21 | def __init__(self): 22 | self.controller_list = [] 23 | self.node_name = os.environ.get("MY_NODE_NAME") 24 | self._set_kube_client() 25 | 26 | def start(self): 27 | try: 28 | my_etcd = MyEtcd() 29 | threading.Thread(target=self._add_device, args=(my_etcd,)).start() 30 | threading.Thread(target=self._add_pods, args=(my_etcd,)).start() 31 | threading.Thread(target=self._disable_pods, args=(my_etcd,)).start() 32 | threading.Thread(target=self._delete_disabled_resources, args=(my_etcd,)).start() 33 | 34 | except Exception as e: 35 | lprint(f"can't connect with etcd. {e}") 36 | 37 | def _set_kube_client(self): 38 | if os.path.exists(os.environ.get("HOME") + "/.kube/config"): 39 | config.load_kube_config() 40 | self.kube_client = client.CoreV1Api() 41 | else: 42 | self.kube_client = client.CoreV1Api(client.ApiClient(self.kube_config)) 43 | 44 | def _add_device(self, my_etcd: MyEtcd): 45 | lprint("start add_device thread...") 46 | while True: 47 | try: 48 | my_node = MyNode(self.kube_client, self.node_name, self.datetime_fmt) 49 | my_node.fetch() 50 | my_etcd.add_device(my_node.own["deviceName"], my_node.own) 51 | 52 | except Exception as e: 53 | lprint(f"can't add_device. {e}") 54 | 55 | finally: 56 | sleep(self.add_device_interval) 57 | 58 | def _add_pods(self, my_etcd: MyEtcd): 59 | lprint("start add_pods thread...") 60 | while True: 61 | try: 62 | for p in MyPod(self.kube_client, self.node_name, self.datetime_fmt): 63 | my_etcd.add_pod(p["deviceNameFk"], p["podName"], p) 64 | 65 | except Exception as e: 66 | lprint(f"failed to add_pods. {e}") 67 | 68 | finally: 69 | sleep(self.add_pod_interval) 70 | 71 | def _disable_pods(self, my_etcd: MyEtcd): 72 | lprint("start disable_pods thread...") 73 | while True: 74 | try: 75 | for pod_on_etcd, _ in my_etcd.get_alive_my_pods(self.node_name): 76 | _pod_on_etcd = json.loads(pod_on_etcd.decode("utf-8")) 77 | pod_name_on_etcd = _pod_on_etcd["podName"] 78 | 79 | is_pod = False 80 | for pod_on_kube in MyPod(self.kube_client, self.node_name, self.datetime_fmt): 81 | if pod_name_on_etcd == pod_on_kube["podName"]: 82 | is_pod = True 83 | break 84 | 85 | if is_pod is False: 86 | my_etcd.disable_pod(_pod_on_etcd["deviceNameFk"], pod_name_on_etcd, _pod_on_etcd, self.datetime_fmt) 87 | 88 | except Exception as e: 89 | lprint(f"failed to disabled_pods. {e}") 90 | 91 | finally: 92 | sleep(self.disable_interval) 93 | 94 | def _delete_disabled_resources(self, my_etcd: MyEtcd): 95 | lprint("start delete_disabled_resources thread...") 96 | delete_moratorium = 60 97 | 98 | while True: 99 | try: 100 | for key, update_at in my_etcd.get_disabled_resources(): 101 | update_at_datetime = dt.strptime(update_at, self.datetime_fmt) 102 | my_etcd.delete_disabled_resource(update_at_datetime, timedelta(seconds=delete_moratorium), key) 103 | 104 | except Exception as e: 105 | lprint(f"failed to delete_disabled_resources. {e}") 106 | 107 | finally: 108 | sleep(self.delete_interval) 109 | 110 | @property 111 | def kube_config(self) -> client.Configuration: 112 | conf = client.Configuration() 113 | conf.verify_ssl = True 114 | conf.host = "https://" + os.environ.get("KUBERNETES_SERVICE_HOST") 115 | conf.api_key_prefix["authorization"] = "Bearer" 116 | conf.ssl_ca_cert = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt" 117 | with open("/var/run/secrets/kubernetes.io/serviceaccount/token") as f: 118 | conf.api_key["authorization"] = f.read() 119 | return conf 120 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "f9490d004214edbd5b4808775b6c73b9fdd3ea39c13771fc8dd8e524d2f55521" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.7" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "cachetools": { 20 | "hashes": [ 21 | "sha256:513d4ff98dd27f85743a8dc0e92f55ddb1b49e060c2d5961512855cda2c01a98", 22 | "sha256:bbaa39c3dede00175df2dc2b03d0cf18dd2d32a7de7beb68072d13043c9edb20" 23 | ], 24 | "markers": "python_version ~= '3.5'", 25 | "version": "==4.1.1" 26 | }, 27 | "certifi": { 28 | "hashes": [ 29 | "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3", 30 | "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41" 31 | ], 32 | "version": "==2020.6.20" 33 | }, 34 | "chardet": { 35 | "hashes": [ 36 | "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", 37 | "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" 38 | ], 39 | "version": "==3.0.4" 40 | }, 41 | "etcd3": { 42 | "hashes": [ 43 | "sha256:89a704cb389bf0a010a1fa050ce19342d23bf6371ebda1c21cfe8ff3ed488726" 44 | ], 45 | "index": "pypi", 46 | "version": "==0.12.0" 47 | }, 48 | "google-auth": { 49 | "hashes": [ 50 | "sha256:2f34dd810090d0d4c9d5787c4ad7b4413d1fbfb941e13682c7a2298d3b6cdcc8", 51 | "sha256:ce1fb80b5c6d3dd038babcc43e221edeafefc72d983b3dc28b67b996f76f00b9" 52 | ], 53 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 54 | "version": "==1.20.1" 55 | }, 56 | "grpcio": { 57 | "hashes": [ 58 | "sha256:013287f99c99b201aa8a5f6bc7918f616739b9be031db132d9e3b8453e95e151", 59 | "sha256:0397616355760cd8282ed5ea34d51830ae4cb6613b7e5f66bed3be5d041b8b9a", 60 | "sha256:074871a184483d5cd0746fd01e7d214d3ee9d36e67e32a5786b0a21f29fb8304", 61 | "sha256:08a9b648dbe8852ff94b73a1c96da126834c3057ba2301d13e8c4adff334c482", 62 | "sha256:0fa86ac4452602c79774783aa68979a1a7625ebb7eaabee2b6550b975b9d61e6", 63 | "sha256:220c46b1fc9c9a6fcca4caac398f08f0ed43cdd63c45b7458983c4a1575ef6df", 64 | "sha256:259240aab2603891553e17ad5b2655693df79e02a9b887ff605bdeb2fcd3dcc9", 65 | "sha256:292635f05b6ce33f87116951d0b3d8d330bdfc5cac74f739370d60981e8c256c", 66 | "sha256:344b50865914cc8e6d023457bffee9a640abb18f75d0f2bb519041961c748da9", 67 | "sha256:3c2aa6d7a5e5bf73fdb1715eee777efe06dd39df03383f1cc095b2fdb34883e6", 68 | "sha256:43d44548ad6ee738b941abd9f09e3b83a5c13f3e1410321023c3c148ba50e796", 69 | "sha256:5043440c45c0a031f387e7f48527541c65d672005fb24cf18ef6857483557d39", 70 | "sha256:58d7121f48cb94535a4cedcce32921d0d0a78563c7372a143dedeec196d1c637", 71 | "sha256:5d7faa89992e015d245750ca9ac916c161bbf72777b2c60abc61da3fae41339e", 72 | "sha256:5fb0923b16590bac338e92d98c7d8effb3cfad1d2e18c71bf86bde32c49cd6dd", 73 | "sha256:63ee8e02d04272c3d103f44b4bce5d43ea757dd288673cea212d2f7da27967d2", 74 | "sha256:64077e3a9a7cf2f59e6c76d503c8de1f18a76428f41a5b000dc53c48a0b772ff", 75 | "sha256:739a72abffbd36083ff7adbb862cf1afc1e311c35834bed9c0361d8e68b063e1", 76 | "sha256:75e383053dccb610590aa53eed5278db5c09bf498d3b5105ce6c776478f59352", 77 | "sha256:7a11b1ebb3210f34913b8be6995936bf9ebc541a65ab69e75db5ce1fe5047e8f", 78 | "sha256:8002a89ea91c0078c15d3c0daf423fd4968946be78f08545e807ea9a5ff8054a", 79 | "sha256:8b42f0ac76be07a5fa31117a3388d754ad35ef05e2e34be185ca9ccbcfac2069", 80 | "sha256:8ca26b489b5dc1e3d31807d329c23d6cb06fe40fbae25b0649b718947936e26a", 81 | "sha256:92e54ab65e782f227e751c7555918afaba8d1229601687e89b80c2b65d2f6642", 82 | "sha256:a9a7ae74cb3108e6457cf15532d4c300324b48fbcf3ef290bcd2835745f20510", 83 | "sha256:ba3e43cb984399064ffaa3c0997576e46a1e268f9da05f97cd9b272f0b59ee71", 84 | "sha256:baaa036540d7ace433bdf38a3fe5e41cf9f84cdf10a88bac805f678a7ca8ddcc", 85 | "sha256:bf00ab06ea4f89976288f4d6224d4aa120780e30c955d4f85c3214ada29b3ddf", 86 | "sha256:bf39977282a79dc1b2765cc3402c0ada571c29a491caec6ed12c0993c1ec115e", 87 | "sha256:c22b19abba63562a5a200e586b5bde39d26c8ec30c92e26d209d81182371693b", 88 | "sha256:c9016ab1eaf4e054099303287195f3746bd4e69f2631d040f9dca43e910a5408", 89 | "sha256:d2c5e05c257859febd03f5d81b5015e1946d6bcf475c7bf63ee99cea8ab0d590", 90 | "sha256:e64bddd09842ef508d72ca354319b0eb126205d951e8ac3128fe9869bd563552", 91 | "sha256:e8c3264b0fd728aadf3f0324471843f65bd3b38872bdab2a477e31ffb685dd5b", 92 | "sha256:ea849210e7362559f326cbe603d5b8d8bb1e556e86a7393b5a8847057de5b084", 93 | "sha256:ebb2ca09fa17537e35508a29dcb05575d4d9401138a68e83d1c605d65e8a1770", 94 | "sha256:ef9fce98b6fe03874c2a6576b02aec1a0df25742cd67d1d7b75a49e30aa74225", 95 | "sha256:f04c59d186af3157dc8811114130aaeae92e90a65283733f41de94eed484e1f7", 96 | "sha256:f5b0870b733bcb7b6bf05a02035e7aaf20f599d3802b390282d4c2309f825f1d" 97 | ], 98 | "version": "==1.31.0" 99 | }, 100 | "idna": { 101 | "hashes": [ 102 | "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", 103 | "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" 104 | ], 105 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 106 | "version": "==2.10" 107 | }, 108 | "kubernetes": { 109 | "hashes": [ 110 | "sha256:1a2472f8b01bc6aa87e3a34781f859bded5a5c8ff791a53d889a8bd6cc550430", 111 | "sha256:4af81201520977139a143f96123fb789fa351879df37f122916b9b6ed050bbaf" 112 | ], 113 | "index": "pypi", 114 | "version": "==11.0" 115 | }, 116 | "oauthlib": { 117 | "hashes": [ 118 | "sha256:bee41cc35fcca6e988463cacc3bcb8a96224f470ca547e697b604cc697b2f889", 119 | "sha256:df884cd6cbe20e32633f1db1072e9356f53638e4361bef4e8b03c9127c9328ea" 120 | ], 121 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 122 | "version": "==3.1.0" 123 | }, 124 | "protobuf": { 125 | "hashes": [ 126 | "sha256:304e08440c4a41a0f3592d2a38934aad6919d692bb0edfb355548786728f9a5e", 127 | "sha256:49ef8ab4c27812a89a76fa894fe7a08f42f2147078392c0dee51d4a444ef6df5", 128 | "sha256:50b5fee674878b14baea73b4568dc478c46a31dd50157a5b5d2f71138243b1a9", 129 | "sha256:5524c7020eb1fb7319472cb75c4c3206ef18b34d6034d2ee420a60f99cddeb07", 130 | "sha256:612bc97e42b22af10ba25e4140963fbaa4c5181487d163f4eb55b0b15b3dfcd2", 131 | "sha256:6f349adabf1c004aba53f7b4633459f8ca8a09654bf7e69b509c95a454755776", 132 | "sha256:85b94d2653b0fdf6d879e39d51018bf5ccd86c81c04e18a98e9888694b98226f", 133 | "sha256:87535dc2d2ef007b9d44e309d2b8ea27a03d2fa09556a72364d706fcb7090828", 134 | "sha256:a7ab28a8f1f043c58d157bceb64f80e4d2f7f1b934bc7ff5e7f7a55a337ea8b0", 135 | "sha256:a96f8fc625e9ff568838e556f6f6ae8eca8b4837cdfb3f90efcb7c00e342a2eb", 136 | "sha256:b5a114ea9b7fc90c2cc4867a866512672a47f66b154c6d7ee7e48ddb68b68122", 137 | "sha256:be04fe14ceed7f8641e30f36077c1a654ff6f17d0c7a5283b699d057d150d82a", 138 | "sha256:bff02030bab8b969f4de597543e55bd05e968567acb25c0a87495a31eb09e925", 139 | "sha256:c9ca9f76805e5a637605f171f6c4772fc4a81eced4e2f708f79c75166a2c99ea", 140 | "sha256:e1464a4a2cf12f58f662c8e6421772c07947266293fb701cb39cd9c1e183f63c", 141 | "sha256:e72736dd822748b0721f41f9aaaf6a5b6d5cfc78f6c8690263aef8bba4457f0e", 142 | "sha256:eafe9fa19fcefef424ee089fb01ac7177ff3691af7cc2ae8791ae523eb6ca907", 143 | "sha256:f4b73736108a416c76c17a8a09bc73af3d91edaa26c682aaa460ef91a47168d3" 144 | ], 145 | "index": "pypi", 146 | "version": "==3.12.2" 147 | }, 148 | "pyasn1": { 149 | "hashes": [ 150 | "sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359", 151 | "sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576", 152 | "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf", 153 | "sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7", 154 | "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d", 155 | "sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00", 156 | "sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8", 157 | "sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86", 158 | "sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12", 159 | "sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776", 160 | "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba", 161 | "sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2", 162 | "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3" 163 | ], 164 | "version": "==0.4.8" 165 | }, 166 | "pyasn1-modules": { 167 | "hashes": [ 168 | "sha256:0845a5582f6a02bb3e1bde9ecfc4bfcae6ec3210dd270522fee602365430c3f8", 169 | "sha256:0fe1b68d1e486a1ed5473f1302bd991c1611d319bba158e98b106ff86e1d7199", 170 | "sha256:15b7c67fabc7fc240d87fb9aabf999cf82311a6d6fb2c70d00d3d0604878c811", 171 | "sha256:426edb7a5e8879f1ec54a1864f16b882c2837bfd06eee62f2c982315ee2473ed", 172 | "sha256:65cebbaffc913f4fe9e4808735c95ea22d7a7775646ab690518c056784bc21b4", 173 | "sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e", 174 | "sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74", 175 | "sha256:a99324196732f53093a84c4369c996713eb8c89d360a496b599fb1a9c47fc3eb", 176 | "sha256:b80486a6c77252ea3a3e9b1e360bc9cf28eaac41263d173c032581ad2f20fe45", 177 | "sha256:c29a5e5cc7a3f05926aff34e097e84f8589cd790ce0ed41b67aed6857b26aafd", 178 | "sha256:cbac4bc38d117f2a49aeedec4407d23e8866ea4ac27ff2cf7fb3e5b570df19e0", 179 | "sha256:f39edd8c4ecaa4556e989147ebf219227e2cd2e8a43c7e7fcb1f1c18c5fd6a3d", 180 | "sha256:fe0644d9ab041506b62782e92b06b8c68cca799e1a9636ec398675459e031405" 181 | ], 182 | "version": "==0.2.8" 183 | }, 184 | "python-dateutil": { 185 | "hashes": [ 186 | "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", 187 | "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a" 188 | ], 189 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 190 | "version": "==2.8.1" 191 | }, 192 | "pyyaml": { 193 | "hashes": [ 194 | "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97", 195 | "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76", 196 | "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2", 197 | "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648", 198 | "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf", 199 | "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f", 200 | "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2", 201 | "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee", 202 | "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d", 203 | "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c", 204 | "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a" 205 | ], 206 | "version": "==5.3.1" 207 | }, 208 | "requests": { 209 | "hashes": [ 210 | "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b", 211 | "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898" 212 | ], 213 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", 214 | "version": "==2.24.0" 215 | }, 216 | "requests-oauthlib": { 217 | "hashes": [ 218 | "sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d", 219 | "sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a", 220 | "sha256:fa6c47b933f01060936d87ae9327fead68768b69c6c9ea2109c48be30f2d4dbc" 221 | ], 222 | "version": "==1.3.0" 223 | }, 224 | "rsa": { 225 | "hashes": [ 226 | "sha256:109ea5a66744dd859bf16fe904b8d8b627adafb9408753161e766a92e7d681fa", 227 | "sha256:6166864e23d6b5195a5cfed6cd9fed0fe774e226d8f854fcb23b7bbef0350233" 228 | ], 229 | "markers": "python_version >= '3.5'", 230 | "version": "==4.6" 231 | }, 232 | "six": { 233 | "hashes": [ 234 | "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", 235 | "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" 236 | ], 237 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 238 | "version": "==1.15.0" 239 | }, 240 | "tenacity": { 241 | "hashes": [ 242 | "sha256:29ae90e7faf488a8628432154bb34ace1cca58244c6ea399fd33f066ac71339a", 243 | "sha256:5a5d3dcd46381abe8b4f82b5736b8726fd3160c6c7161f53f8af7f1eb9b82173" 244 | ], 245 | "version": "==6.2.0" 246 | }, 247 | "urllib3": { 248 | "hashes": [ 249 | "sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a", 250 | "sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461" 251 | ], 252 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", 253 | "version": "==1.25.10" 254 | }, 255 | "websocket-client": { 256 | "hashes": [ 257 | "sha256:0fc45c961324d79c781bab301359d5a1b00b13ad1b10415a4780229ef71a5549", 258 | "sha256:d735b91d6d1692a6a181f2a8c9e0238e5f6373356f561bb9dc4c7af36f452010" 259 | ], 260 | "version": "==0.57.0" 261 | } 262 | }, 263 | "develop": { 264 | "astroid": { 265 | "hashes": [ 266 | "sha256:2f4078c2a41bf377eea06d71c9d2ba4eb8f6b1af2135bec27bbbb7d8f12bb703", 267 | "sha256:bc58d83eb610252fd8de6363e39d4f1d0619c894b0ed24603b881c02e64c7386" 268 | ], 269 | "markers": "python_version >= '3.5'", 270 | "version": "==2.4.2" 271 | }, 272 | "flake8": { 273 | "hashes": [ 274 | "sha256:15e351d19611c887e482fb960eae4d44845013cc142d42896e9862f775d8cf5c", 275 | "sha256:f04b9fcbac03b0a3e58c0ab3a0ecc462e023a9faf046d57794184028123aa208" 276 | ], 277 | "index": "pypi", 278 | "version": "==3.8.3" 279 | }, 280 | "importlib-metadata": { 281 | "hashes": [ 282 | "sha256:90bb658cdbbf6d1735b6341ce708fc7024a3e14e99ffdc5783edea9f9b077f83", 283 | "sha256:dc15b2969b4ce36305c51eebe62d418ac7791e9a157911d58bfb1f9ccd8e2070" 284 | ], 285 | "markers": "python_version < '3.8'", 286 | "version": "==1.7.0" 287 | }, 288 | "isort": { 289 | "hashes": [ 290 | "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1", 291 | "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd" 292 | ], 293 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 294 | "version": "==4.3.21" 295 | }, 296 | "lazy-object-proxy": { 297 | "hashes": [ 298 | "sha256:0c4b206227a8097f05c4dbdd323c50edf81f15db3b8dc064d08c62d37e1a504d", 299 | "sha256:194d092e6f246b906e8f70884e620e459fc54db3259e60cf69a4d66c3fda3449", 300 | "sha256:1be7e4c9f96948003609aa6c974ae59830a6baecc5376c25c92d7d697e684c08", 301 | "sha256:4677f594e474c91da97f489fea5b7daa17b5517190899cf213697e48d3902f5a", 302 | "sha256:48dab84ebd4831077b150572aec802f303117c8cc5c871e182447281ebf3ac50", 303 | "sha256:5541cada25cd173702dbd99f8e22434105456314462326f06dba3e180f203dfd", 304 | "sha256:59f79fef100b09564bc2df42ea2d8d21a64fdcda64979c0fa3db7bdaabaf6239", 305 | "sha256:8d859b89baf8ef7f8bc6b00aa20316483d67f0b1cbf422f5b4dc56701c8f2ffb", 306 | "sha256:9254f4358b9b541e3441b007a0ea0764b9d056afdeafc1a5569eee1cc6c1b9ea", 307 | "sha256:9651375199045a358eb6741df3e02a651e0330be090b3bc79f6d0de31a80ec3e", 308 | "sha256:97bb5884f6f1cdce0099f86b907aa41c970c3c672ac8b9c8352789e103cf3156", 309 | "sha256:9b15f3f4c0f35727d3a0fba4b770b3c4ebbb1fa907dbcc046a1d2799f3edd142", 310 | "sha256:a2238e9d1bb71a56cd710611a1614d1194dc10a175c1e08d75e1a7bcc250d442", 311 | "sha256:a6ae12d08c0bf9909ce12385803a543bfe99b95fe01e752536a60af2b7797c62", 312 | "sha256:ca0a928a3ddbc5725be2dd1cf895ec0a254798915fb3a36af0964a0a4149e3db", 313 | "sha256:cb2c7c57005a6804ab66f106ceb8482da55f5314b7fcb06551db1edae4ad1531", 314 | "sha256:d74bb8693bf9cf75ac3b47a54d716bbb1a92648d5f781fc799347cfc95952383", 315 | "sha256:d945239a5639b3ff35b70a88c5f2f491913eb94871780ebfabb2568bd58afc5a", 316 | "sha256:eba7011090323c1dadf18b3b689845fd96a61ba0a1dfbd7f24b921398affc357", 317 | "sha256:efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4", 318 | "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0" 319 | ], 320 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 321 | "version": "==1.4.3" 322 | }, 323 | "mccabe": { 324 | "hashes": [ 325 | "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", 326 | "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" 327 | ], 328 | "version": "==0.6.1" 329 | }, 330 | "pycodestyle": { 331 | "hashes": [ 332 | "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367", 333 | "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e" 334 | ], 335 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 336 | "version": "==2.6.0" 337 | }, 338 | "pyflakes": { 339 | "hashes": [ 340 | "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92", 341 | "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8" 342 | ], 343 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 344 | "version": "==2.2.0" 345 | }, 346 | "pylint": { 347 | "hashes": [ 348 | "sha256:7dd78437f2d8d019717dbf287772d0b2dbdfd13fc016aa7faa08d67bccc46adc", 349 | "sha256:d0ece7d223fe422088b0e8f13fa0a1e8eb745ebffcb8ed53d3e95394b6101a1c" 350 | ], 351 | "index": "pypi", 352 | "version": "==2.5.3" 353 | }, 354 | "six": { 355 | "hashes": [ 356 | "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", 357 | "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" 358 | ], 359 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 360 | "version": "==1.15.0" 361 | }, 362 | "toml": { 363 | "hashes": [ 364 | "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f", 365 | "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88" 366 | ], 367 | "version": "==0.10.1" 368 | }, 369 | "typed-ast": { 370 | "hashes": [ 371 | "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355", 372 | "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919", 373 | "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa", 374 | "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652", 375 | "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75", 376 | "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01", 377 | "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d", 378 | "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1", 379 | "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907", 380 | "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c", 381 | "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3", 382 | "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b", 383 | "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614", 384 | "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb", 385 | "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b", 386 | "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41", 387 | "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6", 388 | "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34", 389 | "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe", 390 | "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4", 391 | "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7" 392 | ], 393 | "markers": "python_version < '3.8' and implementation_name == 'cpython'", 394 | "version": "==1.4.1" 395 | }, 396 | "wrapt": { 397 | "hashes": [ 398 | "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7" 399 | ], 400 | "version": "==1.12.1" 401 | }, 402 | "zipp": { 403 | "hashes": [ 404 | "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b", 405 | "sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96" 406 | ], 407 | "markers": "python_version >= '3.6'", 408 | "version": "==3.1.0" 409 | } 410 | } 411 | } 412 | --------------------------------------------------------------------------------