implement document view
Run Tests / Run Tests (push) Successful in 38s Details

This commit is contained in:
James Ravenscroft 2024-12-10 16:05:24 +00:00
parent 7b15955bf6
commit 9222739df1
10 changed files with 329 additions and 24 deletions

View File

@ -43,7 +43,8 @@ INSTALLED_APPS = [
"django.contrib.messages", "django.contrib.messages",
"django.contrib.staticfiles", "django.contrib.staticfiles",
"webui", "webui",
"markdown_deux" "markdown_deux",
"markdownify.apps.MarkdownifyConfig",
] ]
MIDDLEWARE = [ MIDDLEWARE = [
@ -74,6 +75,15 @@ TEMPLATES = [
}, },
] ]
MARKDOWNIFY = {
"default": {
"MARKDOWN_EXTENSIONS": [
"markdown.extensions.fenced_code", # dotted path
"fenced_code", # also works
]
}
}
WSGI_APPLICATION = "penparse.wsgi.application" WSGI_APPLICATION = "penparse.wsgi.application"

View File

@ -11,13 +11,13 @@ from django.conf import settings
from .models import ImageMemo, MemoStatus from .models import ImageMemo, MemoStatus
from datetime import datetime from datetime import datetime
TRANSCRIBE_PROMPT = """Transcribe the hand written notes in the attached image and present them as markdown inside a fence like so TRANSCRIBE_PROMPT = """Transcribe the hand written notes in the attached image and present them as markdown.
```markdown Do not use a fence, simply respond using markdown.
<Content>
```
If any words or letters are unclear, denote them with a '?<word>?'. For example if you were not sure whether a word is blow or blew you would transcribe it as '?blow?' If any words or letters are unclear, denote them with a '?<word>?'.
For example if you were not sure whether a word is blow or blew you would transcribe it as '?blow?'
""" """

View File

@ -23,20 +23,26 @@
alt="{{ document.title }} thumbnail" alt="{{ document.title }} thumbnail"
class="w-full h-48 object-cover mb-4 rounded" class="w-full h-48 object-cover mb-4 rounded"
/> />
<div class="flex justify-between items-center mb-2"> <table class="w-full text-sm">
<h3 class="text-xl font-semibold">{{ document.title }}</h3> <tr>
<span <td class="font-medium pr-4">Status:</td>
class="px-2 py-1 text-xs font-semibold rounded-full {% if document.status == 'pending' %} bg-gray-200 text-gray-800 {% elif document.status == 'processing' %} bg-blue-200 text-blue-800 {% elif document.status == 'done' %} bg-green-200 text-green-800 {% elif document.status == 'error' %} bg-red-200 text-red-800 {% endif %}" <td>
> <span
{{ document.status|title }} class="px-3 py-1 text-sm font-medium rounded-full {% if document.status == 'pending' %} bg-gray-200 text-gray-800 {% elif document.status == 'processing' %} bg-blue-200 text-blue-800 {% elif document.status == 'done' %} bg-green-200 text-green-800 {% elif document.status == 'error' %} bg-red-200 text-red-800 {% endif %}"
</span> >
</div> {{ document.status|title }}
<p class="text-gray-600 mb-4"> </span>
Created: {{ document.created_at }} </td>
</p> </tr>
<p class="text-gray-600 mb-4"> <tr>
Last Updated: {{ document.updated_at }} <td class="font-medium pr-4">Created:</td>
</p> <td class="text-gray-600">{{ document.created_at|date:"d/m/Y H:i" }}</td>
</tr>
<tr>
<td class="font-medium pr-4">Updated:</td>
<td class="text-gray-600">{{ document.updated_at|date:"d/m/Y H:i" }}</td>
</tr>
</table>
{% if document.content %} {% if document.content %}
<div class="text-gray-700 mb-4"> <div class="text-gray-700 mb-4">
<h4 class="font-semibold mb-2">Content Preview:</h4> <h4 class="font-semibold mb-2">Content Preview:</h4>

View File

