diff options
| author | 2026-07-01 23:06:39 +0200 | |
|---|---|---|
| committer | 2026-07-01 23:06:39 +0200 | |
| commit | 4105c3b777e82cb4b6dadf27c109dd21010ad8ce (patch) | |
| tree | ab497c0bac381d27765cff389bcf855a9e0bf0b7 | |
| parent | d8968acab83c7a91b01eee4b35828a2b05c8dd6b (diff) | |
| download | server-4105c3b777e82cb4b6dadf27c109dd21010ad8ce.tar.gz server-4105c3b777e82cb4b6dadf27c109dd21010ad8ce.tar.bz2 server-4105c3b777e82cb4b6dadf27c109dd21010ad8ce.zip | |
Added db support
| -rw-r--r-- | alembic.ini | 149 | ||||
| -rw-r--r-- | conf/__init__.py | 8 | ||||
| -rw-r--r-- | conf/local.py | 3 | ||||
| -rw-r--r-- | db/__init__.py | 0 | ||||
| -rw-r--r-- | db/migrations/README | 1 | ||||
| -rw-r--r-- | db/migrations/env.py | 82 | ||||
| -rw-r--r-- | db/migrations/script.py.mako | 29 | ||||
| -rw-r--r-- | db/migrations/versions/202607012255-40e20fbf63c9_.py | 35 | ||||
| -rw-r--r-- | db/migrations/versions/202607012255-6ebaa6ab0f48_.py | 39 | ||||
| -rw-r--r-- | db/models.py | 13 | ||||
| -rw-r--r-- | db/session.py | 21 | ||||
| -rw-r--r-- | proxy/endpoints.py | 2 | ||||
| -rw-r--r-- | proxy/queue.py (renamed from ttun_server/proxy_queue.py) | 0 | ||||
| -rw-r--r-- | proxy/websockets.py | 2 | ||||
| -rw-r--r-- | pyproject.toml | 3 | ||||
| -rw-r--r-- | ttun_server/websockets.py | 2 | ||||
| -rw-r--r-- | utils/__init__.py | 0 | ||||
| -rw-r--r-- | utils/json.py | 16 | ||||
| -rw-r--r-- | uv.lock | 121 |
19 files changed, 523 insertions, 3 deletions
diff --git a/alembic.ini b/alembic.ini new file mode 100644 index 0000000..67f7bd4 --- /dev/null +++ b/alembic.ini | |||
| @@ -0,0 +1,149 @@ | |||
| 1 | # A generic, single database configuration. | ||
| 2 | |||
| 3 | [alembic] | ||
| 4 | # path to migration scripts. | ||
| 5 | # this is typically a path given in POSIX (e.g. forward slashes) | ||
| 6 | # format, relative to the token %(here)s which refers to the location of this | ||
| 7 | # ini file | ||
| 8 | script_location = %(here)s/db/migrations | ||
| 9 | |||
| 10 | # template used to generate migration file names; The default value is %%(rev)s_%%(slug)s | ||
| 11 | # Uncomment the line below if you want the files to be prepended with date and time | ||
| 12 | # see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file | ||
| 13 | # for all available tokens | ||
| 14 | file_template = %%(year)d%%(month).2d%%(day).2d%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s | ||
| 15 | # Or organize into date-based subdirectories (requires recursive_version_locations = true) | ||
| 16 | # file_template = %%(year)d/%%(month).2d/%%(day).2d_%%(hour).2d%%(minute).2d_%%(second).2d_%%(rev)s_%%(slug)s | ||
| 17 | |||
| 18 | # sys.path path, will be prepended to sys.path if present. | ||
| 19 | # defaults to the current working directory. for multiple paths, the path separator | ||
| 20 | # is defined by "path_separator" below. | ||
| 21 | prepend_sys_path = . | ||
| 22 | |||
| 23 | |||
| 24 | # timezone to use when rendering the date within the migration file | ||
| 25 | # as well as the filename. | ||
| 26 | # If specified, requires the tzdata library which can be installed by adding | ||
| 27 | # `alembic[tz]` to the pip requirements. | ||
| 28 | # string value is passed to ZoneInfo() | ||
| 29 | # leave blank for localtime | ||
| 30 | # timezone = | ||
| 31 | |||
| 32 | # max length of characters to apply to the "slug" field | ||
| 33 | # truncate_slug_length = 40 | ||
| 34 | |||
| 35 | # set to 'true' to run the environment during | ||
| 36 | # the 'revision' command, regardless of autogenerate | ||
| 37 | # revision_environment = false | ||
| 38 | |||
| 39 | # set to 'true' to allow .pyc and .pyo files without | ||
| 40 | # a source .py file to be detected as revisions in the | ||
| 41 | # versions/ directory | ||
| 42 | # sourceless = false | ||
| 43 | |||
| 44 | # version location specification; This defaults | ||
| 45 | # to <script_location>/versions. When using multiple version | ||
| 46 | # directories, initial revisions must be specified with --version-path. | ||
| 47 | # The path separator used here should be the separator specified by "path_separator" | ||
| 48 | # below. | ||
| 49 | # version_locations = %(here)s/bar:%(here)s/bat:%(here)s/alembic/versions | ||
| 50 | |||
| 51 | # path_separator; This indicates what character is used to split lists of file | ||
| 52 | # paths, including version_locations and prepend_sys_path within configparser | ||
| 53 | # files such as alembic.ini. | ||
| 54 | # The default rendered in new alembic.ini files is "os", which uses os.pathsep | ||
| 55 | # to provide os-dependent path splitting. | ||
| 56 | # | ||
| 57 | # Note that in order to support legacy alembic.ini files, this default does NOT | ||
| 58 | # take place if path_separator is not present in alembic.ini. If this | ||
| 59 | # option is omitted entirely, fallback logic is as follows: | ||
| 60 | # | ||
| 61 | # 1. Parsing of the version_locations option falls back to using the legacy | ||
| 62 | # "version_path_separator" key, which if absent then falls back to the legacy | ||
| 63 | # behavior of splitting on spaces and/or commas. | ||
| 64 | # 2. Parsing of the prepend_sys_path option falls back to the legacy | ||
| 65 | # behavior of splitting on spaces, commas, or colons. | ||
| 66 | # | ||
| 67 | # Valid values for path_separator are: | ||
| 68 | # | ||
| 69 | # path_separator = : | ||
| 70 | # path_separator = ; | ||
| 71 | # path_separator = space | ||
| 72 | # path_separator = newline | ||
| 73 | # | ||
| 74 | # Use os.pathsep. Default configuration used for new projects. | ||
| 75 | path_separator = os | ||
| 76 | |||
| 77 | # set to 'true' to search source files recursively | ||
| 78 | # in each "version_locations" directory | ||
| 79 | # new in Alembic version 1.10 | ||
| 80 | # recursive_version_locations = false | ||
| 81 | |||
| 82 | # the output encoding used when revision files | ||
| 83 | # are written from script.py.mako | ||
| 84 | # output_encoding = utf-8 | ||
| 85 | |||
| 86 | # database URL. This is consumed by the user-maintained env.py script only. | ||
| 87 | # other means of configuring database URLs may be customized within the env.py | ||
| 88 | # file. | ||
| 89 | sqlalchemy.url = driver://user:pass@localhost/dbname | ||
| 90 | |||
| 91 | |||
| 92 | [post_write_hooks] | ||
| 93 | # post_write_hooks defines scripts or Python functions that are run | ||
| 94 | # on newly generated revision scripts. See the documentation for further | ||
| 95 | # detail and examples | ||
| 96 | |||
| 97 | # format using "black" - use the console_scripts runner, against the "black" entrypoint | ||
| 98 | # hooks = black | ||
| 99 | # black.type = console_scripts | ||
| 100 | # black.entrypoint = black | ||
| 101 | # black.options = -l 79 REVISION_SCRIPT_FILENAME | ||
| 102 | |||
| 103 | # lint with attempts to fix using "ruff" - use the module runner, against the "ruff" module | ||
| 104 | # hooks = ruff | ||
| 105 | # ruff.type = module | ||
| 106 | # ruff.module = ruff | ||
| 107 | # ruff.options = check --fix REVISION_SCRIPT_FILENAME | ||
| 108 | |||
| 109 | # Alternatively, use the exec runner to execute a binary found on your PATH | ||
| 110 | # hooks = ruff | ||
| 111 | # ruff.type = exec | ||
| 112 | # ruff.executable = ruff | ||
| 113 | # ruff.options = check --fix REVISION_SCRIPT_FILENAME | ||
| 114 | |||
| 115 | # Logging configuration. This is also consumed by the user-maintained | ||
| 116 | # env.py script only. | ||
| 117 | [loggers] | ||
| 118 | keys = root,sqlalchemy,alembic | ||
| 119 | |||
| 120 | [handlers] | ||
| 121 | keys = console | ||
| 122 | |||
| 123 | [formatters] | ||
| 124 | keys = generic | ||
| 125 | |||
| 126 | [logger_root] | ||
| 127 | level = WARNING | ||
| 128 | handlers = console | ||
| 129 | qualname = | ||
| 130 | |||
| 131 | [logger_sqlalchemy] | ||
| 132 | level = WARNING | ||
| 133 | handlers = | ||
| 134 | qualname = sqlalchemy.engine | ||
| 135 | |||
| 136 | [logger_alembic] | ||
| 137 | level = INFO | ||
| 138 | handlers = | ||
| 139 | qualname = alembic | ||
| 140 | |||
| 141 | [handler_console] | ||
| 142 | class = StreamHandler | ||
| 143 | args = (sys.stderr,) | ||
| 144 | level = NOTSET | ||
| 145 | formatter = generic | ||
| 146 | |||
| 147 | [formatter_generic] | ||
| 148 | format = %(levelname)-5.5s [%(name)s] %(message)s | ||
| 149 | datefmt = %H:%M:%S | ||
diff --git a/conf/__init__.py b/conf/__init__.py new file mode 100644 index 0000000..c08dcee --- /dev/null +++ b/conf/__init__.py | |||
| @@ -0,0 +1,8 @@ | |||
| 1 | import os | ||
| 2 | |||
| 3 | try: | ||
| 4 | from .local import * | ||
| 5 | except ImportError: | ||
| 6 | pass | ||
| 7 | |||
| 8 | DATABASE_URL = os.environ.get('DATABASE_URL') | ||
diff --git a/conf/local.py b/conf/local.py new file mode 100644 index 0000000..608e0e5 --- /dev/null +++ b/conf/local.py | |||
| @@ -0,0 +1,3 @@ | |||
| 1 | import os | ||
| 2 | |||
| 3 | os.environ.setdefault('DATABASE_URL', 'postgresql:///ttun') | ||
diff --git a/db/__init__.py b/db/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/db/__init__.py | |||
diff --git a/db/migrations/README b/db/migrations/README new file mode 100644 index 0000000..98e4f9c --- /dev/null +++ b/db/migrations/README | |||
| @@ -0,0 +1 @@ | |||
| Generic single-database configuration. \ No newline at end of file | |||
diff --git a/db/migrations/env.py b/db/migrations/env.py new file mode 100644 index 0000000..f1b3156 --- /dev/null +++ b/db/migrations/env.py | |||
| @@ -0,0 +1,82 @@ | |||
| 1 | from logging.config import fileConfig | ||
| 2 | |||
| 3 | from sqlalchemy import engine_from_config | ||
| 4 | from sqlalchemy import pool | ||
| 5 | |||
| 6 | from alembic import context | ||
| 7 | |||
| 8 | from conf import DATABASE_URL | ||
| 9 | from db.models import * | ||
| 10 | |||
| 11 | # this is the Alembic Config object, which provides | ||
| 12 | # access to the values within the .ini file in use. | ||
| 13 | config = context.config | ||
| 14 | |||
| 15 | # Interpret the config file for Python logging. | ||
| 16 | # This line sets up loggers basically. | ||
| 17 | if config.config_file_name is not None: | ||
| 18 | fileConfig(config.config_file_name) | ||
| 19 | |||
| 20 | # add your model's MetaData object here | ||
| 21 | # for 'autogenerate' support | ||
| 22 | # from myapp import mymodel | ||
| 23 | # target_metadata = mymodel.Base.metadata | ||
| 24 | target_metadata = SQLModel.metadata | ||
| 25 | |||
| 26 | # other values from the config, defined by the needs of env.py, | ||
| 27 | # can be acquired: | ||
| 28 | # my_important_option = config.get_main_option("my_important_option") | ||
| 29 | # ... etc. | ||
| 30 | config.set_main_option('sqlalchemy.url', DATABASE_URL) | ||
| 31 | |||
| 32 | |||
| 33 | def run_migrations_offline() -> None: | ||
| 34 | """Run migrations in 'offline' mode. | ||
| 35 | |||
| 36 | This configures the context with just a URL | ||
| 37 | and not an Engine, though an Engine is acceptable | ||
| 38 | here as well. By skipping the Engine creation | ||
| 39 | we don't even need a DBAPI to be available. | ||
| 40 | |||
| 41 | Calls to context.execute() here emit the given string to the | ||
| 42 | script output. | ||
| 43 | |||
| 44 | """ | ||
| 45 | url = config.get_main_option("sqlalchemy.url") | ||
| 46 | context.configure( | ||
| 47 | url=url, | ||
| 48 | target_metadata=target_metadata, | ||
| 49 | literal_binds=True, | ||
| 50 | dialect_opts={"paramstyle": "named"}, | ||
| 51 | ) | ||
| 52 | |||
| 53 | with context.begin_transaction(): | ||
| 54 | context.run_migrations() | ||
| 55 | |||
| 56 | |||
| 57 | def run_migrations_online() -> None: | ||
| 58 | """Run migrations in 'online' mode. | ||
| 59 | |||
| 60 | In this scenario we need to create an Engine | ||
| 61 | and associate a connection with the context. | ||
| 62 | |||
| 63 | """ | ||
| 64 | connectable = engine_from_config( | ||
| 65 | config.get_section(config.config_ini_section, {}), | ||
| 66 | prefix="sqlalchemy.", | ||
| 67 | poolclass=pool.NullPool, | ||
| 68 | ) | ||
| 69 | |||
| 70 | with connectable.connect() as connection: | ||
| 71 | context.configure( | ||
| 72 | connection=connection, target_metadata=target_metadata | ||
| 73 | ) | ||
| 74 | |||
| 75 | with context.begin_transaction(): | ||
| 76 | context.run_migrations() | ||
| 77 | |||
| 78 | |||
| 79 | if context.is_offline_mode(): | ||
| 80 | run_migrations_offline() | ||
| 81 | else: | ||
| 82 | run_migrations_online() | ||
diff --git a/db/migrations/script.py.mako b/db/migrations/script.py.mako new file mode 100644 index 0000000..697cf67 --- /dev/null +++ b/db/migrations/script.py.mako | |||
| @@ -0,0 +1,29 @@ | |||
| 1 | """${message} | ||
| 2 | |||
| 3 | Revision ID: ${up_revision} | ||
| 4 | Revises: ${down_revision | comma,n} | ||
| 5 | Create Date: ${create_date} | ||
| 6 | |||
| 7 | """ | ||
| 8 | from typing import Sequence, Union | ||
| 9 | |||
| 10 | from alembic import op | ||
| 11 | import sqlalchemy as sa | ||
| 12 | import sqlmodel | ||
| 13 | ${imports if imports else ""} | ||
| 14 | |||
| 15 | # revision identifiers, used by Alembic. | ||
| 16 | revision: str = ${repr(up_revision)} | ||
| 17 | down_revision: Union[str, Sequence[str], None] = ${repr(down_revision)} | ||
| 18 | branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} | ||
| 19 | depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} | ||
| 20 | |||
| 21 | |||
| 22 | def upgrade() -> None: | ||
| 23 | """Upgrade schema.""" | ||
| 24 | ${upgrades if upgrades else "pass"} | ||
| 25 | |||
| 26 | |||
| 27 | def downgrade() -> None: | ||
| 28 | """Downgrade schema.""" | ||
| 29 | ${downgrades if downgrades else "pass"} | ||
diff --git a/db/migrations/versions/202607012255-40e20fbf63c9_.py b/db/migrations/versions/202607012255-40e20fbf63c9_.py new file mode 100644 index 0000000..a32131a --- /dev/null +++ b/db/migrations/versions/202607012255-40e20fbf63c9_.py | |||
| @@ -0,0 +1,35 @@ | |||
| 1 | """empty message | ||
| 2 | |||
| 3 | Revision ID: 40e20fbf63c9 | ||
| 4 | Revises: 6ebaa6ab0f48 | ||
| 5 | Create Date: 2026-07-01 22:55:55.017591 | ||
| 6 | |||
| 7 | """ | ||
| 8 | from typing import Sequence, Union | ||
| 9 | |||
| 10 | from alembic import op | ||
| 11 | import sqlalchemy as sa | ||
| 12 | import sqlmodel | ||
| 13 | |||
| 14 | |||
| 15 | # revision identifiers, used by Alembic. | ||
| 16 | revision: str = '40e20fbf63c9' | ||
| 17 | down_revision: Union[str, Sequence[str], None] = '6ebaa6ab0f48' | ||
| 18 | branch_labels: Union[str, Sequence[str], None] = None | ||
| 19 | depends_on: Union[str, Sequence[str], None] = None | ||
| 20 | |||
| 21 | |||
| 22 | def upgrade() -> None: | ||
| 23 | """Upgrade schema.""" | ||
| 24 | # ### commands auto generated by Alembic - please adjust! ### | ||
| 25 | op.add_column('user', sa.Column('external_id', sa.Uuid(), nullable=False, server_default=sa.text('gen_random_uuid()'))) | ||
| 26 | op.create_index(op.f('ix_user_external_id'), 'user', ['external_id'], unique=True) | ||
| 27 | # ### end Alembic commands ### | ||
| 28 | |||
| 29 | |||
| 30 | def downgrade() -> None: | ||
| 31 | """Downgrade schema.""" | ||
| 32 | # ### commands auto generated by Alembic - please adjust! ### | ||
| 33 | op.drop_index(op.f('ix_user_external_id'), table_name='user') | ||
| 34 | op.drop_column('user', 'external_id') | ||
| 35 | # ### end Alembic commands ### | ||
diff --git a/db/migrations/versions/202607012255-6ebaa6ab0f48_.py b/db/migrations/versions/202607012255-6ebaa6ab0f48_.py new file mode 100644 index 0000000..9d4daa2 --- /dev/null +++ b/db/migrations/versions/202607012255-6ebaa6ab0f48_.py | |||
| @@ -0,0 +1,39 @@ | |||
| 1 | """empty message | ||
| 2 | |||
| 3 | Revision ID: 6ebaa6ab0f48 | ||
| 4 | Revises: | ||
| 5 | Create Date: 2026-07-01 22:55:14.146711 | ||
| 6 | |||
| 7 | """ | ||
| 8 | from typing import Sequence, Union | ||
| 9 | |||
| 10 | from alembic import op | ||
| 11 | import sqlalchemy as sa | ||
| 12 | import sqlmodel | ||
| 13 | |||
| 14 | |||
| 15 | # revision identifiers, used by Alembic. | ||
| 16 | revision: str = '6ebaa6ab0f48' | ||
| 17 | down_revision: Union[str, Sequence[str], None] = None | ||
| 18 | branch_labels: Union[str, Sequence[str], None] = None | ||
| 19 | depends_on: Union[str, Sequence[str], None] = None | ||
| 20 | |||
| 21 | |||
| 22 | def upgrade() -> None: | ||
| 23 | """Upgrade schema.""" | ||
| 24 | # ### commands auto generated by Alembic - please adjust! ### | ||
| 25 | op.create_table('user', | ||
| 26 | sa.Column('id', sa.Integer(), nullable=False), | ||
| 27 | sa.Column('username', sqlmodel.sql.sqltypes.AutoString(), nullable=False), | ||
| 28 | sa.PrimaryKeyConstraint('id') | ||
| 29 | ) | ||
| 30 | op.create_index(op.f('ix_user_username'), 'user', ['username'], unique=True) | ||
| 31 | # ### end Alembic commands ### | ||
| 32 | |||
| 33 | |||
| 34 | def downgrade() -> None: | ||
| 35 | """Downgrade schema.""" | ||
| 36 | # ### commands auto generated by Alembic - please adjust! ### | ||
| 37 | op.drop_index(op.f('ix_user_username'), table_name='user') | ||
| 38 | op.drop_table('user') | ||
| 39 | # ### end Alembic commands ### | ||
diff --git a/db/models.py b/db/models.py new file mode 100644 index 0000000..b66f5bc --- /dev/null +++ b/db/models.py | |||
| @@ -0,0 +1,13 @@ | |||
| 1 | from uuid import uuid4, UUID | ||
| 2 | |||
| 3 | from sqlmodel import SQLModel, Field | ||
| 4 | |||
| 5 | |||
| 6 | class BaseModel(SQLModel): | ||
| 7 | id: int | None = Field(primary_key=True, default=None) | ||
| 8 | |||
| 9 | class ExternalModelMixin: | ||
| 10 | external_id: UUID = Field(index=True, unique=True, default_factory=uuid4) | ||
| 11 | |||
| 12 | class User(BaseModel, ExternalModelMixin, table=True): | ||
| 13 | username: str = Field(unique=True, index=True) | ||
diff --git a/db/session.py b/db/session.py new file mode 100644 index 0000000..4d83649 --- /dev/null +++ b/db/session.py | |||
| @@ -0,0 +1,21 @@ | |||
| 1 | from contextlib import contextmanager | ||
| 2 | |||
| 3 | from sqlalchemy import create_engine | ||
| 4 | from sqlalchemy.orm import sessionmaker, scoped_session | ||
| 5 | from sqlmodel import Session | ||
| 6 | |||
| 7 | from conf import DATABASE_URL | ||
| 8 | from utils.json import json_serializer | ||
| 9 | |||
| 10 | |||
| 11 | engine = create_engine(DATABASE_URL, echo=True, future=True, json_serializer=json_serializer, pool_size=100, pool_recycle=3600) | ||
| 12 | session_factory = sessionmaker(engine, class_=Session) | ||
| 13 | ScopedSession = scoped_session(session_factory=session_factory) | ||
| 14 | |||
| 15 | def get_session(): | ||
| 16 | with ScopedSession() as session: | ||
| 17 | yield session | ||
| 18 | |||
| 19 | @contextmanager | ||
| 20 | def get_session_context(): | ||
| 21 | yield from get_session() | ||
diff --git a/proxy/endpoints.py b/proxy/endpoints.py index c287a88..0a23e7b 100644 --- a/proxy/endpoints.py +++ b/proxy/endpoints.py | |||
| @@ -7,7 +7,7 @@ from starlette.requests import Request | |||
| 7 | from starlette.responses import Response | 7 | from starlette.responses import Response |
| 8 | 8 | ||
| 9 | from proxy.utils import get_path_with_query_string | 9 | from proxy.utils import get_path_with_query_string |
| 10 | from ttun_server.proxy_queue import ProxyQueue | 10 | from proxy.queue import ProxyQueue |
| 11 | from ttun_server.types import HttpMessage, HttpMessageType, HttpRequestData | 11 | from ttun_server.types import HttpMessage, HttpMessageType, HttpRequestData |
| 12 | 12 | ||
| 13 | logger = logging.getLogger(__name__) | 13 | logger = logging.getLogger(__name__) |
diff --git a/ttun_server/proxy_queue.py b/proxy/queue.py index cfa0f3c..cfa0f3c 100644 --- a/ttun_server/proxy_queue.py +++ b/proxy/queue.py | |||
diff --git a/proxy/websockets.py b/proxy/websockets.py index ef80ec2..ec65e6d 100644 --- a/proxy/websockets.py +++ b/proxy/websockets.py | |||
| @@ -10,7 +10,7 @@ from starlette.endpoints import WebSocketEndpoint | |||
| 10 | from starlette.websockets import WebSocket | 10 | from starlette.websockets import WebSocket |
| 11 | 11 | ||
| 12 | from proxy.utils import get_path_with_query_string | 12 | from proxy.utils import get_path_with_query_string |
| 13 | from ttun_server.proxy_queue import ProxyQueue | 13 | from proxy.queue import ProxyQueue |
| 14 | from ttun_server.types import WebsocketMessage, WebsocketMessageType, WebsocketConnectData, WebsocketMessageData, \ | 14 | from ttun_server.types import WebsocketMessage, WebsocketMessageType, WebsocketConnectData, WebsocketMessageData, \ |
| 15 | WebsocketDisconnectData | 15 | WebsocketDisconnectData |
| 16 | 16 | ||
diff --git a/pyproject.toml b/pyproject.toml index 70f2683..557c2f2 100644 --- a/pyproject.toml +++ b/pyproject.toml | |||
| @@ -3,7 +3,10 @@ name = "ttun-server" | |||
| 3 | version = "0.1.0" | 3 | version = "0.1.0" |
| 4 | requires-python = ">=3.14" | 4 | requires-python = ">=3.14" |
| 5 | dependencies = [ | 5 | dependencies = [ |
| 6 | "alembic>=1.18.5", | ||
| 6 | "fastapi[standard]>=0.138.2", | 7 | "fastapi[standard]>=0.138.2", |
| 8 | "psycopg2>=2.9.12", | ||
| 7 | "redis[hiredis]~=7.4.0", | 9 | "redis[hiredis]~=7.4.0", |
| 10 | "sqlmodel>=0.0.39", | ||
| 8 | "uvicorn[standard]~=0.44.0", | 11 | "uvicorn[standard]~=0.44.0", |
| 9 | ] | 12 | ] |
diff --git a/ttun_server/websockets.py b/ttun_server/websockets.py index ea9ae94..c791625 100644 --- a/ttun_server/websockets.py +++ b/ttun_server/websockets.py | |||
| @@ -6,7 +6,7 @@ from uuid import uuid4 | |||
| 6 | from fastapi import WebSocket, WebSocketDisconnect | 6 | from fastapi import WebSocket, WebSocketDisconnect |
| 7 | 7 | ||
| 8 | import ttun_server | 8 | import ttun_server |
| 9 | from ttun_server.proxy_queue import ProxyQueue | 9 | from proxy.queue import ProxyQueue |
| 10 | from ttun_server.types import ( | 10 | from ttun_server.types import ( |
| 11 | Config, | 11 | Config, |
| 12 | Message, | 12 | Message, |
diff --git a/utils/__init__.py b/utils/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/utils/__init__.py | |||
diff --git a/utils/json.py b/utils/json.py new file mode 100644 index 0000000..05d66cb --- /dev/null +++ b/utils/json.py | |||
| @@ -0,0 +1,16 @@ | |||
| 1 | import json | ||
| 2 | from json import JSONEncoder | ||
| 3 | |||
| 4 | from pydantic import BaseModel | ||
| 5 | |||
| 6 | |||
| 7 | class Encoder(JSONEncoder): | ||
| 8 | def default(self, o): | ||
| 9 | if isinstance(o, BaseModel): | ||
| 10 | return o.model_dump() | ||
| 11 | |||
| 12 | return super().default(o) | ||
| 13 | |||
| 14 | |||
| 15 | def json_serializer(item: any): | ||
| 16 | return json.dumps(item, cls=Encoder) | ||
| @@ -3,6 +3,20 @@ revision = 3 | |||
| 3 | requires-python = ">=3.14" | 3 | requires-python = ">=3.14" |
| 4 | 4 | ||
| 5 | [[package]] | 5 | [[package]] |
| 6 | name = "alembic" | ||
| 7 | version = "1.18.5" | ||
| 8 | source = { registry = "https://pypi.org/simple" } | ||
| 9 | dependencies = [ | ||
| 10 | { name = "mako" }, | ||
| 11 | { name = "sqlalchemy" }, | ||
| 12 | { name = "typing-extensions" }, | ||
| 13 | ] | ||
| 14 | sdist = { url = "https://files.pythonhosted.org/packages/1a/cc/ac0bed8e562e7407fe55c3ba85a4dce86e6dbd8730887bd1e406a6c5c18a/alembic-1.18.5.tar.gz", hash = "sha256:1554982221dd17e9a749b53902407578eb305e453f71999e8c7f0a48389fff8e", size = 2060480, upload-time = "2026-06-25T15:20:54.888Z" } | ||
| 15 | wheels = [ | ||
| 16 | { url = "https://files.pythonhosted.org/packages/96/78/5fe6dc3a3a5b2f5a2a4faef8bfe336d5fa049a38884ab3172e0098160c01/alembic-1.18.5-py3-none-any.whl", hash = "sha256:06d8ba9d04558022f5395e9317de03d270f3dced49cee01f89fe7a13c26f14bc", size = 264664, upload-time = "2026-06-25T15:20:56.673Z" }, | ||
| 17 | ] | ||
| 18 | |||
| 19 | [[package]] | ||
| 6 | name = "annotated-doc" | 20 | name = "annotated-doc" |
| 7 | version = "0.0.4" | 21 | version = "0.0.4" |
| 8 | source = { registry = "https://pypi.org/simple" } | 22 | source = { registry = "https://pypi.org/simple" } |
| @@ -203,6 +217,45 @@ wheels = [ | |||
| 203 | ] | 217 | ] |
| 204 | 218 | ||
| 205 | [[package]] | 219 | [[package]] |
| 220 | name = "greenlet" | ||
| 221 | version = "3.5.3" | ||
| 222 | source = { registry = "https://pypi.org/simple" } | ||
| 223 | sdist = { url = "https://files.pythonhosted.org/packages/e2/f1/fbbfef6af0bad0548f09bc28948ea3c275b4edb19e17fc5ca9900a6a634d/greenlet-3.5.3.tar.gz", hash = "sha256:a61efc018fd3eb317eeca31aba90ee9e7f26f22884a79b6c6ec715bf71bb62f1", size = 200270, upload-time = "2026-06-26T19:28:24.832Z" } | ||
| 224 | wheels = [ | ||
| 225 | { url = "https://files.pythonhosted.org/packages/c3/93/43e116ee114b28737ba7e12952a0d4e2f55944d0f84e42bc91ba7192a3c9/greenlet-3.5.3-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:fd2e02fa07485778536a036222d616ab957b1d533f36b3ed98ce725d9c9d3117", size = 288202, upload-time = "2026-06-26T18:23:49.604Z" }, | ||
| 226 | { url = "https://files.pythonhosted.org/packages/82/2f/146d218299046a43d1f029fd544b3d110d0f175a09c715c7e8da4a4a345d/greenlet-3.5.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df0a0628d1597eb0897b62f55d1343f772405fd25f3b2a796c76874b0c2e22e8", size = 654096, upload-time = "2026-06-26T19:07:12.71Z" }, | ||
| 227 | { url = "https://files.pythonhosted.org/packages/a0/cc/04738cafb3f45fa991ea44f9de94c47dcec964f5a972300988a6751f49d9/greenlet-3.5.3-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ebd933a6adabc298bab47731a130fe6bfb888bd934eee37810f151159544540d", size = 666304, upload-time = "2026-06-26T19:10:09.503Z" }, | ||
| 228 | { url = "https://files.pythonhosted.org/packages/ce/aa/4e0dad5e605c270c784ab911c43da6adb136ccd4d81180f763ca429a723d/greenlet-3.5.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4b9d501b40e80b70e32323c799dd9b420a5577a9601469d362ae1ffb690f3a7c", size = 663635, upload-time = "2026-06-26T18:32:20.802Z" }, | ||
| 229 | { url = "https://files.pythonhosted.org/packages/d1/50/13efdbea246fe3d3b735e191fec08fb50809f53cd2383ebe123d0809e44b/greenlet-3.5.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a1fad1d11e7d6aab184107baa8e4ece11ccba3ec9599cd7efa5ff4d70d43256a", size = 1621252, upload-time = "2026-06-26T19:09:05.647Z" }, | ||
| 230 | { url = "https://files.pythonhosted.org/packages/f7/22/c0a336ae4a1410fd5f5121098e5bfbf1865f64c5ef80b4b5412886c4a332/greenlet-3.5.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:fad5aec764399f1b5cc347ad250a59660f20c8f8888ea6bae1f93b769cce1154", size = 1684824, upload-time = "2026-06-26T18:31:47.738Z" }, | ||
| 231 | { url = "https://files.pythonhosted.org/packages/7a/94/91aec0030bea75c4b3244251d0de60a1f3432d1ecb53ab6c437fb5c3ba61/greenlet-3.5.3-cp314-cp314-win_amd64.whl", hash = "sha256:7669aa24cf2a1041d6f7899575b494a3ab4cf68bfcc8609b1dc0be7272db835e", size = 240754, upload-time = "2026-06-26T18:22:15.669Z" }, | ||
| 232 | { url = "https://files.pythonhosted.org/packages/e5/06/68d0983e79e02138f64b4d303c500c27ddb48e5e77f3debb80888a921eae/greenlet-3.5.3-cp314-cp314-win_arm64.whl", hash = "sha256:5b4807c4082c9d1b6d9eed56fcd041863e37f2228106eef24c30ca096e238605", size = 239549, upload-time = "2026-06-26T18:22:42.996Z" }, | ||
| 233 | { url = "https://files.pythonhosted.org/packages/91/95/3e161213d7f1d378d15aa9e792093e9bfe01844680d04b7fd6e0107c9098/greenlet-3.5.3-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:271a8ea7c1024e8a0d7dd2be66dd66dda8a07193f41a17b9e924f7600f5b62be", size = 296389, upload-time = "2026-06-26T18:22:20.657Z" }, | ||
| 234 | { url = "https://files.pythonhosted.org/packages/00/92/715c44721abe2b4d1ae9abde4179411868a5bff312479f54e105d372f131/greenlet-3.5.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:19131729ae0ddc3c2e1ef85e650169b5e37ee32e400f215f78b94d7b0d567310", size = 653382, upload-time = "2026-06-26T19:07:14.209Z" }, | ||
| 235 | { url = "https://files.pythonhosted.org/packages/a0/83/37a10372a1090a6624cca8e74c12df1a36c2dc36429ed0255b7fb1aeee23/greenlet-3.5.3-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1540dd8e5fc2a5aec40fbb98ef8e149fa47c89a4b4a1cf2575a14d3d1869d7a8", size = 659401, upload-time = "2026-06-26T19:10:10.876Z" }, | ||
| 236 | { url = "https://files.pythonhosted.org/packages/db/e2/d1509cad4207da559cc42986ecdd8fc67ad0d1bba2bf03023c467fd5e0f3/greenlet-3.5.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e81fa194a1d20967877bdf9c7794db2bc99063e5be36aee710c08f04c5bb087f", size = 656969, upload-time = "2026-06-26T18:32:22.272Z" }, | ||
| 237 | { url = "https://files.pythonhosted.org/packages/86/7d/eaf70de20aadca3a5884aec58362861c64ce45e7b277f47ed026926a3b89/greenlet-3.5.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:55cf4d777485d43110e47133cbba6d74a8885a87ec1227ef0267f9ee80c5aa21", size = 1617822, upload-time = "2026-06-26T19:09:06.893Z" }, | ||
| 238 | { url = "https://files.pythonhosted.org/packages/8a/f9/414d38fc400ae4350d4185eaad1827676f7cf5287b9136e0ed1cbbe20a7f/greenlet-3.5.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:12a248ba75f6a9a236375f52296c498c89ff1d8badf32deb9eca7abd5853f7da", size = 1677983, upload-time = "2026-06-26T18:31:49.396Z" }, | ||
| 239 | { url = "https://files.pythonhosted.org/packages/e4/15/7edb977e08f9bff702fe42d6c902702786ff6b9694058b4e6a2a6ac90e57/greenlet-3.5.3-cp314-cp314t-win_amd64.whl", hash = "sha256:efc6bd60ea02e085862c74a3ef64b147ffc6f1a5ea7d9f26e7a939943f68c1e3", size = 243626, upload-time = "2026-06-26T18:24:41.485Z" }, | ||
| 240 | { url = "https://files.pythonhosted.org/packages/2c/8a/93928dce91e6b3598b5e779e8d1fd6576a504640c58e78627077f6a7a91a/greenlet-3.5.3-cp315-cp315-macosx_11_0_universal2.whl", hash = "sha256:ea03f2f04367845d6b58eeed276e1e56e51f0b97d8ad5a88a7d20a91dc9056cc", size = 288860, upload-time = "2026-06-26T18:22:48.07Z" }, | ||
| 241 | { url = "https://files.pythonhosted.org/packages/4f/ca/69db42d447a1378043e2c8f19c09cbbd1263371505053c496b49066d3d16/greenlet-3.5.3-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78dbef602fda6d97d957eb7937f70c9ce9e9527330347f8f6b6f9e554a9e7a47", size = 659747, upload-time = "2026-06-26T19:07:15.565Z" }, | ||
| 242 | { url = "https://files.pythonhosted.org/packages/a8/0b/af7ac2ef8dd41e3da1a40dda6305c23b9a03e13ba975ec916357b50f8575/greenlet-3.5.3-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f73857adb8fee13fa56c172bd11262f888c0c648f9fea113e777bb2c7904a81", size = 670419, upload-time = "2026-06-26T19:10:12.293Z" }, | ||
| 243 | { url = "https://files.pythonhosted.org/packages/51/1e/1d51640cacbfc455dbe9f9a9f594c49e4e244f63b9971a2f4764e46cc53d/greenlet-3.5.3-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:232fec92e823addaf02d9472cf7381e24a1d046a6ced1103c5caa4c21b9dfc1d", size = 668787, upload-time = "2026-06-26T18:32:24.298Z" }, | ||
| 244 | { url = "https://files.pythonhosted.org/packages/21/66/4030d5b0b5894500023f003bb054d9bb354dfbd1e186c3a296759172f5f5/greenlet-3.5.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:2421c3564da9429d5586d46ca31ebb26516b5498a802cf65c041a8e8a8980d34", size = 1626305, upload-time = "2026-06-26T19:09:08.281Z" }, | ||
| 245 | { url = "https://files.pythonhosted.org/packages/0e/50/5221371c7550108dfa3c378debc41d032aa9c78e89abb01d8011cfc93289/greenlet-3.5.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e0f0d160f0b2e558e6c75f7930967183255dc9735e5f5b8cae58ee09c9576d8b", size = 1688631, upload-time = "2026-06-26T18:31:51.278Z" }, | ||
| 246 | { url = "https://files.pythonhosted.org/packages/68/5d/00d469daae3c65d2bf620b10eee82eb022127d483c6bc8c69fae6f3fbf17/greenlet-3.5.3-cp315-cp315-win_amd64.whl", hash = "sha256:dd99329bbc15ca78dcc583dba05d0b1b0bae01ab6c2174989f5aaee3e41ac930", size = 241027, upload-time = "2026-06-26T18:22:38.203Z" }, | ||
| 247 | { url = "https://files.pythonhosted.org/packages/e7/e8/883785b44c5780ed71e83d3e4437e710470be17a2e181e8b601e2da0dc4a/greenlet-3.5.3-cp315-cp315-win_arm64.whl", hash = "sha256:499fef2acede88c1864a57bb586b4bf533c81e1b82df7ab93451cdb47dfec227", size = 240085, upload-time = "2026-06-26T18:23:54.217Z" }, | ||
| 248 | { url = "https://files.pythonhosted.org/packages/1c/da/4f4a8450962fad137c1c8981a3f1b8919d06c829993d4d476f9c525d5173/greenlet-3.5.3-cp315-cp315t-macosx_11_0_universal2.whl", hash = "sha256:176bc16a721fa5fc294d70b87b4dfa5fbdd251b3da5d5372735ecef9bd7d6d0c", size = 297221, upload-time = "2026-06-26T18:23:27.176Z" }, | ||
| 249 | { url = "https://files.pythonhosted.org/packages/57/66/b3bfae3e220a9b63ea539a0eea681800c69ab1aada757eae8789f183e7ce/greenlet-3.5.3-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:629b614d2b786e89c50440e246f33eea78f58a962d0bdbbcc809e6d13605903f", size = 657221, upload-time = "2026-06-26T19:07:16.973Z" }, | ||
| 250 | { url = "https://files.pythonhosted.org/packages/7b/81/b6d4d73a709684fc77e7fa034d7c2fe82cffa9fc920fadcaa659c2626213/greenlet-3.5.3-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2b2e857ae16f5f72142edf75f9f176fe7526ba19a2841df1420516f83831c9f2", size = 663226, upload-time = "2026-06-26T19:10:13.723Z" }, | ||
| 251 | { url = "https://files.pythonhosted.org/packages/f5/07/e210b02b589f16e74ff48b730690e4a34ffe984219fce4f3c1a0e7ec8545/greenlet-3.5.3-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e515757e2e36bcbf1fad09a46e1557e8b1ae1797d4b44d09da7deed88ad28608", size = 660802, upload-time = "2026-06-26T18:32:26.081Z" }, | ||
| 252 | { url = "https://files.pythonhosted.org/packages/eb/2e/5303eb3fa06bca089060f479707182a93e360683bc252acf846c3090d34e/greenlet-3.5.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:b363d46ed1ea431825fdb01471bb024fc08399bad1572a616e853c7684415adb", size = 1622157, upload-time = "2026-06-26T19:09:09.527Z" }, | ||
| 253 | { url = "https://files.pythonhosted.org/packages/54/70/50de47a488f14df260b50ae34fb5d56016e308b098eab02c878b5223c26a/greenlet-3.5.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:e44da2f5bbdaabaf7d80b73dbb430c7035771e9f244e3c8b769715c9d8fa0a16", size = 1681159, upload-time = "2026-06-26T18:31:52.986Z" }, | ||
| 254 | { url = "https://files.pythonhosted.org/packages/a7/13/1055e1dda7882073eda533e2b96c62e55bbd2db7fda6d5ece992febc7071/greenlet-3.5.3-cp315-cp315t-win_amd64.whl", hash = "sha256:8ff8bed3e3baa20a3ea261ce00526f1898ad4801d4886fd2220580ee0ad8fadf", size = 244007, upload-time = "2026-06-26T18:22:04.353Z" }, | ||
| 255 | { url = "https://files.pythonhosted.org/packages/b4/0d/ca7d15afbdc397e3401134c9e1800d51d12b829661786187a4ad08fe484f/greenlet-3.5.3-cp315-cp315t-win_arm64.whl", hash = "sha256:b7068bd09f761f3f5b4d214c2bed063186b2a86148c740b3873e3f56d79bac31", size = 242586, upload-time = "2026-06-26T18:23:37.93Z" }, | ||
| 256 | ] | ||
| 257 | |||
| 258 | [[package]] | ||
| 206 | name = "h11" | 259 | name = "h11" |
| 207 | version = "0.16.0" | 260 | version = "0.16.0" |
| 208 | source = { registry = "https://pypi.org/simple" } | 261 | source = { registry = "https://pypi.org/simple" } |
| @@ -310,6 +363,18 @@ wheels = [ | |||
| 310 | ] | 363 | ] |
| 311 | 364 | ||
| 312 | [[package]] | 365 | [[package]] |
| 366 | name = "mako" | ||
| 367 | version = "1.3.12" | ||
| 368 | source = { registry = "https://pypi.org/simple" } | ||
| 369 | dependencies = [ | ||
| 370 | { name = "markupsafe" }, | ||
| 371 | ] | ||
| 372 | sdist = { url = "https://files.pythonhosted.org/packages/00/62/791b31e69ae182791ec67f04850f2f062716bbd205483d63a215f3e062d3/mako-1.3.12.tar.gz", hash = "sha256:9f778e93289bd410bb35daadeb4fc66d95a746f0b75777b942088b7fd7af550a", size = 400219, upload-time = "2026-04-28T19:01:08.512Z" } | ||
| 373 | wheels = [ | ||
| 374 | { url = "https://files.pythonhosted.org/packages/bc/b1/a0ec7a5a9db730a08daef1fdfb8090435b82465abbf758a596f0ea88727e/mako-1.3.12-py3-none-any.whl", hash = "sha256:8f61569480282dbf557145ce441e4ba888be453c30989f879f0d652e39f53ea9", size = 78521, upload-time = "2026-04-28T19:01:10.393Z" }, | ||
| 375 | ] | ||
| 376 | |||
| 377 | [[package]] | ||
| 313 | name = "markdown-it-py" | 378 | name = "markdown-it-py" |
| 314 | version = "4.2.0" | 379 | version = "4.2.0" |
| 315 | source = { registry = "https://pypi.org/simple" } | 380 | source = { registry = "https://pypi.org/simple" } |
| @@ -361,6 +426,15 @@ wheels = [ | |||
| 361 | ] | 426 | ] |
| 362 | 427 | ||
| 363 | [[package]] | 428 | [[package]] |
| 429 | name = "psycopg2" | ||
| 430 | version = "2.9.12" | ||
| 431 | source = { registry = "https://pypi.org/simple" } | ||
| 432 | sdist = { url = "https://files.pythonhosted.org/packages/c7/bc/f66df707ed1aec949fbf24e4460e4f4277a7ba23cdadb3965bb1f634ddb9/psycopg2-2.9.12.tar.gz", hash = "sha256:1dedb1c7a1d8552c4a6044c6b1c41a52e6a8e2d144af83eccac758076b1b7c15", size = 379683, upload-time = "2026-04-20T23:36:11.013Z" } | ||
| 433 | wheels = [ | ||
| 434 | { url = "https://files.pythonhosted.org/packages/73/4a/a3566f77501c21a6c2a1fc234dbe5dff74a86e3f150ef070e4ffb835e7f9/psycopg2-2.9.12-cp314-cp314-win_amd64.whl", hash = "sha256:a73d5513bfe929c56555006c7a9cc7ae6e4276aa99dd2b1e2544eb8bb54f8b23", size = 2848588, upload-time = "2026-04-20T23:33:25.983Z" }, | ||
| 435 | ] | ||
| 436 | |||
| 437 | [[package]] | ||
| 364 | name = "pydantic" | 438 | name = "pydantic" |
| 365 | version = "2.13.4" | 439 | version = "2.13.4" |
| 366 | source = { registry = "https://pypi.org/simple" } | 440 | source = { registry = "https://pypi.org/simple" } |
| @@ -603,6 +677,47 @@ wheels = [ | |||
| 603 | ] | 677 | ] |
| 604 | 678 | ||
| 605 | [[package]] | 679 | [[package]] |
| 680 | name = "sqlalchemy" | ||
| 681 | version = "2.0.51" | ||
| 682 | source = { registry = "https://pypi.org/simple" } | ||
| 683 | dependencies = [ | ||
| 684 | { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, | ||
| 685 | { name = "typing-extensions" }, | ||
| 686 | ] | ||
| 687 | sdist = { url = "https://files.pythonhosted.org/packages/02/f1/a7a892f18d4d224e6b26f706531eafccc41e37594d37d304786969ee13cb/sqlalchemy-2.0.51.tar.gz", hash = "sha256:804dccd8a4a6242c4e30ad961e540e18a588f6527202f2d6791b01845d59fdc9", size = 9912201, upload-time = "2026-06-15T15:41:20.012Z" } | ||
| 688 | wheels = [ | ||
| 689 | { url = "https://files.pythonhosted.org/packages/b1/49/a739be2e1d02a96a658eb71ab45d921c874249252358ad24a5bffdd02525/sqlalchemy-2.0.51-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6ea306caaae6bd5afd0a46050003c88f6bf33227377a49298c498c3cb88ff491", size = 2158999, upload-time = "2026-06-15T16:08:51.759Z" }, | ||
| 690 | { url = "https://files.pythonhosted.org/packages/23/6b/2e0e38cf75c8780eca78d9b2e78164f8bcfd70125e5caa588ff5cbb9c9f4/sqlalchemy-2.0.51-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c45a496d6bc05dec41dcd4c3a2b183723f47473255c159cd80b503c8f246424d", size = 3282539, upload-time = "2026-06-15T16:19:51.065Z" }, | ||
| 691 | { url = "https://files.pythonhosted.org/packages/dd/a1/e77854cb5336fd37dc3c6ae3b71de242c98caac5725120be0b526b31cbd0/sqlalchemy-2.0.51-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4004ada0aafe8ae1991b2cd1d99c6d9146126e123bd6f883c260d974aa012e54", size = 3287545, upload-time = "2026-06-15T16:26:44.735Z" }, | ||
| 692 | { url = "https://files.pythonhosted.org/packages/f6/ab/9e17272fd4dac8df3b83c4fbe52b998a1c9d89a843c8c35ff29b74ff7364/sqlalchemy-2.0.51-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0f6bcad487aee1c638d707235682fc96f741de00663619881ab235400d03289e", size = 3230929, upload-time = "2026-06-15T16:19:52.625Z" }, | ||
| 693 | { url = "https://files.pythonhosted.org/packages/02/3c/52f408ea701781caee975606beccc48845f2aee8711ac29843d612c0306c/sqlalchemy-2.0.51-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:39a76529db6305693d8d4affa58ad5b5e2e18edd62daea628b29b97930b3513d", size = 3252888, upload-time = "2026-06-15T16:26:46.454Z" }, | ||
| 694 | { url = "https://files.pythonhosted.org/packages/24/16/3efd2ee6bc4ca4693a30a1dd17a91b606cae15d517d2a4746611d9b73ce8/sqlalchemy-2.0.51-cp314-cp314-win32.whl", hash = "sha256:08a204d8b5638717c26a24df18fcf40af45a6b22e35b70b1d62f0113c2e278e8", size = 2120551, upload-time = "2026-06-15T16:23:15.629Z" }, | ||
| 695 | { url = "https://files.pythonhosted.org/packages/7b/78/55b12e70f45bccc40d9e483925c065027b3b98ea4cbbdf6f8c2546feaf6c/sqlalchemy-2.0.51-cp314-cp314-win_amd64.whl", hash = "sha256:96747bfbadb055466e5b46d572618170046b45ce5a4879167f50d70a5319a499", size = 2146318, upload-time = "2026-06-15T16:23:17.108Z" }, | ||
| 696 | { url = "https://files.pythonhosted.org/packages/21/db/a9574ed40fed418924b1b1a3e54f47ee3963053b3d3d325a0d36b41f2c08/sqlalchemy-2.0.51-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e5ea1a213be1fcd5e49d9904c3b9939211ded90bc2a64e93f4c01963474285de", size = 2178920, upload-time = "2026-06-15T15:59:56.285Z" }, | ||
| 697 | { url = "https://files.pythonhosted.org/packages/bf/90/a1bb5c7cbba76b7bc1fbd586d0a5479a7bc9c27b4a8298f22ec9423b2bb3/sqlalchemy-2.0.51-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c6b36ed71f41942bdcd2ad2522be46bfce09d5705be5640ecf19bbc7660e4b7", size = 3566534, upload-time = "2026-06-15T15:58:35.024Z" }, | ||
| 698 | { url = "https://files.pythonhosted.org/packages/15/4b/481f1fed30e0e9e8dd24aecbb49f29eb57fe7657ece5cf06ee9b84bb97d8/sqlalchemy-2.0.51-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0c2c62877097e1a0db401fba5cb4debee33265e5b2a55c4ccb489c02c53b4f72", size = 3535844, upload-time = "2026-06-15T16:02:43.973Z" }, | ||
| 699 | { url = "https://files.pythonhosted.org/packages/02/71/0aa64aeda645510af0a43f7d9ee70932f0d1dc4263aed34c50ee891d9df3/sqlalchemy-2.0.51-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0378d055e9e8cd6ce4d8dff683bdd3d7d413533c4ee51d67a2b1e0f9eacc0f23", size = 3475355, upload-time = "2026-06-15T15:58:36.592Z" }, | ||
| 700 | { url = "https://files.pythonhosted.org/packages/05/db/6061db32316446135a3abae5f308d144ab988a34234726042da3e58b1c63/sqlalchemy-2.0.51-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6e46fc36029eff666391e0531e5387b62ce6c4f1d8e50b3fb3099eaca1b42522", size = 3486591, upload-time = "2026-06-15T16:02:45.346Z" }, | ||
| 701 | { url = "https://files.pythonhosted.org/packages/0d/c9/f14fdf71bb8957e0c7e39db69bbdf12b5c80f4ef775fdfa127bf4e0d6760/sqlalchemy-2.0.51-cp314-cp314t-win32.whl", hash = "sha256:9161cfc9efce70d1715f47d6ff40f79c6778c00d53be4fbc09d70301e4b83ba7", size = 2151313, upload-time = "2026-06-15T16:03:39.127Z" }, | ||
| 702 | { url = "https://files.pythonhosted.org/packages/6a/c6/673e618e6f4f297e126d9b56ea2f6478708f6c1af4e3223835c22e2c3697/sqlalchemy-2.0.51-cp314-cp314t-win_amd64.whl", hash = "sha256:159bb6ba32059f57ad7375a8f50d844dd2f19d14954ecf820cd33e20debd46b2", size = 2186280, upload-time = "2026-06-15T16:03:40.569Z" }, | ||
| 703 | { url = "https://files.pythonhosted.org/packages/e2/22/dbf013a12ec759e54a34a119e9e217435b3f71b2dd5c61a7ade0a25dae87/sqlalchemy-2.0.51-py3-none-any.whl", hash = "sha256:bb024d8b621d0be75f4f44ecc7c950450026e76d66dc8f791bb5331d7fed59d5", size = 1944334, upload-time = "2026-06-15T16:09:22.418Z" }, | ||
| 704 | ] | ||
| 705 | |||
| 706 | [[package]] | ||
| 707 | name = "sqlmodel" | ||
| 708 | version = "0.0.39" | ||
| 709 | source = { registry = "https://pypi.org/simple" } | ||
| 710 | dependencies = [ | ||
| 711 | { name = "pydantic" }, | ||
| 712 | { name = "sqlalchemy" }, | ||
| 713 | { name = "typing-extensions" }, | ||
| 714 | ] | ||
| 715 | sdist = { url = "https://files.pythonhosted.org/packages/68/ee/22a0559283c3cf6048678e787ed5d4959dcd00dedd8ba4567eeae684eeb1/sqlmodel-0.0.39.tar.gz", hash = "sha256:23d8e50a8d8ee936032ed79c55023a5d618dd6bc3c510bbf4909d1a7a605a570", size = 91057, upload-time = "2026-06-25T13:01:38.475Z" } | ||
| 716 | wheels = [ | ||
| 717 | { url = "https://files.pythonhosted.org/packages/cf/7d/b9813a582d4eb310be35e1fc7dfaae71207d7b62e9e53be314ebd251b53b/sqlmodel-0.0.39-py3-none-any.whl", hash = "sha256:90ebe92ce5cc11d7fff8dc7cb594790a102333c8fe7c14865254f6fc5c939795", size = 29680, upload-time = "2026-06-25T13:01:37.494Z" }, | ||
| 718 | ] | ||
| 719 | |||
| 720 | [[package]] | ||
| 606 | name = "starlette" | 721 | name = "starlette" |
| 607 | version = "1.0.0" | 722 | version = "1.0.0" |
| 608 | source = { registry = "https://pypi.org/simple" } | 723 | source = { registry = "https://pypi.org/simple" } |
| @@ -619,15 +734,21 @@ name = "ttun-server" | |||
| 619 | version = "0.1.0" | 734 | version = "0.1.0" |
| 620 | source = { virtual = "." } | 735 | source = { virtual = "." } |
| 621 | dependencies = [ | 736 | dependencies = [ |
| 737 | { name = "alembic" }, | ||
| 622 | { name = "fastapi", extra = ["standard"] }, | 738 | { name = "fastapi", extra = ["standard"] }, |
| 739 | { name = "psycopg2" }, | ||
| 623 | { name = "redis", extra = ["hiredis"] }, | 740 | { name = "redis", extra = ["hiredis"] }, |
| 741 | { name = "sqlmodel" }, | ||
| 624 | { name = "uvicorn", extra = ["standard"] }, | 742 | { name = "uvicorn", extra = ["standard"] }, |
| 625 | ] | 743 | ] |
| 626 | 744 | ||
| 627 | [package.metadata] | 745 | [package.metadata] |
| 628 | requires-dist = [ | 746 | requires-dist = [ |
| 747 | { name = "alembic", specifier = ">=1.18.5" }, | ||
| 629 | { name = "fastapi", extras = ["standard"], specifier = ">=0.138.2" }, | 748 | { name = "fastapi", extras = ["standard"], specifier = ">=0.138.2" }, |
| 749 | { name = "psycopg2", specifier = ">=2.9.12" }, | ||
| 630 | { name = "redis", extras = ["hiredis"], specifier = "~=7.4.0" }, | 750 | { name = "redis", extras = ["hiredis"], specifier = "~=7.4.0" }, |
| 751 | { name = "sqlmodel", specifier = ">=0.0.39" }, | ||
| 631 | { name = "uvicorn", extras = ["standard"], specifier = "~=0.44.0" }, | 752 | { name = "uvicorn", extras = ["standard"], specifier = "~=0.44.0" }, |
| 632 | ] | 753 | ] |
| 633 | 754 | ||
