├── .env.sample ├── .flake8 ├── .gitignore ├── CONTRIBUTING.md ├── Dockerfile ├── Makefile ├── README.md ├── app ├── api │ ├── campaigns.py │ ├── deps.py │ ├── discount_codes.py │ ├── host_users.py │ ├── hosts.py │ ├── users.py │ ├── widget_templates.py │ └── widgets.py ├── config.py ├── crud │ ├── __init__.py │ ├── base.py │ ├── crud_campaign.py │ ├── crud_discount_code.py │ ├── crud_host.py │ ├── crud_host_user.py │ ├── crud_user.py │ ├── crud_widget.py │ └── crud_widget_template.py ├── database.py ├── db.py ├── main.py ├── models │ ├── __init__.py │ ├── base.py │ ├── campaign.py │ ├── discount_code.py │ ├── host.py │ ├── host_user.py │ ├── user.py │ ├── widget.py │ └── widget_template.py ├── schemas │ ├── campaign.py │ ├── discount_code.py │ ├── host.py │ ├── host_user.py │ ├── user.py │ ├── widget.py │ └── widget_template.py ├── security.py ├── services │ └── widget_service.py └── utils.py ├── assets ├── ict-pic1.png ├── ict-pic2.png └── interactor.png ├── docker-compose.yml ├── docs ├── README.md ├── api.md ├── config.md ├── ict-challenge-en.md ├── ict-challenge-fa.md ├── models.md └── services.md ├── poetry.lock ├── pyproject.toml └── tests └── test_users.py /.env.sample: -------------------------------------------------------------------------------- 1 | DATABASE_URL=postgresql://user:password@db:5432/mydatabase 2 | API_V1_STR=/api/v1 3 | SECRET_KEY=your-secret-key 4 | ALGORITHM=HS256 5 | ACCESS_TOKEN_EXPIRE_MINUTES=60 6 | WIDGET_SERVICE_URL=http://localhost:9000/api/v1 7 | WIDGET_SERVICE_TOKEN=widget-service-token 8 | DEFAULT_JS_URL=http://localhost:3000/userTracker.js 9 | OPENAI_API_KEY=secret 10 | OPENAI_API_URL=https://api.openai.com/v1 -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 88 3 | extend-ignore = E203, E266, E501, W503 4 | exclude = .git,__pycache__,venv -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | *.py[cod] 3 | *.pyo 4 | *.pyd 5 | *.db 6 | *.sqlite3 7 | *.env 8 | *.venv 9 | *.egg-info/ 10 | dist/ 11 | build/ 12 | *.log 13 | *.pot 14 | *.pyc 15 | *.pdb 16 | *.coverage 17 | *.mypy_cache/ 18 | *.pytest_cache/ 19 | *.DS_Store 20 | *.egg 21 | *.egg-info/ 22 | *.whl 23 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to This Project -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Use an official Python runtime as a parent image 2 | FROM python:3.11 3 | 4 | # Set environment variables 5 | ENV PYTHONDONTWRITEBYTECODE 1 6 | ENV PYTHONUNBUFFERED 1 7 | 8 | # Set work directory 9 | WORKDIR /app 10 | 11 | # Install system dependencies 12 | RUN apt-get update && apt-get install -y --no-install-recommends \ 13 | gcc \ 14 | && rm -rf /var/lib/apt/lists/* 15 | 16 | # Install Poetry 17 | RUN pip install --no-cache-dir poetry 18 | 19 | # Copy only requirements to cache them in docker layer 20 | COPY pyproject.toml poetry.lock* /app/ 21 | 22 | # Project initialization: 23 | RUN poetry config virtualenvs.create false \ 24 | && poetry install --no-interaction --no-ansi 25 | 26 | # Copy project 27 | COPY . /app/ 28 | 29 | # Run the application 30 | CMD ["poetry", "run", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: format lint test 2 | 3 | format: 4 | black . 5 | 6 | lint: 7 | flake8 . 8 | 9 | test: 10 | python -m pytest 11 | 12 | check: format lint test 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Interactor 2 | 3 | This project is written for 9th round of [Sharif ICT Challenge](https://sharifict.ir/). Our team won the second place. So we shared our project with public to help others learn more about the competition and our project. 4 | 5 | This project is a widget management system implemented as a microservice architecture. It provides APIs to create, read, update, and delete widgets. The project is structured into several modules, each responsible for different aspects of the application, and is distributed across three GitLab repositories. 6 | 7 | ## Challenge 8 | 9 | To understand the challenge and our solution, you can read the [Persian Version](./docs/ict-challenge-fa.md) or [English Version](./docs/ict-challenge-en.md) of the challenge. 10 | 11 | # Project Microservices 12 | 13 | - [Interactor Backend](https://github.com/shahriarshm/ict9-interactor-backend.git) 14 | - [Interactor Frontend](https://github.com/MSNP1381/ICT9-interactor-campaigns.git) 15 | - [Interactor Widget Service](https://github.com/shahriarshm/ict9-interactor-widget-service.git) 16 | 17 | ## Architecture 18 | 19 | ![Interactor](./assets/interactor.png) 20 | 21 | ## Table of Contents 22 | 23 | - [Overview](#overview) 24 | - [Installation](#installation) 25 | - [Usage](#usage) 26 | - [Documentation](#documentation) 27 | - [Contributing](#contributing) 28 | - [License](#license) 29 | 30 | ## Overview 31 | 32 | The widget management system is designed to manage widgets efficiently. It includes the following modules: 33 | 34 | - **API**: Handles the HTTP requests and responses. 35 | - **Models**: Defines the data structures. 36 | - **Services**: Contains the business logic. 37 | - **CRUD**: Handles the database operations. 38 | - **Config**: Manages the configuration settings. 39 | 40 | ## Installation 41 | 42 | To install the project, follow these steps: 43 | 44 | 1. Clone the repositories: 45 | ```sh 46 | git clone https://github.com/shahriarshm/ict9-interactor-backend.git 47 | git clone https://github.com/MSNP1381/ICT9-interactor-campaigns.git 48 | git clone https://github.com/shahriarshm/ict9-interactor-widget-service.git 49 | ``` 50 | 2. Navigate to each project directory and follow the specific installation instructions in their respective README files. 51 | 52 | 3. Run using docker: 53 | ```sh 54 | docker compose up -d 55 | ``` 56 | 57 | ## Usage 58 | 59 | To run the application, use the following command in each project directory: 60 | -------------------------------------------------------------------------------- /app/api/campaigns.py: -------------------------------------------------------------------------------- 1 | from uuid import UUID 2 | from fastapi import APIRouter, Depends, HTTPException 3 | from sqlalchemy.orm import Session 4 | from typing import List, Optional 5 | from app import crud 6 | from app.schemas.campaign import ( 7 | Campaign, 8 | CampaignCreate, 9 | CampaignUpdate, 10 | CampaignStatus, 11 | ) 12 | from app.api import deps 13 | from app.models.user import User 14 | 15 | router = APIRouter() 16 | 17 | 18 | @router.post("/", response_model=Campaign) 19 | def create_campaign( 20 | campaign: CampaignCreate, 21 | db: Session = Depends(deps.get_db), 22 | current_user: User = Depends(deps.get_current_user), 23 | ): 24 | # Check if the user owns the host 25 | host = crud.host.get(db, id=campaign.host_id) 26 | if not host or host.owner_id != current_user.id: 27 | raise HTTPException( 28 | status_code=403, detail="Not authorized to create campaign for this host" 29 | ) 30 | return crud.campaign.create_with_user_and_host( 31 | db=db, obj_in=campaign, user_id=current_user.id, host_id=campaign.host_id 32 | ) 33 | 34 | 35 | @router.get("/", response_model=List[Campaign]) 36 | def read_campaigns( 37 | skip: int = 0, 38 | limit: int = 100, 39 | host_id: Optional[UUID] = None, 40 | status: Optional[CampaignStatus] = None, 41 | db: Session = Depends(deps.get_db), 42 | current_user: User = Depends(deps.get_current_user), 43 | ): 44 | filters = [] 45 | if host_id: 46 | # Check if the user owns the host 47 | host = crud.host.get(db, id=host_id) 48 | if not host or host.owner_id != current_user.id: 49 | raise HTTPException( 50 | status_code=403, detail="Not authorized to view campaigns for this host" 51 | ) 52 | filters.append(("host_id", host_id)) 53 | if status: 54 | filters.append(("status", status)) 55 | 56 | campaigns = crud.campaign.get_multi_with_filters( 57 | db, skip=skip, limit=limit, filters=filters 58 | ) 59 | return campaigns 60 | 61 | 62 | @router.get("/{campaign_id}", response_model=Campaign) 63 | def read_campaign( 64 | campaign_id: UUID, 65 | db: Session = Depends(deps.get_db), 66 | current_user: User = Depends(deps.get_current_user), 67 | ): 68 | campaign = crud.campaign.get(db, id=campaign_id) 69 | if not campaign: 70 | raise HTTPException(status_code=404, detail="Campaign not found") 71 | # Check if the user owns the host associated with the campaign 72 | host = crud.host.get(db, id=campaign.host_id) 73 | if not host or host.owner_id != current_user.id: 74 | raise HTTPException( 75 | status_code=403, detail="Not authorized to view this campaign" 76 | ) 77 | return campaign 78 | 79 | 80 | @router.put("/{campaign_id}", response_model=Campaign) 81 | def update_campaign( 82 | campaign_id: UUID, 83 | campaign: CampaignUpdate, 84 | db: Session = Depends(deps.get_db), 85 | current_user: User = Depends(deps.get_current_user), 86 | ): 87 | db_campaign = crud.campaign.get(db, id=campaign_id) 88 | if not db_campaign: 89 | raise HTTPException(status_code=404, detail="Campaign not found") 90 | # Check if the user owns the host associated with the campaign 91 | host = crud.host.get(db, id=db_campaign.host_id) 92 | if not host or host.owner_id != current_user.id: 93 | raise HTTPException( 94 | status_code=403, detail="Not authorized to update this campaign" 95 | ) 96 | return crud.campaign.update(db, db_obj=db_campaign, obj_in=campaign) 97 | 98 | 99 | @router.delete("/{campaign_id}", response_model=Campaign) 100 | def delete_campaign( 101 | campaign_id: UUID, 102 | db: Session = Depends(deps.get_db), 103 | current_user: User = Depends(deps.get_current_user), 104 | ): 105 | campaign = crud.campaign.get(db, id=campaign_id) 106 | if not campaign: 107 | raise HTTPException(status_code=404, detail="Campaign not found") 108 | # Check if the user owns the host associated with the campaign 109 | host = crud.host.get(db, id=campaign.host_id) 110 | if not host or host.owner_id != current_user.id: 111 | raise HTTPException( 112 | status_code=403, detail="Not authorized to delete this campaign" 113 | ) 114 | return crud.campaign.remove(db, id=campaign_id) 115 | -------------------------------------------------------------------------------- /app/api/deps.py: -------------------------------------------------------------------------------- 1 | from typing import Generator 2 | from fastapi import Depends, HTTPException, status 3 | from fastapi.security import OAuth2PasswordBearer 4 | from jose import JWTError, jwt 5 | from sqlalchemy.orm import Session 6 | 7 | from app.db import SessionLocal 8 | from app.config import settings 9 | from app import crud 10 | from app.schemas.user import TokenData 11 | 12 | 13 | def get_db() -> Generator: 14 | try: 15 | db = SessionLocal() 16 | yield db 17 | finally: 18 | db.close() 19 | 20 | 21 | oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") 22 | 23 | 24 | async def get_current_user( 25 | token: str = Depends(oauth2_scheme), db: Session = Depends(get_db) 26 | ): 27 | credentials_exception = HTTPException( 28 | status_code=status.HTTP_401_UNAUTHORIZED, 29 | detail="Could not validate credentials", 30 | headers={"WWW-Authenticate": "Bearer"}, 31 | ) 32 | try: 33 | payload = jwt.decode( 34 | token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM] 35 | ) 36 | email: str = payload.get("sub") 37 | if email is None: 38 | raise credentials_exception 39 | token_data = TokenData(email=email) 40 | except JWTError: 41 | raise credentials_exception 42 | user = crud.user.get_by_email(db, email=token_data.email) 43 | if user is None: 44 | raise credentials_exception 45 | return user 46 | -------------------------------------------------------------------------------- /app/api/discount_codes.py: -------------------------------------------------------------------------------- 1 | from uuid import UUID 2 | from fastapi import APIRouter, Depends, HTTPException 3 | from sqlalchemy.orm import Session 4 | from typing import List, Optional 5 | from app import crud 6 | from app.schemas.discount_code import ( 7 | DiscountCode, 8 | DiscountCodeCreate, 9 | DiscountCodeUpdate, 10 | BulkDiscountCodeCreate, 11 | ) 12 | from app.api import deps 13 | from app.models.user import User 14 | 15 | router = APIRouter() 16 | 17 | 18 | @router.post("/", response_model=DiscountCode) 19 | def create_discount_code( 20 | discount_code: DiscountCodeCreate, 21 | db: Session = Depends(deps.get_db), 22 | current_user: User = Depends(deps.get_current_user), 23 | ): 24 | # Check if the user owns the campaign 25 | campaign = crud.campaign.get(db, id=discount_code.campaign_id) 26 | if not campaign or campaign.user_id != current_user.id: 27 | raise HTTPException( 28 | status_code=403, 29 | detail="Not authorized to create discount code for this campaign", 30 | ) 31 | return crud.discount_code.create_with_campaign( 32 | db=db, obj_in=discount_code, campaign_id=discount_code.campaign_id 33 | ) 34 | 35 | 36 | @router.post("/bulk", response_model=List[DiscountCode]) 37 | def create_bulk_discount_codes( 38 | bulk_discount_codes: BulkDiscountCodeCreate, 39 | db: Session = Depends(deps.get_db), 40 | current_user: User = Depends(deps.get_current_user), 41 | ): 42 | # Check if the user owns the campaign 43 | campaign = crud.campaign.get(db, id=bulk_discount_codes.campaign_id) 44 | if not campaign or campaign.creator_id != current_user.id: 45 | raise HTTPException( 46 | status_code=403, 47 | detail="Not authorized to create discount codes for this campaign", 48 | ) 49 | 50 | created_codes = [] 51 | for _ in range(bulk_discount_codes.count): 52 | discount_code = DiscountCodeCreate( 53 | campaign_id=bulk_discount_codes.campaign_id, 54 | code=bulk_discount_codes.code_prefix 55 | + crud.discount_code.generate_unique_code(db), 56 | discount_type=bulk_discount_codes.discount_type, 57 | discount_value=bulk_discount_codes.discount_value, 58 | max_uses=bulk_discount_codes.max_uses, 59 | expiration_date=bulk_discount_codes.expiration_date, 60 | ) 61 | created_code = crud.discount_code.create_with_campaign( 62 | db=db, obj_in=discount_code, campaign_id=discount_code.campaign_id 63 | ) 64 | created_codes.append(created_code) 65 | 66 | return created_codes 67 | 68 | 69 | @router.get("/", response_model=List[DiscountCode]) 70 | def read_discount_codes( 71 | skip: int = 0, 72 | limit: int = 100, 73 | campaign_id: Optional[UUID] = None, 74 | db: Session = Depends(deps.get_db), 75 | current_user: User = Depends(deps.get_current_user), 76 | ): 77 | if campaign_id: 78 | # Check if the user owns the campaign 79 | campaign = crud.campaign.get(db, id=campaign_id) 80 | if not campaign or campaign.user_id != current_user.id: 81 | raise HTTPException( 82 | status_code=403, 83 | detail="Not authorized to view discount codes for this campaign", 84 | ) 85 | discount_codes = crud.discount_code.get_discount_codes_by_campaign( 86 | db, campaign_id=campaign_id 87 | ) 88 | else: 89 | discount_codes = crud.discount_code.get_multi(db, skip=skip, limit=limit) 90 | return discount_codes 91 | 92 | 93 | @router.get("/{discount_code_id}", response_model=DiscountCode) 94 | def read_discount_code( 95 | discount_code_id: UUID, 96 | db: Session = Depends(deps.get_db), 97 | current_user: User = Depends(deps.get_current_user), 98 | ): 99 | discount_code = crud.discount_code.get(db, id=discount_code_id) 100 | if not discount_code: 101 | raise HTTPException(status_code=404, detail="Discount code not found") 102 | # Check if the user owns the campaign associated with the discount code 103 | campaign = crud.campaign.get(db, id=discount_code.campaign_id) 104 | if not campaign or campaign.user_id != current_user.id: 105 | raise HTTPException( 106 | status_code=403, detail="Not authorized to view this discount code" 107 | ) 108 | return discount_code 109 | 110 | 111 | @router.put("/{discount_code_id}", response_model=DiscountCode) 112 | def update_discount_code( 113 | discount_code_id: UUID, 114 | discount_code: DiscountCodeUpdate, 115 | db: Session = Depends(deps.get_db), 116 | current_user: User = Depends(deps.get_current_user), 117 | ): 118 | db_discount_code = crud.discount_code.get(db, id=discount_code_id) 119 | if not db_discount_code: 120 | raise HTTPException(status_code=404, detail="Discount code not found") 121 | # Check if the user owns the campaign associated with the discount code 122 | campaign = crud.campaign.get(db, id=db_discount_code.campaign_id) 123 | if not campaign or campaign.user_id != current_user.id: 124 | raise HTTPException( 125 | status_code=403, detail="Not authorized to update this discount code" 126 | ) 127 | return crud.discount_code.update(db, db_obj=db_discount_code, obj_in=discount_code) 128 | 129 | 130 | @router.delete("/{discount_code_id}", response_model=DiscountCode) 131 | def delete_discount_code( 132 | discount_code_id: UUID, 133 | db: Session = Depends(deps.get_db), 134 | current_user: User = Depends(deps.get_current_user), 135 | ): 136 | discount_code = crud.discount_code.get(db, id=discount_code_id) 137 | if not discount_code: 138 | raise HTTPException(status_code=404, detail="Discount code not found") 139 | # Check if the user owns the campaign associated with the discount code 140 | campaign = crud.campaign.get(db, id=discount_code.campaign_id) 141 | if not campaign or campaign.user_id != current_user.id: 142 | raise HTTPException( 143 | status_code=403, detail="Not authorized to delete this discount code" 144 | ) 145 | return crud.discount_code.remove(db, id=discount_code_id) 146 | -------------------------------------------------------------------------------- /app/api/host_users.py: -------------------------------------------------------------------------------- 1 | from uuid import UUID 2 | from fastapi import APIRouter, Depends, HTTPException 3 | from sqlalchemy.orm import Session 4 | from app import crud 5 | from app.schemas.host_user import HostUser, HostUserUpdate 6 | from app.api.deps import get_db 7 | 8 | router = APIRouter() 9 | 10 | 11 | @router.get("/{host_user_id}", response_model=HostUser) 12 | def read_host_user(host_user_id: UUID, db: Session = Depends(get_db)): 13 | db_host_user = crud.host_user.get_host_user(db, host_user_id) 14 | if db_host_user is None: 15 | raise HTTPException(status_code=404, detail="Host user not found") 16 | return db_host_user 17 | 18 | 19 | @router.put("/{host_user_id}", response_model=HostUser) 20 | def update_host_user( 21 | host_user_id: UUID, host_user: HostUserUpdate, db: Session = Depends(get_db) 22 | ): 23 | db_host_user = crud.host_user.update_host_user(db, host_user_id, host_user) 24 | if db_host_user is None: 25 | raise HTTPException(status_code=404, detail="Host user not found") 26 | return db_host_user 27 | 28 | 29 | @router.delete("/{host_user_id}", response_model=HostUser) 30 | def delete_host_user(host_user_id: UUID, db: Session = Depends(get_db)): 31 | db_host_user = crud.host_user.delete_host_user(db, host_user_id) 32 | if db_host_user is None: 33 | raise HTTPException(status_code=404, detail="Host user not found") 34 | return db_host_user 35 | -------------------------------------------------------------------------------- /app/api/hosts.py: -------------------------------------------------------------------------------- 1 | from uuid import UUID 2 | from fastapi import APIRouter, Depends, HTTPException 3 | from sqlalchemy.orm import Session 4 | from app import crud 5 | from app.schemas.host import Host, HostCreate, HostUpdate 6 | from app.api.deps import get_current_user, get_db 7 | from app.schemas.user import User 8 | 9 | router = APIRouter() 10 | 11 | 12 | @router.post("/", response_model=Host) 13 | def create_host( 14 | host: HostCreate, 15 | db: Session = Depends(get_db), 16 | current_user: User = Depends(get_current_user), 17 | ): 18 | return crud.host.create_with_user(db, obj_in=host, user=current_user) 19 | 20 | 21 | @router.get("/default", response_model=Host) 22 | def read_default_host( 23 | db: Session = Depends(get_db), current_user: User = Depends(get_current_user) 24 | ): 25 | db_host = crud.host.get_default_with_user(db, user=current_user) 26 | if db_host is None: 27 | raise HTTPException(status_code=404, detail="Host not found") 28 | return db_host 29 | 30 | 31 | @router.get("/{host_id}", response_model=Host) 32 | def read_host( 33 | host_id: UUID, 34 | db: Session = Depends(get_db), 35 | current_user: User = Depends(get_current_user), 36 | ): 37 | db_host = crud.host.get(db, host_id) 38 | if db_host is None: 39 | raise HTTPException(status_code=404, detail="Host not found") 40 | return db_host 41 | 42 | 43 | @router.put("/{host_id}", response_model=Host) 44 | def update_host( 45 | host_id: UUID, 46 | host: HostUpdate, 47 | db: Session = Depends(get_db), 48 | current_user: User = Depends(get_current_user), 49 | ): 50 | db_host = crud.host.update(db, host_id, host) 51 | if db_host is None: 52 | raise HTTPException(status_code=404, detail="Host not found") 53 | return db_host 54 | 55 | 56 | @router.delete("/{host_id}", response_model=Host) 57 | def delete_host( 58 | host_id: UUID, 59 | db: Session = Depends(get_db), 60 | current_user: User = Depends(get_current_user), 61 | ): 62 | db_host = crud.host.delete(db, host_id) 63 | if db_host is None: 64 | raise HTTPException(status_code=404, detail="Host not found") 65 | return db_host 66 | -------------------------------------------------------------------------------- /app/api/users.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | from uuid import UUID 3 | from fastapi import APIRouter, Depends, HTTPException, status 4 | from sqlalchemy.orm import Session 5 | from typing import List 6 | from app import crud 7 | from app.schemas.user import Token, UserCreate, UserLogin, UserUpdate, User 8 | from app.api import deps 9 | from app.config import settings 10 | from app.security import create_access_token 11 | from app.schemas.host import HostCreate 12 | 13 | router = APIRouter(prefix="/users", tags=["users"]) 14 | 15 | 16 | @router.post("/", response_model=User) 17 | def create_user(user: UserCreate, db: Session = Depends(deps.get_db)): 18 | db_user = crud.user.get_by_email(db, email=user.email) 19 | if db_user: 20 | raise HTTPException(status_code=400, detail="Email already registered") 21 | new_user = crud.user.create(db=db, obj_in=user) 22 | 23 | # Create default host for the new user 24 | default_host = HostCreate(name=f"Default Host") 25 | crud.host.create_with_user(db=db, obj_in=default_host, user=new_user) 26 | 27 | return new_user 28 | 29 | 30 | @router.get("/", response_model=List[User]) 31 | def read_users( 32 | skip: int = 0, 33 | limit: int = 100, 34 | db: Session = Depends(deps.get_db), 35 | current_user: User = Depends(deps.get_current_user), 36 | ): 37 | users = crud.user.get_multi(db, skip=skip, limit=limit) 38 | return users 39 | 40 | 41 | @router.get("/me", response_model=User) 42 | def read_current_user( 43 | db: Session = Depends(deps.get_db), 44 | current_user: User = Depends(deps.get_current_user), 45 | ): 46 | return current_user 47 | 48 | 49 | @router.get("/{user_id}", response_model=User) 50 | def read_user( 51 | user_id: UUID, 52 | db: Session = Depends(deps.get_db), 53 | current_user: User = Depends(deps.get_current_user), 54 | ): 55 | db_user = crud.user.get(db, id=user_id) 56 | if db_user is None: 57 | raise HTTPException(status_code=404, detail="User not found") 58 | return db_user 59 | 60 | 61 | @router.put("/{user_id}", response_model=User) 62 | def update_user( 63 | user_id: UUID, 64 | user: UserUpdate, 65 | db: Session = Depends(deps.get_db), 66 | current_user: User = Depends(deps.get_current_user), 67 | ): 68 | db_user = crud.user.get(db, id=user_id) 69 | if db_user is None: 70 | raise HTTPException(status_code=404, detail="User not found") 71 | return crud.user.update(db, db_obj=db_user, obj_in=user) 72 | 73 | 74 | @router.delete("/{user_id}", response_model=User) 75 | def delete_user( 76 | user_id: UUID, 77 | db: Session = Depends(deps.get_db), 78 | current_user: User = Depends(deps.get_current_user), 79 | ): 80 | db_user = crud.user.get(db, id=user_id) 81 | if db_user is None: 82 | raise HTTPException(status_code=404, detail="User not found") 83 | return crud.user.remove(db, id=user_id) 84 | 85 | 86 | @router.post("/token", response_model=Token) 87 | async def login_for_access_token(data: UserLogin, db: Session = Depends(deps.get_db)): 88 | user = crud.user.authenticate(db, email=data.email, password=data.password) 89 | if not user: 90 | raise HTTPException( 91 | status_code=status.HTTP_401_UNAUTHORIZED, 92 | detail="Incorrect username or password", 93 | headers={"WWW-Authenticate": "Bearer"}, 94 | ) 95 | access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) 96 | access_token = create_access_token( 97 | data={"sub": user.email}, expires_delta=access_token_expires 98 | ) 99 | return {"access_token": access_token, "token_type": "bearer"} 100 | -------------------------------------------------------------------------------- /app/api/widget_templates.py: -------------------------------------------------------------------------------- 1 | from uuid import UUID 2 | from fastapi import APIRouter, Body, Depends, HTTPException 3 | import openai 4 | from sqlalchemy.orm import Session 5 | from typing import List, Optional 6 | from app import crud, utils 7 | from app.schemas.widget_template import ( 8 | GenerateWidgetTemplate, 9 | WidgetTemplate, 10 | WidgetTemplateCreate, 11 | WidgetTemplateUpdate, 12 | ) 13 | from app.api import deps 14 | from app.config import settings 15 | from app.models.user import User 16 | 17 | router = APIRouter() 18 | client = openai.OpenAI( 19 | base_url=settings.OPENAI_API_URL, api_key=settings.OPENAI_API_KEY 20 | ) 21 | 22 | 23 | @router.post("/", response_model=WidgetTemplate) 24 | def create_widget_template( 25 | widget_template: WidgetTemplateCreate, 26 | db: Session = Depends(deps.get_db), 27 | current_user: User = Depends(deps.get_current_user), 28 | ): 29 | # Check if the user owns the host 30 | host = crud.host.get(db, id=widget_template.host_id) 31 | if not host or host.owner_id != current_user.id: 32 | raise HTTPException( 33 | status_code=403, 34 | detail="Not authorized to create widget template for this host", 35 | ) 36 | 37 | 38 | # Check if the widget template is safe 39 | is_safe, reason = utils.is_html_safe(widget_template.template) 40 | if not is_safe: 41 | raise HTTPException( 42 | status_code=400, 43 | detail=f"Widget template is not safe: {reason}", 44 | ) 45 | 46 | return crud.widget_template.create_with_host( 47 | db=db, obj_in=widget_template, host_id=widget_template.host_id 48 | ) 49 | 50 | 51 | @router.get("/", response_model=List[WidgetTemplate]) 52 | def read_widget_templates( 53 | skip: int = 0, 54 | limit: int = 100, 55 | host_id: Optional[UUID] = None, 56 | db: Session = Depends(deps.get_db), 57 | current_user: User = Depends(deps.get_current_user), 58 | ): 59 | if host_id: 60 | # Check if the user owns the host 61 | host = crud.host.get(db, id=host_id) 62 | if not host or host.owner_id != current_user.id: 63 | raise HTTPException( 64 | status_code=403, 65 | detail="Not authorized to view widget templates for this host", 66 | ) 67 | widget_templates = crud.widget_template.get_widget_templates_by_host( 68 | db, host_id=host_id 69 | ) 70 | else: 71 | widget_templates = crud.widget_template.get_multi(db, skip=skip, limit=limit) 72 | return widget_templates 73 | 74 | 75 | @router.get("/{widget_template_id}", response_model=WidgetTemplate) 76 | def read_widget_template( 77 | widget_template_id: UUID, 78 | db: Session = Depends(deps.get_db), 79 | current_user: User = Depends(deps.get_current_user), 80 | ): 81 | widget_template = crud.widget_template.get(db, id=widget_template_id) 82 | if not widget_template: 83 | raise HTTPException(status_code=404, detail="Widget template not found") 84 | # Check if the user owns the host associated with the widget template 85 | host = crud.host.get(db, id=widget_template.host_id) 86 | if not host or host.owner_id != current_user.id: 87 | raise HTTPException( 88 | status_code=403, detail="Not authorized to view this widget template" 89 | ) 90 | return widget_template 91 | 92 | 93 | @router.put("/{widget_template_id}", response_model=WidgetTemplate) 94 | def update_widget_template( 95 | widget_template_id: UUID, 96 | widget_template: WidgetTemplateUpdate, 97 | db: Session = Depends(deps.get_db), 98 | current_user: User = Depends(deps.get_current_user), 99 | ): 100 | db_widget_template = crud.widget_template.get(db, id=widget_template_id) 101 | if not db_widget_template: 102 | raise HTTPException(status_code=404, detail="Widget template not found") 103 | # Check if the user owns the host associated with the widget template 104 | host = crud.host.get(db, id=db_widget_template.host_id) 105 | if not host or host.owner_id != current_user.id: 106 | raise HTTPException( 107 | status_code=403, detail="Not authorized to update this widget template" 108 | ) 109 | return crud.widget_template.update( 110 | db, db_obj=db_widget_template, obj_in=widget_template 111 | ) 112 | 113 | 114 | @router.delete("/{widget_template_id}", response_model=WidgetTemplate) 115 | def delete_widget_template( 116 | widget_template_id: UUID, 117 | db: Session = Depends(deps.get_db), 118 | current_user: User = Depends(deps.get_current_user), 119 | ): 120 | widget_template = crud.widget_template.get(db, id=widget_template_id) 121 | if not widget_template: 122 | raise HTTPException(status_code=404, detail="Widget template not found") 123 | # Check if the user owns the host associated with the widget template 124 | host = crud.host.get(db, id=widget_template.host_id) 125 | if not host or host.owner_id != current_user.id: 126 | raise HTTPException( 127 | status_code=403, detail="Not authorized to delete this widget template" 128 | ) 129 | return crud.widget_template.remove(db, id=widget_template_id) 130 | 131 | 132 | @router.post("/generate_template_with_ai", response_model=GenerateWidgetTemplate) 133 | def generate_template_with_ai( 134 | prompt: str = Body(..., embed=True), 135 | db: Session = Depends(deps.get_db), 136 | current_user: User = Depends(deps.get_current_user), 137 | ): 138 | try: 139 | response = client.chat.completions.create( 140 | model="gpt-4o-mini", 141 | messages=[ 142 | {"role": "system", "content": "You are an expert HTML developer specializing in creating interactive, compact, and user-friendly campaign widgets such as small games, banners, and forms. Your goal is to generate HTML that maximizes user engagement. IMPORTANT: Only return the HTML content, without any explanations or additional text."}, 143 | {"role": "user", "content": f"Create a compact, interactive HTML widget for a campaign based on this prompt: {prompt}. The widget should be engaging, user-friendly, and designed to increase user interaction. Include any necessary inline CSS and JavaScript to make the widget self-contained and easily embeddable. IMPORTANT: Only provide the HTML content, nothing else."}, 144 | ], 145 | max_tokens=1000, 146 | temperature=0.7, 147 | ) 148 | generated_content = response.choices[0].message.content 149 | generated_content = generated_content.replace("```html", "").replace("```", "") 150 | except Exception as e: 151 | raise HTTPException(status_code=500, detail=f"OpenAI API error: {str(e)}") 152 | 153 | return GenerateWidgetTemplate( 154 | prompt=prompt, 155 | template=generated_content, 156 | config={}, 157 | ) 158 | -------------------------------------------------------------------------------- /app/api/widgets.py: -------------------------------------------------------------------------------- 1 | from uuid import UUID 2 | from fastapi import APIRouter, Depends, HTTPException 3 | from sqlalchemy.orm import Session 4 | from typing import List, Optional 5 | from app import crud 6 | from app.models.campaign import CampaignStatus 7 | from app.schemas.widget import Widget, WidgetCreate, WidgetUpdate 8 | from app.api import deps 9 | from app.models.user import User 10 | from app.services.widget_service import widget_service 11 | from app.utils import add_default_js_to_html 12 | 13 | router = APIRouter() 14 | 15 | 16 | @router.post("/", response_model=Widget) 17 | def create_widget( 18 | widget: WidgetCreate, 19 | db: Session = Depends(deps.get_db), 20 | current_user: User = Depends(deps.get_current_user), 21 | ): 22 | template = crud.widget_template.get(db, id=widget.widget_template_id) 23 | if template is None: 24 | raise HTTPException(status_code=404, detail="Widget template not found") 25 | 26 | campaign = crud.campaign.get(db, id=widget.campaign_id) 27 | if campaign is None: 28 | raise HTTPException(status_code=404, detail="Campaign not found") 29 | 30 | if campaign.status != CampaignStatus.active: 31 | raise HTTPException( 32 | status_code=400, detail="Cannot create widget for inactive campaign" 33 | ) 34 | 35 | template.template = add_default_js_to_html(template.template) 36 | 37 | obj = crud.widget.create_with_template(db=db, obj_in=widget, template=template) 38 | 39 | widget_data = { 40 | "widget_id": obj.id, 41 | "host_id": obj.host_id, 42 | "campaign_id": obj.campaign_id, 43 | "body": obj.body, 44 | "config": obj.config, 45 | } 46 | widget_service.create_widget(widget_data) 47 | return obj 48 | 49 | 50 | @router.get("/", response_model=List[Widget]) 51 | def read_widgets( 52 | skip: int = 0, 53 | limit: int = 100, 54 | campaign_id: Optional[UUID] = None, 55 | db: Session = Depends(deps.get_db), 56 | current_user: User = Depends(deps.get_current_user), 57 | ): 58 | if campaign_id: 59 | widgets = crud.widget.get_multi_by_campaign( 60 | db, skip=skip, limit=limit, campaign_id=campaign_id 61 | ) 62 | else: 63 | widgets = crud.widget.get_multi(db, skip=skip, limit=limit) 64 | return widgets 65 | 66 | 67 | @router.get("/{widget_id}", response_model=Widget) 68 | def read_widget( 69 | widget_id: UUID, 70 | db: Session = Depends(deps.get_db), 71 | current_user: User = Depends(deps.get_current_user), 72 | ): 73 | db_widget = crud.widget.get(db, id=widget_id) 74 | if db_widget is None: 75 | raise HTTPException(status_code=404, detail="Widget not found") 76 | return db_widget 77 | 78 | 79 | @router.put("/{widget_id}", response_model=Widget) 80 | def update_widget( 81 | widget_id: UUID, 82 | widget: WidgetUpdate, 83 | db: Session = Depends(deps.get_db), 84 | current_user: User = Depends(deps.get_current_user), 85 | ): 86 | db_widget = crud.widget.get(db, id=widget_id) 87 | if db_widget is None: 88 | raise HTTPException(status_code=404, detail="Widget not found") 89 | obj = crud.widget.update(db, db_obj=db_widget, obj_in=widget) 90 | 91 | widget_data = { 92 | "host_id": obj.host_id, 93 | "campaign_id": obj.campaign_id, 94 | "body": obj.body, 95 | "config": obj.config, 96 | } 97 | widget_service.update_widget(obj.id, widget_data) 98 | return obj 99 | 100 | 101 | @router.delete("/{widget_id}", response_model=Widget) 102 | def delete_widget( 103 | widget_id: UUID, 104 | db: Session = Depends(deps.get_db), 105 | current_user: User = Depends(deps.get_current_user), 106 | ): 107 | db_widget = crud.widget.get(db, id=widget_id) 108 | if db_widget is None: 109 | raise HTTPException(status_code=404, detail="Widget not found") 110 | obj = crud.widget.remove(db, id=widget_id) 111 | widget_service.delete_widget(obj.id) 112 | return obj 113 | -------------------------------------------------------------------------------- /app/config.py: -------------------------------------------------------------------------------- 1 | from pydantic_settings import BaseSettings 2 | import os 3 | 4 | 5 | class Settings(BaseSettings): 6 | DATABASE_URL: str = "postgresql://user:password@db:5432/mydatabase" 7 | API_V1_STR: str = "/api/v1" 8 | SECRET_KEY: str = "your-secret-key" 9 | ALGORITHM: str = "HS256" 10 | ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 11 | WIDGET_SERVICE_URL: str = "http://192.168.1.15:9000/api/v1" 12 | WIDGET_SERVICE_TOKEN: str = "widget-service-token" 13 | DEFAULT_JS_URL: str = "http://192.168.1.12:3000/userTracker.js" 14 | OPENAI_API_KEY: str = "secret" 15 | OPENAI_API_URL: str = "https://api.openai.com/v1" 16 | 17 | class Config: 18 | env_file = ".env" 19 | 20 | 21 | settings = Settings() 22 | -------------------------------------------------------------------------------- /app/crud/__init__.py: -------------------------------------------------------------------------------- 1 | from .crud_user import user 2 | from .crud_host import host 3 | from .crud_campaign import campaign 4 | from .crud_discount_code import discount_code 5 | from .crud_widget_template import widget_template 6 | from .crud_widget import widget 7 | from .crud_host_user import host_user 8 | 9 | 10 | __all__ = [ 11 | "user", 12 | "host", 13 | "campaign", 14 | "discount_code", 15 | "widget_template", 16 | "widget", 17 | "host_user", 18 | ] 19 | -------------------------------------------------------------------------------- /app/crud/base.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, Generic, List, Optional, Type, TypeVar, Union 2 | 3 | from fastapi.encoders import jsonable_encoder 4 | from pydantic import BaseModel 5 | from sqlalchemy.orm import Session 6 | 7 | from app.models.base import Base 8 | 9 | ModelType = TypeVar("ModelType", bound=Base) 10 | CreateSchemaType = TypeVar("CreateSchemaType", bound=BaseModel) 11 | UpdateSchemaType = TypeVar("UpdateSchemaType", bound=BaseModel) 12 | 13 | 14 | class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]): 15 | def __init__(self, model: Type[ModelType]): 16 | """ 17 | CRUD object with default methods to Create, Read, Update, Delete (CRUD). 18 | **Parameters** 19 | * `model`: A SQLAlchemy model class 20 | * `schema`: A Pydantic model (schema) class 21 | """ 22 | self.model = model 23 | 24 | def get(self, db: Session, id: Any) -> Optional[ModelType]: 25 | return db.query(self.model).filter(self.model.id == id).first() 26 | 27 | def get_multi( 28 | self, db: Session, *, skip: int = 0, limit: int = 100 29 | ) -> List[ModelType]: 30 | return db.query(self.model).offset(skip).limit(limit).all() 31 | 32 | def create(self, db: Session, *, obj_in: CreateSchemaType) -> ModelType: 33 | obj_in_data = jsonable_encoder(obj_in) 34 | db_obj = self.model(**obj_in_data) 35 | db.add(db_obj) 36 | db.commit() 37 | db.refresh(db_obj) 38 | return db_obj 39 | 40 | def update( 41 | self, 42 | db: Session, 43 | *, 44 | db_obj: ModelType, 45 | obj_in: Union[UpdateSchemaType, Dict[str, Any]] 46 | ) -> ModelType: 47 | obj_data = jsonable_encoder(db_obj) 48 | if isinstance(obj_in, dict): 49 | update_data = obj_in 50 | else: 51 | update_data = obj_in.dict(exclude_unset=True) 52 | for field in obj_data: 53 | if field in update_data: 54 | setattr(db_obj, field, update_data[field]) 55 | db.add(db_obj) 56 | db.commit() 57 | db.refresh(db_obj) 58 | return db_obj 59 | 60 | def remove(self, db: Session, *, id: int) -> ModelType: 61 | obj = db.query(self.model).get(id) 62 | db.delete(obj) 63 | db.commit() 64 | return obj 65 | -------------------------------------------------------------------------------- /app/crud/crud_campaign.py: -------------------------------------------------------------------------------- 1 | from uuid import UUID 2 | from sqlalchemy.orm import Session 3 | from app.crud.base import CRUDBase 4 | from app.models.campaign import Campaign 5 | from app.schemas.campaign import CampaignCreate, CampaignUpdate 6 | from typing import List, Tuple, Any 7 | 8 | 9 | class CRUDCampaign(CRUDBase[Campaign, CampaignCreate, CampaignUpdate]): 10 | def create_with_user_and_host( 11 | self, db: Session, *, obj_in: CampaignCreate, user_id: UUID, host_id: UUID 12 | ) -> Campaign: 13 | db_obj = Campaign( 14 | name=obj_in.name, 15 | creator_id=user_id, 16 | host_id=host_id, 17 | description=obj_in.description, 18 | start_date=obj_in.start_date, 19 | end_date=obj_in.end_date, 20 | status=obj_in.status, 21 | type=obj_in.type, 22 | ) 23 | db.add(db_obj) 24 | db.commit() 25 | db.refresh(db_obj) 26 | return db_obj 27 | 28 | def get_multi_with_filters( 29 | self, 30 | db: Session, 31 | *, 32 | skip: int = 0, 33 | limit: int = 100, 34 | filters: List[Tuple[str, Any]] = [] 35 | ) -> List[Campaign]: 36 | query = db.query(self.model) 37 | for field, value in filters: 38 | query = query.filter(getattr(self.model, field) == value) 39 | return query.offset(skip).limit(limit).all() 40 | 41 | 42 | campaign = CRUDCampaign(Campaign) 43 | -------------------------------------------------------------------------------- /app/crud/crud_discount_code.py: -------------------------------------------------------------------------------- 1 | from uuid import UUID 2 | import uuid 3 | from sqlalchemy.orm import Session 4 | from app.crud.base import CRUDBase 5 | from app.models.discount_code import DiscountCode 6 | from app.schemas.discount_code import DiscountCodeCreate, DiscountCodeUpdate 7 | 8 | 9 | class CRUDDiscountCode(CRUDBase[DiscountCode, DiscountCodeCreate, DiscountCodeUpdate]): 10 | def create_with_campaign( 11 | self, db: Session, *, obj_in: DiscountCodeCreate, campaign_id: UUID 12 | ) -> DiscountCode: 13 | db_obj = DiscountCode( 14 | code=obj_in.code, 15 | discount_value=obj_in.discount_value, 16 | discount_type=obj_in.discount_type, 17 | max_uses=obj_in.max_uses, 18 | expiration_date=obj_in.expiration_date, 19 | is_active=obj_in.is_active, 20 | campaign_id=campaign_id, 21 | ) 22 | db.add(db_obj) 23 | db.commit() 24 | db.refresh(db_obj) 25 | return db_obj 26 | 27 | def get_discount_codes_by_campaign(self, db: Session, campaign_id: UUID): 28 | return ( 29 | db.query(DiscountCode).filter(DiscountCode.campaign_id == campaign_id).all() 30 | ) 31 | 32 | def generate_unique_code(self, db: Session) -> str: 33 | unique_id = str(uuid.uuid4()) 34 | last_part = unique_id.split('-')[-1] 35 | return f"{last_part}" 36 | 37 | 38 | discount_code = CRUDDiscountCode(DiscountCode) 39 | -------------------------------------------------------------------------------- /app/crud/crud_host.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm import Session 2 | from app.crud.base import CRUDBase 3 | from app.models.host import Host 4 | from app.models.user import User 5 | from app.schemas.host import HostCreate, HostUpdate 6 | 7 | 8 | class CRUDHost(CRUDBase[Host, HostCreate, HostUpdate]): 9 | def create_with_user(self, db: Session, *, obj_in: HostCreate, user: User) -> Host: 10 | db_obj = Host(**obj_in.dict(), owner_id=user.id) 11 | db.add(db_obj) 12 | db.commit() 13 | db.refresh(db_obj) 14 | return db_obj 15 | 16 | def get_default_with_user(self, db: Session, *, user: User) -> Host: 17 | return db.query(Host).filter(Host.owner_id == user.id).first() 18 | 19 | 20 | host = CRUDHost(Host) 21 | -------------------------------------------------------------------------------- /app/crud/crud_host_user.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm import Session 2 | from app.crud.base import CRUDBase 3 | from app.models.host_user import HostUser 4 | from app.schemas.host_user import HostUserCreate, HostUserUpdate 5 | 6 | 7 | class CRUDHostUser(CRUDBase[HostUser, HostUserCreate, HostUserUpdate]): 8 | def get_or_create_by_client_ref_id(self, db: Session, client_reference_id: str): 9 | db_host_user = ( 10 | db.query(HostUser) 11 | .filter(HostUser.client_reference_id == client_reference_id) 12 | .first() 13 | ) 14 | if db_host_user is None: 15 | db_host_user = HostUser(client_reference_id=client_reference_id) 16 | db.add(db_host_user) 17 | db.commit() 18 | db.refresh(db_host_user) 19 | return db_host_user 20 | 21 | 22 | host_user = CRUDHostUser(HostUser) 23 | -------------------------------------------------------------------------------- /app/crud/crud_user.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, Optional, Union 2 | 3 | from sqlalchemy.orm import Session 4 | 5 | from app.security import get_password_hash, verify_password 6 | from app.crud.base import CRUDBase 7 | from app.models.user import User 8 | from app.schemas.user import UserCreate, UserUpdate 9 | 10 | 11 | class CRUDUser(CRUDBase[User, UserCreate, UserUpdate]): 12 | def authenticate(self, db: Session, *, email: str, password: str) -> Optional[User]: 13 | user = self.get_by_email(db, email=email) 14 | if not user: 15 | return None 16 | if not verify_password(password, user.hashed_password): 17 | return None 18 | return user 19 | 20 | def get_by_email(self, db: Session, *, email: str) -> Optional[User]: 21 | return db.query(User).filter(User.email == email).first() 22 | 23 | def create(self, db: Session, *, obj_in: UserCreate) -> User: 24 | db_obj = User( 25 | email=obj_in.email, 26 | hashed_password=get_password_hash(obj_in.password), 27 | ) 28 | db.add(db_obj) 29 | db.commit() 30 | db.refresh(db_obj) 31 | return db_obj 32 | 33 | def update( 34 | self, db: Session, *, db_obj: User, obj_in: Union[UserUpdate, Dict[str, Any]] 35 | ) -> User: 36 | if isinstance(obj_in, dict): 37 | update_data = obj_in 38 | else: 39 | update_data = obj_in.dict(exclude_unset=True) 40 | if update_data.get("password"): 41 | hashed_password = update_data[ 42 | "password" 43 | ] # In a real app, hash the password here 44 | del update_data["password"] 45 | update_data["hashed_password"] = hashed_password 46 | return super().update(db, db_obj=db_obj, obj_in=update_data) 47 | 48 | 49 | user = CRUDUser(User) 50 | -------------------------------------------------------------------------------- /app/crud/crud_widget.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm import Session 2 | from app.crud.base import CRUDBase 3 | from app.models.widget import Widget 4 | from app.models.widget_template import WidgetTemplate 5 | from app.schemas.widget import WidgetCreate, WidgetUpdate 6 | from typing import List, Optional 7 | from uuid import UUID 8 | 9 | 10 | class CRUDWidget(CRUDBase[Widget, WidgetCreate, WidgetUpdate]): 11 | def create_with_template( 12 | self, db: Session, *, obj_in: WidgetCreate, template: WidgetTemplate 13 | ) -> Widget: 14 | db_obj = self.model( 15 | **obj_in.model_dump(), 16 | body=template.template, 17 | ) 18 | db.add(db_obj) 19 | db.commit() 20 | db.refresh(db_obj) 21 | return db_obj 22 | 23 | def get_multi_by_campaign( 24 | self, 25 | db: Session, 26 | *, 27 | skip: int = 0, 28 | limit: int = 100, 29 | campaign_id: Optional[UUID] = None 30 | ) -> List[Widget]: 31 | query = db.query(self.model).filter(self.model.campaign_id == campaign_id) 32 | return query.offset(skip).limit(limit).all() 33 | 34 | 35 | widget = CRUDWidget(Widget) 36 | -------------------------------------------------------------------------------- /app/crud/crud_widget_template.py: -------------------------------------------------------------------------------- 1 | from uuid import UUID 2 | from sqlalchemy.orm import Session 3 | from app.crud.base import CRUDBase 4 | from app.models.widget_template import WidgetTemplate 5 | from app.schemas.widget_template import WidgetTemplateCreate, WidgetTemplateUpdate 6 | 7 | 8 | class CRUDWidgetTemplate( 9 | CRUDBase[WidgetTemplate, WidgetTemplateCreate, WidgetTemplateUpdate] 10 | ): 11 | def create_with_host( 12 | self, db: Session, *, obj_in: WidgetTemplateCreate, host_id: UUID 13 | ) -> WidgetTemplate: 14 | db_obj = WidgetTemplate( 15 | name=obj_in.name, 16 | description=obj_in.description, 17 | type=obj_in.type, 18 | template=obj_in.template, 19 | host_id=host_id, 20 | ) 21 | db.add(db_obj) 22 | db.commit() 23 | db.refresh(db_obj) 24 | return db_obj 25 | 26 | def get_widget_templates_by_host(self, db: Session, host_id: UUID): 27 | return db.query(WidgetTemplate).filter(WidgetTemplate.host_id == host_id).all() 28 | 29 | 30 | widget_template = CRUDWidgetTemplate(WidgetTemplate) 31 | -------------------------------------------------------------------------------- /app/database.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import create_engine 2 | from sqlalchemy.ext.declarative import declarative_base 3 | from sqlalchemy.orm import sessionmaker 4 | import os 5 | 6 | DATABASE_URL = os.getenv( 7 | "DATABASE_URL", "postgresql://user:password@localhost:5432/mydatabase" 8 | ) 9 | 10 | engine = create_engine(DATABASE_URL) 11 | SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) 12 | 13 | Base = declarative_base() 14 | -------------------------------------------------------------------------------- /app/db.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import create_engine 2 | from sqlalchemy.orm import sessionmaker 3 | from app.config import settings 4 | 5 | engine = create_engine(settings.DATABASE_URL) 6 | SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) 7 | -------------------------------------------------------------------------------- /app/main.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from fastapi.middleware.cors import CORSMiddleware 3 | from app.api import ( 4 | discount_codes, 5 | users, 6 | hosts, 7 | campaigns, 8 | widget_templates, 9 | widgets, 10 | host_users, 11 | ) 12 | from app.db import engine 13 | from app.models import base 14 | 15 | app = FastAPI(title="Interactor") 16 | 17 | # Configure CORS 18 | app.add_middleware( 19 | CORSMiddleware, 20 | allow_origins=["*"], # Allow all origins 21 | allow_credentials=True, 22 | allow_methods=["*"], 23 | allow_headers=["*"], 24 | ) 25 | 26 | # Create database tables 27 | base.Base.metadata.create_all(bind=engine) 28 | 29 | # Include routers 30 | app.include_router(users.router, prefix="/api/v1/users", tags=["users"]) 31 | app.include_router(hosts.router, prefix="/api/v1/hosts", tags=["hosts"]) 32 | app.include_router(campaigns.router, prefix="/api/v1/campaigns", tags=["campaigns"]) 33 | app.include_router( 34 | discount_codes.router, prefix="/api/v1/discount-codes", tags=["discount_codes"] 35 | ) 36 | app.include_router( 37 | widget_templates.router, 38 | prefix="/api/v1/widget-templates", 39 | tags=["widget_templates"], 40 | ) 41 | app.include_router(widgets.router, prefix="/api/v1/widgets", tags=["widgets"]) 42 | app.include_router(host_users.router, prefix="/api/v1/host-users", tags=["host_users"]) 43 | 44 | 45 | @app.get("/") 46 | async def root(): 47 | return {"message": "Hello World"} 48 | -------------------------------------------------------------------------------- /app/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shahriarshm/ict9-interactor-backend/cbd85b7db21271dc2e42a5fb49ce21de6d157773/app/models/__init__.py -------------------------------------------------------------------------------- /app/models/base.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | from sqlalchemy.ext.declarative import declarative_base 3 | from sqlalchemy import UUID, Column, DateTime 4 | from sqlalchemy.sql import func 5 | 6 | 7 | class Base: 8 | __abstract__ = True 9 | 10 | id = Column(UUID(as_uuid=True), primary_key=True, index=True, default=uuid.uuid4) 11 | created_at = Column(DateTime(timezone=True), server_default=func.now()) 12 | updated_at = Column(DateTime(timezone=True), onupdate=func.now()) 13 | 14 | def dict(self): 15 | return {c.name: getattr(self, c.name) for c in self.__table__.columns} 16 | 17 | 18 | Base = declarative_base(cls=Base) 19 | -------------------------------------------------------------------------------- /app/models/campaign.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, String, ForeignKey, Date, Enum, Text 2 | from sqlalchemy.orm import relationship 3 | from sqlalchemy.dialects.postgresql import UUID 4 | from app.models.base import Base 5 | import enum 6 | 7 | 8 | class CampaignStatus(enum.Enum): 9 | draft = "draft" 10 | active = "active" 11 | expired = "expired" 12 | 13 | 14 | class CampaignType(enum.Enum): 15 | email = "email" 16 | social_media = "social_media" 17 | display_ads = "display_ads" 18 | content_marketing = "content_marketing" 19 | event = "event" 20 | 21 | 22 | class Campaign(Base): 23 | __tablename__ = "campaigns" 24 | 25 | name = Column(String(255), nullable=False) 26 | description = Column(Text) 27 | start_date = Column(Date, nullable=False) 28 | end_date = Column(Date, nullable=False) 29 | status = Column(Enum(CampaignStatus), nullable=False, default=CampaignStatus.draft) 30 | type = Column(Enum(CampaignType), nullable=False) 31 | 32 | creator_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False) 33 | host_id = Column(UUID(as_uuid=True), ForeignKey("hosts.id"), nullable=False) 34 | 35 | creator = relationship("User", back_populates="campaigns") 36 | host = relationship("Host", back_populates="campaigns") 37 | widgets = relationship("Widget", back_populates="campaign") 38 | discount_codes = relationship("DiscountCode", back_populates="campaign") 39 | -------------------------------------------------------------------------------- /app/models/discount_code.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, String, ForeignKey, Date, Enum, Integer, Numeric, Boolean 2 | from sqlalchemy.orm import relationship 3 | from sqlalchemy.dialects.postgresql import UUID 4 | from app.models.base import Base 5 | import enum 6 | 7 | 8 | class DiscountType(enum.Enum): 9 | percentage = "percentage" 10 | fixed_amount = "fixed_amount" 11 | free_shipping = "free_shipping" 12 | 13 | 14 | class DiscountCode(Base): 15 | __tablename__ = "discount_codes" 16 | 17 | code = Column(String(50), nullable=False, unique=True) 18 | discount_value = Column(Numeric(10, 2), nullable=False) 19 | discount_type = Column(Enum(DiscountType), nullable=False) 20 | max_uses = Column(Integer, nullable=True) 21 | current_uses = Column(Integer, default=0, nullable=False) 22 | expiration_date = Column(Date, nullable=True) 23 | is_active = Column(Boolean, default=True, nullable=False) 24 | 25 | campaign_id = Column(UUID(as_uuid=True), ForeignKey("campaigns.id"), nullable=False) 26 | campaign = relationship("Campaign", back_populates="discount_codes") 27 | -------------------------------------------------------------------------------- /app/models/host.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, String, ForeignKey 2 | from sqlalchemy.orm import relationship 3 | from sqlalchemy.dialects.postgresql import UUID 4 | import uuid 5 | from app.models.base import Base 6 | 7 | 8 | class Host(Base): 9 | __tablename__ = "hosts" 10 | 11 | name = Column(String, nullable=False) 12 | api_key = Column( 13 | UUID(as_uuid=True), unique=True, default=uuid.uuid4, nullable=False 14 | ) 15 | owner_id = Column( 16 | UUID(as_uuid=True), ForeignKey("users.id"), unique=True, nullable=False 17 | ) 18 | 19 | owner = relationship("User", back_populates="host") 20 | campaigns = relationship("Campaign", back_populates="host") 21 | widget_templates = relationship("WidgetTemplate", back_populates="host") 22 | widgets = relationship("Widget", back_populates="host") 23 | 24 | host_users = relationship("HostUser", back_populates="host") 25 | -------------------------------------------------------------------------------- /app/models/host_user.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, String, ForeignKey 2 | from sqlalchemy.dialects.postgresql import UUID 3 | from sqlalchemy.orm import relationship 4 | from app.models.base import Base 5 | 6 | 7 | class HostUser(Base): 8 | __tablename__ = "host_users" 9 | 10 | host_id = Column(UUID(as_uuid=True), ForeignKey("hosts.id"), nullable=False) 11 | client_reference_id = Column(String(255), nullable=True) 12 | 13 | host = relationship("Host", back_populates="host_users") 14 | -------------------------------------------------------------------------------- /app/models/user.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, String, Boolean 2 | from sqlalchemy.orm import relationship 3 | from app.models.base import Base 4 | 5 | 6 | class User(Base): 7 | __tablename__ = "users" 8 | 9 | email = Column(String, unique=True, index=True) 10 | hashed_password = Column(String) 11 | is_active = Column(Boolean, default=True) 12 | 13 | host = relationship("Host", back_populates="owner", uselist=False) 14 | campaigns = relationship("Campaign", back_populates="creator") 15 | -------------------------------------------------------------------------------- /app/models/widget.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import JSON, Column, String, ForeignKey, Text 2 | from sqlalchemy.orm import relationship 3 | from sqlalchemy.dialects.postgresql import UUID 4 | from app.models.base import Base 5 | from app.models.widget_template import WidgetTemplate 6 | 7 | 8 | class Widget(Base): 9 | __tablename__ = "widgets" 10 | 11 | host_id = Column(UUID(as_uuid=True), ForeignKey("hosts.id")) 12 | campaign_id = Column(UUID(as_uuid=True), ForeignKey("campaigns.id")) 13 | name = Column(String(255), nullable=False) 14 | description = Column(Text) 15 | body = Column(Text, nullable=False) 16 | config = Column(JSON, nullable=False) 17 | widget_template_id = Column(UUID(as_uuid=True), ForeignKey("widget_templates.id")) 18 | 19 | host = relationship("Host", back_populates="widgets") 20 | campaign = relationship("Campaign", back_populates="widgets") 21 | widget_template = relationship("WidgetTemplate", back_populates="widgets") 22 | -------------------------------------------------------------------------------- /app/models/widget_template.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import JSON, Column, String, ForeignKey, Enum, Text 2 | from sqlalchemy.orm import relationship 3 | from app.models.base import Base 4 | import enum 5 | from sqlalchemy.dialects.postgresql import UUID 6 | 7 | 8 | class WidgetType(enum.Enum): 9 | game = "game" 10 | survey = "survey" 11 | quiz = "quiz" 12 | form = "form" 13 | calculator = "calculator" 14 | 15 | 16 | class WidgetTemplate(Base): 17 | __tablename__ = "widget_templates" 18 | 19 | name = Column(String(255), nullable=False) 20 | description = Column(Text) 21 | type = Column(Enum(WidgetType), nullable=False) 22 | template = Column(Text, nullable=False) 23 | config = Column(JSON, nullable=False) 24 | host_id = Column(UUID(as_uuid=True), ForeignKey("hosts.id"), nullable=False) 25 | 26 | host = relationship("Host", back_populates="widget_templates") 27 | widgets = relationship("Widget", back_populates="widget_template") 28 | -------------------------------------------------------------------------------- /app/schemas/campaign.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, UUID4 2 | from typing import Optional 3 | from datetime import date 4 | from enum import Enum 5 | 6 | 7 | class CampaignStatus(str, Enum): 8 | DRAFT = "draft" 9 | ACTIVE = "active" 10 | PAUSED = "paused" 11 | COMPLETED = "completed" 12 | 13 | 14 | class CampaignType(str, Enum): 15 | email = "email" 16 | social_media = "social_media" 17 | display_ads = "display_ads" 18 | content_marketing = "content_marketing" 19 | event = "event" 20 | 21 | 22 | class CampaignBase(BaseModel): 23 | name: str 24 | host_id: UUID4 25 | description: Optional[str] = None 26 | start_date: date 27 | end_date: date 28 | status: CampaignStatus 29 | type: CampaignType 30 | 31 | 32 | class CampaignCreate(CampaignBase): 33 | pass 34 | 35 | 36 | class CampaignUpdate(CampaignBase): 37 | name: Optional[str] = None 38 | description: Optional[str] = None 39 | start_date: Optional[date] = None 40 | end_date: Optional[date] = None 41 | status: Optional[CampaignStatus] = None 42 | type: Optional[CampaignType] = None 43 | 44 | 45 | class Campaign(CampaignBase): 46 | id: UUID4 47 | creator_id: UUID4 48 | host_id: UUID4 49 | 50 | class Config: 51 | from_attributes = True 52 | -------------------------------------------------------------------------------- /app/schemas/discount_code.py: -------------------------------------------------------------------------------- 1 | from pydantic import UUID4, BaseModel, constr 2 | from typing import Optional 3 | from datetime import date, datetime 4 | from enum import Enum 5 | from decimal import Decimal 6 | 7 | 8 | class DiscountType(str, Enum): 9 | percentage = "percentage" 10 | fixed_amount = "fixed_amount" 11 | free_shipping = "free_shipping" 12 | 13 | 14 | class DiscountCodeBase(BaseModel): 15 | code: constr(max_length=50) 16 | discount_value: Decimal 17 | discount_type: DiscountType 18 | max_uses: Optional[int] = None 19 | expiration_date: Optional[date] = None 20 | is_active: bool = True 21 | campaign_id: UUID4 22 | 23 | 24 | class DiscountCodeCreate(DiscountCodeBase): 25 | pass 26 | 27 | 28 | class DiscountCodeUpdate(BaseModel): 29 | discount_value: Optional[Decimal] = None 30 | discount_type: Optional[DiscountType] = None 31 | max_uses: Optional[int] = None 32 | expiration_date: Optional[date] = None 33 | is_active: Optional[bool] = None 34 | 35 | 36 | class DiscountCode(DiscountCodeBase): 37 | id: UUID4 38 | 39 | class Config: 40 | from_attributes = True 41 | 42 | 43 | class BulkDiscountCodeCreate(BaseModel): 44 | campaign_id: UUID4 45 | code_prefix: str 46 | count: int 47 | discount_type: str 48 | discount_value: float 49 | max_uses: Optional[int] = None 50 | expiration_date: Optional[datetime] = None 51 | -------------------------------------------------------------------------------- /app/schemas/host.py: -------------------------------------------------------------------------------- 1 | from pydantic import UUID4, BaseModel 2 | from typing import Optional 3 | 4 | 5 | class HostBase(BaseModel): 6 | name: str 7 | 8 | 9 | class HostCreate(HostBase): 10 | pass 11 | 12 | 13 | class HostUpdate(HostBase): 14 | name: Optional[str] = None 15 | 16 | 17 | class Host(HostBase): 18 | id: UUID4 19 | owner_id: UUID4 20 | 21 | class Config: 22 | from_attributes = True 23 | -------------------------------------------------------------------------------- /app/schemas/host_user.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, UUID4 2 | from typing import Optional 3 | 4 | 5 | class HostUserBase(BaseModel): 6 | client_reference_id: Optional[str] = None 7 | 8 | 9 | class HostUserCreate(HostUserBase): 10 | host_id: UUID4 11 | 12 | 13 | class HostUserUpdate(HostUserBase): 14 | pass 15 | 16 | 17 | class HostUser(HostUserBase): 18 | id: UUID4 19 | host_id: UUID4 20 | 21 | class Config: 22 | from_attributes = True 23 | -------------------------------------------------------------------------------- /app/schemas/user.py: -------------------------------------------------------------------------------- 1 | from pydantic import UUID4, BaseModel, EmailStr 2 | from typing import Optional 3 | 4 | 5 | class UserBase(BaseModel): 6 | email: EmailStr 7 | 8 | 9 | class UserCreate(UserBase): 10 | password: str 11 | 12 | 13 | class User(UserBase): 14 | id: UUID4 15 | is_active: bool 16 | 17 | class Config: 18 | from_attributes = True 19 | 20 | 21 | class UserUpdate(UserBase): 22 | password: Optional[str] = None 23 | 24 | 25 | class UserLogin(BaseModel): 26 | email: EmailStr 27 | password: str 28 | 29 | 30 | class Token(BaseModel): 31 | access_token: str 32 | token_type: str 33 | 34 | 35 | class TokenData(BaseModel): 36 | email: str | None = None 37 | -------------------------------------------------------------------------------- /app/schemas/widget.py: -------------------------------------------------------------------------------- 1 | from pydantic import UUID4, BaseModel, constr, Json 2 | from typing import Optional 3 | 4 | 5 | class WidgetBase(BaseModel): 6 | name: constr(max_length=255) 7 | description: Optional[str] = None 8 | config: dict 9 | 10 | 11 | class WidgetCreate(WidgetBase): 12 | host_id: UUID4 13 | campaign_id: UUID4 14 | widget_template_id: UUID4 15 | 16 | 17 | class WidgetUpdate(BaseModel): 18 | name: Optional[constr(max_length=255)] = None 19 | description: Optional[str] = None 20 | config: Optional[dict] = None 21 | 22 | 23 | class Widget(WidgetBase): 24 | id: UUID4 25 | host_id: UUID4 26 | campaign_id: UUID4 27 | widget_template_id: UUID4 28 | 29 | class Config: 30 | from_attributes = True 31 | -------------------------------------------------------------------------------- /app/schemas/widget_template.py: -------------------------------------------------------------------------------- 1 | from pydantic import UUID4, BaseModel, constr 2 | from typing import Optional 3 | from enum import Enum 4 | 5 | from app.models.widget_template import WidgetType 6 | 7 | 8 | class WidgetTemplateBase(BaseModel): 9 | name: constr(max_length=255) 10 | description: Optional[str] = None 11 | type: WidgetType 12 | template: str 13 | config: dict 14 | host_id: UUID4 15 | 16 | 17 | class WidgetTemplateCreate(WidgetTemplateBase): 18 | pass 19 | 20 | 21 | class WidgetTemplateUpdate(BaseModel): 22 | name: Optional[constr(max_length=255)] = None 23 | description: Optional[str] = None 24 | type: Optional[WidgetType] = None 25 | template: Optional[str] = None 26 | config: Optional[dict] = None 27 | host_id: Optional[UUID4] = None 28 | 29 | 30 | class WidgetTemplate(WidgetTemplateBase): 31 | id: UUID4 32 | 33 | class Config: 34 | from_attributes = True 35 | 36 | 37 | class GenerateWidgetTemplate(BaseModel): 38 | prompt: str 39 | template: str 40 | config: dict = {} 41 | -------------------------------------------------------------------------------- /app/security.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | from jose import jwt 3 | from passlib.context import CryptContext 4 | from app.config import settings 5 | 6 | pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") 7 | 8 | 9 | def verify_password(plain_password, hashed_password): 10 | return pwd_context.verify(plain_password, hashed_password) 11 | 12 | 13 | def get_password_hash(password): 14 | return pwd_context.hash(password) 15 | 16 | 17 | def create_access_token(data: dict, expires_delta: timedelta | None = None): 18 | to_encode = data.copy() 19 | if expires_delta: 20 | expire = datetime.utcnow() + expires_delta 21 | else: 22 | expire = datetime.utcnow() + timedelta( 23 | minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES 24 | ) 25 | to_encode.update({"exp": expire}) 26 | encoded_jwt = jwt.encode( 27 | to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM 28 | ) 29 | return encoded_jwt 30 | -------------------------------------------------------------------------------- /app/services/widget_service.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | import requests 3 | from typing import Dict, Any, Optional 4 | from uuid import UUID 5 | from fastapi import HTTPException 6 | from app.config import settings 7 | import json 8 | 9 | 10 | class UUIDEncoder(json.JSONEncoder): 11 | def default(self, obj): 12 | if isinstance(obj, UUID): 13 | return str(obj) 14 | if isinstance(obj, datetime): 15 | return obj.isoformat() 16 | return json.JSONEncoder.default(self, obj) 17 | 18 | 19 | class WidgetService: 20 | def __init__(self): 21 | self.base_url = settings.WIDGET_SERVICE_URL 22 | self.headers = { 23 | "X-API-Key": settings.WIDGET_SERVICE_TOKEN, 24 | "Content-Type": "application/json", 25 | } 26 | 27 | def create_widget(self, widget_data: Dict[str, Any]) -> Dict[str, Any]: 28 | json_data = json.dumps(widget_data, cls=UUIDEncoder) 29 | response = requests.post( 30 | f"{self.base_url}/widgets", data=json_data, headers=self.headers 31 | ) 32 | return self._handle_response(response) 33 | 34 | def update_widget( 35 | self, widget_id: UUID, widget_data: Dict[str, Any] 36 | ) -> Dict[str, Any]: 37 | json_data = json.dumps(widget_data, cls=UUIDEncoder) 38 | response = requests.put( 39 | f"{self.base_url}/widgets/{widget_id}", data=json_data, headers=self.headers 40 | ) 41 | return self._handle_response(response) 42 | 43 | def delete_widget(self, widget_id: UUID) -> Dict[str, Any]: 44 | response = requests.delete(f"{self.base_url}/widgets/{widget_id}") 45 | return self._handle_response(response) 46 | 47 | def get_widget_interactions( 48 | self, widget_id: UUID, client_reference_id: Optional[str] = None 49 | ) -> Dict[str, Any]: 50 | response = requests.get( 51 | f"{self.base_url}/widgets/{widget_id}/interactions", 52 | params={"client_reference_id": client_reference_id}, 53 | headers=self.headers, 54 | ) 55 | return self._handle_response(response) 56 | 57 | def _handle_response(self, response: requests.Response) -> Dict[str, Any]: 58 | if response.status_code == 200: 59 | return response.json() 60 | elif response.status_code == 404: 61 | raise HTTPException(status_code=404, detail="Widget not found") 62 | else: 63 | raise HTTPException( 64 | status_code=response.status_code, 65 | detail=f"Widget service error: {response.text}", 66 | ) 67 | 68 | 69 | widget_service = WidgetService() 70 | -------------------------------------------------------------------------------- /app/utils.py: -------------------------------------------------------------------------------- 1 | from bs4 import BeautifulSoup 2 | from app.config import settings 3 | import openai 4 | 5 | client = openai.OpenAI( 6 | base_url=settings.OPENAI_API_URL, api_key=settings.OPENAI_API_KEY 7 | ) 8 | 9 | 10 | def add_default_js_to_html(html_string: str) -> str: 11 | """ 12 | Adds the default JS URL as a script tag in the HTML head. 13 | 14 | Args: 15 | html_string (str): The original HTML string. 16 | 17 | Returns: 18 | str: The modified HTML string with the default JS script tag added. 19 | """ 20 | soup = BeautifulSoup(html_string, "html.parser") 21 | 22 | script_tag = soup.new_tag("script", src=settings.DEFAULT_JS_URL) 23 | 24 | if not soup.head: 25 | head = soup.new_tag("head") 26 | soup.html.insert(0, head) 27 | 28 | soup.head.append(script_tag) 29 | 30 | return str(soup) 31 | 32 | 33 | def is_html_safe(html_content: str) -> tuple[bool, str]: 34 | """ 35 | Checks if the given HTML content is safe using GPT-4o-mini. 36 | 37 | This function sends the HTML content to the GPT-4o-mini model to analyze for potential security risks such as: 38 | - Sensitive content (e.g., personal information, explicit content) 39 | - Malicious script tags 40 | - Iframe injections 41 | - Potentially harmful attributes (e.g., onload, onerror) 42 | 43 | Args: 44 | html_content (str): The HTML content to be checked. 45 | 46 | Returns: 47 | tuple[bool, str]: A tuple containing a boolean indicating if the HTML is safe, 48 | and a string description of why it's not safe (empty if safe). 49 | """ 50 | prompt = f""" 51 | Analyze the following HTML content for safety, considering it may contain Jinja templates. Consider the following aspects: 52 | 1. Presence of sensitive content (e.g., personal information, explicit content) 53 | 2. Potentially malicious script tags (excluding those that are part of Jinja templates) 54 | 3. Iframe injections (excluding those that are part of Jinja templates) 55 | 4. Harmful attributes (e.g., onload, onerror) (excluding those that are part of Jinja templates) 56 | 5. Any other security concerns 57 | 58 | Note: Jinja template syntax should not be marked as unsafe. 59 | 60 | If the HTML is safe, respond with 'Yes'. 61 | If it's not safe, respond with 'No' followed by a brief description of the security concerns. 62 | 63 | HTML Content: 64 | {html_content} 65 | """ 66 | 67 | try: 68 | response = client.chat.completions.create( 69 | model="gpt-4o-mini", 70 | messages=[ 71 | {"role": "system", "content": "You are a security expert analyzing HTML content."}, 72 | {"role": "user", "content": prompt}, 73 | ], 74 | max_tokens=100, 75 | ) 76 | result = response.choices[0].message.content.strip() 77 | if result.lower().startswith("yes"): 78 | return True, "" 79 | else: 80 | return False, result[3:].strip() # Remove 'No ' prefix and trim 81 | except Exception as e: 82 | print(f"Error in LLM analysis: {str(e)}") 83 | return False, "Error occurred during analysis" # Default to unsafe if there's an error 84 | -------------------------------------------------------------------------------- /assets/ict-pic1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shahriarshm/ict9-interactor-backend/cbd85b7db21271dc2e42a5fb49ce21de6d157773/assets/ict-pic1.png -------------------------------------------------------------------------------- /assets/ict-pic2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shahriarshm/ict9-interactor-backend/cbd85b7db21271dc2e42a5fb49ce21de6d157773/assets/ict-pic2.png -------------------------------------------------------------------------------- /assets/interactor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shahriarshm/ict9-interactor-backend/cbd85b7db21271dc2e42a5fb49ce21de6d157773/assets/interactor.png -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | web: 5 | build: . 6 | ports: 7 | - "8000:8000" 8 | depends_on: 9 | - db 10 | environment: 11 | - DATABASE_URL=postgresql://user:password@db:5432/mydatabase 12 | 13 | db: 14 | image: postgres:13 15 | ports: 16 | - "5432:5432" 17 | volumes: 18 | - postgres_data:/var/lib/postgresql/data 19 | environment: 20 | - POSTGRES_USER=user 21 | - POSTGRES_PASSWORD=password 22 | - POSTGRES_DB=mydatabase 23 | 24 | volumes: 25 | postgres_data: -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Project Overview 2 | 3 | This project is a widget management system. It provides APIs to create, read, update, and delete widgets. The project is structured into several modules, each responsible for different aspects of the application. 4 | 5 | ## Modules 6 | 7 | - **API**: Handles the HTTP requests and responses. 8 | - **Models**: Defines the data structures. 9 | - **Services**: Contains the business logic. 10 | - **CRUD**: Handles the database operations. 11 | - **Config**: Manages the configuration settings. 12 | 13 | ## Directory Structure 14 | 15 | - `app/api/`: Contains the API endpoints. 16 | - `app/models/`: Contains the data models. 17 | - `app/services/`: Contains the service layer. 18 | - `app/crud/`: Contains the CRUD operations. 19 | - `app/config.py`: Contains the configuration settings. 20 | 21 | ## Documentation 22 | 23 | - [API Documentation](api.md) 24 | - [Models Documentation](models.md) 25 | - [Services Documentation](services.md) 26 | - [Configuration Documentation](config.md) -------------------------------------------------------------------------------- /docs/api.md: -------------------------------------------------------------------------------- 1 | # API Documentation 2 | 3 | ## Endpoints 4 | 5 | ### Widget Templates 6 | 7 | - **GET /widget-templates**: Retrieve a list of widget templates. 8 | - **POST /widget-templates**: Create a new widget template. 9 | 10 | ### Widgets 11 | 12 | - **GET /widgets**: Retrieve a list of widgets. 13 | - **POST /widgets**: Create a new widget. 14 | - **GET /widgets/{id}**: Retrieve a widget by ID. 15 | - **PUT /widgets/{id}**: Update a widget by ID. 16 | - **DELETE /widgets/{id}**: Delete a widget by ID. 17 | 18 | ## Files 19 | 20 | - `app/api/widget_templates.py`: Handles widget template endpoints. 21 | - `app/api/widgets.py`: Handles widget endpoints. -------------------------------------------------------------------------------- /docs/config.md: -------------------------------------------------------------------------------- 1 | # Configuration Documentation 2 | 3 | ## Configuration Settings 4 | 5 | Manages the configuration settings for the application. 6 | 7 | ### Fields 8 | 9 | - **DATABASE_URL**: URL for the database connection. 10 | - **SECRET_KEY**: Secret key for the application. 11 | - **DEBUG**: Debug mode flag. 12 | 13 | ## Files 14 | 15 | - `app/config.py`: Contains the configuration settings. -------------------------------------------------------------------------------- /docs/ict-challenge-en.md: -------------------------------------------------------------------------------- 1 | # Modern Advertising Project 2 | 3 | Snapp, Tapsi, Digikala, Fidibo, Taaghche, Cinema Ticket, Iran Concert, and the late Bamilo and Reyhoon, along with many others, have been offering creative games and events on their websites for various occasions for years, aimed at attracting, retaining, and engaging users. The wheel of fortune is one of the simplest examples that most websites display during Black Friday, Yalda Night, founding anniversary, Nowruz, and other occasions to present their discounts in a more attractive manner. 4 | 5 | Years ago, book reading campaigns for Zoodfood were one of the effective methods in line with the brand's social responsibility to promote reading culture and attract new audiences. The "Chand Khat" (A Few Lines) campaign, in addition to promoting reading culture and providing engaging content, offered discount codes from other companies to its customers in an integrated and coordinated manner. 6 | 7 | ![pic1](../assets/ict-pic1.png) 8 | 9 | Surely you have seen treasure hunt games, World Cup and Olympic prediction campaigns, and many other ideas on various websites and services over the years. 10 | 11 | ![pic2](../assets/ict-pic2.png) 12 | 13 | These creative games and events not only increase user engagement but also help increase sales and attract new customers. Due to their attractiveness and entertainment value, these types of campaigns encourage users to visit websites more often, spend more time on them, and make purchases. 14 | 15 | Ultimately, these types of campaigns help businesses stand out in today's competitive world and establish closer relationships with their customers. Given the rapid changes in customer preferences and needs, taking initiative in designing creative games and events can be the key to success in the market. 16 | 17 | But why doesn't everyone implement such campaigns? Perhaps time-consuming creativity, production costs, having the right team, etc., can be counted as reasons for this. 18 | 19 | We intend to suggest that you implement a comprehensive system that allows different companies to display games, events, and campaigns provided by you as widgets on their websites. 20 | 21 | We expect you to design the interactive advertising system described in this competition with the following quality: 22 | 23 | 1. Ability to provide discount codes, either as pre-generated codes or generated on-the-fly 24 | 2. Ability to provide the user's game score to the host for future services 25 | 3. Widget for displaying campaigns, games, etc. 26 | 4. Providing an exclusive profile of each user to the host, as a web service 27 | 5. Ability to provide a list of ready-made campaigns or create and offer custom games and campaigns by third parties (programmers and companies) 28 | 6. Use of your services by the host website user without the need to log in 29 | 7. Providing a leaderboard if needed by the host or appropriate to the campaign 30 | 8. Each campaign can include unique information that is important for the business to collect and process. For example, if a book reading campaign is offered, statistics on the number of lines read, average response time, and the number of book titles read, etc., should be made available to the host. 31 | 9. Ability to provide discount codes from other companies from a list of public codes (some companies will provide discount codes for other companies to use) 32 | 10. Providing a simple admin panel for the host (if you don't have enough time, provide web services and disregard the appearance) 33 | 11. Providing real-time and comprehensive statistics and information to the host, such as the number of visits, number of people present in the campaign, etc. 34 | 12. Creating a scalable system (you should be able to provide services to a large number of hosts and their users) 35 | 13. Adherence to security principles 36 | 14. Providing relevant and appropriate documentation, along with the solution architecture 37 | 38 | Please note that additional explanations will be provided in the competition hall, followed by a Q&A session for a better understanding of the product. 39 | 40 | The judges will be present alongside your team as business owners; you can ask them about any ambiguous issues. -------------------------------------------------------------------------------- /docs/ict-challenge-fa.md: -------------------------------------------------------------------------------- 1 | # پروژه تبلیغات نوین 2 | 3 | اسنپ، تپسی، دیجی کالا، فیدیبو، طاقچه، سینما تیکت، ایران کنسرت و خدابیامرزان بامیلوو ریحون و بسیاری دیگر، سالهاست که به مناسبت‌های مختلف، در وبسایت خود بازی‌ها و رویدادهای خلاقانه‌ در جهت جذب، ماندگاری و تعامل با کاربر ارائه می‌دهند. گردونه شانس یکی از ساده‌ترین این موارد است که اکثر وبسایت‌ها در بلک فرایدی، شب یلدا، سالگرد تاسیس، نوروز و ... در وبسایت خود نمایش می‌دهند تا تخفیف‌های خود را به شکلی جذاب‌تر ارائه کنند. 4 | 5 | سال‌ها پیش کمپین‌های کتابخوانی برای زود فود یکی از روش‌های موثر در راستای مسئولیت اجتماعی برند در ترویج فرهنگ مطالعه و جذب مخاطبان جدید بود. کمپین «چند خط» علاوه بر ترویج فرهنگ کتابخوانی و نیز ارائه مطالب جذاب، به صورت یکپارچه و هماهنگ با سایر شرکت‌ها، کدهای تخفیف آنها را نیز به مشتریان خود ارائه می‌داد. 6 | 7 | ![pic1](../assets/ict-pic1.png) 8 | 9 | مطمئنا بازی‌های گنج، کمپین‌های پیش‌بینی جام جهانی و المپیک و کلی ایده دیگر را در این سالها در وبسای‎تها و سرویس‌های مختلف دیده‌اید. 10 | 11 | ![pic2](../assets/ict-pic2.png) 12 | 13 | این بازی‌ها و رویدادهای خلاقانه‌ نه تنها باعث افزایش تعامل کاربران می‌شوند، بلکه به افزایش فروش و جذب مشتریان جدید نیز کمک می‌کنند. این نوع کمپین‌ها به دلیل جذابیت و سرگرم‌کنندگی، کاربران را ترغیب می‌کنند تا بیشتر به وبسایتها سر بزنند، وقت بیشتری در آن بگذرانند و به خرید بپردازند. 14 | 15 | در نهایت، این نوع کمپین‌ها به کسب و کارها کمک می‌کند تا در دنیای رقابتی امروز متمایز شوند و ارتباط نزدیک‌تری با مشتریان خود برقرار کنند. با توجه به تغییرات سریع در سلیقه و نیازهای مشتریان، ابتکار عمل در طراحی بازی‌ها و رویدادهای خلاقانه می‌تواند کلید موفقیت در بازار باشد. 16 | 17 | اما چرا همه چنین کمپین هایی اجرا نمیکنند؟ شاید زمانبر بودن خلاقیت، هزینه های تولید، داشتن تیم مناسب و ... را به توان دلایل این امر برشمرد. 18 | 19 | در نظر داریم به شما پیشنهاد کنیم سامانه جامعی را پیاده کنید که شرکت های مختلف بتوانند بازی ها، رویدادها و کمپین های ارائه شده توسط شما را به صورت ویجت هایی در وبسایت خود نمایش دهند. 20 | 21 | 22 | از شما انتظار داریم در این مسابقه نسبت به طراحی سامانه تبلیغات تعاملی بیان شده، به کیفیت زیر اقدام کنید. 23 | 24 | 1. امکان ارائه کدهای تخفیف، به صورت کدهای تولید شده از قبل یا تولید آنی 25 | 2. امکان ارائه امتیاز بازی کاربر به میزبان، جهت ارائه  خدمات آتی 26 | 3. ویجت نمایش کمپین، بازی و ... 27 | 4. ارائه پروفایل اختصاصی هر کاربر به میزبان، به صورت وب سرویس 28 | 5. امکان ارائه لیست کمپین های آماده و یا ساخت و ارائه بازی ها و کمپین های اختصاصی توسط طرف سوم (برنامه نویسان و شرکت¬ها) 29 | 6. استفاده از خدمات شما توسط کاربر وبسایت میزبان بدون نیاز به لاگین 30 | 7. ارائه لیدربورد در صورت نیاز میزبان یا متناسب با کمپین 31 | 8. هر کمپین میتواند شامل اطلاعات منحصر به خود باشد که جمع آوری و پردازش آنها برای کسب و کار مهم است. برای مثال اگر کمپین کتابخوانی ارائه شده است، آمار تعداد سطور مطالعه شده، مدت زمان میانگین پاسخگویی و تعداد عنوان کتاب مطالعه شده و ... میبایست در اختیار میزبان قرار بگیرد.  32 | 9. امکان ارائه کدهای تخفیف سایر شرکت ها از لیست کدهای عمومی (برخی از شرکت ها کدهای تخفیفی ارائه خواهند داد، تا سایر شرکت ها بتوانند از آنها استفاده کنند) 33 | 10. ارائه پنل ادمین ساده برای میزبان (اگر وقت کافی ندارید، وب سرویس هایی ارائه دهید و از ظاهر کار صرف نظر کنید.) 34 | 11. ارائه آمار و اطلاعات لحظه ای و جامع به میزبان مثل میزان بازدید، تعداد افراد حاضر در کمپین و ... 35 | 12. ایجاد سامانه مقیاس پذیر (شما باید بتوانید به تعداد زیادی میزبان و کاربران آنها خدمات ارائه کنید) 36 | 13. رعایت اصول امنیتی 37 | 14. ارائه داکیومنت های مرتبط و مناسب، به همراه معماری راه حل 38 | 39 | توجه داشته باشید، توضیحات تکمیلی در سالن مسابقه ارائه خواهد شد و پس از آن جلسه پرسش و پاسخ برای درک بهتر محصول برگزار خواهد شد. 40 | 41 | داورها به عنوان مالک کسب و کار در کنار تیم شما حضور خواهند داشت، میتوانید موارد مبهم را از آنها بپرسید. -------------------------------------------------------------------------------- /docs/models.md: -------------------------------------------------------------------------------- 1 | # Models Documentation 2 | 3 | ## Widget Model 4 | 5 | Defines the structure of a widget. 6 | 7 | ### Fields 8 | 9 | - **id**: Unique identifier for the widget. 10 | - **name**: Name of the widget. 11 | - **description**: Description of the widget. 12 | - **created_at**: Timestamp when the widget was created. 13 | - **updated_at**: Timestamp when the widget was last updated. 14 | 15 | ## Files 16 | 17 | - `app/models/widget.py`: Contains the Widget model. 18 | - `app/models/base.py`: Base model for all database models. -------------------------------------------------------------------------------- /docs/services.md: -------------------------------------------------------------------------------- 1 | # Services Documentation 2 | 3 | ## Widget Service 4 | 5 | Contains the business logic for managing widgets. 6 | 7 | ### Methods 8 | 9 | - **create_widget**: Creates a new widget. 10 | - **get_widget**: Retrieves a widget by ID. 11 | - **update_widget**: Updates a widget by ID. 12 | - **delete_widget**: Deletes a widget by ID. 13 | 14 | ## Files 15 | 16 | - `app/services/widget_service.py`: Contains the widget service logic. -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "alembic" 5 | version = "1.13.2" 6 | description = "A database migration tool for SQLAlchemy." 7 | optional = false 8 | python-versions = ">=3.8" 9 | files = [ 10 | {file = "alembic-1.13.2-py3-none-any.whl", hash = "sha256:6b8733129a6224a9a711e17c99b08462dbf7cc9670ba8f2e2ae9af860ceb1953"}, 11 | {file = "alembic-1.13.2.tar.gz", hash = "sha256:1ff0ae32975f4fd96028c39ed9bb3c867fe3af956bd7bb37343b54c9fe7445ef"}, 12 | ] 13 | 14 | [package.dependencies] 15 | Mako = "*" 16 | SQLAlchemy = ">=1.3.0" 17 | typing-extensions = ">=4" 18 | 19 | [package.extras] 20 | tz = ["backports.zoneinfo"] 21 | 22 | [[package]] 23 | name = "annotated-types" 24 | version = "0.7.0" 25 | description = "Reusable constraint types to use with typing.Annotated" 26 | optional = false 27 | python-versions = ">=3.8" 28 | files = [ 29 | {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, 30 | {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, 31 | ] 32 | 33 | [[package]] 34 | name = "anyio" 35 | version = "4.4.0" 36 | description = "High level compatibility layer for multiple asynchronous event loop implementations" 37 | optional = false 38 | python-versions = ">=3.8" 39 | files = [ 40 | {file = "anyio-4.4.0-py3-none-any.whl", hash = "sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7"}, 41 | {file = "anyio-4.4.0.tar.gz", hash = "sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94"}, 42 | ] 43 | 44 | [package.dependencies] 45 | idna = ">=2.8" 46 | sniffio = ">=1.1" 47 | 48 | [package.extras] 49 | doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] 50 | test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] 51 | trio = ["trio (>=0.23)"] 52 | 53 | [[package]] 54 | name = "bcrypt" 55 | version = "4.2.0" 56 | description = "Modern password hashing for your software and your servers" 57 | optional = false 58 | python-versions = ">=3.7" 59 | files = [ 60 | {file = "bcrypt-4.2.0-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:096a15d26ed6ce37a14c1ac1e48119660f21b24cba457f160a4b830f3fe6b5cb"}, 61 | {file = "bcrypt-4.2.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c02d944ca89d9b1922ceb8a46460dd17df1ba37ab66feac4870f6862a1533c00"}, 62 | {file = "bcrypt-4.2.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1d84cf6d877918620b687b8fd1bf7781d11e8a0998f576c7aa939776b512b98d"}, 63 | {file = "bcrypt-4.2.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:1bb429fedbe0249465cdd85a58e8376f31bb315e484f16e68ca4c786dcc04291"}, 64 | {file = "bcrypt-4.2.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:655ea221910bcac76ea08aaa76df427ef8625f92e55a8ee44fbf7753dbabb328"}, 65 | {file = "bcrypt-4.2.0-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:1ee38e858bf5d0287c39b7a1fc59eec64bbf880c7d504d3a06a96c16e14058e7"}, 66 | {file = "bcrypt-4.2.0-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:0da52759f7f30e83f1e30a888d9163a81353ef224d82dc58eb5bb52efcabc399"}, 67 | {file = "bcrypt-4.2.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3698393a1b1f1fd5714524193849d0c6d524d33523acca37cd28f02899285060"}, 68 | {file = "bcrypt-4.2.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:762a2c5fb35f89606a9fde5e51392dad0cd1ab7ae64149a8b935fe8d79dd5ed7"}, 69 | {file = "bcrypt-4.2.0-cp37-abi3-win32.whl", hash = "sha256:5a1e8aa9b28ae28020a3ac4b053117fb51c57a010b9f969603ed885f23841458"}, 70 | {file = "bcrypt-4.2.0-cp37-abi3-win_amd64.whl", hash = "sha256:8f6ede91359e5df88d1f5c1ef47428a4420136f3ce97763e31b86dd8280fbdf5"}, 71 | {file = "bcrypt-4.2.0-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:c52aac18ea1f4a4f65963ea4f9530c306b56ccd0c6f8c8da0c06976e34a6e841"}, 72 | {file = "bcrypt-4.2.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3bbbfb2734f0e4f37c5136130405332640a1e46e6b23e000eeff2ba8d005da68"}, 73 | {file = "bcrypt-4.2.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3413bd60460f76097ee2e0a493ccebe4a7601918219c02f503984f0a7ee0aebe"}, 74 | {file = "bcrypt-4.2.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:8d7bb9c42801035e61c109c345a28ed7e84426ae4865511eb82e913df18f58c2"}, 75 | {file = "bcrypt-4.2.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3d3a6d28cb2305b43feac298774b997e372e56c7c7afd90a12b3dc49b189151c"}, 76 | {file = "bcrypt-4.2.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:9c1c4ad86351339c5f320ca372dfba6cb6beb25e8efc659bedd918d921956bae"}, 77 | {file = "bcrypt-4.2.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:27fe0f57bb5573104b5a6de5e4153c60814c711b29364c10a75a54bb6d7ff48d"}, 78 | {file = "bcrypt-4.2.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:8ac68872c82f1add6a20bd489870c71b00ebacd2e9134a8aa3f98a0052ab4b0e"}, 79 | {file = "bcrypt-4.2.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:cb2a8ec2bc07d3553ccebf0746bbf3d19426d1c6d1adbd4fa48925f66af7b9e8"}, 80 | {file = "bcrypt-4.2.0-cp39-abi3-win32.whl", hash = "sha256:77800b7147c9dc905db1cba26abe31e504d8247ac73580b4aa179f98e6608f34"}, 81 | {file = "bcrypt-4.2.0-cp39-abi3-win_amd64.whl", hash = "sha256:61ed14326ee023917ecd093ee6ef422a72f3aec6f07e21ea5f10622b735538a9"}, 82 | {file = "bcrypt-4.2.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:39e1d30c7233cfc54f5c3f2c825156fe044efdd3e0b9d309512cc514a263ec2a"}, 83 | {file = "bcrypt-4.2.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f4f4acf526fcd1c34e7ce851147deedd4e26e6402369304220250598b26448db"}, 84 | {file = "bcrypt-4.2.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:1ff39b78a52cf03fdf902635e4c81e544714861ba3f0efc56558979dd4f09170"}, 85 | {file = "bcrypt-4.2.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:373db9abe198e8e2c70d12b479464e0d5092cc122b20ec504097b5f2297ed184"}, 86 | {file = "bcrypt-4.2.0.tar.gz", hash = "sha256:cf69eaf5185fd58f268f805b505ce31f9b9fc2d64b376642164e9244540c1221"}, 87 | ] 88 | 89 | [package.extras] 90 | tests = ["pytest (>=3.2.1,!=3.3.0)"] 91 | typecheck = ["mypy"] 92 | 93 | [[package]] 94 | name = "beautifulsoup4" 95 | version = "4.12.3" 96 | description = "Screen-scraping library" 97 | optional = false 98 | python-versions = ">=3.6.0" 99 | files = [ 100 | {file = "beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed"}, 101 | {file = "beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051"}, 102 | ] 103 | 104 | [package.dependencies] 105 | soupsieve = ">1.2" 106 | 107 | [package.extras] 108 | cchardet = ["cchardet"] 109 | chardet = ["chardet"] 110 | charset-normalizer = ["charset-normalizer"] 111 | html5lib = ["html5lib"] 112 | lxml = ["lxml"] 113 | 114 | [[package]] 115 | name = "black" 116 | version = "24.8.0" 117 | description = "The uncompromising code formatter." 118 | optional = false 119 | python-versions = ">=3.8" 120 | files = [ 121 | {file = "black-24.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:09cdeb74d494ec023ded657f7092ba518e8cf78fa8386155e4a03fdcc44679e6"}, 122 | {file = "black-24.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:81c6742da39f33b08e791da38410f32e27d632260e599df7245cccee2064afeb"}, 123 | {file = "black-24.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:707a1ca89221bc8a1a64fb5e15ef39cd755633daa672a9db7498d1c19de66a42"}, 124 | {file = "black-24.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:d6417535d99c37cee4091a2f24eb2b6d5ec42b144d50f1f2e436d9fe1916fe1a"}, 125 | {file = "black-24.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fb6e2c0b86bbd43dee042e48059c9ad7830abd5c94b0bc518c0eeec57c3eddc1"}, 126 | {file = "black-24.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:837fd281f1908d0076844bc2b801ad2d369c78c45cf800cad7b61686051041af"}, 127 | {file = "black-24.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62e8730977f0b77998029da7971fa896ceefa2c4c4933fcd593fa599ecbf97a4"}, 128 | {file = "black-24.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:72901b4913cbac8972ad911dc4098d5753704d1f3c56e44ae8dce99eecb0e3af"}, 129 | {file = "black-24.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7c046c1d1eeb7aea9335da62472481d3bbf3fd986e093cffd35f4385c94ae368"}, 130 | {file = "black-24.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:649f6d84ccbae73ab767e206772cc2d7a393a001070a4c814a546afd0d423aed"}, 131 | {file = "black-24.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b59b250fdba5f9a9cd9d0ece6e6d993d91ce877d121d161e4698af3eb9c1018"}, 132 | {file = "black-24.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:6e55d30d44bed36593c3163b9bc63bf58b3b30e4611e4d88a0c3c239930ed5b2"}, 133 | {file = "black-24.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:505289f17ceda596658ae81b61ebbe2d9b25aa78067035184ed0a9d855d18afd"}, 134 | {file = "black-24.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b19c9ad992c7883ad84c9b22aaa73562a16b819c1d8db7a1a1a49fb7ec13c7d2"}, 135 | {file = "black-24.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f13f7f386f86f8121d76599114bb8c17b69d962137fc70efe56137727c7047e"}, 136 | {file = "black-24.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:f490dbd59680d809ca31efdae20e634f3fae27fba3ce0ba3208333b713bc3920"}, 137 | {file = "black-24.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:eab4dd44ce80dea27dc69db40dab62d4ca96112f87996bca68cd75639aeb2e4c"}, 138 | {file = "black-24.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3c4285573d4897a7610054af5a890bde7c65cb466040c5f0c8b732812d7f0e5e"}, 139 | {file = "black-24.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e84e33b37be070ba135176c123ae52a51f82306def9f7d063ee302ecab2cf47"}, 140 | {file = "black-24.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:73bbf84ed136e45d451a260c6b73ed674652f90a2b3211d6a35e78054563a9bb"}, 141 | {file = "black-24.8.0-py3-none-any.whl", hash = "sha256:972085c618ee94f402da1af548a4f218c754ea7e5dc70acb168bfaca4c2542ed"}, 142 | {file = "black-24.8.0.tar.gz", hash = "sha256:2500945420b6784c38b9ee885af039f5e7471ef284ab03fa35ecdde4688cd83f"}, 143 | ] 144 | 145 | [package.dependencies] 146 | click = ">=8.0.0" 147 | mypy-extensions = ">=0.4.3" 148 | packaging = ">=22.0" 149 | pathspec = ">=0.9.0" 150 | platformdirs = ">=2" 151 | 152 | [package.extras] 153 | colorama = ["colorama (>=0.4.3)"] 154 | d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] 155 | jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] 156 | uvloop = ["uvloop (>=0.15.2)"] 157 | 158 | [[package]] 159 | name = "bs4" 160 | version = "0.0.2" 161 | description = "Dummy package for Beautiful Soup (beautifulsoup4)" 162 | optional = false 163 | python-versions = "*" 164 | files = [ 165 | {file = "bs4-0.0.2-py2.py3-none-any.whl", hash = "sha256:abf8742c0805ef7f662dce4b51cca104cffe52b835238afc169142ab9b3fbccc"}, 166 | {file = "bs4-0.0.2.tar.gz", hash = "sha256:a48685c58f50fe127722417bae83fe6badf500d54b55f7e39ffe43b798653925"}, 167 | ] 168 | 169 | [package.dependencies] 170 | beautifulsoup4 = "*" 171 | 172 | [[package]] 173 | name = "certifi" 174 | version = "2024.8.30" 175 | description = "Python package for providing Mozilla's CA Bundle." 176 | optional = false 177 | python-versions = ">=3.6" 178 | files = [ 179 | {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, 180 | {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, 181 | ] 182 | 183 | [[package]] 184 | name = "cffi" 185 | version = "1.17.1" 186 | description = "Foreign Function Interface for Python calling C code." 187 | optional = false 188 | python-versions = ">=3.8" 189 | files = [ 190 | {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, 191 | {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, 192 | {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"}, 193 | {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"}, 194 | {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"}, 195 | {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"}, 196 | {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"}, 197 | {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"}, 198 | {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"}, 199 | {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"}, 200 | {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"}, 201 | {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"}, 202 | {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"}, 203 | {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"}, 204 | {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"}, 205 | {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"}, 206 | {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"}, 207 | {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"}, 208 | {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"}, 209 | {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"}, 210 | {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"}, 211 | {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"}, 212 | {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"}, 213 | {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"}, 214 | {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"}, 215 | {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"}, 216 | {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"}, 217 | {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"}, 218 | {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"}, 219 | {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"}, 220 | {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"}, 221 | {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"}, 222 | {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"}, 223 | {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"}, 224 | {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"}, 225 | {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"}, 226 | {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"}, 227 | {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"}, 228 | {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"}, 229 | {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"}, 230 | {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"}, 231 | {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"}, 232 | {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"}, 233 | {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"}, 234 | {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"}, 235 | {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"}, 236 | {file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"}, 237 | {file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"}, 238 | {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"}, 239 | {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"}, 240 | {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"}, 241 | {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"}, 242 | {file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"}, 243 | {file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"}, 244 | {file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"}, 245 | {file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"}, 246 | {file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"}, 247 | {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"}, 248 | {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"}, 249 | {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"}, 250 | {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"}, 251 | {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"}, 252 | {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"}, 253 | {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"}, 254 | {file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"}, 255 | {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, 256 | {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, 257 | ] 258 | 259 | [package.dependencies] 260 | pycparser = "*" 261 | 262 | [[package]] 263 | name = "charset-normalizer" 264 | version = "3.3.2" 265 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 266 | optional = false 267 | python-versions = ">=3.7.0" 268 | files = [ 269 | {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, 270 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, 271 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, 272 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, 273 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, 274 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, 275 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, 276 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, 277 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, 278 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, 279 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, 280 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, 281 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, 282 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, 283 | {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, 284 | {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, 285 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, 286 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, 287 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, 288 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, 289 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, 290 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, 291 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, 292 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, 293 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, 294 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, 295 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, 296 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, 297 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, 298 | {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, 299 | {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, 300 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, 301 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, 302 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, 303 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, 304 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, 305 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, 306 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, 307 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, 308 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, 309 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, 310 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, 311 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, 312 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, 313 | {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, 314 | {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, 315 | {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, 316 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, 317 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, 318 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, 319 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, 320 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, 321 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, 322 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, 323 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, 324 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, 325 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, 326 | {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, 327 | {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, 328 | {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, 329 | {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, 330 | {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, 331 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, 332 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, 333 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, 334 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, 335 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, 336 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, 337 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, 338 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, 339 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, 340 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, 341 | {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, 342 | {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, 343 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, 344 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, 345 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, 346 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, 347 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, 348 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, 349 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, 350 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, 351 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, 352 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, 353 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, 354 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, 355 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, 356 | {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, 357 | {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, 358 | {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, 359 | ] 360 | 361 | [[package]] 362 | name = "click" 363 | version = "8.1.7" 364 | description = "Composable command line interface toolkit" 365 | optional = false 366 | python-versions = ">=3.7" 367 | files = [ 368 | {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, 369 | {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, 370 | ] 371 | 372 | [package.dependencies] 373 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 374 | 375 | [[package]] 376 | name = "colorama" 377 | version = "0.4.6" 378 | description = "Cross-platform colored terminal text." 379 | optional = false 380 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 381 | files = [ 382 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 383 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 384 | ] 385 | 386 | [[package]] 387 | name = "cryptography" 388 | version = "43.0.1" 389 | description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." 390 | optional = false 391 | python-versions = ">=3.7" 392 | files = [ 393 | {file = "cryptography-43.0.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:8385d98f6a3bf8bb2d65a73e17ed87a3ba84f6991c155691c51112075f9ffc5d"}, 394 | {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27e613d7077ac613e399270253259d9d53872aaf657471473ebfc9a52935c062"}, 395 | {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68aaecc4178e90719e95298515979814bda0cbada1256a4485414860bd7ab962"}, 396 | {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:de41fd81a41e53267cb020bb3a7212861da53a7d39f863585d13ea11049cf277"}, 397 | {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f98bf604c82c416bc829e490c700ca1553eafdf2912a91e23a79d97d9801372a"}, 398 | {file = "cryptography-43.0.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:61ec41068b7b74268fa86e3e9e12b9f0c21fcf65434571dbb13d954bceb08042"}, 399 | {file = "cryptography-43.0.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:014f58110f53237ace6a408b5beb6c427b64e084eb451ef25a28308270086494"}, 400 | {file = "cryptography-43.0.1-cp37-abi3-win32.whl", hash = "sha256:2bd51274dcd59f09dd952afb696bf9c61a7a49dfc764c04dd33ef7a6b502a1e2"}, 401 | {file = "cryptography-43.0.1-cp37-abi3-win_amd64.whl", hash = "sha256:666ae11966643886c2987b3b721899d250855718d6d9ce41b521252a17985f4d"}, 402 | {file = "cryptography-43.0.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:ac119bb76b9faa00f48128b7f5679e1d8d437365c5d26f1c2c3f0da4ce1b553d"}, 403 | {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bbcce1a551e262dfbafb6e6252f1ae36a248e615ca44ba302df077a846a8806"}, 404 | {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58d4e9129985185a06d849aa6df265bdd5a74ca6e1b736a77959b498e0505b85"}, 405 | {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d03a475165f3134f773d1388aeb19c2d25ba88b6a9733c5c590b9ff7bbfa2e0c"}, 406 | {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:511f4273808ab590912a93ddb4e3914dfd8a388fed883361b02dea3791f292e1"}, 407 | {file = "cryptography-43.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:80eda8b3e173f0f247f711eef62be51b599b5d425c429b5d4ca6a05e9e856baa"}, 408 | {file = "cryptography-43.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38926c50cff6f533f8a2dae3d7f19541432610d114a70808f0926d5aaa7121e4"}, 409 | {file = "cryptography-43.0.1-cp39-abi3-win32.whl", hash = "sha256:a575913fb06e05e6b4b814d7f7468c2c660e8bb16d8d5a1faf9b33ccc569dd47"}, 410 | {file = "cryptography-43.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:d75601ad10b059ec832e78823b348bfa1a59f6b8d545db3a24fd44362a1564cb"}, 411 | {file = "cryptography-43.0.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ea25acb556320250756e53f9e20a4177515f012c9eaea17eb7587a8c4d8ae034"}, 412 | {file = "cryptography-43.0.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c1332724be35d23a854994ff0b66530119500b6053d0bd3363265f7e5e77288d"}, 413 | {file = "cryptography-43.0.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fba1007b3ef89946dbbb515aeeb41e30203b004f0b4b00e5e16078b518563289"}, 414 | {file = "cryptography-43.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5b43d1ea6b378b54a1dc99dd8a2b5be47658fe9a7ce0a58ff0b55f4b43ef2b84"}, 415 | {file = "cryptography-43.0.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:88cce104c36870d70c49c7c8fd22885875d950d9ee6ab54df2745f83ba0dc365"}, 416 | {file = "cryptography-43.0.1-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:9d3cdb25fa98afdd3d0892d132b8d7139e2c087da1712041f6b762e4f807cc96"}, 417 | {file = "cryptography-43.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e710bf40870f4db63c3d7d929aa9e09e4e7ee219e703f949ec4073b4294f6172"}, 418 | {file = "cryptography-43.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7c05650fe8023c5ed0d46793d4b7d7e6cd9c04e68eabe5b0aeea836e37bdcec2"}, 419 | {file = "cryptography-43.0.1.tar.gz", hash = "sha256:203e92a75716d8cfb491dc47c79e17d0d9207ccffcbcb35f598fbe463ae3444d"}, 420 | ] 421 | 422 | [package.dependencies] 423 | cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} 424 | 425 | [package.extras] 426 | docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] 427 | docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"] 428 | nox = ["nox"] 429 | pep8test = ["check-sdist", "click", "mypy", "ruff"] 430 | sdist = ["build"] 431 | ssh = ["bcrypt (>=3.1.5)"] 432 | test = ["certifi", "cryptography-vectors (==43.0.1)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] 433 | test-randomorder = ["pytest-randomly"] 434 | 435 | [[package]] 436 | name = "distro" 437 | version = "1.9.0" 438 | description = "Distro - an OS platform information API" 439 | optional = false 440 | python-versions = ">=3.6" 441 | files = [ 442 | {file = "distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2"}, 443 | {file = "distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed"}, 444 | ] 445 | 446 | [[package]] 447 | name = "dnspython" 448 | version = "2.6.1" 449 | description = "DNS toolkit" 450 | optional = false 451 | python-versions = ">=3.8" 452 | files = [ 453 | {file = "dnspython-2.6.1-py3-none-any.whl", hash = "sha256:5ef3b9680161f6fa89daf8ad451b5f1a33b18ae8a1c6778cdf4b43f08c0a6e50"}, 454 | {file = "dnspython-2.6.1.tar.gz", hash = "sha256:e8f0f9c23a7b7cb99ded64e6c3a6f3e701d78f50c55e002b839dea7225cff7cc"}, 455 | ] 456 | 457 | [package.extras] 458 | dev = ["black (>=23.1.0)", "coverage (>=7.0)", "flake8 (>=7)", "mypy (>=1.8)", "pylint (>=3)", "pytest (>=7.4)", "pytest-cov (>=4.1.0)", "sphinx (>=7.2.0)", "twine (>=4.0.0)", "wheel (>=0.42.0)"] 459 | dnssec = ["cryptography (>=41)"] 460 | doh = ["h2 (>=4.1.0)", "httpcore (>=1.0.0)", "httpx (>=0.26.0)"] 461 | doq = ["aioquic (>=0.9.25)"] 462 | idna = ["idna (>=3.6)"] 463 | trio = ["trio (>=0.23)"] 464 | wmi = ["wmi (>=1.5.1)"] 465 | 466 | [[package]] 467 | name = "ecdsa" 468 | version = "0.19.0" 469 | description = "ECDSA cryptographic signature library (pure python)" 470 | optional = false 471 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.6" 472 | files = [ 473 | {file = "ecdsa-0.19.0-py2.py3-none-any.whl", hash = "sha256:2cea9b88407fdac7bbeca0833b189e4c9c53f2ef1e1eaa29f6224dbc809b707a"}, 474 | {file = "ecdsa-0.19.0.tar.gz", hash = "sha256:60eaad1199659900dd0af521ed462b793bbdf867432b3948e87416ae4caf6bf8"}, 475 | ] 476 | 477 | [package.dependencies] 478 | six = ">=1.9.0" 479 | 480 | [package.extras] 481 | gmpy = ["gmpy"] 482 | gmpy2 = ["gmpy2"] 483 | 484 | [[package]] 485 | name = "email-validator" 486 | version = "2.2.0" 487 | description = "A robust email address syntax and deliverability validation library." 488 | optional = false 489 | python-versions = ">=3.8" 490 | files = [ 491 | {file = "email_validator-2.2.0-py3-none-any.whl", hash = "sha256:561977c2d73ce3611850a06fa56b414621e0c8faa9d66f2611407d87465da631"}, 492 | {file = "email_validator-2.2.0.tar.gz", hash = "sha256:cb690f344c617a714f22e66ae771445a1ceb46821152df8e165c5f9a364582b7"}, 493 | ] 494 | 495 | [package.dependencies] 496 | dnspython = ">=2.0.0" 497 | idna = ">=2.0.0" 498 | 499 | [[package]] 500 | name = "fastapi" 501 | version = "0.115.0" 502 | description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" 503 | optional = false 504 | python-versions = ">=3.8" 505 | files = [ 506 | {file = "fastapi-0.115.0-py3-none-any.whl", hash = "sha256:17ea427674467486e997206a5ab25760f6b09e069f099b96f5b55a32fb6f1631"}, 507 | {file = "fastapi-0.115.0.tar.gz", hash = "sha256:f93b4ca3529a8ebc6fc3fcf710e5efa8de3df9b41570958abf1d97d843138004"}, 508 | ] 509 | 510 | [package.dependencies] 511 | pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" 512 | starlette = ">=0.37.2,<0.39.0" 513 | typing-extensions = ">=4.8.0" 514 | 515 | [package.extras] 516 | all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.7)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] 517 | standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "jinja2 (>=2.11.2)", "python-multipart (>=0.0.7)", "uvicorn[standard] (>=0.12.0)"] 518 | 519 | [[package]] 520 | name = "flake8" 521 | version = "7.1.1" 522 | description = "the modular source code checker: pep8 pyflakes and co" 523 | optional = false 524 | python-versions = ">=3.8.1" 525 | files = [ 526 | {file = "flake8-7.1.1-py2.py3-none-any.whl", hash = "sha256:597477df7860daa5aa0fdd84bf5208a043ab96b8e96ab708770ae0364dd03213"}, 527 | {file = "flake8-7.1.1.tar.gz", hash = "sha256:049d058491e228e03e67b390f311bbf88fce2dbaa8fa673e7aea87b7198b8d38"}, 528 | ] 529 | 530 | [package.dependencies] 531 | mccabe = ">=0.7.0,<0.8.0" 532 | pycodestyle = ">=2.12.0,<2.13.0" 533 | pyflakes = ">=3.2.0,<3.3.0" 534 | 535 | [[package]] 536 | name = "greenlet" 537 | version = "3.1.0" 538 | description = "Lightweight in-process concurrent programming" 539 | optional = false 540 | python-versions = ">=3.7" 541 | files = [ 542 | {file = "greenlet-3.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a814dc3100e8a046ff48faeaa909e80cdb358411a3d6dd5293158425c684eda8"}, 543 | {file = "greenlet-3.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a771dc64fa44ebe58d65768d869fcfb9060169d203446c1d446e844b62bdfdca"}, 544 | {file = "greenlet-3.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0e49a65d25d7350cca2da15aac31b6f67a43d867448babf997fe83c7505f57bc"}, 545 | {file = "greenlet-3.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2cd8518eade968bc52262d8c46727cfc0826ff4d552cf0430b8d65aaf50bb91d"}, 546 | {file = "greenlet-3.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76dc19e660baea5c38e949455c1181bc018893f25372d10ffe24b3ed7341fb25"}, 547 | {file = "greenlet-3.1.0-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c0a5b1c22c82831f56f2f7ad9bbe4948879762fe0d59833a4a71f16e5fa0f682"}, 548 | {file = "greenlet-3.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2651dfb006f391bcb240635079a68a261b227a10a08af6349cba834a2141efa1"}, 549 | {file = "greenlet-3.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3e7e6ef1737a819819b1163116ad4b48d06cfdd40352d813bb14436024fcda99"}, 550 | {file = "greenlet-3.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:ffb08f2a1e59d38c7b8b9ac8083c9c8b9875f0955b1e9b9b9a965607a51f8e54"}, 551 | {file = "greenlet-3.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9730929375021ec90f6447bff4f7f5508faef1c02f399a1953870cdb78e0c345"}, 552 | {file = "greenlet-3.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:713d450cf8e61854de9420fb7eea8ad228df4e27e7d4ed465de98c955d2b3fa6"}, 553 | {file = "greenlet-3.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c3446937be153718250fe421da548f973124189f18fe4575a0510b5c928f0cc"}, 554 | {file = "greenlet-3.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ddc7bcedeb47187be74208bc652d63d6b20cb24f4e596bd356092d8000da6d6"}, 555 | {file = "greenlet-3.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44151d7b81b9391ed759a2f2865bbe623ef00d648fed59363be2bbbd5154656f"}, 556 | {file = "greenlet-3.1.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6cea1cca3be76c9483282dc7760ea1cc08a6ecec1f0b6ca0a94ea0d17432da19"}, 557 | {file = "greenlet-3.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:619935a44f414274a2c08c9e74611965650b730eb4efe4b2270f91df5e4adf9a"}, 558 | {file = "greenlet-3.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:221169d31cada333a0c7fd087b957c8f431c1dba202c3a58cf5a3583ed973e9b"}, 559 | {file = "greenlet-3.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:01059afb9b178606b4b6e92c3e710ea1635597c3537e44da69f4531e111dd5e9"}, 560 | {file = "greenlet-3.1.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:24fc216ec7c8be9becba8b64a98a78f9cd057fd2dc75ae952ca94ed8a893bf27"}, 561 | {file = "greenlet-3.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d07c28b85b350564bdff9f51c1c5007dfb2f389385d1bc23288de51134ca303"}, 562 | {file = "greenlet-3.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:243a223c96a4246f8a30ea470c440fe9db1f5e444941ee3c3cd79df119b8eebf"}, 563 | {file = "greenlet-3.1.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26811df4dc81271033a7836bc20d12cd30938e6bd2e9437f56fa03da81b0f8fc"}, 564 | {file = "greenlet-3.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9d86401550b09a55410f32ceb5fe7efcd998bd2dad9e82521713cb148a4a15f"}, 565 | {file = "greenlet-3.1.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:26d9c1c4f1748ccac0bae1dbb465fb1a795a75aba8af8ca871503019f4285e2a"}, 566 | {file = "greenlet-3.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:cd468ec62257bb4544989402b19d795d2305eccb06cde5da0eb739b63dc04665"}, 567 | {file = "greenlet-3.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a53dfe8f82b715319e9953330fa5c8708b610d48b5c59f1316337302af5c0811"}, 568 | {file = "greenlet-3.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:28fe80a3eb673b2d5cc3b12eea468a5e5f4603c26aa34d88bf61bba82ceb2f9b"}, 569 | {file = "greenlet-3.1.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:76b3e3976d2a452cba7aa9e453498ac72240d43030fdc6d538a72b87eaff52fd"}, 570 | {file = "greenlet-3.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:655b21ffd37a96b1e78cc48bf254f5ea4b5b85efaf9e9e2a526b3c9309d660ca"}, 571 | {file = "greenlet-3.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c6f4c2027689093775fd58ca2388d58789009116844432d920e9147f91acbe64"}, 572 | {file = "greenlet-3.1.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:76e5064fd8e94c3f74d9fd69b02d99e3cdb8fc286ed49a1f10b256e59d0d3a0b"}, 573 | {file = "greenlet-3.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a4bf607f690f7987ab3291406e012cd8591a4f77aa54f29b890f9c331e84989"}, 574 | {file = "greenlet-3.1.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:037d9ac99540ace9424cb9ea89f0accfaff4316f149520b4ae293eebc5bded17"}, 575 | {file = "greenlet-3.1.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:90b5bbf05fe3d3ef697103850c2ce3374558f6fe40fd57c9fac1bf14903f50a5"}, 576 | {file = "greenlet-3.1.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:726377bd60081172685c0ff46afbc600d064f01053190e4450857483c4d44484"}, 577 | {file = "greenlet-3.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:d46d5069e2eeda111d6f71970e341f4bd9aeeee92074e649ae263b834286ecc0"}, 578 | {file = "greenlet-3.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81eeec4403a7d7684b5812a8aaa626fa23b7d0848edb3a28d2eb3220daddcbd0"}, 579 | {file = "greenlet-3.1.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4a3dae7492d16e85ea6045fd11cb8e782b63eac8c8d520c3a92c02ac4573b0a6"}, 580 | {file = "greenlet-3.1.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b5ea3664eed571779403858d7cd0a9b0ebf50d57d2cdeafc7748e09ef8cd81a"}, 581 | {file = "greenlet-3.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a22f4e26400f7f48faef2d69c20dc055a1f3043d330923f9abe08ea0aecc44df"}, 582 | {file = "greenlet-3.1.0-cp37-cp37m-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13ff8c8e54a10472ce3b2a2da007f915175192f18e6495bad50486e87c7f6637"}, 583 | {file = "greenlet-3.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:f9671e7282d8c6fcabc32c0fb8d7c0ea8894ae85cee89c9aadc2d7129e1a9954"}, 584 | {file = "greenlet-3.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:184258372ae9e1e9bddce6f187967f2e08ecd16906557c4320e3ba88a93438c3"}, 585 | {file = "greenlet-3.1.0-cp37-cp37m-win32.whl", hash = "sha256:a0409bc18a9f85321399c29baf93545152d74a49d92f2f55302f122007cfda00"}, 586 | {file = "greenlet-3.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:9eb4a1d7399b9f3c7ac68ae6baa6be5f9195d1d08c9ddc45ad559aa6b556bce6"}, 587 | {file = "greenlet-3.1.0-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:a8870983af660798dc1b529e1fd6f1cefd94e45135a32e58bd70edd694540f33"}, 588 | {file = "greenlet-3.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cfcfb73aed40f550a57ea904629bdaf2e562c68fa1164fa4588e752af6efdc3f"}, 589 | {file = "greenlet-3.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f9482c2ed414781c0af0b35d9d575226da6b728bd1a720668fa05837184965b7"}, 590 | {file = "greenlet-3.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d58ec349e0c2c0bc6669bf2cd4982d2f93bf067860d23a0ea1fe677b0f0b1e09"}, 591 | {file = "greenlet-3.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd65695a8df1233309b701dec2539cc4b11e97d4fcc0f4185b4a12ce54db0491"}, 592 | {file = "greenlet-3.1.0-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:665b21e95bc0fce5cab03b2e1d90ba9c66c510f1bb5fdc864f3a377d0f553f6b"}, 593 | {file = "greenlet-3.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d3c59a06c2c28a81a026ff11fbf012081ea34fb9b7052f2ed0366e14896f0a1d"}, 594 | {file = "greenlet-3.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5415b9494ff6240b09af06b91a375731febe0090218e2898d2b85f9b92abcda0"}, 595 | {file = "greenlet-3.1.0-cp38-cp38-win32.whl", hash = "sha256:1544b8dd090b494c55e60c4ff46e238be44fdc472d2589e943c241e0169bcea2"}, 596 | {file = "greenlet-3.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:7f346d24d74c00b6730440f5eb8ec3fe5774ca8d1c9574e8e57c8671bb51b910"}, 597 | {file = "greenlet-3.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:db1b3ccb93488328c74e97ff888604a8b95ae4f35f4f56677ca57a4fc3a4220b"}, 598 | {file = "greenlet-3.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:44cd313629ded43bb3b98737bba2f3e2c2c8679b55ea29ed73daea6b755fe8e7"}, 599 | {file = "greenlet-3.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fad7a051e07f64e297e6e8399b4d6a3bdcad3d7297409e9a06ef8cbccff4f501"}, 600 | {file = "greenlet-3.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3967dcc1cd2ea61b08b0b276659242cbce5caca39e7cbc02408222fb9e6ff39"}, 601 | {file = "greenlet-3.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d45b75b0f3fd8d99f62eb7908cfa6d727b7ed190737dec7fe46d993da550b81a"}, 602 | {file = "greenlet-3.1.0-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2d004db911ed7b6218ec5c5bfe4cf70ae8aa2223dffbb5b3c69e342bb253cb28"}, 603 | {file = "greenlet-3.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b9505a0c8579899057cbefd4ec34d865ab99852baf1ff33a9481eb3924e2da0b"}, 604 | {file = "greenlet-3.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fd6e94593f6f9714dbad1aaba734b5ec04593374fa6638df61592055868f8b8"}, 605 | {file = "greenlet-3.1.0-cp39-cp39-win32.whl", hash = "sha256:d0dd943282231480aad5f50f89bdf26690c995e8ff555f26d8a5b9887b559bcc"}, 606 | {file = "greenlet-3.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:ac0adfdb3a21dc2a24ed728b61e72440d297d0fd3a577389df566651fcd08f97"}, 607 | {file = "greenlet-3.1.0.tar.gz", hash = "sha256:b395121e9bbe8d02a750886f108d540abe66075e61e22f7353d9acb0b81be0f0"}, 608 | ] 609 | 610 | [package.extras] 611 | docs = ["Sphinx", "furo"] 612 | test = ["objgraph", "psutil"] 613 | 614 | [[package]] 615 | name = "h11" 616 | version = "0.14.0" 617 | description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" 618 | optional = false 619 | python-versions = ">=3.7" 620 | files = [ 621 | {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, 622 | {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, 623 | ] 624 | 625 | [[package]] 626 | name = "httpcore" 627 | version = "1.0.5" 628 | description = "A minimal low-level HTTP client." 629 | optional = false 630 | python-versions = ">=3.8" 631 | files = [ 632 | {file = "httpcore-1.0.5-py3-none-any.whl", hash = "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5"}, 633 | {file = "httpcore-1.0.5.tar.gz", hash = "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61"}, 634 | ] 635 | 636 | [package.dependencies] 637 | certifi = "*" 638 | h11 = ">=0.13,<0.15" 639 | 640 | [package.extras] 641 | asyncio = ["anyio (>=4.0,<5.0)"] 642 | http2 = ["h2 (>=3,<5)"] 643 | socks = ["socksio (==1.*)"] 644 | trio = ["trio (>=0.22.0,<0.26.0)"] 645 | 646 | [[package]] 647 | name = "httpx" 648 | version = "0.27.2" 649 | description = "The next generation HTTP client." 650 | optional = false 651 | python-versions = ">=3.8" 652 | files = [ 653 | {file = "httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0"}, 654 | {file = "httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2"}, 655 | ] 656 | 657 | [package.dependencies] 658 | anyio = "*" 659 | certifi = "*" 660 | httpcore = "==1.*" 661 | idna = "*" 662 | sniffio = "*" 663 | 664 | [package.extras] 665 | brotli = ["brotli", "brotlicffi"] 666 | cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] 667 | http2 = ["h2 (>=3,<5)"] 668 | socks = ["socksio (==1.*)"] 669 | zstd = ["zstandard (>=0.18.0)"] 670 | 671 | [[package]] 672 | name = "idna" 673 | version = "3.10" 674 | description = "Internationalized Domain Names in Applications (IDNA)" 675 | optional = false 676 | python-versions = ">=3.6" 677 | files = [ 678 | {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, 679 | {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, 680 | ] 681 | 682 | [package.extras] 683 | all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] 684 | 685 | [[package]] 686 | name = "iniconfig" 687 | version = "2.0.0" 688 | description = "brain-dead simple config-ini parsing" 689 | optional = false 690 | python-versions = ">=3.7" 691 | files = [ 692 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, 693 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, 694 | ] 695 | 696 | [[package]] 697 | name = "jiter" 698 | version = "0.5.0" 699 | description = "Fast iterable JSON parser." 700 | optional = false 701 | python-versions = ">=3.8" 702 | files = [ 703 | {file = "jiter-0.5.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:b599f4e89b3def9a94091e6ee52e1d7ad7bc33e238ebb9c4c63f211d74822c3f"}, 704 | {file = "jiter-0.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a063f71c4b06225543dddadbe09d203dc0c95ba352d8b85f1221173480a71d5"}, 705 | {file = "jiter-0.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:acc0d5b8b3dd12e91dd184b87273f864b363dfabc90ef29a1092d269f18c7e28"}, 706 | {file = "jiter-0.5.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c22541f0b672f4d741382a97c65609332a783501551445ab2df137ada01e019e"}, 707 | {file = "jiter-0.5.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:63314832e302cc10d8dfbda0333a384bf4bcfce80d65fe99b0f3c0da8945a91a"}, 708 | {file = "jiter-0.5.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a25fbd8a5a58061e433d6fae6d5298777c0814a8bcefa1e5ecfff20c594bd749"}, 709 | {file = "jiter-0.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:503b2c27d87dfff5ab717a8200fbbcf4714516c9d85558048b1fc14d2de7d8dc"}, 710 | {file = "jiter-0.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6d1f3d27cce923713933a844872d213d244e09b53ec99b7a7fdf73d543529d6d"}, 711 | {file = "jiter-0.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:c95980207b3998f2c3b3098f357994d3fd7661121f30669ca7cb945f09510a87"}, 712 | {file = "jiter-0.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:afa66939d834b0ce063f57d9895e8036ffc41c4bd90e4a99631e5f261d9b518e"}, 713 | {file = "jiter-0.5.0-cp310-none-win32.whl", hash = "sha256:f16ca8f10e62f25fd81d5310e852df6649af17824146ca74647a018424ddeccf"}, 714 | {file = "jiter-0.5.0-cp310-none-win_amd64.whl", hash = "sha256:b2950e4798e82dd9176935ef6a55cf6a448b5c71515a556da3f6b811a7844f1e"}, 715 | {file = "jiter-0.5.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d4c8e1ed0ef31ad29cae5ea16b9e41529eb50a7fba70600008e9f8de6376d553"}, 716 | {file = "jiter-0.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c6f16e21276074a12d8421692515b3fd6d2ea9c94fd0734c39a12960a20e85f3"}, 717 | {file = "jiter-0.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5280e68e7740c8c128d3ae5ab63335ce6d1fb6603d3b809637b11713487af9e6"}, 718 | {file = "jiter-0.5.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:583c57fc30cc1fec360e66323aadd7fc3edeec01289bfafc35d3b9dcb29495e4"}, 719 | {file = "jiter-0.5.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:26351cc14507bdf466b5f99aba3df3143a59da75799bf64a53a3ad3155ecded9"}, 720 | {file = "jiter-0.5.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4829df14d656b3fb87e50ae8b48253a8851c707da9f30d45aacab2aa2ba2d614"}, 721 | {file = "jiter-0.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a42a4bdcf7307b86cb863b2fb9bb55029b422d8f86276a50487982d99eed7c6e"}, 722 | {file = "jiter-0.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04d461ad0aebf696f8da13c99bc1b3e06f66ecf6cfd56254cc402f6385231c06"}, 723 | {file = "jiter-0.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e6375923c5f19888c9226582a124b77b622f8fd0018b843c45eeb19d9701c403"}, 724 | {file = "jiter-0.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:2cec323a853c24fd0472517113768c92ae0be8f8c384ef4441d3632da8baa646"}, 725 | {file = "jiter-0.5.0-cp311-none-win32.whl", hash = "sha256:aa1db0967130b5cab63dfe4d6ff547c88b2a394c3410db64744d491df7f069bb"}, 726 | {file = "jiter-0.5.0-cp311-none-win_amd64.whl", hash = "sha256:aa9d2b85b2ed7dc7697597dcfaac66e63c1b3028652f751c81c65a9f220899ae"}, 727 | {file = "jiter-0.5.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9f664e7351604f91dcdd557603c57fc0d551bc65cc0a732fdacbf73ad335049a"}, 728 | {file = "jiter-0.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:044f2f1148b5248ad2c8c3afb43430dccf676c5a5834d2f5089a4e6c5bbd64df"}, 729 | {file = "jiter-0.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:702e3520384c88b6e270c55c772d4bd6d7b150608dcc94dea87ceba1b6391248"}, 730 | {file = "jiter-0.5.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:528d742dcde73fad9d63e8242c036ab4a84389a56e04efd854062b660f559544"}, 731 | {file = "jiter-0.5.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8cf80e5fe6ab582c82f0c3331df27a7e1565e2dcf06265afd5173d809cdbf9ba"}, 732 | {file = "jiter-0.5.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:44dfc9ddfb9b51a5626568ef4e55ada462b7328996294fe4d36de02fce42721f"}, 733 | {file = "jiter-0.5.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c451f7922992751a936b96c5f5b9bb9312243d9b754c34b33d0cb72c84669f4e"}, 734 | {file = "jiter-0.5.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:308fce789a2f093dca1ff91ac391f11a9f99c35369117ad5a5c6c4903e1b3e3a"}, 735 | {file = "jiter-0.5.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7f5ad4a7c6b0d90776fdefa294f662e8a86871e601309643de30bf94bb93a64e"}, 736 | {file = "jiter-0.5.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ea189db75f8eca08807d02ae27929e890c7d47599ce3d0a6a5d41f2419ecf338"}, 737 | {file = "jiter-0.5.0-cp312-none-win32.whl", hash = "sha256:e3bbe3910c724b877846186c25fe3c802e105a2c1fc2b57d6688b9f8772026e4"}, 738 | {file = "jiter-0.5.0-cp312-none-win_amd64.whl", hash = "sha256:a586832f70c3f1481732919215f36d41c59ca080fa27a65cf23d9490e75b2ef5"}, 739 | {file = "jiter-0.5.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:f04bc2fc50dc77be9d10f73fcc4e39346402ffe21726ff41028f36e179b587e6"}, 740 | {file = "jiter-0.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6f433a4169ad22fcb550b11179bb2b4fd405de9b982601914ef448390b2954f3"}, 741 | {file = "jiter-0.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad4a6398c85d3a20067e6c69890ca01f68659da94d74c800298581724e426c7e"}, 742 | {file = "jiter-0.5.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6baa88334e7af3f4d7a5c66c3a63808e5efbc3698a1c57626541ddd22f8e4fbf"}, 743 | {file = "jiter-0.5.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ece0a115c05efca597c6d938f88c9357c843f8c245dbbb53361a1c01afd7148"}, 744 | {file = "jiter-0.5.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:335942557162ad372cc367ffaf93217117401bf930483b4b3ebdb1223dbddfa7"}, 745 | {file = "jiter-0.5.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:649b0ee97a6e6da174bffcb3c8c051a5935d7d4f2f52ea1583b5b3e7822fbf14"}, 746 | {file = "jiter-0.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f4be354c5de82157886ca7f5925dbda369b77344b4b4adf2723079715f823989"}, 747 | {file = "jiter-0.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5206144578831a6de278a38896864ded4ed96af66e1e63ec5dd7f4a1fce38a3a"}, 748 | {file = "jiter-0.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8120c60f8121ac3d6f072b97ef0e71770cc72b3c23084c72c4189428b1b1d3b6"}, 749 | {file = "jiter-0.5.0-cp38-none-win32.whl", hash = "sha256:6f1223f88b6d76b519cb033a4d3687ca157c272ec5d6015c322fc5b3074d8a5e"}, 750 | {file = "jiter-0.5.0-cp38-none-win_amd64.whl", hash = "sha256:c59614b225d9f434ea8fc0d0bec51ef5fa8c83679afedc0433905994fb36d631"}, 751 | {file = "jiter-0.5.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:0af3838cfb7e6afee3f00dc66fa24695199e20ba87df26e942820345b0afc566"}, 752 | {file = "jiter-0.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:550b11d669600dbc342364fd4adbe987f14d0bbedaf06feb1b983383dcc4b961"}, 753 | {file = "jiter-0.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:489875bf1a0ffb3cb38a727b01e6673f0f2e395b2aad3c9387f94187cb214bbf"}, 754 | {file = "jiter-0.5.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b250ca2594f5599ca82ba7e68785a669b352156260c5362ea1b4e04a0f3e2389"}, 755 | {file = "jiter-0.5.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8ea18e01f785c6667ca15407cd6dabbe029d77474d53595a189bdc813347218e"}, 756 | {file = "jiter-0.5.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:462a52be85b53cd9bffd94e2d788a09984274fe6cebb893d6287e1c296d50653"}, 757 | {file = "jiter-0.5.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:92cc68b48d50fa472c79c93965e19bd48f40f207cb557a8346daa020d6ba973b"}, 758 | {file = "jiter-0.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1c834133e59a8521bc87ebcad773608c6fa6ab5c7a022df24a45030826cf10bc"}, 759 | {file = "jiter-0.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab3a71ff31cf2d45cb216dc37af522d335211f3a972d2fe14ea99073de6cb104"}, 760 | {file = "jiter-0.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:cccd3af9c48ac500c95e1bcbc498020c87e1781ff0345dd371462d67b76643eb"}, 761 | {file = "jiter-0.5.0-cp39-none-win32.whl", hash = "sha256:368084d8d5c4fc40ff7c3cc513c4f73e02c85f6009217922d0823a48ee7adf61"}, 762 | {file = "jiter-0.5.0-cp39-none-win_amd64.whl", hash = "sha256:ce03f7b4129eb72f1687fa11300fbf677b02990618428934662406d2a76742a1"}, 763 | {file = "jiter-0.5.0.tar.gz", hash = "sha256:1d916ba875bcab5c5f7d927df998c4cb694d27dceddf3392e58beaf10563368a"}, 764 | ] 765 | 766 | [[package]] 767 | name = "mako" 768 | version = "1.3.5" 769 | description = "A super-fast templating language that borrows the best ideas from the existing templating languages." 770 | optional = false 771 | python-versions = ">=3.8" 772 | files = [ 773 | {file = "Mako-1.3.5-py3-none-any.whl", hash = "sha256:260f1dbc3a519453a9c856dedfe4beb4e50bd5a26d96386cb6c80856556bb91a"}, 774 | {file = "Mako-1.3.5.tar.gz", hash = "sha256:48dbc20568c1d276a2698b36d968fa76161bf127194907ea6fc594fa81f943bc"}, 775 | ] 776 | 777 | [package.dependencies] 778 | MarkupSafe = ">=0.9.2" 779 | 780 | [package.extras] 781 | babel = ["Babel"] 782 | lingua = ["lingua"] 783 | testing = ["pytest"] 784 | 785 | [[package]] 786 | name = "markupsafe" 787 | version = "2.1.5" 788 | description = "Safely add untrusted strings to HTML/XML markup." 789 | optional = false 790 | python-versions = ">=3.7" 791 | files = [ 792 | {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, 793 | {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, 794 | {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, 795 | {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, 796 | {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, 797 | {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, 798 | {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, 799 | {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, 800 | {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, 801 | {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, 802 | {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, 803 | {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, 804 | {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, 805 | {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, 806 | {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, 807 | {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, 808 | {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, 809 | {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, 810 | {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, 811 | {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, 812 | {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, 813 | {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, 814 | {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, 815 | {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, 816 | {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, 817 | {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, 818 | {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, 819 | {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, 820 | {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, 821 | {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, 822 | {file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"}, 823 | {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"}, 824 | {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"}, 825 | {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"}, 826 | {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"}, 827 | {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"}, 828 | {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"}, 829 | {file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"}, 830 | {file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"}, 831 | {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"}, 832 | {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"}, 833 | {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"}, 834 | {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"}, 835 | {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"}, 836 | {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"}, 837 | {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"}, 838 | {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"}, 839 | {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"}, 840 | {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"}, 841 | {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"}, 842 | {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"}, 843 | {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"}, 844 | {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"}, 845 | {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"}, 846 | {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"}, 847 | {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"}, 848 | {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"}, 849 | {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"}, 850 | {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"}, 851 | {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, 852 | ] 853 | 854 | [[package]] 855 | name = "mccabe" 856 | version = "0.7.0" 857 | description = "McCabe checker, plugin for flake8" 858 | optional = false 859 | python-versions = ">=3.6" 860 | files = [ 861 | {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, 862 | {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, 863 | ] 864 | 865 | [[package]] 866 | name = "mypy-extensions" 867 | version = "1.0.0" 868 | description = "Type system extensions for programs checked with the mypy type checker." 869 | optional = false 870 | python-versions = ">=3.5" 871 | files = [ 872 | {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, 873 | {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, 874 | ] 875 | 876 | [[package]] 877 | name = "openai" 878 | version = "1.46.0" 879 | description = "The official Python library for the openai API" 880 | optional = false 881 | python-versions = ">=3.7.1" 882 | files = [ 883 | {file = "openai-1.46.0-py3-none-any.whl", hash = "sha256:8e423690b121d0268c7bb83b552e14f339b0ba250e1d0f70d145c194e79c4e1b"}, 884 | {file = "openai-1.46.0.tar.gz", hash = "sha256:0c5a783530d7cd90e2370dbd52d9239d2d53dc7a0badf9ee1e2e23d3f148969b"}, 885 | ] 886 | 887 | [package.dependencies] 888 | anyio = ">=3.5.0,<5" 889 | distro = ">=1.7.0,<2" 890 | httpx = ">=0.23.0,<1" 891 | jiter = ">=0.4.0,<1" 892 | pydantic = ">=1.9.0,<3" 893 | sniffio = "*" 894 | tqdm = ">4" 895 | typing-extensions = ">=4.11,<5" 896 | 897 | [package.extras] 898 | datalib = ["numpy (>=1)", "pandas (>=1.2.3)", "pandas-stubs (>=1.1.0.11)"] 899 | 900 | [[package]] 901 | name = "packaging" 902 | version = "24.1" 903 | description = "Core utilities for Python packages" 904 | optional = false 905 | python-versions = ">=3.8" 906 | files = [ 907 | {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, 908 | {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, 909 | ] 910 | 911 | [[package]] 912 | name = "passlib" 913 | version = "1.7.4" 914 | description = "comprehensive password hashing framework supporting over 30 schemes" 915 | optional = false 916 | python-versions = "*" 917 | files = [ 918 | {file = "passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1"}, 919 | {file = "passlib-1.7.4.tar.gz", hash = "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04"}, 920 | ] 921 | 922 | [package.extras] 923 | argon2 = ["argon2-cffi (>=18.2.0)"] 924 | bcrypt = ["bcrypt (>=3.1.0)"] 925 | build-docs = ["cloud-sptheme (>=1.10.1)", "sphinx (>=1.6)", "sphinxcontrib-fulltoc (>=1.2.0)"] 926 | totp = ["cryptography"] 927 | 928 | [[package]] 929 | name = "pathspec" 930 | version = "0.12.1" 931 | description = "Utility library for gitignore style pattern matching of file paths." 932 | optional = false 933 | python-versions = ">=3.8" 934 | files = [ 935 | {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, 936 | {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, 937 | ] 938 | 939 | [[package]] 940 | name = "platformdirs" 941 | version = "4.3.6" 942 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." 943 | optional = false 944 | python-versions = ">=3.8" 945 | files = [ 946 | {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, 947 | {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, 948 | ] 949 | 950 | [package.extras] 951 | docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] 952 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] 953 | type = ["mypy (>=1.11.2)"] 954 | 955 | [[package]] 956 | name = "pluggy" 957 | version = "1.5.0" 958 | description = "plugin and hook calling mechanisms for python" 959 | optional = false 960 | python-versions = ">=3.8" 961 | files = [ 962 | {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, 963 | {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, 964 | ] 965 | 966 | [package.extras] 967 | dev = ["pre-commit", "tox"] 968 | testing = ["pytest", "pytest-benchmark"] 969 | 970 | [[package]] 971 | name = "psycopg2-binary" 972 | version = "2.9.9" 973 | description = "psycopg2 - Python-PostgreSQL Database Adapter" 974 | optional = false 975 | python-versions = ">=3.7" 976 | files = [ 977 | {file = "psycopg2-binary-2.9.9.tar.gz", hash = "sha256:7f01846810177d829c7692f1f5ada8096762d9172af1b1a28d4ab5b77c923c1c"}, 978 | {file = "psycopg2_binary-2.9.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c2470da5418b76232f02a2fcd2229537bb2d5a7096674ce61859c3229f2eb202"}, 979 | {file = "psycopg2_binary-2.9.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c6af2a6d4b7ee9615cbb162b0738f6e1fd1f5c3eda7e5da17861eacf4c717ea7"}, 980 | {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:75723c3c0fbbf34350b46a3199eb50638ab22a0228f93fb472ef4d9becc2382b"}, 981 | {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83791a65b51ad6ee6cf0845634859d69a038ea9b03d7b26e703f94c7e93dbcf9"}, 982 | {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0ef4854e82c09e84cc63084a9e4ccd6d9b154f1dbdd283efb92ecd0b5e2b8c84"}, 983 | {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed1184ab8f113e8d660ce49a56390ca181f2981066acc27cf637d5c1e10ce46e"}, 984 | {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d2997c458c690ec2bc6b0b7ecbafd02b029b7b4283078d3b32a852a7ce3ddd98"}, 985 | {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:b58b4710c7f4161b5e9dcbe73bb7c62d65670a87df7bcce9e1faaad43e715245"}, 986 | {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:0c009475ee389757e6e34611d75f6e4f05f0cf5ebb76c6037508318e1a1e0d7e"}, 987 | {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8dbf6d1bc73f1d04ec1734bae3b4fb0ee3cb2a493d35ede9badbeb901fb40f6f"}, 988 | {file = "psycopg2_binary-2.9.9-cp310-cp310-win32.whl", hash = "sha256:3f78fd71c4f43a13d342be74ebbc0666fe1f555b8837eb113cb7416856c79682"}, 989 | {file = "psycopg2_binary-2.9.9-cp310-cp310-win_amd64.whl", hash = "sha256:876801744b0dee379e4e3c38b76fc89f88834bb15bf92ee07d94acd06ec890a0"}, 990 | {file = "psycopg2_binary-2.9.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ee825e70b1a209475622f7f7b776785bd68f34af6e7a46e2e42f27b659b5bc26"}, 991 | {file = "psycopg2_binary-2.9.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1ea665f8ce695bcc37a90ee52de7a7980be5161375d42a0b6c6abedbf0d81f0f"}, 992 | {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:143072318f793f53819048fdfe30c321890af0c3ec7cb1dfc9cc87aa88241de2"}, 993 | {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c332c8d69fb64979ebf76613c66b985414927a40f8defa16cf1bc028b7b0a7b0"}, 994 | {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7fc5a5acafb7d6ccca13bfa8c90f8c51f13d8fb87d95656d3950f0158d3ce53"}, 995 | {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:977646e05232579d2e7b9c59e21dbe5261f403a88417f6a6512e70d3f8a046be"}, 996 | {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b6356793b84728d9d50ead16ab43c187673831e9d4019013f1402c41b1db9b27"}, 997 | {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:bc7bb56d04601d443f24094e9e31ae6deec9ccb23581f75343feebaf30423359"}, 998 | {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:77853062a2c45be16fd6b8d6de2a99278ee1d985a7bd8b103e97e41c034006d2"}, 999 | {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:78151aa3ec21dccd5cdef6c74c3e73386dcdfaf19bced944169697d7ac7482fc"}, 1000 | {file = "psycopg2_binary-2.9.9-cp311-cp311-win32.whl", hash = "sha256:dc4926288b2a3e9fd7b50dc6a1909a13bbdadfc67d93f3374d984e56f885579d"}, 1001 | {file = "psycopg2_binary-2.9.9-cp311-cp311-win_amd64.whl", hash = "sha256:b76bedd166805480ab069612119ea636f5ab8f8771e640ae103e05a4aae3e417"}, 1002 | {file = "psycopg2_binary-2.9.9-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:8532fd6e6e2dc57bcb3bc90b079c60de896d2128c5d9d6f24a63875a95a088cf"}, 1003 | {file = "psycopg2_binary-2.9.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0605eaed3eb239e87df0d5e3c6489daae3f7388d455d0c0b4df899519c6a38d"}, 1004 | {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f8544b092a29a6ddd72f3556a9fcf249ec412e10ad28be6a0c0d948924f2212"}, 1005 | {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2d423c8d8a3c82d08fe8af900ad5b613ce3632a1249fd6a223941d0735fce493"}, 1006 | {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e5afae772c00980525f6d6ecf7cbca55676296b580c0e6abb407f15f3706996"}, 1007 | {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e6f98446430fdf41bd36d4faa6cb409f5140c1c2cf58ce0bbdaf16af7d3f119"}, 1008 | {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c77e3d1862452565875eb31bdb45ac62502feabbd53429fdc39a1cc341d681ba"}, 1009 | {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:cb16c65dcb648d0a43a2521f2f0a2300f40639f6f8c1ecbc662141e4e3e1ee07"}, 1010 | {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:911dda9c487075abd54e644ccdf5e5c16773470a6a5d3826fda76699410066fb"}, 1011 | {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:57fede879f08d23c85140a360c6a77709113efd1c993923c59fde17aa27599fe"}, 1012 | {file = "psycopg2_binary-2.9.9-cp312-cp312-win32.whl", hash = "sha256:64cf30263844fa208851ebb13b0732ce674d8ec6a0c86a4e160495d299ba3c93"}, 1013 | {file = "psycopg2_binary-2.9.9-cp312-cp312-win_amd64.whl", hash = "sha256:81ff62668af011f9a48787564ab7eded4e9fb17a4a6a74af5ffa6a457400d2ab"}, 1014 | {file = "psycopg2_binary-2.9.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2293b001e319ab0d869d660a704942c9e2cce19745262a8aba2115ef41a0a42a"}, 1015 | {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03ef7df18daf2c4c07e2695e8cfd5ee7f748a1d54d802330985a78d2a5a6dca9"}, 1016 | {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a602ea5aff39bb9fac6308e9c9d82b9a35c2bf288e184a816002c9fae930b77"}, 1017 | {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8359bf4791968c5a78c56103702000105501adb557f3cf772b2c207284273984"}, 1018 | {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:275ff571376626195ab95a746e6a04c7df8ea34638b99fc11160de91f2fef503"}, 1019 | {file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:f9b5571d33660d5009a8b3c25dc1db560206e2d2f89d3df1cb32d72c0d117d52"}, 1020 | {file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:420f9bbf47a02616e8554e825208cb947969451978dceb77f95ad09c37791dae"}, 1021 | {file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:4154ad09dac630a0f13f37b583eae260c6aa885d67dfbccb5b02c33f31a6d420"}, 1022 | {file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a148c5d507bb9b4f2030a2025c545fccb0e1ef317393eaba42e7eabd28eb6041"}, 1023 | {file = "psycopg2_binary-2.9.9-cp37-cp37m-win32.whl", hash = "sha256:68fc1f1ba168724771e38bee37d940d2865cb0f562380a1fb1ffb428b75cb692"}, 1024 | {file = "psycopg2_binary-2.9.9-cp37-cp37m-win_amd64.whl", hash = "sha256:281309265596e388ef483250db3640e5f414168c5a67e9c665cafce9492eda2f"}, 1025 | {file = "psycopg2_binary-2.9.9-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:60989127da422b74a04345096c10d416c2b41bd7bf2a380eb541059e4e999980"}, 1026 | {file = "psycopg2_binary-2.9.9-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:246b123cc54bb5361588acc54218c8c9fb73068bf227a4a531d8ed56fa3ca7d6"}, 1027 | {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34eccd14566f8fe14b2b95bb13b11572f7c7d5c36da61caf414d23b91fcc5d94"}, 1028 | {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18d0ef97766055fec15b5de2c06dd8e7654705ce3e5e5eed3b6651a1d2a9a152"}, 1029 | {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d3f82c171b4ccd83bbaf35aa05e44e690113bd4f3b7b6cc54d2219b132f3ae55"}, 1030 | {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ead20f7913a9c1e894aebe47cccf9dc834e1618b7aa96155d2091a626e59c972"}, 1031 | {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ca49a8119c6cbd77375ae303b0cfd8c11f011abbbd64601167ecca18a87e7cdd"}, 1032 | {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:323ba25b92454adb36fa425dc5cf6f8f19f78948cbad2e7bc6cdf7b0d7982e59"}, 1033 | {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:1236ed0952fbd919c100bc839eaa4a39ebc397ed1c08a97fc45fee2a595aa1b3"}, 1034 | {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:729177eaf0aefca0994ce4cffe96ad3c75e377c7b6f4efa59ebf003b6d398716"}, 1035 | {file = "psycopg2_binary-2.9.9-cp38-cp38-win32.whl", hash = "sha256:804d99b24ad523a1fe18cc707bf741670332f7c7412e9d49cb5eab67e886b9b5"}, 1036 | {file = "psycopg2_binary-2.9.9-cp38-cp38-win_amd64.whl", hash = "sha256:a6cdcc3ede532f4a4b96000b6362099591ab4a3e913d70bcbac2b56c872446f7"}, 1037 | {file = "psycopg2_binary-2.9.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:72dffbd8b4194858d0941062a9766f8297e8868e1dd07a7b36212aaa90f49472"}, 1038 | {file = "psycopg2_binary-2.9.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:30dcc86377618a4c8f3b72418df92e77be4254d8f89f14b8e8f57d6d43603c0f"}, 1039 | {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:31a34c508c003a4347d389a9e6fcc2307cc2150eb516462a7a17512130de109e"}, 1040 | {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:15208be1c50b99203fe88d15695f22a5bed95ab3f84354c494bcb1d08557df67"}, 1041 | {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1873aade94b74715be2246321c8650cabf5a0d098a95bab81145ffffa4c13876"}, 1042 | {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a58c98a7e9c021f357348867f537017057c2ed7f77337fd914d0bedb35dace7"}, 1043 | {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4686818798f9194d03c9129a4d9a702d9e113a89cb03bffe08c6cf799e053291"}, 1044 | {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ebdc36bea43063116f0486869652cb2ed7032dbc59fbcb4445c4862b5c1ecf7f"}, 1045 | {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:ca08decd2697fdea0aea364b370b1249d47336aec935f87b8bbfd7da5b2ee9c1"}, 1046 | {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ac05fb791acf5e1a3e39402641827780fe44d27e72567a000412c648a85ba860"}, 1047 | {file = "psycopg2_binary-2.9.9-cp39-cp39-win32.whl", hash = "sha256:9dba73be7305b399924709b91682299794887cbbd88e38226ed9f6712eabee90"}, 1048 | {file = "psycopg2_binary-2.9.9-cp39-cp39-win_amd64.whl", hash = "sha256:f7ae5d65ccfbebdfa761585228eb4d0df3a8b15cfb53bd953e713e09fbb12957"}, 1049 | ] 1050 | 1051 | [[package]] 1052 | name = "pyasn1" 1053 | version = "0.6.1" 1054 | description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)" 1055 | optional = false 1056 | python-versions = ">=3.8" 1057 | files = [ 1058 | {file = "pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629"}, 1059 | {file = "pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034"}, 1060 | ] 1061 | 1062 | [[package]] 1063 | name = "pycodestyle" 1064 | version = "2.12.1" 1065 | description = "Python style guide checker" 1066 | optional = false 1067 | python-versions = ">=3.8" 1068 | files = [ 1069 | {file = "pycodestyle-2.12.1-py2.py3-none-any.whl", hash = "sha256:46f0fb92069a7c28ab7bb558f05bfc0110dac69a0cd23c61ea0040283a9d78b3"}, 1070 | {file = "pycodestyle-2.12.1.tar.gz", hash = "sha256:6838eae08bbce4f6accd5d5572075c63626a15ee3e6f842df996bf62f6d73521"}, 1071 | ] 1072 | 1073 | [[package]] 1074 | name = "pycparser" 1075 | version = "2.22" 1076 | description = "C parser in Python" 1077 | optional = false 1078 | python-versions = ">=3.8" 1079 | files = [ 1080 | {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, 1081 | {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, 1082 | ] 1083 | 1084 | [[package]] 1085 | name = "pydantic" 1086 | version = "2.9.2" 1087 | description = "Data validation using Python type hints" 1088 | optional = false 1089 | python-versions = ">=3.8" 1090 | files = [ 1091 | {file = "pydantic-2.9.2-py3-none-any.whl", hash = "sha256:f048cec7b26778210e28a0459867920654d48e5e62db0958433636cde4254f12"}, 1092 | {file = "pydantic-2.9.2.tar.gz", hash = "sha256:d155cef71265d1e9807ed1c32b4c8deec042a44a50a4188b25ac67ecd81a9c0f"}, 1093 | ] 1094 | 1095 | [package.dependencies] 1096 | annotated-types = ">=0.6.0" 1097 | email-validator = {version = ">=2.0.0", optional = true, markers = "extra == \"email\""} 1098 | pydantic-core = "2.23.4" 1099 | typing-extensions = [ 1100 | {version = ">=4.12.2", markers = "python_version >= \"3.13\""}, 1101 | {version = ">=4.6.1", markers = "python_version < \"3.13\""}, 1102 | ] 1103 | 1104 | [package.extras] 1105 | email = ["email-validator (>=2.0.0)"] 1106 | timezone = ["tzdata"] 1107 | 1108 | [[package]] 1109 | name = "pydantic-core" 1110 | version = "2.23.4" 1111 | description = "Core functionality for Pydantic validation and serialization" 1112 | optional = false 1113 | python-versions = ">=3.8" 1114 | files = [ 1115 | {file = "pydantic_core-2.23.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:b10bd51f823d891193d4717448fab065733958bdb6a6b351967bd349d48d5c9b"}, 1116 | {file = "pydantic_core-2.23.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4fc714bdbfb534f94034efaa6eadd74e5b93c8fa6315565a222f7b6f42ca1166"}, 1117 | {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63e46b3169866bd62849936de036f901a9356e36376079b05efa83caeaa02ceb"}, 1118 | {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed1a53de42fbe34853ba90513cea21673481cd81ed1be739f7f2efb931b24916"}, 1119 | {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cfdd16ab5e59fc31b5e906d1a3f666571abc367598e3e02c83403acabc092e07"}, 1120 | {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255a8ef062cbf6674450e668482456abac99a5583bbafb73f9ad469540a3a232"}, 1121 | {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a7cd62e831afe623fbb7aabbb4fe583212115b3ef38a9f6b71869ba644624a2"}, 1122 | {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f09e2ff1f17c2b51f2bc76d1cc33da96298f0a036a137f5440ab3ec5360b624f"}, 1123 | {file = "pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e38e63e6f3d1cec5a27e0afe90a085af8b6806ee208b33030e65b6516353f1a3"}, 1124 | {file = "pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0dbd8dbed2085ed23b5c04afa29d8fd2771674223135dc9bc937f3c09284d071"}, 1125 | {file = "pydantic_core-2.23.4-cp310-none-win32.whl", hash = "sha256:6531b7ca5f951d663c339002e91aaebda765ec7d61b7d1e3991051906ddde119"}, 1126 | {file = "pydantic_core-2.23.4-cp310-none-win_amd64.whl", hash = "sha256:7c9129eb40958b3d4500fa2467e6a83356b3b61bfff1b414c7361d9220f9ae8f"}, 1127 | {file = "pydantic_core-2.23.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:77733e3892bb0a7fa797826361ce8a9184d25c8dffaec60b7ffe928153680ba8"}, 1128 | {file = "pydantic_core-2.23.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b84d168f6c48fabd1f2027a3d1bdfe62f92cade1fb273a5d68e621da0e44e6d"}, 1129 | {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df49e7a0861a8c36d089c1ed57d308623d60416dab2647a4a17fe050ba85de0e"}, 1130 | {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ff02b6d461a6de369f07ec15e465a88895f3223eb75073ffea56b84d9331f607"}, 1131 | {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:996a38a83508c54c78a5f41456b0103c30508fed9abcad0a59b876d7398f25fd"}, 1132 | {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d97683ddee4723ae8c95d1eddac7c192e8c552da0c73a925a89fa8649bf13eea"}, 1133 | {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:216f9b2d7713eb98cb83c80b9c794de1f6b7e3145eef40400c62e86cee5f4e1e"}, 1134 | {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6f783e0ec4803c787bcea93e13e9932edab72068f68ecffdf86a99fd5918878b"}, 1135 | {file = "pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d0776dea117cf5272382634bd2a5c1b6eb16767c223c6a5317cd3e2a757c61a0"}, 1136 | {file = "pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d5f7a395a8cf1621939692dba2a6b6a830efa6b3cee787d82c7de1ad2930de64"}, 1137 | {file = "pydantic_core-2.23.4-cp311-none-win32.whl", hash = "sha256:74b9127ffea03643e998e0c5ad9bd3811d3dac8c676e47db17b0ee7c3c3bf35f"}, 1138 | {file = "pydantic_core-2.23.4-cp311-none-win_amd64.whl", hash = "sha256:98d134c954828488b153d88ba1f34e14259284f256180ce659e8d83e9c05eaa3"}, 1139 | {file = "pydantic_core-2.23.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f3e0da4ebaef65158d4dfd7d3678aad692f7666877df0002b8a522cdf088f231"}, 1140 | {file = "pydantic_core-2.23.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f69a8e0b033b747bb3e36a44e7732f0c99f7edd5cea723d45bc0d6e95377ffee"}, 1141 | {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:723314c1d51722ab28bfcd5240d858512ffd3116449c557a1336cbe3919beb87"}, 1142 | {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb2802e667b7051a1bebbfe93684841cc9351004e2badbd6411bf357ab8d5ac8"}, 1143 | {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d18ca8148bebe1b0a382a27a8ee60350091a6ddaf475fa05ef50dc35b5df6327"}, 1144 | {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33e3d65a85a2a4a0dc3b092b938a4062b1a05f3a9abde65ea93b233bca0e03f2"}, 1145 | {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:128585782e5bfa515c590ccee4b727fb76925dd04a98864182b22e89a4e6ed36"}, 1146 | {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:68665f4c17edcceecc112dfed5dbe6f92261fb9d6054b47d01bf6371a6196126"}, 1147 | {file = "pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:20152074317d9bed6b7a95ade3b7d6054845d70584216160860425f4fbd5ee9e"}, 1148 | {file = "pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9261d3ce84fa1d38ed649c3638feefeae23d32ba9182963e465d58d62203bd24"}, 1149 | {file = "pydantic_core-2.23.4-cp312-none-win32.whl", hash = "sha256:4ba762ed58e8d68657fc1281e9bb72e1c3e79cc5d464be146e260c541ec12d84"}, 1150 | {file = "pydantic_core-2.23.4-cp312-none-win_amd64.whl", hash = "sha256:97df63000f4fea395b2824da80e169731088656d1818a11b95f3b173747b6cd9"}, 1151 | {file = "pydantic_core-2.23.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7530e201d10d7d14abce4fb54cfe5b94a0aefc87da539d0346a484ead376c3cc"}, 1152 | {file = "pydantic_core-2.23.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df933278128ea1cd77772673c73954e53a1c95a4fdf41eef97c2b779271bd0bd"}, 1153 | {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cb3da3fd1b6a5d0279a01877713dbda118a2a4fc6f0d821a57da2e464793f05"}, 1154 | {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c6dcb030aefb668a2b7009c85b27f90e51e6a3b4d5c9bc4c57631292015b0d"}, 1155 | {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:696dd8d674d6ce621ab9d45b205df149399e4bb9aa34102c970b721554828510"}, 1156 | {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2971bb5ffe72cc0f555c13e19b23c85b654dd2a8f7ab493c262071377bfce9f6"}, 1157 | {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8394d940e5d400d04cad4f75c0598665cbb81aecefaca82ca85bd28264af7f9b"}, 1158 | {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0dff76e0602ca7d4cdaacc1ac4c005e0ce0dcfe095d5b5259163a80d3a10d327"}, 1159 | {file = "pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7d32706badfe136888bdea71c0def994644e09fff0bfe47441deaed8e96fdbc6"}, 1160 | {file = "pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed541d70698978a20eb63d8c5d72f2cc6d7079d9d90f6b50bad07826f1320f5f"}, 1161 | {file = "pydantic_core-2.23.4-cp313-none-win32.whl", hash = "sha256:3d5639516376dce1940ea36edf408c554475369f5da2abd45d44621cb616f769"}, 1162 | {file = "pydantic_core-2.23.4-cp313-none-win_amd64.whl", hash = "sha256:5a1504ad17ba4210df3a045132a7baeeba5a200e930f57512ee02909fc5c4cb5"}, 1163 | {file = "pydantic_core-2.23.4-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d4488a93b071c04dc20f5cecc3631fc78b9789dd72483ba15d423b5b3689b555"}, 1164 | {file = "pydantic_core-2.23.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:81965a16b675b35e1d09dd14df53f190f9129c0202356ed44ab2728b1c905658"}, 1165 | {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ffa2ebd4c8530079140dd2d7f794a9d9a73cbb8e9d59ffe24c63436efa8f271"}, 1166 | {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:61817945f2fe7d166e75fbfb28004034b48e44878177fc54d81688e7b85a3665"}, 1167 | {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:29d2c342c4bc01b88402d60189f3df065fb0dda3654744d5a165a5288a657368"}, 1168 | {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5e11661ce0fd30a6790e8bcdf263b9ec5988e95e63cf901972107efc49218b13"}, 1169 | {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d18368b137c6295db49ce7218b1a9ba15c5bc254c96d7c9f9e924a9bc7825ad"}, 1170 | {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ec4e55f79b1c4ffb2eecd8a0cfba9955a2588497d96851f4c8f99aa4a1d39b12"}, 1171 | {file = "pydantic_core-2.23.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:374a5e5049eda9e0a44c696c7ade3ff355f06b1fe0bb945ea3cac2bc336478a2"}, 1172 | {file = "pydantic_core-2.23.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5c364564d17da23db1106787675fc7af45f2f7b58b4173bfdd105564e132e6fb"}, 1173 | {file = "pydantic_core-2.23.4-cp38-none-win32.whl", hash = "sha256:d7a80d21d613eec45e3d41eb22f8f94ddc758a6c4720842dc74c0581f54993d6"}, 1174 | {file = "pydantic_core-2.23.4-cp38-none-win_amd64.whl", hash = "sha256:5f5ff8d839f4566a474a969508fe1c5e59c31c80d9e140566f9a37bba7b8d556"}, 1175 | {file = "pydantic_core-2.23.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a4fa4fc04dff799089689f4fd502ce7d59de529fc2f40a2c8836886c03e0175a"}, 1176 | {file = "pydantic_core-2.23.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0a7df63886be5e270da67e0966cf4afbae86069501d35c8c1b3b6c168f42cb36"}, 1177 | {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dcedcd19a557e182628afa1d553c3895a9f825b936415d0dbd3cd0bbcfd29b4b"}, 1178 | {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f54b118ce5de9ac21c363d9b3caa6c800341e8c47a508787e5868c6b79c9323"}, 1179 | {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86d2f57d3e1379a9525c5ab067b27dbb8a0642fb5d454e17a9ac434f9ce523e3"}, 1180 | {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:de6d1d1b9e5101508cb37ab0d972357cac5235f5c6533d1071964c47139257df"}, 1181 | {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1278e0d324f6908e872730c9102b0112477a7f7cf88b308e4fc36ce1bdb6d58c"}, 1182 | {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9a6b5099eeec78827553827f4c6b8615978bb4b6a88e5d9b93eddf8bb6790f55"}, 1183 | {file = "pydantic_core-2.23.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e55541f756f9b3ee346b840103f32779c695a19826a4c442b7954550a0972040"}, 1184 | {file = "pydantic_core-2.23.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a5c7ba8ffb6d6f8f2ab08743be203654bb1aaa8c9dcb09f82ddd34eadb695605"}, 1185 | {file = "pydantic_core-2.23.4-cp39-none-win32.whl", hash = "sha256:37b0fe330e4a58d3c58b24d91d1eb102aeec675a3db4c292ec3928ecd892a9a6"}, 1186 | {file = "pydantic_core-2.23.4-cp39-none-win_amd64.whl", hash = "sha256:1498bec4c05c9c787bde9125cfdcc63a41004ff167f495063191b863399b1a29"}, 1187 | {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f455ee30a9d61d3e1a15abd5068827773d6e4dc513e795f380cdd59932c782d5"}, 1188 | {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1e90d2e3bd2c3863d48525d297cd143fe541be8bbf6f579504b9712cb6b643ec"}, 1189 | {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e203fdf807ac7e12ab59ca2bfcabb38c7cf0b33c41efeb00f8e5da1d86af480"}, 1190 | {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e08277a400de01bc72436a0ccd02bdf596631411f592ad985dcee21445bd0068"}, 1191 | {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f220b0eea5965dec25480b6333c788fb72ce5f9129e8759ef876a1d805d00801"}, 1192 | {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d06b0c8da4f16d1d1e352134427cb194a0a6e19ad5db9161bf32b2113409e728"}, 1193 | {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ba1a0996f6c2773bd83e63f18914c1de3c9dd26d55f4ac302a7efe93fb8e7433"}, 1194 | {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:9a5bce9d23aac8f0cf0836ecfc033896aa8443b501c58d0602dbfd5bd5b37753"}, 1195 | {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:78ddaaa81421a29574a682b3179d4cf9e6d405a09b99d93ddcf7e5239c742e21"}, 1196 | {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:883a91b5dd7d26492ff2f04f40fbb652de40fcc0afe07e8129e8ae779c2110eb"}, 1197 | {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88ad334a15b32a791ea935af224b9de1bf99bcd62fabf745d5f3442199d86d59"}, 1198 | {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:233710f069d251feb12a56da21e14cca67994eab08362207785cf8c598e74577"}, 1199 | {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:19442362866a753485ba5e4be408964644dd6a09123d9416c54cd49171f50744"}, 1200 | {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:624e278a7d29b6445e4e813af92af37820fafb6dcc55c012c834f9e26f9aaaef"}, 1201 | {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f5ef8f42bec47f21d07668a043f077d507e5bf4e668d5c6dfe6aaba89de1a5b8"}, 1202 | {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:aea443fffa9fbe3af1a9ba721a87f926fe548d32cab71d188a6ede77d0ff244e"}, 1203 | {file = "pydantic_core-2.23.4.tar.gz", hash = "sha256:2584f7cf844ac4d970fba483a717dbe10c1c1c96a969bf65d61ffe94df1b2863"}, 1204 | ] 1205 | 1206 | [package.dependencies] 1207 | typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" 1208 | 1209 | [[package]] 1210 | name = "pydantic-settings" 1211 | version = "2.5.2" 1212 | description = "Settings management using Pydantic" 1213 | optional = false 1214 | python-versions = ">=3.8" 1215 | files = [ 1216 | {file = "pydantic_settings-2.5.2-py3-none-any.whl", hash = "sha256:2c912e55fd5794a59bf8c832b9de832dcfdf4778d79ff79b708744eed499a907"}, 1217 | {file = "pydantic_settings-2.5.2.tar.gz", hash = "sha256:f90b139682bee4d2065273d5185d71d37ea46cfe57e1b5ae184fc6a0b2484ca0"}, 1218 | ] 1219 | 1220 | [package.dependencies] 1221 | pydantic = ">=2.7.0" 1222 | python-dotenv = ">=0.21.0" 1223 | 1224 | [package.extras] 1225 | azure-key-vault = ["azure-identity (>=1.16.0)", "azure-keyvault-secrets (>=4.8.0)"] 1226 | toml = ["tomli (>=2.0.1)"] 1227 | yaml = ["pyyaml (>=6.0.1)"] 1228 | 1229 | [[package]] 1230 | name = "pyflakes" 1231 | version = "3.2.0" 1232 | description = "passive checker of Python programs" 1233 | optional = false 1234 | python-versions = ">=3.8" 1235 | files = [ 1236 | {file = "pyflakes-3.2.0-py2.py3-none-any.whl", hash = "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a"}, 1237 | {file = "pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f"}, 1238 | ] 1239 | 1240 | [[package]] 1241 | name = "pytest" 1242 | version = "7.4.4" 1243 | description = "pytest: simple powerful testing with Python" 1244 | optional = false 1245 | python-versions = ">=3.7" 1246 | files = [ 1247 | {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, 1248 | {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, 1249 | ] 1250 | 1251 | [package.dependencies] 1252 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 1253 | iniconfig = "*" 1254 | packaging = "*" 1255 | pluggy = ">=0.12,<2.0" 1256 | 1257 | [package.extras] 1258 | testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] 1259 | 1260 | [[package]] 1261 | name = "python-dotenv" 1262 | version = "1.0.1" 1263 | description = "Read key-value pairs from a .env file and set them as environment variables" 1264 | optional = false 1265 | python-versions = ">=3.8" 1266 | files = [ 1267 | {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, 1268 | {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, 1269 | ] 1270 | 1271 | [package.extras] 1272 | cli = ["click (>=5.0)"] 1273 | 1274 | [[package]] 1275 | name = "python-jose" 1276 | version = "3.3.0" 1277 | description = "JOSE implementation in Python" 1278 | optional = false 1279 | python-versions = "*" 1280 | files = [ 1281 | {file = "python-jose-3.3.0.tar.gz", hash = "sha256:55779b5e6ad599c6336191246e95eb2293a9ddebd555f796a65f838f07e5d78a"}, 1282 | {file = "python_jose-3.3.0-py2.py3-none-any.whl", hash = "sha256:9b1376b023f8b298536eedd47ae1089bcdb848f1535ab30555cd92002d78923a"}, 1283 | ] 1284 | 1285 | [package.dependencies] 1286 | ecdsa = "!=0.15" 1287 | pyasn1 = "*" 1288 | rsa = "*" 1289 | 1290 | [package.extras] 1291 | cryptography = ["cryptography (>=3.4.0)"] 1292 | pycrypto = ["pyasn1", "pycrypto (>=2.6.0,<2.7.0)"] 1293 | pycryptodome = ["pyasn1", "pycryptodome (>=3.3.1,<4.0.0)"] 1294 | 1295 | [[package]] 1296 | name = "python-multipart" 1297 | version = "0.0.9" 1298 | description = "A streaming multipart parser for Python" 1299 | optional = false 1300 | python-versions = ">=3.8" 1301 | files = [ 1302 | {file = "python_multipart-0.0.9-py3-none-any.whl", hash = "sha256:97ca7b8ea7b05f977dc3849c3ba99d51689822fab725c3703af7c866a0c2b215"}, 1303 | {file = "python_multipart-0.0.9.tar.gz", hash = "sha256:03f54688c663f1b7977105f021043b0793151e4cb1c1a9d4a11fc13d622c4026"}, 1304 | ] 1305 | 1306 | [package.extras] 1307 | dev = ["atomicwrites (==1.4.1)", "attrs (==23.2.0)", "coverage (==7.4.1)", "hatch", "invoke (==2.2.0)", "more-itertools (==10.2.0)", "pbr (==6.0.0)", "pluggy (==1.4.0)", "py (==1.11.0)", "pytest (==8.0.0)", "pytest-cov (==4.1.0)", "pytest-timeout (==2.2.0)", "pyyaml (==6.0.1)", "ruff (==0.2.1)"] 1308 | 1309 | [[package]] 1310 | name = "requests" 1311 | version = "2.32.3" 1312 | description = "Python HTTP for Humans." 1313 | optional = false 1314 | python-versions = ">=3.8" 1315 | files = [ 1316 | {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, 1317 | {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, 1318 | ] 1319 | 1320 | [package.dependencies] 1321 | certifi = ">=2017.4.17" 1322 | charset-normalizer = ">=2,<4" 1323 | idna = ">=2.5,<4" 1324 | urllib3 = ">=1.21.1,<3" 1325 | 1326 | [package.extras] 1327 | socks = ["PySocks (>=1.5.6,!=1.5.7)"] 1328 | use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] 1329 | 1330 | [[package]] 1331 | name = "rsa" 1332 | version = "4.9" 1333 | description = "Pure-Python RSA implementation" 1334 | optional = false 1335 | python-versions = ">=3.6,<4" 1336 | files = [ 1337 | {file = "rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7"}, 1338 | {file = "rsa-4.9.tar.gz", hash = "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21"}, 1339 | ] 1340 | 1341 | [package.dependencies] 1342 | pyasn1 = ">=0.1.3" 1343 | 1344 | [[package]] 1345 | name = "six" 1346 | version = "1.16.0" 1347 | description = "Python 2 and 3 compatibility utilities" 1348 | optional = false 1349 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 1350 | files = [ 1351 | {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, 1352 | {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, 1353 | ] 1354 | 1355 | [[package]] 1356 | name = "sniffio" 1357 | version = "1.3.1" 1358 | description = "Sniff out which async library your code is running under" 1359 | optional = false 1360 | python-versions = ">=3.7" 1361 | files = [ 1362 | {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, 1363 | {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, 1364 | ] 1365 | 1366 | [[package]] 1367 | name = "soupsieve" 1368 | version = "2.6" 1369 | description = "A modern CSS selector implementation for Beautiful Soup." 1370 | optional = false 1371 | python-versions = ">=3.8" 1372 | files = [ 1373 | {file = "soupsieve-2.6-py3-none-any.whl", hash = "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9"}, 1374 | {file = "soupsieve-2.6.tar.gz", hash = "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb"}, 1375 | ] 1376 | 1377 | [[package]] 1378 | name = "sqlalchemy" 1379 | version = "2.0.35" 1380 | description = "Database Abstraction Library" 1381 | optional = false 1382 | python-versions = ">=3.7" 1383 | files = [ 1384 | {file = "SQLAlchemy-2.0.35-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:67219632be22f14750f0d1c70e62f204ba69d28f62fd6432ba05ab295853de9b"}, 1385 | {file = "SQLAlchemy-2.0.35-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4668bd8faf7e5b71c0319407b608f278f279668f358857dbfd10ef1954ac9f90"}, 1386 | {file = "SQLAlchemy-2.0.35-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb8bea573863762bbf45d1e13f87c2d2fd32cee2dbd50d050f83f87429c9e1ea"}, 1387 | {file = "SQLAlchemy-2.0.35-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f552023710d4b93d8fb29a91fadf97de89c5926c6bd758897875435f2a939f33"}, 1388 | {file = "SQLAlchemy-2.0.35-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:016b2e665f778f13d3c438651dd4de244214b527a275e0acf1d44c05bc6026a9"}, 1389 | {file = "SQLAlchemy-2.0.35-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7befc148de64b6060937231cbff8d01ccf0bfd75aa26383ffdf8d82b12ec04ff"}, 1390 | {file = "SQLAlchemy-2.0.35-cp310-cp310-win32.whl", hash = "sha256:22b83aed390e3099584b839b93f80a0f4a95ee7f48270c97c90acd40ee646f0b"}, 1391 | {file = "SQLAlchemy-2.0.35-cp310-cp310-win_amd64.whl", hash = "sha256:a29762cd3d116585278ffb2e5b8cc311fb095ea278b96feef28d0b423154858e"}, 1392 | {file = "SQLAlchemy-2.0.35-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e21f66748ab725ade40fa7af8ec8b5019c68ab00b929f6643e1b1af461eddb60"}, 1393 | {file = "SQLAlchemy-2.0.35-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8a6219108a15fc6d24de499d0d515c7235c617b2540d97116b663dade1a54d62"}, 1394 | {file = "SQLAlchemy-2.0.35-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:042622a5306c23b972192283f4e22372da3b8ddf5f7aac1cc5d9c9b222ab3ff6"}, 1395 | {file = "SQLAlchemy-2.0.35-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:627dee0c280eea91aed87b20a1f849e9ae2fe719d52cbf847c0e0ea34464b3f7"}, 1396 | {file = "SQLAlchemy-2.0.35-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4fdcd72a789c1c31ed242fd8c1bcd9ea186a98ee8e5408a50e610edfef980d71"}, 1397 | {file = "SQLAlchemy-2.0.35-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:89b64cd8898a3a6f642db4eb7b26d1b28a497d4022eccd7717ca066823e9fb01"}, 1398 | {file = "SQLAlchemy-2.0.35-cp311-cp311-win32.whl", hash = "sha256:6a93c5a0dfe8d34951e8a6f499a9479ffb9258123551fa007fc708ae2ac2bc5e"}, 1399 | {file = "SQLAlchemy-2.0.35-cp311-cp311-win_amd64.whl", hash = "sha256:c68fe3fcde03920c46697585620135b4ecfdfc1ed23e75cc2c2ae9f8502c10b8"}, 1400 | {file = "SQLAlchemy-2.0.35-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:eb60b026d8ad0c97917cb81d3662d0b39b8ff1335e3fabb24984c6acd0c900a2"}, 1401 | {file = "SQLAlchemy-2.0.35-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6921ee01caf375363be5e9ae70d08ce7ca9d7e0e8983183080211a062d299468"}, 1402 | {file = "SQLAlchemy-2.0.35-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8cdf1a0dbe5ced887a9b127da4ffd7354e9c1a3b9bb330dce84df6b70ccb3a8d"}, 1403 | {file = "SQLAlchemy-2.0.35-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93a71c8601e823236ac0e5d087e4f397874a421017b3318fd92c0b14acf2b6db"}, 1404 | {file = "SQLAlchemy-2.0.35-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e04b622bb8a88f10e439084486f2f6349bf4d50605ac3e445869c7ea5cf0fa8c"}, 1405 | {file = "SQLAlchemy-2.0.35-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1b56961e2d31389aaadf4906d453859f35302b4eb818d34a26fab72596076bb8"}, 1406 | {file = "SQLAlchemy-2.0.35-cp312-cp312-win32.whl", hash = "sha256:0f9f3f9a3763b9c4deb8c5d09c4cc52ffe49f9876af41cc1b2ad0138878453cf"}, 1407 | {file = "SQLAlchemy-2.0.35-cp312-cp312-win_amd64.whl", hash = "sha256:25b0f63e7fcc2a6290cb5f7f5b4fc4047843504983a28856ce9b35d8f7de03cc"}, 1408 | {file = "SQLAlchemy-2.0.35-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:f021d334f2ca692523aaf7bbf7592ceff70c8594fad853416a81d66b35e3abf9"}, 1409 | {file = "SQLAlchemy-2.0.35-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:05c3f58cf91683102f2f0265c0db3bd3892e9eedabe059720492dbaa4f922da1"}, 1410 | {file = "SQLAlchemy-2.0.35-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:032d979ce77a6c2432653322ba4cbeabf5a6837f704d16fa38b5a05d8e21fa00"}, 1411 | {file = "SQLAlchemy-2.0.35-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:2e795c2f7d7249b75bb5f479b432a51b59041580d20599d4e112b5f2046437a3"}, 1412 | {file = "SQLAlchemy-2.0.35-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:cc32b2990fc34380ec2f6195f33a76b6cdaa9eecf09f0c9404b74fc120aef36f"}, 1413 | {file = "SQLAlchemy-2.0.35-cp37-cp37m-win32.whl", hash = "sha256:9509c4123491d0e63fb5e16199e09f8e262066e58903e84615c301dde8fa2e87"}, 1414 | {file = "SQLAlchemy-2.0.35-cp37-cp37m-win_amd64.whl", hash = "sha256:3655af10ebcc0f1e4e06c5900bb33e080d6a1fa4228f502121f28a3b1753cde5"}, 1415 | {file = "SQLAlchemy-2.0.35-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4c31943b61ed8fdd63dfd12ccc919f2bf95eefca133767db6fbbd15da62078ec"}, 1416 | {file = "SQLAlchemy-2.0.35-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a62dd5d7cc8626a3634208df458c5fe4f21200d96a74d122c83bc2015b333bc1"}, 1417 | {file = "SQLAlchemy-2.0.35-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0630774b0977804fba4b6bbea6852ab56c14965a2b0c7fc7282c5f7d90a1ae72"}, 1418 | {file = "SQLAlchemy-2.0.35-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d625eddf7efeba2abfd9c014a22c0f6b3796e0ffb48f5d5ab106568ef01ff5a"}, 1419 | {file = "SQLAlchemy-2.0.35-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:ada603db10bb865bbe591939de854faf2c60f43c9b763e90f653224138f910d9"}, 1420 | {file = "SQLAlchemy-2.0.35-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:c41411e192f8d3ea39ea70e0fae48762cd11a2244e03751a98bd3c0ca9a4e936"}, 1421 | {file = "SQLAlchemy-2.0.35-cp38-cp38-win32.whl", hash = "sha256:d299797d75cd747e7797b1b41817111406b8b10a4f88b6e8fe5b5e59598b43b0"}, 1422 | {file = "SQLAlchemy-2.0.35-cp38-cp38-win_amd64.whl", hash = "sha256:0375a141e1c0878103eb3d719eb6d5aa444b490c96f3fedab8471c7f6ffe70ee"}, 1423 | {file = "SQLAlchemy-2.0.35-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ccae5de2a0140d8be6838c331604f91d6fafd0735dbdcee1ac78fc8fbaba76b4"}, 1424 | {file = "SQLAlchemy-2.0.35-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2a275a806f73e849e1c309ac11108ea1a14cd7058577aba962cd7190e27c9e3c"}, 1425 | {file = "SQLAlchemy-2.0.35-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:732e026240cdd1c1b2e3ac515c7a23820430ed94292ce33806a95869c46bd139"}, 1426 | {file = "SQLAlchemy-2.0.35-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:890da8cd1941fa3dab28c5bac3b9da8502e7e366f895b3b8e500896f12f94d11"}, 1427 | {file = "SQLAlchemy-2.0.35-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c0d8326269dbf944b9201911b0d9f3dc524d64779a07518199a58384c3d37a44"}, 1428 | {file = "SQLAlchemy-2.0.35-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b76d63495b0508ab9fc23f8152bac63205d2a704cd009a2b0722f4c8e0cba8e0"}, 1429 | {file = "SQLAlchemy-2.0.35-cp39-cp39-win32.whl", hash = "sha256:69683e02e8a9de37f17985905a5eca18ad651bf592314b4d3d799029797d0eb3"}, 1430 | {file = "SQLAlchemy-2.0.35-cp39-cp39-win_amd64.whl", hash = "sha256:aee110e4ef3c528f3abbc3c2018c121e708938adeeff9006428dd7c8555e9b3f"}, 1431 | {file = "SQLAlchemy-2.0.35-py3-none-any.whl", hash = "sha256:2ab3f0336c0387662ce6221ad30ab3a5e6499aab01b9790879b6578fd9b8faa1"}, 1432 | {file = "sqlalchemy-2.0.35.tar.gz", hash = "sha256:e11d7ea4d24f0a262bccf9a7cd6284c976c5369dac21db237cff59586045ab9f"}, 1433 | ] 1434 | 1435 | [package.dependencies] 1436 | greenlet = {version = "!=0.4.17", markers = "python_version < \"3.13\" 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\")"} 1437 | typing-extensions = ">=4.6.0" 1438 | 1439 | [package.extras] 1440 | aiomysql = ["aiomysql (>=0.2.0)", "greenlet (!=0.4.17)"] 1441 | aioodbc = ["aioodbc", "greenlet (!=0.4.17)"] 1442 | aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing_extensions (!=3.10.0.1)"] 1443 | asyncio = ["greenlet (!=0.4.17)"] 1444 | asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (!=0.4.17)"] 1445 | mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5)"] 1446 | mssql = ["pyodbc"] 1447 | mssql-pymssql = ["pymssql"] 1448 | mssql-pyodbc = ["pyodbc"] 1449 | mypy = ["mypy (>=0.910)"] 1450 | mysql = ["mysqlclient (>=1.4.0)"] 1451 | mysql-connector = ["mysql-connector-python"] 1452 | oracle = ["cx_oracle (>=8)"] 1453 | oracle-oracledb = ["oracledb (>=1.0.1)"] 1454 | postgresql = ["psycopg2 (>=2.7)"] 1455 | postgresql-asyncpg = ["asyncpg", "greenlet (!=0.4.17)"] 1456 | postgresql-pg8000 = ["pg8000 (>=1.29.1)"] 1457 | postgresql-psycopg = ["psycopg (>=3.0.7)"] 1458 | postgresql-psycopg2binary = ["psycopg2-binary"] 1459 | postgresql-psycopg2cffi = ["psycopg2cffi"] 1460 | postgresql-psycopgbinary = ["psycopg[binary] (>=3.0.7)"] 1461 | pymysql = ["pymysql"] 1462 | sqlcipher = ["sqlcipher3_binary"] 1463 | 1464 | [[package]] 1465 | name = "starlette" 1466 | version = "0.38.5" 1467 | description = "The little ASGI library that shines." 1468 | optional = false 1469 | python-versions = ">=3.8" 1470 | files = [ 1471 | {file = "starlette-0.38.5-py3-none-any.whl", hash = "sha256:632f420a9d13e3ee2a6f18f437b0a9f1faecb0bc42e1942aa2ea0e379a4c4206"}, 1472 | {file = "starlette-0.38.5.tar.gz", hash = "sha256:04a92830a9b6eb1442c766199d62260c3d4dc9c4f9188360626b1e0273cb7077"}, 1473 | ] 1474 | 1475 | [package.dependencies] 1476 | anyio = ">=3.4.0,<5" 1477 | 1478 | [package.extras] 1479 | full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7)", "pyyaml"] 1480 | 1481 | [[package]] 1482 | name = "tqdm" 1483 | version = "4.66.5" 1484 | description = "Fast, Extensible Progress Meter" 1485 | optional = false 1486 | python-versions = ">=3.7" 1487 | files = [ 1488 | {file = "tqdm-4.66.5-py3-none-any.whl", hash = "sha256:90279a3770753eafc9194a0364852159802111925aa30eb3f9d85b0e805ac7cd"}, 1489 | {file = "tqdm-4.66.5.tar.gz", hash = "sha256:e1020aef2e5096702d8a025ac7d16b1577279c9d63f8375b63083e9a5f0fcbad"}, 1490 | ] 1491 | 1492 | [package.dependencies] 1493 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 1494 | 1495 | [package.extras] 1496 | dev = ["pytest (>=6)", "pytest-cov", "pytest-timeout", "pytest-xdist"] 1497 | notebook = ["ipywidgets (>=6)"] 1498 | slack = ["slack-sdk"] 1499 | telegram = ["requests"] 1500 | 1501 | [[package]] 1502 | name = "typing-extensions" 1503 | version = "4.12.2" 1504 | description = "Backported and Experimental Type Hints for Python 3.8+" 1505 | optional = false 1506 | python-versions = ">=3.8" 1507 | files = [ 1508 | {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, 1509 | {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, 1510 | ] 1511 | 1512 | [[package]] 1513 | name = "urllib3" 1514 | version = "2.2.3" 1515 | description = "HTTP library with thread-safe connection pooling, file post, and more." 1516 | optional = false 1517 | python-versions = ">=3.8" 1518 | files = [ 1519 | {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"}, 1520 | {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"}, 1521 | ] 1522 | 1523 | [package.extras] 1524 | brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] 1525 | h2 = ["h2 (>=4,<5)"] 1526 | socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] 1527 | zstd = ["zstandard (>=0.18.0)"] 1528 | 1529 | [[package]] 1530 | name = "uvicorn" 1531 | version = "0.21.1" 1532 | description = "The lightning-fast ASGI server." 1533 | optional = false 1534 | python-versions = ">=3.7" 1535 | files = [ 1536 | {file = "uvicorn-0.21.1-py3-none-any.whl", hash = "sha256:e47cac98a6da10cd41e6fd036d472c6f58ede6c5dbee3dbee3ef7a100ed97742"}, 1537 | {file = "uvicorn-0.21.1.tar.gz", hash = "sha256:0fac9cb342ba099e0d582966005f3fdba5b0290579fed4a6266dc702ca7bb032"}, 1538 | ] 1539 | 1540 | [package.dependencies] 1541 | click = ">=7.0" 1542 | h11 = ">=0.8" 1543 | 1544 | [package.extras] 1545 | standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] 1546 | 1547 | [metadata] 1548 | lock-version = "2.0" 1549 | python-versions = "^3.11" 1550 | content-hash = "765eb2b8e75d6fe535cd10651aa77ccc29d8c6b9f890000db3adbfb226ab562e" 1551 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "Interactor" 3 | version = "0.1.0" 4 | description = "An ecommerce application" 5 | authors = ["Shahriar Shariati "] 6 | 7 | [tool.poetry.dependencies] 8 | python = "^3.11" 9 | fastapi = "^0.115.0" 10 | sqlalchemy = "^2.0.9" 11 | alembic = "^1.10.3" 12 | pydantic = {extras = ["email"], version = "^2.9.2"} 13 | uvicorn = "^0.21.1" 14 | python-dotenv = "^1.0.0" 15 | passlib = "^1.7.4" 16 | python-jose = "^3.3.0" 17 | python-multipart = "^0.0.9" 18 | bcrypt = "^4.2.0" 19 | cryptography = "^43.0.1" 20 | pydantic-settings = "^2.5.2" 21 | psycopg2-binary = "^2.9.9" 22 | bs4 = "^0.0.2" 23 | openai = "^1.46.0" 24 | requests = "^2.32.3" 25 | 26 | [tool.poetry.dev-dependencies] 27 | pytest = "^7.3.0" 28 | 29 | [tool.poetry.group.dev.dependencies] 30 | black = "^24.8.0" 31 | flake8 = "^7.1.1" 32 | 33 | [build-system] 34 | requires = ["poetry-core>=1.0.0"] 35 | build-backend = "poetry.core.masonry.api" -------------------------------------------------------------------------------- /tests/test_users.py: -------------------------------------------------------------------------------- 1 | from fastapi.testclient import TestClient 2 | from app.main import app 3 | 4 | client = TestClient(app) 5 | 6 | 7 | def test_create_user(): 8 | response = client.post( 9 | "/users/", json={"email": "test@example.com", "password": "testpassword"} 10 | ) 11 | assert response.status_code == 200 12 | data = response.json() 13 | assert data["email"] == "test@example.com" 14 | assert "id" in data 15 | 16 | 17 | def test_read_user(): 18 | response = client.get("/users/1") 19 | assert response.status_code == 200 20 | data = response.json() 21 | assert "email" in data 22 | assert "id" in data 23 | --------------------------------------------------------------------------------