Compare commits

..

13 Commits

63 changed files with 2396 additions and 1511 deletions

View File

@ -39,13 +39,12 @@ revision:
venv-api:
cd api && \
poetry env activate \
poetry install
install:
make migrate head && \
cd api && \
poetry run python3 -m api.utils.init
poetry run python3 api/utils/init.py
%::
echo $(MESSAGE)

View File

@ -9,10 +9,9 @@ from uvicorn import run
from api.config import get_settings, DefaultSettings
from api.endpoints import list_of_routes
from api.utils.common import get_hostname
from api.services.middleware import MiddlewareAccessTokenValidadtion
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)
logger.setLevel(logging.INFO)
def bind_routes(application: FastAPI, setting: DefaultSettings) -> None:
@ -21,7 +20,6 @@ def bind_routes(application: FastAPI, setting: DefaultSettings) -> None:
application.include_router(route, prefix=setting.PATH_PREFIX)
def get_app() -> FastAPI:
"""Creates application and all dependable objects."""
loguru.logger.remove()
@ -34,7 +32,7 @@ def get_app() -> FastAPI:
description=description,
docs_url="/swagger",
openapi_url="/openapi",
version="0.0.2",
version="0.1.0",
)
settings = get_settings()
bind_routes(application, settings)
@ -46,7 +44,6 @@ app = get_app()
dev_origins = [
"http://localhost:3000",
"http://127.0.0.1:64775",
]
prod_origins = [""]
@ -80,7 +77,4 @@ app.add_middleware(
allow_credentials=True,
allow_methods=["GET", "POST", "OPTIONS", "DELETE", "PUT"],
allow_headers=["*"],
)
app.add_middleware(MiddlewareAccessTokenValidadtion)

View File

@ -44,17 +44,6 @@ class DefaultSettings(BaseSettings):
REDIS_DB: int = int(environ.get("REDIS_DB", "0"))
REDIS_PASSWORD: str = environ.get("REDIS_PASSWORD", "hackme")
SECRET_KEY: str = environ.get("SECRET_KEY", "secret")
ALGORITHM: str = environ.get("ALGORITHM", "HS256")
ACCESS_TOKEN_EXPIRE_MINUTES: int = int(
environ.get("ACCESS_TOKEN_EXPIRE_MINUTES", 600)
)
REFRESH_TOKEN_EXPIRE_DAYS: int = int(
environ.get("REFRESH_TOKEN_EXPIRE_DAYS_LONG", 365)
)
@cached_property
def database_settings(self) -> dict:
"""Get all settings for connection with database."""

View File

