finish for init core
This commit is contained in:
parent
ab1e864478
commit
bc8815f40e
.env.example.gitignoreTaskfile.yaml
dev/data
fuware
app.pyconst.py
poetry.lockpyproject.tomlcore
db
main.pyrepos
routes
schemas
services
@ -1 +1,2 @@
|
|||||||
PRODUCTION=false
|
PRODUCTION=false
|
||||||
|
TESTING=false
|
||||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -8,3 +8,4 @@
|
|||||||
node_modules
|
node_modules
|
||||||
node_modules/
|
node_modules/
|
||||||
.sqlite
|
.sqlite
|
||||||
|
*.db
|
||||||
|
@ -12,7 +12,11 @@ dotenv:
|
|||||||
- .env
|
- .env
|
||||||
- .dev.env
|
- .dev.env
|
||||||
tasks:
|
tasks:
|
||||||
py:
|
py:setupdb:
|
||||||
|
desc: runs it first for init db
|
||||||
|
cmds:
|
||||||
|
- poetry run python fuware/db/init_db.py
|
||||||
|
py:dev:
|
||||||
desc: runs the backend server
|
desc: runs the backend server
|
||||||
cmds:
|
cmds:
|
||||||
- poetry run python fuware/main.py
|
- poetry run python fuware/app.py
|
||||||
|
0
dev/data/.gitkeep
Normal file
0
dev/data/.gitkeep
Normal file
60
fuware/app.py
Normal file
60
fuware/app.py
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
from fastapi import FastAPI, Request, HTTPException
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from fastapi.middleware.gzip import GZipMiddleware
|
||||||
|
|
||||||
|
from fuware.core.config import get_app_settings
|
||||||
|
from fuware import __version__
|
||||||
|
from fuware.routes import router
|
||||||
|
import uvicorn
|
||||||
|
|
||||||
|
settings = get_app_settings()
|
||||||
|
|
||||||
|
description = f"""
|
||||||
|
fuware is a web application for managing your hours items and tracking them.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# event.listen(models.User.__table__, 'after_create', initialize_table)
|
||||||
|
|
||||||
|
app = FastAPI(
|
||||||
|
title="Fuware",
|
||||||
|
description=description,
|
||||||
|
version=__version__,
|
||||||
|
docs_url=settings.DOCS_URL,
|
||||||
|
redoc_url=settings.REDOC_URL
|
||||||
|
)
|
||||||
|
|
||||||
|
app.add_middleware(GZipMiddleware, minimum_size=1000)
|
||||||
|
|
||||||
|
if not settings.PRODUCTION:
|
||||||
|
allowed_origins = ["http://localhost:3000"]
|
||||||
|
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=allowed_origins,
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.exception_handler(HTTPException)
|
||||||
|
async def unicorn_exception_handler(request: Request, exc: HTTPException):
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=exc.status_code,
|
||||||
|
content={"status": exc.status_code, "data": exc.detail},
|
||||||
|
)
|
||||||
|
|
||||||
|
def api_routers():
|
||||||
|
app.include_router(router)
|
||||||
|
|
||||||
|
|
||||||
|
api_routers()
|
||||||
|
|
||||||
|
# app.include_router(authR.authRouter)
|
||||||
|
# app.include_router(userR.userRouter)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
uvicorn.run("app:app", host="0.0.0.0", port=settings.API_PORT, reload=True, workers=1, forwarded_allow_ips="*")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
@ -1,15 +0,0 @@
|
|||||||
import os
|
|
||||||
from dotenv import load_dotenv
|
|
||||||
|
|
||||||
load_dotenv()
|
|
||||||
|
|
||||||
SERCET_KEY = b"oWNhXlfo666JlMHk6UHYxeNB6z_CA2MisDDZJe4N0yc="
|
|
||||||
COOKIE_KEY = os.getenv('VITE_LOGIN_KEY') or '7fo24CMyIc'
|
|
||||||
# URL_DATABASE = "postgresql://{0}:{1}@{2}:{3}/{4}".format(
|
|
||||||
# os.getenv('LOL_DB_USER'),
|
|
||||||
# os.getenv('LOL_DB_PASSWORD'),
|
|
||||||
# os.getenv('LOL_DB_HOST'),
|
|
||||||
# os.getenv('LOL_DB_PORT'),
|
|
||||||
# os.getenv('LOL_DB_NAME'),
|
|
||||||
# )
|
|
||||||
URL_DATABASE = "sqlite:///./test.db"
|
|
@ -4,8 +4,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
from fuware.core.settings.settings import AppSettings, app_settings_constructor
|
||||||
from .settings import AppDirectories, AppSettings
|
|
||||||
|
|
||||||
CWD = Path(__file__).parent
|
CWD = Path(__file__).parent
|
||||||
BASE_DIR = CWD.parent.parent
|
BASE_DIR = CWD.parent.parent
|
||||||
@ -27,14 +26,6 @@ def determine_data_dir() -> Path:
|
|||||||
|
|
||||||
return BASE_DIR.joinpath("dev", "data")
|
return BASE_DIR.joinpath("dev", "data")
|
||||||
|
|
||||||
@lru_cache
|
|
||||||
def get_app_dirs() -> AppDirectories:
|
|
||||||
return AppDirectories(determine_data_dir())
|
|
||||||
|
|
||||||
|
|
||||||
@lru_cache
|
@lru_cache
|
||||||
def get_app_settings() -> AppSettings:
|
def get_app_settings() -> AppSettings:
|
||||||
return app_settings_constructor(env_file=ENV, production=PRODUCTION, data_dir=determine_data_dir())
|
return app_settings_constructor(env_file=ENV, production=PRODUCTION, data_dir=determine_data_dir())
|
||||||
|
|
||||||
|
|
||||||
print(get_app_settings())
|
|
||||||
|
1
fuware/core/dependencies/__init__.py
Normal file
1
fuware/core/dependencies/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from .dependencies import *
|
20
fuware/core/dependencies/dependencies.py
Normal file
20
fuware/core/dependencies/dependencies.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
|
||||||
|
from fastapi import Depends, HTTPException, Request
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from fuware.core.config import get_app_settings
|
||||||
|
from fuware.db.db_setup import generate_session
|
||||||
|
|
||||||
|
settings = get_app_settings()
|
||||||
|
|
||||||
|
async def get_auth_user(request: Request, db: Session = Depends(generate_session)):
|
||||||
|
"""verify that user has a valid session"""
|
||||||
|
session_id = request.cookies.get(settings.COOKIE_KEY)
|
||||||
|
if not session_id:
|
||||||
|
raise HTTPException(status_code=401, detail="Unauthorized")
|
||||||
|
# decrypt_user = decryptString(session_id).split(',')
|
||||||
|
# db_user = get_user_by_username(db, decrypt_user[0])
|
||||||
|
# if not db_user:
|
||||||
|
# raise HTTPException(status_code=403)
|
||||||
|
# if not verify_password(decrypt_user[1], db_user.password):
|
||||||
|
# raise HTTPException(status_code=401, detail="Your username or password input is wrong!")
|
||||||
|
return True
|
67
fuware/core/logger/config.py
Normal file
67
fuware/core/logger/config.py
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import pathlib
|
||||||
|
import typing
|
||||||
|
from logging import config as logging_config
|
||||||
|
|
||||||
|
__dir = pathlib.Path(__file__).parent
|
||||||
|
__conf: dict[str, str] | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def _load_config(path: pathlib.Path, substitutions: dict[str, str] | None = None) -> dict[str, typing.Any]:
|
||||||
|
with open(path) as file:
|
||||||
|
if substitutions:
|
||||||
|
contents = file.read()
|
||||||
|
for key, value in substitutions.items():
|
||||||
|
# Replaces the key matches
|
||||||
|
#
|
||||||
|
# Example:
|
||||||
|
# {"key": "value"}
|
||||||
|
# "/path/to/${key}/file" -> "/path/to/value/file"
|
||||||
|
contents = contents.replace(f"${{{key}}}", value)
|
||||||
|
|
||||||
|
json_data = json.loads(contents)
|
||||||
|
|
||||||
|
else:
|
||||||
|
json_data = json.load(file)
|
||||||
|
|
||||||
|
return json_data
|
||||||
|
|
||||||
|
|
||||||
|
def log_config() -> dict[str, str]:
|
||||||
|
if __conf is None:
|
||||||
|
raise ValueError("logger not configured, must call configured_logger first")
|
||||||
|
|
||||||
|
return __conf
|
||||||
|
|
||||||
|
|
||||||
|
def configured_logger(
|
||||||
|
*,
|
||||||
|
mode: str,
|
||||||
|
config_override: pathlib.Path | None = None,
|
||||||
|
substitutions: dict[str, str] | None = None,
|
||||||
|
) -> logging.Logger:
|
||||||
|
"""
|
||||||
|
Configure the logger based on the mode and return the root logger
|
||||||
|
|
||||||
|
Args:
|
||||||
|
mode (str): The mode to configure the logger for (production, development, testing)
|
||||||
|
config_override (pathlib.Path, optional): A path to a custom logging config. Defaults to None.
|
||||||
|
substitutions (dict[str, str], optional): A dictionary of substitutions to apply to the logging config.
|
||||||
|
"""
|
||||||
|
global __conf
|
||||||
|
|
||||||
|
if config_override:
|
||||||
|
__conf = _load_config(config_override, substitutions)
|
||||||
|
else:
|
||||||
|
if mode == "production":
|
||||||
|
__conf = _load_config(__dir / "logconf.prod.json", substitutions)
|
||||||
|
elif mode == "development":
|
||||||
|
__conf = _load_config(__dir / "logconf.dev.json", substitutions)
|
||||||
|
elif mode == "testing":
|
||||||
|
__conf = _load_config(__dir / "logconf.test.json", substitutions)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Invalid mode: {mode}")
|
||||||
|
|
||||||
|
logging_config.dictConfig(config=__conf)
|
||||||
|
return logging.getLogger()
|
0
fuware/core/security/__init__.py
Normal file
0
fuware/core/security/__init__.py
Normal file
34
fuware/core/security/hasher.py
Normal file
34
fuware/core/security/hasher.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
from functools import lru_cache
|
||||||
|
from typing import Protocol
|
||||||
|
import bcrypt
|
||||||
|
|
||||||
|
from fuware.core.config import get_app_settings
|
||||||
|
|
||||||
|
|
||||||
|
class Hasher(Protocol):
|
||||||
|
def hash(self, password: str) -> str: ...
|
||||||
|
|
||||||
|
def verify(self, password: str, hashed: str) -> bool: ...
|
||||||
|
|
||||||
|
class FakeHasher:
|
||||||
|
def hash(self, password: str) -> str:
|
||||||
|
return password
|
||||||
|
|
||||||
|
def verify(self, password: str, hashed: str) -> bool:
|
||||||
|
return password == hashed
|
||||||
|
|
||||||
|
class BcryptHasher:
|
||||||
|
def hash(self, password: str) -> str:
|
||||||
|
return bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
|
||||||
|
|
||||||
|
def verify(self, password: str, hashed: str) -> bool:
|
||||||
|
return bcrypt.checkpw(password.encode('utf-8'), hashed.encode('utf-8'))
|
||||||
|
|
||||||
|
@lru_cache(maxsize=1)
|
||||||
|
def get_hasher() -> Hasher:
|
||||||
|
settings = get_app_settings()
|
||||||
|
|
||||||
|
if settings.TESTING:
|
||||||
|
return FakeHasher()
|
||||||
|
|
||||||
|
return BcryptHasher()
|
@ -17,7 +17,7 @@ class SQLiteProvider(AbstractDBProvider, BaseModel):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def db_path(self):
|
def db_path(self):
|
||||||
return self.data_dir / f"{self.prefix}mealie.db"
|
return self.data_dir / f"{self.prefix}fuware.db"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def db_url(self) -> str:
|
def db_url(self) -> str:
|
||||||
|
@ -1,26 +1,23 @@
|
|||||||
import secrets
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from fuware.core.settings.db_providers import AbstractDBProvider, SQLiteProvider
|
from fuware.core.settings.db_providers import AbstractDBProvider, SQLiteProvider
|
||||||
from pydantic_settings import BaseSettings # type: ignore
|
from pydantic_settings import BaseSettings # type: ignore
|
||||||
|
|
||||||
|
|
||||||
def determine_secrets(data_dir: Path, production: bool) -> str:
|
def determine_secrets(production: bool) -> str:
|
||||||
if not production:
|
if not production:
|
||||||
return "shh-secret-test-key"
|
return "shh-secret-test-key"
|
||||||
|
|
||||||
secrets_file = data_dir.joinpath(".secret")
|
return "oWNhXlfo666JlMHk6UHYxeNB6z_CA2MisDDZJe4N0yc="
|
||||||
if secrets_file.is_file():
|
|
||||||
with open(secrets_file) as f:
|
def determine_cookie(production: bool) -> str:
|
||||||
return f.read()
|
if not production:
|
||||||
else:
|
return "logcook"
|
||||||
data_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
with open(secrets_file, "w") as f:
|
return "7fo24CMyIc"
|
||||||
new_secret = secrets.token_hex(32)
|
|
||||||
f.write(new_secret)
|
|
||||||
return new_secret
|
|
||||||
|
|
||||||
class AppSettings(BaseSettings):
|
class AppSettings(BaseSettings):
|
||||||
PRODUCTION: bool
|
PRODUCTION: bool
|
||||||
|
TESTING: bool
|
||||||
BASE_URL: str = "http://localhost:8080"
|
BASE_URL: str = "http://localhost:8080"
|
||||||
"""trailing slashes are trimmed (ex. `http://localhost:8080/` becomes ``http://localhost:8080`)"""
|
"""trailing slashes are trimmed (ex. `http://localhost:8080/` becomes ``http://localhost:8080`)"""
|
||||||
|
|
||||||
@ -32,6 +29,9 @@ class AppSettings(BaseSettings):
|
|||||||
|
|
||||||
ALLOW_SIGNUP: bool = False
|
ALLOW_SIGNUP: bool = False
|
||||||
|
|
||||||
|
SECRET: str
|
||||||
|
COOKIE_KEY: str
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def DOCS_URL(self) -> str | None:
|
def DOCS_URL(self) -> str | None:
|
||||||
return "/docs" if self.API_DOCS else None
|
return "/docs" if self.API_DOCS else None
|
||||||
@ -43,7 +43,6 @@ class AppSettings(BaseSettings):
|
|||||||
# ===============================================
|
# ===============================================
|
||||||
# Database Configuration
|
# Database Configuration
|
||||||
|
|
||||||
DB_ENGINE: str = "sqlite" # Options: 'sqlite', 'postgres'
|
|
||||||
DB_PROVIDER: AbstractDBProvider | None = None
|
DB_PROVIDER: AbstractDBProvider | None = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -63,7 +62,7 @@ def app_settings_constructor(data_dir: Path, production: bool, env_file: Path, e
|
|||||||
app_settings = AppSettings(
|
app_settings = AppSettings(
|
||||||
_env_file=env_file, # type: ignore
|
_env_file=env_file, # type: ignore
|
||||||
_env_file_encoding=env_encoding, # type: ignore
|
_env_file_encoding=env_encoding, # type: ignore
|
||||||
**{"SECRET": determine_secrets(data_dir, production)},
|
**{"SECRET": determine_secrets(production), 'COOKIE_KEY': determine_cookie(production)},
|
||||||
)
|
)
|
||||||
|
|
||||||
app_settings.DB_PROVIDER = SQLiteProvider(data_dir=data_dir)
|
app_settings.DB_PROVIDER = SQLiteProvider(data_dir=data_dir)
|
||||||
|
@ -1,22 +0,0 @@
|
|||||||
import os
|
|
||||||
from dotenv import load_dotenv
|
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
from fuware import __version__
|
|
||||||
|
|
||||||
load_dotenv()
|
|
||||||
|
|
||||||
APP_VERSION = __version__
|
|
||||||
CWD = Path(__file__).parent
|
|
||||||
BASE_DIR = CWD.parent.parent.parent
|
|
||||||
|
|
||||||
SERCET_KEY = b"oWNhXlfo666JlMHk6UHYxeNB6z_CA2MisDDZJe4N0yc="
|
|
||||||
COOKIE_KEY = os.getenv('VITE_LOGIN_KEY') or '7fo24CMyIc'
|
|
||||||
# URL_DATABASE = "postgresql://{0}:{1}@{2}:{3}/{4}".format(
|
|
||||||
# os.getenv('LOL_DB_USER'),
|
|
||||||
# os.getenv('LOL_DB_PASSWORD'),
|
|
||||||
# os.getenv('LOL_DB_HOST'),
|
|
||||||
# os.getenv('LOL_DB_PORT'),
|
|
||||||
# os.getenv('LOL_DB_NAME'),
|
|
||||||
# )
|
|
||||||
URL_DATABASE = "sqlite:///./test.db"
|
|
@ -1,20 +0,0 @@
|
|||||||
from sqlalchemy.orm import Session
|
|
||||||
from db.models import User
|
|
||||||
from ultis import get_password_hash
|
|
||||||
import schemas
|
|
||||||
|
|
||||||
def get_user(db: Session, user_id: str):
|
|
||||||
return db.query(User).filter(User.id == user_id).first()
|
|
||||||
|
|
||||||
def get_user_by_username(db: Session, usn: str):
|
|
||||||
return db.query(User).filter(User.username == usn).first()
|
|
||||||
|
|
||||||
def get_users(db: Session, skip: int = 0, limit: int = 100):
|
|
||||||
return db.query(User).offset(skip).limit(limit).all()
|
|
||||||
|
|
||||||
def create_user(db: Session, user: schemas.UserCreate):
|
|
||||||
db_user = User(username=user.username, password=get_password_hash(user.password), name=user.name)
|
|
||||||
db.add(db_user)
|
|
||||||
db.commit()
|
|
||||||
db.refresh(db_user)
|
|
||||||
return db_user
|
|
@ -1,17 +1,37 @@
|
|||||||
from sqlalchemy import create_engine
|
from collections.abc import Generator
|
||||||
from sqlalchemy.orm import sessionmaker
|
from sqlalchemy.orm.session import Session
|
||||||
from sqlalchemy.ext.declarative import declarative_base
|
from sqlalchemy import create_engine, event, Engine, text
|
||||||
from const import URL_DATABASE
|
from sqlalchemy.orm import scoped_session, sessionmaker
|
||||||
|
|
||||||
engine = create_engine(URL_DATABASE)
|
from fuware.core.config import get_app_settings
|
||||||
|
|
||||||
SessionLocal = sessionmaker(autocommit=False ,autoflush=False, bind=engine)
|
settings = get_app_settings()
|
||||||
|
|
||||||
Base = declarative_base()
|
def sql_global_init(db_url: str):
|
||||||
|
connect_args = {"check_same_thread": False}
|
||||||
|
|
||||||
def get_db():
|
engine = create_engine(db_url, echo=True, connect_args=connect_args, pool_pre_ping=True, future=True)
|
||||||
|
|
||||||
|
SessionLocal = scoped_session(sessionmaker(autocommit=False, autoflush=False, bind=engine, future=True))
|
||||||
|
|
||||||
|
return SessionLocal, engine
|
||||||
|
|
||||||
|
SessionLocal, engine = sql_global_init(settings.DB_URL) # type: ignore
|
||||||
|
|
||||||
|
@event.listens_for(Engine, "connect")
|
||||||
|
def set_sqlite_pragma(dbapi_connection, connection_record):
|
||||||
|
cursor = dbapi_connection.cursor()
|
||||||
|
cursor.execute("PRAGMA foreign_keys=ON")
|
||||||
|
cursor.close()
|
||||||
|
|
||||||
|
# with engine.connect() as connection:
|
||||||
|
# result = connection.execute(text('select "Hello"'))
|
||||||
|
|
||||||
|
# print(result.all())
|
||||||
|
|
||||||
|
def generate_session() -> Generator[Session, None, None]:
|
||||||
db = SessionLocal()
|
db = SessionLocal()
|
||||||
try:
|
try:
|
||||||
yield db
|
yield db
|
||||||
finally:
|
finally:
|
||||||
db.close()
|
db.close()
|
||||||
|
5
fuware/db/init_db.py
Normal file
5
fuware/db/init_db.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
from db_setup import engine
|
||||||
|
from models._model_base import Model
|
||||||
|
from models.users import *
|
||||||
|
|
||||||
|
Model.metadata.create_all(bind=engine)
|
@ -1 +1 @@
|
|||||||
from .user import *
|
from .users import *
|
||||||
|
20
fuware/db/models/_model_base.py
Normal file
20
fuware/db/models/_model_base.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from sqlalchemy import DateTime, Integer
|
||||||
|
from sqlalchemy.orm import declarative_base, Mapped, mapped_column
|
||||||
|
from text_unidecode import unidecode
|
||||||
|
|
||||||
|
from fuware.db.db_setup import SessionLocal
|
||||||
|
|
||||||
|
Model = declarative_base()
|
||||||
|
Model.query = SessionLocal.query_property()
|
||||||
|
|
||||||
|
class SqlAlchemyBase(Model):
|
||||||
|
__abstract__ = True
|
||||||
|
|
||||||
|
created_at: Mapped[datetime | None] = mapped_column(DateTime, default=datetime.utcnow(), index=True)
|
||||||
|
update_at: Mapped[datetime | None] = mapped_column(DateTime, default=datetime.utcnow(), onupdate=datetime.utcnow())
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def normalize(cls, val: str) -> str:
|
||||||
|
return unidecode(val).lower().strip()
|
@ -1,8 +0,0 @@
|
|||||||
from datetime import datetime
|
|
||||||
from sqlalchemy import Column, DateTime
|
|
||||||
from sqlalchemy.orm import declarative_mixin
|
|
||||||
|
|
||||||
@declarative_mixin
|
|
||||||
class Timestamp:
|
|
||||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
|
||||||
updated_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
|
@ -1,15 +0,0 @@
|
|||||||
from db import Base
|
|
||||||
from sqlalchemy import Boolean, Column, String
|
|
||||||
from .mixins import Timestamp
|
|
||||||
from sqlalchemy.dialects.postgresql import UUID
|
|
||||||
import uuid
|
|
||||||
|
|
||||||
class User(Base, Timestamp):
|
|
||||||
__tablename__ = 'users'
|
|
||||||
|
|
||||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, index=True)
|
|
||||||
username = Column(String(100), unique=True, index=True, nullable=False)
|
|
||||||
password = Column(String, index=True, nullable=False)
|
|
||||||
name = Column(String, index=True, nullable=True)
|
|
||||||
is_admin = Column(Boolean, default=False)
|
|
||||||
is_lock = Column(Boolean, default=False)
|
|
1
fuware/db/models/users/__init__.py
Normal file
1
fuware/db/models/users/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from .users import *
|
20
fuware/db/models/users/users.py
Normal file
20
fuware/db/models/users/users.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import uuid
|
||||||
|
from sqlalchemy import Boolean, Column, String
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
|
from sqlalchemy.dialects.postgresql import UUID
|
||||||
|
|
||||||
|
from .._model_base import SqlAlchemyBase
|
||||||
|
|
||||||
|
class User(SqlAlchemyBase):
|
||||||
|
__tablename__ = 'users'
|
||||||
|
|
||||||
|
id: Mapped[UUID] = mapped_column(UUID, primary_key=True, default=uuid.uuid4, index=True)
|
||||||
|
username: Mapped[str | None] = mapped_column(String, unique=True, index=True, nullable=False)
|
||||||
|
password: Mapped[str | None] = mapped_column(String, index=True, nullable=False)
|
||||||
|
name: Mapped[str | None] = mapped_column(String, index=True, nullable=True)
|
||||||
|
is_admin: Mapped[bool | None] = mapped_column(Boolean, default=False)
|
||||||
|
is_lock: Mapped[bool | None] = mapped_column(Boolean, default=False)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"{self.__class__.__name__}, name: {self.name}, username: {self.username}"
|
@ -1,34 +1,10 @@
|
|||||||
from fastapi import FastAPI, Request, HTTPException
|
|
||||||
from fastapi.responses import JSONResponse
|
|
||||||
from routes import authR, userR
|
|
||||||
# from db import engine, models
|
|
||||||
# from sqlalchemy import event
|
|
||||||
# from db.seeds import initialize_table
|
|
||||||
import uvicorn
|
import uvicorn
|
||||||
|
from fuware.app import settings
|
||||||
|
|
||||||
# event.listen(models.User.__table__, 'after_create', initialize_table)
|
|
||||||
|
|
||||||
app = FastAPI()
|
|
||||||
|
|
||||||
# models.Base.metadata.create_all(bind=engine)
|
|
||||||
|
|
||||||
@app.exception_handler(HTTPException)
|
|
||||||
async def unicorn_exception_handler(request: Request, exc: HTTPException):
|
|
||||||
return JSONResponse(
|
|
||||||
status_code=exc.status_code,
|
|
||||||
content={"status": exc.status_code, "data": exc.detail},
|
|
||||||
)
|
|
||||||
|
|
||||||
app.include_router(authR.authRouter)
|
|
||||||
app.include_router(userR.userRouter)
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
uvicorn.run(
|
uvicorn.run("app:app", host=settings.API_HOST, port=settings.API_PORT, reload=True, workers=1, forwarded_allow_ips=settings.HOST_IP)
|
||||||
"main:app",
|
|
||||||
port=8000,
|
|
||||||
host="0.0.0.0",
|
|
||||||
reload=True
|
|
||||||
)
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
1
fuware/repos/__init__.py
Normal file
1
fuware/repos/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from .repository_users import *
|
28
fuware/repos/repository_users.py
Normal file
28
fuware/repos/repository_users.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
from fuware.core.security.hasher import get_hasher
|
||||||
|
from fuware.db.models.users.users import User
|
||||||
|
from fuware.schemas import UserCreate
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
|
||||||
|
class RepositoryUsers:
|
||||||
|
def __init__(self):
|
||||||
|
self.user = User()
|
||||||
|
|
||||||
|
def get_all(self, skip: int = 0, limit: int = 100):
|
||||||
|
return self.user.query.offset(skip).limit(limit).all()
|
||||||
|
|
||||||
|
def get_by_username(self, username: str):
|
||||||
|
return self.user.query.filter_by(username=username).first()
|
||||||
|
|
||||||
|
def create(self, db: Session, user: UserCreate):
|
||||||
|
try:
|
||||||
|
hasher = get_hasher()
|
||||||
|
db_user = User(username=user.username, password=hasher.hash(user.password), name=user.name)
|
||||||
|
db.add(db_user)
|
||||||
|
db.commit()
|
||||||
|
except Exception:
|
||||||
|
db.rollback()
|
||||||
|
raise
|
||||||
|
|
||||||
|
db.refresh(db_user)
|
||||||
|
return db_user
|
7
fuware/routes/__init__.py
Normal file
7
fuware/routes/__init__.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
from fastapi import APIRouter
|
||||||
|
|
||||||
|
from . import (auth)
|
||||||
|
|
||||||
|
router = APIRouter(prefix='/api')
|
||||||
|
|
||||||
|
router.include_router(auth.router)
|
9
fuware/routes/_base/routers.py
Normal file
9
fuware/routes/_base/routers.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
from enum import Enum
|
||||||
|
from fastapi import APIRouter, Depends
|
||||||
|
|
||||||
|
from fuware.core.dependencies import get_auth_user
|
||||||
|
|
||||||
|
|
||||||
|
class PrivateAPIRouter(APIRouter):
|
||||||
|
def __init__(self, tags: list[str | Enum] | None = None, prefix: str = "", **kwargs):
|
||||||
|
super().__init__(tags=tags, prefix=prefix, dependencies=[Depends(get_auth_user)], **kwargs)
|
7
fuware/routes/auth/__init__.py
Normal file
7
fuware/routes/auth/__init__.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
|
||||||
|
from fastapi import APIRouter
|
||||||
|
from . import auth
|
||||||
|
|
||||||
|
router = APIRouter(prefix='/auth')
|
||||||
|
|
||||||
|
router.include_router(auth.public_router)
|
35
fuware/routes/auth/auth.py
Normal file
35
fuware/routes/auth/auth.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
from typing import Any
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, Response
|
||||||
|
|
||||||
|
from fastapi.encoders import jsonable_encoder
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from fuware.db.db_setup import generate_session
|
||||||
|
from fuware.schemas import ReturnValue, PrivateUser, UserRequest
|
||||||
|
from fuware.schemas.user.user import UserCreate
|
||||||
|
from fuware.services import UserService
|
||||||
|
|
||||||
|
|
||||||
|
public_router = APIRouter(tags=["Users: Authentication"])
|
||||||
|
|
||||||
|
user_service = UserService()
|
||||||
|
|
||||||
|
@public_router.put('/register')
|
||||||
|
def register_user(user: UserCreate, db: Session = Depends(generate_session)) -> ReturnValue[Any]:
|
||||||
|
db_user = user_service.get_by_username(username=user.username)
|
||||||
|
if db_user:
|
||||||
|
raise HTTPException(status_code=400, detail="Username already registered!")
|
||||||
|
user_return = user_service.create(db=db, user=user)
|
||||||
|
return ReturnValue(status=200, data=jsonable_encoder(user_return))
|
||||||
|
|
||||||
|
# @public_router.post('/login', response_model=ReturnValue[PrivateUser])
|
||||||
|
# def user_login(user: UserRequest, response: Response) -> ReturnValue[Any]:
|
||||||
|
# db_user = UserService.get_by_username(user.username)
|
||||||
|
# if not db_user:
|
||||||
|
# raise HTTPException(status_code=401, detail="Your username or password input is wrong!")
|
||||||
|
# if not verify_password(user.password, db_user.password):
|
||||||
|
# raise HTTPException(status_code=401, detail="Your username or password input is wrong!")
|
||||||
|
# if db_user.is_lock is True:
|
||||||
|
# raise HTTPException(status_code=401, detail="Your Account is banned")
|
||||||
|
# cookieEncode = encryptString(user.username + ',' + user.password)
|
||||||
|
# response.set_cookie(key=COOKIE_KEY, value=cookieEncode.decode('utf-8'))
|
||||||
|
# return ReturnValue(status=200, data=jsonable_encoder(db_user))
|
@ -1,50 +0,0 @@
|
|||||||
from typing import Any
|
|
||||||
from fastapi import APIRouter, HTTPException, Response, Request, Depends
|
|
||||||
from fastapi.encoders import jsonable_encoder
|
|
||||||
from schemas import ReturnValue, User, UserCreate, UserRequest
|
|
||||||
from ultis import root_api_path_build, encryptString, decryptString, verify_password
|
|
||||||
from const import COOKIE_KEY
|
|
||||||
from sqlalchemy.orm import Session
|
|
||||||
from db.controller import get_user_by_username, create_user
|
|
||||||
from db import get_db
|
|
||||||
|
|
||||||
authRouter=APIRouter(prefix=root_api_path_build('/auth'))
|
|
||||||
|
|
||||||
@authRouter.put('/register')
|
|
||||||
def register_user(user: UserCreate, db: Session = Depends(get_db)) -> ReturnValue[Any]:
|
|
||||||
db_user = get_user_by_username(db=db, usn=user.username)
|
|
||||||
if db_user:
|
|
||||||
raise HTTPException(status_code=400, detail="Username already registered!")
|
|
||||||
user_return = create_user(db=db, user=user)
|
|
||||||
return ReturnValue(status=200, data=jsonable_encoder(user_return))
|
|
||||||
|
|
||||||
@authRouter.post('/login', response_model=ReturnValue[User])
|
|
||||||
def user_login(user: UserRequest, response: Response, db: Session = Depends(get_db)) -> ReturnValue[Any]:
|
|
||||||
db_user = get_user_by_username(db, user.username)
|
|
||||||
if not db_user:
|
|
||||||
raise HTTPException(status_code=401, detail="Your username or password input is wrong!")
|
|
||||||
if not verify_password(user.password, db_user.password):
|
|
||||||
raise HTTPException(status_code=401, detail="Your username or password input is wrong!")
|
|
||||||
if db_user.is_lock is True:
|
|
||||||
raise HTTPException(status_code=401, detail="Your Account is banned")
|
|
||||||
cookieEncode = encryptString(user.username + ',' + user.password)
|
|
||||||
response.set_cookie(key=COOKIE_KEY, value=cookieEncode.decode('utf-8'))
|
|
||||||
return ReturnValue(status=200, data=jsonable_encoder(db_user))
|
|
||||||
|
|
||||||
@authRouter.get('/logout')
|
|
||||||
def user_logout(response: Response) -> ReturnValue[Any]:
|
|
||||||
response.delete_cookie(key=COOKIE_KEY)
|
|
||||||
return ReturnValue(status=200, data='Logged out')
|
|
||||||
|
|
||||||
def get_auth_user(request: Request, db: Session = Depends(get_db)):
|
|
||||||
"""verify that user has a valid session"""
|
|
||||||
session_id = request.cookies.get(COOKIE_KEY)
|
|
||||||
if not session_id:
|
|
||||||
raise HTTPException(status_code=401, detail="Unauthorized")
|
|
||||||
decrypt_user = decryptString(session_id).split(',')
|
|
||||||
db_user = get_user_by_username(db, decrypt_user[0])
|
|
||||||
if not db_user:
|
|
||||||
raise HTTPException(status_code=403)
|
|
||||||
if not verify_password(decrypt_user[1], db_user.password):
|
|
||||||
raise HTTPException(status_code=401, detail="Your username or password input is wrong!")
|
|
||||||
return True
|
|
@ -1,11 +0,0 @@
|
|||||||
from typing import Any
|
|
||||||
from fastapi import APIRouter, Depends
|
|
||||||
from schemas import ReturnValue
|
|
||||||
from ultis import root_api_path_build
|
|
||||||
from routes import authR
|
|
||||||
|
|
||||||
userRouter=APIRouter(prefix=root_api_path_build('/user'))
|
|
||||||
|
|
||||||
@userRouter.get('/get-data/', dependencies=[Depends(authR.get_auth_user)])
|
|
||||||
def get_data(url: str = '') -> ReturnValue[Any]:
|
|
||||||
return ReturnValue(status=200, data=url)
|
|
27
fuware/schemas/fuware_model.py
Normal file
27
fuware/schemas/fuware_model.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
from typing import ClassVar, Protocol, TypeVar
|
||||||
|
from humps import camelize
|
||||||
|
from enum import Enum
|
||||||
|
from pydantic import UUID4, BaseModel, ConfigDict
|
||||||
|
|
||||||
|
T = TypeVar("T", bound=BaseModel)
|
||||||
|
|
||||||
|
class SearchType(Enum):
|
||||||
|
fuzzy = "fuzzy"
|
||||||
|
tokenized = "tokenized"
|
||||||
|
|
||||||
|
class FuwareModel(BaseModel):
|
||||||
|
_searchable_properties: ClassVar[list[str]] = []
|
||||||
|
"""
|
||||||
|
Searchable properties for the search API.
|
||||||
|
The first property will be used for sorting (order_by)
|
||||||
|
"""
|
||||||
|
model_config = ConfigDict(alias_generator=camelize, populate_by_name=True)
|
||||||
|
|
||||||
|
def cast(self, cls: type[T], **kwargs) -> T:
|
||||||
|
"""
|
||||||
|
Cast the current model to another with additional arguments. Useful for
|
||||||
|
transforming DTOs into models that are saved to a database
|
||||||
|
"""
|
||||||
|
create_data = {field: getattr(self, field) for field in self.__fields__ if field in cls.__fields__}
|
||||||
|
create_data.update(kwargs or {})
|
||||||
|
return cls(**create_data)
|
@ -1,8 +1,10 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel, ConfigDict
|
||||||
from fastapi import Form
|
from fastapi import Form
|
||||||
|
|
||||||
class UserBase(BaseModel):
|
from fuware.schemas.fuware_model import FuwareModel
|
||||||
|
|
||||||
|
class UserBase(FuwareModel):
|
||||||
username: str = Form(...)
|
username: str = Form(...)
|
||||||
|
|
||||||
class UserRequest(UserBase):
|
class UserRequest(UserBase):
|
||||||
@ -12,13 +14,11 @@ class UserCreate(UserRequest):
|
|||||||
password: str = Form(...)
|
password: str = Form(...)
|
||||||
name: str
|
name: str
|
||||||
|
|
||||||
class User(UserBase):
|
class PrivateUser(UserBase):
|
||||||
id: str
|
id: str
|
||||||
name: str
|
name: str
|
||||||
is_admin: bool
|
is_admin: bool
|
||||||
is_lock: bool
|
is_lock: bool
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
updated_at: datetime
|
updated_at: datetime
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
class Config:
|
|
||||||
from_attributes = True
|
|
1
fuware/services/__init__.py
Normal file
1
fuware/services/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from .user import *
|
6
fuware/services/_base_service/__init__.py
Normal file
6
fuware/services/_base_service/__init__.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from fuware.core.config import get_app_settings
|
||||||
|
|
||||||
|
|
||||||
|
class BaseService:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.setting = get_app_settings()
|
1
fuware/services/user/__init__.py
Normal file
1
fuware/services/user/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from .user_service import *
|
19
fuware/services/user/user_service.py
Normal file
19
fuware/services/user/user_service.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from fuware.repos import RepositoryUsers
|
||||||
|
from fuware.services._base_service import BaseService
|
||||||
|
from fuware.schemas import UserCreate
|
||||||
|
|
||||||
|
|
||||||
|
class UserService(BaseService):
|
||||||
|
def __init__(self):
|
||||||
|
self.repos = RepositoryUsers()
|
||||||
|
|
||||||
|
def get_all(self, skip: int = 0, limit: int = 100):
|
||||||
|
return self.repos.get_all(skip=skip, limit=limit)
|
||||||
|
|
||||||
|
def get_by_username(self, username: str):
|
||||||
|
return self.repos.get_by_username(username)
|
||||||
|
|
||||||
|
def create(self, db: Session, user: UserCreate):
|
||||||
|
return self.repos.create(db=db, user=user)
|
83
poetry.lock
generated
83
poetry.lock
generated
@ -33,6 +33,46 @@ doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphin
|
|||||||
test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"]
|
test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"]
|
||||||
trio = ["trio (>=0.23)"]
|
trio = ["trio (>=0.23)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bcrypt"
|
||||||
|
version = "4.1.3"
|
||||||
|
description = "Modern password hashing for your software and your servers"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
files = [
|
||||||
|
{file = "bcrypt-4.1.3-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:48429c83292b57bf4af6ab75809f8f4daf52aa5d480632e53707805cc1ce9b74"},
|
||||||
|
{file = "bcrypt-4.1.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a8bea4c152b91fd8319fef4c6a790da5c07840421c2b785084989bf8bbb7455"},
|
||||||
|
{file = "bcrypt-4.1.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d3b317050a9a711a5c7214bf04e28333cf528e0ed0ec9a4e55ba628d0f07c1a"},
|
||||||
|
{file = "bcrypt-4.1.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:094fd31e08c2b102a14880ee5b3d09913ecf334cd604af27e1013c76831f7b05"},
|
||||||
|
{file = "bcrypt-4.1.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:4fb253d65da30d9269e0a6f4b0de32bd657a0208a6f4e43d3e645774fb5457f3"},
|
||||||
|
{file = "bcrypt-4.1.3-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:193bb49eeeb9c1e2db9ba65d09dc6384edd5608d9d672b4125e9320af9153a15"},
|
||||||
|
{file = "bcrypt-4.1.3-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:8cbb119267068c2581ae38790e0d1fbae65d0725247a930fc9900c285d95725d"},
|
||||||
|
{file = "bcrypt-4.1.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6cac78a8d42f9d120b3987f82252bdbeb7e6e900a5e1ba37f6be6fe4e3848286"},
|
||||||
|
{file = "bcrypt-4.1.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:01746eb2c4299dd0ae1670234bf77704f581dd72cc180f444bfe74eb80495b64"},
|
||||||
|
{file = "bcrypt-4.1.3-cp37-abi3-win32.whl", hash = "sha256:037c5bf7c196a63dcce75545c8874610c600809d5d82c305dd327cd4969995bf"},
|
||||||
|
{file = "bcrypt-4.1.3-cp37-abi3-win_amd64.whl", hash = "sha256:8a893d192dfb7c8e883c4576813bf18bb9d59e2cfd88b68b725990f033f1b978"},
|
||||||
|
{file = "bcrypt-4.1.3-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:0d4cf6ef1525f79255ef048b3489602868c47aea61f375377f0d00514fe4a78c"},
|
||||||
|
{file = "bcrypt-4.1.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f5698ce5292a4e4b9e5861f7e53b1d89242ad39d54c3da451a93cac17b61921a"},
|
||||||
|
{file = "bcrypt-4.1.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec3c2e1ca3e5c4b9edb94290b356d082b721f3f50758bce7cce11d8a7c89ce84"},
|
||||||
|
{file = "bcrypt-4.1.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3a5be252fef513363fe281bafc596c31b552cf81d04c5085bc5dac29670faa08"},
|
||||||
|
{file = "bcrypt-4.1.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:5f7cd3399fbc4ec290378b541b0cf3d4398e4737a65d0f938c7c0f9d5e686611"},
|
||||||
|
{file = "bcrypt-4.1.3-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:c4c8d9b3e97209dd7111bf726e79f638ad9224b4691d1c7cfefa571a09b1b2d6"},
|
||||||
|
{file = "bcrypt-4.1.3-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:31adb9cbb8737a581a843e13df22ffb7c84638342de3708a98d5c986770f2834"},
|
||||||
|
{file = "bcrypt-4.1.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:551b320396e1d05e49cc18dd77d970accd52b322441628aca04801bbd1d52a73"},
|
||||||
|
{file = "bcrypt-4.1.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6717543d2c110a155e6821ce5670c1f512f602eabb77dba95717ca76af79867d"},
|
||||||
|
{file = "bcrypt-4.1.3-cp39-abi3-win32.whl", hash = "sha256:6004f5229b50f8493c49232b8e75726b568535fd300e5039e255d919fc3a07f2"},
|
||||||
|
{file = "bcrypt-4.1.3-cp39-abi3-win_amd64.whl", hash = "sha256:2505b54afb074627111b5a8dc9b6ae69d0f01fea65c2fcaea403448c503d3991"},
|
||||||
|
{file = "bcrypt-4.1.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:cb9c707c10bddaf9e5ba7cdb769f3e889e60b7d4fea22834b261f51ca2b89fed"},
|
||||||
|
{file = "bcrypt-4.1.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:9f8ea645eb94fb6e7bea0cf4ba121c07a3a182ac52876493870033141aa687bc"},
|
||||||
|
{file = "bcrypt-4.1.3-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:f44a97780677e7ac0ca393bd7982b19dbbd8d7228c1afe10b128fd9550eef5f1"},
|
||||||
|
{file = "bcrypt-4.1.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d84702adb8f2798d813b17d8187d27076cca3cd52fe3686bb07a9083930ce650"},
|
||||||
|
{file = "bcrypt-4.1.3.tar.gz", hash = "sha256:2ee15dd749f5952fe3f0430d0ff6b74082e159c50332a1413d51b5689cf06623"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
tests = ["pytest (>=3.2.1,!=3.3.0)"]
|
||||||
|
typecheck = ["mypy"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cffi"
|
name = "cffi"
|
||||||
version = "1.16.0"
|
version = "1.16.0"
|
||||||
@ -488,6 +528,36 @@ files = [
|
|||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0"
|
typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pydantic-settings"
|
||||||
|
version = "2.2.1"
|
||||||
|
description = "Settings management using Pydantic"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
files = [
|
||||||
|
{file = "pydantic_settings-2.2.1-py3-none-any.whl", hash = "sha256:0235391d26db4d2190cb9b31051c4b46882d28a51533f97440867f012d4da091"},
|
||||||
|
{file = "pydantic_settings-2.2.1.tar.gz", hash = "sha256:00b9f6a5e95553590434c0fa01ead0b216c3e10bc54ae02e37f359948643c5ed"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
pydantic = ">=2.3.0"
|
||||||
|
python-dotenv = ">=0.21.0"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
toml = ["tomli (>=2.0.1)"]
|
||||||
|
yaml = ["pyyaml (>=6.0.1)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyhumps"
|
||||||
|
version = "3.8.0"
|
||||||
|
description = "🐫 Convert strings (and dictionary keys) between snake case, camel case and pascal case in Python. Inspired by Humps for Node"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
files = [
|
||||||
|
{file = "pyhumps-3.8.0-py3-none-any.whl", hash = "sha256:060e1954d9069f428232a1adda165db0b9d8dfdce1d265d36df7fbff540acfd6"},
|
||||||
|
{file = "pyhumps-3.8.0.tar.gz", hash = "sha256:498026258f7ee1a8e447c2e28526c0bea9407f9a59c03260aee4bd6c04d681a3"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "python-dotenv"
|
name = "python-dotenv"
|
||||||
version = "1.0.1"
|
version = "1.0.1"
|
||||||
@ -703,6 +773,17 @@ anyio = ">=3.4.0,<5"
|
|||||||
[package.extras]
|
[package.extras]
|
||||||
full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7)", "pyyaml"]
|
full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7)", "pyyaml"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "text-unidecode"
|
||||||
|
version = "1.3"
|
||||||
|
description = "The most basic Text::Unidecode port"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
files = [
|
||||||
|
{file = "text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"},
|
||||||
|
{file = "text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typing-extensions"
|
name = "typing-extensions"
|
||||||
version = "4.11.0"
|
version = "4.11.0"
|
||||||
@ -955,4 +1036,4 @@ files = [
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = "^3.10"
|
python-versions = "^3.10"
|
||||||
content-hash = "539143b0355e37d8d3941ee1bbe1011faa541e29c88aebdc30b8ba813ab15d7f"
|
content-hash = "22414c8adf366f3f1121008853a3209f20781962a1eda18e4d8b8e4f551dc28a"
|
||||||
|
@ -13,6 +13,11 @@ sqlalchemy = "^2.0.29"
|
|||||||
cryptography = "^42.0.5"
|
cryptography = "^42.0.5"
|
||||||
passlib = "^1.7.4"
|
passlib = "^1.7.4"
|
||||||
uvicorn = {version = "^0.29.0", extras = ["standard"]}
|
uvicorn = {version = "^0.29.0", extras = ["standard"]}
|
||||||
|
pydantic = "^2.7.1"
|
||||||
|
pydantic-settings = "^2.2.1"
|
||||||
|
text-unidecode = "^1.3"
|
||||||
|
pyhumps = "^3.8.0"
|
||||||
|
bcrypt = "^4.1.3"
|
||||||
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
[tool.poetry.group.dev.dependencies]
|
||||||
ruff = "^0.4.1"
|
ruff = "^0.4.1"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user