@ -0,0 +1,181 @@
{% extends "main.html" %} {% load markdown_deux_tags %} {% load markdownify %}
{% block content %}
<section class="max-w-6xl mx-auto px-4 py-8">
<h1 class="text-3xl font-bold text-gray-900 mb-6">Document View</h1>
<div class="bg-white shadow-md rounded-lg overflow-hidden">
<div class="p-6">
<div class="flex flex-col lg:flex-row gap-8">
<div class="w-full lg:w-1/2">
<h2 class="text-2xl font-semibold text-gray-800 mb-4">
{{ document.title }}
</h2>
<div class="mb-4">
<table class="w-full text-sm">
<tr>
<td class="font-medium pr-4">Status:</td>
<td>
<span
class="px-3 py-1 text-sm font-medium rounded-full {% if document.status == 'pending' %} bg-gray-200 text-gray-800 {% elif document.status == 'processing' %} bg-blue-200 text-blue-800 {% elif document.status == 'done' %} bg-green-200 text-green-800 {% elif document.status == 'error' %} bg-red-200 text-red-800 {% endif %}"
>
{{ document.status|title }}
</span>
</td>
</tr>
<tr>
<td class="font-medium pr-4">Created:</td>
<td class="text-gray-600">{{ document.created_at|date:"d/m/Y H:i" }}</td>
</tr>
<tr>
<td class="font-medium pr-4">Updated:</td>
<td class="text-gray-600">{{ document.updated_at|date:"d/m/Y H:i" }}</td>
</tr>
</table>
</div>
<div class="prose max-w-full">
<div class="flex justify-between items-center mb-3">
<h3 class="text-xl font-semibold text-gray-800">Content:</h3>
<button
id="copyButton"
class="bg-gray-200 text-gray-700 px-3 py-1 rounded-md hover:bg-gray-300 transition duration-300 flex items-center text-sm"
>
<svg
class="w-4 h-4 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
></path>
</svg>
Copy to Clipboard
</button>
</div>
<div id="markdown-rendered" class="bg-gray-50 p-4 rounded-md">
{{ document.content|markdown }}
</div>
<div id="markdown-content" class="hidden">{{ document.content}}</div>
</div>
<div class="mt-8 flex space-x-4">
<a
href="{% url 'download_document' document.id %}"
class="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 transition duration-300 flex items-center"
>
<svg
class="w-5 h-5 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
></path>
</svg>
Export
</a>
<form
action="{% url 'delete_document' document.id %}"
method="post"
onsubmit="return confirm('Are you sure you want to delete this document?');"
>
{% csrf_token %}
<button
type="submit"
class="bg-red-600 text-white px-4 py-2 rounded-md hover:bg-red-700 transition duration-300 flex items-center"
>
<svg
class="w-5 h-5 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
></path>
</svg>
Delete
</button>
</form>
</div>
</div>
<div class="w-full lg:w-1/2">
<div class="bg-gray-100 rounded-lg overflow-hidden">
<img
src="{% url 'document_image' pk=document.id %}"
alt="{{ document.title }}"
class="w-full h-auto object-contain"
/>
</div>
</div>
</div>
</div>
</div>
</section>
<section class="max-w-6xl mx-auto px-4 py-4">
<a
href="{% url 'dashboard' %}"
class="text-blue-600 hover:text-blue-800 flex items-center"
>
<svg
class="w-5 h-5 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M10 19l-7-7m0 0l7-7m-7 7h18"
></path>
</svg>
Back to Dashboard
</a>
</section>
{% endblock %} {% block extra_js %}
<script>
document.addEventListener("DOMContentLoaded", (event) => {
// Syntax highlighting
document.querySelectorAll("#markdown-content pre code").forEach((block) => {
hljs.highlightElement(block);
});
// Copy to clipboard functionality
const copyButton = document.getElementById("copyButton");
const markdownContent = document.getElementById("markdown-content");
copyButton.addEventListener("click", () => {
const textToCopy = markdownContent.innerText;
navigator.clipboard
.writeText(textToCopy)
.then(() => {
copyButton.textContent = "Copied!";
copyButton.classList.remove("bg-gray-200", "text-gray-700");
copyButton.classList.add("bg-green-500", "text-white");
setTimeout(() => {
copyButton.innerHTML =
'<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path></svg>Copy to Clipboard';
copyButton.classList.remove("bg-green-500", "text-white");
copyButton.classList.add("bg-gray-200", "text-gray-700");
}, 2000);
})
.catch((err) => {
console.error("Failed to copy text: ", err);
});
});
});
</script>
{% endblock %}

