Compare commits

...

18 Commits
main ... dev

Author SHA1 Message Date
Calum Andrew Morrell a4007a423a Linked news into main navigation. 2025-11-13 20:31:03 +00:00
Calum Andrew Morrell e9efe8ba77 Created form template to add a news item and minor updates to models & views. 2025-11-13 11:19:20 +00:00
Calum Andrew Morrell 06d235d602 Added view and path to create a new news item. 2025-11-13 10:13:31 +00:00
Calum Andrew Morrell 5484b89e3a Added a class and path to redirect to original article. 2025-11-11 15:48:11 +00:00
Calum Andrew Morrell 28bbef0ea5 Created a file to hold project wide todos and related. 2025-11-10 20:59:37 +00:00
Calum Andrew Morrell 4c643a63ee Corrected variable names. 2025-11-10 20:59:03 +00:00
Calum Andrew Morrell d347a78f18 Added origin text field to hold link text displayed to user. 2025-11-10 20:58:31 +00:00
Calum Andrew Morrell 98884e2ddf Initial migration for news app. 2025-11-09 21:45:29 +00:00
Calum Andrew Morrell f4000948b4 Added missing / to media url. 2025-11-09 21:44:53 +00:00
Calum Andrew Morrell 63bf8211a6 Links news into project. 2025-11-09 21:43:30 +00:00
Calum Andrew Morrell 7538220e9a Created basic templates to display news items, categories and tags. 2025-11-09 21:43:02 +00:00
Calum Andrew Morrell 284bddd0d8 Added views & paths for news items, categories and tags. 2025-11-09 21:19:23 +00:00
Calum Andrew Morrell f3c6b66e7d Linked news items and categories into admin. 2025-11-09 21:00:50 +00:00
Calum Andrew Morrell 988483a9bc Added model for news items. 2025-11-09 21:00:21 +00:00
Calum Andrew Morrell ee4b010411 Category model added. 2025-11-09 15:24:48 +00:00
Calum Andrew Morrell 59be6da865 Prepared project for markdownx & tagulous. 2025-11-09 15:17:01 +00:00
Calum Andrew Morrell 87e0fdff4f Added the packages django-markdownx and django-tagulous. 2025-11-09 15:06:40 +00:00
Calum Andrew Morrell 51709ec157 Created app to handle news items. 2025-11-09 14:34:21 +00:00
22 changed files with 611 additions and 1 deletions

9
TODO.md Normal file
View File

@ -0,0 +1,9 @@
# News
TODO: Add the header image into the news item list view.
TODO: Add the header image into the news item detail view.
TODO: Add the origin text into the news item list view.
TODO: Add the origin text into the news item detail view.
TODO: Create a RedirectView for the origin link.
TODO: On clicking the origin link, add one to the origin followed count.

View File

