diff --git a/backend/Dockerfile b/backend/Dockerfile index 78ac029..5eb67eb 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -30,5 +30,5 @@ EXPOSE 8000 HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" || exit 1 -# Run the application -CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] +# Run migrations and start the application +CMD ["sh", "-c", "alembic upgrade head && uvicorn app.main:app --host 0.0.0.0 --port 8000"] diff --git a/backend/alembic/env.py b/backend/alembic/env.py index d5cad64..3fc5323 100644 --- a/backend/alembic/env.py +++ b/backend/alembic/env.py @@ -19,7 +19,8 @@ config = context.config # Override sqlalchemy.url with environment variable if available database_url = os.getenv("DATABASE_URL") if database_url: - config.set_main_option("sqlalchemy.url", database_url) + # Escape % for ConfigParser interpolation + config.set_main_option("sqlalchemy.url", database_url.replace("%", "%%")) # Interpret the config file for Python logging. if config.config_file_name is not None: diff --git a/backend/alembic/versions/882512221354_initial.py b/backend/alembic/versions/882512221354_initial.py new file mode 100644 index 0000000..eea68dc --- /dev/null +++ b/backend/alembic/versions/882512221354_initial.py @@ -0,0 +1,270 @@ +"""initial + +Revision ID: 882512221354 +Revises: +Create Date: 2026-02-06 22:48:52.480626 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '882512221354' +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: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('etf_prices', + sa.Column('ticker', sa.String(length=20), nullable=False), + sa.Column('date', sa.Date(), nullable=False), + sa.Column('close', sa.Numeric(precision=12, scale=2), nullable=False), + sa.Column('nav', sa.Numeric(precision=12, scale=2), nullable=True), + sa.Column('volume', sa.BigInteger(), nullable=True), + sa.PrimaryKeyConstraint('ticker', 'date') + ) + op.create_table('etfs', + sa.Column('ticker', sa.String(length=20), nullable=False), + sa.Column('name', sa.String(length=100), nullable=False), + sa.Column('asset_class', sa.Enum('EQUITY', 'BOND', 'GOLD', 'MIXED', name='assetclass'), nullable=False), + sa.Column('market', sa.String(length=20), nullable=False), + sa.Column('expense_ratio', sa.Numeric(precision=5, scale=4), nullable=True), + sa.PrimaryKeyConstraint('ticker') + ) + op.create_table('financials', + sa.Column('ticker', sa.String(length=20), nullable=False), + sa.Column('base_date', sa.Date(), nullable=False), + sa.Column('report_type', sa.Enum('ANNUAL', 'QUARTERLY', name='reporttype'), nullable=False), + sa.Column('account', sa.String(length=50), nullable=False), + sa.Column('value', sa.Numeric(precision=20, scale=2), nullable=True), + sa.PrimaryKeyConstraint('ticker', 'base_date', 'report_type', 'account') + ) + op.create_table('job_logs', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('job_name', sa.String(length=50), nullable=False), + sa.Column('status', sa.String(length=20), nullable=False), + sa.Column('started_at', sa.DateTime(), nullable=True), + sa.Column('finished_at', sa.DateTime(), nullable=True), + sa.Column('records_count', sa.Integer(), nullable=True), + sa.Column('error_msg', sa.Text(), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_job_logs_id'), 'job_logs', ['id'], unique=False) + op.create_table('prices', + sa.Column('ticker', sa.String(length=20), nullable=False), + sa.Column('date', sa.Date(), nullable=False), + sa.Column('open', sa.Numeric(precision=12, scale=2), nullable=False), + sa.Column('high', sa.Numeric(precision=12, scale=2), nullable=False), + sa.Column('low', sa.Numeric(precision=12, scale=2), nullable=False), + sa.Column('close', sa.Numeric(precision=12, scale=2), nullable=False), + sa.Column('volume', sa.BigInteger(), nullable=False), + sa.PrimaryKeyConstraint('ticker', 'date') + ) + op.create_table('sectors', + sa.Column('ticker', sa.String(length=20), nullable=False), + sa.Column('sector_code', sa.String(length=10), nullable=False), + sa.Column('company_name', sa.String(length=100), nullable=False), + sa.Column('sector_name', sa.String(length=50), nullable=False), + sa.Column('base_date', sa.Date(), nullable=False), + sa.PrimaryKeyConstraint('ticker') + ) + op.create_table('stocks', + sa.Column('ticker', sa.String(length=20), nullable=False), + sa.Column('name', sa.String(length=100), nullable=False), + sa.Column('market', sa.String(length=20), nullable=False), + sa.Column('close_price', sa.Numeric(precision=12, scale=2), nullable=True), + sa.Column('market_cap', sa.BigInteger(), nullable=True), + sa.Column('eps', sa.Numeric(precision=12, scale=2), nullable=True), + sa.Column('forward_eps', sa.Numeric(precision=12, scale=2), nullable=True), + sa.Column('bps', sa.Numeric(precision=12, scale=2), nullable=True), + sa.Column('dividend_per_share', sa.Numeric(precision=12, scale=2), nullable=True), + sa.Column('stock_type', sa.Enum('COMMON', 'SPAC', 'PREFERRED', 'REIT', 'OTHER', name='stocktype'), nullable=True), + sa.Column('base_date', sa.Date(), nullable=False), + sa.PrimaryKeyConstraint('ticker') + ) + op.create_table('users', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('username', sa.String(length=50), nullable=False), + sa.Column('email', sa.String(length=100), nullable=False), + sa.Column('hashed_password', sa.String(length=255), nullable=False), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('updated_at', sa.DateTime(), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_users_email'), 'users', ['email'], unique=True) + op.create_index(op.f('ix_users_id'), 'users', ['id'], unique=False) + op.create_index(op.f('ix_users_username'), 'users', ['username'], unique=True) + op.create_table('valuations', + sa.Column('ticker', sa.String(length=20), nullable=False), + sa.Column('base_date', sa.Date(), nullable=False), + sa.Column('per', sa.Numeric(precision=10, scale=2), nullable=True), + sa.Column('pbr', sa.Numeric(precision=10, scale=2), nullable=True), + sa.Column('psr', sa.Numeric(precision=10, scale=2), nullable=True), + sa.Column('pcr', sa.Numeric(precision=10, scale=2), nullable=True), + sa.Column('dividend_yield', sa.Numeric(precision=6, scale=2), nullable=True), + sa.PrimaryKeyConstraint('ticker', 'base_date') + ) + op.create_table('backtests', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('strategy_type', sa.String(length=50), nullable=False), + sa.Column('strategy_params', sa.JSON(), nullable=True), + sa.Column('start_date', sa.Date(), nullable=False), + sa.Column('end_date', sa.Date(), nullable=False), + sa.Column('rebalance_period', sa.Enum('MONTHLY', 'QUARTERLY', 'SEMI_ANNUAL', 'ANNUAL', name='rebalanceperiod'), nullable=True), + sa.Column('initial_capital', sa.Numeric(precision=20, scale=2), nullable=False), + sa.Column('commission_rate', sa.Numeric(precision=10, scale=6), nullable=True), + sa.Column('slippage_rate', sa.Numeric(precision=10, scale=6), nullable=True), + sa.Column('benchmark', sa.String(length=20), nullable=True), + sa.Column('top_n', sa.Integer(), nullable=True), + sa.Column('status', sa.Enum('PENDING', 'RUNNING', 'COMPLETED', 'FAILED', name='backteststatus'), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('completed_at', sa.DateTime(), nullable=True), + sa.Column('error_message', sa.Text(), nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_backtests_id'), 'backtests', ['id'], unique=False) + op.create_table('portfolios', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=100), nullable=False), + sa.Column('portfolio_type', sa.Enum('PENSION', 'GENERAL', name='portfoliotype'), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('updated_at', sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_portfolios_id'), 'portfolios', ['id'], unique=False) + op.create_table('backtest_equity_curve', + sa.Column('backtest_id', sa.Integer(), nullable=False), + sa.Column('date', sa.Date(), nullable=False), + sa.Column('portfolio_value', sa.Numeric(precision=20, scale=2), nullable=True), + sa.Column('benchmark_value', sa.Numeric(precision=20, scale=2), nullable=True), + sa.Column('drawdown', sa.Numeric(precision=10, scale=4), nullable=True), + sa.ForeignKeyConstraint(['backtest_id'], ['backtests.id'], ), + sa.PrimaryKeyConstraint('backtest_id', 'date') + ) + op.create_table('backtest_holdings', + sa.Column('backtest_id', sa.Integer(), nullable=False), + sa.Column('rebalance_date', sa.Date(), nullable=False), + sa.Column('ticker', sa.String(length=20), nullable=False), + sa.Column('name', sa.String(length=100), nullable=True), + sa.Column('weight', sa.Numeric(precision=10, scale=4), nullable=True), + sa.Column('shares', sa.Integer(), nullable=True), + sa.Column('price', sa.Numeric(precision=12, scale=2), nullable=True), + sa.ForeignKeyConstraint(['backtest_id'], ['backtests.id'], ), + sa.PrimaryKeyConstraint('backtest_id', 'rebalance_date', 'ticker') + ) + op.create_table('backtest_results', + sa.Column('backtest_id', sa.Integer(), nullable=False), + sa.Column('total_return', sa.Numeric(precision=10, scale=4), nullable=True), + sa.Column('cagr', sa.Numeric(precision=10, scale=4), nullable=True), + sa.Column('mdd', sa.Numeric(precision=10, scale=4), nullable=True), + sa.Column('sharpe_ratio', sa.Numeric(precision=10, scale=4), nullable=True), + sa.Column('volatility', sa.Numeric(precision=10, scale=4), nullable=True), + sa.Column('benchmark_return', sa.Numeric(precision=10, scale=4), nullable=True), + sa.Column('excess_return', sa.Numeric(precision=10, scale=4), nullable=True), + sa.ForeignKeyConstraint(['backtest_id'], ['backtests.id'], ), + sa.PrimaryKeyConstraint('backtest_id') + ) + op.create_table('backtest_transactions', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('backtest_id', sa.Integer(), nullable=False), + sa.Column('date', sa.Date(), nullable=False), + sa.Column('ticker', sa.String(length=20), nullable=False), + sa.Column('action', sa.String(length=10), nullable=False), + sa.Column('shares', sa.Integer(), nullable=False), + sa.Column('price', sa.Numeric(precision=12, scale=2), nullable=False), + sa.Column('commission', sa.Numeric(precision=12, scale=2), nullable=False), + sa.ForeignKeyConstraint(['backtest_id'], ['backtests.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_backtest_transactions_id'), 'backtest_transactions', ['id'], unique=False) + op.create_table('holdings', + sa.Column('portfolio_id', sa.Integer(), nullable=False), + sa.Column('ticker', sa.String(length=20), nullable=False), + sa.Column('quantity', sa.Integer(), nullable=False), + sa.Column('avg_price', sa.Numeric(precision=12, scale=2), nullable=False), + sa.ForeignKeyConstraint(['portfolio_id'], ['portfolios.id'], ), + sa.PrimaryKeyConstraint('portfolio_id', 'ticker') + ) + op.create_table('portfolio_snapshots', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('portfolio_id', sa.Integer(), nullable=False), + sa.Column('total_value', sa.Numeric(precision=15, scale=2), nullable=False), + sa.Column('snapshot_date', sa.Date(), nullable=False), + sa.ForeignKeyConstraint(['portfolio_id'], ['portfolios.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_portfolio_snapshots_id'), 'portfolio_snapshots', ['id'], unique=False) + op.create_table('targets', + sa.Column('portfolio_id', sa.Integer(), nullable=False), + sa.Column('ticker', sa.String(length=20), nullable=False), + sa.Column('target_ratio', sa.Numeric(precision=5, scale=2), nullable=False), + sa.ForeignKeyConstraint(['portfolio_id'], ['portfolios.id'], ), + sa.PrimaryKeyConstraint('portfolio_id', 'ticker') + ) + op.create_table('transactions', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('portfolio_id', sa.Integer(), nullable=False), + sa.Column('ticker', sa.String(length=20), nullable=False), + sa.Column('tx_type', sa.Enum('BUY', 'SELL', name='transactiontype'), nullable=False), + sa.Column('quantity', sa.Integer(), nullable=False), + sa.Column('price', sa.Numeric(precision=12, scale=2), nullable=False), + sa.Column('executed_at', sa.DateTime(), nullable=False), + sa.Column('memo', sa.Text(), nullable=True), + sa.ForeignKeyConstraint(['portfolio_id'], ['portfolios.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_transactions_id'), 'transactions', ['id'], unique=False) + op.create_table('snapshot_holdings', + sa.Column('snapshot_id', sa.Integer(), nullable=False), + sa.Column('ticker', sa.String(length=20), nullable=False), + sa.Column('quantity', sa.Integer(), nullable=False), + sa.Column('price', sa.Numeric(precision=12, scale=2), nullable=False), + sa.Column('value', sa.Numeric(precision=15, scale=2), nullable=False), + sa.Column('current_ratio', sa.Numeric(precision=5, scale=2), nullable=False), + sa.ForeignKeyConstraint(['snapshot_id'], ['portfolio_snapshots.id'], ), + sa.PrimaryKeyConstraint('snapshot_id', 'ticker') + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('snapshot_holdings') + op.drop_index(op.f('ix_transactions_id'), table_name='transactions') + op.drop_table('transactions') + op.drop_table('targets') + op.drop_index(op.f('ix_portfolio_snapshots_id'), table_name='portfolio_snapshots') + op.drop_table('portfolio_snapshots') + op.drop_table('holdings') + op.drop_index(op.f('ix_backtest_transactions_id'), table_name='backtest_transactions') + op.drop_table('backtest_transactions') + op.drop_table('backtest_results') + op.drop_table('backtest_holdings') + op.drop_table('backtest_equity_curve') + op.drop_index(op.f('ix_portfolios_id'), table_name='portfolios') + op.drop_table('portfolios') + op.drop_index(op.f('ix_backtests_id'), table_name='backtests') + op.drop_table('backtests') + op.drop_table('valuations') + op.drop_index(op.f('ix_users_username'), table_name='users') + op.drop_index(op.f('ix_users_id'), table_name='users') + op.drop_index(op.f('ix_users_email'), table_name='users') + op.drop_table('users') + op.drop_table('stocks') + op.drop_table('sectors') + op.drop_table('prices') + op.drop_index(op.f('ix_job_logs_id'), table_name='job_logs') + op.drop_table('job_logs') + op.drop_table('financials') + op.drop_table('etfs') + op.drop_table('etf_prices') + # ### end Alembic commands ### diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index 2992945..8833992 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -13,7 +13,7 @@ from sqlalchemy.pool import StaticPool from app.main import app from app.core.database import Base, get_db from app.models.user import User -from app.core.auth import get_password_hash, create_access_token +from app.core.security import get_password_hash, create_access_token # Use in-memory SQLite for tests @@ -71,7 +71,6 @@ def test_user(db: Session) -> User: username="testuser", email="test@example.com", hashed_password=get_password_hash("testpassword"), - is_admin=False, ) db.add(user) db.commit() @@ -86,7 +85,6 @@ def admin_user(db: Session) -> User: username="admin", email="admin@example.com", hashed_password=get_password_hash("adminpassword"), - is_admin=True, ) db.add(user) db.commit() diff --git a/backend/tests/e2e/test_portfolio_flow.py b/backend/tests/e2e/test_portfolio_flow.py index b876de7..20ea1fc 100644 --- a/backend/tests/e2e/test_portfolio_flow.py +++ b/backend/tests/e2e/test_portfolio_flow.py @@ -76,7 +76,7 @@ def test_targets_flow(client: TestClient, auth_headers): assert response.status_code == 200 result = response.json() assert len(result) == 3 - assert sum(t["target_ratio"] for t in result) == 100 + assert sum(float(t["target_ratio"]) for t in result) == 100 # Get targets response = client.get( diff --git a/backend/tests/e2e/test_strategy_flow.py b/backend/tests/e2e/test_strategy_flow.py index 6d66908..6775c9f 100644 --- a/backend/tests/e2e/test_strategy_flow.py +++ b/backend/tests/e2e/test_strategy_flow.py @@ -10,14 +10,16 @@ def test_multi_factor_strategy(client: TestClient, auth_headers): response = client.post( "/api/strategy/multi-factor", json={ - "market": "KOSPI", - "min_market_cap": 100000000000, + "universe": { + "markets": ["KOSPI"], + "min_market_cap": 10000, # 1조원 (억원 단위) + }, "top_n": 20, "weights": { "value": 0.3, "quality": 0.3, "momentum": 0.2, - "f_score": 0.2, + "low_vol": 0.2, }, }, headers=auth_headers, @@ -28,8 +30,8 @@ def test_multi_factor_strategy(client: TestClient, auth_headers): if response.status_code == 200: data = response.json() assert "stocks" in data - assert "strategy_type" in data - assert data["strategy_type"] == "multi_factor" + assert "strategy_name" in data + assert data["strategy_name"] == "multi_factor" def test_quality_strategy(client: TestClient, auth_headers): @@ -37,10 +39,12 @@ def test_quality_strategy(client: TestClient, auth_headers): response = client.post( "/api/strategy/quality", json={ - "market": "KOSPI", - "min_market_cap": 100000000000, + "universe": { + "markets": ["KOSPI"], + "min_market_cap": 10000, + }, "top_n": 20, - "min_f_score": 6, + "min_fscore": 6, }, headers=auth_headers, ) @@ -49,7 +53,7 @@ def test_quality_strategy(client: TestClient, auth_headers): if response.status_code == 200: data = response.json() assert "stocks" in data - assert data["strategy_type"] == "quality" + assert data["strategy_name"] == "quality" def test_value_momentum_strategy(client: TestClient, auth_headers): @@ -57,8 +61,10 @@ def test_value_momentum_strategy(client: TestClient, auth_headers): response = client.post( "/api/strategy/value-momentum", json={ - "market": "KOSPI", - "min_market_cap": 100000000000, + "universe": { + "markets": ["KOSPI"], + "min_market_cap": 10000, + }, "top_n": 20, "value_weight": 0.5, "momentum_weight": 0.5, @@ -70,7 +76,7 @@ def test_value_momentum_strategy(client: TestClient, auth_headers): if response.status_code == 200: data = response.json() assert "stocks" in data - assert data["strategy_type"] == "value_momentum" + assert data["strategy_name"] == "value_momentum" def test_strategy_requires_auth(client: TestClient): @@ -78,8 +84,9 @@ def test_strategy_requires_auth(client: TestClient): response = client.post( "/api/strategy/multi-factor", json={ - "market": "KOSPI", - "min_market_cap": 100000000000, + "universe": { + "markets": ["KOSPI"], + }, "top_n": 20, }, ) diff --git a/backend/uv.lock b/backend/uv.lock index b15a585..900569a 100644 --- a/backend/uv.lock +++ b/backend/uv.lock @@ -54,68 +54,21 @@ wheels = [ [[package]] name = "bcrypt" -version = "5.0.0" +version = "4.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d4/36/3329e2518d70ad8e2e5817d5a4cac6bba05a47767ec416c7d020a965f408/bcrypt-5.0.0.tar.gz", hash = "sha256:f748f7c2d6fd375cc93d3fba7ef4a9e3a092421b8dbf34d8d4dc06be9492dfdd", size = 25386, upload-time = "2025-09-25T19:50:47.829Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/ae/3af7d006aacf513975fd1948a6b4d6f8b4a307f8a244e1a3d3774b297aad/bcrypt-4.0.1.tar.gz", hash = "sha256:27d375903ac8261cfe4047f6709d16f7d18d39b1ec92aaf72af989552a650ebd", size = 25498, upload-time = "2022-10-09T15:36:49.775Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/13/85/3e65e01985fddf25b64ca67275bb5bdb4040bd1a53b66d355c6c37c8a680/bcrypt-5.0.0-cp313-cp313t-macosx_10_12_universal2.whl", hash = "sha256:f3c08197f3039bec79cee59a606d62b96b16669cff3949f21e74796b6e3cd2be", size = 481806, upload-time = "2025-09-25T19:49:05.102Z" }, - { url = "https://files.pythonhosted.org/packages/44/dc/01eb79f12b177017a726cbf78330eb0eb442fae0e7b3dfd84ea2849552f3/bcrypt-5.0.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:200af71bc25f22006f4069060c88ed36f8aa4ff7f53e67ff04d2ab3f1e79a5b2", size = 268626, upload-time = "2025-09-25T19:49:06.723Z" }, - { url = "https://files.pythonhosted.org/packages/8c/cf/e82388ad5959c40d6afd94fb4743cc077129d45b952d46bdc3180310e2df/bcrypt-5.0.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:baade0a5657654c2984468efb7d6c110db87ea63ef5a4b54732e7e337253e44f", size = 271853, upload-time = "2025-09-25T19:49:08.028Z" }, - { url = "https://files.pythonhosted.org/packages/ec/86/7134b9dae7cf0efa85671651341f6afa695857fae172615e960fb6a466fa/bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:c58b56cdfb03202b3bcc9fd8daee8e8e9b6d7e3163aa97c631dfcfcc24d36c86", size = 269793, upload-time = "2025-09-25T19:49:09.727Z" }, - { url = "https://files.pythonhosted.org/packages/cc/82/6296688ac1b9e503d034e7d0614d56e80c5d1a08402ff856a4549cb59207/bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4bfd2a34de661f34d0bda43c3e4e79df586e4716ef401fe31ea39d69d581ef23", size = 289930, upload-time = "2025-09-25T19:49:11.204Z" }, - { url = "https://files.pythonhosted.org/packages/d1/18/884a44aa47f2a3b88dd09bc05a1e40b57878ecd111d17e5bba6f09f8bb77/bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:ed2e1365e31fc73f1825fa830f1c8f8917ca1b3ca6185773b349c20fd606cec2", size = 272194, upload-time = "2025-09-25T19:49:12.524Z" }, - { url = "https://files.pythonhosted.org/packages/0e/8f/371a3ab33c6982070b674f1788e05b656cfbf5685894acbfef0c65483a59/bcrypt-5.0.0-cp313-cp313t-manylinux_2_34_aarch64.whl", hash = "sha256:83e787d7a84dbbfba6f250dd7a5efd689e935f03dd83b0f919d39349e1f23f83", size = 269381, upload-time = "2025-09-25T19:49:14.308Z" }, - { url = "https://files.pythonhosted.org/packages/b1/34/7e4e6abb7a8778db6422e88b1f06eb07c47682313997ee8a8f9352e5a6f1/bcrypt-5.0.0-cp313-cp313t-manylinux_2_34_x86_64.whl", hash = "sha256:137c5156524328a24b9fac1cb5db0ba618bc97d11970b39184c1d87dc4bf1746", size = 271750, upload-time = "2025-09-25T19:49:15.584Z" }, - { url = "https://files.pythonhosted.org/packages/c0/1b/54f416be2499bd72123c70d98d36c6cd61a4e33d9b89562c22481c81bb30/bcrypt-5.0.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:38cac74101777a6a7d3b3e3cfefa57089b5ada650dce2baf0cbdd9d65db22a9e", size = 303757, upload-time = "2025-09-25T19:49:17.244Z" }, - { url = "https://files.pythonhosted.org/packages/13/62/062c24c7bcf9d2826a1a843d0d605c65a755bc98002923d01fd61270705a/bcrypt-5.0.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:d8d65b564ec849643d9f7ea05c6d9f0cd7ca23bdd4ac0c2dbef1104ab504543d", size = 306740, upload-time = "2025-09-25T19:49:18.693Z" }, - { url = "https://files.pythonhosted.org/packages/d5/c8/1fdbfc8c0f20875b6b4020f3c7dc447b8de60aa0be5faaf009d24242aec9/bcrypt-5.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:741449132f64b3524e95cd30e5cd3343006ce146088f074f31ab26b94e6c75ba", size = 334197, upload-time = "2025-09-25T19:49:20.523Z" }, - { url = "https://files.pythonhosted.org/packages/a6/c1/8b84545382d75bef226fbc6588af0f7b7d095f7cd6a670b42a86243183cd/bcrypt-5.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:212139484ab3207b1f0c00633d3be92fef3c5f0af17cad155679d03ff2ee1e41", size = 352974, upload-time = "2025-09-25T19:49:22.254Z" }, - { url = "https://files.pythonhosted.org/packages/10/a6/ffb49d4254ed085e62e3e5dd05982b4393e32fe1e49bb1130186617c29cd/bcrypt-5.0.0-cp313-cp313t-win32.whl", hash = "sha256:9d52ed507c2488eddd6a95bccee4e808d3234fa78dd370e24bac65a21212b861", size = 148498, upload-time = "2025-09-25T19:49:24.134Z" }, - { url = "https://files.pythonhosted.org/packages/48/a9/259559edc85258b6d5fc5471a62a3299a6aa37a6611a169756bf4689323c/bcrypt-5.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f6984a24db30548fd39a44360532898c33528b74aedf81c26cf29c51ee47057e", size = 145853, upload-time = "2025-09-25T19:49:25.702Z" }, - { url = "https://files.pythonhosted.org/packages/2d/df/9714173403c7e8b245acf8e4be8876aac64a209d1b392af457c79e60492e/bcrypt-5.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:9fffdb387abe6aa775af36ef16f55e318dcda4194ddbf82007a6f21da29de8f5", size = 139626, upload-time = "2025-09-25T19:49:26.928Z" }, - { url = "https://files.pythonhosted.org/packages/f8/14/c18006f91816606a4abe294ccc5d1e6f0e42304df5a33710e9e8e95416e1/bcrypt-5.0.0-cp314-cp314t-macosx_10_12_universal2.whl", hash = "sha256:4870a52610537037adb382444fefd3706d96d663ac44cbb2f37e3919dca3d7ef", size = 481862, upload-time = "2025-09-25T19:49:28.365Z" }, - { url = "https://files.pythonhosted.org/packages/67/49/dd074d831f00e589537e07a0725cf0e220d1f0d5d8e85ad5bbff251c45aa/bcrypt-5.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48f753100931605686f74e27a7b49238122aa761a9aefe9373265b8b7aa43ea4", size = 268544, upload-time = "2025-09-25T19:49:30.39Z" }, - { url = "https://files.pythonhosted.org/packages/f5/91/50ccba088b8c474545b034a1424d05195d9fcbaaf802ab8bfe2be5a4e0d7/bcrypt-5.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f70aadb7a809305226daedf75d90379c397b094755a710d7014b8b117df1ebbf", size = 271787, upload-time = "2025-09-25T19:49:32.144Z" }, - { url = "https://files.pythonhosted.org/packages/aa/e7/d7dba133e02abcda3b52087a7eea8c0d4f64d3e593b4fffc10c31b7061f3/bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:744d3c6b164caa658adcb72cb8cc9ad9b4b75c7db507ab4bc2480474a51989da", size = 269753, upload-time = "2025-09-25T19:49:33.885Z" }, - { url = "https://files.pythonhosted.org/packages/33/fc/5b145673c4b8d01018307b5c2c1fc87a6f5a436f0ad56607aee389de8ee3/bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a28bc05039bdf3289d757f49d616ab3efe8cf40d8e8001ccdd621cd4f98f4fc9", size = 289587, upload-time = "2025-09-25T19:49:35.144Z" }, - { url = "https://files.pythonhosted.org/packages/27/d7/1ff22703ec6d4f90e62f1a5654b8867ef96bafb8e8102c2288333e1a6ca6/bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:7f277a4b3390ab4bebe597800a90da0edae882c6196d3038a73adf446c4f969f", size = 272178, upload-time = "2025-09-25T19:49:36.793Z" }, - { url = "https://files.pythonhosted.org/packages/c8/88/815b6d558a1e4d40ece04a2f84865b0fef233513bd85fd0e40c294272d62/bcrypt-5.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:79cfa161eda8d2ddf29acad370356b47f02387153b11d46042e93a0a95127493", size = 269295, upload-time = "2025-09-25T19:49:38.164Z" }, - { url = "https://files.pythonhosted.org/packages/51/8c/e0db387c79ab4931fc89827d37608c31cc57b6edc08ccd2386139028dc0d/bcrypt-5.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a5393eae5722bcef046a990b84dff02b954904c36a194f6cfc817d7dca6c6f0b", size = 271700, upload-time = "2025-09-25T19:49:39.917Z" }, - { url = "https://files.pythonhosted.org/packages/06/83/1570edddd150f572dbe9fc00f6203a89fc7d4226821f67328a85c330f239/bcrypt-5.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7f4c94dec1b5ab5d522750cb059bb9409ea8872d4494fd152b53cca99f1ddd8c", size = 334034, upload-time = "2025-09-25T19:49:41.227Z" }, - { url = "https://files.pythonhosted.org/packages/c9/f2/ea64e51a65e56ae7a8a4ec236c2bfbdd4b23008abd50ac33fbb2d1d15424/bcrypt-5.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0cae4cb350934dfd74c020525eeae0a5f79257e8a201c0c176f4b84fdbf2a4b4", size = 352766, upload-time = "2025-09-25T19:49:43.08Z" }, - { url = "https://files.pythonhosted.org/packages/d7/d4/1a388d21ee66876f27d1a1f41287897d0c0f1712ef97d395d708ba93004c/bcrypt-5.0.0-cp314-cp314t-win32.whl", hash = "sha256:b17366316c654e1ad0306a6858e189fc835eca39f7eb2cafd6aaca8ce0c40a2e", size = 152449, upload-time = "2025-09-25T19:49:44.971Z" }, - { url = "https://files.pythonhosted.org/packages/3f/61/3291c2243ae0229e5bca5d19f4032cecad5dfb05a2557169d3a69dc0ba91/bcrypt-5.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:92864f54fb48b4c718fc92a32825d0e42265a627f956bc0361fe869f1adc3e7d", size = 149310, upload-time = "2025-09-25T19:49:46.162Z" }, - { url = "https://files.pythonhosted.org/packages/3e/89/4b01c52ae0c1a681d4021e5dd3e45b111a8fb47254a274fa9a378d8d834b/bcrypt-5.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dd19cf5184a90c873009244586396a6a884d591a5323f0e8a5922560718d4993", size = 143761, upload-time = "2025-09-25T19:49:47.345Z" }, - { url = "https://files.pythonhosted.org/packages/84/29/6237f151fbfe295fe3e074ecc6d44228faa1e842a81f6d34a02937ee1736/bcrypt-5.0.0-cp38-abi3-macosx_10_12_universal2.whl", hash = "sha256:fc746432b951e92b58317af8e0ca746efe93e66555f1b40888865ef5bf56446b", size = 494553, upload-time = "2025-09-25T19:49:49.006Z" }, - { url = "https://files.pythonhosted.org/packages/45/b6/4c1205dde5e464ea3bd88e8742e19f899c16fa8916fb8510a851fae985b5/bcrypt-5.0.0-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c2388ca94ffee269b6038d48747f4ce8df0ffbea43f31abfa18ac72f0218effb", size = 275009, upload-time = "2025-09-25T19:49:50.581Z" }, - { url = "https://files.pythonhosted.org/packages/3b/71/427945e6ead72ccffe77894b2655b695ccf14ae1866cd977e185d606dd2f/bcrypt-5.0.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:560ddb6ec730386e7b3b26b8b4c88197aaed924430e7b74666a586ac997249ef", size = 278029, upload-time = "2025-09-25T19:49:52.533Z" }, - { url = "https://files.pythonhosted.org/packages/17/72/c344825e3b83c5389a369c8a8e58ffe1480b8a699f46c127c34580c4666b/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d79e5c65dcc9af213594d6f7f1fa2c98ad3fc10431e7aa53c176b441943efbdd", size = 275907, upload-time = "2025-09-25T19:49:54.709Z" }, - { url = "https://files.pythonhosted.org/packages/0b/7e/d4e47d2df1641a36d1212e5c0514f5291e1a956a7749f1e595c07a972038/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2b732e7d388fa22d48920baa267ba5d97cca38070b69c0e2d37087b381c681fd", size = 296500, upload-time = "2025-09-25T19:49:56.013Z" }, - { url = "https://files.pythonhosted.org/packages/0f/c3/0ae57a68be2039287ec28bc463b82e4b8dc23f9d12c0be331f4782e19108/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0c8e093ea2532601a6f686edbc2c6b2ec24131ff5c52f7610dd64fa4553b5464", size = 278412, upload-time = "2025-09-25T19:49:57.356Z" }, - { url = "https://files.pythonhosted.org/packages/45/2b/77424511adb11e6a99e3a00dcc7745034bee89036ad7d7e255a7e47be7d8/bcrypt-5.0.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5b1589f4839a0899c146e8892efe320c0fa096568abd9b95593efac50a87cb75", size = 275486, upload-time = "2025-09-25T19:49:59.116Z" }, - { url = "https://files.pythonhosted.org/packages/43/0a/405c753f6158e0f3f14b00b462d8bca31296f7ecfc8fc8bc7919c0c7d73a/bcrypt-5.0.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:89042e61b5e808b67daf24a434d89bab164d4de1746b37a8d173b6b14f3db9ff", size = 277940, upload-time = "2025-09-25T19:50:00.869Z" }, - { url = "https://files.pythonhosted.org/packages/62/83/b3efc285d4aadc1fa83db385ec64dcfa1707e890eb42f03b127d66ac1b7b/bcrypt-5.0.0-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:e3cf5b2560c7b5a142286f69bde914494b6d8f901aaa71e453078388a50881c4", size = 310776, upload-time = "2025-09-25T19:50:02.393Z" }, - { url = "https://files.pythonhosted.org/packages/95/7d/47ee337dacecde6d234890fe929936cb03ebc4c3a7460854bbd9c97780b8/bcrypt-5.0.0-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f632fd56fc4e61564f78b46a2269153122db34988e78b6be8b32d28507b7eaeb", size = 312922, upload-time = "2025-09-25T19:50:04.232Z" }, - { url = "https://files.pythonhosted.org/packages/d6/3a/43d494dfb728f55f4e1cf8fd435d50c16a2d75493225b54c8d06122523c6/bcrypt-5.0.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:801cad5ccb6b87d1b430f183269b94c24f248dddbbc5c1f78b6ed231743e001c", size = 341367, upload-time = "2025-09-25T19:50:05.559Z" }, - { url = "https://files.pythonhosted.org/packages/55/ab/a0727a4547e383e2e22a630e0f908113db37904f58719dc48d4622139b5c/bcrypt-5.0.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3cf67a804fc66fc217e6914a5635000259fbbbb12e78a99488e4d5ba445a71eb", size = 359187, upload-time = "2025-09-25T19:50:06.916Z" }, - { url = "https://files.pythonhosted.org/packages/1b/bb/461f352fdca663524b4643d8b09e8435b4990f17fbf4fea6bc2a90aa0cc7/bcrypt-5.0.0-cp38-abi3-win32.whl", hash = "sha256:3abeb543874b2c0524ff40c57a4e14e5d3a66ff33fb423529c88f180fd756538", size = 153752, upload-time = "2025-09-25T19:50:08.515Z" }, - { url = "https://files.pythonhosted.org/packages/41/aa/4190e60921927b7056820291f56fc57d00d04757c8b316b2d3c0d1d6da2c/bcrypt-5.0.0-cp38-abi3-win_amd64.whl", hash = "sha256:35a77ec55b541e5e583eb3436ffbbf53b0ffa1fa16ca6782279daf95d146dcd9", size = 150881, upload-time = "2025-09-25T19:50:09.742Z" }, - { url = "https://files.pythonhosted.org/packages/54/12/cd77221719d0b39ac0b55dbd39358db1cd1246e0282e104366ebbfb8266a/bcrypt-5.0.0-cp38-abi3-win_arm64.whl", hash = "sha256:cde08734f12c6a4e28dc6755cd11d3bdfea608d93d958fffbe95a7026ebe4980", size = 144931, upload-time = "2025-09-25T19:50:11.016Z" }, - { url = "https://files.pythonhosted.org/packages/5d/ba/2af136406e1c3839aea9ecadc2f6be2bcd1eff255bd451dd39bcf302c47a/bcrypt-5.0.0-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:0c418ca99fd47e9c59a301744d63328f17798b5947b0f791e9af3c1c499c2d0a", size = 495313, upload-time = "2025-09-25T19:50:12.309Z" }, - { url = "https://files.pythonhosted.org/packages/ac/ee/2f4985dbad090ace5ad1f7dd8ff94477fe089b5fab2040bd784a3d5f187b/bcrypt-5.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddb4e1500f6efdd402218ffe34d040a1196c072e07929b9820f363a1fd1f4191", size = 275290, upload-time = "2025-09-25T19:50:13.673Z" }, - { url = "https://files.pythonhosted.org/packages/e4/6e/b77ade812672d15cf50842e167eead80ac3514f3beacac8902915417f8b7/bcrypt-5.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7aeef54b60ceddb6f30ee3db090351ecf0d40ec6e2abf41430997407a46d2254", size = 278253, upload-time = "2025-09-25T19:50:15.089Z" }, - { url = "https://files.pythonhosted.org/packages/36/c4/ed00ed32f1040f7990dac7115f82273e3c03da1e1a1587a778d8cea496d8/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f0ce778135f60799d89c9693b9b398819d15f1921ba15fe719acb3178215a7db", size = 276084, upload-time = "2025-09-25T19:50:16.699Z" }, - { url = "https://files.pythonhosted.org/packages/e7/c4/fa6e16145e145e87f1fa351bbd54b429354fd72145cd3d4e0c5157cf4c70/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a71f70ee269671460b37a449f5ff26982a6f2ba493b3eabdd687b4bf35f875ac", size = 297185, upload-time = "2025-09-25T19:50:18.525Z" }, - { url = "https://files.pythonhosted.org/packages/24/b4/11f8a31d8b67cca3371e046db49baa7c0594d71eb40ac8121e2fc0888db0/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f8429e1c410b4073944f03bd778a9e066e7fad723564a52ff91841d278dfc822", size = 278656, upload-time = "2025-09-25T19:50:19.809Z" }, - { url = "https://files.pythonhosted.org/packages/ac/31/79f11865f8078e192847d2cb526e3fa27c200933c982c5b2869720fa5fce/bcrypt-5.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:edfcdcedd0d0f05850c52ba3127b1fce70b9f89e0fe5ff16517df7e81fa3cbb8", size = 275662, upload-time = "2025-09-25T19:50:21.567Z" }, - { url = "https://files.pythonhosted.org/packages/d4/8d/5e43d9584b3b3591a6f9b68f755a4da879a59712981ef5ad2a0ac1379f7a/bcrypt-5.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:611f0a17aa4a25a69362dcc299fda5c8a3d4f160e2abb3831041feb77393a14a", size = 278240, upload-time = "2025-09-25T19:50:23.305Z" }, - { url = "https://files.pythonhosted.org/packages/89/48/44590e3fc158620f680a978aafe8f87a4c4320da81ed11552f0323aa9a57/bcrypt-5.0.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:db99dca3b1fdc3db87d7c57eac0c82281242d1eabf19dcb8a6b10eb29a2e72d1", size = 311152, upload-time = "2025-09-25T19:50:24.597Z" }, - { url = "https://files.pythonhosted.org/packages/5f/85/e4fbfc46f14f47b0d20493669a625da5827d07e8a88ee460af6cd9768b44/bcrypt-5.0.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:5feebf85a9cefda32966d8171f5db7e3ba964b77fdfe31919622256f80f9cf42", size = 313284, upload-time = "2025-09-25T19:50:26.268Z" }, - { url = "https://files.pythonhosted.org/packages/25/ae/479f81d3f4594456a01ea2f05b132a519eff9ab5768a70430fa1132384b1/bcrypt-5.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3ca8a166b1140436e058298a34d88032ab62f15aae1c598580333dc21d27ef10", size = 341643, upload-time = "2025-09-25T19:50:28.02Z" }, - { url = "https://files.pythonhosted.org/packages/df/d2/36a086dee1473b14276cd6ea7f61aef3b2648710b5d7f1c9e032c29b859f/bcrypt-5.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:61afc381250c3182d9078551e3ac3a41da14154fbff647ddf52a769f588c4172", size = 359698, upload-time = "2025-09-25T19:50:31.347Z" }, - { url = "https://files.pythonhosted.org/packages/c0/f6/688d2cd64bfd0b14d805ddb8a565e11ca1fb0fd6817175d58b10052b6d88/bcrypt-5.0.0-cp39-abi3-win32.whl", hash = "sha256:64d7ce196203e468c457c37ec22390f1a61c85c6f0b8160fd752940ccfb3a683", size = 153725, upload-time = "2025-09-25T19:50:34.384Z" }, - { url = "https://files.pythonhosted.org/packages/9f/b9/9d9a641194a730bda138b3dfe53f584d61c58cd5230e37566e83ec2ffa0d/bcrypt-5.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:64ee8434b0da054d830fa8e89e1c8bf30061d539044a39524ff7dec90481e5c2", size = 150912, upload-time = "2025-09-25T19:50:35.69Z" }, - { url = "https://files.pythonhosted.org/packages/27/44/d2ef5e87509158ad2187f4dd0852df80695bb1ee0cfe0a684727b01a69e0/bcrypt-5.0.0-cp39-abi3-win_arm64.whl", hash = "sha256:f2347d3534e76bf50bca5500989d6c1d05ed64b440408057a37673282c654927", size = 144953, upload-time = "2025-09-25T19:50:37.32Z" }, + { url = "https://files.pythonhosted.org/packages/78/d4/3b2657bd58ef02b23a07729b0df26f21af97169dbd0b5797afa9e97ebb49/bcrypt-4.0.1-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:b1023030aec778185a6c16cf70f359cbb6e0c289fd564a7cfa29e727a1c38f8f", size = 473446, upload-time = "2022-10-09T15:36:25.481Z" }, + { url = "https://files.pythonhosted.org/packages/ec/0a/1582790232fef6c2aa201f345577306b8bfe465c2c665dec04c86a016879/bcrypt-4.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:08d2947c490093a11416df18043c27abe3921558d2c03e2076ccb28a116cb6d0", size = 583044, upload-time = "2022-10-09T15:37:09.447Z" }, + { url = "https://files.pythonhosted.org/packages/41/16/49ff5146fb815742ad58cafb5034907aa7f166b1344d0ddd7fd1c818bd17/bcrypt-4.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0eaa47d4661c326bfc9d08d16debbc4edf78778e6aaba29c1bc7ce67214d4410", size = 583189, upload-time = "2022-10-09T15:37:10.69Z" }, + { url = "https://files.pythonhosted.org/packages/aa/48/fd2b197a9741fa790ba0b88a9b10b5e88e62ff5cf3e1bc96d8354d7ce613/bcrypt-4.0.1-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae88eca3024bb34bb3430f964beab71226e761f51b912de5133470b649d82344", size = 593473, upload-time = "2022-10-09T15:36:27.195Z" }, + { url = "https://files.pythonhosted.org/packages/7d/50/e683d8418974a602ba40899c8a5c38b3decaf5a4d36c32fc65dce454d8a8/bcrypt-4.0.1-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:a522427293d77e1c29e303fc282e2d71864579527a04ddcfda6d4f8396c6c36a", size = 593249, upload-time = "2022-10-09T15:36:28.481Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a7/ee4561fd9b78ca23c8e5591c150cc58626a5dfb169345ab18e1c2c664ee0/bcrypt-4.0.1-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:fbdaec13c5105f0c4e5c52614d04f0bca5f5af007910daa8b6b12095edaa67b3", size = 583586, upload-time = "2022-10-09T15:37:11.962Z" }, + { url = "https://files.pythonhosted.org/packages/64/fe/da28a5916128d541da0993328dc5cf4b43dfbf6655f2c7a2abe26ca2dc88/bcrypt-4.0.1-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:ca3204d00d3cb2dfed07f2d74a25f12fc12f73e606fcaa6975d1f7ae69cacbb2", size = 593659, upload-time = "2022-10-09T15:36:30.049Z" }, + { url = "https://files.pythonhosted.org/packages/dd/4f/3632a69ce344c1551f7c9803196b191a8181c6a1ad2362c225581ef0d383/bcrypt-4.0.1-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:089098effa1bc35dc055366740a067a2fc76987e8ec75349eb9484061c54f535", size = 613116, upload-time = "2022-10-09T15:37:14.107Z" }, + { url = "https://files.pythonhosted.org/packages/87/69/edacb37481d360d06fc947dab5734aaf511acb7d1a1f9e2849454376c0f8/bcrypt-4.0.1-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:e9a51bbfe7e9802b5f3508687758b564069ba937748ad7b9e890086290d2f79e", size = 624290, upload-time = "2022-10-09T15:36:31.251Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ca/6a534669890725cbb8c1fb4622019be31813c8edaa7b6d5b62fc9360a17e/bcrypt-4.0.1-cp36-abi3-win32.whl", hash = "sha256:2caffdae059e06ac23fce178d31b4a702f2a3264c20bfb5ff541b338194d8fab", size = 159428, upload-time = "2022-10-09T15:36:32.893Z" }, + { url = "https://files.pythonhosted.org/packages/46/81/d8c22cd7e5e1c6a7d48e41a1d1d46c92f17dae70a54d9814f746e6027dec/bcrypt-4.0.1-cp36-abi3-win_amd64.whl", hash = "sha256:8a68f4341daf7522fe8d73874de8906f3a339048ba406be6ddc1b3ccb16fc0d9", size = 152930, upload-time = "2022-10-09T15:36:34.635Z" }, ] [[package]] @@ -427,6 +380,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl", hash = "sha256:597bfef186b6f60181535a29fbe44865ce137a5079f295b479886c82729d5f3f", size = 11298, upload-time = "2025-10-30T08:19:00.758Z" }, ] +[[package]] +name = "dnspython" +version = "2.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, +] + [[package]] name = "ecdsa" version = "0.19.1" @@ -439,6 +401,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/a3/460c57f094a4a165c84a1341c373b0a4f5ec6ac244b998d5021aade89b77/ecdsa-0.19.1-py2.py3-none-any.whl", hash = "sha256:30638e27cf77b7e15c4c4cc1973720149e1033827cfd00661ca5c8cc0cdb24c3", size = 150607, upload-time = "2025-03-13T11:52:41.757Z" }, ] +[[package]] +name = "email-validator" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dnspython" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/22/900cb125c76b7aaa450ce02fd727f452243f2e91a61af068b40adba60ea9/email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426", size = 51238, upload-time = "2025-08-26T13:09:06.831Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604, upload-time = "2025-08-26T13:09:05.858Z" }, +] + [[package]] name = "fastapi" version = "0.115.6" @@ -495,12 +470,13 @@ wheels = [ ] [[package]] -name = "galaxy-po-backend" +name = "galaxis-po-backend" version = "1.0.0" source = { virtual = "." } dependencies = [ { name = "alembic" }, { name = "apscheduler" }, + { name = "bcrypt" }, { name = "beautifulsoup4" }, { name = "fastapi" }, { name = "httpx" }, @@ -509,12 +485,13 @@ dependencies = [ { name = "pandas" }, { name = "passlib", extra = ["bcrypt"] }, { name = "psycopg2-binary" }, - { name = "pydantic" }, + { name = "pydantic", extra = ["email"] }, { name = "pydantic-settings" }, { name = "pykrx" }, { name = "python-jose", extra = ["cryptography"] }, { name = "python-multipart" }, { name = "requests" }, + { name = "setuptools" }, { name = "sqlalchemy" }, { name = "uvicorn", extra = ["standard"] }, ] @@ -535,6 +512,7 @@ dev = [ requires-dist = [ { name = "alembic", specifier = "==1.14.0" }, { name = "apscheduler", specifier = "==3.10.4" }, + { name = "bcrypt", specifier = "==4.0.1" }, { name = "beautifulsoup4", specifier = "==4.12.3" }, { name = "fastapi", specifier = "==0.115.6" }, { name = "httpx", specifier = "==0.28.1" }, @@ -543,7 +521,7 @@ requires-dist = [ { name = "pandas", specifier = "==2.2.3" }, { name = "passlib", extras = ["bcrypt"], specifier = "==1.7.4" }, { name = "psycopg2-binary", specifier = "==2.9.10" }, - { name = "pydantic", specifier = "==2.10.4" }, + { name = "pydantic", extras = ["email"], specifier = "==2.10.4" }, { name = "pydantic-settings", specifier = "==2.7.1" }, { name = "pykrx", specifier = "==1.0.45" }, { name = "pytest", marker = "extra == 'dev'", specifier = "==8.3.4" }, @@ -551,6 +529,7 @@ requires-dist = [ { name = "python-jose", extras = ["cryptography"], specifier = "==3.3.0" }, { name = "python-multipart", specifier = "==0.0.20" }, { name = "requests", specifier = "==2.32.3" }, + { name = "setuptools" }, { name = "sqlalchemy", specifier = "==2.0.36" }, { name = "uvicorn", extras = ["standard"], specifier = "==0.34.0" }, ] @@ -1173,6 +1152,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f3/26/3e1bbe954fde7ee22a6e7d31582c642aad9e84ffe4b5fb61e63b87cd326f/pydantic-2.10.4-py3-none-any.whl", hash = "sha256:597e135ea68be3a37552fb524bc7d0d66dcf93d395acd93a00682f1efcb8ee3d", size = 431765, upload-time = "2024-12-18T17:09:21.953Z" }, ] +[package.optional-dependencies] +email = [ + { name = "email-validator" }, +] + [[package]] name = "pydantic-core" version = "2.27.2" @@ -1410,6 +1394,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" }, ] +[[package]] +name = "setuptools" +version = "80.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/76/95/faf61eb8363f26aa7e1d762267a8d602a1b26d4f3a1e758e92cb3cb8b054/setuptools-80.10.2.tar.gz", hash = "sha256:8b0e9d10c784bf7d262c4e5ec5d4ec94127ce206e8738f29a437945fbc219b70", size = 1200343, upload-time = "2026-01-25T22:38:17.252Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/b8/f1f62a5e3c0ad2ff1d189590bfa4c46b4f3b6e49cef6f26c6ee4e575394d/setuptools-80.10.2-py3-none-any.whl", hash = "sha256:95b30ddfb717250edb492926c92b5221f7ef3fbcc2b07579bcd4a27da21d0173", size = 1064234, upload-time = "2026-01-25T22:38:15.216Z" }, +] + [[package]] name = "six" version = "1.17.0" diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index 0c499a7..0c93501 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -1,10 +1,19 @@ import type { Metadata } from 'next'; -import { Inter } from 'next/font/google'; +import { Inter, Noto_Sans_KR } from 'next/font/google'; import './globals.css'; import { ThemeProvider } from '@/components/providers/theme-provider'; import { Toaster } from '@/components/ui/sonner'; -const inter = Inter({ subsets: ['latin'] }); +const inter = Inter({ + subsets: ['latin'], + variable: '--font-inter', +}); + +const notoSansKR = Noto_Sans_KR({ + subsets: ['latin'], + weight: ['400', '500', '600', '700'], + variable: '--font-noto-sans-kr', +}); export const metadata: Metadata = { title: 'Galaxis-Po', @@ -18,7 +27,7 @@ export default function RootLayout({ }>) { return ( -
+