14 Commits
v0.2 ... v1.0

Author SHA1 Message Date
eneller
e286ca4f02 doc: readme 2025-04-05 03:14:40 +02:00
eneller
60ad75ecb0 feat: nginx config 2025-04-04 23:18:15 +00:00
eneller
54eda4f9ea usable celery dev setup
commit 5438243b4874879196294a4072f245ebf22afd5d
Author: eneller <erikneller@gmx.de>
Date:   Sat Apr 5 00:12:55 2025 +0200

    minor celery

commit 3a03c498a7e6acff1afe350ce1ae196375d19029
Author: eneller <erikneller@gmx.de>
Date:   Fri Apr 4 23:40:23 2025 +0200

    begin celery
2025-04-05 00:15:11 +02:00
eneller
20de18fa13 build: upgrade uv.lock 2025-04-02 15:21:57 +02:00
eneller
ea3942651e feat: add waiting page 2025-03-24 12:04:04 +01:00
eneller
04103b82a2 chore: modular template 2025-03-24 11:48:05 +01:00
eneller
41eabd6026 chore: prepare for template hierarchy 2025-03-24 11:35:41 +01:00
eneller
24c986cbe9 feat: clickable title 2025-03-23 23:24:45 +01:00
eneller
9fe5d3dcde fix: sidescrolling 2025-03-23 23:14:15 +01:00
eneller
a2390e4359 minor fixes 2025-03-22 18:26:22 +01:00
eneller
66809ff88c remove django admin app 2025-03-21 20:37:26 +01:00
eneller
af36e86059 begin gunicorn 2025-03-21 20:17:38 +01:00
eneller
823639588a feat: favicon, webmanifest 2025-03-21 19:43:34 +01:00
eneller
82fea63583 fix: table headers 2025-03-20 22:20:09 +01:00
25 changed files with 262 additions and 135 deletions

View File