@ -45,6 +45,8 @@ INSTALLED_APPS = [
"django.contrib.staticfiles",
"core",
"dashboard",
"news",
"markdownx",
]
MIDDLEWARE = [
@ -128,6 +130,8 @@ USE_TZ = True
STATIC_URL = "static/"
STATIC_ROOT = BASE_DIR / "static"
MEDIA_URL = "media/"
MEDIA_ROOT = BASE_DIR / "media"
# User model and authentication
@ -136,6 +140,31 @@ LOGIN_REDIRECT_URL = "dashboard:dashboard"
LOGOUT_REDIRECT_URL = "core:homepage"
# Email backend configuration
# DEFAULT_FROM_EMAIL = 'system@dazed-gerbil.com'
# EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
# EMAIL_HOST = os.getenv('EMAIL_HOST')
# EMAIL_PORT = os.getenv('EMAIL_PORT')
# EMAIL_USE_TLS = (os.getenv('EMAIL_USE_TLS') == 'True')
# EMAIL_HOST_USER = os.getenv('EMAIL_HOST_USER')
# EMAIL_HOST_PASSWORD = os.getenv('EMAIL_HOST_PASSWORD')
# Tagulous config
SERIALIZATION_MODULES = {
"xml": "tagulous.serializers.xml_serializer",
"json": "tagulous.serializers.json",
"python": "tagulous.serializers.python",
"yaml": "tagulous.serializers.pyyaml",
}
# Markdownx config
MARKDOWNX_MARKDOWN_EXTENSIONS = [
"markdown.extensions.extra",
]
# Default primary key field type
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field

View File

@ -23,4 +23,6 @@ urlpatterns = [
path("", include("core.urls", namespace="core")),
path("accounts/", include("accounts.urls")),
path("dashboard/", include("dashboard.urls", namespace="dashboard")),
path("news/", include("news.urls", namespace="news")),
path("markdownx/", include("markdownx.urls")),
]

View File

@ -1,4 +1,12 @@
<header>
<h1><a href="{% url 'core:homepage' %}">Astronomy Base by The Dazed Gerbil</a></h1>
<div>
<h1><a href="{% url 'core:homepage' %}">Astronomy Base by The Dazed Gerbil</a></h1>
</div>
<div>
<nav>
<a href="{% url 'core:homepage' %}">Home</a>
<a href="{% url 'news:list' %}">News</a>
</nav>
</div>
</header>

0
news/__init__.py Normal file
View File

30
news/admin.py Normal file
View File

@ -0,0 +1,30 @@
import tagulous.admin
from django.contrib import admin
from markdownx.admin import MarkdownxModelAdmin
from .models import Category, NewsItem
@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
list_display = ["title", "slug"]
prepopulated_fields = {"slug": ("title",)}
class NewsItemAdmin(MarkdownxModelAdmin):
list_display = [
"title",
"category",
"tags",
"created_at",
"is_published",
"is_featured",
"owner",
]
list_filter = ["is_published", "is_featured", "category", "tags", "owner"]
ordering = ["is_published", "-created_at"]
prepopulated_fields = {"slug": ("title",)}
list_display_links = ["title"]
tagulous.admin.register(NewsItem, NewsItemAdmin)

6
news/apps.py Normal file
View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class NewsConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "news"

View File

@ -0,0 +1,126 @@
# Generated by Django 5.2.8 on 2025-11-09 21:45
import django.db.models.deletion
import markdownx.models
import tagulous.models.fields
import tagulous.models.models
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name="Category",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("title", models.CharField(max_length=40, unique=True)),
("slug", models.SlugField(max_length=40, unique=True)),
],
options={
"ordering": ["title"],
},
),
migrations.CreateModel(
name="Tagulous_NewsItem_tags",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=255, unique=True)),
("slug", models.SlugField()),
(
"count",
models.IntegerField(
default=0,
help_text="Internal counter of how many times this tag is in use",
),
),
(
"protected",
models.BooleanField(
default=False,
help_text="Will not be deleted when the count reaches 0",
),
),
],
options={
"ordering": ("name",),
"abstract": False,
"unique_together": {("slug",)},
},
bases=(tagulous.models.models.BaseTagModel, models.Model),
),
migrations.CreateModel(
name="NewsItem",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("title", models.CharField(max_length=200)),
("slug", models.SlugField(max_length=200, unique=True)),
("body", markdownx.models.MarkdownxField()),
("origin_link", models.URLField(blank=True, null=True)),
("origin_times_followed", models.PositiveIntegerField()),
("header_img", models.ImageField(blank=True, null=True, upload_to="")),
("created_at", models.DateTimeField(auto_now_add=True)),
("is_published", models.BooleanField(default=False)),
("is_featured", models.BooleanField(default=False)),
(
"category",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="news_items",
to="news.category",
),
),
(
"owner",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="news_items",
to=settings.AUTH_USER_MODEL,
),
),
(
"tags",
tagulous.models.fields.TagField(
_set_tag_meta=True,
force_lowercase=True,
help_text="Enter a comma-separated tag string",
to="news.tagulous_newsitem_tags",
),
),
],
options={
"ordering": ["-created_at"],
},
),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 5.2.8 on 2025-11-10 19:55
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("news", "0001_initial"),
]
operations = [
migrations.AddField(
model_name="newsitem",
name="origin_text",
field=models.CharField(blank=True, max_length=200, null=True),
),
migrations.AlterField(
model_name="newsitem",
name="origin_times_followed",
field=models.PositiveIntegerField(default=0),
),
]

View File

68
news/models.py Normal file
View File

@ -0,0 +1,68 @@
from django.contrib.auth import get_user_model
from django.db import models
from django.urls import reverse_lazy
from django.utils.text import slugify
from markdownx.models import MarkdownxField
from markdownx.utils import markdownify
from tagulous.models import TagField
class Category(models.Model):
title = models.CharField(max_length=40, unique=True)
slug = models.SlugField(max_length=40, unique=True)
def save(self):
if not self.slug:
self.slug = slugify(self.title)
return super(Category, self).save()
def get_absolute_url(self):
return reverse_lazy("news:list_category", args=[self.slug])
def __str__(self):
return self.title
class Meta:
ordering = ["title"]
class NewsItem(models.Model):
title = models.CharField(max_length=200)
slug = models.SlugField(max_length=200, unique=True)
category = models.ForeignKey(
Category, on_delete=models.PROTECT, related_name="news_items"
)
body = MarkdownxField()
origin_text = models.CharField(max_length=200, blank=True, null=True)
origin_link = models.URLField(blank=True, null=True)
origin_times_followed = models.PositiveIntegerField(default=0)
header_img = models.ImageField(blank=True, null=True)
tags = TagField(
force_lowercase=True,
get_absolute_url=lambda tag: reverse_lazy(
"news:list_tag", kwargs={"tag": tag.slug}
),
)
created_at = models.DateTimeField(auto_now_add=True)
is_published = models.BooleanField(default=False)
is_featured = models.BooleanField(default=False)
owner = models.ForeignKey(
get_user_model(), on_delete=models.CASCADE, related_name="news_items"
)
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.title)
return super(NewsItem, self).save(*args, **kwargs)
def get_absolute_url(self):
return reverse_lazy("news:detail", args=[self.category.slug, self.slug])
def body_as_markdown(self):
return markdownify(self.body)
def __str__(self):
return self.title
class Meta:
ordering = ["-created_at"]

