├── backend ├── app │ ├── __init__.py │ ├── core │ │ ├── __init__.py │ │ └── security.py │ ├── crud │ │ ├── __init__.py │ │ ├── crud_user.py │ │ └── crud_product.py │ ├── models │ │ ├── __init__.py │ │ ├── model_user.py │ │ ├── base.py │ │ └── model_product.py │ ├── routers │ │ ├── __init__.py │ │ ├── accounts.py │ │ ├── authentication.py │ │ └── products.py │ ├── schemas │ │ ├── __init__.py │ │ ├── pyd_token.py │ │ ├── pyd_user.py │ │ └── pyd_product.py │ ├── utils │ │ ├── __init__.py │ │ └── zapiex.py │ ├── database.py │ ├── settings.py │ ├── main.py │ └── dependencies.py ├── alembic │ ├── README │ ├── script.py.mako │ ├── versions │ │ ├── 506cf1588def_add_column_price.py │ │ ├── e3662d962406_fixed_hashed_password_column_and_rename_.py │ │ ├── 005a8708d74d_changed_from_product_to_information_in_.py │ │ ├── 9da9ef9e0ad5_add_email_column_in_users_table.py │ │ ├── e833149c7595_dropped_user_id_column_and_email_column.py │ │ ├── 1c1cf1730b14_update_update_dt.py │ │ ├── a6f78acbd1c0_create_users_table.py │ │ ├── d7fb708c25b2_added_create_dt_update_dt_fields.py │ │ ├── 908530dcb166_restore_some_columns.py │ │ ├── e5381fcd65d0_changed_index_and_type_in_product_.py │ │ ├── d6d6cb7eee1d_first_migration.py │ │ └── d88bf9ac6f99_changed_column_names_in_productdetail_.py │ └── env.py ├── Procfile ├── .gitignore ├── Pipfile ├── alembic.ini └── Pipfile.lock ├── frontend ├── styles │ └── globals.css ├── Procfile ├── .eslintrc.json ├── public │ └── images │ │ ├── prepare.png │ │ ├── AED_logo.png │ │ ├── github_logo.png │ │ ├── notion_logo.png │ │ ├── tistory_logo.png │ │ ├── review_red_star.png │ │ └── review_empty_star.png ├── next.config.js ├── next-env.d.ts ├── pages │ ├── api │ │ └── hello.ts │ ├── aboutus.tsx │ ├── _app.tsx │ ├── index.tsx │ ├── signup.tsx │ ├── signin.tsx │ ├── products.tsx │ └── product │ │ └── [product_id].tsx ├── src │ └── components │ │ ├── layout.tsx │ │ ├── autocomplete.tsx │ │ ├── footer.tsx │ │ ├── header.tsx │ │ └── pagination.tsx ├── .gitignore ├── tsconfig.json ├── package.json └── README.md ├── AED_ERD.png ├── .vscode ├── settings.json └── launch.json ├── .gitignore └── README.md /backend/app/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/app/core/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/app/crud/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/styles/globals.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/app/models/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/app/routers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/app/schemas/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/app/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/Procfile: -------------------------------------------------------------------------------- 1 | web: npm run start -------------------------------------------------------------------------------- /backend/alembic/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /backend/Procfile: -------------------------------------------------------------------------------- 1 | web: uvicorn app.main:app --host=0.0.0.0 --port=${PORT:-5000} -------------------------------------------------------------------------------- /frontend/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /AED_ERD.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veluga29/Ali_Express_Dropshipping/HEAD/AED_ERD.png -------------------------------------------------------------------------------- /frontend/public/images/prepare.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veluga29/Ali_Express_Dropshipping/HEAD/frontend/public/images/prepare.png -------------------------------------------------------------------------------- /frontend/public/images/AED_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veluga29/Ali_Express_Dropshipping/HEAD/frontend/public/images/AED_logo.png -------------------------------------------------------------------------------- /frontend/public/images/github_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veluga29/Ali_Express_Dropshipping/HEAD/frontend/public/images/github_logo.png -------------------------------------------------------------------------------- /frontend/public/images/notion_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veluga29/Ali_Express_Dropshipping/HEAD/frontend/public/images/notion_logo.png -------------------------------------------------------------------------------- /frontend/public/images/tistory_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veluga29/Ali_Express_Dropshipping/HEAD/frontend/public/images/tistory_logo.png -------------------------------------------------------------------------------- /frontend/public/images/review_red_star.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veluga29/Ali_Express_Dropshipping/HEAD/frontend/public/images/review_red_star.png -------------------------------------------------------------------------------- /frontend/public/images/review_empty_star.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veluga29/Ali_Express_Dropshipping/HEAD/frontend/public/images/review_empty_star.png -------------------------------------------------------------------------------- /frontend/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | module.exports = { 3 | reactStrictMode: true, 4 | images: { 5 | domains: ['ae01.alicdn.com'], 6 | }, 7 | } 8 | -------------------------------------------------------------------------------- /backend/app/schemas/pyd_token.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, EmailStr 2 | 3 | 4 | class Token(BaseModel): 5 | access_token: str 6 | token_type: str 7 | 8 | 9 | class TokenPayload(BaseModel): 10 | sub: EmailStr 11 | -------------------------------------------------------------------------------- /frontend/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | 5 | // NOTE: This file should not be edited 6 | // see https://nextjs.org/docs/basic-features/typescript for more information. 7 | -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | *vscode 3 | .vscode/* 4 | !.vscode/settings.json 5 | !.vscode/tasks.json 6 | !.vscode/launch.json 7 | !.vscode/extensions.json 8 | *.code-workspace 9 | 10 | app/.DS_Store 11 | *DS_Store 12 | 13 | # Local History for Visual Studio Code 14 | .history/ 15 | 16 | # Ignore Files 17 | *.pyc 18 | *~ 19 | __pycache__ 20 | /static 21 | secret_bash 22 | -------------------------------------------------------------------------------- /backend/app/database.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import create_engine 2 | from sqlalchemy.ext.declarative import declarative_base 3 | from sqlalchemy.orm import sessionmaker 4 | 5 | from app.settings import DATABASE_URL 6 | 7 | 8 | engine = create_engine(DATABASE_URL) 9 | SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) 10 | 11 | Base = declarative_base() 12 | -------------------------------------------------------------------------------- /frontend/pages/api/hello.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import type { NextApiRequest, NextApiResponse } from 'next' 3 | 4 | type Data = { 5 | name: string 6 | } 7 | 8 | export default function handler( 9 | req: NextApiRequest, 10 | res: NextApiResponse 11 | ) { 12 | res.status(200).json({ name: 'John Doe' }) 13 | } 14 | -------------------------------------------------------------------------------- /frontend/src/components/layout.tsx: -------------------------------------------------------------------------------- 1 | import Header from './header' 2 | import Footer from './footer' 3 | 4 | 5 | export default function Layout({ children }: { children: React.ReactNode }) { 6 | return ( 7 |
8 |
9 |
10 |
11 |
12 | {children} 13 |
14 |
15 |
16 |
17 |
18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /backend/app/models/model_user.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, String 2 | from sqlalchemy_utils import EmailType 3 | 4 | from app.models.base import AbstractBase 5 | 6 | 7 | class User(AbstractBase): 8 | __tablename__ = "users" 9 | 10 | email = Column(EmailType, index=True, unique=True, nullable=False) 11 | password = Column(String(60), nullable=False) 12 | first_name = Column(String(30)) 13 | last_name = Column(String(30)) 14 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.pythonPath": "C:\\Users\\sungyoon\\.virtualenvs\\ali_express_dropshipping-yFSCRtQA\\Scripts\\python.exe", 3 | "python.linting.pylintEnabled": true, 4 | "python.linting.enabled": true, 5 | "python.linting.lintOnSave": true, 6 | "python.formatting.provider": "black", 7 | "python.formatting.blackArgs": [ 8 | "--line-length", 9 | "100" 10 | ], 11 | "editor.formatOnSave": true 12 | } -------------------------------------------------------------------------------- /backend/app/schemas/pyd_user.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | from pydantic import BaseModel, EmailStr 3 | 4 | 5 | class UserBase(BaseModel): 6 | first_name: Optional[str] = None 7 | last_name: Optional[str] = None 8 | 9 | 10 | class UserCreate(UserBase): 11 | email: EmailStr 12 | password: str 13 | 14 | 15 | class UserUpdate(UserBase): 16 | password: Optional[str] = None 17 | 18 | 19 | class User(UserBase): 20 | id: int 21 | email: EmailStr 22 | 23 | class Config: 24 | orm_mode = True 25 | -------------------------------------------------------------------------------- /backend/app/models/base.py: -------------------------------------------------------------------------------- 1 | import sqlalchemy as sa 2 | from sqlalchemy import Column, Integer 3 | from sqlalchemy.dialects import postgresql 4 | 5 | from app.database import Base 6 | 7 | 8 | class AbstractBase(Base): 9 | __abstract__ = True 10 | 11 | id = Column(Integer, primary_key=True, autoincrement=True) 12 | create_dt = Column(postgresql.TIMESTAMP(timezone=True), server_default=sa.func.now()) 13 | update_dt = Column( 14 | postgresql.TIMESTAMP(timezone=True), default=sa.func.now(), onupdate=sa.func.now() 15 | ) 16 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | -------------------------------------------------------------------------------- /backend/app/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | CORS_ORIGIN = os.getenv("CORS_ORIGIN", "") 4 | API_KEY = os.getenv("AED_ZAPIEX_API_KEY", "") 5 | DB_HOST = os.getenv("AED_DB_HOST", "localhost") 6 | DB_PORT = os.getenv("AED_DB_PORT", "5432") 7 | DB_USER_NAME = os.getenv("AED_DB_USER_NAME", "") 8 | DB_USER_PASSWORD = os.getenv("AED_DB_PASSWORD", "") 9 | DB_NAME = os.getenv("AED_DB_NAME", "") 10 | TOKEN_SECRET_KEY = os.getenv("AED_TOKEN_SECRET_KEY", "") 11 | 12 | DATABASE_URL = ( 13 | f"postgresql://{DB_USER_NAME}:" f"{DB_USER_PASSWORD}" f"@{DB_HOST}" f":{DB_PORT}" f"/{DB_NAME}" 14 | ) 15 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2015", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": false, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve" 16 | }, 17 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 18 | "exclude": ["node_modules"] 19 | } 20 | -------------------------------------------------------------------------------- /frontend/pages/aboutus.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image' 2 | import Head from 'next/head' 3 | 4 | import Layout from '../src/components/layout' 5 | 6 | 7 | export default function Aboutus() { 8 | return ( 9 | 10 | 11 | Aboutus 12 | 13 |
14 | Comming soon 19 |
20 |
21 | ) 22 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Python: FastAPI", 9 | "type": "python", 10 | "request": "launch", 11 | "module": "uvicorn", 12 | "args": [ 13 | "main:app", "--reload" 14 | ], 15 | "jinja": true 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /frontend/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import 'bootstrap/dist/css/bootstrap.css' 2 | 3 | import Head from "next/head" 4 | import '../styles/globals.css' 5 | import type { AppProps } from 'next/app' 6 | import { useEffect } from 'react'; 7 | 8 | function MyApp({ Component, pageProps }: AppProps) { 9 | useEffect(() => { 10 | require("bootstrap/dist/js/bootstrap"); 11 | }, []); 12 | 13 | return ( 14 | <> 15 | 16 | 17 | 18 | 19 | 20 | ) 21 | } 22 | export default MyApp 23 | -------------------------------------------------------------------------------- /backend/alembic/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | ${imports if imports else ""} 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = ${repr(up_revision)} 14 | down_revision = ${repr(down_revision)} 15 | branch_labels = ${repr(branch_labels)} 16 | depends_on = ${repr(depends_on)} 17 | 18 | 19 | def upgrade(): 20 | ${upgrades if upgrades else "pass"} 21 | 22 | 23 | def downgrade(): 24 | ${downgrades if downgrades else "pass"} 25 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aed-ui", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start -p $PORT", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@popperjs/core": "^2.10.2", 13 | "axios": "^0.21.4", 14 | "bootstrap": "^5.1.3", 15 | "form-data": "^4.0.0", 16 | "next": "11.1.2", 17 | "react": "17.0.2", 18 | "react-cookie": "^4.1.1", 19 | "react-dom": "17.0.2" 20 | }, 21 | "devDependencies": { 22 | "@types/bootstrap": "^5.1.6", 23 | "@types/react": "17.0.21", 24 | "eslint": "7.32.0", 25 | "eslint-config-next": "11.1.2", 26 | "typescript": "4.4.3" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /backend/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | fastapi = "*" 8 | uvicorn = {extras = ["standard"], version = "*"} 9 | requests = "*" 10 | sqlalchemy = "*" 11 | psycopg2 = "*" 12 | python-multipart = "*" 13 | alembic = "*" 14 | psycopg2-binary = "*" 15 | python-jose = {extras = ["cryptography"], version = "*"} 16 | passlib = {extras = ["bcrypt"], version = "*"} 17 | fastapi-pagination = {extras = ["sqlalchemy"], version = "*"} 18 | sqlalchemy-utils = "*" 19 | pydantic = {extras = ["email"], version = "*"} 20 | pytz = "*" 21 | 22 | [dev-packages] 23 | pylint = "*" 24 | black = "*" 25 | 26 | [requires] 27 | python_version = "3.9" 28 | 29 | [pipenv] 30 | allow_prereleases = true 31 | -------------------------------------------------------------------------------- /backend/alembic/versions/506cf1588def_add_column_price.py: -------------------------------------------------------------------------------- 1 | """Add column price 2 | 3 | Revision ID: 506cf1588def 4 | Revises: 908530dcb166 5 | Create Date: 2021-07-09 14:03:44.310681 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '506cf1588def' 14 | down_revision = '908530dcb166' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.add_column('product_details', sa.Column('price', sa.JSON(), nullable=True)) 22 | # ### end Alembic commands ### 23 | 24 | 25 | def downgrade(): 26 | # ### commands auto generated by Alembic - please adjust! ### 27 | op.drop_column('product_details', 'price') 28 | # ### end Alembic commands ### 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | *vscode 3 | .vscode/* 4 | !.vscode/settings.json 5 | !.vscode/tasks.json 6 | !.vscode/launch.json 7 | !.vscode/extensions.json 8 | *.code-workspace 9 | 10 | app/.DS_Store 11 | *DS_Store 12 | 13 | # Local History for Visual Studio Code 14 | .history/ 15 | 16 | # Ignore Files 17 | *.pyc 18 | *~ 19 | __pycache__ 20 | /static 21 | secret_bash 22 | 23 | 24 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 25 | 26 | # dependencies 27 | /node_modules 28 | /.pnp 29 | .pnp.js 30 | 31 | # testing 32 | /coverage 33 | 34 | # next.js 35 | /.next/ 36 | /out/ 37 | 38 | # production 39 | /build 40 | 41 | # misc 42 | .DS_Store 43 | *.pem 44 | 45 | # debug 46 | npm-debug.log* 47 | yarn-debug.log* 48 | yarn-error.log* 49 | 50 | # local env files 51 | .env.local 52 | .env.development.local 53 | .env.test.local 54 | .env.production.local 55 | 56 | # vercel 57 | .vercel 58 | -------------------------------------------------------------------------------- /backend/app/main.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | 3 | # from fastapi_pagination import add_pagination 4 | from fastapi.middleware.cors import CORSMiddleware 5 | 6 | from app.database import Base, engine 7 | from app.routers import accounts, authentication, products 8 | from app.settings import CORS_ORIGIN 9 | 10 | 11 | app = FastAPI() 12 | 13 | origins = eval(CORS_ORIGIN) 14 | 15 | app.add_middleware( 16 | CORSMiddleware, 17 | allow_origins=origins, 18 | allow_methods=["*"], 19 | allow_headers=["*"], 20 | expose_headers=["*"], 21 | ) 22 | 23 | # 데이터 베이스 이니셜라이즈 24 | Base.metadata.create_all(bind=engine) 25 | 26 | # 라우터 정의 27 | app.include_router(products.router) 28 | app.include_router(authentication.router) 29 | app.include_router(accounts.router) 30 | 31 | # add_pagination(app) 32 | 33 | 34 | @app.get("/") 35 | async def root(): 36 | return {"message": "Welcome to Ali Express Dropshipping service!"} 37 | -------------------------------------------------------------------------------- /backend/app/utils/zapiex.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | from app.settings import API_KEY 4 | 5 | 6 | class ZapiexAPI: 7 | headers = {"x-api-key": API_KEY} 8 | 9 | def search_products(self, text: str, page: int): 10 | URL = "https://api.zapiex.com/v3/search" 11 | data = {"text": text, "shipTo": "KR", "page": page} 12 | response = requests.post(URL, headers=ZapiexAPI.headers, data=json.dumps(data)) 13 | return response.json() 14 | 15 | def get_product(self, product_id: str): 16 | URL = "https://api.zapiex.com/v3/product/details" 17 | data = { 18 | "productId": product_id, 19 | "shipTo": "KR", 20 | "getSellerDetails": "true", 21 | "getShipping": "true", 22 | "getHtmlDescription": "true", 23 | } 24 | response = requests.post(URL, headers=ZapiexAPI.headers, data=data) 25 | return response.json() 26 | 27 | 28 | zapiex_apis = ZapiexAPI() 29 | -------------------------------------------------------------------------------- /backend/alembic/versions/e3662d962406_fixed_hashed_password_column_and_rename_.py: -------------------------------------------------------------------------------- 1 | """Fixed hashed_password column and rename to password 2 | 3 | Revision ID: e3662d962406 4 | Revises: 9da9ef9e0ad5 5 | Create Date: 2021-07-28 00:43:31.016334 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "e3662d962406" 14 | down_revision = "9da9ef9e0ad5" 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.add_column("users", sa.Column("password", sa.String(length=60), nullable=False)) 22 | op.drop_column("users", "hashed_password") 23 | # ### end Alembic commands ### 24 | 25 | 26 | def downgrade(): 27 | # ### commands auto generated by Alembic - please adjust! ### 28 | op.add_column( 29 | "users", 30 | sa.Column("hashed_password", sa.VARCHAR(length=200), autoincrement=False, nullable=True), 31 | ) 32 | op.drop_column("users", "password") 33 | # ### end Alembic commands ### 34 | -------------------------------------------------------------------------------- /backend/alembic/versions/005a8708d74d_changed_from_product_to_information_in_.py: -------------------------------------------------------------------------------- 1 | """Changed from product to information in product_list 2 | 3 | Revision ID: 005a8708d74d 4 | Revises: d7fb708c25b2 5 | Create Date: 2021-07-03 17:33:38.064234 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | from sqlalchemy.dialects import postgresql 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '005a8708d74d' 14 | down_revision = 'd7fb708c25b2' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.add_column('product_lists', sa.Column('information', sa.JSON(), nullable=True)) 22 | op.drop_column('product_lists', 'product') 23 | # ### end Alembic commands ### 24 | 25 | 26 | def downgrade(): 27 | # ### commands auto generated by Alembic - please adjust! ### 28 | op.add_column('product_lists', sa.Column('product', postgresql.JSON(astext_type=sa.Text()), autoincrement=False, nullable=True)) 29 | op.drop_column('product_lists', 'information') 30 | # ### end Alembic commands ### 31 | -------------------------------------------------------------------------------- /frontend/src/components/autocomplete.tsx: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { useEffect, useState } from 'react'; 3 | import { useCookies } from 'react-cookie'; 4 | 5 | export default function Autocomplete({ searchText }) { 6 | const [ cookies ] = useCookies(["access_token"]); 7 | let access_token = cookies.access_token; 8 | let [ textData, setTextData ]= useState([]); 9 | const autocompleteSearchText = async () => { 10 | try { 11 | const params = { search: searchText } 12 | const headers = { 13 | Authorization: `bearer ${access_token}` 14 | } 15 | const response = await axios.get(`${process.env.NEXT_PUBLIC_API_URL}/products/search`, { params, headers}); 16 | setTextData(response.data); 17 | } catch (error) { 18 | } 19 | }; 20 | useEffect(() => { 21 | if (!searchText) { 22 | return; 23 | } 24 | autocompleteSearchText(); 25 | }, [searchText]); 26 | 27 | return ( 28 | 29 | { 30 | textData && textData.map(({ text }) => { 31 | return ( 32 | 37 | ) 38 | } -------------------------------------------------------------------------------- /backend/alembic/versions/9da9ef9e0ad5_add_email_column_in_users_table.py: -------------------------------------------------------------------------------- 1 | """Add email column in users table 2 | 3 | Revision ID: 9da9ef9e0ad5 4 | Revises: e833149c7595 5 | Create Date: 2021-07-27 18:09:44.161450 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | import sqlalchemy_utils 11 | 12 | 13 | # revision identifiers, used by Alembic. 14 | revision = "9da9ef9e0ad5" 15 | down_revision = "e833149c7595" 16 | branch_labels = None 17 | depends_on = None 18 | 19 | 20 | def upgrade(): 21 | # ### commands auto generated by Alembic - please adjust! ### 22 | op.add_column( 23 | "users", 24 | sa.Column("email", sqlalchemy_utils.types.email.EmailType(length=255), nullable=False), 25 | ) 26 | op.create_index(op.f("ix_users_email"), "users", ["email"], unique=True) 27 | op.create_unique_constraint("uq_users_email", "users", ["email"]) 28 | # ### end Alembic commands ### 29 | 30 | 31 | def downgrade(): 32 | # ### commands auto generated by Alembic - please adjust! ### 33 | op.drop_constraint("uq_users_email", "users", type_="unique") 34 | op.drop_index(op.f("ix_users_email"), table_name="users") 35 | op.drop_column("users", "email") 36 | # ### end Alembic commands ### 37 | -------------------------------------------------------------------------------- /backend/app/crud/crud_user.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm import Session 2 | 3 | from app.core.security import get_password_hash 4 | from app.models import model_user 5 | from app.schemas import pyd_user 6 | 7 | 8 | # accounts 9 | def get_user_by_email(db: Session, email: pyd_user.EmailStr): 10 | return db.query(model_user.User).filter_by(email=email).first() 11 | 12 | 13 | def create_user(db: Session, user: pyd_user.UserCreate): 14 | hashed_password = get_password_hash(user.password) 15 | db_user = model_user.User( 16 | email=user.email, 17 | password=hashed_password, 18 | first_name=user.first_name, 19 | last_name=user.last_name, 20 | ) 21 | db.add(db_user) 22 | db.commit() 23 | return db_user 24 | 25 | 26 | def update_user(db: Session, db_user: model_user.User, update_data: pyd_user.UserUpdate): 27 | if update_data.password is not None: 28 | update_data.password = get_password_hash(update_data.password) 29 | update_data_dict = update_data.dict(exclude_unset=True) 30 | for key, value in update_data_dict.items(): 31 | setattr(db_user, key, value) 32 | db.commit() 33 | db.refresh(db_user) 34 | return db_user 35 | 36 | 37 | def delete_user(db: Session, db_user: model_user.User): 38 | db.delete(db_user) 39 | db.commit() 40 | return db_user 41 | -------------------------------------------------------------------------------- /backend/app/dependencies.py: -------------------------------------------------------------------------------- 1 | from fastapi import Depends, status 2 | from fastapi.exceptions import HTTPException 3 | from fastapi.security import OAuth2PasswordBearer 4 | from jose import jwt 5 | from sqlalchemy.orm import Session 6 | from pydantic import ValidationError 7 | 8 | from app.crud.crud_user import get_user_by_email 9 | from app.core.security import TOKEN_ALGORITHM 10 | from app.schemas import pyd_token 11 | from app.database import SessionLocal 12 | from app.settings import TOKEN_SECRET_KEY 13 | 14 | 15 | oauth2_scheme = OAuth2PasswordBearer(tokenUrl="aaa/token") 16 | 17 | 18 | def get_db(): 19 | db = SessionLocal() 20 | try: 21 | yield db 22 | finally: 23 | db.close() 24 | 25 | 26 | def get_current_user(db: Session = Depends(get_db), token: str = Depends(oauth2_scheme)): 27 | try: 28 | payload = jwt.decode(token, TOKEN_SECRET_KEY, algorithms=[TOKEN_ALGORITHM]) 29 | token_data = pyd_token.TokenPayload(**payload) 30 | except (jwt.JWTError, ValidationError): 31 | raise HTTPException( 32 | status_code=status.HTTP_403_FORBIDDEN, 33 | detail="Could not validate credentials", 34 | ) 35 | user = get_user_by_email(db, email=token_data.sub) 36 | if not user: 37 | raise HTTPException(status_code=404, detail="User not found") 38 | return user 39 | -------------------------------------------------------------------------------- /backend/app/core/security.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm import Session 2 | from jose import jwt 3 | from passlib.context import CryptContext 4 | from datetime import datetime, timedelta 5 | from typing import Optional 6 | 7 | from app.models import model_user 8 | 9 | 10 | TOKEN_ALGORITHM = "HS256" 11 | 12 | # login_for_access_token 13 | pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") 14 | 15 | 16 | def verify_password(plain_password, hashed_password): 17 | return pwd_context.verify(plain_password, hashed_password) 18 | 19 | 20 | def get_password_hash(password): 21 | return pwd_context.hash(password) 22 | 23 | 24 | def authenticate_user(db: Session, email: str, password: str): 25 | user = db.query(model_user.User).filter_by(email=email).first() 26 | if not user: 27 | return False 28 | if not verify_password(password, user.password): 29 | return False 30 | return user 31 | 32 | 33 | def create_access_token( 34 | data: dict, 35 | token_secret_key: str, 36 | token_algorithm: str, 37 | expires_delta: Optional[timedelta] = None, 38 | ): 39 | to_encode = data.copy() 40 | if expires_delta: 41 | expire = datetime.utcnow() + expires_delta 42 | else: 43 | expire = datetime.utcnow() + timedelta(minutes=15) 44 | to_encode.update({"exp": expire}) 45 | encoded_jwt = jwt.encode(to_encode, token_secret_key, algorithm=token_algorithm) 46 | return encoded_jwt 47 | -------------------------------------------------------------------------------- /backend/alembic/versions/e833149c7595_dropped_user_id_column_and_email_column.py: -------------------------------------------------------------------------------- 1 | """Dropped user_id column and email column 2 | 3 | Revision ID: e833149c7595 4 | Revises: a6f78acbd1c0 5 | Create Date: 2021-07-27 18:07:43.312987 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'e833149c7595' 14 | down_revision = 'a6f78acbd1c0' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.drop_index('ix_users_user_id', table_name='users') 22 | op.drop_constraint('uq_users_email', 'users', type_='unique') 23 | op.drop_constraint('uq_users_user_id', 'users', type_='unique') 24 | op.drop_column('users', 'user_id') 25 | op.drop_column('users', 'email') 26 | # ### end Alembic commands ### 27 | 28 | 29 | def downgrade(): 30 | # ### commands auto generated by Alembic - please adjust! ### 31 | op.add_column('users', sa.Column('email', sa.VARCHAR(length=120), autoincrement=False, nullable=True)) 32 | op.add_column('users', sa.Column('user_id', sa.VARCHAR(length=100), autoincrement=False, nullable=True)) 33 | op.create_unique_constraint('uq_users_user_id', 'users', ['user_id']) 34 | op.create_unique_constraint('uq_users_email', 'users', ['email']) 35 | op.create_index('ix_users_user_id', 'users', ['user_id'], unique=False) 36 | # ### end Alembic commands ### 37 | -------------------------------------------------------------------------------- /frontend/src/components/footer.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image' 2 | 3 | 4 | export default function Footer() { 5 | return ( 6 |
7 |
8 | 9 | Github 15 | 16 |      17 | 18 | Tistory 24 | 25 |
26 |
27 |
Personal project:
28 |
29 |
30 |
31 |
32 |

Ali Express Dropshipping (AED)

33 |
34 |
35 | made by SungYoon Cho 36 |
37 |
38 |
39 |
40 |

41 | ⓒ Copyright 2021 - 2028. 42 |
43 | All rights reserved. Powered by Lucian. 44 |

45 |
46 |
47 | ) 48 | } -------------------------------------------------------------------------------- /backend/app/routers/accounts.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Depends, status 2 | from fastapi.exceptions import HTTPException 3 | from sqlalchemy.orm import Session 4 | 5 | from app.crud import crud_user 6 | from app.schemas import pyd_user 7 | from app.models import model_user 8 | from app.dependencies import get_db, get_current_user 9 | 10 | 11 | router = APIRouter(prefix="/me", tags=["me"]) 12 | 13 | 14 | @router.post("/", response_model=pyd_user.User) 15 | def create_me(user: pyd_user.UserCreate, db: Session = Depends(get_db)): 16 | db_user = crud_user.get_user_by_email(db, email=user.email) 17 | if db_user: 18 | raise HTTPException( 19 | status_code=status.HTTP_400_BAD_REQUEST, detail="Email already registered" 20 | ) 21 | return crud_user.create_user(db=db, user=user) 22 | 23 | 24 | @router.get("/", response_model=pyd_user.User) 25 | def read_me(current_user: model_user.User = Depends(get_current_user)): 26 | return current_user 27 | 28 | 29 | @router.patch("/", response_model=pyd_user.User) 30 | def update_me( 31 | update_data: pyd_user.UserUpdate, 32 | current_user: model_user.User = Depends(get_current_user), 33 | db: Session = Depends(get_db), 34 | ): 35 | return crud_user.update_user(db, db_user=current_user, update_data=update_data) 36 | 37 | 38 | @router.delete("/", response_model=pyd_user.User) 39 | def delete_me( 40 | current_user: model_user.User = Depends(get_current_user), db: Session = Depends(get_db) 41 | ): 42 | return crud_user.delete_user(db, db_user=current_user) 43 | -------------------------------------------------------------------------------- /backend/app/schemas/pyd_product.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | from pydantic import BaseModel 3 | 4 | 5 | # search text 6 | class SearchText(BaseModel): 7 | text: str 8 | 9 | 10 | class SearchTextOutput(SearchText): 11 | class Config: 12 | orm_mode = True 13 | 14 | 15 | # search products 16 | class ProductList(BaseModel): 17 | id: int 18 | information: dict 19 | search_text_id: int 20 | 21 | class Config: 22 | orm_mode = True 23 | 24 | 25 | # product details 26 | class ProductDetail(BaseModel): 27 | id: int 28 | productUrl: str 29 | productId: str 30 | statusId: str 31 | status: str 32 | currency: str 33 | locale: str 34 | shipTo: str 35 | title: str 36 | totalStock: int 37 | totalOrders: int 38 | wishlistCount: int 39 | unitName: str 40 | unitNamePlural: str 41 | unitsPerProduct: int 42 | hasPurchaseLimit: bool 43 | maxPurchaseLimit: Optional[int] 44 | processingTimeInDays: int 45 | productImages: List[str] 46 | productCategory: dict 47 | seller: dict 48 | sellerDetails: dict 49 | hasSinglePrice: bool 50 | priceSummary: Optional[dict] 51 | price: Optional[dict] 52 | hasAttributes: bool 53 | attributes: List[dict] 54 | hasReviewsRatings: bool 55 | reviewsRatings: dict 56 | hasProperties: bool 57 | properties: List[dict] 58 | hasVariations: bool 59 | variations: List[dict] 60 | shipping: dict 61 | htmlDescription: str 62 | 63 | class Config: 64 | orm_mode = True 65 | -------------------------------------------------------------------------------- /frontend/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from 'next' 2 | import Head from 'next/head' 3 | import { useRouter } from 'next/router' 4 | 5 | import axios from 'axios' 6 | import { useEffect } from 'react' 7 | import { useCookies } from "react-cookie" 8 | 9 | 10 | const Home: NextPage = () => { 11 | const router = useRouter(); 12 | const [ cookies, ,removeCookie ] = useCookies(["access_token"]); 13 | let access_token = cookies.access_token; 14 | 15 | useEffect(() => { 16 | if (!access_token) { 17 | router.push('/signin'); 18 | return; 19 | } 20 | const verifyToken = async () => { 21 | try{ 22 | const response = await axios.get(`${process.env.NEXT_PUBLIC_API_URL}/aaa/token`, { 23 | headers: { 24 | Authorization: `bearer ${access_token}` 25 | } 26 | }); 27 | if (response.data.valid) { 28 | router.push('/products'); 29 | } 30 | } catch (error) { 31 | // Delete access token cookie 32 | removeCookie('access_token'); 33 | router.push('/signin'); 34 | } 35 | } 36 | verifyToken(); 37 | }); 38 | 39 | return ( 40 |
41 | 42 | Index 43 | 44 |
45 | Loading... 46 |
47 |
48 | ) 49 | } 50 | 51 | export default Home 52 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | ``` 12 | 13 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 14 | 15 | You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. 16 | 17 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`. 18 | 19 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 20 | 21 | ## Learn More 22 | 23 | To learn more about Next.js, take a look at the following resources: 24 | 25 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 26 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 27 | 28 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 29 | 30 | ## Deploy on Vercel 31 | 32 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 33 | 34 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 35 | -------------------------------------------------------------------------------- /backend/app/routers/authentication.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Depends, status 2 | from fastapi.exceptions import HTTPException 3 | from fastapi.security import OAuth2PasswordRequestForm 4 | from sqlalchemy.orm import Session 5 | 6 | from app.core.security import authenticate_user, create_access_token, TOKEN_ALGORITHM 7 | from app.schemas import pyd_token 8 | from app.dependencies import get_current_user, get_db 9 | from app.settings import TOKEN_SECRET_KEY 10 | 11 | from datetime import timedelta 12 | 13 | 14 | ACCESS_TOKEN_EXPIRE_DAYS = 1 15 | 16 | 17 | router = APIRouter(prefix="/aaa", tags=["aaa"]) 18 | 19 | 20 | @router.post("/token", response_model=pyd_token.Token) 21 | def login_for_access_token( 22 | form_data: OAuth2PasswordRequestForm = Depends(), 23 | db: Session = Depends(get_db), 24 | ): 25 | user = authenticate_user(db, email=form_data.username, password=form_data.password) 26 | if not user: 27 | raise HTTPException( 28 | status_code=status.HTTP_401_UNAUTHORIZED, 29 | detail="Incorrect email or password", 30 | headers={"WWW-Authenticate": "Bearer"}, 31 | ) 32 | access_token_expires = timedelta(days=ACCESS_TOKEN_EXPIRE_DAYS) 33 | access_token = create_access_token( 34 | data={"sub": user.email}, 35 | token_secret_key=TOKEN_SECRET_KEY, 36 | token_algorithm=TOKEN_ALGORITHM, 37 | expires_delta=access_token_expires, 38 | ) 39 | 40 | return {"access_token": access_token, "token_type": "bearer"} 41 | 42 | 43 | @router.get("/token", dependencies=[Depends(get_current_user)]) 44 | def verify_access_token(): 45 | try: 46 | return {"valid": True} 47 | except Exception: 48 | raise 49 | -------------------------------------------------------------------------------- /backend/alembic/versions/1c1cf1730b14_update_update_dt.py: -------------------------------------------------------------------------------- 1 | """update update_dt 2 | 3 | Revision ID: 1c1cf1730b14 4 | Revises: 506cf1588def 5 | Create Date: 2021-07-11 19:42:20.748324 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | from sqlalchemy.dialects import postgresql 11 | 12 | 13 | # revision identifiers, used by Alembic. 14 | revision = "1c1cf1730b14" 15 | down_revision = "506cf1588def" 16 | branch_labels = None 17 | depends_on = None 18 | 19 | 20 | def upgrade(): 21 | # ### commands auto generated by Alembic - please adjust! ### 22 | op.drop_column("product_lists", "update_dt") 23 | op.add_column( 24 | "product_lists", 25 | sa.Column( 26 | "update_dt", 27 | postgresql.TIMESTAMP(timezone=True), 28 | nullable=True, 29 | onupdate=sa.text("now()"), 30 | ), 31 | ) 32 | op.drop_column("search_texts", "update_dt") 33 | op.add_column( 34 | "search_texts", 35 | sa.Column( 36 | "update_dt", 37 | postgresql.TIMESTAMP(timezone=True), 38 | nullable=True, 39 | onupdate=sa.text("now()"), 40 | ), 41 | ) 42 | op.drop_column("product_details", "update_dt") 43 | op.add_column( 44 | "product_details", 45 | sa.Column( 46 | "update_dt", 47 | postgresql.TIMESTAMP(timezone=True), 48 | nullable=True, 49 | onupdate=sa.text("now()"), 50 | ), 51 | ) 52 | # ### end Alembic commands ### 53 | 54 | 55 | def downgrade(): 56 | # ### commands auto generated by Alembic - please adjust! ### 57 | op.drop_column("product_lists", "update_dt") 58 | op.drop_column("search_texts", "update_dt") 59 | op.drop_column("product_details", "update_dt") 60 | # ### end Alembic commands ### 61 | -------------------------------------------------------------------------------- /backend/alembic/versions/a6f78acbd1c0_create_users_table.py: -------------------------------------------------------------------------------- 1 | """Create users table 2 | 3 | Revision ID: a6f78acbd1c0 4 | Revises: 1c1cf1730b14 5 | Create Date: 2021-07-20 01:10:58.789896 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | from sqlalchemy.dialects import postgresql 11 | 12 | 13 | # revision identifiers, used by Alembic. 14 | revision = "a6f78acbd1c0" 15 | down_revision = "1c1cf1730b14" 16 | branch_labels = None 17 | depends_on = None 18 | 19 | 20 | def upgrade(): 21 | # ### commands auto generated by Alembic - please adjust! ### 22 | op.create_table( 23 | "users", 24 | sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), 25 | sa.Column( 26 | "create_dt", 27 | postgresql.TIMESTAMP(timezone=True), 28 | server_default=sa.text("now()"), 29 | nullable=True, 30 | ), 31 | sa.Column("update_dt", postgresql.TIMESTAMP(timezone=True), nullable=True), 32 | sa.Column("user_id", sa.String(length=100)), 33 | sa.Column("email", sa.String(length=120)), 34 | sa.Column("hashed_password", sa.String(length=200)), 35 | sa.Column("first_name", sa.String(length=30)), 36 | sa.Column("last_name", sa.String(length=30)), 37 | sa.PrimaryKeyConstraint("id"), 38 | ) 39 | op.create_index(op.f("ix_users_user_id"), "users", ["user_id"], unique=False) 40 | op.create_unique_constraint("uq_users_user_id", "users", ["user_id"]) 41 | op.create_unique_constraint("uq_users_email", "users", ["email"]) 42 | # ### end Alembic commands ### 43 | 44 | 45 | def downgrade(): 46 | # ### commands auto generated by Alembic - please adjust! ### 47 | op.drop_index(op.f("ix_users_user_id"), table_name="users") 48 | op.drop_constraint("uq_users_user_id", "user_id", type_="unique") 49 | op.drop_constraint("uq_users_email", "email", type_="unique") 50 | op.drop_table("users") 51 | # ### end Alembic commands ### 52 | -------------------------------------------------------------------------------- /backend/alembic/versions/d7fb708c25b2_added_create_dt_update_dt_fields.py: -------------------------------------------------------------------------------- 1 | """added create_dt update_dt fields 2 | 3 | Revision ID: d7fb708c25b2 4 | Revises: d6d6cb7eee1d 5 | Create Date: 2021-07-03 16:34:40.807677 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | from sqlalchemy.dialects import postgresql 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "d7fb708c25b2" 14 | down_revision = "d6d6cb7eee1d" 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.add_column( 22 | "product_lists", 23 | sa.Column( 24 | "create_dt", 25 | postgresql.TIMESTAMP(timezone=True), 26 | server_default=sa.text("now()"), 27 | nullable=True, 28 | ), 29 | ) 30 | op.add_column( 31 | "product_lists", sa.Column("update_dt", postgresql.TIMESTAMP(timezone=True), nullable=True) 32 | ) 33 | op.drop_index("ix_product_lists_id", table_name="product_lists") 34 | op.add_column( 35 | "search_texts", 36 | sa.Column( 37 | "create_dt", 38 | postgresql.TIMESTAMP(timezone=True), 39 | server_default=sa.text("now()"), 40 | nullable=True, 41 | ), 42 | ) 43 | op.add_column( 44 | "search_texts", sa.Column("update_dt", postgresql.TIMESTAMP(timezone=True), nullable=True) 45 | ) 46 | op.drop_index("ix_search_texts_id", table_name="search_texts") 47 | # ### end Alembic commands ### 48 | 49 | 50 | def downgrade(): 51 | # ### commands auto generated by Alembic - please adjust! ### 52 | op.create_index("ix_search_texts_id", "search_texts", ["id"], unique=False) 53 | op.drop_column("search_texts", "update_dt") 54 | op.drop_column("search_texts", "create_dt") 55 | op.create_index("ix_product_lists_id", "product_lists", ["id"], unique=False) 56 | op.drop_column("product_lists", "update_dt") 57 | op.drop_column("product_lists", "create_dt") 58 | # ### end Alembic commands ### 59 | -------------------------------------------------------------------------------- /backend/app/crud/crud_product.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm import Session 2 | 3 | # from fastapi_pagination.ext.sqlalchemy import paginate 4 | 5 | from app.models import model_product 6 | 7 | 8 | # search functions 9 | def create_search_text_with_product_list(db: Session, text: str, page: int, information: dict): 10 | db_text = model_product.SearchText(text=text, page=page) 11 | db.add(db_text) 12 | db.flush() 13 | db_product = model_product.ProductList(information=information, search_text_id=db_text.id) 14 | db.add(db_product) 15 | db.commit() 16 | return db_product 17 | 18 | 19 | def update_product_list(db: Session, search_text_id: int, information: dict): 20 | db_product_list = ( 21 | db.query(model_product.ProductList).filter_by(search_text_id=search_text_id).first() 22 | ) 23 | db_product_list.information = information 24 | db.commit() 25 | db.refresh(db_product_list) 26 | return db_product_list 27 | 28 | 29 | def get_search_text_and_page(db: Session, text: str, page: int): 30 | return ( 31 | db.query(model_product.SearchText) 32 | .filter(model_product.SearchText.text == text, model_product.SearchText.page == page) 33 | .first() 34 | ) 35 | 36 | 37 | # autocomplete search text 38 | def get_search_text_like(db: Session, text: str): 39 | # return paginate( 40 | return ( 41 | db.query(model_product.SearchText) 42 | .filter(model_product.SearchText.text.like(f"%{text}%")) 43 | .distinct(model_product.SearchText.text) 44 | .all() 45 | # .distinct(model_product.SearchText.text) 46 | ) 47 | 48 | 49 | # product details 50 | def create_product_details(db: Session, information: dict): 51 | db_item = model_product.ProductDetail(**information) 52 | db.add(db_item) 53 | db.commit() 54 | return db_item 55 | 56 | 57 | def update_product_details(db: Session, product_id: str, information: dict): 58 | db_item = db.query(model_product.ProductDetail).filter_by(productId=product_id).first() 59 | for key, value in information.items(): 60 | setattr(db_item, key, value) 61 | db.commit() 62 | db.refresh(db_item) 63 | return db_item 64 | 65 | 66 | def get_product_detail(db: Session, product_id: str): 67 | return ( 68 | db.query(model_product.ProductDetail) 69 | .filter(model_product.ProductDetail.productId == product_id) 70 | .first() 71 | ) 72 | -------------------------------------------------------------------------------- /backend/alembic.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # path to migration scripts 5 | script_location = alembic 6 | 7 | # template used to generate migration files 8 | # file_template = %%(rev)s_%%(slug)s 9 | 10 | # sys.path path, will be prepended to sys.path if present. 11 | # defaults to the current working directory. 12 | prepend_sys_path = . 13 | 14 | # timezone to use when rendering the date 15 | # within the migration file as well as the filename. 16 | # string value is passed to dateutil.tz.gettz() 17 | # leave blank for localtime 18 | # timezone = 19 | 20 | # max length of characters to apply to the 21 | # "slug" field 22 | # truncate_slug_length = 40 23 | 24 | # set to 'true' to run the environment during 25 | # the 'revision' command, regardless of autogenerate 26 | # revision_environment = false 27 | 28 | # set to 'true' to allow .pyc and .pyo files without 29 | # a source .py file to be detected as revisions in the 30 | # versions/ directory 31 | # sourceless = false 32 | 33 | # version location specification; this defaults 34 | # to alembic/versions. When using multiple version 35 | # directories, initial revisions must be specified with --version-path 36 | # version_locations = %(here)s/bar %(here)s/bat alembic/versions 37 | 38 | # the output encoding used when revision files 39 | # are written from script.py.mako 40 | # output_encoding = utf-8 41 | 42 | sqlalchemy.url = %(DATABASE_URL)s 43 | 44 | 45 | [post_write_hooks] 46 | # post_write_hooks defines scripts or Python functions that are run 47 | # on newly generated revision scripts. See the documentation for further 48 | # detail and examples 49 | 50 | # format using "black" - use the console_scripts runner, against the "black" entrypoint 51 | # hooks = black 52 | # black.type = console_scripts 53 | # black.entrypoint = black 54 | # black.options = -l 79 REVISION_SCRIPT_FILENAME 55 | 56 | # Logging configuration 57 | [loggers] 58 | keys = root,sqlalchemy,alembic 59 | 60 | [handlers] 61 | keys = console 62 | 63 | [formatters] 64 | keys = generic 65 | 66 | [logger_root] 67 | level = WARN 68 | handlers = console 69 | qualname = 70 | 71 | [logger_sqlalchemy] 72 | level = WARN 73 | handlers = 74 | qualname = sqlalchemy.engine 75 | 76 | [logger_alembic] 77 | level = INFO 78 | handlers = 79 | qualname = alembic 80 | 81 | [handler_console] 82 | class = StreamHandler 83 | args = (sys.stderr,) 84 | level = NOTSET 85 | formatter = generic 86 | 87 | [formatter_generic] 88 | format = %(levelname)-5.5s [%(name)s] %(message)s 89 | datefmt = %H:%M:%S 90 | -------------------------------------------------------------------------------- /backend/app/models/model_product.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Boolean, Column, ForeignKey, Integer, String, JSON 2 | from sqlalchemy.orm import relationship 3 | 4 | from app.models.base import AbstractBase 5 | 6 | 7 | class SearchText(AbstractBase): 8 | __tablename__ = "search_texts" 9 | 10 | text = Column(String, index=True) 11 | page = Column(Integer, default=1) 12 | 13 | product_list = relationship("ProductList", back_populates="search_text") 14 | 15 | 16 | class ProductList(AbstractBase): 17 | __tablename__ = "product_lists" 18 | 19 | information = Column(JSON, nullable=True) 20 | search_text_id = Column(Integer, ForeignKey("search_texts.id")) 21 | 22 | search_text = relationship("SearchText", back_populates="product_list") 23 | 24 | 25 | class ProductDetail(AbstractBase): 26 | __tablename__ = "product_details" 27 | 28 | productUrl = Column("product_url", String) 29 | productId = Column("product_id", String, index=True) 30 | statusId = Column("status_id", String) 31 | status = Column(String) 32 | currency = Column(String, index=True) 33 | locale = Column(String) 34 | shipTo = Column("ship_to", String, index=True) 35 | title = Column(String) 36 | totalStock = Column("total_stock", Integer) 37 | totalOrders = Column("total_orders", Integer) 38 | wishlistCount = Column("wishlist_count", Integer) 39 | unitName = Column("unit_name", String) 40 | unitNamePlural = Column("unit_name_plural", String) 41 | unitsPerProduct = Column("units_per_product", Integer) 42 | hasPurchaseLimit = Column("has_purchase_limit", Boolean, default=False) 43 | maxPurchaseLimit = Column("max_purchase_limit", Integer) 44 | processingTimeInDays = Column("processing_time_in_days", Integer) 45 | productImages = Column("product_images", JSON) 46 | productCategory = Column("product_category", JSON) 47 | seller = Column(JSON) 48 | sellerDetails = Column("seller_details", JSON) 49 | hasSinglePrice = Column("has_single_price", Boolean) 50 | price = Column(JSON) 51 | priceSummary = Column("price_summary", JSON) 52 | hasAttributes = Column("has_attributes", Boolean) 53 | attributes = Column(JSON) 54 | hasReviewsRatings = Column("has_reviews_ratings", Boolean) 55 | reviewsRatings = Column("reviews_ratings", JSON) 56 | hasProperties = Column("has_properties", Boolean) 57 | properties = Column(JSON) 58 | hasVariations = Column("has_variations", Boolean) 59 | variations = Column(JSON) 60 | shipping = Column(JSON) 61 | htmlDescription = Column("html_description", String) 62 | -------------------------------------------------------------------------------- /backend/alembic/env.py: -------------------------------------------------------------------------------- 1 | from logging.config import fileConfig 2 | 3 | from sqlalchemy import engine_from_config 4 | from sqlalchemy import pool 5 | 6 | from alembic import context 7 | 8 | 9 | from app.database import Base 10 | from app.settings import DATABASE_URL 11 | 12 | # this is the Alembic Config object, which provides 13 | # access to the values within the .ini file in use. 14 | config = context.config 15 | section = config.config_ini_section 16 | config.set_section_option(section, "DATABASE_URL", DATABASE_URL) 17 | 18 | # Interpret the config file for Python logging. 19 | # This line sets up loggers basically. 20 | fileConfig(config.config_file_name) 21 | 22 | # add your model's MetaData object here 23 | # for 'autogenerate' support 24 | # from myapp import mymodel 25 | # target_metadata = mymodel.Base.metadata 26 | from app.models import * 27 | 28 | target_metadata = Base.metadata 29 | 30 | # other values from the config, defined by the needs of env.py, 31 | # can be acquired: 32 | # my_important_option = config.get_main_option("my_important_option") 33 | # ... etc. 34 | 35 | 36 | def run_migrations_offline(): 37 | """Run migrations in 'offline' mode. 38 | 39 | This configures the context with just a URL 40 | and not an Engine, though an Engine is acceptable 41 | here as well. By skipping the Engine creation 42 | we don't even need a DBAPI to be available. 43 | 44 | Calls to context.execute() here emit the given string to the 45 | script output. 46 | 47 | """ 48 | url = config.get_main_option("sqlalchemy.url") 49 | context.configure( 50 | url=url, 51 | target_metadata=target_metadata, 52 | literal_binds=True, 53 | dialect_opts={"paramstyle": "named"}, 54 | compare_type=True, 55 | ) 56 | 57 | with context.begin_transaction(): 58 | context.run_migrations() 59 | 60 | 61 | def run_migrations_online(): 62 | """Run migrations in 'online' mode. 63 | 64 | In this scenario we need to create an Engine 65 | and associate a connection with the context. 66 | 67 | """ 68 | connectable = engine_from_config( 69 | config.get_section(config.config_ini_section), 70 | prefix="sqlalchemy.", 71 | poolclass=pool.NullPool, 72 | ) 73 | 74 | with connectable.connect() as connection: 75 | context.configure(connection=connection, target_metadata=target_metadata, compare_type=True) 76 | 77 | with context.begin_transaction(): 78 | context.run_migrations() 79 | 80 | 81 | if context.is_offline_mode(): 82 | run_migrations_offline() 83 | else: 84 | run_migrations_online() 85 | -------------------------------------------------------------------------------- /frontend/src/components/header.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import Image from 'next/image' 3 | import { useRouter } from 'next/router' 4 | 5 | import { useCookies } from "react-cookie" 6 | 7 | 8 | export default function Header() { 9 | const router = useRouter(); 10 | const [cookies, , removeCookie] = useCookies(["access_token"]); 11 | let access_token = cookies.access_token; 12 | 13 | const handleClickSignout = () => { 14 | removeCookie('access_token'); 15 | router.push('/signin'); 16 | } 17 | 18 | let dropDownMenu; 19 | if (router.pathname == "/signin") { 20 | dropDownMenu = ( 21 |
  • 22 | 23 | Sign-up 24 | 25 |
  • 26 | ) 27 | } else if (router.pathname == "/signup") { 28 | dropDownMenu = ( 29 |
  • 30 | 31 | Sign-in 32 | 33 |
  • 34 | ) 35 | } else if (router.pathname == "/aboutus" && !access_token) { 36 | dropDownMenu = ( 37 |
  • 38 | 39 | Sign-in 40 | 41 |
  • 42 | ) 43 | } else { 44 | dropDownMenu = ( 45 |
  • 46 | Sign out 47 |
  • 48 | ) 49 | } 50 | 51 | return ( 52 |
    53 | 89 |
    90 | ) 91 | } 92 | -------------------------------------------------------------------------------- /backend/alembic/versions/908530dcb166_restore_some_columns.py: -------------------------------------------------------------------------------- 1 | """Restore some columns 2 | 3 | Revision ID: 908530dcb166 4 | Revises: d88bf9ac6f99 5 | Create Date: 2021-07-07 23:39:49.514426 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '908530dcb166' 14 | down_revision = 'd88bf9ac6f99' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.add_column('product_details', sa.Column('product_images', sa.JSON(), nullable=True)) 22 | op.add_column('product_details', sa.Column('product_category', sa.JSON(), nullable=True)) 23 | op.add_column('product_details', sa.Column('seller', sa.JSON(), nullable=True)) 24 | op.add_column('product_details', sa.Column('seller_details', sa.JSON(), nullable=True)) 25 | op.add_column('product_details', sa.Column('price_summary', sa.JSON(), nullable=True)) 26 | op.add_column('product_details', sa.Column('attributes', sa.JSON(), nullable=True)) 27 | op.add_column('product_details', sa.Column('reviews_ratings', sa.JSON(), nullable=True)) 28 | op.add_column('product_details', sa.Column('properties', sa.JSON(), nullable=True)) 29 | op.add_column('product_details', sa.Column('variations', sa.JSON(), nullable=True)) 30 | op.add_column('product_details', sa.Column('shipping', sa.JSON(), nullable=True)) 31 | op.drop_index('ix_product_details_has_attributes', table_name='product_details') 32 | op.drop_index('ix_product_details_has_reviews_ratings', table_name='product_details') 33 | op.drop_index('ix_product_details_has_single_price', table_name='product_details') 34 | op.drop_index('ix_product_details_locale', table_name='product_details') 35 | op.drop_index('ix_product_details_product_url', table_name='product_details') 36 | op.drop_index('ix_product_details_status', table_name='product_details') 37 | op.drop_index('ix_product_details_status_id', table_name='product_details') 38 | op.drop_index('ix_product_details_title', table_name='product_details') 39 | # ### end Alembic commands ### 40 | 41 | 42 | def downgrade(): 43 | # ### commands auto generated by Alembic - please adjust! ### 44 | op.create_index('ix_product_details_title', 'product_details', ['title'], unique=False) 45 | op.create_index('ix_product_details_status_id', 'product_details', ['status_id'], unique=False) 46 | op.create_index('ix_product_details_status', 'product_details', ['status'], unique=False) 47 | op.create_index('ix_product_details_product_url', 'product_details', ['product_url'], unique=False) 48 | op.create_index('ix_product_details_locale', 'product_details', ['locale'], unique=False) 49 | op.create_index('ix_product_details_has_single_price', 'product_details', ['has_single_price'], unique=False) 50 | op.create_index('ix_product_details_has_reviews_ratings', 'product_details', ['has_reviews_ratings'], unique=False) 51 | op.create_index('ix_product_details_has_attributes', 'product_details', ['has_attributes'], unique=False) 52 | op.drop_column('product_details', 'shipping') 53 | op.drop_column('product_details', 'variations') 54 | op.drop_column('product_details', 'properties') 55 | op.drop_column('product_details', 'reviews_ratings') 56 | op.drop_column('product_details', 'attributes') 57 | op.drop_column('product_details', 'price_summary') 58 | op.drop_column('product_details', 'seller_details') 59 | op.drop_column('product_details', 'seller') 60 | op.drop_column('product_details', 'product_category') 61 | op.drop_column('product_details', 'product_images') 62 | # ### end Alembic commands ### 63 | -------------------------------------------------------------------------------- /frontend/pages/signup.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import Head from 'next/head' 3 | import { useRouter } from 'next/router' 4 | import Layout from '../src/components/layout' 5 | 6 | import { useState } from "react"; 7 | import { useEffect } from 'react' 8 | import axios from 'axios'; 9 | import { useCookies } from "react-cookie" 10 | 11 | export default function Signup() { 12 | const router = useRouter(); 13 | const [ user, setUser ] = useState({}); 14 | const [ cookies, ,removeCookie ] = useCookies(); 15 | let access_token = cookies.access_token; 16 | 17 | useEffect(() => { 18 | if (!access_token) { 19 | return; 20 | } 21 | const verifyToken = async () => { 22 | try{ 23 | const response = await axios.get(`${process.env.NEXT_PUBLIC_API_URL}/aaa/token`, { 24 | headers: { 25 | Authorization: `bearer ${cookies.access_token}` 26 | } 27 | }); 28 | if (response.data.valid) { 29 | router.push('/products'); 30 | } 31 | } catch (error) { 32 | // Delete access token cookie 33 | removeCookie('access_token'); 34 | } 35 | } 36 | verifyToken(); 37 | }) 38 | 39 | 40 | const handleChange = ({ target }) => { 41 | const { name, value } = target; 42 | setUser({ 43 | ...user, 44 | [name]: value 45 | }); 46 | }; 47 | const handleSubmit = async (event) => { 48 | event.preventDefault(); 49 | try { 50 | const response = await axios.post(`${process.env.NEXT_PUBLIC_API_URL}/me`, user); 51 | if (response.status == 200) { 52 | router.push('/signin'); 53 | } 54 | } catch (error) { 55 | console.log(error); 56 | } 57 | }; 58 | 59 | return ( 60 | 61 | 62 | Signup 63 | 64 |
    65 |
    66 |
    67 |
    68 | 74 |
    75 |
    76 | 82 |
    83 |
    84 | 90 |
    91 |
    92 | 98 |
    99 |
    100 | 101 | 102 |
    103 | Sign-in 104 |
    105 |
    106 | 107 | 108 |
    109 |
    110 |
    111 |
    112 |
    113 | ) 114 | } -------------------------------------------------------------------------------- /frontend/src/components/pagination.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | import { useCookies } from "react-cookie" 3 | import axios from 'axios'; 4 | 5 | 6 | export default function Pagination({ searchText, totalPages, setProductList }) { 7 | const [ pageQuotient, setPageQuotient ] = useState(1); 8 | const [ currentPage, setCurrentPage ] = useState(1); 9 | const [ cookies ] = useCookies(["access_token"]); 10 | let access_token = cookies.access_token; 11 | 12 | useEffect(() => { 13 | setPageQuotient(1) 14 | setCurrentPage(1) 15 | }, [totalPages]) 16 | 17 | const handleClickPage = async (e) => { 18 | try { 19 | const params = { text: searchText, page: e.target.getAttribute('data-value') } 20 | const headers = { 21 | Authorization: `bearer ${access_token}` 22 | } 23 | const response = await axios.get(`${process.env.NEXT_PUBLIC_API_URL}/products/`, { params, headers }); 24 | setProductList(response.data.information.items.slice(0, 12)); 25 | setCurrentPage(Number(e.target.getAttribute('data-value'))); 26 | } catch (error) { 27 | } 28 | }; 29 | const handleClickNext = async () => { 30 | if (pageQuotient + 1 > Math.ceil(totalPages / 5)) { 31 | return 32 | } 33 | const nextPage = 1 + 5 * ((pageQuotient + 1) - 1); 34 | setPageQuotient(prevPageQuotient => prevPageQuotient + 1); 35 | setCurrentPage(nextPage); 36 | try { 37 | const params = { text: searchText, page: nextPage } 38 | const headers = { 39 | Authorization: `bearer ${access_token}` 40 | } 41 | const response = await axios.get(`${process.env.NEXT_PUBLIC_API_URL}/products/`, { params, headers }); 42 | setProductList(response.data.information.items.slice(0, 12)); 43 | } catch (error) { 44 | } 45 | } 46 | const handleClickPrevious = async () => { 47 | if (pageQuotient <= 1) { 48 | return 49 | } 50 | const prevPage = 5 + 5 * ((pageQuotient - 1) - 1) 51 | try { 52 | const params = { text: searchText, page: prevPage } 53 | const headers = { 54 | Authorization: `bearer ${access_token}` 55 | } 56 | const response = await axios.get(`${process.env.NEXT_PUBLIC_API_URL}/products/`, { params, headers }); 57 | setProductList(response.data.information.items.slice(0, 12)); 58 | setPageQuotient(prevPageQuotient => prevPageQuotient - 1) 59 | setCurrentPage(prevPage) 60 | } catch (error) { 61 | } 62 | } 63 | 64 | const pageIdxs = [] 65 | const skip = 5 * (pageQuotient - 1) 66 | const limit = (totalPages - skip < 5) ? totalPages - skip : 5 67 | for (let i = 1; i < limit + 1; i++) { 68 | pageIdxs.push(i) 69 | } 70 | const pageIdxsList = pageIdxs.map((pageIdx) => { 71 | return ( 72 |
  • 73 | 74 |
    75 | { skip + pageIdx } 76 |
    77 |
    78 |
  • 79 | ) 80 | }) 81 | 82 | return ( 83 | 98 | ) 99 | } -------------------------------------------------------------------------------- /frontend/pages/signin.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import Head from 'next/head' 3 | import { useRouter } from 'next/router' 4 | import Layout from '../src/components/layout' 5 | 6 | import { useState } from "react"; 7 | import { useEffect } from 'react' 8 | import { useCookies } from "react-cookie" 9 | import axios from 'axios'; 10 | import FormData from 'form-data'; 11 | 12 | export default function Signin() { 13 | const [cookies, setCookie, removeCookie] = useCookies(["access_token"]); 14 | const router = useRouter(); 15 | 16 | let access_token = cookies.access_token; 17 | useEffect(() => { 18 | // authentication of JWT token 19 | if (!access_token) { 20 | return; 21 | } 22 | const verifyTokenForReturl = async () => { 23 | try{ 24 | let retUrl = router.query.retUrl; 25 | const response = await axios.get(`${process.env.NEXT_PUBLIC_API_URL}/aaa/token`, { 26 | headers: { 27 | Authorization: `bearer ${access_token}` 28 | } 29 | }); 30 | if (response.data.valid && retUrl && typeof retUrl === 'string') { 31 | router.push(retUrl); 32 | } else if (response.data.valid) { 33 | router.push('/products'); 34 | } 35 | } catch (error) { 36 | // Delete access token cookie 37 | removeCookie('access_token'); 38 | } 39 | } 40 | verifyTokenForReturl(); 41 | }) 42 | 43 | const [ email, setEmail ] = useState(""); 44 | const [ password, setPassword ] = useState(""); 45 | const [ isInvalid, setIsInvalid ] = useState(false); 46 | 47 | const handleEmailChange = ({ target: { value } }) => setEmail(value); 48 | const handlePasswordChange = ({ target: { value } }) => setPassword(value); 49 | const handleSubmit = async (event) => { 50 | event.preventDefault(); 51 | const formData = new FormData(); 52 | formData.append('username', email); 53 | formData.append('password', password); 54 | try { 55 | const response = await axios.post(`${process.env.NEXT_PUBLIC_API_URL}/aaa/token`, 56 | formData, { 57 | headers: { 58 | 'Content-Type': 'multipart/form-data' 59 | } 60 | }); 61 | if (response.status == 200) { 62 | const accessToken = response.data.access_token; 63 | const afterOneDay = new Date(); 64 | afterOneDay.setDate(afterOneDay.getDate() + 1); 65 | setCookie( 66 | 'access_token', 67 | accessToken, 68 | { 69 | expires: afterOneDay, 70 | } 71 | ); 72 | } 73 | } catch (error) { 74 | if (error.response.status === 401) { 75 | setIsInvalid(true); 76 | } 77 | console.log(error); 78 | } 79 | }; 80 | 81 | return ( 82 | 83 | 84 | Signin 85 | 86 |
    87 |
    88 |
    89 |
    90 | 96 |
    97 |
    98 | 104 |
    105 |
    108 | You entered wrong email or password, Please try to enter the right email and password! 109 |
    110 |
    111 | 112 | 113 |
    114 | Register 115 |
    116 |
    117 | 118 | 119 |
    120 |
    121 |
    122 |
    123 |
    124 | ) 125 | } -------------------------------------------------------------------------------- /backend/app/routers/products.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | from fastapi import APIRouter, Depends, Query 3 | from fastapi.exceptions import HTTPException 4 | from sqlalchemy.orm import Session 5 | from datetime import datetime, timedelta 6 | 7 | # from fastapi_pagination import Page 8 | import pytz 9 | 10 | from app.crud import crud_product 11 | from app.schemas import pyd_product 12 | from app.dependencies import get_current_user, get_db 13 | from app.utils.zapiex import zapiex_apis 14 | 15 | 16 | router = APIRouter(prefix="/products", tags=["products"]) 17 | utc = pytz.UTC 18 | 19 | 20 | @router.get("/", dependencies=[Depends(get_current_user)], response_model=pyd_product.ProductList) 21 | def search_items_by_text(text: str, page: int, db: Session = Depends(get_db)): 22 | try: 23 | search_text = crud_product.get_search_text_and_page(db, text=text, page=page) 24 | if search_text: 25 | product_list = search_text.product_list[0] 26 | update_date = product_list.update_dt 27 | one_day_before_now = utc.localize(datetime.utcnow() - timedelta(days=1)) 28 | if update_date >= one_day_before_now: 29 | return search_text.product_list[0] 30 | search_info = zapiex_apis.search_products(text, page) 31 | status_code = search_info["statusCode"] 32 | information = search_info["data"] 33 | if status_code == 200: 34 | return crud_product.update_product_list( 35 | db, search_text_id=search_text.id, information=information 36 | ) 37 | search_info = zapiex_apis.search_products(text, page) 38 | status_code = search_info["statusCode"] 39 | information = search_info["data"] 40 | if status_code == 200: 41 | return crud_product.create_search_text_with_product_list( 42 | db=db, text=text, page=page, information=information 43 | ) 44 | else: 45 | raise HTTPException(status_code=status_code, detail=search_info["errorMessage"]) 46 | except Exception: 47 | raise 48 | 49 | 50 | @router.get( 51 | "/search", 52 | dependencies=[Depends(get_current_user)], 53 | # response_model=Page[pyd_product.SearchTextOutput], 54 | response_model=List[pyd_product.SearchTextOutput], 55 | ) 56 | def autocomplete_search_text( 57 | search: str = Query(..., max_length=50, regex="[A-Za-z0-9]"), db: Session = Depends(get_db) 58 | ): 59 | try: 60 | return crud_product.get_search_text_like(db, text=search) 61 | except Exception as e: 62 | raise HTTPException(status_code=400, detail=e) 63 | 64 | 65 | @router.get( 66 | "/{product_id}", 67 | dependencies=[Depends(get_current_user)], 68 | response_model=pyd_product.ProductDetail, 69 | ) 70 | def create_details(product_id: str, db: Session = Depends(get_db)): 71 | try: 72 | db_detail = crud_product.get_product_detail(db, product_id=product_id) 73 | if db_detail: 74 | update_date = db_detail.update_dt 75 | one_day_before_now = utc.localize(datetime.utcnow() - timedelta(days=1)) 76 | if update_date >= one_day_before_now: 77 | return db_detail 78 | product_info = zapiex_apis.get_product(product_id) 79 | status_code = product_info["statusCode"] 80 | information = product_info["data"] 81 | if status_code == 200 and information["status"] != "active": 82 | return 83 | if status_code == 200: 84 | return crud_product.update_product_details( 85 | db=db, product_id=product_id, information=information 86 | ) 87 | elif status_code == 201: 88 | raise HTTPException(status_code=status_code) 89 | else: 90 | raise HTTPException(status_code=status_code, detail=product_info["errorMessage"]) 91 | product_info = zapiex_apis.get_product(product_id) 92 | status_code = product_info["statusCode"] 93 | information = product_info["data"] 94 | if status_code == 200 and information["status"] != "active": 95 | return 96 | if status_code == 200: 97 | return crud_product.create_product_details(db=db, information=information) 98 | elif status_code == 201: 99 | # ex) '1230192314802471024333333333332433256164', 'asdf5625435', ';;;;' 100 | return 101 | else: 102 | # ex) '강아지' 103 | raise HTTPException(status_code=status_code, detail=product_info["errorMessage"]) 104 | except Exception: 105 | raise 106 | -------------------------------------------------------------------------------- /frontend/pages/products.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import Head from 'next/head' 3 | import Layout from '../src/components/layout' 4 | import Autocomplete from '../src/components/autocomplete' 5 | import Pagination from '../src/components/pagination' 6 | import Image from 'next/image' 7 | 8 | import { useState, useEffect } from "react"; 9 | import { useCookies } from "react-cookie" 10 | import axios from 'axios'; 11 | import router from 'next/router' 12 | 13 | 14 | export default function Products({ productsData }) { 15 | const [cookies, , removeCookie] = useCookies(["access_token"]); 16 | let access_token = cookies.access_token; 17 | const [searchText, setSearchText] = useState(""); 18 | 19 | useEffect(() => { 20 | if (!access_token) { 21 | router.push('/signin'); 22 | return; 23 | } 24 | const verifyToken = async () => { 25 | try{ 26 | await axios.get(`${process.env.NEXT_PUBLIC_API_URL}/aaa/token`, { 27 | headers: { 28 | Authorization: `bearer ${cookies.access_token}` 29 | } 30 | }); 31 | let searchText = localStorage.getItem("latestSearchText") 32 | if (searchText) { 33 | setSearchText(searchText); 34 | const response = await axios.get(`${process.env.NEXT_PUBLIC_API_URL}/products?text=${searchText}&page=1`, { 35 | headers: { 36 | Authorization: `bearer ${cookies.access_token}` 37 | } 38 | }); 39 | const productsInfo = response.data; 40 | setTotalPages(productsInfo.information.numberOfPages); 41 | setProductList(productsInfo.information.items.slice(0, 12)); 42 | } 43 | } catch (error) { 44 | // Delete access token cookie 45 | removeCookie('access_token'); 46 | router.push('/signin'); 47 | } 48 | } 49 | verifyToken(); 50 | }, []) 51 | 52 | 53 | 54 | const [totalPages, setTotalPages] = useState(null); 55 | const [productList, setProductList] = useState(productsData); 56 | const handleChange = ({ target: { value } }) => setSearchText(value); 57 | const handleSubmit = async (event) => { 58 | event.preventDefault(); 59 | try { 60 | const response = await axios.get(`${process.env.NEXT_PUBLIC_API_URL}/products?text=${searchText}&page=1`, { 61 | headers: { 62 | Authorization: `bearer ${cookies.access_token}` 63 | } 64 | }); 65 | const productsInfo = response.data; 66 | setTotalPages(productsInfo.information.numberOfPages); 67 | setProductList(productsInfo.information.items.slice(0, 12)); 68 | localStorage.setItem('latestSearchText', searchText); 69 | } catch (e) { 70 | console.log(e); 71 | setProductList(null); 72 | } 73 | } 74 | 75 | let products; 76 | if (productList) { 77 | const productListArray = productList.map((product: any) => { 78 | return ( 79 |
  • 80 |
    81 | 82 | 83 | Product Image 89 | 90 | 91 |
    92 |
    {product.title}
    93 |
    94 |

    95 | Total Orders: {product.totalOrders || 0}
    96 | Average Rating: {product.averageRating || 0}
    97 | Minimum Shipping Price: {product.shippingMinPrice.value || 0} 98 |

    99 | 100 | See item detail 101 | 102 |
    103 |
    104 |
    105 |
  • 106 | ) 107 | }); 108 | products = ( 109 |
    110 |
    111 |
      112 | {productListArray} 113 |
    114 |
    115 |
    116 | 117 |
    118 |
    119 | ); 120 | 121 | } else { 122 | products = ( 123 |
    124 |

    Please search your products

    125 |
    126 | ) 127 | } 128 | 129 | return ( 130 | 131 | 132 | Products 133 | 134 |
    135 |
    136 | 145 | 146 | 147 | 148 |
    149 |
    150 | {products} 151 |
    152 |
    153 | ) 154 | } 155 | -------------------------------------------------------------------------------- /backend/alembic/versions/e5381fcd65d0_changed_index_and_type_in_product_.py: -------------------------------------------------------------------------------- 1 | """Changed index and type in product_details and dropped items and users table 2 | 3 | Revision ID: e5381fcd65d0 4 | Revises: 005a8708d74d 5 | Create Date: 2021-07-06 17:45:12.453887 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | from sqlalchemy.dialects import postgresql 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'e5381fcd65d0' 14 | down_revision = '005a8708d74d' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.drop_index('ix_items_description', table_name='items') 22 | op.drop_index('ix_items_id', table_name='items') 23 | op.drop_index('ix_items_title', table_name='items') 24 | op.drop_table('items') 25 | op.drop_index('ix_users_email', table_name='users') 26 | op.drop_index('ix_users_id', table_name='users') 27 | op.drop_table('users') 28 | op.add_column('product_details', sa.Column('create_dt', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=True)) 29 | op.add_column('product_details', sa.Column('update_dt', postgresql.TIMESTAMP(timezone=True), nullable=True)) 30 | op.drop_index('ix_product_details_hasProperties', table_name='product_details') 31 | op.drop_index('ix_product_details_hasPurchaseLimit', table_name='product_details') 32 | op.drop_index('ix_product_details_hasVariations', table_name='product_details') 33 | op.drop_index('ix_product_details_htmlDescription', table_name='product_details') 34 | op.drop_index('ix_product_details_id', table_name='product_details') 35 | op.drop_index('ix_product_details_maxPurchaseLimit', table_name='product_details') 36 | op.drop_index('ix_product_details_priceSummary', table_name='product_details') 37 | op.drop_index('ix_product_details_processingTimeInDays', table_name='product_details') 38 | op.drop_index('ix_product_details_properties', table_name='product_details') 39 | op.drop_index('ix_product_details_shipping', table_name='product_details') 40 | op.drop_index('ix_product_details_totalOrders', table_name='product_details') 41 | op.drop_index('ix_product_details_totalStock', table_name='product_details') 42 | op.drop_index('ix_product_details_unitName', table_name='product_details') 43 | op.drop_index('ix_product_details_unitNamePlural', table_name='product_details') 44 | op.drop_index('ix_product_details_unitsPerProduct', table_name='product_details') 45 | op.drop_index('ix_product_details_variations', table_name='product_details') 46 | op.drop_index('ix_product_details_wishlistCount', table_name='product_details') 47 | op.drop_column('product_details', 'priceSummary') 48 | # ### end Alembic commands ### 49 | 50 | 51 | def downgrade(): 52 | # ### commands auto generated by Alembic - please adjust! ### 53 | op.add_column('product_details', sa.Column('priceSummary', sa.INTEGER(), autoincrement=False, nullable=True)) 54 | op.create_index('ix_product_details_wishlistCount', 'product_details', ['wishlistCount'], unique=False) 55 | op.create_index('ix_product_details_variations', 'product_details', ['variations'], unique=False) 56 | op.create_index('ix_product_details_unitsPerProduct', 'product_details', ['unitsPerProduct'], unique=False) 57 | op.create_index('ix_product_details_unitNamePlural', 'product_details', ['unitNamePlural'], unique=False) 58 | op.create_index('ix_product_details_unitName', 'product_details', ['unitName'], unique=False) 59 | op.create_index('ix_product_details_totalStock', 'product_details', ['totalStock'], unique=False) 60 | op.create_index('ix_product_details_totalOrders', 'product_details', ['totalOrders'], unique=False) 61 | op.create_index('ix_product_details_shipping', 'product_details', ['shipping'], unique=False) 62 | op.create_index('ix_product_details_properties', 'product_details', ['properties'], unique=False) 63 | op.create_index('ix_product_details_processingTimeInDays', 'product_details', ['processingTimeInDays'], unique=False) 64 | op.create_index('ix_product_details_priceSummary', 'product_details', ['priceSummary'], unique=False) 65 | op.create_index('ix_product_details_maxPurchaseLimit', 'product_details', ['maxPurchaseLimit'], unique=False) 66 | op.create_index('ix_product_details_id', 'product_details', ['id'], unique=False) 67 | op.create_index('ix_product_details_htmlDescription', 'product_details', ['htmlDescription'], unique=False) 68 | op.create_index('ix_product_details_hasVariations', 'product_details', ['hasVariations'], unique=False) 69 | op.create_index('ix_product_details_hasPurchaseLimit', 'product_details', ['hasPurchaseLimit'], unique=False) 70 | op.create_index('ix_product_details_hasProperties', 'product_details', ['hasProperties'], unique=False) 71 | op.drop_column('product_details', 'update_dt') 72 | op.drop_column('product_details', 'create_dt') 73 | op.create_table('users', 74 | sa.Column('id', sa.INTEGER(), server_default=sa.text("nextval('users_id_seq'::regclass)"), autoincrement=True, nullable=False), 75 | sa.Column('email', sa.VARCHAR(), autoincrement=False, nullable=True), 76 | sa.Column('hashed_password', sa.VARCHAR(), autoincrement=False, nullable=True), 77 | sa.Column('is_active', sa.BOOLEAN(), autoincrement=False, nullable=True), 78 | sa.PrimaryKeyConstraint('id', name='users_pkey'), 79 | postgresql_ignore_search_path=False 80 | ) 81 | op.create_index('ix_users_id', 'users', ['id'], unique=False) 82 | op.create_index('ix_users_email', 'users', ['email'], unique=False) 83 | op.create_table('items', 84 | sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), 85 | sa.Column('title', sa.VARCHAR(), autoincrement=False, nullable=True), 86 | sa.Column('description', sa.VARCHAR(), autoincrement=False, nullable=True), 87 | sa.Column('owner_id', sa.INTEGER(), autoincrement=False, nullable=True), 88 | sa.ForeignKeyConstraint(['owner_id'], ['users.id'], name='items_owner_id_fkey'), 89 | sa.PrimaryKeyConstraint('id', name='items_pkey') 90 | ) 91 | op.create_index('ix_items_title', 'items', ['title'], unique=False) 92 | op.create_index('ix_items_id', 'items', ['id'], unique=False) 93 | op.create_index('ix_items_description', 'items', ['description'], unique=False) 94 | # ### end Alembic commands ### 95 | -------------------------------------------------------------------------------- /frontend/pages/product/[product_id].tsx: -------------------------------------------------------------------------------- 1 | import Head from 'next/head' 2 | import Image from 'next/image' 3 | import Link from 'next/link'; 4 | 5 | import Layout from '../../src/components/layout' 6 | 7 | import axios from 'axios' 8 | 9 | 10 | export default function ProductDetail( { productData } ) { 11 | let product; 12 | if (productData) { 13 | const carouselIndicatorsArray = productData.productImages.map((productImage, idx) => { 14 | return ( 15 | 53 | 62 | 63 | ) 64 | const reviewStar = [] 65 | const limit = Math.floor(productData.reviewsRatings.averageRating) 66 | for (let i = 0; i < 5; i++) { 67 | const starImage = i < limit ? ( 68 | Star 75 | ) : ( 76 | Star 83 | ) 84 | reviewStar.push(starImage) 85 | } 86 | 87 | product = ( 88 |
    89 |
    90 | {carousel} 91 |
    92 |
    93 |
    94 |

    {productData.title}

    95 |

    96 | {reviewStar} 97 |   98 | {productData.reviewsRatings.averageRating} 99 |     100 | {productData.reviewsRatings.totalCount} Review 101 |   102 | {productData.totalOrders} Order 103 |

    104 |
    105 |
    106 |
    107 |

    108 | Max discounted price(Web): {productData.currency} 109 |   110 | {productData.priceSummary.web.discountedPrice.min.display||productData.price.web.discountedPrice.min.display||'Please go to Ali-Express to check the price'} 111 |

    112 |

    113 | Max discounted price(App): {productData.currency} 114 |   115 | {productData.priceSummary.app.discountedPrice.min.display||productData.price.app.discountedPrice.min.display||'Please go to Ali-Express to check the price'} 116 |

    117 |
    118 |
    119 |
    120 |
    Total stock: {productData.totalStock} ({productData.unitNamePlural})
    121 |
    Wishlist count: {productData.wishlistCount}
    122 |
    Shipping from: {productData.shipping.shipFrom}
    123 |
    Shipping to: {productData.shipTo}
    124 |
    Delivery processing time: {productData.processingTimeInDays} (Days)
    125 |
    Company: {productData.seller.storeName}
    126 |
    127 |
    128 | 137 |
    138 |
    139 | ) 140 | } else { 141 | product = ( 142 |
    143 |

    Sorry, the product is not available at this moment

    144 |
    145 | ) 146 | } 147 | 148 | return ( 149 | 150 | 151 | Product Detail 152 | 153 |
    154 | {product} 155 |
    156 |
    157 | ) 158 | } 159 | 160 | export async function getServerSideProps({ params, req }) { 161 | let props = { productData: undefined }; 162 | try { 163 | const cookieString = req ? req.headers.cookie : ''; 164 | let access_token; 165 | // cookie parsing 166 | if (cookieString) { 167 | const cookieArray = cookieString.split(';'); 168 | for (let cookie of cookieArray) { 169 | const pureCookie = cookie.trim(); 170 | const key = pureCookie.split('=')[0]; 171 | const value = pureCookie.split('=')[1]; 172 | if (key === 'access_token') { 173 | access_token = value; 174 | } 175 | } 176 | } 177 | 178 | // authentication of JWT token 179 | if (!access_token) { 180 | return { 181 | redirect: { 182 | destination: `/signin?retUrl=/product/${params.product_id}`, 183 | permanent: false, 184 | }, 185 | } 186 | } 187 | try{ 188 | await axios.get(`${process.env.NEXT_PUBLIC_API_URL}/aaa/token`, { 189 | headers: { 190 | Authorization: `bearer ${access_token}` 191 | } 192 | }); 193 | } catch (error) { 194 | return { 195 | redirect: { 196 | destination: `/signin?retUrl=/product/${params.product_id}`, 197 | permanent: false, 198 | }, 199 | } 200 | } 201 | 202 | const response = await axios.get(`${process.env.NEXT_PUBLIC_API_URL}/products/${params.product_id}`, { 203 | headers: { 204 | Authorization: `bearer ${access_token}` 205 | } 206 | }); 207 | const productData = response.data 208 | return { 209 | props: { 210 | productData, 211 | } 212 | } 213 | } catch { 214 | props.productData = null; 215 | } 216 | return { 217 | props 218 | } 219 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ali Express Dropshipping service (AED) 2 | 3 | :page_facing_up: Personal project / :date: 2021.7 - 2021.11 4 | 5 | Ali Express에서 제공하는 상품 정보를 Zapiex API를 통해 가져와 DB에 저장하고, 사용자들이 drop shipping할 상품들을 검색할 수 있는 기능을 제공하는 서비스입니다. 6 | 7 | ​ 8 | 9 | > :bulb: What is Zapiex API? 10 | > 11 | > *Zapiex API is an unofficial AliExpress API that provides the following **realtime** services on AliExpress* 12 | > 13 | > \- [Zapiex docs](https://docs.zapiex.com/v3/) 14 | 15 | ​ 16 | 17 | ## :bookmark: Demo link 18 | 19 | * Frontend: [https://aed-frontend-app.herokuapp.com](https://aed-frontend-app.herokuapp.com/) 20 | 21 | * Backend: https://aed-backend-app.herokuapp.com/docs 22 | 23 | ​ 24 | 25 | ## :bookmark: Tech stack 26 | 27 | ### Backend 28 | 29 | * Python 3.9 30 | * FastAPI 0.68 31 | * PostgreSQL 13 32 | * Alembic 1.7 33 | * Heroku 34 | 35 | ### Frontend 36 | 37 | * JavaScript 38 | * TypeScript 39 | * React 17.0 40 | * Next.js 11.1 41 | * Bootstrap 42 | * Heroku 43 | 44 | ​ 45 | 46 | ## :bookmark: ERD 47 | 48 | * `search_texts` 49 | 50 | * 같은 검색어도 페이지가 다르면 구별하여 저장합니다. (Zapiex API 호출 시 검색어와 페이지 인자를 동시에 요구하므로) 51 | 52 | Ex. ('welsh corgi', 1 page) != ('welsh corgi', 5 page) 53 | 54 | * `product_lists` 55 | * `search_text_id`를 Foreign key로 사용하여, `search_texts` 테이블과 1-to-1 realationship을 구축합니다. 56 | * 모든 table은 3NF 정규화를 충족합니다. 57 | 58 | ![AED ERD](AED_ERD.png) 59 | 60 | ​ 61 | 62 | ## :bookmark: Core features - Backend 63 | 64 | ### :paperclip: API 65 | 66 | #### 1. Product 관련 APIs (products.py) 67 | 68 | Zapiex API를 사용해 Ali-Express의 상품 정보를 조회해오는 API 구현 (2개) 69 | 70 | 검색어 자동완성 API 구현 (1개) 71 | 72 | - `search_items_by_text` :pushpin: [코드 확인](https://github.com/veluminous/Ali_Express_Dropshipping/blob/3157387e66446b25b7e288149395ef3fc69ef8e3/backend/app/routers/products.py#L21) 73 | 74 | - 검색어와 페이지를 query로 받아 Zapiex API를 호출하고, Ali-Express에서 해당하는 상품 리스트 정보를 조회해주는 API입니다. 75 | - Zapiex API에 call 개수 제한이 있어, 최대한 호출 수를 줄이는 방향으로 구현했습니다. 76 | - 처음 검색시 API를 call하여 검색어 및 페이지에 해당하는 상품 리스트 정보를 받고 DB에 저장 77 | - 이후, update date를 확인하여 하루가 안지났다면 DB의 기존 data를 리턴 78 | - 하루가 지났다면 새로 API call하여 DB를 update하고 리턴 79 | - Zapiex API에 page를 query로 보낼 수 있어, pagination 기능을 활용하는 방향으로 구현했습니다. 80 | 81 | - `create_details` :pushpin: [코드 확인](https://github.com/veluminous/Ali_Express_Dropshipping/blob/3157387e66446b25b7e288149395ef3fc69ef8e3/backend/app/routers/products.py#L70) 82 | 83 | - 상품의 id를 query로 받아 Zapiex API를 호출하고, 특정 상품의 detail 정보를 조회하는 API입니다. 84 | 85 | - Zapiex API에는 call 개수 제한이 있어, 최대한 호출 수를 줄이는 방향으로 구현했습니다. (`search_items_by_text`와 동일 방식) 86 | 87 | - 응답 코드가 200일 때, 응답 정보 중 'status'가 active한 상품일 때 DB에 저장했습니다. 88 | 89 | - Zapiex API에서 입력될 수 있는 id의 유형에 따라 예외처리를 진행했습니다. 90 | 91 | * 비정상적인 id를 던질 때, 응답 코드가 201이어서 함수를 그대로 종료 92 | 93 | Ex. '1230192314802471024333333333332433256164', 'asdf5625435', ';;;;' 94 | 95 | * 한국어를 id로 던지는 등 이외의 상황에서는 해당 에러를 그대로 예외 처리 96 | 97 | Ex. '강아지' 98 | 99 | - `autocomplete_search_text` :pushpin: [코드 확인](https://github.com/veluminous/Ali_Express_Dropshipping/blob/3157387e66446b25b7e288149395ef3fc69ef8e3/backend/app/routers/products.py#L56) 100 | 101 | - 검색어 자동완성 API입니다. 102 | - 프론트에서 검색어 텍스트가 입력될 때마다 API가 호출됩니다. 103 | - DB에서 저장된 검색어 중 해당 텍스트가 포함된 모든 검색어를 조회해 리턴합니다. 104 | - 검색어 텍스트는 영어 및 숫자만 입력받을 수 있게 parameter에서 validation합니다. 105 | 106 | ​ 107 | 108 | #### 2. 유저 CRUD관련 APIs (accounts.py) 109 | 110 | User 정보에 대한 CRUD API 구현 111 | 112 | - `create_me` :pushpin: [코드 확인](https://github.com/veluminous/Ali_Express_Dropshipping/blob/3157387e66446b25b7e288149395ef3fc69ef8e3/backend/app/routers/accounts.py#L15) 113 | - 유저 정보를 입력 받아 새로운 유저를 DB에 생성하고 리턴합니다. 114 | - 패스워드는 `passlib` 라이브러리로 bcrypt 알고리즘을 사용해 해시하여 저장합니다. 115 | - DB에 유저 존재 유무는 이메일을 활용해 확인합니다. 116 | 117 | - `read_me` :pushpin: [코드 확인](https://github.com/veluminous/Ali_Express_Dropshipping/blob/3157387e66446b25b7e288149395ef3fc69ef8e3/backend/app/routers/accounts.py#L25) 118 | - `get_current_user` dependency를 사용해 현재 유저 정보를 DB에서 조회하고 리턴합니다. 119 | - `update_me` :pushpin: [코드 확인](https://github.com/veluminous/Ali_Express_Dropshipping/blob/3157387e66446b25b7e288149395ef3fc69ef8e3/backend/app/routers/accounts.py#L30) 120 | - `get_current_user` dependency를 사용해 현재 유저 정보를 DB에서 조회하고, 입력된 사용자 정보를 DB에서 수정하여 리턴합니다. 121 | - 사용자 정보는 Patch method로 전달합니다. (Partial update합니다.) 122 | - 다만, 이메일은 수정할 수 없습니다. 123 | 124 | - `delete_me` :pushpin: [코드 확인](https://github.com/veluminous/Ali_Express_Dropshipping/blob/3157387e66446b25b7e288149395ef3fc69ef8e3/backend/app/routers/accounts.py#L39) 125 | - `get_current_user` dependency를 사용해 현재 유저 정보를 DB에서 조회하고 삭제합니다. 126 | 127 | 128 | ​ 129 | 130 | #### 3. 인증 및 인가 관련 APIs (authentication.py) 131 | 132 | JWT Athentication 및 Authorization API 구현 133 | 134 | * `login_for_access_token` :pushpin: [코드 확인](https://github.com/veluminous/Ali_Express_Dropshipping/blob/3157387e66446b25b7e288149395ef3fc69ef8e3/backend/app/routers/authentication.py#L21) 135 | * 로그인을 통해 authentication을 진행하여 인증된 사용자라면 JWT token을 발급해주는 API입니다. 136 | * 발급된 JWT는 프론트 측에서 쿠키로 저장됩니다. 137 | * 프론트 측에서도 만료 날짜(1 day)를 동일하게 설정해, 토큰의 만료와 브라우저에서 토큰이 자동으로 제거되는 것이 동시에 이뤄지게 했습니다. 138 | 139 | * `verify_access_token` :pushpin: [코드 확인](https://github.com/veluminous/Ali_Express_Dropshipping/blob/3157387e66446b25b7e288149395ef3fc69ef8e3/backend/app/routers/authentication.py#L44) 140 | * 유효한 token인지 authorization해주는 API입니다. 141 | * 프론트에서 특정 페이지 접근을 회원만 할 수 있도록 제한하기 위해 만들었습니다. 142 | 143 | 144 | ​ 145 | 146 | ### :paperclip: Dependency 147 | 148 | * `get_current_user` :pushpin: [코드 확인](https://github.com/veluminous/Ali_Express_Dropshipping/blob/3157387e66446b25b7e288149395ef3fc69ef8e3/backend/app/dependencies.py#L26) 149 | * HTTP header로 JWT 토큰을 전달 받으면 token을 decoding해 검증하고, 현재 사용자의 DB 존재 유무 및 정보를 리턴합니다. 150 | * `creat_me`, `login_for_access_token`을 제외한 모든 API를 authorization하는데 사용합니다. 151 | 152 | * `get_db` :pushpin: [코드 확인](https://github.com/veluminous/Ali_Express_Dropshipping/blob/3157387e66446b25b7e288149395ef3fc69ef8e3/backend/app/dependencies.py#L18) 153 | * DB session을 열어 DB 작업이 필요한 API에 잠시 넘겨주고, 작업이 끝나면 다시 session을 돌려받아 닫습니다. 154 | 155 | ​ 156 | 157 | ### :paperclip: ETC 158 | 159 | * Zapiex API는 클래스로 만들어 사용했습니다. :pushpin: [코드 확인](https://github.com/veluminous/Ali_Express_Dropshipping/blob/3157387e66446b25b7e288149395ef3fc69ef8e3/backend/app/utils/zapiex.py#L6) 160 | * 보안이 필요한 정보는 환경변수에 저장해 사용했습니다. (API key, JWT secret key, DB information...) :pushpin: [코드 확인](https://github.com/veluminous/Ali_Express_Dropshipping/blob/3157387e66446b25b7e288149395ef3fc69ef8e3/backend/app/settings.py#L4) 161 | * CORS 문제는 미들웨어(Middleware)를 이용해 허용했습니다. :pushpin: [코드 확인](https://github.com/veluminous/Ali_Express_Dropshipping/blob/3157387e66446b25b7e288149395ef3fc69ef8e3/backend/app/main.py#L15) 162 | 163 | ​ 164 | 165 | ## :bookmark: Trouble shooting - Backend 166 | 167 | * 특정 상황을 Alembic autogeneration이 잘 감지하지 못하는 문제 :pushpin: [코드 확인](https://github.com/veluminous/Ali_Express_Dropshipping/blob/3157387e66446b25b7e288149395ef3fc69ef8e3/backend/alembic/env.py#L54) 168 | * Column의 타입 변화 같은 alter 관련 테이블 변화가 autogeneration 시 잘 반영되지 않았습니다. 169 | * `run_migrations_offline` 및 `run_migrations_online`에 `compare_type=True`를 설정해서 해결 170 | 171 | * Zapiex API 호출 시 상품 정보 리스트의 갯수가 균일하게 오지 않는 현상 :pushpin: [코드 확인](https://github.com/veluminous/Ali_Express_Dropshipping/blob/06385b224bf2af0b28161494a96138eea0505ed5/frontend/pages/products.tsx#L67) 172 | * 20개의 상품에 대한 정보 리스트가 와야하는데, 약 15~18개가 무작위로 오는 현상이 발생했습니다. 173 | * API document에서 AliExpress-promoted products는 규격이 맞지 않아 제외하고 보내진다는 점을 확인했습니다. 174 | * 프론트 측에서 몇몇 아이템이 제외되더라도 안전하게 12개씩 pagination하는 방식으로 해결했습니다. 175 | 176 | * Product detail 관련 Zapiex API를 호출할 시 input으로 사용되는 id의 종류에 따라 다른 상태코드 응답 :pushpin: [코드 확인](https://github.com/veluminous/Ali_Express_Dropshipping/blob/3157387e66446b25b7e288149395ef3fc69ef8e3/backend/app/routers/products.py#L94) 177 | * 비정상적인 input이 될 수 있는 경우를 시도해보면서 경우의 수를 나누며 해결했습니다. 178 | 179 | * `datetime` 객체 비교 에러 (`search_items_by_text`, `create_details` API) :pushpin: [코드 확인](https://github.com/veluminous/Ali_Express_Dropshipping/blob/06385b224bf2af0b28161494a96138eea0505ed5/backend/app/routers/products.py#L27) 180 | * update date를 비교할 때, `TypeError: can't compare offset-naive and offset-aware datetimes` 발생했습니다. 181 | * 시간대가 포함된 aware 타입 datetime 객체(DB의 date)와 그렇지 않은 naive 타입 datetime 객체(새로 만든 date) 간 비교로 인한 문제였습니다. 182 | * `pytz` 라이브러리를 사용해 naive 타입 객체를 aware 타입으로 바꿔서 해결했습니다. 183 | 184 | * `fastapi_pagination` 외부 라이브러리 적용 시 서버 배포 에러 :pushpin: [코드 확인](https://github.com/veluminous/Ali_Express_Dropshipping/blob/06385b224bf2af0b28161494a96138eea0505ed5/backend/app/crud/crud_product.py#L39) 185 | * 원래 검색어 자동 완성 API에는 `fastapi_pagination` 라이브러리를 적용해 pagination을 구현했습니다. 186 | * 다만, Heroku 서버 배포 시 `TypeError: typing.ClassVar...`가 발생했습니다. 187 | * `fastapi_pagination`을 적용할 때 FastApi app 인스턴스를 감싸 nested 되는 부분이 존재하는데, 라이브러리의 기본 사용법에 해당하는 부분이라 라이브러리에 문제가 있다고 판단했습니다. 188 | * 라이브러리 적용을 해제하는 방향으로 해결했습니다. 189 | 190 | 191 | ​ 192 | 193 | ## :bookmark: Features - Frontend 194 | 195 | - 페이지가 변화해도 Header, Footer 컴포넌트는 유지되는 SPA(Single Page Application) 형태로 제작했습니다. 196 | - 회원 가입, 로그인, 서비스 소개, 상품 리스트, 특정 상품 정보 페이지를 구현했습니다. :pushpin: [코드 확인](https://github.com/veluminous/Ali_Express_Dropshipping/tree/master/frontend/pages) 197 | - Product detail 페이지는 모든 페이지를 미리 생성해둘 수 없으므로 서버 사이드 랜더링(Server-side rendering)으로 구현하고, 그 외의 페이지는 클라이언트 사이드 랜더링(Client-side rendering)으로 구현했습니다. :pushpin: [코드 확인](https://github.com/veluminous/Ali_Express_Dropshipping/blob/6714a11f64eaa8d5b060fb31c71782892958c6b4/frontend/pages/product/%5Bproduct_id%5D.tsx#L160) 198 | - 특정 페이지는 로그인을 했을 경우에만 접근 가능하도록 구현했습니다. :pushpin: [코드 예시 확인](https://github.com/veluminous/Ali_Express_Dropshipping/blob/6714a11f64eaa8d5b060fb31c71782892958c6b4/frontend/pages/product/%5Bproduct_id%5D.tsx#L179) 199 | - 검색어를 입력할 때마다 API 호출로 자동 완성되도록 `Autocomplete` 컴포넌트를 구현했습니다. :pushpin: [코드 확인](https://github.com/veluminous/Ali_Express_Dropshipping/blob/06385b224bf2af0b28161494a96138eea0505ed5/frontend/src/components/autocomplete.tsx#L5) 200 | - 상품 리스트 페이지에 pagination 기능을 부여하는 `Pagination` 컴포넌트를 구현했습니다. :pushpin: [코드 확인](https://github.com/veluminous/Ali_Express_Dropshipping/blob/06385b224bf2af0b28161494a96138eea0505ed5/frontend/src/components/pagination.tsx#L6) 201 | - Bootstrap으로 디자인을 적용했습니다. 202 | 203 | ​ 204 | -------------------------------------------------------------------------------- /backend/alembic/versions/d6d6cb7eee1d_first_migration.py: -------------------------------------------------------------------------------- 1 | """first migration 2 | 3 | Revision ID: d6d6cb7eee1d 4 | Revises: 5 | Create Date: 2021-07-03 16:11:17.979257 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'd6d6cb7eee1d' 14 | down_revision = None 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.create_table('product_details', 22 | sa.Column('id', sa.Integer(), nullable=False), 23 | sa.Column('productUrl', sa.String(), nullable=True), 24 | sa.Column('productId', sa.String(), nullable=True), 25 | sa.Column('statusId', sa.String(), nullable=True), 26 | sa.Column('status', sa.String(), nullable=True), 27 | sa.Column('currency', sa.String(), nullable=True), 28 | sa.Column('locale', sa.String(), nullable=True), 29 | sa.Column('shipTo', sa.String(), nullable=True), 30 | sa.Column('title', sa.String(), nullable=True), 31 | sa.Column('totalStock', sa.Integer(), nullable=True), 32 | sa.Column('totalOrders', sa.Integer(), nullable=True), 33 | sa.Column('wishlistCount', sa.Integer(), nullable=True), 34 | sa.Column('unitName', sa.String(), nullable=True), 35 | sa.Column('unitNamePlural', sa.String(), nullable=True), 36 | sa.Column('unitsPerProduct', sa.Integer(), nullable=True), 37 | sa.Column('hasPurchaseLimit', sa.Boolean(), nullable=True), 38 | sa.Column('maxPurchaseLimit', sa.Integer(), nullable=True), 39 | sa.Column('processingTimeInDays', sa.Integer(), nullable=True), 40 | sa.Column('productImages', sa.String(), nullable=True), 41 | sa.Column('productCategory', sa.String(), nullable=True), 42 | sa.Column('seller', sa.String(), nullable=True), 43 | sa.Column('sellerDetails', sa.String(), nullable=True), 44 | sa.Column('hasSinglePrice', sa.Boolean(), nullable=True), 45 | sa.Column('price', sa.String(), nullable=True), 46 | sa.Column('hasAttributes', sa.Boolean(), nullable=True), 47 | sa.Column('attributes', sa.String(), nullable=True), 48 | sa.Column('hasReviewsRatings', sa.Boolean(), nullable=True), 49 | sa.Column('reviewsRatings', sa.String(), nullable=True), 50 | sa.Column('hasProperties', sa.Boolean(), nullable=True), 51 | sa.Column('properties', sa.String(), nullable=True), 52 | sa.Column('hasVariations', sa.Boolean(), nullable=True), 53 | sa.Column('variations', sa.String(), nullable=True), 54 | sa.Column('shipping', sa.String(), nullable=True), 55 | sa.Column('htmlDescription', sa.String(), nullable=True), 56 | sa.Column('priceSummary', sa.Integer(), nullable=True), 57 | sa.PrimaryKeyConstraint('id') 58 | ) 59 | op.create_index(op.f('ix_product_details_attributes'), 'product_details', ['attributes'], unique=False) 60 | op.create_index(op.f('ix_product_details_currency'), 'product_details', ['currency'], unique=False) 61 | op.create_index(op.f('ix_product_details_hasAttributes'), 'product_details', ['hasAttributes'], unique=False) 62 | op.create_index(op.f('ix_product_details_hasProperties'), 'product_details', ['hasProperties'], unique=False) 63 | op.create_index(op.f('ix_product_details_hasPurchaseLimit'), 'product_details', ['hasPurchaseLimit'], unique=False) 64 | op.create_index(op.f('ix_product_details_hasReviewsRatings'), 'product_details', ['hasReviewsRatings'], unique=False) 65 | op.create_index(op.f('ix_product_details_hasSinglePrice'), 'product_details', ['hasSinglePrice'], unique=False) 66 | op.create_index(op.f('ix_product_details_hasVariations'), 'product_details', ['hasVariations'], unique=False) 67 | op.create_index(op.f('ix_product_details_htmlDescription'), 'product_details', ['htmlDescription'], unique=False) 68 | op.create_index(op.f('ix_product_details_id'), 'product_details', ['id'], unique=False) 69 | op.create_index(op.f('ix_product_details_locale'), 'product_details', ['locale'], unique=False) 70 | op.create_index(op.f('ix_product_details_maxPurchaseLimit'), 'product_details', ['maxPurchaseLimit'], unique=False) 71 | op.create_index(op.f('ix_product_details_price'), 'product_details', ['price'], unique=False) 72 | op.create_index(op.f('ix_product_details_priceSummary'), 'product_details', ['priceSummary'], unique=False) 73 | op.create_index(op.f('ix_product_details_processingTimeInDays'), 'product_details', ['processingTimeInDays'], unique=False) 74 | op.create_index(op.f('ix_product_details_productCategory'), 'product_details', ['productCategory'], unique=False) 75 | op.create_index(op.f('ix_product_details_productId'), 'product_details', ['productId'], unique=False) 76 | op.create_index(op.f('ix_product_details_productImages'), 'product_details', ['productImages'], unique=False) 77 | op.create_index(op.f('ix_product_details_productUrl'), 'product_details', ['productUrl'], unique=False) 78 | op.create_index(op.f('ix_product_details_properties'), 'product_details', ['properties'], unique=False) 79 | op.create_index(op.f('ix_product_details_reviewsRatings'), 'product_details', ['reviewsRatings'], unique=False) 80 | op.create_index(op.f('ix_product_details_seller'), 'product_details', ['seller'], unique=False) 81 | op.create_index(op.f('ix_product_details_sellerDetails'), 'product_details', ['sellerDetails'], unique=False) 82 | op.create_index(op.f('ix_product_details_shipTo'), 'product_details', ['shipTo'], unique=False) 83 | op.create_index(op.f('ix_product_details_shipping'), 'product_details', ['shipping'], unique=False) 84 | op.create_index(op.f('ix_product_details_status'), 'product_details', ['status'], unique=False) 85 | op.create_index(op.f('ix_product_details_statusId'), 'product_details', ['statusId'], unique=False) 86 | op.create_index(op.f('ix_product_details_title'), 'product_details', ['title'], unique=False) 87 | op.create_index(op.f('ix_product_details_totalOrders'), 'product_details', ['totalOrders'], unique=False) 88 | op.create_index(op.f('ix_product_details_totalStock'), 'product_details', ['totalStock'], unique=False) 89 | op.create_index(op.f('ix_product_details_unitName'), 'product_details', ['unitName'], unique=False) 90 | op.create_index(op.f('ix_product_details_unitNamePlural'), 'product_details', ['unitNamePlural'], unique=False) 91 | op.create_index(op.f('ix_product_details_unitsPerProduct'), 'product_details', ['unitsPerProduct'], unique=False) 92 | op.create_index(op.f('ix_product_details_variations'), 'product_details', ['variations'], unique=False) 93 | op.create_index(op.f('ix_product_details_wishlistCount'), 'product_details', ['wishlistCount'], unique=False) 94 | op.create_table('search_texts', 95 | sa.Column('id', sa.Integer(), nullable=False), 96 | sa.Column('text', sa.String(), nullable=True), 97 | sa.Column('page', sa.Integer(), nullable=True), 98 | sa.PrimaryKeyConstraint('id') 99 | ) 100 | op.create_index(op.f('ix_search_texts_id'), 'search_texts', ['id'], unique=False) 101 | op.create_index(op.f('ix_search_texts_text'), 'search_texts', ['text'], unique=False) 102 | op.create_table('users', 103 | sa.Column('id', sa.Integer(), nullable=False), 104 | sa.Column('email', sa.String(), nullable=True), 105 | sa.Column('hashed_password', sa.String(), nullable=True), 106 | sa.Column('is_active', sa.Boolean(), nullable=True), 107 | sa.PrimaryKeyConstraint('id') 108 | ) 109 | op.create_index(op.f('ix_users_email'), 'users', ['email'], unique=True) 110 | op.create_index(op.f('ix_users_id'), 'users', ['id'], unique=False) 111 | op.create_table('items', 112 | sa.Column('id', sa.Integer(), nullable=False), 113 | sa.Column('title', sa.String(), nullable=True), 114 | sa.Column('description', sa.String(), nullable=True), 115 | sa.Column('owner_id', sa.Integer(), nullable=True), 116 | sa.ForeignKeyConstraint(['owner_id'], ['users.id'], ), 117 | sa.PrimaryKeyConstraint('id') 118 | ) 119 | op.create_index(op.f('ix_items_description'), 'items', ['description'], unique=False) 120 | op.create_index(op.f('ix_items_id'), 'items', ['id'], unique=False) 121 | op.create_index(op.f('ix_items_title'), 'items', ['title'], unique=False) 122 | op.create_table('product_lists', 123 | sa.Column('id', sa.Integer(), nullable=False), 124 | sa.Column('product', sa.JSON(), nullable=True), 125 | sa.Column('search_text_id', sa.Integer(), nullable=True), 126 | sa.ForeignKeyConstraint(['search_text_id'], ['search_texts.id'], ), 127 | sa.PrimaryKeyConstraint('id') 128 | ) 129 | op.create_index(op.f('ix_product_lists_id'), 'product_lists', ['id'], unique=False) 130 | # ### end Alembic commands ### 131 | 132 | 133 | def downgrade(): 134 | # ### commands auto generated by Alembic - please adjust! ### 135 | op.drop_index(op.f('ix_product_lists_id'), table_name='product_lists') 136 | op.drop_table('product_lists') 137 | op.drop_index(op.f('ix_items_title'), table_name='items') 138 | op.drop_index(op.f('ix_items_id'), table_name='items') 139 | op.drop_index(op.f('ix_items_description'), table_name='items') 140 | op.drop_table('items') 141 | op.drop_index(op.f('ix_users_id'), table_name='users') 142 | op.drop_index(op.f('ix_users_email'), table_name='users') 143 | op.drop_table('users') 144 | op.drop_index(op.f('ix_search_texts_text'), table_name='search_texts') 145 | op.drop_index(op.f('ix_search_texts_id'), table_name='search_texts') 146 | op.drop_table('search_texts') 147 | op.drop_index(op.f('ix_product_details_wishlistCount'), table_name='product_details') 148 | op.drop_index(op.f('ix_product_details_variations'), table_name='product_details') 149 | op.drop_index(op.f('ix_product_details_unitsPerProduct'), table_name='product_details') 150 | op.drop_index(op.f('ix_product_details_unitNamePlural'), table_name='product_details') 151 | op.drop_index(op.f('ix_product_details_unitName'), table_name='product_details') 152 | op.drop_index(op.f('ix_product_details_totalStock'), table_name='product_details') 153 | op.drop_index(op.f('ix_product_details_totalOrders'), table_name='product_details') 154 | op.drop_index(op.f('ix_product_details_title'), table_name='product_details') 155 | op.drop_index(op.f('ix_product_details_statusId'), table_name='product_details') 156 | op.drop_index(op.f('ix_product_details_status'), table_name='product_details') 157 | op.drop_index(op.f('ix_product_details_shipping'), table_name='product_details') 158 | op.drop_index(op.f('ix_product_details_shipTo'), table_name='product_details') 159 | op.drop_index(op.f('ix_product_details_sellerDetails'), table_name='product_details') 160 | op.drop_index(op.f('ix_product_details_seller'), table_name='product_details') 161 | op.drop_index(op.f('ix_product_details_reviewsRatings'), table_name='product_details') 162 | op.drop_index(op.f('ix_product_details_properties'), table_name='product_details') 163 | op.drop_index(op.f('ix_product_details_productUrl'), table_name='product_details') 164 | op.drop_index(op.f('ix_product_details_productImages'), table_name='product_details') 165 | op.drop_index(op.f('ix_product_details_productId'), table_name='product_details') 166 | op.drop_index(op.f('ix_product_details_productCategory'), table_name='product_details') 167 | op.drop_index(op.f('ix_product_details_processingTimeInDays'), table_name='product_details') 168 | op.drop_index(op.f('ix_product_details_priceSummary'), table_name='product_details') 169 | op.drop_index(op.f('ix_product_details_price'), table_name='product_details') 170 | op.drop_index(op.f('ix_product_details_maxPurchaseLimit'), table_name='product_details') 171 | op.drop_index(op.f('ix_product_details_locale'), table_name='product_details') 172 | op.drop_index(op.f('ix_product_details_id'), table_name='product_details') 173 | op.drop_index(op.f('ix_product_details_htmlDescription'), table_name='product_details') 174 | op.drop_index(op.f('ix_product_details_hasVariations'), table_name='product_details') 175 | op.drop_index(op.f('ix_product_details_hasSinglePrice'), table_name='product_details') 176 | op.drop_index(op.f('ix_product_details_hasReviewsRatings'), table_name='product_details') 177 | op.drop_index(op.f('ix_product_details_hasPurchaseLimit'), table_name='product_details') 178 | op.drop_index(op.f('ix_product_details_hasProperties'), table_name='product_details') 179 | op.drop_index(op.f('ix_product_details_hasAttributes'), table_name='product_details') 180 | op.drop_index(op.f('ix_product_details_currency'), table_name='product_details') 181 | op.drop_index(op.f('ix_product_details_attributes'), table_name='product_details') 182 | op.drop_table('product_details') 183 | # ### end Alembic commands ### 184 | -------------------------------------------------------------------------------- /backend/alembic/versions/d88bf9ac6f99_changed_column_names_in_productdetail_.py: -------------------------------------------------------------------------------- 1 | """Changed column names in ProductDetail and altered type of some columns 2 | 3 | Revision ID: d88bf9ac6f99 4 | Revises: e5381fcd65d0 5 | Create Date: 2021-07-07 23:31:02.770436 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'd88bf9ac6f99' 14 | down_revision = 'e5381fcd65d0' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.add_column('product_details', sa.Column('product_url', sa.String(), nullable=True)) 22 | op.add_column('product_details', sa.Column('product_id', sa.String(), nullable=True)) 23 | op.add_column('product_details', sa.Column('status_id', sa.String(), nullable=True)) 24 | op.add_column('product_details', sa.Column('ship_to', sa.String(), nullable=True)) 25 | op.add_column('product_details', sa.Column('total_stock', sa.Integer(), nullable=True)) 26 | op.add_column('product_details', sa.Column('total_orders', sa.Integer(), nullable=True)) 27 | op.add_column('product_details', sa.Column('wishlist_count', sa.Integer(), nullable=True)) 28 | op.add_column('product_details', sa.Column('unit_name', sa.String(), nullable=True)) 29 | op.add_column('product_details', sa.Column('unit_name_plural', sa.String(), nullable=True)) 30 | op.add_column('product_details', sa.Column('units_per_product', sa.Integer(), nullable=True)) 31 | op.add_column('product_details', sa.Column('has_purchase_limit', sa.Boolean(), nullable=True)) 32 | op.add_column('product_details', sa.Column('max_purchase_limit', sa.Integer(), nullable=True)) 33 | op.add_column('product_details', sa.Column('processing_time_in_days', sa.Integer(), nullable=True)) 34 | op.add_column('product_details', sa.Column('has_single_price', sa.Boolean(), nullable=True)) 35 | op.add_column('product_details', sa.Column('has_attributes', sa.Boolean(), nullable=True)) 36 | op.add_column('product_details', sa.Column('has_reviews_ratings', sa.Boolean(), nullable=True)) 37 | op.add_column('product_details', sa.Column('has_properties', sa.Boolean(), nullable=True)) 38 | op.add_column('product_details', sa.Column('has_variations', sa.Boolean(), nullable=True)) 39 | op.add_column('product_details', sa.Column('html_description', sa.String(), nullable=True)) 40 | op.drop_index('ix_product_details_attributes', table_name='product_details') 41 | op.drop_index('ix_product_details_hasAttributes', table_name='product_details') 42 | op.drop_index('ix_product_details_hasReviewsRatings', table_name='product_details') 43 | op.drop_index('ix_product_details_hasSinglePrice', table_name='product_details') 44 | op.drop_index('ix_product_details_price', table_name='product_details') 45 | op.drop_index('ix_product_details_productCategory', table_name='product_details') 46 | op.drop_index('ix_product_details_productId', table_name='product_details') 47 | op.drop_index('ix_product_details_productImages', table_name='product_details') 48 | op.drop_index('ix_product_details_productUrl', table_name='product_details') 49 | op.drop_index('ix_product_details_reviewsRatings', table_name='product_details') 50 | op.drop_index('ix_product_details_seller', table_name='product_details') 51 | op.drop_index('ix_product_details_sellerDetails', table_name='product_details') 52 | op.drop_index('ix_product_details_shipTo', table_name='product_details') 53 | op.drop_index('ix_product_details_statusId', table_name='product_details') 54 | op.create_index(op.f('ix_product_details_has_attributes'), 'product_details', ['has_attributes'], unique=False) 55 | op.create_index(op.f('ix_product_details_has_reviews_ratings'), 'product_details', ['has_reviews_ratings'], unique=False) 56 | op.create_index(op.f('ix_product_details_has_single_price'), 'product_details', ['has_single_price'], unique=False) 57 | op.create_index(op.f('ix_product_details_product_id'), 'product_details', ['product_id'], unique=False) 58 | op.create_index(op.f('ix_product_details_product_url'), 'product_details', ['product_url'], unique=False) 59 | op.create_index(op.f('ix_product_details_ship_to'), 'product_details', ['ship_to'], unique=False) 60 | op.create_index(op.f('ix_product_details_status_id'), 'product_details', ['status_id'], unique=False) 61 | op.drop_column('product_details', 'productUrl') 62 | op.drop_column('product_details', 'productCategory') 63 | op.drop_column('product_details', 'hasVariations') 64 | op.drop_column('product_details', 'hasProperties') 65 | op.drop_column('product_details', 'processingTimeInDays') 66 | op.drop_column('product_details', 'shipTo') 67 | op.drop_column('product_details', 'shipping') 68 | op.drop_column('product_details', 'maxPurchaseLimit') 69 | op.drop_column('product_details', 'totalOrders') 70 | op.drop_column('product_details', 'unitNamePlural') 71 | op.drop_column('product_details', 'attributes') 72 | op.drop_column('product_details', 'totalStock') 73 | op.drop_column('product_details', 'hasReviewsRatings') 74 | op.drop_column('product_details', 'seller') 75 | op.drop_column('product_details', 'variations') 76 | op.drop_column('product_details', 'productId') 77 | op.drop_column('product_details', 'price') 78 | op.drop_column('product_details', 'unitsPerProduct') 79 | op.drop_column('product_details', 'sellerDetails') 80 | op.drop_column('product_details', 'wishlistCount') 81 | op.drop_column('product_details', 'hasPurchaseLimit') 82 | op.drop_column('product_details', 'productImages') 83 | op.drop_column('product_details', 'htmlDescription') 84 | op.drop_column('product_details', 'hasSinglePrice') 85 | op.drop_column('product_details', 'hasAttributes') 86 | op.drop_column('product_details', 'properties') 87 | op.drop_column('product_details', 'unitName') 88 | op.drop_column('product_details', 'statusId') 89 | op.drop_column('product_details', 'reviewsRatings') 90 | # ### end Alembic commands ### 91 | 92 | 93 | def downgrade(): 94 | # ### commands auto generated by Alembic - please adjust! ### 95 | op.add_column('product_details', sa.Column('reviewsRatings', sa.VARCHAR(), autoincrement=False, nullable=True)) 96 | op.add_column('product_details', sa.Column('statusId', sa.VARCHAR(), autoincrement=False, nullable=True)) 97 | op.add_column('product_details', sa.Column('unitName', sa.VARCHAR(), autoincrement=False, nullable=True)) 98 | op.add_column('product_details', sa.Column('properties', sa.VARCHAR(), autoincrement=False, nullable=True)) 99 | op.add_column('product_details', sa.Column('hasAttributes', sa.BOOLEAN(), autoincrement=False, nullable=True)) 100 | op.add_column('product_details', sa.Column('hasSinglePrice', sa.BOOLEAN(), autoincrement=False, nullable=True)) 101 | op.add_column('product_details', sa.Column('htmlDescription', sa.VARCHAR(), autoincrement=False, nullable=True)) 102 | op.add_column('product_details', sa.Column('productImages', sa.VARCHAR(), autoincrement=False, nullable=True)) 103 | op.add_column('product_details', sa.Column('hasPurchaseLimit', sa.BOOLEAN(), autoincrement=False, nullable=True)) 104 | op.add_column('product_details', sa.Column('wishlistCount', sa.INTEGER(), autoincrement=False, nullable=True)) 105 | op.add_column('product_details', sa.Column('sellerDetails', sa.VARCHAR(), autoincrement=False, nullable=True)) 106 | op.add_column('product_details', sa.Column('unitsPerProduct', sa.INTEGER(), autoincrement=False, nullable=True)) 107 | op.add_column('product_details', sa.Column('price', sa.VARCHAR(), autoincrement=False, nullable=True)) 108 | op.add_column('product_details', sa.Column('productId', sa.VARCHAR(), autoincrement=False, nullable=True)) 109 | op.add_column('product_details', sa.Column('variations', sa.VARCHAR(), autoincrement=False, nullable=True)) 110 | op.add_column('product_details', sa.Column('seller', sa.VARCHAR(), autoincrement=False, nullable=True)) 111 | op.add_column('product_details', sa.Column('hasReviewsRatings', sa.BOOLEAN(), autoincrement=False, nullable=True)) 112 | op.add_column('product_details', sa.Column('totalStock', sa.INTEGER(), autoincrement=False, nullable=True)) 113 | op.add_column('product_details', sa.Column('attributes', sa.VARCHAR(), autoincrement=False, nullable=True)) 114 | op.add_column('product_details', sa.Column('unitNamePlural', sa.VARCHAR(), autoincrement=False, nullable=True)) 115 | op.add_column('product_details', sa.Column('totalOrders', sa.INTEGER(), autoincrement=False, nullable=True)) 116 | op.add_column('product_details', sa.Column('maxPurchaseLimit', sa.INTEGER(), autoincrement=False, nullable=True)) 117 | op.add_column('product_details', sa.Column('shipping', sa.VARCHAR(), autoincrement=False, nullable=True)) 118 | op.add_column('product_details', sa.Column('shipTo', sa.VARCHAR(), autoincrement=False, nullable=True)) 119 | op.add_column('product_details', sa.Column('processingTimeInDays', sa.INTEGER(), autoincrement=False, nullable=True)) 120 | op.add_column('product_details', sa.Column('hasProperties', sa.BOOLEAN(), autoincrement=False, nullable=True)) 121 | op.add_column('product_details', sa.Column('hasVariations', sa.BOOLEAN(), autoincrement=False, nullable=True)) 122 | op.add_column('product_details', sa.Column('productCategory', sa.VARCHAR(), autoincrement=False, nullable=True)) 123 | op.add_column('product_details', sa.Column('productUrl', sa.VARCHAR(), autoincrement=False, nullable=True)) 124 | op.drop_index(op.f('ix_product_details_status_id'), table_name='product_details') 125 | op.drop_index(op.f('ix_product_details_ship_to'), table_name='product_details') 126 | op.drop_index(op.f('ix_product_details_product_url'), table_name='product_details') 127 | op.drop_index(op.f('ix_product_details_product_id'), table_name='product_details') 128 | op.drop_index(op.f('ix_product_details_has_single_price'), table_name='product_details') 129 | op.drop_index(op.f('ix_product_details_has_reviews_ratings'), table_name='product_details') 130 | op.drop_index(op.f('ix_product_details_has_attributes'), table_name='product_details') 131 | op.create_index('ix_product_details_statusId', 'product_details', ['statusId'], unique=False) 132 | op.create_index('ix_product_details_shipTo', 'product_details', ['shipTo'], unique=False) 133 | op.create_index('ix_product_details_sellerDetails', 'product_details', ['sellerDetails'], unique=False) 134 | op.create_index('ix_product_details_seller', 'product_details', ['seller'], unique=False) 135 | op.create_index('ix_product_details_reviewsRatings', 'product_details', ['reviewsRatings'], unique=False) 136 | op.create_index('ix_product_details_productUrl', 'product_details', ['productUrl'], unique=False) 137 | op.create_index('ix_product_details_productImages', 'product_details', ['productImages'], unique=False) 138 | op.create_index('ix_product_details_productId', 'product_details', ['productId'], unique=False) 139 | op.create_index('ix_product_details_productCategory', 'product_details', ['productCategory'], unique=False) 140 | op.create_index('ix_product_details_price', 'product_details', ['price'], unique=False) 141 | op.create_index('ix_product_details_hasSinglePrice', 'product_details', ['hasSinglePrice'], unique=False) 142 | op.create_index('ix_product_details_hasReviewsRatings', 'product_details', ['hasReviewsRatings'], unique=False) 143 | op.create_index('ix_product_details_hasAttributes', 'product_details', ['hasAttributes'], unique=False) 144 | op.create_index('ix_product_details_attributes', 'product_details', ['attributes'], unique=False) 145 | op.drop_column('product_details', 'html_description') 146 | op.drop_column('product_details', 'has_variations') 147 | op.drop_column('product_details', 'has_properties') 148 | op.drop_column('product_details', 'has_reviews_ratings') 149 | op.drop_column('product_details', 'has_attributes') 150 | op.drop_column('product_details', 'has_single_price') 151 | op.drop_column('product_details', 'processing_time_in_days') 152 | op.drop_column('product_details', 'max_purchase_limit') 153 | op.drop_column('product_details', 'has_purchase_limit') 154 | op.drop_column('product_details', 'units_per_product') 155 | op.drop_column('product_details', 'unit_name_plural') 156 | op.drop_column('product_details', 'unit_name') 157 | op.drop_column('product_details', 'wishlist_count') 158 | op.drop_column('product_details', 'total_orders') 159 | op.drop_column('product_details', 'total_stock') 160 | op.drop_column('product_details', 'ship_to') 161 | op.drop_column('product_details', 'status_id') 162 | op.drop_column('product_details', 'product_id') 163 | op.drop_column('product_details', 'product_url') 164 | # ### end Alembic commands ### 165 | -------------------------------------------------------------------------------- /backend/Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "d580b887dea84381409b5c0d8c2ee0239384ef5c53e53279df66e84920eb3ac4" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.9" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "alembic": { 20 | "hashes": [ 21 | "sha256:bc5bdf03d1b9814ee4d72adc0b19df2123f6c50a60c1ea761733f3640feedb8d", 22 | "sha256:d0c580041f9f6487d5444df672a83da9be57398f39d6c1802bbedec6fefbeef6" 23 | ], 24 | "index": "pypi", 25 | "version": "==1.7.3" 26 | }, 27 | "asgiref": { 28 | "hashes": [ 29 | "sha256:4ef1ab46b484e3c706329cedeff284a5d40824200638503f5768edb6de7d58e9", 30 | "sha256:ffc141aa908e6f175673e7b1b3b7af4fdb0ecb738fc5c8b88f69f055c2415214" 31 | ], 32 | "markers": "python_version >= '3.6'", 33 | "version": "==3.4.1" 34 | }, 35 | "bcrypt": { 36 | "hashes": [ 37 | "sha256:5b93c1726e50a93a033c36e5ca7fdcd29a5c7395af50a6892f5d9e7c6cfbfb29", 38 | "sha256:63d4e3ff96188e5898779b6057878fecf3f11cfe6ec3b313ea09955d587ec7a7", 39 | "sha256:81fec756feff5b6818ea7ab031205e1d323d8943d237303baca2c5f9c7846f34", 40 | "sha256:a67fb841b35c28a59cebed05fbd3e80eea26e6d75851f0574a9273c80f3e9b55", 41 | "sha256:c95d4cbebffafcdd28bd28bb4e25b31c50f6da605c81ffd9ad8a3d1b2ab7b1b6", 42 | "sha256:cd1ea2ff3038509ea95f687256c46b79f5fc382ad0aa3664d200047546d511d1", 43 | "sha256:cdcdcb3972027f83fe24a48b1e90ea4b584d35f1cc279d76de6fc4b13376239d" 44 | ], 45 | "version": "==3.2.0" 46 | }, 47 | "certifi": { 48 | "hashes": [ 49 | "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee", 50 | "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8" 51 | ], 52 | "version": "==2021.5.30" 53 | }, 54 | "cffi": { 55 | "hashes": [ 56 | "sha256:06c54a68935738d206570b20da5ef2b6b6d92b38ef3ec45c5422c0ebaf338d4d", 57 | "sha256:0c0591bee64e438883b0c92a7bed78f6290d40bf02e54c5bf0978eaf36061771", 58 | "sha256:19ca0dbdeda3b2615421d54bef8985f72af6e0c47082a8d26122adac81a95872", 59 | "sha256:22b9c3c320171c108e903d61a3723b51e37aaa8c81255b5e7ce102775bd01e2c", 60 | "sha256:26bb2549b72708c833f5abe62b756176022a7b9a7f689b571e74c8478ead51dc", 61 | "sha256:33791e8a2dc2953f28b8d8d300dde42dd929ac28f974c4b4c6272cb2955cb762", 62 | "sha256:3c8d896becff2fa653dc4438b54a5a25a971d1f4110b32bd3068db3722c80202", 63 | "sha256:4373612d59c404baeb7cbd788a18b2b2a8331abcc84c3ba40051fcd18b17a4d5", 64 | "sha256:487d63e1454627c8e47dd230025780e91869cfba4c753a74fda196a1f6ad6548", 65 | "sha256:48916e459c54c4a70e52745639f1db524542140433599e13911b2f329834276a", 66 | "sha256:4922cd707b25e623b902c86188aca466d3620892db76c0bdd7b99a3d5e61d35f", 67 | "sha256:55af55e32ae468e9946f741a5d51f9896da6b9bf0bbdd326843fec05c730eb20", 68 | "sha256:57e555a9feb4a8460415f1aac331a2dc833b1115284f7ded7278b54afc5bd218", 69 | "sha256:5d4b68e216fc65e9fe4f524c177b54964af043dde734807586cf5435af84045c", 70 | "sha256:64fda793737bc4037521d4899be780534b9aea552eb673b9833b01f945904c2e", 71 | "sha256:6d6169cb3c6c2ad50db5b868db6491a790300ade1ed5d1da29289d73bbe40b56", 72 | "sha256:7bcac9a2b4fdbed2c16fa5681356d7121ecabf041f18d97ed5b8e0dd38a80224", 73 | "sha256:80b06212075346b5546b0417b9f2bf467fea3bfe7352f781ffc05a8ab24ba14a", 74 | "sha256:818014c754cd3dba7229c0f5884396264d51ffb87ec86e927ef0be140bfdb0d2", 75 | "sha256:8eb687582ed7cd8c4bdbff3df6c0da443eb89c3c72e6e5dcdd9c81729712791a", 76 | "sha256:99f27fefe34c37ba9875f224a8f36e31d744d8083e00f520f133cab79ad5e819", 77 | "sha256:9f3e33c28cd39d1b655ed1ba7247133b6f7fc16fa16887b120c0c670e35ce346", 78 | "sha256:a8661b2ce9694ca01c529bfa204dbb144b275a31685a075ce123f12331be790b", 79 | "sha256:a9da7010cec5a12193d1af9872a00888f396aba3dc79186604a09ea3ee7c029e", 80 | "sha256:aedb15f0a5a5949ecb129a82b72b19df97bbbca024081ed2ef88bd5c0a610534", 81 | "sha256:b315d709717a99f4b27b59b021e6207c64620790ca3e0bde636a6c7f14618abb", 82 | "sha256:ba6f2b3f452e150945d58f4badd92310449876c4c954836cfb1803bdd7b422f0", 83 | "sha256:c33d18eb6e6bc36f09d793c0dc58b0211fccc6ae5149b808da4a62660678b156", 84 | "sha256:c9a875ce9d7fe32887784274dd533c57909b7b1dcadcc128a2ac21331a9765dd", 85 | "sha256:c9e005e9bd57bc987764c32a1bee4364c44fdc11a3cc20a40b93b444984f2b87", 86 | "sha256:d2ad4d668a5c0645d281dcd17aff2be3212bc109b33814bbb15c4939f44181cc", 87 | "sha256:d950695ae4381ecd856bcaf2b1e866720e4ab9a1498cba61c602e56630ca7195", 88 | "sha256:e22dcb48709fc51a7b58a927391b23ab37eb3737a98ac4338e2448bef8559b33", 89 | "sha256:e8c6a99be100371dbb046880e7a282152aa5d6127ae01783e37662ef73850d8f", 90 | "sha256:e9dc245e3ac69c92ee4c167fbdd7428ec1956d4e754223124991ef29eb57a09d", 91 | "sha256:eb687a11f0a7a1839719edd80f41e459cc5366857ecbed383ff376c4e3cc6afd", 92 | "sha256:eb9e2a346c5238a30a746893f23a9535e700f8192a68c07c0258e7ece6ff3728", 93 | "sha256:ed38b924ce794e505647f7c331b22a693bee1538fdf46b0222c4717b42f744e7", 94 | "sha256:f0010c6f9d1a4011e429109fda55a225921e3206e7f62a0c22a35344bfd13cca", 95 | "sha256:f0c5d1acbfca6ebdd6b1e3eded8d261affb6ddcf2186205518f1428b8569bb99", 96 | "sha256:f10afb1004f102c7868ebfe91c28f4a712227fe4cb24974350ace1f90e1febbf", 97 | "sha256:f174135f5609428cc6e1b9090f9268f5c8935fddb1b25ccb8255a2d50de6789e", 98 | "sha256:f3ebe6e73c319340830a9b2825d32eb6d8475c1dac020b4f0aa774ee3b898d1c", 99 | "sha256:f627688813d0a4140153ff532537fbe4afea5a3dffce1f9deb7f91f848a832b5", 100 | "sha256:fd4305f86f53dfd8cd3522269ed7fc34856a8ee3709a5e28b2836b2db9d4cd69" 101 | ], 102 | "version": "==1.14.6" 103 | }, 104 | "charset-normalizer": { 105 | "hashes": [ 106 | "sha256:5d209c0a931f215cee683b6445e2d77677e7e75e159f78def0db09d68fafcaa6", 107 | "sha256:5ec46d183433dcbd0ab716f2d7f29d8dee50505b3fdb40c6b985c7c4f5a3591f" 108 | ], 109 | "markers": "python_version >= '3'", 110 | "version": "==2.0.6" 111 | }, 112 | "click": { 113 | "hashes": [ 114 | "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a", 115 | "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6" 116 | ], 117 | "markers": "python_version >= '3.6'", 118 | "version": "==8.0.1" 119 | }, 120 | "colorama": { 121 | "hashes": [ 122 | "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b", 123 | "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2" 124 | ], 125 | "version": "==0.4.4" 126 | }, 127 | "cryptography": { 128 | "hashes": [ 129 | "sha256:0a7dcbcd3f1913f664aca35d47c1331fce738d44ec34b7be8b9d332151b0b01e", 130 | "sha256:1eb7bb0df6f6f583dd8e054689def236255161ebbcf62b226454ab9ec663746b", 131 | "sha256:21ca464b3a4b8d8e86ba0ee5045e103a1fcfac3b39319727bc0fc58c09c6aff7", 132 | "sha256:34dae04a0dce5730d8eb7894eab617d8a70d0c97da76b905de9efb7128ad7085", 133 | "sha256:3520667fda779eb788ea00080124875be18f2d8f0848ec00733c0ec3bb8219fc", 134 | "sha256:3c4129fc3fdc0fa8e40861b5ac0c673315b3c902bbdc05fc176764815b43dd1d", 135 | "sha256:3fa3a7ccf96e826affdf1a0a9432be74dc73423125c8f96a909e3835a5ef194a", 136 | "sha256:5b0fbfae7ff7febdb74b574055c7466da334a5371f253732d7e2e7525d570498", 137 | "sha256:695104a9223a7239d155d7627ad912953b540929ef97ae0c34c7b8bf30857e89", 138 | "sha256:8695456444f277af73a4877db9fc979849cd3ee74c198d04fc0776ebc3db52b9", 139 | "sha256:94cc5ed4ceaefcbe5bf38c8fba6a21fc1d365bb8fb826ea1688e3370b2e24a1c", 140 | "sha256:94fff993ee9bc1b2440d3b7243d488c6a3d9724cc2b09cdb297f6a886d040ef7", 141 | "sha256:9965c46c674ba8cc572bc09a03f4c649292ee73e1b683adb1ce81e82e9a6a0fb", 142 | "sha256:a00cf305f07b26c351d8d4e1af84ad7501eca8a342dedf24a7acb0e7b7406e14", 143 | "sha256:a305600e7a6b7b855cd798e00278161b681ad6e9b7eca94c721d5f588ab212af", 144 | "sha256:cd65b60cfe004790c795cc35f272e41a3df4631e2fb6b35aa7ac6ef2859d554e", 145 | "sha256:d2a6e5ef66503da51d2110edf6c403dc6b494cc0082f85db12f54e9c5d4c3ec5", 146 | "sha256:d9ec0e67a14f9d1d48dd87a2531009a9b251c02ea42851c060b25c782516ff06", 147 | "sha256:f44d141b8c4ea5eb4dbc9b3ad992d45580c1d22bf5e24363f2fbf50c2d7ae8a7" 148 | ], 149 | "version": "==3.4.8" 150 | }, 151 | "dnspython": { 152 | "hashes": [ 153 | "sha256:95d12f6ef0317118d2a1a6fc49aac65ffec7eb8087474158f42f26a639135216", 154 | "sha256:e4a87f0b573201a0f3727fa18a516b055fd1107e0e5477cded4a2de497df1dd4" 155 | ], 156 | "markers": "python_version >= '3.6'", 157 | "version": "==2.1.0" 158 | }, 159 | "ecdsa": { 160 | "hashes": [ 161 | "sha256:5cf31d5b33743abe0dfc28999036c849a69d548f994b535e527ee3cb7f3ef676", 162 | "sha256:b9f500bb439e4153d0330610f5d26baaf18d17b8ced1bc54410d189385ea68aa" 163 | ], 164 | "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", 165 | "version": "==0.17.0" 166 | }, 167 | "email-validator": { 168 | "hashes": [ 169 | "sha256:5675c8ceb7106a37e40e2698a57c056756bf3f272cfa8682a4f87ebd95d8440b", 170 | "sha256:aa237a65f6f4da067119b7df3f13e89c25c051327b2b5b66dc075f33d62480d7" 171 | ], 172 | "version": "==1.1.3" 173 | }, 174 | "fastapi": { 175 | "hashes": [ 176 | "sha256:644bb815bae326575c4b2842469fb83053a4b974b82fa792ff9283d17fbbd99d", 177 | "sha256:94d2820906c36b9b8303796fb7271337ec89c74223229e3cfcf056b5a7d59e23" 178 | ], 179 | "index": "pypi", 180 | "version": "==0.68.1" 181 | }, 182 | "fastapi-pagination": { 183 | "extras": [ 184 | "sqlalchemy" 185 | ], 186 | "hashes": [ 187 | "sha256:1ee9784a5fcf82c44a92673567845fbf4e4ad07e9b9d9f395783c0ae15c1dfe9", 188 | "sha256:ade455825866f71560882e053381caa06a2141d36811d1bb3a37879df808f3b6" 189 | ], 190 | "index": "pypi", 191 | "version": "==0.9.0" 192 | }, 193 | "greenlet": { 194 | "hashes": [ 195 | "sha256:04e1849c88aa56584d4a0a6e36af5ec7cc37993fdc1fda72b56aa1394a92ded3", 196 | "sha256:05e72db813c28906cdc59bd0da7c325d9b82aa0b0543014059c34c8c4ad20e16", 197 | "sha256:07e6d88242e09b399682b39f8dfa1e7e6eca66b305de1ff74ed9eb1a7d8e539c", 198 | "sha256:090126004c8ab9cd0787e2acf63d79e80ab41a18f57d6448225bbfcba475034f", 199 | "sha256:1796f2c283faab2b71c67e9b9aefb3f201fdfbee5cb55001f5ffce9125f63a45", 200 | "sha256:2f89d74b4f423e756a018832cd7a0a571e0a31b9ca59323b77ce5f15a437629b", 201 | "sha256:34e6675167a238bede724ee60fe0550709e95adaff6a36bcc97006c365290384", 202 | "sha256:3e594015a2349ec6dcceda9aca29da8dc89e85b56825b7d1f138a3f6bb79dd4c", 203 | "sha256:3f8fc59bc5d64fa41f58b0029794f474223693fd00016b29f4e176b3ee2cfd9f", 204 | "sha256:3fc6a447735749d651d8919da49aab03c434a300e9f0af1c886d560405840fd1", 205 | "sha256:40abb7fec4f6294225d2b5464bb6d9552050ded14a7516588d6f010e7e366dcc", 206 | "sha256:44556302c0ab376e37939fd0058e1f0db2e769580d340fb03b01678d1ff25f68", 207 | "sha256:476ba9435afaead4382fbab8f1882f75e3fb2285c35c9285abb3dd30237f9142", 208 | "sha256:4870b018ca685ff573edd56b93f00a122f279640732bb52ce3a62b73ee5c4a92", 209 | "sha256:4adaf53ace289ced90797d92d767d37e7cdc29f13bd3830c3f0a561277a4ae83", 210 | "sha256:4eae94de9924bbb4d24960185363e614b1b62ff797c23dc3c8a7c75bbb8d187e", 211 | "sha256:5317701c7ce167205c0569c10abc4bd01c7f4cf93f642c39f2ce975fa9b78a3c", 212 | "sha256:5c3b735ccf8fc8048664ee415f8af5a3a018cc92010a0d7195395059b4b39b7d", 213 | "sha256:5cde7ee190196cbdc078511f4df0be367af85636b84d8be32230f4871b960687", 214 | "sha256:655ab836324a473d4cd8cf231a2d6f283ed71ed77037679da554e38e606a7117", 215 | "sha256:6ce9d0784c3c79f3e5c5c9c9517bbb6c7e8aa12372a5ea95197b8a99402aa0e6", 216 | "sha256:6e0696525500bc8aa12eae654095d2260db4dc95d5c35af2b486eae1bf914ccd", 217 | "sha256:75ff270fd05125dce3303e9216ccddc541a9e072d4fc764a9276d44dee87242b", 218 | "sha256:8039f5fe8030c43cd1732d9a234fdcbf4916fcc32e21745ca62e75023e4d4649", 219 | "sha256:84488516639c3c5e5c0e52f311fff94ebc45b56788c2a3bfe9cf8e75670f4de3", 220 | "sha256:84782c80a433d87530ae3f4b9ed58d4a57317d9918dfcc6a59115fa2d8731f2c", 221 | "sha256:8ddb38fb6ad96c2ef7468ff73ba5c6876b63b664eebb2c919c224261ae5e8378", 222 | "sha256:98b491976ed656be9445b79bc57ed21decf08a01aaaf5fdabf07c98c108111f6", 223 | "sha256:990e0f5e64bcbc6bdbd03774ecb72496224d13b664aa03afd1f9b171a3269272", 224 | "sha256:9b02e6039eafd75e029d8c58b7b1f3e450ca563ef1fe21c7e3e40b9936c8d03e", 225 | "sha256:a11b6199a0b9dc868990456a2667167d0ba096c5224f6258e452bfbe5a9742c5", 226 | "sha256:a414f8e14aa7bacfe1578f17c11d977e637d25383b6210587c29210af995ef04", 227 | "sha256:a91ee268f059583176c2c8b012a9fce7e49ca6b333a12bbc2dd01fc1a9783885", 228 | "sha256:ac991947ca6533ada4ce7095f0e28fe25d5b2f3266ad5b983ed4201e61596acf", 229 | "sha256:b050dbb96216db273b56f0e5960959c2b4cb679fe1e58a0c3906fa0a60c00662", 230 | "sha256:b97a807437b81f90f85022a9dcfd527deea38368a3979ccb49d93c9198b2c722", 231 | "sha256:bad269e442f1b7ffa3fa8820b3c3aa66f02a9f9455b5ba2db5a6f9eea96f56de", 232 | "sha256:bf3725d79b1ceb19e83fb1aed44095518c0fcff88fba06a76c0891cfd1f36837", 233 | "sha256:c0f22774cd8294078bdf7392ac73cf00bfa1e5e0ed644bd064fdabc5f2a2f481", 234 | "sha256:c1862f9f1031b1dee3ff00f1027fcd098ffc82120f43041fe67804b464bbd8a7", 235 | "sha256:c8d4ed48eed7414ccb2aaaecbc733ed2a84c299714eae3f0f48db085342d5629", 236 | "sha256:cf31e894dabb077a35bbe6963285d4515a387ff657bd25b0530c7168e48f167f", 237 | "sha256:d15cb6f8706678dc47fb4e4f8b339937b04eda48a0af1cca95f180db552e7663", 238 | "sha256:dfcb5a4056e161307d103bc013478892cfd919f1262c2bb8703220adcb986362", 239 | "sha256:e02780da03f84a671bb4205c5968c120f18df081236d7b5462b380fd4f0b497b", 240 | "sha256:e2002a59453858c7f3404690ae80f10c924a39f45f6095f18a985a1234c37334", 241 | "sha256:e22a82d2b416d9227a500c6860cf13e74060cf10e7daf6695cbf4e6a94e0eee4", 242 | "sha256:e41f72f225192d5d4df81dad2974a8943b0f2d664a2a5cfccdf5a01506f5523c", 243 | "sha256:f253dad38605486a4590f9368ecbace95865fea0f2b66615d121ac91fd1a1563", 244 | "sha256:fddfb31aa2ac550b938d952bca8a87f1db0f8dc930ffa14ce05b5c08d27e7fd1" 245 | ], 246 | "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')))))", 247 | "version": "==1.1.1" 248 | }, 249 | "h11": { 250 | "hashes": [ 251 | "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6", 252 | "sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042" 253 | ], 254 | "markers": "python_version >= '3.6'", 255 | "version": "==0.12.0" 256 | }, 257 | "httptools": { 258 | "hashes": [ 259 | "sha256:01b392a166adcc8bc2f526a939a8aabf89fe079243e1543fd0e7dc1b58d737cb", 260 | "sha256:200fc1cdf733a9ff554c0bb97a4047785cfaad9875307d6087001db3eb2b417f", 261 | "sha256:3ab1f390d8867f74b3b5ee2a7ecc9b8d7f53750bd45714bf1cb72a953d7dfa77", 262 | "sha256:78d03dd39b09c99ec917d50189e6743adbfd18c15d5944392d2eabda688bf149", 263 | "sha256:79dbc21f3612a78b28384e989b21872e2e3cf3968532601544696e4ed0007ce5", 264 | "sha256:80ffa04fe8c8dfacf6e4cef8277347d35b0442c581f5814f3b0cf41b65c43c6e", 265 | "sha256:813871f961edea6cb2fe312f2d9b27d12a51ba92545380126f80d0de1917ea15", 266 | "sha256:94505026be56652d7a530ab03d89474dc6021019d6b8682281977163b3471ea0", 267 | "sha256:a23166e5ae2775709cf4f7ad4c2048755ebfb272767d244e1a96d55ac775cca7", 268 | "sha256:a289c27ccae399a70eacf32df9a44059ca2ba4ac444604b00a19a6c1f0809943", 269 | "sha256:a7594f9a010cdf1e16a58b3bf26c9da39bbf663e3b8d46d39176999d71816658", 270 | "sha256:b08d00d889a118f68f37f3c43e359aab24ee29eb2e3fe96d64c6a2ba8b9d6557", 271 | "sha256:cc9be041e428c10f8b6ab358c6b393648f9457094e1dcc11b4906026d43cd380", 272 | "sha256:d5682eeb10cca0606c4a8286a3391d4c3c5a36f0c448e71b8bd05be4e1694bfb", 273 | "sha256:fd3b8905e21431ad306eeaf56644a68fdd621bf8f3097eff54d0f6bdf7262065" 274 | ], 275 | "version": "==0.2.0" 276 | }, 277 | "idna": { 278 | "hashes": [ 279 | "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a", 280 | "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3" 281 | ], 282 | "markers": "python_version >= '3'", 283 | "version": "==3.2" 284 | }, 285 | "importlib-resources": { 286 | "hashes": [ 287 | "sha256:2480d8e07d1890056cb53c96e3de44fead9c62f2ba949b0f2e4c4345f4afa977", 288 | "sha256:a65882a4d0fe5fbf702273456ba2ce74fe44892c25e42e057aca526b702a6d4b" 289 | ], 290 | "markers": "python_version < '3.9'", 291 | "version": "==5.2.2" 292 | }, 293 | "mako": { 294 | "hashes": [ 295 | "sha256:169fa52af22a91900d852e937400e79f535496191c63712e3b9fda5a9bed6fc3", 296 | "sha256:6804ee66a7f6a6416910463b00d76a7b25194cd27f1918500c5bd7be2a088a23" 297 | ], 298 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 299 | "version": "==1.1.5" 300 | }, 301 | "markupsafe": { 302 | "hashes": [ 303 | "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298", 304 | "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64", 305 | "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b", 306 | "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567", 307 | "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff", 308 | "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724", 309 | "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74", 310 | "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646", 311 | "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35", 312 | "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6", 313 | "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6", 314 | "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad", 315 | "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26", 316 | "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38", 317 | "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac", 318 | "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7", 319 | "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6", 320 | "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75", 321 | "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f", 322 | "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135", 323 | "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8", 324 | "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a", 325 | "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a", 326 | "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9", 327 | "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864", 328 | "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914", 329 | "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18", 330 | "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8", 331 | "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2", 332 | "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d", 333 | "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b", 334 | "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b", 335 | "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f", 336 | "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb", 337 | "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833", 338 | "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28", 339 | "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415", 340 | "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902", 341 | "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d", 342 | "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9", 343 | "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d", 344 | "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145", 345 | "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066", 346 | "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c", 347 | "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1", 348 | "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f", 349 | "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53", 350 | "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134", 351 | "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85", 352 | "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5", 353 | "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94", 354 | "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509", 355 | "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51", 356 | "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872" 357 | ], 358 | "markers": "python_version >= '3.6'", 359 | "version": "==2.0.1" 360 | }, 361 | "passlib": { 362 | "extras": [ 363 | "bcrypt" 364 | ], 365 | "hashes": [ 366 | "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1", 367 | "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04" 368 | ], 369 | "index": "pypi", 370 | "version": "==1.7.4" 371 | }, 372 | "psycopg2": { 373 | "hashes": [ 374 | "sha256:079d97fc22de90da1d370c90583659a9f9a6ee4007355f5825e5f1c70dffc1fa", 375 | "sha256:2087013c159a73e09713294a44d0c8008204d06326006b7f652bef5ace66eebb", 376 | "sha256:2c992196719fadda59f72d44603ee1a2fdcc67de097eea38d41c7ad9ad246e62", 377 | "sha256:7640e1e4d72444ef012e275e7b53204d7fab341fb22bc76057ede22fe6860b25", 378 | "sha256:7f91312f065df517187134cce8e395ab37f5b601a42446bdc0f0d51773621854", 379 | "sha256:830c8e8dddab6b6716a4bf73a09910c7954a92f40cf1d1e702fb93c8a919cc56", 380 | "sha256:89409d369f4882c47f7ea20c42c5046879ce22c1e4ea20ef3b00a4dfc0a7f188", 381 | "sha256:bf35a25f1aaa8a3781195595577fcbb59934856ee46b4f252f56ad12b8043bcf", 382 | "sha256:de5303a6f1d0a7a34b9d40e4d3bef684ccc44a49bbe3eb85e3c0bffb4a131b7c" 383 | ], 384 | "index": "pypi", 385 | "version": "==2.9.1" 386 | }, 387 | "psycopg2-binary": { 388 | "hashes": [ 389 | "sha256:0b7dae87f0b729922e06f85f667de7bf16455d411971b2043bbd9577af9d1975", 390 | "sha256:0f2e04bd2a2ab54fa44ee67fe2d002bb90cee1c0f1cc0ebc3148af7b02034cbd", 391 | "sha256:123c3fb684e9abfc47218d3784c7b4c47c8587951ea4dd5bc38b6636ac57f616", 392 | "sha256:1473c0215b0613dd938db54a653f68251a45a78b05f6fc21af4326f40e8360a2", 393 | "sha256:14db1752acdd2187d99cb2ca0a1a6dfe57fc65c3281e0f20e597aac8d2a5bd90", 394 | "sha256:1e3a362790edc0a365385b1ac4cc0acc429a0c0d662d829a50b6ce743ae61b5a", 395 | "sha256:1e85b74cbbb3056e3656f1cc4781294df03383127a8114cbc6531e8b8367bf1e", 396 | "sha256:20f1ab44d8c352074e2d7ca67dc00843067788791be373e67a0911998787ce7d", 397 | "sha256:2f62c207d1740b0bde5c4e949f857b044818f734a3d57f1d0d0edc65050532ed", 398 | "sha256:3242b9619de955ab44581a03a64bdd7d5e470cc4183e8fcadd85ab9d3756ce7a", 399 | "sha256:35c4310f8febe41f442d3c65066ca93cccefd75013df3d8c736c5b93ec288140", 400 | "sha256:4235f9d5ddcab0b8dbd723dca56ea2922b485ea00e1dafacf33b0c7e840b3d32", 401 | "sha256:5ced67f1e34e1a450cdb48eb53ca73b60aa0af21c46b9b35ac3e581cf9f00e31", 402 | "sha256:7360647ea04db2e7dff1648d1da825c8cf68dc5fbd80b8fb5b3ee9f068dcd21a", 403 | "sha256:8c13d72ed6af7fd2c8acbd95661cf9477f94e381fce0792c04981a8283b52917", 404 | "sha256:988b47ac70d204aed01589ed342303da7c4d84b56c2f4c4b8b00deda123372bf", 405 | "sha256:995fc41ebda5a7a663a254a1dcac52638c3e847f48307b5416ee373da15075d7", 406 | "sha256:a36c7eb6152ba5467fb264d73844877be8b0847874d4822b7cf2d3c0cb8cdcb0", 407 | "sha256:aed4a9a7e3221b3e252c39d0bf794c438dc5453bc2963e8befe9d4cd324dff72", 408 | "sha256:aef9aee84ec78af51107181d02fe8773b100b01c5dfde351184ad9223eab3698", 409 | "sha256:b0221ca5a9837e040ebf61f48899926b5783668b7807419e4adae8175a31f773", 410 | "sha256:b4d7679a08fea64573c969f6994a2631908bb2c0e69a7235648642f3d2e39a68", 411 | "sha256:c250a7ec489b652c892e4f0a5d122cc14c3780f9f643e1a326754aedf82d9a76", 412 | "sha256:ca86db5b561b894f9e5f115d6a159fff2a2570a652e07889d8a383b5fae66eb4", 413 | "sha256:cfc523edecddaef56f6740d7de1ce24a2fdf94fd5e704091856a201872e37f9f", 414 | "sha256:da113b70f6ec40e7d81b43d1b139b9db6a05727ab8be1ee559f3a69854a69d34", 415 | "sha256:f6fac64a38f6768e7bc7b035b9e10d8a538a9fadce06b983fb3e6fa55ac5f5ce", 416 | "sha256:f8559617b1fcf59a9aedba2c9838b5b6aa211ffedecabca412b92a1ff75aac1a", 417 | "sha256:fbb42a541b1093385a2d8c7eec94d26d30437d0e77c1d25dae1dcc46741a385e" 418 | ], 419 | "index": "pypi", 420 | "version": "==2.9.1" 421 | }, 422 | "pyasn1": { 423 | "hashes": [ 424 | "sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359", 425 | "sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576", 426 | "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf", 427 | "sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7", 428 | "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d", 429 | "sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00", 430 | "sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8", 431 | "sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86", 432 | "sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12", 433 | "sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776", 434 | "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba", 435 | "sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2", 436 | "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3" 437 | ], 438 | "version": "==0.4.8" 439 | }, 440 | "pycparser": { 441 | "hashes": [ 442 | "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0", 443 | "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705" 444 | ], 445 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 446 | "version": "==2.20" 447 | }, 448 | "pydantic": { 449 | "extras": [ 450 | "email" 451 | ], 452 | "hashes": [ 453 | "sha256:021ea0e4133e8c824775a0cfe098677acf6fa5a3cbf9206a376eed3fc09302cd", 454 | "sha256:05ddfd37c1720c392f4e0d43c484217b7521558302e7069ce8d318438d297739", 455 | "sha256:05ef5246a7ffd2ce12a619cbb29f3307b7c4509307b1b49f456657b43529dc6f", 456 | "sha256:10e5622224245941efc193ad1d159887872776df7a8fd592ed746aa25d071840", 457 | "sha256:18b5ea242dd3e62dbf89b2b0ec9ba6c7b5abaf6af85b95a97b00279f65845a23", 458 | "sha256:234a6c19f1c14e25e362cb05c68afb7f183eb931dd3cd4605eafff055ebbf287", 459 | "sha256:244ad78eeb388a43b0c927e74d3af78008e944074b7d0f4f696ddd5b2af43c62", 460 | "sha256:26464e57ccaafe72b7ad156fdaa4e9b9ef051f69e175dbbb463283000c05ab7b", 461 | "sha256:41b542c0b3c42dc17da70554bc6f38cbc30d7066d2c2815a94499b5684582ecb", 462 | "sha256:4a03cbbe743e9c7247ceae6f0d8898f7a64bb65800a45cbdc52d65e370570820", 463 | "sha256:4be75bebf676a5f0f87937c6ddb061fa39cbea067240d98e298508c1bda6f3f3", 464 | "sha256:54cd5121383f4a461ff7644c7ca20c0419d58052db70d8791eacbbe31528916b", 465 | "sha256:589eb6cd6361e8ac341db97602eb7f354551482368a37f4fd086c0733548308e", 466 | "sha256:8621559dcf5afacf0069ed194278f35c255dc1a1385c28b32dd6c110fd6531b3", 467 | "sha256:8b223557f9510cf0bfd8b01316bf6dd281cf41826607eada99662f5e4963f316", 468 | "sha256:99a9fc39470010c45c161a1dc584997f1feb13f689ecf645f59bb4ba623e586b", 469 | "sha256:a7c6002203fe2c5a1b5cbb141bb85060cbff88c2d78eccbc72d97eb7022c43e4", 470 | "sha256:a83db7205f60c6a86f2c44a61791d993dff4b73135df1973ecd9eed5ea0bda20", 471 | "sha256:ac8eed4ca3bd3aadc58a13c2aa93cd8a884bcf21cb019f8cfecaae3b6ce3746e", 472 | "sha256:e710876437bc07bd414ff453ac8ec63d219e7690128d925c6e82889d674bb505", 473 | "sha256:ea5cb40a3b23b3265f6325727ddfc45141b08ed665458be8c6285e7b85bd73a1", 474 | "sha256:fec866a0b59f372b7e776f2d7308511784dace622e0992a0b59ea3ccee0ae833" 475 | ], 476 | "index": "pypi", 477 | "version": "==1.8.2" 478 | }, 479 | "python-dotenv": { 480 | "hashes": [ 481 | "sha256:aae25dc1ebe97c420f50b81fb0e5c949659af713f31fdb63c749ca68748f34b1", 482 | "sha256:f521bc2ac9a8e03c736f62911605c5d83970021e3fa95b37d769e2bbbe9b6172" 483 | ], 484 | "version": "==0.19.0" 485 | }, 486 | "python-jose": { 487 | "extras": [ 488 | "cryptography" 489 | ], 490 | "hashes": [ 491 | "sha256:55779b5e6ad599c6336191246e95eb2293a9ddebd555f796a65f838f07e5d78a", 492 | "sha256:9b1376b023f8b298536eedd47ae1089bcdb848f1535ab30555cd92002d78923a" 493 | ], 494 | "index": "pypi", 495 | "version": "==3.3.0" 496 | }, 497 | "python-multipart": { 498 | "hashes": [ 499 | "sha256:f7bb5f611fc600d15fa47b3974c8aa16e93724513b49b5f95c81e6624c83fa43" 500 | ], 501 | "index": "pypi", 502 | "version": "==0.0.5" 503 | }, 504 | "pytz": { 505 | "hashes": [ 506 | "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da", 507 | "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798" 508 | ], 509 | "index": "pypi", 510 | "version": "==2021.1" 511 | }, 512 | "pyyaml": { 513 | "hashes": [ 514 | "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf", 515 | "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696", 516 | "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393", 517 | "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77", 518 | "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922", 519 | "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5", 520 | "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8", 521 | "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10", 522 | "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc", 523 | "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018", 524 | "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e", 525 | "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253", 526 | "sha256:72a01f726a9c7851ca9bfad6fd09ca4e090a023c00945ea05ba1638c09dc3347", 527 | "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183", 528 | "sha256:895f61ef02e8fed38159bb70f7e100e00f471eae2bc838cd0f4ebb21e28f8541", 529 | "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb", 530 | "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185", 531 | "sha256:bfb51918d4ff3d77c1c856a9699f8492c612cde32fd3bcd344af9be34999bfdc", 532 | "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db", 533 | "sha256:cb333c16912324fd5f769fff6bc5de372e9e7a202247b48870bc251ed40239aa", 534 | "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46", 535 | "sha256:d483ad4e639292c90170eb6f7783ad19490e7a8defb3e46f97dfe4bacae89122", 536 | "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b", 537 | "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63", 538 | "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df", 539 | "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc", 540 | "sha256:fd7f6999a8070df521b6384004ef42833b9bd62cfee11a09bda1079b4b704247", 541 | "sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6", 542 | "sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0" 543 | ], 544 | "version": "==5.4.1" 545 | }, 546 | "requests": { 547 | "hashes": [ 548 | "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24", 549 | "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7" 550 | ], 551 | "index": "pypi", 552 | "version": "==2.26.0" 553 | }, 554 | "rsa": { 555 | "hashes": [ 556 | "sha256:78f9a9bf4e7be0c5ded4583326e7461e3a3c5aae24073648b4bdfa797d78c9d2", 557 | "sha256:9d689e6ca1b3038bc82bf8d23e944b6b6037bc02301a574935b2dd946e0353b9" 558 | ], 559 | "markers": "python_version >= '3.5' and python_version < '4.0'", 560 | "version": "==4.7.2" 561 | }, 562 | "six": { 563 | "hashes": [ 564 | "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", 565 | "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" 566 | ], 567 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 568 | "version": "==1.16.0" 569 | }, 570 | "sqlalchemy": { 571 | "hashes": [ 572 | "sha256:0566a6e90951590c0307c75f9176597c88ef4be2724958ca1d28e8ae05ec8822", 573 | "sha256:08d9396a2a38e672133266b31ed39b2b1f2b5ec712b5bff5e08033970563316a", 574 | "sha256:0b08a53e40b34205acfeb5328b832f44437956d673a6c09fce55c66ab0e54916", 575 | "sha256:16ef07e102d2d4f974ba9b0d4ac46345a411ad20ad988b3654d59ff08e553b1c", 576 | "sha256:1adf3d25e2e33afbcd48cfad8076f9378793be43e7fec3e4334306cac6bec138", 577 | "sha256:1b38db2417b9f7005d6ceba7ce2a526bf10e3f6f635c0f163e6ed6a42b5b62b2", 578 | "sha256:1ebd69365717becaa1b618220a3df97f7c08aa68e759491de516d1c3667bba54", 579 | "sha256:26b0cd2d5c7ea96d3230cb20acac3d89de3b593339c1447b4d64bfcf4eac1110", 580 | "sha256:2ed67aae8cde4d32aacbdba4f7f38183d14443b714498eada5e5a7a37769c0b7", 581 | "sha256:33a1e86abad782e90976de36150d910748b58e02cd7d35680d441f9a76806c18", 582 | "sha256:41a916d815a3a23cb7fff8d11ad0c9b93369ac074e91e428075e088fe57d5358", 583 | "sha256:6003771ea597346ab1e97f2f58405c6cacbf6a308af3d28a9201a643c0ac7bb3", 584 | "sha256:6400b22e4e41cc27623a9a75630b7719579cd9a3a2027bcf16ad5aaa9a7806c0", 585 | "sha256:6b602e3351f59f3999e9fb8b87e5b95cb2faab6a6ecdb482382ac6fdfbee5266", 586 | "sha256:75cd5d48389a7635393ff5a9214b90695c06b3d74912109c3b00ce7392b69c6c", 587 | "sha256:7ad59e2e16578b6c1a2873e4888134112365605b08a6067dd91e899e026efa1c", 588 | "sha256:7b7778a205f956755e05721eebf9f11a6ac18b2409bff5db53ce5fe7ede79831", 589 | "sha256:842c49dd584aedd75c2ee05f6c950730c3ffcddd21c5824ed0f820808387e1e3", 590 | "sha256:90fe429285b171bcc252e21515703bdc2a4721008d1f13aa5b7150336f8a8493", 591 | "sha256:91cd87d1de0111eaca11ccc3d31af441c753fa2bc22df72e5009cfb0a1af5b03", 592 | "sha256:9a1df8c93a0dd9cef0839917f0c6c49f46c75810cf8852be49884da4a7de3c59", 593 | "sha256:9ebe49c3960aa2219292ea2e5df6acdc425fc828f2f3d50b4cfae1692bcb5f02", 594 | "sha256:a28fe28c359835f3be20c89efd517b35e8f97dbb2ca09c6cf0d9ac07f62d7ef6", 595 | "sha256:a36ea43919e51b0de0c0bc52bcfdad7683f6ea9fb81b340cdabb9df0e045e0f7", 596 | "sha256:a505ecc0642f52e7c65afb02cc6181377d833b7df0994ecde15943b18d0fa89c", 597 | "sha256:a79abdb404d9256afb8aeaa0d3a4bc7d3b6d8b66103d8b0f2f91febd3909976e", 598 | "sha256:c211e8ec81522ce87b0b39f0cf0712c998d4305a030459a0e115a2b3dc71598f", 599 | "sha256:dd4ed12a775f2cde4519f4267d3601990a97d8ecde5c944ab06bfd6e8e8ea177", 600 | "sha256:e37621b37c73b034997b5116678862f38ee70e5a054821c7b19d0e55df270dec", 601 | "sha256:e93978993a2ad0af43f132be3ea8805f56b2f2cd223403ec28d3e7d5c6d39ed1" 602 | ], 603 | "index": "pypi", 604 | "version": "==1.4.25" 605 | }, 606 | "sqlalchemy-utils": { 607 | "hashes": [ 608 | "sha256:a6aaee154f798be4e479af0ceffaa5034d35fcf6f40707c0947d21bde64e05e5", 609 | "sha256:b1bf67d904fed16b16ef1dc07f03e5e93a6b23899f920f6b41c09be45fbb85f2" 610 | ], 611 | "index": "pypi", 612 | "version": "==0.37.8" 613 | }, 614 | "starlette": { 615 | "hashes": [ 616 | "sha256:3c8e48e52736b3161e34c9f0e8153b4f32ec5d8995a3ee1d59410d92f75162ed", 617 | "sha256:7d49f4a27f8742262ef1470608c59ddbc66baf37c148e938c7038e6bc7a998aa" 618 | ], 619 | "markers": "python_version >= '3.6'", 620 | "version": "==0.14.2" 621 | }, 622 | "typing-extensions": { 623 | "hashes": [ 624 | "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e", 625 | "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7", 626 | "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34" 627 | ], 628 | "version": "==3.10.0.2" 629 | }, 630 | "urllib3": { 631 | "hashes": [ 632 | "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece", 633 | "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844" 634 | ], 635 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4.0'", 636 | "version": "==1.26.7" 637 | }, 638 | "uvicorn": { 639 | "extras": [ 640 | "standard" 641 | ], 642 | "hashes": [ 643 | "sha256:17f898c64c71a2640514d4089da2689e5db1ce5d4086c2d53699bf99513421c1", 644 | "sha256:d9a3c0dd1ca86728d3e235182683b4cf94cd53a867c288eaeca80ee781b2caff" 645 | ], 646 | "index": "pypi", 647 | "version": "==0.15.0" 648 | }, 649 | "watchgod": { 650 | "hashes": [ 651 | "sha256:48140d62b0ebe9dd9cf8381337f06351e1f2e70b2203fa9c6eff4e572ca84f29", 652 | "sha256:d6c1ea21df37847ac0537ca0d6c2f4cdf513562e95f77bb93abbcf05573407b7" 653 | ], 654 | "version": "==0.7" 655 | }, 656 | "websockets": { 657 | "hashes": [ 658 | "sha256:01db0ecd1a0ca6702d02a5ed40413e18b7d22f94afb3bbe0d323bac86c42c1c8", 659 | "sha256:085bb8a6e780d30eaa1ba48ac7f3a6707f925edea787cfb761ce5a39e77ac09b", 660 | "sha256:1ac35426fe3e7d3d0fac3d63c8965c76ed67a8fd713937be072bf0ce22808539", 661 | "sha256:1f6b814cff6aadc4288297cb3a248614829c6e4ff5556593c44a115e9dd49939", 662 | "sha256:2a43072e434c041a99f2e1eb9b692df0232a38c37c61d00e9f24db79474329e4", 663 | "sha256:5b2600e01c7ca6f840c42c747ffbe0254f319594ed108db847eb3d75f4aacb80", 664 | "sha256:62160772314920397f9d219147f958b33fa27a12c662d4455c9ccbba9a07e474", 665 | "sha256:706e200fc7f03bed99ad0574cd1ea8b0951477dd18cc978ccb190683c69dba76", 666 | "sha256:71358c7816e2762f3e4af3adf0040f268e219f5a38cb3487a9d0fc2e554fef6a", 667 | "sha256:7d2e12e4f901f1bc062dfdf91831712c4106ed18a9a4cdb65e2e5f502124ca37", 668 | "sha256:7f79f02c7f9a8320aff7d3321cd1c7e3a7dbc15d922ac996cca827301ee75238", 669 | "sha256:82b17524b1ce6ae7f7dd93e4d18e9b9474071e28b65dbf1dfe9b5767778db379", 670 | "sha256:82bd921885231f4a30d9bc550552495b3fc36b1235add6d374e7c65c3babd805", 671 | "sha256:8bbf8660c3f833ddc8b1afab90213f2e672a9ddac6eecb3cde968e6b2807c1c7", 672 | "sha256:9a4d889162bd48588e80950e07fa5e039eee9deb76a58092e8c3ece96d7ef537", 673 | "sha256:b4ade7569b6fd17912452f9c3757d96f8e4044016b6d22b3b8391e641ca50456", 674 | "sha256:b8176deb6be540a46695960a765a77c28ac8b2e3ef2ec95d50a4f5df901edb1c", 675 | "sha256:c4fc9a1d242317892590abe5b61a9127f1a61740477bfb121743f290b8054002", 676 | "sha256:c5880442f5fc268f1ef6d37b2c152c114deccca73f48e3a8c48004d2f16f4567", 677 | "sha256:cd8c6f2ec24aedace251017bc7a414525171d4e6578f914acab9349362def4da", 678 | "sha256:d67646ddd17a86117ae21c27005d83c1895c0cef5d7be548b7549646372f868a", 679 | "sha256:e42a1f1e03437b017af341e9bbfdc09252cd48ef32a8c3c3ead769eab3b17368", 680 | "sha256:eb282127e9c136f860c6068a4fba5756eb25e755baffb5940b6f1eae071928b2", 681 | "sha256:fe83b3ec9ef34063d86dfe1029160a85f24a5a94271036e5714a57acfdd089a1", 682 | "sha256:ff59c6bdb87b31f7e2d596f09353d5a38c8c8ff571b0e2238e8ee2d55ad68465" 683 | ], 684 | "version": "==10.0" 685 | }, 686 | "zipp": { 687 | "hashes": [ 688 | "sha256:957cfda87797e389580cb8b9e3870841ca991e2125350677b2ca83a0e99390a3", 689 | "sha256:f5812b1e007e48cff63449a5e9f4e7ebea716b4111f9c4f9a645f91d579bf0c4" 690 | ], 691 | "markers": "python_version < '3.10'", 692 | "version": "==3.5.0" 693 | } 694 | }, 695 | "develop": { 696 | "astroid": { 697 | "hashes": [ 698 | "sha256:dcc06f6165f415220013801642bd6c9808a02967070919c4b746c6864c205471", 699 | "sha256:fe81f80c0b35264acb5653302ffbd935d394f1775c5e4487df745bf9c2442708" 700 | ], 701 | "markers": "python_version ~= '3.6'", 702 | "version": "==2.8.0" 703 | }, 704 | "black": { 705 | "hashes": [ 706 | "sha256:380f1b5da05e5a1429225676655dddb96f5ae8c75bdf91e53d798871b902a115", 707 | "sha256:7de4cfc7eb6b710de325712d40125689101d21d25283eed7e9998722cf10eb91" 708 | ], 709 | "index": "pypi", 710 | "version": "==21.9b0" 711 | }, 712 | "click": { 713 | "hashes": [ 714 | "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a", 715 | "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6" 716 | ], 717 | "markers": "python_version >= '3.6'", 718 | "version": "==8.0.1" 719 | }, 720 | "colorama": { 721 | "hashes": [ 722 | "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b", 723 | "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2" 724 | ], 725 | "version": "==0.4.4" 726 | }, 727 | "isort": { 728 | "hashes": [ 729 | "sha256:9c2ea1e62d871267b78307fe511c0838ba0da28698c5732d54e2790bf3ba9899", 730 | "sha256:e17d6e2b81095c9db0a03a8025a957f334d6ea30b26f9ec70805411e5c7c81f2" 731 | ], 732 | "markers": "python_version < '4.0' and python_full_version >= '3.6.1'", 733 | "version": "==5.9.3" 734 | }, 735 | "lazy-object-proxy": { 736 | "hashes": [ 737 | "sha256:17e0967ba374fc24141738c69736da90e94419338fd4c7c7bef01ee26b339653", 738 | "sha256:1fee665d2638491f4d6e55bd483e15ef21f6c8c2095f235fef72601021e64f61", 739 | "sha256:22ddd618cefe54305df49e4c069fa65715be4ad0e78e8d252a33debf00f6ede2", 740 | "sha256:24a5045889cc2729033b3e604d496c2b6f588c754f7a62027ad4437a7ecc4837", 741 | "sha256:410283732af311b51b837894fa2f24f2c0039aa7f220135192b38fcc42bd43d3", 742 | "sha256:4732c765372bd78a2d6b2150a6e99d00a78ec963375f236979c0626b97ed8e43", 743 | "sha256:489000d368377571c6f982fba6497f2aa13c6d1facc40660963da62f5c379726", 744 | "sha256:4f60460e9f1eb632584c9685bccea152f4ac2130e299784dbaf9fae9f49891b3", 745 | "sha256:5743a5ab42ae40caa8421b320ebf3a998f89c85cdc8376d6b2e00bd12bd1b587", 746 | "sha256:85fb7608121fd5621cc4377a8961d0b32ccf84a7285b4f1d21988b2eae2868e8", 747 | "sha256:9698110e36e2df951c7c36b6729e96429c9c32b3331989ef19976592c5f3c77a", 748 | "sha256:9d397bf41caad3f489e10774667310d73cb9c4258e9aed94b9ec734b34b495fd", 749 | "sha256:b579f8acbf2bdd9ea200b1d5dea36abd93cabf56cf626ab9c744a432e15c815f", 750 | "sha256:b865b01a2e7f96db0c5d12cfea590f98d8c5ba64ad222300d93ce6ff9138bcad", 751 | "sha256:bf34e368e8dd976423396555078def5cfc3039ebc6fc06d1ae2c5a65eebbcde4", 752 | "sha256:c6938967f8528b3668622a9ed3b31d145fab161a32f5891ea7b84f6b790be05b", 753 | "sha256:d1c2676e3d840852a2de7c7d5d76407c772927addff8d742b9808fe0afccebdf", 754 | "sha256:d7124f52f3bd259f510651450e18e0fd081ed82f3c08541dffc7b94b883aa981", 755 | "sha256:d900d949b707778696fdf01036f58c9876a0d8bfe116e8d220cfd4b15f14e741", 756 | "sha256:ebfd274dcd5133e0afae738e6d9da4323c3eb021b3e13052d8cbd0e457b1256e", 757 | "sha256:ed361bb83436f117f9917d282a456f9e5009ea12fd6de8742d1a4752c3017e93", 758 | "sha256:f5144c75445ae3ca2057faac03fda5a902eff196702b0a24daf1d6ce0650514b" 759 | ], 760 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", 761 | "version": "==1.6.0" 762 | }, 763 | "mccabe": { 764 | "hashes": [ 765 | "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", 766 | "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" 767 | ], 768 | "version": "==0.6.1" 769 | }, 770 | "mypy-extensions": { 771 | "hashes": [ 772 | "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d", 773 | "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8" 774 | ], 775 | "version": "==0.4.3" 776 | }, 777 | "pathspec": { 778 | "hashes": [ 779 | "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a", 780 | "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1" 781 | ], 782 | "version": "==0.9.0" 783 | }, 784 | "platformdirs": { 785 | "hashes": [ 786 | "sha256:367a5e80b3d04d2428ffa76d33f124cf11e8fff2acdaa9b43d545f5c7d661ef2", 787 | "sha256:8868bbe3c3c80d42f20156f22e7131d2fb321f5bc86a2a345375c6481a67021d" 788 | ], 789 | "markers": "python_version >= '3.6'", 790 | "version": "==2.4.0" 791 | }, 792 | "pylint": { 793 | "hashes": [ 794 | "sha256:0f358e221c45cbd4dad2a1e4b883e75d28acdcccd29d40c76eb72b307269b126", 795 | "sha256:2c9843fff1a88ca0ad98a256806c82c5a8f86086e7ccbdb93297d86c3f90c436" 796 | ], 797 | "index": "pypi", 798 | "version": "==2.11.1" 799 | }, 800 | "regex": { 801 | "hashes": [ 802 | "sha256:0628ed7d6334e8f896f882a5c1240de8c4d9b0dd7c7fb8e9f4692f5684b7d656", 803 | "sha256:09eb62654030f39f3ba46bc6726bea464069c29d00a9709e28c9ee9623a8da4a", 804 | "sha256:0bba1f6df4eafe79db2ecf38835c2626dbd47911e0516f6962c806f83e7a99ae", 805 | "sha256:10a7a9cbe30bd90b7d9a1b4749ef20e13a3528e4215a2852be35784b6bd070f0", 806 | "sha256:17310b181902e0bb42b29c700e2c2346b8d81f26e900b1328f642e225c88bce1", 807 | "sha256:1e8d1898d4fb817120a5f684363b30108d7b0b46c7261264b100d14ec90a70e7", 808 | "sha256:2054dea683f1bda3a804fcfdb0c1c74821acb968093d0be16233873190d459e3", 809 | "sha256:29385c4dbb3f8b3a55ce13de6a97a3d21bd00de66acd7cdfc0b49cb2f08c906c", 810 | "sha256:295bc8a13554a25ad31e44c4bedabd3c3e28bba027e4feeb9bb157647a2344a7", 811 | "sha256:2cdb3789736f91d0b3333ac54d12a7e4f9efbc98f53cb905d3496259a893a8b3", 812 | "sha256:3baf3eaa41044d4ced2463fd5d23bf7bd4b03d68739c6c99a59ce1f95599a673", 813 | "sha256:4e61100200fa6ab7c99b61476f9f9653962ae71b931391d0264acfb4d9527d9c", 814 | "sha256:6266fde576e12357b25096351aac2b4b880b0066263e7bc7a9a1b4307991bb0e", 815 | "sha256:650c4f1fc4273f4e783e1d8e8b51a3e2311c2488ba0fcae6425b1e2c248a189d", 816 | "sha256:658e3477676009083422042c4bac2bdad77b696e932a3de001c42cc046f8eda2", 817 | "sha256:6adc1bd68f81968c9d249aab8c09cdc2cbe384bf2d2cb7f190f56875000cdc72", 818 | "sha256:6c4d83d21d23dd854ffbc8154cf293f4e43ba630aa9bd2539c899343d7f59da3", 819 | "sha256:6f74b6d8f59f3cfb8237e25c532b11f794b96f5c89a6f4a25857d85f84fbef11", 820 | "sha256:7783d89bd5413d183a38761fbc68279b984b9afcfbb39fa89d91f63763fbfb90", 821 | "sha256:7e3536f305f42ad6d31fc86636c54c7dafce8d634e56fef790fbacb59d499dd5", 822 | "sha256:821e10b73e0898544807a0692a276e539e5bafe0a055506a6882814b6a02c3ec", 823 | "sha256:835962f432bce92dc9bf22903d46c50003c8d11b1dc64084c8fae63bca98564a", 824 | "sha256:85c61bee5957e2d7be390392feac7e1d7abd3a49cbaed0c8cee1541b784c8561", 825 | "sha256:86f9931eb92e521809d4b64ec8514f18faa8e11e97d6c2d1afa1bcf6c20a8eab", 826 | "sha256:8a5c2250c0a74428fd5507ae8853706fdde0f23bfb62ee1ec9418eeacf216078", 827 | "sha256:8aec4b4da165c4a64ea80443c16e49e3b15df0f56c124ac5f2f8708a65a0eddc", 828 | "sha256:8c268e78d175798cd71d29114b0a1f1391c7d011995267d3b62319ec1a4ecaa1", 829 | "sha256:8d80087320632457aefc73f686f66139801959bf5b066b4419b92be85be3543c", 830 | "sha256:95e89a8558c8c48626dcffdf9c8abac26b7c251d352688e7ab9baf351e1c7da6", 831 | "sha256:9c371dd326289d85906c27ec2bc1dcdedd9d0be12b543d16e37bad35754bde48", 832 | "sha256:9c7cb25adba814d5f419733fe565f3289d6fa629ab9e0b78f6dff5fa94ab0456", 833 | "sha256:a731552729ee8ae9c546fb1c651c97bf5f759018fdd40d0e9b4d129e1e3a44c8", 834 | "sha256:aea4006b73b555fc5bdb650a8b92cf486d678afa168cf9b38402bb60bf0f9c18", 835 | "sha256:b0e3f59d3c772f2c3baaef2db425e6fc4149d35a052d874bb95ccfca10a1b9f4", 836 | "sha256:b15dc34273aefe522df25096d5d087abc626e388a28a28ac75a4404bb7668736", 837 | "sha256:c000635fd78400a558bd7a3c2981bb2a430005ebaa909d31e6e300719739a949", 838 | "sha256:c31f35a984caffb75f00a86852951a337540b44e4a22171354fb760cefa09346", 839 | "sha256:c50a6379763c733562b1fee877372234d271e5c78cd13ade5f25978aa06744db", 840 | "sha256:c94722bf403b8da744b7d0bb87e1f2529383003ceec92e754f768ef9323f69ad", 841 | "sha256:dcbbc9cfa147d55a577d285fd479b43103188855074552708df7acc31a476dd9", 842 | "sha256:fb9f5844db480e2ef9fce3a72e71122dd010ab7b2920f777966ba25f7eb63819" 843 | ], 844 | "version": "==2021.9.24" 845 | }, 846 | "toml": { 847 | "hashes": [ 848 | "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", 849 | "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" 850 | ], 851 | "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", 852 | "version": "==0.10.2" 853 | }, 854 | "tomli": { 855 | "hashes": [ 856 | "sha256:8dd0e9524d6f386271a36b41dbf6c57d8e32fd96fd22b6584679dc569d20899f", 857 | "sha256:a5b75cb6f3968abb47af1b40c1819dc519ea82bcc065776a866e8d74c5ca9442" 858 | ], 859 | "markers": "python_version >= '3.6'", 860 | "version": "==1.2.1" 861 | }, 862 | "typing-extensions": { 863 | "hashes": [ 864 | "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e", 865 | "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7", 866 | "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34" 867 | ], 868 | "version": "==3.10.0.2" 869 | }, 870 | "wrapt": { 871 | "hashes": [ 872 | "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7" 873 | ], 874 | "version": "==1.12.1" 875 | } 876 | } 877 | } 878 | --------------------------------------------------------------------------------