@@ -4,3 +4,5 @@ A simple Website to provide a `NNI (Non-Nerd Interface)` to [epub2go.py](https:/
## Development ## Development
This project uses [watchman](https://facebook.github.io/watchman/) for file watching and reloading. This project uses [watchman](https://facebook.github.io/watchman/) for file watching and reloading.
Follow the [official instructions](https://facebook.github.io/watchman/docs/install.html) for your system to install, django will default to its standard watcher otherwise. Follow the [official instructions](https://facebook.github.io/watchman/docs/install.html) for your system to install, django will default to its standard watcher otherwise.
Also, [celery](https://docs.celeryq.dev/en/stable/) is used as a task queue with [redis](https://hub.docker.com/_/redis) as backend.

6
docker-compose.yml Normal file
View File

@@ -0,0 +1,6 @@
name: task-queue
services:
redis:
ports:
- 6379:6379
image: redis

15
nginx.conf Normal file
View File

@@ -0,0 +1,15 @@
server {
listen 80;
server_name epub2go.example.org;
access_log /var/log/nginx/epub2go.log;
location /static {
alias /www/epub2go;
}
location / {
proxy_pass http://127.0.0.1:50000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}

View File

@@ -5,11 +5,13 @@ description = "Web Interface to epub2go.py"
readme = "README.md" readme = "README.md"
requires-python = ">=3.12" requires-python = ">=3.12"
dependencies = [ dependencies = [
"celery>=5.4.0", "celery[redis]>=5.4.0",
"django>=5.1.6", "django>=5.1.6",
"epub2go", "epub2go",
"gunicorn>=23.0.0",
"python-dotenv>=1.0.1", "python-dotenv>=1.0.1",
"pywatchman>=2.0.0", "pywatchman>=2.0.0",
"redis>=5.2.1",
] ]
[tool.uv.sources] [tool.uv.sources]

View File

@@ -1,24 +1,23 @@
from django.shortcuts import render from django.shortcuts import render
from django.http import HttpRequest, HttpResponse, FileResponse, HttpResponseBadRequest from django.http import HttpRequest, HttpResponse, FileResponse, HttpResponseBadRequest
from django.conf import settings
from celery import shared_task
from epub2go.convert import get_all_books, Book, GBConvert, allbooks_url from epub2go.convert import get_all_books, Book, allbooks_url
import os import os
from urllib.parse import urlparse from urllib.parse import urlparse
import logging import logging
from epub2go_web.tasks import getEpub
logger = logging.getLogger(__name__) #TODO configure logging logger = logging.getLogger(__name__) #TODO configure logging
converter = GBConvert(downloaddir=settings.MEDIA_ROOT)
books = sorted(get_all_books(), key= lambda b: b.title)# TODO get from pickle books = sorted(get_all_books(), key= lambda b: b.title)# TODO get from pickle
gbnetloc = urlparse(allbooks_url).netloc gbnetloc = urlparse(allbooks_url).netloc
def index(request: HttpRequest): def index(request: HttpRequest):
context = { context = {
'title': 'epub2go', 'title': 'epub2go',
'http_host': request.META['HTTP_HOST'], 'http_host': f'http://{ request.META['HTTP_HOST'] }',
'books': books, 'books': books,
'book_count': len(books), 'book_count': len(books),
'allbooks_url': allbooks_url, 'allbooks_url': allbooks_url,
@@ -28,7 +27,8 @@ def index(request: HttpRequest):
if targetParam: if targetParam:
if validateUrl(targetParam): if validateUrl(targetParam):
# download file # download file
fpath = getEpub(targetParam) result = getEpub.delay(targetParam)
fpath = result.get(timeout=60)
fname = os.path.basename(fpath) fname = os.path.basename(fpath)
file = open(fpath, 'rb') file = open(fpath, 'rb')
response = FileResponse(file) response = FileResponse(file)
@@ -38,7 +38,7 @@ def index(request: HttpRequest):
else: return HttpResponseBadRequest('Input URL invalid.') else: return HttpResponseBadRequest('Input URL invalid.')
else: else:
# return base view # return base view
return render(request, 'index.html', context) return render(request, 'home.html', context)
def validateUrl(param)->bool : def validateUrl(param)->bool :
@@ -46,9 +46,3 @@ def validateUrl(param)->bool :
if(netloc == gbnetloc): return True if(netloc == gbnetloc): return True
return False return False
# TODO make this async and show some indication of progress/loading
#@shared_task
def getEpub(book_url):
# TODO check for existing file and age
return converter.download(book_url)

View File

@@ -0,0 +1,5 @@
# This will make sure the app is always imported when
# Django starts so that shared_task will use this app.
from .celery import app as celery_app
__all__ = ('celery_app',)

23
src/epub2go_web/celery.py Normal file
View File

@@ -0,0 +1,23 @@
# Start Celery workers from src/ using `celery -A epub2go_web worker --loglevel=INFO`
import os
from celery import Celery
# Set the default Django settings module for the 'celery' program.
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'epub2go_web.settings')
app = Celery('epub2go_web')
# Using a string here means the worker doesn't have to serialize
# the configuration object to child processes.
# - namespace='CELERY' means all celery-related configuration keys
# should have a `CELERY_` prefix.
app.config_from_object('django.conf:settings', namespace='CELERY')
# Load task modules from all registered Django apps.
app.autodiscover_tasks()
@app.task(bind=True, ignore_result=True)
def debug_task(self):
print(f'Request: {self.request!r}')

View File

@@ -32,8 +32,6 @@ ALLOWED_HOSTS = ['*']
# Application definition # Application definition
INSTALLED_APPS = [ INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes", "django.contrib.contenttypes",
"django.contrib.sessions", "django.contrib.sessions",
"django.contrib.messages", "django.contrib.messages",
@@ -46,7 +44,6 @@ MIDDLEWARE = [
"django.contrib.sessions.middleware.SessionMiddleware", "django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware", "django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware", "django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware", "django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware",
] ]
@@ -64,7 +61,6 @@ TEMPLATES = [
"context_processors": [ "context_processors": [
"django.template.context_processors.debug", "django.template.context_processors.debug",
"django.template.context_processors.request", "django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages", "django.contrib.messages.context_processors.messages",
], ],
}, },
@@ -73,48 +69,18 @@ TEMPLATES = [
WSGI_APPLICATION = "epub2go_web.wsgi.application" WSGI_APPLICATION = "epub2go_web.wsgi.application"
# Database
# https://docs.djangoproject.com/en/5.1/ref/settings/#databases # https://docs.djangoproject.com/en/5.1/ref/settings/#databases
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "db.sqlite3",
}
}
#DATABASES = { # Celery settings
# "default": { CELERY_BROKER_URL = "redis://localhost:6379/0"
# "ENGINE": "django.db.backends.sqlite3", CELERY_RESULT_BACKEND = "redis://localhost:6379/0"
# "NAME": BASE_DIR / "db.sqlite3", CELERY_TASK_ALWAYS_EAGER = True
# }
#}
# Password validation
# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
},
{
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
},
{
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
},
{
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
},
]
# Internationalization
# https://docs.djangoproject.com/en/5.1/topics/i18n/
LANGUAGE_CODE = "en-us"
TIME_ZONE = "UTC"
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images) # Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.1/howto/static-files/ # https://docs.djangoproject.com/en/5.1/howto/static-files/
@@ -127,8 +93,3 @@ STATIC_ROOT = PROJ_DIR/ "staticfiles"
MEDIA_URL = "media/" MEDIA_URL = "media/"
MEDIA_ROOT = PROJ_DIR / "media/" MEDIA_ROOT = PROJ_DIR / "media/"
# Default primary key field type
# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

View File

@@ -7,6 +7,7 @@ const table_r = Array.from(table.getElementsByClassName('table-entry'));
document.addEventListener('keydown', (event)=>{ document.addEventListener('keydown', (event)=>{
if (event.ctrlKey && event.key === 'k'){ if (event.ctrlKey && event.key === 'k'){
event.preventDefault(); event.preventDefault();
searchInput.focus();
searchInput.select(); searchInput.select();
} }
}); });

View File

@@ -0,0 +1,21 @@
{
"name": "epub2go Converter",
"short_name": "epub2go",
"icons": [
{
"src": "/static/favicon/web-app-manifest-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/static/favicon/web-app-manifest-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}

View File

@@ -11,12 +11,13 @@ html{
} }
body{ body{
background-color: var(--bg); background-color: var(--bg);
margin:40px auto; overflow-x: hidden;
max-width:800px; margin:.5em auto;
max-width:min(100%,800px);
line-height:1.4; line-height:1.4;
font-size:18px; font-size:18px;
color:var(--fg); color:var(--fg);
padding:0 10px; padding:0 .2em;
} }
h1,h2,h3{ h1,h2,h3{
@@ -26,14 +27,18 @@ h1,h2,h3{
header{ header{
text-align: center; text-align: center;
justify-content: center; justify-content: center;
margin-bottom: 4%;
} }
small{ small{
color: var(--fg-deemph); color: var(--fg-deemph);
} }
.searchbar{ .search-bar{
width: 100%; width: 100%;
} }
.search-container{
text-align: center;
justify-content: center;
margin-bottom: 4%;
}
#searchInput { #searchInput {
background-color: var(--bg); background-color: var(--bg);
width: 100%; width: 100%;
@@ -43,9 +48,13 @@ small{
border-color: var(--bg-acc); border-color: var(--bg-acc);
box-shadow: var(--bg-acc) 2px 2px; box-shadow: var(--bg-acc) 2px 2px;
} }
.main-banner{
text-align: center;
}
table, tr{ table, tr{
/* make table not resize when elements are hidden by searching */ /* make table not resize when elements are hidden by searching */
width: 100%; width: 100%;
word-break: break-all;
} }
th{ th{
text-align: left; text-align: left;
@@ -70,9 +79,9 @@ a:hover, a:any-link{
color: inherit; color: inherit;
} }
.table-link{ .table-link{
/* TODO fix links with no title/content being almost unclickable */
display: block; display: block;
width: 100%; width: 100%;
min-height: 1em;
height: 100%; height: 100%;
padding: 1px; padding: 1px;
} }

11
src/epub2go_web/tasks.py Normal file
View File

@@ -0,0 +1,11 @@
from celery import shared_task
from django.conf import settings
from epub2go.convert import GBConvert
converter = GBConvert(downloaddir=settings.MEDIA_ROOT)
@shared_task
def getEpub(book_url):
# TODO check for existing file and age
return converter.download(book_url)

View File

@@ -0,0 +1,49 @@
{% extends 'index.html' %}
{% load static %}
{% block main %}
<main>
<div class="search-container">
<search>
<form onsubmit="submitSearch(event)" class="search-bar">
<input type="search" id="searchInput" placeholder="Suche nach Titel" minlength="3">
</form>
</search>
<small>Im Moment finden sich hier <a href="{{ allbooks_url }}">{{ book_count }} Bücher.</a> </small>
<a href="javascript:void(window.open('{{ http_host }}/?t=' + encodeURIComponent(window.location.toString())))" title="Als Lesezeichen speichern">
<img src="{% static 'bookmark.svg' %}" alt="Bookmarklet" class="header-icon">
</a>
<a href="https://github.com/eneller/epub2go-web" title="Projekt auf GitHub ansehen">
<img src="{% static 'github.svg' %}" alt="GitHub" class="header-icon">
</a>
</div>
<!-- NOTE use dl here?-->
<table id="table">
<thead>
<tr>
<th></th>
<th>Titel</th>
<th>Autor</th>
</tr>
</thead>
<tbody>
{% for item in books %}
<tr class="table-entry">
<td>
<a href= {{ item.url }} target="_blank" rel="noopener noreferrer" class="table-link">
<img src="{% static 'open-link.svg' %}" alt="Open Link" class="inline-icon">
</a>
</td>
<td class="table-data">
<a href="./?t= {{ item.url }}" class="table-link">
{{ item.title }}
</a>
</td>
<td>
<a> {{ item.author }}</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</main>
{% endblock %}

View File

@@ -6,57 +6,22 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0" > <meta name="viewport" content="width=device-width,initial-scale=1.0" >
<link rel="stylesheet" type="text/css" href="{% static 'styles.css' %}"> <link rel="stylesheet" type="text/css" href="{% static 'styles.css' %}">
<link rel="icon" type="image/png" href="{% static '/favicon/favicon-96x96.png' %}" sizes="96x96" />
<link rel="icon" type="image/svg+xml" href="{% static '/favicon/favicon.svg' %}" />
<link rel="shortcut icon" href="{% static '/favicon/favicon.ico' %}" />
<link rel="apple-touch-icon" sizes="180x180" href="{% static '/favicon/apple-touch-icon.png' %}" />
<meta name="apple-mobile-web-app-title" content="epub2go" />
<link rel="manifest" href="{% static 'site.webmanifest' %}" />
</head> </head>
<body> <body>
<header> <header>
<div> <h1><a href="{{ http_host }}">{{ title }}</a></h1>
<h1>{{ title }}</h1>
<h2>1 Click Download für Literatur</h2> <h2>1 Click Download für Literatur</h2>
</div>
<search>
<form onsubmit="submitSearch(event)" class="searchbar">
<input type="search" id="searchInput" placeholder="Suche nach Titel" minlength="3">
</form>
</search>
<small>Im Moment finden sich hier <a href="{{ allbooks_url }}">{{ book_count }} Bücher.</a> </small>
<a href="javascript:void(window.open('http://{{ http_host }}/?t=' + encodeURIComponent(window.location.toString())))" title="Als Lesezeichen speichern"><!--TODO fix domain part as variable-->
<img src="{% static 'bookmark.svg' %}" alt="Bookmarklet" class="header-icon">
</a>
<a href="https://github.com/eneller/epub2go-web">
<img src="{% static 'github.svg' %}" alt="GitHub" class="header-icon">
</a>
</header> </header>
<main> {% block main %}
<!-- NOTE use dl here?--> {% endblock %}
<table id="table">
<thead>
<tr>
<th></th>
<th>Titel</th>
<th>Autor</th>
</tr>
</thead>
<tbody>
{% for item in books %}
<tr class="table-entry">
<td>
<a href= {{ item.url }} target="_blank" rel="noopener noreferrer" class="table-link">
<img src="{% static 'open-link.svg' %}" alt="Open Link" class="inline-icon">
</a>
</td>
<td class="table-data">
<a href="./?t= {{ item.url }}" class="table-link">
{{ item.title }}
</a>
</td>
<td>
<a> {{ item.author }}</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</main>
<footer> <footer>
</footer> </footer>
</body> </body>

View File

@@ -0,0 +1,6 @@
{% extends 'index.html' %}
{% block main %}
<main>
<p class="main-banner">Einen Moment bitte. Deine Datei wird heruntergeladen.</p>
</main>
{% endblock %}

View File

@@ -15,7 +15,6 @@ Including another URLconf
2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
""" """
from django.contrib import admin
from django.urls import path, include from django.urls import path, include
from django.http import HttpRequest, HttpResponse from django.http import HttpRequest, HttpResponse
from django.shortcuts import render, redirect from django.shortcuts import render, redirect
@@ -23,6 +22,5 @@ from epub2go.convert import get_all_books, GBConvert, Book
urlpatterns = [ urlpatterns = [
path("admin/", admin.site.urls),
path('',include('core.urls')) path('',include('core.urls'))
] ]

18
src/gunicorn.py Normal file
View File

@@ -0,0 +1,18 @@
# Django WSGI application path in pattern MODULE_NAME:VARIABLE_NAME
wsgi_app = "epub2go_web.wsgi"
# The granularity of Error log outputs
loglevel = "debug"
# The number of worker processes for handling requests
workers = 4
# The socket to bind
bind = "127.0.0.1:50000"
# Restart workers when code changes (development only!)
#reload = True
# Write access and error info to /var/log
#accesslog = errorlog = "/var/log/gunicorn/dev.log"
# Redirect stdout/stderr to log file
capture_output = True
# PID file so you can easily fetch process ID
#pidfile = "/var/run/gunicorn/dev.pid"
# Daemonize the Gunicorn process (detach & enter background)
#daemon = True

72
uv.lock generated
View File

@@ -46,7 +46,7 @@ wheels = [
[[package]] [[package]]
name = "celery" name = "celery"
version = "5.4.0" version = "5.5.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "billiard" }, { name = "billiard" },
@@ -56,12 +56,16 @@ dependencies = [
{ name = "click-repl" }, { name = "click-repl" },
{ name = "kombu" }, { name = "kombu" },
{ name = "python-dateutil" }, { name = "python-dateutil" },
{ name = "tzdata" },
{ name = "vine" }, { name = "vine" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/8a/9c/cf0bce2cc1c8971bf56629d8f180e4ca35612c7e79e6e432e785261a8be4/celery-5.4.0.tar.gz", hash = "sha256:504a19140e8d3029d5acad88330c541d4c3f64c789d85f94756762d8bca7e706", size = 1575692 } sdist = { url = "https://files.pythonhosted.org/packages/07/0c/0aaf310f4a2a048f010ebf63b8b0aaeb02ede2a79864fdbcdf31757a139d/celery-5.5.0.tar.gz", hash = "sha256:10d49f9926d16237310109b0e6e1e2f7a2133b84e684bb36534d7663e66919bb", size = 1665645 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/90/c4/6a4d3772e5407622feb93dd25c86ce3c0fee746fa822a777a627d56b4f2a/celery-5.4.0-py3-none-any.whl", hash = "sha256:369631eb580cf8c51a82721ec538684994f8277637edde2dfc0dacd73ed97f64", size = 425983 }, { url = "https://files.pythonhosted.org/packages/1a/98/d4eefe492ef810f304203cfff0f60554fab95514ec51b701a4529b66fa92/celery-5.5.0-py3-none-any.whl", hash = "sha256:f4170c6e5952281318448a899d9e9a15b9cbd007e002091766900dc8f71b9394", size = 438396 },
]
[package.optional-dependencies]
redis = [
{ name = "redis" },
] ]
[[package]] [[package]]
@@ -182,8 +186,8 @@ wheels = [
[[package]] [[package]]
name = "epub2go" name = "epub2go"
version = "2.1" version = "2.2.1"
source = { git = "https://github.com/eneller/epub2go.py#42677007633c1a4f7f87a2f8f02b9faa1715f037" } source = { git = "https://github.com/eneller/epub2go.py#401d02e0ca26ca0a12ee331140a2cde273559251" }
dependencies = [ dependencies = [
{ name = "beautifulsoup4" }, { name = "beautifulsoup4" },
{ name = "click" }, { name = "click" },
@@ -198,20 +202,36 @@ name = "epub2go-py-web"
version = "0.1.0" version = "0.1.0"
source = { virtual = "." } source = { virtual = "." }
dependencies = [ dependencies = [
{ name = "celery" }, { name = "celery", extra = ["redis"] },
{ name = "django" }, { name = "django" },
{ name = "epub2go" }, { name = "epub2go" },
{ name = "gunicorn" },
{ name = "python-dotenv" }, { name = "python-dotenv" },
{ name = "pywatchman" }, { name = "pywatchman" },
{ name = "redis" },
] ]
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "celery", specifier = ">=5.4.0" }, { name = "celery", extras = ["redis"], specifier = ">=5.4.0" },
{ name = "django", specifier = ">=5.1.6" }, { name = "django", specifier = ">=5.1.6" },
{ name = "epub2go", git = "https://github.com/eneller/epub2go.py" }, { name = "epub2go", git = "https://github.com/eneller/epub2go.py" },
{ name = "gunicorn", specifier = ">=23.0.0" },
{ name = "python-dotenv", specifier = ">=1.0.1" }, { name = "python-dotenv", specifier = ">=1.0.1" },
{ name = "pywatchman", specifier = ">=2.0.0" }, { name = "pywatchman", specifier = ">=2.0.0" },
{ name = "redis", specifier = ">=5.2.1" },
]
[[package]]
name = "gunicorn"
version = "23.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "packaging" },
]
sdist = { url = "https://files.pythonhosted.org/packages/34/72/9614c465dc206155d93eff0ca20d42e1e35afc533971379482de953521a4/gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec", size = 375031 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/7d/6dac2a6e1eba33ee43f318edbed4ff29151a49b5d37f080aad1e6469bca4/gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d", size = 85029 },
] ]
[[package]] [[package]]
@@ -225,16 +245,25 @@ wheels = [
[[package]] [[package]]
name = "kombu" name = "kombu"
version = "5.5.0" version = "5.5.2"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "amqp" }, { name = "amqp" },
{ name = "tzdata" }, { name = "tzdata" },
{ name = "vine" }, { name = "vine" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/0f/a5/eddeaf1044b94d802ba9d69f5446f758a14b6770bcfd7498d5270fc2e68f/kombu-5.5.0.tar.gz", hash = "sha256:72e65c062e903ee1b4e8b68d348f63c02afc172eda409e3aca85867752e79c0b", size = 460768 } sdist = { url = "https://files.pythonhosted.org/packages/c8/12/7a340f48920f30d6febb65d0c4aca70ed01b29e116131152977df78a9a39/kombu-5.5.2.tar.gz", hash = "sha256:2dd27ec84fd843a4e0a7187424313f87514b344812cb98c25daddafbb6a7ff0e", size = 461522 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/23/29/f4f109c490d3cab3cbbba53403895e6052c84a40f18c28f75cd4dd53f11d/kombu-5.5.0-py3-none-any.whl", hash = "sha256:526c6cf038c986b998639109a1eb762502f831e8da148cc928f1f95cd91eb874", size = 209313 }, { url = "https://files.pythonhosted.org/packages/af/ba/939f3db0fca87715c883e42cc93045347d61a9d519c270a38e54a06db6e1/kombu-5.5.2-py3-none-any.whl", hash = "sha256:40f3674ed19603b8a771b6c74de126dbf8879755a0337caac6602faa82d539cd", size = 209763 },
]
[[package]]
name = "packaging"
version = "24.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 },
] ]
[[package]] [[package]]
@@ -272,11 +301,11 @@ wheels = [
[[package]] [[package]]
name = "python-dotenv" name = "python-dotenv"
version = "1.0.1" version = "1.1.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115 } sdist = { url = "https://files.pythonhosted.org/packages/88/2c/7bb1416c5620485aa793f2de31d3df393d3686aa8a8506d11e10e13c5baf/python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5", size = 39920 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 }, { url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256 },
] ]
[[package]] [[package]]
@@ -288,6 +317,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/51/7f/5d68d803489770cffa5d2b44be99b978c866f8a4d8e835f9da850415ed8a/pywatchman-2.0.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:51c2b4c72bea6b9fd90caf20759f5bc47febf0fd27bf2f247b87c66e2f6bab02", size = 52557 }, { url = "https://files.pythonhosted.org/packages/51/7f/5d68d803489770cffa5d2b44be99b978c866f8a4d8e835f9da850415ed8a/pywatchman-2.0.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:51c2b4c72bea6b9fd90caf20759f5bc47febf0fd27bf2f247b87c66e2f6bab02", size = 52557 },
] ]
[[package]]
name = "redis"
version = "5.2.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/47/da/d283a37303a995cd36f8b92db85135153dc4f7a8e4441aa827721b442cfb/redis-5.2.1.tar.gz", hash = "sha256:16f2e22dff21d5125e8481515e386711a34cbec50f0e44413dd7d9c060a54e0f", size = 4608355 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3c/5f/fa26b9b2672cbe30e07d9a5bdf39cf16e3b80b42916757c5f92bca88e4ba/redis-5.2.1-py3-none-any.whl", hash = "sha256:ee7e1056b9aea0f04c6c2ed59452947f34c4940ee025f5dd83e6a6418b6989e4", size = 261502 },
]
[[package]] [[package]]
name = "requests" name = "requests"
version = "2.32.3" version = "2.32.3"
@@ -344,11 +382,11 @@ wheels = [
[[package]] [[package]]
name = "tzdata" name = "tzdata"
version = "2025.1" version = "2025.2"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/43/0f/fa4723f22942480be4ca9527bbde8d43f6c3f2fe8412f00e7f5f6746bc8b/tzdata-2025.1.tar.gz", hash = "sha256:24894909e88cdb28bd1636c6887801df64cb485bd593f2fd83ef29075a81d694", size = 194950 } sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/0f/dd/84f10e23edd882c6f968c21c2434fe67bd4a528967067515feca9e611e5e/tzdata-2025.1-py2.py3-none-any.whl", hash = "sha256:7e127113816800496f027041c570f50bcd464a020098a3b6b199517772303639", size = 346762 }, { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839 },
] ]
[[package]] [[package]]