View File

@ -0,0 +1,21 @@
{% extends 'base.html' %}
{% block content %}
<main>
{% if object_list %}
<h3>Category List</h3>
<hr>
<ul>
{% for category in object_list %}
<li>
<a href="{{ category.get_absolute_url }}">{{ category.title }}</a>
<!-- {% if category.introduction %} -->
<!-- <hr> -->
<!-- <p>{{ category.introduction }}</p> -->
<!-- {% endif %} -->
</li>
{% endfor %}
</ul>
{% endif %}
</main>
{% endblock %}

View File

@ -0,0 +1,3 @@
{% for tag in newsitem.tags.all %}
<a href="{{ tag.get_absolute_url }}">{{ tag }}</a>{% if tag != newsitem.tags.last %},{% endif %}
{% endfor %}

View File

@ -0,0 +1,17 @@
{% extends 'base.html' %}
{% block content %}
<article>
<h3>{{ newsitem.title }}</h3>
<p>{{ newsitem.created_at | date:'jS \o\f F, Y' }}</p>
<hr>
<div>
{{ newsitem.body_as_markdown|safe }}
</div>
<hr>
<div>
<span>Posted in the <a href="{% url 'news:list_category' category=newsitem.category.slug %}">{{ newsitem.category }}</a> category</span>
<span>{% include 'news/includes/display_tags.html' %}</span>
</div>
</article>
{% endblock %}

View File

@ -0,0 +1,13 @@
{% extends 'base.html' %}
{% block content %}
<div>
<h1>News Item</h1>
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Create Item">
<a href="{% url 'dashboard:dashboard' %}">Cancel</a>
</form>
</div>
{% endblock %}

View File

@ -0,0 +1,42 @@
{% extends 'base.html' %}
{% block content %}
<main>
{% if object_list %}
{% if category %}
<h3>{{ category }} News</h3>
{% elif tag %}
<h3>News for tag: {{ tag }}</h3>
{% else %}
<h3>Recent News</h3>
{% endif %}
<hr>
<div>
{% for newsitem in object_list %}
<div>
<a href="{{ newsitem.get_absolute_url }}">{{ newsitem.title }}</a>
<hr>
{{ newsitem.body_as_markdown|safe }}
<hr>
<div>
<span>
<a href="{% url 'news:list_category' category=newsitem.category.slug %}">{{ newsitem.category }} </a>
&bull; {{ newsitem.created_at | date:'jS \o\f F, Y' }}
</span>
<span>{% include 'news/includes/display_tags.html' %}</span>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div>
<h3>Oops!</h3>
<h5>It looks like there is no news for that {% if category %}category{% elif tag %}tag{% endif %} yet.</h5>
<p>(sorry not sorry)</p>
</div>
{% endif %}
{% if page_obj.has_previous or page_obj.has_next %}
{% include 'pagination.html' %}
{% endif %}
</main>
{% endblock %}

View File

@ -0,0 +1,17 @@
{% extends 'base.html' %}
{% block content %}
<main>
{% if object_list %}
<h3>List of Tags</h3>
<hr>
<ul>
{% for tag in object_list %}
<li>
<a href="{{ tag.get_absolute_url }}">{{ tag }}</a>
</li>
{% endfor %}
</ul>
{% endif %}
</main>
{% endblock %}

3
news/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

17
news/urls.py Normal file
View File

@ -0,0 +1,17 @@
from django.urls import path
from .views import (CategoryList, NewsItemCreate, NewsItemDetail, NewsItemList,
NewsItemRedirectView, TagList)
app_name = "news"
urlpatterns = [
path("", NewsItemList.as_view(), name="list"),
path("add-news/", NewsItemCreate.as_view(), name="add_news"),
path("categories/", CategoryList.as_view(), name="categories"),
path("categories/<slug:category>/", NewsItemList.as_view(), name="list_category"),
path("tags/", TagList.as_view(), name="tags"),
path("tags/<slug:tag>/", NewsItemList.as_view(), name="list_tag"),
path("<slug:category>/<slug:slug>/", NewsItemDetail.as_view(), name="detail"),
path("origin/<str:origin_text>/", NewsItemRedirectView.as_view(), name="origin"),
]

77
news/views.py Normal file
View File

