aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--alembic.ini149
-rw-r--r--conf/__init__.py8
-rw-r--r--conf/local.py3
-rw-r--r--db/__init__.py0
-rw-r--r--db/migrations/README1
-rw-r--r--db/migrations/env.py82
-rw-r--r--db/migrations/script.py.mako29
-rw-r--r--db/migrations/versions/202607012255-40e20fbf63c9_.py35
-rw-r--r--db/migrations/versions/202607012255-6ebaa6ab0f48_.py39
-rw-r--r--db/models.py13
-rw-r--r--db/session.py21
-rw-r--r--proxy/endpoints.py2
-rw-r--r--proxy/queue.py (renamed from ttun_server/proxy_queue.py)0
-rw-r--r--proxy/websockets.py2
-rw-r--r--pyproject.toml3
-rw-r--r--ttun_server/websockets.py2
-rw-r--r--utils/__init__.py0
-rw-r--r--utils/json.py16
-rw-r--r--uv.lock121
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
8script_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
14file_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.
21prepend_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.
75path_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.
89sqlalchemy.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]
118keys = root,sqlalchemy,alembic
119
120[handlers]
121keys = console
122
123[formatters]
124keys = generic
125
126[logger_root]
127level = WARNING
128handlers = console
129qualname =
130
131[logger_sqlalchemy]
132level = WARNING
133handlers =
134qualname = sqlalchemy.engine
135
136[logger_alembic]
137level = INFO
138handlers =
139qualname = alembic
140
141[handler_console]
142class = StreamHandler
143args = (sys.stderr,)
144level = NOTSET
145formatter = generic
146
147[formatter_generic]
148format = %(levelname)-5.5s [%(name)s] %(message)s
149datefmt = %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 @@
1import os
2
3try:
4 from .local import *
5except ImportError:
6 pass
7
8DATABASE_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 @@
1import os
2
3os.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 @@
1from logging.config import fileConfig
2
3from sqlalchemy import engine_from_config
4from sqlalchemy import pool
5
6from alembic import context
7
8from conf import DATABASE_URL
9from db.models import *
10
11# this is the Alembic Config object, which provides
12# access to the values within the .ini file in use.
13config = context.config
14
15# Interpret the config file for Python logging.
16# This line sets up loggers basically.
17if 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
24target_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.
30config.set_main_option('sqlalchemy.url', DATABASE_URL)
31
32
33def 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
57def 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
79if context.is_offline_mode():
80 run_migrations_offline()
81else:
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
3Revision ID: ${up_revision}
4Revises: ${down_revision | comma,n}
5Create Date: ${create_date}
6
7"""
8from typing import Sequence, Union
9
10from alembic import op
11import sqlalchemy as sa
12import sqlmodel
13${imports if imports else ""}
14
15# revision identifiers, used by Alembic.
16revision: str = ${repr(up_revision)}
17down_revision: Union[str, Sequence[str], None] = ${repr(down_revision)}
18branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
19depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
20
21
22def upgrade() -> None:
23 """Upgrade schema."""
24 ${upgrades if upgrades else "pass"}
25
26
27def 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
3Revision ID: 40e20fbf63c9
4Revises: 6ebaa6ab0f48
5Create Date: 2026-07-01 22:55:55.017591
6
7"""
8from typing import Sequence, Union
9
10from alembic import op
11import sqlalchemy as sa
12import sqlmodel
13
14
15# revision identifiers, used by Alembic.
16revision: str = '40e20fbf63c9'
17down_revision: Union[str, Sequence[str], None] = '6ebaa6ab0f48'
18branch_labels: Union[str, Sequence[str], None] = None
19depends_on: Union[str, Sequence[str], None] = None
20
21
22def 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
30def 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
3Revision ID: 6ebaa6ab0f48
4Revises:
5Create Date: 2026-07-01 22:55:14.146711
6
7"""
8from typing import Sequence, Union
9
10from alembic import op
11import sqlalchemy as sa
12import sqlmodel
13
14
15# revision identifiers, used by Alembic.
16revision: str = '6ebaa6ab0f48'
17down_revision: Union[str, Sequence[str], None] = None
18branch_labels: Union[str, Sequence[str], None] = None
19depends_on: Union[str, Sequence[str], None] = None
20
21
22def 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
34def 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 @@
1from uuid import uuid4, UUID
2
3from sqlmodel import SQLModel, Field
4
5
6class BaseModel(SQLModel):
7 id: int | None = Field(primary_key=True, default=None)
8
9class ExternalModelMixin:
10 external_id: UUID = Field(index=True, unique=True, default_factory=uuid4)
11
12class 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 @@
1from contextlib import contextmanager
2
3from sqlalchemy import create_engine
4from sqlalchemy.orm import sessionmaker, scoped_session
5from sqlmodel import Session
6
7from conf import DATABASE_URL
8from utils.json import json_serializer
9
10
11engine = create_engine(DATABASE_URL, echo=True, future=True, json_serializer=json_serializer, pool_size=100, pool_recycle=3600)
12session_factory = sessionmaker(engine, class_=Session)
13ScopedSession = scoped_session(session_factory=session_factory)
14
15def get_session():
16 with ScopedSession() as session:
17 yield session
18
19@contextmanager
20def 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
7from starlette.responses import Response 7from starlette.responses import Response
8 8
9from proxy.utils import get_path_with_query_string 9from proxy.utils import get_path_with_query_string
10from ttun_server.proxy_queue import ProxyQueue 10from proxy.queue import ProxyQueue
11from ttun_server.types import HttpMessage, HttpMessageType, HttpRequestData 11from ttun_server.types import HttpMessage, HttpMessageType, HttpRequestData
12 12
13logger = logging.getLogger(__name__) 13logger = 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
10from starlette.websockets import WebSocket 10from starlette.websockets import WebSocket
11 11
12from proxy.utils import get_path_with_query_string 12from proxy.utils import get_path_with_query_string
13from ttun_server.proxy_queue import ProxyQueue 13from proxy.queue import ProxyQueue
14from ttun_server.types import WebsocketMessage, WebsocketMessageType, WebsocketConnectData, WebsocketMessageData, \ 14from 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"
3version = "0.1.0" 3version = "0.1.0"
4requires-python = ">=3.14" 4requires-python = ">=3.14"
5dependencies = [ 5dependencies = [
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
6from fastapi import WebSocket, WebSocketDisconnect 6from fastapi import WebSocket, WebSocketDisconnect
7 7
8import ttun_server 8import ttun_server
9from ttun_server.proxy_queue import ProxyQueue 9from proxy.queue import ProxyQueue
10from ttun_server.types import ( 10from 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 @@
1import json
2from json import JSONEncoder
3
4from pydantic import BaseModel
5
6
7class 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
15def json_serializer(item: any):
16 return json.dumps(item, cls=Encoder)
diff --git a/uv.lock b/uv.lock
index 5688a0b..00dc2b1 100644
--- a/uv.lock
+++ b/uv.lock
@@ -3,6 +3,20 @@ revision = 3
3requires-python = ">=3.14" 3requires-python = ">=3.14"
4 4
5[[package]] 5[[package]]
6name = "alembic"
7version = "1.18.5"
8source = { registry = "https://pypi.org/simple" }
9dependencies = [
10 { name = "mako" },
11 { name = "sqlalchemy" },
12 { name = "typing-extensions" },
13]
14sdist = { 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" }
15wheels = [
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]]
6name = "annotated-doc" 20name = "annotated-doc"
7version = "0.0.4" 21version = "0.0.4"
8source = { registry = "https://pypi.org/simple" } 22source = { registry = "https://pypi.org/simple" }
@@ -203,6 +217,45 @@ wheels = [
203] 217]
204 218
205[[package]] 219[[package]]
220name = "greenlet"
221version = "3.5.3"
222source = { registry = "https://pypi.org/simple" }
223sdist = { 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" }
224wheels = [
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]]
206name = "h11" 259name = "h11"
207version = "0.16.0" 260version = "0.16.0"
208source = { registry = "https://pypi.org/simple" } 261source = { registry = "https://pypi.org/simple" }
@@ -310,6 +363,18 @@ wheels = [
310] 363]
311 364
312[[package]] 365[[package]]
366name = "mako"
367version = "1.3.12"
368source = { registry = "https://pypi.org/simple" }
369dependencies = [
370 { name = "markupsafe" },
371]
372sdist = { 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" }
373wheels = [
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]]
313name = "markdown-it-py" 378name = "markdown-it-py"
314version = "4.2.0" 379version = "4.2.0"
315source = { registry = "https://pypi.org/simple" } 380source = { registry = "https://pypi.org/simple" }
@@ -361,6 +426,15 @@ wheels = [
361] 426]
362 427
363[[package]] 428[[package]]
429name = "psycopg2"
430version = "2.9.12"
431source = { registry = "https://pypi.org/simple" }
432sdist = { 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" }
433wheels = [
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]]
364name = "pydantic" 438name = "pydantic"
365version = "2.13.4" 439version = "2.13.4"
366source = { registry = "https://pypi.org/simple" } 440source = { registry = "https://pypi.org/simple" }
@@ -603,6 +677,47 @@ wheels = [
603] 677]
604 678
605[[package]] 679[[package]]
680name = "sqlalchemy"
681version = "2.0.51"
682source = { registry = "https://pypi.org/simple" }
683dependencies = [
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]
687sdist = { 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" }
688wheels = [
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]]
707name = "sqlmodel"
708version = "0.0.39"
709source = { registry = "https://pypi.org/simple" }
710dependencies = [
711 { name = "pydantic" },
712 { name = "sqlalchemy" },
713 { name = "typing-extensions" },
714]
715sdist = { 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" }
716wheels = [
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]]
606name = "starlette" 721name = "starlette"
607version = "1.0.0" 722version = "1.0.0"
608source = { registry = "https://pypi.org/simple" } 723source = { registry = "https://pypi.org/simple" }
@@ -619,15 +734,21 @@ name = "ttun-server"
619version = "0.1.0" 734version = "0.1.0"
620source = { virtual = "." } 735source = { virtual = "." }
621dependencies = [ 736dependencies = [
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]
628requires-dist = [ 746requires-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