@ -1,138 +0,0 @@
"""empty message
Revision ID: f1b06efacec0
Revises:
Create Date: 2025-04-23 15:09:14.833213
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision: str = 'f1b06efacec0'
down_revision: Union[str, None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('account',
sa.Column('id', sa.Integer().with_variant(mysql.INTEGER(unsigned=True), 'mysql'), autoincrement=True, nullable=False),
sa.Column('name', sa.String(length=100), nullable=False),
sa.Column('login', sa.String(length=100), nullable=False),
sa.Column('email', sa.String(length=100), nullable=True),
sa.Column('bind_tenant_id', sa.String(length=40), nullable=True),
sa.Column('role', sa.Enum('OWNER', 'ADMIN', 'EDITOR', 'VIEWER', name='accountrole'), nullable=False),
sa.Column('meta', sa.JSON(), nullable=True),
sa.Column('creator_id', sa.Integer().with_variant(mysql.INTEGER(unsigned=True), 'mysql'), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
sa.Column('status', sa.Enum('ACTIVE', 'DISABLED', 'BLOCKED', 'DELETED', name='accountstatus'), nullable=False),
sa.ForeignKeyConstraint(['creator_id'], ['account.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index('idx_login', 'account', ['login'], unique=False)
op.create_index('idx_name', 'account', ['name'], unique=False)
op.create_table('account_keyring',
sa.Column('owner_id', sa.Integer().with_variant(mysql.INTEGER(unsigned=True), 'mysql'), nullable=False),
sa.Column('key_type', sa.Enum('PASSWORD', 'ACCESS_TOKEN', 'REFRESH_TOKEN', 'API_KEY', name='keytype'), nullable=False),
sa.Column('key_id', sa.String(length=40), nullable=False),
sa.Column('key_value', sa.String(length=255), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
sa.Column('expiry', sa.DateTime(timezone=True), nullable=True),
sa.Column('status', sa.Enum('ACTIVE', 'EXPIRED', 'DELETED', name='keystatus'), nullable=False),
sa.ForeignKeyConstraint(['owner_id'], ['account.id'], ),
sa.PrimaryKeyConstraint('owner_id', 'key_type', 'key_id')
)
op.create_table('list_events',
sa.Column('id', sa.Integer().with_variant(mysql.INTEGER(unsigned=True), 'mysql'), autoincrement=True, nullable=False),
sa.Column('name', sa.String(length=40, collation='latin1_bin'), nullable=False),
sa.Column('title', sa.String(length=64), nullable=False),
sa.Column('creator_id', sa.Integer().with_variant(mysql.INTEGER(unsigned=True), 'mysql'), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
sa.Column('schema', sa.JSON(), nullable=True),
sa.Column('state', sa.Enum('AUTO', 'DESCRIPTED', name='eventstate'), nullable=False),
sa.Column('status', sa.Enum('ACTIVE', 'DISABLED', 'DELETED', name='eventstatus'), nullable=False),
sa.ForeignKeyConstraint(['creator_id'], ['account.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('name')
)
op.create_table('process_schema',
sa.Column('id', sa.Integer().with_variant(mysql.INTEGER(unsigned=True), 'mysql'), autoincrement=True, nullable=False),
sa.Column('title', sa.String(length=100), nullable=False),
sa.Column('description', sa.Text(), nullable=False),
sa.Column('owner_id', sa.Integer().with_variant(mysql.INTEGER(unsigned=True), 'mysql'), nullable=False),
sa.Column('creator_id', sa.Integer().with_variant(mysql.INTEGER(unsigned=True), 'mysql'), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
sa.Column('settings', sa.JSON(), nullable=True),
sa.Column('status', sa.Enum('ACTIVE', 'STOPPING', 'STOPPED', 'DELETED', name='processstatus'), nullable=False),
sa.ForeignKeyConstraint(['creator_id'], ['account.id'], ),
sa.ForeignKeyConstraint(['owner_id'], ['account.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index('idx_owner_id', 'process_schema', ['owner_id'], unique=False)
op.create_table('process_version_archive',
sa.Column('id', sa.Integer().with_variant(mysql.INTEGER(unsigned=True), 'mysql'), autoincrement=True, nullable=False),
sa.Column('ps_id', sa.Integer().with_variant(mysql.INTEGER(unsigned=True), 'mysql'), nullable=False),
sa.Column('version', sa.Integer().with_variant(mysql.INTEGER(unsigned=True), 'mysql'), nullable=False),
sa.Column('snapshot', sa.JSON(), nullable=True),
sa.Column('owner_id', sa.Integer().with_variant(mysql.INTEGER(unsigned=True), 'mysql'), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
sa.Column('is_last', sa.Integer().with_variant(mysql.INTEGER(unsigned=True), 'mysql'), nullable=True),
sa.ForeignKeyConstraint(['owner_id'], ['account.id'], ),
sa.ForeignKeyConstraint(['ps_id'], ['process_schema.id'], ),
sa.PrimaryKeyConstraint('id', 'version')
)
op.create_table('ps_node',
sa.Column('id', sa.Integer().with_variant(mysql.INTEGER(unsigned=True), 'mysql'), autoincrement=True, nullable=False),
sa.Column('ps_id', sa.Integer().with_variant(mysql.INTEGER(unsigned=True), 'mysql'), nullable=False),
sa.Column('node_type', sa.Enum('TYPE1', 'TYPE2', 'TYPE3', name='nodetype'), nullable=False),
sa.Column('settings', sa.JSON(), nullable=True),
sa.Column('creator_id', sa.Integer().with_variant(mysql.INTEGER(unsigned=True), 'mysql'), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
sa.Column('status', sa.Enum('ACTIVE', 'DISABLED', 'DELETED', name='nodestatus'), nullable=False),
sa.ForeignKeyConstraint(['creator_id'], ['account.id'], ),
sa.ForeignKeyConstraint(['ps_id'], ['process_schema.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index('idx_ps_id', 'ps_node', ['ps_id'], unique=False)
op.create_table('node_link',
sa.Column('id', sa.Integer().with_variant(mysql.INTEGER(unsigned=True), 'mysql'), autoincrement=True, nullable=False),
sa.Column('link_name', sa.String(length=20), nullable=False),
sa.Column('node_id', sa.Integer().with_variant(mysql.INTEGER(unsigned=True), 'mysql'), nullable=False),
sa.Column('next_node_id', sa.Integer().with_variant(mysql.INTEGER(unsigned=True), 'mysql'), nullable=False),
sa.Column('settings', sa.JSON(), nullable=True),
sa.Column('creator_id', sa.Integer().with_variant(mysql.INTEGER(unsigned=True), 'mysql'), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
sa.Column('status', sa.Enum('ACTIVE', 'STOPPING', 'STOPPED', 'DELETED', name='nodelinkstatus'), nullable=False),
sa.ForeignKeyConstraint(['creator_id'], ['account.id'], ),
sa.ForeignKeyConstraint(['next_node_id'], ['ps_node.id'], ),
sa.ForeignKeyConstraint(['node_id'], ['ps_node.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index('idx_next_node_id', 'node_link', ['next_node_id'], unique=False)
op.create_index('idx_node_id', 'node_link', ['node_id'], unique=False)
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index('idx_node_id', table_name='node_link')
op.drop_index('idx_next_node_id', table_name='node_link')
op.drop_table('node_link')
op.drop_index('idx_ps_id', table_name='ps_node')
op.drop_table('ps_node')
op.drop_table('process_version_archive')
op.drop_index('idx_owner_id', table_name='process_schema')
op.drop_table('process_schema')
op.drop_table('list_events')
op.drop_table('account_keyring')
op.drop_index('idx_name', table_name='account')
op.drop_index('idx_login', table_name='account')
op.drop_table('account')
# ### end Alembic commands ###

View File

@ -1,98 +0,0 @@
from typing import Optional
from datetime import datetime, timezone
from sqlalchemy import insert, select
from sqlalchemy.ext.asyncio import AsyncConnection
from enum import Enum
from api.db.tables.account import account_table
from api.schemas.account.account import User
from api.schemas.endpoints.account import UserUpdate, Role, Status
async def get_user_id(connection: AsyncConnection, id: int) -> Optional[User]:
"""
Получает юзера по id.
"""
query = (
select(account_table)
.where(account_table.c.id == id)
)
user_db_cursor = await connection.execute(query)
user_db = user_db_cursor.one_or_none()
if not user_db:
return None
user_data = {
column.name: (getattr(user_db, column.name).name if isinstance(
getattr(user_db, column.name), Enum) else getattr(user_db, column.name))
for column in account_table.columns
}
return User.model_validate(user_data)
async def get_user_login(connection: AsyncConnection, login: str) -> Optional[User]:
"""
Получает юзера по login.
"""
query = (
select(account_table)
.where(account_table.c.login == login)
)
user_db_cursor = await connection.execute(query)
user_db = user_db_cursor.one_or_none()
if not user_db:
return None
user_data = {
column.name: (getattr(user_db, column.name).name if isinstance(
getattr(user_db, column.name), Enum) else getattr(user_db, column.name))
for column in account_table.columns
}
return User.model_validate(user_data)
async def update_user_id(connection: AsyncConnection, update_values, user) -> Optional[User]:
"""
Вносит изменеия в нужное поле таблицы account_table.
"""
await connection.execute(
account_table.update()
.where(account_table.c.id == user.id)
.values(**update_values)
)
await connection.commit()
async def create_user(connection: AsyncConnection, user: User, creator_id: int) -> Optional[User]:
"""
Создает нове поле в таблице account_table.
"""
query = insert(account_table).values(
name=user.name,
login=user.login,
email=user.email,
bind_tenant_id=user.bind_tenant_id,
role=user.role.value,
meta=user.meta,
creator_id=creator_id,
created_at=datetime.now(timezone.utc),
status=user.status.value
)
await connection.execute(query)
await connection.commit()
return user

View File

@ -1,86 +0,0 @@
from typing import Optional
from sqlalchemy import select, update
from sqlalchemy.ext.asyncio import AsyncConnection
from enum import Enum
from api.db.tables.account import account_table, account_keyring_table, KeyType, KeyStatus
from api.schemas.account.account import User
from api.schemas.account.account_keyring import AccountKeyring
from api.utils.key_id_gen import KeyIdGenerator
from datetime import datetime, timezone
async def get_user(connection: AsyncConnection, login: str) -> Optional[User]:
query = (
select(account_table, account_keyring_table)
.join(account_keyring_table, account_table.c.id == account_keyring_table.c.owner_id)
.where(account_table.c.login == login,
account_keyring_table.c.key_type == KeyType.PASSWORD)
)
user_db_cursor = await connection.execute(query)
user_db = user_db_cursor.one_or_none()
print("Raw user data from DB:", user_db)
if not user_db:
return None, None
user_data = {
column.name: (getattr(user_db, column.name).name if isinstance(
getattr(user_db, column.name), Enum) else getattr(user_db, column.name))
for column in account_table.columns
}
password_data = {
column.name: (getattr(user_db, column.name).name if isinstance(
getattr(user_db, column.name), Enum) else getattr(user_db, column.name))
for column in account_keyring_table.columns
}
user = User.model_validate(user_data)
password = AccountKeyring.model_validate(password_data)
return user, password
async def upgrade_old_refresh_token(connection: AsyncConnection, user,refresh_token) -> Optional[User]:
new_status = KeyStatus.EXPIRED
update_query = (
update(account_keyring_table)
.where(
account_table.c.id == user.id,
account_keyring_table.c.status == KeyStatus.ACTIVE,
account_keyring_table.c.key_type == KeyType.REFRESH_TOKEN,
account_keyring_table.c.key_value == refresh_token
)
.values(status=new_status)
)
await connection.execute(update_query)
await connection.commit()
async def add_new_refresh_token(connection: AsyncConnection, new_refresh_token, new_refresh_token_expires_time, user) -> Optional[User]:
new_refresh_token = account_keyring_table.insert().values(
owner_id=user.id,
key_type=KeyType.REFRESH_TOKEN,
key_id=KeyIdGenerator(),
key_value=new_refresh_token,
created_at=datetime.now(timezone.utc),
expiry=new_refresh_token_expires_time,
status=KeyStatus.ACTIVE,
)
await connection.execute(new_refresh_token)
await connection.commit()

View File

@ -1,73 +0,0 @@
from typing import Optional
from datetime import datetime, timezone
from enum import Enum
from sqlalchemy import insert, select
from sqlalchemy.ext.asyncio import AsyncConnection
from api.db.tables.account import account_keyring_table
from api.schemas.account.account_keyring import AccountKeyring
from api.schemas.endpoints.account_keyring import AccountKeyringUpdate, StatusKey, TypeKey
async def get_key_id(connection: AsyncConnection, key_id: str) -> Optional[AccountKeyring]:
"""
Получает key по key_id.
"""
query = (
select(account_keyring_table)
.where(account_keyring_table.c.key_id == key_id)
)
user_db_cursor = await connection.execute(query)
user_db = user_db_cursor.one_or_none()
if not user_db:
return None
user_data = {
column.name: (getattr(user_db, column.name).name if isinstance(
getattr(user_db, column.name), Enum) else getattr(user_db, column.name))
for column in account_keyring_table.columns
}
return AccountKeyring.model_validate(user_data)
async def update_key_id(connection: AsyncConnection, update_values, key) -> Optional[AccountKeyring]:
"""
Вносит изменеия в нужное поле таблицы account_keyring_table.
"""
await connection.execute(
account_keyring_table.update()
.where(account_keyring_table.c.key_id == key.key_id)
.values(**update_values)
)
await connection.commit()
async def create_key(connection: AsyncConnection, key: AccountKeyring, key_id:int) -> Optional[AccountKeyring]:
"""
Создает нове поле в таблице account_keyring_table).
"""
query = insert(account_keyring_table).values(
owner_id = key.owner_id,
key_type= key.key_type.value,
key_id= key_id,
key_value= key.key_value,
created_at= datetime.now(timezone.utc),
expiry= key.expiry,
status= key.status.value
)
key.created_at= datetime.now(timezone.utc)
key.key_id= key_id
await connection.execute(query)
await connection.commit()
return key

View File

@ -1,9 +1,12 @@
from sqlalchemy import Table, Column, String, Enum as SQLAEnum, JSON, ForeignKey, DateTime, Index
from sqlalchemy import Table, Column, Integer, String, Enum as SQLAEnum, JSON, ForeignKey, DateTime, Index
from sqlalchemy.sql import func
from datetime import datetime, timedelta
from enum import Enum, auto
from api.db.sql_types import UnsignedInt
from api.db import metadata
@ -20,6 +23,7 @@ class AccountStatus(str,Enum):
DELETED = auto()
account_table = Table(
'account', metadata,
Column('id', UnsignedInt, primary_key=True, autoincrement=True),
@ -52,8 +56,18 @@ account_keyring_table = Table(
'account_keyring', metadata,
Column('owner_id', UnsignedInt, ForeignKey('account.id'), primary_key=True, nullable=False),
Column('key_type', SQLAEnum(KeyType), primary_key=True, nullable=False),
Column('key_id', String(40),primary_key=True, default=None),
Column('key_value', String(255), nullable=False),
Column('key_id', String(40), default=None),
Column('key_value', String(64), nullable=False),
Column('created_at', DateTime(timezone=True), server_default=func.now()),
Column('expiry', DateTime(timezone=True), nullable=True),
Column('status', SQLAEnum(KeyStatus), nullable=False), )
def set_expiry_for_key_type(key_type: KeyType) -> datetime:
match key_type:
case KeyType.ACCESS_TOKEN:
return datetime.now() + timedelta(hours=1) # 1 hour
case KeyType.REFRESH_TOKEN:
return datetime.now() + timedelta(days=365) # 1 year
case KeyType.API_KEY:
return datetime.max # max datetime

View File

@ -7,14 +7,14 @@ from api.db.sql_types import UnsignedInt
from api.db import metadata
# Определение перечислений для статуса процесса
class ProcessStatus(str, Enum):
ACTIVE = auto()
STOPPING = auto()
STOPPED = auto()
DELETED = auto()
# Определение таблицы process_schema
process_schema_table = Table(
'process_schema', metadata,
Column('id', UnsignedInt, primary_key=True, autoincrement=True),

View File

@ -1,12 +1,4 @@
from api.endpoints.auth import api_router as auth_router
from api.endpoints.profile import api_router as profile_router
from api.endpoints.account import api_router as account_router
from api.endpoints.keyring import api_router as keyring_router
list_of_routes = [
auth_router,
profile_router,
account_router,
keyring_router]
list_of_routes = []
__all__ = [
"list_of_routes",

View File

@ -1,148 +0,0 @@
from fastapi import (
APIRouter,
Body,
Depends,
Form,
HTTPException,
Request,
Response,
status,
)
from sqlalchemy.ext.asyncio import AsyncConnection
from api.db.connection.session import get_connection_dep
from api.db.logic.account import get_user_id, update_user_id, create_user,get_user_login
from api.schemas.account.account import User,Status
from api.schemas.endpoints.account import UserUpdate
from api.services.user_role_validation import db_user_role_validation
from api.services.update_data_validation import update_user_data_changes
api_router = APIRouter(
prefix="/account",
tags=["User accountModel"],
)
@api_router.get("/{user_id}")
async def get_account(user_id: int,
request: Request,
connection: AsyncConnection = Depends(get_connection_dep)
):
current_user = request.state.current_user
authorize_user = await db_user_role_validation(connection, current_user)
user = await get_user_id(connection, user_id)
if user is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Account not found")
return user
@api_router.post("")
async def create_account(
user: UserUpdate,
request: Request,
connection: AsyncConnection = Depends(get_connection_dep)
):
current_user = request.state.current_user
authorize_user = await db_user_role_validation(connection, current_user)
user_validation = await get_user_login(connection, user.login)
if user_validation is None:
await create_user(connection,user,authorize_user.id)
user_new = await get_user_login(connection, user.login)
return user_new
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="An account with this information already exists.")
@api_router.put("/{user_id}")
async def update_account(
user_id: int,
request: Request,
user_update: UserUpdate,
connection: AsyncConnection = Depends(get_connection_dep)
):
current_user = request.state.current_user
authorize_user = await db_user_role_validation(connection, current_user)
user = await get_user_id(connection, user_id)
if user is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Account not found")
update_values = update_user_data_changes(user_update,user)
if update_values is None:
return user
user_update_data = User.model_validate({**user.model_dump(), **update_values})
await update_user_id(connection, update_values, user)
user = await get_user_id(connection, user_id)
return user
@api_router.delete("/{user_id}")
async def delete_account(
user_id: int,
request: Request,
connection: AsyncConnection = Depends(get_connection_dep)
):
current_user = request.state.current_user
authorize_user = await db_user_role_validation(connection, current_user)
user = await get_user_id(connection, user_id)
if user is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Account not found")
user_update = UserUpdate(status=Status.DELETED.value)
update_values = update_user_data_changes(user_update,user)
if update_values is None:
return user
await update_user_id(connection, update_values, user)
user = await get_user_id(connection, user_id)
return user

View File

@ -1,148 +0,0 @@
from datetime import datetime, timedelta, timezone
from fastapi import (
APIRouter,
Body,
Depends,
Form,
HTTPException,
Request,
Response,
status,
)
from loguru import logger
from pydantic.main import BaseModel
from fastapi_jwt_auth import AuthJWT
from pydantic import BaseModel
from sqlalchemy.ext.asyncio import AsyncConnection
from api.config import get_settings
from api.db.connection.session import get_connection_dep
from api.services.auth import authenticate_user
from api.db.logic.auth import add_new_refresh_token,upgrade_old_refresh_token
from api.schemas.endpoints.auth import Auth
api_router = APIRouter(
prefix="/auth",
tags=["User auth"],
)
class Settings(BaseModel):
authjwt_secret_key: str = get_settings().SECRET_KEY
# Configure application to store and get JWT from cookies
authjwt_token_location: set = {"headers", "cookies"}
authjwt_cookie_domain: str = get_settings().DOMAIN
# Only allow JWT cookies to be sent over https
authjwt_cookie_secure: bool = get_settings().ENV == "prod"
# Enable csrf double submit protection. default is True
authjwt_cookie_csrf_protect: bool = False
authjwt_cookie_samesite: str = "lax"
@AuthJWT.load_config
def get_config():
return Settings()
@api_router.post("")
async def login_for_access_token(
user: Auth,
response: Response,
connection: AsyncConnection = Depends(get_connection_dep),
Authorize: AuthJWT = Depends(),
):
"""Авторизирует, выставляет токены в куки."""
user = await authenticate_user(connection, user.login, user.password)
print("login_for_access_token", user)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
# headers={"WWW-Authenticate": "Bearer"},
)
access_token_expires = timedelta(
minutes=get_settings().ACCESS_TOKEN_EXPIRE_MINUTES)
refresh_token_expires = timedelta(
days=get_settings().REFRESH_TOKEN_EXPIRE_DAYS
)
logger.debug(f"refresh_token_expires {refresh_token_expires}")
access_token = Authorize.create_access_token(
subject=user.login, expires_time=access_token_expires
)
refresh_token = Authorize.create_refresh_token(
subject=user.login, expires_time=refresh_token_expires
)
refresh_token_expires_time = datetime.now(timezone.utc) + refresh_token_expires
await add_new_refresh_token(connection,refresh_token,refresh_token_expires_time,user)
Authorize.set_refresh_cookies(refresh_token)
return {
"access_token": access_token,
# "access_token_expires": access_token_expires_time,
# "refresh_token": refresh_token,
# "refresh_token_expires": refresh_token_expires_time
}
@api_router.post("/refresh")
async def refresh(
request: Request,
connection: AsyncConnection = Depends(get_connection_dep),
Authorize: AuthJWT = Depends()
):
refresh_token = request.cookies.get("refresh_token_cookie")
print("Refresh Token:", refresh_token)
if not refresh_token:
raise HTTPException(status_code=401, detail="Refresh token is missing")
try:
Authorize.jwt_refresh_token_required()
current_user = Authorize.get_jwt_subject()
print(current_user)
except Exception as e:
await upgrade_old_refresh_token(connection,current_user,refresh_token)
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid refresh token",
)
access_token_expires = timedelta(minutes=get_settings().ACCESS_TOKEN_EXPIRE_MINUTES)
new_access_token = Authorize.create_access_token(
subject=current_user, expires_time=access_token_expires
)
return {
"access_token": new_access_token,
# "access_token_expires": access_token_expires_time,
# "refresh_token": refresh_token,
# "refresh_token_expires": refresh_token_expires_time
}

View File

@ -1,151 +0,0 @@
from fastapi import (
APIRouter,
Body,
Depends,
Form,
HTTPException,
Request,
Response,
status,
)
from sqlalchemy.ext.asyncio import AsyncConnection
from api.db.connection.session import get_connection_dep
from api.db.logic.keyring import get_key_id,create_key,update_key_id
from api.schemas.account.account import Status
from api.schemas.endpoints.account_keyring import AccountKeyringUpdate
from api.schemas.account.account_keyring import AccountKeyring
from api.services.user_role_validation import db_user_role_validation
from api.services.update_data_validation import update_key_data_changes
api_router = APIRouter(
prefix="/keyring",
tags=["User KeyringModel"],
)
@api_router.get("/{user_id}/{key_id}")
async def get_keyring(
key_id: str,
request: Request,
connection: AsyncConnection = Depends(get_connection_dep)
):
current_user = request.state.current_user
authorize_user = await db_user_role_validation(connection, current_user)
keyring = await get_key_id(connection, key_id)
if keyring is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Key not found")
return keyring
@api_router.post("/{user_id}/{key_id}")
async def create_keyring(
user_id: int,
key_id: str,
request: Request,
key: AccountKeyringUpdate,
connection: AsyncConnection = Depends(get_connection_dep)
):
current_user = request.state.current_user
authorize_user = await db_user_role_validation(connection, current_user)
keyring = await get_key_id(connection, key_id)
if keyring is None:
print(key.key_type)
user_new = await create_key(connection,key, key_id, )
return user_new
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="An keyring with this information already exists.")
@api_router.put("/{user_id}/{key_id}")
async def update_keyring(
user_id: int,
key_id: str,
request: Request,
keyring_update: AccountKeyringUpdate,
connection: AsyncConnection = Depends(get_connection_dep)
):
current_user = request.state.current_user
authorize_user = await db_user_role_validation(connection, current_user)
keyring = await get_key_id(connection, key_id)
if keyring is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="keyring not found")
update_values = update_key_data_changes(keyring_update,keyring)
if update_values is None:
return keyring
keyring_update_data = AccountKeyring.model_validate({**keyring.model_dump(), **update_values})
await update_key_id(connection, update_values, keyring)
keyring = await get_key_id(connection, key_id)
return keyring
@api_router.delete("/{user_id}/{key_id}")
async def delete_keyring(
user_id: int,
key_id: str,
request: Request,
connection: AsyncConnection = Depends(get_connection_dep)
):
current_user = request.state.current_user
authorize_user = await db_user_role_validation(connection, current_user)
keyring = await get_key_id(connection, key_id)
if keyring is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="keyring not found")
keyring_update = AccountKeyringUpdate(status=Status.DELETED.value)
update_values = update_key_data_changes(keyring_update,keyring)
if update_values is None:
return keyring
await update_key_id(connection, update_values, keyring)
keyring = await get_key_id(connection, key_id)
return keyring

View File

@ -1,77 +0,0 @@
from fastapi import (
APIRouter,
Body,
Depends,
Form,
HTTPException,
Request,
Response,
status,
)
from sqlalchemy.ext.asyncio import AsyncConnection
from api.db.connection.session import get_connection_dep
from api.db.logic.account import get_user_id, update_user_id,get_user_login
from api.services.update_data_validation import update_user_data_changes
from api.schemas.endpoints.account import UserUpdate
api_router = APIRouter(
prefix="/profile",
tags=["User accountModel"],
)
@api_router.get("")
async def get_profile(
request: Request,
connection: AsyncConnection = Depends(get_connection_dep),
):
# Извлекаем текущего пользователя из request.state
current_user = request.state.current_user
user = await get_user_login(connection, current_user)
if user is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Account not found")
return user
@api_router.put("")
async def update_profile(
request: Request,
user_updata: UserUpdate,
connection: AsyncConnection = Depends(get_connection_dep),
):
current_user = request.state.current_user
user = await get_user_login(connection, current_user)
if user is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Account not found")
if user_updata.role == None and user_updata.login == None:
update_values = update_user_data_changes(user_updata,user)
if update_values is None:
return user
await update_user_id(connection, update_values, user)
user = await get_user_id(connection, user.id)
return user
else:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY ,
detail="Bad body")

View File

@ -1,31 +1,30 @@
import datetime
from enum import Enum
from datetime import datetime
from typing import Optional
from pydantic import BaseModel, EmailStr, Field
# Модель для хранения информации из запроса
class Role(Enum):
OWNER = 'OWNER'
ADMIN = 'ADMIN'
EDITOR = 'EDITOR'
VIEWER = 'VIEWER'
OWNER = "Owner"
ADMIN = "Admin"
EDITOR = "Editor"
VIEWER = "Viewer"
class Status(Enum):
ACTIVE = 'ACTIVE'
DISABLED = 'DISABLED'
BLOCKED = 'BLOCKED'
DELETED = 'DELETED'
ACTIVE = "Active"
DISABLED = "Disabled"
BLOCKED = "Blocked"
DELETED = "Deleted"
class User(BaseModel):
id: Optional[int] = None
id: int
name: str = Field(..., max_length=100)
login: str = Field(..., max_length=100)
email: Optional[EmailStr] = Field(None, max_length=100) # Электронная почта (может быть None)
bind_tenant_id: Optional[str] = Field(None, max_length=40)
email: EmailStr = Field(..., max_length=100)
bind_tenant_id: str = Field(..., max_length=40)
role: Role
meta: dict
creator_id: Optional[int] = None
creator_id: int
is_active: bool
created_at: datetime
status: Status

View File

@ -1,28 +1,27 @@
import datetime
from enum import Enum
from typing import Optional, Dict
from pydantic import BaseModel, Field
from datetime import datetime
# Модель для хранения информации из запроса
class TypeKey(Enum):
PASSWORD = "PASSWORD"
ACCESS_TOKEN = "ACCESS_TOKEN"
REFRESH_TOKEN = "REFRESH_TOKEN"
API_KEY = "API_KEY"
class StatusKey(Enum):
ACTIVE = "ACTIVE"
EXPIRED = "EXPIRED"
DELETED = "DELETED"
class Type(Enum):
PASSWORD = "password"
ACCESS_TOKEN = "access_token"
REFRESH_TOKEN = "refresh_token"
API_KEY = "api_key"
class Status(Enum):
ACTIVE = "Active"
EXPIRED = "Expired"
DELETED = "Deleted"
class AccountKeyring(BaseModel):
owner_id: int
key_type: TypeKey # Используем тот же KeyType
key_id: Optional[str] = Field(None, max_length=40) # Изменено на None как default
key_value: str = Field(..., max_length=255)
key_type: Type
key_id: str = Field(..., max_length=40)
key_value: str = Field(..., max_length=64)
created_at: datetime
expiry: Optional[datetime] = None
status: StatusKey
expiry: datetime
status: Status

View File

@ -1,30 +0,0 @@
from enum import Enum
from typing import Optional
from datetime import datetime
from pydantic import BaseModel, EmailStr, Field
# Таблица для получения информации из запроса
class Role(Enum):
OWNER = 'OWNER'
ADMIN = 'ADMIN'
EDITOR = 'EDITOR'
VIEWER = 'VIEWER'
class Status(Enum):
ACTIVE = 'ACTIVE'
DISABLED = 'DISABLED'
BLOCKED = 'BLOCKED'
DELETED = 'DELETED'
class UserUpdate(BaseModel):
id: Optional[int] = None
name: Optional[str] = Field(None, max_length=100)
login: Optional[str] = Field(None, max_length=100)
email: Optional[EmailStr] = None
bind_tenant_id: Optional[str] = Field(None, max_length=40)
role: Optional[Role] = None
meta: Optional[dict] = None
creator_id: Optional[int] = None
created_at: Optional[datetime] = None
status: Optional[Status] = None

View File

@ -1,28 +0,0 @@
import datetime
from enum import Enum
from typing import Optional
from pydantic import BaseModel, Field
from datetime import datetime
# Таблица для получения информации из запроса
class TypeKey(Enum):
PASSWORD = "PASSWORD"
ACCESS_TOKEN = "ACCESS_TOKEN"
REFRESH_TOKEN = "REFRESH_TOKEN"
API_KEY = "API_KEY"
class StatusKey(Enum):
ACTIVE = "ACTIVE"
EXPIRED = "EXPIRED"
DELETED = "DELETED"
class AccountKeyringUpdate(BaseModel):
owner_id: Optional[int] = None
key_type: Optional[TypeKey] = None
key_id: Optional[str] = Field(None, max_length=40)
key_value: Optional[str] = Field(None, max_length=255)
created_at: Optional[datetime] = None
expiry: Optional[datetime] = None
status: Optional[StatusKey] = None

View File

@ -1,11 +0,0 @@
from pydantic import BaseModel
# Таблица для получения информации из запроса
class Auth(BaseModel):
login: str
password: str
class Refresh(BaseModel):
refresh_token: str

View File

@ -1,9 +0,0 @@
from fastapi import HTTPException, status
from fastapi_jwt_auth import AuthJWT
from fastapi.responses import JSONResponse
def AccessTokenValidadtion(Authorize):
Authorize.jwt_required()
current_user = Authorize.get_jwt_subject()
return current_user

View File

@ -1,36 +0,0 @@
from datetime import datetime, timedelta
from typing import Optional
from fastapi import Depends, HTTPException, Request, status
# from fastapi_jwt_auth import AuthJWT
# from fastapi_jwt_auth.exceptions import JWTDecodeError
# from jose import JWTError, jwt
from loguru import logger
from sqlalchemy.ext.asyncio import AsyncConnection
from api.config import get_settings
from api.db.connection.session import get_connection_dep
from api.db.logic.auth import get_user
# # from backend.schemas.users.token import TokenData
from api.schemas.account.account import User,Status
from api.schemas.account.account_keyring import AccountKeyring
from api.utils.hasher import Hasher
async def authenticate_user(
connection: AsyncConnection, username: str, password: str
) -> Optional[User]:
sql_user,sql_password = await get_user(connection, username)
if not sql_user or sql_user.status != Status.ACTIVE :
return None
hasher = Hasher()
if not hasher.verify_data(password, sql_password.key_value):
return None
return sql_user

View File

@ -1,75 +0,0 @@
from starlette.middleware.base import BaseHTTPMiddleware
from fastapi import (
Request,
status,
)
from fastapi.responses import JSONResponse
from api.config import get_settings
import re
from re import escape
from fastapi_jwt_auth import AuthJWT
class MiddlewareAccessTokenValidadtion(BaseHTTPMiddleware):
def __init__(self, app):
super().__init__(app)
self.prefix = escape(get_settings().PATH_PREFIX)
self.excluded_routes = [
re.compile(r'^' + re.escape(self.prefix) + r'/auth/refresh/?$'),
re.compile(r'^' + re.escape(self.prefix) + r'/auth/?$')
]
async def dispatch(self,
request: Request,
call_next):
if request.method in ["GET", "POST", "PUT", "DELETE"]:
if any(pattern.match(request.url.path) for pattern in self.excluded_routes):
return await call_next(request)
else:
auth_header = request.headers.get("Authorization")
if not auth_header:
return JSONResponse(
status_code=status.HTTP_401_UNAUTHORIZED,
content={"detail": "Missing authorization header."},
headers={"WWW-Authenticate": "Bearer"}
)
token = auth_header.split(" ")[1]
print(token)
Authorize = AuthJWT(request)
try:
current_user = Authorize.get_jwt_subject()
print(current_user)
request.state.current_user = current_user
return await call_next(request)
except Exception:
return JSONResponse(
status_code=status.HTTP_401_UNAUTHORIZED,
content={"detail": "The access token is invalid or expired."},
headers={"WWW-Authenticate": "Bearer"}
)
# async with get_connection() as connection:
# authorize_user = await get_user_login(connection, current_user)
# print(authorize_user)
# if authorize_user is None :
# return JSONResponse(
# status_code=status.HTTP_404_NOT_FOUND ,
# detail="User not found.")

View File

@ -1,70 +0,0 @@
from enum import Enum
from typing import Optional
from api.schemas.endpoints.account import UserUpdate, Role, Status
from api.schemas.endpoints.account_keyring import AccountKeyringUpdate, StatusKey, TypeKey
def update_user_data_changes(update_data: UserUpdate, user) -> Optional[dict]:
"""
Сравнивает данные для обновления с текущими значениями пользователя.
Возвращает:
- None, если нет изменений
- Словарь {поле: новое_значение} для измененных полей
"""
update_values = {}
changes = {}
for field, value in update_data.model_dump(exclude_unset=True).items():
if value is None:
continue
if isinstance(value, (Role, Status)):
update_values[field] = value.value
else:
update_values[field] = value
for field, new_value in update_values.items():
if not hasattr(user, field):
continue
current_value = getattr(user, field)
if isinstance(current_value, Enum):
current_value = current_value.value
if current_value != new_value:
changes[field] = new_value
return changes if changes else None
def update_key_data_changes(update_data: AccountKeyringUpdate, key) -> Optional[dict]:
"""
Сравнивает данные для обновления с текущими значениями пользователя.
Возвращает:
- None, если нет изменений
- Словарь {поле: новое_значение} для измененных полей
"""
update_values = {}
changes = {}
for field, value in update_data.model_dump(exclude_unset=True).items():
if value is None:
continue
if isinstance(value, (TypeKey, StatusKey)):
update_values[field] = value.value
else:
update_values[field] = value
for field, new_value in update_values.items():
if not hasattr(key, field):
continue
current_value = getattr(key, field)
if isinstance(current_value, Enum):
current_value = current_value.value
if current_value != new_value:
changes[field] = new_value
return changes if changes else None

View File

@ -1,15 +0,0 @@
from fastapi import (
HTTPException,
status,
)
from api.db.logic.account import get_user_login
from api.schemas.account.account import Role,Status
async def db_user_role_validation(connection, current_user):
authorize_user = await get_user_login(connection, current_user)
if authorize_user.role not in {Role.OWNER, Role.ADMIN}:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="You do not have enough permissions")
return authorize_user

View File

@ -1,15 +0,0 @@
import hashlib
# Хешер для работы с паролем.
class Hasher:
def __init__(self):
pass
def hash_data(self, password: str) -> str:
# Хеширует пароль с использованием SHA-256.
return hashlib.sha256(password.encode()).hexdigest()
def verify_data(self, password: str, hashed: str) -> bool:
# Проверяет пароль путем сравнения его хеша с сохраненным хешем.
return self.hash_data(password) == hashed

View File

@ -5,7 +5,6 @@ import secrets
from api.db.connection.session import get_connection
from api.db.tables.account import account_table, account_keyring_table, AccountRole, KeyType, KeyStatus
from api.utils.key_id_gen import KeyIdGenerator
INIT_LOCK_FILE = "../init.lock"
DEFAULT_LOGIN = "vorkout"
@ -40,7 +39,6 @@ async def init():
create_key_query = account_keyring_table.insert().values(
owner_id=user_id,
key_type=KeyType.PASSWORD,
key_id=KeyIdGenerator(),
key_value=hashed_password,
status=KeyStatus.ACTIVE,
)

View File

@ -1,9 +0,0 @@
import random
from datetime import datetime
# Генератор key_id для таблицы account_keyring
def KeyIdGenerator():
random_number = random.randint(1000, 9999)
result = f"{datetime.now().strftime('%Y-%m-%d')}-{random_number}"
return result

324
api/poetry.lock generated
View File

@ -273,43 +273,6 @@ ssh = ["bcrypt (>=3.1.5)"]
test = ["certifi (>=2024)", "cryptography-vectors (==44.0.2)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"]
test-randomorder = ["pytest-randomly"]
[[package]]
name = "dnspython"
version = "2.7.0"
description = "DNS toolkit"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "dnspython-2.7.0-py3-none-any.whl", hash = "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86"},
{file = "dnspython-2.7.0.tar.gz", hash = "sha256:ce9c432eda0dc91cf618a5cedf1a4e142651196bbcd2c80e89ed5a907e5cfaf1"},
]
[package.extras]
dev = ["black (>=23.1.0)", "coverage (>=7.0)", "flake8 (>=7)", "hypercorn (>=0.16.0)", "mypy (>=1.8)", "pylint (>=3)", "pytest (>=7.4)", "pytest-cov (>=4.1.0)", "quart-trio (>=0.11.0)", "sphinx (>=7.2.0)", "sphinx-rtd-theme (>=2.0.0)", "twine (>=4.0.0)", "wheel (>=0.42.0)"]
dnssec = ["cryptography (>=43)"]
doh = ["h2 (>=4.1.0)", "httpcore (>=1.0.0)", "httpx (>=0.26.0)"]
doq = ["aioquic (>=1.0.0)"]
idna = ["idna (>=3.7)"]
trio = ["trio (>=0.23)"]
wmi = ["wmi (>=1.5.1)"]
[[package]]
name = "email-validator"
version = "2.2.0"
description = "A robust email address syntax and deliverability validation library."
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "email_validator-2.2.0-py3-none-any.whl", hash = "sha256:561977c2d73ce3611850a06fa56b414621e0c8faa9d66f2611407d87465da631"},
{file = "email_validator-2.2.0.tar.gz", hash = "sha256:cb690f344c617a714f22e66ae771445a1ceb46821152df8e165c5f9a364582b7"},
]
[package.dependencies]
dnspython = ">=2.0.0"
idna = ">=2.0.0"
[[package]]
name = "exceptiongroup"
version = "1.2.2"
@ -346,32 +309,6 @@ typing-extensions = ">=4.8.0"
all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=3.1.5)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "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)"]
standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"]
[[package]]
name = "fastapi-jwt-auth"
version = "0.5.0"
description = "FastAPI extension that provides JWT Auth support (secure, easy to use and lightweight)"
optional = false
python-versions = ">=3.6"
groups = ["main"]
files = []
develop = false
[package.dependencies]
fastapi = ">=0.61.0"
PyJWT = ">=1.7.1,<2.0.0"
[package.extras]
asymmetric = ["cryptography (>=2.6,<4.0.0)"]
dev = ["cryptography (>=2.6,<4.0.0)", "uvicorn (>=0.11.5,<0.12.0)"]
doc = ["markdown-include (>=0.5.1,<0.6.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=5.5.0,<6.0.0)"]
test = ["coveralls (==2.1.2)", "pytest (==6.0.1)", "pytest-cov (==2.10.0)"]
[package.source]
type = "git"
url = "https://github.com/vvpreo/fastapi-jwt-auth"
reference = "HEAD"
resolved_reference = "6876598ec846a3b21774ddd45daf427b995e36e0"
[[package]]
name = "greenlet"
version = "3.1.1"
@ -838,22 +775,20 @@ files = [
[[package]]
name = "pydantic"
version = "2.11.3"
version = "2.10.6"
description = "Data validation using Python type hints"
optional = false
python-versions = ">=3.9"
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "pydantic-2.11.3-py3-none-any.whl", hash = "sha256:a082753436a07f9ba1289c6ffa01cd93db3548776088aa917cc43b63f68fa60f"},
{file = "pydantic-2.11.3.tar.gz", hash = "sha256:7471657138c16adad9322fe3070c0116dd6c3ad8d649300e3cbdfe91f4db4ec3"},
{file = "pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584"},
{file = "pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236"},
]
[package.dependencies]
annotated-types = ">=0.6.0"
email-validator = {version = ">=2.0.0", optional = true, markers = "extra == \"email\""}
pydantic-core = "2.33.1"
pydantic-core = "2.27.2"
typing-extensions = ">=4.12.2"
typing-inspection = ">=0.4.0"
[package.extras]
email = ["email-validator (>=2.0.0)"]
@ -861,111 +796,112 @@ timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows
[[package]]
name = "pydantic-core"
version = "2.33.1"
version = "2.27.2"
description = "Core functionality for Pydantic validation and serialization"
optional = false
python-versions = ">=3.9"
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "pydantic_core-2.33.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3077cfdb6125cc8dab61b155fdd714663e401f0e6883f9632118ec12cf42df26"},
{file = "pydantic_core-2.33.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8ffab8b2908d152e74862d276cf5017c81a2f3719f14e8e3e8d6b83fda863927"},
{file = "pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5183e4f6a2d468787243ebcd70cf4098c247e60d73fb7d68d5bc1e1beaa0c4db"},
{file = "pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:398a38d323f37714023be1e0285765f0a27243a8b1506b7b7de87b647b517e48"},
{file = "pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:87d3776f0001b43acebfa86f8c64019c043b55cc5a6a2e313d728b5c95b46969"},
{file = "pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c566dd9c5f63d22226409553531f89de0cac55397f2ab8d97d6f06cfce6d947e"},
{file = "pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0d5f3acc81452c56895e90643a625302bd6be351e7010664151cc55b7b97f89"},
{file = "pydantic_core-2.33.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d3a07fadec2a13274a8d861d3d37c61e97a816beae717efccaa4b36dfcaadcde"},
{file = "pydantic_core-2.33.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f99aeda58dce827f76963ee87a0ebe75e648c72ff9ba1174a253f6744f518f65"},
{file = "pydantic_core-2.33.1-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:902dbc832141aa0ec374f4310f1e4e7febeebc3256f00dc359a9ac3f264a45dc"},
{file = "pydantic_core-2.33.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fe44d56aa0b00d66640aa84a3cbe80b7a3ccdc6f0b1ca71090696a6d4777c091"},
{file = "pydantic_core-2.33.1-cp310-cp310-win32.whl", hash = "sha256:ed3eb16d51257c763539bde21e011092f127a2202692afaeaccb50db55a31383"},
{file = "pydantic_core-2.33.1-cp310-cp310-win_amd64.whl", hash = "sha256:694ad99a7f6718c1a498dc170ca430687a39894a60327f548e02a9c7ee4b6504"},
{file = "pydantic_core-2.33.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6e966fc3caaf9f1d96b349b0341c70c8d6573bf1bac7261f7b0ba88f96c56c24"},
{file = "pydantic_core-2.33.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bfd0adeee563d59c598ceabddf2c92eec77abcb3f4a391b19aa7366170bd9e30"},
{file = "pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91815221101ad3c6b507804178a7bb5cb7b2ead9ecd600041669c8d805ebd595"},
{file = "pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9fea9c1869bb4742d174a57b4700c6dadea951df8b06de40c2fedb4f02931c2e"},
{file = "pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d20eb4861329bb2484c021b9d9a977566ab16d84000a57e28061151c62b349a"},
{file = "pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb935c5591573ae3201640579f30128ccc10739b45663f93c06796854405505"},
{file = "pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c964fd24e6166420d18fb53996d8c9fd6eac9bf5ae3ec3d03015be4414ce497f"},
{file = "pydantic_core-2.33.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:681d65e9011f7392db5aa002b7423cc442d6a673c635668c227c6c8d0e5a4f77"},
{file = "pydantic_core-2.33.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e100c52f7355a48413e2999bfb4e139d2977a904495441b374f3d4fb4a170961"},
{file = "pydantic_core-2.33.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:048831bd363490be79acdd3232f74a0e9951b11b2b4cc058aeb72b22fdc3abe1"},
{file = "pydantic_core-2.33.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:bdc84017d28459c00db6f918a7272a5190bec3090058334e43a76afb279eac7c"},
{file = "pydantic_core-2.33.1-cp311-cp311-win32.whl", hash = "sha256:32cd11c5914d1179df70406427097c7dcde19fddf1418c787540f4b730289896"},
{file = "pydantic_core-2.33.1-cp311-cp311-win_amd64.whl", hash = "sha256:2ea62419ba8c397e7da28a9170a16219d310d2cf4970dbc65c32faf20d828c83"},
{file = "pydantic_core-2.33.1-cp311-cp311-win_arm64.whl", hash = "sha256:fc903512177361e868bc1f5b80ac8c8a6e05fcdd574a5fb5ffeac5a9982b9e89"},
{file = "pydantic_core-2.33.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:1293d7febb995e9d3ec3ea09caf1a26214eec45b0f29f6074abb004723fc1de8"},
{file = "pydantic_core-2.33.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:99b56acd433386c8f20be5c4000786d1e7ca0523c8eefc995d14d79c7a081498"},
{file = "pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35a5ec3fa8c2fe6c53e1b2ccc2454398f95d5393ab398478f53e1afbbeb4d939"},
{file = "pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b172f7b9d2f3abc0efd12e3386f7e48b576ef309544ac3a63e5e9cdd2e24585d"},
{file = "pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9097b9f17f91eea659b9ec58148c0747ec354a42f7389b9d50701610d86f812e"},
{file = "pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc77ec5b7e2118b152b0d886c7514a4653bcb58c6b1d760134a9fab915f777b3"},
{file = "pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5e3d15245b08fa4a84cefc6c9222e6f37c98111c8679fbd94aa145f9a0ae23d"},
{file = "pydantic_core-2.33.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ef99779001d7ac2e2461d8ab55d3373fe7315caefdbecd8ced75304ae5a6fc6b"},
{file = "pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:fc6bf8869e193855e8d91d91f6bf59699a5cdfaa47a404e278e776dd7f168b39"},
{file = "pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:b1caa0bc2741b043db7823843e1bde8aaa58a55a58fda06083b0569f8b45693a"},
{file = "pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ec259f62538e8bf364903a7d0d0239447059f9434b284f5536e8402b7dd198db"},
{file = "pydantic_core-2.33.1-cp312-cp312-win32.whl", hash = "sha256:e14f369c98a7c15772b9da98987f58e2b509a93235582838bd0d1d8c08b68fda"},
{file = "pydantic_core-2.33.1-cp312-cp312-win_amd64.whl", hash = "sha256:1c607801d85e2e123357b3893f82c97a42856192997b95b4d8325deb1cd0c5f4"},
{file = "pydantic_core-2.33.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d13f0276806ee722e70a1c93da19748594f19ac4299c7e41237fc791d1861ea"},
{file = "pydantic_core-2.33.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:70af6a21237b53d1fe7b9325b20e65cbf2f0a848cf77bed492b029139701e66a"},
{file = "pydantic_core-2.33.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:282b3fe1bbbe5ae35224a0dbd05aed9ccabccd241e8e6b60370484234b456266"},
{file = "pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b315e596282bbb5822d0c7ee9d255595bd7506d1cb20c2911a4da0b970187d3"},
{file = "pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1dfae24cf9921875ca0ca6a8ecb4bb2f13c855794ed0d468d6abbec6e6dcd44a"},
{file = "pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6dd8ecfde08d8bfadaea669e83c63939af76f4cf5538a72597016edfa3fad516"},
{file = "pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2f593494876eae852dc98c43c6f260f45abdbfeec9e4324e31a481d948214764"},
{file = "pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:948b73114f47fd7016088e5186d13faf5e1b2fe83f5e320e371f035557fd264d"},
{file = "pydantic_core-2.33.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e11f3864eb516af21b01e25fac915a82e9ddad3bb0fb9e95a246067398b435a4"},
{file = "pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:549150be302428b56fdad0c23c2741dcdb5572413776826c965619a25d9c6bde"},
{file = "pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:495bc156026efafd9ef2d82372bd38afce78ddd82bf28ef5276c469e57c0c83e"},
{file = "pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ec79de2a8680b1a67a07490bddf9636d5c2fab609ba8c57597e855fa5fa4dacd"},
{file = "pydantic_core-2.33.1-cp313-cp313-win32.whl", hash = "sha256:ee12a7be1742f81b8a65b36c6921022301d466b82d80315d215c4c691724986f"},
{file = "pydantic_core-2.33.1-cp313-cp313-win_amd64.whl", hash = "sha256:ede9b407e39949d2afc46385ce6bd6e11588660c26f80576c11c958e6647bc40"},
{file = "pydantic_core-2.33.1-cp313-cp313-win_arm64.whl", hash = "sha256:aa687a23d4b7871a00e03ca96a09cad0f28f443690d300500603bd0adba4b523"},
{file = "pydantic_core-2.33.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:401d7b76e1000d0dd5538e6381d28febdcacb097c8d340dde7d7fc6e13e9f95d"},
{file = "pydantic_core-2.33.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7aeb055a42d734c0255c9e489ac67e75397d59c6fbe60d155851e9782f276a9c"},
{file = "pydantic_core-2.33.1-cp313-cp313t-win_amd64.whl", hash = "sha256:338ea9b73e6e109f15ab439e62cb3b78aa752c7fd9536794112e14bee02c8d18"},
{file = "pydantic_core-2.33.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:5ab77f45d33d264de66e1884fca158bc920cb5e27fd0764a72f72f5756ae8bdb"},
{file = "pydantic_core-2.33.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e7aaba1b4b03aaea7bb59e1b5856d734be011d3e6d98f5bcaa98cb30f375f2ad"},
{file = "pydantic_core-2.33.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7fb66263e9ba8fea2aa85e1e5578980d127fb37d7f2e292773e7bc3a38fb0c7b"},
{file = "pydantic_core-2.33.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3f2648b9262607a7fb41d782cc263b48032ff7a03a835581abbf7a3bec62bcf5"},
{file = "pydantic_core-2.33.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:723c5630c4259400818b4ad096735a829074601805d07f8cafc366d95786d331"},
{file = "pydantic_core-2.33.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d100e3ae783d2167782391e0c1c7a20a31f55f8015f3293647544df3f9c67824"},
{file = "pydantic_core-2.33.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:177d50460bc976a0369920b6c744d927b0ecb8606fb56858ff542560251b19e5"},
{file = "pydantic_core-2.33.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a3edde68d1a1f9af1273b2fe798997b33f90308fb6d44d8550c89fc6a3647cf6"},
{file = "pydantic_core-2.33.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a62c3c3ef6a7e2c45f7853b10b5bc4ddefd6ee3cd31024754a1a5842da7d598d"},
{file = "pydantic_core-2.33.1-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:c91dbb0ab683fa0cd64a6e81907c8ff41d6497c346890e26b23de7ee55353f96"},
{file = "pydantic_core-2.33.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9f466e8bf0a62dc43e068c12166281c2eca72121dd2adc1040f3aa1e21ef8599"},
{file = "pydantic_core-2.33.1-cp39-cp39-win32.whl", hash = "sha256:ab0277cedb698749caada82e5d099dc9fed3f906a30d4c382d1a21725777a1e5"},
{file = "pydantic_core-2.33.1-cp39-cp39-win_amd64.whl", hash = "sha256:5773da0ee2d17136b1f1c6fbde543398d452a6ad2a7b54ea1033e2daa739b8d2"},
{file = "pydantic_core-2.33.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c834f54f8f4640fd7e4b193f80eb25a0602bba9e19b3cd2fc7ffe8199f5ae02"},
{file = "pydantic_core-2.33.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:049e0de24cf23766f12cc5cc71d8abc07d4a9deb9061b334b62093dedc7cb068"},
{file = "pydantic_core-2.33.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a28239037b3d6f16916a4c831a5a0eadf856bdd6d2e92c10a0da3a59eadcf3e"},
{file = "pydantic_core-2.33.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d3da303ab5f378a268fa7d45f37d7d85c3ec19769f28d2cc0c61826a8de21fe"},
{file = "pydantic_core-2.33.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:25626fb37b3c543818c14821afe0fd3830bc327a43953bc88db924b68c5723f1"},
{file = "pydantic_core-2.33.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3ab2d36e20fbfcce8f02d73c33a8a7362980cff717926bbae030b93ae46b56c7"},
{file = "pydantic_core-2.33.1-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:2f9284e11c751b003fd4215ad92d325d92c9cb19ee6729ebd87e3250072cdcde"},
{file = "pydantic_core-2.33.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:048c01eee07d37cbd066fc512b9d8b5ea88ceeb4e629ab94b3e56965ad655add"},
{file = "pydantic_core-2.33.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5ccd429694cf26af7997595d627dd2637e7932214486f55b8a357edaac9dae8c"},
{file = "pydantic_core-2.33.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3a371dc00282c4b84246509a5ddc808e61b9864aa1eae9ecc92bb1268b82db4a"},
{file = "pydantic_core-2.33.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f59295ecc75a1788af8ba92f2e8c6eeaa5a94c22fc4d151e8d9638814f85c8fc"},
{file = "pydantic_core-2.33.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08530b8ac922003033f399128505f513e30ca770527cc8bbacf75a84fcc2c74b"},
{file = "pydantic_core-2.33.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bae370459da6a5466978c0eacf90690cb57ec9d533f8e63e564ef3822bfa04fe"},
{file = "pydantic_core-2.33.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e3de2777e3b9f4d603112f78006f4ae0acb936e95f06da6cb1a45fbad6bdb4b5"},
{file = "pydantic_core-2.33.1-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3a64e81e8cba118e108d7126362ea30e021291b7805d47e4896e52c791be2761"},
{file = "pydantic_core-2.33.1-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:52928d8c1b6bda03cc6d811e8923dffc87a2d3c8b3bfd2ce16471c7147a24850"},
{file = "pydantic_core-2.33.1-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:1b30d92c9412beb5ac6b10a3eb7ef92ccb14e3f2a8d7732e2d739f58b3aa7544"},
{file = "pydantic_core-2.33.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f995719707e0e29f0f41a8aa3bcea6e761a36c9136104d3189eafb83f5cec5e5"},
{file = "pydantic_core-2.33.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7edbc454a29fc6aeae1e1eecba4f07b63b8d76e76a748532233c4c167b4cb9ea"},
{file = "pydantic_core-2.33.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:ad05b683963f69a1d5d2c2bdab1274a31221ca737dbbceaa32bcb67359453cdd"},
{file = "pydantic_core-2.33.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df6a94bf9452c6da9b5d76ed229a5683d0306ccb91cca8e1eea883189780d568"},
{file = "pydantic_core-2.33.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7965c13b3967909a09ecc91f21d09cfc4576bf78140b988904e94f130f188396"},
{file = "pydantic_core-2.33.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3f1fdb790440a34f6ecf7679e1863b825cb5ffde858a9197f851168ed08371e5"},
{file = "pydantic_core-2.33.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:5277aec8d879f8d05168fdd17ae811dd313b8ff894aeeaf7cd34ad28b4d77e33"},
{file = "pydantic_core-2.33.1-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:8ab581d3530611897d863d1a649fb0644b860286b4718db919bfd51ece41f10b"},
{file = "pydantic_core-2.33.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0483847fa9ad5e3412265c1bd72aad35235512d9ce9d27d81a56d935ef489672"},
{file = "pydantic_core-2.33.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:de9e06abe3cc5ec6a2d5f75bc99b0bdca4f5c719a5b34026f8c57efbdecd2ee3"},
{file = "pydantic_core-2.33.1.tar.gz", hash = "sha256:bcc9c6fdb0ced789245b02b7d6603e17d1563064ddcfc36f046b61c0c05dd9df"},
{file = "pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa"},
{file = "pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c"},
{file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a"},
{file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5"},
{file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c"},
{file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7"},
{file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a"},
{file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236"},
{file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962"},
{file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9"},
{file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af"},
{file = "pydantic_core-2.27.2-cp310-cp310-win32.whl", hash = "sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4"},
{file = "pydantic_core-2.27.2-cp310-cp310-win_amd64.whl", hash = "sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31"},
{file = "pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc"},
{file = "pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7"},
{file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15"},
{file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306"},
{file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99"},
{file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459"},
{file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048"},
{file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d"},
{file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b"},
{file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474"},
{file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6"},
{file = "pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c"},
{file = "pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc"},
{file = "pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4"},
{file = "pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0"},
{file = "pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef"},
{file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7"},
{file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934"},
{file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6"},
{file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c"},
{file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2"},
{file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4"},
{file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3"},
{file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4"},
{file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57"},
{file = "pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc"},
{file = "pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9"},
{file = "pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b"},
{file = "pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b"},
{file = "pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154"},
{file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9"},
{file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9"},
{file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1"},
{file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a"},
{file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e"},
{file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4"},
{file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27"},
{file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee"},
{file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1"},
{file = "pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130"},
{file = "pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee"},
{file = "pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b"},
{file = "pydantic_core-2.27.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d3e8d504bdd3f10835468f29008d72fc8359d95c9c415ce6e767203db6127506"},
{file = "pydantic_core-2.27.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:521eb9b7f036c9b6187f0b47318ab0d7ca14bd87f776240b90b21c1f4f149320"},
{file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85210c4d99a0114f5a9481b44560d7d1e35e32cc5634c656bc48e590b669b145"},
{file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d716e2e30c6f140d7560ef1538953a5cd1a87264c737643d481f2779fc247fe1"},
{file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f66d89ba397d92f840f8654756196d93804278457b5fbede59598a1f9f90b228"},
{file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:669e193c1c576a58f132e3158f9dfa9662969edb1a250c54d8fa52590045f046"},
{file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdbe7629b996647b99c01b37f11170a57ae675375b14b8c13b8518b8320ced5"},
{file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d262606bf386a5ba0b0af3b97f37c83d7011439e3dc1a9298f21efb292e42f1a"},
{file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cabb9bcb7e0d97f74df8646f34fc76fbf793b7f6dc2438517d7a9e50eee4f14d"},
{file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:d2d63f1215638d28221f664596b1ccb3944f6e25dd18cd3b86b0a4c408d5ebb9"},
{file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bca101c00bff0adb45a833f8451b9105d9df18accb8743b08107d7ada14bd7da"},
{file = "pydantic_core-2.27.2-cp38-cp38-win32.whl", hash = "sha256:f6f8e111843bbb0dee4cb6594cdc73e79b3329b526037ec242a3e49012495b3b"},
{file = "pydantic_core-2.27.2-cp38-cp38-win_amd64.whl", hash = "sha256:fd1aea04935a508f62e0d0ef1f5ae968774a32afc306fb8545e06f5ff5cdf3ad"},
{file = "pydantic_core-2.27.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c10eb4f1659290b523af58fa7cffb452a61ad6ae5613404519aee4bfbf1df993"},
{file = "pydantic_core-2.27.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ef592d4bad47296fb11f96cd7dc898b92e795032b4894dfb4076cfccd43a9308"},
{file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c61709a844acc6bf0b7dce7daae75195a10aac96a596ea1b776996414791ede4"},
{file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c5f762659e47fdb7b16956c71598292f60a03aa92f8b6351504359dbdba6cf"},
{file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c9775e339e42e79ec99c441d9730fccf07414af63eac2f0e48e08fd38a64d76"},
{file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57762139821c31847cfb2df63c12f725788bd9f04bc2fb392790959b8f70f118"},
{file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d1e85068e818c73e048fe28cfc769040bb1f475524f4745a5dc621f75ac7630"},
{file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:097830ed52fd9e427942ff3b9bc17fab52913b2f50f2880dc4a5611446606a54"},
{file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:044a50963a614ecfae59bb1eaf7ea7efc4bc62f49ed594e18fa1e5d953c40e9f"},
{file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:4e0b4220ba5b40d727c7f879eac379b822eee5d8fff418e9d3381ee45b3b0362"},
{file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5e4f4bb20d75e9325cc9696c6802657b58bc1dbbe3022f32cc2b2b632c3fbb96"},
{file = "pydantic_core-2.27.2-cp39-cp39-win32.whl", hash = "sha256:cca63613e90d001b9f2f9a9ceb276c308bfa2a43fafb75c8031c4f66039e8c6e"},
{file = "pydantic_core-2.27.2-cp39-cp39-win_amd64.whl", hash = "sha256:77d1bca19b0f7021b3a982e6f903dcd5b2b06076def36a652e3907f596e29f67"},
{file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e"},
{file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8"},
{file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3"},
{file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f"},
{file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133"},
{file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc"},
{file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50"},
{file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9"},
{file = "pydantic_core-2.27.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151"},
{file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c33939a82924da9ed65dab5a65d427205a73181d8098e79b6b426bdf8ad4e656"},
{file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:00bad2484fa6bda1e216e7345a798bd37c68fb2d97558edd584942aa41b7d278"},
{file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c817e2b40aba42bac6f457498dacabc568c3b7a986fc9ba7c8d9d260b71485fb"},
{file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:251136cdad0cb722e93732cb45ca5299fb56e1344a833640bf93b2803f8d1bfd"},
{file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d2088237af596f0a524d3afc39ab3b036e8adb054ee57cbb1dcf8e09da5b29cc"},
{file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d4041c0b966a84b4ae7a09832eb691a35aec90910cd2dbe7a208de59be77965b"},
{file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:8083d4e875ebe0b864ffef72a4304827015cff328a1be6e22cc850753bfb122b"},
{file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f141ee28a0ad2123b6611b6ceff018039df17f32ada8b534e6aa039545a3efb2"},
{file = "pydantic_core-2.27.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7d0c8399fcc1848491f00e0314bd59fb34a9c008761bcb422a057670c3f65e35"},
{file = "pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39"},
]
[package.dependencies]
@ -992,23 +928,6 @@ azure-key-vault = ["azure-identity (>=1.16.0)", "azure-keyvault-secrets (>=4.8.0
toml = ["tomli (>=2.0.1)"]
yaml = ["pyyaml (>=6.0.1)"]
[[package]]
name = "pyjwt"
version = "1.7.1"
description = "JSON Web Token implementation in Python"
optional = false
python-versions = "*"
groups = ["main"]
files = [
{file = "PyJWT-1.7.1-py2.py3-none-any.whl", hash = "sha256:5c6eca3c2940464d106b99ba83b00c6add741c9becaec087fb7ccdefea71350e"},
{file = "PyJWT-1.7.1.tar.gz", hash = "sha256:8d59a976fb773f3e6a39c85636357c4f0e242707394cadadd9814f5cbaa20e96"},
]
[package.extras]
crypto = ["cryptography (>=1.4)"]
flake8 = ["flake8", "flake8-import-order", "pep8-naming"]
test = ["pytest (>=4.0.1,<5.0.0)", "pytest-cov (>=2.6.0,<3.0.0)", "pytest-runner (>=4.2,<5.0.0)"]
[[package]]
name = "pymysql"
version = "1.1.1"
@ -1040,18 +959,6 @@ files = [
[package.extras]
cli = ["click (>=5.0)"]
[[package]]
name = "python-multipart"
version = "0.0.20"
description = "A streaming multipart parser for Python"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104"},
{file = "python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13"},
]
[[package]]
name = "sniffio"
version = "1.3.1"
@ -1192,21 +1099,6 @@ files = [
{file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"},
]
[[package]]
name = "typing-inspection"
version = "0.4.0"
description = "Runtime typing introspection tools"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f"},
{file = "typing_inspection-0.4.0.tar.gz", hash = "sha256:9765c87de36671694a67904bf2c96e395be9c6439bb6c87b5142569dcdd65122"},
]
[package.dependencies]
typing-extensions = ">=4.12.0"
[[package]]
name = "uvicorn"
version = "0.34.0"
@ -1342,4 +1234,4 @@ propcache = ">=0.2.0"
[metadata]
lock-version = "2.1"
python-versions = ">=3.11,<4.0"
content-hash = "146687a6e082e27748cc339242d924d2fb0741f7f2eb842a025e137f5fb41378"
content-hash = "41aad583954d7d7884829d1082d0044c9d5b43d29f1d0522df6c2990a2e23336"

View File

@ -16,9 +16,6 @@ dependencies = [
"loguru (>=0.7.3,<0.8.0)",
"pydantic-settings (>=2.8.1,<3.0.0)",
"cryptography (>=44.0.2,<45.0.0)",
"pydantic[email] (>=2.11.3,<3.0.0)",
"python-multipart (>=0.0.20,<0.0.21)",
"fastapi-jwt-auth @ git+https://github.com/vvpreo/fastapi-jwt-auth",
]

1174
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +1,9 @@
{
"name": "client",
"version": "0.0.1",
"version": "0.0.2",
"private": true,
"dependencies": {
"@ant-design/icons": "^5.6.1",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.2.0",
@ -11,8 +12,13 @@
"@types/node": "^16.18.126",
"@types/react": "^19.0.11",
"@types/react-dom": "^19.0.4",
"antd": "^5.24.7",
"i18next": "^25.0.1",
"i18next-browser-languagedetector": "^8.0.5",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-i18next": "^15.5.1",
"react-router-dom": "^7.5.0",
"react-scripts": "5.0.1",
"typescript": "^4.9.5",
"web-vitals": "^2.1.4"

View File

@ -0,0 +1,3 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 18C1.45 18 0.979167 17.8042 0.5875 17.4125C0.195833 17.0208 0 16.55 0 16V2C0 1.45 0.195833 0.979167 0.5875 0.5875C0.979167 0.195833 1.45 0 2 0H10V2H2V16H16V8H18V16C18 16.55 17.8042 17.0208 17.4125 17.4125C17.0208 17.8042 16.55 18 16 18H2ZM3 14H15L11.25 9L8.25 13L6 10L3 14ZM14 6V4H12V2H14V0H16V2H18V4H16V6H14Z" fill="#606060"/>
</svg>

After

Width:  |  Height:  |  Size: 443 B

View File

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.825 9L9.425 14.6L8 16L0 8L8 0L9.425 1.4L3.825 7H16V9H3.825Z" fill="#606060"/>
</svg>

After

Width:  |  Height:  |  Size: 193 B

View File

@ -0,0 +1,3 @@
<svg width="16" height="18" viewBox="0 0 16 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 18C2.45 18 1.97917 17.8042 1.5875 17.4125C1.19583 17.0208 1 16.55 1 16V3H0V1H5V0H11V1H16V3H15V16C15 16.55 14.8042 17.0208 14.4125 17.4125C14.0208 17.8042 13.55 18 13 18H3ZM13 3H3V16H13V3ZM5 14H7V5H5V14ZM9 14H11V5H9V14Z" fill="#FF0000"/>
</svg>

After

Width:  |  Height:  |  Size: 352 B

View File

@ -0,0 +1,3 @@
<svg width="21" height="17" viewBox="0 0 21 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.5 16V13.2C0.5 12.65 0.641667 12.1333 0.925 11.65C1.20833 11.1667 1.6 10.8 2.1 10.55C2.95 10.1167 3.90833 9.75 4.975 9.45C6.04167 9.15 7.21667 9 8.5 9C9 9 9.4875 9.025 9.9625 9.075C10.4375 9.125 10.9 9.2 11.35 9.3L9.6 11.05C9.41667 11.0167 9.2375 11 9.0625 11H8.5C7.31667 11 6.25417 11.1417 5.3125 11.425C4.37083 11.7083 3.6 12.0167 3 12.35C2.85 12.4333 2.72917 12.55 2.6375 12.7C2.54583 12.85 2.5 13.0167 2.5 13.2V14H8.75L10.75 16H0.5ZM14.05 16.4L10.6 12.95L12 11.55L14.05 13.6L19.1 8.55L20.5 9.95L14.05 16.4ZM8.5 8C7.4 8 6.45833 7.60833 5.675 6.825C4.89167 6.04167 4.5 5.1 4.5 4C4.5 2.9 4.89167 1.95833 5.675 1.175C6.45833 0.391667 7.4 0 8.5 0C9.6 0 10.5417 0.391667 11.325 1.175C12.1083 1.95833 12.5 2.9 12.5 4C12.5 5.1 12.1083 6.04167 11.325 6.825C10.5417 7.60833 9.6 8 8.5 8ZM8.5 6C9.05 6 9.52083 5.80417 9.9125 5.4125C10.3042 5.02083 10.5 4.55 10.5 4C10.5 3.45 10.3042 2.97917 9.9125 2.5875C9.52083 2.19583 9.05 2 8.5 2C7.95 2 7.47917 2.19583 7.0875 2.5875C6.69583 2.97917 6.5 3.45 6.5 4C6.5 4.55 6.69583 5.02083 7.0875 5.4125C7.47917 5.80417 7.95 6 8.5 6Z" fill="#606060"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,3 @@
<svg width="19" height="18" viewBox="0 0 19 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18.5 4V16C18.5 16.55 18.3042 17.0208 17.9125 17.4125C17.5208 17.8042 17.05 18 16.5 18H2.5C1.95 18 1.47917 17.8042 1.0875 17.4125C0.695833 17.0208 0.5 16.55 0.5 16V2C0.5 1.45 0.695833 0.979167 1.0875 0.5875C1.47917 0.195833 1.95 0 2.5 0H14.5L18.5 4ZM16.5 4.85L13.65 2H2.5V16H16.5V4.85ZM9.5 15C10.3333 15 11.0417 14.7083 11.625 14.125C12.2083 13.5417 12.5 12.8333 12.5 12C12.5 11.1667 12.2083 10.4583 11.625 9.875C11.0417 9.29167 10.3333 9 9.5 9C8.66667 9 7.95833 9.29167 7.375 9.875C6.79167 10.4583 6.5 11.1667 6.5 12C6.5 12.8333 6.79167 13.5417 7.375 14.125C7.95833 14.7083 8.66667 15 9.5 15ZM3.5 7H12.5V3H3.5V7Z" fill="#606060"/>
</svg>

After

Width:  |  Height:  |  Size: 744 B

View File

@ -0,0 +1,3 @@
<svg width="19" height="18" viewBox="0 0 19 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.125 18V10H0.125V8H8.125V0H10.125V8H18.125V10H10.125V18H8.125Z" fill="#606060"/>
</svg>

After

Width:  |  Height:  |  Size: 195 B

View File

@ -0,0 +1,3 @@
<svg width="4" height="16" viewBox="0 0 4 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M2 4C3.1 4 4 3.1 4 2C4 0.9 3.1 0 2 0C0.9 0 0 0.9 0 2C0 3.1 0.9 4 2 4ZM2 6C0.9 6 0 6.9 0 8C0 9.1 0.9 10 2 10C3.1 10 4 9.1 4 8C4 6.9 3.1 6 2 6ZM2 12C0.9 12 0 12.9 0 14C0 15.1 0.9 16 2 16C3.1 16 4 15.1 4 14C4 12.9 3.1 12 2 12Z" fill="#606060"/>
</svg>

After

Width:  |  Height:  |  Size: 392 B

View File

@ -0,0 +1,3 @@
<svg width="18" height="12" viewBox="0 0 18 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 12V10H18V12H0ZM0 7V5H18V7H0ZM0 2V0H18V2H0Z" fill="#606060"/>
</svg>

After

Width:  |  Height:  |  Size: 176 B

View File

@ -0,0 +1,3 @@
<svg width="18" height="12" viewBox="0 0 18 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 12V10H13V12H0ZM16.6 11L11.6 6L16.6 1L18 2.4L14.4 6L18 9.6L16.6 11ZM0 7V5H10V7H0ZM0 2V0H13V2H0Z" fill="#606060"/>
</svg>

After

Width:  |  Height:  |  Size: 228 B

View File

@ -0,0 +1,3 @@
<svg width="18" height="19" viewBox="0 0 18 19" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.4 19L12 17.6L13.575 16L12 14.425L13.4 13L15 14.6L16.575 13L18 14.425L16.4 16L18 17.6L16.575 19L15 17.425L13.4 19ZM3 17C3.28333 17 3.52083 16.9042 3.7125 16.7125C3.90417 16.5208 4 16.2833 4 16C4 15.7167 3.90417 15.4792 3.7125 15.2875C3.52083 15.0958 3.28333 15 3 15C2.71667 15 2.47917 15.0958 2.2875 15.2875C2.09583 15.4792 2 15.7167 2 16C2 16.2833 2.09583 16.5208 2.2875 16.7125C2.47917 16.9042 2.71667 17 3 17ZM3 19C2.16667 19 1.45833 18.7083 0.875 18.125C0.291667 17.5417 0 16.8333 0 16C0 15.1667 0.291667 14.4583 0.875 13.875C1.45833 13.2917 2.16667 13 3 13C3.61667 13 4.17917 13.1708 4.6875 13.5125C5.19583 13.8542 5.56667 14.3167 5.8 14.9C6.45 14.7167 6.97917 14.3583 7.3875 13.825C7.79583 13.2917 8 12.6833 8 12V8C8 6.61667 8.4875 5.4375 9.4625 4.4625C10.4375 3.4875 11.6167 3 13 3H14.15L12.575 1.425L14 0L18 4L14 8L12.575 6.6L14.15 5H13C12.1667 5 11.4583 5.29167 10.875 5.875C10.2917 6.45833 10 7.16667 10 8V12C10 13.2167 9.60833 14.2875 8.825 15.2125C8.04167 16.1375 7.05 16.7083 5.85 16.925C5.65 17.5417 5.2875 18.0417 4.7625 18.425C4.2375 18.8083 3.65 19 3 19ZM1.4 7L0 5.6L1.575 4L0 2.425L1.4 1L3 2.6L4.575 1L6 2.425L4.4 4L6 5.6L4.575 7L3 5.425L1.4 7Z" fill="#606060"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,3 @@
<svg width="20" height="16" viewBox="0 0 20 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13 16V14H9V9H7V11H0V5H7V7H9V2H13V0H20V6H13V4H11V12H13V10H20V16H13ZM15 14H18V12H15V14ZM2 9H5V7H2V9ZM15 4H18V2H15V4Z" fill="#606060"/>
</svg>

After

Width:  |  Height:  |  Size: 246 B

View File

@ -0,0 +1,3 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 18V12H10V14H18V16H10V18H8ZM0 16V14H6V16H0ZM4 12V10H0V8H4V6H6V12H4ZM8 10V8H18V10H8ZM12 6V0H14V2H18V4H14V6H12ZM0 4V2H10V4H0Z" fill="#606060"/>
</svg>

After

Width:  |  Height:  |  Size: 256 B

View File

@ -24,7 +24,7 @@
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
<title>VORKOUT</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>

View File

@ -1,7 +1,19 @@
import React from 'react';
import { Route, Routes } from 'react-router-dom';
import MainLayout from './pages/MainLayout';
import ProtectedRoute from './pages/ProtectedRoute';
function App() {
return <div className="App"></div>;
return (
<div className="App">
<Routes>
<Route path="/login" element={<div>login</div>} />
<Route element={<ProtectedRoute />}>
<Route path="*" element={<MainLayout />}></Route>
</Route>
</Routes>
</div>
);
}
export default App;

View File

@ -0,0 +1,161 @@
import { Drawer } from 'antd';
import { useEffect, useState } from 'react';
import { Avatar, Typography } from 'antd';
import { useTranslation } from 'react-i18next';
interface ContentDrawerProps {
open: boolean;
closeDrawer: () => void;
children: React.ReactNode;
type: 'create' | 'edit';
}
export default function ContentDrawer({
open,
closeDrawer,
children,
type,
}: ContentDrawerProps) {
const { t } = useTranslation();
const [width, setWidth] = useState<number | string>('30%');
const calculateWidths = () => {
const windowWidth = window.innerWidth;
const expanded = Math.max(windowWidth * 0.3, 300);
setWidth(expanded);
};
useEffect(() => {
calculateWidths();
window.addEventListener('resize', calculateWidths);
return () => window.removeEventListener('resize', calculateWidths);
}, []);
const editDrawerTitle = (
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
gap: 12,
}}
>
<div
onClick={closeDrawer}
style={{
display: 'flex',
alignItems: 'center',
height: '24px',
width: '24px',
cursor: 'pointer',
}}
>
<img
src="./icons/drawer/arrow_back.svg"
alt="close_drawer"
style={{ height: '16px', width: '16px' }}
/>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, flex: 1 }}>
<Avatar
src="https://cdn-icons-png.flaticon.com/512/219/219986.png"
size={40}
style={{ flexShrink: 0 }}
/>
<div>
<Typography.Text strong style={{ display: 'block' }}>
Александр Александров
</Typography.Text>
<Typography.Text type="secondary" style={{ fontSize: 14 }}>
alexandralex@vorkout.ru
</Typography.Text>
</div>
</div>
<div
style={{
display: 'flex',
alignItems: 'center',
height: '24px',
width: '24px',
}}
>
<img
src="./icons/drawer/delete.svg"
alt="delete"
style={{ height: '18px', width: '16px' }}
/>
</div>
</div>
);
const createDrawerTitle = (
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
gap: 12,
}}
>
<div
onClick={closeDrawer}
style={{
display: 'flex',
alignItems: 'center',
height: '24px',
width: '24px',
cursor: 'pointer',
}}
>
<img
src="./icons/drawer/arrow_back.svg"
alt="close_drawer"
style={{ height: '16px', width: '16px' }}
/>
</div>
<div
style={{
display: 'flex',
alignItems: 'center',
gap: 12,
flex: 1,
fontSize: '20px',
}}
>
{t('newAccount')}
</div>
<div
style={{
display: 'flex',
alignItems: 'center',
height: '24px',
width: '24px',
}}
onClick={closeDrawer}
>
<img
src="./icons/drawer/delete.svg"
alt="delete"
style={{ height: '18px', width: '16px' }}
/>
</div>
</div>
);
return (
<Drawer
title={type === 'create' ? createDrawerTitle : editDrawerTitle}
placement="right"
open={open}
width={width}
destroyOnClose={true}
closable={false}
>
{children}
</Drawer>
);
}

View File

@ -0,0 +1,55 @@
import { Avatar } from 'antd';
import Title from 'antd/es/typography/Title';
interface HeaderProps {
title: string;
additionalContent?: React.ReactNode;
}
export default function Header({ title, additionalContent }: HeaderProps) {
return (
<div
style={{
height: '72px',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
}}
>
<Title style={{ marginLeft: '16px' }} level={3}>
{title}
</Title>
<div
style={{
display: 'flex',
alignItems: 'center',
gap: '24px',
marginRight: '26px',
}}
>
{additionalContent}
<img
src="./icons/header/more.svg"
alt="more"
style={{ height: '16px', width: '16px' }}
/>
<div
style={{
borderRadius: '50%',
border: '2px solid #8BC34A',
height: '32px',
width: '32px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<Avatar
size={25.77}
src={`https://cdn-icons-png.flaticon.com/512/219/219986.png`}
/>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,118 @@
import { Divider, Menu, Tooltip } from 'antd';
import React from 'react';
import { useTranslation } from 'react-i18next';
interface SiderMenuProps {
collapsed: boolean;
selectedKey: string;
hangleMenuClick: (e: any) => void;
}
export default function SiderMenu({
collapsed,
selectedKey,
hangleMenuClick,
}: SiderMenuProps) {
const { t } = useTranslation();
const collapseStyle = collapsed
? { fontSize: '12px' }
: { fontSize: '12px', paddingLeft: '52px' };
const menuItems = [
{
key: 'toggle',
icon: (
<img
src={
!collapsed
? './icons/sider/menu_open.svg'
: './icons/sider/menu.svg'
}
alt="toggle"
/>
),
label: 'VORKOUT',
style: { fontSize: '20px' },
},
{
key: '/process-diagram',
icon: (
<img src="./icons/sider/process-diagram.svg" alt="process diagram" />
),
label: (
<Tooltip title={t('processDiagrams')}>{t('processDiagrams')}</Tooltip>
),
},
{
key: '/running-processes',
icon: (
<img
src="./icons/sider/running-processes.svg"
alt="running processes"
/>
),
label: (
<Tooltip title={t('runningProcesses')}>{t('runningProcesses')}</Tooltip>
),
},
!collapsed
? {
key: 'divider',
label: <Divider />,
style: {
marginBottom: '-16px',
marginTop: '-4px',
cursor: 'default',
width: '100%',
},
disabled: true,
}
: null,
{
key: 'sub1',
icon: <img src="./icons/sider/settings.svg" alt="settings" />,
label: t('settings'),
className: 'no-expand-icon',
children: [
{
key: '/accounts',
label: !collapsed ? (
<Tooltip title={t('accounts')}>{t('accounts')}</Tooltip>
) : (
t('accounts')
),
style: collapseStyle,
},
{
key: '/events-list',
label: !collapsed ? (
<Tooltip title={t('eventsList')}>{t('eventsList')}</Tooltip>
) : (
t('eventsList')
),
style: collapseStyle,
},
{
key: '/configuration',
label: !collapsed ? (
<Tooltip title={t('configuration')}>{t('configuration')}</Tooltip>
) : (
t('configuration')
),
style: collapseStyle,
},
],
},
];
return (
<Menu
theme="light"
selectedKeys={[selectedKey]}
mode="inline"
onClick={hangleMenuClick}
defaultOpenKeys={['sub1']}
items={menuItems}
/>
);
}

View File

@ -0,0 +1,229 @@
import {
Button,
Form,
Input,
Select,
Upload,
Image,
UploadFile,
GetProp,
UploadProps,
} from 'antd';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
const { Option } = Select;
type FileType = Parameters<GetProp<UploadProps, 'beforeUpload'>>[0];
const getBase64 = (file: FileType): Promise<string> =>
new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result as string);
reader.onerror = (error) => reject(error);
});
export default function UserCreate() {
const { t } = useTranslation();
const [previewOpen, setPreviewOpen] = useState(false);
const [previewImage, setPreviewImage] = useState('');
const [fileList, setFileList] = useState<UploadFile[]>([]);
const handlePreview = async (file: UploadFile) => {
if (!file.url && !file.preview) {
file.preview = await getBase64(file.originFileObj as FileType);
}
setPreviewImage(file.url || (file.preview as string));
setPreviewOpen(true);
};
const handleChange: UploadProps['onChange'] = ({ fileList: newFileList }) =>
setFileList(newFileList);
const customUploadButton = (
<div>
<div
style={{
height: '102px',
width: '102px',
backgroundColor: '#E2E2E2',
borderRadius: '50%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
marginBottom: 8,
marginTop: 30,
cursor: 'pointer',
}}
>
<img
src="./icons/drawer/add_photo_alternate.svg"
alt="add_photo_alternate"
style={{ height: '18px', width: '18px' }}
/>
</div>
<span style={{ fontSize: '14px', color: '#8c8c8c' }}>
{t('selectPhoto')}
</span>
</div>
);
const photoToUpload = (
<div style={{ height: '102px' }}>
<Upload
listType="picture-circle"
fileList={fileList}
onPreview={handlePreview}
onChange={handleChange}
beforeUpload={() => false}
>
{fileList.length > 0 ? null : customUploadButton}
</Upload>
{previewImage && (
<Image
wrapperStyle={{ display: 'none' }}
preview={{
visible: previewOpen,
onVisibleChange: (visible) => setPreviewOpen(visible),
afterOpenChange: (visible) => !visible && setPreviewImage(''),
}}
src={previewImage}
/>
)}
</div>
);
return (
<div
style={{
display: 'flex',
flexDirection: 'column',
height: '100%',
}}
>
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
marginBottom: '36px',
}}
>
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}}
>
{photoToUpload}
</div>
</div>
<Form
name="user-edit-form"
layout="vertical"
// onFinish={onFinish}
initialValues={{
name: '',
login: '',
password: '',
email: '',
tenant: '',
role: '',
status: '',
}}
style={{ flex: 1, display: 'flex', flexDirection: 'column' }}
>
<Form.Item
label={t('name')}
name="name"
rules={[{ required: true, message: t('nameMessage') }]}
>
<Input />
</Form.Item>
<Form.Item
label={t('login')}
name="login"
rules={[{ required: true, message: t('loginMessage') }]}
>
<Input />
</Form.Item>
<Form.Item
label={t('password')}
name="password"
rules={[{ required: true, message: t('passwordMessage') }]}
>
<Input.Password />
</Form.Item>
<Form.Item
label={t('email')}
name="email"
rules={[
{ required: true, message: t('emailMessage') },
{ type: 'email', message: t('emailErrorMessage') },
]}
>
<Input />
</Form.Item>
<Form.Item
label={t('tenant')}
name="tenant"
rules={[{ required: true, message: t('tenantMessage') }]}
>
<Input />
</Form.Item>
<Form.Item
label={t('role')}
name="role"
rules={[{ required: true, message: t('roleMessage') }]}
>
<Select placeholder={t('roleMessage')}>
<Option value="Директор магазина">Директор магазина</Option>
<Option value="Менеджер">Менеджер</Option>
<Option value="Кассир">Кассир</Option>
</Select>
</Form.Item>
<Form.Item
label={t('status')}
name="status"
rules={[{ required: true, message: t('statusMessage') }]}
>
<Select placeholder={t('statusMessage')}>
<Option value="ACTIVE">Активен</Option>
<Option value="DISABLED">Неактивен</Option>
<Option value="BLOCKED">Заблокирован</Option>
<Option value="DELETED">Удален</Option>
</Select>
</Form.Item>
<div style={{ flexGrow: 1 }} />
<Form.Item>
<Button
type="primary"
htmlType="submit"
block
style={{ color: '#000' }}
>
<img
src="/icons/drawer/reg.svg"
alt="save"
style={{ height: '18px', width: '18px' }}
/>{' '}
{t('addAccount')}
</Button>
</Form.Item>
</Form>
</div>
);
}

View File

@ -0,0 +1,113 @@
import { Button, Form, Input, Select } from 'antd';
import { useTranslation } from 'react-i18next';
const { Option } = Select;
export default function UserEdit() {
const { t } = useTranslation();
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<Form
name="user-edit-form"
layout="vertical"
// onFinish={onFinish}
initialValues={{
name: 'Александр Александров',
login: 'alexandralex@vorkout.ru',
password: 'jKUUl776GHd',
email: 'alexandralex@vorkout.ru',
tenant: 'text',
role: 'Директор магазина',
status: 'Активен',
}}
style={{ flex: 1, display: 'flex', flexDirection: 'column' }}
>
<Form.Item
label={t('name')}
name="name"
rules={[{ required: true, message: t('nameMessage') }]}
>
<Input />
</Form.Item>
<Form.Item
label={t('login')}
name="login"
rules={[{ required: true, message: t('loginMessage') }]}
>
<Input />
</Form.Item>
<Form.Item
label={t('password')}
name="password"
rules={[{ required: true, message: t('passwordMessage') }]}
>
<Input.Password />
</Form.Item>
<Form.Item
label={t('email')}
name="email"
rules={[
{ required: true, message: t('emailMessage') },
{ type: 'email', message: t('emailErrorMessage') },
]}
>
<Input />
</Form.Item>
<Form.Item
label={t('tenant')}
name="tenant"
rules={[{ required: true, message: t('tenantMessage') }]}
>
<Input />
</Form.Item>
<Form.Item
label={t('role')}
name="role"
rules={[{ required: true, message: t('roleMessage') }]}
>
<Select placeholder={t('roleMessage')}>
<Option value="Директор магазина">Директор магазина</Option>
<Option value="Менеджер">Менеджер</Option>
<Option value="Кассир">Кассир</Option>
</Select>
</Form.Item>
<Form.Item
label={t('status')}
name="status"
rules={[{ required: true, message: t('statusMessage') }]}
>
<Select placeholder={t('statusMessage')}>
<Option value="ACTIVE">Активен</Option>
<Option value="DISABLED">Неактивен</Option>
<Option value="BLOCKED">Заблокирован</Option>
<Option value="DELETED">Удален</Option>
</Select>
</Form.Item>
<div style={{ flexGrow: 1 }} />
<Form.Item>
<Button
type="primary"
htmlType="submit"
block
style={{ color: '#000' }}
>
<img
src="/icons/drawer/save.svg"
alt="save"
style={{ height: '18px', width: '18px' }}
/>{' '}
{t('save')}
</Button>
</Form.Item>
</Form>
</div>
);
}

View File

@ -0,0 +1,24 @@
import './i18n';
import { ConfigProvider } from 'antd';
import { useTranslation } from 'react-i18next';
import { theme } from './customTheme';
import en from 'antd/locale/en_US';
import ru from 'antd/locale/ru_RU';
const antdLocales = {
en: en,
ru: ru,
};
export default function AppWrapper({ children }: any) {
const { i18n } = useTranslation();
const currentLang = i18n.language.split('-')[0] as 'en' | 'ru';
return (
<ConfigProvider locale={antdLocales[currentLang]} theme={theme}>
{children}
</ConfigProvider>
);
}

View File

@ -0,0 +1,15 @@
export const theme = {
token: {
fontFamily: 'Roboto, sans-serif',
colorPrimary: '#C2DA3D',
Menu: {
itemColor: 'f2f2f2',
itemBg: '#f2f2f2',
subMenuItemBg: '#f2f2f2',
iconSize: '18px',
},
Layout: {
bodyBg: '#f2f2f2',
},
},
};

74
client/src/config/i18n.ts Normal file
View File

@ -0,0 +1,74 @@
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
i18n
.use(LanguageDetector)
.use(initReactI18next)
.init({
fallbackLng: 'en',
supportedLngs: ['en', 'ru'],
interpolation: { escapeValue: false },
resources: {
en: {
translation: {
accounts: 'Accounts',
processDiagrams: 'Process diagrams',
runningProcesses: 'Running processes',
settings: 'Settings',
eventsList: 'Events list',
configuration: 'Configuration',
selectPhoto: 'Select photo',
name: 'Name',
login: 'Login',
password: 'Password',
email: 'Email',
tenant: 'Tenant',
role: 'Role',
status: 'Status',
nameMessage: 'Enter name',
loginMessage: 'Enter login',
passwordMessage: 'Enter password',
emailMessage: 'Enter email',
emailErrorMessage: 'Incorrect email',
tenantMessage: 'Enter tenant',
roleMessage: 'Choose role',
statusMessage: 'Choose status',
addAccount: 'Add account',
save: 'Save changes',
newAccount: 'New account',
},
},
ru: {
translation: {
accounts: 'Учетные записи',
processDiagrams: 'Схемы процессов',
runningProcesses: 'Запущенные процессы',
settings: 'Настройки',
eventsList: 'Справочкин событий',
configuration: 'Конфигурация',
selectPhoto: 'Выбрать фото',
name: 'Имя',
login: 'Логин',
password: 'Пароль',
email: 'Имейл',
tenant: 'Привязка',
role: 'Роль',
status: 'Статус',
nameMessage: 'Введите имя',
loginMessage: 'Введите логин',
passwordMessage: 'Введите пароль',
emailMessage: 'Введите имейл',
emailErrorMessage: 'Некорректный имейл',
tenantMessage: 'Введите привязку',
roleMessage: 'Выберите роль',
statusMessage: 'Выберите статус',
addAccount: 'Добавить аккаунт',
save: 'Сохранить изменения',
newAccount: 'Новая учетная запись',
},
},
},
});
export default i18n;

View File

@ -11,3 +11,34 @@ code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
.ant-menu-inline .ant-menu-submenu-selected {
border: none;
}
.ant-menu-inline .ant-menu-submenu-selected > .ant-menu-submenu-title {
color: #548d10;
}
.ant-menu-inline .ant-menu-item-selected {
background-color: transparent;
border: none;
color: #548d10;
}
.ant-menu-item-selected {
background-color: transparent !important;
}
.ant-menu-item-selected a,
.ant-menu-item-selected {
color: #548d10 !important;
}
.sider {
background-color: #f2f2f2;
}
.no-expand-icon .ant-menu-submenu-arrow {
display: none !important;
}

View File

@ -2,8 +2,17 @@ import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import { BrowserRouter } from 'react-router-dom';
import AppWrapper from './config/AppWrapper';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(<App />);
root.render(
<AppWrapper>
<BrowserRouter>
<App />
</BrowserRouter>
</AppWrapper>
);

View File

@ -0,0 +1,37 @@
import Header from '../components/Header';
import { useState } from 'react';
import ContentDrawer from '../components/ContentDrawer';
import UserCreate from '../components/UserCreate';
import { useTranslation } from 'react-i18next';
export default function AccountsPage() {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const showDrawer = () => setOpen(true);
const closeDrawer = () => setOpen(false);
return (
<>
<Header
title={t('accounts')}
additionalContent={
<img
src="./icons/header/add_2.svg"
alt="add"
style={{
height: '18px',
width: '18px',
cursor: 'pointer',
}}
onClick={showDrawer}
/>
}
/>
<ContentDrawer open={open} closeDrawer={closeDrawer} type="create">
<UserCreate />
</ContentDrawer>
</>
);
}

View File

@ -0,0 +1,11 @@
import { useTranslation } from 'react-i18next';
import Header from '../components/Header';
export default function ConfigurationPage() {
const { t } = useTranslation();
return (
<>
<Header title={t('configuration')} />
</>
);
}

View File

@ -0,0 +1,11 @@
import { useTranslation } from 'react-i18next';
import Header from '../components/Header';
export default function EventsListPage() {
const { t } = useTranslation();
return (
<>
<Header title={t('eventsList')} />
</>
);
}

View File

@ -0,0 +1,87 @@
/* eslint-disable react-hooks/exhaustive-deps */
import React, { useEffect, useState } from 'react';
import { Layout } from 'antd';
import Sider from 'antd/es/layout/Sider';
import SiderMenu from '../components/SiderMenu';
import { Route, Routes, useLocation, useNavigate } from 'react-router-dom';
import ProcessDiagramPage from './ProcessDiagramPage';
import RunningProcessesPage from './RunningProcessesPage';
import AccountsPage from './AccountsPage';
import EventsListPage from './EventsListPage';
import ConfigurationPage from './ConfigurationPage';
export default function MainLayout() {
const navigate = useNavigate();
const location = useLocation();
const [collapsed, setCollapsed] = useState(false);
const [selectedKey, setSelectedKey] = useState('1');
const [width, setWidth] = useState<number | string>('15%');
const [collapsedWidth, setCollapsedWidth] = useState(50);
const calculateWidths = () => {
const windowWidth = window.innerWidth;
const expanded = Math.min(Math.max(windowWidth * 0.15, 180), 240);
const collapsed = Math.max(windowWidth * 0.038, 50);
setWidth(expanded);
setCollapsedWidth(collapsed);
};
useEffect(() => {
calculateWidths();
window.addEventListener('resize', calculateWidths);
return () => window.removeEventListener('resize', calculateWidths);
}, []);
useEffect(() => {
if (location.pathname === '/') {
navigate('/process-diagram');
}
setSelectedKey(location.pathname);
}, [location.pathname]);
function hangleMenuClick(e: any) {
const key = e.key;
if (key === 'toggle') {
setCollapsed(!collapsed);
return;
}
if (key === 'divider') {
return;
}
setSelectedKey(key);
navigate(key);
}
return (
<Layout style={{ minHeight: '100vh' }}>
<Sider
className="sider"
collapsible
collapsed={collapsed}
onCollapse={(value) => setCollapsed(value)}
theme="light"
width={width}
collapsedWidth={collapsedWidth}
trigger={null}
>
<SiderMenu
collapsed={collapsed}
selectedKey={selectedKey}
hangleMenuClick={hangleMenuClick}
/>
</Sider>
<Layout>
<Routes>
<Route path="/process-diagram" element={<ProcessDiagramPage />} />
<Route path="/running-processes" element={<RunningProcessesPage />} />
<Route path="/accounts" element={<AccountsPage />} />
<Route path="/events-list" element={<EventsListPage />} />
<Route path="/configuration" element={<ConfigurationPage />} />
<Route path="*" element={<div>404 Not Found</div>} />
</Routes>
</Layout>
</Layout>
);
}

View File

@ -0,0 +1,11 @@
import { useTranslation } from 'react-i18next';
import Header from '../components/Header';
export default function ProcessDiagramPage() {
const { t } = useTranslation();
return (
<>
<Header title={t('processDiagrams')} />
</>
);
}

View File

@ -0,0 +1,8 @@
// ProtectedRoute.js
import { Outlet } from 'react-router-dom';
import React from 'react';
const ProtectedRoute = (): React.JSX.Element => {
return <Outlet />;
};
export default ProtectedRoute;

View File

@ -0,0 +1,11 @@
import { useTranslation } from 'react-i18next';
import Header from '../components/Header';
export default function RunningProcessesPage() {
const { t } = useTranslation();
return (
<>
<Header title={t('runningProcesses')} />
</>
);
}