@ -0,0 +1,77 @@
from django.contrib.auth.decorators import login_required
from django.shortcuts import get_object_or_404
from django.urls import reverse_lazy
from django.utils.decorators import method_decorator
from django.views.generic import (CreateView, DetailView, ListView,
RedirectView, UpdateView)
from .models import Category, NewsItem
class CategoryList(ListView):
model = Category
@method_decorator(login_required, name="dispatch")
class NewsItemCreate(CreateView):
model = NewsItem
fields = [
"title",
"category",
"body",
"origin_text",
"origin_link",
"header_img",
"tags",
"is_published",
"is_featured",
]
success_url = reverse_lazy("dashboard:dashboard")
def form_valid(self, form):
form.instance.owner = self.request.user
return super().form_valid(form)
class NewsItemDetail(DetailView):
model = NewsItem
class NewsItemList(ListView):
category = None
tag = None
paginate_by = 6
def get_queryset(self):
category_slug = self.kwargs.get("category", None)
self.tag = self.kwargs.get("tag", None)
if category_slug:
self.category = get_object_or_404(Category, slug=category_slug)
return NewsItem.objects.filter(category=self.category).filter(
is_published=True
)
elif self.tag:
return NewsItem.objects.filter(tags=self.tag).filter(is_published=True)
else:
return NewsItem.objects.filter(is_published=True)
def get_context_data(self, *, object_list=None, **kwargs):
context = super(NewsItemList, self).get_context_data(**kwargs)
context["category"] = self.category
context["tag"] = self.tag
return context
class NewsItemRedirectView(RedirectView):
def get_redirect_url(self, *args, **kwargs):
url = get_object_or_404(NewsItem, origin_text=kwargs["origin_text"])
url.origin_times_followed += 1
url.save()
return url.origin_link
class TagList(ListView):
template_name = "news/tag_list.html"
def get_queryset(self):
return NewsItem.tags.tag_model.objects.all()

View File

@ -6,6 +6,8 @@ readme = "README.md"
requires-python = ">=3.13"
dependencies = [
"django>=5.2.8",
"django-markdownx>=4.0.9",
"django-tagulous>=2.1.1",
"psycopg>=3.2.12",
"python-dotenv>=1.2.1",
]

97
uv.lock
View File

