aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore264
-rw-r--r--.python-version1
-rw-r--r--README.md0
-rw-r--r--chat/__init__.py0
-rw-r--r--chat/asgi.py28
-rw-r--r--chat/consumers.py65
-rw-r--r--chat/routing.py7
-rw-r--r--chat/settings.py135
-rw-r--r--chat/templates/chat.html136
-rw-r--r--chat/templates/room.html451
-rw-r--r--chat/urls.py26
-rw-r--r--chat/views.py14
-rw-r--r--chat/wsgi.py16
-rwxr-xr-xmanage.py22
-rw-r--r--pyproject.toml11
-rw-r--r--requirements.txt3
-rw-r--r--uv.lock393
17 files changed, 1572 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..6ee6aff
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,264 @@
1# Created by https://www.toptal.com/developers/gitignore/api/pycharm+all,python
2# Edit at https://www.toptal.com/developers/gitignore?templates=pycharm+all,python
3
4### PyCharm+all ###
5# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
6# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
7
8# User-specific stuff
9.idea/**/workspace.xml
10.idea/**/tasks.xml
11.idea/**/usage.statistics.xml
12.idea/**/dictionaries
13.idea/**/shelf
14
15# AWS User-specific
16.idea/**/aws.xml
17
18# Generated files
19.idea/**/contentModel.xml
20
21# Sensitive or high-churn files
22.idea/**/dataSources/
23.idea/**/dataSources.ids
24.idea/**/dataSources.local.xml
25.idea/**/sqlDataSources.xml
26.idea/**/dynamic.xml
27.idea/**/uiDesigner.xml
28.idea/**/dbnavigator.xml
29
30# Gradle
31.idea/**/gradle.xml
32.idea/**/libraries
33
34# Gradle and Maven with auto-import
35# When using Gradle or Maven with auto-import, you should exclude module files,
36# since they will be recreated, and may cause churn. Uncomment if using
37# auto-import.
38# .idea/artifacts
39# .idea/compiler.xml
40# .idea/jarRepositories.xml
41# .idea/modules.xml
42# .idea/*.iml
43# .idea/modules
44# *.iml
45# *.ipr
46
47# CMake
48cmake-build-*/
49
50# Mongo Explorer plugin
51.idea/**/mongoSettings.xml
52
53# File-based project format
54*.iws
55
56# IntelliJ
57out/
58
59# mpeltonen/sbt-idea plugin
60.idea_modules/
61
62# JIRA plugin
63atlassian-ide-plugin.xml
64
65# Cursive Clojure plugin
66.idea/replstate.xml
67
68# SonarLint plugin
69.idea/sonarlint/
70
71# Crashlytics plugin (for Android Studio and IntelliJ)
72com_crashlytics_export_strings.xml
73crashlytics.properties
74crashlytics-build.properties
75fabric.properties
76
77# Editor-based Rest Client
78.idea/httpRequests
79
80# Android studio 3.1+ serialized cache file
81.idea/caches/build_file_checksums.ser
82
83### PyCharm+all Patch ###
84# Ignore everything but code style settings and run configurations
85# that are supposed to be shared within teams.
86
87.idea/*
88
89!.idea/codeStyles
90!.idea/runConfigurations
91
92### Python ###
93# Byte-compiled / optimized / DLL files
94__pycache__/
95*.py[cod]
96*$py.class
97
98# C extensions
99*.so
100
101# Distribution / packaging
102.Python
103build/
104develop-eggs/
105dist/
106downloads/
107eggs/
108.eggs/
109lib/
110lib64/
111parts/
112sdist/
113var/
114wheels/
115share/python-wheels/
116*.egg-info/
117.installed.cfg
118*.egg
119MANIFEST
120
121# PyInstaller
122# Usually these files are written by a python script from a template
123# before PyInstaller builds the exe, so as to inject date/other infos into it.
124*.manifest
125*.spec
126
127# Installer logs
128pip-log.txt
129pip-delete-this-directory.txt
130
131# Unit test / coverage reports
132htmlcov/
133.tox/
134.nox/
135.coverage
136.coverage.*
137.cache
138nosetests.xml
139coverage.xml
140*.cover
141*.py,cover
142.hypothesis/
143.pytest_cache/
144cover/
145
146# Translations
147*.mo
148*.pot
149
150# Django stuff:
151*.log
152local_settings.py
153db.sqlite3
154db.sqlite3-journal
155
156# Flask stuff:
157instance/
158.webassets-cache
159
160# Scrapy stuff:
161.scrapy
162
163# Sphinx documentation
164docs/_build/
165
166# PyBuilder
167.pybuilder/
168target/
169
170# Jupyter Notebook
171.ipynb_checkpoints
172
173# IPython
174profile_default/
175ipython_config.py
176
177# pyenv
178# For a library or package, you might want to ignore these files since the code is
179# intended to run in multiple environments; otherwise, check them in:
180# .python-version
181
182# pipenv
183# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
184# However, in case of collaboration, if having platform-specific dependencies or dependencies
185# having no cross-platform support, pipenv may install dependencies that don't work, or not
186# install all needed dependencies.
187#Pipfile.lock
188
189# poetry
190# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
191# This is especially recommended for binary packages to ensure reproducibility, and is more
192# commonly ignored for libraries.
193# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
194#poetry.lock
195
196# pdm
197# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
198#pdm.lock
199# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
200# in version control.
201# https://pdm.fming.dev/#use-with-ide
202.pdm.toml
203
204# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
205__pypackages__/
206
207# Celery stuff
208celerybeat-schedule
209celerybeat.pid
210
211# SageMath parsed files
212*.sage.py
213
214# Environments
215.env
216.venv
217env/
218venv/
219ENV/
220env.bak/
221venv.bak/
222
223# Spyder project settings
224.spyderproject
225.spyproject
226
227# Rope project settings
228.ropeproject
229
230# mkdocs documentation
231/site
232
233# mypy
234.mypy_cache/
235.dmypy.json
236dmypy.json
237
238# Pyre type checker
239.pyre/
240
241# pytype static type analyzer
242.pytype/
243
244# Cython debug symbols
245cython_debug/
246
247# PyCharm
248# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
249# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
250# and can be added to the global gitignore or merged into this file. For a more nuclear
251# option (not recommended) you can uncomment the following to ignore the entire idea folder.
252#.idea/
253
254### Python Patch ###
255# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
256poetry.toml
257
258# ruff
259.ruff_cache/
260
261# LSP config files
262pyrightconfig.json
263
264# End of https://www.toptal.com/developers/gitignore/api/pycharm+all,python
diff --git a/.python-version b/.python-version
new file mode 100644
index 0000000..24ee5b1
--- /dev/null
+++ b/.python-version
@@ -0,0 +1 @@
3.13
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/README.md
diff --git a/chat/__init__.py b/chat/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/chat/__init__.py
diff --git a/chat/asgi.py b/chat/asgi.py
new file mode 100644
index 0000000..247393e
--- /dev/null
+++ b/chat/asgi.py
@@ -0,0 +1,28 @@
1"""
2ASGI config for chat project.
3
4It exposes the ASGI callable as a module-level variable named ``application``.
5
6For more information on this file, see
7https://docs.djangoproject.com/en/5.0/howto/deployment/asgi/
8"""
9
10import os
11
12from channels.auth import AuthMiddlewareStack
13from channels.routing import ProtocolTypeRouter, URLRouter
14from channels.security.websocket import AllowedHostsOriginValidator
15from django.core.asgi import get_asgi_application
16
17from chat.routing import websocket_urlpatterns
18
19os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'chat.settings')
20
21application = ProtocolTypeRouter(
22 {
23 "http": get_asgi_application(),
24 "websocket": AllowedHostsOriginValidator(
25 AuthMiddlewareStack(URLRouter(websocket_urlpatterns))
26 ),
27 }
28)
diff --git a/chat/consumers.py b/chat/consumers.py
new file mode 100644
index 0000000..bfba381
--- /dev/null
+++ b/chat/consumers.py
@@ -0,0 +1,65 @@
1import json
2
3from asgiref.sync import async_to_sync
4from channels.generic.websocket import WebsocketConsumer
5
6
7class ChatConsumer(WebsocketConsumer):
8 def connect(self):
9 self.room_name = self.scope["url_route"]["kwargs"]["room_name"]
10 self.room_group_name = f"chat_{self.room_name}"
11 self.username = "Anonymous"
12
13 async_to_sync(self.channel_layer.group_add)(
14 self.room_group_name, self.channel_name
15 )
16 self.accept()
17
18 def disconnect(self, close_code):
19 async_to_sync(self.channel_layer.group_discard)(
20 self.room_group_name, self.channel_name
21 )
22 async_to_sync(self.channel_layer.group_send)(
23 self.room_group_name,
24 {"type": "system.message", "text": f"{self.username} left the chat"},
25 )
26
27 def receive(self, text_data):
28 text_data_json = json.loads(text_data)
29 msg_type = text_data_json.get("type", "message")
30
31 if msg_type == "join":
32 self.username = text_data_json.get("username", "Anonymous")
33 async_to_sync(self.channel_layer.group_send)(
34 self.room_group_name,
35 {"type": "system.message", "text": f"{self.username} joined the chat"},
36 )
37 return
38
39 if msg_type == "rename":
40 old_name = self.username
41 self.username = text_data_json.get("username", self.username)
42 async_to_sync(self.channel_layer.group_send)(
43 self.room_group_name,
44 {"type": "system.message", "text": f"{old_name} is now known as {self.username}"},
45 )
46 return
47
48 message = text_data_json["message"]
49 username = text_data_json.get("username", self.username)
50 async_to_sync(self.channel_layer.group_send)(
51 self.room_group_name,
52 {"type": "chat.message", "message": message, "username": username},
53 )
54
55 def chat_message(self, event):
56 self.send(text_data=json.dumps({
57 "message": event["message"],
58 "username": event["username"],
59 }))
60
61 def system_message(self, event):
62 self.send(text_data=json.dumps({
63 "type": "system",
64 "text": event["text"],
65 }))
diff --git a/chat/routing.py b/chat/routing.py
new file mode 100644
index 0000000..9f08ddf
--- /dev/null
+++ b/chat/routing.py
@@ -0,0 +1,7 @@
1from django.urls import re_path
2
3from . import consumers
4
5websocket_urlpatterns = [
6 re_path(r"ws/chat/(?P<room_name>\w+)/$", consumers.ChatConsumer.as_asgi()),
7]
diff --git a/chat/settings.py b/chat/settings.py
new file mode 100644
index 0000000..653eea9
--- /dev/null
+++ b/chat/settings.py
@@ -0,0 +1,135 @@
1"""
2Django settings for chat project.
3
4Generated by 'django-admin startproject' using Django 5.0.3.
5
6For more information on this file, see
7https://docs.djangoproject.com/en/5.0/topics/settings/
8
9For the full list of settings and their values, see
10https://docs.djangoproject.com/en/5.0/ref/settings/
11"""
12
13from pathlib import Path
14
15# Build paths inside the project like this: BASE_DIR / 'subdir'.
16BASE_DIR = Path(__file__).resolve().parent.parent
17
18
19# Quick-start development settings - unsuitable for production
20# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/
21
22# SECURITY WARNING: keep the secret key used in production secret!
23SECRET_KEY = 'django-insecure-dxn^kf6b8qm@#cqg4+jncop793g7*3mgok&p$40di_cxbm+y$3'
24
25# SECURITY WARNING: don't run with debug turned on in production!
26DEBUG = True
27
28ALLOWED_HOSTS = ['*']
29
30
31# Application definition
32
33INSTALLED_APPS = [
34 'chat',
35 'daphne',
36 'django.contrib.admin',
37 'django.contrib.auth',
38 'django.contrib.contenttypes',
39 'django.contrib.sessions',
40 'django.contrib.messages',
41 'django.contrib.staticfiles',
42]
43
44MIDDLEWARE = [
45 'django.middleware.security.SecurityMiddleware',
46 'django.contrib.sessions.middleware.SessionMiddleware',
47 'django.middleware.common.CommonMiddleware',
48 'django.middleware.csrf.CsrfViewMiddleware',
49 'django.contrib.auth.middleware.AuthenticationMiddleware',
50 'django.contrib.messages.middleware.MessageMiddleware',
51 'django.middleware.clickjacking.XFrameOptionsMiddleware',
52]
53
54ROOT_URLCONF = 'chat.urls'
55
56TEMPLATES = [
57 {
58 'BACKEND': 'django.template.backends.django.DjangoTemplates',
59 'DIRS': [],
60 'APP_DIRS': True,
61 'OPTIONS': {
62 'context_processors': [
63 'django.template.context_processors.debug',
64 'django.template.context_processors.request',
65 'django.contrib.auth.context_processors.auth',
66 'django.contrib.messages.context_processors.messages',
67 ],
68 },
69 },
70]
71
72WSGI_APPLICATION = 'chat.wsgi.application'
73ASGI_APPLICATION = 'chat.asgi.application'
74
75
76# Database
77# https://docs.djangoproject.com/en/5.0/ref/settings/#databases
78
79DATABASES = {
80 'default': {
81 'ENGINE': 'django.db.backends.sqlite3',
82 'NAME': BASE_DIR / 'db.sqlite3',
83 }
84}
85
86
87# Password validation
88# https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators
89
90AUTH_PASSWORD_VALIDATORS = [
91 {
92 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
93 },
94 {
95 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
96 },
97 {
98 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
99 },
100 {
101 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
102 },
103]
104
105
106# Internationalization
107# https://docs.djangoproject.com/en/5.0/topics/i18n/
108
109LANGUAGE_CODE = 'en-us'
110
111TIME_ZONE = 'UTC'
112
113USE_I18N = True
114
115USE_TZ = True
116
117
118# Static files (CSS, JavaScript, Images)
119# https://docs.djangoproject.com/en/5.0/howto/static-files/
120
121STATIC_URL = 'static/'
122
123# Default primary key field type
124# https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field
125
126DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
127
128CHANNEL_LAYERS = {
129 "default": {
130 "BACKEND": "channels_redis.core.RedisChannelLayer",
131 "CONFIG": {
132 "hosts": [("127.0.0.1", 6379)],
133 },
134 },
135}
diff --git a/chat/templates/chat.html b/chat/templates/chat.html
new file mode 100644
index 0000000..8542368
--- /dev/null
+++ b/chat/templates/chat.html
@@ -0,0 +1,136 @@
1<!DOCTYPE html>
2<html lang="en">
3<head>
4 <meta charset="utf-8"/>
5 <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
6 <meta name="theme-color" content="#c9b8ff">
7 <title>dreamchat</title>
8 <style>
9 :root {
10 --grad-start: #c9b8ff;
11 --grad-end: #f9b8e8;
12 --accent: #8b6cef;
13 --surface: #ffffff;
14 --text-main: #2d2040;
15 --text-muted: #8c7faa;
16 --radius-card: 24px;
17 --shadow: 0 8px 32px rgba(139, 108, 239, 0.18);
18 --font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
19 }
20
21 * { box-sizing: border-box; margin: 0; padding: 0; }
22
23 body {
24 min-height: 100dvh;
25 background: linear-gradient(135deg, var(--grad-start), var(--grad-end));
26 display: flex;
27 align-items: center;
28 justify-content: center;
29 font-family: var(--font);
30 padding: 24px;
31 }
32
33 .card {
34 background: var(--surface);
35 border-radius: var(--radius-card);
36 box-shadow: var(--shadow);
37 padding: 40px 32px;
38 width: 100%;
39 max-width: 400px;
40 text-align: center;
41 }
42
43 .logo {
44 font-size: 3rem;
45 margin-bottom: 12px;
46 line-height: 1;
47 }
48
49 .title {
50 font-size: 2rem;
51 font-weight: 700;
52 background: linear-gradient(135deg, var(--accent), #e06aba);
53 -webkit-background-clip: text;
54 -webkit-text-fill-color: transparent;
55 background-clip: text;
56 margin-bottom: 8px;
57 }
58
59 .subtitle {
60 color: var(--text-muted);
61 margin-bottom: 28px;
62 font-size: 0.95rem;
63 }
64
65 .input-row {
66 display: flex;
67 gap: 10px;
68 }
69
70 .input-row input {
71 flex: 1;
72 min-width: 0;
73 border: 2px solid #e8e0ff;
74 border-radius: 50px;
75 padding: 12px 18px;
76 font-size: 1rem;
77 font-family: var(--font);
78 color: var(--text-main);
79 outline: none;
80 transition: border-color 0.2s;
81 }
82
83 .input-row input:focus {
84 border-color: var(--accent);
85 }
86
87 .input-row input::placeholder {
88 color: var(--text-muted);
89 }
90
91 .input-row button {
92 background: linear-gradient(135deg, var(--accent), #b06add);
93 color: #fff;
94 border: none;
95 border-radius: 50px;
96 padding: 12px 22px;
97 font-size: 1rem;
98 font-weight: 600;
99 font-family: var(--font);
100 cursor: pointer;
101 white-space: nowrap;
102 transition: opacity 0.15s, transform 0.1s;
103 }
104
105 .input-row button:hover { opacity: 0.9; }
106 .input-row button:active { transform: scale(0.96); }
107 </style>
108</head>
109<body>
110 <div class="card">
111 <div class="logo">💬</div>
112 <h1 class="title">dreamchat</h1>
113 <p class="subtitle">Where do you want to go?</p>
114 <div class="input-row">
115 <input id="room-name-input" type="text" placeholder="Room name"
116 aria-label="Room name"
117 autocomplete="off" autocapitalize="off" spellcheck="false">
118 <button id="room-name-submit">Enter</button>
119 </div>
120 </div>
121
122 <script>
123 document.querySelector('#room-name-input').focus();
124 document.querySelector('#room-name-input').onkeyup = function(event) {
125 if (event.key === 'Enter') {
126 document.querySelector('#room-name-submit').click();
127 }
128 };
129
130 document.querySelector('#room-name-submit').onclick = function() {
131 const roomName = document.querySelector('#room-name-input').value;
132 window.location.pathname = '/' + roomName + '/';
133 };
134 </script>
135</body>
136</html>
diff --git a/chat/templates/room.html b/chat/templates/room.html
new file mode 100644
index 0000000..de76f67
--- /dev/null
+++ b/chat/templates/room.html
@@ -0,0 +1,451 @@
1<!DOCTYPE html>
2<html lang="en">
3<head>
4 <meta charset="utf-8"/>
5 <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
6 <meta name="theme-color" content="#c9b8ff">
7 <title>{{ room_name }} — dreamchat</title>
8 <style>
9 :root {
10 --grad-start: #c9b8ff;
11 --grad-end: #f9b8e8;
12 --accent: #8b6cef;
13 --bubble-in-bg: #f0ebff;
14 --bubble-in-text: #2d2040;
15 --surface: #ffffff;
16 --text-main: #2d2040;
17 --text-muted: #8c7faa;
18 --radius-bubble: 18px;
19 --font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
20 }
21
22 * { box-sizing: border-box; margin: 0; padding: 0; }
23
24 body {
25 height: 100dvh;
26 display: flex;
27 flex-direction: column;
28 background: linear-gradient(160deg, #ede7ff 0%, #fce4f6 100%);
29 font-family: var(--font);
30 overflow: hidden;
31 }
32
33 /* ---- Header ---- */
34 .room-header {
35 display: flex;
36 align-items: center;
37 gap: 12px;
38 padding: 10px 16px;
39 padding-top: calc(10px + env(safe-area-inset-top));
40 background: rgba(255, 255, 255, 0.75);
41 backdrop-filter: blur(12px);
42 -webkit-backdrop-filter: blur(12px);
43 border-bottom: 1px solid rgba(139, 108, 239, 0.12);
44 flex-shrink: 0;
45 }
46
47 .back-btn {
48 display: flex;
49 align-items: center;
50 justify-content: center;
51 width: 32px;
52 height: 32px;
53 border-radius: 50%;
54 color: var(--accent);
55 text-decoration: none;
56 font-size: 1.2rem;
57 font-weight: 600;
58 transition: background 0.15s;
59 flex-shrink: 0;
60 }
61
62 .back-btn:hover { background: #f0ebff; }
63
64 .header-center {
65 flex: 1;
66 min-width: 0;
67 display: flex;
68 flex-direction: column;
69 gap: 2px;
70 }
71
72 .room-title {
73 font-size: 1.05rem;
74 font-weight: 700;
75 background: linear-gradient(135deg, var(--accent), #e06aba);
76 -webkit-background-clip: text;
77 -webkit-text-fill-color: transparent;
78 background-clip: text;
79 white-space: nowrap;
80 overflow: hidden;
81 text-overflow: ellipsis;
82 }
83
84 .username-row { display: flex; align-items: center; }
85
86 .username-chip {
87 display: flex;
88 align-items: center;
89 gap: 4px;
90 background: none;
91 border: none;
92 padding: 0;
93 font-size: 0.78rem;
94 font-family: var(--font);
95 color: var(--text-muted);
96 cursor: pointer;
97 transition: color 0.15s;
98 }
99
100 .username-chip:hover { color: var(--accent); }
101 .edit-icon { font-size: 0.7rem; }
102
103 .username-edit {
104 font-size: 0.78rem;
105 font-family: var(--font);
106 color: var(--text-main);
107 border: none;
108 border-bottom: 1.5px solid var(--accent);
109 outline: none;
110 background: transparent;
111 width: 130px;
112 padding: 1px 2px;
113 }
114
115 .disconnect-btn {
116 display: flex;
117 align-items: center;
118 justify-content: center;
119 width: 32px;
120 height: 32px;
121 background: none;
122 border: 1.5px solid #d0c5f0;
123 color: var(--text-muted);
124 border-radius: 50%;
125 font-size: 0.85rem;
126 cursor: pointer;
127 flex-shrink: 0;
128 transition: background 0.15s, border-color 0.15s;
129 }
130
131 .disconnect-btn:hover { background: #f0ebff; border-color: var(--accent); }
132
133 /* ---- Message area ---- */
134 .chat-log {
135 flex: 1;
136 overflow-y: auto;
137 -webkit-overflow-scrolling: touch;
138 padding: 16px 12px;
139 display: flex;
140 flex-direction: column;
141 gap: 6px;
142 }
143
144 /* ---- Bubbles ---- */
145 .bubble-row {
146 display: flex;
147 width: 100%;
148 }
149
150 .bubble-row.sent { justify-content: flex-end; }
151 .bubble-row.recv { justify-content: flex-start; }
152
153 .bubble-wrapper {
154 display: flex;
155 flex-direction: column;
156 max-width: 72%;
157 gap: 3px;
158 }
159
160 .bubble-row.sent .bubble-wrapper { align-items: flex-end; }
161 .bubble-row.recv .bubble-wrapper { align-items: flex-start; }
162
163 .bubble {
164 padding: 10px 15px;
165 border-radius: var(--radius-bubble);
166 font-size: 0.97rem;
167 line-height: 1.45;
168 word-wrap: break-word;
169 animation: pop-in 0.15s ease-out;
170 }
171
172 .sent .bubble {
173 background: linear-gradient(135deg, var(--accent), #b06add);
174 color: #ffffff;
175 border-bottom-right-radius: 4px;
176 }
177
178 .recv .bubble {
179 background: var(--bubble-in-bg);
180 color: var(--bubble-in-text);
181 border-bottom-left-radius: 4px;
182 box-shadow: 0 2px 8px rgba(139, 108, 239, 0.1);
183 }
184
185 .bubble-sender {
186 font-size: 0.75rem;
187 font-weight: 600;
188 color: var(--accent);
189 padding: 0 4px;
190 }
191
192 .bubble-time {
193 font-size: 0.7rem;
194 color: var(--text-muted);
195 padding: 0 4px;
196 }
197
198 @keyframes pop-in {
199 from { opacity: 0; transform: scale(0.9) translateY(4px); }
200 to { opacity: 1; transform: scale(1) translateY(0); }
201 }
202
203 /* ---- Input bar ---- */
204 .input-bar {
205 display: flex;
206 align-items: center;
207 gap: 10px;
208 padding: 12px 16px;
209 padding-bottom: calc(12px + env(safe-area-inset-bottom));
210 background: rgba(255, 255, 255, 0.85);
211 backdrop-filter: blur(12px);
212 -webkit-backdrop-filter: blur(12px);
213 border-top: 1px solid rgba(139, 108, 239, 0.12);
214 flex-shrink: 0;
215 }
216
217 .input-bar input {
218 flex: 1;
219 min-width: 0;
220 border: 2px solid #e8e0ff;
221 border-radius: 50px;
222 padding: 11px 18px;
223 font-size: 1rem;
224 font-family: var(--font);
225 color: var(--text-main);
226 background: #fff;
227 outline: none;
228 transition: border-color 0.2s;
229 }
230
231 .input-bar input:focus { border-color: var(--accent); }
232 .input-bar input::placeholder { color: var(--text-muted); }
233
234 .send-btn {
235 display: flex;
236 align-items: center;
237 justify-content: center;
238 width: 44px;
239 height: 44px;
240 background: linear-gradient(135deg, var(--accent), #b06add);
241 color: #fff;
242 border: none;
243 border-radius: 50%;
244 font-size: 1rem;
245 cursor: pointer;
246 flex-shrink: 0;
247 transition: opacity 0.15s, transform 0.1s;
248 }
249
250 .send-btn:hover { opacity: 0.9; }
251 .send-btn:active { transform: scale(0.93); }
252
253 /* ---- System messages ---- */
254 .system-msg {
255 display: flex;
256 align-items: center;
257 gap: 10px;
258 font-size: 0.72rem;
259 color: var(--text-muted);
260 padding: 4px 0;
261 animation: pop-in 0.2s ease-out;
262 }
263
264 .system-msg::before,
265 .system-msg::after {
266 content: '';
267 flex: 1;
268 height: 1px;
269 background: rgba(139, 108, 239, 0.2);
270 }
271 </style>
272</head>
273<body>
274 <header class="room-header">
275 <a href="/" class="back-btn" aria-label="Back to rooms">←</a>
276 <div class="header-center">
277 <span class="room-title">{{ room_name }}</span>
278 <div class="username-row">
279 <button id="username-display" class="username-chip" aria-label="Edit username">
280 <span id="username-text"></span>
281 <span class="edit-icon">✏️</span>
282 </button>
283 <input id="username-input" class="username-edit"
284 aria-label="Your username" hidden
285 maxlength="30" autocomplete="off" spellcheck="false">
286 </div>
287 </div>
288 <button id="chat-message-disconnect" class="disconnect-btn" hidden aria-label="Disconnect">✕</button>
289 </header>
290
291 <main id="chat-log" class="chat-log" aria-live="polite" aria-label="Chat messages"></main>
292
293 <footer class="input-bar">
294 <input id="chat-message-input" type="text" placeholder="Message…"
295 aria-label="Message"
296 autocomplete="off" autocapitalize="sentences">
297 <button id="chat-message-submit" class="send-btn" aria-label="Send">➤</button>
298 </footer>
299
300 {{ room_name|json_script:"room-name" }}
301 <script>
302 const ADJECTIVES = ['Dreamy','Cosmic','Fuzzy','Glowing','Misty','Silky','Starry','Velvet','Cozy','Dewy','Wispy','Breezy'];
303 const ANIMALS = ['Fox','Moth','Owl','Bear','Deer','Lark','Lynx','Mink','Swan','Wolf','Wren','Panda'];
304
305 function generateUsername() {
306 const adj = ADJECTIVES[Math.floor(Math.random() * ADJECTIVES.length)];
307 const animal = ANIMALS[Math.floor(Math.random() * ANIMALS.length)];
308 return adj + animal;
309 }
310
311 let username = localStorage.getItem('dreamchat_username') || generateUsername();
312 localStorage.setItem('dreamchat_username', username);
313 document.getElementById('username-text').textContent = username;
314
315 document.getElementById('username-display').onclick = function() {
316 const input = document.getElementById('username-input');
317 input.value = username;
318 input.removeAttribute('hidden');
319 this.setAttribute('hidden', '');
320 input.focus();
321 input.select();
322 };
323
324 function commitUsername() {
325 const input = document.getElementById('username-input');
326 const newName = input.value.trim();
327 if (newName && newName !== username) {
328 username = newName;
329 localStorage.setItem('dreamchat_username', username);
330 document.getElementById('username-text').textContent = username;
331 if (chatSocket.readyState === WebSocket.OPEN) {
332 chatSocket.send(JSON.stringify({ type: 'rename', username }));
333 }
334 }
335 input.setAttribute('hidden', '');
336 document.getElementById('username-display').removeAttribute('hidden');
337 }
338
339 document.getElementById('username-input').onblur = commitUsername;
340 document.getElementById('username-input').onkeyup = function(event) {
341 if (event.key === 'Enter') { this.blur(); }
342 if (event.key === 'Escape') {
343 this.setAttribute('hidden', '');
344 document.getElementById('username-display').removeAttribute('hidden');
345 }
346 };
347
348 function formatTime(date) {
349 return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
350 }
351
352 const roomName = JSON.parse(document.getElementById('room-name').textContent);
353 const chatLog = document.getElementById('chat-log');
354 const pendingEchoes = new Set();
355
356 function appendSystemMessage(text) {
357 const el = document.createElement('div');
358 el.className = 'system-msg';
359 el.textContent = text;
360 chatLog.appendChild(el);
361 chatLog.scrollTop = chatLog.scrollHeight;
362 }
363
364 function appendBubble(text, direction, sender, time) {
365 const row = document.createElement('div');
366 row.className = 'bubble-row ' + direction;
367
368 const wrapper = document.createElement('div');
369 wrapper.className = 'bubble-wrapper';
370
371 if (direction === 'recv') {
372 const nameEl = document.createElement('div');
373 nameEl.className = 'bubble-sender';
374 nameEl.textContent = sender;
375 wrapper.appendChild(nameEl);
376 }
377
378 const bubble = document.createElement('div');
379 bubble.className = 'bubble';
380 bubble.textContent = text;
381 wrapper.appendChild(bubble);
382
383 const timeEl = document.createElement('div');
384 timeEl.className = 'bubble-time';
385 timeEl.textContent = time;
386 wrapper.appendChild(timeEl);
387
388 row.appendChild(wrapper);
389 chatLog.appendChild(row);
390 chatLog.scrollTop = chatLog.scrollHeight;
391 }
392
393 const chatSocket = new WebSocket(
394 'ws://'
395 + window.location.host
396 + '/ws/chat/'
397 + roomName
398 + '/'
399 + '?token=123'
400 );
401
402 chatSocket.onopen = function() {
403 document.querySelector('#chat-message-disconnect').removeAttribute('hidden');
404 chatSocket.send(JSON.stringify({ type: 'join', username }));
405 };
406
407 chatSocket.onmessage = function(e) {
408 const data = JSON.parse(e.data);
409
410 if (data.type === 'system') {
411 appendSystemMessage(data.text);
412 return;
413 }
414
415 const key = data.username + '|' + data.message;
416 if (pendingEchoes.has(key)) {
417 pendingEchoes.delete(key);
418 return;
419 }
420 appendBubble(data.message, 'recv', data.username, formatTime(new Date()));
421 };
422
423 chatSocket.onclose = function() {
424 console.error('Chat socket closed unexpectedly');
425 document.querySelector('#chat-message-disconnect').setAttribute('hidden', '');
426 };
427
428 document.querySelector('#chat-message-input').focus();
429 document.querySelector('#chat-message-input').onkeyup = function(event) {
430 if (event.key === 'Enter') {
431 document.querySelector('#chat-message-submit').click();
432 }
433 };
434
435 document.querySelector('#chat-message-submit').onclick = function() {
436 const messageInputDom = document.querySelector('#chat-message-input');
437 const message = messageInputDom.value.trim();
438 if (!message) return;
439 const time = formatTime(new Date());
440 pendingEchoes.add(username + '|' + message);
441 appendBubble(message, 'sent', username, time);
442 chatSocket.send(JSON.stringify({ message, username }));
443 messageInputDom.value = '';
444 };
445
446 document.querySelector('#chat-message-disconnect').onclick = function() {
447 chatSocket.close();
448 };
449 </script>
450</body>
451</html>
diff --git a/chat/urls.py b/chat/urls.py
new file mode 100644
index 0000000..b96b107
--- /dev/null
+++ b/chat/urls.py
@@ -0,0 +1,26 @@
1"""
2URL configuration for chat project.
3
4The `urlpatterns` list routes URLs to views. For more information please see:
5 https://docs.djangoproject.com/en/5.0/topics/http/urls/
6Examples:
7Function views
8 1. Add an import: from my_app import views
9 2. Add a URL to urlpatterns: path('', views.home, name='home')
10Class-based views
11 1. Add an import: from other_app.views import Home
12 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
13Including another URLconf
14 1. Import the include() function: from django.urls import include, path
15 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
16"""
17from django.contrib import admin
18from django.urls import path
19
20from chat.views import Chat, Room
21
22urlpatterns = [
23 path('admin/', admin.site.urls),
24 path('<str:room>/', Room.as_view()),
25 path('', Chat.as_view()),
26]
diff --git a/chat/views.py b/chat/views.py
new file mode 100644
index 0000000..512837e
--- /dev/null
+++ b/chat/views.py
@@ -0,0 +1,14 @@
1from django.views.generic import TemplateView
2
3
4class Chat(TemplateView):
5 template_name = 'chat.html'
6
7class Room(TemplateView):
8 template_name = 'room.html'
9
10 def get_context_data(self, **kwargs):
11 return {
12 **super().get_context_data(**kwargs),
13 'room_name': kwargs.get('room'),
14 }
diff --git a/chat/wsgi.py b/chat/wsgi.py
new file mode 100644
index 0000000..cdfbb23
--- /dev/null
+++ b/chat/wsgi.py
@@ -0,0 +1,16 @@
1"""
2WSGI config for chat project.
3
4It exposes the WSGI callable as a module-level variable named ``application``.
5
6For more information on this file, see
7https://docs.djangoproject.com/en/5.0/howto/deployment/wsgi/
8"""
9
10import os
11
12from django.core.wsgi import get_wsgi_application
13
14os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'chat.settings')
15
16application = get_wsgi_application()
diff --git a/manage.py b/manage.py
new file mode 100755
index 0000000..60eb248
--- /dev/null
+++ b/manage.py
@@ -0,0 +1,22 @@
1#!/usr/bin/env python
2"""Django's command-line utility for administrative tasks."""
3import os
4import sys
5
6
7def main():
8 """Run administrative tasks."""
9 os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'chat.settings')
10 try:
11 from django.core.management import execute_from_command_line
12 except ImportError as exc:
13 raise ImportError(
14 "Couldn't import Django. Are you sure it's installed and "
15 "available on your PYTHONPATH environment variable? Did you "
16 "forget to activate a virtual environment?"
17 ) from exc
18 execute_from_command_line(sys.argv)
19
20
21if __name__ == '__main__':
22 main()
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..d13bf24
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,11 @@
1[project]
2name = "websocket-chat"
3version = "0.1.0"
4description = "Add your description here"
5readme = "README.md"
6requires-python = ">=3.13"
7dependencies = [
8 "channels-redis>=4.3.0",
9 "channels[daphne]>=4.3.1",
10 "django>=5.2.5",
11]
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..46e2756
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,3 @@
1Django
2channels[daphne]
3channels_redis
diff --git a/uv.lock b/uv.lock
new file mode 100644
index 0000000..17e8a17
--- /dev/null
+++ b/uv.lock
@@ -0,0 +1,393 @@
1version = 1
2revision = 1
3requires-python = ">=3.13"
4
5[[package]]
6name = "asgiref"
7version = "3.9.1"
8source = { registry = "https://pypi.org/simple" }
9sdist = { url = "https://files.pythonhosted.org/packages/90/61/0aa957eec22ff70b830b22ff91f825e70e1ef732c06666a805730f28b36b/asgiref-3.9.1.tar.gz", hash = "sha256:a5ab6582236218e5ef1648f242fd9f10626cfd4de8dc377db215d5d5098e3142", size = 36870 }
10wheels = [
11 { url = "https://files.pythonhosted.org/packages/7c/3c/0464dcada90d5da0e71018c04a140ad6349558afb30b3051b4264cc5b965/asgiref-3.9.1-py3-none-any.whl", hash = "sha256:f3bba7092a48005b5f5bacd747d36ee4a5a61f4a269a6df590b43144355ebd2c", size = 23790 },
12]
13
14[[package]]
15name = "attrs"
16version = "25.3.0"
17source = { registry = "https://pypi.org/simple" }
18sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032 }
19wheels = [
20 { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815 },
21]
22
23[[package]]
24name = "autobahn"
25version = "24.4.2"
26source = { registry = "https://pypi.org/simple" }
27dependencies = [
28 { name = "cryptography" },
29 { name = "hyperlink" },
30 { name = "setuptools" },
31 { name = "txaio" },
32]
33sdist = { url = "https://files.pythonhosted.org/packages/38/f2/8dffb3b709383ba5b47628b0cc4e43e8d12d59eecbddb62cfccac2e7cf6a/autobahn-24.4.2.tar.gz", hash = "sha256:a2d71ef1b0cf780b6d11f8b205fd2c7749765e65795f2ea7d823796642ee92c9", size = 482700 }
34wheels = [
35 { url = "https://files.pythonhosted.org/packages/13/ee/a6475f39ef6c6f41c33da6b193e0ffd2c6048f52e1698be6253c59301b72/autobahn-24.4.2-py2.py3-none-any.whl", hash = "sha256:c56a2abe7ac78abbfb778c02892d673a4de58fd004d088cd7ab297db25918e81", size = 666965 },
36]
37
38[[package]]
39name = "automat"
40version = "25.4.16"
41source = { registry = "https://pypi.org/simple" }
42sdist = { url = "https://files.pythonhosted.org/packages/e3/0f/d40bbe294bbf004d436a8bcbcfaadca8b5140d39ad0ad3d73d1a8ba15f14/automat-25.4.16.tar.gz", hash = "sha256:0017591a5477066e90d26b0e696ddc143baafd87b588cfac8100bc6be9634de0", size = 129977 }
43wheels = [
44 { url = "https://files.pythonhosted.org/packages/02/ff/1175b0b7371e46244032d43a56862d0af455823b5280a50c63d99cc50f18/automat-25.4.16-py3-none-any.whl", hash = "sha256:04e9bce696a8d5671ee698005af6e5a9fa15354140a87f4870744604dcdd3ba1", size = 42842 },
45]
46
47[[package]]
48name = "cffi"
49version = "1.17.1"
50source = { registry = "https://pypi.org/simple" }
51dependencies = [
52 { name = "pycparser" },
53]
54sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 }
55wheels = [
56 { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989 },
57 { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802 },
58 { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792 },
59 { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893 },
60 { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810 },
61 { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200 },
62 { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447 },
63 { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358 },
64 { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469 },
65 { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475 },
66 { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009 },
67]
68
69[[package]]
70name = "channels"
71version = "4.3.1"
72source = { registry = "https://pypi.org/simple" }
73dependencies = [
74 { name = "asgiref" },
75 { name = "django" },
76]
77sdist = { url = "https://files.pythonhosted.org/packages/12/a0/46450fcf9e56af18a6b0440ba49db6635419bb7bc84142c35f4143b1a66c/channels-4.3.1.tar.gz", hash = "sha256:97413ffd674542db08e16a9ef09cd86ec0113e5f8125fbd33cf0854adcf27cdb", size = 26896 }
78wheels = [
79 { url = "https://files.pythonhosted.org/packages/89/1c/eae1c2a8c195760376e7f65d0bdcc3e966695d29cfbe5c54841ce5c71408/channels-4.3.1-py3-none-any.whl", hash = "sha256:b091d4b26f91d807de3e84aead7ba785314f27eaf5bac31dd51b1c956b883859", size = 31286 },
80]
81
82[package.optional-dependencies]
83daphne = [
84 { name = "daphne" },
85]
86
87[[package]]
88name = "channels-redis"
89version = "4.3.0"
90source = { registry = "https://pypi.org/simple" }
91dependencies = [
92 { name = "asgiref" },
93 { name = "channels" },
94 { name = "msgpack" },
95 { name = "redis" },
96]
97sdist = { url = "https://files.pythonhosted.org/packages/ab/69/fd3407ad407a80e72ca53850eb7a4c306273e67d5bbb71a86d0e6d088439/channels_redis-4.3.0.tar.gz", hash = "sha256:740ee7b54f0e28cf2264a940a24453d3f00526a96931f911fcb69228ef245dd2", size = 31440 }
98wheels = [
99 { url = "https://files.pythonhosted.org/packages/df/fe/b7224a401ad227b263e5ba84753ffb5a88df048f3b15efd2797903543ce4/channels_redis-4.3.0-py3-none-any.whl", hash = "sha256:48f3e902ae2d5fef7080215524f3b4a1d3cea4e304150678f867a1a822c0d9f5", size = 20641 },
100]
101
102[[package]]
103name = "constantly"
104version = "23.10.4"
105source = { registry = "https://pypi.org/simple" }
106sdist = { url = "https://files.pythonhosted.org/packages/4d/6f/cb2a94494ff74aa9528a36c5b1422756330a75a8367bf20bd63171fc324d/constantly-23.10.4.tar.gz", hash = "sha256:aa92b70a33e2ac0bb33cd745eb61776594dc48764b06c35e0efd050b7f1c7cbd", size = 13300 }
107wheels = [
108 { url = "https://files.pythonhosted.org/packages/b8/40/c199d095151addf69efdb4b9ca3a4f20f70e20508d6222bffb9b76f58573/constantly-23.10.4-py3-none-any.whl", hash = "sha256:3fd9b4d1c3dc1ec9757f3c52aef7e53ad9323dbe39f51dfd4c43853b68dfa3f9", size = 13547 },
109]
110
111[[package]]
112name = "cryptography"
113version = "45.0.7"
114source = { registry = "https://pypi.org/simple" }
115dependencies = [
116 { name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
117]
118sdist = { url = "https://files.pythonhosted.org/packages/a7/35/c495bffc2056f2dadb32434f1feedd79abde2a7f8363e1974afa9c33c7e2/cryptography-45.0.7.tar.gz", hash = "sha256:4b1654dfc64ea479c242508eb8c724044f1e964a47d1d1cacc5132292d851971", size = 744980 }
119wheels = [
120 { url = "https://files.pythonhosted.org/packages/0c/91/925c0ac74362172ae4516000fe877912e33b5983df735ff290c653de4913/cryptography-45.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:3be4f21c6245930688bd9e162829480de027f8bf962ede33d4f8ba7d67a00cee", size = 7041105 },
121 { url = "https://files.pythonhosted.org/packages/fc/63/43641c5acce3a6105cf8bd5baeceeb1846bb63067d26dae3e5db59f1513a/cryptography-45.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:67285f8a611b0ebc0857ced2081e30302909f571a46bfa7a3cc0ad303fe015c6", size = 4205799 },
122 { url = "https://files.pythonhosted.org/packages/bc/29/c238dd9107f10bfde09a4d1c52fd38828b1aa353ced11f358b5dd2507d24/cryptography-45.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:577470e39e60a6cd7780793202e63536026d9b8641de011ed9d8174da9ca5339", size = 4430504 },
123 { url = "https://files.pythonhosted.org/packages/62/62/24203e7cbcc9bd7c94739428cd30680b18ae6b18377ae66075c8e4771b1b/cryptography-45.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:4bd3e5c4b9682bc112d634f2c6ccc6736ed3635fc3319ac2bb11d768cc5a00d8", size = 4209542 },
124 { url = "https://files.pythonhosted.org/packages/cd/e3/e7de4771a08620eef2389b86cd87a2c50326827dea5528feb70595439ce4/cryptography-45.0.7-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:465ccac9d70115cd4de7186e60cfe989de73f7bb23e8a7aa45af18f7412e75bf", size = 3889244 },
125 { url = "https://files.pythonhosted.org/packages/96/b8/bca71059e79a0bb2f8e4ec61d9c205fbe97876318566cde3b5092529faa9/cryptography-45.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:16ede8a4f7929b4b7ff3642eba2bf79aa1d71f24ab6ee443935c0d269b6bc513", size = 4461975 },
126 { url = "https://files.pythonhosted.org/packages/58/67/3f5b26937fe1218c40e95ef4ff8d23c8dc05aa950d54200cc7ea5fb58d28/cryptography-45.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8978132287a9d3ad6b54fcd1e08548033cc09dc6aacacb6c004c73c3eb5d3ac3", size = 4209082 },
127 { url = "https://files.pythonhosted.org/packages/0e/e4/b3e68a4ac363406a56cf7b741eeb80d05284d8c60ee1a55cdc7587e2a553/cryptography-45.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:b6a0e535baec27b528cb07a119f321ac024592388c5681a5ced167ae98e9fff3", size = 4460397 },
128 { url = "https://files.pythonhosted.org/packages/22/49/2c93f3cd4e3efc8cb22b02678c1fad691cff9dd71bb889e030d100acbfe0/cryptography-45.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:a24ee598d10befaec178efdff6054bc4d7e883f615bfbcd08126a0f4931c83a6", size = 4337244 },
129 { url = "https://files.pythonhosted.org/packages/04/19/030f400de0bccccc09aa262706d90f2ec23d56bc4eb4f4e8268d0ddf3fb8/cryptography-45.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:fa26fa54c0a9384c27fcdc905a2fb7d60ac6e47d14bc2692145f2b3b1e2cfdbd", size = 4568862 },
130 { url = "https://files.pythonhosted.org/packages/29/56/3034a3a353efa65116fa20eb3c990a8c9f0d3db4085429040a7eef9ada5f/cryptography-45.0.7-cp311-abi3-win32.whl", hash = "sha256:bef32a5e327bd8e5af915d3416ffefdbe65ed975b646b3805be81b23580b57b8", size = 2936578 },
131 { url = "https://files.pythonhosted.org/packages/b3/61/0ab90f421c6194705a99d0fa9f6ee2045d916e4455fdbb095a9c2c9a520f/cryptography-45.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:3808e6b2e5f0b46d981c24d79648e5c25c35e59902ea4391a0dcb3e667bf7443", size = 3405400 },
132 { url = "https://files.pythonhosted.org/packages/63/e8/c436233ddf19c5f15b25ace33979a9dd2e7aa1a59209a0ee8554179f1cc0/cryptography-45.0.7-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bfb4c801f65dd61cedfc61a83732327fafbac55a47282e6f26f073ca7a41c3b2", size = 7021824 },
133 { url = "https://files.pythonhosted.org/packages/bc/4c/8f57f2500d0ccd2675c5d0cc462095adf3faa8c52294ba085c036befb901/cryptography-45.0.7-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:81823935e2f8d476707e85a78a405953a03ef7b7b4f55f93f7c2d9680e5e0691", size = 4202233 },
134 { url = "https://files.pythonhosted.org/packages/eb/ac/59b7790b4ccaed739fc44775ce4645c9b8ce54cbec53edf16c74fd80cb2b/cryptography-45.0.7-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3994c809c17fc570c2af12c9b840d7cea85a9fd3e5c0e0491f4fa3c029216d59", size = 4423075 },
135 { url = "https://files.pythonhosted.org/packages/b8/56/d4f07ea21434bf891faa088a6ac15d6d98093a66e75e30ad08e88aa2b9ba/cryptography-45.0.7-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:dad43797959a74103cb59c5dac71409f9c27d34c8a05921341fb64ea8ccb1dd4", size = 4204517 },
136 { url = "https://files.pythonhosted.org/packages/e8/ac/924a723299848b4c741c1059752c7cfe09473b6fd77d2920398fc26bfb53/cryptography-45.0.7-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ce7a453385e4c4693985b4a4a3533e041558851eae061a58a5405363b098fcd3", size = 3882893 },
137 { url = "https://files.pythonhosted.org/packages/83/dc/4dab2ff0a871cc2d81d3ae6d780991c0192b259c35e4d83fe1de18b20c70/cryptography-45.0.7-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b04f85ac3a90c227b6e5890acb0edbaf3140938dbecf07bff618bf3638578cf1", size = 4450132 },
138 { url = "https://files.pythonhosted.org/packages/12/dd/b2882b65db8fc944585d7fb00d67cf84a9cef4e77d9ba8f69082e911d0de/cryptography-45.0.7-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:48c41a44ef8b8c2e80ca4527ee81daa4c527df3ecbc9423c41a420a9559d0e27", size = 4204086 },
139 { url = "https://files.pythonhosted.org/packages/5d/fa/1d5745d878048699b8eb87c984d4ccc5da4f5008dfd3ad7a94040caca23a/cryptography-45.0.7-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:f3df7b3d0f91b88b2106031fd995802a2e9ae13e02c36c1fc075b43f420f3a17", size = 4449383 },
140 { url = "https://files.pythonhosted.org/packages/36/8b/fc61f87931bc030598e1876c45b936867bb72777eac693e905ab89832670/cryptography-45.0.7-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:dd342f085542f6eb894ca00ef70236ea46070c8a13824c6bde0dfdcd36065b9b", size = 4332186 },
141 { url = "https://files.pythonhosted.org/packages/0b/11/09700ddad7443ccb11d674efdbe9a832b4455dc1f16566d9bd3834922ce5/cryptography-45.0.7-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1993a1bb7e4eccfb922b6cd414f072e08ff5816702a0bdb8941c247a6b1b287c", size = 4561639 },
142 { url = "https://files.pythonhosted.org/packages/71/ed/8f4c1337e9d3b94d8e50ae0b08ad0304a5709d483bfcadfcc77a23dbcb52/cryptography-45.0.7-cp37-abi3-win32.whl", hash = "sha256:18fcf70f243fe07252dcb1b268a687f2358025ce32f9f88028ca5c364b123ef5", size = 2926552 },
143 { url = "https://files.pythonhosted.org/packages/bc/ff/026513ecad58dacd45d1d24ebe52b852165a26e287177de1d545325c0c25/cryptography-45.0.7-cp37-abi3-win_amd64.whl", hash = "sha256:7285a89df4900ed3bfaad5679b1e668cb4b38a8de1ccbfc84b05f34512da0a90", size = 3392742 },
144]
145
146[[package]]
147name = "daphne"
148version = "4.2.1"
149source = { registry = "https://pypi.org/simple" }
150dependencies = [
151 { name = "asgiref" },
152 { name = "autobahn" },
153 { name = "twisted", extra = ["tls"] },
154]
155sdist = { url = "https://files.pythonhosted.org/packages/cd/9d/322b605fdc03b963cf2d33943321c8f4405e8d82e698bf49d1eed1ca40c4/daphne-4.2.1.tar.gz", hash = "sha256:5f898e700a1fda7addf1541d7c328606415e96a7bd768405f0463c312fcb31b3", size = 45600 }
156wheels = [
157 { url = "https://files.pythonhosted.org/packages/01/34/6171ab34715ed210bcd6c2b38839cc792993cff4fe2493f50bc92b0086a0/daphne-4.2.1-py3-none-any.whl", hash = "sha256:881e96b387b95b35ad85acd855f229d7f5b79073d6649089c8a33f661885e055", size = 29015 },
158]
159
160[[package]]
161name = "django"
162version = "5.2.5"
163source = { registry = "https://pypi.org/simple" }
164dependencies = [
165 { name = "asgiref" },
166 { name = "sqlparse" },
167 { name = "tzdata", marker = "sys_platform == 'win32'" },
168]
169sdist = { url = "https://files.pythonhosted.org/packages/62/9b/779f853c3d2d58b9e08346061ff3e331cdec3fe3f53aae509e256412a593/django-5.2.5.tar.gz", hash = "sha256:0745b25681b129a77aae3d4f6549b62d3913d74407831abaa0d9021a03954bae", size = 10859748 }
170wheels = [
171 { url = "https://files.pythonhosted.org/packages/9d/6e/98a1d23648e0085bb5825326af17612ecd8fc76be0ce96ea4dc35e17b926/django-5.2.5-py3-none-any.whl", hash = "sha256:2b2ada0ee8a5ff743a40e2b9820d1f8e24c11bac9ae6469cd548f0057ea6ddcd", size = 8302999 },
172]
173
174[[package]]
175name = "hyperlink"
176version = "21.0.0"
177source = { registry = "https://pypi.org/simple" }
178dependencies = [
179 { name = "idna" },
180]
181sdist = { url = "https://files.pythonhosted.org/packages/3a/51/1947bd81d75af87e3bb9e34593a4cf118115a8feb451ce7a69044ef1412e/hyperlink-21.0.0.tar.gz", hash = "sha256:427af957daa58bc909471c6c40f74c5450fa123dd093fc53efd2e91d2705a56b", size = 140743 }
182wheels = [
183 { url = "https://files.pythonhosted.org/packages/6e/aa/8caf6a0a3e62863cbb9dab27135660acba46903b703e224f14f447e57934/hyperlink-21.0.0-py2.py3-none-any.whl", hash = "sha256:e6b14c37ecb73e89c77d78cdb4c2cc8f3fb59a885c5b3f819ff4ed80f25af1b4", size = 74638 },
184]
185
186[[package]]
187name = "idna"
188version = "3.10"
189source = { registry = "https://pypi.org/simple" }
190sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 }
191wheels = [
192 { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
193]
194
195[[package]]
196name = "incremental"
197version = "24.7.2"
198source = { registry = "https://pypi.org/simple" }
199dependencies = [
200 { name = "setuptools" },
201]
202sdist = { url = "https://files.pythonhosted.org/packages/27/87/156b374ff6578062965afe30cc57627d35234369b3336cf244b240c8d8e6/incremental-24.7.2.tar.gz", hash = "sha256:fb4f1d47ee60efe87d4f6f0ebb5f70b9760db2b2574c59c8e8912be4ebd464c9", size = 28157 }
203wheels = [
204 { url = "https://files.pythonhosted.org/packages/0d/38/221e5b2ae676a3938c2c1919131410c342b6efc2baffeda395dd66eeca8f/incremental-24.7.2-py3-none-any.whl", hash = "sha256:8cb2c3431530bec48ad70513931a760f446ad6c25e8333ca5d95e24b0ed7b8fe", size = 20516 },
205]
206
207[[package]]
208name = "msgpack"
209version = "1.1.1"
210source = { registry = "https://pypi.org/simple" }
211sdist = { url = "https://files.pythonhosted.org/packages/45/b1/ea4f68038a18c77c9467400d166d74c4ffa536f34761f7983a104357e614/msgpack-1.1.1.tar.gz", hash = "sha256:77b79ce34a2bdab2594f490c8e80dd62a02d650b91a75159a63ec413b8d104cd", size = 173555 }
212wheels = [
213 { url = "https://files.pythonhosted.org/packages/a1/38/561f01cf3577430b59b340b51329803d3a5bf6a45864a55f4ef308ac11e3/msgpack-1.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3765afa6bd4832fc11c3749be4ba4b69a0e8d7b728f78e68120a157a4c5d41f0", size = 81677 },
214 { url = "https://files.pythonhosted.org/packages/09/48/54a89579ea36b6ae0ee001cba8c61f776451fad3c9306cd80f5b5c55be87/msgpack-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8ddb2bcfd1a8b9e431c8d6f4f7db0773084e107730ecf3472f1dfe9ad583f3d9", size = 78603 },
215 { url = "https://files.pythonhosted.org/packages/a0/60/daba2699b308e95ae792cdc2ef092a38eb5ee422f9d2fbd4101526d8a210/msgpack-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:196a736f0526a03653d829d7d4c5500a97eea3648aebfd4b6743875f28aa2af8", size = 420504 },
216 { url = "https://files.pythonhosted.org/packages/20/22/2ebae7ae43cd8f2debc35c631172ddf14e2a87ffcc04cf43ff9df9fff0d3/msgpack-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d592d06e3cc2f537ceeeb23d38799c6ad83255289bb84c2e5792e5a8dea268a", size = 423749 },
217 { url = "https://files.pythonhosted.org/packages/40/1b/54c08dd5452427e1179a40b4b607e37e2664bca1c790c60c442c8e972e47/msgpack-1.1.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4df2311b0ce24f06ba253fda361f938dfecd7b961576f9be3f3fbd60e87130ac", size = 404458 },
218 { url = "https://files.pythonhosted.org/packages/2e/60/6bb17e9ffb080616a51f09928fdd5cac1353c9becc6c4a8abd4e57269a16/msgpack-1.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e4141c5a32b5e37905b5940aacbc59739f036930367d7acce7a64e4dec1f5e0b", size = 405976 },
219 { url = "https://files.pythonhosted.org/packages/ee/97/88983e266572e8707c1f4b99c8fd04f9eb97b43f2db40e3172d87d8642db/msgpack-1.1.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b1ce7f41670c5a69e1389420436f41385b1aa2504c3b0c30620764b15dded2e7", size = 408607 },
220 { url = "https://files.pythonhosted.org/packages/bc/66/36c78af2efaffcc15a5a61ae0df53a1d025f2680122e2a9eb8442fed3ae4/msgpack-1.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4147151acabb9caed4e474c3344181e91ff7a388b888f1e19ea04f7e73dc7ad5", size = 424172 },
221 { url = "https://files.pythonhosted.org/packages/8c/87/a75eb622b555708fe0427fab96056d39d4c9892b0c784b3a721088c7ee37/msgpack-1.1.1-cp313-cp313-win32.whl", hash = "sha256:500e85823a27d6d9bba1d057c871b4210c1dd6fb01fbb764e37e4e8847376323", size = 65347 },
222 { url = "https://files.pythonhosted.org/packages/ca/91/7dc28d5e2a11a5ad804cf2b7f7a5fcb1eb5a4966d66a5d2b41aee6376543/msgpack-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:6d489fba546295983abd142812bda76b57e33d0b9f5d5b71c09a583285506f69", size = 72341 },
223]
224
225[[package]]
226name = "pyasn1"
227version = "0.6.1"
228source = { registry = "https://pypi.org/simple" }
229sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322 }
230wheels = [
231 { url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135 },
232]
233
234[[package]]
235name = "pyasn1-modules"
236version = "0.4.2"
237source = { registry = "https://pypi.org/simple" }
238dependencies = [
239 { name = "pyasn1" },
240]
241sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892 }
242wheels = [
243 { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259 },
244]
245
246[[package]]
247name = "pycparser"
248version = "2.22"
249source = { registry = "https://pypi.org/simple" }
250sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 }
251wheels = [
252 { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 },
253]
254
255[[package]]
256name = "pyopenssl"
257version = "25.1.0"
258source = { registry = "https://pypi.org/simple" }
259dependencies = [
260 { name = "cryptography" },
261]
262sdist = { url = "https://files.pythonhosted.org/packages/04/8c/cd89ad05804f8e3c17dea8f178c3f40eeab5694c30e0c9f5bcd49f576fc3/pyopenssl-25.1.0.tar.gz", hash = "sha256:8d031884482e0c67ee92bf9a4d8cceb08d92aba7136432ffb0703c5280fc205b", size = 179937 }
263wheels = [
264 { url = "https://files.pythonhosted.org/packages/80/28/2659c02301b9500751f8d42f9a6632e1508aa5120de5e43042b8b30f8d5d/pyopenssl-25.1.0-py3-none-any.whl", hash = "sha256:2b11f239acc47ac2e5aca04fd7fa829800aeee22a2eb30d744572a157bd8a1ab", size = 56771 },
265]
266
267[[package]]
268name = "redis"
269version = "6.4.0"
270source = { registry = "https://pypi.org/simple" }
271sdist = { url = "https://files.pythonhosted.org/packages/0d/d6/e8b92798a5bd67d659d51a18170e91c16ac3b59738d91894651ee255ed49/redis-6.4.0.tar.gz", hash = "sha256:b01bc7282b8444e28ec36b261df5375183bb47a07eb9c603f284e89cbc5ef010", size = 4647399 }
272wheels = [
273 { url = "https://files.pythonhosted.org/packages/e8/02/89e2ed7e85db6c93dfa9e8f691c5087df4e3551ab39081a4d7c6d1f90e05/redis-6.4.0-py3-none-any.whl", hash = "sha256:f0544fa9604264e9464cdf4814e7d4830f74b165d52f2a330a760a88dd248b7f", size = 279847 },
274]
275
276[[package]]
277name = "service-identity"
278version = "24.2.0"
279source = { registry = "https://pypi.org/simple" }
280dependencies = [
281 { name = "attrs" },
282 { name = "cryptography" },
283 { name = "pyasn1" },
284 { name = "pyasn1-modules" },
285]
286sdist = { url = "https://files.pythonhosted.org/packages/07/a5/dfc752b979067947261dbbf2543470c58efe735c3c1301dd870ef27830ee/service_identity-24.2.0.tar.gz", hash = "sha256:b8683ba13f0d39c6cd5d625d2c5f65421d6d707b013b375c355751557cbe8e09", size = 39245 }
287wheels = [
288 { url = "https://files.pythonhosted.org/packages/08/2c/ca6dd598b384bc1ce581e24aaae0f2bed4ccac57749d5c3befbb5e742081/service_identity-24.2.0-py3-none-any.whl", hash = "sha256:6b047fbd8a84fd0bb0d55ebce4031e400562b9196e1e0d3e0fe2b8a59f6d4a85", size = 11364 },
289]
290
291[[package]]
292name = "setuptools"
293version = "80.9.0"
294source = { registry = "https://pypi.org/simple" }
295sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958 }
296wheels = [
297 { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486 },
298]
299
300[[package]]
301name = "sqlparse"
302version = "0.5.3"
303source = { registry = "https://pypi.org/simple" }
304sdist = { url = "https://files.pythonhosted.org/packages/e5/40/edede8dd6977b0d3da179a342c198ed100dd2aba4be081861ee5911e4da4/sqlparse-0.5.3.tar.gz", hash = "sha256:09f67787f56a0b16ecdbde1bfc7f5d9c3371ca683cfeaa8e6ff60b4807ec9272", size = 84999 }
305wheels = [
306 { url = "https://files.pythonhosted.org/packages/a9/5c/bfd6bd0bf979426d405cc6e71eceb8701b148b16c21d2dc3c261efc61c7b/sqlparse-0.5.3-py3-none-any.whl", hash = "sha256:cf2196ed3418f3ba5de6af7e82c694a9fbdbfecccdfc72e281548517081f16ca", size = 44415 },
307]
308
309[[package]]
310name = "twisted"
311version = "25.5.0"
312source = { registry = "https://pypi.org/simple" }
313dependencies = [
314 { name = "attrs" },
315 { name = "automat" },
316 { name = "constantly" },
317 { name = "hyperlink" },
318 { name = "incremental" },
319 { name = "typing-extensions" },
320 { name = "zope-interface" },
321]
322sdist = { url = "https://files.pythonhosted.org/packages/13/0f/82716ed849bf7ea4984c21385597c949944f0f9b428b5710f79d0afc084d/twisted-25.5.0.tar.gz", hash = "sha256:1deb272358cb6be1e3e8fc6f9c8b36f78eb0fa7c2233d2dbe11ec6fee04ea316", size = 3545725 }
323wheels = [
324 { url = "https://files.pythonhosted.org/packages/eb/66/ab7efd8941f0bc7b2bd555b0f0471bff77df4c88e0cc31120c82737fec77/twisted-25.5.0-py3-none-any.whl", hash = "sha256:8559f654d01a54a8c3efe66d533d43f383531ebf8d81d9f9ab4769d91ca15df7", size = 3204767 },
325]
326
327[package.optional-dependencies]
328tls = [
329 { name = "idna" },
330 { name = "pyopenssl" },
331 { name = "service-identity" },
332]
333
334[[package]]
335name = "txaio"
336version = "25.6.1"
337source = { registry = "https://pypi.org/simple" }
338sdist = { url = "https://files.pythonhosted.org/packages/10/21/f1d3305ae1d3ca05aa71d509f02f4db11c1357001f7e31f9713e610efc5b/txaio-25.6.1.tar.gz", hash = "sha256:d8c03dca823515c9bca920df33504923ae54f2dabf476cc5a9ed5cc1691ed687", size = 58709 }
339wheels = [
340 { url = "https://files.pythonhosted.org/packages/52/56/aff5af8caa210321d0c206eb19897a4e0b29b4e24c4e24226325950efe0b/txaio-25.6.1-py2.py3-none-any.whl", hash = "sha256:f461b917a14d46077fb1668d0bea4998695d93c9c569cd05fd7f193abdd22414", size = 31250 },
341]
342
343[[package]]
344name = "typing-extensions"
345version = "4.15.0"
346source = { registry = "https://pypi.org/simple" }
347sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391 }
348wheels = [
349 { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614 },
350]
351
352[[package]]
353name = "tzdata"
354version = "2025.2"
355source = { registry = "https://pypi.org/simple" }
356sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380 }
357wheels = [
358 { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839 },
359]
360
361[[package]]
362name = "websocket-chat"
363version = "0.1.0"
364source = { virtual = "." }
365dependencies = [
366 { name = "channels", extra = ["daphne"] },
367 { name = "channels-redis" },
368 { name = "django" },
369]
370
371[package.metadata]
372requires-dist = [
373 { name = "channels", extras = ["daphne"], specifier = ">=4.3.1" },
374 { name = "channels-redis", specifier = ">=4.3.0" },
375 { name = "django", specifier = ">=5.2.5" },
376]
377
378[[package]]
379name = "zope-interface"
380version = "7.2"
381source = { registry = "https://pypi.org/simple" }
382dependencies = [
383 { name = "setuptools" },
384]
385sdist = { url = "https://files.pythonhosted.org/packages/30/93/9210e7606be57a2dfc6277ac97dcc864fd8d39f142ca194fdc186d596fda/zope.interface-7.2.tar.gz", hash = "sha256:8b49f1a3d1ee4cdaf5b32d2e738362c7f5e40ac8b46dd7d1a65e82a4872728fe", size = 252960 }
386wheels = [
387 { url = "https://files.pythonhosted.org/packages/c6/3b/e309d731712c1a1866d61b5356a069dd44e5b01e394b6cb49848fa2efbff/zope.interface-7.2-cp313-cp313-macosx_10_9_x86_64.whl", hash = "sha256:3e0350b51e88658d5ad126c6a57502b19d5f559f6cb0a628e3dc90442b53dd98", size = 208961 },
388 { url = "https://files.pythonhosted.org/packages/49/65/78e7cebca6be07c8fc4032bfbb123e500d60efdf7b86727bb8a071992108/zope.interface-7.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:15398c000c094b8855d7d74f4fdc9e73aa02d4d0d5c775acdef98cdb1119768d", size = 209356 },
389 { url = "https://files.pythonhosted.org/packages/11/b1/627384b745310d082d29e3695db5f5a9188186676912c14b61a78bbc6afe/zope.interface-7.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:802176a9f99bd8cc276dcd3b8512808716492f6f557c11196d42e26c01a69a4c", size = 264196 },
390 { url = "https://files.pythonhosted.org/packages/b8/f6/54548df6dc73e30ac6c8a7ff1da73ac9007ba38f866397091d5a82237bd3/zope.interface-7.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb23f58a446a7f09db85eda09521a498e109f137b85fb278edb2e34841055398", size = 259237 },
391 { url = "https://files.pythonhosted.org/packages/b6/66/ac05b741c2129fdf668b85631d2268421c5cd1a9ff99be1674371139d665/zope.interface-7.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a71a5b541078d0ebe373a81a3b7e71432c61d12e660f1d67896ca62d9628045b", size = 264696 },
392 { url = "https://files.pythonhosted.org/packages/0a/2f/1bccc6f4cc882662162a1158cda1a7f616add2ffe322b28c99cb031b4ffc/zope.interface-7.2-cp313-cp313-win_amd64.whl", hash = "sha256:4893395d5dd2ba655c38ceb13014fd65667740f09fa5bb01caa1e6284e48c0cd", size = 212472 },
393]