View File

@ -5,6 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>PenParse - Intelligent OCR for Handwritten Notes</title> <title>PenParse - Intelligent OCR for Handwritten Notes</title>
<script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.tailwindcss.com"></script>
{% block extra_js %}{% endblock %}
</head> </head>
<body class="bg-gray-100 font-sans"> <body class="bg-gray-100 font-sans">
<header class="bg-white shadow-sm">{% include "partial/nav.html" %}</header> <header class="bg-white shadow-sm">{% include "partial/nav.html" %}</header>

View File

@ -13,6 +13,11 @@ urlpatterns = [
views.document_thumbnail, views.document_thumbnail,
name="document_thumbnail", name="document_thumbnail",
), ),
path(
"documents/<str:pk>/image",
views.document_image,
name="document_image",
),
path( path(
"documents/<str:pk>/download", views.download_document, name="download_document" "documents/<str:pk>/download", views.download_document, name="download_document"
), ),

View File

@ -3,7 +3,7 @@ import os
from django.contrib import messages from django.contrib import messages
from django.shortcuts import redirect, render from django.shortcuts import redirect, render
from django.http import HttpRequest from django.http import HttpRequest, HttpResponse
from django.core.files.storage import default_storage from django.core.files.storage import default_storage
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
from ..models import ImageMemo from ..models import ImageMemo
@ -13,7 +13,7 @@ from django.contrib.auth.decorators import login_required
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
from .thumbnail import document_thumbnail from .thumbnail import document_thumbnail, document_image
from .register import register from .register import register
from .upload import upload_document from .upload import upload_document
from .delete import delete_document from .delete import delete_document
@ -27,6 +27,7 @@ def index(request):
__all__ = [ __all__ = [
"index", "index",
"document_thumbnail", "document_thumbnail",
"document_image",
"register", "register",
"dashboard", "dashboard",
"settings", "settings",
@ -54,8 +55,19 @@ def settings(request):
@login_required @login_required
def view_document(request): def view_document(request: HttpRequest, pk: str):
return render(request, "document.html")
## check that the document exists and belongs to the user
# find document with given ID (pk path param) and current user id
document = ImageMemo.objects.filter(id=pk, author__id=request.user.id).first()
if not document:
logger.debug(f"No memo found for user={request.user.id} and memo_id={pk}")
return HttpResponse(content="Document not found", status=404)
return render(request, "document.html", context={"document": document})
@login_required @login_required

View File

@ -13,6 +13,29 @@ from django.contrib.auth.decorators import login_required
from ..models import ImageMemo from ..models import ImageMemo
@login_required
def document_image(request, pk):
"""Given a document uuid, look it up, ensure that it belongs to the current user and respond with an image"""
# find document with given ID (pk path param) and current user id
document = ImageMemo.objects.filter(id=pk, author__id=request.user.id).first()
if not document:
logger.debug(f"No memo found for user={request.user.id} and memo_id={pk}")
return HttpResponse(content="Document not found", status=404)
# look up the file on disk
if not default_storage.exists(document.image.name):
logger.warning(
f"The file associated with memo {document.id} does not exist"
)
return HttpResponse(content="Document not found", status=404)
# Return the thumbnail as an HTTP response
with default_storage.open(document.image.name,'rb') as f:
return HttpResponse(f.read(), content_type="image/jpeg")
@login_required @login_required
def document_thumbnail(request: HttpRequest, pk: str): def document_thumbnail(request: HttpRequest, pk: str):
"""Given a document uuid, look it up, ensure that it belongs to the current user and respond with a thumbnail""" """Given a document uuid, look it up, ensure that it belongs to the current user and respond with a thumbnail"""

View File

@ -7,6 +7,7 @@ requires-python = ">=3.9"
dependencies = [ dependencies = [
"celery>=5.4.0", "celery>=5.4.0",
"django-markdown-deux>=1.0.6", "django-markdown-deux>=1.0.6",
"django-markdownify>=0.9.5",
"django>=4.2.16", "django>=4.2.16",
"litellm>=1.54.1", "litellm>=1.54.1",
"loguru>=0.7.3", "loguru>=0.7.3",

66
uv.lock
View File

@ -190,6 +190,23 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/30/da/43b15f28fe5f9e027b41c539abc5469052e9d48fd75f8ff094ba2a0ae767/billiard-4.2.1-py3-none-any.whl", hash = "sha256:40b59a4ac8806ba2c2369ea98d876bc6108b051c227baffd928c644d15d8f3cb", size = 86766 }, { url = "https://files.pythonhosted.org/packages/30/da/43b15f28fe5f9e027b41c539abc5469052e9d48fd75f8ff094ba2a0ae767/billiard-4.2.1-py3-none-any.whl", hash = "sha256:40b59a4ac8806ba2c2369ea98d876bc6108b051c227baffd928c644d15d8f3cb", size = 86766 },
] ]
[[package]]
name = "bleach"
version = "6.2.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "webencodings" },
]
sdist = { url = "https://files.pythonhosted.org/packages/76/9a/0e33f5054c54d349ea62c277191c020c2d6ef1d65ab2cb1993f91ec846d1/bleach-6.2.0.tar.gz", hash = "sha256:123e894118b8a599fd80d3ec1a6d4cc7ce4e5882b1317a7e1ba69b56e95f991f", size = 203083 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fc/55/96142937f66150805c25c4d0f31ee4132fd33497753400734f9dfdcbdc66/bleach-6.2.0-py3-none-any.whl", hash = "sha256:117d9c6097a7c3d22fd578fcd8d35ff1e125df6736f554da4e432fdd63f31e5e", size = 163406 },
]
[package.optional-dependencies]
css = [
{ name = "tinycss2" },
]
[[package]] [[package]]
name = "celery" name = "celery"
version = "5.4.0" version = "5.4.0"
@ -393,6 +410,20 @@ dependencies = [
] ]
sdist = { url = "https://files.pythonhosted.org/packages/26/af/3ed785b661e4545709ba1618926bb33bd585d1fd2faa42a548756743e874/django-markdown-deux-1.0.6.zip", hash = "sha256:1f7b4da6b4dd1a9a84e3da90887d356f8afdd9a1e7d6468c081b8ac50a7980b1", size = 18157 } sdist = { url = "https://files.pythonhosted.org/packages/26/af/3ed785b661e4545709ba1618926bb33bd585d1fd2faa42a548756743e874/django-markdown-deux-1.0.6.zip", hash = "sha256:1f7b4da6b4dd1a9a84e3da90887d356f8afdd9a1e7d6468c081b8ac50a7980b1", size = 18157 }
[[package]]
name = "django-markdownify"
version = "0.9.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "bleach", extra = ["css"] },
{ name = "django" },
{ name = "markdown" },
]
sdist = { url = "https://files.pythonhosted.org/packages/6c/33/3abb966e2b238af4c9a5d3ee38a7aa7e51b644b4b20bf8533b6fd1c1bf96/django_markdownify-0.9.5.tar.gz", hash = "sha256:34c34eba4a797282a5c5bd97b13cec84d6a4c0673ad47ce1c1d000d74dd8d4ab", size = 7939 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1b/35/c7a4bd957b279a8e7c808116bed399b73874ed3da78689993ee76f30d9f6/django_markdownify-0.9.5-py3-none-any.whl", hash = "sha256:2c4ae44e386c209453caf5e9ea1b74f64535985d338ad2d5ad5e7089cc94be86", size = 10342 },
]
[[package]] [[package]]
name = "exceptiongroup" name = "exceptiongroup"
version = "1.2.2" version = "1.2.2"
@ -751,6 +782,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c", size = 61595 }, { url = "https://files.pythonhosted.org/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c", size = 61595 },
] ]
[[package]]
name = "markdown"
version = "3.7"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "importlib-metadata", marker = "python_full_version < '3.10'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/54/28/3af612670f82f4c056911fbbbb42760255801b3068c48de792d354ff4472/markdown-3.7.tar.gz", hash = "sha256:2ae2471477cfd02dbbf038d5d9bc226d40def84b4fe2986e49b59b6b472bbed2", size = 357086 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3f/08/83871f3c50fc983b88547c196d11cf8c3340e37c32d2e9d6152abe2c61f7/Markdown-3.7-py3-none-any.whl", hash = "sha256:7eb6df5690b81a1d7942992c97fad2938e956e79df20cbc6186e9c3a77b1c803", size = 106349 },
]
[[package]] [[package]]
name = "markdown2" name = "markdown2"
version = "2.5.1" version = "2.5.1"
@ -951,6 +994,7 @@ dependencies = [
{ name = "celery" }, { name = "celery" },
{ name = "django" }, { name = "django" },
{ name = "django-markdown-deux" }, { name = "django-markdown-deux" },
{ name = "django-markdownify" },
{ name = "litellm" }, { name = "litellm" },
{ name = "loguru" }, { name = "loguru" },
{ name = "pillow" }, { name = "pillow" },
@ -966,6 +1010,7 @@ requires-dist = [
{ name = "celery", specifier = ">=5.4.0" }, { name = "celery", specifier = ">=5.4.0" },
{ name = "django", specifier = ">=4.2.16" }, { name = "django", specifier = ">=4.2.16" },
{ name = "django-markdown-deux", specifier = ">=1.0.6" }, { name = "django-markdown-deux", specifier = ">=1.0.6" },
{ name = "django-markdownify", specifier = ">=0.9.5" },
{ name = "litellm", specifier = ">=1.54.1" }, { name = "litellm", specifier = ">=1.54.1" },
{ name = "loguru", specifier = ">=0.7.3" }, { name = "loguru", specifier = ">=0.7.3" },
{ name = "pillow", specifier = ">=11.0.0" }, { name = "pillow", specifier = ">=11.0.0" },
@ -1686,6 +1731,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d5/3b/7c8812952ca55e1bab08afc1dda3c5991804c71b550b9402e82a082ab795/tiktoken-0.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:1473cfe584252dc3fa62adceb5b1c763c1874e04511b197da4e6de51d6ce5a02", size = 884803 }, { url = "https://files.pythonhosted.org/packages/d5/3b/7c8812952ca55e1bab08afc1dda3c5991804c71b550b9402e82a082ab795/tiktoken-0.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:1473cfe584252dc3fa62adceb5b1c763c1874e04511b197da4e6de51d6ce5a02", size = 884803 },
] ]
[[package]]
name = "tinycss2"
version = "1.4.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "webencodings" },
]
sdist = { url = "https://files.pythonhosted.org/packages/7a/fd/7a5ee21fd08ff70d3d33a5781c255cbe779659bd03278feb98b19ee550f4/tinycss2-1.4.0.tar.gz", hash = "sha256:10c0972f6fc0fbee87c3edb76549357415e94548c1ae10ebccdea16fb404a9b7", size = 87085 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e6/34/ebdc18bae6aa14fbee1a08b63c015c72b64868ff7dae68808ab500c492e2/tinycss2-1.4.0-py3-none-any.whl", hash = "sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289", size = 26610 },
]
[[package]] [[package]]
name = "tokenizers" name = "tokenizers"
version = "0.21.0" version = "0.21.0"
@ -1807,6 +1864,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166 }, { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166 },
] ]
[[package]]
name = "webencodings"
version = "0.5.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/0b/02/ae6ceac1baeda530866a85075641cec12989bd8d31af6d5ab4a3e8c92f47/webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923", size = 9721 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", size = 11774 },
]
[[package]] [[package]]
name = "win32-setctime" name = "win32-setctime"
version = "1.2.0" version = "1.2.0"