@ -17,6 +17,8 @@ version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "django" },
{ name = "django-markdownx" },
{ name = "django-tagulous" },
{ name = "psycopg" },
{ name = "python-dotenv" },
]
@ -24,6 +26,8 @@ dependencies = [
[package.metadata]
requires-dist = [
{ name = "django", specifier = ">=5.2.8" },
{ name = "django-markdownx", specifier = ">=4.0.9" },
{ name = "django-tagulous", specifier = ">=2.1.1" },
{ name = "psycopg", specifier = ">=3.2.12" },
{ name = "python-dotenv", specifier = ">=1.2.1" },
]
@ -42,6 +46,99 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/5e/3d/a035a4ee9b1d4d4beee2ae6e8e12fe6dee5514b21f62504e22efcbd9fb46/django-5.2.8-py3-none-any.whl", hash = "sha256:37e687f7bd73ddf043e2b6b97cfe02fcbb11f2dbb3adccc6a2b18c6daa054d7f", size = 8289692, upload-time = "2025-11-05T14:07:28.761Z" },
]
[[package]]
name = "django-markdownx"
version = "4.0.9"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "django" },
{ name = "markdown" },
{ name = "pillow" },
]
sdist = { url = "https://files.pythonhosted.org/packages/4a/80/e4ff7b93b7518efe7fbe9b18ac5735b53fae943063df6fbef53bc454e045/django-markdownx-4.0.9.tar.gz", hash = "sha256:f82949beaddcaf5cbe765f580b1bf062b3aa5eea94633fdbbb6466514311a907", size = 32649, upload-time = "2025-04-23T12:13:32.864Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1a/76/53574402650befe1ee7b7bb2ddf49b2862a2a080c8042ff3c853922363bb/django_markdownx-4.0.9-py2.py3-none-any.whl", hash = "sha256:0e78759a8701073fee3e4883764a9bf1cdf3fe4c2941747acbe4f093f9285805", size = 42411, upload-time = "2025-04-23T12:13:31.554Z" },
]
[[package]]
name = "django-tagulous"
version = "2.1.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "django" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f4/f7/bef3d5ff4e0c7b786732ab77ca41eff8dd704f6552fb70b2fada1a0b64bc/django_tagulous-2.1.1.tar.gz", hash = "sha256:772941e0e359bb5478597fdafdb89e5619310a038e777e9599d0a5c036a05ceb", size = 291557, upload-time = "2025-06-22T23:21:34.262Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/66/03/e5bf7c3535f06d3962065a8e320aaca1cf6bef85e23e3fd8725e95c551be/django_tagulous-2.1.1-py3-none-any.whl", hash = "sha256:f5a6453407bcf4047f619f92be42e8b56615e00bbbd718d8810ce6fd5cf4888b", size = 286350, upload-time = "2025-06-22T23:21:32.588Z" },
]
[[package]]
name = "markdown"
version = "3.10"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/7dd27d9d863b3376fcf23a5a13cb5d024aed1db46f963f1b5735ae43b3be/markdown-3.10.tar.gz", hash = "sha256:37062d4f2aa4b2b6b32aefb80faa300f82cc790cb949a35b8caede34f2b68c0e", size = 364931, upload-time = "2025-11-03T19:51:15.007Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/70/81/54e3ce63502cd085a0c556652a4e1b919c45a446bd1e5300e10c44c8c521/markdown-3.10-py3-none-any.whl", hash = "sha256:b5b99d6951e2e4948d939255596523444c0e677c669700b1d17aa4a8a464cb7c", size = 107678, upload-time = "2025-11-03T19:51:13.887Z" },
]
[[package]]
name = "pillow"
version = "12.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/cace85a1b0c9775a9f8f5d5423c8261c858760e2466c79b2dd184638b056/pillow-12.0.0.tar.gz", hash = "sha256:87d4f8125c9988bfbed67af47dd7a953e2fc7b0cc1e7800ec6d2080d490bb353", size = 47008828, upload-time = "2025-10-15T18:24:14.008Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/62/f2/de993bb2d21b33a98d031ecf6a978e4b61da207bef02f7b43093774c480d/pillow-12.0.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0869154a2d0546545cde61d1789a6524319fc1897d9ee31218eae7a60ccc5643", size = 4045493, upload-time = "2025-10-15T18:22:25.758Z" },
{ url = "https://files.pythonhosted.org/packages/0e/b6/bc8d0c4c9f6f111a783d045310945deb769b806d7574764234ffd50bc5ea/pillow-12.0.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a7921c5a6d31b3d756ec980f2f47c0cfdbce0fc48c22a39347a895f41f4a6ea4", size = 4120461, upload-time = "2025-10-15T18:22:27.286Z" },
{ url = "https://files.pythonhosted.org/packages/5d/57/d60d343709366a353dc56adb4ee1e7d8a2cc34e3fbc22905f4167cfec119/pillow-12.0.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:1ee80a59f6ce048ae13cda1abf7fbd2a34ab9ee7d401c46be3ca685d1999a399", size = 3576912, upload-time = "2025-10-15T18:22:28.751Z" },
{ url = "https://files.pythonhosted.org/packages/a4/a4/a0a31467e3f83b94d37568294b01d22b43ae3c5d85f2811769b9c66389dd/pillow-12.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c50f36a62a22d350c96e49ad02d0da41dbd17ddc2e29750dbdba4323f85eb4a5", size = 5249132, upload-time = "2025-10-15T18:22:30.641Z" },
{ url = "https://files.pythonhosted.org/packages/83/06/48eab21dd561de2914242711434c0c0eb992ed08ff3f6107a5f44527f5e9/pillow-12.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5193fde9a5f23c331ea26d0cf171fbf67e3f247585f50c08b3e205c7aeb4589b", size = 4650099, upload-time = "2025-10-15T18:22:32.73Z" },
{ url = "https://files.pythonhosted.org/packages/fc/bd/69ed99fd46a8dba7c1887156d3572fe4484e3f031405fcc5a92e31c04035/pillow-12.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bde737cff1a975b70652b62d626f7785e0480918dece11e8fef3c0cf057351c3", size = 6230808, upload-time = "2025-10-15T18:22:34.337Z" },
{ url = "https://files.pythonhosted.org/packages/ea/94/8fad659bcdbf86ed70099cb60ae40be6acca434bbc8c4c0d4ef356d7e0de/pillow-12.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a6597ff2b61d121172f5844b53f21467f7082f5fb385a9a29c01414463f93b07", size = 8037804, upload-time = "2025-10-15T18:22:36.402Z" },
{ url = "https://files.pythonhosted.org/packages/20/39/c685d05c06deecfd4e2d1950e9a908aa2ca8bc4e6c3b12d93b9cafbd7837/pillow-12.0.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b817e7035ea7f6b942c13aa03bb554fc44fea70838ea21f8eb31c638326584e", size = 6345553, upload-time = "2025-10-15T18:22:38.066Z" },
{ url = "https://files.pythonhosted.org/packages/38/57/755dbd06530a27a5ed74f8cb0a7a44a21722ebf318edbe67ddbd7fb28f88/pillow-12.0.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4f1231b7dec408e8670264ce63e9c71409d9583dd21d32c163e25213ee2a344", size = 7037729, upload-time = "2025-10-15T18:22:39.769Z" },
{ url = "https://files.pythonhosted.org/packages/ca/b6/7e94f4c41d238615674d06ed677c14883103dce1c52e4af16f000338cfd7/pillow-12.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e51b71417049ad6ab14c49608b4a24d8fb3fe605e5dfabfe523b58064dc3d27", size = 6459789, upload-time = "2025-10-15T18:22:41.437Z" },
{ url = "https://files.pythonhosted.org/packages/9c/14/4448bb0b5e0f22dd865290536d20ec8a23b64e2d04280b89139f09a36bb6/pillow-12.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d120c38a42c234dc9a8c5de7ceaaf899cf33561956acb4941653f8bdc657aa79", size = 7130917, upload-time = "2025-10-15T18:22:43.152Z" },
{ url = "https://files.pythonhosted.org/packages/dd/ca/16c6926cc1c015845745d5c16c9358e24282f1e588237a4c36d2b30f182f/pillow-12.0.0-cp313-cp313-win32.whl", hash = "sha256:4cc6b3b2efff105c6a1656cfe59da4fdde2cda9af1c5e0b58529b24525d0a098", size = 6302391, upload-time = "2025-10-15T18:22:44.753Z" },
{ url = "https://files.pythonhosted.org/packages/6d/2a/dd43dcfd6dae9b6a49ee28a8eedb98c7d5ff2de94a5d834565164667b97b/pillow-12.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:4cf7fed4b4580601c4345ceb5d4cbf5a980d030fd5ad07c4d2ec589f95f09905", size = 7007477, upload-time = "2025-10-15T18:22:46.838Z" },
{ url = "https://files.pythonhosted.org/packages/77/f0/72ea067f4b5ae5ead653053212af05ce3705807906ba3f3e8f58ddf617e6/pillow-12.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:9f0b04c6b8584c2c193babcccc908b38ed29524b29dd464bc8801bf10d746a3a", size = 2435918, upload-time = "2025-10-15T18:22:48.399Z" },
{ url = "https://files.pythonhosted.org/packages/f5/5e/9046b423735c21f0487ea6cb5b10f89ea8f8dfbe32576fe052b5ba9d4e5b/pillow-12.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7fa22993bac7b77b78cae22bad1e2a987ddf0d9015c63358032f84a53f23cdc3", size = 5251406, upload-time = "2025-10-15T18:22:49.905Z" },
{ url = "https://files.pythonhosted.org/packages/12/66/982ceebcdb13c97270ef7a56c3969635b4ee7cd45227fa707c94719229c5/pillow-12.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f135c702ac42262573fe9714dfe99c944b4ba307af5eb507abef1667e2cbbced", size = 4653218, upload-time = "2025-10-15T18:22:51.587Z" },
{ url = "https://files.pythonhosted.org/packages/16/b3/81e625524688c31859450119bf12674619429cab3119eec0e30a7a1029cb/pillow-12.0.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c85de1136429c524e55cfa4e033b4a7940ac5c8ee4d9401cc2d1bf48154bbc7b", size = 6266564, upload-time = "2025-10-15T18:22:53.215Z" },
{ url = "https://files.pythonhosted.org/packages/98/59/dfb38f2a41240d2408096e1a76c671d0a105a4a8471b1871c6902719450c/pillow-12.0.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:38df9b4bfd3db902c9c2bd369bcacaf9d935b2fff73709429d95cc41554f7b3d", size = 8069260, upload-time = "2025-10-15T18:22:54.933Z" },
{ url = "https://files.pythonhosted.org/packages/dc/3d/378dbea5cd1874b94c312425ca77b0f47776c78e0df2df751b820c8c1d6c/pillow-12.0.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d87ef5795da03d742bf49439f9ca4d027cde49c82c5371ba52464aee266699a", size = 6379248, upload-time = "2025-10-15T18:22:56.605Z" },
{ url = "https://files.pythonhosted.org/packages/84/b0/d525ef47d71590f1621510327acec75ae58c721dc071b17d8d652ca494d8/pillow-12.0.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aff9e4d82d082ff9513bdd6acd4f5bd359f5b2c870907d2b0a9c5e10d40c88fe", size = 7066043, upload-time = "2025-10-15T18:22:58.53Z" },
{ url = "https://files.pythonhosted.org/packages/61/2c/aced60e9cf9d0cde341d54bf7932c9ffc33ddb4a1595798b3a5150c7ec4e/pillow-12.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8d8ca2b210ada074d57fcee40c30446c9562e542fc46aedc19baf758a93532ee", size = 6490915, upload-time = "2025-10-15T18:23:00.582Z" },
{ url = "https://files.pythonhosted.org/packages/ef/26/69dcb9b91f4e59f8f34b2332a4a0a951b44f547c4ed39d3e4dcfcff48f89/pillow-12.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:99a7f72fb6249302aa62245680754862a44179b545ded638cf1fef59befb57ef", size = 7157998, upload-time = "2025-10-15T18:23:02.627Z" },
{ url = "https://files.pythonhosted.org/packages/61/2b/726235842220ca95fa441ddf55dd2382b52ab5b8d9c0596fe6b3f23dafe8/pillow-12.0.0-cp313-cp313t-win32.whl", hash = "sha256:4078242472387600b2ce8d93ade8899c12bf33fa89e55ec89fe126e9d6d5d9e9", size = 6306201, upload-time = "2025-10-15T18:23:04.709Z" },
{ url = "https://files.pythonhosted.org/packages/c0/3d/2afaf4e840b2df71344ababf2f8edd75a705ce500e5dc1e7227808312ae1/pillow-12.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2c54c1a783d6d60595d3514f0efe9b37c8808746a66920315bfd34a938d7994b", size = 7013165, upload-time = "2025-10-15T18:23:06.46Z" },
{ url = "https://files.pythonhosted.org/packages/6f/75/3fa09aa5cf6ed04bee3fa575798ddf1ce0bace8edb47249c798077a81f7f/pillow-12.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:26d9f7d2b604cd23aba3e9faf795787456ac25634d82cd060556998e39c6fa47", size = 2437834, upload-time = "2025-10-15T18:23:08.194Z" },
{ url = "https://files.pythonhosted.org/packages/54/2a/9a8c6ba2c2c07b71bec92cf63e03370ca5e5f5c5b119b742bcc0cde3f9c5/pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:beeae3f27f62308f1ddbcfb0690bf44b10732f2ef43758f169d5e9303165d3f9", size = 4045531, upload-time = "2025-10-15T18:23:10.121Z" },
{ url = "https://files.pythonhosted.org/packages/84/54/836fdbf1bfb3d66a59f0189ff0b9f5f666cee09c6188309300df04ad71fa/pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:d4827615da15cd59784ce39d3388275ec093ae3ee8d7f0c089b76fa87af756c2", size = 4120554, upload-time = "2025-10-15T18:23:12.14Z" },
{ url = "https://files.pythonhosted.org/packages/0d/cd/16aec9f0da4793e98e6b54778a5fbce4f375c6646fe662e80600b8797379/pillow-12.0.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:3e42edad50b6909089750e65c91aa09aaf1e0a71310d383f11321b27c224ed8a", size = 3576812, upload-time = "2025-10-15T18:23:13.962Z" },
{ url = "https://files.pythonhosted.org/packages/f6/b7/13957fda356dc46339298b351cae0d327704986337c3c69bb54628c88155/pillow-12.0.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e5d8efac84c9afcb40914ab49ba063d94f5dbdf5066db4482c66a992f47a3a3b", size = 5252689, upload-time = "2025-10-15T18:23:15.562Z" },
{ url = "https://files.pythonhosted.org/packages/fc/f5/eae31a306341d8f331f43edb2e9122c7661b975433de5e447939ae61c5da/pillow-12.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:266cd5f2b63ff316d5a1bba46268e603c9caf5606d44f38c2873c380950576ad", size = 4650186, upload-time = "2025-10-15T18:23:17.379Z" },
{ url = "https://files.pythonhosted.org/packages/86/62/2a88339aa40c4c77e79108facbd307d6091e2c0eb5b8d3cf4977cfca2fe6/pillow-12.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58eea5ebe51504057dd95c5b77d21700b77615ab0243d8152793dc00eb4faf01", size = 6230308, upload-time = "2025-10-15T18:23:18.971Z" },
{ url = "https://files.pythonhosted.org/packages/c7/33/5425a8992bcb32d1cb9fa3dd39a89e613d09a22f2c8083b7bf43c455f760/pillow-12.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f13711b1a5ba512d647a0e4ba79280d3a9a045aaf7e0cc6fbe96b91d4cdf6b0c", size = 8039222, upload-time = "2025-10-15T18:23:20.909Z" },
{ url = "https://files.pythonhosted.org/packages/d8/61/3f5d3b35c5728f37953d3eec5b5f3e77111949523bd2dd7f31a851e50690/pillow-12.0.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6846bd2d116ff42cba6b646edf5bf61d37e5cbd256425fa089fee4ff5c07a99e", size = 6346657, upload-time = "2025-10-15T18:23:23.077Z" },
{ url = "https://files.pythonhosted.org/packages/3a/be/ee90a3d79271227e0f0a33c453531efd6ed14b2e708596ba5dd9be948da3/pillow-12.0.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c98fa880d695de164b4135a52fd2e9cd7b7c90a9d8ac5e9e443a24a95ef9248e", size = 7038482, upload-time = "2025-10-15T18:23:25.005Z" },
{ url = "https://files.pythonhosted.org/packages/44/34/a16b6a4d1ad727de390e9bd9f19f5f669e079e5826ec0f329010ddea492f/pillow-12.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa3ed2a29a9e9d2d488b4da81dcb54720ac3104a20bf0bd273f1e4648aff5af9", size = 6461416, upload-time = "2025-10-15T18:23:27.009Z" },
{ url = "https://files.pythonhosted.org/packages/b6/39/1aa5850d2ade7d7ba9f54e4e4c17077244ff7a2d9e25998c38a29749eb3f/pillow-12.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d034140032870024e6b9892c692fe2968493790dd57208b2c37e3fb35f6df3ab", size = 7131584, upload-time = "2025-10-15T18:23:29.752Z" },
{ url = "https://files.pythonhosted.org/packages/bf/db/4fae862f8fad0167073a7733973bfa955f47e2cac3dc3e3e6257d10fab4a/pillow-12.0.0-cp314-cp314-win32.whl", hash = "sha256:1b1b133e6e16105f524a8dec491e0586d072948ce15c9b914e41cdadd209052b", size = 6400621, upload-time = "2025-10-15T18:23:32.06Z" },
{ url = "https://files.pythonhosted.org/packages/2b/24/b350c31543fb0107ab2599464d7e28e6f856027aadda995022e695313d94/pillow-12.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:8dc232e39d409036af549c86f24aed8273a40ffa459981146829a324e0848b4b", size = 7142916, upload-time = "2025-10-15T18:23:34.71Z" },
{ url = "https://files.pythonhosted.org/packages/0f/9b/0ba5a6fd9351793996ef7487c4fdbde8d3f5f75dbedc093bb598648fddf0/pillow-12.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:d52610d51e265a51518692045e372a4c363056130d922a7351429ac9f27e70b0", size = 2523836, upload-time = "2025-10-15T18:23:36.967Z" },
{ url = "https://files.pythonhosted.org/packages/f5/7a/ceee0840aebc579af529b523d530840338ecf63992395842e54edc805987/pillow-12.0.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1979f4566bb96c1e50a62d9831e2ea2d1211761e5662afc545fa766f996632f6", size = 5255092, upload-time = "2025-10-15T18:23:38.573Z" },
{ url = "https://files.pythonhosted.org/packages/44/76/20776057b4bfd1aef4eeca992ebde0f53a4dce874f3ae693d0ec90a4f79b/pillow-12.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b2e4b27a6e15b04832fe9bf292b94b5ca156016bbc1ea9c2c20098a0320d6cf6", size = 4653158, upload-time = "2025-10-15T18:23:40.238Z" },
{ url = "https://files.pythonhosted.org/packages/82/3f/d9ff92ace07be8836b4e7e87e6a4c7a8318d47c2f1463ffcf121fc57d9cb/pillow-12.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fb3096c30df99fd01c7bf8e544f392103d0795b9f98ba71a8054bcbf56b255f1", size = 6267882, upload-time = "2025-10-15T18:23:42.434Z" },
{ url = "https://files.pythonhosted.org/packages/9f/7a/4f7ff87f00d3ad33ba21af78bfcd2f032107710baf8280e3722ceec28cda/pillow-12.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7438839e9e053ef79f7112c881cef684013855016f928b168b81ed5835f3e75e", size = 8071001, upload-time = "2025-10-15T18:23:44.29Z" },
{ url = "https://files.pythonhosted.org/packages/75/87/fcea108944a52dad8cca0715ae6247e271eb80459364a98518f1e4f480c1/pillow-12.0.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d5c411a8eaa2299322b647cd932586b1427367fd3184ffbb8f7a219ea2041ca", size = 6380146, upload-time = "2025-10-15T18:23:46.065Z" },
{ url = "https://files.pythonhosted.org/packages/91/52/0d31b5e571ef5fd111d2978b84603fce26aba1b6092f28e941cb46570745/pillow-12.0.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7e091d464ac59d2c7ad8e7e08105eaf9dafbc3883fd7265ffccc2baad6ac925", size = 7067344, upload-time = "2025-10-15T18:23:47.898Z" },
{ url = "https://files.pythonhosted.org/packages/7b/f4/2dd3d721f875f928d48e83bb30a434dee75a2531bca839bb996bb0aa5a91/pillow-12.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:792a2c0be4dcc18af9d4a2dfd8a11a17d5e25274a1062b0ec1c2d79c76f3e7f8", size = 6491864, upload-time = "2025-10-15T18:23:49.607Z" },
{ url = "https://files.pythonhosted.org/packages/30/4b/667dfcf3d61fc309ba5a15b141845cece5915e39b99c1ceab0f34bf1d124/pillow-12.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:afbefa430092f71a9593a99ab6a4e7538bc9eabbf7bf94f91510d3503943edc4", size = 7158911, upload-time = "2025-10-15T18:23:51.351Z" },
{ url = "https://files.pythonhosted.org/packages/a2/2f/16cabcc6426c32218ace36bf0d55955e813f2958afddbf1d391849fee9d1/pillow-12.0.0-cp314-cp314t-win32.whl", hash = "sha256:3830c769decf88f1289680a59d4f4c46c72573446352e2befec9a8512104fa52", size = 6408045, upload-time = "2025-10-15T18:23:53.177Z" },
{ url = "https://files.pythonhosted.org/packages/35/73/e29aa0c9c666cf787628d3f0dcf379f4791fba79f4936d02f8b37165bdf8/pillow-12.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:905b0365b210c73afb0ebe9101a32572152dfd1c144c7e28968a331b9217b94a", size = 7148282, upload-time = "2025-10-15T18:23:55.316Z" },
{ url = "https://files.pythonhosted.org/packages/c1/70/6b41bdcddf541b437bbb9f47f94d2db5d9ddef6c37ccab8c9107743748a4/pillow-12.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:99353a06902c2e43b43e8ff74ee65a7d90307d82370604746738a1e0661ccca7", size = 2525630, upload-time = "2025-10-15T18:23:57.149Z" },
]
[[package]]
name = "psycopg"
version = "3.2.12"