├── stock ├── __init__.py ├── wait.sh ├── Dockerfile ├── db.py ├── model.py ├── message_queue.py └── cron.py ├── delivery ├── __init__.py ├── wait.sh ├── Dockerfile ├── db.py ├── model.py ├── message_queue.py └── cron.py ├── .gitignore ├── order ├── wait.sh ├── main.py ├── Dockerfile └── app │ ├── db.py │ ├── model.py │ ├── __init__.py │ └── message_queue.py ├── success.png ├── Pipfile ├── dump.sql ├── README.md ├── docker-compose.yml └── Pipfile.lock /stock/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /delivery/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | __pycache__/ -------------------------------------------------------------------------------- /order/wait.sh: -------------------------------------------------------------------------------- 1 | #/bin/bash 2 | sleep 5 -------------------------------------------------------------------------------- /success.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teamhide/python-saga-pattern-example/HEAD/success.png -------------------------------------------------------------------------------- /stock/wait.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | while ! nc -z rabbitmq 5672; do sleep 3; done 3 | python3 stock/cron.py -------------------------------------------------------------------------------- /delivery/wait.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | while ! nc -z rabbitmq 5672; do sleep 3; done 3 | python3 delivery/cron.py -------------------------------------------------------------------------------- /order/main.py: -------------------------------------------------------------------------------- 1 | import uvicorn 2 | 3 | if __name__ == '__main__': 4 | uvicorn.run(app='app:app', host='0.0.0.0', port=8000, reload=True) 5 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | fastapi = "*" 10 | sqlalchemy = "*" 11 | uvicorn = "*" 12 | pymysql = "*" 13 | pika = "*" 14 | 15 | [requires] 16 | python_version = "3.8" 17 | -------------------------------------------------------------------------------- /order/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8.1 2 | MAINTAINER hide 3 | 4 | COPY . /home 5 | RUN pip3 install fastapi 6 | RUN pip3 install sqlalchemy 7 | RUN pip3 install uvicorn 8 | RUN pip3 install pymysql 9 | RUN pip3 install pika 10 | WORKDIR /home 11 | 12 | CMD python3 order/main.py -------------------------------------------------------------------------------- /stock/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8.1 2 | MAINTAINER hide 3 | 4 | COPY . /home 5 | RUN pip3 install sqlalchemy 6 | RUN pip3 install pymysql 7 | RUN pip3 install pika 8 | WORKDIR /home 9 | RUN apt-get update && apt-get install -y netcat 10 | RUN chmod +x stock/wait.sh 11 | 12 | CMD stock/wait.sh -------------------------------------------------------------------------------- /delivery/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8.1 2 | MAINTAINER hide 3 | 4 | COPY . /home 5 | RUN pip3 install sqlalchemy 6 | RUN pip3 install pymysql 7 | RUN pip3 install pika 8 | WORKDIR /home 9 | RUN apt-get update && apt-get install -y netcat 10 | RUN chmod +x delivery/wait.sh 11 | 12 | CMD delivery/wait.sh -------------------------------------------------------------------------------- /stock/db.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import create_engine 2 | from sqlalchemy.ext.declarative import declarative_base 3 | from sqlalchemy.orm import scoped_session, sessionmaker 4 | 5 | engine = create_engine(f'mysql+pymysql://order:order@order-db:3306/order') 6 | session = scoped_session( 7 | sessionmaker( 8 | autocommit=False, 9 | autoflush=False, 10 | bind=engine, 11 | ) 12 | ) 13 | Base = declarative_base() 14 | -------------------------------------------------------------------------------- /delivery/db.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import create_engine 2 | from sqlalchemy.ext.declarative import declarative_base 3 | from sqlalchemy.orm import scoped_session, sessionmaker 4 | 5 | engine = create_engine(f'mysql+pymysql://order:order@order-db:3306/order') 6 | session = scoped_session( 7 | sessionmaker( 8 | autocommit=False, 9 | autoflush=False, 10 | bind=engine, 11 | ) 12 | ) 13 | Base = declarative_base() 14 | -------------------------------------------------------------------------------- /order/app/db.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import create_engine 2 | from sqlalchemy.ext.declarative import declarative_base 3 | from sqlalchemy.orm import scoped_session, sessionmaker 4 | 5 | engine = create_engine(f'mysql+pymysql://order:order@order-db:3306/order') 6 | session = scoped_session( 7 | sessionmaker( 8 | autocommit=False, 9 | autoflush=False, 10 | bind=engine, 11 | ) 12 | ) 13 | Base = declarative_base() 14 | -------------------------------------------------------------------------------- /delivery/model.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, BigInteger, func, DateTime 2 | 3 | from db import Base 4 | 5 | 6 | class Delivery(Base): 7 | __tablename__ = 'deliveries' 8 | 9 | id = Column(BigInteger, primary_key=True, autoincrement=True) 10 | order_id = Column(BigInteger, nullable=False) 11 | created_at = Column(DateTime, default=func.now(), nullable=False) 12 | updated_at = Column( 13 | DateTime, 14 | default=func.now(), 15 | onupdate=func.now(), 16 | nullable=False, 17 | ) 18 | -------------------------------------------------------------------------------- /stock/model.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, BigInteger, func, DateTime, Integer 2 | 3 | from db import Base 4 | 5 | 6 | class Stock(Base): 7 | __tablename__ = 'stocks' 8 | 9 | id = Column(BigInteger, primary_key=True, autoincrement=True) 10 | item_id = Column(BigInteger, nullable=False) 11 | count = Column(Integer, default=100) 12 | created_at = Column(DateTime, default=func.now(), nullable=False) 13 | updated_at = Column( 14 | DateTime, 15 | default=func.now(), 16 | onupdate=func.now(), 17 | nullable=False, 18 | ) 19 | -------------------------------------------------------------------------------- /order/app/model.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, BigInteger, Unicode, DateTime, func 2 | 3 | from .db import Base 4 | 5 | 6 | class Order(Base): 7 | __tablename__ = 'orders' 8 | 9 | id = Column(BigInteger, primary_key=True, autoincrement=True) 10 | item_id = Column(BigInteger, nullable=False) 11 | status = Column(Unicode(255), default='pending') 12 | created_at = Column(DateTime, default=func.now(), nullable=False) 13 | updated_at = Column( 14 | DateTime, 15 | default=func.now(), 16 | onupdate=func.now(), 17 | nullable=False, 18 | ) 19 | -------------------------------------------------------------------------------- /dump.sql: -------------------------------------------------------------------------------- 1 | USE order; 2 | 3 | CREATE TABLE orders( 4 | id bigint primary key auto_increment, 5 | item_id bigint not null, 6 | status varchar(255) default 'pending', 7 | created_at datetime, 8 | updated_at datetime 9 | ); 10 | 11 | CREATE TABLE stocks( 12 | id bigint primary key auto_increment, 13 | item_id bigint not null, 14 | count int default 100, 15 | created_at datetime, 16 | updated_at datetime 17 | ); 18 | 19 | CREATE TABLE deliveries( 20 | id bigint primary key auto_increment, 21 | order_id bigint not null, 22 | created_at datetime, 23 | updated_at datetime 24 | ); 25 | 26 | INSERT INTO stocks(item_id, count) VALUES(1, 100); 27 | INSERT INTO stocks(item_id, count) VALUES(2, 100); 28 | INSERT INTO stocks(item_id, count) VALUES(3, 100); -------------------------------------------------------------------------------- /order/app/__init__.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI, Request 2 | 3 | from .db import session, Base, engine 4 | from .model import Order 5 | from .message_queue import MQ 6 | 7 | app = FastAPI() 8 | 9 | 10 | @app.middleware('http') 11 | async def remove_session(request: Request, call_next): 12 | response = await call_next(request) 13 | session.remove() 14 | return response 15 | 16 | 17 | @app.get('/order/{item_id}') 18 | async def create_order(item_id: int): 19 | order = Order(item_id=item_id, status='pending') 20 | session.add(order) 21 | session.commit() 22 | 23 | mq = MQ( 24 | id='admin', 25 | password='admin', 26 | host='mq', 27 | port=5672, 28 | ) 29 | mq.produce( 30 | exchange='', 31 | routing_key='order_created', 32 | body=f'{order.id}:{item_id}', 33 | ) 34 | mq.consume(queue='delivery_success') 35 | mq.close() 36 | return {'status': True} 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python SAGA Pattern Example 2 | 3 | Implemented SAGA Pattern through python for distributed transaction. 4 | 5 | - FastAPI 6 | - RabbitMQ 7 | 8 | 9 | There is three microsevices. 10 | - Order 11 | - Stock 12 | - Delivery 13 | 14 | **Case of success** 15 | ![ex_screenshot](./success.png) 16 | 17 | Each service is listening to a specific queue, as shown above. 18 | 19 | To briefly explain the flow, 20 | 21 | 1. **User** - Order product through `/order/{item_id}` 22 | 2. **Order** - Execute local transaction 23 | 3. **Order** - Produce event to `ORDER_CREATED` 24 | 4. **Stock** - Consume `ORDER_CREATED` and if got some, execute local transaction 25 | 5. **Stock** - Produce event to `STOCK_SUCCESS` 26 | 6. **DELIVERY** - Consume `STOCK_SUCCESS` and if got some, execute local transaction 27 | 7. **DELIVERY** - Produce event to `DELIVERY_SUCCESS` 28 | 8. **Order** - Consume `DELIVERY_SUCCESS` 29 | 9. **Order** - Change order status to `complete` and distributed transaction is end -------------------------------------------------------------------------------- /delivery/message_queue.py: -------------------------------------------------------------------------------- 1 | import pika 2 | 3 | 4 | class Producer: 5 | def __init__( 6 | self, 7 | id: str, 8 | password: str, 9 | host: str, 10 | port: str, 11 | queue: str, 12 | ): 13 | self.conn = pika.BlockingConnection( 14 | pika.URLParameters(f'amqp://{id}:{password}@{host}:{port}'), 15 | ) 16 | self.channel = self.conn.channel() 17 | self.channel.queue_declare(queue=queue) 18 | 19 | def produce(self, exchange: str, routing_key: str, body: str) -> None: 20 | self.channel.basic_publish( 21 | exchange=exchange, 22 | routing_key=routing_key, 23 | body=body, 24 | ) 25 | self.conn.close() 26 | 27 | 28 | class Consumer: 29 | def __init__( 30 | self, 31 | id: str, 32 | password: str, 33 | host: str, 34 | port: int, 35 | queue: str, 36 | ): 37 | self.conn = pika.BlockingConnection( 38 | pika.URLParameters(f'amqp://{id}:{password}@{host}:{port}'), 39 | ) 40 | self.channel = self.conn.channel() 41 | self.channel.queue_declare(queue=queue) 42 | 43 | def callback(self, ch, method, properties, body: str) -> None: 44 | print(" [x] Received %r" % body) 45 | 46 | def consume(self, queue: str) -> None: 47 | self.channel.basic_consume( 48 | on_message_callback=self.callback, 49 | queue=queue, 50 | auto_ack=True, 51 | ) 52 | self.channel.start_consuming() 53 | -------------------------------------------------------------------------------- /order/app/message_queue.py: -------------------------------------------------------------------------------- 1 | import pika 2 | from .db import session 3 | from .model import Order 4 | 5 | 6 | class MQ: 7 | def __init__( 8 | self, 9 | id: str, 10 | password: str, 11 | host: str, 12 | port: int, 13 | ): 14 | self.conn = pika.BlockingConnection( 15 | pika.URLParameters(f'amqp://{id}:{password}@{host}:{port}'), 16 | ) 17 | self.channel = self.conn.channel() 18 | self.channel = self.conn.channel() 19 | self.channel.queue_declare(queue='order_created') 20 | self.channel.queue_declare(queue='stock_success') 21 | self.channel.queue_declare(queue='delivery_success') 22 | self.channel.queue_declare(queue='stock_fail') 23 | self.channel.queue_declare(queue='delivery_fail') 24 | 25 | def callback(self, ch, method, properties, body: bytes) -> None: 26 | order_id = body.decode('utf8') 27 | order = session.query(Order).filter(Order.id == order_id).first() 28 | order.status = 'complete' 29 | session.add(order) 30 | session.commit() 31 | print('[*] Transaction end') 32 | 33 | def consume(self, queue: str) -> None: 34 | self.channel.basic_consume( 35 | on_message_callback=self.callback, 36 | queue=queue, 37 | auto_ack=True, 38 | ) 39 | self.channel.start_consuming() 40 | 41 | def produce(self, exchange: str, routing_key: str, body: str) -> None: 42 | self.channel.basic_publish( 43 | exchange=exchange, 44 | routing_key=routing_key, 45 | body=body, 46 | ) 47 | 48 | def close(self): 49 | self.conn.close() 50 | -------------------------------------------------------------------------------- /stock/message_queue.py: -------------------------------------------------------------------------------- 1 | import pika 2 | from model import Stock 3 | from db import session 4 | 5 | 6 | class Producer: 7 | def __init__( 8 | self, 9 | id: str, 10 | password: str, 11 | host: str, 12 | port: str, 13 | queue: str, 14 | ): 15 | self.conn = pika.BlockingConnection( 16 | pika.URLParameters(f'amqp://{id}:{password}@{host}:{port}'), 17 | ) 18 | self.channel = self.conn.channel() 19 | self.channel.queue_declare(queue=queue) 20 | 21 | def produce(self, exchange: str, routing_key: str, body: str) -> None: 22 | self.channel.basic_publish( 23 | exchange=exchange, 24 | routing_key=routing_key, 25 | body=body, 26 | ) 27 | self.conn.close() 28 | 29 | 30 | class Consumer: 31 | def __init__( 32 | self, 33 | id: str, 34 | password: str, 35 | host: str, 36 | port: int, 37 | queue: str, 38 | ): 39 | self.queue = queue 40 | self.conn = pika.BlockingConnection( 41 | pika.URLParameters(f'amqp://{id}:{password}@{host}:{port}'), 42 | ) 43 | self.channel = self.conn.channel() 44 | self.channel.queue_declare(queue=self.queue) 45 | 46 | def callback(self, ch, method, properties, body: bytes) -> None: 47 | stock = Stock() 48 | session.add(stock) 49 | session.commit() 50 | print(" [x] Received %r" % body.decode('utf8')) 51 | 52 | def consume(self) -> None: 53 | self.channel.basic_consume( 54 | on_message_callback=self.callback, 55 | queue=self.queue, 56 | auto_ack=True, 57 | ) 58 | self.channel.start_consuming() 59 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | 3 | services: 4 | rabbitmq: # login guest:guest 5 | container_name: mq 6 | hostname: mq 7 | image: rabbitmq 8 | environment: 9 | - RABBITMQ_DEFAULT_USER=admin 10 | - RABBITMQ_DEFAULT_PASS=admin 11 | ports: 12 | - "4369:4369" 13 | - "5671:5671" 14 | - "5672:5672" 15 | - "25672:25672" 16 | - "15671:15671" 17 | - "15672:15672" 18 | healthcheck: 19 | test: ["CMD", "curl", "-f", "http://localhost:15672"] 20 | interval: 30s 21 | timeout: 10s 22 | retries: 5 23 | networks: 24 | - mq-network 25 | 26 | 27 | order-db: 28 | image: mysql:5.7 29 | container_name: order-db 30 | hostname: order-db 31 | networks: 32 | - order-network 33 | volumes: 34 | - ./dump.sql:/docker-entrypoint-initdb.d/dump.sql 35 | environment: 36 | - MYSQL_ROOT_PASSWORD=order 37 | - MYSQL_USER=order 38 | - MYSQL_PASSWORD=order 39 | - MYSQL_DATABASE=order 40 | ports: 41 | - 3306:3306 42 | 43 | order-api: 44 | build: 45 | context: . 46 | dockerfile: order/Dockerfile 47 | container_name: order-api 48 | hostname: order-api 49 | networks: 50 | - order-network 51 | - mq-network 52 | ports: 53 | - 8000:8000 54 | depends_on: 55 | - order-db 56 | - rabbitmq 57 | 58 | stock-cron: 59 | build: 60 | context: . 61 | dockerfile: stock/Dockerfile 62 | restart: on-failure 63 | depends_on: 64 | - rabbitmq 65 | networks: 66 | - order-network 67 | - mq-network 68 | 69 | delivery-cron: 70 | build: 71 | context: . 72 | dockerfile: delivery/Dockerfile 73 | restart: on-failure 74 | depends_on: 75 | - rabbitmq 76 | networks: 77 | - order-network 78 | - mq-network 79 | 80 | networks: 81 | order-network: 82 | driver: bridge 83 | mq-network: 84 | driver: bridge -------------------------------------------------------------------------------- /delivery/cron.py: -------------------------------------------------------------------------------- 1 | import pika 2 | from model import Delivery 3 | from db import session 4 | 5 | 6 | class MQ: 7 | def __init__( 8 | self, 9 | id: str, 10 | password: str, 11 | host: str, 12 | port: int, 13 | ): 14 | self.conn = pika.BlockingConnection( 15 | pika.URLParameters(f'amqp://{id}:{password}@{host}:{port}'), 16 | ) 17 | self.channel = self.conn.channel() 18 | self.channel = self.conn.channel() 19 | self.channel.queue_declare(queue='order_created') 20 | self.channel.queue_declare(queue='stock_success') 21 | self.channel.queue_declare(queue='delivery_success') 22 | self.channel.queue_declare(queue='stock_fail') 23 | self.channel.queue_declare(queue='delivery_fail') 24 | 25 | def callback(self, ch, method, properties, body: bytes) -> None: 26 | order_id = body.decode('utf8') 27 | delivery = Delivery(order_id=order_id) 28 | session.add(delivery) 29 | session.commit() 30 | 31 | self.produce(exchange='', routing_key='delivery_success', body=order_id) 32 | print(f'[*] Produce to `delivery_success` -> `{order_id}`') 33 | 34 | def consume(self, queue: str) -> None: 35 | self.channel.basic_consume( 36 | on_message_callback=self.callback, 37 | queue=queue, 38 | auto_ack=True, 39 | ) 40 | self.channel.start_consuming() 41 | 42 | def produce(self, exchange: str, routing_key: str, body: str) -> None: 43 | self.channel.basic_publish( 44 | exchange=exchange, 45 | routing_key=routing_key, 46 | body=body, 47 | ) 48 | 49 | def close(self): 50 | self.conn.close() 51 | 52 | 53 | class Cron: 54 | def __init__(self, queue: MQ): 55 | self.queue = queue 56 | 57 | def run(self): 58 | self.queue.consume(queue='stock_success') 59 | 60 | 61 | if __name__ == '__main__': 62 | cron = Cron(queue=MQ( 63 | id='admin', 64 | password='admin', 65 | host='mq', 66 | port=5672, 67 | )) 68 | cron.run() 69 | -------------------------------------------------------------------------------- /stock/cron.py: -------------------------------------------------------------------------------- 1 | import pika 2 | from model import Stock 3 | from db import session 4 | 5 | 6 | class MQ: 7 | def __init__( 8 | self, 9 | id: str, 10 | password: str, 11 | host: str, 12 | port: int, 13 | ): 14 | self.conn = pika.BlockingConnection( 15 | pika.URLParameters(f'amqp://{id}:{password}@{host}:{port}'), 16 | ) 17 | self.channel = self.conn.channel() 18 | self.channel = self.conn.channel() 19 | self.channel.queue_declare(queue='order_created') 20 | self.channel.queue_declare(queue='stock_success') 21 | self.channel.queue_declare(queue='delivery_success') 22 | self.channel.queue_declare(queue='stock_fail') 23 | self.channel.queue_declare(queue='delivery_fail') 24 | 25 | def callback(self, ch, method, properties, body: bytes) -> None: 26 | order_id, item_id = body.decode('utf8').split(':') # order_id:item_id 27 | stock = session.query(Stock).filter(Stock.item_id == item_id).first() 28 | stock.count = Stock.count - 1 29 | session.add(stock) 30 | session.commit() 31 | 32 | self.produce(exchange='', routing_key='stock_success', body=order_id) 33 | print(f'[*] Produce to `stock_success` -> `{order_id}`') 34 | 35 | def consume(self, queue: str) -> None: 36 | self.channel.basic_consume( 37 | on_message_callback=self.callback, 38 | queue=queue, 39 | auto_ack=True, 40 | ) 41 | self.channel.start_consuming() 42 | 43 | def produce(self, exchange: str, routing_key: str, body: str) -> None: 44 | self.channel.basic_publish( 45 | exchange=exchange, 46 | routing_key=routing_key, 47 | body=body, 48 | ) 49 | 50 | def close(self): 51 | self.conn.close() 52 | 53 | 54 | class Cron: 55 | def __init__(self, queue: MQ): 56 | self.queue = queue 57 | 58 | def run(self): 59 | self.queue.consume(queue='order_created') 60 | 61 | 62 | if __name__ == '__main__': 63 | cron = Cron(queue=MQ( 64 | id='admin', 65 | password='admin', 66 | host='mq', 67 | port=5672, 68 | )) 69 | cron.run() 70 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "663ce7a502c6b54e93e277509622b279e419f2fa3ac0662bf2eaebe4d317efa2" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.8" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "click": { 20 | "hashes": [ 21 | "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", 22 | "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc" 23 | ], 24 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", 25 | "version": "==7.1.2" 26 | }, 27 | "fastapi": { 28 | "hashes": [ 29 | "sha256:39569a18914075b2f1aaa03bcb9dc96a38e0e5dabaf3972e088c9077dfffa379", 30 | "sha256:8359e55d8412a5571c0736013d90af235d6949ec4ce978e9b63500c8f4b6f714" 31 | ], 32 | "index": "pypi", 33 | "version": "==0.65.2" 34 | }, 35 | "h11": { 36 | "hashes": [ 37 | "sha256:33d4bca7be0fa039f4e84d50ab00531047e53d6ee8ffbc83501ea602c169cae1", 38 | "sha256:4bc6d6a1238b7615b266ada57e0618568066f57dd6fa967d1290ec9309b2f2f1" 39 | ], 40 | "version": "==0.9.0" 41 | }, 42 | "httptools": { 43 | "hashes": [ 44 | "sha256:07659649fe6b3948b6490825f89abe5eb1cec79ebfaaa0b4bf30f3f33f3c2ba8", 45 | "sha256:08b79e09114e6ab5c3dbf560bba2cb2257ea38cdaeaf99b7cb80d8f92622fcd9", 46 | "sha256:1e35aa179b67086cc600a984924a88589b90793c9c1b260152ca4908786e09df", 47 | "sha256:31629e1f1b89959f8c0927bad12184dc07977dcf71e24f4772934aa490aa199b", 48 | "sha256:851026bd63ec0af7e7592890d97d15c92b62d9e17094353f19a52c8e2b33710a", 49 | "sha256:8fcca4b7efe353b13a24017211334c57d055a6e132c7adffed13a10d28efca57", 50 | "sha256:9abd788465aa46a0f288bd3a99e53edd184177d6379e2098fd6097bb359ad9d6", 51 | "sha256:aebdf0bd7bf7c90ae6b3be458692bf6e9e5b610b501f9f74c7979015a51db4c4", 52 | "sha256:bda99a5723e7eab355ce57435c70853fc137a65aebf2f1cd4d15d96e2956da7b", 53 | "sha256:c1c63d860749841024951b0a78e4dec6f543d23751ef061d6ab60064c7b8b524", 54 | "sha256:c4111a0a8a00eff1e495d43ea5230aaf64968a48ddba8ea2d5f982efae827404", 55 | "sha256:dce59ee45dd6ee6c434346a5ac527c44014326f560866b4b2f414a692ee1aca8", 56 | "sha256:f759717ca1b2ef498c67ba4169c2b33eecf943a89f5329abcff8b89d153eb500", 57 | "sha256:fb7199b8fb0c50a22e77260bb59017e0c075fa80cb03bb2c8692de76e7bb7fe7", 58 | "sha256:fbf7ecd31c39728f251b1c095fd27c84e4d21f60a1d079a0333472ff3ae59d34" 59 | ], 60 | "markers": "sys_platform != 'win32' and sys_platform != 'cygwin' and platform_python_implementation != 'PyPy'", 61 | "version": "==0.1.2" 62 | }, 63 | "pika": { 64 | "hashes": [ 65 | "sha256:4e1a1a6585a41b2341992ec32aadb7a919d649eb82904fd8e4a4e0871c8cf3af", 66 | "sha256:9fa76ba4b65034b878b2b8de90ff8660a59d925b087c5bb88f8fdbb4b64a1dbf" 67 | ], 68 | "index": "pypi", 69 | "version": "==1.1.0" 70 | }, 71 | "pydantic": { 72 | "hashes": [ 73 | "sha256:021ea0e4133e8c824775a0cfe098677acf6fa5a3cbf9206a376eed3fc09302cd", 74 | "sha256:05ddfd37c1720c392f4e0d43c484217b7521558302e7069ce8d318438d297739", 75 | "sha256:05ef5246a7ffd2ce12a619cbb29f3307b7c4509307b1b49f456657b43529dc6f", 76 | "sha256:10e5622224245941efc193ad1d159887872776df7a8fd592ed746aa25d071840", 77 | "sha256:18b5ea242dd3e62dbf89b2b0ec9ba6c7b5abaf6af85b95a97b00279f65845a23", 78 | "sha256:234a6c19f1c14e25e362cb05c68afb7f183eb931dd3cd4605eafff055ebbf287", 79 | "sha256:244ad78eeb388a43b0c927e74d3af78008e944074b7d0f4f696ddd5b2af43c62", 80 | "sha256:26464e57ccaafe72b7ad156fdaa4e9b9ef051f69e175dbbb463283000c05ab7b", 81 | "sha256:41b542c0b3c42dc17da70554bc6f38cbc30d7066d2c2815a94499b5684582ecb", 82 | "sha256:4a03cbbe743e9c7247ceae6f0d8898f7a64bb65800a45cbdc52d65e370570820", 83 | "sha256:4be75bebf676a5f0f87937c6ddb061fa39cbea067240d98e298508c1bda6f3f3", 84 | "sha256:54cd5121383f4a461ff7644c7ca20c0419d58052db70d8791eacbbe31528916b", 85 | "sha256:589eb6cd6361e8ac341db97602eb7f354551482368a37f4fd086c0733548308e", 86 | "sha256:8621559dcf5afacf0069ed194278f35c255dc1a1385c28b32dd6c110fd6531b3", 87 | "sha256:8b223557f9510cf0bfd8b01316bf6dd281cf41826607eada99662f5e4963f316", 88 | "sha256:99a9fc39470010c45c161a1dc584997f1feb13f689ecf645f59bb4ba623e586b", 89 | "sha256:a7c6002203fe2c5a1b5cbb141bb85060cbff88c2d78eccbc72d97eb7022c43e4", 90 | "sha256:a83db7205f60c6a86f2c44a61791d993dff4b73135df1973ecd9eed5ea0bda20", 91 | "sha256:ac8eed4ca3bd3aadc58a13c2aa93cd8a884bcf21cb019f8cfecaae3b6ce3746e", 92 | "sha256:e710876437bc07bd414ff453ac8ec63d219e7690128d925c6e82889d674bb505", 93 | "sha256:ea5cb40a3b23b3265f6325727ddfc45141b08ed665458be8c6285e7b85bd73a1", 94 | "sha256:fec866a0b59f372b7e776f2d7308511784dace622e0992a0b59ea3ccee0ae833" 95 | ], 96 | "markers": "python_full_version >= '3.6.1'", 97 | "version": "==1.8.2" 98 | }, 99 | "pymysql": { 100 | "hashes": [ 101 | "sha256:3943fbbbc1e902f41daf7f9165519f140c4451c179380677e6a848587042561a", 102 | "sha256:d8c059dcd81dedb85a9f034d5e22dcb4442c0b201908bede99e306d65ea7c8e7" 103 | ], 104 | "index": "pypi", 105 | "version": "==0.9.3" 106 | }, 107 | "sqlalchemy": { 108 | "hashes": [ 109 | "sha256:083e383a1dca8384d0ea6378bd182d83c600ed4ff4ec8247d3b2442cf70db1ad", 110 | "sha256:0a690a6486658d03cc6a73536d46e796b6570ac1f8a7ec133f9e28c448b69828", 111 | "sha256:114b6ace30001f056e944cebd46daef38fdb41ebb98f5e5940241a03ed6cad43", 112 | "sha256:128f6179325f7597a46403dde0bf148478f868df44841348dfc8d158e00db1f9", 113 | "sha256:13d48cd8b925b6893a4e59b2dfb3e59a5204fd8c98289aad353af78bd214db49", 114 | "sha256:211a1ce7e825f7142121144bac76f53ac28b12172716a710f4bf3eab477e730b", 115 | "sha256:2dc57ee80b76813759cccd1a7affedf9c4dbe5b065a91fb6092c9d8151d66078", 116 | "sha256:3e625e283eecc15aee5b1ef77203bfb542563fa4a9aa622c7643c7b55438ff49", 117 | "sha256:43078c7ec0457387c79b8d52fff90a7ad352ca4c7aa841c366238c3e2cf52fdf", 118 | "sha256:5b1bf3c2c2dca738235ce08079783ef04f1a7fc5b21cf24adaae77f2da4e73c3", 119 | "sha256:6056b671aeda3fc451382e52ab8a753c0d5f66ef2a5ccc8fa5ba7abd20988b4d", 120 | "sha256:68d78cf4a9dfade2e6cf57c4be19f7b82ed66e67dacf93b32bb390c9bed12749", 121 | "sha256:7025c639ce7e170db845e94006cf5f404e243e6fc00d6c86fa19e8ad8d411880", 122 | "sha256:7224e126c00b8178dfd227bc337ba5e754b197a3867d33b9f30dc0208f773d70", 123 | "sha256:7d98e0785c4cd7ae30b4a451416db71f5724a1839025544b4edbd92e00b91f0f", 124 | "sha256:8d8c21e9d4efef01351bf28513648ceb988031be4159745a7ad1b3e28c8ff68a", 125 | "sha256:bbb545da054e6297242a1bb1ba88e7a8ffb679f518258d66798ec712b82e4e07", 126 | "sha256:d00b393f05dbd4ecd65c989b7f5a81110eae4baea7a6a4cdd94c20a908d1456e", 127 | "sha256:e18752cecaef61031252ca72031d4d6247b3212ebb84748fc5d1a0d2029c23ea" 128 | ], 129 | "index": "pypi", 130 | "version": "==1.3.16" 131 | }, 132 | "starlette": { 133 | "hashes": [ 134 | "sha256:3c8e48e52736b3161e34c9f0e8153b4f32ec5d8995a3ee1d59410d92f75162ed", 135 | "sha256:7d49f4a27f8742262ef1470608c59ddbc66baf37c148e938c7038e6bc7a998aa" 136 | ], 137 | "markers": "python_version >= '3.6'", 138 | "version": "==0.14.2" 139 | }, 140 | "typing-extensions": { 141 | "hashes": [ 142 | "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e", 143 | "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7", 144 | "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34" 145 | ], 146 | "version": "==3.10.0.2" 147 | }, 148 | "uvicorn": { 149 | "hashes": [ 150 | "sha256:1d46a22cc55a52f5567e0c66f000ae56f26263e44cef59b7c885bf10f487ce6e", 151 | "sha256:b50f7f4c0c499c9b8d0280924cfbd24b90ba02456e3dc80934b9a786a291f09f" 152 | ], 153 | "index": "pypi", 154 | "version": "==0.11.7" 155 | }, 156 | "uvloop": { 157 | "hashes": [ 158 | "sha256:04ff57aa137230d8cc968f03481176041ae789308b4d5079118331ab01112450", 159 | "sha256:089b4834fd299d82d83a25e3335372f12117a7d38525217c2258e9b9f4578897", 160 | "sha256:1e5f2e2ff51aefe6c19ee98af12b4ae61f5be456cd24396953244a30880ad861", 161 | "sha256:30ba9dcbd0965f5c812b7c2112a1ddf60cf904c1c160f398e7eed3a6b82dcd9c", 162 | "sha256:3a19828c4f15687675ea912cc28bbcb48e9bb907c801873bd1519b96b04fb805", 163 | "sha256:6224f1401025b748ffecb7a6e2652b17768f30b1a6a3f7b44660e5b5b690b12d", 164 | "sha256:647e481940379eebd314c00440314c81ea547aa636056f554d491e40503c8464", 165 | "sha256:6ccd57ae8db17d677e9e06192e9c9ec4bd2066b77790f9aa7dede2cc4008ee8f", 166 | "sha256:772206116b9b57cd625c8a88f2413df2fcfd0b496eb188b82a43bed7af2c2ec9", 167 | "sha256:8e0d26fa5875d43ddbb0d9d79a447d2ace4180d9e3239788208527c4784f7cab", 168 | "sha256:98d117332cc9e5ea8dfdc2b28b0a23f60370d02e1395f88f40d1effd2cb86c4f", 169 | "sha256:b572256409f194521a9895aef274cea88731d14732343da3ecdb175228881638", 170 | "sha256:bd53f7f5db562f37cd64a3af5012df8cac2c464c97e732ed556800129505bd64", 171 | "sha256:bd8f42ea1ea8f4e84d265769089964ddda95eb2bb38b5cbe26712b0616c3edee", 172 | "sha256:e814ac2c6f9daf4c36eb8e85266859f42174a4ff0d71b99405ed559257750382", 173 | "sha256:f74bc20c7b67d1c27c72601c78cf95be99d5c2cdd4514502b4f3eb0933ff1228" 174 | ], 175 | "markers": "sys_platform != 'win32' and sys_platform != 'cygwin' and platform_python_implementation != 'PyPy'", 176 | "version": "==0.16.0" 177 | }, 178 | "websockets": { 179 | "hashes": [ 180 | "sha256:0e4fb4de42701340bd2353bb2eee45314651caa6ccee80dbd5f5d5978888fed5", 181 | "sha256:1d3f1bf059d04a4e0eb4985a887d49195e15ebabc42364f4eb564b1d065793f5", 182 | "sha256:20891f0dddade307ffddf593c733a3fdb6b83e6f9eef85908113e628fa5a8308", 183 | "sha256:295359a2cc78736737dd88c343cd0747546b2174b5e1adc223824bcaf3e164cb", 184 | "sha256:2db62a9142e88535038a6bcfea70ef9447696ea77891aebb730a333a51ed559a", 185 | "sha256:3762791ab8b38948f0c4d281c8b2ddfa99b7e510e46bd8dfa942a5fff621068c", 186 | "sha256:3db87421956f1b0779a7564915875ba774295cc86e81bc671631379371af1170", 187 | "sha256:3ef56fcc7b1ff90de46ccd5a687bbd13a3180132268c4254fc0fa44ecf4fc422", 188 | "sha256:4f9f7d28ce1d8f1295717c2c25b732c2bc0645db3215cf757551c392177d7cb8", 189 | "sha256:5c01fd846263a75bc8a2b9542606927cfad57e7282965d96b93c387622487485", 190 | "sha256:5c65d2da8c6bce0fca2528f69f44b2f977e06954c8512a952222cea50dad430f", 191 | "sha256:751a556205d8245ff94aeef23546a1113b1dd4f6e4d102ded66c39b99c2ce6c8", 192 | "sha256:7ff46d441db78241f4c6c27b3868c9ae71473fe03341340d2dfdbe8d79310acc", 193 | "sha256:965889d9f0e2a75edd81a07592d0ced54daa5b0785f57dc429c378edbcffe779", 194 | "sha256:9b248ba3dd8a03b1a10b19efe7d4f7fa41d158fdaa95e2cf65af5a7b95a4f989", 195 | "sha256:9bef37ee224e104a413f0780e29adb3e514a5b698aabe0d969a6ba426b8435d1", 196 | "sha256:c1ec8db4fac31850286b7cd3b9c0e1b944204668b8eb721674916d4e28744092", 197 | "sha256:c8a116feafdb1f84607cb3b14aa1418424ae71fee131642fc568d21423b51824", 198 | "sha256:ce85b06a10fc65e6143518b96d3dca27b081a740bae261c2fb20375801a9d56d", 199 | "sha256:d705f8aeecdf3262379644e4b55107a3b55860eb812b673b28d0fbc347a60c55", 200 | "sha256:e898a0863421650f0bebac8ba40840fc02258ef4714cb7e1fd76b6a6354bda36", 201 | "sha256:f8a7bff6e8664afc4e6c28b983845c5bc14965030e3fb98789734d416af77c4b" 202 | ], 203 | "markers": "python_full_version >= '3.6.1'", 204 | "version": "==8.1" 205 | } 206 | }, 207 | "develop": {} 208 | } 209 | --------------------------------------------------------------------------------